Skip to content
Projects

Profiling regions

Download notebookView on GitHubRun when the site was built.

Install for this page: pip install scope-profiler (base install only).

A region is the fundamental unit of measurement in scope-profiler. Every time a region is entered and exited, the profiler records a start/end timestamp pair and increments the call counter.

Regions only record while profiling is configured, so the cells on this page share one profiling session, started here and written at the end. The output goes to the scratch directory.

run = ProfileManager.setup(file_path="regions.h5")

Ideal for wrapping entire functions:

@ProfileManager.profile("solver")
def solve(A, b):
return np.linalg.solve(A, b)
A, b = np.eye(3), np.ones(3)
print(solve(A, b))
print(ProfileManager.get_region("solver").num_calls)
[1. 1. 1.]
1

Without an explicit name the function name is used:

@ProfileManager.profile
def solve(A, b):
return np.linalg.solve(A, b)
# region name: "solve"
print(solve(A, b))
print("solve" in ProfileManager.recorded_regions())
[1. 1. 1.]
True

You can recursively profile nested Python function calls from a decorated entrypoint:

def run_pipeline(A, b):
return sum(b)
@ProfileManager.profile("solve", recursive=True)
def solve(A, b):
return run_pipeline(A, b)
print(solve(A, b))
3.0

Nested calls are stored as additional regions using fully-qualified names such as my_module.run_pipeline.

[name for name in ProfileManager.recorded_regions() if "run_pipeline" in name]
['__main__.run_pipeline']

Context manager — ProfileManager.region()

Section titled “Context manager — ProfileManager.region()”

Ideal for profiling a section of code inside a function:

def compute_rhs(state):
return 2.0 * state
def time_step(state, dt):
with ProfileManager.region("rhs_evaluation"):
rhs = compute_rhs(state)
with ProfileManager.region("state_update"):
state += dt * rhs
time_step(np.ones(3), 0.1)

profile_region() is the original name for this method and still works — the two are the same object, so existing instrumentation needs no change.

# Bound methods are created on access, so compare the underlying functions.
ProfileManager.profile_region.__func__ is ProfileManager.region.__func__
True

Decorators and context managers can be mixed freely. They share the same region registry, so using the same name in both places accumulates into a single region:

def process(chunk):
time.sleep(0.001)
@ProfileManager.profile("compute")
def compute_batch(data):
for chunk in data:
with ProfileManager.region("compute"):
process(chunk)
# Both the decorator and the context manager contribute
# to the same "compute" region.
compute_batch([1, 2, 3])
print(ProfileManager.get_region("compute").num_calls)
4

One call of the decorated function plus three entries of the context manager make four calls of the single compute region.

Regions are identified by name (a string). The first call to region("foo") creates the region; subsequent calls with the same name return the existing instance. This means:

  • The same region can be entered from multiple call sites.
  • Call counts and timestamps accumulate across all sites.
  • Regions are scoped to the current ProfileManager.setup() session. Calling setup() again clears all regions.
ProfileManager.region("foo") is ProfileManager.region("foo")
True

Regions can be nested arbitrarily:

def work():
time.sleep(0.001)
with ProfileManager.region("outer"):
with ProfileManager.region("inner"):
work()

Each region independently records its own timestamps. The library does not compute parent-child relationships — that can be done in post-processing by comparing the recorded intervals.

Before calling finalize(), you can inspect in-memory data:

region = ProfileManager.get_region("solver")
# Number of completed calls
print(region.num_calls)
# Durations of buffered (not yet flushed) calls, in nanoseconds
durations = region.get_durations_numpy()
print(durations.shape, durations.dtype)
1
(1,) int64

All registered regions:

for name, region in ProfileManager.get_all_regions().items():
print(f"{name}: {region.num_calls} calls")
solver: 1 calls
solve: 2 calls
__main__.run_pipeline: 1 calls
rhs_evaluation: 1 calls
state_update: 1 calls
compute: 4 calls
foo: 0 calls
outer: 1 calls
inner: 1 calls

Finalizing writes the file. A following setup() starts a new session, which clears all regions:

ProfileManager.finalize(verbose=False)
ProfileManager.setup(file_path="regions.h5")
print(ProfileManager.recorded_regions())
ProfileManager.finalize(verbose=False)
()