Skip to content

Commit b09232e

Browse files
authored
Expose counter information in profiler-cli (#6084)
Add `counter list` and `counter info <handle>` commands, and list each counter under its process in `profile info`, to inspect any counter track from the terminal. Counters get stable `c-N` handles, like threads and functions. Per-counter stats come from the counter's own tooltip schema, reusing the timeline tooltips' labels and formatters so the CLI and UI agree. Stats respect the current zoom. Closes #6040
1 parent 4bd8b75 commit b09232e

16 files changed

Lines changed: 892 additions & 8 deletions

File tree

profiler-cli/guide.txt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ HANDLE SYSTEM
117117
t-0, t-1 Thread handles (from "profile info")
118118
m-1234 Marker handles (from "thread markers")
119119
f-12 Function handles (from "thread samples", "thread functions")
120+
c-0, c-1 Counter handles (from "counter list" or "profile info")
120121
ts-6 Timestamp handles (named points in time, usable with "zoom push")
121122

122123
Handle lifetime and stability:
@@ -127,6 +128,8 @@ HANDLE SYSTEM
127128
m-N thread markers No -- rebuilt each time the daemon starts
128129
f-N thread samples, Yes -- direct index into the profile's function
129130
thread functions table; same profile always yields the same f-N
131+
c-N counter list Yes -- direct index into the profile's counter
132+
array; same profile always yields the same c-N
130133
ts-N thread markers No -- position-based, session-scoped
131134
──────────────────────────────────────────────────────────────────────────
132135

@@ -215,6 +218,31 @@ FILTERS
215218
profiler-cli filter push --during-marker --search Paint
216219

217220

221+
COUNTERS
222+
223+
Counters are time series the profiler records alongside samples: memory usage,
224+
network bandwidth, process CPU, power, and similar. Each counter has a handle
225+
(c-0, c-1, ...) and carries its own display metadata (label, unit, graph type).
226+
227+
profiler-cli counter list List all counters with one-line summaries
228+
profiler-cli counter info c-0 Detailed info and stats for one counter
229+
230+
Counters also appear in "profile info", listed under their owning process
231+
next to that process's threads (much like the timeline track list).
232+
233+
The stats shown come from the counter's own tooltip schema, so they match the
234+
timeline tooltips. Each counter reports its whole-range aggregates, e.g. the
235+
memory range for Memory, data transferred for Bandwidth, or energy used (with a
236+
CO2e estimate) for Power.
237+
238+
All counter stats respect the current zoom: with no zoom they cover the whole
239+
profile; after "zoom push" they cover the committed range. Combine with zoom to
240+
see, for example, how much memory a specific time window allocated:
241+
242+
profiler-cli zoom push 2.7,3.1
243+
profiler-cli counter info c-0
244+
245+
218246
JSON OUTPUT
219247

220248
Add --json to any command to get structured JSON output, suitable for piping to jq

profiler-cli/schemas.txt

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,39 @@ profiler-cli profile info --json
1919
processes: [{
2020
pid, name, cpuMs,
2121
threads: [{ threadHandle, threadIndex, name, tid, cpuMs }],
22-
remainingThreads?: { count, combinedCpuMs, maxCpuMs }
22+
remainingThreads?: { count, combinedCpuMs, maxCpuMs },
23+
counters?: [CounterSummary]
2324
}],
2425
remainingProcesses?: { count, combinedCpuMs, maxCpuMs },
2526
context: SessionContext
2627
}
2728

29+
CounterSummary:
30+
{
31+
counterHandle, counterIndex, name, label, category,
32+
unit, graphType,
33+
color, pid, mainThreadIndex, mainThreadHandle, mainThreadName,
34+
rangeSampleCount,
35+
stats: [{ source, label, value, formattedValue, carbon? }]
36+
}
37+
38+
profiler-cli counter list --json
39+
{
40+
type: "counter-list",
41+
counters: [CounterSummary],
42+
context: SessionContext
43+
}
44+
45+
profiler-cli counter info --json
46+
{
47+
type: "counter-info",
48+
...CounterSummary,
49+
description,
50+
sampleCount,
51+
rangeStart, rangeEnd,
52+
context: SessionContext
53+
}
54+
2855
profiler-cli thread samples --json
2956
{
3057
type: "thread-samples",
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
/**
6+
* `profiler-cli counter` command.
7+
*/
8+
9+
import type { Command } from 'commander';
10+
import { sendCommand } from '../client';
11+
import { formatOutput } from '../output';
12+
import { addGlobalOptions } from './shared';
13+
14+
export function registerCounterCommand(
15+
program: Command,
16+
sessionDir: string
17+
): void {
18+
const counter = program
19+
.command('counter')
20+
.description('Counter-level commands');
21+
22+
addGlobalOptions(
23+
counter
24+
.command('list')
25+
.description('List all counters with one-line summaries')
26+
).action(async (opts) => {
27+
const result = await sendCommand(
28+
sessionDir,
29+
{ command: 'counter', subcommand: 'list' },
30+
opts.session
31+
);
32+
console.log(formatOutput(result, opts.json ?? false));
33+
});
34+
35+
addGlobalOptions(
36+
counter
37+
.command('info [handle]')
38+
.description('Show detailed information about a counter (e.g. c-0)')
39+
.option('--counter <handle>', 'Counter handle')
40+
).action(async (handleArg: string | undefined, opts) => {
41+
const counterHandle = handleArg ?? opts.counter;
42+
const result = await sendCommand(
43+
sessionDir,
44+
{ command: 'counter', subcommand: 'info', counter: counterHandle },
45+
opts.session
46+
);
47+
console.log(formatOutput(result, opts.json ?? false));
48+
});
49+
}

profiler-cli/src/daemon.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,18 @@ export class Daemon {
380380
default:
381381
throw assertExhaustiveCheck(command);
382382
}
383+
case 'counter':
384+
switch (command.subcommand) {
385+
case 'list':
386+
return this.querier!.counterList();
387+
case 'info':
388+
if (!command.counter) {
389+
throw new Error('counter handle required for counter info');
390+
}
391+
return this.querier!.counterInfo(command.counter);
392+
default:
393+
throw assertExhaustiveCheck(command);
394+
}
383395
case 'sample':
384396
switch (command.subcommand) {
385397
case 'info':

profiler-cli/src/formatters.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ import type {
3535
SampleFilterSpec,
3636
ProfileLogsResult,
3737
ThreadSelectResult,
38+
CounterSummary,
39+
CounterListResult,
40+
CounterInfoResult,
3841
} from './protocol';
3942
import { truncateFunctionName } from '../../src/profile-query/function-list';
4043
import { describeSpec } from '../../src/profile-query/filter-stack';
@@ -428,6 +431,10 @@ Name: ${result.name}\n`;
428431
if (process.remainingThreads) {
429432
output += ` + ${process.remainingThreads.count} more threads with combined CPU time ${process.remainingThreads.combinedCpuMs.toFixed(3)}ms and max CPU time ${process.remainingThreads.maxCpuMs.toFixed(3)}ms (use --all to see all)\n`;
430433
}
434+
435+
for (const counter of process.counters ?? []) {
436+
output += ` ${counter.counterHandle}: ${counter.label}${formatCounterStats(counter)}\n`;
437+
}
431438
}
432439

433440
if (result.remainingProcesses) {
@@ -451,6 +458,85 @@ Name: ${result.name}\n`;
451458
return output;
452459
}
453460

461+
function formatCounterStatInline(
462+
stat: CounterSummary['stats'][number]
463+
): string {
464+
const value = stat.carbon
465+
? `${stat.formattedValue} (${stat.carbon})`
466+
: stat.formattedValue;
467+
return `${stat.label}: ${value}`;
468+
}
469+
470+
/** The ` - stat; stat [N samples]` trailer shared by counter list and profile info. */
471+
function formatCounterStats(counter: CounterSummary): string {
472+
const stats =
473+
counter.stats.length > 0
474+
? ` - ${counter.stats.map(formatCounterStatInline).join('; ')}`
475+
: '';
476+
return `${stats} [${counter.rangeSampleCount} samples]`;
477+
}
478+
479+
function formatCounterSummaryLine(counter: CounterSummary): string {
480+
return ` ${counter.counterHandle}: ${counter.label} (${counter.category})${formatCounterStats(counter)}`;
481+
}
482+
483+
/**
484+
* Format a CounterListResult as plain text.
485+
*/
486+
export function formatCounterListResult(
487+
result: WithContext<CounterListResult>
488+
): string {
489+
const contextHeader = formatContextHeader(result.context);
490+
if (result.counters.length === 0) {
491+
return `${contextHeader}\n\nNo counters in this profile.`;
492+
}
493+
const lines = result.counters.map(formatCounterSummaryLine);
494+
return `${contextHeader}\n\nCounters (${result.counters.length}):\n${lines.join('\n')}`;
495+
}
496+
497+
/**
498+
* Format a CounterInfoResult as plain text.
499+
*/
500+
export function formatCounterInfoResult(
501+
result: WithContext<CounterInfoResult>
502+
): string {
503+
const contextHeader = formatContextHeader(result.context);
504+
const lines = [
505+
contextHeader,
506+
'',
507+
`Counter ${result.counterHandle}: ${result.label}`,
508+
` Name: ${result.name}`,
509+
` Category: ${result.category}`,
510+
];
511+
if (result.description) {
512+
lines.push(` Description: ${result.description}`);
513+
}
514+
lines.push(` Unit: ${result.unit || '(none)'}`);
515+
lines.push(` Graph type: ${result.graphType}`);
516+
lines.push(
517+
` Main thread: ${result.mainThreadHandle} (${result.mainThreadName})`
518+
);
519+
lines.push(
520+
` Samples: ${result.sampleCount} total, ${result.rangeSampleCount} in current range`
521+
);
522+
if (result.rangeStart !== null && result.rangeEnd !== null) {
523+
const zeroAt = result.context.rootRange.start;
524+
lines.push(
525+
` Time span: ${formatDuration(result.rangeStart - zeroAt)}${formatDuration(result.rangeEnd - zeroAt)}`
526+
);
527+
}
528+
if (result.stats.length > 0) {
529+
lines.push(' Stats (current range):');
530+
for (const stat of result.stats) {
531+
const value = stat.carbon
532+
? `${stat.formattedValue} (${stat.carbon})`
533+
: stat.formattedValue;
534+
lines.push(` ${stat.label}: ${value}`);
535+
}
536+
}
537+
return lines.join('\n');
538+
}
539+
454540
/**
455541
* Helper function to format a call tree node recursively.
456542
*

profiler-cli/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { registerProfileCommand } from './commands/profile';
3737
import { registerThreadCommand } from './commands/thread';
3838
import { registerMarkerCommand } from './commands/marker';
3939
import { registerFunctionCommand } from './commands/function';
40+
import { registerCounterCommand } from './commands/counter';
4041
import { registerZoomCommand } from './commands/zoom';
4142
import { registerFilterCommand } from './commands/filter';
4243
import { registerSessionCommand } from './commands/session';
@@ -85,6 +86,8 @@ Examples:
8586
profiler-cli thread samples
8687
profiler-cli thread functions --search GC --min-self 1
8788
profiler-cli thread markers --search DOMEvent --category Graphics
89+
profiler-cli counter list
90+
profiler-cli counter info c-0
8891
profiler-cli zoom push 2.7,3.1
8992
profiler-cli filter push --excludes-function f-184
9093
profiler-cli status
@@ -180,6 +183,7 @@ Examples:
180183
registerThreadCommand(program, SESSION_DIR);
181184
registerMarkerCommand(program, SESSION_DIR);
182185
registerFunctionCommand(program, SESSION_DIR);
186+
registerCounterCommand(program, SESSION_DIR);
183187
registerZoomCommand(program, SESSION_DIR);
184188
registerFilterCommand(program, SESSION_DIR);
185189
registerSessionCommand(program, SESSION_DIR);

profiler-cli/src/output.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import {
2828
formatProfileLogsResult,
2929
formatThreadPageLoadResult,
3030
formatThreadSelectResult,
31+
formatCounterListResult,
32+
formatCounterInfoResult,
3133
} from './formatters';
3234

3335
/**
@@ -88,6 +90,10 @@ export function formatOutput(
8890
return formatThreadPageLoadResult(result);
8991
case 'thread-select':
9092
return formatThreadSelectResult(result);
93+
case 'counter-list':
94+
return formatCounterListResult(result);
95+
case 'counter-info':
96+
return formatCounterInfoResult(result);
9197
default:
9298
throw assertExhaustiveCheck(result);
9399
}

profiler-cli/src/protocol.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ export type {
5050
ProfileInfoResult,
5151
ProfileLogsResult,
5252
ThreadSelectResult,
53+
CounterSummary,
54+
CounterListResult,
55+
CounterInfoResult,
5356
} from '../../src/profile-query/types';
5457
export type { CallTreeCollectionOptions } from '../../src/profile-query/formatters/call-tree';
5558

@@ -79,6 +82,8 @@ import type {
7982
FilterStackResult,
8083
ProfileLogsResult,
8184
ThreadSelectResult,
85+
CounterListResult,
86+
CounterInfoResult,
8287
} from '../../src/profile-query/types';
8388
import type { CallTreeCollectionOptions } from '../../src/profile-query/formatters/call-tree';
8489

@@ -141,6 +146,11 @@ export type ClientCommand =
141146
subcommand: 'info' | 'select' | 'stack';
142147
marker?: string;
143148
}
149+
| {
150+
command: 'counter';
151+
subcommand: 'list' | 'info';
152+
counter?: string;
153+
}
144154
| { command: 'sample'; subcommand: 'info' | 'select'; sample?: string }
145155
| {
146156
command: 'function';
@@ -195,7 +205,9 @@ export type CommandResult =
195205
| WithContext<FunctionAnnotateResult>
196206
| WithContext<ProfileLogsResult>
197207
| WithContext<ThreadPageLoadResult>
198-
| WithContext<ThreadSelectResult>;
208+
| WithContext<ThreadSelectResult>
209+
| WithContext<CounterListResult>
210+
| WithContext<CounterInfoResult>;
199211

200212
export interface SessionMetadata {
201213
id: string;

0 commit comments

Comments
 (0)