Skip to content

Native CPU-PMU backends for sparkles: enhancement proposal

A milestoned plan for a thin per-OS acquisition core under the sparkles benchmarking harness, sketched in D against the existing metrics.d seamMetricDescriptor/MetricCell/ MetricClass plus the CounterGroups open/close/count contract. Each backend advertises capabilities aligned with the survey's seven concerns; the harness reports "capability unavailable" instead of silently degraded results. Every milestone cross-links the prior art it borrows from.

Last reviewed: July 11, 2026

FieldValue
Targetsparkles:test-runner --bench + wired runtime bench
Composition seamMetricDescriptor/MetricCell/MetricClass + CounterGroups
Baseline auditedsparkles-baseline.md (gap analysis §Gap analysis)
Evidence basecomparison.md capability matrix + the per-subject deep-dives
Shape6 milestones: seam → Linux depth → macOS floor → Windows floor → precise memory → sampling

NOTE

Sequencing and delivery tracking live in the test-runner delivery plan, which interleaves these six milestones (as B1–B6) with the workload-harness track and carries the research-derived amendments per milestone. This page remains the design rationale.


1. Abstract & problem statement

The baseline is a Linux-only counting layer with a closed set of seven generic events and honest-but-empty stubs elsewhere. The survey found that (a) Linux exposes far more than the layer uses — per-microarchitecture event vocabularies, multiplex-scaled estimates, user-space counter reads, precise memory sampling, and a complete decode stack; (b) macOS and Windows have real, smaller floors — an unprivileged per-process instructions/cycles read on macOS (proc_pid_rusage, [hw-verified: aarch64-darwin]) and a driver-free CycleTime plus admin-gated ETW counting on Windows (windows.md); and (c) no existing library spans these — libpfm4 has no RISC-V and no non-Linux OS layer, and nothing portable names events across OSes.

The conclusion the proposal operationalizes: the harness must own a small capability-typed backend interface, implement it per OS at whatever depth that OS permits, and make every absence explicit. Linux advertises the full set across ISAs; Windows/macOS advertise subsets; the reporting layer renders what is advertised and names what is not.

Non-goals: a general profiler UI; kernel-driver work on any OS; wrapping closed vendor drivers (VTune SEP / uProf).


2. Milestone 1: the capability-typed backend seam

Goal: make "what can this host measure?" a first-class, reportable value, and turn CounterGroups's implicit contract into an interface new backends implement.

2.1 The capability model

One flag per survey concern (plus sub-capabilities where the survey found real-world splits), advertised per backend instance after its open handshake — capability is a runtime probe result, never a compile-time assumption (privilege gating varies per box: paranoid, tracefs modes, SIP, allowlists).

d
/// What an opened backend can actually deliver on this host, this run.
enum Capability : uint
{
    none            = 0,
    counting        = 1 << 0,  /// concern 1: scalar counting
    countingRaw     = 1 << 1,  ///   + raw/µarch-specific event selectors
    countingScaled  = 1 << 2,  ///   + multiplexed estimates (labeled)
    selfMonitoring  = 1 << 3,  ///   + user-space reads (rdpmc/PMUSERENR)
    ipSampling      = 1 << 4,  /// concern 2: overflow/IP sampling
    preciseMemory   = 1 << 5,  /// concern 3: data-source/address sampling
    symbolization   = 1 << 6,  /// concern 4: address → symbol/line
    eventTracing    = 1 << 7,  /// concern 5: OS-event gating (tracepoints/ETW)
    numaAttribution = 1 << 8,  /// concern 6: page → node classification
    eventNaming     = 1 << 9,  /// concern 7: name → encoding tables
}

struct CapabilityReport
{
    Capability available;
    /// Why each absent capability is absent — rendered by --list-metrics
    /// and the bench header, e.g. "preciseMemory: no IBS/PEBS PMU (cpu
    /// max_precise=0)" or "eventTracing: tracefs ids unreadable (0700)".
    string[Capability] unavailableBecause;
}

2.2 The backend interface

Design-by-Introspection like the rest of sparkles: a backend is any type with the required primitives; optional primitives unlock optional capabilities (same pattern as the DbI guidelines). The required surface is exactly what CounterGroups already demands of a tier, made nameable:

d
enum isCounterBackend(B) =
       is(typeof(B.init.tryOpen()) == CapabilityReport)
    && is(typeof(B.init.close()))
    && is(typeof(B.init.family(true)) == MetricDescriptor[])
    && is(typeof((ref BenchStats row, scope void delegate() timed,
                  scope void delegate() between, ulong iters)
              => B.init.countInto(row, timed, between, iters)));
// Optional: B.preciseSample(...), B.classifyPages(...), B.resolveName(...)
// — presence detected by introspection, reflected in the CapabilityReport.

BenchStats grows one Nullable!XStats per backend exactly as today (the documented one-field-plus-three-lines contract); the catalog seam is untouched — a backend ships its XCells/XFamily pair and every downstream consumer (table, --metrics, --list-metrics, --bench-json) works unchanged.

2.3 Reporting absences

--list-metrics gains a per-backend capability block, and the bench header prints one line per absent-but-requested capability from unavailableBecause. Acceptance: on the survey's own hosts the report matches the survey's findings — the Zen 4 box shows preciseMemory present via ibs_op but eventTracing absent (root-only tracefs, as today's --syscalls correctly detects); mac-bsn shows counting present-unprivileged only as process-scope rusage.

Borrows: the availability handshake and status-string discipline of perf.d; the "absence is a finding" spine of the comparison matrix.


3. Milestone 2: Linux counting depth (naming, scaling, self-monitoring)

Goal: lift the three counting-tier gaps the audit found, in place, behind Capability flags.

3.1 countingRaw + eventNaming

Accept raw selectors (--metrics 'raw:r04c2'-style, mapping to perf_event_attr{type: RAW, config: 0x…}) and, optionally, named µarch events resolved through libpfm4 when present. Two survey findings constrain the design (event-naming.md):

  • Auto-detect lags silicon. Stock libpfm 4.13.0 fails to detect the Zen 4 test box (family 25/model 0x61) — the backend must resolve the PMU name from CPUID itself and force it (LIBPFM_FORCE_PMU semantics via the API), not trust pfm_initialize detection. [hw-verified: x86_64-linux]
  • Modifiers ride exclude_*. The :u/:k grammar maps to attr fields, not config bits — the probe pfm4-name-roundtrip.d demonstrates the exact round trip the backend needs, including the 40-byte pfm_perf_encode_arg_t ABI check.

libpfm4 stays a soft dependency (dlopen or link-time optional): without it, countingRaw still works with numeric selectors; eventNaming is advertised absent with reason "libpfm unavailable".

3.2 countingScaled

Where a requested event set exceeds the PMU (the baseline's LLC-drop scenario), offer labeled estimates instead of silent column loss: read time_enabled/time_running, scale by the kernel-documented formula, and render scaled cells with an explicit marker (the survey's counting probe validates the estimate to ~1% on a 10-events-on-6-PMCs oversubscription, and also shows the failure mode — a 5.7× scale from a 0.58 ms slice is visibly noisy, hence labeled). Default stays exact-or-drop; scaling is opt-in.

3.3 selfMonitoring

For very short bodies where the per-iteration ioctl/read bracket dominates, add an rdpmc fast path: mmap the leader's perf_event_mmap_page, check cap_user_rdpmc, and read counters in-loop via the documented seqlock (lock/index/offset/pmc_widthlinux-perf-events.md quotes the uAPI pseudocode verbatim). On ARM the same userpage is gated by PMUSERENR/perf_user_access (arm.md); on RISC-V by scounteren (riscv.md) — same capability flag, per-ISA probe.

Borrows: perf_group.d's bracket (unchanged for the syscall path); the uAPI rdpmc contract; libpfm4's encoding pipeline; the wired bench's need for per-byte normalization (raw events make cyc/B per-engine tables µarch-portable).


4. Milestone 3: the macOS floor

Goal: replace the empty Darwin stub with the two things macOS actually permits unprivileged, per macos.md ([hw-verified: aarch64-darwin]).

  • counting (process scope): proc_pid_rusage(RUSAGE_INFO_V4)ri_instructions/ri_cycles — true retired instructions and cycles with no root and no entitlement (measured IPC 2.82 on mac-bsn). Delivered as a tier-0-style backend: snapshot pairs around the counting pass, per-iteration averages, quantitative class. Note the scope honestly: process-wide fixed counters, not per-bracket configurable events.
  • Capability ads for the rest: configurable per-region events → unavailableBecause = "kpc requires root ('root or the blessed pid', xnu kern_kpc.c); event allowlist RESTRICT_TO_KNOWN"; sampling/symbolication → "via Instruments/xctrace only". An optional root-mode kpc backend (dlopen kperf.framework, kpep-plist naming) is sketched but explicitly deferred — the survey shows even root cannot program unlisted selectors, and the private-framework surface is unstable across releases.

Build note: the mac toolchain path is ldc2 driven directly (dub fork-ENOMEMs on the reference box); the backend is version-gated D in the same modules, no new packages.

Borrows: the three-tier privilege map (rusage / kpc / xctrace) and its EPERM matrix; tier0.d's snapshot-pair shape.


5. Milestone 4: the Windows floor

Goal: a Windows backend advertising exactly what an unelevated (and, separately, an elevated) process can get, per windows.md.

  • counting (thread scope, driver-free): EnableThreadProfiling / ReadThreadProfilingDataCycleTime only — the sole hardware datum that needs no kernel driver. Rendered as one quantitative column; the 16 HwCounters slots are advertised absent with reason "requires KeSetHardwareCounterConfiguration driver (system-global, single-tenant)".
  • counting under elevation (later, optional): ETW kernel-logger PMC counting on context switches (TracePmcCounterListInfo), admin + SeSystemProfilePrivilege gated; consumption model per krabsetw.
  • numaAttribution groundwork: GetLogicalProcessorInformationEx + QueryWorkingSetEx (the move_pages-query analog) — cheap, documented, unprivileged.
  • D bindings: druntime core.sys.windows covers only DbgHelp+psapi; the HCP/ETW/NUMA declarations are pulled from the windows-d generated modules (hardwarecounterprofiling.d, etw.d, systeminformation.d, processstatus.d) rather than hand-written extern(Windows) prototypes.

Borrows: the W1-role-replacement table (which Linux role each Windows surface substitutes); krabsetw's session/provider split for any future ETW work.


6. Milestone 5: precise memory tier (Linux)

Goal: a preciseMemory backend delivering data-source / latency / address→node columns on hardware that has an engine, per precise-sampling.md.

  • One decoder, three engines. Target the vendor-neutral perf_mem_data_src union; open ibs_op on AMD (with the swfilt recipe — bare exclude_kernel is EINVAL on Zen 4 [hw-verified: x86_64-linux]), cpu + precise_ip on Intel (PEBS), SPE on ARM (deferred until aarch64-linux hardware exists to verify against — advertised absent with reason until then).
  • Node classification. Sampled data addresses classified via the raw get_mempolicy(MPOL_F_NODE|MPOL_F_ADDR) / move_pages query oracles (no libs "numa" link — numactl ships no numa.pc and the syscalls aren't in glibc; the probe mem-latency-numa.d is the reference implementation). The IBS "remote" bit alone is not node attribution — hardware says remote/local, the oracle says which node.
  • Rendering. New diagnostic columns: latency percentiles (WEIGHT), data-source level distribution, local/remote split. All absent (with reasons) on hosts without an engine — exactly what the capability seam exists for.

Borrows: the IBS attr matrix and decode tables from the survey experiments; libnuma.md's missing-helper finding; the single-NUMA-node caveat (cross-node classification remains [literature]-verified until multi-socket hardware is available — an open question carried in comparison.md).


7. Milestone 6: sampling & symbolization tier (Linux)

Goal: ipSampling + symbolization — turn "IPC fell" into "IPC fell in sumSquares at readers.d:137".

  • Ring-buffer consumption is pure D. druntime's core.sys.linux.perf_event already carries the mmap page, record headers, and sample formats — the survey's sampling probe consumes PERF_RECORD_SAMPLE/MMAP2 with no C shim.
  • Address-space model + build-id discipline. PERF_RECORD_MMAP2 arrives only for mappings created while enabled; pre-existing code comes from /proc/self/maps (or dwfl_linux_proc_report, which reads the same file). Build-ids are validated before symbolizing — the stale-binary hazard is a bench-harness reality (rebuilt-binary + old-run comparisons).
  • Symbolization via libdwfl (dwfl_addrmoduledwfl_module_addrinfodwfl_module_getsrc), with the survey's hw-hit gotcha encoded: addrinfo's offset/sym arguments must be non-NULL. Soft dependency like libpfm4: absent libdw ⇒ symbolization advertised absent, samples still counted.
  • Optional DWARF-CFI unwind (STACK_USER+REGS_USERdwfl_getthread_frames) for frame-pointer-less builds — the unwind probe proved the full path in-process on a --frame-pointer=none build.
  • Output shape: a per-case flat profile (top-N symbols by samples) as a diagnostic table section, off by default (--bench-profile), never polluting the timing pass.

Borrows: perf's consumer ordering (linux-perf-events.md); elfutils' documented call sequence (elfutils.md); the counting/ timing pass separation from bench.d (the profile pass is a third pass, like counting).


8. Milestone summary

#MilestoneCapabilities unlockedDepends onEvidence base
1Capability seamreporting of absences (all backends)baseline, comparison
2Linux counting depthcountingRaw, eventNaming, countingScaled, selfMonitoring1linux, event-naming, probes
3macOS floorcounting (process scope) on Darwin1macos [hw-verified]
4Windows floorcounting (CycleTime), NUMA groundwork1windows
5Precise memory tierpreciseMemory, numaAttribution1, 2precise-sampling [hw-verified]
6Sampling & symbolizationipSampling, symbolization1linux, elfutils, probes

Sequencing rationale: M1 is pure refactoring value (better reporting today); M2 deepens the mode the harness already lives in; M3/M4 are small, independent, and kill the "all counters vanish off Linux" cliff; M5/M6 add the where dimension and are the only milestones with new soft C dependencies (libdw, libpfm4) — both degrade to advertised absence.

ISA notes carried across milestones: on big.LITTLE ARM the backend must open events on the PMU whose cpumask contains the pinned core (arm.md); on RISC-V the backend is a capability subset by construction — counting always (SBI), sampling iff Sscofpmf, branch records never until a CTR consumer lands (riscv.md).

Sources