Skip to content
Merged
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
3 changes: 3 additions & 0 deletions profiler-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ profiler-cli filter push <filter-flag> # Push a sticky sample filter (see fi
profiler-cli filter pop [N] # Pop the last N filters (default: 1)
profiler-cli filter list # List active filters for current thread
profiler-cli filter clear # Remove all filters for current thread
profiler-cli sourcemap sources # List bundle sources eligible for a source map (src-N handles)
profiler-cli sourcemap apply <path> # Apply a .map file to de-minify JS stacks [--to <src-N>]
profiler-cli status # Show session status (selected thread, zoom ranges, filters)
profiler-cli stop # Stop current daemon
profiler-cli stop <id> # Stop a specific session
Expand Down Expand Up @@ -93,6 +95,7 @@ profiler-cli thread info --thread t-0 # View info for specific thread witho
| `--jank-limit <N>` | Max jank periods to show in `thread page-load` (default: 10, 0 = show all) |
| `--list` | Show a flat chronological list of individual markers (for `thread markers`) |
| `--all` | Show all threads in `profile info` (overrides default top-5 limit) |
| `--to <src-N>` | Target source for `sourcemap apply`, skipping auto-matching (from `sourcemap sources`) |
| `--session <id>` | Use a specific session instead of the current one |

## Sample Filter Flags
Expand Down
21 changes: 21 additions & 0 deletions profiler-cli/guide.txt
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ HANDLE SYSTEM
f-12 Function handles (from "thread samples", "thread functions")
c-0, c-1 Counter handles (from "counter list" or "profile info")
ts-6 Timestamp handles (named points in time, usable with "zoom push")
src-3 Source handles (from "sourcemap sources")

Handle lifetime and stability:

Expand All @@ -137,6 +138,8 @@ HANDLE SYSTEM
c-N counter list Yes -- direct index into the profile's counter
array; same profile always yields the same c-N
ts-N thread markers No -- position-based, session-scoped
src-N sourcemap Yes -- direct index into the profile's source
sources table; same profile always yields the same src-N
──────────────────────────────────────────────────────────────────────────

Function handles (f-N) can be saved and reused across sessions for the same profile.
Expand Down Expand Up @@ -249,6 +252,24 @@ COUNTERS
profiler-cli counter info c-0


SOURCE MAPS

If a JavaScript stack shows minified names (e.g. "a", "t.exports"), you can
de-minify it by applying a source map.

profiler-cli sourcemap sources List sources that carry a source map URL
profiler-cli sourcemap apply bundle.js.map Apply a .map, auto-matching a source

"apply" re-symbolicates the affected stacks in place. When the map could match
more than one source it exits non-zero and prints the candidates, so re-run with
one of their src-N handles:

profiler-cli sourcemap apply bundle.js.map --to src-3

Maps that carry "sourcesContent" also give you the original source, so
"function annotate f-N" can show it with per-line sample counts.


JSON OUTPUT

Add --json to any command to get structured JSON output, suitable for piping to jq
Expand Down
3 changes: 2 additions & 1 deletion profiler-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"pq": "dist/profiler-cli.js"
},
"files": [
"dist/profiler-cli.js"
"dist/profiler-cli.js",
"dist/mappings.wasm"
],
"engines": {
"node": ">= 24"
Expand Down
20 changes: 20 additions & 0 deletions profiler-cli/schemas.txt
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,23 @@ profiler-cli status --json
rootRange: { start, end },
filterStacks: [{ threadHandle, filters: FilterEntry[] }]
}

profiler-cli sourcemap sources --json
{
type: "sourcemap-sources",
sources: [{ sourceHandle, sourceIndex, filename, sourceMap: SourceMapLocation }],
context: SessionContext
}

SourceMapLocation = { kind: "url", url } | { kind: "inline", mediaType: string | null, byteLength }

profiler-cli sourcemap apply <path> --json
{
type: "sourcemap-applied" | "sourcemap-unchanged" | "sourcemap-ambiguous" | "sourcemap-error",
sourceHandle?,
filename?,
reason?: "multiple-matches" | "no-matches",
candidates?: [{ sourceHandle, sourceIndex, filename, sourceMap: SourceMapLocation }],
error?: "invalid-source-map" | "no-eligible-sources" | "symbolication-failed",
context: SessionContext
}
78 changes: 78 additions & 0 deletions profiler-cli/src/commands/sourcemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
* `profiler-cli sourcemap` command.
*/

import * as fs from 'fs';
import * as path from 'path';
import type { Command } from 'commander';
import { addGlobalOptions, runCommand } from './shared';
import { sendCommand } from '../client';
import { formatOutput } from '../output';

export function registerSourcemapCommand(
program: Command,
sessionDir: string
): void {
const sourcemap = program
.command('sourcemap')
.description('Apply source maps to de-minify JavaScript stacks');

addGlobalOptions(
sourcemap
.command('sources')
.description(
'List bundle sources eligible for a source map (src-N handles)'
)
).action(async (opts) => {
await runCommand(
sessionDir,
{ command: 'sourcemap', subcommand: 'sources' },
opts
);
});

addGlobalOptions(
sourcemap
.command('apply <path>')
.description('Apply a .map file, auto-matching it to a bundle source')
.option(
'--to <src-N>',
'Apply to this source instead of auto-matching (from "sourcemap sources")'
)
).action(async (mapPath: string, opts) => {
// Resolve to an absolute path here: the daemon runs with a different cwd,
// so it can only read the file by absolute path (mirrors the load flow).
const absolutePath = path.resolve(mapPath);
if (!fs.existsSync(absolutePath)) {
console.error(`Error: Source map file not found: ${absolutePath}`);
process.exitCode = 1;
return;
}

const result = await sendCommand(
sessionDir,
{
command: 'sourcemap',
subcommand: 'apply',
path: absolutePath,
to: opts.to,
},
opts.session
);
console.log(formatOutput(result, opts.json ?? false));

// `ambiguous` (needs disambiguation) and `error` are failures, so exit
// non-zero and let scripts branch on them. `applied` / `unchanged` exit 0.
if (
typeof result !== 'string' &&
(result.type === 'sourcemap-ambiguous' ||
result.type === 'sourcemap-error')
) {
process.exitCode = 1;
}
});
}
12 changes: 12 additions & 0 deletions profiler-cli/src/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,18 @@ export class Daemon {
default:
throw assertExhaustiveCheck(command);
}
case 'sourcemap':
switch (command.subcommand) {
case 'sources':
return this.querier.listSourceMapSources();
case 'apply':
if (!command.path) {
throw new Error('path is required for sourcemap apply');
}
return this.querier.applySourceMap(command.path, command.to);
default:
throw assertExhaustiveCheck(command);
}
case 'status':
return this.querier.getStatus();
default:
Expand Down
76 changes: 76 additions & 0 deletions profiler-cli/src/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ import type {
CounterSummary,
CounterListResult,
CounterInfoResult,
SourceEntry,
SourceMapLocation,
SourceMapSourcesResult,
ApplySourceMapResult,
} from './protocol';
import { assertExhaustiveCheck } from 'firefox-profiler/utils/types';
import { truncateFunctionName } from '../../src/profile-query/function-list';
import { describeSpec } from '../../src/profile-query/filter-stack';
import {
Expand Down Expand Up @@ -2092,3 +2097,74 @@ export function formatThreadSelectResult(
}
return `Selected ${count} threads: ${result.threadHandle} (${names})`;
}

function describeSourceMapLocation(sourceMap: SourceMapLocation): string {
switch (sourceMap.kind) {
case 'url':
return sourceMap.url;
case 'inline': {
const mediaType = sourceMap.mediaType ?? 'unknown media type';
return `inline data: URL, ${mediaType}, ${formatBytes(sourceMap.byteLength)}`;
}
default:
throw assertExhaustiveCheck(sourceMap);
}
}

/** One `src-N filename (sourceMapURL: ...)` line, shared by sources and ambiguous. */
function formatSourceEntry(entry: SourceEntry): string {
return ` ${entry.sourceHandle} ${entry.filename} (sourceMapURL: ${describeSourceMapLocation(entry.sourceMap)})`;
}

export function formatSourceMapSourcesResult(
result: WithContext<SourceMapSourcesResult>
): string {
const contextHeader = formatContextHeader(result.context);
if (result.sources.length === 0) {
return `${contextHeader}\n\nNo sources with a source map URL in this profile.`;
}
const lines = result.sources.map(formatSourceEntry);
return `${contextHeader}\n\nSources with source maps (${result.sources.length}):\n${lines.join('\n')}`;
}

const APPLY_SOURCE_MAP_AMBIGUOUS_HEADERS: Record<
Extract<ApplySourceMapResult, { type: 'sourcemap-ambiguous' }>['reason'],
string
> = {
'multiple-matches':
'The source map matches more than one source. Re-run with --to <src-N> to pick one:',
'no-matches':
'The source map does not match any source in this profile. Re-run with --to <src-N> to apply it to one of these anyway:',
};

const APPLY_SOURCE_MAP_ERROR_MESSAGES: Record<
Extract<ApplySourceMapResult, { type: 'sourcemap-error' }>['error'],
string
> = {
'invalid-source-map':
'The file is not a valid source map (invalid JSON, or not a source map).',
'no-eligible-sources':
'No sources in this profile carry a source map URL, so nothing to apply to.',
'symbolication-failed': 'Source map symbolication failed.',
};

export function formatApplySourceMapResult(
result: WithContext<ApplySourceMapResult>
): string {
switch (result.type) {
case 'sourcemap-applied':
return `Applied source map to ${result.filename} (${result.sourceHandle}). Re-run thread commands to see de-minified names.`;
case 'sourcemap-unchanged':
return `Source map applied to ${result.filename} (${result.sourceHandle}) but nothing changed (no stack positions mapped).`;
case 'sourcemap-ambiguous': {
const lines = result.candidates.map(formatSourceEntry);
return [APPLY_SOURCE_MAP_AMBIGUOUS_HEADERS[result.reason], ...lines].join(
'\n'
);
}
case 'sourcemap-error':
return `Error: ${APPLY_SOURCE_MAP_ERROR_MESSAGES[result.error]}`;
default:
throw assertExhaustiveCheck(result);
}
}
4 changes: 4 additions & 0 deletions profiler-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { registerFunctionCommand } from './commands/function';
import { registerCounterCommand } from './commands/counter';
import { registerZoomCommand } from './commands/zoom';
import { registerFilterCommand } from './commands/filter';
import { registerSourcemapCommand } from './commands/sourcemap';
import { registerSessionCommand } from './commands/session';

// Read session directory from environment (only place this is read)
Expand Down Expand Up @@ -90,6 +91,8 @@ Examples:
profiler-cli counter info c-0
profiler-cli zoom push 2.7,3.1
profiler-cli filter push --excludes-function f-184
profiler-cli sourcemap sources
profiler-cli sourcemap apply bundle.js.map
profiler-cli status
profiler-cli stop --all`
);
Expand Down Expand Up @@ -193,6 +196,7 @@ Examples:
registerCounterCommand(program, SESSION_DIR);
registerZoomCommand(program, SESSION_DIR);
registerFilterCommand(program, SESSION_DIR);
registerSourcemapCommand(program, SESSION_DIR);
registerSessionCommand(program, SESSION_DIR);

try {
Expand Down
9 changes: 9 additions & 0 deletions profiler-cli/src/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import {
formatThreadSelectResult,
formatCounterListResult,
formatCounterInfoResult,
formatSourceMapSourcesResult,
formatApplySourceMapResult,
} from './formatters';

/**
Expand Down Expand Up @@ -97,6 +99,13 @@ export function formatOutput(
return formatCounterListResult(result);
case 'counter-info':
return formatCounterInfoResult(result);
case 'sourcemap-sources':
return formatSourceMapSourcesResult(result);
case 'sourcemap-applied':
case 'sourcemap-unchanged':
case 'sourcemap-ambiguous':
case 'sourcemap-error':
return formatApplySourceMapResult(result);
default:
throw assertExhaustiveCheck(result);
}
Expand Down
18 changes: 17 additions & 1 deletion profiler-cli/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ export type {
CounterSummary,
CounterListResult,
CounterInfoResult,
SourceEntry,
SourceMapLocation,
SourceMapSourcesResult,
ApplySourceMapResult,
} from '../../src/profile-query/types';
export type { CallTreeCollectionOptions } from '../../src/profile-query/formatters/call-tree';

Expand Down Expand Up @@ -92,6 +96,8 @@ import type {
ThreadSelectResult,
CounterListResult,
CounterInfoResult,
SourceMapSourcesResult,
ApplySourceMapResult,
} from '../../src/profile-query/types';
import type { CallTreeCollectionOptions } from '../../src/profile-query/formatters/call-tree';

Expand Down Expand Up @@ -186,6 +192,14 @@ export type ClientCommand =
spec?: SampleFilterSpec;
count?: number;
}
| {
command: 'sourcemap';
subcommand: 'sources' | 'apply';
/** Absolute path to the `.map` file (resolved client-side). */
path?: string;
/** `src-N` handle of the target source; skips auto-matching when set. */
to?: string;
}
| { command: 'status' };

export type ServerResponse =
Expand Down Expand Up @@ -221,7 +235,9 @@ export type CommandResult =
| WithContext<ThreadPageLoadResult>
| WithContext<ThreadSelectResult>
| WithContext<CounterListResult>
| WithContext<CounterInfoResult>;
| WithContext<CounterInfoResult>
| WithContext<SourceMapSourcesResult>
| WithContext<ApplySourceMapResult>;

export interface SessionMetadata {
id: string;
Expand Down
Loading
Loading