swarm/bench/Benchmark.hs

120 lines
3.8 KiB
Haskell
Raw Normal View History

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Main where
import Control.Lens ((&), (.~))
import Control.Monad (replicateM_)
import Control.Monad.Except (runExceptT)
import Control.Monad.State (evalStateT, execStateT)
import Criterion.Main (Benchmark, bench, bgroup, defaultConfig, defaultMainWith, whnfAppIO)
import Criterion.Types (Config (timeLimit))
import Data.Int (Int64)
import Linear.V2 (V2 (V2))
import Swarm.Game.CESK (emptyStore, initMachine)
import Swarm.Game.Robot (Robot, mkRobot)
import Swarm.Game.State (GameState, GameType (ClassicGame), addRobot, creativeMode, initGameState, world)
import Swarm.Game.Step (gameTick)
import Swarm.Game.Terrain (TerrainType (DirtT))
import Swarm.Game.World (newWorld)
import qualified Swarm.Language.Context as Context
import Swarm.Language.Pipeline (ProcessedTerm)
import Swarm.Language.Pipeline.QQ (tmQ)
import Swarm.Language.Syntax (north)
-- | The program of a robot that does nothing.
idleProgram :: ProcessedTerm
idleProgram = [tmQ| {} |]
-- | The program of a robot which waits a random number of ticks, changes its
-- appearence, then waits another random number of ticks, places a tree, and
-- then self-destructs.
treeProgram :: ProcessedTerm
treeProgram =
[tmQ|
Only update robots that are not waiting. (#176) Resolves #20 by introducing an active robots set and a waiting robots queue as discussed in the issue. Growing trees benchmark before change: ``` benchmarking run 1000 game ticks/10 trees time 141.1 ms (137.3 ms .. 146.4 ms) 0.997 R² (0.995 R² .. 1.000 R²) mean 140.1 ms (138.5 ms .. 142.2 ms) std dev 3.807 ms (2.703 ms .. 5.189 ms) benchmarking run 1000 game ticks/20 trees time 298.9 ms (281.7 ms .. 326.0 ms) 0.989 R² (0.972 R² .. 0.998 R²) mean 301.7 ms (295.0 ms .. 311.0 ms) std dev 13.93 ms (10.02 ms .. 19.92 ms) benchmarking run 1000 game ticks/30 trees time 470.9 ms (432.0 ms .. 513.0 ms) 0.991 R² (0.978 R² .. 1.000 R²) mean 450.1 ms (441.9 ms .. 468.4 ms) std dev 18.85 ms (5.941 ms .. 30.21 ms) variance introduced by outliers: 11% (moderately inflated) ``` After change: ``` benchmarking run 1000 game ticks/10 trees time 4.666 ms (4.529 ms .. 4.955 ms) 0.963 R² (0.899 R² .. 0.999 R²) mean 4.447 ms (4.361 ms .. 4.668 ms) std dev 539.2 μs (185.5 μs .. 1.097 ms) variance introduced by outliers: 80% (severely inflated) benchmarking run 1000 game ticks/20 trees time 6.955 ms (6.826 ms .. 7.122 ms) 0.996 R² (0.993 R² .. 0.999 R²) mean 6.855 ms (6.795 ms .. 6.936 ms) std dev 262.8 μs (197.0 μs .. 340.6 μs) variance introduced by outliers: 25% (moderately inflated) benchmarking run 1000 game ticks/30 trees time 10.53 ms (9.738 ms .. 11.47 ms) 0.954 R² (0.914 R² .. 0.988 R²) mean 9.504 ms (9.226 ms .. 9.974 ms) std dev 1.211 ms (809.9 μs .. 1.948 ms) variance introduced by outliers: 76% (severely inflated) ```
2021-10-05 23:54:12 +03:00
{
r <- random 100;
wait (r + 300);
appear "|";
r <- random 100;
wait (r + 300);
place "tree";
selfdestruct
}
|]
-- | The program of a robot that moves forward forever.
moverProgram :: ProcessedTerm
moverProgram =
[tmQ|
let forever : cmd () -> cmd () = \c. c; forever c
in forever move
|]
-- | The program of a robot that moves in circles forever.
circlerProgram :: ProcessedTerm
circlerProgram =
[tmQ|
let forever : cmd () -> cmd () = \c. c; forever c
in forever (
move;
turn east;
move;
turn south;
move;
turn west;
move;
turn north
)
|]
-- | Initializes a robot with program prog at location loc facing north.
initRobot :: ProcessedTerm -> V2 Int64 -> Robot
initRobot prog loc = mkRobot "" north loc (initMachine prog Context.empty emptyStore) []
-- | Creates a GameState with numRobot copies of robot on a blank map, aligned
-- in a row starting at (0,0) and spreading east.
mkGameState :: (V2 Int64 -> Robot) -> Int -> IO GameState
mkGameState robotMaker numRobots = do
let robots = [robotMaker (V2 (fromIntegral x) 0) | x <- [0 .. numRobots -1]]
Right initState <- runExceptT (initGameState (ClassicGame 0))
execStateT
(mapM addRobot robots)
( initState
& creativeMode .~ True
& world .~ newWorld (const (fromEnum DirtT, Nothing))
)
-- | Runs numGameTicks ticks of the game.
runGame :: Int -> GameState -> IO ()
runGame numGameTicks = evalStateT (replicateM_ numGameTicks gameTick)
main :: IO ()
main = do
idlers <- mkGameStates idleProgram [10, 20 .. 40]
trees <- mkGameStates treeProgram [10, 20 .. 40]
circlers <- mkGameStates circlerProgram [10, 20 .. 40]
movers <- mkGameStates moverProgram [10, 20 .. 40]
-- In theory we should force the evaluation of these game states to normal
-- form before running the benchmarks. In practice, the first of the many
-- criterion runs for each of these benchmarks doesn't look like an outlier.
defaultMainWith
(defaultConfig {timeLimit = 10})
[ bgroup
"run 1000 game ticks"
[ bgroup "idlers" (toBenchmarks idlers)
, bgroup "trees" (toBenchmarks trees)
, bgroup "circlers" (toBenchmarks circlers)
, bgroup "movers" (toBenchmarks movers)
]
]
where
mkGameStates :: ProcessedTerm -> [Int] -> IO [(Int, GameState)]
mkGameStates prog sizes = zip sizes <$> mapM (mkGameState (initRobot prog)) sizes
toBenchmarks :: [(Int, GameState)] -> [Benchmark]
toBenchmarks gameStates =
[ bench (show n) $ whnfAppIO (runGame 1000) gameState
| (n, gameState) <- gameStates
]