Skip to content

Reuse the corner vectors in ExtrusionGeometry - #351

Merged
sophiedeziel merged 1 commit into
developfrom
perf/reuse-corner-vectors
Aug 22, 2026
Merged

sophiedeziel merged 1 commit into
developfrom
perf/reuse-corner-vectors

Conversation

@sophiedeziel

@sophiedeziel sophiedeziel commented Aug 21, 2026 •

Copy link
Copy Markdown
Collaborator

computeCornerAngles runs once per point on every path, and allocates five Vector3 on each call:

const tangent = new Vector3();
const N = new Vector3();
const B = new Vector3();
const vec = new Vector3();

tangent.copy(P).sub(points[i - 1] || P).normalize()
  .add((points[i + 1] || P).clone().sub(P).normalize())   // <- fifth
  .normalize();

None of them outlive the call. Every one is fully overwritten before it is read, and generateSegment consumes the returned [P, N, B] before the next call happens — so one shared set at constructor scope does the same work.

Benchmark

3.5 MB 3DBenchy: 163,474 lines → 7023 paths → 104,037 points.

before after
Vector3 allocated per tube render 520,185 5
tube geometry generation 103 ms 94 ms

Chrome, median of 7 runs after warm-up, building every extrusion geometry in the model.

The wall-clock saving is the smallest of the three ExtrusionGeometry wins, but it removes half a million short-lived objects from every render, which is the kind of garbage that shows up as jank rather than as throughput.

Correctness

Positions and normals compared element-by-element against a reimplementation of the previous allocating version across 500 geometries — all identical, no tolerance. Model renders unchanged.

Sharing scratch state is exactly the kind of change that breaks quietly, so the two tests added target that failure mode directly rather than the timing:

  • a path that turns 90 degrees twice must produce a different ring at each corner — stale state leaking from one point into the next would show up as a corner oriented like its predecessor
  • a straight run must produce an identical ring at every point — the mirror case, catching the opposite error

npm run check passes: 12 test files, typecheck, lint.

Related

Two other independent optimizations in the same file: #349 pre-sizes the geometry buffers, #350 caches the ring of sines and cosines. All three touch ExtrusionGeometry, so whichever merge later will need a small rebase. Together they take tube geometry generation from 103 ms to 39 ms.

Assisted by Claude Code - Opus 5

@sophiedeziel sophiedeziel changed the title perf: reuse the corner vectors in ExtrusionGeometry Reuse the corner vectors in ExtrusionGeometry Aug 21, 2026
@github-actions

github-actions Bot commented Aug 21, 2026 •

Copy link
Copy Markdown

Visit the preview URL for this PR (updated for commit 2d3bd84):

https://gcode-preview--pr351-perf-reuse-corner-ve-zjb14i1a.web.app

(expires Mon, 21 Sep 2026 03:04:25 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

Sign: 59bd114ae4847b32c2bba0b68620b9069a3e3531

@remcoder remcoder left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct and clean. Verified locally (157/157 pass) and checked the aliasing carefully:

  • The shared scratch vectors (tangent, cornerNormal, cornerBinormal, cross, nextDirection) are all distinct objects, and three.js crossVectors/copy read their arguments before writing this, so no in-place aliasing hazard.
  • generateSegment consumes the returned N/B ring fully inside its loop before the next computeCornerAngles(i+1) call reuses the scratches — safe.
  • Behavior-identical: the .clone().sub(P).normalize() chain is replaced by an equivalent .copy(...).sub(P).normalize(), same op order, no numeric drift.

Tests are well chosen — the 90-degree-turn test is exactly the right probe for stale shared state leaking between corners, and the straight-run test covers the mirror case. Both assert on rings, which is the unit the sharing affects.

Non-blocking: computeCornerAngles still returns [P, N, B] referencing shared scratch, so a future caller that stores N/B past the next call would silently break. A one-line JSDoc note (e.g. "returned vectors are scratch and must be consumed before the next call") would prevent that footgun, but it's not required for this change.

computeCornerAngles runs once per point and allocated five Vector3 every
call: tangent, N, B, vec, and a clone of the next point. On the 3.5 MB
3DBenchy that is 520,185 short lived objects per tube render.

They are all fully overwritten before use and generateSegment consumes
the result before the next call, so a single shared set does the job.
Tube geometry generation drops from 103ms to 94ms, and the garbage goes
away entirely.

Positions and normals compared element by element against the previous
implementation across 500 geometries, all identical.
@sophiedeziel
sophiedeziel force-pushed the perf/reuse-corner-vectors branch from 6079a5b to 2d3bd84 Compare August 22, 2026 03:03
@sophiedeziel

Copy link
Copy Markdown
Collaborator Author

I applied your suggestion

@sophiedeziel
sophiedeziel merged commit 25dc78c into develop Aug 22, 2026
25 checks passed
@sophiedeziel
sophiedeziel deleted the perf/reuse-corner-vectors branch August 22, 2026 03:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Performance improvements or optimizations

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants