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
2 changes: 1 addition & 1 deletion apps/benchmarks/scripts/run-presentation-demo-probe.mts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ try {
});
page.on('pageerror', (error) => consoleProblems.push(`pageerror: ${error.message}`));
await page.goto(
`http://127.0.0.1:${String(address.port)}/presentation?mode=benchmark&technique=mtsdf&backend=webgpu&delivery=baked&dpr=1&font=inter&workload=off-axis-3d`,
`http://127.0.0.1:${String(address.port)}/presentation?mode=benchmark&technique=mtsdf&backend=webgpu&delivery=baked&dpr=2&font=inter&workload=off-axis-3d`,
{ waitUntil: 'domcontentloaded' },
);

Expand Down
171 changes: 153 additions & 18 deletions apps/benchmarks/scripts/run-presentation-workload-probe.mts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const presentationSamplePeriodMs = 300;
let browser: Browser | undefined;
try {
browser = await launchProjectChromium({
headless: false,
headless: true,
args: ['--enable-gpu', '--ignore-gpu-blocklist', '--enable-unsafe-webgpu'],
});
const page = await browser.newPage({ viewport: { width: 1_280, height: 720 } });
Expand All @@ -77,7 +77,7 @@ try {
});
page.on('pageerror', (error) => consoleProblems.push(`pageerror: ${error.message}`));
await page.goto(
`http://127.0.0.1:${String(address.port)}/presentation?mode=benchmark&technique=${technique}&backend=webgpu&delivery=baked&dpr=1&font=inter&workload=text-ladder`,
`http://127.0.0.1:${String(address.port)}/presentation?mode=benchmark&technique=${technique}&backend=webgpu&delivery=baked&dpr=2&font=inter&workload=text-ladder`,
{ waitUntil: 'domcontentloaded' },
);
const workloadControl = page.getByLabel('Live workload', { exact: true });
Expand All @@ -95,9 +95,58 @@ try {
);
});
await page.evaluate(() => {
const scope = globalThis as typeof globalThis & { presentationProbeCanvas: Element | undefined };
scope.presentationProbeCanvas =
document.querySelector('canvas[data-configured-renderer-active="true"]') ?? undefined;
const canvas = document.querySelector<HTMLCanvasElement>('canvas[data-configured-renderer-active="true"]');
const scope = globalThis as typeof globalThis & {
presentationProbeCanvas: HTMLCanvasElement | undefined;
presentationProbeCanvasEvidence: CanvasEvidence | undefined;
presentationProbeCanvasEvents: string[];
presentationProbeInputEvents: string[];
};
scope.presentationProbeCanvas = canvas ?? undefined;
if (canvas === null) {
scope.presentationProbeCanvasEvidence = undefined;
} else {
const bounds = canvas.getBoundingClientRect();
scope.presentationProbeCanvasEvidence = {
backingHeight: canvas.height,
backingWidth: canvas.width,
connected: canvas.isConnected,
cssHeight: bounds.height,
cssWidth: bounds.width,
};
}
scope.presentationProbeCanvasEvents = [];
scope.presentationProbeInputEvents = [];
document.addEventListener(
'click',
(event) => {
const target = event.target instanceof Element ? event.target : undefined;
scope.presentationProbeInputEvents.push(
`${performance.now().toFixed(1)}ms click ${target?.getAttribute('aria-label') ?? target?.textContent?.trim().slice(0, 80) ?? 'unknown'}`,
);
},
{ capture: true },
);
if (canvas !== null) {
new MutationObserver((records) => {
for (const record of records) {
scope.presentationProbeCanvasEvents.push(
`${performance.now().toFixed(1)}ms attribute ${record.attributeName ?? 'unknown'} ${record.oldValue ?? 'null'} -> ${canvas.getAttribute(record.attributeName ?? '') ?? 'null'}`,
);
}
}).observe(canvas, { attributeFilter: ['height', 'width'], attributeOldValue: true, attributes: true });
new ResizeObserver(() => {
const bounds = canvas.getBoundingClientRect();
scope.presentationProbeCanvasEvents.push(
`${performance.now().toFixed(1)}ms css ${String(bounds.width)}x${String(bounds.height)}`,
);
}).observe(canvas);
new MutationObserver(() => {
if (!canvas.isConnected) {
scope.presentationProbeCanvasEvents.push(`${performance.now().toFixed(1)}ms disconnected`);
}
}).observe(document.documentElement, { childList: true, subtree: true });
}
});

for (const workload of workloads) {
Expand Down Expand Up @@ -139,6 +188,37 @@ try {
throw new Error(`${workload.id} did not retain exactly one configured renderer`);
}
}
if (technique === 'bitmap') {
const dprMenu = page.getByRole('button', { name: 'Device pixel ratio: 2×', exact: true });
await dprMenu.click();
await page
.getByRole('button', { name: '1×', exact: true })
.evaluate((element) => (element as HTMLButtonElement).click());
await page.waitForFunction(() => new URLSearchParams(location.search).get('dpr') === '1');
await assertCanvasHandoff(page, 'DPR 2→1');
await page.getByRole('button', { name: 'Device pixel ratio: 1×', exact: true }).click();
await page
.getByRole('button', { name: '2×', exact: true })
.evaluate((element) => (element as HTMLButtonElement).click());
await page.waitForFunction(() => new URLSearchParams(location.search).get('dpr') === '2');
await assertCanvasHandoff(page, 'DPR 1→2');

const mtsdfControl = page.getByRole('button', { name: 'MSDF', exact: true });
await mtsdfControl.evaluate((element) => (element as HTMLButtonElement).click());
await page.waitForFunction(() => {
const viewport = document.querySelector<HTMLElement>('[data-testid="comparison-live-viewport"]');
return viewport?.dataset.technique === 'mtsdf' && viewport.dataset.presentationPending === 'false';
});
await assertCanvasHandoff(page, 'Bitmap→MTSDF');
await page
.getByRole('button', { name: 'Bitmap', exact: true })
.evaluate((element) => (element as HTMLButtonElement).click());
await page.waitForFunction(() => {
const viewport = document.querySelector<HTMLElement>('[data-testid="comparison-live-viewport"]');
return viewport?.dataset.technique === 'bitmap' && viewport.dataset.presentationPending === 'false';
});
await assertCanvasHandoff(page, 'MTSDF→Bitmap');
}
if (consoleProblems.length > 0) {
throw new Error(`Presentation emitted browser warnings or errors: ${consoleProblems.join(' | ')}`);
}
Expand All @@ -151,6 +231,24 @@ try {
await server.close();
}

async function assertCanvasHandoff(page: Page, label: string): Promise<void> {
const evidence = await page.evaluate(() => {
const scope = globalThis as typeof globalThis & {
presentationProbeCanvas: HTMLCanvasElement | undefined;
presentationProbeCanvasEvents: string[];
};
const current = document.querySelector('canvas[data-configured-renderer-active="true"]');
return {
disconnected: scope.presentationProbeCanvasEvents.some((event) => event.endsWith('disconnected')),
rendererCount: Number(document.documentElement.dataset.activeConfiguredRenderers),
retained: current !== null && current === scope.presentationProbeCanvas && current.isConnected,
};
});
if (!evidence.retained || evidence.disconnected || evidence.rendererCount !== 1) {
throw new Error(`${label} did not retain one continuously attached renderer canvas: ${JSON.stringify(evidence)}`);
}
}

function presentationTechnique(value: string | undefined): 'bitmap' | 'mtsdf' | 'slug' {
if (value === undefined || value === 'mtsdf') return 'mtsdf';
if (value === 'bitmap' || value === 'slug') return value;
Expand All @@ -159,19 +257,41 @@ function presentationTechnique(value: string | undefined): 'bitmap' | 'mtsdf' |

async function assertPresentationRemainsVisible(page: Page, workload: string): Promise<void> {
const minimumRequiredInkPixels = workload === 'zoom-text' ? 32 : 300;
let minimumVisibleInkPixels = Number.POSITIVE_INFINITY;
let sample = 0;
const startedAt = await page.evaluate(() => performance.now());
while (true) {
const screenshot = await page.screenshot();
const visibleInkPixels = await visiblePresentationInkPixels(page, screenshot.toString('base64'));
minimumVisibleInkPixels = Math.min(minimumVisibleInkPixels, visibleInkPixels);
if (visibleInkPixels < minimumRequiredInkPixels) {
await page.screenshot({ path: `/tmp/pmndrs-text-presentation-${workload}-blank-${String(sample)}.png` });
throw new Error(
`${workload} rendered only ${String(visibleInkPixels)} visible foreground pixels at sample ${String(sample)}`,
);
}
const canvasFailure = await page.evaluate(() => {
const scope = globalThis as typeof globalThis & {
presentationProbeCanvas: HTMLCanvasElement | undefined;
presentationProbeCanvasEvidence: CanvasEvidence | undefined;
presentationProbeCanvasEvents: string[];
presentationProbeInputEvents: string[];
};
const canvas = document.querySelector<HTMLCanvasElement>('canvas[data-configured-renderer-active="true"]');
const expected = scope.presentationProbeCanvasEvidence;
if (canvas === null || expected === undefined) return 'the configured renderer canvas is missing';
if (canvas !== scope.presentationProbeCanvas) return 'the configured renderer canvas identity changed';
const bounds = canvas.getBoundingClientRect();
const actual: CanvasEvidence = {
backingHeight: canvas.height,
backingWidth: canvas.width,
connected: canvas.isConnected,
cssHeight: bounds.height,
cssWidth: bounds.width,
};
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
const navigationStatus =
document.querySelector<HTMLElement>('[data-testid="canvas-navigation-status"]')?.textContent ?? 'missing';
return `the canvas dimensions changed from ${JSON.stringify(expected)} to ${JSON.stringify(actual)}; browser DPR ${String(devicePixelRatio)}; URL ${location.search}; status ${navigationStatus.trim()}; canvas events ${scope.presentationProbeCanvasEvents.slice(-12).join(' | ')}; input events ${scope.presentationProbeInputEvents.slice(-12).join(' | ')}`;
}
const rendererCount = Number(document.documentElement.dataset.activeConfiguredRenderers);
if (rendererCount !== 1) return `the active renderer count changed to ${String(rendererCount)}`;
const viewport = document.querySelector<HTMLElement>('[data-testid="comparison-live-viewport"]');
if (Number(viewport?.dataset.glyphCount) <= 0 || Number(viewport?.dataset.drawCount) <= 0) {
return 'the active workload stopped publishing glyphs or draw calls';
}
return undefined;
});
if (canvasFailure !== undefined) throw new Error(`${workload}: ${canvasFailure}`);
const elapsedMs = await page.evaluate((start) => performance.now() - start, startedAt);
if (elapsedMs >= presentationIntervalMs) break;
const nextSampleAt = startedAt + Math.min(presentationIntervalMs, elapsedMs + presentationSamplePeriodMs);
Expand All @@ -186,9 +306,24 @@ async function assertPresentationRemainsVisible(page: Page, workload: string): P
}),
nextSampleAt,
);
sample += 1;
}
console.log('presentation-workload-visible', workload, minimumVisibleInkPixels);

// Full-page capture can perturb a hardware-composited canvas on some Chromium/macOS combinations. Keep cadence
// monitoring capture-free, then take one bounded headless proof after the workload has survived the interval.
const screenshot = await page.screenshot();
const visibleInkPixels = await visiblePresentationInkPixels(page, screenshot.toString('base64'));
if (visibleInkPixels < minimumRequiredInkPixels) {
throw new Error(`${workload} rendered only ${String(visibleInkPixels)} visible foreground pixels`);
}
console.log('presentation-workload-visible', workload, visibleInkPixels);
}

interface CanvasEvidence {
readonly backingHeight: number;
readonly backingWidth: number;
readonly connected: boolean;
readonly cssHeight: number;
readonly cssWidth: number;
}

async function visiblePresentationInkPixels(page: Page, screenshotBase64: string): Promise<number> {
Expand Down
6 changes: 5 additions & 1 deletion apps/benchmarks/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1392,7 +1392,6 @@ function Scene({
presentationPreset={presentationPreset}
showLayoutBounds={showLayoutBounds}
workloadAmount={workloadAmount}
key={`${location.mode}-${location.backend}-${location.delivery}-${String(dpr)}`}
showcaseFrame={showcaseFrame}
stats={liveStats}
technique={location.technique}
Expand Down Expand Up @@ -1593,6 +1592,7 @@ function BenchmarkSurface({
animationSpeed={animationSpeed}
backend={backend}
delivery={delivery}
demoMode={demoMode}
suppressLoading={demoMode || presentation === 'presentation'}
dpr={dpr}
fontSize={fontSize}
Expand Down Expand Up @@ -3574,6 +3574,7 @@ function ComparisonWorkloadViewport({
animationSpeed,
backend,
delivery,
demoMode,
dpr,
fontFixture,
fontSize,
Expand All @@ -3596,6 +3597,7 @@ function ComparisonWorkloadViewport({
readonly animationSpeed: number;
readonly backend: GraphicsBackend;
readonly delivery: FontDelivery;
readonly demoMode: boolean;
readonly dpr: 1 | 2;
readonly fontFixture: BenchmarkFontFixture;
readonly fontSize: number;
Expand Down Expand Up @@ -3650,6 +3652,7 @@ function ComparisonWorkloadViewport({
paintStrokeWidth,
showGrid: grid,
showLayoutBounds,
textLadderExitEnabled: demoMode && workload === 'text-ladder',
workload,
}),
);
Expand Down Expand Up @@ -3728,6 +3731,7 @@ function ComparisonWorkloadViewport({
animationEnabled,
animationSpeed,
dpr,
demoMode,
fontFixture,
fontSize,
layoutWidthRatio,
Expand Down
3 changes: 3 additions & 0 deletions apps/benchmarks/src/benchmark/advanced-shaping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ describe('advanced-shaping timeline', () => {
missingGlyphCount: 0,
renderedGlyphCount: 625,
drawCount: 72,
coldReadyObservationCount: 5,
warmLifecyclePublicationCount: 63,
warmReadyWaitCount: 0,
},
};
expect(scenario.validate([measurement])).toContain('68 frames/sample');
Expand Down
8 changes: 4 additions & 4 deletions apps/benchmarks/src/benchmark/package-size-budgets.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export const packageSizeBudgets = {
'browser-core': {
rawBytes: 333_000,
minifiedBytes: 253_000,
gzipBytes: 74_000,
brotliBytes: 57_000,
rawBytes: 341_000,
minifiedBytes: 258_000,
gzipBytes: 75_000,
brotliBytes: 57_500,
},
'font-validator-js': {
rawBytes: 741_000,
Expand Down
24 changes: 12 additions & 12 deletions apps/benchmarks/src/benchmark/package-sizes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ describe('independent package-size report', () => {
it('bounds the accepted coverage-capability growth from its pre-coverage baseline', () => {
const coverageGrowth = {
'browser-core': {
rawBytes: { baseline: 324_269, maximumGrowth: 7_900 },
minifiedBytes: { baseline: 247_205, maximumGrowth: 5_400 },
gzipBytes: { baseline: 72_108, maximumGrowth: 1_300 },
brotliBytes: { baseline: 55_251, maximumGrowth: 1_050 },
rawBytes: { baseline: 324_269, maximumGrowth: 16_000 },
minifiedBytes: { baseline: 247_205, maximumGrowth: 10_500 },
gzipBytes: { baseline: 72_108, maximumGrowth: 2_250 },
brotliBytes: { baseline: 55_251, maximumGrowth: 1_900 },
},
'bitmap-baker-js': {
rawBytes: { baseline: 17_478, maximumGrowth: 5_700 },
Expand All @@ -86,10 +86,10 @@ describe('independent package-size report', () => {
brotliBytes: { baseline: 173_552, maximumGrowth: 7_000 },
},
'bitmap-runtime-js': {
rawBytes: { baseline: 361_809, maximumGrowth: 12_300 },
minifiedBytes: { baseline: 271_005, maximumGrowth: 8_000 },
gzipBytes: { baseline: 78_673, maximumGrowth: 1_750 },
brotliBytes: { baseline: 60_857, maximumGrowth: 1_600 },
rawBytes: { baseline: 361_809, maximumGrowth: 20_500 },
minifiedBytes: { baseline: 271_005, maximumGrowth: 13_000 },
gzipBytes: { baseline: 78_673, maximumGrowth: 2_800 },
brotliBytes: { baseline: 60_857, maximumGrowth: 2_350 },
},
'mtsdf-baker-wasm': {
rawBytes: { baseline: 534_709, maximumGrowth: 18_500 },
Expand All @@ -104,10 +104,10 @@ describe('independent package-size report', () => {
brotliBytes: { baseline: 4_176, maximumGrowth: 800 },
},
'mtsdf-runtime-js': {
rawBytes: { baseline: 370_255, maximumGrowth: 11_500 },
minifiedBytes: { baseline: 275_271, maximumGrowth: 7_400 },
gzipBytes: { baseline: 79_993, maximumGrowth: 1_700 },
brotliBytes: { baseline: 62_081, maximumGrowth: 1_500 },
rawBytes: { baseline: 370_255, maximumGrowth: 19_750 },
minifiedBytes: { baseline: 275_271, maximumGrowth: 12_500 },
gzipBytes: { baseline: 79_993, maximumGrowth: 2_800 },
brotliBytes: { baseline: 62_081, maximumGrowth: 2_300 },
},
} as const;
const fields = ['rawBytes', 'minifiedBytes', 'gzipBytes', 'brotliBytes'] as const;
Expand Down
5 changes: 4 additions & 1 deletion apps/benchmarks/src/benchmark/scenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,10 @@ function advancedShapingValidation(values: readonly import('./contracts').Benchm
metrics.missingGlyphCount !== 0 ||
metrics.glyphCount !== 709 ||
metrics.renderedGlyphCount !== 625 ||
metrics.drawCount !== 72
metrics.drawCount !== 72 ||
metrics.coldReadyObservationCount !== ADVANCED_SHAPING_CASES.length ||
metrics.warmLifecyclePublicationCount !== frameCount - ADVANCED_SHAPING_CASES.length ||
metrics.warmReadyWaitCount !== 0
) {
throw new Error('Advanced shaping did not preserve its complete authored frame matrix');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
{
"id": "package-sizes",
"path": "apps/benchmarks/src/generated/package-sizes.json",
"sha256": "049c110460b749edda229b47a1e3151b1c487a28757d28282dd1d751b24946a9",
"sha256": "2e7af35a43c9723209db882d67991a257a06133b3908369b12a8c9692554eb8b",
"bytes": 6825
},
{
Expand Down
Loading
Loading