Skip to content

Commit 8fb0ca7

Browse files
committed
feat: parallelize the generator, while keeping it deterministic
1 parent 3e40d9c commit 8fb0ca7

File tree

2 files changed

+34
-9
lines changed

2 files changed

+34
-9
lines changed

testgen-hs/Main.hs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ module Main where
33
import CLI (GenSize (..), NumCases (..), Seed (..))
44
import qualified CLI
55
import qualified Codec.CBOR.Write as C
6+
import qualified Control.Concurrent.Async as Async
7+
import Control.Concurrent.MVar (modifyMVar_, newMVar)
68
import Data.Aeson (FromJSON, ToJSON)
79
import qualified Data.Aeson as J
810
import qualified Data.Aeson.Encode.Pretty as J
@@ -11,6 +13,7 @@ import qualified Data.ByteString.Base16 as B16
1113
import qualified Data.ByteString.Char8 as B8
1214
import qualified Data.ByteString.Lazy as BL
1315
import qualified Data.ByteString.Lazy.Char8 as BL8
16+
import Data.Foldable (foldl')
1417
import Data.Proxy (Proxy (..))
1518
import Data.Text (Text)
1619
import qualified Data.Text as T
@@ -71,17 +74,37 @@ runGenerate (CLI.GenerateOptions maybeSeed genSize numCases command) = do
7174
genSize
7275
numCases
7376

77+
-- | We have to do this in multiple threads, otherwise this generator code is a
78+
-- bottleneck for Rust tests. We still want to deterministically get the same
79+
-- set of test cases for the same seed, but keeping the order is irrelevant,
80+
-- which lets us do less of the costly sync.
7481
writeRandom :: forall a. (Arbitrary a, Show a, G.OurCBOR a) => Proxy a -> Seed -> GenSize -> NumCases -> IO ()
7582
writeRandom _ (Seed seed) (GenSize generatorSize) (NumCases numCases) = do
76-
loop numCases (QC.mkQCGen seed)
83+
let numGreenThreads = 64 -- changing this, changes determinism – how the seed influences the cases
84+
putsLock <- newMVar ()
85+
let chunks =
86+
snd $
87+
foldl'
88+
( \(prevRng, acc) chunk ->
89+
let (rngL, rngR) = System.Random.split prevRng
90+
in (rngL, (chunk, rngR) : acc)
91+
)
92+
(QC.mkQCGen seed, [])
93+
(fairChunks numCases numGreenThreads)
94+
let worker :: Int -> QC.QCGen -> IO ()
95+
worker 0 _ = pure ()
96+
worker n rng1 = do
97+
let (value :: a, rng2) = splittingUnGen QC.arbitrary rng1 generatorSize
98+
testCase :: TestCase a = mkTestCase value
99+
modifyMVar_ putsLock . const . BL8.putStrLn $ J.encode testCase
100+
worker (n - 1) rng2
101+
Async.mapConcurrently_ (uncurry worker) chunks
102+
103+
-- | Split `total` into `n` fair chunks, e.g. `chunks 20 3 == [7,7,6]`.
104+
fairChunks :: Int -> Int -> [Int]
105+
fairChunks total n = replicate remainder (base + 1) ++ replicate (n - remainder) base
77106
where
78-
loop :: Int -> QC.QCGen -> IO ()
79-
loop 0 _ = pure ()
80-
loop n rng1 = do
81-
let (value :: a, rng2) = splittingUnGen QC.arbitrary rng1 generatorSize
82-
testCase :: TestCase a = mkTestCase value
83-
BL8.putStrLn $ J.encode testCase
84-
loop (n - 1) rng2
107+
(base, remainder) = total `divMod` n
85108

86109
-- | For streaming, we need a version of `unGen` that returns the next RNG –
87110
-- purely and deterministically.

testgen-hs/testgen-hs.cabal

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ executable testgen-hs
1212
build-depends:
1313
, aeson
1414
, aeson-pretty
15+
, async
1516
, base >=4.7 && <5
1617
, base16-bytestring
1718
, bytestring
@@ -60,6 +61,7 @@ executable testgen-hs
6061
ghc-options:
6162
-O2 -Werror -Wall -Wcompat -Widentities -Wincomplete-record-updates
6263
-Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints
63-
-Wunused-packages
64+
-Wunused-packages -threaded -rtsopts -O2 -Werror
65+
"-with-rtsopts=-A16m -T -N"
6466

6567
default-language: Haskell2010

0 commit comments

Comments
 (0)