Skip to content
Merged
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();
});
}
3 changes: 3 additions & 0 deletions doc/cost-models/shared/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,9 @@ const PAGES = [
'Returns the number of <code>(currency symbol, token name)</code> pairs in a Plutus ' +
'<code>Value</code> in O(1) time. ' +
'(2D visualization: Value Size vs Time - constant cost)'],
['policies', 'Policies',
'Returns the currency symbols of a Plutus <code>Value</code>; linear in the number ' +
'of policies. (2D visualization: Policy Count vs Time)'],
['listtoarray', 'ListToArray',
'Converts a Plutus list to an array representation. ' +
'(2D visualization: List Size vs Time)'],
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 number of policies in the `Value`.
50 changes: 50 additions & 0 deletions plutus-core/cost-model/budgeting-bench/Benchmarks/Values.hs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import PlutusCore
( AssetCount
, InsertCoin
, LookupCoin
, Policies
, ScaleValue
, UnValueData
, UnionValue
Expand All @@ -37,6 +38,7 @@ import PlutusCore.Builtin (BuiltinResult (BuiltinFailure, BuiltinSuccess, Builti
import PlutusCore.Evaluation.Machine.ExMemoryUsage
( DataNodeCount (..)
, ValueMaxDepth (..)
, ValueOuterSize (..)
, ValueTotalSize (..)
)
import PlutusCore.Value
Expand Down Expand Up @@ -67,6 +69,7 @@ makeBenchmarks gen =
, unionValueBenchmark gen
, scaleValueBenchmark gen
, assetCountBenchmark gen
, policiesBenchmark gen
]

----------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -390,6 +393,53 @@ assetCountBenchmark gen =
[]
(generateTestValues gen)

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

{- Note [Benchmarking policies]
`policies` is \(O(m)\) in the size of the outer map, which is exactly what
`ValueOuterSize` (the measure the denotation uses) reports, so the fit applies to any
shape of `Value`.

The number of policies is sampled uniformly on a log scale, which puts a similar number
of points in 1-10, 10-100, 100-1000 and so on. Sampling uniformly on the count itself
would put almost every point above 1000, leaving the sizes that occur on chain
unmeasured.

The token count per policy is random under a total-size cap: the inner maps are never
traversed, so it must not show in the measurements, and the fixed-size stacks (1000 and
5000 policies at several token counts) would make a violation visible as vertical spread
at a single x.
-}
policiesBenchmark :: StdGen -> Benchmark
policiesBenchmark gen =
createOneTermBuiltinBenchWithWrapper_NF ValueOuterSize Policies [] (runBenchGen gen policiesArgs)
where
-- 40k pairs: safely above the largest `Value` a script can build within the
-- CPU budget (roughly 14k `insertCoin` applications).
maxTotalSize :: Int
maxTotalSize = Value.valueDataMaxSize

policiesArgs :: StatefulGen g m => g -> m [Value]
policiesArgs g = do
randoms <- replicateM 100 do
u <- uniformRM (0 :: Double, log (fromIntegral maxTotalSize)) g
let numPolicies = min maxTotalSize (max 1 (round (exp u)))
numTokens <- uniformRM (1, maxTotalSize `div` numPolicies) g
generate g numPolicies numTokens
stacks <-
sequence
[ generate g m k
| (m, ks) <- [(1000, [1, 2, 5, 10, 20, 40]), (5000, [1, 2, 4, 8])]
, k <- ks
]
pure $ Value.empty : randoms <> stacks

generate :: StatefulGen g m => g -> Int -> Int -> m Value
generate g numPolicies numTokens = do
policyIds <- replicateM numPolicies (generateKey g)
tokenNames <- replicateM numTokens (generateKey g)
pure $ buildValue policyIds tokenNames (mkQuantity 1)

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,10 @@ builtinMemoryModels =
-- intercept keeps the cost nonzero for the empty index list.
paramMultiIndexArray = Id $ ModelTwoArgumentsLinearInY $ OneVariableLinearFunction 4 3
, paramAssetCount = Id $ ModelOneArgumentConstantCost 10
, -- `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 number of policies (`ValueOuterSize`).
paramPolicies = Id $ ModelOneArgumentLinearInX $ OneVariableLinearFunction 4 3
}
where
identityFunction = OneVariableLinearFunction 0 1
Loading
Loading