package size calculator
This commit is contained in:
108
docs/index.html
108
docs/index.html
@@ -544,18 +544,24 @@
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>EMF metrics <span style="color:#4a5568">(CloudWatch embedded-metric-format on stdout — currently empty, will populate after improvement #3)</span></summary>
|
||||
<summary>EMF metrics <span style="color:#4a5568">(CloudWatch embedded-metric-format on stdout — populates when the function emits, e.g. <code>sign_pdfs_optimized</code>)</span></summary>
|
||||
<pre id="tester-emf"><span class="empty">No EMF metrics detected.</span></pre>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Packaging <span style="color:#4a5568">(deployment sizes from the live pod — function zips, shared layer, largest deps, vs AWS caps)</span></summary>
|
||||
<pre id="tester-packaging"><span class="empty">Loading…</span></pre>
|
||||
</details>
|
||||
|
||||
<h3>History</h3>
|
||||
<p class="lead" style="margin-bottom:8px">Click a row to view its full record. Cleared on page reload (FastAPI keeps the last 200 invocations server-side regardless).</p>
|
||||
<div id="tester-history-summary" style="font-family:'JetBrains Mono',monospace; font-size:12px; color:#b4bccf; margin-bottom:8px"></div>
|
||||
<table class="cmp-table tester-history">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th><th>Time</th><th>Function</th><th>Start</th>
|
||||
<th class="num">Init (ms)</th><th class="num">Duration (ms)</th>
|
||||
<th class="num">Max RSS (MB)</th><th>Status</th>
|
||||
<th class="num">Total (ms)</th><th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tester-history-body">
|
||||
@@ -667,6 +673,12 @@ function renderReport(rec) {
|
||||
const initLine = m.init_duration_ms != null
|
||||
? `<span class="k">Init Duration:</span> <span class="v">${m.init_duration_ms.toFixed(2)} ms</span>\n`
|
||||
: '';
|
||||
// GB-s and projected cost — AWS Lambda bills (memory_GB × duration_s).
|
||||
// Prices: $0.0000166667 per GB-s on x86, $0.0000133334 on arm64. +$0.20/1M requests.
|
||||
const gbS = (m.memory_size_mb / 1024) * (m.duration_ms / 1000);
|
||||
const costX86 = gbS * 0.0000166667;
|
||||
const costArm = gbS * 0.0000133334;
|
||||
const per1M = (cost) => (cost * 1_000_000 + 0.20).toFixed(2);
|
||||
$('tester-report').innerHTML = (
|
||||
`${tag} <span class="k">REPORT RequestId:</span> <span class="v">${rec.invocation_id}</span>\n` +
|
||||
`<span class="k">Function:</span> <span class="v">${rec.function}</span>\n` +
|
||||
@@ -674,7 +686,12 @@ function renderReport(rec) {
|
||||
`<span class="k">Billed Duration:</span> <span class="v">${m.billed_duration_ms} ms</span>\n` +
|
||||
`<span class="k">Memory Size:</span> <span class="v">${m.memory_size_mb} MB</span> ` +
|
||||
`<span class="k">Max Memory Used:</span> <span class="v">${m.max_memory_used_mb.toFixed(2)} MB</span>\n` +
|
||||
initLine
|
||||
initLine +
|
||||
`<span class="k">GB-seconds:</span> <span class="v">${gbS.toFixed(6)} GB-s</span>\n` +
|
||||
`<span class="k">Cost (x86):</span> <span class="v">$${costX86.toFixed(9)}</span> ` +
|
||||
`<span class="k">×1M:</span> <span class="v">$${per1M(costX86)}</span>\n` +
|
||||
`<span class="k">Cost (arm64):</span> <span class="v">$${costArm.toFixed(9)}</span> ` +
|
||||
`<span class="k">×1M:</span> <span class="v">$${per1M(costArm)}</span>`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -727,8 +744,28 @@ async function loadHistory() {
|
||||
} catch (e) { /* ignore on first load if backend is still booting */ }
|
||||
}
|
||||
|
||||
function _pctile(arr, p) {
|
||||
if (!arr.length) return null;
|
||||
const s = arr.slice().sort((a, b) => a - b);
|
||||
const i = Math.min(s.length - 1, Math.floor((p / 100) * s.length));
|
||||
return s[i];
|
||||
}
|
||||
|
||||
function _renderHistorySummary() {
|
||||
const el = $('tester-history-summary');
|
||||
if (!el) return;
|
||||
if (!_history.length) { el.textContent = ''; return; }
|
||||
const cold = _history.filter(h => h.cold_start && h.init_duration_ms != null).map(h => h.init_duration_ms);
|
||||
const warm = _history.filter(h => !h.cold_start).map(h => h.duration_ms);
|
||||
const parts = [`<span class="k">${_history.length} invocations</span>`];
|
||||
if (cold.length) parts.push(`<span class="cold">cold init p50</span> <span class="v">${_pctile(cold, 50).toFixed(0)} ms</span> p99 <span class="v">${_pctile(cold, 99).toFixed(0)} ms</span>`);
|
||||
if (warm.length) parts.push(`<span class="warm">warm p50</span> <span class="v">${_pctile(warm, 50).toFixed(0)} ms</span> p99 <span class="v">${_pctile(warm, 99).toFixed(0)} ms</span>`);
|
||||
el.innerHTML = parts.join(' | ');
|
||||
}
|
||||
|
||||
function renderHistory() {
|
||||
const tbody = $('tester-history-body');
|
||||
_renderHistorySummary();
|
||||
if (!_history.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" style="color:#4a5568; text-align:center; font-style:italic;">No invocations yet.</td></tr>';
|
||||
return;
|
||||
@@ -751,7 +788,7 @@ function renderHistory() {
|
||||
<td>${startBadge}</td>
|
||||
<td class="num">${h.init_duration_ms != null ? h.init_duration_ms.toFixed(2) : '—'}</td>
|
||||
<td class="num">${h.duration_ms.toFixed(2)}</td>
|
||||
<td class="num">${h.max_memory_used_mb.toFixed(2)}</td>
|
||||
<td class="num">${((h.init_duration_ms || 0) + h.duration_ms).toFixed(2)}</td>
|
||||
<td>${statusBadge}</td>`;
|
||||
tr.addEventListener('click', () => loadHistoryDetail(h.invocation_id, tr));
|
||||
tbody.appendChild(tr);
|
||||
@@ -773,10 +810,20 @@ async function loadHistoryDetail(id, row) {
|
||||
}
|
||||
}
|
||||
|
||||
function _clearTesterPanels() {
|
||||
// Wipe per-invocation panels (everything except the History table) so stale
|
||||
// data from a previous run never leaks into the new one if it errors mid-flight.
|
||||
$('tester-result').innerHTML = '<span class="placeholder" style="border:none; padding:0; display:block;">…running…</span>';
|
||||
$('tester-stdout').innerHTML = '<span class="empty">…running…</span>';
|
||||
$('tester-logs').innerHTML = '<span class="empty">…running…</span>';
|
||||
$('tester-emf').innerHTML = '<span class="empty">…running…</span>';
|
||||
}
|
||||
|
||||
async function invoke() {
|
||||
const btn = $('tester-invoke');
|
||||
btn.disabled = true;
|
||||
$('tester-report').innerHTML = '<span class="k">…running…</span>';
|
||||
_clearTesterPanels();
|
||||
try {
|
||||
const fn = $('tester-function').value;
|
||||
if (!fn) throw new Error('No function selected.');
|
||||
@@ -819,6 +866,58 @@ async function resetCold() {
|
||||
}
|
||||
}
|
||||
|
||||
function _fmtBytes(n) {
|
||||
if (n < 1024) return n + ' B';
|
||||
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
|
||||
if (n < 1024 * 1024 * 1024) return (n / (1024 * 1024)).toFixed(2) + ' MB';
|
||||
return (n / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
||||
}
|
||||
|
||||
function _bar(used, cap) {
|
||||
const pct = Math.min(100, (used / cap) * 100);
|
||||
const colour = pct > 80 ? '#ff3d00' : pct > 50 ? '#ffc107' : '#00c853';
|
||||
return `<span style="color:${colour}">${pct.toFixed(2)}%</span>`;
|
||||
}
|
||||
|
||||
async function loadPackaging() {
|
||||
const pre = $('tester-packaging');
|
||||
try {
|
||||
const p = await api('/packaging');
|
||||
const L = p.limits;
|
||||
let out = '<span class="k">Functions (one deployment zip per folder):</span>\n';
|
||||
for (const f of p.functions) {
|
||||
out += ` ${f.name.padEnd(24)} `
|
||||
+ `<span class="k">zip:</span> <span class="v">${_fmtBytes(f.folder_zip_bytes).padStart(10)}</span> `
|
||||
+ `${_bar(f.folder_zip_bytes, L.zip_upload_max)} of 50 MB upload cap `
|
||||
+ `<span class="k">unzipped:</span> <span class="v">${_fmtBytes(f.folder_bytes)}</span>\n`;
|
||||
}
|
||||
out += '\n<span class="k">Shared layer (would be a Lambda Layer in AWS):</span>\n';
|
||||
out += ` shared/ `
|
||||
+ `<span class="k">zip:</span> <span class="v">${_fmtBytes(p.shared_layer.zip_bytes).padStart(10)}</span> `
|
||||
+ `<span class="k">unzipped:</span> <span class="v">${_fmtBytes(p.shared_layer.bytes)}</span>\n`;
|
||||
const totalUnz = p.dependencies_total_bytes
|
||||
+ p.shared_layer.bytes
|
||||
+ p.functions.reduce((s, f) => s + f.folder_bytes, 0);
|
||||
out += '\n<span class="k">Largest installed dependencies (top 25, ≥50 KB):</span>\n';
|
||||
for (const d of p.dependencies) {
|
||||
out += ` ${d.name.padEnd(24)} <span class="v">${_fmtBytes(d.bytes).padStart(10)}</span>\n`;
|
||||
}
|
||||
out += `\n<span class="k">Total deps:</span> <span class="v">${_fmtBytes(p.dependencies_total_bytes)}</span>\n`;
|
||||
out += `<span class="k">Total unzipped (function + shared + deps):</span> <span class="v">${_fmtBytes(totalUnz)}</span> `
|
||||
+ `${_bar(totalUnz, L.unzipped_max)} of 250 MB unzipped cap\n`;
|
||||
out += `\n<span class="k">AWS caps for reference:</span>\n`;
|
||||
out += ` <span class="k">zip upload</span> ${_fmtBytes(L.zip_upload_max)} `
|
||||
+ `<span class="k">unzipped</span> ${_fmtBytes(L.unzipped_max)} `
|
||||
+ `<span class="k">container image</span> ${_fmtBytes(L.container_image_max)}\n`;
|
||||
out += ` <span class="k">/tmp default</span> ${_fmtBytes(L.tmp_default)} `
|
||||
+ `<span class="k">/tmp max</span> ${_fmtBytes(L.tmp_max)} `
|
||||
+ `<span class="k">sync response</span> ${_fmtBytes(L.response_max)}\n`;
|
||||
pre.innerHTML = out;
|
||||
} catch (e) {
|
||||
pre.innerHTML = `<span class="err">[ERROR]</span> ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadScripts() {
|
||||
const fn = $('tester-function').value;
|
||||
const sel = $('script-name');
|
||||
@@ -883,6 +982,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
$('script-run').addEventListener('click', runScript);
|
||||
loadFunctions();
|
||||
loadHistory();
|
||||
loadPackaging();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user