Skip to content

Commit

Permalink
python: Add py-spy build logic, and upgrade it (#68)
Browse files Browse the repository at this point in the history
* Upgrade py-spy to v0.3.6 (commit fcf4aa16587ae0b425d7533b828827901d14b24e).
* Build py-spy as part of gProfiler's build, so upgrading it as as easy as changing the revision. It's also nicer when there's just a single build script.
  • Loading branch information
Jongy authored May 12, 2021
1 parent 4201d64 commit 7a60f2c
Show file tree
Hide file tree
Showing 10 changed files with 99 additions and 16 deletions.
11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
FROM rust:latest AS pyspy-builder

COPY scripts/pyspy_env.sh .
RUN ./pyspy_env.sh

COPY scripts/pyspy_build.sh .
RUN ./pyspy_build.sh


FROM ubuntu:20.04 as bcc-builder

RUN apt-get update
Expand Down Expand Up @@ -26,6 +35,8 @@ COPY --from=bcc-builder /bcc/bcc/LICENSE.txt gprofiler/resources/python/pyperf/
COPY --from=bcc-builder /bcc/bcc/licenses gprofiler/resources/python/pyperf/licenses
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 scripts/build.sh scripts/build.sh
RUN ./scripts/build.sh

Expand Down
2 changes: 0 additions & 2 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,3 @@ black
mypy
isort
docker
pyinstaller==4.0
staticx
3 changes: 3 additions & 0 deletions exe-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# requirements for the standalone executable
pyinstaller==4.0
staticx==0.12.1
15 changes: 13 additions & 2 deletions pyi.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# copied from Dockerfile
FROM rust:latest AS pyspy-builder

COPY scripts/pyspy_env.sh .
RUN ./pyspy_env.sh

COPY scripts/pyspy_build.sh .
RUN ./pyspy_build.sh

# Centos 7 image is used to grab an old version of `glibc` during `pyinstaller` bundling.
# This will allow the executable to run on older versions of the kernel, eventually leading to the executable running on a wider range of machines.
FROM centos:7 AS build-stage
Expand Down Expand Up @@ -43,8 +52,8 @@ RUN yum install -y gcc python3 curl python3-pip patchelf python3-devel upx
COPY requirements.txt requirements.txt
RUN python3 -m pip install -r requirements.txt

COPY dev-requirements.txt dev-requirements.txt
RUN python3 -m pip install -r dev-requirements.txt
COPY exe-requirements.txt exe-requirements.txt
RUN python3 -m pip install -r exe-requirements.txt

COPY scripts/build.sh scripts/build.sh
RUN ./scripts/build.sh
Expand All @@ -56,6 +65,8 @@ RUN cp /bcc/bcc/LICENSE.txt gprofiler/resources/python/pyperf/
RUN cp -r /bcc/bcc/licenses gprofiler/resources/python/pyperf/licenses
RUN cp /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 gprofiler gprofiler

# run PyInstaller and make sure no 'gprofiler.*' modules are missing.
Expand Down
5 changes: 0 additions & 5 deletions scripts/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@ curl -fL https://github.com/Granulate/async-profiler/releases/download/v2.0g1/as
-z build/async-profiler-2.0-linux-x64.tar.gz -o build/async-profiler-2.0-linux-x64.tar.gz
tar -xzf build/async-profiler-2.0-linux-x64.tar.gz -C gprofiler/resources/java --strip-components=2 async-profiler-2.0-linux-x64/build

# py-spy
mkdir -p gprofiler/resources/python
curl -fL https://github.com/Granulate/py-spy/releases/download/v0.3.5g1/py-spy -o gprofiler/resources/python/py-spy
chmod +x gprofiler/resources/python/py-spy

# pyperf - just create the directory for it, it will be built/downloaded later
mkdir -p gprofiler/resources/python/pyperf

Expand Down
10 changes: 10 additions & 0 deletions scripts/pyspy_build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
#
# Copyright (c) Granulate. All rights reserved.
# Licensed under the AGPL3 License. See LICENSE.md in the project root for license information.
#
set -euo pipefail

git clone --depth 1 -b v0.3.6g1 https://github.com/Granulate/py-spy.git && git -C py-spy reset --hard fcf4aa16587ae0b425d7533b828827901d14b24e
cd py-spy
cargo build --release --target=x86_64-unknown-linux-musl
39 changes: 39 additions & 0 deletions scripts/pyspy_env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
#
# Copyright (c) Granulate. All rights reserved.
# Licensed under the AGPL3 License. See LICENSE.md in the project root for license information.
#
set -euo pipefail

# prepares the environment for building py-spy:
# 1. installs the rust target x86_64-unknown-linux-musl - this can build static binaries
# 2. downloads, builds & installs libunwind with musl
# 2. downloads, builds & installs libz with musl
# I use musl because it builds statically. otherwise, we need to build with old glibc; I tried to
# build on centos:7 but it caused some errors out-of-the-box (libunwind was built w/o -fPIC and rust tried
# to build it as shared (?))
# in any way, building it static solves all issues. and I find it better to use more recent versions of libraries
# like libunwind/zlib.

rustup target add x86_64-unknown-linux-musl

apt-get update && apt-get install -y musl-dev musl-tools

mkdir builds && cd builds

wget https://github.com/libunwind/libunwind/releases/download/v1.5/libunwind-1.5.0.tar.gz
tar -xf libunwind-1.5.0.tar.gz
cd libunwind-1.5.0
CC=musl-gcc ./configure --disable-minidebuginfo --enable-ptrace --disable-tests --disable-documentation
make
make install

wget https://zlib.net/zlib-1.2.11.tar.xz
tar -xf zlib-1.2.11.tar.xz
cd zlib-1.2.11
# note the use of --prefix here. it matches the directory https://github.com/benfred/remoteprocess/blob/master/build.rs expects to find libs for musl.
# the libunwind configure may install it in /usr/local/lib for all I care, but if we override /usr/local/lib/libz... with the musl ones,
# it won't do any good...
CC=musl-gcc ./configure --prefix=/usr/local/musl/x86_64-unknown-linux-musl
make
make install
3 changes: 3 additions & 0 deletions tests/test_libpython.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from gprofiler.python import get_python_profiler
from tests import CONTAINERS_DIRECTORY
from tests.utils import copy_pyspy_from_image


@pytest.fixture
Expand All @@ -30,11 +31,13 @@ def test_python_select_by_libpython(
tmp_path,
application_docker_container,
assert_collapsed,
gprofiler_docker_image: Image,
) -> None:
"""
Tests that profiling of processes running Python, whose basename(readlink("/proc/pid/exe")) isn't "python".
(for example, uwsgi). We expect to select these because they have "libpython" in their "/proc/pid/maps".
"""
copy_pyspy_from_image(gprofiler_docker_image)
with get_python_profiler(1000, 1, Event(), str(tmp_path)) as profiler:
process_collapsed = profiler.snapshot()
assert_collapsed(process_collapsed.get(application_docker_container.attrs["State"]["Pid"]))
16 changes: 9 additions & 7 deletions tests/test_sanity.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from gprofiler.merge import parse_one_collapsed
from gprofiler.python import PySpyProfiler, PythonEbpfProfiler
from gprofiler.utils import resource_path
from tests.utils import copy_file_from_image, run_privileged_container
from tests.utils import copy_file_from_image, copy_pyspy_from_image, run_privileged_container


@pytest.mark.parametrize("runtime", ["java"])
Expand All @@ -24,20 +24,22 @@ def test_java_from_host(
application_pid: int,
assert_collapsed: Callable[[Optional[Mapping[str, int]]], None],
) -> None:
profiler = JavaProfiler(1000, 1, True, Event(), str(tmp_path))
process_collapsed = profiler.snapshot()
assert_collapsed(process_collapsed.get(application_pid))
with JavaProfiler(1000, 1, True, Event(), str(tmp_path)) as profiler:
process_collapsed = profiler.snapshot()
assert_collapsed(process_collapsed.get(application_pid))


@pytest.mark.parametrize("runtime", ["python"])
def test_pyspy(
tmp_path: Path,
application_pid: int,
assert_collapsed: Callable[[Optional[Mapping[str, int]]], None],
gprofiler_docker_image: Image,
) -> None:
profiler = PySpyProfiler(1000, 1, Event(), str(tmp_path))
process_collapsed = profiler.snapshot()
assert_collapsed(process_collapsed.get(application_pid))
copy_pyspy_from_image(gprofiler_docker_image)
with PySpyProfiler(1000, 1, Event(), str(tmp_path)) as profiler:
process_collapsed = profiler.snapshot()
assert_collapsed(process_collapsed.get(application_pid))


@pytest.mark.parametrize("runtime", ["python"])
Expand Down
11 changes: 11 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from docker import DockerClient
from docker.models.images import Image

from gprofiler.utils import resource_path


def run_privileged_container(
docker_client: DockerClient,
Expand Down Expand Up @@ -48,3 +50,12 @@ def chmod_path_parts(path: Path, add_mode: int) -> None:
for i in range(1, len(path.parts)):
subpath = os.path.join(*path.parts[:i])
os.chmod(subpath, os.stat(subpath).st_mode | add_mode)


def copy_pyspy_from_image(gprofiler_docker_image: Image):
# get py-spy from the built container
copy_file_from_image(
gprofiler_docker_image,
os.path.join("/app", "gprofiler", "resources", "python", "py-spy"),
resource_path("python/py-spy"),
)

0 comments on commit 7a60f2c

Please sign in to comment.