Skip to content

Commit c11b6a1

Browse files
committed
Decouple PySpyProfiler and EbpfProfiler from PythonProfiler
1 parent 5d3b189 commit c11b6a1

File tree

10 files changed

+299
-212
lines changed

10 files changed

+299
-212
lines changed

gprofiler/main.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,9 @@
5050
from gprofiler.profilers.factory import get_profilers
5151
from gprofiler.profilers.profiler_base import NoopProfiler, ProcessProfilerBase, ProfilerInterface
5252
from gprofiler.profilers.registry import (
53-
ProfilerConfig,
53+
get_preferred_or_first_profiler,
5454
get_profilers_registry,
5555
get_runtime_possible_modes,
56-
get_sorted_profilers,
5756
)
5857
from gprofiler.spark.sampler import SparkSampler
5958
from gprofiler.state import State, init_state
@@ -824,13 +823,12 @@ def _add_profilers_arguments(parser: configargparse.ArgumentParser) -> None:
824823
for runtime, configs in get_profilers_registry().items():
825824
arg_group = parser.add_argument_group(runtime)
826825
mode_var = f"{runtime.lower().replace('-', '_')}_mode"
827-
sorted_profilers = get_sorted_profilers(runtime)
828-
# TODO: marcin-ol: organize options and usage for runtime - single source of runtime options?
829-
preferred_profiler = sorted_profilers[0]
826+
# TODO: organize options and usage for runtime - single source of runtime options?
827+
preferred_profiler = get_preferred_or_first_profiler(runtime)
830828
arg_group.add_argument(
831829
f"--{runtime.lower()}-mode",
832830
dest=mode_var,
833-
default=ProfilerConfig.ENABLED_MODE if len(sorted_profilers) > 1 else sorted_profilers[0].default_mode,
831+
default=preferred_profiler.default_mode,
834832
help=preferred_profiler.profiler_mode_help,
835833
choices=get_runtime_possible_modes(runtime),
836834
)

gprofiler/profilers/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
# NOTE: Make sure to import any new process profilers to load it
22
from gprofiler.platform import is_linux
33
from gprofiler.profilers.dotnet import DotnetProfiler
4-
from gprofiler.profilers.python import PythonProfiler
4+
from gprofiler.profilers.python import PySpyProfiler
55

66
if is_linux():
77
from gprofiler.profilers.java import JavaProfiler
88
from gprofiler.profilers.perf import SystemProfiler
99
from gprofiler.profilers.php import PHPSpyProfiler
10+
from gprofiler.profilers.python_ebpf import PythonEbpfProfiler
1011
from gprofiler.profilers.ruby import RbSpyProfiler
1112

12-
__all__ = ["PythonProfiler", "DotnetProfiler"]
13+
__all__ = ["PySpyProfiler", "DotnetProfiler"]
1314

1415
if is_linux():
15-
__all__ += ["JavaProfiler", "PHPSpyProfiler", "RbSpyProfiler", "SystemProfiler"]
16+
__all__ += ["JavaProfiler", "PHPSpyProfiler", "RbSpyProfiler", "SystemProfiler", "PythonEbpfProfiler"]
1617

1718
del is_linux

gprofiler/profilers/factory.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
from gprofiler.metadata.system_metadata import get_arch
66
from gprofiler.profilers.perf import SystemProfiler
77
from gprofiler.profilers.profiler_base import NoopProfiler
8-
from gprofiler.profilers.registry import ProfilerConfig, get_profilers_registry, get_sorted_profilers
8+
from gprofiler.profilers.registry import (
9+
ProfilerConfig,
10+
get_profiler_arguments,
11+
get_profilers_registry,
12+
get_sorted_profilers,
13+
)
914

1015
if TYPE_CHECKING:
1116
from gprofiler.gprofiler_types import UserArgs
@@ -27,7 +32,7 @@ def get_profilers(
2732
return system_profiler, process_profilers_instances
2833
arch = get_arch()
2934
for runtime in get_profilers_registry():
30-
runtime_args_prefix = runtime.lower()
35+
runtime_args_prefix = runtime.lower().replace("-", "_")
3136
runtime_mode = user_args.get(f"{runtime_args_prefix}_mode")
3237
if runtime_mode in ProfilerConfig.DISABLED_MODES:
3338
continue
@@ -53,11 +58,18 @@ def get_profilers(
5358
continue
5459
# create instances of selected profilers one by one, select first that is ready
5560
ready_profiler = None
61+
runtime_arg_names = [arg.dest for config in get_profilers_registry()[runtime] for arg in config.profiler_args]
5662
for profiler_config in selected_configs:
5763
profiler_name = profiler_config.profiler_name
5864
profiler_kwargs = profiler_init_kwargs.copy()
65+
profiler_arg_names = [arg.dest for arg in get_profiler_arguments(runtime, profiler_name)]
5966
for key, value in user_args.items():
60-
if key.startswith(runtime_args_prefix) or key in COMMON_PROFILER_ARGUMENT_NAMES:
67+
if (
68+
key in profiler_arg_names
69+
or key in COMMON_PROFILER_ARGUMENT_NAMES
70+
or key.startswith(runtime_args_prefix)
71+
and key not in runtime_arg_names
72+
):
6173
profiler_kwargs[key] = value
6274
try:
6375
profiler_instance = profiler_config.profiler_class(**profiler_kwargs)

gprofiler/profilers/python.py

Lines changed: 23 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -27,28 +27,17 @@
2727
ProcessStoppedException,
2828
StopEventSetException,
2929
)
30-
from gprofiler.gprofiler_types import (
31-
ProcessToProfileData,
32-
ProcessToStackSampleCounters,
33-
ProfileData,
34-
StackToSampleCount,
35-
nonnegative_integer,
36-
)
30+
from gprofiler.gprofiler_types import ProcessToStackSampleCounters, ProfileData, StackToSampleCount
3731
from gprofiler.log import get_logger_adapter
3832
from gprofiler.metadata import application_identifiers
3933
from gprofiler.metadata.application_metadata import ApplicationMetadata
4034
from gprofiler.metadata.py_module_version import get_modules_versions
41-
from gprofiler.metadata.system_metadata import get_arch
4235
from gprofiler.platform import is_linux, is_windows
4336
from gprofiler.profiler_state import ProfilerState
44-
from gprofiler.profilers.profiler_base import ProfilerInterface, SpawningProcessProfilerBase
45-
from gprofiler.profilers.registry import ProfilerArgument, register_profiler
46-
from gprofiler.utils.collapsed_format import parse_one_collapsed_file
47-
48-
if is_linux():
49-
from gprofiler.profilers.python_ebpf import PythonEbpfProfiler, PythonEbpfError
50-
37+
from gprofiler.profilers.profiler_base import SpawningProcessProfilerBase
38+
from gprofiler.profilers.registry import register_profiler
5139
from gprofiler.utils import pgrep_exe, pgrep_maps, random_prefix, removed_path, resource_path, run_process
40+
from gprofiler.utils.collapsed_format import parse_one_collapsed_file
5241
from gprofiler.utils.process import process_comm, search_proc_maps
5342

5443
logger = get_logger_adapter(__name__)
@@ -163,6 +152,19 @@ def make_application_metadata(self, process: Process) -> Dict[str, Any]:
163152
return metadata
164153

165154

155+
@register_profiler(
156+
"Python",
157+
profiler_name="PySpy",
158+
# py-spy is like pyspy, it's confusing and I mix between them
159+
possible_modes=["auto", "pyspy", "py-spy"],
160+
default_mode="auto",
161+
# we build pyspy for both,.
162+
supported_archs=["x86_64", "aarch64"],
163+
supported_windows_archs=["AMD64"],
164+
# profiler arguments are defined by preferred profiler of the runtime, that is PythonEbpfProfiler
165+
profiler_arguments=[],
166+
supported_profiling_modes=["cpu"],
167+
)
166168
class PySpyProfiler(SpawningProcessProfilerBase):
167169
MAX_FREQUENCY = 50
168170
_EXTRA_TIMEOUT = 10 # give py-spy some seconds to run (added to the duration)
@@ -173,10 +175,14 @@ def __init__(
173175
duration: int,
174176
profiler_state: ProfilerState,
175177
*,
176-
add_versions: bool,
178+
python_mode: str,
179+
python_add_versions: bool,
177180
):
178181
super().__init__(frequency, duration, profiler_state)
179-
self.add_versions = add_versions
182+
if python_mode == "py-spy":
183+
python_mode = "pyspy"
184+
assert python_mode in ("auto", "pyspy"), f"unexpected mode: {python_mode}"
185+
self.add_versions = python_add_versions
180186
self._metadata = PythonMetadata(self._profiler_state.stop_event)
181187

182188
def _make_command(self, pid: int, output_path: str, duration: int) -> List[str]:
@@ -289,153 +295,5 @@ def _should_skip_process(self, process: Process) -> bool:
289295

290296
return False
291297

292-
293-
@register_profiler(
294-
"Python",
295-
# py-spy is like pyspy, it's confusing and I mix between them
296-
possible_modes=["auto", "pyperf", "pyspy", "py-spy", "disabled"],
297-
default_mode="auto",
298-
# we build pyspy for both, pyperf only for x86_64.
299-
# TODO: this inconsistency shows that py-spy and pyperf should have different Profiler classes,
300-
# we should split them in the future.
301-
supported_archs=["x86_64", "aarch64"],
302-
supported_windows_archs=["AMD64"],
303-
profiler_mode_argument_help="Select the Python profiling mode: auto (try PyPerf, resort to py-spy if it fails), "
304-
"pyspy (always use py-spy), pyperf (always use PyPerf, and avoid py-spy even if it fails)"
305-
" or disabled (no runtime profilers for Python).",
306-
profiler_arguments=[
307-
# TODO should be prefixed with --python-
308-
ProfilerArgument(
309-
"--no-python-versions",
310-
dest="python_add_versions",
311-
action="store_false",
312-
default=True,
313-
help="Don't add version information to Python frames. If not set, frames from packages are displayed with "
314-
"the name of the package and its version, and frames from Python built-in modules are displayed with "
315-
"Python's full version.",
316-
),
317-
# TODO should be prefixed with --python-
318-
ProfilerArgument(
319-
"--pyperf-user-stacks-pages",
320-
dest="python_pyperf_user_stacks_pages",
321-
default=None,
322-
type=nonnegative_integer,
323-
help="Number of user stack-pages that PyPerf will collect, this controls the maximum stack depth of native "
324-
"user frames. Pass 0 to disable user native stacks altogether.",
325-
),
326-
ProfilerArgument(
327-
"--python-pyperf-verbose",
328-
dest="python_pyperf_verbose",
329-
action="store_true",
330-
help="Enable PyPerf in verbose mode (max verbosity)",
331-
),
332-
],
333-
supported_profiling_modes=["cpu"],
334-
)
335-
class PythonProfiler(ProfilerInterface):
336-
"""
337-
Controls PySpyProfiler & PythonEbpfProfiler as needed, providing a clean interface
338-
to GProfiler.
339-
"""
340-
341-
def __init__(
342-
self,
343-
frequency: int,
344-
duration: int,
345-
profiler_state: ProfilerState,
346-
python_mode: str,
347-
python_add_versions: bool,
348-
python_pyperf_user_stacks_pages: Optional[int],
349-
python_pyperf_verbose: bool,
350-
):
351-
if python_mode == "py-spy":
352-
python_mode = "pyspy"
353-
354-
assert python_mode in ("auto", "pyperf", "pyspy"), f"unexpected mode: {python_mode}"
355-
356-
if get_arch() != "x86_64" or is_windows():
357-
if python_mode == "pyperf":
358-
raise Exception(f"PyPerf is supported only on x86_64 (and not on this arch {get_arch()})")
359-
python_mode = "pyspy"
360-
361-
if python_mode in ("auto", "pyperf"):
362-
self._ebpf_profiler = self._create_ebpf_profiler(
363-
frequency,
364-
duration,
365-
profiler_state,
366-
python_add_versions,
367-
python_pyperf_user_stacks_pages,
368-
python_pyperf_verbose,
369-
)
370-
else:
371-
self._ebpf_profiler = None
372-
373-
if python_mode == "pyspy" or (self._ebpf_profiler is None and python_mode == "auto"):
374-
self._pyspy_profiler: Optional[PySpyProfiler] = PySpyProfiler(
375-
frequency,
376-
duration,
377-
profiler_state,
378-
add_versions=python_add_versions,
379-
)
380-
else:
381-
self._pyspy_profiler = None
382-
383-
if is_linux():
384-
385-
def _create_ebpf_profiler(
386-
self,
387-
frequency: int,
388-
duration: int,
389-
profiler_state: ProfilerState,
390-
add_versions: bool,
391-
user_stacks_pages: Optional[int],
392-
verbose: bool,
393-
) -> Optional[PythonEbpfProfiler]:
394-
try:
395-
profiler = PythonEbpfProfiler(
396-
frequency,
397-
duration,
398-
profiler_state,
399-
add_versions=add_versions,
400-
user_stacks_pages=user_stacks_pages,
401-
verbose=verbose,
402-
)
403-
profiler.test()
404-
return profiler
405-
except Exception as e:
406-
logger.debug(f"eBPF profiler error: {str(e)}")
407-
logger.info("Python eBPF profiler initialization failed")
408-
return None
409-
410298
def check_readiness(self) -> bool:
411299
return True
412-
413-
def start(self) -> None:
414-
if self._ebpf_profiler is not None:
415-
self._ebpf_profiler.start()
416-
elif self._pyspy_profiler is not None:
417-
self._pyspy_profiler.start()
418-
419-
def snapshot(self) -> ProcessToProfileData:
420-
if self._ebpf_profiler is not None:
421-
try:
422-
return self._ebpf_profiler.snapshot()
423-
except PythonEbpfError as e:
424-
assert not self._ebpf_profiler.is_running()
425-
logger.warning(
426-
"Python eBPF profiler failed, restarting PyPerf...",
427-
pyperf_exit_code=e.returncode,
428-
pyperf_stdout=e.stdout,
429-
pyperf_stderr=e.stderr,
430-
)
431-
self._ebpf_profiler.start()
432-
return {} # empty this round
433-
else:
434-
assert self._pyspy_profiler is not None
435-
return self._pyspy_profiler.snapshot()
436-
437-
def stop(self) -> None:
438-
if self._ebpf_profiler is not None:
439-
self._ebpf_profiler.stop()
440-
elif self._pyspy_profiler is not None:
441-
self._pyspy_profiler.stop()

0 commit comments

Comments
 (0)