Reduce stalls when opening large diffs (#4574)

* perf(app): reduce avoidable work when opening large diffs

Large snapshots were recursively reconstructed for reference stability, while native scroll worklets captured complete file models. Preserve identity at the data producers and prepare text for the visible window without rebuilding unchanged files.

* test(app): await commit diff query completion without polling

* chore(app): remove profiling artifacts from the source tree
This commit is contained in:
Mohamed Boudra
2026-09-09 16:05:44 +02:00
committed by GitHub
parent 0b3d82a469
commit d311797653
21 changed files with 816 additions and 163 deletions
@@ -5,12 +5,11 @@ import {
type SkFont,
type SkParagraph,
} from "@shopify/react-native-skia";
import { fragmentTextForRange } from "./model";
import { fragmentTextForRange, visibleRowRange } from "./model";
import { codeTextColor } from "./palette";
import {
createCachedAsciiTextMetrics,
createChunkedAdvanceMeasurer,
createFallbackAwareTextMeasurer,
requiresShaping,
type CachedAsciiTextMetrics,
} from "./text-measurement";
@@ -86,7 +85,8 @@ export function shapeNativeHeaderText(input: {
export interface NativeTextLayoutStore {
font: SkFont;
asciiMetrics: CachedAsciiTextMetrics;
paragraphsByCell: WeakMap<DiffCell, Array<SkParagraph | null>>;
paragraphsByCell: Map<DiffCell, Array<SkParagraph | null>>;
textStyles: Map<string, ReturnType<typeof textStyle>>;
ownedParagraphs: Set<SkParagraph>;
families: string[];
fontSize: number;
@@ -97,6 +97,8 @@ export interface NativeTextLayoutStore {
export function disposeNativeTextLayout(layout: NativeTextLayoutStore): void {
for (const paragraph of layout.ownedParagraphs) paragraph.dispose();
layout.ownedParagraphs.clear();
layout.paragraphsByCell.clear();
layout.textStyles.clear();
layout.font.dispose();
}
@@ -114,7 +116,8 @@ export function createNativeTextLayoutStore(input: {
glyphIds: (text) => font.getGlyphIDs(text),
measure: (text) => font.getTextWidth(text),
}),
paragraphsByCell: new WeakMap(),
paragraphsByCell: new Map(),
textStyles: new Map(),
ownedParagraphs: new Set(),
families,
fontSize: input.fontSize,
@@ -126,30 +129,64 @@ export function createNativeTextLayoutStore(input: {
export function prepareNativeTextLayout(
store: NativeTextLayoutStore,
model: DiffDocumentModel,
window?: { top: number; height: number },
): NativeTextLayout {
const paragraphs = model.rows.map((row) => {
if (row.kind !== "line") return [];
return row.cells.map((cell) => {
const range = window
? visibleRowRange(model.rows, window.top, window.height)
: { start: 0, end: model.rows.length };
const retainedCells = new Set<DiffCell>();
const retainedParagraphs = new Set<SkParagraph>();
const paragraphs: NativeTextLayout["paragraphs"] = [];
paragraphs.length = model.rows.length;
for (let index = range.start; index < range.end; index++) {
const row = model.rows[index]!;
if (row.kind !== "line") continue;
paragraphs[index] = row.cells.map((cell) => {
if (!cell) return [];
retainedCells.add(cell);
const cached = store.paragraphsByCell.get(cell);
if (cached) return cached;
const cellParagraphs = cell.fragments.map((fragment) => {
if (!requiresRetainedParagraph(fragment.text, store.asciiMetrics)) return null;
const paragraph = createFragmentParagraph({
cell,
fragment,
families: store.families,
fontSize: store.fontSize,
lineHeight: store.lineHeight,
palette: store.palette,
});
const start = window ? Math.max(0, Math.floor((window.top - row.top) / model.lineHeight)) : 0;
const end = window
? Math.min(
cell.fragments.length,
Math.ceil((window.top + window.height - row.top) / model.lineHeight),
)
: cell.fragments.length;
const cellParagraphs: Array<SkParagraph | null> = [];
cellParagraphs.length = cell.fragments.length;
for (let fragmentIndex = start; fragmentIndex < end; fragmentIndex++) {
const fragment = cell.fragments[fragmentIndex]!;
if (!requiresRetainedParagraph(fragment.text, store.asciiMetrics)) {
cellParagraphs[fragmentIndex] = null;
continue;
}
const paragraph =
cached?.[fragmentIndex] ??
createFragmentParagraph({
cell,
fragment,
families: store.families,
fontSize: store.fontSize,
lineHeight: store.lineHeight,
palette: store.palette,
textStyles: store.textStyles,
});
store.ownedParagraphs.add(paragraph);
return paragraph;
});
retainedParagraphs.add(paragraph);
cellParagraphs[fragmentIndex] = paragraph;
}
store.paragraphsByCell.set(cell, cellParagraphs);
return cellParagraphs;
});
});
}
for (const paragraph of store.ownedParagraphs) {
if (retainedParagraphs.has(paragraph)) continue;
paragraph.dispose();
store.ownedParagraphs.delete(paragraph);
}
for (const cell of store.paragraphsByCell.keys()) {
if (!retainedCells.has(cell)) store.paragraphsByCell.delete(cell);
}
return { font: store.font, paragraphs };
}
@@ -164,23 +201,25 @@ export function createNativeTextMeasurer(input: {
measure: (text: string) => font.getTextWidth(text),
};
const asciiMetrics = createCachedAsciiTextMetrics(primary);
const fallbackAware = createFallbackAwareTextMeasurer({
primary,
measureWithSystemFallback(text) {
const paragraph = createParagraph({
text,
families,
fontSize: input.fontSize,
lineHeight: Math.round(input.fontSize * 1.5),
color: Skia.Color("black"),
});
const width = paragraph.getLongestLine();
paragraph.dispose();
return width;
},
});
const black = Skia.Color("black");
const measureParagraph = (text: string) => {
const paragraph = createParagraph({
text,
families,
fontSize: input.fontSize,
lineHeight: Math.round(input.fontSize * 1.5),
color: black,
});
const width = paragraph.getLongestLine();
paragraph.dispose();
return width;
};
return {
...fallbackAware,
measure(text) {
return requiresRetainedParagraph(text, asciiMetrics)
? measureParagraph(text)
: asciiMetrics.measure(text);
},
measureAdvances: createChunkedAdvanceMeasurer({
requiresShaping: (text) => requiresRetainedParagraph(text, asciiMetrics),
measureAdditive: (graphemes) => asciiMetrics.measureAdvances(graphemes),
@@ -190,7 +229,7 @@ export function createNativeTextMeasurer(input: {
families,
fontSize: input.fontSize,
lineHeight: Math.round(input.fontSize * 1.5),
color: Skia.Color("black"),
color: black,
});
let end = 0;
const advances = graphemes.map((grapheme) => {
@@ -219,23 +258,21 @@ function createFragmentParagraph(input: {
fontSize: number;
lineHeight: number;
palette: DiffPalette;
textStyles: NativeTextLayoutStore["textStyles"];
}): SkParagraph {
const builder = Skia.ParagraphBuilder.Make(
paragraphStyle(input.families, input.fontSize, input.lineHeight),
);
if (input.cell.tokens.length === 0 || input.cell.type === "header") {
const color = codeTextColor(input.cell, input.palette);
builder
.pushStyle(textStyle(input.families, input.fontSize, color))
.addText(input.fragment.text)
.pop();
builder.pushStyle(retainedTextStyle(input, color)).addText(input.fragment.text).pop();
} else {
for (const run of input.cell.tokens) {
const start = Math.max(input.fragment.start, run.start);
const end = Math.min(input.fragment.end, run.end);
if (end <= start) continue;
builder
.pushStyle(textStyle(input.families, input.fontSize, run.color))
.pushStyle(retainedTextStyle(input, run.color))
.addText(fragmentTextForRange(input.fragment, start, end))
.pop();
}
@@ -281,6 +318,18 @@ function paragraphStyle(families: string[], fontSize: number, lineHeight: number
};
}
function retainedTextStyle(
input: { families: string[]; fontSize: number; textStyles: NativeTextLayoutStore["textStyles"] },
color: string,
) {
let style = input.textStyles.get(color);
if (!style) {
style = textStyle(input.families, input.fontSize, color);
input.textStyles.set(color, style);
}
return style;
}
function textStyle(families: string[], fontSize: number, color: string) {
return { fontFamilies: families, fontSize, color: Skia.Color(color) };
}