Documentation
README
rust β how Carbon writes Rust (tokio + heavy C++ FFI)
The workspace: crates/{occt-bridge,collision,converter,planner} (cxx bridges
to OpenCASCADE + FCL, geometry, motion planning) and apps/assembler (axum
service). The defining constraint: CPU-bound, non-yielding C++ behind async
HTTP β most rules below exist because of it. Several are measured facts from
this repo, not folklore.
Async/tokio discipline β the rules that carry this service
- Sync C++/CPU work never runs on an async worker. Tokio's ~cores worker
threads poll every socket; a non-awaiting FFI call pins one for its full
duration. Measured here: running convert inline at c=64 took
/healthp99 from 7ms to 296ms β one full convert of starvation; real files block minutes. Default:tokio::task::spawn_blocking(move || ffi_work()).await. (block_in_placeonly when you must keep borrowing!Sendlocals; never on a current-thread runtime.) - The blocking pool is the queue. Cap it near core count
(
Builder::new_multi_thread().max_blocking_threads(n)) and excessspawn_blockingtasks queue inside tokio β graceful backpressure with zero 429/queue code.apps/assembler/src/main.rsdoes exactly this; keep +2 headroom sotokio::fsops don't starve behind long converts. - Never hold a lock across
.await. House pattern: do all locking inside sync/blocking scopes (apps/assembler/src/cache.rsβ LRU behindstd::sync::Mutex, lock held for map ops only, never crosses an await). Preferstd::sync::Mutexfor short non-await sections (faster);tokio::sync::Mutexonly when a guard genuinely must cross an await β restructure first. - rayon stays off the tokio runtime. The planner's
par_itersweeps run insidespawn_blockingclosures. Never.par_iter()in anasync fn. Remember rayon's pool is process-global: per-request fan-out multiplied by concurrent requests oversubscribes β measured here, N single-threaded requests across N cores beat NΓall-core fan-outs under load (ASSEMBLER_MESH_PARALLEL=0exists for the same reason on the C++ side). - Timeout β cancellation for blocking work.
tokio::time::timeoutstops the await, not the FFI already running on the blocking pool. Bound the work itself (early-stop callbacks β see the planner's FCLcanStop()threshold nodes) or accept run-to-completion. - Channels:
oneshotto bridge a blocking/rayon result to a handler,mpsc::channel(n)for bounded manyβone,watchfor latest-value config,broadcastfor shutdown fan-out. Bound all fan-out (buffer_unordered(limit),Semaphore) β never unboundedspawnloops. - Shutdown: prefer awaiting completion (
axum's.with_graceful_shutdown(...),JoinSet::join_next()) over cancel-then-sleep β a fixed sleep is fake-graceful.
Ownership & shared state β decision table
| Need | Use | In-repo example |
|---|---|---|
| Share read-mostly across tasks | Arc<T> |
AppState fields |
| Concurrent map, sharded locks | DashMap |
plan job store, progress store |
| Byte-bounded LRU | Mutex<LruCache> (ns-held, sync scopes only) |
cache.rs |
| One-time global init | OnceLock / std::sync::Once |
shared reqwest::Client; C++ std::call_once for OCCT init |
| Counters / phase flags | atomics (AtomicU64, AtomicU8, relaxed) |
progress.rs |
| Lazy memo inside a struct | OnceCell/OnceLock field |
Component::vol_cache, sym_axis_cache |
| Zero-copy payload sharing | bytes::Bytes (clone = refcount) |
cached GLB/graph uploads |
Signatures take &str/&[T]; clone only for ownership transfer. _else
variants (unwrap_or_else, ok_or_else) when the eager argument allocates.
FFI (cxx) β the sharp edges
- Bridges live in
crates/occt-bridge(OCCT) andcrates/collision(FCL), via thecxxcrate; C++ exceptions map toResultat the bridge. - C++ types are
!Send/!Syncby default. Keep handles thread-confined inside onespawn_blockingclosure; move plain data (Vec/f64) out. When a wrapper genuinely is shareable,unsafe impl Send/Syncwith the justification in a comment β in-repo precedent:SharedBvhandCollisionWorld(immutable after construction, so concurrent reads are sound). No justification comment = review reject. - Global C++ state is real: OCCT config must be initialized once
(
std::call_onceinocct.cc) β per-call mutation of process-global state was this repo's original concurrency bug. // SAFETY:comment on everyunsafeblock, no exceptions.- Don't set
panic = "abort"without checking the C++ exception/unwind story at the cxx boundary first.
Errors β the house convention (verified, not aspirational)
Carbon does not use thiserror/anyhow. Errors are hand-rolled structs
mirroring the wire contract: ConvertError { code, message }
(crates/converter), ApiError { status, code, message } (apps/assembler),
with From impls at the boundary. Follow that for service-contract errors.
thiserror is fine for a new library crate with no wire contract; never leak
anyhow from a library. ? over match chains; no unwrap/expect outside
tests and provably-infallible spots.
Performance β measured habits
- Measure, don't guess; always
--release. This repo's profiling loop: macOSsample <pid>(worked where samply left hex frames), phase-timing via env-gatedeprintln!(OCCT_PROFILE=1),cargo build --profile profiling(release + debug symbols, defined in the workspaceCargo.toml) for symbolicated native stacks. - Contention shows up as user time, not lock waits: zero
ulock/syswith bad thread scaling = cache-line/allocator trouble. Discriminate software vs hardware with the multi-process control (N processes vs N threads of the same work β this exposed the OCCT allocator true-sharing bug). - Allocation hygiene:
with_capacitywhen size is known; no intermediate.collect()(passimpl Iterator);SmallVeconly where measured (the contacts type here β kept, measured as noise); memoize expensive derived values inOnceCellfields rather than recomputing per iteration. - Allocator:
tikv-jemallocatoris Linux-only inapps/assemblerβ measured a ~6% loss on macOS. Don't "fix" that gate. #[inline], LTO,codegen-units=1: only with a benchmark proving them.
Tooling
cargo build --release -p assemblerβ workspace root; build.rs resolves OCCT viaOCCT_PREFIXβ cached static build (~/.cache/carbon-occt/) β brew (seeapps/assembler/AGENTS.md).- Tests: unit in-module; integration in
crates/*/tests/β behavioral synthetic-geometry tests (crates/planner/tests/synthetic_plan.rs) are the model: build inputs in code, assert invariants, no fixtures/goldens. Env-gated tests (ASSEMBLER_FIXTURES,ASSEMBLER_ASSEMBLIES) skip silently when unset β fine for corpus tests, never for core logic. cargo clippy --all-targets -- -D warningsbefore committing Rust;#[expect(lint, reason = "...")]over blanket#[allow].