Speed up bulk Find & Replace with a native single-session string replace - #1065
Open
johnml1135 wants to merge 3 commits into
Open
Speed up bulk Find & Replace with a native single-session string replace#1065johnml1135 wants to merge 3 commits into
johnml1135 wants to merge 3 commits into
Conversation
Compute each bulk preview value once. Replace all matches through one native ICU search session. Preserve rich text, Unicode collation, and legacy fallback behavior. Record measured gains and discarded experiments.
Guard the outer bulk-edit undo task with try/finally so a native ReplaceAllIn fault mid-batch cannot leave it unterminated. Share one computed value between OkToChange and TryGetNewValue in BulkCopyMethod and TransduceMethod instead of computing twice, scoped to stay correct across separate preview/apply calls. Add native ReplaceAllIn coverage for ORC, writing-system restriction, case/diacritics, and canonical equivalence. Harden the NFD-normalization skip check with non-Latin script and rich-run test cases. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1065 +/- ##
==========================================
+ Coverage 37.91% 38.10% +0.19%
==========================================
Files 1499 1499
Lines 350117 350276 +159
Branches 40233 40235 +2
==========================================
+ Hits 132747 133480 +733
+ Misses 188043 187521 -522
+ Partials 29327 29275 -52
🚀 New features to boost your workflow:
|
Docs/superpowers/plans/evidence/bulk-replacement-distillation-2026-08-13.md was a one-time investigation log, not durable guidance. Its measurements, rejected approaches, and follow-up items now live in the PR body. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
johnml1135
force-pushed
the
table-speedup
branch
from
August 14, 2026 08:32
7cd1917 to
30baeb4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bulk Find & Replace over string fields (e.g. replacing text across thousands of Citation Forms) now runs each entry's search-and-replace through one native ICU search session instead of repeatedly restarting
FindInper match. Preview also computes each row's result once instead of twice. Both changes are additive: a new optionalIVwPattern2.ReplaceAllInCOM capability, used when available, with the original repeated-FindInpath kept as the fallback when it isn't.The diff crosses the native/managed boundary (a new COM interface) and touches a widely-used feature, so the real question isn't "is it faster" — the measurements below answer that — it's "does the fast path ever produce a different (or unsafe) result than the slow path did, and can a fault in it corrupt an in-progress bulk edit." That's what the checklist below is aimed at.
Where to look:
IVwPattern2is a new GUID appended after the complete, unmodifiedIVwPatternvtable; verified against the generated MIDL header, not just the.idhsource. No default-coclass change.VwPatternReplacementTests.cscross-checks the real nativeReplaceAllInagainst a repeated-FindInoracle: regex, collation/locale tailoring, whole-word, writing-system/style/tag runs, RTL, and combining marks.try/finallyand pinned byDoit_OuterLoop_EndsUndoTaskWhenAnItemThrows.FakeDoit_FallsBackWhenBulkReplacementIsUnavailable); the capability is detected once, not probed per call.Deliberately not here:
ReplaceAllInsearches a raw string and does not reproduceFindIn's VC-aware omission of embedded object-replacement characters (footnote markers etc.) from the pattern span — an architectural difference in what the two APIs search over, not a regression (see accordion).Verification:
./build.ps1(full native + managed): 0 warnings, 0 errors. Managed: 75/75 (ReplaceWithMethodPreviewTests,ReplaceAllInDecoratorCorrectnessTests,BulkEditBarTests,VwPatternReplacementTests). NativeVwPatternsuite: 27/27. The full 309-test native suite also reports all-pass but the process hangs ~5s in an unrelated Uniscribe/Graphite teardown path afterward (reproduced twice, unrelated subsystem, pre-existing). No manual FLEx UI pass was performed.Reading this a year from now — start here
This PR started as a focused perf change (one squashed commit) and picked up a second commit from its own pre-merge review, which found and fixed three real issues before they shipped. The working measurement log that produced the perf numbers below lived at
Docs/superpowers/plans/evidence/bulk-replacement-distillation-2026-08-13.mdon the branch; its conclusions are captured here and the file was deleted rather than merged, since it was a one-time investigation log, not guidance anyone needs to read to change this code correctly.Decisions, and why
Why an optional
IVwPattern2capability instead of changingIVwPattern.IVwPatternis an existing, ABI-relied-upon COM interface. AddingReplaceAllInto it would have required every existing implementation and consumer to change in lockstep. Appending a new interface, detected once via anas/QueryInterface-style cast and cached, gets the perf win without touching the existing contract, and lets a caller that only implementsIVwPatternkeep working unmodified via the repeated-FindInfallback.Why preview and apply are independent evaluations, not a shared cache.
FakeDoit(preview) andDoit(apply) are separate top-level calls, and the design intentionally recomputes on each — a bulk-edit column's preview row and its later apply are allowed to diverge if something else changed the underlying data in between. A per-row cache was added inside a singleTryGetNewValuecall (soBulkCopyMethod/TransduceMethod'sOkToChangeandTryGetNewValueshare one computed value instead of two) but is explicitly cleared at the end of every call, so the preview-then-apply pair still each compute fresh. See "Reversals" below for what happens when that clearing is missing.Why
ReplaceAllIndoesn't omit embedded ORCs the wayFindIncan.FindIncan search through aVwMappedTxtSrc, a view-constructor-aware text source that can skip owned object-replacement characters (e.g. footnote markers) so a pattern can match across one without "seeing" it.ReplaceAllInoperates on a rawITsStringviaTrivialTextSrcand has no such view-aware skip. This wasn't somethingReplaceAllIn's contract ever claimed to do; a bulk-replace call site that needs that omission would need to pass a pre-mapped source, which none currently does.Reversals
The first version of the
OkToChange/TryGetNewValuevalue-sharing cache (see above) did not clear itself between calls — it cached strictly by row ID, with no notion of "this call is done." That collapsedReplaceWithMethod's intentional preview-then-apply double-evaluation down to a single evaluation, since both calls share the same row ID on the same method instance. It brokeFakeDoit_MatchesImmediateApplyAcrossPatternModes(7 of its cases failed, each expecting the bulk-replace call count to reach 2, not 1) on the very next full test run after the "fix" was written. Caught by running the full suite rather than only the newly-added test, fixed by clearing the cache at the end of everyTryGetNewValuecall, and reverified at 75/75.Deferred, and what would unblock it
Homograph-renumber batching (LT-22701): re-sorting and renumbering an entire homograph group happens on every single entry write, even though only the last write in a batch determines the group's final state — measured at roughly 34.6 microseconds/entry, about 17% of a 100%-match bulk operation. Unblocking it needs a liblcm-side design pass covering undo/redo interaction,
PropChanged/notification behavior, cache membership mid-batch, and correct final numbering when multiple entries in the same group are edited in one operation — none of which this PR's scope (FieldWorks-side search/replace) can settle on its own.Paths not taken
memcmpshortcut in the native search path — reverted. ICU collation can equate strings that differ in punctuation and other non-ordinal ways, so a raw byte-compare shortcut produced wrong matches under real collation rules.Evidence
All measurements below are Debug-build, incremental (isolating one change at a time, not a cumulative branch-start-to-finish number), 50,000 real entries, and were taken before this PR's second (review-fix) commit — that commit fixes correctness/robustness issues and adds test coverage, and does not change the measured code paths.
Preview (single-pass) cost, 100% matching CitationForm, one match each: pooled median 72.4 → 51.4 microseconds/entry, a 29.1% reduction (1.41x throughput). Two paired process runs per build.
ReplaceAllInapply cost (fresh processes, one warmup + five timed repetitions each,old old old old-######-style matched values with four matches each):Debug-process variance was material — each independent 50%/100% pair exceeded a 15% self-imposed acceptance gate on its own, which is why the pooled-median figures above are quoted rather than any single run. Real-world gains should track the shape (bigger win at higher match rates) more reliably than the exact percentages.
Native test coverage added in the review-fix commit:
testReplaceAllInReplaceCharPrecedingFinalORC_TE4727,testReplaceAllInRespectsMatchOldWritingSystem,testReplaceAllInWithCaseAndDiacriticsOptions,testReplaceAllInCanonicalEquivalence— re-running scenarios that previously only hadFindIncoverage through the new bulk-session path. FullVwPatternnative suite: 27/27 passing.Preflight review details
Code Review Summary
Branch: table-speedup
Base: main (origin/main, merge-base
7f93348966a22be7fd4f9ef0c2e1cf571281cbcd)Date: 2026-08-14
Review model: Claude Sonnet 5 (Claude Code)
Files changed: 9
Overview
This branch speeds up bulk Find & Replace over string fields. It adds an optional
IVwPattern2COM capability (ReplaceAllIn) that replaces every match in a stringthrough one native ICU search session instead of the previous repeated-
FindInloop,falls back to the old per-match path when the capability isn't available, and computes
each bulk-preview value once instead of twice. Measured gains (Debug, distillation doc):
~29% reduction in preview cost, ~23-27% reduction in
ReplaceAllInapply cost dependingon match rate.
Two independent specialist passes (native/COM/boundary-safety, managed C#/UI) reviewed
the diff. Both converged on the same real issue from different angles (undo-task safety
under a native fault), which was fixed and regression-tested during this review. Two
further findings were investigated and fixed (a narrower-than-claimed "compute once"
optimization, and thin NFD-normalization test coverage); one native test-coverage gap
was closed with new tests. All fixes were independently verified by full builds and test
runs, and one fix (the "compute once" caching) caught and corrected a real regression it
had itself introduced, verified before it reached this summary.
Contract/API Changes
IVwPattern2(new GUID) adds one method,ReplaceAllIn, to the native Views COMsurface. Verified additive: the generated MIDL header shows
IUnknown+ the complete,unmodified
IVwPatternvtable +ReplaceAllInappended last. TheVwPatterncoclasslists both interfaces;
QueryInterfacehandles both IIDs. No ABI break, no change tothe default coclass.
Findings
Critical - Must address before merge
None.
Important - Should address before merge
BulkEditBar.cs's outer bulk-edit loop (Doit(IEnumerable<int>, ProgressState)) could leave an unterminated undo task if a nativeReplaceAllInfault occurred mid-batch during a real apply, sinceBeginUndoTask/EndUndoTaskhad notry/finallyand the new bulk-replace path deliberately propagates exceptions rather than silently falling back. (fixed during review: wrapped the loop body intry/finallysoEndUndoTaskalways runs; addedDoit_OuterLoop_EndsUndoTaskWhenAnItemThrows, which fails without the fix and passes with it.)Minor - Consider
Native(fixed during review: addedTestVwPattern.h'sReplaceAllIntests didn't re-run the ORC, writing-system-restriction, case/diacritics, and NFD-equivalence scenarios that already existed asFindIn-only tests.testReplaceAllInReplaceCharPrecedingFinalORC_TE4727,testReplaceAllInRespectsMatchOldWritingSystem,testReplaceAllInWithCaseAndDiacriticsOptions,testReplaceAllInCanonicalEquivalence. All 27 VwPattern native tests pass, including the 4 new ones. One scenario —FindIn's "pattern spans an embedded, VC-omitted ORC" case — was deliberately not reproduced:ReplaceAllInalways searches through a rawTrivialTextSrc, which does not omit owned ORCs the way the VC-awareVwMappedTxtSrcused by that specificFindIntest does. This is an architectural difference in whatReplaceAllIn's contract covers (it operates on a plainITsString, not a VC-mapped text source), not a bug; a TE4727-style adjacent-ORC scenario was used instead to still exercise real ORC-preservation in the bulk path.)The "compute preview once" win only reached(fixed during review: addedReplaceWithMethod;BulkCopyMethodandTransduceMethodstill calledNewValuetwice per row inOkToChangeandTryGetNewValue.DoItMethod.NewValueCached, a per-call cache that lets anOkToChangeoverride share its computed value withTryGetNewValueinstead of recomputing. The cache is cleared at the end of everyTryGetNewValuecall so a later, separate call for the same row — e.g. preview, then apply — still recomputes, since the underlying design intentionally treats those as independent evaluations. AddedBulkCopy_ComputesSourceValueOnce. Self-caught regression: the first version of this cache did not clear between calls, which collapsedReplaceWithMethod's intentional preview-then-apply double-evaluation down to one, breakingFakeDoit_MatchesImmediateApplyAcrossPatternModes(7 failures). Caught by rerunning the full suite, fixed, and reverified at 75/75 passing.)(fixed during review: addedNormalizeResult's NFD-skip check (IsNormalizedbefore callingget_NormalizedForm) was covered by only one Latin-diacritic test case (café).FakeDoit_NormalizesNonLatinScriptReplacementResultToNfd(Hangul syllable decomposition, a non-Latin script with a real multi-character canonical decomposition, unlike the single-diacritic Latin case) andFakeDoit_PreservesRichRunPropertiesWhenNormalizingNonLatinReplacementResult(a 1-character-to-3-character Hangul decomposition inside a styled, alternate-writing-system run, confirming run-property/offset-fixup survives a stronger decomposition than the existing café case). Both pass.)Required Validation / Evidence
./build.ps1(full native + managed) - 0 warnings, 0 errors../test.ps1forxWorksTests(ReplaceWithMethodPreviewTests,ReplaceAllInDecoratorCorrectnessTests,BulkEditBarTests,VwPatternReplacementTests) - 75/75 passed against the final combined build../test.ps1 -SkipManaged -TestProject TestViews(nativeVwPatternsuite, isolated viaTestViews.exe -v VwPattern) - 27/27 passed, including the 4 new cross-coverage tests../test.ps1 -SkipManaged -TestProject TestViews(full native suite, twice) - both runs reportTests [Ok-Fail-Error]: [309-0-0](all pass). Both runs then hang for ~5s during process teardown in an unrelated Uniscribe/Graphite rendering-engine subsystem (FindBreakPoint returned an error code), whichtest.ps1reports as a failure after killing the hung process. This subsystem is not touched by this branch's diff, the hang is fully reproducible independent of any change here, and it occurs strictly after all tests report passing. Treated as a pre-existing environmental flake in the native test harness, not a regression from this branch.Positive Observations
.idhsource)._allocabuffer to a persistentVector<OLECHAR>member so it survives multipleNextAcceptedMatchcalls across one bulk session.CheckedPatternPositionguards offset arithmetic againstintoverflow with an explicit failure instead of silent truncation.VwPatternReplacementTests.cscross-checks the real nativeReplaceAllInagainst a repeated-FindInoracle across regex, collation/locale, whole-word, writing-system/style/tag-run, RTL, and combining-mark edge cases — genuine end-to-end integration coverage, not native-only or managed-only.FakeDoit_PropagatesBulkReplacementFailureWithoutFallback), not an oversight — the review's finding was specifically about the interaction with the outer undo-task wrapper, now fixed.memcmpshortcut, an ordinal precheck, a collation-ignorable cache) are documented with why they were unsafe, inDocs/superpowers/plans/evidence/bulk-replacement-distillation-2026-08-13.md, rather than silently dropped.Interview Notes
BulkCopyMethod/TransduceMethoddouble-NewValuecall before deciding whether to fix or just document; investigation found only two call sites (OkToChangeoverrides in those two classes), no external callers ofOkToChangeoutside this file, and a freshDoItMethodinstance constructed per preview/apply phase (no cross-phase staleness risk) — low blast radius, so it was fixed rather than just documented.Suggested Review Focus
try/finallyfix and its regression test match the team's expectations for how a mid-batch native fault during a real (non-preview) bulk apply should behave.ReplaceAllIn-vs-VwMappedTxtSrcORC-omission architectural difference undocumented in code (noted here and in the PR) rather than adding a doc comment onIVwPattern2::ReplaceAllInitself.This change is