Skip to content

Commit bc6b2d7

Browse files
author
Firefox Profiler [bot]
committed
πŸ”ƒ Daily sync: main -> l10n (December 12, 2025)
2 parents afbe217 + 2ce8ab6 commit bc6b2d7

File tree

8 files changed

+308518
-6
lines changed

8 files changed

+308518
-6
lines changed

β€Žsrc/actions/app.tsβ€Ž

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,12 @@ export function setupInitialUrlState(
150150
return;
151151
}
152152

153-
// Validate the initial URL state. We can't refresh on a from-file URL.
154-
if (urlState.dataSource === 'from-file') {
153+
// Validate the initial URL state. We can't refresh on from-file or
154+
// unpublished URLs.
155+
if (
156+
urlState.dataSource === 'from-file' ||
157+
urlState.dataSource === 'unpublished'
158+
) {
155159
urlState = null;
156160
}
157161

β€Žsrc/actions/receive-profile.tsβ€Ž

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1400,9 +1400,10 @@ export function retrieveProfileForRawUrl(
14001400
}
14011401

14021402
let dataSource = ensureIsValidDataSource(possibleDataSource);
1403-
if (dataSource === 'from-file') {
1404-
// Redirect to 'none' if `dataSource` is 'from-file' since initial urls can't
1405-
// be 'from-file' and needs to be redirected to home page.
1403+
// Redirect to 'none' for from-file and unpublished data sources since initial
1404+
// urls can't be 'from-file' or 'unpublished' and need to be redirected to
1405+
// home page. 'unpublished' is a transient state after profile deletion.
1406+
if (dataSource === 'from-file' || dataSource === 'unpublished') {
14061407
dataSource = 'none';
14071408
}
14081409
dispatch(setDataSource(dataSource));
@@ -1475,7 +1476,6 @@ export function retrieveProfileForRawUrl(
14751476
case 'uploaded-recordings':
14761477
case 'none':
14771478
case 'local':
1478-
case 'unpublished':
14791479
// There is no profile to download for these datasources.
14801480
break;
14811481
default:
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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+
import type { Milliseconds } from 'firefox-profiler/types/units';
6+
import type {
7+
CategoryList,
8+
IndexIntoCategoryList,
9+
IndexIntoFrameTable,
10+
IndexIntoFuncTable,
11+
IndexIntoStackTable,
12+
Profile,
13+
} from 'firefox-profiler/types/profile';
14+
import {
15+
getEmptyProfile,
16+
getEmptyThread,
17+
} from 'firefox-profiler/profile-logic/data-structures';
18+
import { GlobalDataCollector } from 'firefox-profiler/profile-logic/global-data-collector';
19+
import { ensureExists } from 'firefox-profiler/utils/types';
20+
21+
/**
22+
* The flamegraph.pl format is a plain text format where each line represents
23+
* a collapsed stack trace followed by a count. This format is commonly used
24+
* as input for flamegraph.pl and similar flame graph visualization tools.
25+
*
26+
* Format: "frame1;frame2;frame3 count"
27+
* Example: "java.lang.Thread.run;MyClass.method_[j];helper 42"
28+
*/
29+
export function isFlameGraphFormat(profile: string): boolean {
30+
if (profile.startsWith('{')) {
31+
// Make sure we don't accidentally match JSON.
32+
return false;
33+
}
34+
35+
const firstLine = profile.substring(0, profile.indexOf('\n'));
36+
return !!firstLine.match(/[^;]*(?:;[^;]*)* [0-9]+/);
37+
}
38+
39+
const CATEGORIES: CategoryList = [
40+
{ name: 'Java', color: 'yellow', subcategories: ['Other'] },
41+
{ name: 'Native', color: 'blue', subcategories: ['Other'] },
42+
];
43+
const JAVA_CATEGORY_INDEX: IndexIntoCategoryList = 0;
44+
const NATIVE_CATEGORY_INDEX: IndexIntoCategoryList = 1;
45+
46+
/**
47+
* Convert the flamegraph.pl input text format into the processed profile format.
48+
*/
49+
export function convertFlameGraphProfile(profileText: string): Profile {
50+
const profile = getEmptyProfile();
51+
profile.meta.product = 'Flamegraph';
52+
profile.meta.categories = CATEGORIES;
53+
54+
const globalDataCollector = new GlobalDataCollector();
55+
const stringTable = globalDataCollector.getStringTable();
56+
57+
const thread = getEmptyThread({
58+
name: 'Program',
59+
pid: '0',
60+
tid: 0,
61+
});
62+
63+
const { frameTable, funcTable, stackTable, samples } = thread;
64+
65+
// Maps to deduplicate stacks, frames, and functions.
66+
const stackMap = new Map<string, IndexIntoStackTable>();
67+
const frameMap = new Map<string, IndexIntoFrameTable>();
68+
const funcMap = new Map<string, IndexIntoFuncTable>();
69+
70+
function getOrCreateStack(
71+
frameIndex: IndexIntoFrameTable,
72+
prefix: IndexIntoStackTable | null
73+
): IndexIntoStackTable {
74+
const key = prefix === null ? `${frameIndex}` : `${frameIndex},${prefix}`;
75+
let stack = stackMap.get(key);
76+
if (stack === undefined) {
77+
stack = stackTable.length;
78+
stackTable.frame.push(frameIndex);
79+
stackTable.prefix.push(prefix);
80+
stackTable.length++;
81+
stackMap.set(key, stack);
82+
}
83+
return stack;
84+
}
85+
86+
function getOrCreateFrame(frameString: string): IndexIntoFrameTable {
87+
let frameIndex = frameMap.get(frameString);
88+
if (frameIndex !== undefined) {
89+
return frameIndex;
90+
}
91+
92+
// Clean the frame name by removing the _[j] suffix.
93+
const cleanedName = frameString.replace(/_\[j\]$/, '');
94+
95+
// Categorize frames based on common conventions in Java profilers.
96+
// _[j] suffix: Java frames (used by async-profiler and similar tools).
97+
let category: IndexIntoCategoryList = NATIVE_CATEGORY_INDEX;
98+
if (frameString.endsWith('_[j]')) {
99+
category = JAVA_CATEGORY_INDEX;
100+
}
101+
102+
// Create or get function.
103+
let funcIndex = funcMap.get(cleanedName);
104+
if (funcIndex === undefined) {
105+
funcIndex = funcTable.length;
106+
funcTable.isJS.push(false);
107+
funcTable.relevantForJS.push(false);
108+
funcTable.name.push(stringTable.indexForString(cleanedName));
109+
funcTable.resource.push(-1);
110+
funcTable.source.push(null);
111+
funcTable.lineNumber.push(null);
112+
funcTable.columnNumber.push(null);
113+
funcTable.length++;
114+
funcMap.set(cleanedName, funcIndex);
115+
}
116+
117+
// Create frame.
118+
frameIndex = frameTable.length;
119+
frameTable.address.push(-1);
120+
frameTable.inlineDepth.push(0);
121+
frameTable.category.push(category);
122+
frameTable.subcategory.push(0);
123+
frameTable.func.push(funcIndex);
124+
frameTable.nativeSymbol.push(null);
125+
frameTable.innerWindowID.push(null);
126+
frameTable.line.push(null);
127+
frameTable.column.push(null);
128+
frameTable.length++;
129+
frameMap.set(frameString, frameIndex);
130+
131+
return frameIndex;
132+
}
133+
134+
function addSample(time: Milliseconds, stackArray: string[]): void {
135+
const stack = stackArray.reduce<IndexIntoStackTable | null>(
136+
(prefix, stackFrame) => {
137+
const frameIndex = getOrCreateFrame(stackFrame);
138+
return getOrCreateStack(frameIndex, prefix);
139+
},
140+
null
141+
);
142+
samples.stack.push(stack);
143+
ensureExists(samples.time).push(time);
144+
samples.length++;
145+
}
146+
147+
// Parse the flamegraph text format.
148+
const lines = profileText.split('\n');
149+
150+
// The flamegraph.pl format doesn't include timestamp information.
151+
// Each line only contains a collapsed stack and a count of how many times
152+
// that stack was observed. To convert this to the profiler's sample-based
153+
// format, we generate fake sequential timestamps for each sample.
154+
let timeStamp: Milliseconds = 0;
155+
for (const line of lines) {
156+
if (line.trim() === '') {
157+
continue;
158+
}
159+
160+
const matched = line.match(/([^;]*(?:;[^;]*)*) ([0-9]+)/);
161+
if (!matched) {
162+
console.warn('unexpected line format', line);
163+
continue;
164+
}
165+
166+
const [, frames, duration] = matched;
167+
const stack = frames.split(';');
168+
let count = parseInt(duration, 10);
169+
while (count-- > 0) {
170+
addSample(timeStamp++, stack);
171+
}
172+
}
173+
174+
// Finalize the profile.
175+
profile.threads.push(thread);
176+
const { shared } = globalDataCollector.finish();
177+
profile.shared = shared;
178+
179+
return profile;
180+
}

β€Žsrc/profile-logic/process-profile.tsβ€Ž

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ import {
2323
isPerfScriptFormat,
2424
convertPerfScriptProfile,
2525
} from './import/linux-perf';
26+
import {
27+
isFlameGraphFormat,
28+
convertFlameGraphProfile,
29+
} from './import/flame-graph';
2630
import { isArtTraceFormat, convertArtTraceProfile } from './import/art-trace';
2731
import {
2832
PROCESSED_PROFILE_VERSION,
@@ -2006,6 +2010,8 @@ export async function unserializeProfileOfArbitraryFormat(
20062010
// The profile could be JSON or the output from `perf script`. Try `perf script` first.
20072011
if (isPerfScriptFormat(arbitraryFormat)) {
20082012
arbitraryFormat = convertPerfScriptProfile(arbitraryFormat);
2013+
} else if (isFlameGraphFormat(arbitraryFormat)) {
2014+
arbitraryFormat = convertFlameGraphProfile(arbitraryFormat);
20092015
} else {
20102016
// Try parsing as JSON.
20112017
arbitraryFormat = JSON.parse(arbitraryFormat);

0 commit comments

Comments
Β (0)