Skip to content

perf(modeling): fix O(n²) performance issues in flatten, splitPolygonByPlane, and extrudeFromSlices#1429

Merged
z3dev merged 10 commits intojscad:masterfrom
jbroll:perf/modeling-optimizations
Feb 7, 2026
Merged

perf(modeling): fix O(n²) performance issues in flatten, splitPolygonByPlane, and extrudeFromSlices#1429
z3dev merged 10 commits intojscad:masterfrom
jbroll:perf/modeling-optimizations

Conversation

@jbroll
Copy link
Copy Markdown
Contributor

@jbroll jbroll commented Jan 26, 2026

Summary

This PR addresses several O(n²) performance issues identified through code review and benchmarking:

Also includes:

  • New benchmark suite (npm run bench) for performance regression testing
  • Unit tests for flatten (13 tests) and splitPolygonByPlane (6 tests)
  • CLAUDE.md for AI assistant guidance

Benchmark Results

flatten (used by 48 files across codebase)

Test Before After Improvement
100 items 38 µs 8.6 µs 4.4x
1000 items 2130 µs 80 µs 26x

Boolean operations (splitPolygonByPlane fix)

Test Before After Improvement
subtract-sphere-16 4.27 ms 1.69 ms 2.5x
subtract-sphere-32 10.41 ms 8.50 ms 1.2x

Test plan

  • All 566 existing tests pass
  • New unit tests for flatten (13 tests)
  • New unit tests for splitPolygonByPlane (6 tests)
  • Manual verification of boolean and extrusion operations
  • Benchmark suite validates performance improvements

Related Issues

Fixes #1419, #1421, #1422

Note: #1427 was closed as invalid (poly3.toPoints already returns direct reference)

🤖 Generated with Claude Code

@hrgdavor
Copy link
Copy Markdown
Contributor

I am extra cautious regarding AI assisted code. I do use it with my ow preferences what I allow it to do. This PR and issues look reasonable, and well guided by @jbroll . I was always bad at doing reviews, so this as well as many other PR's will have to be approved by @z3dev.

I am liking what I am seeing here regardless of my guarded/cautious stance on AI coding agents.

@jbroll
Copy link
Copy Markdown
Contributor Author

jbroll commented Jan 26, 2026

I actually didn't mean for this PR to become public quite yet, the agent made the pr against upstream instead of my local repo. if you like I'm happy to break this up into individual PRs for each optimization

@hrgdavor
Copy link
Copy Markdown
Contributor

hrgdavor commented Jan 26, 2026

if you like I'm happy to break this up into individual PRs for each optimization

I personally would not require you to do that, so please keep it as is, until @z3dev finds time to review it :)

@jbroll jbroll force-pushed the perf/modeling-optimizations branch from 558ddb4 to 14f6cfe Compare January 26, 2026 22:10
@z3dev
Copy link
Copy Markdown
Member

z3dev commented Jan 26, 2026

@jbroll Nice! Performance improvements are always welcome. Thanks!

Please drop the small performance tests as there aren’t any performance suites for the CI. There’s a test suite under JSCAD Community. You can try this with these changes to get an understanding of the performance impact.

https://github.com/jscad-community/jscad-performance

Also, V3 has performance changes. So, these changes should be integrated in the future.

I typically work on V3, and integrate changes into V2 if important. You’re welcome to work on either version currently but there will be a day that V3 becomes master.

@z3dev
Copy link
Copy Markdown
Member

z3dev commented Jan 27, 2026

@jbroll Fun stuff! I reviewed most of the changes which look good. There are a few changes that I need to understand better. I’ll complete a full review this weekend.

Please keep the PR scope small as that makes the reviews easy and quick.

@jbroll jbroll closed this Jan 28, 2026
@jbroll jbroll deleted the perf/modeling-optimizations branch January 28, 2026 17:45
@jbroll jbroll restored the perf/modeling-optimizations branch January 28, 2026 18:06
@jbroll jbroll reopened this Jan 28, 2026
jbroll and others added 8 commits January 28, 2026 13:38
Replace recursive concat-based flatten with iterative stack-based
implementation. This eliminates the O(n²) behavior caused by creating
a new array on every concat call.

Benchmark results:
- flatten-flat-100:  38µs → 8.6µs  (4.4x faster)
- flatten-flat-1000: 2130µs → 80µs (26x faster)
- flatten-mixed-20x20: 94µs → 14µs (6.6x faster)

Scaling is now linear (O(n)) instead of quadratic.

The iterative approach also avoids stack overflow on deeply nested
arrays and doesn't hit spread operator limits.

Fixes jscad#1422

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds comprehensive test coverage for the flatten utility function:
- Empty and flat arrays
- Single and deep nesting
- Mixed nesting depths
- Empty nested arrays
- Order preservation
- Object reference preservation
- Various types (primitives, objects, functions)
- Large arrays (1000+ items)
- Input immutability

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…oval

Replace splice-in-loop pattern with a helper function that builds
a filtered array in O(n) time.

Before: splice() is O(n) called in a loop = O(n²)
After: Single pass building new array = O(n)

Benchmark improvement:
- subtract-sphere-16: 4.27ms → 1.69ms (2.5x faster)
- subtract-sphere-32: 10.41ms → 8.50ms (1.2x faster)

This is in the hot path for all CSG boolean operations.

Fixes jscad#1421

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds direct test coverage for the polygon splitting function:
- Coplanar-front detection (type 0)
- Polygon entirely in front (type 2)
- Polygon entirely in back (type 3)
- Spanning polygon split (type 4)
- Duplicate vertex removal verification
- Complex polygon splitting

This provides regression protection for the performance fix in jscad#1421.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace O(n²) array concatenation pattern with O(n) push operations.

Each concat() creates a new array and copies all elements. In a loop
with N iterations, this results in O(n²) total copying. Using push()
with a for-loop avoids this overhead.

Uses for-loop instead of spread operator to avoid stack limits with
large polygon arrays (as noted by @hrgdavor in jscad#1420).

Fixes jscad#1419

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix O(n²) splice in reTesselateCoplanarPolygons with mark-and-filter
- Reuse mat4 for constant X rotation in extrudeRotate
- Reuse vec3 increment in extrudeWalls repartitionEdges
- Replace spread with loop in retessellate to prevent stack overflow
- Add unit tests for all optimizations
- Add benchmark suite for primitives and boolean operations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace immediate splice with lazy cleanup to eliminate O(n²) cost
when many nodes are removed during boolean operations.

- Remove indexOf + splice from remove() - just mark node as removed
- Add lazy compaction in getPolygons() to clean up dead nodes
- All iteration paths already skip removed nodes via isRemoved() or
  polygon !== null checks

Performance improvement on union operations:
- union-sphere-32: 26.9ms -> 10.5ms (61% faster)
- union-sphere-64: 238ms -> 89ms (63% faster)

No memory impact - dead nodes cleaned up when getPolygons() called.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comment explaining that getPolygons() is only called on root node
via Tree.allPolygons(), addressing code review feedback about children
array mutation safety.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@jbroll jbroll force-pushed the perf/modeling-optimizations branch from 14f6cfe to bf6fbf5 Compare January 28, 2026 18:39
@z3dev z3dev requested review from hrgdavor, platypii and z3dev January 31, 2026 01:22
Copy link
Copy Markdown
Member

@z3dev z3dev left a comment

Choose a reason for hiding this comment

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

@jbroll please see my comments. there may be some additional improvements available.

Comment thread packages/modeling/src/operations/booleans/trees/splitPolygonByPlane.js Outdated
Comment thread packages/modeling/src/utils/flatten.js
Comment thread packages/modeling/src/operations/booleans/trees/PolygonTreeNode.js Outdated
jbroll and others added 2 commits February 1, 2026 16:50
- splitPolygonByPlane: move vertices.length < 3 check to call sites
  to avoid function invocation overhead per z3dev's feedback
- PolygonTreeNode: replace .filter() with for loop for faster
  compaction on large polygon sets per z3dev's feedback

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Per z3dev's suggestion, replace the stack-based implementation with the
native Array.flat(Infinity) method. This is simpler and equally performant
for JSCAD's use cases (normalizing variadic arguments).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@z3dev z3dev self-requested a review February 7, 2026 05:54
Copy link
Copy Markdown
Member

@z3dev z3dev left a comment

Choose a reason for hiding this comment

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

@jbroll Thanks for these performance changes. 👍

@z3dev z3dev merged commit 1a6ef03 into jscad:master Feb 7, 2026
2 checks passed
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.

perf(modeling): O(n²) array concatenation in extrudeFromSlices.js

4 participants