diff --git a/Dockerfile b/Dockerfile index e41121522..db051dfa9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 @@ -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 diff --git a/dev-requirements.txt b/dev-requirements.txt index 351f69544..06cc9f092 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -4,5 +4,3 @@ black mypy isort docker -pyinstaller==4.0 -staticx diff --git a/exe-requirements.txt b/exe-requirements.txt new file mode 100644 index 000000000..88c4802b1 --- /dev/null +++ b/exe-requirements.txt @@ -0,0 +1,3 @@ +# requirements for the standalone executable +pyinstaller==4.0 +staticx==0.12.1 diff --git a/pyi.Dockerfile b/pyi.Dockerfile index 54f399ad7..db13ae1f1 100644 --- a/pyi.Dockerfile +++ b/pyi.Dockerfile @@ -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 @@ -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 @@ -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. diff --git a/scripts/build.sh b/scripts/build.sh index d6d0fc4d1..4d91aab0d 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -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 diff --git a/scripts/pyspy_build.sh b/scripts/pyspy_build.sh new file mode 100755 index 000000000..84827e3db --- /dev/null +++ b/scripts/pyspy_build.sh @@ -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 diff --git a/scripts/pyspy_env.sh b/scripts/pyspy_env.sh new file mode 100755 index 000000000..25e9fe1af --- /dev/null +++ b/scripts/pyspy_env.sh @@ -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 diff --git a/tests/test_libpython.py b/tests/test_libpython.py index 575a84d95..e786c873a 100644 --- a/tests/test_libpython.py +++ b/tests/test_libpython.py @@ -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 @@ -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"])) diff --git a/tests/test_sanity.py b/tests/test_sanity.py index 955f8ca5f..775d66038 100644 --- a/tests/test_sanity.py +++ b/tests/test_sanity.py @@ -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"]) @@ -24,9 +24,9 @@ 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"]) @@ -34,10 +34,12 @@ 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"]) diff --git a/tests/utils.py b/tests/utils.py index da536454d..f408daef0 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -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, @@ -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"), + )