Scope IME display attribute enumeration to the edited ranges - #11872
Scope IME display attribute enumeration to the edited ranges#11872akon47 wants to merge 2 commits into
Conversation
TextServicesDisplayAttributePropertyRanges.OnEndEdit enumerated the display
attribute property with a null target range, which enumerates the property
over the entire document. The property accumulates a range per previously
composed run, so both the enumeration and the COM round trip it costs per
range grow with the length of the document - on every keystroke.
This only shows up with IMEs that set display attributes (for example the
modern Korean TSF IME, which underlines the composition). Measured on a
1,200 character RichTextBox while typing with that IME, a single OnEndEdit
call enumerated 1,077 ranges of which exactly one carried an attribute, and
blocked the UI thread for about 3 seconds.
Use GetPropertyUpdate(editRecord) to obtain the ranges this edit actually
changed and enumerate the property within each of them, which is what the
base class TextServicesPropertyRanges.OnEndEdit already does; this override
had lost that scoping. GetPropertyUpdate is promoted from private to
protected so the override can reuse it.
Measured with an automated SendInput macro typing "안녕하세요 " 300 times
into a RichTextBox (Windows 11 26200, .NET 8, x64 Release):
before: mean 181.3 ms/iteration, max 3577.6 ms,
13 UI thread stalls totalling 15,898 ms
after: mean 100.9 ms/iteration, max 127.4 ms,
1 stall of 53 ms
which matches the legacy IME baseline (mean 101.5 ms, no stalls) on the same
harness. The composition underline and the Hanja candidate window continue to
render correctly.
Contributes to dotnet#7397
|
@dotnet-policy-service agree |
Address review-relevant edge case in the previous change: scoping the display attribute enumeration to only the ranges GetPropertyUpdate reports could drop an attribute that stayed unchanged in an edit but must still be drawn. This happens with input methods whose composition holds several attribute ranges at once - for example Japanese clause conversion, where moving the target clause updates only some clauses' attributes. Enumerate over the smallest ACP span covering (a) every active composition and (b) every range this edit changed. Display attributes can only live in those places, so this window finds every range the whole-document scan would find, for any IME, while keeping the cost proportional to the composition rather than to the document. Verified on Windows 11 (.NET 8, x64 Release) with an automated SendInput macro of 150 iterations per phase, alternating the stock and fixed builds (built from the same source) in the same process: Korean stock max 1729 ms, 6-15 UI stalls -> fixed max 152 ms, 0 stalls Japanese stock max 1498 ms, 59 stalls -> fixed max 491 ms, 12 stalls Chinese stock max 596 ms, 3 stalls -> fixed max 226 ms, 0 stalls English (no composition): stock and fixed identical, no stalls Equivalence was checked with an in-build oracle that, on every edit, also ran the original whole-document scan and compared the attribute range set with the scoped scan: 11,447 edits across Korean, Japanese and Chinese, zero mismatches, including Japanese multi-clause compositions (scan window averaging 7.5 characters). The oracle was removed from this commit. Also replaces the Korean comment left on GetPropertyUpdate with English. Contributes to dotnet#7397 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lwjr1n1MHufyQd4Kn9hNC2
|
Updated the commit to address an edge case with input methods whose composition holds several display attribute ranges at once. What changed and whyThe first version scoped the enumeration to the ranges The updated version enumerates over the smallest ACP span covering (a) every active composition ( VerificationAutomated
(stalls = iterations over 300 ms; Korean and English were run twice.) Equivalence was checked with a temporary in-build oracle that, on every edit, also ran the original whole-document enumeration and compared its attribute-range set against the scoped scan:
11,447 edits, zero mismatches — the scoped scan returned exactly the same set of display attribute ranges as the whole-document scan, including Japanese multi-clause compositions. The oracle was removed from the committed change. The composition underline and candidate window render correctly in all three IMEs. The analysis and the verification harness were done with the help of Claude Code; every number above is a measurement from actual runs on my machine, and I can share the harness if it helps. 🤖 Generated with Claude Code |
Fixes the typing lag reported in #7397.
Cause
TextServicesDisplayAttributePropertyRanges.OnEndEditenumerates the display attribute property with a null target range, which enumerates it over the whole document:The property accumulates a range per previously composed run, so both the enumeration and the COM round trip it costs per range grow with document length — on every keystroke.
Only IMEs that set display attributes hit this (for example the modern Korean TSF IME, which underlines the composition). Measured on a 1,200 character
RichTextBox, a singleOnEndEditenumerated 1,077 ranges of which exactly 1 carried an attribute, blocking the UI thread for ~3 seconds.Fix
Use
GetPropertyUpdate(editRecord)to obtain the ranges this edit actually changed and enumerate the property within each of them — which is what the base classTextServicesPropertyRanges.OnEndEditalready does. This override had lost that scoping.GetPropertyUpdateis promoted fromprivatetoprotectedso the override can reuse it.Measurement
SendInputmacro typing안녕하세요300 times into aRichTextBox(Windows 11 10.0.26200, .NET 8, x64 Release):The same harness with the IME in English mode showed no stalls before or after, confirming the cost is in the composition path.
Composition underline and the Hanja candidate window continue to render correctly, including mid-document, while scrolled, and immediately after fast typing.
Notes
PresentationFramework4.8.9340.0), consistent with the original report that 4.8 is affected.IMECompositionTraceTarget) plus stack sampling during the stalls; analysis was done with the help of Claude Code, and every number above is a measurement from an actual run on my machine.Microsoft Reviewers: Open in CodeFlow