Repository navigation
mmap page-aligned dictionary cache - #13
Merged
Merged
Conversation
Parsing the 10+ MB text dictionary dominated switch-in. Each dictionary now also gets a binary cache built for memory mapping, and Load() maps it read-only and answers queries in place instead of deserializing anything. Format (version 2, magic CANTOMAP): a header page (source size + mtime signature, counts, region offsets), a page-aligned index with one entry per data block (first page, page count, first key), then 4 KB data pages packed with sorted entries that never straddle a page, each page carrying its own entry count and offset table. An entry too big for one page gets a run of whole pages to itself, flagged by its page count in the index. Strings are UTF-16 so keys compare in place and candidates need no decode. Lookup binary-searches the index, then the one page. Prefix scans start there and walk forward across page boundaries. The header and whole index are validated on map, and every in-page offset and length is bounds-checked on access, so a stale, truncated, or damaged cache falls back to the text parse (which rewrites the cache) and never crashes the host app. Overrides move to a small in-memory overlay consulted on top of the base (same prepend + dedup semantics), since the mapping is read-only. Caches are written atomically (temp file + rename; a mapped old cache is renamed aside). The installer prebuilds both stock caches with the new canto-mkcache.exe, the options dialog prebuilds a locked-in custom dictionary per user, and CI runs canto-dicttest.exe, which checks every query on the mapped cache against the text parse plus stale/truncated/corrupt handling.
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.
Memory-maps the dictionary. Branched from
main, so none of the service-process work from #12 is in here.Mapping the text file directly would not help much, so the cache stays. Its format is now built for mmap: page-aligned, with a page index, and pages packed tight.
Cache format (version 2, magic
CANTOMAP).txtsize + last-write time (a mismatch means the cache is stale), entry/block/page counts, index and data offsets, max key length.u16 count, u16 reserved, u16 offset[count], so the search inside a page is also binary. An entry isu16 keyLen, u16 candCount, key, candCount x (u16 len, chars).pageCount > 1).std::wstringand candidates need no UTF-8 decode. That is also smaller than UTF-8 for CJK candidates.Lookup path
CreateFileW(FILE_SHARE_READ | FILE_SHARE_DELETE) +CreateFileMapping+MapViewOfFile, read-only. Both handles are closed right after mapping (the view keeps the section alive). The view is owned by ashared_ptrwith anUnmapViewOfFiledeleter, soDictionarystays movable for the existing background-load handoff, and reload/destroy unmaps.KeysWithPrefix: same start point, then walk forward across page boundaries while keys match.LongestExactPrefixLen/HasExactuse exact lookup.Lookupnow returns by value because candidates are decoded from the mapping. The two call sites bind the result to a reference or a range-for, so they need no changes.Overrides
The mapping is read-only, so overrides move to a small in-memory overlay that every query checks on top of the base. Semantics are unchanged: override candidates come first, stock duplicates are dropped, and override-only keys are added, including to prefix scans and the segmenter.
Safety and fallback
The cache is treated as untrusted input:
Loadparses the text instead (the path that existed before). It then writes the cache and switches that instance to the mapping, which also frees the parsed map's memory in the host process.Writes are atomic: the cache goes to a uniquely named temp file, then
MoveFileExreplaces the old one. Windows refuses to replace a file that another process has mapped. In that case the old cache is renamed aside to.cache.oldand deleted once nothing maps it.Where caches live
This brings back the location policy from the earlier #11 iteration:
traditional.txt.cache,simplified.txt.cache). The installer prebuilds both with the newcanto-mkcache.exe. If the folder is read-only, it falls back to the per-user location.%LOCALAPPDATA%\CantoIME\cache\<fnv64-of-path>.cache. The options dialog prebuilds it in the background when a custom dictionary is locked in.{app}\*.cache,*.cache.old, and%LOCALAPPDATA%\CantoIME.Test (runs in CI)
canto-dicttest.exe traditional.txt simplified.txtruns on a synthetic dictionary (a 5000-candidate entry that needs a multi-page run, among 1500 small keys) and on both stock dictionaries. For each one it:Lookup,HasExactandLongestExactPrefixLenmatch the text parse for every key;KeysWithPrefixfor every first character (this crosses every page boundary and must cover every key), and capped (64) scans for every 1 to 3 character prefix;The test log also prints parse time against map time.
Still needs VM testing