Home Manual Reference Source Test

tests/system/performance.test.js

  1. import { describe, it, beforeAll, afterAll } from "vitest";
  2. import { chromium } from "playwright";
  3. import { writeFileSync, mkdirSync } from "fs";
  4. import { dirname, join } from "path";
  5. import { findChrome } from "../../config/find-chrome.js";
  6. const APP_URL = "http://localhost:8080";
  7. const WARMUP_RUNS = 1;
  8. const N_RUNS = 5;
  9. const RESULTS_PATH = join(process.cwd(), "docs/benchmark.json");
  10. const DEFAULTS = {
  11. voices: 2,
  12. bars: 4,
  13. numerator: 4,
  14. denominator: 4,
  15. parts: ["A"],
  16. seed_pct: 0,
  17. };
  18. const results = {};
  19. async function measureRun(page, config) {
  20. const merged = { ...DEFAULTS, ...config };
  21. await page.evaluate((cfg) => {
  22. const p = window.Alpine.$data(
  23. document.querySelector("[x-data]"),
  24. ).parameters;
  25. const each = (arr, k) => (v) => arr.forEach((o) => (o[k] = v));
  26. const setters = {
  27. voices: (v) => {
  28. p.step3.voices = v;
  29. while (p.step3.voice.length < v) {
  30. p.step3.voice.push({ ...p.step3.voice[p.step3.voice.length % 4] });
  31. }
  32. },
  33. parts: (v) => {
  34. p.step1.parts = v.length;
  35. v.forEach((label, i) => {
  36. if (p.step1.part[i]) p.step1.part[i].label = label;
  37. });
  38. },
  39. seed_pct: (v) => (p.step3.seed_pct = v),
  40. bars: each(p.step1.part, "bars"),
  41. numerator: each(p.step1.part, "numerator"),
  42. denominator: each(p.step1.part, "denominator"),
  43. step2_enable: each(p.step2.part, "enable"),
  44. chord_tones: each(p.step3.voice, "chord_tones"),
  45. allow_repetitions: each(p.step3.voice, "allow_repetitions"),
  46. min_pitch: each(p.step3.voice, "min_pitch"),
  47. max_pitch: each(p.step3.voice, "max_pitch"),
  48. pitch_ranges: (v) =>
  49. v.forEach(([lo, hi], i) => {
  50. if (p.step3.voice[i]) {
  51. p.step3.voice[i].min_pitch = lo;
  52. p.step3.voice[i].max_pitch = hi;
  53. }
  54. }),
  55. dissonance: (v) => (p.step3.dissonance = v),
  56. max_interval: (v) => (p.step3.max_interval = v),
  57. forbid_unisons: (v) => (p.step3.forbid_unisons = v),
  58. forbid_parallels: (v) => (p.step3.forbid_parallels = v),
  59. };
  60. for (const [k, v] of Object.entries(cfg)) {
  61. if (v !== undefined) setters[k]?.(v);
  62. }
  63. }, merged);
  64. const t0 = await page.evaluate(() => {
  65. performance.clearMeasures();
  66. return performance.now();
  67. });
  68. await page.click("#run-optimization-button");
  69. await page.waitForFunction(() => {
  70. const el = document.getElementById("midi-player");
  71. return el?.getAttribute("src")?.startsWith("data:audio/midi;base64,");
  72. });
  73. const t1 = await page.evaluate(() => performance.now());
  74. const steps = await page.evaluate(() => {
  75. const entries = performance.getEntriesByType("measure");
  76. const sum = (prefix) =>
  77. entries
  78. .filter((e) => e.name.startsWith(prefix))
  79. .reduce((acc, e) => acc + e.duration, 0);
  80. return {
  81. step1: sum("compose:step1"),
  82. step2: sum("compose:step2"),
  83. step3: sum("compose:step3"),
  84. };
  85. });
  86. return { config: merged, ms: t1 - t0, steps };
  87. }
  88. async function measureConfig(page, config, label) {
  89. const bucket = (results[label] ??= []);
  90. for (let i = 0; i < N_RUNS + WARMUP_RUNS; i++) {
  91. const r = await measureRun(page, config);
  92. if (i >= WARMUP_RUNS) bucket.push(r);
  93. }
  94. }
  95. describe("system: performance benchmark", () => {
  96. let browser, page;
  97. beforeAll(async () => {
  98. const resp = await fetch(APP_URL).catch(() => null);
  99. if (!resp?.ok) {
  100. throw new Error(
  101. `App not reachable at ${APP_URL}. Start it first: npm run app`,
  102. );
  103. }
  104. browser = await chromium.launch({
  105. executablePath: findChrome(),
  106. headless: false,
  107. args: [
  108. "--no-sandbox",
  109. "--disable-setuid-sandbox",
  110. "--disable-dev-shm-usage",
  111. ],
  112. });
  113. page = await browser.newPage();
  114. await page.goto(APP_URL);
  115. const status = page.locator("#output-status");
  116. await status.waitFor({ state: "visible", timeout: 20_000 });
  117. }, 60_000);
  118. afterAll(async () => {
  119. mkdirSync(dirname(RESULTS_PATH), { recursive: true });
  120. writeFileSync(RESULTS_PATH, JSON.stringify(results, null, 2));
  121. await browser?.close();
  122. });
  123. describe("B0: stability", () => {
  124. it("6 runs at default for warmup curve and steady-state variance", async () => {
  125. const bucket = (results.B0 ??= []);
  126. for (let i = 0; i < 6; i++) {
  127. bucket.push(await measureRun(page, {}));
  128. }
  129. });
  130. });
  131. describe("B1: scaling voices", () => {
  132. for (const voices of [
  133. 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
  134. ]) {
  135. it(`solves with voices=${voices}`, async () => {
  136. await measureConfig(page, { voices }, "B1");
  137. });
  138. }
  139. });
  140. describe("B2: scaling bars", () => {
  141. for (const bars of [2, 4, 8, 16, 32]) {
  142. it(`solves with bars=${bars}`, async () => {
  143. await measureConfig(page, { bars }, "B2");
  144. });
  145. }
  146. });
  147. describe("B3: scaling constraint set", () => {
  148. const B3_STAGES = {
  149. 1: {
  150. step2_enable: false,
  151. chord_tones: "atonal",
  152. min_pitch: 0,
  153. max_pitch: 127,
  154. max_interval: 88,
  155. allow_repetitions: true,
  156. forbid_unisons: false,
  157. dissonance: 0.5,
  158. forbid_parallels: false,
  159. },
  160. };
  161. B3_STAGES[2] = { ...B3_STAGES[1], step2_enable: true };
  162. B3_STAGES[3] = {
  163. ...B3_STAGES[2],
  164. chord_tones: "chord",
  165. pitch_ranges: [
  166. [60, 84],
  167. [52, 76],
  168. [45, 69],
  169. [36, 60],
  170. ],
  171. };
  172. B3_STAGES[4] = { ...B3_STAGES[3], forbid_unisons: true, dissonance: 0.3 };
  173. B3_STAGES[5] = {
  174. ...B3_STAGES[4],
  175. max_interval: 6,
  176. allow_repetitions: false,
  177. forbid_parallels: true,
  178. };
  179. for (const stage of [1, 2, 3, 4, 5]) {
  180. it(`solves with constraint stage=${stage}`, async () => {
  181. await measureConfig(
  182. page,
  183. {
  184. voices: 4,
  185. bars: 16,
  186. constraint_stage: stage,
  187. ...B3_STAGES[stage],
  188. },
  189. "B3",
  190. );
  191. });
  192. }
  193. });
  194. });