Skip to content
Closed
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
120 changes: 120 additions & 0 deletions doc/cost-models/keeppolicies/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KeepPolicies - 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>KeepPolicies Cost Model Visualization</h1>
<p>
Interactive 3D visualization of benchmark data and the fitted cost model for the
<code>KeepPolicies</code> builtin. The x-axis is the length of the policy list, the y-axis the total size
of the <code>Value</code>, and the z-axis execution time. Every benchmark point appears twice at the
same place on the floor: as a blue dot for what it measured, and as a red cross for what the model
charges for it. Where a cross sits above its dot the model overcharges that input; for a few of
the largest inputs the fitted plane sits below the measurement. The sizes are on log
axes because they are sampled log-uniformly, which leaves out the few points at p = 0 or n = 0.
</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="axis-scale">Axes:</label>
<select id="axis-scale">
<option value="log">Logarithmic</option>
<option value="linear">Linear</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>Data points:</dt>
<dd id="info-data-points">-</dd>

<dt>Policy list length (arg1):</dt>
<dd id="info-x-range">-</dd>

<dt>Value size range (arg2):</dt>
<dd id="info-y-range">-</dd>

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

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

<div class="info-section">
<h3>The two fits</h3>
<div id="fit-comparison">-</div>
</div>

<div class="info-section">
<dl>
<dt>Model type:</dt>
<dd id="info-model-type">-</dd>

<dt>Model formula:</dt>
<dd id="info-model-formula">-</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>
135 changes: 135 additions & 0 deletions doc/cost-models/keeppolicies/plot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// KeepPolicies plot configuration and rendering (3D)

const FUNCTION_NAME = 'KeepPolicies'; // CSV uses PascalCase
const COST_MODEL_NAME = 'keepPolicies'; // JSON uses camelCase
const ARITY = 2;

let benchmarkData = [];
let costModel = null;
let stockModel = null;
let overhead = 0;
let showModel = true;
let axisScale = 'log';

setupCostModelPage({
slug: 'keeppolicies',
functionName: FUNCTION_NAME,
costModelName: COST_MODEL_NAME,
arity: ARITY,
render(data) {
({ benchmarkData, costModel, overhead } = data);
// The plain `lm(t ~ x_mem + y_mem)` of models.R, refit in the browser on the same
// points the shipped fit was built from. The shipped model comes from that same fit
// (negative coefficients clamped), so the two blocks agreeing checks that the JSON
// matches the fit; the undercharge lines show where the plane sits below the data.
stockModel = fitLinearInXAndY(benchmarkData.filter(d => d.args[1] > 0), overhead);
updateInfoPanel();
renderPlot();
},
setupControls
});

function updateInfoPanel() {
const stats = calculateStats(benchmarkData);
document.getElementById('info-data-points').textContent = stats.dataPoints;

const xs = benchmarkData.map(d => d.args[0]);
const ys = benchmarkData.map(d => d.args[1]);
document.getElementById('info-x-range').textContent =
`${Math.min(...xs)} - ${Math.max(...xs)}`;
document.getElementById('info-y-range').textContent =
`${Math.min(...ys)} - ${Math.max(...ys)}`;
document.getElementById('info-time-range').textContent = stats.timeRange;
document.getElementById('info-overhead').textContent = overhead > 0
? `${overhead.toFixed(2)} ns (arity ${ARITY}) added to predictions`
: 'Not calculated';

// Summarize both fits over the population they were built on (the y = 0 controls are
// out of the fit, so they would only distort the undercharge and overcharge lines).
const fitted = benchmarkData.filter(d => d.args[1] > 0);
document.getElementById('fit-comparison').innerHTML =
fitSummary('The shipped model (from the cost-model JSON)',
costModel, fitted, overhead, ['p', 'n'])
+ fitSummary('The same least-squares fit recomputed from the CSV',
stockModel, fitted, overhead, ['p', 'n']);

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

/* The two argument sizes on the floor, time up. Every benchmark point appears twice at the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the floor? The x-y plane at the bottom? "Every benchmark point appears twice at the same place on the floor" makes it sound as if those points are actually on the plane, not above it.

same place on the floor, once as what it measured and once as what the model charges for it,
so the model is safe exactly where the red crosses sit above the blue dots. The sizes are
sampled log-uniformly, so a log floor is what spreads them out; the cost of that is the few
points at p = 0 or n = 0, which a log axis cannot place and which are out of the fit anyway. */
function plotTraces() {
const traces = [{
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',
marker: { size: 4, color: '#0033AD', opacity: 0.8 },
hovertemplate: 'p %{x}, n %{y}<br>measured %{z:.3s} ns<extra></extra>'
}];

if (showModel && costModel) {
traces.push({
x: benchmarkData.map(d => d.args[0]),
y: benchmarkData.map(d => d.args[1]),
z: benchmarkData.map(d => modelCharge(costModel, d.args, overhead)),
mode: 'markers',
type: 'scatter3d',
name: 'Model Predictions',
marker: { size: 4, color: '#E53E3E', opacity: 0.6, symbol: 'x' },
hovertemplate: 'p %{x}, n %{y}<br>charged %{z:.3s} ns<extra></extra>'
});
}
return traces;
}

const PLOT_CONFIG = { responsive: true, displayModeBar: true, displaylogo: false };

function renderPlot() {
ensurePlotPanel('plot-3d');
const scaled = axisScale;
const suffix = axisScale === 'log' ? ', log' : '';
Plotly.react('plot-3d', plotTraces(), {
title: { text: `${FUNCTION_NAME} - Benchmark vs Model (3D)`, font: { size: 20 } },
scene: {
xaxis: { title: `Policy list length (p${suffix})`, gridcolor: '#E0E0E0', type: scaled },
yaxis: { title: `Value total size (n${suffix})`, gridcolor: '#E0E0E0', type: scaled },
zaxis: { title: `Time (ns${suffix})`, gridcolor: '#E0E0E0', type: scaled },
camera: { eye: { x: 1.7, y: -1.7, z: 0.7 } }
},
showlegend: true,
legend: {
x: 0.02,
y: 0.98,
bgcolor: 'rgba(255, 255, 255, 0.8)',
bordercolor: '#BDC3C7',
borderwidth: 1
},
margin: { t: 60, b: 10, l: 10, r: 10 },
height: 700,
paper_bgcolor: 'rgba(0,0,0,0)'
}, PLOT_CONFIG);
}

function setupControls() {
document.getElementById('show-model').addEventListener('change', e => {
showModel = e.target.checked;
renderPlot();
});
document.getElementById('axis-scale').addEventListener('change', e => {
axisScale = e.target.value;
renderPlot();
});
}
112 changes: 112 additions & 0 deletions doc/cost-models/shared/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,115 @@ function generateModelPredictions(benchmarkData, costModel, overhead) {
return predictions;
}

/**
* Fit t = intercept + slope1*x + slope2*y by least squares, the way models.R's plain
* `lm(t ~ x_mem + y_mem)` does: on times with the call overhead taken off, and with a
* negative coefficient clamped to 1000 ps the way `adjustModels` clamps it.
*
* `points` are {args: [x, y], time} in nanoseconds; the result is in picoseconds, so it
* can be handed to `evaluateCostModel` alongside a model read from the JSON.
*/
function fitLinearInXAndY(points, overhead) {
const rows = points.map(p => [1, p.args[0], p.args[1]]);
const rhs = points.map(p => p.time - (overhead || 0));

// Normal equations, solved by Gauss-Jordan on the 3x4 augmented matrix.
const m = [0, 1, 2].map(i =>
[0, 1, 2].map(j => rows.reduce((acc, r, k) => acc + r[i] * r[j], 0))
.concat(rows.reduce((acc, r, k) => acc + r[i] * rhs[k], 0)));

for (let i = 0; i < 3; i++) {
let pivot = i;
for (let r = i + 1; r < 3; r++) {
if (Math.abs(m[r][i]) > Math.abs(m[pivot][i])) pivot = r;
}
[m[i], m[pivot]] = [m[pivot], m[i]];
if (m[i][i] === 0) return null;
const d = m[i][i];
for (let c = i; c < 4; c++) m[i][c] /= d;
for (let r = 0; r < 3; r++) {
if (r === i || m[r][i] === 0) continue;
const f = m[r][i];
for (let c = i; c < 4; c++) m[r][c] -= f * m[i][c];
}
}

// `adjustModels` in models.R replaces a negative coefficient with 1 ns, ie 1000 ps.
const clamp = v => (v < 0 ? 1000 : v);
return {
modelType: 'linear_in_x_and_y',
coefficients: {
intercept: clamp(m[0][3] * 1000),
slope1: clamp(m[1][3] * 1000),
slope2: clamp(m[2][3] * 1000)
}
};
}

/**
* A model's predicted total time in nanoseconds for one benchmark point, overhead
* included, or null when the model is missing or cannot be evaluated.
*/
function modelCharge(model, args, overhead) {
if (!model) return null;
const ps = evaluateCostModel(model.modelType, model.coefficients, args);
return ps === null ? null : ps / 1000 + overhead;
}

function median(values) {
if (values.length === 0) return 0;
const sorted = [...values].sort((a, b) => a - b);
return sorted[Math.floor(sorted.length / 2)];
}

/**
* One info-panel block per fit: coefficients, how many of the given points the model
* undercharges, and by how much. `points` should be the population the fit was built on,
* and `terms` names the two size variables in the charge line (e.g. ['p', 'n']).
*/
function fitSummary(name, model, points, overhead, terms) {
if (!model) return `<p>${name}: not available</p>`;
const c = model.coefficients;
const ratios = [];
let undercharged = 0;
let worstShortfall = 0;
let worstRatio = 1;
for (const d of points) {
const predicted = modelCharge(model, d.args, overhead);
if (predicted === null) return `<p>${name}: not evaluable (${model.modelType})</p>`;
ratios.push(predicted / d.time);
if (predicted < d.time) {
undercharged += 1;
worstShortfall = Math.max(worstShortfall, d.time - predicted);
worstRatio = Math.min(worstRatio, predicted / d.time);
}
}
const fmt = v => v.toLocaleString('en-US', { maximumFractionDigits: 0 });
return `
<p><strong>${name}</strong></p>
<dl>
<dt>Charge (ps):</dt>
<dd>${fmt(c.intercept)} + ${fmt(c.slope1)}&middot;${terms[0]} + ${fmt(c.slope2)}&middot;${terms[1]}</dd>
<dt>Undercharged points:</dt>
<dd>${undercharged} of ${points.length}${undercharged
? `, worst ${(worstShortfall / 1000).toFixed(1)} us (${worstRatio.toFixed(2)}x)`
: ''}</dd>
<dt>Median overcharge:</dt>
<dd>${median(ratios).toFixed(2)}x</dd>
</dl>`;
}

/* The shared loader replaces the contents of `#plot-container` with its own status message
before it calls a page's render, so the plot div has to be the page's to create. */
function ensurePlotPanel(id) {
if (document.getElementById(id)) return;
const container = document.getElementById('plot-container');
container.innerHTML = '';
const panel = document.createElement('div');
panel.id = id;
container.appendChild(panel);
}

/**
* Format model formula as human-readable string
*/
Expand Down Expand Up @@ -528,6 +637,9 @@ const PAGES = [
['policies', 'Policies',
'Returns the currency symbols of a Plutus <code>Value</code>; linear in the number ' +
'of policies. (2D visualization: Policy Count vs Time)'],
['keeppolicies', 'KeepPolicies',
'Retains only the listed currencies of a Plutus <code>Value</code>. ' +
'(3D visualization: List Length \u00d7 Value Size \u00d7 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

- A cost model for the `keepPolicies` builtin ([CIP-0168](https://cips.cardano.org/cip/CIP-0168)), with five new cost model parameters. Linear in the length of the policy list and in the total size of the `Value`.
Loading
Loading