Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <file>` 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 <file>` 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.
Expand Down
4 changes: 2 additions & 2 deletions docs/architecture/backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
72 changes: 71 additions & 1 deletion docs/benchmarks-browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion docs/test-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading