mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-07 08:54:42 +00:00
* perf(app): optimize wrapped measurements and checkpoint native layout experiment Compute full grapheme geometry only for final wrap fragments. Preserve shaped-text search behavior while bounding additive wrap probes. Keep the Android background layout experiment isolated from the normal diff renderer; native renderer integration and iOS bindings remain unfinished. * perf(server): reduce cold diff generation work Keep active diff reads owned across invalidation and wait for the initial watcher inventory before cold generation. Batch patch and blob reads while preserving per-file size isolation, bounded output, and daemon responsiveness. Avoid constructing fallback tokens when full-file highlighting already covers both sides. * test(server): exercise oversized highlighted diff rejection The single-line fixture now bypasses highlighting under the long-line guard, so it no longer reaches the structured payload limit. Use short dense lines to keep testing oversized rejection and continued socket usability. * fix(server): queue fresh diffs for forced reads A forced request following a repository mutation must not reuse an older active build. Invalidate that result and coalesce waiting callers onto one subsequent build. Give the cache a named object input and completed-value type. * test(server): share deferred diff loader setup * test(server): name completed diff cache setup * refactor(server): use aliases for new diff imports * fix(server): keep shipped diff imports resolvable by Node The TypeScript build preserves path aliases without a runtime resolver. Restore relative imports for shipped code and declarations, and correct the server import guideline that prompted the regression.
144 lines
4.9 KiB
TypeScript
144 lines
4.9 KiB
TypeScript
import type { ParsedDiffFile } from "@getpaseo/protocol/messages";
|
|
import { describe, expect, it } from "vitest";
|
|
import { buildDiffDocumentModel, measureFragments } from "./model";
|
|
import type { BuildDiffDocumentModelInput, TextMeasurer } from "./types";
|
|
|
|
/**
|
|
* A text backend that can only measure a whole string costs O(string length)
|
|
* per call, so measuring cumulative advances by re-shaping a growing prefix
|
|
* shapes n^2/2 characters for one line of n characters. That froze the renderer
|
|
* for ~10s on a 42-line CHANGELOG.md diff and for over 15 minutes on a diff
|
|
* containing a single 1.2M-character line.
|
|
*
|
|
* Cost is counted in *shaped characters* rather than calls, because that is the
|
|
* quantity that maps to renderer CPU time. The budgets below are the linear
|
|
* contract of the shared advance measurement in `text-measurement.ts`: work
|
|
* stays proportional to the text, with a constant bounded by the shaped-run cap.
|
|
*/
|
|
const SHAPED_RUN_LIMIT = 64;
|
|
|
|
function countingMeasurer(): { stats: { shapedCharacters: number }; measurer: TextMeasurer } {
|
|
const stats = { shapedCharacters: 0 };
|
|
return {
|
|
stats,
|
|
measurer: {
|
|
measure(text) {
|
|
stats.shapedCharacters += text.length;
|
|
return text.length * 10;
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Ligature-bearing source, so measurement takes the shaping path rather than the cache. */
|
|
function codeLine(length: number): string {
|
|
const unit = "const value = (a, b) => a === b ? 1 : 0; ";
|
|
return unit.repeat(Math.ceil(length / unit.length)).slice(0, length);
|
|
}
|
|
|
|
function diffFile(lines: readonly string[]): ParsedDiffFile {
|
|
return {
|
|
path: "src/a.ts",
|
|
isNew: false,
|
|
isDeleted: false,
|
|
additions: lines.length,
|
|
deletions: 0,
|
|
hunks: [
|
|
{
|
|
oldStart: 1,
|
|
oldCount: 0,
|
|
newStart: 1,
|
|
newCount: lines.length,
|
|
lines: lines.map((content) => ({ type: "add" as const, content })),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
function shapedCharactersFor(options: { lineCount: number; lineLength: number }): number {
|
|
const { stats, measurer } = countingMeasurer();
|
|
const line = codeLine(options.lineLength);
|
|
const input: BuildDiffDocumentModelInput = {
|
|
files: [diffFile(Array.from({ length: options.lineCount }, () => line))],
|
|
collapsedFilePaths: new Set(),
|
|
layout: "unified",
|
|
wrapLines: false,
|
|
viewportWidth: 1200,
|
|
typography: { family: "monospace", size: 12, lineHeight: 18 },
|
|
measureText: measurer,
|
|
palette: {
|
|
surface: "#000",
|
|
headerSurface: "#111",
|
|
border: "#222",
|
|
foreground: "#fff",
|
|
foregroundMuted: "#aaa",
|
|
addition: "green",
|
|
deletion: "red",
|
|
additionBackground: "#010",
|
|
deletionBackground: "#100",
|
|
emptyBackground: "#111",
|
|
selection: "blue",
|
|
headerActiveSurface: "#222",
|
|
headerBorder: "#333",
|
|
statusSuccess: "green",
|
|
statusDanger: "red",
|
|
statusWarning: "orange",
|
|
syntax: {},
|
|
},
|
|
labels: { binary: "Binary", tooLarge: "Too large" },
|
|
};
|
|
buildDiffDocumentModel(input);
|
|
return stats.shapedCharacters;
|
|
}
|
|
|
|
describe("diff document measurement cost", () => {
|
|
it("keeps additive wrapped search work proportional to long source lines", () => {
|
|
function measuredGraphemes(length: number): number {
|
|
let count = 0;
|
|
measureFragments({
|
|
text: "a".repeat(length),
|
|
availableWidth: 200,
|
|
wrapLines: true,
|
|
lineHeight: 18,
|
|
measureText: {
|
|
measure: (text) => text.length * 10,
|
|
measureWidth(graphemes) {
|
|
count += graphemes.length;
|
|
return graphemes.length * 10;
|
|
},
|
|
measureAdvances(graphemes) {
|
|
count += graphemes.length;
|
|
return graphemes.map((_, index) => (index + 1) * 10);
|
|
},
|
|
},
|
|
});
|
|
return count;
|
|
}
|
|
const short = measuredGraphemes(100);
|
|
const long = measuredGraphemes(1600);
|
|
expect(long / short).toBeLessThan(32);
|
|
});
|
|
|
|
it("shapes a bounded number of characters per character of text", () => {
|
|
// The shape of the CHANGELOG.md working diff that froze the renderer: 42
|
|
// lines, longest 597 characters.
|
|
const lineCount = 42;
|
|
const lineLength = 597;
|
|
const shaped = shapedCharactersFor({ lineCount, lineLength });
|
|
expect(shaped).toBeLessThanOrEqual(lineCount * lineLength * SHAPED_RUN_LIMIT);
|
|
});
|
|
|
|
it("does not grow super-linearly with line length", () => {
|
|
const short = shapedCharactersFor({ lineCount: 1, lineLength: 50 });
|
|
const long = shapedCharactersFor({ lineCount: 1, lineLength: 800 });
|
|
// 16x the characters must not cost more than ~16x the shaping work.
|
|
expect(long / short).toBeLessThan(32);
|
|
});
|
|
|
|
it("grows no faster than linearly with line count", () => {
|
|
const ten = shapedCharactersFor({ lineCount: 10, lineLength: 100 });
|
|
const hundred = shapedCharactersFor({ lineCount: 100, lineLength: 100 });
|
|
expect(hundred).toBeLessThanOrEqual(ten * 10);
|
|
});
|
|
});
|