Skip to content

Give ported objects an HVO from the target cache, so a copied cache never shares an HVO between two objects - #405

Open
johnml1135 wants to merge 2 commits into
sillsdev:masterfrom
johnml1135:fix/cross-cache-surrogate-hvo-pr
Open

johnml1135 wants to merge 2 commits into
sillsdev:masterfrom
johnml1135:fix/cross-cache-surrogate-hvo-pr

Conversation

@johnml1135

@johnml1135 johnml1135 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

A cache copied with InitializeFromSource / LcmCache.CreateCacheCopy can give two objects the same HVO, so GetObject(hvo) returns the wrong object. This one-line fix gives every ported object an HVO from the target cache. No schema, model version, persisted field or file format changes.

What goes wrong. The cross-cache CmObjectSurrogate(LcmCache, ICmObjectSurrogate) constructor keeps the source surrogate's CmObjectIdWithHvo. Registering that surrogate as inactive does not reserve the number in the target, and the target's allocator starts at 1. So objects activated or created in the copy are handed numbers that dormant ported objects already carry, and m_extantObjectsByHvo[hvo] is overwritten. A source that was fully activated before copying is unaffected, which is why callers today work around this by activating every object first. On large projects that costs seconds.

The fix. The constructor always takes an id from the target identity map (CreateObjectIdWithHvo(guid)), as already happened for activated sources.

Tests.

  • New BEPPortTests.PortFromPartlyActivatedSourceGivesMemoryOnlyTargetUniqueHvos, in its own commit before the fix. It fails without the fix (HVOs shared … 2 (2 objects), 4 (2 objects) …) and passes with it.
  • SIL.LCModel.Tests net8.0 on this branch: 1,717 passed, 0 failed, 18 skipped. Before and after the fix on the earlier base, the full net8.0 suites (Core, Utils, FixData) were otherwise unchanged. net462 was not run.
Evidence from a downstream consumer (Motif)
  • On the Indonesian sample, a memory-only copy from a partly activated source had 8–624 duplicate-HVO groups and 244–253 wrong GetObject lookups across five activation patterns. After the fix: none.
  • With the fix, a memory-only copy made without activating the source first matches a file copy in an exhaustive per-object, per-field comparison on three projects (16k, 48k and 149k objects).
  • Cold copy time drops from 1.7–15.6 s (activate everything, then copy) to 0.1–0.36 s.

Related, not fixed here: AUni alternatives in a writing system the cache does not yet know are dropped on activation, depending on activation order. Filed separately.

🤖 Generated with Claude Code


This change is Reviewable

johnml1135 and others added 2 commits October 12, 2026 14:44
…nique

Copying a cache whose objects are only partly activated currently carries
the source cache's HVOs onto dormant objects in the target, where the
target's own HVO counter hands the same numbers out again. This test fails
until the copy gives each object an HVO from the target cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a cache is copied with InitializeFromSource, dormant source objects
kept the HVO their CmObjectIdWithHvo had in the source cache. The target
never reserved those numbers, so its own HVO counter handed them out again
to activated or newly created objects, and GetObject(hvo) could return the
wrong object. The cross-cache surrogate constructor now always takes a new
id from the target identity map, as already happened for activated sources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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