Repository navigation
Keep small member trees inside their edge record - #84
Merged
Merged
Conversation
A stored reference used to start with a raw page id, with page id 0 meaning an empty tree. References now start with a one byte tag (empty, stored, inline), and the new inline variant carries a small frozen leaf directly in the parent value instead of pointing at a page. Inline leaves are limited to 32 entries, no nested trees, and an encoded size no bigger than a stored reference can take, so a parent leaf that budgeted for a reference can never overflow. Inline references compare by their entries, not by identity, and a catalog leaf counts the heap of the inline leaves it holds. Nothing creates inline references yet. The storage format moves to 6 because every reference on disk gained the tag byte.
When a commit writes a catalog leaf, a member or order tree that is a single leaf of at most 32 entries, small enough to fit where a stored reference would go, now stays in the edge record instead of getting a page of its own. A three member edge no longer costs an extra page, an extra cache entry and an extra read. Only codecs that opt in through inlinesSmallTrees get this, which today is just the catalog. Slot roots and branch children are still always written as pages. The page quota check skips trees that will be inlined. Growing past the limit turns the tree into a page on its next commit, and shrinking below it brings it back the next time its leaf is rewritten. Reference counts need no special handling since an inline tree has no page to count. Refs #55
…aves Most member leaves have no roles, weights, validity, data or qualifiers, so every Incidence in them is just Incidence.of(member). Such a leaf now keeps only its member ids (the keys themselves for set edges) and builds the Incidence when someone asks for a value. The file format is the same: the codec spots a leaf whose column flags are all clear, and writes one back out straight from the ids. A decoded 200 member leaf drops from about 77 to 9 bytes per member on the heap for set edges and to 17 for ordered ones, and decoding it allocates about 2 KB instead of 16 KB. Writable copies still hold real Incidence objects, so the write path is unchanged.
Covers the tagged reference encoding, when a tree is kept inline and why that can't overflow a page, how equality, reference counts, compaction and the change feed treat inline trees, the leaves that build their Incidence values on demand, and the format 6 refusal message. The example outputs from clinical-claims.hql were regenerated, and the benchmark page gets a before and after comparison.
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.
Stacked on #83. Closes #55.
Every hyperedge kept its members in a tree of its own, even with three members, so reading them meant loading the edge record and then one more page. At scale 1 that is 100,000 extra pages and node-cache entries. This PR keeps small member trees inside the edge record and stops holding one
Incidenceobject per member in plain member leaves.What changes
Ref.Inline(Leaf)only accepts a frozen leaf with 1 to 32 entries, no nested trees and an encoded size withinRef.maxEncodedSize()(108 bytes). OnlyRef.read(in, schema)can decode an inline leaf; the schema-lessRef.read(in)refuses one, so roots, branch children, WALRootrecords, postings and property directories never hold one. The storage format goes from 5 to 6, and older directories are refused with the usual export-and-reload message.Materializerinlines a pending leaf only for nested references in codecs that opt in (ValueCodec.inlinesSmallTrees(), onlyAtomCodectoday) and only when it fits. A tree that grows past the limit gets a page; one that shrinks back below it becomes inline the next time its leaf is rewritten.Ref.sameandInline.equalscompare inline trees by their entries, so conflict checks and the set algebra still see an unchanged edge as unchanged. A stored and an inline reference are never "same", which only sends a caller down its slower path.TreeWalkeronly reports stored references, and a catalog leaf whose edges are all inline leavesREFERS_TO_PAGESclear. Compaction moves whole pages, so inline trees travel with their catalog page.Incidence.of(key)when a value is asked for. The file format is unchanged by this part, and the write path still works on real objects. A decoded 200-member set leaf now holds 8.8 bytes per member on the heap instead of 76.8.ValueCodec.forEachRefvisits nested references without building records.TreeWalker,TreeVerifierand the heap estimate use it, and the catalog leaf's heap estimate now includes its inline trees.Testing
InlineMembersTest(24 cases):TreeTestcovers the encoding, content equality, the fit rule, and inline summaries and fingerprints matching the stored page's. For implied values it checks equal values, a smaller heap estimate, and byte-identical re-encoding.EngineTestchecks that a format-5 directory is refused.examples/clinical-claims.hqlloads andhstore checkpasses on 20 pages, where the same data needed 105. After a SIGKILL partway through a 20,000-insert load, reopening replayed 475 commits and the reference counts matched.COMPACTran andcheckpassed again.Measurements
HStore only, scale 1, async, 64 MiB node cache, 3 alternating runs per build, medians. The machine was loaded, so trust allocation, bytes read and disk over throughput.
read.membersread.twohopingest.edgesallocation per edgeKnown costs
large.probeallocates about 64 bytes more per lookup, because a single member'sIncidenceis now built when asked for.churn.deleteallocation moved between 75 and 91 KB per op across runs, which looks like a wash but is worth watching.