fix(analyzer): Summary storage improvement - #365
Open
Saloed wants to merge 4 commits into
Open
Conversation
…er storage TreeSetWithCompression built an AccessTreeSoftInterner in its constructor, and there is one of those storages per premise trie node. A ThingsBoard heap dump (-Xmx12g, ssrf, 300 s, dumped at t=251 s) holds 2,272,578 AccessTreeSoftInterner instances, of which only 369,423 - 16.3 % - have ever created the AccessTreeInterner behind their soft reference. The other 83.7 % are 32 bytes of nothing each, ~58 MiB. They are empty because internImpl gates twice before it touches the interner: one attempt in INTERN_RATE, and only once some tree in the storage has reached MIN_SIZE_TO_INTERN. internImpl now takes the interner through a `getInterner` lambda evaluated after both gates, so the holder can allocate there instead of up front. The function is inline, so the lambda costs nothing. Representation only: the same trees are interned, by the same interner, at the same moments. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit 4799efe77a5ec4dda06c937545d8f284b8579bc5) (cherry picked from commit e24a636)
…rst child Every AccessBasedStorage node built a ConcurrentReadSafeInt2ObjectMap in its constructor. Measured on the same ThingsBoard dump: of the 1,652,208 IF2FFStorage nodes, 85.51 % have no children at all and 94.21 % have at most one - mean 0.947, max 194. The children maps are 610.6 MiB of a 9.70 GiB live heap and the empty ones alone are ~345 MiB (map object + two 17-slot tables = ~256 B each). The field is now nullable and installed on the first getOrCreateChild, under a double-checked monitor so racing writers cannot each install a table. @volatile is load-bearing here rather than decorative. As a `val` the map got final-field publication for free; a plain `var` assigned after construction would let a lock-free reader observe a non-null map whose key/value tables are not yet visible. The map's seqlock cannot repair that - it guards mutations of a published map, not the publication of the map itself. The volatile write is the release that pairs with the reader's acquiring load, once per node. Representation only: the same children, reachable the same way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit 276d2d995f3c20e9bae6a0ba8755d758e8d881cf) (cherry picked from commit ea2524b)
TreeSetWithCompression gave every storage an arrayOfNulls(maxInstIdx + 1) per column, sized to the whole method whether or not the premise reached those instructions. Measured on a ThingsBoard heap dump (-Xmx12g, ssrf, 300 s, dumped at t=251 s), across the 1,652,208 EdgeNonUniverseExclusionMergingStorage instances: exclusions/edges array length : mean 29.65 (58.0 % are 3, 8.8 % exceed 64, max 1573) non-null slots : mean 2.64 (84.9 % use <= 2, max 377) 8.9 % occupancy, and the two dense arrays are 434.0 MiB of a 9.70 GiB live heap. The non-null distributions of exclusions and edges are byte-identical, because add() writes both at the same index - so one table carries both as two columns. Instructions now live in an ascending key array beside a value array of `columns` slots per key, the two wrapped in an immutable Row published through a single @volatile field. Lookup scans linearly up to eight keys (93.8 % of storages) and binary-searches above that. Growth copies; with a mean of 2.64 rows the copies are noise. This is safer than what it replaces, not merely smaller: a reader takes one acquiring load and cannot pair a resized key array with a stale value array, where two plain arrays gave no ordering at all. Writes into an existing row are still plain stores, exactly as before, and access is still stored before the exclusion that marks the row populated. Representation only: same facts, same instructions, same merge order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit a603820e8d017f291b3aea780f36f622e3e5ce21) (cherry picked from commit 0e305bb)
…et per node AccessTreeIndexImpl gave every trie node a java.util.BitSet over all indexed items. A BitSet's backing long[] is sized by the largest index it holds, not by how many, and the indices here run to tens of thousands. Measured on a ThingsBoard heap dump (-Xmx12g, ssrf, 300 s, dumped at t=251 s) across all 260,670 nodes: long[] length : mean 775 words = 6.2 KiB (98.4 % exceed 32 words, max 2746) set bits : mean 7.46; 87.75 % hold exactly one, 95.4 % at most three 1545.7 MiB - 15.8 % of a 9.70 GiB live heap, and 97 % of every long[] byte in it, to store 1.9 M bits. A node now keeps an ascending IntArray of item indices and promotes to a BitSet only past SPARSE_LIMIT. At the mean 7.46 items that is ~48 B rather than 6.2 KiB. The limit is set at 512 to bound the O(n) insert, not because a BitSet becomes cheaper there - at 512 items sparse is still 2 KiB against dense 6.2 KiB, since the dense cost tracks the maximum index rather than the count. findStartsWith returned the node's BitSet, so both call sites now iterate instead: AccessTreeIndex.forEachStartsWith, and DefaultNDF2FSubStorage's relevantStorageIndices becomes forEachRelevantStorageIndex (automata and cactus iterate the BitSet they already build). Materialising a BitSet at the boundary would have cost more than the set it describes. Representation only: same indices, same ascending order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit 0a564b83fb12c4bf88569aac2b32230ff242ae4e) (cherry picked from commit 77a97b6)
Saloed
force-pushed
the
saloed/10-heap-compression-main
branch
from
August 20, 2026 19:15
77a97b6 to
3617ae9
Compare
Contributor
Author
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.
No description provided.