Skip to content

Commit ad811f2

Browse files
committed
Add comprehensive benchmarks for lookupCoin and valueContains
Implements extensive benchmarking infrastructure for the new built-in functions: - Tests various Value sizes from tiny (1 entry) to huge (30KB) - Uses pseudo-random test data generation with StdGen for reproducibility - Covers realistic Cardano constraints (28-byte policy IDs, variable token names) - Tests pathological cases (empty keys, huge keys, edge case sizes) - Memory budget-conscious generation prevents DoS during benchmarking - Generates data for both early-hit and late-hit scenarios The benchmark suite enables accurate performance measurement to determine optimal cost model coefficients for real-world usage patterns.
1 parent db9dea9 commit ad811f2

File tree

3 files changed

+338
-0
lines changed

3 files changed

+338
-0
lines changed
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
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

plutus-core/cost-model/budgeting-bench/Main.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import Benchmarks.Pairs qualified
1717
import Benchmarks.Strings qualified
1818
import Benchmarks.Tracing qualified
1919
import Benchmarks.Unit qualified
20+
import Benchmarks.Values qualified
2021

2122
import Criterion.Main
2223
import Criterion.Types as C
@@ -60,6 +61,7 @@ main = do
6061
<> Benchmarks.Strings.makeBenchmarks gen
6162
<> Benchmarks.Tracing.makeBenchmarks gen
6263
<> Benchmarks.Unit.makeBenchmarks gen
64+
<> Benchmarks.Values.makeBenchmarks gen
6365

6466
{- Run the nop benchmarks with a large time limit (30 seconds) in an attempt to
6567
get accurate results. -}

plutus-core/plutus-core.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,7 @@ executable cost-model-budgeting-bench
935935
Benchmarks.Strings
936936
Benchmarks.Tracing
937937
Benchmarks.Unit
938+
Benchmarks.Values
938939
Common
939940
CriterionExtensions
940941
Generators

0 commit comments

Comments
 (0)