Skip to content

Scope IME display attribute enumeration to the edited ranges - #11872

Open
akon47 wants to merge 2 commits into
dotnet:mainfrom
akon47:fix/ime-display-attribute-enum-scope
Open

Scope IME display attribute enumeration to the edited ranges#11872
akon47 wants to merge 2 commits into
dotnet:mainfrom
akon47:fix/ime-display-attribute-enum-scope

Conversation

@akon47

@akon47 akon47 commented Aug 27, 2026

Copy link
Copy Markdown

Fixes the typing lag reported in #7397.

Cause

TextServicesDisplayAttributePropertyRanges.OnEndEdit enumerates the display attribute property with a null target range, which enumerates it over the whole document:

displayAttributeProperty.EnumRanges(ecReadOnly, out attributeRangeEnumerator, null);

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 single OnEndEdit enumerated 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 class TextServicesPropertyRanges.OnEndEdit already does. This override had lost that scoping.

GetPropertyUpdate is promoted from private to protected so the override can reuse it.

Measurement

SendInput macro typing 안녕하세요 300 times into a RichTextBox (Windows 11 10.0.26200, .NET 8, x64 Release):

mean / iteration max UI thread stalls
before 181.3 ms 3577.6 ms 13, totalling 15,898 ms
after 100.9 ms 127.4 ms 1, of 53 ms
legacy IME (reference) 101.5 ms 173.1 ms none

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

  • The same code is present on .NET Framework (PresentationFramework 4.8.9340.0), consistent with the original report that 4.8 is affected.
  • Root cause was located with WPF's own IME tracer (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

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
@akon47
akon47 requested a review from a team August 27, 2026 04:23
@akon47
akon47 requested a review from a team as a code owner August 27, 2026 04:23
@dotnet-policy-service dotnet-policy-service Bot added PR metadata: Label to tag PRs, to facilitate with triage Community Contribution A label for all community Contributions labels Aug 27, 2026
@akon47

akon47 commented Aug 27, 2026

Copy link
Copy Markdown
Author

@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
@akon47

akon47 commented Sep 4, 2026

Copy link
Copy Markdown
Author

Updated the commit to address an edge case with input methods whose composition holds several display attribute ranges at once.

What changed and why

The first version scoped the enumeration to the ranges GetPropertyUpdate reports (the ranges an edit changed). That is enough for the Korean IME, whose composition is a single run, but it could drop an attribute for an IME where the composition has several attribute ranges and an edit updates only some of them — for example Japanese clause conversion, where moving the target clause changes only some clauses' display attributes while the rest must still be drawn.

The updated version enumerates over the smallest ACP span covering (a) every active composition (ITfContextComposition.EnumCompositionsITfCompositionView.GetRange) and (b) every range this edit changed (GetPropertyUpdate). Display attributes can only live inside a composition or on a range the edit just touched, so this window finds every range the whole-document scan would find, for any IME, while keeping the cost proportional to the composition instead of the document.

Verification

Automated SendInput macro, 150 iterations per phase, alternating the stock and fixed builds (both built from the same source) in the same process. Windows 11 10.0.26200, .NET 8, x64 Release.

IME stock max stock stalls fixed max fixed stalls
Korean 1729 ms 6, 15 152 ms 0, 0
Japanese 1498 ms 59 491 ms 12
Chinese 596 ms 3 226 ms 0
English (no composition) 0 0

(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:

IME edits mismatches scan window (avg)
Korean 2109 ×2 0 1.3 chars
Japanese 3459 0 7.5 chars (multi-clause)
Chinese 1811 0 7.3 chars
English 1959 0

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Community Contribution A label for all community Contributions PR metadata: Label to tag PRs, to facilitate with triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant