Skip to content

Commit 33eeee5

Browse files
elopezclaude
andcommitted
Fix unbounded thunk accumulation in GenDict constants update
Every call sequence updates the constants dictionary with the lazy Data.Map's `unionWith Set.union`, which allocates a suspended `Set.union old new` per colliding key. Such a suspension is only forced when the generator samples that key's AbiType (genWithDict), and only the argument types of fuzzed functions are ever sampled. Since callseq unconditionally inserts an AbiAddressType entry into the additions map (even when no contract was created), any target ABI without an `address`-typed parameter accumulates a never-forced chain of union thunks: one per call sequence per worker, for the campaign lifetime. On a minimal one-function contract this leaks ~0.4 B/call (~31 kB/s at ~75k calls/s), 100% of live-heap growth: the heap census shows a single monotonically growing THUNK band, info-table profiling points at the Map.unionWith specialization's collision branch, and retainer profiling roots it at callseq's WorkerState.genDict. Note this is distinct from the dictionary's known value growth -- it happens even when no new constants are harvested at all. Use Data.Map.Strict.unionWith so per-key sets are forced as they are unioned. Measured on the reproducer: live-heap slope drops from ~31,010 B/s to ~3 B/s over 93M calls, with identical coverage and corpus behavior. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 1b444b6 commit 33eeee5

1 file changed

Lines changed: 8 additions & 1 deletion

File tree

lib/Echidna/Campaign.hs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import Data.List qualified as List
2020
import Data.List.NonEmpty qualified as NEList
2121
import Data.Map (Map, (\\))
2222
import Data.Map qualified as Map
23+
import Data.Map.Strict qualified as MapStrict
2324
import Data.Maybe (isJust, mapMaybe, fromJust)
2425
import Data.Set (Set)
2526
import Data.Set qualified as Set
@@ -502,7 +503,13 @@ callseq vm txSeq = do
502503
additions = Map.unionsWith Set.union [resultMap, eventDiffs, diffs]
503504
-- append to the constants dictionary
504505
updatedDict = workerState.genDict
505-
{ constants = Map.unionWith Set.union workerState.genDict.constants additions
506+
-- the union must be strict in the per-key sets: with the lazy Map a
507+
-- `Set.union old new` suspension is allocated per colliding key per
508+
-- callseq, and it is only ever forced if the generator samples that
509+
-- AbiType -- types that never occur as a fuzzed function argument
510+
-- (always AbiAddressType, inserted unconditionally above) accumulate
511+
-- an unbounded thunk chain
512+
{ constants = MapStrict.unionWith Set.union workerState.genDict.constants additions
506513
, dictValues = Set.union (mkDictValues $ Set.unions $ Map.elems additions)
507514
workerState.genDict.dictValues
508515
}

0 commit comments

Comments
 (0)