Some programs cannot be restarted just to profile them. A worker that slows down after hours of work, or a server handling live traffic, has to be measured while it runs. Since Python 3.15, the profiling package provides the profiling.sampling module, which can attach to a running process and sample its call stack from the outside. The process keeps running while it is measured, and it continues normally after the profiler detaches.
The following script imitates a worker that repeats the same job forever:
import time
rows = [(f'user{i % 500}', i) for i in range(200000)]
def unique_names(records):
names = []
for name, _ in records:
if name not in names:
names.append(name)
return names
while True:
unique_names(rows)
time.sleep(0.01)
Start it and leave it running:
python main.py
Open a second terminal and find the process ID:
pgrep -f main.py
2514
The attach command takes that process ID and samples the process for the given number of seconds:
sudo python -m profiling.sampling attach -d 5 2514
Captured 5000 samples in 5.00 seconds
Sample rate: 999.97 samples/sec
Error rate: 0.00
Profile Stats:
nsamples sample% tottime (s) cumul% cumtime (s) filename:lineno(function)
4674/4674 93.5 4.674 93.5 4.674 main.py:9(unique_names)
276/276 5.5 0.276 5.5 0.276 main.py:16(<module>)
50/50 1.0 0.050 1.0 0.050 main.py:8(unique_names)
0/4724 0.0 0.000 94.5 4.724 main.py:15(<module>)
The -d option sets how long to sample, so -d 5 collects five seconds of data and then stops. Without it the profiler keeps sampling until it is interrupted. The last number is the process ID.
The number after the file name is the line that was executing when the sample was taken, not the line where the function is defined. Line 9 is if name not in names, and it accounts for 93.5% of the time. Line 8 is the for statement, and line 15 is the call to unique_names. Line 16 is time.sleep, which appears because the profiler measures elapsed time, including the time a program spends waiting.
The command needs sudo because reading the memory of another process requires the CAP_SYS_PTRACE capability. On Ubuntu, the kernel.yama.ptrace_scope setting defaults to 1, which allows a process to trace only its own children. The profiler is not the parent of the worker, so it needs the capability instead.
Leave a Comment
Cancel reply