XXNU Source Handbook Offline reading instructions
Part 03 · Chapter 03.21

Recount tracks, execution snapshots, and resource attribution

Runtime · Time and accounting · Clocks · Ledgers · Bank charging

Recount turns differences between execution-counter snapshots into thread, task, processor, work-interval and coalition statistics. Its central operation is addition of a measured delta to several owners. The owners have different topologies and lifetimes; a consistent read of one track does not freeze the whole machine. This chapter traces ARM64/common implementation and selected reporting/teardown paths in xnu-12377.121.6.

Inspect the live recount_usage, recount_metrics, recount_thread, recount_task layouts for the selected source profile.

1. Objects and where their counters live

Inspect the Recount track layout in the offline HTML handbook to compare sizes and offsets with different cycle/instruction and energy flags.

A recount_track contains rt_sync and rt_usage. Each usage holds metrics indexed by execution level: Mach ticks, plus instructions/cycles when configured. Energy is a separate ru_energy_nj field because its producer samples out of band. A recount_plan describes the array topology; the plan is needed whenever indexing, reading, rolling up or destroying an array. Definitions.

Owner and field Plan/topology Lifetime and meaning
Thread th_recount.rth_lifetime CPU kind Accumulates this thread's execution, separated by performance/efficiency-kind slots and execution level.
Task tk_recount.rtk_lifetime CPU Receives non-idle thread deltas directly during execution. Includes execution by threads that later terminate.
Task rtk_terminated CPU kind, plain usage array Separate retained sum of terminated threads for interfaces that need that subset. It is not added to task lifetime totals.
Processor pr_recount.rpr_active System topology, one embedded track per processor Counts active non-idle work. rpr_snap stores that CPU's preceding snapshot; separate idle/interrupt fields support other views.
Work interval wi_recount.rwi_current_instance CPU Receives execution while the thread is associated with an interval that has telemetry enabled.
Resource coalition rco_exited CPU kind, plain usage array Retains removed tasks' lifetime totals. Reporting combines it with current members.

The concrete plans are in recount.c; owner layouts and lock contracts are in recount.h. “System topology” means a single slot in that owner's storage, not one global counter shared by all processors.

Per-CPU updates require preemption disabled so a writer does not change CPU/slot halfway through an update. CPU-kind and single-slot topologies require their owner to exclude simultaneous writers. The sequence counter supports readers; it does not replace the single-writer contract. Topology contracts.

2. A context switch attributes a delta to the outgoing thread

The scheduler takes a recount_snap around switching, then passes the outgoing thread/task to recount_switch_thread(). ARM snapshot helpers read the timebase and optional performance counters, with an ordered snapshot adding the required instruction barrier. recount_snap_diff() subtracts the processor's previous snapshot and asserts that the counters did not go backward. Snapshot helpers, delta calculation, scheduler calls, switch accounting.

osfmk/kern/recount.c, lines 1083–1103:

recount_switch_thread(struct recount_snap *cur, struct thread *off_thread,
    struct task *off_task)
{
	if (__improbable(!_recount_started)) {
		return;
	}

	processor_t processor = current_processor();

	struct recount_snap *last = recount_get_snap(processor);
	struct recount_snap diff = { 0 };
	recount_snap_diff(&diff, cur, last);
	recount_absorb_snap(&diff, off_thread, off_task, processor,
#if RECOUNT_THREAD_BASED_LEVEL
	    off_thread->th_recount.rth_current_level
#else // RECOUNT_THREAD_BASED_LEVEL
	    RCT_LVL_KERNEL
#endif // !RECOUNT_THREAD_BASED_LEVEL
	    );
	memcpy(last, cur, sizeof(*last));
}

recount_switch_thread() selects the outgoing thread's execution level and passes it to recount_absorb_snap(). Absorption updates the thread's track and normally its task, processor and telemetry-enabled work interval too. It starts all selected sequence updates, publishes that phase, adds the delta to each, publishes the data and completes the sequence updates. Back in recount_switch_thread(), the final memcpy advances the CPU's baseline to the new snapshot. Absorption and baseline update.

Recount tracks, execution snapshots, and resource attribution diagram
Recount tracks, execution snapshots, and resource attribution diagram · Open SVG
Mermaid source
flowchart TD
    Old["Processor rpr_snap: previous snapshot"] --> Diff["Current counters minus previous counters"]
    New["Scheduler or level-boundary snapshot"] --> Diff
    Diff --> Th["Outgoing thread lifetime slot"]
    Diff --> Idle{"Idle thread?"}
    Idle -->|No| Tk["Task lifetime CPU slot"]
    Idle -->|No| CPU["Processor active track"]
    Idle -->|No| WI{"Current interval has telemetry?"}
    WI -->|Yes| WT["Interval CPU slot"]
    Idle -->|Yes| Own["Only thread gets this execution delta"]
    New --> Base["Replace processor snapshot baseline"]

Idle execution has an intentional exception: the idle thread receives its own usage, but its task, processor-active track and interval do not receive that execution as ordinary active work. Processor idle duration is tracked separately. Treating all processor elapsed time as task execution would erase this distinction. Idle branch, processor idle/usage accounting.

3. User/kernel transitions and metrics with different producers

ARM64 tracks a thread's current execution level. Transition helpers first account the elapsed interval under the old level, record the new level, and replace the processor baseline. Local assembly calls Recount when leaving/entering userspace; secure-level tracking exists under CONFIG_SPTM. The exact counter precision also depends on PRECISE_USER_KERNEL_TIME and the PMC configuration. Transition implementation, ARM64 user exit, ARM64 user return, level/configuration definitions.

Interrupt enter/leave hooks maintain duration counters as well. Interrupt duration is an additional view of execution already occurring in kernel context, not an independent third category to add on top of user and kernel totals. Interrupt duration, ARM64 interrupt hooks.

recount_add_energy() takes an energy delta supplied by its caller and adds it to the corresponding owners. Its selector recount_update_single_start() does not begin the rt_sync sequence transaction used by the time/CPI path. Thus energy is not guaranteed to be the same measurement instant as the time/instruction/cycle tuple simply because they share a structure. Energy support requires both the relevant build option and CPU counter support; the PMGR/CLPC producer's complete measurement implementation is outside this code path. Energy update, single-field selector, configuration, field contract.

When precise user/kernel time is disabled, _fix_time_precision() moves system time into the reported user field and zeros the kernel field. A zero kernel result from such a build is not evidence that no kernel execution occurred. Output normalization.

4. What a consistent read guarantees

A writer increments rt_sync to an odd generation, orders the update phase, changes the fields, orders their publication, and increments to an even generation. A reader obtains a generation, waits/retries around an in-progress update, copies the usage, then validates the generation. It can retry without taking a lock that blocks the scheduler writer. Sequence primitives.

osfmk/kern/recount.c, lines 336–346:

static void
recount_read_track(struct recount_usage *stats,
    const struct recount_track *track)
{
	uintptr_t on_enter = 0;
	do {
		const struct recount_usage *vol_stats =
		    recount_read_start(track, &on_enter);
		*stats = *vol_stats;
	} while (!recount_try_read_end(track, on_enter));
}

recount_rollup() repeats this operation across the source topology and maps each source slot into the destination topology. Each read can be consistent while the aggregate spans several moments. No single generation protects the entire thread/task/processor/coalition graph. Topology rollup, topology conversion.

Ordinary thread/task usage helpers sum stored tracks. The current-thread helper has a special contract: with interrupts disabled it sums the current thread's stable tracks, takes a fresh counter snapshot, and adds the still-uncommitted delta to the local result as kernel execution. It does not advance the persistent processor baseline; later accounting must still absorb that interval. Current-thread read, ordinary thread helper, task helper.

For example, if a stored thread total is 100 ticks and the CPU baseline is 8 ticks behind now, the current-thread result can be 108 while stored tracks still contain 100. Adding 108 back to those tracks would double count; this helper is a reporting operation.

5. Thread exit, task exit, and coalition totals

Thread reaping rolls rth_lifetime into the task's rtk_terminated before removing the thread from task membership, under the task lock. This preserves the terminated-thread subset for reporting after the thread storage disappears. The task's rtk_lifetime already received the same execution as it happened. Termination queue operation, rollup helpers.

A resource coalition retains removed tasks in a different manner. Its removal callback holds the coalition lock, updates exit/resource counters, rolls the departing task's lifetime usage into rco_exited, and removes it from the live queue. Exec-copy/did-exec cases are explicitly excluded from some accounting to avoid counting the transition copies as independent resource owners. Resource removal.

Recount tracks, execution snapshots, and resource attribution diagram
Recount tracks, execution snapshots, and resource attribution diagram · Open SVG
Mermaid source
flowchart LR
    Exec["Thread executes"] --> Life["Task rtk_lifetime receives deltas immediately"]
    Exec --> Thread["Thread lifetime tracks"]
    Thread -->|Thread reap under task lock| Term["Task rtk_terminated subset"]
    Life -->|Task leaves resource coalition| Exited["Coalition rco_exited"]
    Live["Current member tasks"] -->|Read each task lifetime| Report["Coalition resource report"]
    Exited -->|Add retained exited usage| Report
    Term --> Subset["Terminated-thread reporting only"]

The diagram deliberately has no arrow adding rtk_terminated to rtk_lifetime. For coalition reporting, coalition_resource_usage_internal() creates its temporary ledger before taking the coalition lock, copies retained/exited values, and walks live tasks under that lock. Membership cannot disappear during that walk, but running tasks can continue updating their per-CPU counters. The result is a protected membership traversal, not a simultaneous global snapshot. Some values, such as current footprint, intentionally use live tasks rather than retaining exited balances. Report construction.

Membership has its own lifetime rules. Adoption rejects duplicate, terminated or reaped states under the coalition lock, publishes the task pointer, and acquires activity/storage references. Multi-coalition adoption rolls back earlier successful adoptions if a later one fails. Removal first clears task membership pointers under the task lock, then invokes each coalition removal path; later reference release is separate from logical removal. Single adoption, rollback/removal. This is why coalition active count, member count and object reference count cannot be used interchangeably.

6. Work-interval ownership and telemetry

The work-interval policy companion follows the separate configuration, scheduling-class, group-selection and auto-join mechanisms. This section concentrates on retained interval identity and accounting storage.

struct work_interval contains its ID, creation flags, creator identity, reference count, optional thread group, policy data, and Recount storage. Telemetry-enabled creation initializes its track array; the getter returns no tracks for absent or telemetry-disabled intervals. recount_absorb_snap() consults the thread's current interval when attributing a delta. Joining does not copy the thread's entire past lifetime into the interval. Structure and telemetry selector, creation.

thread_set_work_interval() consumes one interval reference on success. Explicit operations require the current thread; auto-join operations have a different thread-lock contract. The operation checks policy/RT admission, changes the thread pointer and relevant group/policy state, then releases the old interval. A remotely running auto-joined thread can take the deferred-unjoin branch, which marks the state for a later safe transition rather than immediately detaching it. Contract and checks, pointer transfer and old release.

Port-based join converts the port to an interval reference and releases it if changing the thread fails; successful adoption transfers it to the thread. Thread termination leaves its interval. The final release frees telemetry storage before object storage. Under CONFIG_SCHED_AUTO_JOIN, a release with THREAD_WI_THREAD_LOCK_HELD defers destruction to an MPSC daemon so allocation locks are not acquired in that context. The branch is controlled by the release options, not merely by the interval's name or general description. Join failure cleanup, thread termination, release/deferred free.

7. Allocation, configuration, and validation boundaries

Track and plain-usage arrays are allocated according to plan topology and zero-initialized. Thread/task/coalition/work-interval initialization and destruction use matching plans. A task owns both lifetime and terminated storage, while a coalition owns only its retained exited usage here. Destroying an owner without respecting the relevant membership and writer quiescence would invalidate readers; Recount allocation alone does not supply those object-lifetime references. Allocation and owner init/deinit.

Option or local test Consequence or inspected contract
CONFIG_PERVASIVE_CPI, precise timing Determines instruction/cycle storage and transition sampling. Snapshot/transition code, precision path.
CONFIG_SPTM Adds the secure execution level; it does not make all secure-service implementation visible here. Configuration.
CONFIG_PERVASIVE_ENERGY and CPU support Controls the separately supplied energy metric. Selection.
Thread-exit regression getrusage_thread_terminate_increasing checks increasing observed usage across thread termination. Its helper checks user time; do not turn its comment into an independent proof of every field. Test.
Coalition reporting tests Exercise coalition resource counters and CPU-kind-related reporting. Tests.
Work-interval telemetry tests Cover telemetry enabled/disabled and multithread cases. Creation flags, test cases.

The local tests were read, not run. This chapter covers selected measurement and ownership paths; it does not claim a complete workload-policy audit, the external energy model, globally atomic reports, or empirical accounting accuracy. Continue with ledgers for balances/limits and bank accounts for charging work performed for another task. The observability guide maps the reporting facilities built on these primitives.

Search the handbook

Search every chapter, interface and code excerpt.

    Diagram

    Source preview

    Open full file