Skip to content

Commit 340f8d4

Browse files
mergify[bot]Julien DanjouKyle-Verhoog
authored
fix(profiling): restart the profiler on uwsgi fork (#2295) (#2302)
In uWSGI, the current code starts in the forked process the ProfilerInstance that has been created in the master process. That actually works fine, except all Events are discarded by the Recorder because its `pid` attribute is not updated: it is still the pid of the master process. Using the `_restart_on_fork` method makes sure that the `ProfilerInstance` gets a new `Recorder` object with the correct PID. The test is updated to not test only for file presence, but making sure that there is at least one sample included. This removes test_call_script_pprof_output_interval as it is flaky by construction and fails with the new extended test. Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> (cherry picked from commit ea5992b) Co-authored-by: Julien Danjou <[email protected]> Co-authored-by: Kyle Verhoog <[email protected]>
1 parent 7198d79 commit 340f8d4

File tree

6 files changed

+38
-29
lines changed

6 files changed

+38
-29
lines changed

ddtrace/profiling/profiler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def start(self, stop_on_exit=True, profile_children=True):
7373

7474
if profile_children:
7575
try:
76-
uwsgi.check_uwsgi(self.start, atexit=self.stop if stop_on_exit else None)
76+
uwsgi.check_uwsgi(self._restart_on_fork, atexit=self.stop if stop_on_exit else None)
7777
except uwsgi.uWSGIMasterProcess:
7878
# Do nothing, the start() method will be called in each worker subprocess
7979
return
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- |
4+
In certain circumstances, the profiles generated in a uWSGI application
5+
could have been empty. This is now fixed and the profiler records correctly
6+
the generated events.

tests/profiling/exporter/test_file.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
from ddtrace.profiling.exporter import file
44

5-
from .. import test_main
5+
from .. import utils
66
from ..exporter import test_pprof
77

88

99
def test_export(tmp_path):
1010
filename = str(tmp_path / "pprof")
1111
exp = file.PprofFileExporter(filename)
1212
exp.export(test_pprof.TEST_EVENTS, 0, 1)
13-
test_main.check_pprof_file(filename + "." + str(os.getpid()) + ".1")
13+
utils.check_pprof_file(filename + "." + str(os.getpid()) + ".1")

tests/profiling/test_main.py

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import gzip
21
import os
32
import subprocess
43

54
import pytest
65

7-
from ddtrace.profiling.exporter import pprof_pb2
6+
from . import utils
87

98

109
def call_program(*args):
@@ -38,15 +37,6 @@ def test_call_script_gevent(monkeypatch):
3837
assert exitcode == 0
3938

4039

41-
def check_pprof_file(filename):
42-
with gzip.open(filename, "rb") as f:
43-
content = f.read()
44-
p = pprof_pb2.Profile()
45-
p.ParseFromString(content)
46-
assert len(p.sample_type) == 11
47-
assert p.string_table[p.sample_type[0].type] == "cpu-samples"
48-
49-
5040
def test_call_script_pprof_output(tmp_path, monkeypatch):
5141
"""This checks if the pprof output and atexit register work correctly.
5242
@@ -58,17 +48,10 @@ def test_call_script_pprof_output(tmp_path, monkeypatch):
5848
monkeypatch.setenv("DD_PROFILING_ENABLED", "1")
5949
_, _, exitcode, pid = call_program("ddtrace-run", os.path.join(os.path.dirname(__file__), "simple_program.py"))
6050
assert exitcode == 42
61-
check_pprof_file(filename + "." + str(pid) + ".1")
51+
utils.check_pprof_file(filename + "." + str(pid) + ".1")
6252
return filename, pid
6353

6454

65-
def test_call_script_pprof_output_interval(tmp_path, monkeypatch):
66-
monkeypatch.setenv("DD_PROFILING_UPLOAD_INTERVAL", "0.1")
67-
filename, pid = test_call_script_pprof_output(tmp_path, monkeypatch)
68-
for i in (2, 3):
69-
check_pprof_file(filename + "." + str(pid) + (".%d" % i))
70-
71-
7255
def test_fork(tmp_path, monkeypatch):
7356
filename = str(tmp_path / "pprof")
7457
monkeypatch.setenv("DD_PROFILING_API_TIMEOUT", "0.1")
@@ -79,8 +62,8 @@ def test_fork(tmp_path, monkeypatch):
7962
)
8063
assert exitcode == 0
8164
child_pid = stdout.decode().strip()
82-
check_pprof_file(filename + "." + str(pid) + ".1")
83-
check_pprof_file(filename + "." + str(child_pid) + ".1")
65+
utils.check_pprof_file(filename + "." + str(pid) + ".1")
66+
utils.check_pprof_file(filename + "." + str(child_pid) + ".1")
8467

8568

8669
@pytest.mark.skipif(not os.getenv("DD_PROFILE_TEST_GEVENT", False), reason="Not testing gevent")

tests/profiling/test_uwsgi.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@
1010

1111
from tests.contrib.uwsgi import run_uwsgi
1212

13+
from . import utils
14+
1315

1416
uwsgi_app = os.path.join(os.path.dirname(__file__), "uwsgi-app.py")
1517

1618

1719
@pytest.fixture
18-
def uwsgi():
20+
def uwsgi(monkeypatch):
21+
# Do not ignore profiler so we have samples in the output pprof
22+
monkeypatch.setenv("DD_PROFILING_IGNORE_PROFILER", "0")
1923
# Do not use pytest tmpdir fixtures which generate directories longer than allowed for a socket file name
2024
socket_name = tempfile.mktemp()
2125
cmd = ["uwsgi", "--need-app", "--die-on-term", "--socket", socket_name, "--wsgi-file", uwsgi_app]
@@ -43,7 +47,7 @@ def test_uwsgi_threads_enabled(uwsgi, tmp_path, monkeypatch):
4347
proc.terminate()
4448
assert proc.wait() == 30
4549
for pid in worker_pids:
46-
assert os.path.exists("%s.%d.1" % (filename, pid))
50+
utils.check_pprof_file("%s.%d.1" % (filename, pid))
4751

4852

4953
def test_uwsgi_threads_processes_no_master(uwsgi, monkeypatch):
@@ -85,7 +89,7 @@ def test_uwsgi_threads_processes_master(uwsgi, tmp_path, monkeypatch):
8589
proc.terminate()
8690
assert proc.wait() == 0
8791
for pid in worker_pids:
88-
assert os.path.exists("%s.%d.1" % (filename, pid))
92+
utils.check_pprof_file("%s.%d.1" % (filename, pid))
8993

9094

9195
def test_uwsgi_threads_processes_master_lazy_apps(uwsgi, tmp_path, monkeypatch):
@@ -98,7 +102,7 @@ def test_uwsgi_threads_processes_master_lazy_apps(uwsgi, tmp_path, monkeypatch):
98102
proc.terminate()
99103
assert proc.wait() == 0
100104
for pid in worker_pids:
101-
assert os.path.exists("%s.%d.1" % (filename, pid))
105+
utils.check_pprof_file("%s.%d.1" % (filename, pid))
102106

103107

104108
# For whatever reason this crashes easily on Python 2.7 with a segfault, and hangs on Python before 3.7.
@@ -154,4 +158,4 @@ def test_uwsgi_threads_processes_no_master_lazy_apps(uwsgi, tmp_path, monkeypatc
154158
except OSError:
155159
break
156160
for pid in worker_pids:
157-
assert os.path.exists("%s.%d.1" % (filename, pid))
161+
utils.check_pprof_file("%s.%d.1" % (filename, pid))

tests/profiling/utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import gzip
2+
3+
from ddtrace.profiling.exporter import pprof_pb2
4+
5+
6+
def check_pprof_file(
7+
filename, # type: str
8+
):
9+
# type: (...) -> None
10+
with gzip.open(filename, "rb") as f:
11+
content = f.read()
12+
p = pprof_pb2.Profile()
13+
p.ParseFromString(content)
14+
assert len(p.sample_type) == 11
15+
assert p.string_table[p.sample_type[0].type] == "cpu-samples"
16+
assert len(p.sample) >= 1

0 commit comments

Comments
 (0)