LT-22674: Cache text analysis only for text NFC leaves unchanged - #1166
Conversation
Line layout now stores an itemized range in the analysis cache only when NFC normalization leaves its text unchanged. Text that NFC rewrites translates offsets on demand instead, so the offset-map builder and the plumbing that carried its maps are deleted. Also registers the view cache tests for collection, adds tests for the analysis cache and its store rule, and adds three render scenarios built from decomposed text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1166 +/- ##
==========================================
- Coverage 39.02% 39.02% -0.01%
==========================================
Files 1522 1523 +1
Lines 352937 353018 +81
Branches 40726 40744 +18
==========================================
+ Hits 137741 137762 +21
- Misses 185893 185938 +45
- Partials 29303 29318 +15
🚀 New features to boost your workflow:
|
Line layout now translates offsets between a paragraph and its NFC form through a table built once per layout pass. The table splits the text where ICU says normalization cannot cross a boundary and records the NFC length before each split, so any offset costs one short normalization instead of renormalizing the prefix. The analysis cache stores text that NFC rewrites again, and serves a shorter request from a longer entry only when the request ends on a boundary. Also adds equivalence tests for the map against normalizing from scratch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
e7d88fc to
b42e64a
Compare
|
My reading of why commit 2 works when the ICU-boundary split was rejected on #1056. @jasonleenaylor, please approve or correct it. LT-22674 comment 3 records that splitting at ICU normalization boundaries was tried for #1056 and rejected. The decomposed proxy still took 3,468 / 2,174 ms against a 298 / 269 ms control, because the linear build still ran once for every analysed range. Commit 2 uses the same split, but I've recorded this on LT-22674 as unconfirmed. Correct it here or there. |
TryOffsetToOrig now walks the whole chunk and records the last offset whose prefix still fits, instead of stopping at the first that does not. The oracle corpus gains a string whose marks reorder and compose, so the NFC prefix length shrinks inside the chunk. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
That reading is right. The split at ICU boundaries is the same one #1056 tried; what changed is that |
johnml1135
left a comment
There was a problem hiding this comment.
Approving. The one open review point is fixed, and CI is green.
Opening a text whose baseline is one long decomposed paragraph took minutes
in 9.3.9 and later. On the LT-22796 project it now takes 15 s on a Debug
build, against 18 s with the analysis cache disabled and 20 s on 9.3.7-era
code.
Start here:
Src/views/lib/NfcOffsetMap.h, thenCallScriptItemizeinSrc/views/lib/UniscribeSegment.cpp. ThenTestNfcOffsetMapinSrc/views/Test/TestViewCaches.h.What it does
Opening or editing a long decomposed paragraph renormalized the text once per
analysed range.
BuildNfcOffsetMapsinSrc/views/lib/UniscribeSegment.cppbuilt an offset map for the #724 analysis cache by NFC-normalizing every prefix
of the range, and
UniscribeEngine::FindBreakPointhanded it the rest of theparagraph on every line. That function is deleted.
NfcOffsetMap(Src/views/lib/NfcOffsetMap.h) replaces it with one table perparagraph layout pass.
LayoutPassCacheholds the map andParaBuilderresetsthe cache when it starts laying out a paragraph, so the text is fetched and
normalized once per pass. The map splits the text where ICU's
hasBoundaryBeforesays normalization cannot cross, records the NFC lengthbefore each boundary, and answers a request inside a chunk by normalizing only
that chunk's tail.
TryOffsetToOrigwalks a chunk to the last offset whoseNFC form still fits, because a mark that reorders ahead of another can compose
with the base and shorten the form. A request whose base is not a boundary is
declined, and
UniscribeSegmentfalls back to its on-demand translation.TextAnalysisCachestores decomposed text again, with the NFC length and aflag saying whether NFC changed it.
CallScriptItemizeserves a shorterrequest from a longer decomposed entry only when the request ends on a
boundary, and otherwise recomputes.
TestViewCaches.his now collected, so its dormant suites run, and gainsTestNfcOffsetMap, which checks every (base, offset) pair in both directionsagainst from-scratch normalization over a 12-string corpus. Three render
scenarios built from decomposed text join
RenderBenchmarkTestsBasewiththeir own baselines.
Where to look
TestNfcOffsetMapchecks every (base, offset) pair inboth directions against from-scratch normalization over a 12-string
corpus (mark reordering, marks that reorder and compose, marks without
base, Arabic, Hangul jamo, surrogates, lengthening and singleton
decompositions).
it ends on a boundary;
CallScriptItemizechecks that and otherwiserecomputes.
TestViewCaches.hwas never collected; its 9 tests run now, plus 7 new.asserted decomposed). New baselines; the 15 existing ones are
pixel-identical.
What is deliberately not here
Analysing a paragraph once and slicing per line: itemization was 7 of 90
layout samples against 27 for offset translation, and slicing needs a proof
about Uniscribe suffix itemization. Stays with LT-22656.
Verification
Build with
-CommentHygiene -TokenHygieneclean. Native Views tests 318/318after the last-fit walk; the new corpus string fails without it
(
OffsetToOrig(5, 0)expected 9, got 7).RootSiteTests render verify + timing 36/36, also green with each cache flag
off. Debug suite average cold render 225 ms default, 229 ms analysis cache
off, 289 ms shape cache off; the decomposed wrap scenario is 222 ms cached vs
323 ms not. Layout-time samples in offset translation: 27 of 90 with
BuildNfcOffsetMapsdeleted and on-demand translation, 5 of 74 with thetable. Full managed suite not run.
Preflight review details
Code Review Summary
Branch: LT-22674-nfc-analysis-cache
Base: main
Date: 2026-09-28
Review model: Claude Fable 5.1
Files changed: 13
Overview
Purpose (author): stop the quadratic NFC offset-map build that #724 added to
the Uniscribe analysis cache, which made a 30,000-character decomposed
paragraph take minutes to lay out (LT-22674, LT-22796), without giving up
what #724 bought. Two commits, reviewable separately:
the map machinery. Restores pre-perf: Views engine render optimizations — warm 99.99% faster, cold 10% faster #724 behavior for decomposed text.
NfcOffsetMap: one table per paragraph layout pass, split at ICUnormalization boundaries, giving every offset translation in one short
normalization. Decomposed text is admitted to the analysis cache again,
with shorter requests served only when they end on a boundary.
No Critical or Important findings. Rendering is pixel-identical to main on
all 15 pre-existing baselines after both commits.
Contract/API Changes
TextAnalysisEntry/TextAnalysisCache::Store(Src/views/lib/LayoutCache.h,internal to Views.dll): commit 1 removed the offset-map fields and
per-entry offset methods and renamed
m_vchNfctom_vchText; commit 2keeps
m_fTextIsNfcand passes the NFC length and flag toStore.LayoutPassCachegainsNfcOffsets(IVwTextSource*).UniscribeSegment: overloads takingTextAnalysisEntry*removed;CallScriptItemizelostppAnalysis; new staticCurrentNfcOffsetMap.Findings
Critical - Must address before merge
None.
Important - Should address before merge
None.
Minor - Consider
Src/views/lib/UniscribeSegment.cpp: the on-demandOffsetToOrigstarts its search at
ichand so can overshoot for characters whose NFCis longer than the source (for example U+0344). The map's
TryOffsetToOrigreturns the definition's answer instead. Theequivalence tests use from-scratch normalization as the oracle, so this
is a correction, not a divergence; noted so nobody expects the fallback
and the map to agree on such text.
Src/views/Views.mak: no header dependency tracking. Adding a memberto
LayoutPassCacheleftVwTextBoxes.objstale and produced Vectorinvariant asserts in
VwParagraph.NormalizeNfduntil the object wasdeleted. Pre-existing build defect (LT-22674 comment 2, item 3); flagged
for anyone rebuilding this branch incrementally.
Positive Observations
recorded length before the nearest boundary plus the NFC length of the
tail, and requests whose base is not a boundary decline so the caller
falls back.
order, marks without base, Arabic harakat, Hangul jamo, surrogate pairs,
lengthening mark, singleton decomposition) checks every (base, offset)
pair both directions against from-scratch normalization.
TestViewCaches.hsuites now collected; analysis-cache andstore-rule tests added; three decomposed render scenarios built at
runtime via ICU NFD with a decomposition assertion.
Required Validation / Evidence
./build.ps1 -CommentHygiene -TokenHygiene-- clean (both commits)../test.ps1 -TestProject TestViews -SkipManaged -CommentHygiene -TokenHygiene-- 318 passed / 0 failed / 0 errors after commit 2 (315 after commit 1).
18/18 with
FW_PERF_P125_PATH2=0and withFW_PERF_P125_PATH1=0.289 ms analysis cache off, 341 ms shape cache off; commit 2 -- 225 ms
default, 229 ms analysis cache off, 289 ms shape cache off. Decomposed
wrap scenario: 222 ms with the cache vs 323 ms without.
build, time to responsive Baseline tab: before fix >10 min (killed);
commit 1 ~20 s; commit 2 15.4 s / 16.0 s default vs 18.1 s / 18.2 s with
the analysis cache off. Native stack samples of the layout portion:
offset translation 27 of 90 samples after commit 1, 5 of 74 after
commit 2.
Interview Notes
compose-on-output stance. No: NFC is a render-private copy for Uniscribe
shaping that predates perf: Views engine render optimizations — warm 99.99% faster, cold 10% faster #724; offsets stay NFD; nothing is written back.
Commit 2 removes the one gap commit 1 left (decomposed text without the
cache benefit).
each line as a slice) was considered and left out: profiling after
commit 1 showed itemization at 7 of 90 layout samples against 27 for
offset translation, and slicing needs a proof that Uniscribe's
itemization of a suffix equals the slice of the whole. LT-22656 scope.
In-Review Quality Check
No in-review changes.
Suggested Review Focus
NfcOffsetMap::Buildboundary logic andTryOffsetToOrigsearch.CallScriptItemizehit rule for decomposed entries (boundary-end).TestNfcOffsetMap::VerifyAgainstOraclecorpus coverage.🤖 Generated with Claude Code
This change is