Skip to content

Add diagnostic tracing and fix ot_analyze_shape O(N^4) wire matching#3

Merged
gsdali merged 1 commit into
mainfrom
feat/dylib-tracing
May 2, 2026
Merged

Add diagnostic tracing and fix ot_analyze_shape O(N^4) wire matching#3
gsdali merged 1 commit into
mainfrom
feat/dylib-tracing

Conversation

@gsdali
Copy link
Copy Markdown
Owner

@gsdali gsdali commented May 1, 2026

Stacked on top of #2.

Two changes that go together — the tracing exposed a real bug in ot_analyze_shape that was almost certainly the cause of the "stuck with no output" symptom reported in the templot-restructured viewer.

1. Diagnostic tracing infrastructure

New src/occt_templot_trace.{h,cpp}. Off by default — enable either way:

OCCT_TEMPLOT_TRACE=1 ./your-app           # env var
ot_set_trace(true);                        // public C API, also bound in pascal/occt_templot.pas

When on:

  • Every ot_* function in the heal module emits [ot] ... entry / phase / exit lines to stderr with immediate flush, so a hung call is still visible.
  • OCCT's own Message::DefaultMessenger() is wired to stderr as [occt:trace|info|warn|alarm|fail] ..., surfacing internal ShapeFix / Sewing / BRepCheck output that was previously swallowed.

Sample on bearing.stl:

[ot] occt_templot_init: trace=1
[ot] ot_analyze_shape: enter (tol=0.0001)
[ot] ot_analyze_shape: BRepCheck_Analyzer start
[ot] ot_analyze_shape: BRepCheck_Analyzer done, valid=1
[ot] ot_analyze_shape: shell scan start
[ot] ot_analyze_shape: shell scan done, shells=0 freeEdges=0
[ot] ot_analyze_shape: edge scan done, edges=74040 smallEdges=0
[ot] ot_analyze_shape: face scan done, faces=24680 smallFaces=0
[ot] ot_analyze_shape: wire ancestor map done, wires=24680
[ot] ot_analyze_shape: wire gap scan done, gaps=0
[ot] ot_analyze_shape: exit ok

The phase granularity inside ot_analyze_shape was deliberate — see (2).

2. Fix ot_analyze_shape O(N⁴) wire matching

The wire-gap scan was a nested-explorer search:

for (TopExp_Explorer wireExp(shape, TopAbs_WIRE); ...) {
    for (TopExp_Explorer faceExp(shape, TopAbs_FACE); ...) {        // for each wire!
        for (TopExp_Explorer innerWireExp(face, TopAbs_WIRE); ...)  // for each face!
            if (innerWireExp.Current().IsSame(wire)) ...
    }
}

That is O(W × F × W_per_F). On a typical solid where W ≈ F, this is O(N⁴) in the face count. On bearing.stl (24,680 faces / 24,680 wires) the inner-loop count is roughly 1.5 × 10¹³ — easily a multi-minute hang on real models. Anything dense enough to look "stuck with no output" hits this.

Replaced with a single TopExp::MapShapesAndAncestors(shape, TopAbs_WIRE, TopAbs_FACE, ...) build (O(F)) and a flat walk over the unique wires:

TopTools_IndexedDataMapOfShapeListOfShape wireToFace;
TopExp::MapShapesAndAncestors(s->shape, TopAbs_WIRE, TopAbs_FACE, wireToFace);
for (Standard_Integer i = 1; i <= wireToFace.Extent(); i++) { ... }

As a side effect this also fixes a duplicate-wire double-counting bug in the old loop (a wire shared between two faces was counted twice, inflating gap_count).

Test plan

  • ctest 4/4 green with tracing off (default) on macOS arm64.
  • OCCT_TEMPLOT_TRACE=1 ./test_heal bearing.stl — 16/16 pass; analyze finishes in ms (was unbounded).
  • All four formerly-suspect calls (analyze / heal / sew / make_solid) traced end-to-end in test_heal output.
  • Linux + Windows CI via PR checks.

How a downstream caller uses this

From the templot-restructured Pascal app:

ot_set_trace(true);                  // anywhere before the hanging call
analysis := ot_analyze_shape(s, tol);
ot_set_trace(false);                 // optional

Or just launch the app with OCCT_TEMPLOT_TRACE=1 set in the environment.

🤖 Generated with Claude Code

@gsdali gsdali changed the base branch from feat/occt-8.0.0-beta1 to main May 2, 2026 01:01
Two changes that go together — the tracing exposed the analyze hang
the user reported on real shapes:

1. Trace infrastructure (src/occt_templot_trace.{h,cpp}). Off by
   default; enable via OCCT_TEMPLOT_TRACE=1 env var or the new
   ot_set_trace(true) public C function. When on, OT_TRACE(...) lines
   from each ot_* call are flushed to stderr immediately so hangs are
   still visible, and OCCT's Message::DefaultMessenger() is wired to
   stderr so ShapeFix / Sewing / BRepCheck internal output surfaces.

2. ot_analyze_shape no longer scans every face inside a per-wire loop.
   The previous code was nested explorer iteration — O(W * F * W_per_F),
   roughly N^4 on dense meshes — and would hang for tens of minutes on
   anything with thousands of faces. Replaced with a single
   TopExp::MapShapesAndAncestors(WIRE, FACE) build (O(F)) and a flat
   walk over the unique wires. As a side effect, the duplicate-wire
   double-counting bug in the old loop is also gone.

Wired tracing into every heal-module entry point (analyze, heal,
heal_detailed, sew, upgrade, make_solid) with phase-level granularity
in analyze so the log says exactly which scan a stuck call is in.

Validated on bearing.stl from the OCCT test data (24,680 faces /
74,040 edges / 24,680 wires) — analyze now finishes in milliseconds;
on the old code it would run ~10^13 inner-loop iterations.

ctest 4/4 green on macOS arm64 with tracing both off (default) and on.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gsdali gsdali force-pushed the feat/dylib-tracing branch from c1919c8 to 41c12a4 Compare May 2, 2026 01:02
@gsdali gsdali merged commit 122a39b into main May 2, 2026
3 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.

1 participant