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
3731from gprofiler .log import get_logger_adapter
3832from gprofiler .metadata import application_identifiers
3933from gprofiler .metadata .application_metadata import ApplicationMetadata
4034from gprofiler .metadata .py_module_version import get_modules_versions
41- from gprofiler .metadata .system_metadata import get_arch
4235from gprofiler .platform import is_linux , is_windows
4336from 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
5139from 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
5241from gprofiler .utils .process import process_comm , search_proc_maps
5342
5443logger = 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+ )
166168class 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