ekp/postmortem/20260730-incremental-live-append-ownership.md
Kinneyzhang 665164911f perf: make live structural commits incremental
Reuse prepared paragraph data, safe Elisp DP states, common layout lines, and dirty-island source reconstruction for exact append commits. Simplify C int32 validation and add the frozen task030 evaluator, regressions, and decision records.
2026-07-30 08:24:54 +08:00

96 lines
4.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Incremental Live-Append Ownership
## Context
After stable live transactions removed per-key whole-paragraph planning,
only native visual-row crossings still paid the complete paragraph
preparation, dynamic program, plan construction, and projection cost.
At 80 pixels the old source benchmark attributed 51187 ms p99 to these
permitted commits.
The C module itself was not the primary owner. In the frozen baseline its
80-pixel module call measured only 2.615/2.655 ms p95/p99, while paragraph
preparation and the surrounding plan dominated. A Rust module would still
enter through `emacs_env`, consume the same prepared vectors, and return the
same DP result.
## Decision
Keep the C module and make the existing ownership model incremental:
- a layout plan retains its prepared paragraph and exact layout context;
- a property-free plain-text append retokenizes from the last complete-word
boundary instead of from paragraph start;
- derived prefix, break, protrusion, and gap data are copied only through the
proven-stable box boundary and recomputed from that boundary onward;
- pure Elisp DP retains its state and resumes from the earliest old state
that can still reach the first new break;
- C runs its full native DP over incrementally prepared vectors because its
sub-millisecond candidate call is already cheaper than adding another ABI;
- the buffer replaces only the dirty source island and reuses common layout
lines before publishing the changed suffix.
Any unsupported property, font, layout-context, tab/newline, parshape,
looseness, or token-boundary case returns nil and takes the unchanged full
planner. The fast path never approximates cache identity or layout output.
## Rejected alternatives
- **Rewrite the module in Rust:** it does not remove the Emacs C ABI or the
Elisp-owned work, while adding Cargo, Rust toolchain, target, packaging,
and Windows support obligations.
- **Move the whole planner behind the module boundary:** font measurement,
text properties, and editor transaction ownership belong to Emacs; moving
them would create a larger, less stable ABI.
- **Reuse stale prefix decisions:** a suffix can change the globally optimal
KP path, so only prepared data and reachable DP states may be reused.
- **Debounce, skip, or delay commits:** this hides latency by making display
state stale and violates the synchronous live-edit contract.
## Correctness trap
The first boundary box is dirty even when every earlier box is unchanged.
Its break permission depends on both neighboring boxes, so copying through
that position preserved a stale forbidden break. The final implementation
copies strictly before the stable boundary and recomputes break data from
the boundary. A regression fixes this ownership rule.
## Results
The four-round frozen source/instrumentation matrix preserves exact
baseline/C/Elisp source and projection hashes, zero conflicts, valid
GC-excluded samples, zero-work ordinary keys, and non-regression at
64/80/96/128/160 pixels. At 80 pixels:
- C improves 77.78% p95 and 78.37% p99 to 25.490/25.785 ms;
- Elisp improves 92.54% p95 and 92.03% p99 to 43.860/47.578 ms;
- the C module itself falls to 0.697/0.701 ms p95/p99.
Those source-instrumented values still miss the locked 16 ms absolute gate.
The production-shaped byte-compiled public path is materially different:
three repeated runs measure append p99 at 1.1581.326 ms for C and
1.4291.438 ms for Elisp, with no GC during the samples. Both measurements
remain documented; the stricter goal is not silently weakened.
## Known design debt
At a 558-pixel GUI width, deleting a suffix after a forward row crossing can
republish a backward layout and violate the existing “preserve typed
projection” visual invariant. The frozen baseline reproduces the same
behavior, and current ERT explicitly treats a backward wrap crossing as a
commit. This is not caused by incremental append work and needs a separate
semantic decision before a narrow tail-shrink predicate is changed.
## Verification and rollback
Exact append-chain equivalence is covered across C and Elisp, varied widths,
unsafe fallback contexts, and the stable-boundary regression. Normal and
permuted 199-test suites, 300 fuzz cases, warning-as-error compilation,
portable C warnings/tests, release checks, and reviewed temporal GUI
evidence pass.
Rollback is a direct revert of retained plan/paragraph state, incremental
append preparation/DP, dirty-island reconstruction, and one-pass C integer
validation. No data migration, compatibility shim, or cache conversion is
required.