Answer getUniqueBlocks from an inverted picblockhash index (92s -> 0.11s) - #154
Conversation
getUniqueBlocks decided "is this block hash unique to the requested samples?" by reading
every function in the database that carries picblockhashes. On a Malpedia-sized instance
that is 9,088,495 documents and 51,364,030 block entries streamed into Python, ~92 s per
call - and the cost does not depend on how many samples were asked about, because the
scan is over the corpus either way.
This adds a `picblockhashes` collection, {_id: <block hash>, sample_ids: [...]}, holding
one document per distinct hash, and answers the same question by reading one document per
*candidate* hash. Measured against the live corpus, identical results both ways:
1 sample 4,095 candidates 92 s -> 0.11 s
5 samples 155,849 candidates 107 s -> 2.12 s
Shape: the sample list is all the question needs. Function ids and offsets would multiply
the document count by the blocks each function carries and are already in hand from the
candidate side; family ids would make a relabel invalidate the index for no gain.
Not the existing _picblockhashes.hash index instead: it answers the same query, but its
$in fan-out pulls whole function documents, so it degrades as the request grows - measured
12.7x for one sample and only 2.3x for five, where this is 407x and 37x on the same runs.
The sample-set case is the one that matters.
Maintenance is on the write paths, with the rebuild as the repair path:
- add uses $addToSet with upsert, which has set semantics, so a retried or replayed insert
converges instead of drifting. That is the difference from the family counters in #151,
where $inc makes a lost or repeated update permanent.
- delete scopes the $pull by _id, taken from the sample's own functions in one indexed
read, so no multikey index on sample_ids is needed (0.24 GB over 12.1M documents on that
instance). It therefore has to run before the function documents are deleted.
- emptied posting lists are removed rather than left behind; they would read correctly
here, but #149 is that same residue in the band index.
- a family reassignment is a no-op, pinned by a test.
- recalculateAllPicHashes rewrites block hashes wholesale and so marks the index stale.
The index is only read when a settings flag says it is complete. An index that merely
exists is not safe to read: a half-filled one reports blocks as unique that are not. A
database with no functions gets the flag for free, so fresh instances never need a rebuild;
an existing database upgrading into this keeps using the scan - which is retained, not
deleted - until an operator calls rebuild_picblockhash_index. Upgrading therefore changes
performance, never results.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwSfzAD1ZwEoWY3eWvbYvX
SmdaReport.fromDict is typed Optional, so ty rejects assigning to .sha256 on the result. The surrounding tests already use this assert; the new helper now matches them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwSfzAD1ZwEoWY3eWvbYvX
|
Rebased onto One conflict needed a judgement rather than a merge. My side of function_minhashes = self._getFunctionMinHashesBySampleId(sample_id)
self._pullBandEntries(function_minhashes)
# drop this sample from the picblockhash index while its functions still exist to be read
self._removeSampleFromPicBlockHashIndex(sample_id)The ordering constraint from the original PR still holds and is why the hook sits there: it reads the sample's own functions, so it has to run before they are deleted. The other three conflicts were adjacent-edit collisions where both sides were needed — 🤖 Generated with Claude Code |
8f3aa65 to
79e981b
Compare
getUniqueBlocksdecides "is this block hash unique to the requested samples?" by reading every function in the database that carries picblockhashes. On this Malpedia-sized instance (8,691 samples / 11,669,208 functions) that is 9,088,495 documents and 51,364,030 block entries streamed into Python, ~92 s per call — and the cost does not depend on how many samples were asked about, because it scans the corpus either way.This adds a
picblockhashescollection and answers the same question by reading one document per candidate hash.Measured, on the live corpus, identical results both ways
win.citadel)12,139,968 distinct hashes, 0.47 GB + 0.25 GB for the
_idindex, 245 s to build.The decisions, since they are the reviewable part
Why this shape. The sample list is the whole question. Function ids and offsets would multiply the document count by the ~5.6 blocks each function averages, and
getUniqueBlocksalready holds them from the candidate side. Family ids would make a relabel invalidate the index for no gain.Why not the existing
_picblockhashes.hashindex instead. It answers the same query and needs no new collection — I measured it as the cheaper option first. But its$infan-out pulls whole 708-byte function documents, so it degrades as the request grows: 12.7x at one sample, 2.3x at five, against 407x and 37x here, on the same runs. The sample-set case is the one that matters — fkie-cad/mcritweb#140 is about to give Unique Blocks its own page taking a set of samples, where today the only caller passes a list of one.Why maintaining this on the write path is safe, when #149 and #151 are both bugs in exactly that. The difference is idempotency:
$addToSetwithupsert, which has set semantics — a retried or replayed insert converges instead of drifting. Family statistics drift from the collections they summarise, and /status reports the drifted values as the corpus size #151's family counters use$inc, where a lost or repeated update is permanent and needs a recount to find.$pullby_id, taking the hashes from the sample's own functions in one indexed read. So no multikey index onsample_idsis needed — that would have cost 0.24 GB over 12.1M documents. The consequence is that the hook must run before the function documents are deleted, which is why it sits where it does indeleteSample.family_idhere has to confront it.recalculateAllPicHashesrewrites block hashes wholesale, so it marks the index stale rather than trying to diff old against new per function — which is what the rebuild does anyway, in one pass.Why a completeness flag rather than "the collection exists". A half-filled index reports blocks as unique that are not — a wrong answer, not a slow one. So the index is read only when a settings flag vouches for it. A database with no functions gets the flag for free (an empty index over an empty corpus is complete), so fresh instances maintain it from the first submit and never need a rebuild. An existing database upgrading into this does not get the flag and keeps using the scan — which is retained, not deleted — until an operator calls the rebuild. Upgrading changes performance, never results. While the flag is unset the write hooks skip their work, since there is no point maintaining an index nothing reads.
clearStoragedrops the collection. Leaving it would keep asserting that hashes are held by samples that no longer exist.Exposure
Worker.rebuildPicBlockHashIndex(@Remote(progress=True), soMinHashIndexforwards it automatically), aGET /rebuild_picblockhash_indexroute, andMcritClient.rebuildPicBlockHashIndex— mirroringrebuild_index/recalculate_pichashes.MemoryStoragegets areturn 0implementation with a comment: it holds every function in a dict already, so its elimination has nothing to index, and the backends stay interchangeable.Tests
Five new mongo-backed tests, all using two samples built from the same report so they carry identical block hashes:
getUniqueBlockscall answered both ways, for one sample and for twoWhat I did not do, and what I could not verify here
$outrebuild replaces the collection atomically but holds no lock against concurrent writes; a submit landing mid-rebuild could be missed. It sets the flag afterwards, so the window is "rebuild then submit" and the fix would be to re-run it. Worth a maintainer's opinion on whether that needs more than a docstring.🤖 Generated with Claude Code
https://claude.ai/code/session_01GwSfzAD1ZwEoWY3eWvbYvX