@magnaboy/cli-trace
v0.0.3
Published
Perfetto traces, simpleperf profiles, cumulative counters and paired-measurement statistics for Node.js CLIs.
Readme
@magnaboy/cli-trace
Perfetto traces, simpleperf profiles, cumulative hardware counters, and the statistics that turn repeated measurements into a defensible number.
Install
npm i @magnaboy/cli-traceRequires Node 25+ and ESM. Import the whole package or one area:
import { integrateEnergy } from '@magnaboy/cli-trace/counters';
import { logRatioInterval } from '@magnaboy/cli-trace/statistics';Cumulative counters
import { integrateEnergy, timeWeightedMean, windowPoints } from '@magnaboy/cli-trace/counters';
const window = integrateEnergy(railSamples, startSeconds, endSeconds, { maxGapSeconds: 2.5 });
console.log(window.watts, window.duplicates);Counter samples are { time, value } with time in seconds on whatever timebase the caller uses
consistently. Timestamps must strictly increase and values must be finite.
windowPoints clips a series to [start, end] and interpolates both endpoints. The series must
bracket the window on both sides; extrapolating past the observed samples would invent measurement
where none exists. A gap longer than maxGapSeconds (default 2.5) inside the window is rejected for
the same reason, because part of the window then went unobserved.
timeWeightedMean trapezoid-integrates an instantaneous series, such as sampled watts, so a level
that held for three seconds counts three times as much as one that held for one. cumulativeDelta
and cumulativeRate read a monotonically rising counter and reject one that was reset mid-series.
cumulativeRate takes a scale to convert units: microjoules need 1e-6 to report watts.
integrateEnergy combines these for a cumulative microjoule rail counter and reports the sample
count and how many duplicates it dropped. A power HAL can return the same cached snapshot to two
consecutive polls; those carry no additional energy, and keeping them would create zero-duration
intervals that are not real measurements. dedupeCounterPoints exposes that step on its own.
Statistics for repeated measurements
import { blockContrast, confidenceInterval, logRatioInterval, studentTQuantile } from '@magnaboy/cli-trace/statistics';
const interval = logRatioInterval(pairs.map(pair => Math.log(pair.baseline / pair.treatment)));
console.log(`${interval.mean.toFixed(1)}% ±${interval.halfWidthPercentagePoints.toFixed(1)}pp`);studentTQuantile(probability, degreesOfFreedom) is the t inverse CDF, computed from the
regularized incomplete beta function rather than a lookup table, so there is no upper limit on the
number of blocks and no dependency on a statistics library. It reproduces the published critical
values to better than 1e-6. incompleteBeta is exported for callers that need it directly.
confidenceInterval(values, confidence) is a two-sided Student-t interval; confidence is the total
coverage, so 0.95 leaves 2.5% in each tail. It returns the critical value and standard error
alongside the bounds, for reporting.
logRatioInterval is for ratios. Power and runtime comparisons are multiplicative, so the analysis
runs on log ratios and converts back with expm1. The interval endpoints are converted rather than
the percentages averaged, which keeps the result correct and asymmetric.
blockContrast takes one balanced ABBA or BAAB block of { treatment, time, value } and returns the
treatment effect as a log ratio with linear drift over time regressed out. A device warms over a long
session and its power climbs with it, so a single ordering makes whatever ran last look worse. Even
spacing already cancels linear drift, and unadjustedLogRatio reports that naive contrast for
comparison; the adjustment earns its place when the windows are unevenly spaced. information
reports how well the block's timing separates treatment from trend, out of 4.
Perfetto
import { assertTraceIsComplete, perfettoConfig, readCounterSeries } from '@magnaboy/cli-trace/perfetto';
const config = perfettoConfig(await readFile('configs/full.pbtx', 'utf8'), { durationMs: 20_000 });
await assertTraceIsComplete(runner, { executable: traceProcessor, trace });
const rails = await readCounterSeries(runner, { executable: traceProcessor, trace, tracks: ['power.rails.*'] });perfettoConfig strips CRLF, which the text config parser rejects and which a repository checked out
on Windows produces. It appends duration_ms so one config serves any window length, and refuses to
append a duration the config already sets.
queryTrace runs one SQL query through the trace_processor CLI and returns rows as string maps.
parseTraceProcessorCsv does the parsing and honors RFC 4180 quoting, so a value containing a
comma, a newline or a doubled quote survives — which a regular expression per row cannot do.
assertTraceIsComplete runs TRACE_ERROR_QUERY and rejects a trace that reported errors or dropped
data. This has to be explicit: a lossy trace still answers every query, just with quietly incomplete
numbers. readCounterSeries reads counter tracks by GLOB pattern and converts Perfetto's
nanosecond timestamps to the seconds the counter helpers expect.
Each queryTrace call loads the trace, so a report needing many queries should combine them. A
persistent session over trace_processor_shell --httpd is not implemented here.
simpleperf
import { parseSampleCounts, rankFoldedStacks, SimpleperfTools } from '@magnaboy/cli-trace/simpleperf';
const tools = new SimpleperfTools({ ndkPath, python: 'python3' });
await runner.run(tools.binaryCacheCommand({ perfData, cacheDirectory, libraryDirectories }));SimpleperfTools builds CommandSpecs for the simpleperf Python tools rather than running them, so
the arguments can be tested. Every command requires cacheDirectory, which becomes its working
directory: all of these tools read and write binary_cache relative to the working directory and the
path is not an option on any of them. Left at the caller's directory they litter wherever the script
was launched from and, in a multi-device session, symbolize one phone's profile against the other's
libraries. Use one cache directory per physical device.
binaryCacheCommand takes libraryDirectories for -lib. The packaged .so files are stripped, so
without the unstripped build output native frames symbolize to raw addresses instead of names.
parseSampleCounts reads the Samples recorded: … Samples lost: … line; lost samples silently bias
a profile toward whatever was cheap to record, so the count is worth asserting on.
parseFoldedStacks and rankFoldedStacks turn stackcollapse.py output into rankings by thread,
self cost, inclusive cost, and frames matching ownedPrefixes. Inclusive weight counts each frame
once per stack, so a recursive frame is not multiplied by its depth.
