November 15, 2022·4 min read·engineeringpythonprofilinglinux

Python 3.12's second alpha gives perf names for Python functions

Python 3.12.0a2 previews Linux perf integration. Small generated trampolines let native stack samples identify Python functions and their source files.

A native stack trace can show the interpreter executing bytecode without identifying the Python function whose bytecode it is executing. That limits the usefulness of system profiling for a Python application. Python 3.12's second alpha previews a bridge between those views by giving Linux perf recognizable native locations associated with Python functions.

The November 15 release announcement lists Python function names in perf traces among the changes available for testing. This is an early developer preview, and its use in production is discouraged. The value at this stage is a concrete implementation that tooling maintainers can inspect and exercise.

I choose Python largely for an ecosystem that includes native extensions. Profiling across that boundary is consequently more useful to me than treating every sample in the interpreter as one undifferentiated cost. A function name and source file can connect system-level evidence to code an application maintainer can change.

Why native symbols lose the Python name

The profiling guide at the alpha tag describes the initial limitation. Native symbols identify functions compiled into native code. Several Python functions can execute through the same interpreter evaluation function, so seeing that C function does not distinguish the Python calls above it.

The guide illustrates the problem with nested Python functions. Without integration, the report contains interpreter evaluation and calling machinery. With integration, entries for the Python functions and their filenames appear within the same stack. The improvement is the extra association between execution addresses and Python code.

That association does not require compiling each Python function's body to native machine code. The interpreter generates a small piece of code and interposes it before executing the Python function. The generated trampoline gives the stack an address that can represent that particular Python function.

The interpreter then describes the relationship through perf map files. Those files let the profiler associate an address range with a symbolic name. The native sampling machinery can therefore report a Python function even though its body still executes through the interpreter.

The filenames matter when different modules use the same function name. An execution report can retain the source context needed to distinguish those callers. That is additional evidence for interpreting a sample, without attributing the native evaluator's entire cost to one guessed Python function.

Enabling the additional stack information

The alpha supports startup configuration through PYTHONPERFSUPPORT and the -X perf command-line option. It also supplies runtime functions that activate and deactivate stack trampolines. The what's new document pinned to the release lists those APIs as part of the profiling support.

import sys

sys.activate_stack_trampoline("perf")
try:
    total = sum(range(100))
finally:
    sys.deactivate_stack_trampoline()

This small example shows the dynamic boundary without pretending that the calculation is a useful profiling workload. The activation surrounds the work whose Python call relationships should become visible. The cleanup keeps subsequent execution outside that explicitly activated interval.

The guide gives the runtime APIs precedence over the command-line option, and the command-line option precedence over the environment variable. That ordering matters for a tool combining launcher configuration with code that controls its own profiling interval. Startup activation and runtime activation are different ways to choose when the extra machinery participates.

Support is currently limited to Linux on selected architectures. The guide directs users to the configuration output or the HAVE_PERF_TRAMPOLINE build setting to determine availability. An interpreter version alone does not establish that this mode exists in a particular build.

Unwinding still needs build support

Knowing the name associated with an address does not itself supply a complete call chain. The profiler must also unwind the stack. The alpha guide recommends compiling Python with frame pointers, including leaf-frame pointers, for the best results.

The reason is specific to the generated code. The trampolines do not have DWARF debugging information available. Frame-pointer-based unwinding provides another way to follow the stack through them. A build that omits the required information can fail to show the expected Python functions even when activation succeeds.

This separates two kinds of metadata. Map files identify generated code ranges. Unwinding information explains how to recover callers. A complete report needs both a usable stack and names for the addresses recovered from it.

I find that separation useful when evaluating profiling tools. A missing function can result from absent naming information or from failure to recover the relevant frame. This preview documents both parts of its mechanism, so the integration can be assessed beyond the appearance of a successful sample report.

The guide's example retains native interpreter functions alongside the Python entries. That is the useful outcome for mixed applications. A sample can identify a Python caller and continue into native execution without discarding either part of the stack. The generated trampoline and its map entry add the Python identity that the native evaluator's symbol cannot provide on its own.