Files
ced4568 51a09241ad feat(loadtest): load-test tooling + capacity model
boot-labs.js drives the real admin Prepare-Labs endpoint (reusing the
existing session-helper.js from Phase 3A) and records boot time/failure
rate; sample-resources.sh samples real docker stats for running Labs.
capacity-model.js turns measured per-Lab CPU/memory into a recommended/
warning/hard-admission-control concurrency threshold - no concurrency
number is hardcoded as a marketing figure, every input is explicitly
placeholder data until measured against the real host.

Plus deploy/rollback/smoke-test scripts used by the CI/CD and deployment
runbooks. All syntax-checked; validate-compose.sh and capacity-model.js
actually run against real inputs.
2026-08-18 17:13:32 -05:00

60 lines
3.0 KiB
JavaScript
Executable File

#!/usr/bin/env node
// Capacity planning worksheet (Phase 4 §25) — computes recommended-concurrent-Labs from real
// inputs, not a guessed marketing number. Inputs come from infra/environments/production/capacity.yml
// (edit that file with real measured values from infra/scripts/loadtest/, then re-run this).
//
// Usage: node capacity-model.js [path-to-capacity.yml]
const fs = require('fs');
const path = require('path');
// Reuses the Provisioner's existing js-yaml dependency (platform/provisioner/src/config.js
// already parses YAML config this same way) rather than adding a new package.json just for
// this one script.
const yaml = require(path.join(__dirname, '../../platform/provisioner/node_modules/js-yaml'));
const configPath = process.argv[2] || path.join(__dirname, '../environments/production/capacity.yml');
if (!fs.existsSync(configPath)) {
console.error(`No capacity config at ${configPath} — copy capacity.yml.example first and fill in real measured values.`);
process.exit(1);
}
const cfg = yaml.load(fs.readFileSync(configPath, 'utf8'));
function computeFor(resource, hostTotal, platformReserved, proxmoxOverheadPct, perLab, safetyMarginPct) {
const usable = hostTotal * (1 - proxmoxOverheadPct / 100) - platformReserved;
const withMargin = usable * (1 - safetyMarginPct / 100);
return Math.floor(withMargin / perLab);
}
const cpuLimit = computeFor(
'CPU', cfg.host_cpu_cores, cfg.platform_reserved_cpu_cores,
cfg.proxmox_overhead_pct, cfg.measured_lab_cpu_cores, cfg.safety_margin_pct
);
const memLimit = computeFor(
'Memory', cfg.host_mem_mb, cfg.platform_reserved_mem_mb,
cfg.proxmox_overhead_pct, cfg.measured_lab_mem_mb, cfg.safety_margin_pct
);
const recommended = Math.min(cpuLimit, memLimit);
const warningThreshold = Math.floor(recommended * 0.8);
const hardAdmissionControl = recommended; // never provision past the more conservative of the two limits
console.log('RootNode capacity model');
console.log('========================');
console.log(`Host: ${cfg.host_cpu_cores} cores, ${cfg.host_mem_mb} MB RAM`);
console.log(`Platform-reserved: ${cfg.platform_reserved_cpu_cores} cores, ${cfg.platform_reserved_mem_mb} MB`);
console.log(`Proxmox overhead: ${cfg.proxmox_overhead_pct}%`);
console.log(`Measured per-Lab: ${cfg.measured_lab_cpu_cores} cores, ${cfg.measured_lab_mem_mb} MB`);
console.log(`Safety margin: ${cfg.safety_margin_pct}%`);
console.log('');
console.log(`CPU-limited concurrent Labs: ${cpuLimit}`);
console.log(`Memory-limited concurrent Labs: ${memLimit}`);
console.log('');
console.log(`RECOMMENDED concurrent Labs: ${recommended} (binding constraint: ${cpuLimit < memLimit ? 'CPU' : 'memory'})`);
console.log(`WARNING threshold (80%): ${warningThreshold}`);
console.log(`HARD admission-control threshold: ${hardAdmissionControl}`);
console.log('');
if (cfg.measured_lab_cpu_cores === undefined || cfg._placeholder) {
console.log('WARNING: this config still has placeholder/example values — see infra/scripts/loadtest/ for how to measure real ones.');
}