Skip to main content

Lib/profile.py

cpython 3.14 @ ab2d84fe1023/Lib/profile.py

Two profiler implementations ship together: the pure-Python Profile in Lib/profile.py and the C-backed cProfile.Profile in Lib/cProfile.py, which wraps Modules/_lsprof.c. Statistics are consumed by pstats.Stats in Lib/pstats.py.

profile.Profile instruments every Python call and return through sys.setprofile. The callback receives one of six event strings, call, return, c_call, c_return, c_exception, and exception. For each event the profiler maintains a self.timings dict keyed by (filename, lineno, funcname) and accumulates local time (total minus sub-calls) by snapshot-differencing self.timer().

cProfile.Profile replaces that pure-Python accounting with the _lsprof C extension, which hooks the same sys.setprofile event stream but records data in C structures. Its output is identical but the overhead is roughly 10x lower.

pstats.Stats consumes the internal timing dict or _lsprof data, formats columns, and implements sort keys such as cumulative, tottime, ncalls, and filename.

Map

LinesSymbolRolegopy
1-80Module header, Profile.__init__, Profile.set_cmdConstruct the profiler; initialize timings, cur, cmd, and install the timer callable.(stdlib pending)
81-200Profile.calibrate, Profile.biasMeasure per-call overhead by running a null loop and computing average timer resolution; stores result in Profile.bias.(stdlib pending)
201-400Profile.enable, Profile.disable, Profile.runcall, Profile.runctx, run, runctxInstall/remove the sys.setprofile hook; module-level convenience wrappers.(stdlib pending)
400-600Profile.dispatch, Profile.trace_dispatch_call, Profile.trace_dispatch_return, Profile.trace_dispatch_c_call, Profile.trace_dispatch_c_returnPer-event handlers that update self.timings entries; the dispatch dict maps event string to method.(stdlib pending)
1-80cProfile.Profile.__init__, enable, disable, getstatsThin wrapper around _lsprof.Profiler; getstats() returns a list of _lsprof.profiler_entry named tuples.(stdlib pending)
80-150cProfile.run, cProfile.runctx, cProfile.runcallModule-level wrappers mirroring profile; accept sort parameter and pass to pstats.Stats.print_stats.(stdlib pending)
1-150Stats.__init__, Stats.load_stats, Stats.addParse raw profiler output (dict or file); merge multiple runs with add; normalize into (cc, nc, tt, ct, callers) tuples.(stdlib pending)
150-300Stats.print_stats, Stats.print_callers, Stats.print_calleesFormat and print the statistics table; apply the active fcn_list sort order.(stdlib pending)
300-500Stats.sort_stats, Stats.strip_dirs, Stats.reverse_order, add_callers, count_callsSort by one or more keys; strip directory components from filenames; helper utilities for callers dict.(stdlib pending)

Reading

sys.setprofile event types and the dispatch table (lines 400 to 600)

cpython 3.14 @ ab2d84fe1023/Lib/profile.py#L400-600

class Profile:
def trace_dispatch(self, frame, event, arg):
timer = self.timer
t = timer()
t = t[0] + t[1] # sum user and sys time from os.times()
...
self.dispatch[event](self, frame, t, arg)
...

dispatch = {
"call": trace_dispatch_call,
"exception": trace_dispatch_exception,
"return": trace_dispatch_return,
"c_call": trace_dispatch_c_call,
"c_exception": trace_dispatch_c_exception,
"c_return": trace_dispatch_c_return,
}

sys.setprofile delivers six event strings. Unlike sys.settrace, the profile callback receives no per-line events, only call/return pairs. The dispatch dict avoids an if/elif chain and lets subclasses replace individual event handlers without overriding trace_dispatch.

trace_dispatch_call records a new entry in self.timings when a frame starts. trace_dispatch_return computes the elapsed time for the current frame and credits its caller's "total time excluding sub-calls" (tt). The cumulative time (ct) accumulates upward through the call chain in self.cur.

The c_call/c_return/c_exception trio handles calls into C extension functions. The arg for these events is the C function object itself, so the profiler synthesizes a fake frame key using arg.__name__ and a sentinel filename "~".

Profile.calibrate (lines 81 to 200)

cpython 3.14 @ ab2d84fe1023/Lib/profile.py#L81-200

def calibrate(self, m, verbose=0):
if self.__class__ is not Profile:
raise TypeError("Subclasses of Profile must override .calibrate().")

saved_bias = self.bias
self.bias = 0
try:
return self._calibrate_inner(m, verbose)
finally:
self.bias = saved_bias

def _calibrate_inner(self, m, verbose):
get_time = self.get_time

# Run a null function m times with profiling on
...
p = Profile()
p.bias = 0
p.run("for i in range(%d): pass" % m)
...
total_calls = ...
reported_time = ...
# bias = overhead per call
return reported_time / total_calls

Calibration answers the question: how much time does a single profiler event add to the reported times? The procedure runs a tight empty loop under the profiler, divides the reported time by the call count, and returns that per-call overhead as the bias. Passing the result to Profile.bias before a real run subtracts it from every timing sample. The default timer is os.times() on POSIX, which measures user plus system time. Calibration is necessary because the timer call itself is visible in the results.

pstats.Stats sort keys and print_stats (lines 150 to 300)

cpython 3.14 @ ab2d84fe1023/Lib/pstats.py#L150-300

sort_arg_dict_default = {
"calls": (((1,-1), ), "call count"),
"ncalls": (((1,-1), ), "call count"),
"cumtime": (((3,-1), ), "cumulative time"),
"cumulative":(((3,-1), ), "cumulative time"),
"filename": (((4, 1), ), "file name"),
"line": (((5, 1), ), "line number"),
"module": (((4, 1), ), "file name"),
"name": (((6, 1), ), "function name"),
"nfl": (((6, 1),(4, 1),(5, 1)), "name/file/line"),
"pcalls": (((0,-1), ), "primitive call count"),
"stdname": (((7, 1), ), "standard name"),
"time": (((2,-1), ), "internal time"),
"tottime": (((2,-1), ), "internal time"),
}

Each sort key maps to a tuple of (column_index, direction) pairs used in sort_stats. The columns in the internal stats tuple are (primitive_calls, total_calls, total_time, cumulative_time, callers). print_stats formats three numeric columns and the function identifier, then optionally filters rows to those whose (filename, lineno, name) matches a list of regexes passed as positional arguments.

gopy mirror

profile.py, cProfile.py, and pstats.py are pure Python and are candidates for bundling verbatim under stdlib/. The C backend _lsprof is non-trivial to port; until it is available, cProfile can delegate to the pure-Python Profile as a compatibility shim. sys.setprofile support is required, which in turn requires the eval loop to fire profile events at call and return boundaries.