salmon-ops-recipes-0.2.0.0: test/Test/GraphScaleSpec.hs
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{- | A cheap guard on how the graph operations scale: a few of them over the
shapes that share nodes ("Test.GraphFixture"), at a size where an operation
that pays per node answers in well under a second and one that pays per
path never answers at all.
The bounds are deliberately loose (a minute where a few milliseconds to a
second are expected): this suite shares its process and its machine, and
the point is to tell "per node" from "per path", not to time anything. The
measurements themselves are the @graph-profiles@ benchmark and
@resources\/graph-profiles.md@.
'Salmon.Op.Dag.foldDag' (and so declaring a graph in @serve@ and every
command-line entry that folds) paid per path when this was written, which is
why the walks here are given a 'Dag' built by 'dagOf', and why 'dagOf' is
checked against the fold first. The fold is per node now, and guarded by
"Test.DagPerNodeSpec" (a thousand diamonds), not here.
-}
module Test.GraphScaleSpec (tests) where
import Control.Exception (evaluate)
import Control.Monad (forM_)
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import System.Timeout (timeout)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertEqual, assertFailure, testCase)
import qualified Salmon.Actions.Concurrent as Concurrent
import qualified Salmon.Actions.Query as Query
import qualified Salmon.Actions.UpDown as UpDown
import Salmon.Builtin.Extension (Extension (..), evalDeps)
import qualified Salmon.Op.Dag as Dag
import Salmon.Reporter (silent)
import Test.GraphFixture
tests :: TestTree
tests =
testGroup
"graph operations at scale"
[ testCase "dagOf builds what foldDag builds" dagOfMatchesTheFold
, testCase "the outline, a selector and the listed paths of a shared graph are per node" outlineIsPerNode
, testCase "the listed paths of a deep chain do not cost its depth per node" listedPathsOfADeepChain
, testCase "rebuilding and walking a shared graph is per node" walksArePerNode
]
-------------------------------------------------------------------------------
-- | The shapes whose expansion is exponentially larger than the graph.
shared :: [Shape]
shared = [Diamonds 2, Layered 8 2, RandomLayered 8 3]
{- | Two hundred diamonds: six hundred nodes and @2^200@ paths. Enough
that anything following paths cannot finish. (The size was also kept small
for 'Dag.stuck', which was quadratic in depth until it went one pass; see
"Test.DagStuckSpec".)
-}
size :: Int
size = 601
-- | Generous: see the module header.
bound :: Int
bound = 60
-- | Fail, rather than hang the suite, if the action is still running at 'bound'.
within :: String -> IO a -> IO a
within what act = do
outcome <- timeout (bound * 1000000) act
case outcome of
Just x -> pure x
Nothing -> assertFailure (what <> ": still running after " <> show bound <> "s, which is what paying per path looks like")
-------------------------------------------------------------------------------
dagOfMatchesTheFold :: IO ()
dagOfMatchesTheFold =
forM_ ([Chain, Fan, Tree 2, Tree 3, Layered 3 3] <> shared) $ \shape ->
forM_ [1, 2, 7, 13, 25] $ \n -> do
let label = show shape <> " at " <> show n
folded = Dag.foldDag Dag.sameRepresentative (evalDeps (generate shape n 3)) :: Dag.Dag Extension
built = dagOf (generate shape n 3)
assertEqual (label <> ": order") (Dag.dagOrder folded) (Dag.dagOrder built)
assertEqual (label <> ": dependencies") (Dag.dagDependencies folded) (Dag.dagDependencies built)
assertEqual (label <> ": dependants") (fmap Set.fromList (Dag.dagDependants folded)) (fmap Set.fromList (Dag.dagDependants built))
assertEqual (label <> ": nodes") (Map.keysSet (Dag.dagNodes folded)) (Map.keysSet (Dag.dagNodes built))
outlineIsPerNode :: IO ()
outlineIsPerNode =
forM_ shared $ \shape -> do
let label = show shape <> " at " <> show size
expected = fromIntegral (counts shape size).countNodes
cograph = evalDeps (generate shape size 3)
nodes <- within (label <> ": outline") (evaluate (Set.size (Query.outlineRefs (Query.outline cograph))))
assertEqual (label <> ": every node outlined") expected nodes
selected <- within (label <> ": resolveSelectors") (evaluate (Set.size (fst (Query.resolveSelectors cograph ["**"] ["n0/n1"]))))
assertEqual (label <> ": every node but one selected") (expected - 1) selected
listed <- within (label <> ": outlinePaths") (evaluate (Map.foldl' (\k ps -> k + length ps) 0 (Query.outlinePaths Query.pathLimit Query.pathCap (Query.outline cograph))))
assertBool (label <> ": at least one path per node, at most pathLimit") (listed >= expected && listed <= expected * Query.pathLimit)
{- | A chain of twenty thousand nodes: the uncapped listing held one path of
up to twenty thousand segments per node (about 1.2 GB of text, and minutes);
capped at 'Query.pathCap' it is a few hundred characters a node.
-}
listedPathsOfADeepChain :: IO ()
listedPathsOfADeepChain = do
let n = 20000
cograph = evalDeps (generate Chain n 3)
held <- within "chain: outlinePaths" (evaluate (Map.foldl' (\k ps -> k + sum [length p.listedSegments | p <- ps]) 0 (Query.outlinePaths Query.pathLimit Query.pathCap (Query.outline cograph))))
assertBool ("chain: segments held " <> show held <> ", at most a cap's worth per node") (held <= n * Query.pathCap `div` 2)
walksArePerNode :: IO ()
walksArePerNode =
forM_ shared $ \shape -> do
let label = show shape <> " at " <> show size
expected = fromIntegral (counts shape size).countNodes
dag = dagOf (generate shape size 3)
rebuilt <- within (label <> ": fromMagma") (evaluate (Dag.fromMagma (Dag.dagNodes dag) (Dag.dagEdges dag)))
assertEqual (label <> ": every node rebuilt") expected (Map.size (Dag.dagNodes rebuilt))
assertEqual (label <> ": every edge rebuilt") (Dag.dagEdges dag) (Dag.dagEdges rebuilt)
merged <- within (label <> ": mergeDag") (evaluate (Dag.mergeDag Dag.sameRepresentative rebuilt dag))
assertEqual (label <> ": merging a graph onto itself adds nothing") (Dag.dagEdges dag) (Dag.dagEdges merged)
cyclic <- within (label <> ": stuck") (evaluate (Set.size (Dag.stuck Dag.dependenciesOf rebuilt)))
assertEqual (label <> ": nothing on a cycle") 0 cyclic
up <- within (label <> ": upDag") (UpDown.upDag UpDown.alwaysRequired silent rebuilt)
assertBool (label <> ": the sequential bring-up succeeds") up
down <- within (label <> ": downDag") (UpDown.downDag UpDown.alwaysRequired silent rebuilt)
assertBool (label <> ": the sequential teardown succeeds") down
upConcurrent <- within (label <> ": upDagConcurrent") (Concurrent.upDagConcurrent UpDown.alwaysRequired silent Concurrent.noMailboxes Nothing rebuilt)
assertBool (label <> ": the concurrent bring-up succeeds") upConcurrent