|
| 1 | +{-# LANGUAGE BlockArguments #-} |
| 2 | +{-# LANGUAGE ImportQualifiedPost #-} |
| 3 | +{-# LANGUAGE NumericUnderscores #-} |
| 4 | + |
| 5 | +module Benchmarks.Values (makeBenchmarks) where |
| 6 | + |
| 7 | +import Prelude |
| 8 | + |
| 9 | +import Common |
| 10 | +import Control.Monad (replicateM) |
| 11 | +import Criterion.Main (Benchmark) |
| 12 | +import Data.ByteString (ByteString) |
| 13 | +import Data.ByteString qualified as BS |
| 14 | +import PlutusCore (DefaultFun (LookupCoin, ValueContains)) |
| 15 | +import PlutusCore.Value (Value) |
| 16 | +import PlutusCore.Value qualified as Value |
| 17 | +import System.Random.Stateful (StatefulGen, StdGen, runStateGen_, uniformByteStringM) |
| 18 | + |
| 19 | +-------------------------------------------------------------------------------- |
| 20 | +-- Benchmarks ------------------------------------------------------------------ |
| 21 | + |
| 22 | +makeBenchmarks :: StdGen -> [Benchmark] |
| 23 | +makeBenchmarks gen = |
| 24 | + [ createThreeTermBuiltinBenchElementwise |
| 25 | + LookupCoin -- the builtin fun |
| 26 | + [] -- no type arguments needed (monomorphic builtin) |
| 27 | + (lookupCoinArgs gen) -- the argument combos to generate benchmarks for |
| 28 | + , createTwoTermBuiltinBenchElementwise |
| 29 | + ValueContains -- the builtin fun |
| 30 | + [] -- no type arguments needed (monomorphic builtin) |
| 31 | + (valueContainsArgs gen) -- the argument combos to generate benchmarks for |
| 32 | + ] |
| 33 | + |
| 34 | +lookupCoinArgs :: StdGen -> [(ByteString, ByteString, Value)] |
| 35 | +lookupCoinArgs gen = runStateGen_ gen $ \g -> do |
| 36 | + let |
| 37 | + -- Key sizes to test (ByteString lengths) |
| 38 | + keySizes = [Size_0, Size_1, Size_30, Size_100, Size_1K, Size_10K, Size_20K] |
| 39 | + |
| 40 | + sequence $ |
| 41 | + concat |
| 42 | + [ -- 1. Standard keys with varying value structures |
| 43 | + [ generateLookupTest g Size_30 Size_30 numPolicies tokensPerPolicy |
| 44 | + | numPolicies <- [Size_1, Size_10, Size_100, Size_1K] |
| 45 | + , tokensPerPolicy <- [Size_1, Size_10, Size_100] |
| 46 | + ] |
| 47 | + , -- 2. Key size impact tests (fixed structure, varying key sizes) |
| 48 | + [ generateLookupTest g pSize tSize Size_100 Size_10 |
| 49 | + | pSize <- keySizes |
| 50 | + , tSize <- [Size_0, Size_30, Size_1K, Size_20K] |
| 51 | + ] |
| 52 | + , -- 3. Budget-constrained tests (at 30KB limit) |
| 53 | + [ generateBudgetTest g pSize tSize Size_30K |
| 54 | + | (pSize, tSize) <- |
| 55 | + [ (Size_20K, Size_1) -- Huge policy, tiny token |
| 56 | + , (Size_1, Size_20K) -- Tiny policy, huge token |
| 57 | + , (Size_10K, Size_10K) -- Both large |
| 58 | + , (Size_1, Size_1) -- Both tiny (max entries) |
| 59 | + , (Size_0, Size_0) -- Empty keys (pathological) |
| 60 | + ] |
| 61 | + ] |
| 62 | + , -- 4. Intermediate budget tests (at 10KB limit) |
| 63 | + [ generateBudgetTest g pSize tSize Size_10K |
| 64 | + | (pSize, tSize) <- |
| 65 | + [ (Size_100, Size_100) |
| 66 | + , (Size_1K, Size_30) |
| 67 | + , (Size_30, Size_1K) |
| 68 | + , (Size_3K, Size_3K) |
| 69 | + ] |
| 70 | + ] |
| 71 | + , -- 5. Structure variation tests (same total entries, different layouts) |
| 72 | + [ generateStructureTest g numPolicies tokensPerPolicy Size_30 Size_30 |
| 73 | + | (numPolicies, tokensPerPolicy) <- |
| 74 | + [ (Size_1K, Size_1) -- 1000 policies x 1 token |
| 75 | + , (Size_100, Size_10) -- 100 policies x 10 tokens |
| 76 | + , (Size_10, Size_100) -- 10 policies x 100 tokens |
| 77 | + , (Size_1, Size_1K) -- 1 policy x 1000 tokens |
| 78 | + ] |
| 79 | + ] |
| 80 | + ] |
| 81 | + |
| 82 | +-- | Generate lookup test with specified parameters |
| 83 | +generateLookupTest |
| 84 | + :: (StatefulGen g m) |
| 85 | + => g |
| 86 | + -> Size -- Policy ID byte size |
| 87 | + -> Size -- Token name byte size |
| 88 | + -> Size -- Number of policies |
| 89 | + -> Size -- Tokens per policy |
| 90 | + -> m (ByteString, ByteString, Value) |
| 91 | +generateLookupTest g pIdSize tNameSize numPolicies tokensPerPolicy = do |
| 92 | + value <- generateConstrainedValue numPolicies tokensPerPolicy pIdSize tNameSize g |
| 93 | + -- Generate lookup keys (may or may not exist in value) |
| 94 | + searchPolicyId <- generatePolicyId pIdSize g |
| 95 | + searchTokenName <- generateTokenName tNameSize g |
| 96 | + pure (searchPolicyId, searchTokenName, value) |
| 97 | + |
| 98 | +-- | Generate budget-constrained test |
| 99 | +generateBudgetTest |
| 100 | + :: (StatefulGen g m) |
| 101 | + => g |
| 102 | + -> Size -- Policy ID byte size |
| 103 | + -> Size -- Token name byte size |
| 104 | + -> Size -- Total budget |
| 105 | + -> m (ByteString, ByteString, Value) |
| 106 | +generateBudgetTest g pIdSize tNameSize budget = do |
| 107 | + value <- generateValueWithBudget pIdSize tNameSize budget g |
| 108 | + searchPolicyId <- generatePolicyId pIdSize g |
| 109 | + searchTokenName <- generateTokenName tNameSize g |
| 110 | + pure (searchPolicyId, searchTokenName, value) |
| 111 | + |
| 112 | +-- | Generate structure test |
| 113 | +generateStructureTest |
| 114 | + :: (StatefulGen g m) |
| 115 | + => g |
| 116 | + -> Size -- Number of policies |
| 117 | + -> Size -- Tokens per policy |
| 118 | + -> Size -- Policy ID byte size |
| 119 | + -> Size -- Token name byte size |
| 120 | + -> m (ByteString, ByteString, Value) |
| 121 | +generateStructureTest g numPolicies tokensPerPolicy pIdSize tNameSize = do |
| 122 | + value <- generateConstrainedValue numPolicies tokensPerPolicy pIdSize tNameSize g |
| 123 | + searchPolicyId <- generatePolicyId pIdSize g |
| 124 | + searchTokenName <- generateTokenName tNameSize g |
| 125 | + pure (searchPolicyId, searchTokenName, value) |
| 126 | + |
| 127 | +valueContainsArgs :: StdGen -> [(Value, Value)] |
| 128 | +valueContainsArgs gen = runStateGen_ gen $ \g -> do |
| 129 | + let |
| 130 | + keySizes = [Size_0, Size_30, Size_1K, Size_10K] |
| 131 | + valueSizes = [Size_1, Size_10, Size_100, Size_1K] |
| 132 | + |
| 133 | + sequence $ |
| 134 | + concat |
| 135 | + [ -- Standard key tests with varying value sizes |
| 136 | + [ generateContainsTest g containerSize containedSize Size_30 |
| 137 | + | containerSize <- valueSizes |
| 138 | + , containedSize <- valueSizes |
| 139 | + , sizeToInt containedSize <= sizeToInt containerSize |
| 140 | + ] |
| 141 | + , -- Key size impact tests |
| 142 | + [ generateContainsTest g Size_100 Size_10 keySize |
| 143 | + | keySize <- keySizes |
| 144 | + ] |
| 145 | + , -- Budget-constrained tests |
| 146 | + [ generateContainsBudgetTest g Size_30K keySize |
| 147 | + | keySize <- [Size_0, Size_30, Size_3K, Size_20K] |
| 148 | + ] |
| 149 | + , -- Edge cases |
| 150 | + [ generateEmptyContainedTest g containerSize Size_30 |
| 151 | + | containerSize <- [Size_10, Size_100, Size_1K] |
| 152 | + ] |
| 153 | + ] |
| 154 | + |
| 155 | +-- | Generate valueContains test with specified parameters |
| 156 | +generateContainsTest |
| 157 | + :: (StatefulGen g m) |
| 158 | + => g |
| 159 | + -> Size -- Container value size |
| 160 | + -> Size -- Contained value size |
| 161 | + -> Size -- Key byte size (for both policy and token) |
| 162 | + -> m (Value, Value) |
| 163 | +generateContainsTest g containerSize containedSize keySize = do |
| 164 | + -- Generate container value |
| 165 | + container <- |
| 166 | + generateConstrainedValue |
| 167 | + containerSize |
| 168 | + Size_10 |
| 169 | + keySize |
| 170 | + keySize |
| 171 | + g |
| 172 | + |
| 173 | + -- Generate contained as subset of container (for true contains relationship) |
| 174 | + let containerList = Value.toList container |
| 175 | + containedEntries = take (sizeToInt containedSize) containerList |
| 176 | + |
| 177 | + let contained = |
| 178 | + Value.fromList $ |
| 179 | + [ (pId, take (sizeToInt containedSize `div` max 1 (length containerList)) tokens) |
| 180 | + | (pId, tokens) <- containedEntries |
| 181 | + ] |
| 182 | + |
| 183 | + pure (container, contained) |
| 184 | + |
| 185 | +-- | Generate budget-constrained contains test |
| 186 | +generateContainsBudgetTest |
| 187 | + :: (StatefulGen g m) |
| 188 | + => g |
| 189 | + -> Size -- Total budget |
| 190 | + -> Size -- Key size |
| 191 | + -> m (Value, Value) |
| 192 | +generateContainsBudgetTest g budget keySize = do |
| 193 | + container <- generateValueWithBudget keySize keySize budget g |
| 194 | + -- Generate smaller contained value (subset) |
| 195 | + let containerList = Value.toList container |
| 196 | + containedEntries = take (length containerList `div` 2) containerList |
| 197 | + pure (container, Value.fromList containedEntries) |
| 198 | + |
| 199 | +-- | Generate test with empty contained value |
| 200 | +generateEmptyContainedTest |
| 201 | + :: (StatefulGen g m) |
| 202 | + => g |
| 203 | + -> Size -- Container size |
| 204 | + -> Size -- Key size |
| 205 | + -> m (Value, Value) |
| 206 | +generateEmptyContainedTest g containerSize keySize = do |
| 207 | + container <- generateConstrainedValue containerSize Size_10 keySize keySize g |
| 208 | + pure (container, Value.empty) |
| 209 | + |
| 210 | +-------------------------------------------------------------------------------- |
| 211 | +-- Generators for LookupCoin benchmarks ---------------------------------------- |
| 212 | + |
| 213 | +-- | Unified size type for comprehensive coverage within memory constraints |
| 214 | +data Size |
| 215 | + = Size_0 -- 0 (empty strings, empty collections) |
| 216 | + | Size_1 -- 1 |
| 217 | + | Size_10 -- 10 |
| 218 | + | Size_30 -- 30 (standard 28-32 byte range) |
| 219 | + | Size_100 -- 100 |
| 220 | + | Size_300 -- 300 |
| 221 | + | Size_1K -- 1,000 |
| 222 | + | Size_3K -- 3,000 |
| 223 | + | Size_10K -- 10,000 |
| 224 | + | Size_20K -- 20,000 |
| 225 | + | Size_30K -- 30,000 (total memory budget) |
| 226 | + |
| 227 | +-- | Convert size to exact integer value |
| 228 | +sizeToInt :: Size -> Int |
| 229 | +sizeToInt Size_0 = 0 |
| 230 | +sizeToInt Size_1 = 1 |
| 231 | +sizeToInt Size_10 = 10 |
| 232 | +sizeToInt Size_30 = 30 |
| 233 | +sizeToInt Size_100 = 100 |
| 234 | +sizeToInt Size_300 = 300 |
| 235 | +sizeToInt Size_1K = 1_000 |
| 236 | +sizeToInt Size_3K = 3_000 |
| 237 | +sizeToInt Size_10K = 10_000 |
| 238 | +sizeToInt Size_20K = 20_000 |
| 239 | +sizeToInt Size_30K = 30_000 |
| 240 | + |
| 241 | +-- | Generate ByteString of specified size |
| 242 | +generateByteString :: (StatefulGen g m) => Size -> g -> m ByteString |
| 243 | +generateByteString size g = |
| 244 | + let len = sizeToInt size |
| 245 | + in if len == 0 |
| 246 | + then pure BS.empty |
| 247 | + else uniformByteStringM len g |
| 248 | + |
| 249 | +-- | Generate policy ID of specified size |
| 250 | +generatePolicyId :: (StatefulGen g m) => Size -> g -> m ByteString |
| 251 | +generatePolicyId = generateByteString |
| 252 | + |
| 253 | +-- | Generate token name of specified size |
| 254 | +generateTokenName :: (StatefulGen g m) => Size -> g -> m ByteString |
| 255 | +generateTokenName = generateByteString |
| 256 | + |
| 257 | +-- | Generate constrained Value with total size budget |
| 258 | +generateConstrainedValue |
| 259 | + :: (StatefulGen g m) |
| 260 | + => Size -- Number of policies |
| 261 | + -> Size -- Number of tokens per policy |
| 262 | + -> Size -- Policy ID byte length |
| 263 | + -> Size -- Token name byte length |
| 264 | + -> g |
| 265 | + -> m Value |
| 266 | +generateConstrainedValue numPoliciesSize tokensPerPolicySize pIdSize tNameSize g = do |
| 267 | + policyIds <- -- Generate policy IDs of specified size |
| 268 | + replicateM (sizeToInt numPoliciesSize) (generatePolicyId pIdSize g) |
| 269 | + |
| 270 | + tokenNames <- -- Generate token names of specified size |
| 271 | + replicateM (sizeToInt tokensPerPolicySize) (generateTokenName tNameSize g) |
| 272 | + |
| 273 | + -- Generate positive quantities (1 to 1000000) |
| 274 | + let quantity :: Int -> Int -> Integer |
| 275 | + quantity policyIdx tokenIdx = |
| 276 | + fromIntegral (1 + (policyIdx * 1_000 + tokenIdx) `mod` 1_000_000) |
| 277 | + |
| 278 | + nestedMap :: [(ByteString, [(ByteString, Integer)])] |
| 279 | + nestedMap = |
| 280 | + [ ( policyId |
| 281 | + , [ (tokenName, quantity policyIdx tokenIdx) |
| 282 | + | (tokenIdx, tokenName) <- zip [0 ..] tokenNames |
| 283 | + ] |
| 284 | + ) |
| 285 | + | (policyIdx, policyId) <- zip [0 ..] policyIds |
| 286 | + ] |
| 287 | + pure $ Value.fromList nestedMap |
| 288 | + |
| 289 | +-- | Generate Value within total size budget |
| 290 | +generateValueWithBudget |
| 291 | + :: (StatefulGen g m) |
| 292 | + => Size -- Policy ID byte length |
| 293 | + -> Size -- Token name byte length |
| 294 | + -> Size -- Target total size budget |
| 295 | + -> g |
| 296 | + -> m Value |
| 297 | +generateValueWithBudget pIdSize tNameSize totalBudget g = do |
| 298 | + let |
| 299 | + pIdBytes = sizeToInt pIdSize |
| 300 | + tNameBytes = sizeToInt tNameSize |
| 301 | + budget = sizeToInt totalBudget |
| 302 | + overhead = 8 -- bytes per amount |
| 303 | + |
| 304 | + -- Calculate maximum possible entries |
| 305 | + bytesPerEntry = pIdBytes + tNameBytes + overhead |
| 306 | + maxEntries = |
| 307 | + if bytesPerEntry > 0 |
| 308 | + then min (budget `div` bytesPerEntry) budget |
| 309 | + else budget -- Handle Size_0 case |
| 310 | + |
| 311 | + -- Simple distribution: try to balance policies and tokens |
| 312 | + numPolicies = max 1 (floor (sqrt (fromIntegral maxEntries :: Double))) |
| 313 | + tokensPerPolicy = if numPolicies > 0 then maxEntries `div` numPolicies else 0 |
| 314 | + |
| 315 | + generateConstrainedValue |
| 316 | + (intToSize numPolicies) |
| 317 | + (intToSize tokensPerPolicy) |
| 318 | + pIdSize |
| 319 | + tNameSize |
| 320 | + g |
| 321 | + |
| 322 | +-- | Convert integer back to nearest Size (helper function) |
| 323 | +intToSize :: Int -> Size |
| 324 | +intToSize n |
| 325 | + | n <= 0 = Size_0 |
| 326 | + | n <= 1 = Size_1 |
| 327 | + | n <= 10 = Size_10 |
| 328 | + | n <= 30 = Size_30 |
| 329 | + | n <= 100 = Size_100 |
| 330 | + | n <= 300 = Size_300 |
| 331 | + | n <= 1000 = Size_1K |
| 332 | + | n <= 3000 = Size_3K |
| 333 | + | n <= 10_000 = Size_10K |
| 334 | + | n <= 20_000 = Size_20K |
| 335 | + | otherwise = Size_30K |
0 commit comments