Created
September 18, 2026 22:09
-
-
Save jogibear9988/35acb171a6ecab1edf9acb60dc437654 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>getBoxQuads designer select-all drag benchmark</title> | |
| <style> | |
| :root { | |
| color-scheme: light; | |
| font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | |
| color: #172027; | |
| background: #f4f6f7; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| margin: 0; | |
| min-width: 980px; | |
| min-height: 100vh; | |
| overflow: hidden; | |
| } | |
| button, | |
| select { | |
| font: inherit; | |
| } | |
| #app { | |
| display: grid; | |
| grid-template-rows: auto minmax(320px, 1fr); | |
| height: 100vh; | |
| } | |
| #toolbar { | |
| position: relative; | |
| z-index: 3; | |
| display: grid; | |
| grid-template-columns: minmax(260px, 1.1fr) minmax(620px, 2fr); | |
| gap: 20px; | |
| padding: 14px 18px 16px; | |
| border-bottom: 1px solid #cbd3d7; | |
| background: #ffffff; | |
| box-shadow: 0 2px 8px rgb(20 34 42 / 8%); | |
| } | |
| h1 { | |
| margin: 0 0 10px; | |
| font-size: 18px; | |
| line-height: 1.25; | |
| letter-spacing: 0; | |
| } | |
| .controls { | |
| display: flex; | |
| align-items: end; | |
| gap: 8px; | |
| } | |
| label { | |
| display: grid; | |
| gap: 4px; | |
| min-width: 220px; | |
| color: #53616a; | |
| font-size: 12px; | |
| font-weight: 650; | |
| } | |
| select, | |
| button { | |
| height: 34px; | |
| border: 1px solid #aebbc1; | |
| border-radius: 5px; | |
| background: #ffffff; | |
| color: #172027; | |
| } | |
| select { | |
| padding: 0 30px 0 9px; | |
| } | |
| button { | |
| padding: 0 13px; | |
| cursor: pointer; | |
| font-weight: 650; | |
| } | |
| button:hover:not(:disabled) { | |
| border-color: #347b67; | |
| background: #f1faf7; | |
| } | |
| button:disabled { | |
| cursor: default; | |
| opacity: 0.48; | |
| } | |
| #run { | |
| border-color: #176b54; | |
| background: #176b54; | |
| color: #ffffff; | |
| } | |
| #run:hover:not(:disabled) { | |
| border-color: #105541; | |
| background: #105541; | |
| } | |
| #fixture-status { | |
| margin-top: 9px; | |
| color: #65737b; | |
| font: 12px/1.35 ui-monospace, SFMono-Regular, Menlo, monospace; | |
| } | |
| #fixture-status.error { | |
| color: #a62b2b; | |
| } | |
| #metrics { | |
| display: grid; | |
| grid-template-columns: repeat(7, minmax(82px, 1fr)); | |
| gap: 8px; | |
| align-content: start; | |
| } | |
| .metric { | |
| min-height: 55px; | |
| padding: 7px 9px; | |
| border-left: 3px solid #7a8d97; | |
| background: #eef2f3; | |
| } | |
| .metric strong { | |
| display: block; | |
| overflow: hidden; | |
| font: 700 17px/1.25 ui-monospace, SFMono-Regular, Menlo, monospace; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .metric span { | |
| display: block; | |
| margin-top: 2px; | |
| color: #65737b; | |
| font-size: 11px; | |
| line-height: 1.2; | |
| } | |
| #progress-wrap { | |
| grid-column: 1 / -1; | |
| display: grid; | |
| grid-template-columns: minmax(140px, auto) 1fr; | |
| gap: 10px; | |
| align-items: center; | |
| height: 24px; | |
| } | |
| #progress-text { | |
| color: #47565e; | |
| font: 12px/1 ui-monospace, SFMono-Regular, Menlo, monospace; | |
| } | |
| #progress-track { | |
| height: 6px; | |
| overflow: hidden; | |
| border-radius: 3px; | |
| background: #dce3e6; | |
| } | |
| #progress { | |
| width: 0; | |
| height: 100%; | |
| background: #1787a3; | |
| transition: width 100ms linear; | |
| } | |
| #workspace { | |
| position: relative; | |
| min-height: 0; | |
| overflow: hidden; | |
| background: | |
| linear-gradient(#d8dfe2 1px, transparent 1px), | |
| linear-gradient(90deg, #d8dfe2 1px, transparent 1px), | |
| #edf1f2; | |
| background-size: 24px 24px; | |
| } | |
| #designer-canvas { | |
| position: absolute; | |
| inset: 0; | |
| contain: paint; | |
| overflow: hidden; | |
| } | |
| #selection-overlays { | |
| position: absolute; | |
| inset: 0; | |
| z-index: 2; | |
| width: 100%; | |
| height: 100%; | |
| overflow: visible; | |
| pointer-events: none; | |
| } | |
| #selection-overlays .selection { | |
| fill: none; | |
| stroke: rgb(9 127 159 / 72%); | |
| stroke-width: 1; | |
| } | |
| #selection-overlays .placement { | |
| fill: rgb(22 107 84 / 4%); | |
| stroke: rgb(22 107 84 / 48%); | |
| stroke-width: 1; | |
| } | |
| #selection-overlays .multiple-selection { | |
| fill: none; | |
| stroke: rgb(166 43 43 / 68%); | |
| stroke-dasharray: 4 3; | |
| stroke-width: 1; | |
| } | |
| #result-panel { | |
| position: absolute; | |
| right: 14px; | |
| bottom: 14px; | |
| z-index: 4; | |
| display: none; | |
| width: min(900px, calc(100% - 28px)); | |
| max-height: min(330px, calc(100% - 28px)); | |
| overflow: auto; | |
| border: 1px solid #adb9be; | |
| border-radius: 6px; | |
| background: rgb(255 255 255 / 96%); | |
| box-shadow: 0 8px 26px rgb(22 34 41 / 20%); | |
| } | |
| #result-panel.visible { | |
| display: block; | |
| } | |
| .result-head { | |
| position: sticky; | |
| top: 0; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 12px; | |
| padding: 8px 10px; | |
| border-bottom: 1px solid #d5dcdf; | |
| background: #ffffff; | |
| } | |
| .result-head strong { | |
| font-size: 13px; | |
| } | |
| .result-actions { | |
| display: flex; | |
| gap: 6px; | |
| } | |
| .result-actions button { | |
| height: 28px; | |
| padding: 0 9px; | |
| font-size: 12px; | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 11px; | |
| font-variant-numeric: tabular-nums; | |
| } | |
| th, | |
| td { | |
| padding: 6px 8px; | |
| border-bottom: 1px solid #e2e7e9; | |
| text-align: right; | |
| white-space: nowrap; | |
| } | |
| th:first-child, | |
| td:first-child, | |
| th:nth-child(2), | |
| td:nth-child(2) { | |
| text-align: left; | |
| } | |
| th { | |
| color: #5c6970; | |
| font-weight: 650; | |
| } | |
| #result-json { | |
| margin: 0; | |
| padding: 10px; | |
| overflow: auto; | |
| border-top: 1px solid #d5dcdf; | |
| background: #f5f7f8; | |
| font: 10px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; | |
| white-space: pre-wrap; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <main id="app"> | |
| <section id="toolbar"> | |
| <div> | |
| <h1>getBoxQuads designer select-all drag benchmark</h1> | |
| <div class="controls"> | |
| <label> | |
| Geometry implementation | |
| <select id="mode"> | |
| <option value="native">Native getBoxQuads</option> | |
| <option value="polyfill-cache">Polyfill, per-batch cache</option> | |
| <option value="polyfill-no-cache">Polyfill, no cache</option> | |
| </select> | |
| </label> | |
| <button id="run" type="button" disabled>Run</button> | |
| </div> | |
| <div id="fixture-status">Loading the 1,008-element designer fixture...</div> | |
| </div> | |
| <div id="metrics"> | |
| <div class="metric"><strong id="fps">-</strong><span>frames / second</span></div> | |
| <div class="metric"><strong id="frame-p95">-</strong><span>frame p95, ms</span></div> | |
| <div class="metric"><strong id="placement">-</strong><span>ancestor move avg, ms</span></div> | |
| <div class="metric"><strong id="arrow-refresh">-</strong><span>arrow batch avg, ms</span></div> | |
| <div class="metric"><strong id="pass-one">-</strong><span>full refresh avg, ms</span></div> | |
| <div class="metric"><strong id="pass-two">-</strong><span>overlay refresh avg, ms</span></div> | |
| <div class="metric"><strong id="calls-second">-</strong><span>quad calls / second</span></div> | |
| <div id="progress-wrap"> | |
| <span id="progress-text">Preparing</span> | |
| <div id="progress-track"><div id="progress"></div></div> | |
| </div> | |
| </div> | |
| </section> | |
| <section id="workspace"> | |
| <div id="designer-canvas"></div> | |
| <svg id="selection-overlays" aria-hidden="true"> | |
| <g id="selection-paths"></g> | |
| <g id="placement-rects"></g> | |
| <path id="multiple-selection-path" class="multiple-selection"></path> | |
| </svg> | |
| <aside id="result-panel"> | |
| <div class="result-head"> | |
| <strong>Benchmark results</strong> | |
| <div class="result-actions"> | |
| <button id="copy" type="button">Copy JSON</button> | |
| <button id="close-results" type="button" aria-label="Close results">Close</button> | |
| </div> | |
| </div> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Browser</th> | |
| <th>Mode</th> | |
| <th>Access pattern</th> | |
| <th>FPS</th> | |
| <th>Frame p95</th> | |
| <th>Move avg</th> | |
| <th>Arrows avg</th> | |
| <th>Full refresh</th> | |
| <th>Overlay avg</th> | |
| <th>Calls/s</th> | |
| <th>Error</th> | |
| </tr> | |
| </thead> | |
| <tbody id="result-rows"></tbody> | |
| </table> | |
| <pre id="result-json"></pre> | |
| </aside> | |
| </section> | |
| </main> | |
| <script type="module"> | |
| const FIXTURE_URL = "https://gist.githubusercontent.com/jogibear9988/11942ee156cb6f6354427548ca246835/raw/a1a411871b2abfdc1784d1f8ed1538b22de777a9/gistfile1.txt"; | |
| const POLYFILL_URL = "https://cdn.jsdelivr.net/gh/jogibear9988/getBoxQuadsPolyfill@f0741159da3f3a6c9d3bedfb5deb1076dd22b8d4/getBoxQuads.js"; | |
| const SHADOW_DEPTH = 6; | |
| const WARMUP_MS = 2000; | |
| const MEASURE_MS = 8000; | |
| const CONNECTOR_REFRESH_INTERVAL_MS = 100; | |
| const params = new URLSearchParams(location.search); | |
| const ACCESS_PATTERN = params.get("access") === "batched" ? | |
| "batched reads, then writes" : "interleaved reads and writes"; | |
| const requestedOverlayBudget = Number( | |
| params.get("budget")); | |
| const OVERLAY_REFRESH_BUDGET_MS = | |
| Number.isFinite(requestedOverlayBudget) && requestedOverlayBudget > 0 | |
| ? requestedOverlayBudget | |
| : 20; | |
| const FULL_OVERLAY_REFRESH_INTERVAL_MS = 250; | |
| const designerCanvas = document.querySelector("#designer-canvas"); | |
| const workspace = document.querySelector("#workspace"); | |
| const selectionOverlays = document.querySelector("#selection-overlays"); | |
| const selectionPathsRoot = document.querySelector("#selection-paths"); | |
| const placementRectsRoot = document.querySelector("#placement-rects"); | |
| const multipleSelectionPath = document.querySelector("#multiple-selection-path"); | |
| const modeSelect = document.querySelector("#mode"); | |
| const runButton = document.querySelector("#run"); | |
| const fixtureStatus = document.querySelector("#fixture-status"); | |
| const progressText = document.querySelector("#progress-text"); | |
| const progress = document.querySelector("#progress"); | |
| const resultPanel = document.querySelector("#result-panel"); | |
| const resultRows = document.querySelector("#result-rows"); | |
| const resultJson = document.querySelector("#result-json"); | |
| const copyButton = document.querySelector("#copy"); | |
| const closeResultsButton = document.querySelector("#close-results"); | |
| const metricElements = { | |
| fps: document.querySelector("#fps"), | |
| frameP95: document.querySelector("#frame-p95"), | |
| placement: document.querySelector("#placement"), | |
| arrowRefresh: document.querySelector("#arrow-refresh"), | |
| passOne: document.querySelector("#pass-one"), | |
| passTwo: document.querySelector("#pass-two"), | |
| callsSecond: document.querySelector("#calls-second"), | |
| }; | |
| let fixtureElements = []; | |
| let targets = []; | |
| let connectors = []; | |
| let selectionPaths = []; | |
| let placementRects = []; | |
| let fixtureRoot = null; | |
| let fixtureBaseTransform = ""; | |
| let running = false; | |
| let animationFrame = 0; | |
| let lastResult = null; | |
| class BenchmarkShadowLayer extends HTMLElement { | |
| constructor() { | |
| super(); | |
| const depth = Number(this.dataset.depth || 0); | |
| const translateX = 2 + depth * 0.7; | |
| const translateY = depth % 2 ? -1.5 : 1.25; | |
| const root = this.attachShadow({ mode: "open" }); | |
| root.innerHTML = ` | |
| <style> | |
| :host { | |
| position: absolute; | |
| inset: 0; | |
| display: block; | |
| overflow: visible; | |
| } | |
| .surface { | |
| position: absolute; | |
| inset: 0; | |
| transform: translate(${translateX}px, ${translateY}px); | |
| transform-origin: 0 0; | |
| } | |
| </style> | |
| <div class="surface"><slot></slot></div> | |
| `; | |
| } | |
| } | |
| customElements.define("benchmark-shadow-layer", BenchmarkShadowLayer); | |
| function resizeOverlay() { | |
| const width = Math.max(1, workspace.clientWidth); | |
| const height = Math.max(1, workspace.clientHeight); | |
| selectionOverlays.setAttribute("viewBox", `0 0 ${width} ${height}`); | |
| } | |
| function browserName() { | |
| const ua = navigator.userAgent; | |
| const firefox = ua.match(/Firefox\/(\d+(?:\.\d+)?)/); | |
| const chromium = ua.match(/(?:Chromium|Chrome)\/(\d+(?:\.\d+)?)/); | |
| if (firefox) return `Firefox ${firefox[1]}`; | |
| if (chromium) return `${ua.includes("Chromium") ? "Chromium" : "Chrome"} ${chromium[1]}`; | |
| return navigator.userAgentData?.brands?.map((brand) => `${brand.brand} ${brand.version}`).join(", ") || ua; | |
| } | |
| function percentile(values, fraction) { | |
| if (!values.length) return 0; | |
| const ordered = [...values].sort((a, b) => a - b); | |
| const index = Math.min(ordered.length - 1, Math.max(0, Math.ceil(ordered.length * fraction) - 1)); | |
| return ordered[index]; | |
| } | |
| function average(values) { | |
| if (!values.length) return 0; | |
| return values.reduce((sum, value) => sum + value, 0) / values.length; | |
| } | |
| function round(value, digits = 2) { | |
| const factor = 10 ** digits; | |
| return Math.round(value * factor) / factor; | |
| } | |
| function quadBounds(quad) { | |
| const points = [quad.p1, quad.p2, quad.p3, quad.p4]; | |
| const xs = points.map((point) => point.x); | |
| const ys = points.map((point) => point.y); | |
| const left = Math.min(...xs); | |
| const top = Math.min(...ys); | |
| return { | |
| left, | |
| top, | |
| right: Math.max(...xs), | |
| bottom: Math.max(...ys), | |
| }; | |
| } | |
| function nativeAvailable() { | |
| return targets.length > 0 && typeof targets[0].getBoxQuads === "function"; | |
| } | |
| async function createGeometryRunner(mode) { | |
| if (mode === "native") { | |
| if (!nativeAvailable()) { | |
| throw new Error("Native getBoxQuads is not available in this browser."); | |
| } | |
| return { | |
| label: "native", | |
| beginBatch() {}, | |
| getBoxQuads(node, options) { | |
| return node.getBoxQuads(options); | |
| }, | |
| }; | |
| } | |
| const moduleUrl = `${POLYFILL_URL}?benchmark=${mode}`; | |
| const polyfill = await import(moduleUrl); | |
| if (mode === "polyfill-cache") { | |
| polyfill.useCache(); | |
| return { | |
| label: "polyfill, per-batch cache", | |
| beginBatch() { | |
| polyfill.clearCache(); | |
| }, | |
| getBoxQuads: polyfill.getBoxQuads, | |
| }; | |
| } | |
| return { | |
| label: "polyfill, no cache", | |
| beginBatch() {}, | |
| getBoxQuads: polyfill.getBoxQuads, | |
| }; | |
| } | |
| function quadPathData(quad) { | |
| return `M ${quad.p1.x} ${quad.p1.y} L ${quad.p2.x} ${quad.p2.y} ` + | |
| `L ${quad.p3.x} ${quad.p3.y} L ${quad.p4.x} ${quad.p4.y} Z`; | |
| } | |
| function updatePlacementRect(rect, canvasRect, overlayRect) { | |
| overlayRect.setAttribute("x", rect.left - canvasRect.left); | |
| overlayRect.setAttribute("y", rect.top - canvasRect.top); | |
| overlayRect.setAttribute("width", rect.width); | |
| overlayRect.setAttribute("height", rect.height); | |
| } | |
| function updateMultipleSelectionBounds(canvasRect, targetRects = null) { | |
| let left = Infinity; | |
| let top = Infinity; | |
| let right = -Infinity; | |
| let bottom = -Infinity; | |
| for (let index = 0; index < targets.length; index += 1) { | |
| const rect = targetRects ? targetRects[index] : | |
| targets[index].getBoundingClientRect(); | |
| left = Math.min(left, rect.left - canvasRect.left); | |
| top = Math.min(top, rect.top - canvasRect.top); | |
| right = Math.max(right, rect.right - canvasRect.left); | |
| bottom = Math.max(bottom, rect.bottom - canvasRect.top); | |
| } | |
| multipleSelectionPath.setAttribute("d", | |
| `M ${left} ${top} H ${right} V ${bottom} H ${left} Z`); | |
| } | |
| function refreshConnectors() { | |
| for (const connector of connectors) { | |
| connector.refresh(); | |
| } | |
| } | |
| function updateLiveMetrics(samples, measuredElapsed) { | |
| const seconds = Math.max(0.001, measuredElapsed / 1000); | |
| metricElements.fps.textContent = (samples.frameIntervals.length / seconds).toFixed(1); | |
| metricElements.frameP95.textContent = percentile(samples.frameIntervals, 0.95).toFixed(1); | |
| metricElements.placement.textContent = average(samples.placementTimes).toFixed(1); | |
| metricElements.arrowRefresh.textContent = average(samples.connectorBatchTimes).toFixed(1); | |
| metricElements.passOne.textContent = average(samples.fullOverlayRefreshTimes).toFixed(1); | |
| metricElements.passTwo.textContent = average(samples.overlayRefreshTimes).toFixed(1); | |
| metricElements.callsSecond.textContent = Math.round(samples.quadCalls / seconds).toLocaleString("en-US"); | |
| } | |
| function addResult(result) { | |
| const row = document.createElement("tr"); | |
| const values = [ | |
| result.browser, | |
| result.mode, | |
| result.accessPattern, | |
| result.fps.toFixed(1), | |
| `${result.frameMs.p95.toFixed(1)} ms`, | |
| `${result.moveLoopMs.avg.toFixed(1)} ms`, | |
| `${result.connectorRefreshMs.avg.toFixed(1)} ms`, | |
| `${result.fullOverlayRefreshMs.avg.toFixed(1)} ms`, | |
| `${result.overlayRefreshMs.avg.toFixed(1)} ms`, | |
| Math.round(result.quadCallsPerSecond).toLocaleString("en-US"), | |
| `${result.correctness.maxAabbErrorPx.toFixed(2)} px`, | |
| ]; | |
| for (const value of values) { | |
| const cell = document.createElement("td"); | |
| cell.textContent = value; | |
| row.appendChild(cell); | |
| } | |
| resultRows.prepend(row); | |
| resultJson.textContent = JSON.stringify(result, null, 2); | |
| resultPanel.classList.add("visible"); | |
| } | |
| function buildResult(runner, samples, measureStart, measureEnd, checksum) { | |
| const measuredSeconds = Math.max(0.001, (measureEnd - measureStart) / 1000); | |
| const stats = (values) => ({ | |
| avg: round(average(values), 3), | |
| p50: round(percentile(values, 0.5), 3), | |
| p95: round(percentile(values, 0.95), 3), | |
| p99: round(percentile(values, 0.99), 3), | |
| }); | |
| return { | |
| benchmark: "getBoxQuads designer select-all drag workload v1", | |
| browser: browserName(), | |
| userAgent: navigator.userAgent, | |
| mode: runner.label, | |
| accessPattern: ACCESS_PATTERN, | |
| nativeGetBoxQuadsAvailable: nativeAvailable(), | |
| fixture: { | |
| source: FIXTURE_URL, | |
| fixtureElements: fixtureElements.length, | |
| selectedElements: targets.length, | |
| movedElements: 1, | |
| movingArrows: connectors.length, | |
| connectors: connectors.length, | |
| connectorRefreshIntervalMs: CONNECTOR_REFRESH_INTERVAL_MS, | |
| shadowRootDepth: SHADOW_DEPTH, | |
| overlayRefreshBudgetMs: OVERLAY_REFRESH_BUDGET_MS, | |
| fullOverlayRefreshIntervalMs: FULL_OVERLAY_REFRESH_INTERVAL_MS, | |
| overlayDomNodes: selectionPaths.length + placementRects.length + 1, | |
| connectorLayoutReadsPerBatch: connectors.length * 3, | |
| connectorPathWritesPerBatch: connectors.length, | |
| warmupMs: WARMUP_MS, | |
| measureMs: MEASURE_MS, | |
| }, | |
| frames: samples.frames, | |
| elapsedMs: round((measureEnd - measureStart), 3), | |
| fps: round(samples.frames / measuredSeconds, 3), | |
| quadCalls: samples.quadCalls, | |
| quadCallsPerSecond: round(samples.quadCalls / measuredSeconds, 3), | |
| frameMs: stats(samples.frameIntervals), | |
| callbackMs: stats(samples.callbackTimes), | |
| moveLoopMs: stats(samples.placementTimes), | |
| connectorRefreshes: samples.connectorBatchTimes.length, | |
| connectorRefreshMs: stats(samples.connectorBatchTimes), | |
| connectorCostPerFrameMs: round(average(samples.connectorFrameTimes), 3), | |
| selectionBoundsMs: stats(samples.selectionBoundsTimes), | |
| overlayRefreshMs: stats(samples.overlayRefreshTimes), | |
| fullOverlayRefreshes: samples.fullOverlayRefreshTimes.length, | |
| fullOverlayRefreshMs: stats(samples.fullOverlayRefreshTimes), | |
| refreshedElementsPerFrame: stats(samples.refreshedElementCounts), | |
| droppedFrames: { | |
| over16_7ms: samples.frameIntervals.filter((value) => value > 16.7).length, | |
| over33_3ms: samples.frameIntervals.filter((value) => value > 33.3).length, | |
| }, | |
| correctness: { | |
| maxAabbErrorPx: round(samples.maxError, 4), | |
| checksum: round(checksum, 3), | |
| }, | |
| }; | |
| } | |
| async function runBenchmark() { | |
| if (running || !targets.length) return; | |
| running = true; | |
| runButton.disabled = true; | |
| modeSelect.disabled = true; | |
| resultPanel.classList.remove("visible"); | |
| progress.style.width = "0%"; | |
| fixtureStatus.classList.remove("error"); | |
| try { | |
| const runner = await createGeometryRunner(modeSelect.value); | |
| const samples = { | |
| frames: 0, | |
| quadCalls: 0, | |
| frameIntervals: [], | |
| callbackTimes: [], | |
| placementTimes: [], | |
| connectorBatchTimes: [], | |
| connectorFrameTimes: [], | |
| selectionBoundsTimes: [], | |
| overlayRefreshTimes: [], | |
| fullOverlayRefreshTimes: [], | |
| refreshedElementCounts: [], | |
| maxError: 0, | |
| }; | |
| let checksum = 0; | |
| let startedAt = 0; | |
| let measureStart = 0; | |
| let previousFrameTime = 0; | |
| let lastUiUpdate = 0; | |
| let lastConnectorRefresh = -Infinity; | |
| let lastFullOverlayRefresh = -Infinity; | |
| await new Promise((resolve, reject) => { | |
| const frame = (now) => { | |
| try { | |
| if (!startedAt) { | |
| startedAt = now; | |
| previousFrameTime = now; | |
| } | |
| const totalElapsed = now - startedAt; | |
| const measuring = totalElapsed >= WARMUP_MS; | |
| if (measuring && !measureStart) { | |
| measureStart = now; | |
| previousFrameTime = now; | |
| } | |
| if (measureStart && now - measureStart >= MEASURE_MS) { | |
| resolve(now); | |
| return; | |
| } | |
| const callbackStart = performance.now(); | |
| const wave = now / 420; | |
| const moveX = Math.sin(wave) * 18; | |
| const moveY = Math.cos(wave * 0.83) * 11; | |
| const moveStart = performance.now(); | |
| // Select-all includes the fixture container and all descendants. | |
| // The designer's child filter therefore moves only this ancestor. | |
| fixtureRoot.style.transform = | |
| `${fixtureBaseTransform} translate(${moveX}px, ${moveY}px)`; | |
| const moveTime = performance.now() - moveStart; | |
| // The fixture's independent timers are disabled below. Refresh | |
| // the same 201 SVG arrows as one deterministic 100 ms batch so | |
| // every browser gets the fixture's real cadence without timer | |
| // registration jitter. Each arrow reads three rectangles and | |
| // writes its path. | |
| let connectorTime = 0; | |
| let connectorRefreshed = false; | |
| if (now - lastConnectorRefresh >= CONNECTOR_REFRESH_INTERVAL_MS) { | |
| const connectorStart = performance.now(); | |
| refreshConnectors(); | |
| connectorTime = performance.now() - connectorStart; | |
| connectorRefreshed = true; | |
| lastConnectorRefresh = now; | |
| } | |
| // ExtensionManager clears the polyfill cache after placement and | |
| // before refreshing overlays for the newly transformed selection. | |
| runner.beginBatch(); | |
| const overlayStart = performance.now(); | |
| const canvasRect = designerCanvas.getBoundingClientRect(); | |
| let selectionBoundsTime = 0; | |
| let refreshedElementCount = 0; | |
| let frameQuadCalls = 0; | |
| let checkQuad = null; | |
| let checkTarget = null; | |
| const fullOverlayRefresh = | |
| now - lastFullOverlayRefresh >= FULL_OVERLAY_REFRESH_INTERVAL_MS; | |
| if (fullOverlayRefresh) { | |
| lastFullOverlayRefresh = now; | |
| } | |
| if (ACCESS_PATTERN === "batched reads, then writes") { | |
| const boundsStart = performance.now(); | |
| const targetRects = targets.map( | |
| (target) => target.getBoundingClientRect()); | |
| selectionBoundsTime = performance.now() - boundsStart; | |
| const refreshed = []; | |
| for (let index = 0; index < targets.length; index += 1) { | |
| const target = targets[index]; | |
| const quad = runner.getBoxQuads(target, { | |
| box: "border", | |
| relativeTo: designerCanvas, | |
| })[0]; | |
| frameQuadCalls += 1; | |
| if (quad) { | |
| refreshed.push({index, quad, rect: targetRects[index]}); | |
| checksum += quad.p3.x * 0.0000001 + quad.p3.y * 0.0000002; | |
| if (!checkQuad) { | |
| checkQuad = quad; | |
| checkTarget = target; | |
| } | |
| } | |
| if (!fullOverlayRefresh && | |
| performance.now() - overlayStart >= OVERLAY_REFRESH_BUDGET_MS) { | |
| break; | |
| } | |
| } | |
| updateMultipleSelectionBounds(canvasRect, targetRects); | |
| for (const {index, quad, rect} of refreshed) { | |
| selectionPaths[index].setAttribute("d", quadPathData(quad)); | |
| updatePlacementRect(rect, canvasRect, placementRects[index]); | |
| } | |
| refreshedElementCount = refreshed.length; | |
| } else { | |
| for (let index = 0; index < targets.length; index += 1) { | |
| const target = targets[index]; | |
| const quad = runner.getBoxQuads(target, { | |
| box: "border", | |
| relativeTo: designerCanvas, | |
| })[0]; | |
| frameQuadCalls += 1; | |
| if (!quad) continue; | |
| // SelectionDefaultExtension writes its SVG after reading the | |
| // quad. PlacementExtension then reads a rectangle and writes | |
| // another SVG node, producing the designer's read/write order. | |
| selectionPaths[index].setAttribute("d", quadPathData(quad)); | |
| if (index === 0) { | |
| const boundsStart = performance.now(); | |
| updateMultipleSelectionBounds(canvasRect); | |
| selectionBoundsTime = performance.now() - boundsStart; | |
| } | |
| const rect = target.getBoundingClientRect(); | |
| updatePlacementRect(rect, canvasRect, placementRects[index]); | |
| refreshedElementCount += 1; | |
| checksum += quad.p3.x * 0.0000001 + quad.p3.y * 0.0000002; | |
| if (!checkQuad) { | |
| checkQuad = quad; | |
| checkTarget = target; | |
| } | |
| if (!fullOverlayRefresh && | |
| performance.now() - overlayStart >= OVERLAY_REFRESH_BUDGET_MS) { | |
| break; | |
| } | |
| } | |
| } | |
| const overlayRefreshTime = performance.now() - overlayStart; | |
| if (measuring && checkQuad) { | |
| const bounds = quadBounds(checkQuad); | |
| const rect = checkTarget.getBoundingClientRect(); | |
| const error = Math.max( | |
| Math.abs(bounds.left - (rect.left - canvasRect.left)), | |
| Math.abs(bounds.top - (rect.top - canvasRect.top)), | |
| Math.abs(bounds.right - (rect.right - canvasRect.left)), | |
| Math.abs(bounds.bottom - (rect.bottom - canvasRect.top)), | |
| ); | |
| samples.maxError = Math.max(samples.maxError, error); | |
| } | |
| const callbackTime = performance.now() - callbackStart; | |
| if (measuring) { | |
| samples.frames += 1; | |
| samples.quadCalls += frameQuadCalls; | |
| if (now > measureStart) { | |
| samples.frameIntervals.push(now - previousFrameTime); | |
| } | |
| samples.callbackTimes.push(callbackTime); | |
| samples.placementTimes.push(moveTime); | |
| samples.connectorFrameTimes.push(connectorTime); | |
| if (connectorRefreshed) { | |
| samples.connectorBatchTimes.push(connectorTime); | |
| } | |
| samples.selectionBoundsTimes.push(selectionBoundsTime); | |
| samples.overlayRefreshTimes.push(overlayRefreshTime); | |
| if (fullOverlayRefresh) { | |
| samples.fullOverlayRefreshTimes.push(overlayRefreshTime); | |
| } | |
| samples.refreshedElementCounts.push(refreshedElementCount); | |
| } | |
| previousFrameTime = now; | |
| if (now - lastUiUpdate > 250) { | |
| lastUiUpdate = now; | |
| if (!measuring) { | |
| const warmupProgress = Math.min(1, totalElapsed / WARMUP_MS); | |
| progressText.textContent = `Warm-up ${Math.round(warmupProgress * 100)}%`; | |
| progress.style.width = `${warmupProgress * 20}%`; | |
| } else { | |
| const measuredElapsed = now - measureStart; | |
| const measureProgress = Math.min(1, measuredElapsed / MEASURE_MS); | |
| progressText.textContent = `Measuring ${Math.round(measureProgress * 100)}%`; | |
| progress.style.width = `${20 + measureProgress * 80}%`; | |
| updateLiveMetrics(samples, measuredElapsed); | |
| } | |
| } | |
| animationFrame = requestAnimationFrame(frame); | |
| } catch (error) { | |
| reject(error); | |
| } | |
| }; | |
| animationFrame = requestAnimationFrame(frame); | |
| }).then((measureEnd) => { | |
| lastResult = buildResult(runner, samples, measureStart, measureEnd, checksum); | |
| window.__getBoxQuadsBenchmarkResult = lastResult; | |
| console.log("getBoxQuads benchmark result", lastResult); | |
| addResult(lastResult); | |
| updateLiveMetrics(samples, measureEnd - measureStart); | |
| progressText.textContent = "Complete"; | |
| progress.style.width = "100%"; | |
| }); | |
| } catch (error) { | |
| console.error(error); | |
| fixtureStatus.textContent = error instanceof Error ? error.message : String(error); | |
| fixtureStatus.classList.add("error"); | |
| progressText.textContent = "Failed"; | |
| } finally { | |
| running = false; | |
| runButton.disabled = false; | |
| modeSelect.disabled = false; | |
| } | |
| } | |
| async function loadFixture() { | |
| resizeOverlay(); | |
| const response = await fetch(FIXTURE_URL); | |
| if (!response.ok) throw new Error(`Fixture request failed: HTTP ${response.status}`); | |
| const source = await response.text(); | |
| const pausedSource = source.replace( | |
| "this._refreshInterval = setInterval(this.refresh, 100);", | |
| "this._refreshInterval = null;", | |
| ); | |
| if (pausedSource === source) { | |
| throw new Error("Could not pause the fixture's connector timers."); | |
| } | |
| const template = document.createElement("template"); | |
| template.innerHTML = pausedSource; | |
| const fixtureScript = template.content.querySelector("script"); | |
| if (!fixtureScript) throw new Error("The fixture script was not found."); | |
| const executableScript = document.createElement("script"); | |
| executableScript.textContent = fixtureScript.textContent; | |
| fixtureScript.remove(); | |
| document.head.appendChild(executableScript); | |
| fixtureRoot = template.content.querySelector("div"); | |
| if (!fixtureRoot) throw new Error("The fixture root was not found."); | |
| fixtureRoot.id = "benchmark-fixture-root"; | |
| let parent = designerCanvas; | |
| for (let depth = 0; depth < SHADOW_DEPTH; depth += 1) { | |
| const layer = document.createElement("benchmark-shadow-layer"); | |
| layer.dataset.depth = String(depth); | |
| parent.appendChild(layer); | |
| parent = layer; | |
| } | |
| parent.appendChild(template.content); | |
| await customElements.whenDefined("ui-place"); | |
| await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); | |
| fixtureElements = [...fixtureRoot.querySelectorAll("ui-place")]; | |
| connectors = [...fixtureRoot.querySelectorAll("ui-move")]; | |
| if (fixtureElements.length !== 1008) { | |
| throw new Error(`Expected 1,008 ui-place elements, found ${fixtureElements.length}.`); | |
| } | |
| if (connectors.length !== 201) { | |
| throw new Error(`Expected 201 ui-move connectors, found ${connectors.length}.`); | |
| } | |
| // Match DesignerCanvas.handleSelectAll(): the primary item comes first, | |
| // followed by the remaining places, connectors, and their common parent. | |
| targets = [fixtureElements[1], fixtureElements[0], ...fixtureElements.slice(2), | |
| ...connectors, fixtureRoot]; | |
| fixtureBaseTransform = fixtureRoot.style.transform || ""; | |
| const svgNamespace = "http://www.w3.org/2000/svg"; | |
| const selectionFragment = document.createDocumentFragment(); | |
| const placementFragment = document.createDocumentFragment(); | |
| for (let index = 0; index < targets.length; index += 1) { | |
| const path = document.createElementNS(svgNamespace, "path"); | |
| path.setAttribute("class", "selection"); | |
| selectionFragment.appendChild(path); | |
| selectionPaths.push(path); | |
| const rect = document.createElementNS(svgNamespace, "rect"); | |
| rect.setAttribute("class", "placement"); | |
| placementFragment.appendChild(rect); | |
| placementRects.push(rect); | |
| } | |
| selectionPathsRoot.appendChild(selectionFragment); | |
| placementRectsRoot.appendChild(placementFragment); | |
| const nativeSupported = nativeAvailable(); | |
| modeSelect.querySelector('option[value="native"]').disabled = !nativeSupported; | |
| modeSelect.value = nativeSupported ? "native" : "polyfill-cache"; | |
| fixtureStatus.textContent = `${targets.length.toLocaleString("en-US")} selected elements | ${ACCESS_PATTERN} | 1 moved ancestor | ${connectors.length} moving arrows | ${OVERLAY_REFRESH_BUDGET_MS} ms drag refresh | full refresh/${FULL_OVERLAY_REFRESH_INTERVAL_MS} ms | ${SHADOW_DEPTH} shadow roots | native ${nativeSupported ? "available" : "unavailable"}`; | |
| runButton.disabled = false; | |
| progressText.textContent = "Ready"; | |
| window.__getBoxQuadsBenchmarkReady = true; | |
| const requestedMode = params.get("mode"); | |
| if (requestedMode && [...modeSelect.options].some((option) => option.value === requestedMode && !option.disabled)) { | |
| modeSelect.value = requestedMode; | |
| } | |
| if (params.get("autorun") !== "0") { | |
| setTimeout(runBenchmark, 500); | |
| } | |
| } | |
| runButton.addEventListener("click", runBenchmark); | |
| window.__runGetBoxQuadsBenchmark = runBenchmark; | |
| copyButton.addEventListener("click", async () => { | |
| if (!lastResult) return; | |
| await navigator.clipboard.writeText(JSON.stringify(lastResult, null, 2)); | |
| copyButton.textContent = "Copied"; | |
| setTimeout(() => { copyButton.textContent = "Copy JSON"; }, 1000); | |
| }); | |
| closeResultsButton.addEventListener("click", () => resultPanel.classList.remove("visible")); | |
| window.addEventListener("resize", resizeOverlay); | |
| loadFixture().catch((error) => { | |
| console.error(error); | |
| fixtureStatus.textContent = error instanceof Error ? error.message : String(error); | |
| fixtureStatus.classList.add("error"); | |
| progressText.textContent = "Failed to load"; | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment