Skip to content

perf(1346): round 2 -- the statusbar and clipboard costs the first budget could not see - #1394

Merged
accesswatch merged 1 commit into
mainfrom
perf/1346-round-2
Aug 13, 2026
Merged

perf(1346): round 2 -- the statusbar and clipboard costs the first budget could not see#1394
accesswatch merged 1 commit into
mainfrom
perf/1346-round-2

Conversation

@accesswatch

Copy link
Copy Markdown
Contributor

Re-examining #1346 found a second layer larger than what round 1 fixed. The round-1 budget test stubbed _refresh_title — and _refresh_title was smuggling a full statusbar refresh onto the synchronous typing path.

What each keystroke still did after round 1

  • The statusbar refreshed twice — once dragged in synchronously by the title refresh, once from the key-up handler — and its cells each re-marshalled the buffer: word count (full copy + full text pass), line/column (full copy + scan to caret), progress (full copy), page (full copy + scan). ~9 full-document copies and 2 full scans per character.
  • The abbreviation expander opened the Windows clipboard on every keystroke. The clipboard is a shared, single-owner cross-process lock; screen readers and clipboard managers poll it, and clipboard_retry deliberately retries up to 10 × 20 ms on contention — per character typed. Of everything in either round, this is the most direct candidate for the reported NVDA/JAWS lag.
  • SetTitle and the tab label were re-set to identical strings per keystroke, each firing an MSAA/UIA name-change event.

The fixes

  • try_expand gains clipboard_provider: the clipboard is read only once an abbreviation has matched and its expansion references ${clipboard} — approximately never. Lazy memoized fetch threaded through the user library, the Quillin-contributed library, and the fill-in fields path.
  • _refresh_title splits into the cached _refresh_title_bar (native calls skipped when unchanged) plus the statusbar half. The typing path uses the title-bar half; the statusbar catches up in the deferred pass. Caret activity coalesces its refresh (90 ms restarting timer) — holding an arrow key costs one refresh when the caret stops, not one per repeat.
  • Display-only reads (statusbar cells, table-transition check) use the new _document_text_for_display()document.text is the same string GetValue would marshal, already in memory, zero copies. The bug: quill crashes when running from source, post startup wizard, and when pressing ctrl+f4. #269 dead-widget contract is kept via an insertion-point liveness probe.
  • Document.set_text compares length before content.

Measured (same modelling basis as round 1)

Document After round 1 After round 2
20 KB 2.1 ms/keystroke 0.014 ms 156x
200 KB 22.4 ms/keystroke 0.26 ms 86x
1 MB 117.9 ms/keystroke 1.28 ms 92x

— before counting the removed clipboard contention, which no model captures.

Gates

  • Budget test: 10 → 16 assertions (clipboard laziness at three levels, title/statusbar split, caret coalescing, zero-copy display reads, title caching).
  • Full tests/unit + tests/stability: 13089 passed, 31 skipped, 0 failed.
  • GATE-11: four rebaselines with dated rationale (_rebaseline_2026_08_13_1346_round2). ruff + scoped mypy clean. Docs (CHANGELOG, PRD §11, user guide) updated, artifacts re-rendered, parity gate passes.
  • Navigation-test fixtures now keep the stub document in sync with the stub editor, as the real app does on every EVT_TEXT — the old fixtures modelled a desync that cannot occur.

🤖 Generated with Claude Code

… not see

Re-examining #1346 found a second layer larger than what round 1 fixed. The
round-1 budget test stubbed _refresh_title, and _refresh_title was smuggling a
full statusbar refresh onto the synchronous typing path.

What each keystroke still did after round 1:

* The statusbar refreshed TWICE (once dragged in synchronously by the title
  refresh, once from the key-up handler), and its cells each re-marshalled the
  buffer to compute themselves: word count (full copy + full text pass),
  line/column (full copy + scan to caret), progress (full copy), page (full
  copy + scan). ~9 full-document copies and 2 full scans per character.
* The abbreviation expander opened the WINDOWS CLIPBOARD on every keystroke.
  The clipboard is a shared, single-owner cross-process lock; screen readers
  and clipboard managers poll it, and clipboard_retry deliberately retries up
  to 10 x 20 ms on contention -- per character typed. Likely the most direct
  contributor to the reported NVDA/JAWS lag of anything in either round.
* SetTitle and the tab label were re-set to identical strings per keystroke,
  each firing an MSAA/UIA name-change event.

The fixes:

* try_expand gains clipboard_provider: the clipboard is read only once an
  abbreviation has matched AND its expansion references ${clipboard} --
  approximately never. The UI passes a lazy memoized fetch through the user
  library, the Quillin-contributed library, and the fill-in fields path.
* _refresh_title splits into the cached _refresh_title_bar (native calls
  skipped when the strings are unchanged) plus the statusbar half. The typing
  path calls the title-bar half only; the statusbar catches up in the deferred
  pass. Caret activity coalesces its refresh (_schedule_statusbar_refresh,
  90 ms restarting timer) -- holding an arrow key costs one refresh when the
  caret stops instead of one per key repeat.
* Display-only reads (statusbar cells, the table-transition check) use the new
  _document_text_for_display(): document.text is the same string GetValue
  would marshal, already in memory, at zero copies. The #269 dead-widget
  contract is kept via an insertion-point liveness probe.
* Document.set_text compares length before content, making the no-op check
  O(1) for insertions and deletions.

Modelled on the same basis as round 1 (UTF-16 round trip per marshal), the
synchronous per-keystroke cost drops a further ~85-155x: 200 KB document
~22 ms -> ~0.3 ms per keystroke; 1 MB ~118 ms -> ~1.3 ms -- before counting
the removed clipboard contention, which no model captures.

The budget test grows from 10 to 16 assertions: typing a plain letter never
touches the clipboard, a non-matching trigger never touches it, a matching
expansion without ${clipboard} never touches it, the typing path may not call
_refresh_title (title-bar half only, no statusbar inside it), caret activity
must coalesce, statusbar stats must come from document.text with zero
GetValue calls, and an unchanged title must not be re-set.

Test fixtures in test_main_frame_navigation now keep the stub document in
sync with the stub editor, as the real app does on every EVT_TEXT -- the old
fixtures modelled a desync that cannot occur.

Suite: 13089 passed, 31 skipped (full tests/unit + tests/stability).
GATE-11: four rebaselines with a dated rationale (_rebaseline_2026_08_13_
1346_round2). Docs: CHANGELOG (the #1346 entry gains the round-2 half), PRD
section 11 (budget extended to clipboard/statusbar/title rules), user guide;
artifacts re-rendered, parity gate passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@accesswatch
accesswatch merged commit 8f4396e into main Aug 13, 2026
14 checks passed
@accesswatch
accesswatch deleted the perf/1346-round-2 branch August 13, 2026 17:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant