Skip to content

Commit

Permalink
Initial PHP support using phpspy (#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
guybortnikov authored May 25, 2021
1 parent 6201bb0 commit 232285e
Show file tree
Hide file tree
Showing 15 changed files with 434 additions and 26 deletions.
10 changes: 10 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ WORKDIR /bcc
COPY ./scripts/pyperf_build.sh .
RUN ./pyperf_build.sh

# ubuntu:20.04
FROM ubuntu@sha256:cf31af331f38d1d7158470e095b132acd126a7180a54f263d386da88eb681d93 as phpspy-builder
RUN apt update && apt install -y git wget make gcc
COPY scripts/phpspy_build.sh .
RUN ./phpspy_build.sh


# ubuntu 20.04
FROM ubuntu@sha256:cf31af331f38d1d7158470e095b132acd126a7180a54f263d386da88eb681d93
Expand All @@ -47,6 +53,10 @@ COPY --from=bcc-builder /bcc/bcc/NOTICE gprofiler/resources/python/pyperf/
COPY --from=pyspy-builder /py-spy/target/x86_64-unknown-linux-musl/release/py-spy gprofiler/resources/python/py-spy
COPY --from=perf-builder /perf gprofiler/resources/perf

RUN mkdir -p gprofiler/resources/python/phpspy
COPY --from=phpspy-builder /phpspy/phpspy gprofiler/resources/php/phpspy
COPY --from=phpspy-builder /binutils/binutils-2.25/bin/bin/objdump gprofiler/resources/php/objdump

COPY scripts/build.sh scripts/build.sh
RUN ./scripts/build.sh

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ Alongside `perf`, gProfiler invokes runtime-specific profilers for processes bas
* The CPython interpreter, versions 2.7 and 3.5-3.9.
* eBPF profiling (based on PyPerf) requires Linux 4.14 or higher. Profiling using eBPF incurs lower overhead. This requires kernel headers to be installed.
* If eBPF is not available for whatever reason, py-spy is used.
* PHP (Zend Engine), versions 7.0-8.0.
* Uses [Granulate's fork](https://github.com/Granulate/phpspy/) of the phpspy project.

The runtime-specific profilers produce stack traces that include runtime information (i.e, stacks of Java/Python functions), unlike `perf` which produces native stacks of the JVM / CPython interpreter.
The runtime stacks are then merged into the data collected by `perf`, substituting the *native* stacks `perf` has collected for those processes.
Expand All @@ -133,6 +135,7 @@ We recommend going through our [contribution guide](https://github.com/granulate
* [async-profiler](https://github.com/jvm-profiling-tools/async-profiler) by [Andrei Pangin](https://github.com/apangin). See [our fork](https://github.com/Granulate/async-profiler).
* [py-spy](https://github.com/benfred/py-spy) by [Ben Frederickson](https://github.com/benfred). See [our fork](https://github.com/Granulate/py-spy).
* [bcc](https://github.com/iovisor/bcc) (for PyPerf) by the IO Visor project. See [our fork](https://github.com/Granulate/bcc).
* [phpspy](https://github.com/adsr/phpspy) by [Adam Saponara](https://github.com/adsr). See [our fork](https://github.com/Granulate/phpspy).

# Footnotes

Expand Down
74 changes: 63 additions & 11 deletions gprofiler/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from pathlib import Path
from socket import gethostname
from threading import Event
from typing import Dict, Optional
from typing import Callable, Dict, Optional

import configargparse
from requests import RequestException, Timeout
Expand All @@ -24,6 +24,7 @@
from .client import DEFAULT_UPLOAD_TIMEOUT, GRANULATE_SERVER_HOST, APIClient, APIError
from .java import JavaProfiler
from .perf import SystemProfiler
from .php import PHPSpyProfiler
from .profiler_base import NoopProfiler
from .python import get_python_profiler
from .utils import (
Expand Down Expand Up @@ -66,6 +67,14 @@ def sigint_handler(sig, frame):
raise KeyboardInterrupt


def create_profiler_or_noop(profiler_constructor_callback: Callable, runtime_name: str):
try:
return profiler_constructor_callback()
except Exception:
logger.exception(f"Couldn't create {runtime_name} profiler, continuing without this runtime profiler")
return NoopProfiler()


class GProfiler:
def __init__(
self,
Expand All @@ -76,6 +85,7 @@ def __init__(
rotating_output: bool,
runtimes: Dict[str, bool],
client: APIClient,
php_process_filter: str = PHPSpyProfiler.DEFAULT_PROCESS_FILTER,
):
self._frequency = frequency
self._duration = duration
Expand All @@ -93,14 +103,29 @@ def __init__(
# files unnecessarily.
self._temp_storage_dir = TemporaryDirectoryWithMode(dir=TEMPORARY_STORAGE_PATH, mode=0o755)
self.java_profiler = (
JavaProfiler(self._frequency, self._duration, True, self._stop_event, self._temp_storage_dir.name)
create_profiler_or_noop(
lambda: JavaProfiler(
self._frequency, self._duration, True, self._stop_event, self._temp_storage_dir.name
),
"java",
)
if self._runtimes["java"]
else NoopProfiler()
)
self.system_profiler = SystemProfiler(
self._frequency, self._duration, self._stop_event, self._temp_storage_dir.name
)
self.initialize_python_profiler()
self.php_profiler = (
create_profiler_or_noop(
lambda: PHPSpyProfiler(
self._frequency, self._duration, self._stop_event, self._temp_storage_dir.name, php_process_filter
),
"php",
)
if self._runtimes["php"]
else NoopProfiler()
)

def __enter__(self):
self.start()
Expand All @@ -111,12 +136,15 @@ def __exit__(self, exc_type, exc_val, exc_tb):

def initialize_python_profiler(self) -> None:
self.python_profiler = (
get_python_profiler(
self._frequency,
self._duration,
self._stop_event,
self._temp_storage_dir.name,
self.initialize_python_profiler,
create_profiler_or_noop(
lambda: get_python_profiler(
self._frequency,
self._duration,
self._stop_event,
self._temp_storage_dir.name,
self.initialize_python_profiler,
),
"python",
)
if self._runtimes["python"]
else NoopProfiler()
Expand Down Expand Up @@ -179,6 +207,7 @@ def start(self):
self.python_profiler,
self.java_profiler,
self.system_profiler,
self.php_profiler,
):
prof.start()

Expand All @@ -190,6 +219,7 @@ def stop(self):
self.python_profiler,
self.java_profiler,
self.system_profiler,
self.php_profiler,
):
prof.stop()

Expand All @@ -201,11 +231,13 @@ def _snapshot(self):
java_future.name = "java"
python_future = self._executor.submit(self.python_profiler.snapshot)
python_future.name = "python"
php_future = self._executor.submit(self.php_profiler.snapshot)
php_future.name = "php"
system_future = self._executor.submit(self.system_profiler.snapshot)
system_future.name = "system"

process_perfs: Dict[int, Dict[str, int]] = {}
for future in concurrent.futures.as_completed([java_future, python_future]):
for future in concurrent.futures.as_completed([java_future, python_future, php_future]):
# if either of these fail - log it, and continue.
try:
process_perfs.update(future.result())
Expand Down Expand Up @@ -325,6 +357,19 @@ def parse_cmd_args():
default=True,
help="Do not invoke runtime-specific profilers for Python processes",
)
parser.add_argument(
"--no-php",
dest="php",
action="store_false",
default=True,
help="Do not invoke runtime-specific profilers for PHP processes",
)
parser.add_argument(
"--php-proc-filter",
dest="php_process_filter",
default=PHPSpyProfiler.DEFAULT_PROCESS_FILTER,
help="Process filter for php processes (default: %(default)s)",
)

parser.add_argument(
"-u",
Expand Down Expand Up @@ -463,9 +508,16 @@ def main():
logger.error(f"Failed to connect to server: {e}")
return

runtimes = {"java": args.java, "python": args.python}
runtimes = {"java": args.java, "python": args.python, "php": args.php}
gprofiler = GProfiler(
args.frequency, args.duration, args.output_dir, args.flamegraph, args.rotating_output, runtimes, client
args.frequency,
args.duration,
args.output_dir,
args.flamegraph,
args.rotating_output,
runtimes,
client,
args.php_process_filter,
)
logger.info("gProfiler initialized and ready to start profiling")

Expand Down
Loading

0 comments on commit 232285e

Please sign in to comment.