Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions profiler-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ profiler-cli profile meta # Print profile metadata (application
profiler-cli profile logs # Print Log markers in MOZ_LOG format [--thread] [--module] [--level] [--search] [--limit]
profiler-cli thread info # Print detailed thread information
profiler-cli thread select <handle> # Select a thread (e.g., t-0, t-1)
profiler-cli thread samples # Show hot functions list for current thread
profiler-cli thread samples # Show hot functions list and category breakdown for current thread
profiler-cli thread samples-top-down # Show top-down call tree (where CPU time is spent)
profiler-cli thread samples-bottom-up # Show bottom-up call tree (what calls hot functions)
profiler-cli thread markers # List markers with aggregated statistics [--list for flat per-marker view]
Expand All @@ -45,7 +45,7 @@ profiler-cli thread page-load # Show page load summary (navigation
profiler-cli marker info <handle> # Show detailed marker information (e.g., m-1234)
profiler-cli marker stack <handle> # Show full stack trace for a marker
profiler-cli function expand <handle> # Show full untruncated function name (e.g., f-123)
profiler-cli function info <handle> # Show detailed function information
profiler-cli function info <handle> # Show detailed function information and category breakdown
profiler-cli function annotate <handle> # Show annotated source/assembly with timing data [--mode src|asm|all] [--context 2|file|N] [--symbol-server <url>]
profiler-cli zoom push <range> # Push a zoom range (e.g., 2.7,3.1 or ts-g,ts-G or m-158)
profiler-cli zoom pop # Pop the most recent zoom range
Expand Down
24 changes: 24 additions & 0 deletions profiler-cli/schemas.txt
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,20 @@ profiler-cli thread samples --json
frames: [{ name, nameWithLibrary, library?,
selfSamples, selfPercentage, totalSamples, totalPercentage }]
},
categoryBreakdown?: CategoryBreakdown,
activeFilters?, ephemeralFilters?,
context: SessionContext
}

CategoryBreakdown:
{
totalSamples,
categories: [{
name, categoryIndex, samples, percentage,
subcategories: [{ name, subcategoryIndex, samples, percentage }]
}]
}

profiler-cli thread samples-top-down --json
{
type: "thread-samples-top-down",
Expand Down Expand Up @@ -216,6 +226,20 @@ profiler-cli thread network --json
context: SessionContext
}

profiler-cli function info --json
{
type: "function-info",
functionHandle, funcIndex, name, fullName, isJS, relevantForJS,
resource?: { name, index },
library?: { name, path, debugName?, debugPath?, breakpadId? },
categoryBreakdown?: {
threadHandle, friendlyThreadName, threadSamples,
running: CategoryBreakdown & { samples, percentageOfThread },
self: CategoryBreakdown & { samples, percentageOfThread }
},
context: SessionContext
}

profiler-cli marker info --json
{
type: "marker-info",
Expand Down
113 changes: 102 additions & 11 deletions profiler-cli/src/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import type {
ProfileNetworkSummary,
MarkerGroupData,
CallTreeNode,
CategoryBreakdown,
InlineStatus,
FilterEntry,
SampleFilterSpec,
Expand Down Expand Up @@ -76,6 +77,68 @@ const INLINE_LEGEND =
'Note: (inl) = inlined by the compiler into the nearest non-inlined ancestor above. ' +
'(inl?) = some calls were inlined by the compiler.';

const BAR_WIDTH = 28;

/**
* Render aligned `label / bar / count / percentage` rows. `barRatio` is the
* fraction of the bar's full width to fill, which is not always the same as
* `percentage`; some tables scale their bars against the largest row instead.
*/
function formatBarRows(
rows: Array<{
label: string;
count: number;
percentage: number;
barRatio: number;
}>
): string[] {
const maxLabelLen = Math.max(...rows.map((row) => row.label.length));

return rows.map((row) => {
const barLen = Math.round(row.barRatio * BAR_WIDTH);
const bar = '█'.repeat(barLen).padEnd(BAR_WIDTH);
const label = row.label.padEnd(maxLabelLen);
const countStr = String(row.count).padStart(6);
const pctStr = row.percentage.toFixed(1).padStart(5);
return ` ${label} ${bar} ${countStr} ${pctStr}%`;
});
}

/**
* Render a category breakdown as a `──── title ────` section, with each
* category followed by its non-empty subcategories, indented.
*/
function formatCategoryBreakdown(
title: string,
breakdown: CategoryBreakdown,
emptyMessage: string
): string[] {
const lines = [`──── ${title} ────`, ''];

if (breakdown.categories.length === 0) {
lines.push(` ${emptyMessage}`);
return lines;
}

const rows = breakdown.categories.flatMap((category) => [
{
label: category.name,
count: Math.round(category.samples),
percentage: category.percentage,
barRatio: Math.abs(category.samples) / breakdown.totalSamples,
},
...category.subcategories.map((subcategory) => ({
label: ` ${subcategory.name}`,
count: Math.round(subcategory.samples),
percentage: subcategory.percentage,
barRatio: Math.abs(subcategory.samples) / breakdown.totalSamples,
})),
]);

lines.push(...formatBarRows(rows));
return lines;
}

/**
* Format a SessionContext as a compact header line.
* Shows current thread selection, zoom range, and full profile duration.
Expand Down Expand Up @@ -227,6 +290,28 @@ Function ${result.functionHandle}:
}
}

const { running, self, threadHandle, friendlyThreadName } =
result.categoryBreakdown;

if (running.samples === 0) {
output += `\n\n No samples for this function on ${threadHandle} (${friendlyThreadName}) in the current view.`;
} else {
for (const [kind, breakdown] of [
['running', running],
['self', self],
] as const) {
const count = Math.round(breakdown.samples);
const share = breakdown.percentageOfThread.toFixed(1);
output +=
'\n\n' +
formatCategoryBreakdown(
`Categories: ${kind} (${count} samples, ${share}% of thread)`,
breakdown,
`No ${kind} samples for this function in the current view.`
).join('\n');
}
}

return output;
}

Expand Down Expand Up @@ -985,6 +1070,13 @@ export function formatThreadSamplesResult(
return output;
}

output +=
formatCategoryBreakdown(
`Categories (${Math.round(result.categoryBreakdown.totalSamples)} running samples)`,
result.categoryBreakdown,
'No samples in the current view.'
).join('\n') + '\n\n';

// Top functions by total time
output += 'Top Functions (by total time):\n';
output +=
Expand Down Expand Up @@ -2028,19 +2120,18 @@ export function formatThreadPageLoadResult(
if (result.categories.length === 0) {
lines.push(' No sample data available during page load.');
} else {
const BAR_WIDTH = 28;
const maxCount = result.categories[0].count;
const maxNameLen = Math.max(...result.categories.map((c) => c.name.length));

for (const cat of result.categories) {
const barLen =
maxCount > 0 ? Math.round((cat.count / maxCount) * BAR_WIDTH) : 0;
const bar = '█'.repeat(barLen).padEnd(BAR_WIDTH);
const name = cat.name.padEnd(maxNameLen);
const countStr = String(cat.count).padStart(6);
const pctStr = cat.percentage.toFixed(1).padStart(5);
lines.push(` ${name} ${bar} ${countStr} ${pctStr}%`);
}
lines.push(
...formatBarRows(
result.categories.map((cat) => ({
label: cat.name,
count: cat.count,
percentage: cat.percentage,
barRatio: maxCount > 0 ? cat.count / maxCount : 0,
}))
)
);
}

lines.push('');
Expand Down
5 changes: 5 additions & 0 deletions profiler-cli/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export type {
ThreadSamplesBottomUpResult,
CallTreeNode,
CallTreeScoringStrategy,
CategoryBreakdown,
CategoryBreakdownEntry,
CategorySubBreakdownEntry,
FunctionCategoryBreakdown,
FunctionCategoryBreakdowns,
InlineStatus,
ThreadMarkersResult,
ThreadNetworkResult,
Expand Down
35 changes: 35 additions & 0 deletions profiler-cli/src/test/integration/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {

import type {
FilterStackResult,
FunctionInfoResult,
ProfileMetaResult,
SessionMetadata,
StatusResult,
Expand Down Expand Up @@ -308,6 +309,40 @@ describe('profiler-cli basic functionality', () => {
expect(status.filterStacks).toHaveLength(0);
});

it('thread samples breaks the samples down by category', async () => {
await cli(ctx, ['load', 'src/test/fixtures/upgrades/processed-1.json']);

const textResult = await cli(ctx, ['thread', 'samples']);
expect(textResult.stdout).toContain('──── Categories (');

const jsonResult = await cli(ctx, ['thread', 'samples', '--json']);
const samples = JSON.parse(
jsonResult.stdout
) as WithContext<ThreadSamplesResult>;
const breakdown = samples.categoryBreakdown;

expect(breakdown.categories.length).toBeGreaterThan(0);
const summed = breakdown.categories.reduce(
(accum, category) => accum + category.samples,
0
);
expect(summed).toBe(breakdown.totalSamples);
});

it('function info reports running and self breakdowns', async () => {
await cli(ctx, ['load', 'src/test/fixtures/upgrades/processed-1.json']);

const result = await cli(ctx, ['function', 'info', 'f-5', '--json']);
const info = JSON.parse(result.stdout) as WithContext<FunctionInfoResult>;
const breakdowns = info.categoryBreakdown;

expect(breakdowns.threadHandle).toBe('t-2');
expect(breakdowns.running.samples).toBeGreaterThan(0);
expect(breakdowns.running.samples).toBeGreaterThanOrEqual(
breakdowns.self.samples
);
});

it('max-lines=0 is rejected instead of silently falling back to the default', async () => {
await cli(ctx, ['load', 'src/test/fixtures/upgrades/processed-1.json']);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`category breakdown formatting renders categories with their subcategories in thread samples 1`] = `
"[Thread: t-0 (Test Thread) | View: Full profile | Full: 1s]

Thread: Test Thread

──── Categories (100 running samples) ────

Layout █████████████████ 60 60.0%
Reflow █████████████ 45 45.0%
Other ████ 15 15.0%
JavaScript ███████████ 40 40.0%

Top Functions (by total time):
(For a call tree starting from these functions, use: profiler-cli thread samples-top-down)

f-0. A - total: 100 (100.0%)

Top Functions (by self time):
(For a call tree showing what calls these functions, use: profiler-cli thread samples-bottom-up)


Heaviest stack (0.0 samples, 0 frames):
(empty)
"
`;

exports[`category breakdown formatting renders the running and self breakdowns in function info 1`] = `
"[Thread: t-0 (Test Thread) | View: Full profile | Full: 1s]

Function f-12:
Full name: libxul.so!nsBlockFrame::Reflow
Short name: nsBlockFrame::Reflow
Is JS: false
Relevant for JS: false

──── Categories: running (100 samples, 50.0% of thread) ────

Layout █████████████████ 60 60.0%
Reflow █████████████ 45 45.0%
Other ████ 15 15.0%
JavaScript ███████████ 40 40.0%

──── Categories: self (0 samples, 0.0% of thread) ────

No self samples for this function in the current view."
`;
Loading
Loading