Skip to content

Commit e3fe59b

Browse files
authored
Deploy March 24, 2026 (#5910)
Changes: [Nazım Can Altınova] Display relative sources correctly in the source view using startLine from the source table (#5902) [dependabot[bot]] Bump flatted from 3.3.3 to 3.4.2 (#5904) [Markus Stange] Construct new stack tables with the correct column lengths. (#5906)
2 parents f37398f + 33c9f6a commit e3fe59b

File tree

8 files changed

+254
-62
lines changed

8 files changed

+254
-62
lines changed

src/components/app/BottomBox.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ import {
3030
getSourceViewCode,
3131
getAssemblyViewCode,
3232
} from 'firefox-profiler/selectors/code';
33-
import { getSourceViewFile } from 'firefox-profiler/selectors/profile';
33+
import {
34+
getSourceViewFile,
35+
getSourceViewStartLine,
36+
} from 'firefox-profiler/selectors/profile';
3437
import explicitConnect from 'firefox-profiler/utils/connect';
3538

3639
import type { ConnectedProps } from 'firefox-profiler/utils/connect';
@@ -55,6 +58,7 @@ type StateProps = {
5558
readonly sourceViewScrollGeneration: number;
5659
readonly sourceViewScrollToLineNumber?: number;
5760
readonly sourceViewHighlightedLine: number | null;
61+
readonly sourceViewStartLine: number;
5862
readonly globalLineTimings: LineTimings;
5963
readonly assemblyViewIsOpen: boolean;
6064
readonly assemblyViewNativeSymbol: NativeSymbolInfo | null;
@@ -161,6 +165,7 @@ class BottomBoxImpl extends React.PureComponent<Props> {
161165
sourceViewScrollGeneration,
162166
sourceViewScrollToLineNumber,
163167
sourceViewHighlightedLine,
168+
sourceViewStartLine,
164169
assemblyViewIsOpen,
165170
assemblyViewScrollGeneration,
166171
assemblyViewScrollToInstructionAddress,
@@ -233,6 +238,7 @@ class BottomBoxImpl extends React.PureComponent<Props> {
233238
scrollGeneration={sourceViewScrollGeneration}
234239
scrollToLineNumber={sourceViewScrollToLineNumber}
235240
highlightedLine={sourceViewHighlightedLine}
241+
startLine={sourceViewStartLine}
236242
ref={this._sourceView}
237243
/>
238244
) : null}
@@ -303,6 +309,7 @@ export const BottomBox = explicitConnect<{}, StateProps, DispatchProps>({
303309
sourceViewScrollGeneration: getSourceViewScrollGeneration(state),
304310
sourceViewScrollToLineNumber: getSourceViewScrollToLineNumber(state),
305311
sourceViewHighlightedLine: getSourceViewHighlightedLine(state),
312+
sourceViewStartLine: getSourceViewStartLine(state),
306313
assemblyViewNativeSymbol: getAssemblyViewNativeSymbol(state),
307314
assemblyViewCode: getAssemblyViewCode(state),
308315
globalAddressTimings:

src/components/shared/SourceView-codemirror.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ const languageConf = new Compartment();
4141
// This "compartment" allows us to swap the highlighted line when it changes.
4242
const highlightedLineConf = new Compartment();
4343

44+
// This "compartment" allows us to reconfigure the line number formatter when
45+
// startLine changes.
46+
const lineNumbersConf = new Compartment();
47+
4448
// Detect the right language based on the file extension.
4549
function _languageExtForPath(
4650
path: string | null
@@ -85,6 +89,16 @@ const codeViewerExtension = [
8589
EditorView.contentAttributes.of({ tabindex: '0' }),
8690
];
8791

92+
// Creates a lineNumbers extension that displays line numbers offset by startLine - 1.
93+
function _lineNumbersForStartLine(startLine: number) {
94+
if (startLine <= 1) {
95+
return lineNumbers();
96+
}
97+
return lineNumbers({
98+
formatNumber: (n) => String(n + startLine - 1),
99+
});
100+
}
101+
88102
export class SourceViewEditor {
89103
_view: EditorView;
90104

@@ -94,13 +108,14 @@ export class SourceViewEditor {
94108
path: string,
95109
timings: LineTimings,
96110
highlightedLine: number | null,
111+
startLine: number,
97112
domParent: Element
98113
) {
99114
let state = EditorState.create({
100115
doc: initialText,
101116
extensions: [
102117
timingsExtension,
103-
lineNumbers(),
118+
lineNumbersConf.of(_lineNumbersForStartLine(startLine)),
104119
languageConf.of(_languageExtForPath(path)),
105120
highlightedLineConf.of(createHighlightedLineExtension(highlightedLine)),
106121
syntaxHighlighting(classHighlighter),
@@ -152,6 +167,13 @@ export class SourceViewEditor {
152167
});
153168
}
154169

170+
setStartLine(startLine: number) {
171+
// Reconfigure the line numbers extension to display the new offset.
172+
this._view.dispatch({
173+
effects: lineNumbersConf.reconfigure(_lineNumbersForStartLine(startLine)),
174+
});
175+
}
176+
155177
scrollToLine(lineNumber: number) {
156178
// Clamp the line number to the document's line count.
157179
lineNumber = clamp(lineNumber, 1, this._view.state.doc.lines);

src/components/shared/SourceView.tsx

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,33 @@ type SourceViewProps = {
4343
readonly scrollGeneration: number;
4444
readonly scrollToLineNumber?: number;
4545
readonly highlightedLine: number | null;
46+
// 1-based line number for the start of the source in the full document.
47+
readonly startLine: number;
4648
};
4749

4850
let editorModulePromise: Promise<any> | null = null;
4951

52+
// Remap absolute line timings to document-relative (1-based) line numbers.
53+
// Timings keys are absolute line numbers; CodeMirror uses doc-relative line numbers.
54+
function remapTimingsToRelative(
55+
timings: LineTimings,
56+
startLine: number
57+
): LineTimings {
58+
if (startLine <= 1) {
59+
return timings;
60+
}
61+
const offset = startLine - 1;
62+
const totalLineHits = new Map<number, number>();
63+
for (const [line, hits] of timings.totalLineHits) {
64+
totalLineHits.set(line - offset, hits);
65+
}
66+
const selfLineHits = new Map<number, number>();
67+
for (const [line, hits] of timings.selfLineHits) {
68+
selfLineHits.set(line - offset, hits);
69+
}
70+
return { totalLineHits, selfLineHits };
71+
}
72+
5073
export class SourceView extends React.PureComponent<SourceViewProps> {
5174
_ref = React.createRef<HTMLDivElement>();
5275
_editor: SourceViewEditor | null = null;
@@ -57,8 +80,28 @@ export class SourceView extends React.PureComponent<SourceViewProps> {
5780
}
5881
}
5982

83+
_toRelativeLine(absoluteLine: number): number {
84+
return Math.max(1, absoluteLine - (this.props.startLine - 1));
85+
}
86+
87+
// Convert an absolute scroll-to line number to a doc-relative line number.
88+
_getRelativeScrollToLineNumber(): number | undefined {
89+
const { scrollToLineNumber } = this.props;
90+
return scrollToLineNumber === undefined
91+
? undefined
92+
: this._toRelativeLine(scrollToLineNumber);
93+
}
94+
95+
// Convert an absolute highlighted line number to a doc-relative line number.
96+
_getRelativeHighlightedLine(): number | null {
97+
const { highlightedLine } = this.props;
98+
return highlightedLine === null
99+
? null
100+
: this._toRelativeLine(highlightedLine);
101+
}
102+
60103
_getMaxLineNumber() {
61-
const { sourceCode, timings } = this.props;
104+
const { sourceCode, timings, startLine } = this.props;
62105
const sourceLines = sourceCode.split('\n');
63106
let maxLineNumber = sourceLines.length;
64107
if (maxLineNumber <= 1) {
@@ -69,7 +112,9 @@ export class SourceView extends React.PureComponent<SourceViewProps> {
69112
// isn't too constrained - if the last known line is chosen as the "hot spot",
70113
// this extra space allows us to display it in the top half of the viewport,
71114
// if the viewport is small enough.
72-
maxLineNumber = Math.max(1, ...timings.totalLineHits.keys()) + 10;
115+
// timings keys are absolute line numbers; convert to doc-relative count.
116+
const maxAbsoluteLine = Math.max(1, ...timings.totalLineHits.keys());
117+
maxLineNumber = Math.max(1, maxAbsoluteLine - startLine + 1) + 10;
73118
}
74119
return maxLineNumber;
75120
}
@@ -108,14 +153,16 @@ export class SourceView extends React.PureComponent<SourceViewProps> {
108153
const editor = new SourceViewEditor(
109154
this._getSourceCodeOrFallback(),
110155
this.props.filePath,
111-
this.props.timings,
112-
this.props.highlightedLine,
156+
remapTimingsToRelative(this.props.timings, this.props.startLine),
157+
this._getRelativeHighlightedLine(),
158+
this.props.startLine,
113159
domParent
114160
);
115161
this._editor = editor;
116162
// If an explicit line number is provided, scroll to it. Otherwise, scroll to the hotspot.
117-
if (this.props.scrollToLineNumber !== undefined) {
118-
this._scrollToLine(Math.max(1, this.props.scrollToLineNumber - 5));
163+
const relativeScrollTo = this._getRelativeScrollToLineNumber();
164+
if (relativeScrollTo !== undefined) {
165+
this._scrollToLine(Math.max(1, relativeScrollTo - 5));
119166
}
120167
})();
121168
}
@@ -131,6 +178,11 @@ export class SourceView extends React.PureComponent<SourceViewProps> {
131178
this._editor.updateLanguageForFilePath(this.props.filePath);
132179
}
133180

181+
const startLineChanged = this.props.startLine !== prevProps.startLine;
182+
if (startLineChanged) {
183+
this._editor.setStartLine(this.props.startLine);
184+
}
185+
134186
let contentsChanged = false;
135187
if (
136188
this.props.sourceCode !== prevProps.sourceCode ||
@@ -147,17 +199,23 @@ export class SourceView extends React.PureComponent<SourceViewProps> {
147199
this.props.scrollGeneration !== prevProps.scrollGeneration
148200
) {
149201
// If an explicit line number is provided, scroll to it. Otherwise, scroll to the hotspot.
150-
if (this.props.scrollToLineNumber !== undefined) {
151-
this._scrollToLine(Math.max(1, this.props.scrollToLineNumber - 5));
202+
const relativeScrollTo = this._getRelativeScrollToLineNumber();
203+
if (relativeScrollTo !== undefined) {
204+
this._scrollToLine(Math.max(1, relativeScrollTo - 5));
152205
}
153206
}
154207

155-
if (this.props.timings !== prevProps.timings) {
156-
this._editor.setTimings(this.props.timings);
208+
if (this.props.timings !== prevProps.timings || startLineChanged) {
209+
this._editor.setTimings(
210+
remapTimingsToRelative(this.props.timings, this.props.startLine)
211+
);
157212
}
158213

159-
if (this.props.highlightedLine !== prevProps.highlightedLine) {
160-
this._editor.setHighlightedLine(this.props.highlightedLine);
214+
if (
215+
this.props.highlightedLine !== prevProps.highlightedLine ||
216+
startLineChanged
217+
) {
218+
this._editor.setHighlightedLine(this._getRelativeHighlightedLine());
161219
}
162220
}
163221
}

src/profile-logic/data-structures.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import type {
1313
RawSamplesTable,
1414
FrameTable,
1515
RawStackTable,
16-
StackTable,
1716
FuncTable,
1817
RawMarkerTable,
1918
JsAllocationsTable,
@@ -33,20 +32,6 @@ import type {
3332
* This module collects all of the creation of new empty profile data structures.
3433
*/
3534

36-
export function getEmptyStackTable(): StackTable {
37-
return {
38-
// Important!
39-
// If modifying this structure, please update all callers of this function to ensure
40-
// that they are pushing on correctly to the data structure. These pushes may not
41-
// be caught by the type system.
42-
frame: [],
43-
prefix: [],
44-
category: new Uint8Array(),
45-
subcategory: new Uint8Array(),
46-
length: 0,
47-
};
48-
}
49-
5035
export function getEmptySamplesTable(): RawSamplesTable {
5136
return {
5237
// Important!

src/profile-logic/profile-data.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2714,6 +2714,34 @@ export function createThreadFromDerivedTables(
27142714
return thread;
27152715
}
27162716

2717+
/**
2718+
* Throws if the column lengths of a StackTable don't match stackTable.length.
2719+
* Call this after constructing a new StackTable to catch bugs early.
2720+
*/
2721+
export function validateStackTableShape(stackTable: StackTable): void {
2722+
const { length, frame, prefix, category, subcategory } = stackTable;
2723+
if (frame.length !== length) {
2724+
throw new Error(
2725+
`StackTable frame column length ${frame.length} does not match stackTable.length ${length}`
2726+
);
2727+
}
2728+
if (prefix.length !== length) {
2729+
throw new Error(
2730+
`StackTable prefix column length ${prefix.length} does not match stackTable.length ${length}`
2731+
);
2732+
}
2733+
if (category.length !== length) {
2734+
throw new Error(
2735+
`StackTable category column length ${category.length} does not match stackTable.length ${length}`
2736+
);
2737+
}
2738+
if (subcategory.length !== length) {
2739+
throw new Error(
2740+
`StackTable subcategory column length ${subcategory.length} does not match stackTable.length ${length}`
2741+
);
2742+
}
2743+
}
2744+
27172745
/**
27182746
* Sometimes we want to update the stacks for a thread, for instance while searching
27192747
* for a text string, or doing a call tree transformation. This function abstracts
@@ -2738,6 +2766,7 @@ export function updateThreadStacksByGeneratingNewStackColumns(
27382766
markerData: Array<MarkerPayload | null>
27392767
) => Array<MarkerPayload | null>
27402768
): Thread {
2769+
validateStackTableShape(newStackTable);
27412770
const { jsAllocations, nativeAllocations, samples, markers } = thread;
27422771

27432772
const newSamples = {

0 commit comments

Comments
 (0)