Skip to content

fix(analyzer): Summary storage improvement - #365

Open
Saloed wants to merge 4 commits into
mainfrom
saloed/10-heap-compression-main
Open

fix(analyzer): Summary storage improvement#365
Saloed wants to merge 4 commits into
mainfrom
saloed/10-heap-compression-main

Conversation

@Saloed

@Saloed Saloed commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

No description provided.

Saloed and others added 4 commits August 20, 2026 18:45
…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
Saloed force-pushed the saloed/10-heap-compression-main branch from 77a97b6 to 3617ae9 Compare August 20, 2026 19:15
@Saloed

Saloed commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

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