k4bench.analysis.heap¶
k4bench.analysis.heap ¶
The heap each component of a k4run job held: {label}_heap.json.
Written by the k4BenchAuditor in the k4run benchmark's memory pass, a run of
the job of its own after the timed one, whose allocation counter records which
component allocated every heap block. A block is charged to the component
whose audited call allocated it, at any depth (a service initialized from
within an algorithm's call is a call of its own), and stays charged to it until
freed, wherever and whenever that happens. What no component allocated — the
Python configuration, ROOT's start-up, the framework between calls — belongs to
:data:NO_COMPONENT.
For every component, in bytes of heap (malloc's 8-byte header and rounding to
16 bytes included, as _components.json's allocation metrics count them):
- held before the first event, which is what initialize and start left, and the most it held at once until then;
- held at the start and the end of each event, and the most it held at once
during the event (from its first top-level call to its last). The most it
held beyond what it held at the start is its working set, the memory the
event needed of it (:meth:
ComponentHeap.working_set); - held at the auditor's finalize: what survived the job's own finalize.
Beside them, at the same moments, the process's heap as malloc counts it
(mallinfo2: the chunks in use, those it mapped on their own included, and
the free ones it caches per thread) and its anonymous RSS. The components and
:data:NO_COMPONENT add up to nearly malloc's count (to 0.06% in CLD
reconstruction); the rest is the blocks allocated while the process was still
loading, before the counter started, and the auditor's own memory. The
anonymous RSS counts something else: it also holds what libraries map
themselves, the free heap malloc has not returned and the counter's own table
(table_mapped_bytes), but not heap that was allocated and never touched,
which takes no pages. In CLD reconstruction that is about a sixth of the heap,
so held bytes are memory allocated, not memory resident.
What the numbers do not say:
- who uses memory: a block is charged to the component that created it, so the event data the input reader creates stays the reader's until the output writer frees it, and the memory a library takes as it loads (its static constructors, ROOT dictionaries) is that of the component whose call loaded it;
- what lies outside malloc's heap: memory a library maps itself (pool
allocators, CPython's arenas, large buffers) and file-backed memory (code,
CVMFS), which
_events.json's file-backed RSS shows; - the timed run's values: the pass is another, shorter run of the same job. It reads the same input, so its events are the timed run's first ones, but recording owners slows every allocation, so its numbers are comparable in kind, not identical in value;
- what a service first used during an event keeps is held from that event on, not before the first: the service is initialized then.
In a job running calls concurrently (threads > 1 in _components.json),
attribution still follows each thread's running component, but a snapshot
taken while other threads run catches them mid-call. A job that had several
events in flight at once has no per-event peaks: the owners' peaks are the
process's.
A missing value is NaN: before the first event of a job that ran none, the
per-event peaks of a job that ran events at once, and an RSS that could not be
read.
HeapTotals
dataclass
¶
HeapTotals(before_first_mb: float, per_event_mb: Series, typical_mb: float, largest_mb: float, largest_event: int | None, growth_mb_per_event: float, after_finalize_mb: float)
The heap of the whole job, every component's and no component's
together (:meth:ComponentHeap.totals), in MB.
ComponentHeap
dataclass
¶
ComponentHeap(execute: dict[str, DataFrame], before_first_event: DataFrame, at_end: DataFrame, process: DataFrame, process_before_first_event: Series, process_at_end: Series, table_peak_entries: int, table_mapped_bytes: int)
One label's _heap.json, as frames.
Components are :data:NO_COMPONENT first, then the auditor's components
in its order, by name, as in the timed run's
:class:~k4bench.analysis.components.ComponentTiming, which they join on.
execute maps each of :data:EVENT_HEAP_METRICS to an events ×
components frame indexed by event number: what each component held as the
event started and at its end, and the most it held during it.
before_first_event is components ×
:data:HEAP_METRICS, at_end components × held_bytes. process
is events × :data:PROCESS_METRICS, and process_before_first_event
and process_at_end the same at those two moments. table_peak_entries
is the most blocks the counter's table held at once, table_mapped_bytes
the memory it mapped for them.
from_json
classmethod
¶
from_json(raw: Mapping, *, source: str | Path | None = None) -> ComponentHeap
Build from a parsed _heap.json, refusing an inconsistent one.
The schema version is checked by the caller
(:func:~k4bench.plugin.schema.validate_heap_schema).
Source code in k4bench/analysis/heap.py
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 | |
working_set ¶
Events × components: the most each component held during each event beyond what it held as the event started, the heap the event needed of it. Its persistent state is left out, and so is the data others created that it read.
Source code in k4bench/analysis/heap.py
holders ¶
The components that held heap at some recorded moment: before the first event, at any event's start, peak or end (the warm-up event's included) or after finalize, in the file's order.
Source code in k4bench/analysis/heap.py
steps ¶
Every rise of :data:STEP_MIN_MB or more in what a component held
from one event's end to the next's, with columns component,
event_number (the event it rose in), step_mb and kept
(whether at every event's end from it to the last the component still
held at least :data:STEP_KEPT_SHARE of it beyond what it held before
it), components in the file's order and each one's events in order.
The first event has no event before it, and the warm_up event's rise
is not one, but its end is what the next event's rise is measured
from.
Source code in k4bench/analysis/heap.py
summary ¶
Each component's heap over the job in MB, indexed by component in
the file's order, with the :data:SUMMARY_COLUMNS:
before_first_mb: what it held before the first event;typical_mb,largest_mb: the median and the largest of what it held at the events' ends;working_set_median_mb,working_set_p95_mb,working_set_max_mb: of its :meth:working_set, andworking_set_max_eventthe event of its largest (NaNwithout one);growth_mb_per_event: the Theil–Sen slope of what it held at the events' ends against their numbers,NaNwith too few events;after_finalize_mb: what it held after the job's own finalize;retained_mb: how much more than at the first event's end that is,NaNwithout events;leak: whether it is suspected of leaking: it grew by :data:LEAK_MIN_MBor more over the events, in both halves of them (each at :data:LEAK_STEADY_SHAREof the whole's rate or more), and after finalize still holds :data:LEAK_MIN_MBor more beyond what it held at the first event, and at least :data:LEAK_KEPT_SHAREof what it gained from the first event to the last. With fewer than :data:LEAK_MIN_EVENTSevents none is;stepped_mb: what its kept :meth:stepsadded up to,0without any;stepped: whether it kept any of its steps.
The warm_up event is left out of every statistic over events.
Source code in k4bench/analysis/heap.py
totals ¶
totals(*, warm_up: int | None = 0) -> HeapTotals
The whole job's heap (:class:HeapTotals): what every component and
no component held together, the warm_up event left out of the
statistics over events.