From c975a6e70e2c04f1ab9ad70273c7b26ebac66c8d Mon Sep 17 00:00:00 2001 From: Yuriy Lazaryev Date: Wed, 29 Jul 2026 19:51:29 +0200 Subject: [PATCH 1/4] feat(cost-model): add cost model parameters for multiIndexArray Adds the CPU and memory parameters for multiIndexArray to the model record, the JSON variants and the ledger API parameter lists. The CPU model is fitted through the ends of the benchmarked range rather than by least squares. Per-index time rises across that range, and least squares then returns a negative intercept, which adjustModel clamps away together with the fixed cost of a call. --- .../create-cost-model/BuiltinMemoryModels.hs | 4 ++ .../CreateBuiltinCostModel.hs | 2 + .../cost-model/data/builtinCostModelA.json | 16 ++++++++ .../cost-model/data/builtinCostModelB.json | 16 ++++++++ .../cost-model/data/builtinCostModelC.json | 16 ++++++++ .../cost-model/data/builtinCostModelD.json | 16 ++++++++ .../cost-model/data/builtinCostModelE.json | 16 ++++++++ plutus-core/cost-model/data/models.R | 38 ++++++++++++++++++- plutus-core/cost-model/test/TestCostModels.hs | 2 + .../Evaluation/Machine/BuiltinCostModel.hs | 2 + .../Evaluation/Machine/ExBudgetingDefaults.hs | 2 + .../src/PlutusLedgerApi/V1/ParamName.hs | 5 +++ .../src/PlutusLedgerApi/V2/ParamName.hs | 5 +++ .../src/PlutusLedgerApi/V3/ParamName.hs | 5 +++ .../test/Spec/CostModelParams.hs | 6 +-- .../test/Spec/Data/CostModelParams.hs | 6 +-- 16 files changed, 150 insertions(+), 7 deletions(-) diff --git a/plutus-core/cost-model/create-cost-model/BuiltinMemoryModels.hs b/plutus-core/cost-model/create-cost-model/BuiltinMemoryModels.hs index 9ebaa999ad6..4bd1db3e5fa 100644 --- a/plutus-core/cost-model/create-cost-model/BuiltinMemoryModels.hs +++ b/plutus-core/cost-model/create-cost-model/BuiltinMemoryModels.hs @@ -378,6 +378,10 @@ builtinMemoryModels = paramUnionValue = Id $ ModelTwoArgumentsAddedSizes $ OneVariableLinearFunction 24 21 , -- See Note [Memory model for Value builtins] paramScaleValue = Id $ ModelTwoArgumentsLinearInY $ OneVariableLinearFunction 12 21 + , -- The result is a list of length y (the index list) whose elements are shared with + -- the array; only the spine is new. The nonzero intercept keeps the charge positive + -- for the empty index list. + paramMultiIndexArray = Id $ ModelTwoArgumentsLinearInY $ OneVariableLinearFunction 4 1 } where identityFunction = OneVariableLinearFunction 0 1 diff --git a/plutus-core/cost-model/create-cost-model/CreateBuiltinCostModel.hs b/plutus-core/cost-model/create-cost-model/CreateBuiltinCostModel.hs index 041b262affc..3cdb4c13cd9 100644 --- a/plutus-core/cost-model/create-cost-model/CreateBuiltinCostModel.hs +++ b/plutus-core/cost-model/create-cost-model/CreateBuiltinCostModel.hs @@ -153,6 +153,7 @@ builtinCostModelNames = , paramInsertCoin = "insertCoinModel" , paramUnionValue = "unionValueModel" , paramScaleValue = "scaleValueModel" + , paramMultiIndexArray = "multiIndexArrayModel" } {-| Loads the models from R. @@ -308,6 +309,7 @@ createBuiltinCostModel bmfile rfile = do paramInsertCoin <- getParams readCF4 paramInsertCoin paramUnionValue <- getParams readCF2 paramUnionValue paramScaleValue <- getParams readCF2 paramScaleValue + paramMultiIndexArray <- getParams readCF2 paramMultiIndexArray pure $ BuiltinCostModelBase {..} diff --git a/plutus-core/cost-model/data/builtinCostModelA.json b/plutus-core/cost-model/data/builtinCostModelA.json index 6448e7ec150..22511b1c9d0 100644 --- a/plutus-core/cost-model/data/builtinCostModelA.json +++ b/plutus-core/cost-model/data/builtinCostModelA.json @@ -1321,5 +1321,21 @@ }, "type": "linear_in_y" } + }, + "multiIndexArray": { + "cpu": { + "arguments": { + "intercept": 268678, + "slope": 42403 + }, + "type": "linear_in_y" + }, + "memory": { + "arguments": { + "intercept": 4, + "slope": 1 + }, + "type": "linear_in_y" + } } } diff --git a/plutus-core/cost-model/data/builtinCostModelB.json b/plutus-core/cost-model/data/builtinCostModelB.json index 80ae94bf2e8..702d1b703da 100644 --- a/plutus-core/cost-model/data/builtinCostModelB.json +++ b/plutus-core/cost-model/data/builtinCostModelB.json @@ -1321,5 +1321,21 @@ }, "type": "linear_in_y" } + }, + "multiIndexArray": { + "cpu": { + "arguments": { + "intercept": 268678, + "slope": 42403 + }, + "type": "linear_in_y" + }, + "memory": { + "arguments": { + "intercept": 4, + "slope": 1 + }, + "type": "linear_in_y" + } } } diff --git a/plutus-core/cost-model/data/builtinCostModelC.json b/plutus-core/cost-model/data/builtinCostModelC.json index 5ae7455e37c..9b59b55357e 100644 --- a/plutus-core/cost-model/data/builtinCostModelC.json +++ b/plutus-core/cost-model/data/builtinCostModelC.json @@ -1339,5 +1339,21 @@ }, "type": "linear_in_y" } + }, + "multiIndexArray": { + "cpu": { + "arguments": { + "intercept": 268678, + "slope": 42403 + }, + "type": "linear_in_y" + }, + "memory": { + "arguments": { + "intercept": 4, + "slope": 1 + }, + "type": "linear_in_y" + } } } diff --git a/plutus-core/cost-model/data/builtinCostModelD.json b/plutus-core/cost-model/data/builtinCostModelD.json index 9abde57904b..23de8e49bd2 100644 --- a/plutus-core/cost-model/data/builtinCostModelD.json +++ b/plutus-core/cost-model/data/builtinCostModelD.json @@ -1321,5 +1321,21 @@ }, "type": "linear_in_y" } + }, + "multiIndexArray": { + "cpu": { + "arguments": { + "intercept": 268678, + "slope": 42403 + }, + "type": "linear_in_y" + }, + "memory": { + "arguments": { + "intercept": 4, + "slope": 1 + }, + "type": "linear_in_y" + } } } diff --git a/plutus-core/cost-model/data/builtinCostModelE.json b/plutus-core/cost-model/data/builtinCostModelE.json index 66c73b35ca0..f6ab436f2bf 100644 --- a/plutus-core/cost-model/data/builtinCostModelE.json +++ b/plutus-core/cost-model/data/builtinCostModelE.json @@ -1339,5 +1339,21 @@ }, "type": "linear_in_y" } + }, + "multiIndexArray": { + "cpu": { + "arguments": { + "intercept": 268678, + "slope": 42403 + }, + "type": "linear_in_y" + }, + "memory": { + "arguments": { + "intercept": 4, + "slope": 1 + }, + "type": "linear_in_y" + } } } diff --git a/plutus-core/cost-model/data/models.R b/plutus-core/cost-model/data/models.R index 81cbddfefa9..178e6275daf 100644 --- a/plutus-core/cost-model/data/models.R +++ b/plutus-core/cost-model/data/models.R @@ -159,6 +159,7 @@ arity <- function(name) { "ValueData" = 1, "UnValueData" = 1, "ScaleValue" = 2, + "MultiIndexArray" = 2, -1 ## Default for missing values ) } @@ -815,6 +816,40 @@ modelFun <- function(path) { lengthOfArrayModel <- constantModel ("LengthOfArray") listToArrayModel <- linearInX ("ListToArray") indexArrayModel <- constantModel ("IndexArray") + ## Cost is linear in the number of indices (y) and independent of the array + ## size (x). Per-index time rises slowly across the benchmarked range, which + ## makes the data slightly convex, and least squares on convex data returns a + ## negative intercept: the fit moves the fixed cost of a call into the slope, + ## adjustModel then clamps the intercept, and the fixed cost is lost. Fitting + ## the line through the extremes of the range keeps it, and for convex data + ## that line is an upper bound in between. The fit is checked against every + ## point below. + multiIndexArrayModel <- { + fname <- "MultiIndexArray" + filtered <- data %>% + filter.and.check.nonempty (fname) %>% + discard.overhead () + lo <- min(filtered$y_mem) + hi <- max(filtered$y_mem) + if (lo == hi) { + stop ("Need at least two distinct index counts for ", fname) + } + ## Slowest observation at each end, so that the line also covers the + ## spread of repeated measurements. + ends <- data.frame( + y_mem = c(lo, hi), + t = c(max(filter(filtered, y_mem == lo)$t), + max(filter(filtered, y_mem == hi)$t)) + ) + m <- lm(t ~ y_mem, ends) + predicted <- predict(m, filtered) + if (any(filtered$t > predicted)) { + cat (sprintf ("*** WARNING: %d of %d %s points lie above the fitted line (worst %.1f%%)\n", + sum(filtered$t > predicted), nrow(filtered), fname, + 100 * max((filtered$t - predicted) / predicted))) + } + mk.result(m, "linear_in_y") + } ## Values @@ -971,7 +1006,8 @@ modelFun <- function(path) { unValueDataModel = unValueDataModel, insertCoinModel = insertCoinModel, unionValueModel = unionValueModel, - scaleValueModel = scaleValueModel + scaleValueModel = scaleValueModel, + multiIndexArrayModel = multiIndexArrayModel ) ## The integer division functions have a complex costing behaviour that requires some negative diff --git a/plutus-core/cost-model/test/TestCostModels.hs b/plutus-core/cost-model/test/TestCostModels.hs index c8f5040956f..917c0e2d0fc 100644 --- a/plutus-core/cost-model/test/TestCostModels.hs +++ b/plutus-core/cost-model/test/TestCostModels.hs @@ -556,4 +556,6 @@ main = , -- FIXME: the test for unValueData fails. This may be because of a rounding error in the top coefficient. -- , $(genTest 1 "unValueData") $(genTest 2 "scaleValue") Everywhere + , -- Arrays (batch 7) + $(genTest 2 "multiIndexArray") Everywhere ] diff --git a/plutus-core/plutus-core/src/PlutusCore/Evaluation/Machine/BuiltinCostModel.hs b/plutus-core/plutus-core/src/PlutusCore/Evaluation/Machine/BuiltinCostModel.hs index 1784d99e627..abeda8fb489 100644 --- a/plutus-core/plutus-core/src/PlutusCore/Evaluation/Machine/BuiltinCostModel.hs +++ b/plutus-core/plutus-core/src/PlutusCore/Evaluation/Machine/BuiltinCostModel.hs @@ -199,6 +199,8 @@ data BuiltinCostModelBase f , paramInsertCoin :: f ModelFourArguments , paramUnionValue :: f ModelTwoArguments , paramScaleValue :: f ModelTwoArguments + , -- Batch 7 + paramMultiIndexArray :: f ModelTwoArguments } deriving stock (Generic) deriving anyclass (FunctorB, TraversableB, ConstraintsB) diff --git a/plutus-core/plutus-core/src/PlutusCore/Evaluation/Machine/ExBudgetingDefaults.hs b/plutus-core/plutus-core/src/PlutusCore/Evaluation/Machine/ExBudgetingDefaults.hs index 39b4755f1db..dc08e6c3c8e 100644 --- a/plutus-core/plutus-core/src/PlutusCore/Evaluation/Machine/ExBudgetingDefaults.hs +++ b/plutus-core/plutus-core/src/PlutusCore/Evaluation/Machine/ExBudgetingDefaults.hs @@ -427,6 +427,8 @@ unitCostBuiltinCostModel = , paramInsertCoin = unitCostFourArguments , paramUnionValue = unitCostTwoArguments , paramScaleValue = unitCostTwoArguments + , -- Batch 7 + paramMultiIndexArray = unitCostTwoArguments } unitCekParameters diff --git a/plutus-ledger-api/src/PlutusLedgerApi/V1/ParamName.hs b/plutus-ledger-api/src/PlutusLedgerApi/V1/ParamName.hs index 2ed299b5a94..b7de963f3f4 100644 --- a/plutus-ledger-api/src/PlutusLedgerApi/V1/ParamName.hs +++ b/plutus-ledger-api/src/PlutusLedgerApi/V1/ParamName.hs @@ -349,5 +349,10 @@ data ParamName | ScaleValue'cpu'arguments'slope | ScaleValue'memory'arguments'intercept | ScaleValue'memory'arguments'slope + | -- Batch 7, not yet deployed + MultiIndexArray'cpu'arguments'intercept + | MultiIndexArray'cpu'arguments'slope + | MultiIndexArray'memory'arguments'intercept + | MultiIndexArray'memory'arguments'slope deriving stock (Eq, Ord, Enum, Ix, Bounded, Generic) deriving (IsParamName) via (GenericParamName ParamName) diff --git a/plutus-ledger-api/src/PlutusLedgerApi/V2/ParamName.hs b/plutus-ledger-api/src/PlutusLedgerApi/V2/ParamName.hs index f1c43dda9af..8679d5387ff 100644 --- a/plutus-ledger-api/src/PlutusLedgerApi/V2/ParamName.hs +++ b/plutus-ledger-api/src/PlutusLedgerApi/V2/ParamName.hs @@ -351,5 +351,10 @@ data ParamName | ScaleValue'cpu'arguments'slope | ScaleValue'memory'arguments'intercept | ScaleValue'memory'arguments'slope + | -- Batch 7, not yet deployed + MultiIndexArray'cpu'arguments'intercept + | MultiIndexArray'cpu'arguments'slope + | MultiIndexArray'memory'arguments'intercept + | MultiIndexArray'memory'arguments'slope deriving stock (Eq, Ord, Enum, Ix, Bounded, Generic) deriving (IsParamName) via (GenericParamName ParamName) diff --git a/plutus-ledger-api/src/PlutusLedgerApi/V3/ParamName.hs b/plutus-ledger-api/src/PlutusLedgerApi/V3/ParamName.hs index a8c1dbc60dc..feb73334f9b 100644 --- a/plutus-ledger-api/src/PlutusLedgerApi/V3/ParamName.hs +++ b/plutus-ledger-api/src/PlutusLedgerApi/V3/ParamName.hs @@ -367,5 +367,10 @@ data ParamName | ScaleValue'cpu'arguments'slope | ScaleValue'memory'arguments'intercept | ScaleValue'memory'arguments'slope + | -- Batch 7, not yet deployed + MultiIndexArray'cpu'arguments'intercept + | MultiIndexArray'cpu'arguments'slope + | MultiIndexArray'memory'arguments'intercept + | MultiIndexArray'memory'arguments'slope deriving stock (Eq, Ord, Enum, Ix, Bounded, Generic) deriving (IsParamName) via (GenericParamName ParamName) diff --git a/plutus-ledger-api/test/Spec/CostModelParams.hs b/plutus-ledger-api/test/Spec/CostModelParams.hs index 311cd0753da..7e424676b8c 100644 --- a/plutus-ledger-api/test/Spec/CostModelParams.hs +++ b/plutus-ledger-api/test/Spec/CostModelParams.hs @@ -39,9 +39,9 @@ tests = "CostModelParams" "costModelParams" [ embed $ testCase "length" do - 332 @=? length v1_ParamNames - 332 @=? length v2_ParamNames - 350 @=? length v3_ParamNames + 336 @=? length v1_ParamNames + 336 @=? length v2_ParamNames + 354 @=? length v3_ParamNames , embed $ testCase "tripping paramname" do for_ v1_ParamNames \p -> assertBool "tripping v1 cm params failed" $ diff --git a/plutus-ledger-api/test/Spec/Data/CostModelParams.hs b/plutus-ledger-api/test/Spec/Data/CostModelParams.hs index 6976821abe8..4ed00474149 100644 --- a/plutus-ledger-api/test/Spec/Data/CostModelParams.hs +++ b/plutus-ledger-api/test/Spec/Data/CostModelParams.hs @@ -39,9 +39,9 @@ tests = "CostModelParams" "costModelParams" [ embed $ testCase "length" do - 332 @=? length v1_ParamNames - 332 @=? length v2_ParamNames - 350 @=? length v3_ParamNames + 336 @=? length v1_ParamNames + 336 @=? length v2_ParamNames + 354 @=? length v3_ParamNames , embed $ testCase "tripping paramname" do for_ v1_ParamNames \p -> assertBool "tripping v1 cm params failed" $ From a3f21bf9a1f2b43c88cc4bc3c5664b24eb08110e Mon Sep 17 00:00:00 2001 From: Yuriy Lazaryev Date: Wed, 29 Jul 2026 19:51:30 +0200 Subject: [PATCH 2/4] feat(builtins): limit the number of indices multiIndexArray accepts Beyond the limit the execution time is not predictable from the index count alone, so the cost model cannot bound it; this is the same kind of limit as the ones on integerToByteString and replicateByte. It is far above any realistic use, since a 16384-byte transaction could not carry that many serialised indices even if it carried nothing else. The lookup moves into PlutusCore.Arrays, where the count is checked during the traversal rather than up front, since taking the length first would walk the list twice. --- ...ev_issue_2259_multi_index_array_costing.md | 3 + plutus-core/plutus-core.cabal | 2 + .../plutus-core/src/PlutusCore/Arrays.hs | 68 +++++++++++++++++++ .../src/PlutusCore/Default/Builtins.hs | 9 +-- 4 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 plutus-core/changelog.d/20260727_115456_yuriy.lazaryev_issue_2259_multi_index_array_costing.md create mode 100644 plutus-core/plutus-core/src/PlutusCore/Arrays.hs diff --git a/plutus-core/changelog.d/20260727_115456_yuriy.lazaryev_issue_2259_multi_index_array_costing.md b/plutus-core/changelog.d/20260727_115456_yuriy.lazaryev_issue_2259_multi_index_array_costing.md new file mode 100644 index 00000000000..9956e20d5f7 --- /dev/null +++ b/plutus-core/changelog.d/20260727_115456_yuriy.lazaryev_issue_2259_multi_index_array_costing.md @@ -0,0 +1,3 @@ +### Changed + +- Replaced the placeholder costing of `multiIndexArray` ([CIP-0156](https://cips.cardano.org/cip/CIP-0156)) with a benchmarked cost model: CPU linear in the number of indices, memory linear in the length of the result. `multiIndexArray` now fails if given more than 4096 indices, because beyond that its execution time is not predictable from the index count alone. diff --git a/plutus-core/plutus-core.cabal b/plutus-core/plutus-core.cabal index 542f6a47ab8..9d3a2a31927 100644 --- a/plutus-core/plutus-core.cabal +++ b/plutus-core/plutus-core.cabal @@ -104,6 +104,7 @@ library PlutusCore.Analysis.Usages PlutusCore.Annotation PlutusCore.Arity + PlutusCore.Arrays PlutusCore.AstSize PlutusCore.Bitwise PlutusCore.Builtin @@ -847,6 +848,7 @@ executable cost-model-budgeting-bench , deepseq , directory , filepath + , ghc-compact , hedgehog , mtl , optparse-applicative diff --git a/plutus-core/plutus-core/src/PlutusCore/Arrays.hs b/plutus-core/plutus-core/src/PlutusCore/Arrays.hs new file mode 100644 index 00000000000..08991e15e7e --- /dev/null +++ b/plutus-core/plutus-core/src/PlutusCore/Arrays.hs @@ -0,0 +1,68 @@ +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +-- | Implementation for the array builtins of CIP-0156 that needs more than a one-liner. +module PlutusCore.Arrays + ( multiIndexArray + , maximumIndexCount + ) where + +import PlutusCore.Builtin.Result (BuiltinResult, builtinResultFailure, emit) + +import Data.Text (pack) +import Data.Vector.Strict (Vector) +import Data.Vector.Strict qualified as Vector + +{- Note [Index count limitation for multiIndexArray] +Looking up a list of indices means walking that list, and the time to walk a +list is not determined by its length alone, so execution time stops being +predictable from the index count beyond some size. We therefore limit the +number of indices, and the cost model is fitted over exactly that range. The +limit is far above any realistic use: a transaction is limited to 16384 bytes, +so a serialised list of this many indices would not fit even if the transaction +carried nothing else. This limit may be raised once costing can bound the cost +without it, but note that doing so would need a second variant of the builtin so +that existing scripts keep their current behaviour. + +Compare Note [Input length limitation for IntegerToByteString], which limits +those builtins for the same underlying reason. + +The count is checked during the traversal rather than up front, because taking +the length first would walk the list twice. -} +maximumIndexCount :: Int +maximumIndexCount = 4096 +{-# INLINE maximumIndexCount #-} + +{-| Look up every index of the given list in the given array. + +Fails if any index is out of bounds, or if there are more than +'maximumIndexCount' of them; see Note [Index count limitation for +multiIndexArray]. + +The elements are read eagerly, as in @indexArray@, so that the read is paid for +inside this builtin rather than wherever the element is later demanded. Forcing +is safe: the vector is strict, so the element is already in normal form as far +as this function is concerned. -} +multiIndexArray :: forall a. Vector a -> [Integer] -> BuiltinResult [a] +multiIndexArray vec = go 0 + where + !len = toInteger (Vector.length vec) + + go :: Int -> [Integer] -> BuiltinResult [a] + go !_ [] = pure [] + go !n (i : is) + | n >= maximumIndexCount = do + emit . pack $ + "multiIndexArray: too many indices (maximum is " + ++ show maximumIndexCount + ++ ")" + builtinResultFailure + | 0 <= i && i < len = do + let !x = Vector.unsafeIndex vec (fromInteger i) + (x :) <$> go (n + 1) is + | otherwise = do + emit "multiIndexArray: array index out of bounds" + emit $ "Index: " <> (pack . show $ i) + builtinResultFailure +{-# INLINE multiIndexArray #-} diff --git a/plutus-core/plutus-core/src/PlutusCore/Default/Builtins.hs b/plutus-core/plutus-core/src/PlutusCore/Default/Builtins.hs index 1a3d2c7bb00..d38465960cf 100644 --- a/plutus-core/plutus-core/src/PlutusCore/Default/Builtins.hs +++ b/plutus-core/plutus-core/src/PlutusCore/Default/Builtins.hs @@ -18,6 +18,7 @@ module PlutusCore.Default.Builtins where import PlutusPrelude +import PlutusCore.Arrays qualified as Arrays import PlutusCore.Builtin import PlutusCore.Data (Data (..)) import PlutusCore.Default.Universe @@ -2484,17 +2485,13 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni DefaultFun where multiIndexArrayDenotation (SomeConstant (Some (ValueOf uni vec))) indices = case uni of DefaultUniArray uniA -> - let len = toInteger (Vector.length vec) - lookupIndex i - | 0 <= i && i < len = pure $ Vector.unsafeIndex vec $ fromInteger i - | otherwise = fail "Array index out of bounds" - in fromValueOf (DefaultUniList uniA) <$> traverse lookupIndex indices + fromValueOf (DefaultUniList uniA) <$> Arrays.multiIndexArray vec indices _ -> throwError $ structuralUnliftingError "Expected an array but got something else" {-# INLINE multiIndexArrayDenotation #-} in makeBuiltinMeaning multiIndexArrayDenotation - (runCostingFunTwoArguments . unimplementedCostingFun) + (runCostingFunTwoArguments . paramMultiIndexArray) toBuiltinMeaning _semvar Policies = let policiesDenotation :: Value -> [ByteString] policiesDenotation = Value.policies From 061278f2cee5d7bdc9cb0a70961d00f9c4e5716f Mon Sep 17 00:00:00 2001 From: Yuriy Lazaryev Date: Wed, 29 Jul 2026 19:51:32 +0200 Subject: [PATCH 3/4] feat(cost-model): benchmark multiIndexArray over the accepted range Index counts run from one to the limit, crossed with log-spaced array sizes, plus off-grid points and controls for element size. The index lists are given unfavourable spacing, so that the fit does not rest on the favourable spacing a straightforward generator produces; a few inputs, including the largest index count, are spaced as widely as a transaction's memory budget would allow, since those are the points the fit takes. makeBenchmarks becomes IO, because fixing the layout requires building the lists into compact regions. --- .../budgeting-bench/Benchmarks/Arrays.hs | 172 +++++++++++++++++- .../cost-model/budgeting-bench/Main.hs | 4 +- .../cost-model/data/benching-conway.csv | 105 +++++++++++ 3 files changed, 271 insertions(+), 10 deletions(-) diff --git a/plutus-core/cost-model/budgeting-bench/Benchmarks/Arrays.hs b/plutus-core/cost-model/budgeting-bench/Benchmarks/Arrays.hs index f75c12932ed..ad0cd8cc878 100644 --- a/plutus-core/cost-model/budgeting-bench/Benchmarks/Arrays.hs +++ b/plutus-core/cost-model/budgeting-bench/Benchmarks/Arrays.hs @@ -1,5 +1,6 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} module Benchmarks.Arrays (makeBenchmarks) where @@ -7,26 +8,44 @@ module Benchmarks.Arrays (makeBenchmarks) where import Prelude import Common -import Control.Monad (replicateM) +import Control.Monad (replicateM, replicateM_) import Criterion.Main (Benchmark) +import Data.Bits (bit) import Data.ByteString (ByteString) +import Data.Set qualified as Set +import Data.Traversable (for) import Data.Vector.Strict (Vector) import Data.Vector.Strict qualified as Vector +import GHC.Compact (compact, compactAdd, getCompact) +import PlutusCore.Arrays qualified as Arrays import PlutusCore.Builtin (mkTyBuiltin) import PlutusCore.Core (Type) -import PlutusCore.Default (DefaultFun (IndexArray, LengthOfArray, ListToArray), DefaultUni) +import PlutusCore.Default + ( DefaultFun (IndexArray, LengthOfArray, ListToArray, MultiIndexArray) + , DefaultUni + ) import PlutusCore.Name.Unique (TyName) -import System.Random.Stateful (StdGen, UniformRange (uniformRM), runStateGen_, uniformByteStringM) +import System.Random (mkStdGen, randomRs) +import System.Random.Stateful + ( StatefulGen + , StdGen + , UniformRange (uniformRM) + , runStateGen_ + , uniformByteStringM + ) -------------------------------------------------------------------------------- -- Benchmarks ------------------------------------------------------------------ -makeBenchmarks :: StdGen -> [Benchmark] -makeBenchmarks gen = - [ benchLengthOfArray gen - , benchListToArray gen - , benchIndexArray gen - ] +makeBenchmarks :: StdGen -> IO [Benchmark] +makeBenchmarks gen = do + multiIndexArray <- benchMultiIndexArray gen + pure + [ benchLengthOfArray gen + , benchListToArray gen + , benchIndexArray gen + , multiIndexArray + ] benchLengthOfArray :: StdGen -> Benchmark benchLengthOfArray gen = @@ -66,6 +85,141 @@ benchIndexArray gen = idx <- uniformRM (0, arraySize - 1) g pure (vec, fromIntegral idx) +{- The cost of `multiIndexArray` is expected to be linear in the number of indices + (y) and independent of the array size (x) and of the element size, since each + lookup is a constant-time read. The inputs are chosen so that the benchmark + data can also demonstrate that independence rather than assume it: + + * a full grid of array sizes crossed with index counts, so that x and y vary + independently; array sizes are log-spaced up to 131072 to cover all cache + regimes (131072 elements is already at the practical limit of what + `listToArray` can build within one transaction budget); + * indices drawn uniformly over the whole array, with replacement -- random + access is the cache-adversarial pattern; + * off-grid random points supporting residual diagnostics; + * two cells with ~1 KiB elements (64 bytes elsewhere) as an empirical check + that element size does not affect the cost; their index counts are offset + from the grid values so that benchmark names stay unique. + + See Note [Scattered index lists] for why the index lists are allocated the + way they are. -} +benchMultiIndexArray :: StdGen -> IO Benchmark +benchMultiIndexArray gen = do + pairs <- for inputs \(vec, indices, widths) -> (vec,) <$> mkScatteredList widths indices + pure (createTwoTermBuiltinBenchElementwise MultiIndexArray [tyArrayOfBS] pairs) + where + arraySizes :: [Int] + arraySizes = [1, 8, 64, 512, 4_096, 32_768, 131_072] + + -- The top of the range is 'Arrays.maximumIndexCount', so that the model is + -- fitted over exactly the domain the denotation accepts. + indexCounts :: [Int] + indexCounts = [1, 10, 50, 100, 500, 1_000, 2_000, 3_000, Arrays.maximumIndexCount] + + inputs :: [(Vector ByteString, [Integer], [Int])] + inputs = dedupeOnSizes $ runStateGen_ gen \g -> do + grid <- + concat <$> for arraySizes \arraySize -> do + vec <- mkArray g 64 arraySize + for indexCounts (fmap (withGaps vec) . mkIndices g arraySize) + cloud <- replicateM 37 do + arraySize <- (2 ^) <$> uniformRM (0 :: Int, 17) g + indexCount <- uniformRM (1, Arrays.maximumIndexCount) g + vec <- mkArray g 64 arraySize + withGaps vec <$> mkIndices g arraySize indexCount + control <- for [999, 1_001] \indexCount -> do + vec <- mkArray g 1_024 4_096 + withGaps vec <$> mkIndices g 4_096 indexCount + -- A distinct array size, so these get benchmark names of their own. + spread <- for spreadCounts \indexCount -> do + vec <- mkArray g 64 2_048 + withBudgetGaps vec <$> mkIndices g 2_048 indexCount + pure (grid <> control <> cloud <> spread) + + mkArray :: StatefulGen g m => g -> Int -> Int -> m (Vector ByteString) + mkArray g maxElemSize arraySize = + Vector.replicateM arraySize do + bsSize <- uniformRM (0, maxElemSize) g + uniformByteStringM bsSize g + + mkIndices :: StatefulGen g m => g -> Int -> Int -> m [Integer] + mkIndices g arraySize indexCount = + replicateM indexCount (fromIntegral <$> uniformRM (0, arraySize - 1) g) + + -- Index counts that additionally get the wider spacing described in Note + -- [Scattered index lists]; the top of the range is among them because the + -- fit takes the slowest observation at each end. + spreadCounts :: [Int] + spreadCounts = [500, 2_000, Arrays.maximumIndexCount] + + withGaps vec indices = (vec, indices, gapWidths) + + withBudgetGaps vec indices = + (vec, indices, repeat (max 1 (budgetScatterBytes `div` (length indices * spacerBytes)))) + + -- Number of spacer objects per gap. Varied rather than fixed: a constant + -- stride is picked up by the hardware prefetcher, which makes a regularly + -- spaced list measurably faster than an irregularly spaced one. The mean + -- is 16, so 4 KiB per gap. + gapWidths :: [Int] + gapWidths = randomRs (2, 30) (mkStdGen 7) + +-- | Bytes of one spacer object, an integer of 2048 bits plus its header. +spacerBytes :: Int +spacerBytes = 256 + +{-| Total scatter for the widely spaced inputs, of the order a transaction's +memory budget allows. -} +budgetScatterBytes :: Int +budgetScatterBytes = 100_000_000 + +{- Note [Scattered index lists] +The time to walk a list depends on where its cons cells sit in memory, not only +on how many there are, and a list built by a plain `replicateM` is allocated +contiguously, which is the favourable end of that range rather than a +representative point. These lists are therefore built with the cells some way +apart, and by varying amounts, so that the model is fitted to the unfavourable +end. Most inputs get gaps of about a page; a few, including the largest index +count, get gaps of the width that a whole transaction's memory budget would +allow, because that is what sets the slope. See also Note +[Index count limitation for multiIndexArray] in PlutusCore.Arrays. + +Three implementation details are needed for that layout to hold, and removing +any of them silently returns the benchmark to measuring contiguous lists: + + * the list is built inside a compact region, because the ordinary heap is + re-laid by every major GC and the layout would not survive; + * `compactAdd` appends in call order, so the cells and the spacers between + them are interleaved as written; + * each gap is filled with several small objects rather than one large one, + because a large object is given a block of its own and the cells would end + up adjacent. -} +mkScatteredList :: [Int] -> [Integer] -> IO [Integer] +mkScatteredList gapWidths values = do + region <- compact () + let go _ acc [] = pure acc + go (width : widths) acc (v : vs) = do + replicateM_ width (compactAdd region spacer) + acc' <- getCompact <$> compactAdd region (v : acc) + go widths acc' vs + go [] acc _ = pure acc + go gapWidths [] (reverse values) + where + spacer = bit 2_048 :: Integer + +-- Benchmark names are derived from the argument sizes; drop the rare random +-- point that collides with another point on both sizes. +dedupeOnSizes + :: [(Vector ByteString, [Integer], [Int])] -> [(Vector ByteString, [Integer], [Int])] +dedupeOnSizes = go Set.empty + where + go _ [] = [] + go seen (input@(vec, indices, _) : rest) + | Set.member key seen = go seen rest + | otherwise = input : go (Set.insert key seen) rest + where + key = (Vector.length vec, length indices) + -------------------------------------------------------------------------------- -- Helpers --------------------------------------------------------------------- diff --git a/plutus-core/cost-model/budgeting-bench/Main.hs b/plutus-core/cost-model/budgeting-bench/Main.hs index af2e3aa4648..5de73a92a98 100644 --- a/plutus-core/cost-model/budgeting-bench/Main.hs +++ b/plutus-core/cost-model/budgeting-bench/Main.hs @@ -45,6 +45,8 @@ main = do -- We use the initial state of gen repeatedly below, but that doesn't matter. gen <- System.Random.getStdGen + arrayBenchmarks <- Benchmarks.Arrays.makeBenchmarks gen + criterionMainWith Start defaultConfig @@ -55,7 +57,7 @@ main = do <> Benchmarks.Data.makeBenchmarks gen <> Benchmarks.Integers.makeBenchmarks gen <> Benchmarks.Lists.makeBenchmarks gen - <> Benchmarks.Arrays.makeBenchmarks gen + <> arrayBenchmarks <> Benchmarks.Misc.makeBenchmarks gen <> Benchmarks.Pairs.makeBenchmarks gen <> Benchmarks.Strings.makeBenchmarks gen diff --git a/plutus-core/cost-model/data/benching-conway.csv b/plutus-core/cost-model/data/benching-conway.csv index b05d0901126..899f5219a1a 100644 --- a/plutus-core/cost-model/data/benching-conway.csv +++ b/plutus-core/cost-model/data/benching-conway.csv @@ -14812,3 +14812,108 @@ EqualsString/19400/19400,2.3089679419295826e-6,2.307911008840468e-6,2.3099943277 EqualsString/19600/19600,2.3625558068055665e-6,2.3601398190871273e-6,2.3650285798307642e-6,8.292779808420567e-9,6.6631051322127245e-9,1.034399864901697e-8 EqualsString/19800/19800,2.4843644080488102e-6,2.4806302153117747e-6,2.488255193448198e-6,1.3014923018139571e-8,1.1712144644861913e-8,1.4644209347218475e-8 EqualsString/20000/20000,2.3788835774653483e-6,2.3780147413486273e-6,2.379744446079645e-6,2.87726733474497e-9,2.3580396483665765e-9,3.492220006626607e-9 +MultiIndexArray/1/1,1.1475654799036273e-6,1.1448647666255405e-6,1.1494661411069994e-6,7.336311295137874e-9,4.9305201847747694e-9,9.41030756760755e-9 +MultiIndexArray/1/10,1.2287963259220548e-6,1.2266598094460012e-6,1.231049946721095e-6,7.119507549868057e-9,6.194678698444273e-9,8.309631247881801e-9 +MultiIndexArray/1/50,1.772882633224413e-6,1.7717321639707322e-6,1.7746752681524706e-6,4.819206016465468e-9,3.0936671367289426e-9,7.0585005894483825e-9 +MultiIndexArray/1/100,2.3141830697380424e-6,2.3125471098691155e-6,2.3166777020821787e-6,6.4580013686935525e-9,4.507409501755394e-9,8.435816731108233e-9 +MultiIndexArray/1/500,7.54895584546493e-6,7.545735731302061e-6,7.555654903574705e-6,1.4711393929776863e-8,7.679703010274258e-9,2.4531083461401574e-8 +MultiIndexArray/1/1000,1.4373719306632369e-5,1.4364656255743034e-5,1.4388399912049631e-5,3.895335429148531e-8,2.5016816849137794e-8,5.8767616985778457e-8 +MultiIndexArray/1/2000,4.097176576218451e-5,4.093763475215707e-5,4.103207890194546e-5,1.4550724883715165e-7,8.981074944513769e-8,2.3391057549129412e-7 +MultiIndexArray/1/3000,8.113751816082237e-5,8.109148491887528e-5,8.11802992087285e-5,1.419540456921008e-7,1.1196868656376282e-7,1.9049490583748957e-7 +MultiIndexArray/1/4096,1.3701346477622284e-4,1.3696701577405333e-4,1.3705763689726027e-4,1.5975502393310089e-7,1.2925721451299196e-7,1.9169143622945698e-7 +MultiIndexArray/8/1,1.1392937345937184e-6,1.1383095530905438e-6,1.1405786428788913e-6,3.526421610808158e-9,2.6648294049810025e-9,5.382127012659372e-9 +MultiIndexArray/8/10,1.2208659818681312e-6,1.2197316788984498e-6,1.2223620214918156e-6,4.374249285666737e-9,3.354642880734952e-9,6.1613569160514995e-9 +MultiIndexArray/8/50,1.7634832002011692e-6,1.7613970037177715e-6,1.7656414271621546e-6,7.168260900226481e-9,5.65386264696535e-9,9.459612525490319e-9 +MultiIndexArray/8/100,2.3059404656518384e-6,2.304650806415747e-6,2.3080089338174866e-6,5.370163722971263e-9,3.957616573618578e-9,9.243882059024596e-9 +MultiIndexArray/8/500,7.518873919207095e-6,7.516823517398488e-6,7.521351331224648e-6,7.725571239991918e-9,6.760193563105144e-9,9.277378736114766e-9 +MultiIndexArray/8/1000,1.4306821413415305e-5,1.4296208582175293e-5,1.4327342978395375e-5,5.1786217652817766e-8,3.246431273223711e-8,7.888591904843396e-8 +MultiIndexArray/8/2000,4.2936908237012715e-5,4.270519891202728e-5,4.3200162013554305e-5,8.352598678852213e-7,6.143744278367033e-7,9.727270830258463e-7 +MultiIndexArray/8/3000,8.426967713660737e-5,8.422303391656001e-5,8.433355547110183e-5,1.8242417045987494e-7,1.3617894415774104e-7,2.7388733989182697e-7 +MultiIndexArray/8/4096,1.4111856886058413e-4,1.4103955923905998e-4,1.4126359118156573e-4,3.406395406303189e-7,2.3992327516243965e-7,5.601111047619069e-7 +MultiIndexArray/64/1,1.1320884766997998e-6,1.1311306213818535e-6,1.132949552122866e-6,3.1395012705821005e-9,2.627034954638711e-9,3.9358854778687525e-9 +MultiIndexArray/64/10,1.2259711396834285e-6,1.2249421123437414e-6,1.2270777354140446e-6,3.5962585334345135e-9,3.026159818235465e-9,4.486410259979114e-9 +MultiIndexArray/64/50,1.7552211270884605e-6,1.753943006688894e-6,1.7565707149845192e-6,4.537811424987793e-9,3.886308255754433e-9,5.471169115751113e-9 +MultiIndexArray/64/100,2.3162329611548037e-6,2.314662175764795e-6,2.3175684496433603e-6,5.040900066020265e-9,4.05014171882415e-9,6.627757915662408e-9 +MultiIndexArray/64/500,7.4815816439649375e-6,7.478412113345307e-6,7.485547490797605e-6,1.20468021151101e-8,9.330349377660013e-9,1.5668631922134665e-8 +MultiIndexArray/64/1000,1.4312130640555895e-5,1.430208968053619e-5,1.4337211667185642e-5,4.9116315090714124e-8,2.5987470147307084e-8,9.505203802834015e-8 +MultiIndexArray/64/2000,4.227015667914413e-5,4.2252467689218185e-5,4.230858305188174e-5,8.42548760861762e-8,2.885618989074955e-8,1.488143277085926e-7 +MultiIndexArray/64/3000,8.343204882953575e-5,8.34017702613872e-5,8.346506271638361e-5,1.0643542974439423e-7,8.664862295930653e-8,1.329079506729003e-7 +MultiIndexArray/64/4096,1.41055418824186e-4,1.4099417972956754e-4,1.4118648327130154e-4,2.765722806791926e-7,1.5691144191559164e-7,4.94464052983363e-7 +MultiIndexArray/512/1,1.1491569963874003e-6,1.147867844970278e-6,1.1504893036573095e-6,4.594687568580564e-9,3.7919507601732794e-9,5.624201227327765e-9 +MultiIndexArray/512/10,1.2139260915999734e-6,1.2120291398556885e-6,1.2163837282742807e-6,7.044246829066238e-9,5.895597023193336e-9,8.48491652380167e-9 +MultiIndexArray/512/50,1.7688493632225068e-6,1.7682129004866559e-6,1.7696391280397623e-6,2.343350885189239e-9,2.0010632088768125e-9,2.744018153434179e-9 +MultiIndexArray/512/100,2.3324068087519933e-6,2.330364957669374e-6,2.3371545574096404e-6,1.0116817788475386e-8,4.3964989190151555e-9,1.902652709184929e-8 +MultiIndexArray/512/500,7.513035940046098e-6,7.511407728964184e-6,7.5151364015400585e-6,6.491319946331427e-9,4.960052804183259e-9,9.31247014325348e-9 +MultiIndexArray/512/1000,1.431227731614314e-5,1.4299559278823689e-5,1.433825492343175e-5,5.8725584636161884e-8,3.2442840872572496e-8,8.953670279358393e-8 +MultiIndexArray/512/2000,4.332768369800322e-5,4.3302717787585554e-5,4.3373650100383065e-5,1.0786290442572388e-7,7.034971384426688e-8,1.8200996406745133e-7 +MultiIndexArray/512/3000,8.456077936202956e-5,8.44777801937079e-5,8.478131150443767e-5,3.865107574762306e-7,1.8536058183793767e-7,7.443922468937351e-7 +MultiIndexArray/512/4096,1.412353937008207e-4,1.4117694700985779e-4,1.412820257318807e-4,1.6689281103757434e-7,1.209659860146293e-7,2.3660800804491056e-7 +MultiIndexArray/4096/1,1.1330780818426288e-6,1.1309826286519326e-6,1.1356419388689413e-6,7.802377479510687e-9,6.015136236752066e-9,1.2392032765013144e-8 +MultiIndexArray/4096/10,1.2234123089556797e-6,1.2222981653012294e-6,1.224542341892705e-6,3.8999605249312146e-9,3.322911526406136e-9,4.906415499922072e-9 +MultiIndexArray/4096/50,1.779932834256638e-6,1.7782447767933912e-6,1.782569516265636e-6,7.553144885592049e-9,4.821681507187308e-9,1.3230294457254247e-8 +MultiIndexArray/4096/100,2.3629184328353204e-6,2.3615837148380844e-6,2.3643934649915683e-6,4.70758529262968e-9,3.995590414070982e-9,5.995015672034042e-9 +MultiIndexArray/4096/500,7.65780242347359e-6,7.64566798555546e-6,7.67058184807567e-6,4.195757724752069e-8,3.7619109878055244e-8,4.871542086748444e-8 +MultiIndexArray/4096/1000,1.4655624969748715e-5,1.4650198658955465e-5,1.466385011974229e-5,2.1975236791326868e-8,1.4916605554880984e-8,3.235149632830576e-8 +MultiIndexArray/4096/2000,4.445840125088027e-5,4.442715379876741e-5,4.454054374865683e-5,1.613248594860919e-7,6.351805534742639e-8,3.4048044327364157e-7 +MultiIndexArray/4096/3000,8.620297907392301e-5,8.616462463083014e-5,8.624949327577044e-5,1.5473820576351706e-7,1.1785814619392178e-7,1.9668064598653916e-7 +MultiIndexArray/4096/4096,1.4286459388620134e-4,1.4277916252570695e-4,1.4317990369608233e-4,4.912531341569871e-7,1.0168418957511721e-7,1.024313150273432e-6 +MultiIndexArray/32768/1,1.139095064820247e-6,1.135867678547784e-6,1.1422286873184639e-6,1.110786565170614e-8,1.0146347183482831e-8,1.2354436664396808e-8 +MultiIndexArray/32768/10,1.215784710912282e-6,1.2145190832833523e-6,1.2172680154626224e-6,4.79895033900198e-9,3.838096557513749e-9,5.931043554246286e-9 +MultiIndexArray/32768/50,1.7646720115865439e-6,1.7631190070124947e-6,1.7673232213897585e-6,6.499390288308522e-9,4.1498692909168484e-9,9.68979108207791e-9 +MultiIndexArray/32768/100,2.345901853766103e-6,2.3415271042014816e-6,2.35979439383955e-6,2.4269945973437666e-8,3.194780840687806e-9,4.9783974016169465e-8 +MultiIndexArray/32768/500,7.69663070352817e-6,7.691841977394466e-6,7.706118855197925e-6,2.128805678151139e-8,1.1803952928196037e-8,3.7417286068938697e-8 +MultiIndexArray/32768/1000,1.5283789391021987e-5,1.5276037662443826e-5,1.5290765682236745e-5,2.524027469112948e-8,1.9995544574569156e-8,3.50107417567014e-8 +MultiIndexArray/32768/2000,4.7466156869556694e-5,4.7456921388254045e-5,4.747615691802702e-5,3.196410826986061e-8,2.354343551161185e-8,4.6684433059645994e-8 +MultiIndexArray/32768/3000,9.273293857269934e-5,9.265639419281239e-5,9.28612221515432e-5,3.474524722195476e-7,2.38497384022849e-7,4.887424602472412e-7 +MultiIndexArray/32768/4096,1.478418033420807e-4,1.477954621365976e-4,1.4788930670819238e-4,1.501350448254087e-7,1.1735054770023855e-7,1.9706071422865002e-7 +MultiIndexArray/131072/1,1.1405141567995614e-6,1.1398900949463115e-6,1.141199796480311e-6,2.185308208014028e-9,1.7995683009088735e-9,2.746686083056155e-9 +MultiIndexArray/131072/10,1.2264376729483102e-6,1.22514086755324e-6,1.2280743055865163e-6,4.847321839671195e-9,4.0680931963426486e-9,6.0676714817473e-9 +MultiIndexArray/131072/50,1.7727715025108328e-6,1.772200307914582e-6,1.7734399543422059e-6,2.0229779837689575e-9,1.5753146306727805e-9,2.618455704434526e-9 +MultiIndexArray/131072/100,2.3358516566583567e-6,2.3347474625533538e-6,2.338580258163134e-6,5.434431634241336e-9,2.5246094663854225e-9,1.101900724745063e-8 +MultiIndexArray/131072/500,7.80633624337251e-6,7.801651206860497e-6,7.812067657571503e-6,1.747587908781229e-8,1.4461629406548155e-8,2.20084019392088e-8 +MultiIndexArray/131072/1000,1.5184784186853412e-5,1.5180518738317496e-5,1.519166061529206e-5,1.783767003442113e-8,1.2142956774713305e-8,2.8893209846427368e-8 +MultiIndexArray/131072/2000,5.137118273073598e-5,5.134764699193158e-5,5.1408435167352416e-5,9.629255282785384e-8,4.9591811440584155e-8,1.7831287364070714e-7 +MultiIndexArray/131072/3000,9.604944727217142e-5,9.60067437455425e-5,9.612357962602096e-5,1.866359335791601e-7,1.1744395698604175e-7,3.508752995715949e-7 +MultiIndexArray/131072/4096,1.502647411983423e-4,1.502136281389219e-4,1.5033325092279765e-4,1.998748526852996e-7,1.4668450618535053e-7,2.6945330304703006e-7 +MultiIndexArray/4096/999,1.4721282684320545e-5,1.4713773533638093e-5,1.4737354542682606e-5,3.5308767633145565e-8,2.0761797816617353e-8,5.673555242207037e-8 +MultiIndexArray/4096/1001,1.459397290521482e-5,1.4573057035927441e-5,1.4629822284386054e-5,8.727279670791087e-8,5.563132795996642e-8,1.2865033798757422e-7 +MultiIndexArray/8/263,4.5250542916656e-6,4.521644276564486e-6,4.530814209738934e-6,1.3945745674475767e-8,8.287353506306776e-9,2.1789086132499053e-8 +MultiIndexArray/16/1524,2.353516106753335e-5,2.352380196133849e-5,2.3563125069345923e-5,5.53277646914903e-8,2.994654972933111e-8,1.0073468802146534e-7 +MultiIndexArray/8/1667,2.627155449868186e-5,2.6257099871177818e-5,2.6290911310549556e-5,5.7208734487057544e-8,3.926089471389008e-8,8.772835684991053e-8 +MultiIndexArray/64/3655,1.1461294093104726e-4,1.1456987918497553e-4,1.1466675903960892e-4,1.5902299177238653e-7,1.224317112766101e-7,2.107107450081871e-7 +MultiIndexArray/32768/3460,1.1346848110292192e-4,1.1341364308116417e-4,1.1358228874055657e-4,2.610285437970092e-7,1.6343007130663884e-7,4.3669176593170376e-7 +MultiIndexArray/256/3239,9.574960288134122e-5,9.5690994621022e-5,9.585958811861784e-5,2.541167867974683e-7,1.7536489048612754e-7,3.7636526239578784e-7 +MultiIndexArray/1/3047,8.252546903090217e-5,8.248224689617068e-5,8.256259573497199e-5,1.329117456911557e-7,1.0006050871687976e-7,1.867489838466605e-7 +MultiIndexArray/8192/2991,8.772723087989315e-5,8.7698323656832e-5,8.775778855318519e-5,1.0332989679305222e-7,7.704805872946415e-8,1.621167010107353e-7 +MultiIndexArray/256/3030,8.492788104086702e-5,8.489646304064705e-5,8.497109943434013e-5,1.2462255823133606e-7,7.858386222648809e-8,2.37266722065449e-7 +MultiIndexArray/16384/3123,9.512370129639336e-5,9.509280245097904e-5,9.515541133674835e-5,1.0827419975506337e-7,9.138505202122216e-8,1.2732090003283284e-7 +MultiIndexArray/4/338,5.47988444175722e-6,5.476923058413485e-6,5.483135774029877e-6,1.068765667380798e-8,7.99774127776263e-9,1.546116088567235e-8 +MultiIndexArray/2/1031,1.4795635890269815e-5,1.4784339843427506e-5,1.4812942385747425e-5,4.9718340706618514e-8,3.487878734529445e-8,7.265277934928343e-8 +MultiIndexArray/4096/4011,1.390622272872614e-4,1.389895143739499e-4,1.3912858192554803e-4,2.4567351393944767e-7,1.938071076867939e-7,3.72087173508944e-7 +MultiIndexArray/8192/4068,1.424863690676321e-4,1.4243422145372408e-4,1.4254091049147865e-4,1.777086836136699e-7,1.438985005082449e-7,2.343514031453711e-7 +MultiIndexArray/16384/985,1.4753610167168232e-5,1.4745602190529964e-5,1.4775973032705035e-5,3.823825665445562e-8,1.2969891692770693e-8,6.720058605117397e-8 +MultiIndexArray/256/1397,2.103095123681931e-5,2.1009082958736477e-5,2.1079080615089097e-5,1.037630178707584e-7,5.6927647374429493e-8,1.9091732462565374e-7 +MultiIndexArray/16/2479,5.985903431555217e-5,5.9829127469029275e-5,5.991642104227068e-5,1.3169560793205766e-7,7.24143994049018e-8,2.639134483206058e-7 +MultiIndexArray/1024/2109,4.636769412175354e-5,4.6336774922766416e-5,4.6426284963908745e-5,1.5168816901747882e-7,9.08900920243067e-8,2.3572553495188033e-7 +MultiIndexArray/65536/233,4.116917961204163e-6,4.113619201489099e-6,4.120101395973758e-6,1.1294020796427668e-8,9.064908666906267e-9,1.4549133101343258e-8 +MultiIndexArray/2048/2546,6.384519492491547e-5,6.382843204654578e-5,6.387694452194656e-5,7.570252063435817e-8,4.957308560057083e-8,1.2656411262420802e-7 +MultiIndexArray/16384/2990,9.03461892176389e-5,9.030935317535446e-5,9.043236463360032e-5,1.7633014211490778e-7,9.23281765024184e-8,3.2716591246767657e-7 +MultiIndexArray/1024/2411,5.831821851659926e-5,5.828135543585365e-5,5.8370462924800955e-5,1.4255734214841028e-7,1.1361708092774552e-7,2.002840843115711e-7 +MultiIndexArray/16384/1208,1.8480162179741e-5,1.8464649959009443e-5,1.850558255246869e-5,6.551899866049371e-8,4.2758893815082787e-8,9.478748818004945e-8 +MultiIndexArray/1024/2062,4.4619444604977835e-5,4.4600924501096825e-5,4.465141505214709e-5,8.377867517991318e-8,5.434323931720216e-8,1.3127280634022865e-7 +MultiIndexArray/2/3608,1.1188293982609802e-4,1.1183945162956827e-4,1.1197090314295894e-4,1.9667735759930496e-7,1.2083727624015923e-7,3.262129500083901e-7 +MultiIndexArray/16/97,2.4823970942278015e-6,2.481537663958302e-6,2.4833037653944116e-6,3.081132886173127e-9,2.621900700890028e-9,3.772393754364903e-9 +MultiIndexArray/32768/3231,9.398453910740249e-5,9.393726830449062e-5,9.40690416818357e-5,1.9889222753302612e-7,1.1846484642570863e-7,3.1930278049249855e-7 +MultiIndexArray/128/2754,5.8043185840921895e-5,5.800201363155572e-5,5.81379189985672e-5,2.04225242168635e-7,1.1116545718764201e-7,3.520235989113438e-7 +MultiIndexArray/65536/766,1.1481053172428057e-5,1.1470379473150677e-5,1.1501112461943436e-5,5.019474883606618e-8,3.246657519999301e-8,7.621161149791926e-8 +MultiIndexArray/16/450,6.93077870315652e-6,6.92275708718419e-6,6.946923581774483e-6,3.7917792623943404e-8,2.1114437978407046e-8,7.259068340845737e-8 +MultiIndexArray/65536/3084,8.550630092711685e-5,8.54726620843229e-5,8.555405070544366e-5,1.260570707376488e-7,9.054816949416432e-8,2.095250762250309e-7 +MultiIndexArray/2/2396,5.131983437353318e-5,5.1292703738830464e-5,5.1344814139262904e-5,8.847353010179114e-8,6.904037339874972e-8,1.211942358841752e-7 +MultiIndexArray/32/2432,5.3625158113025835e-5,5.358443585000692e-5,5.3692570003589664e-5,1.795031276719089e-7,1.3259950936975455e-7,2.91639524715725e-7 +MultiIndexArray/65536/1945,4.0132919756194e-5,4.011879828921036e-5,4.0146385977486055e-5,4.6121676649527215e-8,3.8768711146407936e-8,5.607901063350481e-8 +MultiIndexArray/2/1289,1.9068479076250185e-5,1.9051591075347515e-5,1.9093807261806103e-5,7.011693718403845e-8,5.279000203213367e-8,8.874610155790319e-8 +MultiIndexArray/512/1263,1.8849133144478524e-5,1.8840978129336474e-5,1.8861763991409942e-5,3.3357142338041636e-8,1.524494327100234e-8,6.031231197953646e-8 +MultiIndexArray/8192/1265,1.9601851029324906e-5,1.9589704033696178e-5,1.9623598996945176e-5,5.4416153053893326e-8,3.177356746550693e-8,9.995780598723585e-8 +MultiIndexArray/2048/500,7.975582554909001e-6,7.970026548609899e-6,7.994467836430784e-6,3.050891915403152e-8,6.182880825230999e-9,6.683492044704189e-8 +MultiIndexArray/2048/2000,6.058574489365401e-5,6.053941682980067e-5,6.064511336602131e-5,1.7535636675356376e-7,1.2146341099574307e-7,2.744845750175178e-7 +MultiIndexArray/2048/4096,1.7478629954996368e-4,1.7473945019987552e-4,1.748418933028428e-4,1.8076306047619194e-7,1.4062703370780312e-7,2.3776232441823307e-7 From 8cbcee07baad30e71583472922f32ebfa6b2d4c1 Mon Sep 17 00:00:00 2001 From: Yuriy Lazaryev Date: Wed, 29 Jul 2026 19:51:33 +0200 Subject: [PATCH 4/4] docs(cost-models): add a multiIndexArray page and update the variant used The new page plots the benchmark data against the model plane and the per-index time distribution. All pages now read builtinCostModelE.json, the PlutusV3 model from PV 11 onwards, in place of C. --- doc/cost-models/README.md | 4 +- doc/cost-models/index.html | 15 +- doc/cost-models/indexarray/index.html | 3 +- doc/cost-models/indexarray/plot.js | 2 +- doc/cost-models/indexbytestring/index.html | 3 +- doc/cost-models/indexbytestring/plot.js | 2 +- doc/cost-models/insertcoin/index.html | 3 +- doc/cost-models/insertcoin/plot.js | 2 +- doc/cost-models/lengthofarray/index.html | 3 +- doc/cost-models/lengthofarray/plot.js | 2 +- doc/cost-models/listtoarray/index.html | 3 +- doc/cost-models/listtoarray/plot.js | 2 +- doc/cost-models/lookupcoin/index.html | 3 +- doc/cost-models/lookupcoin/plot.js | 2 +- doc/cost-models/multiindexarray/index.html | 166 +++++++ doc/cost-models/multiindexarray/plot.js | 489 +++++++++++++++++++++ doc/cost-models/scalevalue/index.html | 3 +- doc/cost-models/scalevalue/plot.js | 2 +- doc/cost-models/shared/utils.js | 2 +- doc/cost-models/unionvalue/index.html | 3 +- doc/cost-models/unionvalue/plot.js | 2 +- doc/cost-models/unvaluedata/index.html | 3 +- doc/cost-models/unvaluedata/plot.js | 2 +- doc/cost-models/valuecontains/index.html | 3 +- doc/cost-models/valuecontains/plot.js | 2 +- doc/cost-models/valuedata/index.html | 3 +- doc/cost-models/valuedata/plot.js | 2 +- 27 files changed, 704 insertions(+), 27 deletions(-) create mode 100644 doc/cost-models/multiindexarray/index.html create mode 100644 doc/cost-models/multiindexarray/plot.js diff --git a/doc/cost-models/README.md b/doc/cost-models/README.md index b057059ff36..61952eda61f 100644 --- a/doc/cost-models/README.md +++ b/doc/cost-models/README.md @@ -59,7 +59,7 @@ doc/cost-models/ ## Data Sources - **Benchmark Data**: `plutus-core/cost-model/data/benching-conway.csv` -- **Cost Models**: `plutus-core/cost-model/data/builtinCostModelC.json` +- **Cost Models**: `plutus-core/cost-model/data/builtinCostModelE.json` Data is loaded dynamically from the Plutus repository using the browser's `fetch()` API. @@ -164,7 +164,7 @@ No build process required - the workflow copies static files directly to `gh-pag **Model not found:** -- Verify function name matches key in `builtinCostModelC.json` +- Verify function name matches key in `builtinCostModelE.json` - Check console for detailed error messages **Plot not rendering:** diff --git a/doc/cost-models/index.html b/doc/cost-models/index.html index a910eb1c6b8..2709b3ee1ad 100644 --- a/doc/cost-models/index.html +++ b/doc/cost-models/index.html @@ -20,6 +20,7 @@
  • ListToArray
  • LengthOfArray
  • IndexArray
  • +
  • MultiIndexArray
  • IndexByteString
  • @@ -135,6 +136,16 @@

    Available Visualizations

    +
  • + MultiIndexArray +

    + Looks up a list of indices in a Plutus array; linear in the number of indices, + independent of the array size. + (3D visualization: Haystack Size × Needles Size vs Time, plus per-index time + distribution histogram) +

    +
  • +
  • IndexByteString

    @@ -157,7 +168,7 @@

    Data Sources

  • Cost Models: - builtinCostModelC.json + builtinCostModelE.json
  • @@ -239,7 +250,7 @@

    Troubleshooting

  • Model not found: Verify function name matches the key in - builtinCostModelC.json + builtinCostModelE.json
  • Plot not rendering: Check Plotly errors in console; diff --git a/doc/cost-models/indexarray/index.html b/doc/cost-models/indexarray/index.html index e725a71a7fb..abea7a3cb8f 100644 --- a/doc/cost-models/indexarray/index.html +++ b/doc/cost-models/indexarray/index.html @@ -18,6 +18,7 @@
  • ListToArray
  • LengthOfArray
  • IndexArray
  • +
  • MultiIndexArray
  • IndexByteString
  • @@ -118,7 +119,7 @@

    Plot Information

    diff --git a/doc/cost-models/indexarray/plot.js b/doc/cost-models/indexarray/plot.js index d9395de3ffa..1f4b7c78816 100644 --- a/doc/cost-models/indexarray/plot.js +++ b/doc/cost-models/indexarray/plot.js @@ -35,7 +35,7 @@ function generateUrlFromBranch(branch) { function getFileUrls(baseUrl) { return { csv: `${baseUrl}/benching-conway.csv`, - json: `${baseUrl}/builtinCostModelC.json` + json: `${baseUrl}/builtinCostModelE.json` }; } diff --git a/doc/cost-models/indexbytestring/index.html b/doc/cost-models/indexbytestring/index.html index 7a231f6c2a7..19f60a6ec37 100644 --- a/doc/cost-models/indexbytestring/index.html +++ b/doc/cost-models/indexbytestring/index.html @@ -21,6 +21,7 @@
  • ListToArray
  • LengthOfArray
  • IndexArray
  • +
  • MultiIndexArray
  • IndexByteString
  • @@ -121,7 +122,7 @@

    Plot Information

    diff --git a/doc/cost-models/indexbytestring/plot.js b/doc/cost-models/indexbytestring/plot.js index 108ba86f7e8..9e7143896e3 100644 --- a/doc/cost-models/indexbytestring/plot.js +++ b/doc/cost-models/indexbytestring/plot.js @@ -35,7 +35,7 @@ function generateUrlFromBranch(branch) { function getFileUrls(baseUrl) { return { csv: `${baseUrl}/benching-conway.csv`, - json: `${baseUrl}/builtinCostModelC.json` + json: `${baseUrl}/builtinCostModelE.json` }; } diff --git a/doc/cost-models/insertcoin/index.html b/doc/cost-models/insertcoin/index.html index 369997f21e4..ca10a825dcf 100644 --- a/doc/cost-models/insertcoin/index.html +++ b/doc/cost-models/insertcoin/index.html @@ -21,6 +21,7 @@
  • ListToArray
  • LengthOfArray
  • IndexArray
  • +
  • MultiIndexArray
  • IndexByteString
  • @@ -111,7 +112,7 @@

    Plot Information

    diff --git a/doc/cost-models/insertcoin/plot.js b/doc/cost-models/insertcoin/plot.js index 335c84332f7..c9c9d22484e 100644 --- a/doc/cost-models/insertcoin/plot.js +++ b/doc/cost-models/insertcoin/plot.js @@ -35,7 +35,7 @@ function generateUrlFromBranch(branch) { function getFileUrls(baseUrl) { return { csv: `${baseUrl}/benching-conway.csv`, - json: `${baseUrl}/builtinCostModelC.json` + json: `${baseUrl}/builtinCostModelE.json` }; } diff --git a/doc/cost-models/lengthofarray/index.html b/doc/cost-models/lengthofarray/index.html index a9b26a54af2..96b2a109971 100644 --- a/doc/cost-models/lengthofarray/index.html +++ b/doc/cost-models/lengthofarray/index.html @@ -18,6 +18,7 @@
  • ListToArray
  • LengthOfArray
  • IndexArray
  • +
  • MultiIndexArray
  • IndexByteString
  • @@ -118,7 +119,7 @@

    Plot Information

    diff --git a/doc/cost-models/lengthofarray/plot.js b/doc/cost-models/lengthofarray/plot.js index f3318e746ce..1c8aca40cac 100644 --- a/doc/cost-models/lengthofarray/plot.js +++ b/doc/cost-models/lengthofarray/plot.js @@ -35,7 +35,7 @@ function generateUrlFromBranch(branch) { function getFileUrls(baseUrl) { return { csv: `${baseUrl}/benching-conway.csv`, - json: `${baseUrl}/builtinCostModelC.json` + json: `${baseUrl}/builtinCostModelE.json` }; } diff --git a/doc/cost-models/listtoarray/index.html b/doc/cost-models/listtoarray/index.html index 79b671c4351..b130db78dab 100644 --- a/doc/cost-models/listtoarray/index.html +++ b/doc/cost-models/listtoarray/index.html @@ -18,6 +18,7 @@
  • ListToArray
  • LengthOfArray
  • IndexArray
  • +
  • MultiIndexArray
  • IndexByteString
  • @@ -118,7 +119,7 @@

    Plot Information

    diff --git a/doc/cost-models/listtoarray/plot.js b/doc/cost-models/listtoarray/plot.js index 0e662273655..ed6040f5aa1 100644 --- a/doc/cost-models/listtoarray/plot.js +++ b/doc/cost-models/listtoarray/plot.js @@ -35,7 +35,7 @@ function generateUrlFromBranch(branch) { function getFileUrls(baseUrl) { return { csv: `${baseUrl}/benching-conway.csv`, - json: `${baseUrl}/builtinCostModelC.json` + json: `${baseUrl}/builtinCostModelE.json` }; } diff --git a/doc/cost-models/lookupcoin/index.html b/doc/cost-models/lookupcoin/index.html index 14aec23a63c..df4df0639ce 100644 --- a/doc/cost-models/lookupcoin/index.html +++ b/doc/cost-models/lookupcoin/index.html @@ -18,6 +18,7 @@
  • ListToArray
  • LengthOfArray
  • IndexArray
  • +
  • MultiIndexArray
  • IndexByteString
  • @@ -118,7 +119,7 @@

    Plot Information

    diff --git a/doc/cost-models/lookupcoin/plot.js b/doc/cost-models/lookupcoin/plot.js index 582afd6854d..e41dcd0bafe 100644 --- a/doc/cost-models/lookupcoin/plot.js +++ b/doc/cost-models/lookupcoin/plot.js @@ -35,7 +35,7 @@ function generateUrlFromBranch(branch) { function getFileUrls(baseUrl) { return { csv: `${baseUrl}/benching-conway.csv`, - json: `${baseUrl}/builtinCostModelC.json` + json: `${baseUrl}/builtinCostModelE.json` }; } diff --git a/doc/cost-models/multiindexarray/index.html b/doc/cost-models/multiindexarray/index.html new file mode 100644 index 00000000000..9015ff206a0 --- /dev/null +++ b/doc/cost-models/multiindexarray/index.html @@ -0,0 +1,166 @@ + + + + + + MultiIndexArray - Plutus Cost Model Visualization + + + + + + +
    +

    MultiIndexArray Cost Model Visualization

    +

    + Interactive visualization of benchmark data and fitted cost model for the MultiIndexArray builtin function. + This function looks up a list of indices (needles) in a Plutus array (haystack); the cost is linear in the + number of indices and independent of the array size, so the model plane in the 3D view is flat along the + haystack axis. The number of indices is limited, because beyond that limit the execution time is not + predictable from the index count alone; the histogram below shows the per-index time distribution. +

    + +
    +

    Data Source Configuration

    +
    +
    + +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + +
    +

    Plot Controls

    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + +
    +
    +
    Loading data and generating plot...
    +
    + +
    +

    Plot Information

    + +
    +
    +
    X-axis:
    +
    Haystack size (array length, log scale)
    + +
    Y-axis:
    +
    Needles size (length of the index list)
    + +
    Z-axis:
    +
    Time (nanoseconds)
    + +
    Description:
    +
    Each point is one benchmark. The model plane is flat along the + haystack axis and slopes along the needles axis.
    +
    +
    + +
    +
    Cost Model Type:
    +
    Loading...
    + +
    Model Formula (net):
    +
    Loading...
    + +
    Overhead:
    +
    Loading...
    +
    + +
    +
    +
    Data points:
    +
    -
    + +
    Index count range:
    +
    -
    + +
    Array size range:
    +
    -
    + +
    Time range:
    +
    -
    +
    +
    + +
    +
    Data sources:
    +
    benching-conway.csv
    +
    builtinCostModelE.json + (PlutusV3 from PV 11 / vanRossem)
    +
    +
    +
    + +

    Per-index time distribution

    +

    + Net time per index, (t − overhead) / indexCount, for benchmarks with at least the + selected number of indices. The dashed line marks the net slope of the fitted model. +

    +
    +
    Loading...
    +
    +
    + + + + + + + diff --git a/doc/cost-models/multiindexarray/plot.js b/doc/cost-models/multiindexarray/plot.js new file mode 100644 index 00000000000..6fcbd32518d --- /dev/null +++ b/doc/cost-models/multiindexarray/plot.js @@ -0,0 +1,489 @@ +// MultiIndexArray plot configuration and rendering +// +// Benchmark names are MultiIndexArray//, so +// args[0] = array size (haystack) and args[1] = index count (needles). The +// cost model is linear_in_y: linear in the number of indices, independent of +// the array size. +// +// The main view is a 3D scatter (haystack size, needles size, time) with the +// fitted model drawn as a plane: flat along the haystack axis, sloping along +// the needles axis. A histogram of net per-index times at large index counts +// makes the distribution of the benchmark results (and any multi-modality) +// directly visible. + +// Configuration +const FUNCTION_NAME = 'MultiIndexArray'; // CSV uses PascalCase +const COST_MODEL_NAME = 'multiIndexArray'; // JSON uses camelCase +const ARITY = 2; + +// Default configuration +const DEFAULT_BRANCH = 'master'; +const URL_TEMPLATE = 'https://raw.githubusercontent.com/IntersectMBO/plutus/{BRANCH}/plutus-core/cost-model/data'; + +// LocalStorage keys +const STORAGE_KEYS = { + BRANCH: 'plutus-viz-branch', + CSV_URL: 'plutus-viz-csv-url', + JSON_URL: 'plutus-viz-json-url', + DATA_SOURCE_COLLAPSED: 'plutus-viz-data-source-collapsed', + PLOT_CONTROLS_COLLAPSED: 'plutus-viz-plot-controls-collapsed' +}; + +// Global state +let benchmarkData = []; +let modelPredictions = []; +let costModel = null; +let overhead = 0; +let showModel = true; +let zAxisMode = 'zero'; +let histThreshold = 2000; + +// Generate URL from template and branch +function generateUrlFromBranch(branch) { + return URL_TEMPLATE.replace('{BRANCH}', branch); +} + +// Get file URLs. Variant E is the PlutusV3 cost model from the vanRossem +// hard fork (PV 11) onwards (C is the previous V3 era). +function getFileUrls(baseUrl) { + return { + csv: `${baseUrl}/benching-conway.csv`, + json: `${baseUrl}/builtinCostModelE.json` + }; +} + +// Load settings from localStorage (URL param takes precedence) +function loadSettings() { + const urlBranch = getBranchFromUrl(); + return { + branch: urlBranch || localStorage.getItem(STORAGE_KEYS.BRANCH) || DEFAULT_BRANCH, + csvUrl: localStorage.getItem(STORAGE_KEYS.CSV_URL) || '', + jsonUrl: localStorage.getItem(STORAGE_KEYS.JSON_URL) || '', + collapsed: localStorage.getItem(STORAGE_KEYS.DATA_SOURCE_COLLAPSED) === 'true' + }; +} + +// Save settings to localStorage +function saveSettings(branch, csvUrl, jsonUrl) { + localStorage.setItem(STORAGE_KEYS.BRANCH, branch); + localStorage.setItem(STORAGE_KEYS.CSV_URL, csvUrl); + localStorage.setItem(STORAGE_KEYS.JSON_URL, jsonUrl); +} + +// Update URL fields based on branch name +function updateUrlsFromBranch() { + const branchInput = document.getElementById('branch-name'); + const csvInput = document.getElementById('csv-url'); + const jsonInput = document.getElementById('json-url'); + + const branch = branchInput.value.trim() || DEFAULT_BRANCH; + const baseUrl = generateUrlFromBranch(branch); + const urls = getFileUrls(baseUrl); + + csvInput.value = urls.csv; + jsonInput.value = urls.json; +} + +// Initialize +async function init() { + // Load saved settings + const settings = loadSettings(); + + // Set up collapsible data source section + const dataSourceControls = document.getElementById('data-source-controls'); + const dataSourceToggle = document.getElementById('data-source-toggle'); + + if (settings.collapsed) { + dataSourceControls.classList.add('collapsed'); + } + + dataSourceToggle.addEventListener('click', () => { + const isCollapsed = dataSourceControls.classList.toggle('collapsed'); + localStorage.setItem(STORAGE_KEYS.DATA_SOURCE_COLLAPSED, isCollapsed); + }); + + // Set up collapsible plot controls section + const plotControls = document.getElementById('plot-controls'); + const plotControlsToggle = document.getElementById('plot-controls-toggle'); + + if (localStorage.getItem(STORAGE_KEYS.PLOT_CONTROLS_COLLAPSED) === 'true') { + plotControls.classList.add('collapsed'); + } + + plotControlsToggle.addEventListener('click', () => { + const isCollapsed = plotControls.classList.toggle('collapsed'); + localStorage.setItem(STORAGE_KEYS.PLOT_CONTROLS_COLLAPSED, isCollapsed); + }); + + // Set up form inputs + const branchInput = document.getElementById('branch-name'); + const csvInput = document.getElementById('csv-url'); + const jsonInput = document.getElementById('json-url'); + + // Initialize with saved or default values + branchInput.value = settings.branch; + + if (settings.csvUrl && settings.jsonUrl) { + csvInput.value = settings.csvUrl; + jsonInput.value = settings.jsonUrl; + } else { + updateUrlsFromBranch(); + } + + // Update URLs when branch name changes + branchInput.addEventListener('input', updateUrlsFromBranch); + + // Load data button + const reloadButton = document.getElementById('reload-data'); + reloadButton.addEventListener('click', async () => { + const branch = branchInput.value.trim() || DEFAULT_BRANCH; + const csvUrl = csvInput.value.trim(); + const jsonUrl = jsonInput.value.trim(); + + // Save settings + saveSettings(branch, csvUrl, jsonUrl); + + // Load data + await loadAndRenderData(); + }); + + // Set up plot control event listeners (only once) + setupControls(); + + // Set up copy link button + document.getElementById('copy-link').addEventListener('click', () => { + const branch = document.getElementById('branch-name').value.trim() || DEFAULT_BRANCH; + const url = new URL(window.location.href); + url.search = ''; + url.searchParams.set('branch', branch); + navigator.clipboard.writeText(url.toString()); + // Brief visual feedback + const btn = document.getElementById('copy-link'); + const original = btn.textContent; + btn.textContent = 'Copied!'; + setTimeout(() => btn.textContent = original, 1500); + }); + + // Initial load + await loadAndRenderData(); +} + +// Load and render data +async function loadAndRenderData() { + // Show loading state + const container = document.getElementById('plot-container'); + container.innerHTML = '
    Loading data and generating plot...
    '; + + try { + // Get URLs from form inputs + const csvUrl = document.getElementById('csv-url').value.trim(); + const jsonUrl = document.getElementById('json-url').value.trim(); + + if (!csvUrl || !jsonUrl) { + showError('Please provide both CSV and JSON file URLs'); + return; + } + + // Load data + const { parsedData, costModelJson, overheadMap } = await loadData(csvUrl, jsonUrl); + + // Filter for this function + benchmarkData = filterByFunction(parsedData, FUNCTION_NAME); + + if (benchmarkData.length === 0) { + showError(`No benchmark data found for ${FUNCTION_NAME}`); + return; + } + + // Extract cost model + costModel = extractCostModel(costModelJson, COST_MODEL_NAME); + + // Get overhead + overhead = overheadMap[ARITY] || 0; + + // Generate model predictions + if (costModel) { + modelPredictions = generateModelPredictions(benchmarkData, costModel, overhead); + } + + // Update info panel + updateInfoPanel(); + + // Render plots + renderPlot(); + renderHistogram(); + + } catch (error) { + console.error('Initialization error:', error); + showError(`Failed to load data. Check console for details. Error: ${error.message}`); + } +} + +function updateInfoPanel() { + // Calculate stats over the index count (second argument) + const stats = calculateStats(benchmarkData, 1); + + // Update data points + document.getElementById('info-data-points').textContent = stats.dataPoints; + + // Update ranges + if (stats.minArg !== undefined) { + document.getElementById('info-x-range').textContent = `${stats.minArg} - ${stats.maxArg}`; + } + + const arraySizes = benchmarkData.map(d => d.args[0]); + document.getElementById('info-array-range').textContent = + `${Math.min(...arraySizes)} - ${Math.max(...arraySizes)}`; + + document.getElementById('info-time-range').textContent = stats.timeRange; + + // Update model info + if (costModel) { + document.getElementById('info-model-type').textContent = costModel.modelType; + document.getElementById('info-model-formula').textContent = formatModelFormula( + costModel.modelType, + costModel.coefficients + ); + } else { + document.getElementById('info-model-type').textContent = 'Not available'; + document.getElementById('info-model-formula').textContent = 'Cost model not found'; + } + + // Update overhead + if (overhead > 0) { + document.getElementById('info-overhead').textContent = + `${overhead.toFixed(2)} ns (arity ${ARITY}) added to predictions`; + } else { + document.getElementById('info-overhead').textContent = 'Not calculated'; + } +} + +// The model plane: z = intercept + slope * needles (+ overhead), flat along +// the haystack axis. +function modelPlaneTrace() { + const haystacks = [...new Set(benchmarkData.map(d => d.args[0]))].sort((a, b) => a - b); + const maxNeedles = Math.max(...benchmarkData.map(d => d.args[1])); + const steps = 30; + const needles = Array.from({ length: steps + 1 }, (_, i) => Math.max(1, Math.round(i * maxNeedles / steps))); + + const intercept = costModel.coefficients.intercept || 0; + const slope = costModel.coefficients.slope || 0; + + // z[i][j] corresponds to (y = needles[i], x = haystacks[j]) + const z = needles.map(n => haystacks.map(() => (intercept + slope * n) / 1000 + overhead)); + + return { + x: haystacks, + y: needles, + z: z, + type: 'surface', + name: 'Model Prediction', + showscale: false, + opacity: 0.35, + colorscale: [[0, '#E53E3E'], [1, '#E53E3E']], + hovertemplate: 'needles: %{y}
    model: %{z:.0f} ns' + }; +} + +function renderPlot() { + const benchmarkTrace = { + x: benchmarkData.map(d => d.args[0]), + y: benchmarkData.map(d => d.args[1]), + z: benchmarkData.map(d => d.time), + mode: 'markers', + type: 'scatter3d', + name: 'Benchmark Data', + hovertemplate: 'haystack: %{x}
    needles: %{y}
    time: %{z:.0f} ns', + marker: { + size: 3.5, + color: '#0033AD', + opacity: 0.75 + } + }; + + const traces = [benchmarkTrace]; + + if (showModel && costModel && costModel.modelType === 'linear_in_y') { + traces.push(modelPlaneTrace()); + } else if (showModel && modelPredictions.length > 0) { + // Fallback for other model shapes: prediction markers at the data points + traces.push({ + x: modelPredictions.map(d => d.args[0]), + y: modelPredictions.map(d => d.args[1]), + z: modelPredictions.map(d => d.predictedTime), + mode: 'markers', + type: 'scatter3d', + name: 'Model Predictions', + marker: { size: 3.5, color: '#E53E3E', opacity: 0.4, symbol: 'x' } + }); + } + + const benchmarkZ = benchmarkTrace.z; + + // Layout configuration + const layout = { + title: { + text: `${FUNCTION_NAME} - Benchmark vs Model (3D)`, + font: { size: 20 } + }, + scene: { + xaxis: { + title: 'Haystack Size (array, log)', + type: 'log', + gridcolor: '#E0E0E0' + }, + yaxis: { + title: 'Needles Size (index count)', + gridcolor: '#E0E0E0' + }, + zaxis: { + title: 'Time (nanoseconds)', + gridcolor: '#E0E0E0' + }, + camera: { + eye: { x: 1.7, y: -1.7, z: 0.6 } + } + }, + showlegend: true, + legend: { + x: 0.02, + y: 0.98, + bgcolor: 'rgba(255, 255, 255, 0.8)', + bordercolor: '#BDC3C7', + borderwidth: 1 + }, + paper_bgcolor: 'rgba(0,0,0,0)' + }; + + // Set Z-axis range based on mode + if (zAxisMode === 'zero') { + layout.scene.zaxis.range = [0, Math.max(...benchmarkZ) * 1.1]; + } else { + const minZ = Math.min(...benchmarkZ); + const maxZ = Math.max(...benchmarkZ); + const padding = (maxZ - minZ) * 0.1; + layout.scene.zaxis.range = [minZ - padding, maxZ + padding]; + } + + // Config + const config = { + responsive: true, + displayModeBar: true, + displaylogo: false + }; + + // Render + const container = document.getElementById('plot-container'); + container.innerHTML = ''; + Plotly.newPlot('plot-container', traces, layout, config); +} + +function renderHistogram() { + const container = document.getElementById('hist-container'); + + // Net per-index time for large index counts + const points = benchmarkData.filter(d => d.args[1] >= histThreshold); + if (points.length === 0) { + container.innerHTML = `

    No benchmarks with index count ≥ ${histThreshold}

    `; + return; + } + + const perIndex = points.map(d => (d.time - overhead) / d.args[1]); + + const histTrace = { + x: perIndex, + type: 'histogram', + name: `per-index time (y ≥ ${histThreshold})`, + xbins: { size: 1 }, + marker: { + color: '#0033AD', + opacity: 0.8 + } + }; + + const layout = { + title: { + text: `Distribution of net per-index time (index count ≥ ${histThreshold}, n = ${points.length})`, + font: { size: 18 } + }, + xaxis: { + title: 'Net time per index (nanoseconds)', + gridcolor: '#E0E0E0' + }, + yaxis: { + title: 'Benchmark count', + gridcolor: '#E0E0E0' + }, + bargap: 0.05, + plot_bgcolor: '#FAFAFA', + paper_bgcolor: 'rgba(0,0,0,0)', + shapes: [], + annotations: [] + }; + + // Mark the model's net slope (ps -> ns) on the distribution + if (costModel && (costModel.modelType === 'linear_in_y')) { + const slopeNs = (costModel.coefficients.slope || 0) / 1000; + layout.shapes.push({ + type: 'line', + x0: slopeNs, x1: slopeNs, + y0: 0, y1: 1, + yref: 'paper', + line: { color: '#E53E3E', width: 2, dash: 'dash' } + }); + layout.annotations.push({ + x: slopeNs, + y: 1, + yref: 'paper', + yanchor: 'bottom', + text: `model slope: ${slopeNs.toFixed(1)} ns/index`, + showarrow: false, + font: { color: '#E53E3E' } + }); + } + + const config = { + responsive: true, + displayModeBar: true, + displaylogo: false + }; + + container.innerHTML = ''; + Plotly.newPlot('hist-container', [histTrace], layout, config); +} + +function setupControls() { + // Show/hide model checkbox + const showModelCheckbox = document.getElementById('show-model'); + showModelCheckbox.addEventListener('change', (e) => { + showModel = e.target.checked; + renderPlot(); + }); + + // Z-axis mode selector + const zAxisModeSelect = document.getElementById('z-axis-mode'); + zAxisModeSelect.addEventListener('change', (e) => { + zAxisMode = e.target.value; + renderPlot(); + }); + + // Histogram threshold selector + const histThresholdSelect = document.getElementById('hist-threshold'); + histThresholdSelect.addEventListener('change', (e) => { + histThreshold = parseInt(e.target.value, 10); + renderHistogram(); + }); +} + +function showError(message) { + const container = document.getElementById('plot-container'); + container.innerHTML = ` +
    +

    Error Loading Data

    +

    ${message}

    +
    + `; +} + +// Start initialization when page loads +document.addEventListener('DOMContentLoaded', init); diff --git a/doc/cost-models/scalevalue/index.html b/doc/cost-models/scalevalue/index.html index a90ea6a96a4..de472c0c792 100644 --- a/doc/cost-models/scalevalue/index.html +++ b/doc/cost-models/scalevalue/index.html @@ -21,6 +21,7 @@
  • ListToArray
  • LengthOfArray
  • IndexArray
  • +
  • MultiIndexArray
  • IndexByteString
  • @@ -111,7 +112,7 @@

    Plot Information

    diff --git a/doc/cost-models/scalevalue/plot.js b/doc/cost-models/scalevalue/plot.js index e75c32dd9bb..e143b0326e7 100644 --- a/doc/cost-models/scalevalue/plot.js +++ b/doc/cost-models/scalevalue/plot.js @@ -35,7 +35,7 @@ function generateUrlFromBranch(branch) { function getFileUrls(baseUrl) { return { csv: `${baseUrl}/benching-conway.csv`, - json: `${baseUrl}/builtinCostModelC.json` + json: `${baseUrl}/builtinCostModelE.json` }; } diff --git a/doc/cost-models/shared/utils.js b/doc/cost-models/shared/utils.js index 6d3b62f7694..786d4b06f6d 100644 --- a/doc/cost-models/shared/utils.js +++ b/doc/cost-models/shared/utils.js @@ -239,7 +239,7 @@ function evaluateCostModel(modelType, coefficients, args) { } /** - * Extract cost model from builtinCostModelC.json for a specific function + * Extract cost model from builtinCostModelE.json for a specific function * Returns { modelType, coefficients } or null if not found */ function extractCostModel(costModelJson, functionName) { diff --git a/doc/cost-models/unionvalue/index.html b/doc/cost-models/unionvalue/index.html index 6355c4b206f..b54196f287a 100644 --- a/doc/cost-models/unionvalue/index.html +++ b/doc/cost-models/unionvalue/index.html @@ -21,6 +21,7 @@
  • ListToArray
  • LengthOfArray
  • IndexArray
  • +
  • MultiIndexArray
  • IndexByteString
  • @@ -113,7 +114,7 @@

    Plot Information

    diff --git a/doc/cost-models/unionvalue/plot.js b/doc/cost-models/unionvalue/plot.js index f67dadce4b3..17904ffc9ac 100644 --- a/doc/cost-models/unionvalue/plot.js +++ b/doc/cost-models/unionvalue/plot.js @@ -35,7 +35,7 @@ function generateUrlFromBranch(branch) { function getFileUrls(baseUrl) { return { csv: `${baseUrl}/benching-conway.csv`, - json: `${baseUrl}/builtinCostModelC.json` + json: `${baseUrl}/builtinCostModelE.json` }; } diff --git a/doc/cost-models/unvaluedata/index.html b/doc/cost-models/unvaluedata/index.html index 3c576666959..633be6f3744 100644 --- a/doc/cost-models/unvaluedata/index.html +++ b/doc/cost-models/unvaluedata/index.html @@ -18,6 +18,7 @@
  • ListToArray
  • LengthOfArray
  • IndexArray
  • +
  • MultiIndexArray
  • IndexByteString
  • @@ -118,7 +119,7 @@

    Plot Information

    diff --git a/doc/cost-models/unvaluedata/plot.js b/doc/cost-models/unvaluedata/plot.js index 9a6d9fe7e00..d7d450522d0 100644 --- a/doc/cost-models/unvaluedata/plot.js +++ b/doc/cost-models/unvaluedata/plot.js @@ -35,7 +35,7 @@ function generateUrlFromBranch(branch) { function getFileUrls(baseUrl) { return { csv: `${baseUrl}/benching-conway.csv`, - json: `${baseUrl}/builtinCostModelC.json` + json: `${baseUrl}/builtinCostModelE.json` }; } diff --git a/doc/cost-models/valuecontains/index.html b/doc/cost-models/valuecontains/index.html index cb08638a1ae..1032af7847c 100644 --- a/doc/cost-models/valuecontains/index.html +++ b/doc/cost-models/valuecontains/index.html @@ -18,6 +18,7 @@
  • ListToArray
  • LengthOfArray
  • IndexArray
  • +
  • MultiIndexArray
  • IndexByteString
  • @@ -124,7 +125,7 @@

    Plot Information

    diff --git a/doc/cost-models/valuecontains/plot.js b/doc/cost-models/valuecontains/plot.js index a5128285e72..ccf9e145efd 100644 --- a/doc/cost-models/valuecontains/plot.js +++ b/doc/cost-models/valuecontains/plot.js @@ -35,7 +35,7 @@ function generateUrlFromBranch(branch) { function getFileUrls(baseUrl) { return { csv: `${baseUrl}/benching-conway.csv`, - json: `${baseUrl}/builtinCostModelC.json` + json: `${baseUrl}/builtinCostModelE.json` }; } diff --git a/doc/cost-models/valuedata/index.html b/doc/cost-models/valuedata/index.html index 4f991ba1059..0a0e0f989b1 100644 --- a/doc/cost-models/valuedata/index.html +++ b/doc/cost-models/valuedata/index.html @@ -18,6 +18,7 @@
  • ListToArray
  • LengthOfArray
  • IndexArray
  • +
  • MultiIndexArray
  • IndexByteString
  • @@ -118,7 +119,7 @@

    Plot Information

    diff --git a/doc/cost-models/valuedata/plot.js b/doc/cost-models/valuedata/plot.js index aed9be39d04..e7d227afbe4 100644 --- a/doc/cost-models/valuedata/plot.js +++ b/doc/cost-models/valuedata/plot.js @@ -35,7 +35,7 @@ function generateUrlFromBranch(branch) { function getFileUrls(baseUrl) { return { csv: `${baseUrl}/benching-conway.csv`, - json: `${baseUrl}/builtinCostModelC.json` + json: `${baseUrl}/builtinCostModelE.json` }; }