Skip to content

Bridge-level thread-handling contract: per-call safety classification, scoped/controllable internal parallelism, encapsulated global state — safety AND throughput #342

Description

@gsdali

Companion to #341 (NCollection race characterisation), from OCCTReconstruct's contention audit (OCCTReconstruct#175/#309, 2026-07-21). Today the thread-safety surface is UNKNOWABLE from the API: OCCTSerial exists but is opt-in, callers discover unsafe calls one crash at a time (#298 was exactly this — the documented 'independent shapes are thread-safe' guarantee was wrong for fillet/chamfer), and the practical downstream response is blanket serialization (measured cost: 4213s vs 2066s for one consumer's suite). A defined and enforced contract at the bridge is both the safety fix and the speed-up.

Measured starting points (from the audit)

  • Bridge calls are not auto-locked; OCCTSerialQueue.swift:6-18 names BSpline adaptor caches, topology mutations, 'various algorithms' as racy — but no per-call classification exists.
  • OCCTBridge_Modeling.mm:1562 hard-codes builder.SetRunParallel(Standard_True): every boolean fans onto the process-shared OSD_ThreadPool::DefaultPool (~ncores workers). N concurrent callers (parallel test tasks, multi-session hosts) oversubscribe by design, and the caller has no knob. Meshing InParallel IS caller-controlled (OCCTBridge_Mesh.mm:211) — the asymmetry is incidental, not designed.
  • The bundled kernel is built WITHOUT TBB (890 OSD_ThreadPool symbols; OSD_Parallel_TBB.cxx.o compiled empty) — internal parallelism is OSD_ThreadPool-only; worth stating in docs since consumers assume TBB.
  • STEP writer mutates Interface_Static process globals (upstream OCCT#1179); downstream wraps its own calls in OCCTSerial (AssemblyExport.swift:290) but every consumer must independently know to.

Proposal (staged)

  1. Classify: annotate every bridge entry point as concurrent-safe (independent shapes, no shared caches) / exclusive (must hold the global lock) / global-state (mutates process globals). Publish the table in docs; back it with the NCollection race under parallel execution: documented downstream as 'run --no-parallel', never filed or characterised upstream — needs the #298 TSan protocol #341/SheetMetal.Builder.build silently returns an empty solid under parallel test execution (8/10 runs) — contradicts the documented "independent shapes are thread-safe" guarantee #298 TSan protocol rather than assertion.
  2. Enforce: auto-acquire OCCTSerial inside the bridge for exclusive/global-state calls (correct-by-default), with a documented fast-path opt-out for callers that already hold the lock. Encapsulate Interface_Static snapshot/restore inside the export calls so no consumer needs to know.
  3. Control internal parallelism: make SetRunParallel/pool sizing caller-controllable (per-call parameter or a process knob), so a consumer running N concurrent pipelines can bound total workers instead of N×ncores. Scoped OSD_ThreadPool instances per heavy call are worth evaluating against the shared default pool.
  4. Throughput payoff: with a trustworthy concurrent-safe surface, consumers stop blanket-serializing — OCCTReconstruct's ~61 pure/independent-shape suites and its per-part sandbox children can use the cores. The speed-up comes from safety, not despite it.

Precedents: #298 + patch 0003 (the classify-then-fix loop works), patch 0010 (#319 — measured-runaway → bounded). Nothing here requires TBB or upstream OCCT changes except where #341's characterisation says so.

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority:P2Normaltype:epicLarge effort tracked via sub-issues

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions