Skip to content

Commit f3e19cd

Browse files
committed
Use broadcast TChan for campaign events
Replace the campaign event queue with an STM broadcast TChan so subscribers keep the existing events-after-subscribe behavior without the Chan root cursor retention issue. Producers now write through STM, and campaign/UI/SSE listeners subscribe with dupTChan/readTChan. Avoid retaining full VM snapshots in queued TestFalsified/TestOptimized events by publishing VM-free EchidnaTest copies. The canonical testRefs state still keeps the VM so shrinking, reports, traces, and final JSON continue to work.
1 parent 1b444b6 commit f3e19cd

6 files changed

Lines changed: 21 additions & 18 deletions

File tree

lib/Echidna.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
module Echidna where
22

3-
import Control.Concurrent (newChan)
43
import Control.Monad.Catch (MonadThrow(..))
54
import Control.Monad.IO.Class (liftIO)
65
import Data.IORef (newIORef)
@@ -14,6 +13,7 @@ import Data.Text qualified as T
1413
import System.Console.ANSI (hNowSupportsANSI)
1514
import System.FilePath ((</>))
1615
import System.IO (stderr, stdout, hPutStrLn)
16+
import UnliftIO.STM (newBroadcastTChanIO)
1717

1818
import EVM (cheatCode)
1919
import EVM.ABI (AbiValue(AbiAddress))
@@ -124,7 +124,7 @@ mkEnv :: EConfig -> BuildOutput -> [EchidnaTest] -> World -> Maybe SlitherInfo -
124124
mkEnv cfg buildOutput tests world slitherInfo = do
125125
codehashMap <- newIORef mempty
126126
chainId <- Onchain.fetchChainIdFrom cfg.rpcUrl
127-
eventQueue <- newChan
127+
eventQueue <- newBroadcastTChanIO
128128
coverageRefInit <- newIORef mempty
129129
coverageRefRuntime <- newIORef mempty
130130
corpusRef <- newIORef mempty

lib/Echidna/Campaign.hs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import Data.Text (Text, unpack)
2727
import Data.Time (LocalTime)
2828
import Data.Vector qualified as V
2929
import System.Random (mkStdGen)
30+
import UnliftIO.STM (TChan, atomically, dupTChan, readTChan)
3031

3132
import EVM (cheatCode)
3233
import EVM.ABI (getAbi, AbiType(AbiAddressType, AbiTupleType), AbiValue(AbiAddress, AbiTuple), abiValueType)
@@ -120,7 +121,7 @@ runSymWorker callback vm dict workerId _ name = do
120121
cfg <- asks (.cfg)
121122
let nworkers = getNFuzzWorkers cfg.campaignConf -- getNFuzzWorkers, NOT getNWorkers
122123
eventQueue <- asks (.eventQueue)
123-
chan <- liftIO $ dupChan eventQueue
124+
chan <- liftIO $ atomically $ dupTChan eventQueue
124125

125126
flip runStateT initialState $
126127
flip evalRandT (mkStdGen effectiveSeed) $ do -- unused but needed for callseq
@@ -644,7 +645,7 @@ updateOpenTest vm reproducer test = do
644645
, result
645646
, workerId
646647
}
647-
pushWorkerEvent (TestFalsified test')
648+
pushWorkerEvent (TestFalsified (test' { Test.vm = Nothing }))
648649
pure $ Just test'
649650

650651
IntValue value' | value' > value -> do
@@ -653,7 +654,7 @@ updateOpenTest vm reproducer test = do
653654
, vm = Just vm
654655
, result
655656
}
656-
pushWorkerEvent (TestOptimized test')
657+
pushWorkerEvent (TestOptimized (test' { Test.vm = Nothing }))
657658
pure $ Just test'
658659
where
659660
value =
@@ -686,7 +687,7 @@ spawnListener handler = do
686687
cfg <- asks (.cfg)
687688
let nworkers = getNWorkers cfg.campaignConf
688689
eventQueue <- asks (.eventQueue)
689-
chan <- liftIO $ dupChan eventQueue
690+
chan <- liftIO $ atomically $ dupTChan eventQueue
690691
stopVar <- liftIO newEmptyMVar
691692
liftIO $ void $ forkFinally (listenerLoop handler chan nworkers) (const $ putMVar stopVar ())
692693
pure stopVar
@@ -697,14 +698,14 @@ listenerLoop
697698
:: (MonadIO m)
698699
=> ((LocalTime, CampaignEvent) -> m ())
699700
-- ^ a function that handles the events
700-
-> Chan (LocalTime, CampaignEvent)
701+
-> TChan (LocalTime, CampaignEvent)
701702
-- ^ event channel
702703
-> Int
703704
-- ^ number of workers which have to stop before loop exits
704705
-> m ()
705706
listenerLoop handler chan !workersAlive =
706707
when (workersAlive > 0) $ do
707-
event <- liftIO $ readChan chan
708+
event <- liftIO $ atomically $ readTChan chan
708709
handler event
709710
case event of
710711
(_, WorkerEvent _ _ (WorkerStopped _)) -> listenerLoop handler chan (workersAlive - 1)

lib/Echidna/Server.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module Echidna.Server where
22

3-
import Control.Concurrent
3+
import Control.Concurrent (MVar, forkIO, putMVar)
44
import Control.Monad (forever, when, void)
55
import Data.Aeson
66
import Data.Binary.Builder (fromLazyByteString)
@@ -40,11 +40,11 @@ instance ToJSON SSE where
4040
runSSEServer :: MVar () -> Env -> Word16 -> Int -> IO ()
4141
runSSEServer serverStopVar env port nworkers = do
4242
aliveRef <- newIORef nworkers
43-
sseChan <- dupChan env.eventQueue
43+
sseChan <- atomically $ dupTChan env.eventQueue
4444
broadcastChan <- newBroadcastTChanIO
4545

4646
void . forkIO . forever $ do
47-
event@(_, campaignEvent) <- readChan sseChan
47+
event@(_, campaignEvent) <- atomically $ readTChan sseChan
4848
let eventName = \case
4949
WorkerEvent _ _ workerEvent ->
5050
case workerEvent of

lib/Echidna/Types/Config.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
module Echidna.Types.Config where
22

3-
import Control.Concurrent (Chan)
43
import Data.Aeson.Key (Key)
54
import Data.IORef (IORef)
65
import Data.Set (Set)
76
import Data.Text (Text)
87
import Data.Time (LocalTime)
98
import Data.Word (Word64)
9+
import UnliftIO.STM (TChan)
1010

1111
import EVM.Dapp (DappInfo)
1212
import EVM.Fetch qualified as Fetch
@@ -78,7 +78,7 @@ data Env = Env
7878

7979
-- | Shared between all workers. Events are fairly rare so contention is
8080
-- minimal.
81-
, eventQueue :: Chan (LocalTime, CampaignEvent)
81+
, eventQueue :: TChan (LocalTime, CampaignEvent)
8282

8383
, testRefs :: [IORef EchidnaTest]
8484
, coverageRefInit :: IORef CoverageMap

lib/Echidna/UI.hs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import System.Signal
2828
import UnliftIO
2929
( MonadUnliftIO, IORef, newIORef, readIORef, hFlush, stdout , writeIORef, timeout)
3030
import UnliftIO.Concurrent hiding (killThread, threadDelay)
31+
import UnliftIO.STM (atomically, writeTChan)
3132

3233
import EVM.Fetch qualified
3334
import EVM.Types (Addr, Contract, VM, VMType(Concrete), W256)
@@ -108,7 +109,7 @@ ui vm dict initialCorpus cliSelectedContract = do
108109
let forwardEvent = void . writeBChanNonBlocking uiChannel . EventReceived
109110

110111
-- Attach the log/event forwarder before workers start so early worker
111-
-- events (like startup logs) are not lost by dupChan.
112+
-- events (like startup logs) are not lost by dupTChan.
112113
uiEventsForwarderStopVar <- spawnListener forwardEvent
113114
workers <- spawnWorkers
114115

@@ -181,7 +182,7 @@ ui vm dict initialCorpus cliSelectedContract = do
181182

182183
let forwardEvent ev = putStrLn =<< runReaderT (ppLogLine vm ev) env
183184
-- Attach the log/event forwarder before workers start so early worker
184-
-- events (like startup logs) are not lost by dupChan.
185+
-- events (like startup logs) are not lost by dupTChan.
185186
uiEventsForwarderStopVar <- spawnListener forwardEvent
186187
workers <- spawnWorkers
187188

@@ -271,7 +272,8 @@ ui vm dict initialCorpus cliSelectedContract = do
271272
_ -> pure ()
272273

273274
time <- liftIO getTimestamp
274-
writeChan env.eventQueue (time, WorkerEvent workerId workerType (WorkerStopped stopReason))
275+
liftIO $ atomically $
276+
writeTChan env.eventQueue (time, WorkerEvent workerId workerType (WorkerStopped stopReason))
275277

276278
pure (threadId, stateRef)
277279

lib/Echidna/Worker.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
module Echidna.Worker where
22

3-
import Control.Concurrent
43
import Control.Monad.Reader (MonadReader, MonadIO, liftIO, ask)
54
import Control.Monad.State.Strict(MonadState(..), gets)
65
import Data.Aeson
76
import Data.Text (unpack)
7+
import UnliftIO.STM (atomically, writeTChan)
88

99
import Echidna.ABI (encodeSig)
1010
import Echidna.Types.Campaign
@@ -47,7 +47,7 @@ pushWorkerEvent event = do
4747
pushCampaignEvent :: Env -> CampaignEvent -> IO ()
4848
pushCampaignEvent env event = do
4949
time <- liftIO getTimestamp
50-
writeChan env.eventQueue (time, event)
50+
atomically $ writeTChan env.eventQueue (time, event)
5151

5252
ppCampaignEvent :: CampaignEvent -> String
5353
ppCampaignEvent = \case

0 commit comments

Comments
 (0)