Skip to content

perf(kernel): build each boolean once; fuse pattern instances in one boolean - #779

Open
w1ne wants to merge 1 commit into
developfrom
perf/circular-pattern-union
Open

w1ne wants to merge 1 commit into
developfrom
perf/circular-pattern-union

Conversation

@w1ne

@w1ne w1ne commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Problem

Evaluating a real part (a hinge with four 36-tooth patternCircular serration rings) took 6-10 minutes hosted and about 2-3 minutes on a Mac. Hosted render_preview then went over the MCP client's 300 s limit.

Root cause (measured)

A CPU profile of one 36-tooth ring showed 92% of the time in fusePatternInstances. That time split evenly between two calls: constructor$BRepAlgoAPI_Fuse_3 (30.0 s) and BRepAlgoAPI_BooleanOperation$Build (27.1 s).

  1. Every history-aware boolean ran twice. The shape-taking constructors (BRepAlgoAPI_Fuse_3/_Cut_3/_Common_3) already run the operation. The Build() call after SetToFillHistory(true) then ran all of it again. This affects every union, subtract and intersect, not only patterns.
  2. Patterns were folded pairwise. N instances took N-1 fuses, each one against the growing accumulator.

Fix

  • runBooleanWithHistory default-constructs the builder, calls SetArguments, SetTools and SetToFillHistory, then builds once.
  • New fuseManyWithHistory(body, tools[]) runs one general-fuse build over all inputs. The pattern lowerer uses it. Per-instance lineage retags are kept. mergeBooleanHistoryMany applies the input maps in order, so the first input wins a shared face, the same as the pairwise fold.
  • booleanBuildStats.builds is a deterministic cost probe for tests.

Numbers

The reproduction part, run through the CLI built from this branch and from develop. Other jobs were loading the Mac (load average 18-30), so wall times vary.

command develop this PR
evaluate 118-203 s 47-103 s
export stl --parts all 145 s 39 s
export step 177 s 58-60 s
parts 109 s 43 s

The published kernelcad@0.15.0 on the same Mac took 146 s for evaluate, 104 s for STL and 83 s for STEP.

Geometry is unchanged. parts --json gives the same volume (7834.9954 / 14632.1058 mm³), surface area, bbox and triangle count (13312 / 6698) for both parts. Both STLs are watertight. The STEP file is the same size (2149749 bytes).

Tests

tests/unit/backends/occt/patternBooleanCost.test.ts counts OCCT builds, not wall time. On develop it fails: a 12-instance circular pattern costs 22 builds, a 5-instance linear pattern 8, and one pairwise fuse 2. On this branch each costs 1. A serration-ring case (teeth that touch along edges) checks the volume (= N × tooth), the bbox and the solid count. It passes on both develop and this branch, which shows the geometry is the same.

Local runs: tests/unit/{backends,naming,patterns,kernel,modeling,compute,capture,intent} plus src/kernel and src/modeling, 2692 passed. The 7 failures are in src/modeling/parts/* and fail the same way on develop: assets/parts was not generated locally.

…boolean

A hinge part with four 36-tooth serration rings (patternCircular) took
minutes to evaluate. A CPU profile of one ring: 92% of the time was in
fusePatternInstances, and it split evenly between the
BRepAlgoAPI_Fuse_3 constructor and BRepAlgoAPI_BooleanOperation::Build.

1. Every history-aware boolean ran twice. The shape-taking constructors
   (BRepAlgoAPI_Fuse_3, _Cut_3, _Common_3) already run the operation; the
   explicit Build() after SetToFillHistory ran all of it again.
   runBooleanWithHistory now default-constructs the builder, sets the
   arguments, tools and history flag, and builds once. This halves the
   cost of every union, subtract and intersect.

2. The pattern lowerer folded N instances with N-1 pairwise fuses, each
   against the growing accumulator. It now fuses all instances in ONE
   general-fuse build (fuseManyWithHistory). Per-instance lineage retags
   are kept; mergeBooleanHistoryMany applies input maps in order, so the
   first input wins a shared face, the same as the pairwise fold.

Reproduction part, CLI on this Mac (machine under other load; wall
times vary): evaluate 118-203 s -> 47-103 s, export stl --parts all
145 s -> 39 s, export step 177 s -> 58-60 s. Output is unchanged: the
same volume, surface area, bbox and triangle count per part, both STLs
watertight, and the STEP file is the same size.

Test: tests/unit/backends/occt/patternBooleanCost.test.ts counts OCCT
builds (not wall time). On the old code a 12-instance circular pattern
costs 22 builds, a 5-instance linear pattern 8, one pairwise fuse 2.
Now each costs 1. A serration-ring case checks the volume, bbox and
solid count of the one-shot fuse.

Signed-off-by: Andrii Shylenko <14119286+w1ne@users.noreply.github.com>

This branch has not been deployed

No deployments
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.

1 participant