perf(tui): carry measured widths through rendering

Carry exact visible widths from Text and Box render results into their owners, and through Editor wrap/layout/render state. Stamp derived widths and render caches with a monotonic Hangul width-config epoch. Retain sidecar entries only by WeakMap owner identity, with immutable publication-time line/width proof, and conservatively remeasure escape-leading padded rows.

Before:
  static_redraw 1633.11 us/op
  editor_edits  1249.71 us/op
After hardened repair:
  static_redraw 1321.51 us/op (1.236x)
  editor_edits  1150.32 us/op (1.086x)
Win:
  Exact byte/grid/scrollback hashes match across all five phases; every phase CV is below 20%, with no B/C regression.
Memory:
  retained heap 251,393,541 -> 251,500,114 bytes (+0.042%).

Op: carry exact widths through render ownership and editor layout
Restores: repeated visible-width measurement in core TUI hot paths
This commit is contained in:
metaphorics
2026-07-17 14:56:32 +09:00
parent 48241afcc4
commit 92380971da
7 changed files with 604 additions and 182 deletions
+48
View File
@@ -30,14 +30,62 @@ export function getHangulCompatibilityJamoWidth(): HangulCompatibilityJamoWidth
return hangulCompatibilityJamoWidth;
}
// Monotonic epoch for width-affecting runtime configuration. Any cache or
// carried-width sidecar derived from `visibleWidth` results must be stamped
// with the epoch at computation time and discarded on mismatch, so a Hangul
// Compatibility Jamo width change invalidates every derived width.
let widthConfigEpoch = 0;
export function getWidthConfigEpoch(): number {
return widthConfigEpoch;
}
interface LineWidthsEntry {
epoch: number;
lines: readonly string[];
widths: readonly number[];
}
// Per-render-result visible widths, keyed by the exact lines array a component
// returned. The copied strings and widths are the single publication snapshot:
// they cannot be changed through either publisher array and do not retain the
// WeakMap key. Entries therefore die with their lines-array owners.
const lineWidthSidecar = new WeakMap<readonly string[], LineWidthsEntry>();
/** Publish exact per-line visible widths for a rendered lines array. */
export function publishLineWidths(lines: readonly string[], widths: readonly number[]): void {
if (lines.length !== widths.length) {
throw new RangeError(`Cannot publish ${widths.length} widths for ${lines.length} lines`);
}
lineWidthSidecar.set(lines, {
epoch: widthConfigEpoch,
lines: [...lines],
widths: Object.freeze([...widths]),
});
}
/** Exact per-line visible widths for an unchanged `lines` array under the current width config. */
export function getPublishedLineWidths(lines: readonly string[]): readonly number[] | undefined {
const entry = lineWidthSidecar.get(lines);
if (entry === undefined || entry.epoch !== widthConfigEpoch || entry.lines.length !== lines.length) {
return undefined;
}
for (let i = 0; i < lines.length; i++) {
if (entry.lines[i] !== lines[i]) return undefined;
}
return entry.widths;
}
export function setHangulCompatibilityJamoWidth(width: HangulCompatibilityJamoWidth): boolean {
const changed = hangulCompatibilityJamoWidth !== width;
hangulCompatibilityJamoWidth = width;
if (changed) widthConfigEpoch++;
nativeSetHangulCompatJamoWidthOverride(nativeHangulCompatibilityJamoOverride(width));
return changed;
}
export function resetHangulCompatibilityJamoWidthForTests(): void {
if (hangulCompatibilityJamoWidth !== "platform") widthConfigEpoch++;
hangulCompatibilityJamoWidth = "platform";
nativeSetHangulCompatJamoWidthOverride(0);
}