Skip to content

Commit 49a804e

Browse files
authored
Merge pull request #1387 from crytic/ui-responsiveness
Improve UI responsiveness
2 parents 175a72c + 222a425 commit 49a804e

File tree

5 files changed

+81
-46
lines changed

5 files changed

+81
-46
lines changed

lib/Echidna.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,10 @@ mkEnv cfg buildOutput tests world slitherInfo = do
125125
(contractCache, slotCache) <- Onchain.loadRpcCache cfg
126126
fetchContractCache <- newIORef contractCache
127127
fetchSlotCache <- newIORef slotCache
128+
contractNameCache <- newIORef mempty
128129
-- TODO put in real path
129130
let dapp = dappInfo "/" buildOutput
130-
pure $ Env { cfg, dapp, codehashMap, fetchContractCache, fetchSlotCache
131+
pure $ Env { cfg, dapp, codehashMap, fetchContractCache, fetchSlotCache, contractNameCache
131132
, chainId, eventQueue, coverageRefInit, coverageRefRuntime, corpusRef, testRefs, world
132133
, slitherInfo
133134
}

lib/Echidna/Types/Config.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ data Env = Env
8080
, codehashMap :: CodehashMap
8181
, fetchContractCache :: IORef (Map Addr (Maybe Contract))
8282
, fetchSlotCache :: IORef (Map Addr (Map W256 (Maybe W256)))
83+
, contractNameCache :: IORef (Map W256 Text)
8384
, chainId :: Maybe W256
8485
, world :: World
8586
}

lib/Echidna/UI.hs

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ ui vm dict initialCorpus cliSelectedContract = do
122122
liftIO $ do
123123
tests <- traverse readIORef env.testRefs
124124
now <- getTimestamp
125-
void $ app UIState
126-
{ campaigns = [initialWorkerState] -- ugly, fix me
125+
let uiState = UIState {
126+
campaigns = [initialWorkerState] -- ugly, fix me
127127
, workersAlive = nworkers
128128
, status = Uninitialized
129129
, timeStarted = now
@@ -143,7 +143,10 @@ ui vm dict initialCorpus cliSelectedContract = do
143143
, numCodehashes = 0
144144
, lastNewCov = now
145145
, tests
146+
, campaignWidget = emptyWidget -- temporary, will be overwritten below
146147
}
148+
initialCampaignWidget <- runReaderT (campaignStatus uiState) env
149+
void $ app uiState { campaignWidget = initialCampaignWidget }
147150

148151
-- Exited from the UI, stop the workers, not needed anymore
149152
stopWorkers workers
@@ -260,12 +263,12 @@ vtyConfig = do
260263
monitor :: MonadReader Env m => m (App UIState UIEvent Name)
261264
monitor = do
262265
let
263-
drawUI :: Env -> UIState -> [Widget Name]
264-
drawUI conf uiState =
266+
drawUI :: UIState -> [Widget Name]
267+
drawUI uiState =
265268
[ if uiState.displayFetchedDialog
266269
then fetchedDialogWidget uiState
267270
else emptyWidget
268-
, runReader (campaignStatus uiState) conf ]
271+
, uiState.campaignWidget ]
269272

270273
toggleFocus :: UIState -> UIState
271274
toggleFocus state =
@@ -280,9 +283,18 @@ monitor = do
280283
(state.focusedPane == LogPane && not state.displayLogPane)
281284
then toggleFocus state else state
282285

283-
onEvent = \case
284-
AppEvent (CampaignUpdated now tests c') ->
285-
modify' $ \state -> state { campaigns = c', status = Running, now, tests }
286+
focusedViewportScroll :: UIState -> ViewportScroll Name
287+
focusedViewportScroll state = case state.focusedPane of
288+
TestsPane -> viewportScroll TestsViewPort
289+
LogPane -> viewportScroll LogViewPort
290+
291+
onEvent env = \case
292+
AppEvent (CampaignUpdated now tests c') -> do
293+
state <- get
294+
let updatedState = state { campaigns = c', status = Running, now, tests }
295+
newWidget <- liftIO $ runReaderT (campaignStatus updatedState) env
296+
-- purposedly using lazy modify here, so unnecesary widget states don't get computed
297+
modify $ const updatedState { campaignWidget = newWidget }
286298
AppEvent (FetchCacheUpdated contracts slots) ->
287299
modify' $ \state ->
288300
state { fetchedContracts = contracts
@@ -317,10 +329,12 @@ monitor = do
317329
refocusIfNeeded $ state { displayTestsPane = not state.displayTestsPane }
318330
VtyEvent (EvKey direction _) | direction == KPageUp || direction == KPageDown -> do
319331
state <- get
320-
let vp = case state.focusedPane of
321-
TestsPane -> viewportScroll TestsViewPort
322-
LogPane -> viewportScroll LogViewPort
323-
vScrollBy vp (if direction == KPageDown then 10 else -10)
332+
let vp = focusedViewportScroll state
333+
vScrollPage vp (if direction == KPageDown then Down else Up)
334+
VtyEvent (EvKey direction _) | direction == KUp || direction == KDown -> do
335+
state <- get
336+
let vp = focusedViewportScroll state
337+
vScrollBy vp (if direction == KDown then 1 else -1)
324338
VtyEvent (EvKey k []) | k == KChar '\t' || k == KBackTab ->
325339
-- just two panes, so both keybindings just toggle the active one
326340
modify' toggleFocus
@@ -350,9 +364,9 @@ monitor = do
350364
_ -> pure ()
351365

352366
env <- ask
353-
pure $ App { appDraw = drawUI env
367+
pure $ App { appDraw = drawUI
354368
, appStartEvent = pure ()
355-
, appHandleEvent = onEvent
369+
, appHandleEvent = onEvent env
356370
, appAttrMap = const attrs
357371
, appChooseCursor = neverShowCursor
358372
}

lib/Echidna/UI/Report.hs

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module Echidna.UI.Report where
33
import Control.Monad (forM)
44
import Control.Monad.Reader (MonadReader, MonadIO (liftIO), asks, ask)
55
import Control.Monad.ST (RealWorld)
6-
import Data.IORef (readIORef)
6+
import Data.IORef (readIORef, atomicModifyIORef')
77
import Data.List (intercalate, nub, sortOn)
88
import Data.Map (toList)
99
import Data.Map qualified as Map
@@ -15,7 +15,8 @@ import Optics
1515

1616
import Echidna.ABI (GenDict(..), encodeSig)
1717
import Echidna.Pretty (ppTxCall)
18-
import Echidna.SourceMapping (findSrcByMetadata)
18+
import Echidna.SourceMapping (findSrcByMetadata, lookupCodehash)
19+
import Echidna.Symbolic (forceWord)
1920
import Echidna.Types (Gas)
2021
import Echidna.Types.Campaign
2122
import Echidna.Types.Config
@@ -27,17 +28,17 @@ import Echidna.Utility (timePrefix)
2728

2829
import EVM.Format (showTraceTree, contractNamePart)
2930
import EVM.Solidity (SolcContract(..))
30-
import EVM.Types (W256, VM(labels), VMType(Concrete), Addr, Expr (LitAddr))
31+
import EVM.Types (W256, VM(labels), VMType(Concrete), Addr, Expr (LitAddr), Contract(..))
3132

32-
ppLogLine :: MonadReader Env m => VM Concrete RealWorld -> (LocalTime, CampaignEvent) -> m String
33+
ppLogLine :: (MonadReader Env m, MonadIO m) => VM Concrete RealWorld -> (LocalTime, CampaignEvent) -> m String
3334
ppLogLine vm (time, event@(WorkerEvent workerId FuzzWorker _)) =
3435
((timePrefix time <> "[Worker " <> show workerId <> "] ") <>) <$> ppCampaignEventLog vm event
3536
ppLogLine vm (time, event@(WorkerEvent workerId SymbolicWorker _)) =
3637
((timePrefix time <> "[Worker " <> show workerId <> ", symbolic] ") <>) <$> ppCampaignEventLog vm event
3738
ppLogLine vm (time, event) =
3839
((timePrefix time <> " ") <>) <$> ppCampaignEventLog vm event
3940

40-
ppCampaignEventLog :: MonadReader Env m => VM Concrete RealWorld -> CampaignEvent -> m String
41+
ppCampaignEventLog :: (MonadReader Env m, MonadIO m) => VM Concrete RealWorld -> CampaignEvent -> m String
4142
ppCampaignEventLog vm ev = (ppCampaignEvent ev <>) <$> ppTxIfHas where
4243
ppTxIfHas = case ev of
4344
(WorkerEvent _ _ (TestFalsified test)) -> ("\n Call sequence:\n" <>) . unlines <$> mapM (ppTx vm $ length (nub $ (.src) <$> test.reproducer) /= 1) test.reproducer
@@ -68,7 +69,7 @@ ppCampaign vm workerStates = do
6869

6970
-- | Given rules for pretty-printing associated addresses, and whether to print
7071
-- them, pretty-print a 'Transaction'.
71-
ppTx :: MonadReader Env m => VM Concrete RealWorld -> Bool -> Tx -> m String
72+
ppTx :: (MonadReader Env m, MonadIO m) => VM Concrete RealWorld -> Bool -> Tx -> m String
7273
ppTx _ _ Tx { call = NoCall, delay } =
7374
pure $ "*wait*" <> ppDelay delay
7475
ppTx vm printName tx = do
@@ -92,16 +93,30 @@ ppTx vm printName tx = do
9293
Nothing -> ""
9394
Just l -> " «" <> T.unpack l <> "»"
9495

95-
contractNameForAddr :: MonadReader Env m => VM Concrete RealWorld -> Addr -> m Text
96+
contractNameForAddr :: (MonadReader Env m, MonadIO m) => VM Concrete RealWorld -> Addr -> m Text
9697
contractNameForAddr vm addr = do
97-
dapp <- asks (.dapp)
98-
maybeName <- case Map.lookup (LitAddr addr) (vm ^. #env % #contracts) of
99-
Just contract ->
100-
case findSrcByMetadata contract dapp of
101-
Just solcContract -> pure $ Just $ contractNamePart solcContract.contractName
102-
Nothing -> pure Nothing
103-
Nothing -> pure Nothing
104-
pure $ fromMaybe (T.pack $ show addr) maybeName
98+
case Map.lookup (LitAddr addr) (vm ^. #env % #contracts) of
99+
Just contract -> do
100+
-- Figure out contract compile-time codehash
101+
codehashMap <- asks (.codehashMap)
102+
dapp <- asks (.dapp)
103+
let codehash = forceWord contract.codehash
104+
compileTimeCodehash <- liftIO $ lookupCodehash codehashMap codehash contract dapp
105+
-- See if we know the name
106+
cache <- asks (.contractNameCache)
107+
nameMap <- liftIO $ readIORef cache
108+
case Map.lookup compileTimeCodehash nameMap of
109+
Just name -> pure name
110+
Nothing -> do
111+
-- Cache miss, compute and store the name
112+
let maybeName = case findSrcByMetadata contract dapp of
113+
Just solcContract -> Just $ contractNamePart solcContract.contractName
114+
Nothing -> Nothing
115+
finalName = fromMaybe (T.pack $ show addr) maybeName
116+
-- Store in cache using compile-time codehash as key
117+
liftIO $ atomicModifyIORef' cache $ \m -> (Map.insert compileTimeCodehash finalName m, ())
118+
pure finalName
119+
Nothing -> pure $ T.pack $ show addr
105120

106121
ppDelay :: (W256, W256) -> [Char]
107122
ppDelay (time, block) =
@@ -123,14 +138,14 @@ ppCorpus = do
123138
pure $ "Corpus size: " <> show (corpusSize corpus)
124139

125140
-- | Pretty-print the gas usage information a 'Campaign' has obtained.
126-
ppGasInfo :: MonadReader Env m => VM Concrete RealWorld -> [WorkerState] -> m String
141+
ppGasInfo :: (MonadReader Env m, MonadIO m) => VM Concrete RealWorld -> [WorkerState] -> m String
127142
ppGasInfo vm workerStates = do
128143
let gasInfo = Map.unionsWith max ((.gasInfo) <$> workerStates)
129144
items <- mapM (ppGasOne vm) $ sortOn (\(_, (n, _)) -> n) $ toList gasInfo
130145
pure $ intercalate "" items
131146

132147
-- | Pretty-print the gas usage for a function.
133-
ppGasOne :: MonadReader Env m => VM Concrete RealWorld -> (Text, (Gas, [Tx])) -> m String
148+
ppGasOne :: (MonadReader Env m, MonadIO m) => VM Concrete RealWorld -> (Text, (Gas, [Tx])) -> m String
134149
ppGasOne _ ("", _) = pure ""
135150
ppGasOne vm (func, (gas, txs)) = do
136151
let header = "\n" <> unpack func <> " used a maximum of " <> show gas <> " gas\n"
@@ -139,7 +154,7 @@ ppGasOne vm (func, (gas, txs)) = do
139154
pure $ header <> unlines ((" " <>) <$> prettyTxs)
140155

141156
-- | Pretty-print the status of a solved test.
142-
ppFail :: MonadReader Env m => Maybe (Int, Int) -> VM Concrete RealWorld -> [Tx] -> m String
157+
ppFail :: (MonadReader Env m, MonadIO m) => Maybe (Int, Int) -> VM Concrete RealWorld -> [Tx] -> m String
143158
ppFail _ _ [] = pure "failed with no transactions made ⁉️ "
144159
ppFail b vm xs = do
145160
let status = case b of
@@ -152,7 +167,7 @@ ppFail b vm xs = do
152167
<> "Traces: \n" <> T.unpack (showTraceTree dappInfo vm)
153168

154169
-- | Pretty-print the status of a solved test.
155-
ppFailWithTraces :: MonadReader Env m => Maybe (Int, Int) -> VM Concrete RealWorld -> [(Tx, VM Concrete RealWorld)] -> m String
170+
ppFailWithTraces :: (MonadReader Env m, MonadIO m) => Maybe (Int, Int) -> VM Concrete RealWorld -> [(Tx, VM Concrete RealWorld)] -> m String
156171
ppFailWithTraces _ _ [] = pure "failed with no transactions made ⁉️ "
157172
ppFailWithTraces b finalVM results = do
158173
dappInfo <- asks (.dapp)
@@ -170,7 +185,7 @@ ppFailWithTraces b finalVM results = do
170185

171186
-- | Pretty-print the status of a test.
172187

173-
ppTS :: MonadReader Env m => TestState -> VM Concrete RealWorld -> [Tx] -> m String
188+
ppTS :: (MonadReader Env m, MonadIO m) => TestState -> VM Concrete RealWorld -> [Tx] -> m String
174189
ppTS (Failed e) _ _ = pure $ "could not evaluate ☣\n " <> show e
175190
ppTS Solved vm l = ppFail Nothing vm l
176191
ppTS Passed _ _ = pure " passed! 🎉"
@@ -180,7 +195,7 @@ ppTS (Large n) vm l = do
180195
m <- asks (.cfg.campaignConf.shrinkLimit)
181196
ppFail (if n < m then Just (n, m) else Nothing) vm l
182197

183-
ppOPT :: MonadReader Env m => TestState -> VM Concrete RealWorld -> [Tx] -> m String
198+
ppOPT :: (MonadReader Env m, MonadIO m) => TestState -> VM Concrete RealWorld -> [Tx] -> m String
184199
ppOPT (Failed e) _ _ = pure $ "could not evaluate ☣\n " <> show e
185200
ppOPT Solved vm l = ppOptimized Nothing vm l
186201
ppOPT Passed _ _ = pure " passed! 🎉"
@@ -190,7 +205,7 @@ ppOPT (Large n) vm l = do
190205
ppOptimized (if n < m then Just (n, m) else Nothing) vm l
191206

192207
-- | Pretty-print the status of an optimized test.
193-
ppOptimized :: MonadReader Env m => Maybe (Int, Int) -> VM Concrete RealWorld -> [Tx] -> m String
208+
ppOptimized :: (MonadReader Env m, MonadIO m) => Maybe (Int, Int) -> VM Concrete RealWorld -> [Tx] -> m String
194209
ppOptimized _ _ [] = pure "Call sequence:\n(no transactions)"
195210
ppOptimized b vm xs = do
196211
let status = case b of
@@ -203,7 +218,7 @@ ppOptimized b vm xs = do
203218
<> "Traces: \n" <> T.unpack (showTraceTree dappInfo vm)
204219

205220
-- | Pretty-print the status of all 'SolTest's in a 'Campaign'.
206-
ppTests :: MonadReader Env m => [EchidnaTest] -> m String
221+
ppTests :: (MonadReader Env m, MonadIO m) => [EchidnaTest] -> m String
207222
ppTests tests = do
208223
unlines . catMaybes <$> mapM pp tests
209224
where

lib/Echidna/UI/Widgets.hs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Brick.Widgets.Border
88
import Brick.Widgets.Center
99
import Brick.Widgets.Dialog qualified as B
1010
import Control.Monad.Reader (MonadReader, asks, ask)
11+
import Control.Monad.IO.Class (MonadIO)
1112
import Control.Monad.ST (RealWorld)
1213
import Data.List (nub, intersperse, sortBy)
1314
import Data.Map (Map)
@@ -60,6 +61,9 @@ data UIState = UIState
6061
, lastNewCov :: LocalTime
6162
-- ^ last timestamp of 'NewCoverage' event
6263

64+
, campaignWidget :: Widget Name
65+
-- ^ Pre-computed widget to avoid IO in drawing
66+
6367
, tests :: [EchidnaTest]
6468
}
6569

@@ -101,7 +105,7 @@ data Name
101105
deriving (Ord, Show, Eq)
102106

103107
-- | Render 'Campaign' progress as a 'Widget'.
104-
campaignStatus :: MonadReader Env m => UIState -> m (Widget Name)
108+
campaignStatus :: (MonadReader Env m, MonadIO m) => UIState -> m (Widget Name)
105109
campaignStatus uiState = do
106110
tests <- testsWidget uiState.tests
107111

@@ -299,7 +303,7 @@ failedFirst :: EchidnaTest -> EchidnaTest -> Ordering
299303
failedFirst t1 _ | didFail t1 = LT
300304
| otherwise = GT
301305

302-
testsWidget :: MonadReader Env m => [EchidnaTest] -> m (Widget Name)
306+
testsWidget :: (MonadReader Env m, MonadIO m) => [EchidnaTest] -> m (Widget Name)
303307
testsWidget tests' =
304308
withClickableVScrollBars SBClick .
305309
withVScrollBars OnRight .
@@ -308,7 +312,7 @@ testsWidget tests' =
308312
foldl (<=>) emptyWidget . intersperse hBorder <$>
309313
traverse testWidget (sortBy failedFirst tests')
310314

311-
testWidget :: MonadReader Env m => EchidnaTest -> m (Widget Name)
315+
testWidget :: (MonadReader Env m, MonadIO m) => EchidnaTest -> m (Widget Name)
312316
testWidget test =
313317
case test.testType of
314318
Exploration -> widget tsWidget "exploration" ""
@@ -325,7 +329,7 @@ testWidget test =
325329
name n = bold $ str (T.unpack n)
326330

327331
tsWidget
328-
:: MonadReader Env m
332+
:: (MonadReader Env m, MonadIO m)
329333
=> TestState
330334
-> EchidnaTest
331335
-> m (Widget Name, Widget Name)
@@ -352,7 +356,7 @@ tracesWidget vm = do
352356
else str "Traces" <+> str ":" <=> txtBreak traces
353357

354358
failWidget
355-
:: MonadReader Env m
359+
:: (MonadReader Env m, MonadIO m)
356360
=> Maybe (Int, Int)
357361
-> EchidnaTest
358362
-> m (Widget Name, Widget Name)
@@ -370,7 +374,7 @@ failWidget b test = do
370374
)
371375

372376
optWidget
373-
:: MonadReader Env m
377+
:: (MonadReader Env m, MonadIO m)
374378
=> TestState
375379
-> EchidnaTest
376380
-> m (Widget Name, Widget Name)
@@ -385,7 +389,7 @@ optWidget (Large n) test = do
385389
maxWidget (if n < m then Just (n,m) else Nothing) test
386390

387391
maxWidget
388-
:: MonadReader Env m
392+
:: (MonadReader Env m, MonadIO m)
389393
=> Maybe (Int, Int)
390394
-> EchidnaTest
391395
-> m (Widget Name, Widget Name)
@@ -411,7 +415,7 @@ shrinkWidget b test =
411415
where
412416
showWorker = maybe "" (\i -> " (worker " <> show i <> ")") test.workerId
413417

414-
seqWidget :: MonadReader Env m => VM Concrete RealWorld -> [Tx] -> m (Widget Name)
418+
seqWidget :: (MonadReader Env m, MonadIO m) => VM Concrete RealWorld -> [Tx] -> m (Widget Name)
415419
seqWidget vm xs = do
416420
ppTxs <- mapM (ppTx vm $ length (nub $ (.src) <$> xs) /= 1) xs
417421
let ordinals = str . printf "%d. " <$> [1 :: Int ..]

0 commit comments

Comments
 (0)