Skip to content

Refactor py-spy & pyperf to separate ProfilerInterface. #805

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 21 additions & 15 deletions gprofiler/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@
from gprofiler.profiler_state import ProfilerState
from gprofiler.profilers.factory import get_profilers
from gprofiler.profilers.profiler_base import NoopProfiler, ProcessProfilerBase, ProfilerInterface
from gprofiler.profilers.registry import get_profilers_registry
from gprofiler.profilers.registry import (
get_preferred_or_first_profiler,
get_profilers_registry,
get_runtime_possible_modes,
)
from gprofiler.spark.sampler import SparkSampler
from gprofiler.state import State, init_state
from gprofiler.system_metrics import Metrics, NoopSystemMetricsMonitor, SystemMetricsMonitor, SystemMetricsMonitorBase
Expand Down Expand Up @@ -816,29 +820,31 @@ def parse_cmd_args() -> configargparse.Namespace:


def _add_profilers_arguments(parser: configargparse.ArgumentParser) -> None:
registry = get_profilers_registry()
for name, config in registry.items():
arg_group = parser.add_argument_group(name)
mode_var = f"{name.lower()}_mode"
for runtime, configs in get_profilers_registry().items():
arg_group = parser.add_argument_group(runtime)
mode_var = f"{runtime.lower().replace('-', '_')}_mode"
# TODO: organize options and usage for runtime - single source of runtime options?
preferred_profiler = get_preferred_or_first_profiler(runtime)
arg_group.add_argument(
f"--{name.lower()}-mode",
f"--{runtime.lower()}-mode",
dest=mode_var,
default=config.default_mode,
help=config.profiler_mode_help,
choices=config.possible_modes,
default=preferred_profiler.default_mode,
help=preferred_profiler.profiler_mode_help,
choices=get_runtime_possible_modes(runtime),
)
arg_group.add_argument(
f"--no-{name.lower()}",
f"--no-{runtime.lower()}",
action="store_const",
const="disabled",
dest=mode_var,
default=True,
help=config.disablement_help,
help=preferred_profiler.disablement_help,
)
for arg in config.profiler_args:
profiler_arg_kwargs = arg.get_dict()
name = profiler_arg_kwargs.pop("name")
arg_group.add_argument(name, **profiler_arg_kwargs)
for config in configs:
for arg in config.profiler_args:
profiler_arg_kwargs = arg.get_dict()
name = profiler_arg_kwargs.pop("name")
arg_group.add_argument(name, **profiler_arg_kwargs)


def verify_preconditions(args: configargparse.Namespace, processes_to_profile: Optional[List[Process]]) -> None:
Expand Down
7 changes: 4 additions & 3 deletions gprofiler/profilers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
# NOTE: Make sure to import any new process profilers to load it
from gprofiler.platform import is_linux
from gprofiler.profilers.dotnet import DotnetProfiler
from gprofiler.profilers.python import PythonProfiler
from gprofiler.profilers.python import PySpyProfiler

if is_linux():
from gprofiler.profilers.java import JavaProfiler
from gprofiler.profilers.perf import SystemProfiler
from gprofiler.profilers.php import PHPSpyProfiler
from gprofiler.profilers.python_ebpf import PythonEbpfProfiler
from gprofiler.profilers.ruby import RbSpyProfiler

__all__ = ["PythonProfiler", "DotnetProfiler"]
__all__ = ["PySpyProfiler", "DotnetProfiler"]

if is_linux():
__all__ += ["JavaProfiler", "PHPSpyProfiler", "RbSpyProfiler", "SystemProfiler"]
__all__ += ["JavaProfiler", "PHPSpyProfiler", "RbSpyProfiler", "SystemProfiler", "PythonEbpfProfiler"]

del is_linux
87 changes: 57 additions & 30 deletions gprofiler/profilers/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@

from gprofiler.log import get_logger_adapter
from gprofiler.metadata.system_metadata import get_arch
from gprofiler.platform import is_windows
from gprofiler.profilers.perf import SystemProfiler
from gprofiler.profilers.profiler_base import NoopProfiler
from gprofiler.profilers.registry import get_profilers_registry
from gprofiler.profilers.registry import (
ProfilerConfig,
get_profiler_arguments,
get_profilers_registry,
get_sorted_profilers,
)

if TYPE_CHECKING:
from gprofiler.gprofiler_types import UserArgs
Expand All @@ -24,44 +28,67 @@ def get_profilers(
process_profilers_instances: List["ProcessProfilerBase"] = []
system_profiler: Union["SystemProfiler", "NoopProfiler"] = NoopProfiler()

if profiling_mode != "none":
arch = get_arch()
for profiler_name, profiler_config in get_profilers_registry().items():
lower_profiler_name = profiler_name.lower()
profiler_mode = user_args.get(f"{lower_profiler_name}_mode")
if profiler_mode in ("none", "disabled"):
continue

supported_archs = (
profiler_config.supported_windows_archs if is_windows() else profiler_config.supported_archs
)
if arch not in supported_archs:
if profiling_mode == "none":
return system_profiler, process_profilers_instances
arch = get_arch()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add a blank line between these 2 lines for clarity

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

for runtime in get_profilers_registry():
runtime_args_prefix = runtime.lower().replace("-", "_")
runtime_mode = user_args.get(f"{runtime_args_prefix}_mode")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Why do you need replace(-, _) on the runtime name?
  2. This logic (these 2 lines) repeat more than once, extract to a function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed now. Was used when exploring different approaches.

if runtime_mode in ProfilerConfig.DISABLED_MODES:
continue
# select configs supporting requested runtime_mode or all configs in order of preference
requested_configs: List[ProfilerConfig] = get_sorted_profilers(runtime)
if runtime_mode != ProfilerConfig.ENABLED_MODE:
requested_configs = [c for c in requested_configs if runtime_mode in c.get_active_modes()]
# select profilers that support this architecture and profiling mode
selected_configs: List[ProfilerConfig] = []
for config in requested_configs:
profiler_name = config.profiler_name
if arch not in config.get_supported_archs() and len(requested_configs) == 1:
logger.warning(f"Disabling {profiler_name} because it doesn't support this architecture ({arch})")
continue

if profiling_mode not in profiler_config.supported_profiling_modes:
if profiling_mode not in config.supported_profiling_modes:
logger.warning(
f"Disabling {profiler_name} because it doesn't support profiling mode {profiling_mode!r}"
)
continue

selected_configs.append(config)
if not selected_configs:
logger.warning(f"Disabling {runtime} profiling because no profilers were selected")
continue
# create instances of selected profilers one by one, select first that is ready
ready_profiler = None
runtime_arg_names = [arg.dest for config in get_profilers_registry()[runtime] for arg in config.profiler_args]
for profiler_config in selected_configs:
profiler_name = profiler_config.profiler_name
profiler_kwargs = profiler_init_kwargs.copy()
profiler_arg_names = [arg.dest for arg in get_profiler_arguments(runtime, profiler_name)]
for key, value in user_args.items():
if key.startswith(lower_profiler_name) or key in COMMON_PROFILER_ARGUMENT_NAMES:
if (
key in profiler_arg_names
or key in COMMON_PROFILER_ARGUMENT_NAMES
or key.startswith(runtime_args_prefix)
and key not in runtime_arg_names
):
profiler_kwargs[key] = value
try:
profiler_instance = profiler_config.profiler_class(**profiler_kwargs)
if profiler_instance.check_readiness():
ready_profiler = profiler_instance
break
except Exception:
logger.critical(
f"Couldn't create the {profiler_name} profiler, not continuing."
f" Run with --no-{profiler_name.lower()} to disable this profiler",
exc_info=True,
)
sys.exit(1)
else:
if isinstance(profiler_instance, SystemProfiler):
system_profiler = profiler_instance
else:
process_profilers_instances.append(profiler_instance)

if len(requested_configs) == 1:
logger.critical(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a new fatal error. In which situations can it happen? When I force --python-mode=pyperf but PyPerf can't be used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's the case.
Previously it was c-tor that could raise an Exception (i.e.: EbpfProfiler.test() called from PythonProfiler.__init__()).

Now the test is wrapped in check_readiness().
If it was the only available profiler (by the size of requested_configs), we raise an Exception, as previously.
Backward compatibility is retained.

f"Couldn't create the {profiler_name} profiler for runtime {runtime}, not continuing."
f" Request different profiler for runtime with --{runtime_args_prefix}-mode, or disable"
f" {runtime} profiling with --no-{runtime_args_prefix} to disable this profiler",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
f" {runtime} profiling with --no-{runtime_args_prefix} to disable this profiler",
f" {runtime} profiling with --{runtime_args_prefix}-mode=none to disable this profiler",

I prefer this way of expressing it (--no-... is legacy to me)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay.

exc_info=True,
)
sys.exit(1)
if isinstance(ready_profiler, SystemProfiler):
system_profiler = ready_profiler
elif ready_profiler is not None:
process_profilers_instances.append(ready_profiler)
else:
logger.warning(f"Disabling {runtime} profiling because no profilers were ready")
return system_profiler, process_profilers_instances
12 changes: 12 additions & 0 deletions gprofiler/profilers/profiler_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ def snapshot(self) -> ProcessToProfileData:
"""
raise NotImplementedError

def check_readiness(self) -> bool:
"""
Check that profiler is ready for use on current platform.
"""
raise NotImplementedError

def stop(self) -> None:
pass

Expand Down Expand Up @@ -99,6 +105,9 @@ def __init__(
f"profiling mode: {profiler_state.profiling_mode}"
)

def check_readiness(self) -> bool:
return True


class NoopProfiler(ProfilerInterface):
"""
Expand All @@ -108,6 +117,9 @@ class NoopProfiler(ProfilerInterface):
def snapshot(self) -> ProcessToProfileData:
return {}

def check_readiness(self) -> bool:
return True

@classmethod
def is_noop_profiler(cls, profile_instance: ProfilerInterface) -> bool:
return isinstance(profile_instance, cls)
Expand Down
Loading