Compile-Time Isolation Refactor
June 6, 2026 ยท View on GitHub
This is the active migration plan for making full-feature debug/selfdev builds faster without removing features from the developer binary.
Goal
Keep the normal debug/selfdev binary production-like, including PDF, embeddings, providers, update/selfdev tooling, and other integrations, while reducing the amount of Rust code that must be recompiled after common edits.
The target is not just "more crates". The target is a wider dependency DAG with smaller serial front-end units and cleaner invalidation boundaries.
Current diagnosis
The workspace already has many crates, but the critical path is dominated by a small number of large crates stacked linearly:
graph LR
base["jcode-base\n~100k+ LOC"] --> appcore["jcode-app-core\n~100k LOC"]
appcore --> tui["jcode-tui\n~100k+ LOC"]
tui --> rootlib["jcode lib"]
rootlib --> bin["jcode bin"]
small["50+ smaller crates"] -. mostly parallel .-> base
From the last available Cargo timing report parsed with scripts/compile_time_probe.sh --skip-build:
- Cargo timing wall: 16.00s
- Known jcode serial stack span: 14.72s
- Known jcode serial stack summed unit time: 17.36s
- Known jcode serial stack frontend time: 11.99s
Slowest units from that timing report:
| Unit | Total | Frontend | Codegen |
|---|---|---|---|
jcode-app-core | 4.73s | 3.82s | 0.91s |
jcode-base | 4.34s | 3.63s | 0.71s |
jcode-tui | 4.18s | 3.14s | 1.04s |
jcode bin | 2.34s | n/a | n/a |
jcode lib | 1.77s | 1.40s | 0.37s |
This means the main bottleneck is rustc front-end serialization in a few mega-crates, not linker choice or third-party cold compile.
Measurement
Use the focused timing probe for each phase:
scripts/compile_time_probe.sh --json target/compile-time-probe.json
scripts/compile_time_probe.sh --touch crates/jcode-tui/src/tui/app/input.rs
scripts/compile_time_probe.sh --touch crates/jcode-app-core/src/server.rs
scripts/compile_time_probe.sh --touch crates/jcode-base/src/provider/mod.rs
For broader repeated measurements, continue using:
scripts/bench_compile.sh selfdev-jcode --runs 3 --touch <path> --json
scripts/bench_selfdev_checkpoints.sh --skip-cold --touch <path> --runs 1
Track at least:
- Full-feature selfdev build wall time.
- Cargo timing wall time.
jcode-base -> jcode-app-core -> jcode-tui -> jcode lib -> jcode binstack span.- Sum of frontend time in the serial stack.
- Incremental rebuild after touching representative high-churn files.
- Static report drift from
scripts/compile_isolation_report.py: LOC, inline tests,async_trait, and target-state dependency advisories.
Target architecture
graph TD
bin["jcode binary\ntiny composition root"] --> cli["jcode-cli"]
bin --> tui["jcode-tui"]
bin --> server["jcode-server"]
bin --> providers["provider leaf crates"]
bin --> tools["tool leaf crates"]
cli --> api["jcode-client-api / app-api"]
tui --> api
server --> api
api --> protocol["protocol + view models"]
protocol --> types["small stable type crates"]
server --> agent["jcode-agent"]
server --> registry["jcode-tool-registry"]
server --> auth["jcode-auth-core"]
server --> session["jcode-session-core"]
server --> memory["jcode-memory-core"]
providers --> provider_core["jcode-provider-core"]
tools --> tool_core["jcode-tool-core"]
Rules:
- TUI and CLI depend on client API, protocol, view models, and small type crates, not full server/provider/tool implementations.
- Provider implementations are leaf crates. AWS/Bedrock dependencies live only in the Bedrock provider crate.
- Tool implementations are leaf crates. Heavy tools like PDF/browser/Gmail/search are isolated behind tool-core interfaces.
- Shared bottom crates are small and stable. Avoid putting high-churn behavior in protocol/type crates.
- Avoid broad
pub use whole_crate::*compatibility ladders in final architecture.
Migration sequence
Phase 0: measurement and guardrails
Status: started.
Deliverables:
scripts/compile_time_probe.shscripts/compile_isolation_report.py- this document
- dependency boundary checks/advisory reports
Success criteria:
- Every structural phase has before/after timing.
- The timing report makes the serial stack visible.
Phase 1: widen the god-crate critical path
Split the three long-pole crates into sibling domain crates. Priority is widening the graph, not extracting more tiny type crates.
Likely first splits:
- From
jcode-base:jcode-auth-corejcode-session-corejcode-memory-core- provider implementation crates, especially Bedrock/AWS as a leaf
- From
jcode-app-core:jcode-serverjcode-agentjcode-tool-registry- service crates for background/swarm/update/selfdev as needed
- From
jcode-tui:jcode-client-api/ view-model boundary first- then move reusable client-side state logic out of the terminal rendering crate only when it creates a real parallel unit
Success criteria:
- Touching common TUI code no longer recompiles app-core/provider/server implementation crates.
- Touching a provider implementation no longer recompiles TUI or broad base code.
- Cargo timing shows multiple medium-sized Jcode crates running in parallel instead of one 4-deep mega-crate ladder.
Phase 2: kill glob re-export ladders
Current compatibility layering preserves the old monolith shape:
pub use jcode_base::*;
pub use jcode_app_core::*;
pub use jcode_tui::*;
Migration approach:
- Keep compatibility re-exports temporarily while moving code.
- Convert high-churn modules to explicit imports from leaf crates.
- Remove glob re-exports once downstream imports are explicit.
Success criteria:
- New code does not rely on whole-layer prelude-style re-exports.
- Dependency direction is visible in imports and Cargo manifests.
Phase 3: move inline tests out of hot crates
Problem:
- Inline
#[cfg(test)]modules makecargo testcompile large production crates plus large test bodies as one rustc unit.
Target:
- Integration tests or dedicated
*-test-supportcrates for broad behavior tests. - Keep tiny unit tests inline only when they are genuinely local and cheap.
Success criteria:
- Targeted tests no longer require monolithic test cfg builds for unrelated domains.
Phase 4: reduce front-end macro tax
Targets:
- Replace
async_traitwith nativeasync fnin traits where the trait is not used asdyn. - Keep
async_traitonly at object-safe plugin/interface boundaries where boxed futures are intentional. - Avoid adding derive-heavy types to broad shared crates unless the type is stable and necessary.
Success criteria:
- Fewer proc-macro expansions in the hot crates.
- No object-safety regressions.
Anti-goals
- Do not make fast debug builds incomplete by default.
- Do not split code into tiny crates unless the split creates a real invalidation or parallelism boundary.
- Do not move high-churn behavior into low-level type/protocol crates.
- Do not do a single giant rewrite. Each phase should build and be measurable.
Validation checklist per phase
Before committing a phase:
scripts/compile_time_probe.sh --skip-build
scripts/compile_isolation_report.py
scripts/check_dependency_boundaries.py
cargo check --profile selfdev -p jcode --bin jcode
For code-moving phases, also run the relevant targeted tests for the moved domain, plus one full selfdev build through the coordinated selfdev path when practical:
selfdev build target=tui