Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions doc/cost-models/policies/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Policies - Plutus Cost Model Visualization</title>
<link rel="stylesheet" href="../shared/styles.css">
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
</head>
<body>
<nav></nav>

<div class="container">
<h1>Policies Cost Model Visualization</h1>
<p>
Interactive visualization of benchmark data and fitted cost model for the <code>Policies</code> builtin function.
This function returns the currency symbols of a <code>Value</code> in ascending order.
</p>

<div class="controls" id="data-source-controls">
<h3 id="data-source-toggle">Data Source Configuration</h3>
<div class="controls-content">
<div class="control-group-vertical">
<label for="branch-name">Branch name:</label>
<div class="branch-input-row">
<input type="text" id="branch-name" placeholder="master">
<button id="copy-link" title="Copy shareable link">Copy Link</button>
</div>
</div>
<div class="control-group-vertical">
<label for="csv-url">CSV file URL:</label>
<input type="text" id="csv-url" style="width: 100%; font-family: monospace;">
</div>
<div class="control-group-vertical">
<label for="json-url">JSON file URL:</label>
<input type="text" id="json-url" style="width: 100%; font-family: monospace;">
</div>
<div class="control-group">
<button id="reload-data">Load Data</button>
</div>
</div>
</div>

<div class="controls" id="plot-controls">
<h3 id="plot-controls-toggle">Plot Controls</h3>
<div class="controls-content">
<div class="control-group">
<input type="checkbox" id="show-model" checked>
<label for="show-model">Show model predictions</label>
</div>
<div class="control-group">
<label for="y-axis-mode">Y-axis range:</label>
<select id="y-axis-mode">
<option value="zero">Start from 0</option>
<option value="auto">Auto-scale from min</option>
</select>
</div>
</div>
</div>

<div class="plot-wrapper">
<div id="plot-container">
<div class="loading">Loading data and generating plot...</div>
</div>

<div class="info-panel">
<h3>Plot Information</h3>

<div class="info-section">
<dl>
<dt>X-axis:</dt>
<dd id="info-x-axis">Value Size</dd>

<dt>Y-axis:</dt>
<dd id="info-y-axis">Time (nanoseconds)</dd>

<dt>Description:</dt>
<dd id="info-description">Each point represents one benchmark run</dd>
</dl>
</div>

<div class="info-section">
<dt>Cost Model Type:</dt>
<dd id="info-model-type">Loading...</dd>

<dt>Model Formula (net):</dt>
<dd class="formula" id="info-model-formula">Loading...</dd>

<dt>Overhead:</dt>
<dd id="info-overhead">Loading...</dd>
</div>

<div class="info-section">
<dl>
<dt>Data points:</dt>
<dd id="info-data-points">-</dd>

<dt>Value Size range:</dt>
<dd id="info-x-range">-</dd>

<dt>Time range:</dt>
<dd id="info-time-range">-</dd>
</dl>
</div>

<div class="info-section">
<dl id="info-data-sources"></dl>
</div>
</div>
</div>
</div>

<footer></footer>

<script src="../shared/utils.js"></script>
<script src="plot.js"></script>
</body>
</html>
171 changes: 171 additions & 0 deletions doc/cost-models/policies/plot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// Policies plot configuration and rendering

// Configuration
const FUNCTION_NAME = 'Policies'; // CSV uses PascalCase
const COST_MODEL_NAME = 'policies'; // JSON uses camelCase
const ARITY = 1;

// Global state
let benchmarkData = [];
let modelPredictions = [];
let costModel = null;
let overhead = 0;
let showModel = true;
let yAxisMode = 'zero';

setupCostModelPage({
slug: 'policies',
functionName: FUNCTION_NAME,
costModelName: COST_MODEL_NAME,
arity: ARITY,
render(data) {
({ benchmarkData, costModel, overhead, modelPredictions } = data);
updateInfoPanel();
renderPlot();
},
setupControls
});

function updateInfoPanel() {
// Calculate stats
const stats = calculateStats(benchmarkData, 0);

// 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}`;
}

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';
}
}

function renderPlot() {
// Prepare benchmark trace
const benchmarkX = benchmarkData.map(d => d.args[0]);
const benchmarkY = benchmarkData.map(d => d.time);

const benchmarkTrace = {
x: benchmarkX,
y: benchmarkY,
mode: 'markers',
type: 'scatter',
name: 'Benchmark Data',
marker: {
size: 6,
color: '#0033AD',
opacity: 0.7
}
};

const traces = [benchmarkTrace];

// Prepare model trace if available
if (showModel && modelPredictions.length > 0) {
const modelX = modelPredictions.map(d => d.args[0]);
const modelY = modelPredictions.map(d => d.predictedTime);

const modelTrace = {
x: modelX,
y: modelY,
mode: 'markers',
type: 'scatter',
name: 'Model Predictions',
marker: {
size: 6,
color: '#E53E3E',
opacity: 0.4,
symbol: 'x'
}
};

traces.push(modelTrace);
}

// Layout configuration
const layout = {
title: {
text: `${FUNCTION_NAME} - Benchmark vs Model`,
font: { size: 20 }
},
xaxis: {
title: 'Value Size',
gridcolor: '#E0E0E0'
},
yaxis: {
title: 'Time (nanoseconds)',
gridcolor: '#E0E0E0'
},
hovermode: 'closest',
showlegend: true,
legend: {
x: 0.02,
y: 0.98,
bgcolor: 'rgba(255, 255, 255, 0.8)',
bordercolor: '#BDC3C7',
borderwidth: 1
},
plot_bgcolor: '#FAFAFA',
paper_bgcolor: 'rgba(0,0,0,0)'
};

// Set Y-axis range based on mode
if (yAxisMode === 'zero') {
layout.yaxis.range = [0, Math.max(...benchmarkY) * 1.1];
} else {
const minY = Math.min(...benchmarkY);
const maxY = Math.max(...benchmarkY);
const padding = (maxY - minY) * 0.1;
layout.yaxis.range = [minY - padding, maxY + padding];
}

// Config
const config = {
responsive: true,
displayModeBar: true,
displaylogo: false
};

// Render
// Clear loading message
const container = document.getElementById('plot-container');
container.innerHTML = '';
Plotly.newPlot('plot-container', traces, layout, config);
}

function setupControls() {
// Show/hide model checkbox
const showModelCheckbox = document.getElementById('show-model');
showModelCheckbox.addEventListener('change', (e) => {
showModel = e.target.checked;
renderPlot();
});

// Y-axis mode selector
const yAxisModeSelect = document.getElementById('y-axis-mode');
yAxisModeSelect.addEventListener('change', (e) => {
yAxisMode = e.target.value;
renderPlot();
});
}
1 change: 1 addition & 0 deletions doc/cost-models/shared/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ const PAGES = [
['insertcoin', 'InsertCoin'],
['unionvalue', 'UnionValue'],
['scalevalue', 'ScaleValue'],
['policies', 'Policies'],
['listtoarray', 'ListToArray'],
['lengthofarray', 'LengthOfArray'],
['indexarray', 'IndexArray'],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Added

- Cost model for the `policies` builtin ([CIP-0168](https://cips.cardano.org/cip/CIP-0168)), with four new cost model parameters. Linear in the size of the `Value`.
55 changes: 54 additions & 1 deletion plutus-core/cost-model/budgeting-bench/Benchmarks/Values.hs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,16 @@ import Data.Map.Strict qualified as Map
import Data.Word (Word8)
import GHC.Stack (HasCallStack)
import PlutusCore
( DefaultFun (InsertCoin, LookupCoin, ScaleValue, UnValueData, UnionValue, ValueContains, ValueData)
( DefaultFun
( InsertCoin
, LookupCoin
, Policies
, ScaleValue
, UnValueData
, UnionValue
, ValueContains
, ValueData
)
)
import PlutusCore.Builtin (BuiltinResult (BuiltinFailure, BuiltinSuccess, BuiltinSuccessWithLogs))

Expand Down Expand Up @@ -57,6 +66,7 @@ makeBenchmarks gen =
, insertCoinBenchmark gen
, unionValueBenchmark gen
, scaleValueBenchmark gen
, policiesBenchmark gen
]

----------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -368,6 +378,49 @@ scaleValueArgs gen = replicateM 200 $ do
value = buildValue policyIds [tokenName] amt
pure (scalar, value)

----------------------------------------------------------------------------------------------------
-- Policies ----------------------------------------------------------------------------------------

{- Note [Benchmarking policies on the one-token-per-policy diagonal]
`policies` is \(O(m)\) in the size of the outer map, but the argument is costed by
`ValueTotalSize` (the `ExMemoryUsage Value` instance the denotation uses), which measures
the total number of `(policy, token)` pairs. Those two agree only when every policy holds
exactly one token, so the generator fixes that shape: the resulting fit is keyed on a size
measure that equals the number of policies.

This is deliberately the worst case per unit of the size measure. For any other shape the
total size exceeds the number of policies, so the real cost is lower than the fitted model
predicts and the model over-charges. Benchmarking off the diagonal instead would fit a
shallower slope and under-charge the one-token-per-policy case, which is the failure
direction that matters.

`nf` rather than `whnf`, for the same reason as `valueData`: the result is a lazy list and
`whnf` would stop at the first cons cell.
-}
policiesBenchmark :: StdGen -> Benchmark
policiesBenchmark gen =
createOneTermBuiltinBenchWithWrapper_NF
ValueTotalSize
Policies
[]
(runBenchGen gen policiesArgs)

{-| Maximum number of policies for `policies` benchmarking, matching the bound used for
`valueData` so the generated `Value`s stay in the same size regime as the other
single-argument `Value` benchmarks. -}
maxPoliciesEntries :: Int
maxPoliciesEntries = Value.valueDataMaxSize

{-| Generate `Value`s holding one token per policy, so that total size equals the number of
policies. See Note [Benchmarking policies on the one-token-per-policy diagonal]. -}
policiesArgs :: StatefulGen g m => g -> m [Value]
policiesArgs gen =
(Value.empty :) <$> replicateM 100 do
numPolicyIds <- uniformRM (1, maxPoliciesEntries) gen
policyIds <- replicateM numPolicyIds (generateKey gen)
tokenName <- generateKey gen
pure $ buildValue policyIds [tokenName] (mkQuantity 1)

----------------------------------------------------------------------------------------------------
-- Value Generators --------------------------------------------------------------------------------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,12 @@ builtinMemoryModels =
-- the array; only the spine is new, at three words per cons cell. The nonzero
-- intercept keeps the cost nonzero for the empty index list.
paramMultiIndexArray = Id $ ModelTwoArgumentsLinearInY $ OneVariableLinearFunction 4 3
, -- `policies` returns the outer map's keys. The bytestrings are shared with the
-- `Value`, so only the list spine is new, at three words per cons cell (as for
-- `multiIndexArray`). The size measure is the total number of (policy, token) pairs,
-- which is at least the number of policies, so this over-charges values holding more
-- than one token per policy rather than under-charging any of them.
paramPolicies = Id $ ModelOneArgumentLinearInX $ OneVariableLinearFunction 4 3
}
where
identityFunction = OneVariableLinearFunction 0 1
Loading
Loading