blob: 2b2122454df68d12a50c520d63baaa6d92994588 [file] [log] [blame]
Łukasz Langaa785c872016-09-09 17:37:37 -07001.. _instrumentation:
2
3===============================================
4Instrumenting CPython with DTrace and SystemTap
5===============================================
6
7:author: David Malcolm
8:author: Łukasz Langa
9
10DTrace and SystemTap are monitoring tools, each providing a way to inspect
11what the processes on a computer system are doing. They both use
12domain-specific languages allowing a user to write scripts which:
13
14 - filter which processes are to be observed
15 - gather data from the processes of interest
16 - generate reports on the data
17
18As of Python 3.6, CPython can be built with embedded "markers", also
19known as "probes", that can be observed by a DTrace or SystemTap script,
20making it easier to monitor what the CPython processes on a system are
21doing.
22
23.. I'm using ".. code-block:: c" for SystemTap scripts, as "c" is syntactically
24 the closest match that Sphinx supports
25
26.. impl-detail::
27
28 DTrace markers are implementation details of the CPython interpreter.
29 No guarantees are made about probe compatibility between versions of
30 CPython. DTrace scripts can stop working or work incorrectly without
31 warning when changing CPython versions.
32
33
34Enabling the static markers
35---------------------------
36
37macOS comes with built-in support for DTrace. On Linux, in order to
38build CPython with the embedded markers for SystemTap, the SystemTap
39development tools must be installed.
40
41On a Linux machine, this can be done via::
42
43 yum install systemtap-sdt-devel
44
45or::
46
47 sudo apt-get install systemtap-sdt-dev
48
49
50CPython must then be configured `--with-dtrace`::
51
52 checking for --with-dtrace... yes
53
54On macOS, you can list available DTrace probes by running a Python
55process in the background and listing all probes made available by the
56Python provider::
57
58 $ python3.6 -q &
59 $ sudo dtrace -l -P python$! # or: dtrace -l -m python3.6
60
61 ID PROVIDER MODULE FUNCTION NAME
62 29564 python18035 python3.6 _PyEval_EvalFrameDefault function-entry
63 29565 python18035 python3.6 dtrace_function_entry function-entry
64 29566 python18035 python3.6 _PyEval_EvalFrameDefault function-return
65 29567 python18035 python3.6 dtrace_function_return function-return
66 29568 python18035 python3.6 collect gc-done
67 29569 python18035 python3.6 collect gc-start
68 29570 python18035 python3.6 _PyEval_EvalFrameDefault line
69 29571 python18035 python3.6 maybe_dtrace_line line
70
71On Linux, you can verify if the SystemTap static markers are present in
72the built binary by seeing if it contains a ".note.stapsdt" section.
73
74.. code-block:: bash
75
76 $ readelf -S ./python | grep .note.stapsdt
77 [30] .note.stapsdt NOTE 0000000000000000 00308d78
78
79If you've built Python as a shared library (with --enable-shared), you
80need to look instead within the shared library. For example:
81
82.. code-block:: bash
83
84 $ readelf -S libpython3.3dm.so.1.0 | grep .note.stapsdt
85 [29] .note.stapsdt NOTE 0000000000000000 00365b68
86
87Sufficiently modern readelf can print the metadata:
88
89.. code-block:: bash
90
91 $ readelf -n ./python
92
93 Displaying notes found at file offset 0x00000254 with length 0x00000020:
94 Owner Data size Description
95 GNU 0x00000010 NT_GNU_ABI_TAG (ABI version tag)
96 OS: Linux, ABI: 2.6.32
97
98 Displaying notes found at file offset 0x00000274 with length 0x00000024:
99 Owner Data size Description
100 GNU 0x00000014 NT_GNU_BUILD_ID (unique build ID bitstring)
101 Build ID: df924a2b08a7e89f6e11251d4602022977af2670
102
103 Displaying notes found at file offset 0x002d6c30 with length 0x00000144:
104 Owner Data size Description
105 stapsdt 0x00000031 NT_STAPSDT (SystemTap probe descriptors)
106 Provider: python
107 Name: gc__start
108 Location: 0x00000000004371c3, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6bf6
109 Arguments: -4@%ebx
110 stapsdt 0x00000030 NT_STAPSDT (SystemTap probe descriptors)
111 Provider: python
112 Name: gc__done
113 Location: 0x00000000004374e1, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6bf8
114 Arguments: -8@%rax
115 stapsdt 0x00000045 NT_STAPSDT (SystemTap probe descriptors)
116 Provider: python
117 Name: function__entry
118 Location: 0x000000000053db6c, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6be8
119 Arguments: 8@%rbp 8@%r12 -4@%eax
120 stapsdt 0x00000046 NT_STAPSDT (SystemTap probe descriptors)
121 Provider: python
122 Name: function__return
123 Location: 0x000000000053dba8, Base: 0x0000000000630ce2, Semaphore: 0x00000000008d6bea
124 Arguments: 8@%rbp 8@%r12 -4@%eax
125
126The above metadata contains information for SystemTap describing how it
127can patch strategically-placed machine code instructions to enable the
128tracing hooks used by a SystemTap script.
129
130
131Static DTrace probes
132--------------------
133
134The following example DTrace script can be used to show the call/return
135hierarchy of a Python script, only tracing within the invocation of
136a function called "start". In other words, import-time function
137invocations are not going to be listed:
138
139.. code-block:: c
140
141 self int indent;
142
143 python$target:::function-entry
144 /copyinstr(arg1) == "start"/
145 {
146 self->trace = 1;
147 }
148
149 python$target:::function-entry
150 /self->trace/
151 {
152 printf("%d\t%*s:", timestamp, 15, probename);
153 printf("%*s", self->indent, "");
154 printf("%s:%s:%d\n", basename(copyinstr(arg0)), copyinstr(arg1), arg2);
155 self->indent++;
156 }
157
158 python$target:::function-return
159 /self->trace/
160 {
161 self->indent--;
162 printf("%d\t%*s:", timestamp, 15, probename);
163 printf("%*s", self->indent, "");
164 printf("%s:%s:%d\n", basename(copyinstr(arg0)), copyinstr(arg1), arg2);
165 }
166
167 python$target:::function-return
168 /copyinstr(arg1) == "start"/
169 {
170 self->trace = 0;
171 }
172
173It can be invoked like this:
174
175.. code-block:: bash
176
177 $ sudo dtrace -q -s call_stack.d -c "python3.6 script.py"
178
179The output looks like this::
180
181 156641360502280 function-entry:call_stack.py:start:23
182 156641360518804 function-entry: call_stack.py:function_1:1
183 156641360532797 function-entry: call_stack.py:function_3:9
184 156641360546807 function-return: call_stack.py:function_3:10
185 156641360563367 function-return: call_stack.py:function_1:2
186 156641360578365 function-entry: call_stack.py:function_2:5
187 156641360591757 function-entry: call_stack.py:function_1:1
188 156641360605556 function-entry: call_stack.py:function_3:9
189 156641360617482 function-return: call_stack.py:function_3:10
190 156641360629814 function-return: call_stack.py:function_1:2
191 156641360642285 function-return: call_stack.py:function_2:6
192 156641360656770 function-entry: call_stack.py:function_3:9
193 156641360669707 function-return: call_stack.py:function_3:10
194 156641360687853 function-entry: call_stack.py:function_4:13
195 156641360700719 function-return: call_stack.py:function_4:14
196 156641360719640 function-entry: call_stack.py:function_5:18
197 156641360732567 function-return: call_stack.py:function_5:21
198 156641360747370 function-return:call_stack.py:start:28
199
200
201Static SystemTap markers
202------------------------
203
204The low-level way to use the SystemTap integration is to use the static
205markers directly. This requires you to explicitly state the binary file
206containing them.
207
208For example, this SystemTap script can be used to show the call/return
209hierarchy of a Python script:
210
211.. code-block:: c
212
213 probe process('python').mark("function__entry") {
214 filename = user_string($arg1);
215 funcname = user_string($arg2);
216 lineno = $arg3;
217
218 printf("%s => %s in %s:%d\\n",
219 thread_indent(1), funcname, filename, lineno);
220 }
221
222 probe process('python').mark("function__return") {
223 filename = user_string($arg1);
224 funcname = user_string($arg2);
225 lineno = $arg3;
226
227 printf("%s <= %s in %s:%d\\n",
228 thread_indent(-1), funcname, filename, lineno);
229 }
230
231It can be invoked like this:
232
233.. code-block:: bash
234
235 $ stap \
236 show-call-hierarchy.stp \
237 -c ./python test.py
238
239The output looks like this::
240
241 11408 python(8274): => __contains__ in Lib/_abcoll.py:362
242 11414 python(8274): => __getitem__ in Lib/os.py:425
243 11418 python(8274): => encode in Lib/os.py:490
244 11424 python(8274): <= encode in Lib/os.py:493
245 11428 python(8274): <= __getitem__ in Lib/os.py:426
246 11433 python(8274): <= __contains__ in Lib/_abcoll.py:366
247
248where the columns are:
249
250 - time in microseconds since start of script
251
252 - name of executable
253
254 - PID of process
255
256and the remainder indicates the call/return hierarchy as the script executes.
257
258For a `--enable-shared` build of CPython, the markers are contained within the
259libpython shared library, and the probe's dotted path needs to reflect this. For
260example, this line from the above example::
261
262 probe process('python').mark("function__entry") {
263
264should instead read::
265
266 probe process('python').library("libpython3.6dm.so.1.0").mark("function__entry") {
267
268(assuming a debug build of CPython 3.6)
269
270
271Available static markers
272------------------------
273
274.. I'm reusing the "c:function" type for markers
275
276.. c:function:: function__entry(str filename, str funcname, int lineno)
277
278 This marker indicates that execution of a Python function has begun.
279 It is only triggered for pure-Python (bytecode) functions.
280
281 The filename, function name, and line number are provided back to the
282 tracing script as positional arguments, which must be accessed using
283 `$arg1`, `$arg2`, `$arg3`:
284
285 * `$arg1` : `(const char *)` filename, accessible using `user_string($arg1)`
286
287 * `$arg2` : `(const char *)` function name, accessible using
288 `user_string($arg2)`
289
290 * `$arg3` : `int` line number
291
292.. c:function:: function__return(str filename, str funcname, int lineno)
293
294 This marker is the converse of `function__entry`, and indicates that
295 execution of a Python function has ended (either via ``return``, or
296 via an exception). It is only triggered for pure-Python (bytecode)
297 functions.
298
299 The arguments are the same as for `function__entry`
300
301.. c:function:: line(str filename, str funcname, int lineno)
302
303 This marker indicates a Python line is about to be executed. It is
304 the equivalent of line-by-line tracing with a Python profiler. It is
305 not triggered within C functions.
306
307 The arguments are the same as for `function__entry`.
308
309.. c:function:: gc__start(int generation)
310
311 Fires when the Python interpreter starts a garbage collection cycle.
312 `arg0` is the generation to scan, like :func:`gc.collect()`.
313
314.. c:function:: gc__done(long collected)
315
316 Fires when the Python interpreter finishes a garbage collection
317 cycle. `arg0` is the number of collected objects.
318
319
320SystemTap Tapsets
321-----------------
322
323The higher-level way to use the SystemTap integration is to use a "tapset":
324SystemTap's equivalent of a library, which hides some of the lower-level
325details of the static markers.
326
327Here is a tapset file, based on a non-shared build of CPython:
328
329.. code-block:: c
330
331 /*
332 Provide a higher-level wrapping around the function__entry and
333 function__return markers:
334 \*/
335 probe python.function.entry = process("python").mark("function__entry")
336 {
337 filename = user_string($arg1);
338 funcname = user_string($arg2);
339 lineno = $arg3;
340 frameptr = $arg4
341 }
342 probe python.function.return = process("python").mark("function__return")
343 {
344 filename = user_string($arg1);
345 funcname = user_string($arg2);
346 lineno = $arg3;
347 frameptr = $arg4
348 }
349
350If this file is installed in SystemTap's tapset directory (e.g.
351`/usr/share/systemtap/tapset`), then these additional probepoints become
352available:
353
354.. c:function:: python.function.entry(str filename, str funcname, int lineno, frameptr)
355
356 This probe point indicates that execution of a Python function has begun.
357 It is only triggered for pure-python (bytecode) functions.
358
359.. c:function:: python.function.return(str filename, str funcname, int lineno, frameptr)
360
361 This probe point is the converse of `python.function.return`, and indicates
362 that execution of a Python function has ended (either via ``return``, or
363 via an exception). It is only triggered for pure-python (bytecode) functions.
364
365
366Examples
367--------
368This SystemTap script uses the tapset above to more cleanly implement the
369example given above of tracing the Python function-call hierarchy, without
370needing to directly name the static markers:
371
372.. code-block:: c
373
374 probe python.function.entry
375 {
376 printf("%s => %s in %s:%d\n",
377 thread_indent(1), funcname, filename, lineno);
378 }
379
380 probe python.function.return
381 {
382 printf("%s <= %s in %s:%d\n",
383 thread_indent(-1), funcname, filename, lineno);
384 }
385
386
387The following script uses the tapset above to provide a top-like view of all
388running CPython code, showing the top 20 most frequently-entered bytecode
389frames, each second, across the whole system:
390
391.. code-block:: c
392
393 global fn_calls;
394
395 probe python.function.entry
396 {
397 fn_calls[pid(), filename, funcname, lineno] += 1;
398 }
399
400 probe timer.ms(1000) {
401 printf("\033[2J\033[1;1H") /* clear screen \*/
402 printf("%6s %80s %6s %30s %6s\n",
403 "PID", "FILENAME", "LINE", "FUNCTION", "CALLS")
404 foreach ([pid, filename, funcname, lineno] in fn_calls- limit 20) {
405 printf("%6d %80s %6d %30s %6d\n",
406 pid, filename, lineno, funcname,
407 fn_calls[pid, filename, funcname, lineno]);
408 }
409 delete fn_calls;
410 }
411