Skip to content

perf(app): hand a map collection to MapLibre only when it changed - #740

Merged
efiten merged 1 commit into
efiten:masterfrom
khagele:perf/739-skip-unchanged-set-data
Oct 2, 2026
Merged

efiten merged 1 commit into
efiten:masterfrom
khagele:perf/739-skip-unchanged-set-data

Conversation

@khagele

@khagele khagele commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

Closes #739

The 1 Hz tick called setData on every source, changed or not, and MapLibre serialises, tiles and uploads whatever it is handed. On a Nijmegen-to-Arnhem export (4559 receptions, 4478 rays, reach on) a tick with nothing new took 96.6 ms of main thread on a MacBook M1.

In this PR

  • put() in huntmap.js writes a source only when the collection is not the object it was last handed (lastSent, rendercache.js). The dots, the pillars, the hex and the noise cells already come out of a cache that returns the same object on a hit, and EMPTY is one constant.
  • The rays are compared, not keyed. They come out of five inputs (records, registry, attribution, selection, theme) and are built in under a millisecond, so sameFeatures compares the answer with last draw's and reuses that object when equal. It is generic over the properties, so a field added to the rays later is compared without anyone adding it.
  • The 3D rays read the terrain height when their buffers are filled, and the mesh's tiles arrive after it is switched on. With the mesh on they are filled on every draw, as before, and once more on the first draw after it went off.
  • A style swap brings every source back empty, so addOverlays clears what counted as sent.

Measured

The real createHuntMap in a browser, same export, five ticks with the same receptions as fresh objects:

before after
setData on dots, hex, rays, noise, pillars 5 each 0
bufferData calls 135 0
main thread per tick 96.6 ms 11.8 ms

Also checked there: one new hearing sends dots, hex and rays once; a selection sends them once and holding it sends nothing; going to 3D and back sends what the view shows; with the mesh on the ray buffers are filled on every draw (2 bufferData per draw); after applyBasemap every source is sent once and holds its 4477 rays and 4559 dots.

Verification

  • rendercache.test.js: 13 new tests for sameFeatures and lastSent. huntmap-contract.test.js: 7 new, reading huntmap.js as text, as that file does: the five collections are never written around put(), and the mount clears the memory.
  • Each was run with its change reverted: 15 mutations, 15 red.
  • app: 1643 tests green (1623 on master), eslint clean, build clean.
  • No changelog entry: nothing looks or behaves differently.

Not in this PR

  • A tick in which a reception arrived costs what it did.
  • A phone was not measured.
  • The map (web/) draws per fetch and per pan, not per second.

🤖 Generated with Claude Code

@efiten

efiten commented Sep 30, 2026

Copy link
Copy Markdown
Owner

#734 is merged (4c2a57b), so this one needs the rebase onto master you planned in #734's thread. It passed its review; it goes in once the rebase is green.

The 1 Hz tick called setData on every source, changed or not, and
MapLibre serialises, tiles and uploads whatever it is handed. On a
Nijmegen-to-Arnhem export (4559 receptions, 4478 rays) a tick with
nothing new took 96.6 ms of main thread on a laptop, and 11.8 ms with
this change.

put() remembers what each source was last handed (lastSent) and skips a
collection that is the same object. The dots, the pillars, the hex and
the noise cells already come out of a cache that returns the same object
on a hit. The rays are built in about a millisecond from five inputs, so
their answer is compared instead (sameFeatures) and an equal one reuses
last draw's object.

The 3D rays read the terrain height when their buffers are filled, so
with the mesh on they are still filled on every draw.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@khagele
khagele force-pushed the perf/739-skip-unchanged-set-data branch from c278234 to 1fae10d Compare October 1, 2026 05:53
@khagele

khagele commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto master (1fae10d). One conflict, in huntmap.js: #734 moved the hex and noise writes into drawCells, so put() now writes hex and hex-b in its slot loop and noise there, and points/points-3d in draw(). The contract test checks the slot loop instead of a literal put('hex', …), red with a direct setData back in drawCells. App: 1727 tests, eslint, build. The 96.6 → 11.8 ms measurement is from before #734 and was not repeated.

@efiten
efiten merged commit c6fc9a9 into efiten:master Oct 2, 2026
6 checks passed
@efiten

efiten commented Oct 2, 2026

Copy link
Copy Markdown
Owner

Merged as c6fc9a9. Fix-check of 1fae10d: every write to the cached sources goes through put()/putRays3D(), and addOverlays clears sent on every style mount. App: 1727 tests, eslint (run). Mutations: a direct setData in the drawCells slot loop, a direct setData on noise, and a removed sent.clear() each turn the contract test red.

Three gaps in the contract test, left for a follow-up if you want them. They can cost speed, not correctness:

  • rays.setData(fcRays.features) in place of putRays3D(fcRays) (huntmap.js:274) stays green, 29/29. expect(mapSrc.match(/rays\.setData\(/g)).toHaveLength(1) would pin it.
  • The sent.clear() slice runs from function addOverlays to function applyBasemap, about 800 lines, so the call can move out of addOverlays and the test still passes.
  • The mesh/raysOnMesh refill logic in putRays3D (huntmap.js:200-205) has no behavioural test.

The 96.6 → 11.8 ms figure is from before #734. If a release note quotes it, it needs a new measurement.

@efiten

efiten commented Oct 2, 2026

Copy link
Copy Markdown
Owner

Post-merge review of c6fc9a9 with the full template: no point to change. It confirms the three test gaps from the comment above and adds two:

  • There is no behavioural test of the lastRays handoff in drawCoverage. Removing sameFeatures there, or swapping the branches, turns nothing red. Only the speed gain would be lost.
  • sameFeature (app/src/rendercache.js) compares properties but not the top-level feature.id. coverageFeatures sets no id today, so nothing is wrong now. If an id is added later for feature-state or promoteId, a changed id with the same properties would leave the map stale. Either compare a.id === b.id, or narrow the comment "generic on purpose" to properties.

efiten added a commit to khagele/core-hunter that referenced this pull request Oct 2, 2026
Resolves the conflict with efiten#740 in app/src/huntmap.js: drawCells writes
the noise source through put(), as efiten#740 does, and keeps passing
vis.noise to drawHexLabels for the noise labels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
efiten pushed a commit that referenced this pull request Oct 2, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>app: 1.31.0</summary>

##
[1.31.0](app-v1.30.0...app-v1.31.0)
(2026-10-02)


### Features

* **app:** show the measured noise floor in the cells and the HUD
([#711](#711))
([d97cec0](d97cec0))


### Performance Improvements

* **app:** hand a map collection to MapLibre only when it changed
([#740](#740))
([c6fc9a9](c6fc9a9))
</details>

<details><summary>web: 1.27.0</summary>

##
[1.27.0](web-v1.26.0...web-v1.27.0)
(2026-10-02)


### Features

* **app:** show the measured noise floor in the cells and the HUD
([#711](#711))
([d97cec0](d97cec0))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

app: every tick hands every map collection to MapLibre again, changed or not

2 participants