Files
ServerKit/scripts/test/measure-installed-runtime.py
Juan Denis ff3c928917 feat(test): passive cgroup-v2 runtime baselines for full installs
Full VM installs and Test Sandbox full mode now record a short
post-install runtime observation (60s settle + 120s sampling) with an
explicit measured/unavailable/failed/interrupted status that never
rewrites install success. Adds the standalone scripts/measure-runtime.py
sampler, the measure-installed-runtime.py harness wrapper, E2E report
rendering, results-API summaries, CI coverage and METRICS.md accounting
boundaries.
2026-09-19 04:52:24 -04:00

102 lines
5.0 KiB
Python

#!/usr/bin/env python3
"""Record a short post-install baseline, with explicit unavailable/error results.
Used by full distro installs (VMs and systemd containers), never quick/provision
checks. This is a regression observation, not a production capacity benchmark.
"""
import argparse
from datetime import datetime, timezone
import hashlib
import importlib.util
import json
import math
import os
from pathlib import Path
import platform
import subprocess
import sys
import time
SPEC = importlib.util.spec_from_file_location('measure_runtime', Path(__file__).parents[1] / 'measure-runtime.py')
runtime = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(runtime)
def source_identity(source, archive=None):
if archive:
digest = hashlib.sha256()
with archive.open('rb') as stream:
for block in iter(lambda: stream.read(1024 * 1024), b''):
digest.update(block)
return 'local-overlay-archive-sha256:' + digest.hexdigest()
try:
revision = runtime.command(['git', '-C', str(source), 'rev-parse', 'HEAD'])
dirty = runtime.command(['git', '-C', str(source), 'status', '--porcelain', '--untracked-files=no'])
return revision + (' (tracked files modified)' if dirty else '')
except (OSError, subprocess.SubprocessError):
version = source / 'VERSION'
return 'release:' + version.read_text().strip() if version.is_file() else 'unknown'
def observe(source, duration=120, settle=60, archive=None):
result = {'schema_version': 1, 'status': 'unavailable',
'scenario': 'post-install-dashboard-closed',
'started_at': datetime.now(timezone.utc).isoformat(),
'settle_seconds': settle, 'requested_duration_seconds': duration,
'scope_note': 'Selected service cgroups only; other services/apps are excluded. '
'Container tests share the host kernel and may run beside other tests.'}
if platform.system() != 'Linux' or not (runtime.CGROUP_ROOT / 'cgroup.controllers').is_file():
result['reason'] = 'Linux cgroup v2 unavailable; no runtime measurement taken'
return result
try:
revision = source_identity(source, archive)
targets = [runtime.resolve_target('unit', 'serverkit.service')]
for unit in ('nginx.service', 'docker.service', 'containerd.service'):
try:
if runtime.command(['systemctl', 'is-active', unit]) == 'active':
targets.append(runtime.resolve_target('unit', unit))
except (OSError, ValueError, subprocess.SubprocessError):
continue
runtime.validate_scopes(targets)
# This pause is after builds/health probes, before measurement or pytest.
time.sleep(settle)
samples, interrupted = runtime.collect(targets, duration, 5)
summary = runtime.summarize(samples, targets)
failed = any(row['failed_samples'] for row in summary)
result.update(status='interrupted' if interrupted else 'failed' if failed else 'measured',
running_revision=revision, targets=targets, summary=summary, samples=samples,
os=platform.freedesktop_os_release().get('PRETTY_NAME'),
kernel=platform.release(), architecture=platform.machine(),
logical_cpus=os.cpu_count(),
definitions={'memory_bytes': 'memory.current including cache/kernel/descendants',
'cpu_percent_one_core': '100% = one logical CPU',
'sampled_max': 'largest sampled value; short spikes can be missed'})
except (OSError, ValueError, subprocess.SubprocessError) as exc:
result.update(status='failed', reason=f'Runtime collection failed: {type(exc).__name__}')
result['finished_at'] = datetime.now(timezone.utc).isoformat()
return result
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--source', type=Path, default=Path('/opt/serverkit'))
parser.add_argument('--source-archive', type=Path, help='VM harness local overlay archive')
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--duration', type=float, default=120)
parser.add_argument('--settle', type=float, default=60)
args = parser.parse_args(argv)
if (not math.isfinite(args.duration) or args.duration < 5
or not math.isfinite(args.settle) or args.settle < 0):
parser.error('duration must be finite and >= 5; settle finite and >= 0')
# A VM reuse explicitly produces a new observation at this harness scratch path.
result = observe(args.source, args.duration, args.settle, args.source_archive)
args.output.write_text(json.dumps(result, indent=2, allow_nan=False) + '\n', encoding='utf-8')
print(json.dumps({key: value for key, value in result.items() if key != 'samples'}, indent=2))
return 0 if result['status'] == 'measured' else 1
if __name__ == '__main__':
raise SystemExit(main())