@xincli/opentui-core 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/dev/keypress-debug-renderer.ts +148 -0
- package/dev/keypress-debug.ts +43 -0
- package/dev/print-env-vars.ts +32 -0
- package/dev/test-tmux-graphics-334.sh +68 -0
- package/dev/test-tmux-theme-queries.sh +151 -0
- package/dev/thai-debug-test.ts +68 -0
- package/dev/theme-mode.ts +290 -0
- package/docs/development.md +144 -0
- package/package.json +102 -0
- package/prebuilt/aarch64-android/libopentui.so +0 -0
- package/scripts/build-android.ts +226 -0
- package/scripts/build-native-termux.sh +525 -0
- package/scripts/build.ts +530 -0
- package/scripts/dist-test.ts +312 -0
- package/scripts/package-prebuilt.ts +110 -0
- package/scripts/publish.ts +60 -0
- package/scripts/test-node-hook.mjs +16 -0
- package/scripts/test-node.ts +241 -0
- package/scripts/vendor-deps.sh +73 -0
- package/src/NativeSpanFeed.ts +375 -0
- package/src/Renderable.ts +1878 -0
- package/src/__snapshots__/buffer.test.ts.snap +28 -0
- package/src/animation/Timeline.test.ts +2709 -0
- package/src/animation/Timeline.ts +598 -0
- package/src/ansi.ts +22 -0
- package/src/audio.ts +479 -0
- package/src/benchmark/attenuation-benchmark.ts +81 -0
- package/src/benchmark/audio-playback-benchmark.ts +344 -0
- package/src/benchmark/box-draw-benchmark.ts +1042 -0
- package/src/benchmark/colormatrix-benchmark.ts +123 -0
- package/src/benchmark/gain-benchmark.ts +80 -0
- package/src/benchmark/latest-all-bench-run.json +707 -0
- package/src/benchmark/latest-async-bench-run.json +336 -0
- package/src/benchmark/latest-default-bench-run.json +657 -0
- package/src/benchmark/latest-large-bench-run.json +707 -0
- package/src/benchmark/latest-quick-bench-run.json +207 -0
- package/src/benchmark/layout-benchmark.ts +2547 -0
- package/src/benchmark/markdown-benchmark.ts +1796 -0
- package/src/benchmark/native-span-feed-async-benchmark.ts +355 -0
- package/src/benchmark/native-span-feed-benchmark.md +56 -0
- package/src/benchmark/native-span-feed-benchmark.ts +596 -0
- package/src/benchmark/native-span-feed-compare.ts +280 -0
- package/src/benchmark/render-traversal-benchmark.ts +928 -0
- package/src/benchmark/text-buffer-render-benchmark.ts +874 -0
- package/src/benchmark/text-table-benchmark.ts +948 -0
- package/src/buffer.test.ts +292 -0
- package/src/buffer.ts +564 -0
- package/src/console.test.ts +629 -0
- package/src/console.ts +1253 -0
- package/src/edit-buffer.test.ts +1852 -0
- package/src/edit-buffer.ts +417 -0
- package/src/editor-view.test.ts +1056 -0
- package/src/editor-view.ts +290 -0
- package/src/index.ts +25 -0
- package/src/lib/KeyHandler.integration.test.ts +292 -0
- package/src/lib/KeyHandler.stopPropagation.test.ts +289 -0
- package/src/lib/KeyHandler.test.ts +662 -0
- package/src/lib/KeyHandler.ts +222 -0
- package/src/lib/RGBA.test.ts +60 -0
- package/src/lib/RGBA.ts +343 -0
- package/src/lib/ascii.font.ts +330 -0
- package/src/lib/border.test.ts +83 -0
- package/src/lib/border.ts +170 -0
- package/src/lib/bunfs.test.ts +27 -0
- package/src/lib/bunfs.ts +18 -0
- package/src/lib/clipboard.test.ts +63 -0
- package/src/lib/clipboard.ts +41 -0
- package/src/lib/clock.ts +35 -0
- package/src/lib/data-paths.test.ts +133 -0
- package/src/lib/data-paths.ts +109 -0
- package/src/lib/debounce.ts +106 -0
- package/src/lib/detect-links.test.ts +98 -0
- package/src/lib/detect-links.ts +56 -0
- package/src/lib/env.test.ts +228 -0
- package/src/lib/env.ts +209 -0
- package/src/lib/extmarks-history.ts +51 -0
- package/src/lib/extmarks-multiwidth.test.ts +324 -0
- package/src/lib/extmarks.test.ts +3547 -0
- package/src/lib/extmarks.ts +861 -0
- package/src/lib/fonts/block.json +405 -0
- package/src/lib/fonts/grid.json +265 -0
- package/src/lib/fonts/huge.json +741 -0
- package/src/lib/fonts/pallet.json +314 -0
- package/src/lib/fonts/shade.json +591 -0
- package/src/lib/fonts/slick.json +321 -0
- package/src/lib/fonts/tiny.json +69 -0
- package/src/lib/hast-styled-text.ts +59 -0
- package/src/lib/index.ts +22 -0
- package/src/lib/keybinding.internal.test.ts +342 -0
- package/src/lib/keybinding.internal.ts +182 -0
- package/src/lib/objects-in-viewport.test.ts +789 -0
- package/src/lib/objects-in-viewport.ts +153 -0
- package/src/lib/output.capture.ts +58 -0
- package/src/lib/parse.keypress-kitty.protocol.test.ts +347 -0
- package/src/lib/parse.keypress-kitty.test.ts +712 -0
- package/src/lib/parse.keypress-kitty.ts +470 -0
- package/src/lib/parse.keypress.test.ts +2020 -0
- package/src/lib/parse.keypress.ts +480 -0
- package/src/lib/parse.mouse.test.ts +552 -0
- package/src/lib/parse.mouse.ts +232 -0
- package/src/lib/paste.ts +18 -0
- package/src/lib/queue.ts +65 -0
- package/src/lib/render-geometry.ts +36 -0
- package/src/lib/renderable.validations.test.ts +87 -0
- package/src/lib/renderable.validations.ts +83 -0
- package/src/lib/scroll-acceleration.ts +98 -0
- package/src/lib/selection.ts +261 -0
- package/src/lib/singleton.ts +34 -0
- package/src/lib/stdin-parser.test.ts +2439 -0
- package/src/lib/stdin-parser.ts +2007 -0
- package/src/lib/styled-text.ts +178 -0
- package/src/lib/terminal-capability-detection.test.ts +226 -0
- package/src/lib/terminal-capability-detection.ts +98 -0
- package/src/lib/terminal-palette.test.ts +954 -0
- package/src/lib/terminal-palette.ts +441 -0
- package/src/lib/tree-sitter/assets/README.md +119 -0
- package/src/lib/tree-sitter/assets/javascript/highlights.scm +205 -0
- package/src/lib/tree-sitter/assets/javascript/tree-sitter-javascript.wasm +0 -0
- package/src/lib/tree-sitter/assets/markdown/highlights.scm +150 -0
- package/src/lib/tree-sitter/assets/markdown/injections.scm +27 -0
- package/src/lib/tree-sitter/assets/markdown/tree-sitter-markdown.wasm +0 -0
- package/src/lib/tree-sitter/assets/markdown_inline/highlights.scm +115 -0
- package/src/lib/tree-sitter/assets/markdown_inline/tree-sitter-markdown_inline.wasm +0 -0
- package/src/lib/tree-sitter/assets/typescript/highlights.scm +604 -0
- package/src/lib/tree-sitter/assets/typescript/tree-sitter-typescript.wasm +0 -0
- package/src/lib/tree-sitter/assets/update.ts +360 -0
- package/src/lib/tree-sitter/assets/zig/highlights.scm +284 -0
- package/src/lib/tree-sitter/assets/zig/tree-sitter-zig.wasm +0 -0
- package/src/lib/tree-sitter/assets.d.ts +9 -0
- package/src/lib/tree-sitter/cache.test.ts +291 -0
- package/src/lib/tree-sitter/client.test.ts +1434 -0
- package/src/lib/tree-sitter/client.ts +847 -0
- package/src/lib/tree-sitter/default-parsers.ts +133 -0
- package/src/lib/tree-sitter/download-utils.ts +148 -0
- package/src/lib/tree-sitter/index.ts +75 -0
- package/src/lib/tree-sitter/parser.worker.ts +1098 -0
- package/src/lib/tree-sitter/parsers-config.ts +81 -0
- package/src/lib/tree-sitter/resolve-ft.test.ts +55 -0
- package/src/lib/tree-sitter/resolve-ft.ts +189 -0
- package/src/lib/tree-sitter/types.ts +132 -0
- package/src/lib/tree-sitter/update-assets.ts +10 -0
- package/src/lib/tree-sitter-styled-text.test.ts +1253 -0
- package/src/lib/tree-sitter-styled-text.ts +314 -0
- package/src/lib/validate-dir-name.ts +55 -0
- package/src/lib/yoga.options.test.ts +628 -0
- package/src/lib/yoga.options.ts +346 -0
- package/src/native-event-worker-repro.test.ts +48 -0
- package/src/native-event-worker-repro.worker.ts +9 -0
- package/src/native-handle.test.ts +221 -0
- package/src/platform/ffi.test.ts +516 -0
- package/src/platform/ffi.ts +653 -0
- package/src/platform/runtime.test.ts +52 -0
- package/src/platform/runtime.ts +121 -0
- package/src/platform/test.ts +45 -0
- package/src/platform/worker-handler-cleanup.fixture.ts +12 -0
- package/src/platform/worker-onmessage-startup.fixture.ts +10 -0
- package/src/platform/worker-startup.fixture.ts +9 -0
- package/src/platform/worker.node-test.ts +153 -0
- package/src/platform/worker.ts +425 -0
- package/src/plugins/core-slot.ts +579 -0
- package/src/plugins/registry.ts +402 -0
- package/src/plugins/types.ts +46 -0
- package/src/post/effects.ts +948 -0
- package/src/post/filters.ts +512 -0
- package/src/post/matrices.ts +288 -0
- package/src/renderables/ASCIIFont.ts +219 -0
- package/src/renderables/Box.test.ts +474 -0
- package/src/renderables/Box.ts +353 -0
- package/src/renderables/Code.test.ts +2388 -0
- package/src/renderables/Code.ts +555 -0
- package/src/renderables/Diff.regression.test.ts +381 -0
- package/src/renderables/Diff.test.ts +3212 -0
- package/src/renderables/Diff.ts +1278 -0
- package/src/renderables/EditBufferRenderable.test.ts +288 -0
- package/src/renderables/EditBufferRenderable.ts +1152 -0
- package/src/renderables/FrameBuffer.ts +47 -0
- package/src/renderables/Input.test.ts +1310 -0
- package/src/renderables/Input.ts +273 -0
- package/src/renderables/LineNumberRenderable.ts +725 -0
- package/src/renderables/Markdown.ts +2144 -0
- package/src/renderables/ScrollBar.ts +423 -0
- package/src/renderables/ScrollBox.ts +903 -0
- package/src/renderables/Select.test.ts +1134 -0
- package/src/renderables/Select.ts +535 -0
- package/src/renderables/Slider.test.ts +456 -0
- package/src/renderables/Slider.ts +342 -0
- package/src/renderables/TabSelect.test.ts +238 -0
- package/src/renderables/TabSelect.ts +447 -0
- package/src/renderables/Text.selection-buffer.test.ts +118 -0
- package/src/renderables/Text.test.ts +2662 -0
- package/src/renderables/Text.ts +147 -0
- package/src/renderables/TextBufferRenderable.ts +504 -0
- package/src/renderables/TextNode.test.ts +1137 -0
- package/src/renderables/TextNode.ts +379 -0
- package/src/renderables/TextTable.test.ts +1411 -0
- package/src/renderables/TextTable.ts +1434 -0
- package/src/renderables/Textarea.ts +425 -0
- package/src/renderables/TimeToFirstDraw.ts +89 -0
- package/src/renderables/__snapshots__/Code.test.ts.snap +13 -0
- package/src/renderables/__snapshots__/Diff.test.ts.snap +785 -0
- package/src/renderables/__snapshots__/Text.test.ts.snap +421 -0
- package/src/renderables/__snapshots__/TextTable.test.ts.snap +215 -0
- package/src/renderables/__tests__/Code.test.ts +55 -0
- package/src/renderables/__tests__/LineNumberRenderable.scrollbox-simple.test.ts +144 -0
- package/src/renderables/__tests__/LineNumberRenderable.scrollbox.test.ts +827 -0
- package/src/renderables/__tests__/LineNumberRenderable.test.ts +1906 -0
- package/src/renderables/__tests__/LineNumberRenderable.wrapping.test.ts +85 -0
- package/src/renderables/__tests__/Markdown.code-colors.test.ts +288 -0
- package/src/renderables/__tests__/Markdown.test.ts +4175 -0
- package/src/renderables/__tests__/MultiRenderable.selection.test.ts +87 -0
- package/src/renderables/__tests__/Textarea.buffer.test.ts +683 -0
- package/src/renderables/__tests__/Textarea.destroyed-events.test.ts +675 -0
- package/src/renderables/__tests__/Textarea.editing.test.ts +2120 -0
- package/src/renderables/__tests__/Textarea.error-handling.test.ts +35 -0
- package/src/renderables/__tests__/Textarea.events.test.ts +738 -0
- package/src/renderables/__tests__/Textarea.highlights.test.ts +590 -0
- package/src/renderables/__tests__/Textarea.keybinding.test.ts +3275 -0
- package/src/renderables/__tests__/Textarea.paste.test.ts +357 -0
- package/src/renderables/__tests__/Textarea.rendering.test.ts +1927 -0
- package/src/renderables/__tests__/Textarea.scroll.test.ts +733 -0
- package/src/renderables/__tests__/Textarea.selection.test.ts +1589 -0
- package/src/renderables/__tests__/Textarea.stress.test.ts +670 -0
- package/src/renderables/__tests__/Textarea.undo-redo.test.ts +383 -0
- package/src/renderables/__tests__/Textarea.visual-lines.test.ts +310 -0
- package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.code.test.ts.snap +221 -0
- package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox-simple.test.ts.snap +89 -0
- package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.scrollbox.test.ts.snap +457 -0
- package/src/renderables/__tests__/__snapshots__/LineNumberRenderable.test.ts.snap +158 -0
- package/src/renderables/__tests__/__snapshots__/Textarea.rendering.test.ts.snap +387 -0
- package/src/renderables/__tests__/markdown-parser.test.ts +241 -0
- package/src/renderables/__tests__/renderable-test-utils.ts +60 -0
- package/src/renderables/composition/README.md +8 -0
- package/src/renderables/composition/VRenderable.ts +32 -0
- package/src/renderables/composition/constructs.ts +127 -0
- package/src/renderables/composition/vnode.ts +277 -0
- package/src/renderables/index.ts +23 -0
- package/src/renderables/markdown-parser.ts +79 -0
- package/src/renderer-theme-mode.ts +199 -0
- package/src/renderer.ts +4975 -0
- package/src/runtime-plugin-support-configure.ts +63 -0
- package/src/runtime-plugin-support.ts +13 -0
- package/src/runtime-plugin.ts +626 -0
- package/src/specs/terminal-startup.md +64 -0
- package/src/syntax-style.test.ts +841 -0
- package/src/syntax-style.ts +274 -0
- package/src/testing/README.md +219 -0
- package/src/testing/bun-test-node.ts +880 -0
- package/src/testing/capture-spans.test.ts +216 -0
- package/src/testing/integration.test.ts +276 -0
- package/src/testing/manual-clock.ts +117 -0
- package/src/testing/mock-keys.test.ts +1378 -0
- package/src/testing/mock-keys.ts +457 -0
- package/src/testing/mock-mouse.test.ts +218 -0
- package/src/testing/mock-mouse.ts +247 -0
- package/src/testing/mock-tree-sitter-client.ts +81 -0
- package/src/testing/spy.ts +13 -0
- package/src/testing/terminal-capabilities.ts +47 -0
- package/src/testing/test-recorder.test.ts +410 -0
- package/src/testing/test-recorder.ts +133 -0
- package/src/testing/test-renderer.ts +375 -0
- package/src/testing/test-renderer.wait.test.ts +278 -0
- package/src/testing/test-streams.ts +31 -0
- package/src/testing.ts +9 -0
- package/src/tests/__snapshots__/absolute-positioning.snapshot.test.ts.snap +481 -0
- package/src/tests/__snapshots__/renderable.snapshot.test.ts.snap +19 -0
- package/src/tests/__snapshots__/scrollbox.test.ts.snap +29 -0
- package/src/tests/absolute-positioning.snapshot.test.ts +638 -0
- package/src/tests/allocator-stats.test.ts +53 -0
- package/src/tests/audio.test.ts +303 -0
- package/src/tests/destroy-during-render.test.ts +200 -0
- package/src/tests/destroy-on-exit.fixture.ts +36 -0
- package/src/tests/destroy-on-exit.test.ts +62 -0
- package/src/tests/ffi-borrowed-pointer-callsites.test.ts +153 -0
- package/src/tests/hover-cursor.test.ts +98 -0
- package/src/tests/native-backed-measurement-lifecycle.test.ts +133 -0
- package/src/tests/native-backed-measurement-parity.test.ts +322 -0
- package/src/tests/native-span-feed-async.test.ts +173 -0
- package/src/tests/native-span-feed-close.test.ts +120 -0
- package/src/tests/native-span-feed-coverage.test.ts +227 -0
- package/src/tests/native-span-feed-edge-cases.test.ts +384 -0
- package/src/tests/native-span-feed-use-after-free.test.ts +45 -0
- package/src/tests/opacity.test.ts +123 -0
- package/src/tests/renderable.snapshot.test.ts +524 -0
- package/src/tests/renderable.test.ts +1851 -0
- package/src/tests/renderer.clock.test.ts +270 -0
- package/src/tests/renderer.console-startup.test.ts +2151 -0
- package/src/tests/renderer.control.test.ts +458 -0
- package/src/tests/renderer.core-slot-binding.test.ts +962 -0
- package/src/tests/renderer.cursor.test.ts +26 -0
- package/src/tests/renderer.custom-stdout.test.ts +1112 -0
- package/src/tests/renderer.destroy-during-render.test.ts +147 -0
- package/src/tests/renderer.focus-restore.test.ts +257 -0
- package/src/tests/renderer.focus.test.ts +329 -0
- package/src/tests/renderer.idle.test.ts +219 -0
- package/src/tests/renderer.input.test.ts +2615 -0
- package/src/tests/renderer.kitty-flags.test.ts +195 -0
- package/src/tests/renderer.lifecycle.test.ts +34 -0
- package/src/tests/renderer.mouse.test.ts +1315 -0
- package/src/tests/renderer.notifications.test.ts +27 -0
- package/src/tests/renderer.palette.test.ts +1199 -0
- package/src/tests/renderer.remote.test.ts +247 -0
- package/src/tests/renderer.render-stats.test.ts +146 -0
- package/src/tests/renderer.scrollback-surface.test.ts +474 -0
- package/src/tests/renderer.selection.test.ts +138 -0
- package/src/tests/renderer.slot-registry.test.ts +684 -0
- package/src/tests/renderer.tracker.test.ts +202 -0
- package/src/tests/renderer.useMouse.test.ts +83 -0
- package/src/tests/runtime-plugin-node-modules-cycle.fixture.ts +76 -0
- package/src/tests/runtime-plugin-node-modules-import-like-string.fixture.ts +48 -0
- package/src/tests/runtime-plugin-node-modules-mjs.fixture.ts +43 -0
- package/src/tests/runtime-plugin-node-modules-no-bare-rewrite.fixture.ts +67 -0
- package/src/tests/runtime-plugin-node-modules-package-type-cache.fixture.ts +72 -0
- package/src/tests/runtime-plugin-node-modules-runtime-specifier.fixture.ts +44 -0
- package/src/tests/runtime-plugin-node-modules-scoped-package-bare-rewrite.fixture.ts +85 -0
- package/src/tests/runtime-plugin-path-alias.fixture.ts +43 -0
- package/src/tests/runtime-plugin-resolve-roots.fixture.ts +65 -0
- package/src/tests/runtime-plugin-support-configure.fixture.ts +31 -0
- package/src/tests/runtime-plugin-support-late-addition.fixture.ts +17 -0
- package/src/tests/runtime-plugin-support.fixture.ts +11 -0
- package/src/tests/runtime-plugin-support.test.ts +54 -0
- package/src/tests/runtime-plugin-windows-file-url.fixture.ts +30 -0
- package/src/tests/runtime-plugin.fixture.ts +40 -0
- package/src/tests/runtime-plugin.test.ts +369 -0
- package/src/tests/scrollbox-culling-bug.test.ts +114 -0
- package/src/tests/scrollbox-hitgrid-resize.test.ts +136 -0
- package/src/tests/scrollbox-hitgrid.test.ts +909 -0
- package/src/tests/scrollbox.test.ts +1895 -0
- package/src/tests/wrap-resize-perf.test.ts +276 -0
- package/src/tests/yoga-api-coverage.test.ts +409 -0
- package/src/tests/yoga-callback-stress.test.ts +122 -0
- package/src/tests/yoga-ffi.test.ts +117 -0
- package/src/tests/yoga-setters.test.ts +921 -0
- package/src/tests/yoga-upstream/YGAlignBaselineTest.test.ts +124 -0
- package/src/tests/yoga-upstream/YGComputedBorderTest.test.ts +27 -0
- package/src/tests/yoga-upstream/YGComputedMarginTest.test.ts +27 -0
- package/src/tests/yoga-upstream/YGComputedPaddingTest.test.ts +27 -0
- package/src/tests/yoga-upstream/YGDirtiedTest.test.ts +157 -0
- package/src/tests/yoga-upstream/YGErrataTest.test.ts +40 -0
- package/src/tests/yoga-upstream/YGFlexBasisAuto.test.ts +23 -0
- package/src/tests/yoga-upstream/YGHasNewLayout.test.ts +81 -0
- package/src/tests/yoga-upstream/YGMeasureCacheTest.test.ts +31 -0
- package/src/tests/yoga-upstream/YGMeasureTest.test.ts +70 -0
- package/src/tests/yoga-upstream/generated/YGAbsolutePositionTest.test.ts +1864 -0
- package/src/tests/yoga-upstream/generated/YGAlignContentTest.test.ts +5829 -0
- package/src/tests/yoga-upstream/generated/YGAlignItemsTest.test.ts +2444 -0
- package/src/tests/yoga-upstream/generated/YGAlignSelfTest.test.ts +293 -0
- package/src/tests/yoga-upstream/generated/YGAndroidNewsFeed.test.ts +315 -0
- package/src/tests/yoga-upstream/generated/YGAspectRatioTest.test.ts +231 -0
- package/src/tests/yoga-upstream/generated/YGAutoTest.test.ts +347 -0
- package/src/tests/yoga-upstream/generated/YGBorderTest.test.ts +256 -0
- package/src/tests/yoga-upstream/generated/YGBoxSizingTest.test.ts +2747 -0
- package/src/tests/yoga-upstream/generated/YGDimensionTest.test.ts +129 -0
- package/src/tests/yoga-upstream/generated/YGDisplayContentsTest.test.ts +109 -0
- package/src/tests/yoga-upstream/generated/YGDisplayTest.test.ts +1097 -0
- package/src/tests/yoga-upstream/generated/YGFlexDirectionTest.test.ts +4562 -0
- package/src/tests/yoga-upstream/generated/YGFlexTest.test.ts +677 -0
- package/src/tests/yoga-upstream/generated/YGFlexWrapTest.test.ts +2071 -0
- package/src/tests/yoga-upstream/generated/YGGapTest.test.ts +3303 -0
- package/src/tests/yoga-upstream/generated/YGIntrinsicSizeTest.test.ts +582 -0
- package/src/tests/yoga-upstream/generated/YGJustifyContentTest.test.ts +2150 -0
- package/src/tests/yoga-upstream/generated/YGMarginTest.test.ts +1963 -0
- package/src/tests/yoga-upstream/generated/YGMinMaxDimensionTest.test.ts +1446 -0
- package/src/tests/yoga-upstream/generated/YGPaddingTest.test.ts +354 -0
- package/src/tests/yoga-upstream/generated/YGPercentageTest.test.ts +1760 -0
- package/src/tests/yoga-upstream/generated/YGRoundingTest.test.ts +1161 -0
- package/src/tests/yoga-upstream/generated/YGSizeOverflowTest.test.ts +207 -0
- package/src/tests/yoga-upstream/generated/YGStaticPositionTest.test.ts +6338 -0
- package/src/tests/yoga-upstream/tools/MeasureCounter.ts +54 -0
- package/src/tests/yoga-upstream/tools/utils.ts +81 -0
- package/src/text-buffer-view.test.ts +722 -0
- package/src/text-buffer-view.ts +193 -0
- package/src/text-buffer.test.ts +512 -0
- package/src/text-buffer.ts +250 -0
- package/src/types.ts +199 -0
- package/src/utils.ts +88 -0
- package/src/yoga.ts +1146 -0
- package/src/zig/ansi.zig +502 -0
- package/src/zig/audio.zig +1206 -0
- package/src/zig/bench/README.md +50 -0
- package/src/zig/bench/buffer-color-blending_bench.zig +350 -0
- package/src/zig/bench/buffer-draw-box_bench.zig +786 -0
- package/src/zig/bench/buffer-draw-text-buffer_bench.zig +894 -0
- package/src/zig/bench/edit-buffer_bench.zig +476 -0
- package/src/zig/bench/editor-view_bench.zig +169 -0
- package/src/zig/bench/native-span-feed_bench.zig +107 -0
- package/src/zig/bench/rope-markers_bench.zig +713 -0
- package/src/zig/bench/rope_bench.zig +514 -0
- package/src/zig/bench/styled-text_bench.zig +476 -0
- package/src/zig/bench/text-buffer-coords_bench.zig +362 -0
- package/src/zig/bench/text-buffer-view_bench.zig +459 -0
- package/src/zig/bench/text-chunk-graphemes_bench.zig +272 -0
- package/src/zig/bench/utf8_bench.zig +799 -0
- package/src/zig/bench-utils.zig +431 -0
- package/src/zig/bench.zig +223 -0
- package/src/zig/buffer-methods.zig +215 -0
- package/src/zig/buffer.zig +2515 -0
- package/src/zig/build.zig +700 -0
- package/src/zig/build.zig.zon +19 -0
- package/src/zig/edit-buffer.zig +900 -0
- package/src/zig/editor-view.zig +812 -0
- package/src/zig/event-bus.zig +28 -0
- package/src/zig/event-emitter.zig +65 -0
- package/src/zig/file-logger.zig +92 -0
- package/src/zig/grapheme.zig +838 -0
- package/src/zig/handles.zig +267 -0
- package/src/zig/lib.zig +2813 -0
- package/src/zig/link.zig +349 -0
- package/src/zig/logger.zig +43 -0
- package/src/zig/mem-registry.zig +126 -0
- package/src/zig/miniaudio_shim.c +21 -0
- package/src/zig/native-renderable.zig +112 -0
- package/src/zig/native-span-feed-bench-lib.zig +7 -0
- package/src/zig/native-span-feed.zig +747 -0
- package/src/zig/renderer-output.zig +677 -0
- package/src/zig/renderer.zig +2106 -0
- package/src/zig/rope.zig +1220 -0
- package/src/zig/split-scrollback.zig +88 -0
- package/src/zig/syntax-style.zig +174 -0
- package/src/zig/terminal.zig +1652 -0
- package/src/zig/test.zig +82 -0
- package/src/zig/tests/README.md +18 -0
- package/src/zig/tests/ansi_test.zig +18 -0
- package/src/zig/tests/audio_test.zig +439 -0
- package/src/zig/tests/buffer-methods_test.zig +1111 -0
- package/src/zig/tests/buffer_test.zig +2926 -0
- package/src/zig/tests/edit-buffer-history_test.zig +299 -0
- package/src/zig/tests/edit-buffer_test.zig +1688 -0
- package/src/zig/tests/editor-view_test.zig +3454 -0
- package/src/zig/tests/event-emitter_test.zig +249 -0
- package/src/zig/tests/grapheme_test.zig +1329 -0
- package/src/zig/tests/handles_test.zig +123 -0
- package/src/zig/tests/link_test.zig +220 -0
- package/src/zig/tests/mem-registry_test.zig +473 -0
- package/src/zig/tests/memory_leak_regression_test.zig +234 -0
- package/src/zig/tests/native-renderable_test.zig +16 -0
- package/src/zig/tests/native-span-feed_test.zig +1266 -0
- package/src/zig/tests/renderer_test.zig +2604 -0
- package/src/zig/tests/rope-nested_test.zig +712 -0
- package/src/zig/tests/rope_fuzz_test.zig +238 -0
- package/src/zig/tests/rope_test.zig +2362 -0
- package/src/zig/tests/segment-merge.test.zig +148 -0
- package/src/zig/tests/split-scrollback_test.zig +70 -0
- package/src/zig/tests/syntax-style_test.zig +558 -0
- package/src/zig/tests/terminal_test.zig +1489 -0
- package/src/zig/tests/test-renderer.zig +126 -0
- package/src/zig/tests/text-buffer-drawing_test.zig +3291 -0
- package/src/zig/tests/text-buffer-highlights_test.zig +706 -0
- package/src/zig/tests/text-buffer-iterators_test.zig +776 -0
- package/src/zig/tests/text-buffer-segment_test.zig +320 -0
- package/src/zig/tests/text-buffer-selection_test.zig +1056 -0
- package/src/zig/tests/text-buffer-selection_viewport_test.zig +358 -0
- package/src/zig/tests/text-buffer-view_test.zig +3672 -0
- package/src/zig/tests/text-buffer_test.zig +2191 -0
- package/src/zig/tests/unicode-width-map.zon +3909 -0
- package/src/zig/tests/utf8_no_zwj_test.zig +260 -0
- package/src/zig/tests/utf8_test.zig +4060 -0
- package/src/zig/tests/utf8_wcwidth_cursor_test.zig +267 -0
- package/src/zig/tests/utf8_wcwidth_test.zig +357 -0
- package/src/zig/tests/word-wrap-editing_test.zig +498 -0
- package/src/zig/tests/wrap-cache-perf_test.zig +113 -0
- package/src/zig/tests/yoga_test.zig +65 -0
- package/src/zig/text-buffer-iterators.zig +496 -0
- package/src/zig/text-buffer-segment.zig +403 -0
- package/src/zig/text-buffer-view.zig +1428 -0
- package/src/zig/text-buffer.zig +1272 -0
- package/src/zig/utf8.zig +1954 -0
- package/src/zig/utils.zig +12 -0
- package/src/zig/uucode-stub.zig +76 -0
- package/src/zig/vendor/miniaudio/miniaudio.h +93468 -0
- package/src/zig/yoga.zig +658 -0
- package/src/zig-structs.ts +381 -0
- package/src/zig.ts +5260 -0
- package/tsconfig.build.json +17 -0
- package/tsconfig.json +27 -0
- package/tsconfig.node-test.json +138 -0
|
@@ -0,0 +1,2547 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
// Calibrated layout benchmark for the current JS Yoga-backed render tree.
|
|
4
|
+
// Scenarios deliberately mutate layout-affecting state before each measured
|
|
5
|
+
// operation, then validate that Yoga is dirty before the frame and clean after
|
|
6
|
+
// all required render passes settle.
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
9
|
+
import path from "node:path"
|
|
10
|
+
|
|
11
|
+
import { BoxRenderable, RGBA, ScrollBoxRenderable, TextRenderable } from "../index.js"
|
|
12
|
+
import type { Renderable, RenderCommand } from "../Renderable.js"
|
|
13
|
+
import { createTestRenderer, type TestRenderer } from "../testing.js"
|
|
14
|
+
|
|
15
|
+
const DEFAULT_ITERATIONS = 100
|
|
16
|
+
const DEFAULT_WARMUP = 50
|
|
17
|
+
const DEFAULT_ROUNDS = 7
|
|
18
|
+
const DEFAULT_MIN_SAMPLE_MS = 500
|
|
19
|
+
const DEFAULT_WIDTH = 140
|
|
20
|
+
const DEFAULT_HEIGHT = 44
|
|
21
|
+
const MAX_LAYOUT_SETTLE_PASSES = 4
|
|
22
|
+
|
|
23
|
+
type BenchmarkKind =
|
|
24
|
+
| "baseline"
|
|
25
|
+
| "layout-recalc"
|
|
26
|
+
| "text-measure"
|
|
27
|
+
| "tree-mutation"
|
|
28
|
+
| "scrollbox"
|
|
29
|
+
| "top-branch"
|
|
30
|
+
| "wide-shallow"
|
|
31
|
+
| "deep-chain"
|
|
32
|
+
| "root-resize"
|
|
33
|
+
type BenchmarkPhase = "calculate-layout" | "layout-update" | "full-render"
|
|
34
|
+
type RenderPassMode = "calculate-only" | "calculate-update" | "single-frame" | "settle-layout"
|
|
35
|
+
|
|
36
|
+
interface BenchmarkArgs {
|
|
37
|
+
iterations: number
|
|
38
|
+
warmupIterations: number
|
|
39
|
+
rounds: number
|
|
40
|
+
minSampleMs: number
|
|
41
|
+
width: number
|
|
42
|
+
height: number
|
|
43
|
+
scenarioNames?: Set<string>
|
|
44
|
+
jsonPath?: string
|
|
45
|
+
listScenarios: boolean
|
|
46
|
+
output: boolean
|
|
47
|
+
skipValidation: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface BenchmarkContext {
|
|
51
|
+
renderer: TestRenderer
|
|
52
|
+
renderOnce: () => Promise<void>
|
|
53
|
+
width: number
|
|
54
|
+
height: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface TreeStats {
|
|
58
|
+
renderables: number
|
|
59
|
+
layoutNodes: number
|
|
60
|
+
layoutOnlyBoxes: number
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface ScenarioRuntime {
|
|
64
|
+
kind: BenchmarkKind
|
|
65
|
+
phase: BenchmarkPhase
|
|
66
|
+
passMode: RenderPassMode
|
|
67
|
+
renderablesPerIteration: number
|
|
68
|
+
layoutNodesPerIteration: number
|
|
69
|
+
layoutOnlyBoxesPerIteration: number
|
|
70
|
+
layoutMutationsPerIteration: number
|
|
71
|
+
runIteration: (iteration: number) => unknown | Promise<unknown>
|
|
72
|
+
validate?: () => void | Promise<void>
|
|
73
|
+
resetMetrics?: () => void
|
|
74
|
+
readMetrics?: () => ScenarioMetricsSnapshot
|
|
75
|
+
cleanup: () => void | Promise<void>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface ScenarioMetricsSnapshot {
|
|
79
|
+
measuredIterations: number
|
|
80
|
+
totalSettlePasses: number
|
|
81
|
+
maxSettlePasses: number
|
|
82
|
+
totalRenderCommands: number
|
|
83
|
+
maxRenderCommands: number
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface ScenarioMetricsTracker {
|
|
87
|
+
recordSettlePasses: (passes: number) => void
|
|
88
|
+
recordRenderCommands: (commands: number) => void
|
|
89
|
+
reset: () => void
|
|
90
|
+
snapshot: () => ScenarioMetricsSnapshot
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface BenchmarkScenario {
|
|
94
|
+
name: string
|
|
95
|
+
description: string
|
|
96
|
+
setup: (ctx: BenchmarkContext) => Promise<ScenarioRuntime>
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface BenchmarkSample {
|
|
100
|
+
round: number
|
|
101
|
+
iterations: number
|
|
102
|
+
durationMs: number
|
|
103
|
+
opsPerSecond: number
|
|
104
|
+
nsPerOperation: number
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface BenchmarkResult {
|
|
108
|
+
name: string
|
|
109
|
+
description: string
|
|
110
|
+
kind: BenchmarkKind
|
|
111
|
+
phase: BenchmarkPhase
|
|
112
|
+
passMode: RenderPassMode
|
|
113
|
+
iterations: number
|
|
114
|
+
warmupIterations: number
|
|
115
|
+
rounds: number
|
|
116
|
+
minSampleMs: number
|
|
117
|
+
batchIterations: number
|
|
118
|
+
totalMeasuredIterations: number
|
|
119
|
+
renderablesPerIteration: number
|
|
120
|
+
layoutNodesPerIteration: number
|
|
121
|
+
layoutOnlyBoxesPerIteration: number
|
|
122
|
+
layoutMutationsPerIteration: number
|
|
123
|
+
measuredMetricIterations: number
|
|
124
|
+
avgSettlePassesPerIteration: number
|
|
125
|
+
maxSettlePasses: number
|
|
126
|
+
avgUpdateCommandsPerIteration: number | null
|
|
127
|
+
maxUpdateCommands: number | null
|
|
128
|
+
medianDurationMs: number
|
|
129
|
+
bestDurationMs: number
|
|
130
|
+
medianOpsPerSecond: number
|
|
131
|
+
meanOpsPerSecond: number
|
|
132
|
+
medianNsPerOperation: number
|
|
133
|
+
p95NsPerOperation: number
|
|
134
|
+
stdDevNsPerOperation: number
|
|
135
|
+
rmePercent: number
|
|
136
|
+
samples: BenchmarkSample[]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface BenchmarkSinkState {
|
|
140
|
+
value: unknown
|
|
141
|
+
checksum: number
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface OpencodeLayoutTreeState {
|
|
145
|
+
root: BoxRenderable
|
|
146
|
+
header: BoxRenderable
|
|
147
|
+
body: BoxRenderable
|
|
148
|
+
sidebar: BoxRenderable
|
|
149
|
+
main: BoxRenderable
|
|
150
|
+
footer: BoxRenderable
|
|
151
|
+
rows: BoxRenderable[]
|
|
152
|
+
badges: BoxRenderable[]
|
|
153
|
+
stats: TreeStats
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface WideShallowTreeState {
|
|
157
|
+
root: BoxRenderable
|
|
158
|
+
container: BoxRenderable
|
|
159
|
+
children: BoxRenderable[]
|
|
160
|
+
stats: TreeStats
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface DeepChainTreeState {
|
|
164
|
+
root: BoxRenderable
|
|
165
|
+
nodes: BoxRenderable[]
|
|
166
|
+
leaf: BoxRenderable
|
|
167
|
+
stats: TreeStats
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface TextReflowTreeState {
|
|
171
|
+
root: BoxRenderable
|
|
172
|
+
rows: BoxRenderable[]
|
|
173
|
+
texts: TextRenderable[]
|
|
174
|
+
stats: TreeStats
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface TreeMutationState {
|
|
178
|
+
root: BoxRenderable
|
|
179
|
+
list: BoxRenderable
|
|
180
|
+
rows: BoxRenderable[]
|
|
181
|
+
stats: TreeStats
|
|
182
|
+
nextRowId: number
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
interface ScrollboxReflowState {
|
|
186
|
+
root: BoxRenderable
|
|
187
|
+
scrollBox: ScrollBoxRenderable
|
|
188
|
+
items: BoxRenderable[]
|
|
189
|
+
stats: TreeStats
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface ValidationOptions {
|
|
193
|
+
expectProbeChange?: boolean
|
|
194
|
+
minimumSettlePasses?: number
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const blackhole: BenchmarkSinkState = {
|
|
198
|
+
value: undefined,
|
|
199
|
+
checksum: 0,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const COLORS = {
|
|
203
|
+
transparent: RGBA.fromInts(0, 0, 0, 0),
|
|
204
|
+
panel: RGBA.fromInts(28, 32, 38),
|
|
205
|
+
element: RGBA.fromInts(40, 46, 56),
|
|
206
|
+
menu: RGBA.fromInts(35, 40, 48),
|
|
207
|
+
accent: RGBA.fromInts(84, 171, 224),
|
|
208
|
+
warning: RGBA.fromInts(219, 186, 96),
|
|
209
|
+
} as const
|
|
210
|
+
|
|
211
|
+
function parseNumberArg(value: string | undefined, fallback: number): number {
|
|
212
|
+
if (!value) {
|
|
213
|
+
return fallback
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const parsed = Number.parseInt(value, 10)
|
|
217
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
218
|
+
throw new Error(`Invalid numeric benchmark argument: ${value}`)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return parsed
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function parseArgs(argv: string[]): BenchmarkArgs {
|
|
225
|
+
let iterations = DEFAULT_ITERATIONS
|
|
226
|
+
let warmupIterations = DEFAULT_WARMUP
|
|
227
|
+
let rounds = DEFAULT_ROUNDS
|
|
228
|
+
let minSampleMs = DEFAULT_MIN_SAMPLE_MS
|
|
229
|
+
let width = DEFAULT_WIDTH
|
|
230
|
+
let height = DEFAULT_HEIGHT
|
|
231
|
+
let scenarioNames: Set<string> | undefined
|
|
232
|
+
let jsonPath: string | undefined
|
|
233
|
+
let listScenarios = false
|
|
234
|
+
let output = true
|
|
235
|
+
let skipValidation = false
|
|
236
|
+
|
|
237
|
+
for (const arg of argv) {
|
|
238
|
+
if (arg === "--list-scenarios") {
|
|
239
|
+
listScenarios = true
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (arg === "--no-output") {
|
|
244
|
+
output = false
|
|
245
|
+
continue
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (arg === "--skip-validation") {
|
|
249
|
+
skipValidation = true
|
|
250
|
+
continue
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (arg === "--json") {
|
|
254
|
+
jsonPath = "latest-layout-bench-run.json"
|
|
255
|
+
continue
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (arg.startsWith("--iterations=")) {
|
|
259
|
+
iterations = parseNumberArg(arg.slice("--iterations=".length), DEFAULT_ITERATIONS)
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (arg.startsWith("--warmup=")) {
|
|
264
|
+
warmupIterations = parseNumberArg(arg.slice("--warmup=".length), DEFAULT_WARMUP)
|
|
265
|
+
continue
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (arg.startsWith("--warmup-iterations=")) {
|
|
269
|
+
warmupIterations = parseNumberArg(arg.slice("--warmup-iterations=".length), DEFAULT_WARMUP)
|
|
270
|
+
continue
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (arg.startsWith("--rounds=")) {
|
|
274
|
+
rounds = parseNumberArg(arg.slice("--rounds=".length), DEFAULT_ROUNDS)
|
|
275
|
+
continue
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (arg.startsWith("--min-sample-ms=")) {
|
|
279
|
+
minSampleMs = parseNumberArg(arg.slice("--min-sample-ms=".length), DEFAULT_MIN_SAMPLE_MS)
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (arg.startsWith("--width=")) {
|
|
284
|
+
width = Math.max(40, parseNumberArg(arg.slice("--width=".length), DEFAULT_WIDTH))
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (arg.startsWith("--height=")) {
|
|
289
|
+
height = Math.max(20, parseNumberArg(arg.slice("--height=".length), DEFAULT_HEIGHT))
|
|
290
|
+
continue
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (arg.startsWith("--scenario=")) {
|
|
294
|
+
const names = arg
|
|
295
|
+
.slice("--scenario=".length)
|
|
296
|
+
.split(",")
|
|
297
|
+
.map((name) => name.trim())
|
|
298
|
+
.filter(Boolean)
|
|
299
|
+
scenarioNames = new Set(names)
|
|
300
|
+
continue
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (arg.startsWith("--json=")) {
|
|
304
|
+
const value = arg.slice("--json=".length)
|
|
305
|
+
jsonPath = value || "latest-layout-bench-run.json"
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
iterations,
|
|
311
|
+
warmupIterations,
|
|
312
|
+
rounds,
|
|
313
|
+
minSampleMs,
|
|
314
|
+
width,
|
|
315
|
+
height,
|
|
316
|
+
scenarioNames,
|
|
317
|
+
jsonPath,
|
|
318
|
+
listScenarios,
|
|
319
|
+
output,
|
|
320
|
+
skipValidation,
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function nowNs(): bigint {
|
|
325
|
+
return process.hrtime.bigint()
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function nsToMs(ns: bigint): number {
|
|
329
|
+
return Number(ns) / 1_000_000
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function median(values: number[]): number {
|
|
333
|
+
const sorted = [...values].sort((left, right) => left - right)
|
|
334
|
+
const middle = Math.floor(sorted.length / 2)
|
|
335
|
+
const value = sorted[middle]
|
|
336
|
+
if (value === undefined) {
|
|
337
|
+
return 0
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (sorted.length % 2 === 1) {
|
|
341
|
+
return value
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const previous = sorted[middle - 1]
|
|
345
|
+
if (previous === undefined) {
|
|
346
|
+
return value
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return (previous + value) / 2
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function mean(values: readonly number[]): number {
|
|
353
|
+
if (values.length === 0) {
|
|
354
|
+
return 0
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let total = 0
|
|
358
|
+
for (const value of values) {
|
|
359
|
+
total += value
|
|
360
|
+
}
|
|
361
|
+
return total / values.length
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function percentile(values: readonly number[], p: number): number {
|
|
365
|
+
if (values.length === 0) {
|
|
366
|
+
return 0
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const sorted = [...values].sort((left, right) => left - right)
|
|
370
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1))
|
|
371
|
+
return sorted[index] ?? 0
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function sampleStdDev(values: readonly number[]): number {
|
|
375
|
+
if (values.length <= 1) {
|
|
376
|
+
return 0
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const average = mean(values)
|
|
380
|
+
let total = 0
|
|
381
|
+
for (const value of values) {
|
|
382
|
+
const delta = value - average
|
|
383
|
+
total += delta * delta
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return Math.sqrt(total / (values.length - 1))
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function tCritical95(degreesOfFreedom: number): number {
|
|
390
|
+
const table = [12.706, 4.303, 3.182, 2.776, 2.571, 2.447, 2.365, 2.306, 2.262, 2.228]
|
|
391
|
+
if (degreesOfFreedom <= 0) {
|
|
392
|
+
return 0
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return table[degreesOfFreedom - 1] ?? 1.96
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function relativeMarginOfError(values: readonly number[]): number {
|
|
399
|
+
if (values.length <= 1) {
|
|
400
|
+
return 0
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const average = mean(values)
|
|
404
|
+
if (average === 0) {
|
|
405
|
+
return 0
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const sem = sampleStdDev(values) / Math.sqrt(values.length)
|
|
409
|
+
return Math.abs((sem * tCritical95(values.length - 1) * 100) / average)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function consume(value: unknown): void {
|
|
413
|
+
blackhole.value = value
|
|
414
|
+
|
|
415
|
+
let contribution = 1
|
|
416
|
+
if (typeof value === "number") {
|
|
417
|
+
contribution = value | 0
|
|
418
|
+
} else if (typeof value === "string" || Array.isArray(value)) {
|
|
419
|
+
contribution = value.length
|
|
420
|
+
} else if (value instanceof Map || value instanceof Set) {
|
|
421
|
+
contribution = value.size
|
|
422
|
+
} else if (typeof value === "boolean") {
|
|
423
|
+
contribution = value ? 1 : 0
|
|
424
|
+
} else if (value === null || value === undefined) {
|
|
425
|
+
contribution = 0
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
blackhole.checksum = (blackhole.checksum + contribution) >>> 0
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function roundIterations(value: number): number {
|
|
432
|
+
if (value <= 100) {
|
|
433
|
+
return Math.max(1, Math.ceil(value))
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (value <= 1_000) {
|
|
437
|
+
return Math.ceil(value / 10) * 10
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (value <= 10_000) {
|
|
441
|
+
return Math.ceil(value / 100) * 100
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return Math.ceil(value / 1_000) * 1_000
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function createMetricsTracker(): ScenarioMetricsTracker {
|
|
448
|
+
const metrics: ScenarioMetricsSnapshot = {
|
|
449
|
+
measuredIterations: 0,
|
|
450
|
+
totalSettlePasses: 0,
|
|
451
|
+
maxSettlePasses: 0,
|
|
452
|
+
totalRenderCommands: 0,
|
|
453
|
+
maxRenderCommands: 0,
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
recordSettlePasses(passes) {
|
|
458
|
+
metrics.measuredIterations += 1
|
|
459
|
+
metrics.totalSettlePasses += passes
|
|
460
|
+
metrics.maxSettlePasses = Math.max(metrics.maxSettlePasses, passes)
|
|
461
|
+
},
|
|
462
|
+
recordRenderCommands(commands) {
|
|
463
|
+
metrics.totalRenderCommands += commands
|
|
464
|
+
metrics.maxRenderCommands = Math.max(metrics.maxRenderCommands, commands)
|
|
465
|
+
},
|
|
466
|
+
reset() {
|
|
467
|
+
metrics.measuredIterations = 0
|
|
468
|
+
metrics.totalSettlePasses = 0
|
|
469
|
+
metrics.maxSettlePasses = 0
|
|
470
|
+
metrics.totalRenderCommands = 0
|
|
471
|
+
metrics.maxRenderCommands = 0
|
|
472
|
+
},
|
|
473
|
+
snapshot() {
|
|
474
|
+
return { ...metrics }
|
|
475
|
+
},
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function withMetrics(
|
|
480
|
+
runtime: Omit<ScenarioRuntime, "resetMetrics" | "readMetrics">,
|
|
481
|
+
metrics: ScenarioMetricsTracker,
|
|
482
|
+
): ScenarioRuntime {
|
|
483
|
+
return {
|
|
484
|
+
...runtime,
|
|
485
|
+
resetMetrics: metrics.reset,
|
|
486
|
+
readMetrics: metrics.snapshot,
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function emptyMetrics(): ScenarioMetricsSnapshot {
|
|
491
|
+
return {
|
|
492
|
+
measuredIterations: 0,
|
|
493
|
+
totalSettlePasses: 0,
|
|
494
|
+
maxSettlePasses: 0,
|
|
495
|
+
totalRenderCommands: 0,
|
|
496
|
+
maxRenderCommands: 0,
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function isRootLayoutDirty(ctx: BenchmarkContext): boolean {
|
|
501
|
+
return ctx.renderer.root.getLayoutNode().isDirty()
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function assertLayoutDirty(ctx: BenchmarkContext, scenarioName: string): void {
|
|
505
|
+
if (!isRootLayoutDirty(ctx)) {
|
|
506
|
+
throw new Error(`${scenarioName}: expected root Yoga node to be dirty after the layout mutation`)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function assertLayoutClean(ctx: BenchmarkContext, scenarioName: string): void {
|
|
511
|
+
if (isRootLayoutDirty(ctx)) {
|
|
512
|
+
throw new Error(`${scenarioName}: expected root Yoga node to be clean after layout settling`)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function renderUntilLayoutClean(ctx: BenchmarkContext, scenarioName: string): Promise<number> {
|
|
517
|
+
let passes = 0
|
|
518
|
+
|
|
519
|
+
do {
|
|
520
|
+
passes += 1
|
|
521
|
+
await ctx.renderOnce()
|
|
522
|
+
} while (isRootLayoutDirty(ctx) && passes < MAX_LAYOUT_SETTLE_PASSES)
|
|
523
|
+
|
|
524
|
+
if (isRootLayoutDirty(ctx)) {
|
|
525
|
+
throw new Error(`${scenarioName}: Yoga layout stayed dirty after ${passes} render passes`)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return passes
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function renderMeasuredUntilLayoutClean(
|
|
532
|
+
ctx: BenchmarkContext,
|
|
533
|
+
scenarioName: string,
|
|
534
|
+
metrics: ScenarioMetricsTracker,
|
|
535
|
+
): Promise<number> {
|
|
536
|
+
const passes = await renderUntilLayoutClean(ctx, scenarioName)
|
|
537
|
+
metrics.recordSettlePasses(passes)
|
|
538
|
+
return passes
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function calculateLayout(ctx: BenchmarkContext, scenarioName: string): number {
|
|
542
|
+
ctx.renderer.root.calculateLayout()
|
|
543
|
+
assertLayoutClean(ctx, scenarioName)
|
|
544
|
+
return 1
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function advanceLayoutFrame(ctx: BenchmarkContext): void {
|
|
548
|
+
;(ctx.renderer as unknown as { _frameId: number })._frameId += 1
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function calculateAndUpdateLayout(
|
|
552
|
+
ctx: BenchmarkContext,
|
|
553
|
+
scenarioName: string,
|
|
554
|
+
metrics: ScenarioMetricsTracker,
|
|
555
|
+
): number {
|
|
556
|
+
calculateLayout(ctx, scenarioName)
|
|
557
|
+
advanceLayoutFrame(ctx)
|
|
558
|
+
|
|
559
|
+
const renderList: RenderCommand[] = []
|
|
560
|
+
ctx.renderer.root.updateLayout(0, renderList)
|
|
561
|
+
assertLayoutClean(ctx, scenarioName)
|
|
562
|
+
|
|
563
|
+
metrics.recordSettlePasses(1)
|
|
564
|
+
metrics.recordRenderCommands(renderList.length)
|
|
565
|
+
return renderList.length
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function assertProbeChanged(scenarioName: string, label: string, before: number, after: number): void {
|
|
569
|
+
if (before === after) {
|
|
570
|
+
throw new Error(`${scenarioName}: ${label} did not change the validation probe`)
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async function validateFullRenderRecalculation(
|
|
575
|
+
ctx: BenchmarkContext,
|
|
576
|
+
scenarioName: string,
|
|
577
|
+
mutate: (iteration: number) => unknown,
|
|
578
|
+
readProbe: () => number,
|
|
579
|
+
options: ValidationOptions = {},
|
|
580
|
+
): Promise<void> {
|
|
581
|
+
await renderUntilLayoutClean(ctx, scenarioName)
|
|
582
|
+
assertLayoutClean(ctx, scenarioName)
|
|
583
|
+
|
|
584
|
+
const before = readProbe()
|
|
585
|
+
consume(mutate(0))
|
|
586
|
+
assertLayoutDirty(ctx, scenarioName)
|
|
587
|
+
const firstPasses = await renderUntilLayoutClean(ctx, scenarioName)
|
|
588
|
+
const afterFirst = readProbe()
|
|
589
|
+
|
|
590
|
+
consume(mutate(1))
|
|
591
|
+
assertLayoutDirty(ctx, scenarioName)
|
|
592
|
+
const secondPasses = await renderUntilLayoutClean(ctx, scenarioName)
|
|
593
|
+
const afterSecond = readProbe()
|
|
594
|
+
|
|
595
|
+
const minimumSettlePasses = options.minimumSettlePasses ?? 1
|
|
596
|
+
if (firstPasses < minimumSettlePasses || secondPasses < minimumSettlePasses) {
|
|
597
|
+
throw new Error(`${scenarioName}: expected at least ${minimumSettlePasses} render pass per layout update`)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (options.expectProbeChange !== false) {
|
|
601
|
+
assertProbeChanged(scenarioName, "first layout mutation", before, afterFirst)
|
|
602
|
+
assertProbeChanged(scenarioName, "second layout mutation", afterFirst, afterSecond)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
consume(before + afterFirst + afterSecond + firstPasses + secondPasses)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async function validateContentRecalculation(
|
|
609
|
+
ctx: BenchmarkContext,
|
|
610
|
+
scenarioName: string,
|
|
611
|
+
mutate: (iteration: number) => unknown,
|
|
612
|
+
readContentProbe: () => number,
|
|
613
|
+
): Promise<void> {
|
|
614
|
+
await renderUntilLayoutClean(ctx, scenarioName)
|
|
615
|
+
assertLayoutClean(ctx, scenarioName)
|
|
616
|
+
|
|
617
|
+
const before = readContentProbe()
|
|
618
|
+
consume(mutate(0))
|
|
619
|
+
assertLayoutDirty(ctx, scenarioName)
|
|
620
|
+
const firstPasses = await renderUntilLayoutClean(ctx, scenarioName)
|
|
621
|
+
const afterFirst = readContentProbe()
|
|
622
|
+
|
|
623
|
+
consume(mutate(1))
|
|
624
|
+
assertLayoutDirty(ctx, scenarioName)
|
|
625
|
+
const secondPasses = await renderUntilLayoutClean(ctx, scenarioName)
|
|
626
|
+
const afterSecond = readContentProbe()
|
|
627
|
+
|
|
628
|
+
assertProbeChanged(scenarioName, "first content mutation", before, afterFirst)
|
|
629
|
+
assertProbeChanged(scenarioName, "second content mutation", afterFirst, afterSecond)
|
|
630
|
+
|
|
631
|
+
consume(before + afterFirst + afterSecond + firstPasses + secondPasses)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function validateCalculateRecalculation(
|
|
635
|
+
ctx: BenchmarkContext,
|
|
636
|
+
scenarioName: string,
|
|
637
|
+
mutate: (iteration: number) => unknown,
|
|
638
|
+
readProbe: () => number,
|
|
639
|
+
): void {
|
|
640
|
+
calculateLayout(ctx, scenarioName)
|
|
641
|
+
assertLayoutClean(ctx, scenarioName)
|
|
642
|
+
|
|
643
|
+
const before = readProbe()
|
|
644
|
+
consume(mutate(0))
|
|
645
|
+
assertLayoutDirty(ctx, scenarioName)
|
|
646
|
+
calculateLayout(ctx, scenarioName)
|
|
647
|
+
const afterFirst = readProbe()
|
|
648
|
+
|
|
649
|
+
consume(mutate(1))
|
|
650
|
+
assertLayoutDirty(ctx, scenarioName)
|
|
651
|
+
calculateLayout(ctx, scenarioName)
|
|
652
|
+
const afterSecond = readProbe()
|
|
653
|
+
|
|
654
|
+
assertProbeChanged(scenarioName, "first calculate-layout mutation", before, afterFirst)
|
|
655
|
+
assertProbeChanged(scenarioName, "second calculate-layout mutation", afterFirst, afterSecond)
|
|
656
|
+
|
|
657
|
+
consume(before + afterFirst + afterSecond)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function validateCalculateUpdateRecalculation(
|
|
661
|
+
ctx: BenchmarkContext,
|
|
662
|
+
scenarioName: string,
|
|
663
|
+
mutate: (iteration: number) => unknown,
|
|
664
|
+
readProbe: () => number,
|
|
665
|
+
): void {
|
|
666
|
+
const metrics = createMetricsTracker()
|
|
667
|
+
calculateAndUpdateLayout(ctx, scenarioName, metrics)
|
|
668
|
+
assertLayoutClean(ctx, scenarioName)
|
|
669
|
+
|
|
670
|
+
const before = readProbe()
|
|
671
|
+
consume(mutate(0))
|
|
672
|
+
assertLayoutDirty(ctx, scenarioName)
|
|
673
|
+
calculateAndUpdateLayout(ctx, scenarioName, metrics)
|
|
674
|
+
const afterFirst = readProbe()
|
|
675
|
+
|
|
676
|
+
consume(mutate(1))
|
|
677
|
+
assertLayoutDirty(ctx, scenarioName)
|
|
678
|
+
calculateAndUpdateLayout(ctx, scenarioName, metrics)
|
|
679
|
+
const afterSecond = readProbe()
|
|
680
|
+
|
|
681
|
+
assertProbeChanged(scenarioName, "first calculate-update mutation", before, afterFirst)
|
|
682
|
+
assertProbeChanged(scenarioName, "second calculate-update mutation", afterFirst, afterSecond)
|
|
683
|
+
|
|
684
|
+
consume(before + afterFirst + afterSecond + metrics.snapshot().totalRenderCommands)
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
async function validateStaticFullRender(
|
|
688
|
+
ctx: BenchmarkContext,
|
|
689
|
+
scenarioName: string,
|
|
690
|
+
readProbe: () => number,
|
|
691
|
+
): Promise<void> {
|
|
692
|
+
await renderUntilLayoutClean(ctx, scenarioName)
|
|
693
|
+
assertLayoutClean(ctx, scenarioName)
|
|
694
|
+
|
|
695
|
+
const before = readProbe()
|
|
696
|
+
await ctx.renderOnce()
|
|
697
|
+
assertLayoutClean(ctx, scenarioName)
|
|
698
|
+
const after = readProbe()
|
|
699
|
+
|
|
700
|
+
if (before !== after) {
|
|
701
|
+
throw new Error(`${scenarioName}: static full-render probe changed without a layout mutation`)
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
consume(before + after)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function layoutChecksum(renderables: readonly Renderable[]): number {
|
|
708
|
+
let checksum = 0
|
|
709
|
+
|
|
710
|
+
for (let index = 0; index < renderables.length; index += 1) {
|
|
711
|
+
const layout = renderables[index]!.getLayoutNode().getComputedLayout()
|
|
712
|
+
checksum =
|
|
713
|
+
(checksum +
|
|
714
|
+
(layout.left | 0) * 3 +
|
|
715
|
+
(layout.top | 0) * 5 +
|
|
716
|
+
(layout.width | 0) * 7 +
|
|
717
|
+
(layout.height | 0) * 11 +
|
|
718
|
+
index) |
|
|
719
|
+
0
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return checksum >>> 0
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function renderableLayoutChecksum(renderables: readonly Renderable[]): number {
|
|
726
|
+
let checksum = 0
|
|
727
|
+
|
|
728
|
+
for (let index = 0; index < renderables.length; index += 1) {
|
|
729
|
+
const renderable = renderables[index]!
|
|
730
|
+
checksum =
|
|
731
|
+
(checksum +
|
|
732
|
+
(renderable.x | 0) * 3 +
|
|
733
|
+
(renderable.y | 0) * 5 +
|
|
734
|
+
(renderable.width | 0) * 7 +
|
|
735
|
+
(renderable.height | 0) * 11 +
|
|
736
|
+
index) |
|
|
737
|
+
0
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return checksum >>> 0
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function renderableIdentityLayoutChecksum(renderables: readonly Renderable[]): number {
|
|
744
|
+
let checksum = renderableLayoutChecksum(renderables)
|
|
745
|
+
|
|
746
|
+
for (let index = 0; index < renderables.length; index += 1) {
|
|
747
|
+
checksum = (checksum + renderables[index]!.num * (index + 1)) | 0
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return checksum >>> 0
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function createScenarios(): BenchmarkScenario[] {
|
|
754
|
+
return [
|
|
755
|
+
{
|
|
756
|
+
name: "static_opencode_full_render",
|
|
757
|
+
description: "No Yoga dirties: full render pass over an OpenCode-like layout tree for update/render baseline",
|
|
758
|
+
setup: async (ctx) => {
|
|
759
|
+
const state = await buildOpencodeLayoutTree(ctx, {
|
|
760
|
+
messageCount: Math.max(48, ctx.height + 12),
|
|
761
|
+
includeVisualBoxes: true,
|
|
762
|
+
includeText: true,
|
|
763
|
+
})
|
|
764
|
+
const probeTargets = [...state.rows.slice(0, 16), ...state.badges.slice(0, 16)]
|
|
765
|
+
await renderUntilLayoutClean(ctx, "static_opencode_full_render")
|
|
766
|
+
const metrics = createMetricsTracker()
|
|
767
|
+
|
|
768
|
+
return withMetrics(
|
|
769
|
+
{
|
|
770
|
+
kind: "baseline",
|
|
771
|
+
phase: "full-render",
|
|
772
|
+
passMode: "single-frame",
|
|
773
|
+
renderablesPerIteration: state.stats.renderables,
|
|
774
|
+
layoutNodesPerIteration: state.stats.layoutNodes,
|
|
775
|
+
layoutOnlyBoxesPerIteration: state.stats.layoutOnlyBoxes,
|
|
776
|
+
layoutMutationsPerIteration: 0,
|
|
777
|
+
runIteration: async () => {
|
|
778
|
+
const passes = await renderMeasuredUntilLayoutClean(ctx, "static_opencode_full_render", metrics)
|
|
779
|
+
consume(passes)
|
|
780
|
+
return renderableLayoutChecksum(probeTargets)
|
|
781
|
+
},
|
|
782
|
+
validate: () =>
|
|
783
|
+
validateStaticFullRender(ctx, "static_opencode_full_render", () =>
|
|
784
|
+
renderableLayoutChecksum(probeTargets),
|
|
785
|
+
),
|
|
786
|
+
cleanup: () => {
|
|
787
|
+
state.root.destroyRecursively()
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
metrics,
|
|
791
|
+
)
|
|
792
|
+
},
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
name: "opencode_leaf_width_calculate_only",
|
|
796
|
+
description: "Dirty one deep leaf width and run only root.calculateLayout()",
|
|
797
|
+
setup: async (ctx) => {
|
|
798
|
+
const state = await buildOpencodeLayoutTree(ctx, {
|
|
799
|
+
messageCount: Math.max(64, ctx.height + 24),
|
|
800
|
+
includeVisualBoxes: false,
|
|
801
|
+
includeText: false,
|
|
802
|
+
})
|
|
803
|
+
calculateLayout(ctx, "opencode_leaf_width_calculate_only")
|
|
804
|
+
const metrics = createMetricsTracker()
|
|
805
|
+
|
|
806
|
+
const badgeWidths = new Array<number>(state.badges.length).fill(0)
|
|
807
|
+
const mutate = (iteration: number): number => {
|
|
808
|
+
const target = state.badges[iteration % state.badges.length]!
|
|
809
|
+
const targetIndex = iteration % state.badges.length
|
|
810
|
+
const nextWidth = badgeWidths[targetIndex] === 6 ? 11 : 6
|
|
811
|
+
badgeWidths[targetIndex] = nextWidth
|
|
812
|
+
target.width = nextWidth
|
|
813
|
+
return target.width
|
|
814
|
+
}
|
|
815
|
+
const readProbe = () => layoutChecksum(state.badges.slice(0, 24))
|
|
816
|
+
|
|
817
|
+
return withMetrics(
|
|
818
|
+
{
|
|
819
|
+
kind: "layout-recalc",
|
|
820
|
+
phase: "calculate-layout",
|
|
821
|
+
passMode: "calculate-only",
|
|
822
|
+
renderablesPerIteration: state.stats.renderables,
|
|
823
|
+
layoutNodesPerIteration: state.stats.layoutNodes,
|
|
824
|
+
layoutOnlyBoxesPerIteration: state.stats.layoutOnlyBoxes,
|
|
825
|
+
layoutMutationsPerIteration: 1,
|
|
826
|
+
runIteration(iteration) {
|
|
827
|
+
consume(mutate(iteration))
|
|
828
|
+
const passes = calculateLayout(ctx, "opencode_leaf_width_calculate_only")
|
|
829
|
+
metrics.recordSettlePasses(passes)
|
|
830
|
+
return readProbe() + passes
|
|
831
|
+
},
|
|
832
|
+
validate: () =>
|
|
833
|
+
validateCalculateRecalculation(ctx, "opencode_leaf_width_calculate_only", mutate, readProbe),
|
|
834
|
+
cleanup: () => {
|
|
835
|
+
state.root.destroyRecursively()
|
|
836
|
+
},
|
|
837
|
+
},
|
|
838
|
+
metrics,
|
|
839
|
+
)
|
|
840
|
+
},
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
name: "opencode_leaf_width_full_render",
|
|
844
|
+
description: "Dirty one deep leaf width and run the full renderer until layout is clean",
|
|
845
|
+
setup: async (ctx) => {
|
|
846
|
+
const state = await buildOpencodeLayoutTree(ctx, {
|
|
847
|
+
messageCount: Math.max(64, ctx.height + 24),
|
|
848
|
+
includeVisualBoxes: true,
|
|
849
|
+
includeText: false,
|
|
850
|
+
})
|
|
851
|
+
await renderUntilLayoutClean(ctx, "opencode_leaf_width_full_render")
|
|
852
|
+
const metrics = createMetricsTracker()
|
|
853
|
+
|
|
854
|
+
const badgeWidths = new Array<number>(state.badges.length).fill(0)
|
|
855
|
+
const mutate = (iteration: number): number => {
|
|
856
|
+
const target = state.badges[iteration % state.badges.length]!
|
|
857
|
+
const targetIndex = iteration % state.badges.length
|
|
858
|
+
const nextWidth = badgeWidths[targetIndex] === 5 ? 12 : 5
|
|
859
|
+
badgeWidths[targetIndex] = nextWidth
|
|
860
|
+
target.width = nextWidth
|
|
861
|
+
return target.width
|
|
862
|
+
}
|
|
863
|
+
const readProbe = () => renderableLayoutChecksum(state.badges.slice(0, 24))
|
|
864
|
+
|
|
865
|
+
return withMetrics(
|
|
866
|
+
{
|
|
867
|
+
kind: "layout-recalc",
|
|
868
|
+
phase: "full-render",
|
|
869
|
+
passMode: "settle-layout",
|
|
870
|
+
renderablesPerIteration: state.stats.renderables,
|
|
871
|
+
layoutNodesPerIteration: state.stats.layoutNodes,
|
|
872
|
+
layoutOnlyBoxesPerIteration: state.stats.layoutOnlyBoxes,
|
|
873
|
+
layoutMutationsPerIteration: 1,
|
|
874
|
+
async runIteration(iteration) {
|
|
875
|
+
consume(mutate(iteration))
|
|
876
|
+
const passes = await renderMeasuredUntilLayoutClean(ctx, "opencode_leaf_width_full_render", metrics)
|
|
877
|
+
return readProbe() + passes
|
|
878
|
+
},
|
|
879
|
+
validate: () => validateFullRenderRecalculation(ctx, "opencode_leaf_width_full_render", mutate, readProbe),
|
|
880
|
+
cleanup: () => {
|
|
881
|
+
state.root.destroyRecursively()
|
|
882
|
+
},
|
|
883
|
+
},
|
|
884
|
+
metrics,
|
|
885
|
+
)
|
|
886
|
+
},
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
name: "opencode_many_rows_full_render",
|
|
890
|
+
description: "Dirty a stripe of row heights and badge widths before a full settled render",
|
|
891
|
+
setup: async (ctx) => {
|
|
892
|
+
const state = await buildOpencodeLayoutTree(ctx, {
|
|
893
|
+
messageCount: Math.max(72, ctx.height * 2),
|
|
894
|
+
includeVisualBoxes: true,
|
|
895
|
+
includeText: false,
|
|
896
|
+
})
|
|
897
|
+
await renderUntilLayoutClean(ctx, "opencode_many_rows_full_render")
|
|
898
|
+
const metrics = createMetricsTracker()
|
|
899
|
+
|
|
900
|
+
const mutationsPerIteration = Math.min(16, state.rows.length)
|
|
901
|
+
const rowHeights = new Array<number>(state.rows.length).fill(0)
|
|
902
|
+
const badgeWidths = new Array<number>(state.badges.length).fill(0)
|
|
903
|
+
const mutate = (iteration: number): number => {
|
|
904
|
+
let checksum = 0
|
|
905
|
+
for (let offset = 0; offset < mutationsPerIteration; offset += 1) {
|
|
906
|
+
const rowIndex = (iteration * 7 + offset * 11) % state.rows.length
|
|
907
|
+
const row = state.rows[rowIndex]!
|
|
908
|
+
const badgeIndex = (rowIndex * 3 + offset) % state.badges.length
|
|
909
|
+
const badge = state.badges[badgeIndex]!
|
|
910
|
+
const nextHeight = rowHeights[rowIndex] === 2 ? 4 : 2
|
|
911
|
+
const nextWidth = badgeWidths[badgeIndex] === 4 ? 10 : 4
|
|
912
|
+
rowHeights[rowIndex] = nextHeight
|
|
913
|
+
badgeWidths[badgeIndex] = nextWidth
|
|
914
|
+
row.height = nextHeight
|
|
915
|
+
badge.width = nextWidth
|
|
916
|
+
checksum += row.height + badge.width
|
|
917
|
+
}
|
|
918
|
+
return checksum
|
|
919
|
+
}
|
|
920
|
+
const readProbe = () => renderableLayoutChecksum([...state.rows.slice(0, 20), ...state.badges.slice(0, 20)])
|
|
921
|
+
|
|
922
|
+
return withMetrics(
|
|
923
|
+
{
|
|
924
|
+
kind: "layout-recalc",
|
|
925
|
+
phase: "full-render",
|
|
926
|
+
passMode: "settle-layout",
|
|
927
|
+
renderablesPerIteration: state.stats.renderables,
|
|
928
|
+
layoutNodesPerIteration: state.stats.layoutNodes,
|
|
929
|
+
layoutOnlyBoxesPerIteration: state.stats.layoutOnlyBoxes,
|
|
930
|
+
layoutMutationsPerIteration: mutationsPerIteration * 2,
|
|
931
|
+
async runIteration(iteration) {
|
|
932
|
+
consume(mutate(iteration))
|
|
933
|
+
const passes = await renderMeasuredUntilLayoutClean(ctx, "opencode_many_rows_full_render", metrics)
|
|
934
|
+
return readProbe() + passes
|
|
935
|
+
},
|
|
936
|
+
validate: () => validateFullRenderRecalculation(ctx, "opencode_many_rows_full_render", mutate, readProbe),
|
|
937
|
+
cleanup: () => {
|
|
938
|
+
state.root.destroyRecursively()
|
|
939
|
+
},
|
|
940
|
+
},
|
|
941
|
+
metrics,
|
|
942
|
+
)
|
|
943
|
+
},
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
name: "top_branch_container_full_render",
|
|
947
|
+
description: "Dirty high-level header/body/sidebar/main layout props before a full settled render",
|
|
948
|
+
setup: async (ctx) => {
|
|
949
|
+
const state = await buildOpencodeLayoutTree(ctx, {
|
|
950
|
+
messageCount: Math.max(56, ctx.height + 16),
|
|
951
|
+
includeVisualBoxes: true,
|
|
952
|
+
includeText: false,
|
|
953
|
+
})
|
|
954
|
+
await renderUntilLayoutClean(ctx, "top_branch_container_full_render")
|
|
955
|
+
const metrics = createMetricsTracker()
|
|
956
|
+
|
|
957
|
+
let wideSidebar = false
|
|
958
|
+
let tallHeader = false
|
|
959
|
+
let paddedBody = false
|
|
960
|
+
let looseMain = false
|
|
961
|
+
const mutate = (): number => {
|
|
962
|
+
wideSidebar = !wideSidebar
|
|
963
|
+
tallHeader = !tallHeader
|
|
964
|
+
paddedBody = !paddedBody
|
|
965
|
+
looseMain = !looseMain
|
|
966
|
+
|
|
967
|
+
const sidebarWidth = wideSidebar ? 30 : 22
|
|
968
|
+
state.sidebar.width = sidebarWidth
|
|
969
|
+
state.sidebar.minWidth = sidebarWidth
|
|
970
|
+
state.sidebar.maxWidth = sidebarWidth
|
|
971
|
+
state.header.height = tallHeader ? 4 : 3
|
|
972
|
+
state.body.paddingLeft = paddedBody ? 3 : 1
|
|
973
|
+
state.body.paddingRight = paddedBody ? 2 : 1
|
|
974
|
+
state.main.gap = looseMain ? 2 : 1
|
|
975
|
+
|
|
976
|
+
return sidebarWidth + state.header.height + (paddedBody ? 5 : 2) + (looseMain ? 2 : 1)
|
|
977
|
+
}
|
|
978
|
+
const readProbe = () =>
|
|
979
|
+
renderableLayoutChecksum([
|
|
980
|
+
state.root,
|
|
981
|
+
state.header,
|
|
982
|
+
state.body,
|
|
983
|
+
state.sidebar,
|
|
984
|
+
state.main,
|
|
985
|
+
state.footer,
|
|
986
|
+
...state.rows.slice(0, 12),
|
|
987
|
+
])
|
|
988
|
+
|
|
989
|
+
return withMetrics(
|
|
990
|
+
{
|
|
991
|
+
kind: "top-branch",
|
|
992
|
+
phase: "full-render",
|
|
993
|
+
passMode: "settle-layout",
|
|
994
|
+
renderablesPerIteration: state.stats.renderables,
|
|
995
|
+
layoutNodesPerIteration: state.stats.layoutNodes,
|
|
996
|
+
layoutOnlyBoxesPerIteration: state.stats.layoutOnlyBoxes,
|
|
997
|
+
layoutMutationsPerIteration: 7,
|
|
998
|
+
async runIteration() {
|
|
999
|
+
consume(mutate())
|
|
1000
|
+
const passes = await renderMeasuredUntilLayoutClean(ctx, "top_branch_container_full_render", metrics)
|
|
1001
|
+
return readProbe() + passes
|
|
1002
|
+
},
|
|
1003
|
+
validate: () => validateFullRenderRecalculation(ctx, "top_branch_container_full_render", mutate, readProbe),
|
|
1004
|
+
cleanup: () => {
|
|
1005
|
+
state.root.destroyRecursively()
|
|
1006
|
+
},
|
|
1007
|
+
},
|
|
1008
|
+
metrics,
|
|
1009
|
+
)
|
|
1010
|
+
},
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
name: "root_layout_size_full_render",
|
|
1014
|
+
description: "Dirty the RootRenderable Yoga size so percentage-based descendants recalculate from the top",
|
|
1015
|
+
setup: async (ctx) => {
|
|
1016
|
+
const state = await buildOpencodeLayoutTree(ctx, {
|
|
1017
|
+
messageCount: Math.max(56, ctx.height + 16),
|
|
1018
|
+
includeVisualBoxes: true,
|
|
1019
|
+
includeText: false,
|
|
1020
|
+
})
|
|
1021
|
+
await renderUntilLayoutClean(ctx, "root_layout_size_full_render")
|
|
1022
|
+
const metrics = createMetricsTracker()
|
|
1023
|
+
|
|
1024
|
+
let compact = false
|
|
1025
|
+
const compactWidth = Math.max(80, ctx.width - 18)
|
|
1026
|
+
const compactHeight = Math.max(24, ctx.height - 4)
|
|
1027
|
+
const mutate = (): number => {
|
|
1028
|
+
compact = !compact
|
|
1029
|
+
const nextWidth = compact ? compactWidth : ctx.width
|
|
1030
|
+
const nextHeight = compact ? compactHeight : ctx.height
|
|
1031
|
+
ctx.renderer.root.resize(nextWidth, nextHeight)
|
|
1032
|
+
return nextWidth + nextHeight
|
|
1033
|
+
}
|
|
1034
|
+
const readProbe = () =>
|
|
1035
|
+
renderableLayoutChecksum([state.root, state.body, state.main, ...state.rows.slice(0, 16)])
|
|
1036
|
+
|
|
1037
|
+
return withMetrics(
|
|
1038
|
+
{
|
|
1039
|
+
kind: "root-resize",
|
|
1040
|
+
phase: "full-render",
|
|
1041
|
+
passMode: "settle-layout",
|
|
1042
|
+
renderablesPerIteration: state.stats.renderables,
|
|
1043
|
+
layoutNodesPerIteration: state.stats.layoutNodes,
|
|
1044
|
+
layoutOnlyBoxesPerIteration: state.stats.layoutOnlyBoxes,
|
|
1045
|
+
layoutMutationsPerIteration: 2,
|
|
1046
|
+
async runIteration() {
|
|
1047
|
+
consume(mutate())
|
|
1048
|
+
const passes = await renderMeasuredUntilLayoutClean(ctx, "root_layout_size_full_render", metrics)
|
|
1049
|
+
return readProbe() + passes
|
|
1050
|
+
},
|
|
1051
|
+
validate: () => validateFullRenderRecalculation(ctx, "root_layout_size_full_render", mutate, readProbe),
|
|
1052
|
+
cleanup: () => {
|
|
1053
|
+
ctx.renderer.root.resize(ctx.width, ctx.height)
|
|
1054
|
+
state.root.destroyRecursively()
|
|
1055
|
+
},
|
|
1056
|
+
},
|
|
1057
|
+
metrics,
|
|
1058
|
+
)
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
{
|
|
1062
|
+
name: "opencode_many_rows_layout_update_only",
|
|
1063
|
+
description: "Dirty many rows, calculate Yoga, then only run updateLayout/render-command collection",
|
|
1064
|
+
setup: async (ctx) => {
|
|
1065
|
+
const state = await buildOpencodeLayoutTree(ctx, {
|
|
1066
|
+
messageCount: Math.max(72, ctx.height * 2),
|
|
1067
|
+
includeVisualBoxes: false,
|
|
1068
|
+
includeText: false,
|
|
1069
|
+
})
|
|
1070
|
+
calculateAndUpdateLayout(ctx, "opencode_many_rows_layout_update_only", createMetricsTracker())
|
|
1071
|
+
const metrics = createMetricsTracker()
|
|
1072
|
+
|
|
1073
|
+
const mutationsPerIteration = Math.min(16, state.rows.length)
|
|
1074
|
+
const rowHeights = new Array<number>(state.rows.length).fill(0)
|
|
1075
|
+
const badgeWidths = new Array<number>(state.badges.length).fill(0)
|
|
1076
|
+
const mutate = (iteration: number): number => {
|
|
1077
|
+
let checksum = 0
|
|
1078
|
+
for (let offset = 0; offset < mutationsPerIteration; offset += 1) {
|
|
1079
|
+
const rowIndex = (iteration * 5 + offset * 7) % state.rows.length
|
|
1080
|
+
const badgeIndex = (iteration * 11 + offset * 13) % state.badges.length
|
|
1081
|
+
const row = state.rows[rowIndex]!
|
|
1082
|
+
const badge = state.badges[badgeIndex]!
|
|
1083
|
+
const nextHeight = rowHeights[rowIndex] === 2 ? 4 : 2
|
|
1084
|
+
const nextWidth = badgeWidths[badgeIndex] === 4 ? 10 : 4
|
|
1085
|
+
rowHeights[rowIndex] = nextHeight
|
|
1086
|
+
badgeWidths[badgeIndex] = nextWidth
|
|
1087
|
+
row.height = nextHeight
|
|
1088
|
+
badge.width = nextWidth
|
|
1089
|
+
checksum += nextHeight + nextWidth
|
|
1090
|
+
}
|
|
1091
|
+
return checksum
|
|
1092
|
+
}
|
|
1093
|
+
const readProbe = () => renderableLayoutChecksum([...state.rows.slice(0, 20), ...state.badges.slice(0, 20)])
|
|
1094
|
+
|
|
1095
|
+
return withMetrics(
|
|
1096
|
+
{
|
|
1097
|
+
kind: "layout-recalc",
|
|
1098
|
+
phase: "layout-update",
|
|
1099
|
+
passMode: "calculate-update",
|
|
1100
|
+
renderablesPerIteration: state.stats.renderables,
|
|
1101
|
+
layoutNodesPerIteration: state.stats.layoutNodes,
|
|
1102
|
+
layoutOnlyBoxesPerIteration: state.stats.layoutOnlyBoxes,
|
|
1103
|
+
layoutMutationsPerIteration: mutationsPerIteration * 2,
|
|
1104
|
+
runIteration(iteration) {
|
|
1105
|
+
consume(mutate(iteration))
|
|
1106
|
+
const commands = calculateAndUpdateLayout(ctx, "opencode_many_rows_layout_update_only", metrics)
|
|
1107
|
+
return readProbe() + commands
|
|
1108
|
+
},
|
|
1109
|
+
validate: () =>
|
|
1110
|
+
validateCalculateUpdateRecalculation(ctx, "opencode_many_rows_layout_update_only", mutate, readProbe),
|
|
1111
|
+
cleanup: () => {
|
|
1112
|
+
state.root.destroyRecursively()
|
|
1113
|
+
},
|
|
1114
|
+
},
|
|
1115
|
+
metrics,
|
|
1116
|
+
)
|
|
1117
|
+
},
|
|
1118
|
+
},
|
|
1119
|
+
{
|
|
1120
|
+
name: "batched_leaf_props_full_render",
|
|
1121
|
+
description: "Dirty 128 mixed leaf and row props before one full settled render",
|
|
1122
|
+
setup: async (ctx) => {
|
|
1123
|
+
const state = await buildOpencodeLayoutTree(ctx, {
|
|
1124
|
+
messageCount: Math.max(96, ctx.height * 3),
|
|
1125
|
+
includeVisualBoxes: true,
|
|
1126
|
+
includeText: false,
|
|
1127
|
+
})
|
|
1128
|
+
await renderUntilLayoutClean(ctx, "batched_leaf_props_full_render")
|
|
1129
|
+
const metrics = createMetricsTracker()
|
|
1130
|
+
|
|
1131
|
+
const rowMutations = Math.min(32, state.rows.length)
|
|
1132
|
+
const badgeMutations = Math.min(96, state.badges.length)
|
|
1133
|
+
const rowHeights = new Array<number>(state.rows.length).fill(0)
|
|
1134
|
+
const badgeWidths = new Array<number>(state.badges.length).fill(0)
|
|
1135
|
+
const mutate = (iteration: number): number => {
|
|
1136
|
+
let checksum = 0
|
|
1137
|
+
for (let offset = 0; offset < rowMutations; offset += 1) {
|
|
1138
|
+
const rowIndex = (iteration * 17 + offset * 19) % state.rows.length
|
|
1139
|
+
const row = state.rows[rowIndex]!
|
|
1140
|
+
const nextHeight = rowHeights[rowIndex] === 2 ? 5 : 2
|
|
1141
|
+
rowHeights[rowIndex] = nextHeight
|
|
1142
|
+
row.height = nextHeight
|
|
1143
|
+
checksum += nextHeight
|
|
1144
|
+
}
|
|
1145
|
+
for (let offset = 0; offset < badgeMutations; offset += 1) {
|
|
1146
|
+
const badgeIndex = (iteration * 23 + offset * 29) % state.badges.length
|
|
1147
|
+
const badge = state.badges[badgeIndex]!
|
|
1148
|
+
const nextWidth = badgeWidths[badgeIndex] === 4 ? 12 : 4
|
|
1149
|
+
badgeWidths[badgeIndex] = nextWidth
|
|
1150
|
+
badge.width = nextWidth
|
|
1151
|
+
checksum += nextWidth
|
|
1152
|
+
}
|
|
1153
|
+
return checksum
|
|
1154
|
+
}
|
|
1155
|
+
const readProbe = () => renderableLayoutChecksum([...state.rows.slice(0, 40), ...state.badges.slice(0, 80)])
|
|
1156
|
+
|
|
1157
|
+
return withMetrics(
|
|
1158
|
+
{
|
|
1159
|
+
kind: "layout-recalc",
|
|
1160
|
+
phase: "full-render",
|
|
1161
|
+
passMode: "settle-layout",
|
|
1162
|
+
renderablesPerIteration: state.stats.renderables,
|
|
1163
|
+
layoutNodesPerIteration: state.stats.layoutNodes,
|
|
1164
|
+
layoutOnlyBoxesPerIteration: state.stats.layoutOnlyBoxes,
|
|
1165
|
+
layoutMutationsPerIteration: rowMutations + badgeMutations,
|
|
1166
|
+
async runIteration(iteration) {
|
|
1167
|
+
consume(mutate(iteration))
|
|
1168
|
+
const passes = await renderMeasuredUntilLayoutClean(ctx, "batched_leaf_props_full_render", metrics)
|
|
1169
|
+
return readProbe() + passes
|
|
1170
|
+
},
|
|
1171
|
+
validate: () => validateFullRenderRecalculation(ctx, "batched_leaf_props_full_render", mutate, readProbe),
|
|
1172
|
+
cleanup: () => {
|
|
1173
|
+
state.root.destroyRecursively()
|
|
1174
|
+
},
|
|
1175
|
+
},
|
|
1176
|
+
metrics,
|
|
1177
|
+
)
|
|
1178
|
+
},
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
name: "wide_shallow_siblings_calculate_only",
|
|
1182
|
+
description: "Dirty one direct child in a wide wrapping container and run only root.calculateLayout()",
|
|
1183
|
+
setup: async (ctx) => {
|
|
1184
|
+
const state = await buildWideShallowTree(ctx, 512)
|
|
1185
|
+
calculateLayout(ctx, "wide_shallow_siblings_calculate_only")
|
|
1186
|
+
const metrics = createMetricsTracker()
|
|
1187
|
+
|
|
1188
|
+
const childWidths = new Array<number>(state.children.length).fill(0)
|
|
1189
|
+
const mutate = (iteration: number): number => {
|
|
1190
|
+
const childIndex = iteration % state.children.length
|
|
1191
|
+
const child = state.children[childIndex]!
|
|
1192
|
+
const nextWidth = childWidths[childIndex] === 13 ? 3 : 13
|
|
1193
|
+
childWidths[childIndex] = nextWidth
|
|
1194
|
+
child.width = nextWidth
|
|
1195
|
+
return nextWidth + childIndex
|
|
1196
|
+
}
|
|
1197
|
+
const readProbe = () => layoutChecksum(state.children.slice(0, 96))
|
|
1198
|
+
|
|
1199
|
+
return withMetrics(
|
|
1200
|
+
{
|
|
1201
|
+
kind: "wide-shallow",
|
|
1202
|
+
phase: "calculate-layout",
|
|
1203
|
+
passMode: "calculate-only",
|
|
1204
|
+
renderablesPerIteration: state.stats.renderables,
|
|
1205
|
+
layoutNodesPerIteration: state.stats.layoutNodes,
|
|
1206
|
+
layoutOnlyBoxesPerIteration: state.stats.layoutOnlyBoxes,
|
|
1207
|
+
layoutMutationsPerIteration: 1,
|
|
1208
|
+
runIteration(iteration) {
|
|
1209
|
+
consume(mutate(iteration))
|
|
1210
|
+
const passes = calculateLayout(ctx, "wide_shallow_siblings_calculate_only")
|
|
1211
|
+
metrics.recordSettlePasses(passes)
|
|
1212
|
+
return readProbe() + passes
|
|
1213
|
+
},
|
|
1214
|
+
validate: () =>
|
|
1215
|
+
validateCalculateRecalculation(ctx, "wide_shallow_siblings_calculate_only", mutate, readProbe),
|
|
1216
|
+
cleanup: () => {
|
|
1217
|
+
state.root.destroyRecursively()
|
|
1218
|
+
},
|
|
1219
|
+
},
|
|
1220
|
+
metrics,
|
|
1221
|
+
)
|
|
1222
|
+
},
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
name: "wide_shallow_siblings_full_render",
|
|
1226
|
+
description: "Dirty a batch of direct children in a wide wrapping container before a full settled render",
|
|
1227
|
+
setup: async (ctx) => {
|
|
1228
|
+
const state = await buildWideShallowTree(ctx, 512)
|
|
1229
|
+
await renderUntilLayoutClean(ctx, "wide_shallow_siblings_full_render")
|
|
1230
|
+
const metrics = createMetricsTracker()
|
|
1231
|
+
|
|
1232
|
+
const mutationsPerIteration = 32
|
|
1233
|
+
const childWidths = new Array<number>(state.children.length).fill(0)
|
|
1234
|
+
const mutate = (iteration: number): number => {
|
|
1235
|
+
let checksum = 0
|
|
1236
|
+
for (let offset = 0; offset < mutationsPerIteration; offset += 1) {
|
|
1237
|
+
const childIndex = (iteration * 31 + offset * 37) % state.children.length
|
|
1238
|
+
const child = state.children[childIndex]!
|
|
1239
|
+
const nextWidth = childWidths[childIndex] === 13 ? 3 : 13
|
|
1240
|
+
childWidths[childIndex] = nextWidth
|
|
1241
|
+
child.width = nextWidth
|
|
1242
|
+
checksum += nextWidth + childIndex
|
|
1243
|
+
}
|
|
1244
|
+
return checksum
|
|
1245
|
+
}
|
|
1246
|
+
const readProbe = () => renderableLayoutChecksum(state.children.slice(0, 128))
|
|
1247
|
+
|
|
1248
|
+
return withMetrics(
|
|
1249
|
+
{
|
|
1250
|
+
kind: "wide-shallow",
|
|
1251
|
+
phase: "full-render",
|
|
1252
|
+
passMode: "settle-layout",
|
|
1253
|
+
renderablesPerIteration: state.stats.renderables,
|
|
1254
|
+
layoutNodesPerIteration: state.stats.layoutNodes,
|
|
1255
|
+
layoutOnlyBoxesPerIteration: state.stats.layoutOnlyBoxes,
|
|
1256
|
+
layoutMutationsPerIteration: mutationsPerIteration,
|
|
1257
|
+
async runIteration(iteration) {
|
|
1258
|
+
consume(mutate(iteration))
|
|
1259
|
+
const passes = await renderMeasuredUntilLayoutClean(ctx, "wide_shallow_siblings_full_render", metrics)
|
|
1260
|
+
return readProbe() + passes
|
|
1261
|
+
},
|
|
1262
|
+
validate: () =>
|
|
1263
|
+
validateFullRenderRecalculation(ctx, "wide_shallow_siblings_full_render", mutate, readProbe),
|
|
1264
|
+
cleanup: () => {
|
|
1265
|
+
state.root.destroyRecursively()
|
|
1266
|
+
},
|
|
1267
|
+
},
|
|
1268
|
+
metrics,
|
|
1269
|
+
)
|
|
1270
|
+
},
|
|
1271
|
+
},
|
|
1272
|
+
{
|
|
1273
|
+
name: "deep_chain_leaf_calculate_only",
|
|
1274
|
+
description: "Dirty the leaf of a long single-child chain and run only root.calculateLayout()",
|
|
1275
|
+
setup: async (ctx) => {
|
|
1276
|
+
const state = await buildDeepChainTree(ctx, 192)
|
|
1277
|
+
calculateLayout(ctx, "deep_chain_leaf_calculate_only")
|
|
1278
|
+
const metrics = createMetricsTracker()
|
|
1279
|
+
|
|
1280
|
+
let wideLeaf = false
|
|
1281
|
+
const mutate = (): number => {
|
|
1282
|
+
wideLeaf = !wideLeaf
|
|
1283
|
+
state.leaf.width = wideLeaf ? 22 : 7
|
|
1284
|
+
state.leaf.height = wideLeaf ? 2 : 1
|
|
1285
|
+
return state.leaf.width + state.leaf.height
|
|
1286
|
+
}
|
|
1287
|
+
const readProbe = () => layoutChecksum([state.leaf, ...state.nodes.slice(-16)])
|
|
1288
|
+
|
|
1289
|
+
return withMetrics(
|
|
1290
|
+
{
|
|
1291
|
+
kind: "deep-chain",
|
|
1292
|
+
phase: "calculate-layout",
|
|
1293
|
+
passMode: "calculate-only",
|
|
1294
|
+
renderablesPerIteration: state.stats.renderables,
|
|
1295
|
+
layoutNodesPerIteration: state.stats.layoutNodes,
|
|
1296
|
+
layoutOnlyBoxesPerIteration: state.stats.layoutOnlyBoxes,
|
|
1297
|
+
layoutMutationsPerIteration: 2,
|
|
1298
|
+
runIteration() {
|
|
1299
|
+
consume(mutate())
|
|
1300
|
+
const passes = calculateLayout(ctx, "deep_chain_leaf_calculate_only")
|
|
1301
|
+
metrics.recordSettlePasses(passes)
|
|
1302
|
+
return readProbe() + passes
|
|
1303
|
+
},
|
|
1304
|
+
validate: () => validateCalculateRecalculation(ctx, "deep_chain_leaf_calculate_only", mutate, readProbe),
|
|
1305
|
+
cleanup: () => {
|
|
1306
|
+
state.root.destroyRecursively()
|
|
1307
|
+
},
|
|
1308
|
+
},
|
|
1309
|
+
metrics,
|
|
1310
|
+
)
|
|
1311
|
+
},
|
|
1312
|
+
},
|
|
1313
|
+
{
|
|
1314
|
+
name: "deep_chain_leaf_full_render",
|
|
1315
|
+
description: "Dirty the leaf of a long single-child chain before a full settled render",
|
|
1316
|
+
setup: async (ctx) => {
|
|
1317
|
+
const state = await buildDeepChainTree(ctx, 192)
|
|
1318
|
+
await renderUntilLayoutClean(ctx, "deep_chain_leaf_full_render")
|
|
1319
|
+
const metrics = createMetricsTracker()
|
|
1320
|
+
|
|
1321
|
+
let wideLeaf = false
|
|
1322
|
+
const mutate = (): number => {
|
|
1323
|
+
wideLeaf = !wideLeaf
|
|
1324
|
+
state.leaf.width = wideLeaf ? 22 : 7
|
|
1325
|
+
state.leaf.height = wideLeaf ? 2 : 1
|
|
1326
|
+
return state.leaf.width + state.leaf.height
|
|
1327
|
+
}
|
|
1328
|
+
const readProbe = () => renderableLayoutChecksum([state.leaf, ...state.nodes.slice(-16)])
|
|
1329
|
+
|
|
1330
|
+
return withMetrics(
|
|
1331
|
+
{
|
|
1332
|
+
kind: "deep-chain",
|
|
1333
|
+
phase: "full-render",
|
|
1334
|
+
passMode: "settle-layout",
|
|
1335
|
+
renderablesPerIteration: state.stats.renderables,
|
|
1336
|
+
layoutNodesPerIteration: state.stats.layoutNodes,
|
|
1337
|
+
layoutOnlyBoxesPerIteration: state.stats.layoutOnlyBoxes,
|
|
1338
|
+
layoutMutationsPerIteration: 2,
|
|
1339
|
+
async runIteration() {
|
|
1340
|
+
consume(mutate())
|
|
1341
|
+
const passes = await renderMeasuredUntilLayoutClean(ctx, "deep_chain_leaf_full_render", metrics)
|
|
1342
|
+
return readProbe() + passes
|
|
1343
|
+
},
|
|
1344
|
+
validate: () => validateFullRenderRecalculation(ctx, "deep_chain_leaf_full_render", mutate, readProbe),
|
|
1345
|
+
cleanup: () => {
|
|
1346
|
+
state.root.destroyRecursively()
|
|
1347
|
+
},
|
|
1348
|
+
},
|
|
1349
|
+
metrics,
|
|
1350
|
+
)
|
|
1351
|
+
},
|
|
1352
|
+
},
|
|
1353
|
+
{
|
|
1354
|
+
name: "text_measure_reflow_full_render",
|
|
1355
|
+
description: "Change wrapped text content so Yoga measure functions and parent row layout recalculate",
|
|
1356
|
+
setup: async (ctx) => {
|
|
1357
|
+
const state = await buildTextReflowTree(ctx, Math.max(36, ctx.height + 8))
|
|
1358
|
+
await renderUntilLayoutClean(ctx, "text_measure_reflow_full_render")
|
|
1359
|
+
const metrics = createMetricsTracker()
|
|
1360
|
+
|
|
1361
|
+
const textVersions = new Array<number>(state.texts.length).fill(0)
|
|
1362
|
+
const textWidths = new Array<number>(state.texts.length).fill(0)
|
|
1363
|
+
const mutate = (iteration: number): number => {
|
|
1364
|
+
const targetIndex = iteration % state.texts.length
|
|
1365
|
+
const text = state.texts[targetIndex]!
|
|
1366
|
+
const nextWidth = textWidths[targetIndex] === 22 ? 44 : 22
|
|
1367
|
+
textWidths[targetIndex] = nextWidth
|
|
1368
|
+
textVersions[targetIndex] += 1
|
|
1369
|
+
text.width = nextWidth
|
|
1370
|
+
text.content = createMeasuredText(textVersions[targetIndex] + iteration)
|
|
1371
|
+
return targetIndex + text.textLength + nextWidth
|
|
1372
|
+
}
|
|
1373
|
+
const readProbe = () => renderableLayoutChecksum([...state.rows.slice(0, 16), ...state.texts.slice(0, 16)])
|
|
1374
|
+
|
|
1375
|
+
return withMetrics(
|
|
1376
|
+
{
|
|
1377
|
+
kind: "text-measure",
|
|
1378
|
+
phase: "full-render",
|
|
1379
|
+
passMode: "settle-layout",
|
|
1380
|
+
renderablesPerIteration: state.stats.renderables,
|
|
1381
|
+
layoutNodesPerIteration: state.stats.layoutNodes,
|
|
1382
|
+
layoutOnlyBoxesPerIteration: state.stats.layoutOnlyBoxes,
|
|
1383
|
+
layoutMutationsPerIteration: 2,
|
|
1384
|
+
async runIteration(iteration) {
|
|
1385
|
+
consume(mutate(iteration))
|
|
1386
|
+
const passes = await renderMeasuredUntilLayoutClean(ctx, "text_measure_reflow_full_render", metrics)
|
|
1387
|
+
return readProbe() + passes
|
|
1388
|
+
},
|
|
1389
|
+
validate: () => validateFullRenderRecalculation(ctx, "text_measure_reflow_full_render", mutate, readProbe),
|
|
1390
|
+
cleanup: () => {
|
|
1391
|
+
state.root.destroyRecursively()
|
|
1392
|
+
},
|
|
1393
|
+
},
|
|
1394
|
+
metrics,
|
|
1395
|
+
)
|
|
1396
|
+
},
|
|
1397
|
+
},
|
|
1398
|
+
{
|
|
1399
|
+
name: "text_content_measure_full_render",
|
|
1400
|
+
description: "Change wrapped text content only, validating dirty Yoga and text line-info changes",
|
|
1401
|
+
setup: async (ctx) => {
|
|
1402
|
+
const state = await buildTextReflowTree(ctx, Math.max(36, ctx.height + 8))
|
|
1403
|
+
await renderUntilLayoutClean(ctx, "text_content_measure_full_render")
|
|
1404
|
+
const metrics = createMetricsTracker()
|
|
1405
|
+
|
|
1406
|
+
const textVersions = new Array<number>(state.texts.length).fill(0)
|
|
1407
|
+
const mutate = (iteration: number): number => {
|
|
1408
|
+
const targetIndex = iteration % state.texts.length
|
|
1409
|
+
const text = state.texts[targetIndex]!
|
|
1410
|
+
textVersions[targetIndex] += 1
|
|
1411
|
+
text.content = createMeasuredText(textVersions[targetIndex] + iteration)
|
|
1412
|
+
return targetIndex + text.textLength + text.virtualLineCount
|
|
1413
|
+
}
|
|
1414
|
+
const readContentProbe = () => {
|
|
1415
|
+
let checksum = 0
|
|
1416
|
+
for (let index = 0; index < Math.min(16, state.texts.length); index += 1) {
|
|
1417
|
+
const text = state.texts[index]!
|
|
1418
|
+
checksum += text.textLength + text.virtualLineCount * 31 + index
|
|
1419
|
+
}
|
|
1420
|
+
return checksum
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
return withMetrics(
|
|
1424
|
+
{
|
|
1425
|
+
kind: "text-measure",
|
|
1426
|
+
phase: "full-render",
|
|
1427
|
+
passMode: "settle-layout",
|
|
1428
|
+
renderablesPerIteration: state.stats.renderables,
|
|
1429
|
+
layoutNodesPerIteration: state.stats.layoutNodes,
|
|
1430
|
+
layoutOnlyBoxesPerIteration: state.stats.layoutOnlyBoxes,
|
|
1431
|
+
layoutMutationsPerIteration: 1,
|
|
1432
|
+
async runIteration(iteration) {
|
|
1433
|
+
consume(mutate(iteration))
|
|
1434
|
+
const passes = await renderMeasuredUntilLayoutClean(ctx, "text_content_measure_full_render", metrics)
|
|
1435
|
+
return readContentProbe() + passes
|
|
1436
|
+
},
|
|
1437
|
+
validate: () =>
|
|
1438
|
+
validateContentRecalculation(ctx, "text_content_measure_full_render", mutate, readContentProbe),
|
|
1439
|
+
cleanup: () => {
|
|
1440
|
+
state.root.destroyRecursively()
|
|
1441
|
+
},
|
|
1442
|
+
},
|
|
1443
|
+
metrics,
|
|
1444
|
+
)
|
|
1445
|
+
},
|
|
1446
|
+
},
|
|
1447
|
+
{
|
|
1448
|
+
name: "insert_remove_rows_full_render",
|
|
1449
|
+
description: "Remove one row, allocate one replacement row, and settle the full render tree",
|
|
1450
|
+
setup: async (ctx) => {
|
|
1451
|
+
const state = await buildTreeMutationState(ctx, Math.max(48, ctx.height + 12))
|
|
1452
|
+
await renderUntilLayoutClean(ctx, "insert_remove_rows_full_render")
|
|
1453
|
+
const metrics = createMetricsTracker()
|
|
1454
|
+
|
|
1455
|
+
const mutate = (iteration: number): number => {
|
|
1456
|
+
const removed = state.rows.shift()
|
|
1457
|
+
if (removed) {
|
|
1458
|
+
removed.destroyRecursively()
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
const row = createMutationRow(ctx, state.stats, state.nextRowId, iteration)
|
|
1462
|
+
state.nextRowId += 1
|
|
1463
|
+
state.list.add(row)
|
|
1464
|
+
state.rows.push(row)
|
|
1465
|
+
return state.rows.length + state.nextRowId
|
|
1466
|
+
}
|
|
1467
|
+
const readProbe = () => renderableIdentityLayoutChecksum(state.rows.slice(0, 24))
|
|
1468
|
+
|
|
1469
|
+
return withMetrics(
|
|
1470
|
+
{
|
|
1471
|
+
kind: "tree-mutation",
|
|
1472
|
+
phase: "full-render",
|
|
1473
|
+
passMode: "settle-layout",
|
|
1474
|
+
renderablesPerIteration: state.stats.renderables,
|
|
1475
|
+
layoutNodesPerIteration: state.stats.layoutNodes,
|
|
1476
|
+
layoutOnlyBoxesPerIteration: state.stats.layoutOnlyBoxes,
|
|
1477
|
+
layoutMutationsPerIteration: 2,
|
|
1478
|
+
async runIteration(iteration) {
|
|
1479
|
+
consume(mutate(iteration))
|
|
1480
|
+
const passes = await renderMeasuredUntilLayoutClean(ctx, "insert_remove_rows_full_render", metrics)
|
|
1481
|
+
return readProbe() + passes
|
|
1482
|
+
},
|
|
1483
|
+
validate: () => validateFullRenderRecalculation(ctx, "insert_remove_rows_full_render", mutate, readProbe),
|
|
1484
|
+
cleanup: () => {
|
|
1485
|
+
state.root.destroyRecursively()
|
|
1486
|
+
},
|
|
1487
|
+
},
|
|
1488
|
+
metrics,
|
|
1489
|
+
)
|
|
1490
|
+
},
|
|
1491
|
+
},
|
|
1492
|
+
{
|
|
1493
|
+
name: "scrollbox_content_reflow_full_render",
|
|
1494
|
+
description:
|
|
1495
|
+
"Dirty scrollbox content item heights with viewport culling and sticky-bottom scrollbar recalculation",
|
|
1496
|
+
setup: async (ctx) => {
|
|
1497
|
+
const state = await buildScrollboxReflowState(ctx, Math.max(120, ctx.height * 6))
|
|
1498
|
+
await renderUntilLayoutClean(ctx, "scrollbox_content_reflow_full_render")
|
|
1499
|
+
const metrics = createMetricsTracker()
|
|
1500
|
+
|
|
1501
|
+
const mutationsPerIteration = Math.min(8, state.items.length)
|
|
1502
|
+
const itemHeights = new Array<number>(state.items.length).fill(0)
|
|
1503
|
+
const mutate = (iteration: number): number => {
|
|
1504
|
+
let checksum = 0
|
|
1505
|
+
for (let offset = 0; offset < mutationsPerIteration; offset += 1) {
|
|
1506
|
+
const itemIndex = (iteration * 13 + offset * 17) % state.items.length
|
|
1507
|
+
const item = state.items[itemIndex]!
|
|
1508
|
+
const nextHeight = itemHeights[itemIndex] === 1 ? 4 : 1
|
|
1509
|
+
itemHeights[itemIndex] = nextHeight
|
|
1510
|
+
item.height = nextHeight
|
|
1511
|
+
checksum += item.height + itemIndex
|
|
1512
|
+
}
|
|
1513
|
+
return checksum
|
|
1514
|
+
}
|
|
1515
|
+
const readProbe = () =>
|
|
1516
|
+
renderableLayoutChecksum(state.items.slice(0, 32)) + state.scrollBox.scrollHeight + state.scrollBox.scrollTop
|
|
1517
|
+
|
|
1518
|
+
return withMetrics(
|
|
1519
|
+
{
|
|
1520
|
+
kind: "scrollbox",
|
|
1521
|
+
phase: "full-render",
|
|
1522
|
+
passMode: "settle-layout",
|
|
1523
|
+
renderablesPerIteration: state.stats.renderables,
|
|
1524
|
+
layoutNodesPerIteration: state.stats.layoutNodes,
|
|
1525
|
+
layoutOnlyBoxesPerIteration: state.stats.layoutOnlyBoxes,
|
|
1526
|
+
layoutMutationsPerIteration: mutationsPerIteration,
|
|
1527
|
+
async runIteration(iteration) {
|
|
1528
|
+
consume(mutate(iteration))
|
|
1529
|
+
const passes = await renderMeasuredUntilLayoutClean(ctx, "scrollbox_content_reflow_full_render", metrics)
|
|
1530
|
+
return readProbe() + passes
|
|
1531
|
+
},
|
|
1532
|
+
validate: () =>
|
|
1533
|
+
validateFullRenderRecalculation(ctx, "scrollbox_content_reflow_full_render", mutate, readProbe),
|
|
1534
|
+
cleanup: () => {
|
|
1535
|
+
state.root.destroyRecursively()
|
|
1536
|
+
},
|
|
1537
|
+
},
|
|
1538
|
+
metrics,
|
|
1539
|
+
)
|
|
1540
|
+
},
|
|
1541
|
+
},
|
|
1542
|
+
]
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
async function buildOpencodeLayoutTree(
|
|
1546
|
+
ctx: BenchmarkContext,
|
|
1547
|
+
options: { messageCount: number; includeVisualBoxes: boolean; includeText: boolean },
|
|
1548
|
+
): Promise<OpencodeLayoutTreeState> {
|
|
1549
|
+
clearRoot(ctx.renderer)
|
|
1550
|
+
resetBuffers(ctx.renderer)
|
|
1551
|
+
|
|
1552
|
+
const stats: TreeStats = {
|
|
1553
|
+
renderables: 1,
|
|
1554
|
+
layoutNodes: 1,
|
|
1555
|
+
layoutOnlyBoxes: 0,
|
|
1556
|
+
}
|
|
1557
|
+
const rows: BoxRenderable[] = []
|
|
1558
|
+
const badges: BoxRenderable[] = []
|
|
1559
|
+
|
|
1560
|
+
const root = trackLayoutBox(
|
|
1561
|
+
stats,
|
|
1562
|
+
new BoxRenderable(ctx.renderer, {
|
|
1563
|
+
id: "bench-layout-root",
|
|
1564
|
+
width: "100%",
|
|
1565
|
+
height: "100%",
|
|
1566
|
+
border: false,
|
|
1567
|
+
backgroundColor: COLORS.transparent,
|
|
1568
|
+
flexDirection: "column",
|
|
1569
|
+
}),
|
|
1570
|
+
)
|
|
1571
|
+
ctx.renderer.root.add(root)
|
|
1572
|
+
|
|
1573
|
+
const header = trackLayoutBox(
|
|
1574
|
+
stats,
|
|
1575
|
+
new BoxRenderable(ctx.renderer, {
|
|
1576
|
+
id: "bench-layout-header",
|
|
1577
|
+
width: "100%",
|
|
1578
|
+
height: 3,
|
|
1579
|
+
flexDirection: "row",
|
|
1580
|
+
paddingLeft: 1,
|
|
1581
|
+
paddingRight: 1,
|
|
1582
|
+
gap: 1,
|
|
1583
|
+
}),
|
|
1584
|
+
)
|
|
1585
|
+
root.add(header)
|
|
1586
|
+
|
|
1587
|
+
for (let i = 0; i < 5; i += 1) {
|
|
1588
|
+
const chip = trackLayoutBox(
|
|
1589
|
+
stats,
|
|
1590
|
+
new BoxRenderable(ctx.renderer, {
|
|
1591
|
+
id: `bench-layout-chip-${i}`,
|
|
1592
|
+
width: 8 + i,
|
|
1593
|
+
height: 1,
|
|
1594
|
+
flexShrink: 0,
|
|
1595
|
+
}),
|
|
1596
|
+
)
|
|
1597
|
+
header.add(chip)
|
|
1598
|
+
badges.push(chip)
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
const body = trackLayoutBox(
|
|
1602
|
+
stats,
|
|
1603
|
+
new BoxRenderable(ctx.renderer, {
|
|
1604
|
+
id: "bench-layout-body",
|
|
1605
|
+
width: "100%",
|
|
1606
|
+
flexGrow: 1,
|
|
1607
|
+
flexDirection: "row",
|
|
1608
|
+
gap: 1,
|
|
1609
|
+
paddingLeft: 1,
|
|
1610
|
+
paddingRight: 1,
|
|
1611
|
+
}),
|
|
1612
|
+
)
|
|
1613
|
+
root.add(body)
|
|
1614
|
+
|
|
1615
|
+
const sidebar = trackLayoutBox(
|
|
1616
|
+
stats,
|
|
1617
|
+
new BoxRenderable(ctx.renderer, {
|
|
1618
|
+
id: "bench-layout-sidebar",
|
|
1619
|
+
width: 22,
|
|
1620
|
+
minWidth: 22,
|
|
1621
|
+
maxWidth: 22,
|
|
1622
|
+
flexShrink: 0,
|
|
1623
|
+
flexDirection: "column",
|
|
1624
|
+
gap: 1,
|
|
1625
|
+
}),
|
|
1626
|
+
)
|
|
1627
|
+
body.add(sidebar)
|
|
1628
|
+
|
|
1629
|
+
for (let i = 0; i < 12; i += 1) {
|
|
1630
|
+
sidebar.add(
|
|
1631
|
+
trackLayoutBox(
|
|
1632
|
+
stats,
|
|
1633
|
+
new BoxRenderable(ctx.renderer, {
|
|
1634
|
+
id: `bench-layout-sidebar-row-${i}`,
|
|
1635
|
+
height: 1,
|
|
1636
|
+
paddingLeft: 1,
|
|
1637
|
+
paddingRight: 1,
|
|
1638
|
+
}),
|
|
1639
|
+
),
|
|
1640
|
+
)
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
const main = trackLayoutBox(
|
|
1644
|
+
stats,
|
|
1645
|
+
new BoxRenderable(ctx.renderer, {
|
|
1646
|
+
id: "bench-layout-main",
|
|
1647
|
+
flexGrow: 1,
|
|
1648
|
+
flexDirection: "column",
|
|
1649
|
+
gap: 1,
|
|
1650
|
+
}),
|
|
1651
|
+
)
|
|
1652
|
+
body.add(main)
|
|
1653
|
+
|
|
1654
|
+
for (let i = 0; i < options.messageCount; i += 1) {
|
|
1655
|
+
const row = trackLayoutBox(
|
|
1656
|
+
stats,
|
|
1657
|
+
new BoxRenderable(ctx.renderer, {
|
|
1658
|
+
id: `bench-layout-row-${i}`,
|
|
1659
|
+
width: "100%",
|
|
1660
|
+
height: i % 4 === 0 ? 3 : 2,
|
|
1661
|
+
flexDirection: "row",
|
|
1662
|
+
}),
|
|
1663
|
+
)
|
|
1664
|
+
rows.push(row)
|
|
1665
|
+
|
|
1666
|
+
const rail = trackLayoutBox(
|
|
1667
|
+
stats,
|
|
1668
|
+
new BoxRenderable(ctx.renderer, {
|
|
1669
|
+
id: `bench-layout-rail-${i}`,
|
|
1670
|
+
width: 3,
|
|
1671
|
+
minWidth: 3,
|
|
1672
|
+
maxWidth: 3,
|
|
1673
|
+
flexShrink: 0,
|
|
1674
|
+
}),
|
|
1675
|
+
)
|
|
1676
|
+
|
|
1677
|
+
const content = trackLayoutBox(
|
|
1678
|
+
stats,
|
|
1679
|
+
new BoxRenderable(ctx.renderer, {
|
|
1680
|
+
id: `bench-layout-content-${i}`,
|
|
1681
|
+
flexGrow: 1,
|
|
1682
|
+
flexDirection: "column",
|
|
1683
|
+
}),
|
|
1684
|
+
)
|
|
1685
|
+
|
|
1686
|
+
const meta = trackLayoutBox(
|
|
1687
|
+
stats,
|
|
1688
|
+
new BoxRenderable(ctx.renderer, {
|
|
1689
|
+
id: `bench-layout-meta-${i}`,
|
|
1690
|
+
width: "100%",
|
|
1691
|
+
height: 1,
|
|
1692
|
+
flexDirection: "row",
|
|
1693
|
+
justifyContent: "space-between",
|
|
1694
|
+
}),
|
|
1695
|
+
)
|
|
1696
|
+
|
|
1697
|
+
const badgeGroup = trackLayoutBox(
|
|
1698
|
+
stats,
|
|
1699
|
+
new BoxRenderable(ctx.renderer, {
|
|
1700
|
+
id: `bench-layout-badges-${i}`,
|
|
1701
|
+
flexDirection: "row",
|
|
1702
|
+
gap: 1,
|
|
1703
|
+
}),
|
|
1704
|
+
)
|
|
1705
|
+
|
|
1706
|
+
for (let badgeIndex = 0; badgeIndex < 3; badgeIndex += 1) {
|
|
1707
|
+
const badge = trackLayoutBox(
|
|
1708
|
+
stats,
|
|
1709
|
+
new BoxRenderable(ctx.renderer, {
|
|
1710
|
+
id: `bench-layout-badge-${i}-${badgeIndex}`,
|
|
1711
|
+
width: 5 + badgeIndex,
|
|
1712
|
+
height: 1,
|
|
1713
|
+
flexShrink: 0,
|
|
1714
|
+
}),
|
|
1715
|
+
)
|
|
1716
|
+
badgeGroup.add(badge)
|
|
1717
|
+
badges.push(badge)
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
const actions = trackLayoutBox(
|
|
1721
|
+
stats,
|
|
1722
|
+
new BoxRenderable(ctx.renderer, {
|
|
1723
|
+
id: `bench-layout-actions-${i}`,
|
|
1724
|
+
flexDirection: "row",
|
|
1725
|
+
gap: 1,
|
|
1726
|
+
flexShrink: 0,
|
|
1727
|
+
}),
|
|
1728
|
+
)
|
|
1729
|
+
|
|
1730
|
+
for (let actionIndex = 0; actionIndex < 2; actionIndex += 1) {
|
|
1731
|
+
const action = trackLayoutBox(
|
|
1732
|
+
stats,
|
|
1733
|
+
new BoxRenderable(ctx.renderer, {
|
|
1734
|
+
id: `bench-layout-action-${i}-${actionIndex}`,
|
|
1735
|
+
width: 4 + actionIndex,
|
|
1736
|
+
height: 1,
|
|
1737
|
+
flexShrink: 0,
|
|
1738
|
+
}),
|
|
1739
|
+
)
|
|
1740
|
+
actions.add(action)
|
|
1741
|
+
badges.push(action)
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
meta.add(badgeGroup)
|
|
1745
|
+
meta.add(actions)
|
|
1746
|
+
content.add(meta)
|
|
1747
|
+
|
|
1748
|
+
if (options.includeVisualBoxes) {
|
|
1749
|
+
content.add(
|
|
1750
|
+
trackVisualBox(
|
|
1751
|
+
stats,
|
|
1752
|
+
new BoxRenderable(ctx.renderer, {
|
|
1753
|
+
id: `bench-layout-leaf-${i}`,
|
|
1754
|
+
width: "100%",
|
|
1755
|
+
height: i % 5 === 0 ? 3 : i % 2 === 0 ? 2 : 1,
|
|
1756
|
+
border: false,
|
|
1757
|
+
backgroundColor: i % 3 === 0 ? COLORS.menu : i % 2 === 0 ? COLORS.panel : COLORS.element,
|
|
1758
|
+
}),
|
|
1759
|
+
),
|
|
1760
|
+
)
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
if (options.includeText && i % 4 === 0) {
|
|
1764
|
+
content.add(
|
|
1765
|
+
trackText(
|
|
1766
|
+
stats,
|
|
1767
|
+
new TextRenderable(ctx.renderer, {
|
|
1768
|
+
id: `bench-layout-text-${i}`,
|
|
1769
|
+
content: `message-${i}`,
|
|
1770
|
+
height: 1,
|
|
1771
|
+
}),
|
|
1772
|
+
),
|
|
1773
|
+
)
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
row.add(rail)
|
|
1777
|
+
row.add(content)
|
|
1778
|
+
main.add(row)
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
const footer = trackLayoutBox(
|
|
1782
|
+
stats,
|
|
1783
|
+
new BoxRenderable(ctx.renderer, {
|
|
1784
|
+
id: "bench-layout-footer",
|
|
1785
|
+
width: "100%",
|
|
1786
|
+
height: 4,
|
|
1787
|
+
flexDirection: "row",
|
|
1788
|
+
gap: 1,
|
|
1789
|
+
paddingLeft: 1,
|
|
1790
|
+
paddingRight: 1,
|
|
1791
|
+
}),
|
|
1792
|
+
)
|
|
1793
|
+
root.add(footer)
|
|
1794
|
+
|
|
1795
|
+
for (let i = 0; i < 6; i += 1) {
|
|
1796
|
+
const item = trackLayoutBox(
|
|
1797
|
+
stats,
|
|
1798
|
+
new BoxRenderable(ctx.renderer, {
|
|
1799
|
+
id: `bench-layout-footer-item-${i}`,
|
|
1800
|
+
width: 12 + i,
|
|
1801
|
+
height: 1,
|
|
1802
|
+
flexDirection: "row",
|
|
1803
|
+
gap: 1,
|
|
1804
|
+
}),
|
|
1805
|
+
)
|
|
1806
|
+
footer.add(item)
|
|
1807
|
+
badges.push(item)
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
return {
|
|
1811
|
+
root,
|
|
1812
|
+
header,
|
|
1813
|
+
body,
|
|
1814
|
+
sidebar,
|
|
1815
|
+
main,
|
|
1816
|
+
footer,
|
|
1817
|
+
rows,
|
|
1818
|
+
badges,
|
|
1819
|
+
stats,
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
async function buildWideShallowTree(ctx: BenchmarkContext, childCount: number): Promise<WideShallowTreeState> {
|
|
1824
|
+
clearRoot(ctx.renderer)
|
|
1825
|
+
resetBuffers(ctx.renderer)
|
|
1826
|
+
|
|
1827
|
+
const stats: TreeStats = {
|
|
1828
|
+
renderables: 1,
|
|
1829
|
+
layoutNodes: 1,
|
|
1830
|
+
layoutOnlyBoxes: 0,
|
|
1831
|
+
}
|
|
1832
|
+
const children: BoxRenderable[] = []
|
|
1833
|
+
|
|
1834
|
+
const root = trackLayoutBox(
|
|
1835
|
+
stats,
|
|
1836
|
+
new BoxRenderable(ctx.renderer, {
|
|
1837
|
+
id: "bench-wide-root",
|
|
1838
|
+
width: "100%",
|
|
1839
|
+
height: "100%",
|
|
1840
|
+
flexDirection: "column",
|
|
1841
|
+
paddingLeft: 1,
|
|
1842
|
+
paddingRight: 1,
|
|
1843
|
+
}),
|
|
1844
|
+
)
|
|
1845
|
+
ctx.renderer.root.add(root)
|
|
1846
|
+
|
|
1847
|
+
const container = trackLayoutBox(
|
|
1848
|
+
stats,
|
|
1849
|
+
new BoxRenderable(ctx.renderer, {
|
|
1850
|
+
id: "bench-wide-container",
|
|
1851
|
+
width: "100%",
|
|
1852
|
+
flexGrow: 1,
|
|
1853
|
+
flexDirection: "row",
|
|
1854
|
+
flexWrap: "wrap",
|
|
1855
|
+
gap: 1,
|
|
1856
|
+
}),
|
|
1857
|
+
)
|
|
1858
|
+
root.add(container)
|
|
1859
|
+
|
|
1860
|
+
for (let i = 0; i < childCount; i += 1) {
|
|
1861
|
+
const child = trackLayoutBox(
|
|
1862
|
+
stats,
|
|
1863
|
+
new BoxRenderable(ctx.renderer, {
|
|
1864
|
+
id: `bench-wide-child-${i}`,
|
|
1865
|
+
width: 3 + (i % 5),
|
|
1866
|
+
height: 1,
|
|
1867
|
+
flexShrink: 0,
|
|
1868
|
+
}),
|
|
1869
|
+
)
|
|
1870
|
+
container.add(child)
|
|
1871
|
+
children.push(child)
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
return {
|
|
1875
|
+
root,
|
|
1876
|
+
container,
|
|
1877
|
+
children,
|
|
1878
|
+
stats,
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
async function buildDeepChainTree(ctx: BenchmarkContext, depth: number): Promise<DeepChainTreeState> {
|
|
1883
|
+
clearRoot(ctx.renderer)
|
|
1884
|
+
resetBuffers(ctx.renderer)
|
|
1885
|
+
|
|
1886
|
+
const stats: TreeStats = {
|
|
1887
|
+
renderables: 1,
|
|
1888
|
+
layoutNodes: 1,
|
|
1889
|
+
layoutOnlyBoxes: 0,
|
|
1890
|
+
}
|
|
1891
|
+
const nodes: BoxRenderable[] = []
|
|
1892
|
+
|
|
1893
|
+
const root = trackLayoutBox(
|
|
1894
|
+
stats,
|
|
1895
|
+
new BoxRenderable(ctx.renderer, {
|
|
1896
|
+
id: "bench-deep-root",
|
|
1897
|
+
width: "100%",
|
|
1898
|
+
height: "100%",
|
|
1899
|
+
flexDirection: "column",
|
|
1900
|
+
}),
|
|
1901
|
+
)
|
|
1902
|
+
ctx.renderer.root.add(root)
|
|
1903
|
+
|
|
1904
|
+
let parent: BoxRenderable = root
|
|
1905
|
+
for (let i = 0; i < depth; i += 1) {
|
|
1906
|
+
const node = trackLayoutBox(
|
|
1907
|
+
stats,
|
|
1908
|
+
new BoxRenderable(ctx.renderer, {
|
|
1909
|
+
id: `bench-deep-node-${i}`,
|
|
1910
|
+
width: "100%",
|
|
1911
|
+
flexDirection: i % 2 === 0 ? "column" : "row",
|
|
1912
|
+
paddingLeft: i % 7 === 0 ? 1 : 0,
|
|
1913
|
+
}),
|
|
1914
|
+
)
|
|
1915
|
+
parent.add(node)
|
|
1916
|
+
nodes.push(node)
|
|
1917
|
+
parent = node
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
const leaf = trackVisualBox(
|
|
1921
|
+
stats,
|
|
1922
|
+
new BoxRenderable(ctx.renderer, {
|
|
1923
|
+
id: "bench-deep-leaf",
|
|
1924
|
+
width: 7,
|
|
1925
|
+
height: 1,
|
|
1926
|
+
flexShrink: 0,
|
|
1927
|
+
backgroundColor: COLORS.panel,
|
|
1928
|
+
}),
|
|
1929
|
+
)
|
|
1930
|
+
parent.add(leaf)
|
|
1931
|
+
|
|
1932
|
+
return {
|
|
1933
|
+
root,
|
|
1934
|
+
nodes,
|
|
1935
|
+
leaf,
|
|
1936
|
+
stats,
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
async function buildTextReflowTree(ctx: BenchmarkContext, rowCount: number): Promise<TextReflowTreeState> {
|
|
1941
|
+
clearRoot(ctx.renderer)
|
|
1942
|
+
resetBuffers(ctx.renderer)
|
|
1943
|
+
|
|
1944
|
+
const stats: TreeStats = {
|
|
1945
|
+
renderables: 1,
|
|
1946
|
+
layoutNodes: 1,
|
|
1947
|
+
layoutOnlyBoxes: 0,
|
|
1948
|
+
}
|
|
1949
|
+
const rows: BoxRenderable[] = []
|
|
1950
|
+
const texts: TextRenderable[] = []
|
|
1951
|
+
|
|
1952
|
+
const root = trackLayoutBox(
|
|
1953
|
+
stats,
|
|
1954
|
+
new BoxRenderable(ctx.renderer, {
|
|
1955
|
+
id: "bench-text-root",
|
|
1956
|
+
width: "100%",
|
|
1957
|
+
height: "100%",
|
|
1958
|
+
flexDirection: "column",
|
|
1959
|
+
paddingLeft: 1,
|
|
1960
|
+
paddingRight: 1,
|
|
1961
|
+
gap: 1,
|
|
1962
|
+
}),
|
|
1963
|
+
)
|
|
1964
|
+
ctx.renderer.root.add(root)
|
|
1965
|
+
|
|
1966
|
+
for (let i = 0; i < rowCount; i += 1) {
|
|
1967
|
+
const row = trackLayoutBox(
|
|
1968
|
+
stats,
|
|
1969
|
+
new BoxRenderable(ctx.renderer, {
|
|
1970
|
+
id: `bench-text-row-${i}`,
|
|
1971
|
+
width: "100%",
|
|
1972
|
+
flexDirection: "row",
|
|
1973
|
+
gap: 1,
|
|
1974
|
+
}),
|
|
1975
|
+
)
|
|
1976
|
+
rows.push(row)
|
|
1977
|
+
|
|
1978
|
+
row.add(
|
|
1979
|
+
trackLayoutBox(
|
|
1980
|
+
stats,
|
|
1981
|
+
new BoxRenderable(ctx.renderer, {
|
|
1982
|
+
id: `bench-text-gutter-${i}`,
|
|
1983
|
+
width: 6,
|
|
1984
|
+
height: 1,
|
|
1985
|
+
flexShrink: 0,
|
|
1986
|
+
}),
|
|
1987
|
+
),
|
|
1988
|
+
)
|
|
1989
|
+
|
|
1990
|
+
const text = trackText(
|
|
1991
|
+
stats,
|
|
1992
|
+
new TextRenderable(ctx.renderer, {
|
|
1993
|
+
id: `bench-text-${i}`,
|
|
1994
|
+
width: Math.max(18, Math.floor(ctx.width * 0.34)),
|
|
1995
|
+
content: createMeasuredText(i),
|
|
1996
|
+
wrapMode: "word",
|
|
1997
|
+
}),
|
|
1998
|
+
)
|
|
1999
|
+
texts.push(text)
|
|
2000
|
+
row.add(text)
|
|
2001
|
+
root.add(row)
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
return {
|
|
2005
|
+
root,
|
|
2006
|
+
rows,
|
|
2007
|
+
texts,
|
|
2008
|
+
stats,
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
async function buildTreeMutationState(ctx: BenchmarkContext, rowCount: number): Promise<TreeMutationState> {
|
|
2013
|
+
clearRoot(ctx.renderer)
|
|
2014
|
+
resetBuffers(ctx.renderer)
|
|
2015
|
+
|
|
2016
|
+
const stats: TreeStats = {
|
|
2017
|
+
renderables: 1,
|
|
2018
|
+
layoutNodes: 1,
|
|
2019
|
+
layoutOnlyBoxes: 0,
|
|
2020
|
+
}
|
|
2021
|
+
const rows: BoxRenderable[] = []
|
|
2022
|
+
|
|
2023
|
+
const root = trackLayoutBox(
|
|
2024
|
+
stats,
|
|
2025
|
+
new BoxRenderable(ctx.renderer, {
|
|
2026
|
+
id: "bench-mutation-root",
|
|
2027
|
+
width: "100%",
|
|
2028
|
+
height: "100%",
|
|
2029
|
+
flexDirection: "column",
|
|
2030
|
+
gap: 1,
|
|
2031
|
+
}),
|
|
2032
|
+
)
|
|
2033
|
+
ctx.renderer.root.add(root)
|
|
2034
|
+
|
|
2035
|
+
const list = trackLayoutBox(
|
|
2036
|
+
stats,
|
|
2037
|
+
new BoxRenderable(ctx.renderer, {
|
|
2038
|
+
id: "bench-mutation-list",
|
|
2039
|
+
width: "100%",
|
|
2040
|
+
flexGrow: 1,
|
|
2041
|
+
flexDirection: "column",
|
|
2042
|
+
}),
|
|
2043
|
+
)
|
|
2044
|
+
root.add(list)
|
|
2045
|
+
|
|
2046
|
+
for (let i = 0; i < rowCount; i += 1) {
|
|
2047
|
+
const row = createMutationRow(ctx, stats, i, i)
|
|
2048
|
+
list.add(row)
|
|
2049
|
+
rows.push(row)
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
return {
|
|
2053
|
+
root,
|
|
2054
|
+
list,
|
|
2055
|
+
rows,
|
|
2056
|
+
stats,
|
|
2057
|
+
nextRowId: rowCount,
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
function createMutationRow(ctx: BenchmarkContext, stats: TreeStats, id: number, variant: number): BoxRenderable {
|
|
2062
|
+
const row = trackLayoutBox(
|
|
2063
|
+
stats,
|
|
2064
|
+
new BoxRenderable(ctx.renderer, {
|
|
2065
|
+
id: `bench-mutation-row-${id}`,
|
|
2066
|
+
width: "100%",
|
|
2067
|
+
height: 1 + (variant % 3),
|
|
2068
|
+
flexDirection: "row",
|
|
2069
|
+
gap: 1,
|
|
2070
|
+
}),
|
|
2071
|
+
)
|
|
2072
|
+
|
|
2073
|
+
for (let i = 0; i < 4; i += 1) {
|
|
2074
|
+
row.add(
|
|
2075
|
+
trackLayoutBox(
|
|
2076
|
+
stats,
|
|
2077
|
+
new BoxRenderable(ctx.renderer, {
|
|
2078
|
+
id: `bench-mutation-cell-${id}-${i}`,
|
|
2079
|
+
width: i === 3 ? "auto" : 8 + ((variant + i) % 5),
|
|
2080
|
+
flexGrow: i === 3 ? 1 : 0,
|
|
2081
|
+
height: 1,
|
|
2082
|
+
}),
|
|
2083
|
+
),
|
|
2084
|
+
)
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
return row
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
async function buildScrollboxReflowState(ctx: BenchmarkContext, itemCount: number): Promise<ScrollboxReflowState> {
|
|
2091
|
+
clearRoot(ctx.renderer)
|
|
2092
|
+
resetBuffers(ctx.renderer)
|
|
2093
|
+
|
|
2094
|
+
const stats: TreeStats = {
|
|
2095
|
+
renderables: 1,
|
|
2096
|
+
layoutNodes: 1,
|
|
2097
|
+
layoutOnlyBoxes: 0,
|
|
2098
|
+
}
|
|
2099
|
+
const items: BoxRenderable[] = []
|
|
2100
|
+
|
|
2101
|
+
const root = trackLayoutBox(
|
|
2102
|
+
stats,
|
|
2103
|
+
new BoxRenderable(ctx.renderer, {
|
|
2104
|
+
id: "bench-scrollbox-root",
|
|
2105
|
+
width: "100%",
|
|
2106
|
+
height: "100%",
|
|
2107
|
+
flexDirection: "column",
|
|
2108
|
+
}),
|
|
2109
|
+
)
|
|
2110
|
+
ctx.renderer.root.add(root)
|
|
2111
|
+
|
|
2112
|
+
const scrollBox = trackLayoutBox(
|
|
2113
|
+
stats,
|
|
2114
|
+
new ScrollBoxRenderable(ctx.renderer, {
|
|
2115
|
+
id: "bench-scrollbox",
|
|
2116
|
+
width: "100%",
|
|
2117
|
+
height: "100%",
|
|
2118
|
+
stickyScroll: true,
|
|
2119
|
+
stickyStart: "bottom",
|
|
2120
|
+
viewportCulling: true,
|
|
2121
|
+
contentOptions: {
|
|
2122
|
+
flexDirection: "column",
|
|
2123
|
+
},
|
|
2124
|
+
}),
|
|
2125
|
+
) as ScrollBoxRenderable
|
|
2126
|
+
root.add(scrollBox)
|
|
2127
|
+
|
|
2128
|
+
// ScrollBoxRenderable owns wrapper, viewport, content, two scrollbars,
|
|
2129
|
+
// and each scrollbar owns a slider plus two arrows.
|
|
2130
|
+
// Count those internal renderables so per-node metrics match traversal scale.
|
|
2131
|
+
stats.renderables += 11
|
|
2132
|
+
stats.layoutNodes += 11
|
|
2133
|
+
stats.layoutOnlyBoxes += 3
|
|
2134
|
+
|
|
2135
|
+
for (let i = 0; i < itemCount; i += 1) {
|
|
2136
|
+
const item = trackLayoutBox(
|
|
2137
|
+
stats,
|
|
2138
|
+
new BoxRenderable(ctx.renderer, {
|
|
2139
|
+
id: `bench-scrollbox-item-${i}`,
|
|
2140
|
+
width: "100%",
|
|
2141
|
+
height: 1 + (i % 3),
|
|
2142
|
+
flexDirection: "row",
|
|
2143
|
+
gap: 1,
|
|
2144
|
+
paddingLeft: 1,
|
|
2145
|
+
paddingRight: 1,
|
|
2146
|
+
}),
|
|
2147
|
+
)
|
|
2148
|
+
|
|
2149
|
+
item.add(
|
|
2150
|
+
trackVisualBox(
|
|
2151
|
+
stats,
|
|
2152
|
+
new BoxRenderable(ctx.renderer, {
|
|
2153
|
+
id: `bench-scrollbox-item-marker-${i}`,
|
|
2154
|
+
width: 2,
|
|
2155
|
+
height: 1,
|
|
2156
|
+
flexShrink: 0,
|
|
2157
|
+
backgroundColor: i % 2 === 0 ? COLORS.accent : COLORS.warning,
|
|
2158
|
+
}),
|
|
2159
|
+
),
|
|
2160
|
+
)
|
|
2161
|
+
item.add(
|
|
2162
|
+
trackLayoutBox(
|
|
2163
|
+
stats,
|
|
2164
|
+
new BoxRenderable(ctx.renderer, {
|
|
2165
|
+
id: `bench-scrollbox-item-fill-${i}`,
|
|
2166
|
+
flexGrow: 1,
|
|
2167
|
+
height: 1,
|
|
2168
|
+
}),
|
|
2169
|
+
),
|
|
2170
|
+
)
|
|
2171
|
+
|
|
2172
|
+
scrollBox.add(item)
|
|
2173
|
+
items.push(item)
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
return {
|
|
2177
|
+
root,
|
|
2178
|
+
scrollBox,
|
|
2179
|
+
items,
|
|
2180
|
+
stats,
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
function trackLayoutBox<T extends BoxRenderable>(stats: TreeStats, box: T): T {
|
|
2185
|
+
stats.renderables += 1
|
|
2186
|
+
stats.layoutNodes += 1
|
|
2187
|
+
stats.layoutOnlyBoxes += 1
|
|
2188
|
+
return box
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
function trackVisualBox<T extends BoxRenderable>(stats: TreeStats, box: T): T {
|
|
2192
|
+
stats.renderables += 1
|
|
2193
|
+
stats.layoutNodes += 1
|
|
2194
|
+
return box
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
function trackText<T extends TextRenderable>(stats: TreeStats, text: T): T {
|
|
2198
|
+
stats.renderables += 1
|
|
2199
|
+
stats.layoutNodes += 1
|
|
2200
|
+
return text
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
function createMeasuredText(iteration: number): string {
|
|
2204
|
+
if (iteration % 3 === 0) {
|
|
2205
|
+
return `short layout line ${iteration}`
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
if (iteration % 3 === 1) {
|
|
2209
|
+
return `medium wrapped layout text ${iteration} with enough words to cross the benchmark column and force a different measured height`
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
return `long wrapped layout text ${iteration} with repeated terminal interface vocabulary layout render buffer yoga measurement viewport reflow update traversal native boundary `.repeat(
|
|
2213
|
+
2,
|
|
2214
|
+
)
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
async function runScenario(
|
|
2218
|
+
scenario: BenchmarkScenario,
|
|
2219
|
+
ctx: BenchmarkContext,
|
|
2220
|
+
args: BenchmarkArgs,
|
|
2221
|
+
): Promise<BenchmarkResult> {
|
|
2222
|
+
const runtime = await scenario.setup(ctx)
|
|
2223
|
+
|
|
2224
|
+
try {
|
|
2225
|
+
if (!args.skipValidation) {
|
|
2226
|
+
await runtime.validate?.()
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
let nextIteration = 0
|
|
2230
|
+
nextIteration = await runIterations(runtime, args.warmupIterations, nextIteration)
|
|
2231
|
+
|
|
2232
|
+
let batchIterations = args.iterations
|
|
2233
|
+
const calibration = await timeIterations(runtime, batchIterations, nextIteration)
|
|
2234
|
+
nextIteration = calibration.nextIteration
|
|
2235
|
+
|
|
2236
|
+
if (calibration.durationMs > 0 && calibration.durationMs < args.minSampleMs) {
|
|
2237
|
+
const scaledIterations = (batchIterations * args.minSampleMs) / calibration.durationMs
|
|
2238
|
+
batchIterations = roundIterations(scaledIterations)
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
if (batchIterations !== args.iterations) {
|
|
2242
|
+
nextIteration = await runIterations(runtime, Math.min(batchIterations, args.warmupIterations), nextIteration)
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
runtime.resetMetrics?.()
|
|
2246
|
+
|
|
2247
|
+
const samples: BenchmarkSample[] = []
|
|
2248
|
+
for (let round = 0; round < args.rounds; round += 1) {
|
|
2249
|
+
const start = nowNs()
|
|
2250
|
+
let sampleIterations = 0
|
|
2251
|
+
let durationMs = 0
|
|
2252
|
+
|
|
2253
|
+
do {
|
|
2254
|
+
nextIteration = await runIterations(runtime, batchIterations, nextIteration)
|
|
2255
|
+
sampleIterations += batchIterations
|
|
2256
|
+
durationMs = nsToMs(nowNs() - start)
|
|
2257
|
+
} while (durationMs < args.minSampleMs)
|
|
2258
|
+
|
|
2259
|
+
samples.push({
|
|
2260
|
+
round: round + 1,
|
|
2261
|
+
iterations: sampleIterations,
|
|
2262
|
+
durationMs,
|
|
2263
|
+
opsPerSecond: (sampleIterations * 1000) / durationMs,
|
|
2264
|
+
nsPerOperation: (durationMs * 1_000_000) / sampleIterations,
|
|
2265
|
+
})
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
const durations = samples.map((sample) => sample.durationMs)
|
|
2269
|
+
const opsPerSecond = samples.map((sample) => sample.opsPerSecond)
|
|
2270
|
+
const nsPerOperation = samples.map((sample) => sample.nsPerOperation)
|
|
2271
|
+
const metrics = runtime.readMetrics?.() ?? emptyMetrics()
|
|
2272
|
+
const avgSettlePassesPerIteration =
|
|
2273
|
+
metrics.measuredIterations > 0 ? metrics.totalSettlePasses / metrics.measuredIterations : 0
|
|
2274
|
+
const hasUpdateCommandMetrics = runtime.phase === "layout-update"
|
|
2275
|
+
const avgUpdateCommandsPerIteration =
|
|
2276
|
+
hasUpdateCommandMetrics && metrics.measuredIterations > 0
|
|
2277
|
+
? metrics.totalRenderCommands / metrics.measuredIterations
|
|
2278
|
+
: null
|
|
2279
|
+
const maxUpdateCommands = hasUpdateCommandMetrics ? metrics.maxRenderCommands : null
|
|
2280
|
+
|
|
2281
|
+
return {
|
|
2282
|
+
name: scenario.name,
|
|
2283
|
+
description: scenario.description,
|
|
2284
|
+
kind: runtime.kind,
|
|
2285
|
+
phase: runtime.phase,
|
|
2286
|
+
passMode: runtime.passMode,
|
|
2287
|
+
iterations: args.iterations,
|
|
2288
|
+
warmupIterations: args.warmupIterations,
|
|
2289
|
+
rounds: args.rounds,
|
|
2290
|
+
minSampleMs: args.minSampleMs,
|
|
2291
|
+
batchIterations,
|
|
2292
|
+
totalMeasuredIterations: samples.reduce((total, sample) => total + sample.iterations, 0),
|
|
2293
|
+
renderablesPerIteration: runtime.renderablesPerIteration,
|
|
2294
|
+
layoutNodesPerIteration: runtime.layoutNodesPerIteration,
|
|
2295
|
+
layoutOnlyBoxesPerIteration: runtime.layoutOnlyBoxesPerIteration,
|
|
2296
|
+
layoutMutationsPerIteration: runtime.layoutMutationsPerIteration,
|
|
2297
|
+
measuredMetricIterations: metrics.measuredIterations,
|
|
2298
|
+
avgSettlePassesPerIteration,
|
|
2299
|
+
maxSettlePasses: metrics.maxSettlePasses,
|
|
2300
|
+
avgUpdateCommandsPerIteration,
|
|
2301
|
+
maxUpdateCommands,
|
|
2302
|
+
medianDurationMs: median(durations),
|
|
2303
|
+
bestDurationMs: Math.min(...durations),
|
|
2304
|
+
medianOpsPerSecond: median(opsPerSecond),
|
|
2305
|
+
meanOpsPerSecond: mean(opsPerSecond),
|
|
2306
|
+
medianNsPerOperation: median(nsPerOperation),
|
|
2307
|
+
p95NsPerOperation: percentile(nsPerOperation, 95),
|
|
2308
|
+
stdDevNsPerOperation: sampleStdDev(nsPerOperation),
|
|
2309
|
+
rmePercent: relativeMarginOfError(nsPerOperation),
|
|
2310
|
+
samples,
|
|
2311
|
+
}
|
|
2312
|
+
} finally {
|
|
2313
|
+
await runtime.cleanup()
|
|
2314
|
+
clearRoot(ctx.renderer)
|
|
2315
|
+
resetBuffers(ctx.renderer)
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
async function timeIterations(
|
|
2320
|
+
runtime: ScenarioRuntime,
|
|
2321
|
+
count: number,
|
|
2322
|
+
startIteration: number,
|
|
2323
|
+
): Promise<{ durationMs: number; nextIteration: number }> {
|
|
2324
|
+
const start = nowNs()
|
|
2325
|
+
const nextIteration = await runIterations(runtime, count, startIteration)
|
|
2326
|
+
return {
|
|
2327
|
+
durationMs: nsToMs(nowNs() - start),
|
|
2328
|
+
nextIteration,
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
async function runIterations(runtime: ScenarioRuntime, count: number, startIteration: number): Promise<number> {
|
|
2333
|
+
for (let iteration = 0; iteration < count; iteration += 1) {
|
|
2334
|
+
consume(await runtime.runIteration(startIteration + iteration))
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
return startIteration + count
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
function clearRoot(renderer: TestRenderer): void {
|
|
2341
|
+
for (const child of renderer.root.getChildren()) {
|
|
2342
|
+
child.destroyRecursively()
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
function resetBuffers(renderer: TestRenderer): void {
|
|
2347
|
+
const buffers = [renderer.currentRenderBuffer, renderer.nextRenderBuffer]
|
|
2348
|
+
for (const buffer of buffers) {
|
|
2349
|
+
buffer.clearScissorRects()
|
|
2350
|
+
buffer.clearOpacity()
|
|
2351
|
+
buffer.clear(COLORS.transparent)
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
function formatNumber(value: number): string {
|
|
2356
|
+
return value.toFixed(2)
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
function formatOptionalNumber(value: number | null): string {
|
|
2360
|
+
return value === null ? "-" : formatNumber(value)
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
function formatOptionalInteger(value: number | null): string {
|
|
2364
|
+
return value === null ? "-" : String(value)
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
function writeLine(enabled: boolean, line: string): void {
|
|
2368
|
+
if (enabled) {
|
|
2369
|
+
console.log(line)
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
function printResults(results: BenchmarkResult[], args: BenchmarkArgs): void {
|
|
2374
|
+
console.log(
|
|
2375
|
+
`layout-benchmark iters=${args.iterations} warmup=${args.warmupIterations} rounds=${args.rounds} min_sample_ms=${args.minSampleMs} renderer=${args.width}x${args.height} scenarios=${results.length} checksum=${blackhole.checksum}`,
|
|
2376
|
+
)
|
|
2377
|
+
console.log("")
|
|
2378
|
+
|
|
2379
|
+
const header = [
|
|
2380
|
+
"scenario",
|
|
2381
|
+
"kind",
|
|
2382
|
+
"phase",
|
|
2383
|
+
"pass mode",
|
|
2384
|
+
"batch",
|
|
2385
|
+
"nodes",
|
|
2386
|
+
"mutations",
|
|
2387
|
+
"avg passes",
|
|
2388
|
+
"max passes",
|
|
2389
|
+
"avg update cmds",
|
|
2390
|
+
"max update cmds",
|
|
2391
|
+
"median ns/op",
|
|
2392
|
+
"p95 ns/op",
|
|
2393
|
+
"rme %",
|
|
2394
|
+
]
|
|
2395
|
+
const rows = results.map((result) => [
|
|
2396
|
+
result.name,
|
|
2397
|
+
result.kind,
|
|
2398
|
+
result.phase,
|
|
2399
|
+
result.passMode,
|
|
2400
|
+
String(result.batchIterations),
|
|
2401
|
+
String(result.layoutNodesPerIteration),
|
|
2402
|
+
String(result.layoutMutationsPerIteration),
|
|
2403
|
+
formatNumber(result.avgSettlePassesPerIteration),
|
|
2404
|
+
String(result.maxSettlePasses),
|
|
2405
|
+
formatOptionalNumber(result.avgUpdateCommandsPerIteration),
|
|
2406
|
+
formatOptionalInteger(result.maxUpdateCommands),
|
|
2407
|
+
formatNumber(result.medianNsPerOperation),
|
|
2408
|
+
formatNumber(result.p95NsPerOperation),
|
|
2409
|
+
formatNumber(result.rmePercent),
|
|
2410
|
+
])
|
|
2411
|
+
|
|
2412
|
+
const widths = header.map((title, index) => Math.max(title.length, ...rows.map((row) => row[index]?.length ?? 0)))
|
|
2413
|
+
const lines = [header, ...rows].map((row, rowIndex) => {
|
|
2414
|
+
const line = row.map((cell, index) => cell.padEnd(widths[index] ?? cell.length)).join(" ")
|
|
2415
|
+
if (rowIndex !== 0) {
|
|
2416
|
+
return line
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
const divider = widths.map((width) => "-".repeat(width)).join(" ")
|
|
2420
|
+
return `${line}\n${divider}`
|
|
2421
|
+
})
|
|
2422
|
+
|
|
2423
|
+
console.log(lines.join("\n"))
|
|
2424
|
+
console.log("")
|
|
2425
|
+
|
|
2426
|
+
for (const result of results) {
|
|
2427
|
+
console.log(`${result.name}: ${result.description}`)
|
|
2428
|
+
for (const sample of result.samples) {
|
|
2429
|
+
console.log(
|
|
2430
|
+
` round ${sample.round}: ${sample.iterations} iters, ${formatNumber(sample.durationMs)} ms, ${formatNumber(sample.nsPerOperation)} ns/op (${formatNumber(sample.opsPerSecond)} ops/sec)`,
|
|
2431
|
+
)
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
function writeResults(results: BenchmarkResult[], args: BenchmarkArgs, jsonPath: string): void {
|
|
2437
|
+
const absolutePath = path.isAbsolute(jsonPath) ? jsonPath : path.resolve(process.cwd(), jsonPath)
|
|
2438
|
+
mkdirSync(path.dirname(absolutePath), { recursive: true })
|
|
2439
|
+
writeFileSync(
|
|
2440
|
+
absolutePath,
|
|
2441
|
+
JSON.stringify(
|
|
2442
|
+
{
|
|
2443
|
+
meta: {
|
|
2444
|
+
timestamp: new Date().toISOString(),
|
|
2445
|
+
iterations: args.iterations,
|
|
2446
|
+
warmupIterations: args.warmupIterations,
|
|
2447
|
+
rounds: args.rounds,
|
|
2448
|
+
minSampleMs: args.minSampleMs,
|
|
2449
|
+
width: args.width,
|
|
2450
|
+
height: args.height,
|
|
2451
|
+
cwd: process.cwd(),
|
|
2452
|
+
args: process.argv.slice(2),
|
|
2453
|
+
runtime: {
|
|
2454
|
+
bun: typeof Bun !== "undefined" ? Bun.version : undefined,
|
|
2455
|
+
node: process.versions.node,
|
|
2456
|
+
v8: process.versions.v8,
|
|
2457
|
+
platform: process.platform,
|
|
2458
|
+
arch: process.arch,
|
|
2459
|
+
},
|
|
2460
|
+
blackholeChecksum: blackhole.checksum,
|
|
2461
|
+
},
|
|
2462
|
+
results,
|
|
2463
|
+
},
|
|
2464
|
+
null,
|
|
2465
|
+
2,
|
|
2466
|
+
),
|
|
2467
|
+
)
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
async function main(): Promise<void> {
|
|
2471
|
+
const args = parseArgs(process.argv.slice(2))
|
|
2472
|
+
const scenarios = createScenarios()
|
|
2473
|
+
|
|
2474
|
+
if (args.listScenarios) {
|
|
2475
|
+
for (const scenario of scenarios) {
|
|
2476
|
+
console.log(`${scenario.name}\t${scenario.description}`)
|
|
2477
|
+
}
|
|
2478
|
+
return
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
if (args.jsonPath) {
|
|
2482
|
+
const absolutePath = path.isAbsolute(args.jsonPath) ? args.jsonPath : path.resolve(process.cwd(), args.jsonPath)
|
|
2483
|
+
if (existsSync(absolutePath)) {
|
|
2484
|
+
throw new Error(`Benchmark output file already exists: ${absolutePath}`)
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
const selectedScenarios = args.scenarioNames
|
|
2489
|
+
? scenarios.filter((scenario) => args.scenarioNames!.has(scenario.name))
|
|
2490
|
+
: scenarios
|
|
2491
|
+
|
|
2492
|
+
if (selectedScenarios.length === 0) {
|
|
2493
|
+
throw new Error("No benchmark scenarios matched the provided --scenario filter")
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
writeLine(args.output, `layout benchmark renderer=${args.width}x${args.height}`)
|
|
2497
|
+
writeLine(
|
|
2498
|
+
args.output,
|
|
2499
|
+
`scenarios=${selectedScenarios.length} iterations=${args.iterations} warmup=${args.warmupIterations} rounds=${args.rounds} min_sample_ms=${args.minSampleMs}`,
|
|
2500
|
+
)
|
|
2501
|
+
|
|
2502
|
+
const { renderer, renderOnce } = await createTestRenderer({
|
|
2503
|
+
width: args.width,
|
|
2504
|
+
height: args.height,
|
|
2505
|
+
targetFps: 60,
|
|
2506
|
+
maxFps: 60,
|
|
2507
|
+
screenMode: "main-screen",
|
|
2508
|
+
externalOutputMode: "passthrough",
|
|
2509
|
+
consoleMode: "disabled",
|
|
2510
|
+
useMouse: false,
|
|
2511
|
+
})
|
|
2512
|
+
|
|
2513
|
+
renderer.requestRender = () => {}
|
|
2514
|
+
|
|
2515
|
+
const ctx: BenchmarkContext = {
|
|
2516
|
+
renderer,
|
|
2517
|
+
renderOnce,
|
|
2518
|
+
width: args.width,
|
|
2519
|
+
height: args.height,
|
|
2520
|
+
}
|
|
2521
|
+
const results: BenchmarkResult[] = []
|
|
2522
|
+
|
|
2523
|
+
try {
|
|
2524
|
+
for (const scenario of selectedScenarios) {
|
|
2525
|
+
writeLine(args.output, `Running ${scenario.name}...`)
|
|
2526
|
+
const result = await runScenario(scenario, ctx, args)
|
|
2527
|
+
results.push(result)
|
|
2528
|
+
writeLine(
|
|
2529
|
+
args.output,
|
|
2530
|
+
` median=${formatNumber(result.medianNsPerOperation)}ns/op p95=${formatNumber(result.p95NsPerOperation)}ns/op rme=${formatNumber(result.rmePercent)}%`,
|
|
2531
|
+
)
|
|
2532
|
+
}
|
|
2533
|
+
} finally {
|
|
2534
|
+
renderer.destroy()
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
if (args.output) {
|
|
2538
|
+
printResults(results, args)
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
if (args.jsonPath) {
|
|
2542
|
+
writeResults(results, args, args.jsonPath)
|
|
2543
|
+
writeLine(args.output, `Wrote benchmark JSON: ${args.jsonPath}`)
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
await main()
|