tests/system/performance.test.js
- import { describe, it, beforeAll, afterAll } from "vitest";
- import { chromium } from "playwright";
- import { writeFileSync, mkdirSync } from "fs";
- import { dirname, join } from "path";
- import { findChrome } from "../../config/find-chrome.js";
- const APP_URL = "http://localhost:8080";
- const WARMUP_RUNS = 1;
- const N_RUNS = 5;
- const RESULTS_PATH = join(process.cwd(), "docs/benchmark.json");
- const DEFAULTS = {
- voices: 2,
- bars: 4,
- numerator: 4,
- denominator: 4,
- parts: ["A"],
- seed_pct: 0,
- };
- const results = {};
- async function measureRun(page, config) {
- const merged = { ...DEFAULTS, ...config };
- await page.evaluate((cfg) => {
- const p = window.Alpine.$data(
- document.querySelector("[x-data]"),
- ).parameters;
- const each = (arr, k) => (v) => arr.forEach((o) => (o[k] = v));
- const setters = {
- voices: (v) => {
- p.step3.voices = v;
- while (p.step3.voice.length < v) {
- p.step3.voice.push({ ...p.step3.voice[p.step3.voice.length % 4] });
- }
- },
- parts: (v) => {
- p.step1.parts = v.length;
- v.forEach((label, i) => {
- if (p.step1.part[i]) p.step1.part[i].label = label;
- });
- },
- seed_pct: (v) => (p.step3.seed_pct = v),
- bars: each(p.step1.part, "bars"),
- numerator: each(p.step1.part, "numerator"),
- denominator: each(p.step1.part, "denominator"),
- step2_enable: each(p.step2.part, "enable"),
- chord_tones: each(p.step3.voice, "chord_tones"),
- allow_repetitions: each(p.step3.voice, "allow_repetitions"),
- min_pitch: each(p.step3.voice, "min_pitch"),
- max_pitch: each(p.step3.voice, "max_pitch"),
- pitch_ranges: (v) =>
- v.forEach(([lo, hi], i) => {
- if (p.step3.voice[i]) {
- p.step3.voice[i].min_pitch = lo;
- p.step3.voice[i].max_pitch = hi;
- }
- }),
- dissonance: (v) => (p.step3.dissonance = v),
- max_interval: (v) => (p.step3.max_interval = v),
- forbid_unisons: (v) => (p.step3.forbid_unisons = v),
- forbid_parallels: (v) => (p.step3.forbid_parallels = v),
- };
- for (const [k, v] of Object.entries(cfg)) {
- if (v !== undefined) setters[k]?.(v);
- }
- }, merged);
- const t0 = await page.evaluate(() => {
- performance.clearMeasures();
- return performance.now();
- });
- await page.click("#run-optimization-button");
- await page.waitForFunction(() => {
- const el = document.getElementById("midi-player");
- return el?.getAttribute("src")?.startsWith("data:audio/midi;base64,");
- });
- const t1 = await page.evaluate(() => performance.now());
- const steps = await page.evaluate(() => {
- const entries = performance.getEntriesByType("measure");
- const sum = (prefix) =>
- entries
- .filter((e) => e.name.startsWith(prefix))
- .reduce((acc, e) => acc + e.duration, 0);
- return {
- step1: sum("compose:step1"),
- step2: sum("compose:step2"),
- step3: sum("compose:step3"),
- };
- });
- return { config: merged, ms: t1 - t0, steps };
- }
- async function measureConfig(page, config, label) {
- const bucket = (results[label] ??= []);
- for (let i = 0; i < N_RUNS + WARMUP_RUNS; i++) {
- const r = await measureRun(page, config);
- if (i >= WARMUP_RUNS) bucket.push(r);
- }
- }
- describe("system: performance benchmark", () => {
- let browser, page;
- beforeAll(async () => {
- const resp = await fetch(APP_URL).catch(() => null);
- if (!resp?.ok) {
- throw new Error(
- `App not reachable at ${APP_URL}. Start it first: npm run app`,
- );
- }
- browser = await chromium.launch({
- executablePath: findChrome(),
- headless: false,
- args: [
- "--no-sandbox",
- "--disable-setuid-sandbox",
- "--disable-dev-shm-usage",
- ],
- });
- page = await browser.newPage();
- await page.goto(APP_URL);
- const status = page.locator("#output-status");
- await status.waitFor({ state: "visible", timeout: 20_000 });
- }, 60_000);
- afterAll(async () => {
- mkdirSync(dirname(RESULTS_PATH), { recursive: true });
- writeFileSync(RESULTS_PATH, JSON.stringify(results, null, 2));
- await browser?.close();
- });
- describe("B0: stability", () => {
- it("6 runs at default for warmup curve and steady-state variance", async () => {
- const bucket = (results.B0 ??= []);
- for (let i = 0; i < 6; i++) {
- bucket.push(await measureRun(page, {}));
- }
- });
- });
- describe("B1: scaling voices", () => {
- for (const voices of [
- 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
- ]) {
- it(`solves with voices=${voices}`, async () => {
- await measureConfig(page, { voices }, "B1");
- });
- }
- });
- describe("B2: scaling bars", () => {
- for (const bars of [2, 4, 8, 16, 32]) {
- it(`solves with bars=${bars}`, async () => {
- await measureConfig(page, { bars }, "B2");
- });
- }
- });
- describe("B3: scaling constraint set", () => {
- const B3_STAGES = {
- 1: {
- step2_enable: false,
- chord_tones: "atonal",
- min_pitch: 0,
- max_pitch: 127,
- max_interval: 88,
- allow_repetitions: true,
- forbid_unisons: false,
- dissonance: 0.5,
- forbid_parallels: false,
- },
- };
- B3_STAGES[2] = { ...B3_STAGES[1], step2_enable: true };
- B3_STAGES[3] = {
- ...B3_STAGES[2],
- chord_tones: "chord",
- pitch_ranges: [
- [60, 84],
- [52, 76],
- [45, 69],
- [36, 60],
- ],
- };
- B3_STAGES[4] = { ...B3_STAGES[3], forbid_unisons: true, dissonance: 0.3 };
- B3_STAGES[5] = {
- ...B3_STAGES[4],
- max_interval: 6,
- allow_repetitions: false,
- forbid_parallels: true,
- };
- for (const stage of [1, 2, 3, 4, 5]) {
- it(`solves with constraint stage=${stage}`, async () => {
- await measureConfig(
- page,
- {
- voices: 4,
- bars: 16,
- constraint_stage: stage,
- ...B3_STAGES[stage],
- },
- "B3",
- );
- });
- }
- });
- });