Skip to content

Commit 9eeb808

Browse files
committed
Sever UI-state generation retention via vty lastPicRef
The interactive (Brick/vty) UI retained one UIState generation per rendered frame, unbounded: vty parks the last Picture in an IORef (lastPicRef) for resize redraws, and Brick builds that Picture lazily -- its image/viewport nodes are renderFinal/viewportScroll thunks closing over the UIState they were rendered from, which through the prior render closes over the previous UIState. The single unforced Picture is thus a thunk chain reaching back through every frame, pinning each generation's campaigns/WorkerState/GenDict and (on falsifying contracts) its test VM snapshots. ghc-debug on a live process showed a single retainer path with 788 chained renderFinal/viewportScroll thunk pairs off lastPicRef. Measured: +12 kB/s unbounded on a trivial non-falsifying contract (interactive), and 681 MB pinned on a 20-failing-assert stress contract. Fixes: - UI.hs: deep-force each Picture before handing it to vty, so lastPicRef holds concrete images instead of a UIState-closing thunk chain (primary). - UI.hs: force the VM-stripped test copies; `t { vm = Nothing }` was a thunk retaining the VM-bearing original. - Campaign.hs: bind NewCoverage.transactions strictly; `force (...)` on a non-strict field was itself a thunk pinning the sequence's VMResults. - Exec.hs: forceVMData now also forces vm.tx.subState, whose lazily-consed Expr EAddr elements otherwise chain a stored VM to every intermediate VM of its transaction. Validated: trivial-contract interactive growth +12,071 -> +517 B/s (UIState generation growth +4.1 MB -> +0); stress-contract live heap 681 -> 39 MB flat; ghc-debug generation chain 788 -> 0.
1 parent c929006 commit 9eeb808

3 files changed

Lines changed: 34 additions & 5 deletions

File tree

lib/Echidna/Campaign.hs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -481,13 +481,15 @@ callseq vm txSeq = do
481481
in (corp', corpusSize corp')
482482

483483
(points, numCodehashes) <- liftIO $ coverageStats env.coverageRefInit env.coverageRefRuntime
484+
-- force the list eagerly: `transactions` is a non-strict field, so a
485+
-- lazy `force (...)` is itself stored as a thunk that pins every
486+
-- VMResult (and its returndata) of the executed sequence until some
487+
-- consumer demands it -- and none does under the default config
488+
let !transactions = force $ fst <$> results
484489
pushWorkerEvent NewCoverage { points
485490
, numCodehashes
486491
, corpusSize = newSize
487-
-- force the list so the event doesn't hold a
488-
-- thunk pinning every VMResult (and its
489-
-- returndata) of the executed sequence
490-
, transactions = force $ fst <$> results
492+
, transactions
491493
}
492494

493495
modify' $ \workerState ->

lib/Echidna/Exec.hs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,12 +336,28 @@ forceVMData :: VM Concrete -> ()
336336
forceVMData vm =
337337
contractsWhnf `seq`
338338
storesWhnf `seq`
339+
subStateWhnf `seq`
339340
Set.size vm.keccakPreImgs `seq`
340341
length vm.constraints `seq`
341342
length vm.logs `seq`
342343
vm.burned `seq`
343344
()
344345
where
346+
-- the substate's list elements are pushed lazily (hevm's pushTo conses
347+
-- an unevaluated Expr EAddr closing over the mid-execution VM), so a
348+
-- stored VM otherwise chains to every intermediate VM of its
349+
-- transaction; Set elements are already forced by Ord on insert, but
350+
-- the suspended inserts still need their spines demanded via size
351+
subStateWhnf =
352+
let ss = vm.tx.subState
353+
whnfElems = foldl (\u x -> x `seq` u) ()
354+
in whnfElems ss.selfdestructs `seq`
355+
whnfElems ss.touchedAccounts `seq`
356+
whnfElems (map fst ss.refunds) `seq`
357+
Set.size ss.accessedAddresses `seq`
358+
Set.size ss.accessedStorageKeys `seq`
359+
Set.size ss.createdContracts `seq`
360+
()
345361
contractsWhnf = Map.size vm.env.contracts
346362
storesWhnf =
347363
Map.foldl' (\acc c -> acc + storeSize c.storage + storeSize c.origStorage)

lib/Echidna/UI.hs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Brick.BChan
77
import Brick.Widgets.Dialog qualified as B
88
import Control.Concurrent (killThread, threadDelay)
99
import Control.Concurrent.MVar (readMVar)
10+
import Control.DeepSeq (deepseq)
1011
import Control.Exception (AsyncException)
1112
import Control.Monad
1213
import Control.Monad.Catch
@@ -151,7 +152,12 @@ ui vm dict initialCorpus cliSelectedContract = do
151152
let output = Vty.outputIface v
152153
when (Vty.supportsMode output Vty.Mouse) $
153154
Vty.setMode output Vty.Mouse True
154-
pure v
155+
-- deep-force every picture before it reaches vty: brick's
156+
-- renderFinal composes pictures lazily and threads suspended
157+
-- viewport state across renders, so the picture parked in
158+
-- vty's lastPicRef otherwise grows a thunk chain that pins
159+
-- one UI state generation (and its test snapshots) per render
160+
pure v { Vty.update = \pic -> pic `deepseq` Vty.update v pic }
155161
initialVty <- liftIO buildVty
156162
app <- customMain initialVty buildVty (Just uiChannel) <$> monitor snapshotRef
157163

@@ -377,7 +383,12 @@ monitor snapshotRef = do
377383
-- snapshot reachable from them would be pinned (the tests pane
378384
-- above is built from the VM-bearing snapshot and renders to
379385
-- forced strings, so nothing else needs the VMs here)
386+
-- the stripped copies must be forced: each `t { vm = Nothing }`
387+
-- is itself a thunk retaining the VM-bearing original until
388+
-- evaluated, and nothing in the render path demands the list
389+
-- elements
380390
let strippedTests = (\t -> t { vm = Nothing } :: EchidnaTest) <$> tests
391+
!_ = foldl (\u t -> t `seq` u) () strippedTests
381392
let updatedState = state { campaigns = c', status = Running, now
382393
, tests = strippedTests
383394
, fetchedContracts = contracts

0 commit comments

Comments
 (0)