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

11 KiB

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/HashMapVec + 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.