diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c2511a..8f1f85a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -- Benchmarks: frame times in a browser. `apps/playground/bench.html` (built with the playground) pans, zooms and scrolls made-up trees of 125 to 10,000 tips as SVG and on the canvas and times every frame; open it in any browser, press Run and save the numbers. `node scripts/bench-browser.mjs` runs it in the Chrome already installed, headless, at pixel density 1 and 2 and with the main thread slowed 4 times, and writes `docs/benchmarks-browser.md`; `--from ` adds a section from numbers saved in another browser. It uses a throwaway profile that it deletes, listens on this machine only and sends nothing anywhere. First numbers (headless Chrome 157, M2 Max, other work running): the canvas held 60 frames a second at every size up to 19,999 nodes, slowed thread included; SVG held it through 1,999 nodes, and through 499 on the slowed thread. The 1,000-node switch is unchanged. Not measured: WebKit, Firefox, a window on a real screen, a real slow device, radial layouts. The benchmark page and script have no tests of their own. +- Benchmarks: frame times in a browser. `apps/playground/bench.html` (built with the playground) pans, zooms and scrolls made-up trees of 125 to 10,000 tips as SVG and on the canvas and times every frame; open it in any browser, press Run and save the numbers. `node scripts/bench-browser.mjs` runs it in the Chrome already installed, headless, at pixel density 1 and 2 and with the main thread slowed 4 times, and writes `docs/benchmarks-browser.md`; `--from ` adds a section from numbers saved in another browser. It uses a throwaway profile that it deletes, listens on this machine only and sends nothing anywhere. First numbers (headless Chrome 157, M2 Max, other work running): the canvas held 60 frames a second at every size up to 19,999 nodes, slowed thread included; SVG held it through 1,999 nodes, and through 499 on the slowed thread. A second section is from a headless build of WebKit 27.2 (Playwright's: the engine Safari and the Mac app use, but neither program), driven by hand as the doc describes, at normal speed only: the canvas held 60 frames a second at every size, and SVG held it through 1,999 nodes. When a browser's clock ticks in whole milliseconds, as WebKit's does, the script says so in the section and prints no decimal places. The 1,000-node switch is unchanged. Not measured: Safari itself, the Mac app's web view, Firefox, a window on a real screen, a real slow device, radial layouts. The benchmark page and script have no tests of their own. - Payload plugin docs: moving a departed person's proposals to another account. The README's "When someone leaves" now says that `author` and `decidedBy` can't be changed through Payload, that the lock is a hook and so doesn't cover a host's own database code, and that a host making that move there must change only those two fields and leave `proposal`, `checksum` and the snapshot alone. A new test makes the move on MongoDB and checks that the proposal still matches its checksum, the version made from it still verifies, and the proposal stays locked. No code changed; an option to allow the move through Payload was considered and not built (ADR 0012). - Payload plugin: people are recorded by id, never by name (changes the unreleased proposals, before anyone used them). `proposals.nameOf` is gone. `proposals.personId({ req })` gives an opaque id for the signed-in person (default: their user document's id), and that id is all a proposal stores about its author and all an accepted proposal seals into the new snapshot for the author and the manager. An id is 1 to 64 letters, digits, `.`, `_`, `:` and `-`, starting with a letter or digit, so an email address or a name with a space in it is refused: the action stops, nothing is saved, and the refused value isn't written to the log. A one-word handle would pass; the plugin can't tell it from an id. `proposals.displayName({ req, personId })` is optional and gives the name to show: it is looked up each time a proposal is read (once per person per request), returned as `authorName` on the proposal and in the preview, and never stored or sealed, so a name changed or removed at the site changes what is shown at once and leaves every snapshot's checksum alone. Why: a name inside a sealed snapshot can't be taken out when a person leaves. Core is unchanged apart from its comments and one error text: `acceptIntoSnapshot` still seals the `author` and `acceptedBy` strings it is given. Still sealed as written, and not screened: a proposal's title, each move's note and the manager's note. No endpoint turns an id from a snapshot's record back into a name. Tested on Payload 3.93 with an in-memory MongoDB (142 plugin tests, 526 in all) and through the example site (47 checks); eight deliberate breaks of the new code each failed a test. Not tested: Postgres or SQLite, and a host with more than one auth collection. - React: names in the drawing are no longer struck through or covered. In rectangular layouts an inner node's name was drawn 6 units above the node, so when its children's rows were one row apart, a child's branch ran straight through it ("Reptilia" in the amniotes classes view). The name now goes in the first of three places the branches to its children leave clear: on the node's own row, just above it, or just below it; when rows are closer than a name is tall, it takes the place with the most room. A folded branch's name started 8 units out, on top of its 12-unit triangle; it now starts 16 units out, past it. The SVG and the canvas use the same rule (`innerLabelAt`, `leafLabelX`), so a name is in the same place in both. A leaf's name hasn't moved. Limits: a long name can still cross a branch further down the tree, since only the node's own children are looked at, and in a radial drawing a folded branch's triangle still points right whatever the branch's angle. diff --git a/docs/architecture/backlog.md b/docs/architecture/backlog.md index 84f73e7..cac2043 100644 --- a/docs/architecture/backlog.md +++ b/docs/architecture/backlog.md @@ -83,8 +83,8 @@ Task-level plan per milestone ([`phase-plan.md`](phase-plan.md)). `[x]` done wit - [x] Phylogeny comparison (`comparePhylogenies`), explicit rooting tools (`rerootPhylogeny`, `midpointRoot`), support-threshold collapsing (`collapseBelowSupport` view action) (2026-11-11; core only, no explorer or Mac app controls yet) - [ ] Deep-time rendering where calibrated; trait overlays; documented discrete-character parsimony (labelled *inferred*) - [x] Nexus and phyloXML adapters (2026-11-11; the Mac app doesn't open them yet) -- [ ] Canvas renderer with level of detail (1–2k visible nodes smoothly). Built 2026-11-11: `TreeView` switches to a canvas above 1,000 drawn nodes, with level of detail, disclosure and hit-testing; planning one frame takes under 11 ms in Node at 50,000 tips (`docs/benchmarks.md`). Measured 2026-11-11 in headless Chrome 157 on an M2 Max Mac (`docs/benchmarks-browser.md`): the canvas held 60 frames a second while panning, zooming and scrolling at every size tried, up to 19,999 nodes, and still did with the main thread slowed 4 times. Not ticked: nothing has been measured in WebKit, which the Mac app and Safari use, or in a window on a real screen. -- [ ] Benchmarks: 10k+ taxa datasets, 1–2k visible nodes; documented devices/browsers. Started 2026-11-11: `scripts/bench-canvas.mjs` times projection, layout and the canvas plan on 1k, 10k and 50k made-up tips in Node and writes `docs/benchmarks.md` with the machine. `scripts/bench-browser.mjs` and `apps/playground/bench.html` time frames for both renderers in a browser and write `docs/benchmarks-browser.md`; one browser on one machine is in it (headless Chrome 157, M2 Max). Not ticked: WebKit and Firefox, a real slow device (only Chrome's 4 times throttle so far, where SVG dropped frames at 999 nodes, just under the 1,000-node switch; the default stays until a real device says otherwise), radial layouts, and a 10k+ dataset that isn't made up. +- [ ] Canvas renderer with level of detail (1–2k visible nodes smoothly). Built 2026-11-11: `TreeView` switches to a canvas above 1,000 drawn nodes, with level of detail, disclosure and hit-testing; planning one frame takes under 11 ms in Node at 50,000 tips (`docs/benchmarks.md`). Measured 2026-11-11 in headless Chrome 157 on an M2 Max Mac (`docs/benchmarks-browser.md`): the canvas held 60 frames a second while panning, zooming and scrolling at every size tried, up to 19,999 nodes, and still did with the main thread slowed 4 times. A headless build of WebKit 27.2 (Playwright's) gave the same result for the canvas at normal speed. Not ticked: nothing has been measured in Safari, in the Mac app's web view, or in a window on a real screen. +- [ ] Benchmarks: 10k+ taxa datasets, 1–2k visible nodes; documented devices/browsers. Started 2026-11-11: `scripts/bench-canvas.mjs` times projection, layout and the canvas plan on 1k, 10k and 50k made-up tips in Node and writes `docs/benchmarks.md` with the machine. `scripts/bench-browser.mjs` and `apps/playground/bench.html` time frames for both renderers in a browser and write `docs/benchmarks-browser.md`; two browsers on one machine are in it (headless Chrome 157 and Playwright's headless WebKit 27.2, M2 Max). Not ticked: Safari, the Mac app's web view and Firefox, a real slow device (only Chrome's 4 times throttle so far, where SVG dropped frames at 999 nodes, just under the 1,000-node switch; the default stays until a real device says otherwise), radial layouts, and a 10k+ dataset that isn't made up. - [x] Accessibility audit (code-level, WCAG 2.2 AA, 2026-11-11: `docs/test-audit.md`; small fixes made, gaps listed there; no screen reader or real browser run yet) ## Phase 2 (not started by design) diff --git a/docs/benchmarks-browser.md b/docs/benchmarks-browser.md index 473850f..3f2b350 100644 --- a/docs/benchmarks-browser.md +++ b/docs/benchmarks-browser.md @@ -6,13 +6,83 @@ How long a frame takes while a made-up tree is panned, zoomed and scrolled, draw **What it doesn't.** -- Other browser engines. Only the browsers with a section below were measured. WebKit, which the Mac app and Safari use, and Firefox may differ. To measure one, open `apps/playground/dist/bench.html` in it, press Run, save the numbers and run `node scripts/bench-browser.mjs --from onetaxonomy-bench.json`. +- Safari, the Mac app and Firefox. Only the browsers with a section below were measured. The WebKit section is Playwright's build of WebKit: the same engine Safari and the Mac app's web view use, but not either program, and headless. It has no slowed-thread pass, because the throttle is Chrome's. To measure another browser, open `apps/playground/dist/bench.html` in it, press Run, save the numbers and run `node scripts/bench-browser.mjs --from onetaxonomy-bench.json`. +- How the WebKit section was made. This script drives Chrome only. For WebKit, `playwright-core` (not a dependency of this repo) opened the built page from disk in its `webkit` browser, headless, once at device scale 1 and once at 2, and called `window.runBench()`, which is what the Run button calls. The two reports went into one file in the shape `--json` prints, and `--from` wrote the section. - A window on a real screen. A headless run draws with the graphics card but shows nothing. - All of painting. Raster and compositing happen off the main thread. They show in the frame time only once they fall behind and the browser holds the next frame back, so a steady frame time means the page kept up, not that painting cost nothing. - A slow device. "Main thread slowed 4 times" is Chrome's own throttle. It slows script, style and layout on the main thread and leaves the graphics card and the other threads at full speed, so it is a rough stand-in, not a phone or an old laptop. - Radial layouts, highlights, traits, support values and folded branches. The trees are plain rectangular ones with names on the tips. - The outline (`TreeOutline`), which isn't virtualized. +## WebKit 27.2 (Playwright's build), headless, 2026-11-11 + +- Machine: Mac14,13, Apple M2 Max, 12 cores. +- Browser: WebKit 27.2 (Playwright's build), headless (no window on a screen). Graphics: Apple GPU. +- Load: the machine was busy with other work. Load average 7.5, 8.3, 8.7 before and 6.9, 7.8, 8.4 after (1, 5 and 15 minutes). +- Clock: this browser gives the time in whole milliseconds. Every time below is rounded to one: a 16.7 ms frame shows as 17, and script that took under a millisecond shows as 0 or 1. +- Drawing: 1000 × 1000 CSS pixels. 180 frames for each motion, after 20 warm-up frames. One run of each; not repeated. + +What the numbers say, counting a size as on time when under 5% of its frames were late in every motion: + +- **Pixel density 1:** SVG was on time up to 1,999 nodes; it first fell behind at 4,999 nodes (33% of frames late while panning, median frame 24 ms, about 42 frames a second). The canvas was on time at every size, up to 19,999 nodes. +- **Pixel density 2:** SVG was on time up to 1,999 nodes; it first fell behind at 4,999 nodes (56% of frames late while panning, median frame 27 ms, about 37 frames a second). The canvas was on time at every size, up to 19,999 nodes. + +`TreeView` draws as SVG up to 1,000 drawn nodes and on the canvas above that. The largest SVG tree measured at or under the switch: pixel density 1: 999 nodes, on time; pixel density 2: 999 nodes, on time. + +### Pixel density 1 + +The display frame here, with nothing to draw: 17 ms on this browser's whole-millisecond clock (60 frames a second is 16.7 ms). + +| Renderer | Tips | Nodes | Pan: frame, late | Zoom: frame, late | Read: frame, late | Script (pan / zoom / read) | First render, script | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| svg | 125 | 249 | 17 / 17 / 19, 0% | 17 / 18 / 18, 0% | 17 / 18 / 18, 0% | 1 / 0 / 0 | 8 | +| canvas | 125 | 249 | 17 / 18 / 18, 0% | 17 / 18 / 28, 1% | 17 / 18 / 18, 0% | 0 / 0 / 0 | 4 | +| svg | 250 | 499 | 17 / 18 / 19, 0% | 17 / 18 / 23, 0% | 17 / 17 / 18, 0% | 1 / 1 / 1 | 5 | +| canvas | 250 | 499 | 17 / 18 / 18, 0% | 17 / 18 / 18, 0% | 17 / 18 / 18, 0% | 0 / 0 / 0 | 2 | +| svg | 500 | 999 | 17 / 18 / 19, 0% | 17 / 18 / 18, 0% | 17 / 18 / 18, 0% | 1 / 1 / 1 | 8 | +| canvas | 500 | 999 | 17 / 18 / 18, 0% | 17 / 18 / 18, 0% | 17 / 17 / 18, 0% | 1 / 0 / 0 | 2 | +| svg | 1,000 | 1,999 | 17 / 18 / 18, 0% | 17 / 18 / 19, 0% | 17 / 18 / 19, 0% | 3 / 3 / 3 | 15 | +| canvas | 1,000 | 1,999 | 17 / 18 / 19, 0% | 17 / 18 / 19, 0% | 17 / 18 / 18, 0% | 1 / 0 / 0 | 4 | +| svg | 2,500 | 4,999 | 24 / 31 / 35, 33% | 17 / 23 / 26, 2% | 17 / 19 / 21, 0% | 7 / 7 / 7 | 38 | +| canvas | 2,500 | 4,999 | 17 / 17 / 18, 0% | 17 / 18 / 18, 0% | 17 / 17 / 18, 0% | 2 / 1 / 0 | 6 | +| svg | 5,000 | 9,999 | 48 / 56 / 63, 100% | 33 / 47 / 62, 100% | 33 / 38 / 44, 98% | 14 / 14 / 14 | 75 | +| canvas | 5,000 | 9,999 | 17 / 18 / 19, 0% | 17 / 18 / 19, 0% | 17 / 18 / 18, 0% | 2 / 1 / 0 | 7 | +| canvas | 10,000 | 19,999 | 17 / 18 / 18, 0% | 17 / 18 / 18, 0% | 17 / 18 / 18, 0% | 3 / 1 / 1 | 14 | + +### Pixel density 2 + +The display frame here, with nothing to draw: 17 ms on this browser's whole-millisecond clock (60 frames a second is 16.7 ms). + +| Renderer | Tips | Nodes | Pan: frame, late | Zoom: frame, late | Read: frame, late | Script (pan / zoom / read) | First render, script | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| svg | 125 | 249 | 17 / 18 / 19, 0% | 17 / 18 / 19, 0% | 17 / 18 / 18, 0% | 1 / 1 / 1 | 15 | +| canvas | 125 | 249 | 17 / 18 / 18, 0% | 17 / 18 / 25, 0% | 17 / 18 / 18, 0% | 1 / 1 / 1 | 7 | +| svg | 250 | 499 | 17 / 18 / 18, 0% | 17 / 18 / 18, 0% | 17 / 18 / 18, 0% | 1 / 1 / 1 | 4 | +| canvas | 250 | 499 | 17 / 18 / 20, 0% | 17 / 18 / 18, 0% | 17 / 18 / 18, 0% | 1 / 0 / 0 | 1 | +| svg | 500 | 999 | 17 / 18 / 18, 0% | 17 / 18 / 18, 0% | 17 / 18 / 20, 0% | 1 / 1 / 1 | 8 | +| canvas | 500 | 999 | 17 / 18 / 18, 0% | 17 / 18 / 18, 0% | 17 / 18 / 18, 0% | 0 / 0 / 0 | 1 | +| svg | 1,000 | 1,999 | 17 / 18 / 18, 0% | 17 / 18 / 18, 0% | 17 / 18 / 27, 1% | 3 / 3 / 3 | 15 | +| canvas | 1,000 | 1,999 | 17 / 18 / 18, 0% | 17 / 17 / 18, 0% | 17 / 17 / 18, 0% | 1 / 1 / 0 | 3 | +| svg | 2,500 | 4,999 | 27 / 34 / 36, 56% | 18 / 25 / 27, 3% | 17 / 19 / 21, 0% | 7 / 7 / 7 | 36 | +| canvas | 2,500 | 4,999 | 17 / 18 / 18, 0% | 17 / 17 / 18, 0% | 17 / 17 / 18, 0% | 2 / 1 / 0 | 6 | +| svg | 5,000 | 9,999 | 48 / 63 / 73, 100% | 35 / 49 / 58, 100% | 33 / 39 / 47, 98% | 14 / 14 / 14 | 75 | +| canvas | 5,000 | 9,999 | 17 / 18 / 19, 0% | 17 / 17 / 18, 0% | 17 / 18 / 18, 0% | 3 / 1 / 0 | 9 | +| canvas | 10,000 | 19,999 | 17 / 18 / 20, 0% | 17 / 18 / 19, 0% | 17 / 18 / 18, 0% | 5 / 1 / 0 | 15 | + +### What was on screen + +The SVG holds every element whatever the view. The canvas plans each frame: it leaves out what is out of view and merges subtrees too small to tell apart. Median for each frame, with the most in any frame in brackets. + +| Tips | Nodes | SVG elements | Canvas, pan: nodes drawn | Canvas, zoom: nodes drawn, names | Canvas, read: nodes drawn, names | +| ---: | ---: | ---: | ---: | ---: | ---: | +| 125 | 249 | 875 | 249 (249) | 181 (249), 0 (55) | 113 (115), 40 (41) | +| 250 | 499 | 1,750 | 483 (499) | 243 (499), 0 (55) | 109 (111), 40 (41) | +| 500 | 999 | 3,500 | 949 (999) | 332 (999), 0 (55) | 114 (116), 40 (41) | +| 1,000 | 1,999 | 7,000 | 1,309 (1,389) | 460 (1,999), 0 (55) | 112 (113), 40 (41) | +| 2,500 | 4,999 | 17,500 | 2,320 (2,479) | 718 (2,686), 0 (55) | 115 (116), 39 (41) | +| 5,000 | 9,999 | 35,000 | 2,369 (2,519) | 1,010 (3,171), 0 (53) | 115 (116), 40 (41) | +| 10,000 | 19,999 | not run | 2,499 (2,679) | 1,429 (3,321), 0 (55) | 112 (114), 40 (41) | + ## Chrome 157.0.8103.40, headless, 2026-11-11 - Machine: Mac14,13, Apple M2 Max, 12 cores. diff --git a/docs/benchmarks.md b/docs/benchmarks.md index 75ad6c8..05a35f0 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -39,5 +39,5 @@ Edges are batched into at most four paths (plain, weak, and each while dragging) ## Not measured here -- Anything in a browser. [benchmarks-browser.md](benchmarks-browser.md) has frame times for both renderers in headless Chrome, and lists what is still unmeasured there: WebKit (the Mac app and Safari), a window on a real screen, and a real slow device. +- Anything in a browser. [benchmarks-browser.md](benchmarks-browser.md) has frame times for both renderers in headless Chrome and in a headless build of WebKit, and lists what is still unmeasured there: Safari and the Mac app themselves, a window on a real screen, and a real slow device. - The outline (`TreeOutline`) with 10,000+ rows: it isn't virtualized, and on large trees it may be slower than the drawing. diff --git a/docs/test-audit.md b/docs/test-audit.md index 69a38ff..268566e 100644 --- a/docs/test-audit.md +++ b/docs/test-audit.md @@ -13,7 +13,7 @@ Scope: `apps/playground/bench.tsx` and `bench.html`, `scripts/bench-browser.mjs` ### Gaps left - **The benchmark page and script have no tests.** They are measuring tools that ship in no package. What they print was checked by hand: the summary lines against the tables of the run in the doc, and two full runs under an hour apart agreed on which sizes kept up, with median frame times within one display frame of each other. The summary's rules are small (on time means under 5% late frames in every motion; the motion named is the one with the most late frames), and a wrong one would show against the table beside it. Worth a test if the script grows. -- **Nothing here is measured in WebKit, Firefox, a window on a real screen, or on a real slow device.** The doc says so above its numbers. A Safari run is on the owner's test list. +- **Nothing here is measured in Safari, the Mac app's web view, Firefox, a window on a real screen, or on a real slow device.** The doc says so above its numbers. The WebKit section, added later the same day, is Playwright's headless build of the engine, run once by hand with a runner that isn't in this repo; the doc says how. The script's whole-millisecond rule (no decimal places, and a line saying so) was checked by hand against that section, and the Chrome section came out byte for byte the same when regenerated from its saved numbers. A Safari run is on the owner's test list. - **The README's advice to try `canvasThreshold={500}` on slow devices rests on Chrome's throttle alone.** It is worded as something to try, and the default wasn't changed. ### Left out as overkill diff --git a/packages/react/README.md b/packages/react/README.md index fa75d30..9aa23dc 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -21,7 +21,7 @@ const view = createView(data, { id: 'turtles', title: 'Turtles', target: { kind: - Clicks are hit-tested (a node's dot, its name or its wedge), so `onNodeClick` and the explorer's select, fold and drag work as on the SVG. `canvasNodeAt` does the same from your own pointer handlers. - In the explorer, PNG download draws the canvas in the light colors with the disclosure underneath. SVG download isn't available for a canvas tree, and it says so. - The planning is pure and exported (`prepareTree`, `planCanvas`, `hitTest`, `drawPlan`, `describeLod`). Timings in Node are in `docs/benchmarks.md`. - - Frame times in a browser are in `docs/benchmarks-browser.md`, from one machine: headless Chrome 157 on an M2 Max Mac, on 2026-11-11, with other work running. Panning, zooming and scrolling a 1000 px square drawing, the canvas kept to the display's 60 frames a second at every size tried, up to 19,999 nodes, and still did with Chrome slowing the main thread 4 times. SVG kept up through 1,999 nodes and fell to about 30 frames a second at 4,999. With the main thread slowed 4 times, SVG kept up at 499 nodes and dropped frames at 999 (58% late while zooming). So the default switch at 1,000 drawn nodes has room to spare on a fast machine and is a little high for a slow one: if your readers are on slow devices, try `canvasThreshold={500}`. Not measured: WebKit (Safari and the Mac app), Firefox, a window on a real screen, a real slow device, and radial layouts. + - Frame times in a browser are in `docs/benchmarks-browser.md`, from one machine: headless Chrome 157 and a headless build of WebKit 27.2 on an M2 Max Mac, on 2026-11-11, with other work running. The Chrome numbers first. Panning, zooming and scrolling a 1000 px square drawing, the canvas kept to the display's 60 frames a second at every size tried, up to 19,999 nodes, and still did with Chrome slowing the main thread 4 times. SVG kept up through 1,999 nodes and fell to about 30 frames a second at 4,999. With the main thread slowed 4 times, SVG kept up at 499 nodes and dropped frames at 999 (58% late while zooming). So the default switch at 1,000 drawn nodes has room to spare on a fast machine and is a little high for a slow one: if your readers are on slow devices, try `canvasThreshold={500}`. In WebKit (Playwright's build, the engine Safari and the Mac app use but neither program), the canvas also kept to 60 frames a second at every size, and SVG kept up through 1,999 nodes and dropped frames at 4,999 (33% to 56% late while panning); there is no slowed-thread run there. Not measured: Safari itself, the Mac app's web view, Firefox, a window on a real screen, a real slow device, and radial layouts. - **On a server (Next.js App Router, React Server Components).** The package can be imported from a Server Component. `TreeView` renders there as SVG with no JavaScript sent for it: ``. Pass it no handlers on a server, since functions can't cross to the browser. `TreeOutline`, `Explorer`, `Legend`, `LessonPlayer`, `TipLinker`, `ComparePanel`, `SupportControl` and `CanvasTreeView` are client components (`'use client'`): render them from a Server Component with plain data as props, and they run in the browser. A tree over the canvas threshold is handed to `CanvasTreeView`, so it's drawn in the browser; that path hasn't been tried from a Server Component yet. Plain functions that live in the client files can only be called in the browser for now: `canvasNodeAt`, `canvasPlanOf`, `canvasPng`, `cameraBox`, `rootedRecord`, `supportSummary`, `tipsUnder`, `proposeTipLinks`, `linkedPhylogeny`, the `PHYLO_LABELS`, `TIP_LINK_LABELS` and `DEFAULT_CANVAS_SIZE` values, and the hooks. Everything else (`layoutFor`, `treeViewBox`, the canvas planning, shapes, export, drag and near-name helpers) works in both places. `apps/payload-example` draws its public pages this way. - **`TreeOutline`**: the WAI-ARIA tree pattern: one tab stop, arrows, Right/Left to open/close, Home/End, type-ahead, Space to select (Shift+Space adds to the selection), Enter to open a branch, `*` to open its siblings, **Alt+Up/Down to reorder** (the keyboard alternative to dragging). Folded descendants are announced ("12 not shown"), and † is read as "extinct". - **`Explorer`**: toolbar (undo, redo, expand/collapse all, layout, living/extinct/unknown filter), drawing, outline and inspector (rank, status, highlights with *clade* or *group* and the hypothesis they were assessed against, flip children, show only this branch). Layouts the data can't support stay disabled, and the reason is written under the toolbar. The toolbar is one Tab stop: Left and Right move between its controls, Home and End jump (in the number field the arrows stay with the field). Everything you can do by dragging has a button: Move up and Move down in the inspector reorder a taxon among its siblings, and the drawing controls pan left, right, up and down. With the drawing focused, arrow keys pan, + and − zoom, 0 fits and Escape clears the selection; the inspector's hint lists the outline keys. Highlights and trait states are drawn in different shapes as well as colors (twelve: circle, square, diamond, four triangles, hexagon, star, plus, cross and hourglass; they repeat after twelve), and the legend shows the same shapes. Nodes in the drawing aren't Tab stops: the outline is the keyboard path to each taxon (ADR 0010). All text is replaceable through `labels`. diff --git a/scripts/bench-browser.mjs b/scripts/bench-browser.mjs index 1120e57..5894614 100644 --- a/scripts/bench-browser.mjs +++ b/scripts/bench-browser.mjs @@ -168,9 +168,11 @@ if (process.argv.includes('--json')) { process.exit(0) } -const f = (ms) => (ms < 10 ? ms.toFixed(1) : ms.toFixed(0)) const n = (x) => Math.round(x).toLocaleString('en-US') const first = run.reports[0] +// Some browsers round performance.now() to whole milliseconds. Then a decimal place would claim more than was measured. +const coarse = run.reports.every((r) => Number.isInteger(r.refresh) && r.results.every((x) => MOTIONS.every((m) => Number.isInteger(x[m].interval.median) && Number.isInteger(x[m].script.median)))) +const f = (ms) => (ms < 10 && !coarse ? ms.toFixed(1) : ms.toFixed(0)) const headless = run.headless !== false /** On time: under 5% late frames in every motion. */ const onTime = (x) => MOTIONS.every((m) => x[m].late < 0.05) @@ -182,6 +184,7 @@ lines.push('') lines.push(`- Machine: ${run.machine ? `${run.machine}, ${run.cores} cores` : `not recorded${run.cores ? ` (${run.cores} cores)` : ''}`}.`) lines.push(`- Browser: ${first.browser.includes(' ') ? first.browser : first.browser.replace('/', ' ')}${headless ? ', headless (no window on a screen)' : ', in a window'}. Graphics: ${first.gpu}.`) lines.push(run.loadBefore ? `- Load: the machine was busy with other work. Load average ${run.loadBefore.join(', ')} before and ${run.loadAfter.join(', ')} after (1, 5 and 15 minutes).` : '- Load: not recorded.') +if (coarse) lines.push('- Clock: this browser gives the time in whole milliseconds. Every time below is rounded to one: a 16.7 ms frame shows as 17, and script that took under a millisecond shows as 0 or 1.') lines.push(`- Drawing: ${first.stage} × ${first.stage} CSS pixels. 180 frames for each motion, after 20 warm-up frames. One run of each; not repeated.`) lines.push('') lines.push('What the numbers say, counting a size as on time when under 5% of its frames were late in every motion:') @@ -196,7 +199,7 @@ for (const r of run.reports) { // The motion with the most late frames. A median alone can hide them: 44% late still has a median of one frame. const worst = MOTIONS.reduce((w, m) => (miss[m].late > miss[w].late ? m : w)) const doing = { pan: 'panning', zoom: 'zooming', read: 'scrolling at reading size' }[worst] - const at = `${name} first fell behind at ${n(miss.nodes)} nodes (${(miss[worst].late * 100).toFixed(0)}% of frames late while ${doing}, median frame ${f(miss[worst].interval.median)} ms, ${(1000 / miss[worst].interval.median).toFixed(0)} frames a second)` + const at = `${name} first fell behind at ${n(miss.nodes)} nodes (${(miss[worst].late * 100).toFixed(0)}% of frames late while ${doing}, median frame ${f(miss[worst].interval.median)} ms, ${coarse ? 'about ' : ''}${(1000 / miss[worst].interval.median).toFixed(0)} frames a second)` return ok ? `${name} was on time up to ${n(ok.nodes)} nodes; ${at.replace(`${name} first`, 'it first')}` : `${at}, the smallest size run` } lines.push(`- **${title(r)}:** ${say('svg', 'SVG')}. ${say('canvas', 'The canvas')}.`) @@ -217,7 +220,7 @@ lines.push('') for (const r of run.reports) { lines.push(`### ${title(r)}`) lines.push('') - lines.push(`The display frame here, with nothing to draw: ${r.refresh.toFixed(1)} ms (${(1000 / r.refresh).toFixed(0)} frames a second).`) + lines.push(coarse ? `The display frame here, with nothing to draw: ${r.refresh} ms on this browser's whole-millisecond clock (60 frames a second is 16.7 ms).` : `The display frame here, with nothing to draw: ${r.refresh.toFixed(1)} ms (${(1000 / r.refresh).toFixed(0)} frames a second).`) lines.push('') lines.push('| Renderer | Tips | Nodes | Pan: frame, late | Zoom: frame, late | Read: frame, late | Script (pan / zoom / read) | First render, script |') lines.push('| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |') @@ -249,7 +252,8 @@ How long a frame takes while a made-up tree is panned, zoomed and scrolled, draw **What it doesn't.** -- Other browser engines. Only the browsers with a section below were measured. WebKit, which the Mac app and Safari use, and Firefox may differ. To measure one, open \`apps/playground/dist/bench.html\` in it, press Run, save the numbers and run \`node scripts/bench-browser.mjs --from onetaxonomy-bench.json\`. +- Safari, the Mac app and Firefox. Only the browsers with a section below were measured. The WebKit section is Playwright's build of WebKit: the same engine Safari and the Mac app's web view use, but not either program, and headless. It has no slowed-thread pass, because the throttle is Chrome's. To measure another browser, open \`apps/playground/dist/bench.html\` in it, press Run, save the numbers and run \`node scripts/bench-browser.mjs --from onetaxonomy-bench.json\`. +- How the WebKit section was made. This script drives Chrome only. For WebKit, \`playwright-core\` (not a dependency of this repo) opened the built page from disk in its \`webkit\` browser, headless, once at device scale 1 and once at 2, and called \`window.runBench()\`, which is what the Run button calls. The two reports went into one file in the shape \`--json\` prints, and \`--from\` wrote the section. - A window on a real screen. A headless run draws with the graphics card but shows nothing. - All of painting. Raster and compositing happen off the main thread. They show in the frame time only once they fall behind and the browser holds the next frame back, so a steady frame time means the page kept up, not that painting cost nothing. - A slow device. "Main thread slowed 4 times" is Chrome's own throttle. It slows script, style and layout on the main thread and leaves the graphics card and the other threads at full speed, so it is a rough stand-in, not a phone or an old laptop.