Skip to content

Commit f525fb0

Browse files
committed
Expose counter information in profiler-cli
Add `counter list` and `counter info <handle>` commands and a `profile info --counters` flag 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. Part of #6040
1 parent 7d0fb66 commit f525fb0

15 files changed

Lines changed: 776 additions & 7 deletions

File tree

profiler-cli/guide.txt

Lines changed: 26 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", "profile info --counters")
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,29 @@ 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+
profiler-cli profile info --counters Append a counters section to profile info
230+
231+
The stats shown come from the counter's own tooltip schema, so they match the
232+
timeline tooltips. Each counter reports its whole-range aggregates, e.g. the
233+
memory range for Memory, data transferred for Bandwidth, or energy used (with a
234+
CO2e estimate) for Power.
235+
236+
All counter stats respect the current zoom: with no zoom they cover the whole
237+
profile; after "zoom push" they cover the committed range. Combine with zoom to
238+
see, for example, how much memory a specific time window allocated:
239+
240+
profiler-cli zoom push 2.7,3.1
241+
profiler-cli counter info c-0
242+
243+
218244
JSON OUTPUT
219245

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

profiler-cli/schemas.txt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,33 @@ profiler-cli profile info --json
2222
remainingThreads?: { count, combinedCpuMs, maxCpuMs }
2323
}],
2424
remainingProcesses?: { count, combinedCpuMs, maxCpuMs },
25+
counters?: [CounterSummary],
26+
context: SessionContext
27+
}
28+
29+
CounterSummary:
30+
{
31+
counterHandle, counterIndex, name, label, category,
32+
unit, graphType,
33+
color, pid, mainThreadIndex, mainThreadHandle, mainThreadName,
34+
rangeSampleCount,
35+
stats: [{ source, label, labelKey?, 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,
2552
context: SessionContext
2653
}
2754

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/commands/profile.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export function registerProfileCommand(
2828
'Show all processes and threads (overrides default top-5 limit)'
2929
)
3030
.option('--search <term>', 'Filter by substring')
31+
.option('--counters', 'Also list the profile counters (eg, Memory)')
3132
).action(async (opts) => {
3233
const result = await sendCommand(
3334
sessionDir,
@@ -36,6 +37,7 @@ export function registerProfileCommand(
3637
subcommand: 'info',
3738
all: opts.all,
3839
search: opts.search,
40+
counters: opts.counters,
3941
},
4042
opts.session
4143
);

profiler-cli/src/daemon.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,11 @@ export class Daemon {
278278
case 'profile':
279279
switch (command.subcommand) {
280280
case 'info':
281-
return this.querier!.profileInfo(command.all, command.search);
281+
return this.querier!.profileInfo(
282+
command.all,
283+
command.search,
284+
command.counters
285+
);
282286
case 'threads':
283287
throw new Error('unimplemented');
284288
case 'logs':
@@ -360,6 +364,18 @@ export class Daemon {
360364
default:
361365
throw assertExhaustiveCheck(command);
362366
}
367+
case 'counter':
368+
switch (command.subcommand) {
369+
case 'list':
370+
return this.querier!.counterList();
371+
case 'info':
372+
if (!command.counter) {
373+
throw new Error('counter handle required for counter info');
374+
}
375+
return this.querier!.counterInfo(command.counter);
376+
default:
377+
throw assertExhaustiveCheck(command);
378+
}
363379
case 'sample':
364380
switch (command.subcommand) {
365381
case 'info':

profiler-cli/src/formatters.ts

Lines changed: 84 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';
@@ -448,9 +451,90 @@ Name: ${result.name}\n`;
448451
output += 'No significant activity.\n';
449452
}
450453

454+
if (result.counters && result.counters.length > 0) {
455+
output += `\nCounters (${result.counters.length}):\n`;
456+
for (const counter of result.counters) {
457+
output += `${formatCounterSummaryLine(counter)}\n`;
458+
}
459+
}
460+
451461
return output;
452462
}
453463

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

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: 14 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

@@ -93,6 +98,7 @@ export type ClientCommand =
9398
subcommand: 'info' | 'threads';
9499
all?: boolean;
95100
search?: string;
101+
counters?: boolean;
96102
}
97103
| {
98104
command: 'profile';
@@ -141,6 +147,11 @@ export type ClientCommand =
141147
subcommand: 'info' | 'select' | 'stack';
142148
marker?: string;
143149
}
150+
| {
151+
command: 'counter';
152+
subcommand: 'list' | 'info';
153+
counter?: string;
154+
}
144155
| { command: 'sample'; subcommand: 'info' | 'select'; sample?: string }
145156
| {
146157
command: 'function';
@@ -195,7 +206,9 @@ export type CommandResult =
195206
| WithContext<FunctionAnnotateResult>
196207
| WithContext<ProfileLogsResult>
197208
| WithContext<ThreadPageLoadResult>
198-
| WithContext<ThreadSelectResult>;
209+
| WithContext<ThreadSelectResult>
210+
| WithContext<CounterListResult>
211+
| WithContext<CounterInfoResult>;
199212

200213
export interface SessionMetadata {
201214
id: string;

0 commit comments

Comments
 (0)