nigig-org/PLAN_PERF_OPTIMIZATION.md
andodeki ac8f8aa002
Some checks failed
p2p-intel / engine (push) Waiting to run
p2p-intel / notifications (push) Waiting to run
p2p-intel / coverage (push) Waiting to run
p2p-intel / makepad-app (push) Waiting to run
p2p-intel / exchange-tab (push) Waiting to run
Payment domain, storage, platform and UI / isolated-payment-tests (push) Waiting to run
Payment domain, storage, platform and UI / payment-ui-tests (push) Waiting to run
repo hygiene / hygiene (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
traffic / gates (push) Has been cancelled
traffic / nigig-traffic (push) Has been cancelled
traffic / supply-chain (push) Has been cancelled
doc-engine / engine (push) Has been cancelled
doc-engine / coverage (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
email / gates (push) Has been cancelled
email / email-domain (push) Has been cancelled
email / nigig-email (push) Has been cancelled
email / supply-chain (push) Has been cancelled
nigig-map / test (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
chore: sync full working tree to gitdab
Whole-tree sync: cad-core/cad-ui split sources, nigig-build
construction_frame migration, pdf port progress, mpesa/pay/uikit/doc
updates, workspace members/profiles/lock, CI workflows and reviews.
See individual file history for details.
2026-09-12 07:15:24 +03:00

241 lines
No EOL
11 KiB
Markdown

# Plan: Performance optimization of the doc + spreadsheet engines
Measurement-driven, phased performance work on the two core engines
(`spreadsheet-engine`, `doc-engine`) and the UI paths that sit on top of
them, applying the techniques in the Algorithmica "High-Performance
Computing" book (https://en.algorithmica.org/hpc/): profiling first,
then CPU basics, branchless code, memory hierarchy and data layout,
hashing and search for small-N dispatch, SIMD, strings and CRDT hot
paths, I/O, and only then parallelism.
This plan follows the existing benchmark culture in this repo
(`BENCH_BASELINE.md`): benches are measurements, not assertions; they
run in `--release`; wall-clock numbers are compared by hand against a
baseline file, never asserted in CI.
Every phase has a `Source` (the Algorithmica chapter that motivates it),
`Targets` (concrete file-level hotspots), `Actions`, and a `Gate` (the
measured bar that must be hit before the next phase starts). Phases are
ordered so the cheap, safe wins land before the risky structural work,
and the CRDT invariants (deterministic op order) are protected by the
existing RNG property tests through every phase.
## Baseline to reproduce before starting
| Benchmark | Crate | Measures |
|---|---|---|
| `bench_formula_parse_eval_chain` | spreadsheet-engine | parse + evaluate a 1000-cell SUM/IF chain |
| `bench_grid_random_access_10k` | spreadsheet-engine | 10k random `get_cell_value` lookups |
| `bench_range_agg_sum_10k` | spreadsheet-engine | SUM over a 10k-cell dense range |
| `bench_crdt_materialize_2k` | doc-engine | `materialize()` on a 2k-char doc |
| `bench_crdt_insert_char_1k` | doc-engine | insert char into a 1k-char block |
| `bench_crdt_save_wire` | doc-ui | full `crdt_save_wire` + `save_doc_state` |
| `bench_projection_layout_50` | doc-ui | `layout_projection` for 50 blocks |
| `bench_import_500k` | both | 500KB .docx / .xlsx import |
| `bench_dashboard_list_saved` | both UI | `list_saved_docs` / `list_spreadsheet_files` |
Record these into `BENCH_BASELINE.md` before any code change. Treat the
debug profile as non-comparable (3-5x slower, shifts ratios).
## Phase 0 — Measurement infrastructure
- **Goal:** ground truth before touching anything; repeatable
before/after table per phase.
- **Source:** HPC "Analyzing performance".
- **Actions:**
- Add a `benches/` crate (or `#[bench]`-backed, `#[ignore]`-d test
harness like the CAD `profile_benchmarks.rs`) under each engine with
the benchmarks above. Criterion or divan, release-only.
- Add a headless `perf record --call-graph dwarf` / samply runbook for
the two engines (no makepad dependency) plus targeted profile
captures for the UI loops (render_cache, import).
- Extend `BENCH_BASELINE.md` with the resulting numbers.
- **Gate:** every benchmark above has a recorded number that CI can
reproduce on a clean checkout.
## Phase 1 — Compiler & codegen hygiene
- **Goal:** biggest free win, zero behavioral risk.
- **Source:** HPC "Compiler" (separate compilation, inlining, constant
folding) and "PGO".
- **Targets:** release profile in `Cargo.toml`, `formula2.rs` eval loop.
- **Actions:**
- Release: `lto = true`, `codegen-units = 1` (tune 4/8/16), a
`native` runtime feature with a portable fallback, `debug = false`
for deps. A/B on Phase 0 benches; confirm makepad/render crates
behave.
- Add a repeatable PGO (profile-guided optimization) script driving
formula eval + CRDT materialize + import; measure, keep the script.
- Replace the `&dyn EvalContext` dynamic dispatch in the hot
`evaluate` path (`formula2.rs:1125`) with an enum or generic
context; do the same for any `Box<dyn>` walkers in
`crdt/document.rs`.
- `#[inline(always)]` only the proven hot leafs (`get_cell_value`,
`CellId` hashing, op lookup); review hot `clone()` sites
(`materialize_block_text`, `doc_import::plain_text_paragraphs`).
- **Gate:** 5-20% on formula/projection benches; all tests green;
identical behavior.
## Phase 2 — Memory hierarchy & data layout
- **Goal:** cache locality, data-oriented design, smaller types. The
single biggest structural target in the spreadsheet path.
- **Source:** HPC "Memory" (cache, data-oriented design, small & fast
types).
- **Targets:** `spreadsheet-engine/src/data.rs` cell storage; dep
tracking; doc CRDT materialize.
- **Actions:**
- Grid storage: replace the `HashMap<CellId, CellData>` hot sheet path
with a dense packed grid for the populated bounding box (rows as
`Vec<Option<CellData>>`, SoA numbers/strings), plus a sparse overflow
map for far cells. Applies to range eval, spill values
(`data.rs:1665`), autofill, and rendering.
- Iterate rows-major (linear, prefetchable) instead of `HashMap` keys
for SUM/COUNTIF/render passes.
- Narrow types: `row_heights`/`col_widths` `HashMap<u32,f32>`
`Vec<f32>` indexed within bounds; consider packing `CellId` down if
still `u64` after the dense grid lands.
- Deps (`data.rs:1721-1722`): `HashMap<CellId, HashSet<CellId>>`
small `SmallVec` adjacency (most cells have <4 deps; reads beat
HashSet).
- Doc engine: `materialize()` clones text per block and builds
`Vec<String>` per char (`crdt/document.rs:252`) — compact to
`Vec<char>`/string chunks via an arena; avoid per-`OpId` `String`
actor ids in hot walks.
- **Gate:** perf cache-miss count on formula/import drops 30%+;
range-eval bench >=1.5x.
## Phase 3 — Branchless code & ILP
- **Goal:** straight-line happy paths, fewer mispredictions.
- **Source:** HPC "CPU" (pipelining, branchless selection).
- **Targets:** `formula2.rs` cycle detection, cell lookup, aggregate
loops, `autofill.rs`.
- **Actions:**
- Replace `RefCell<HashSet<(u32,u32)>>` cycle probing
(`data.rs:1275`) with a depth budget so the common no-cycle path is a
straight scan, no hash lookups.
- Cell lookup → index arithmetic (`base + row*stride + col`) instead
of hashing; indexed check instead of `map.get`.
- Manual unroll x4-8 in SUM/AVERAGE/MIN/MAX/SUMPRODUCT and autofill;
keep state in locals (no `RefCell`/dyn in inner loops); no `format!`
in eval-path error/budget branches.
- **Gate:** branch-miss % down on the profile; sum-of-10k bench ~1.3x+.
## Phase 4 — Hashing & search for small-N dispatch
- **Goal:** have a hash map not a hash drag.
- **Source:** HPC "Hash tables", "Binary search".
- **Targets:** remaining maps, dashboard file lists.
- **Actions:**
- Upgrade hashers on `HashMap<CellId,_>` (e.g. `foldhash`/
`rustc-hash`) — cheap immediate win.
- Tiny-N dispatches (few sheets, col/row widths, CRDT op indices):
`BTreeMap`/`HashMap``Vec` + `partition_point`.
- Cache dashboard listings (`list_saved_docs`,
`list_spreadsheet_files`) sorted once; rescan on mtime change, not
per frame.
- **Gate:** map-heavy benches >=1.2x; dashboard refresh no longer scans
disk per frame.
## Phase 5 — SIMD
- **Goal:** vectorize only where profiles and the dense grid say so.
- **Source:** HPC "SIMD".
- **Targets:** dense-grid aggregates, import string scanning, UTF-8
width counting.
- **Actions:**
- Enable autovectorization on dense-grid SUM/COLUMN/ROW/SUMPRODUCT/
COUNTIF/AVERAGE and formatting passes; Phase 1 `native` flags should
already be emitting SIMD.
- If profiling confirms the bottleneck: hand-vectorize chosen hot
scalar loops (`#[target_feature]` + runtime dispatch) for numeric
aggregates and for string scanning (whitespace/`§`/`|` split in
`doc_import.rs`, RTF/XML walkers, text width in `text_measure.rs` /
`projection_layout.rs`).
- **Gate:** >=2x on aggregate-range benches and import parser hot loops.
## Phase 6 — Strings & CRDT (correctness-sensitive)
- **Goal:** fewer, smaller allocations per edit; incremental layout.
- **Source:** HPC "String algorithms", "Data structures" (avoid
re-allocation).
- **Targets:** `doc-engine/src/crdt/document.rs`, doc-ui
`projection_layout.rs`, `projection_session.rs`, both persistence
paths.
- **Actions:**
- Per-char `String` atoms → `Vec<char>`/char-array atoms; CRDT walkers
(`visit_text`, `materialize_block_text`) borrow slices instead of
clone+push.
- Incremental projection: `layout_projection` rebuilds per keystroke
(`projection_layout.rs:418`) — dirty-range layer so typing in block 3
does not re-layout blocks 1-50. `glyph_index_of` (O(n))
→ Fenwick tree over block/line lengths (O(log n)).
- Save path: `crdt_save_wire` serializes the whole doc on every edit
(`workspace.rs:446/848`, `save_doc_state`) — debounce (200-500ms)
and/or op-log delta + occasional snapshot instead of full `to_json`
per keystroke. Same for spreadsheet `save()`.
- Import (`doc_import.rs`): reuse buffers per paragraph.
- **Gate:** typing latency flat at 10k-char doc; save no longer blocks
the UI thread; CRDT + RNG property tests pass unchanged.
## Phase 7 — I/O & persistence
- **Goal:** never block the UI thread on a full-document write.
- **Source:** HPC "Fast I/O" (buffered, batched, mmap).
- **Targets:** persistence modules, import paths, dashboard lists.
- **Actions:**
- Background-save queue (single worker) for doc & spreadsheet
autosave; UI thread never does sync `fs::write` of a whole doc.
- `.xlsx`/`.odt`/`.docx` import: mmap large files; single-pass
read/scan.
- Dashboard file listings: cache + mtime guard (ties into Phase 4).
- **Gate:** 1-5MB office-file import dropped 30%+; no UI hitches during
autosave.
## Phase 8 — Horizontal parallelism (last, and only if needed)
- **Goal:** multicore scaling after the single-core work is done.
- **Source:** HPC "Concurrency".
- **Targets:** engine-only, never inside makepad draw/handle.
- **Actions:**
- Parallel dirty-cell recalculation (chunk sheets per core; join at
boundaries) via `rayon` or a hand-rolled scoped pool.
- Parallel import parse (zip-entry decode) if benches justify.
- **Gate:** >=3x on large-workbook recalc with unchanged formula results
and identical ordering behavior.
## Phase 9 — Regression gate & rollout
- Port the top benches into CI (`cargo bench`); wire perf smoke tests
into the existing `tests.rs` / `tests_pure.rs` (CRDT RNG property
tests especially — invariant preservation is non-negotiable).
- Update `BENCH_BASELINE.md` and the `PHASE*_SUMMARY.md` docs with the
phase table; each phase 1-8 ships its before/after numbers.
- Mobile/config check (pageflipnav / Android): `target-cpu=native`
fallbacks, no unsupported intrinsics at runtime, keep CI on the
portable path.
## Ordering and gates
```
0 (measure) -> 1 (codegen) -> 2 (layout) -> 3 (branchless/ILP)
-> 4 (hash/search) -> 5 (SIMD) -> 6 (strings/CRDT) -> 7 (IO)
-> 8 (parallelism) -> 9 (regression gate)
```
Phase 6 is the riskiest for the CRDT invariants, so it lands after the
safe wins and carries its own property-test gate. Nothing in phases
1-8 may change observable model behavior; every phase is gated on the
existing test suites plus the baseline numbers.
## Risks
- **CRDT determinism:** changes to `materialize`/op iteration must keep
deterministic order; RNG tests are the gate.
- **`target-cpu=native`** breaks portable/mobile builds — feature-gated
with a fallback.
- **HashMap-to-dense-grid** resize cost on huge sparse imports — keep
the sparse overflow map; profile first.
- **Premature SIMD/parallelism** — phases 5/8 explicitly gated on
profiles showing the bottleneck.