diff --git a/.gitignore b/.gitignore index 9d915f92d..1314fa6f5 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ lerna-debug.log /packages/studio/scripting/src/api.declaration.d.ts tsconfig.tsbuildinfo /packages/server/api.opendaw.studio/ +/packages/server/logs.opendaw.studio/ +/assets.opendaw.studio/models/**/model.onnx diff --git a/.moises-scope-applied b/.moises-scope-applied index 92ffcba71..741025989 100644 --- a/.moises-scope-applied +++ b/.moises-scope-applied @@ -1,3 +1,3 @@ -Scope transformation applied: 2026-05-04T17:20:37.082Z +Scope transformation applied: 2026-05-11T18:02:20.466Z Old scope: @opendaw New scope: @moises-ai diff --git a/package-lock.json b/package-lock.json index c705cca06..290c58fa1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11094,7 +11094,7 @@ }, "packages/app/lab": { "name": "@opendaw/lab", - "version": "0.0.137", + "version": "0.0.138", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-box": "*", @@ -11133,7 +11133,7 @@ }, "packages/app/nam-test": { "name": "@opendaw/nam-test", - "version": "0.0.12", + "version": "0.0.13", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-dom": "*", @@ -11168,7 +11168,7 @@ }, "packages/app/studio": { "name": "@opendaw/app-studio", - "version": "0.0.137", + "version": "0.0.138", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-box": "*", @@ -11221,15 +11221,15 @@ }, "packages/config/eslint": { "name": "@opendaw/eslint-config", - "version": "0.0.27" + "version": "0.0.28" }, "packages/config/typescript": { "name": "@opendaw/typescript-config", - "version": "0.0.29" + "version": "0.0.30" }, "packages/lib/box": { "name": "@moises-ai/lib-box", - "version": "0.0.83", + "version": "0.0.84", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-runtime": "*", @@ -11242,7 +11242,7 @@ }, "packages/lib/box-forge": { "name": "@opendaw/lib-box-forge", - "version": "0.0.85", + "version": "0.0.86", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-box": "*", @@ -11259,7 +11259,7 @@ }, "packages/lib/dawproject": { "name": "@moises-ai/lib-dawproject", - "version": "0.0.67", + "version": "0.0.68", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-dsp": "*", @@ -11344,7 +11344,7 @@ }, "packages/lib/dom": { "name": "@moises-ai/lib-dom", - "version": "0.0.80", + "version": "0.0.81", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-runtime": "*", @@ -11358,7 +11358,7 @@ }, "packages/lib/dsp": { "name": "@moises-ai/lib-dsp", - "version": "0.0.81", + "version": "0.0.82", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-std": "*" @@ -11370,7 +11370,7 @@ }, "packages/lib/fusion": { "name": "@moises-ai/lib-fusion", - "version": "0.0.90", + "version": "0.0.91", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-box": "*", @@ -12076,7 +12076,7 @@ }, "packages/lib/jsx": { "name": "@moises-ai/lib-jsx", - "version": "0.0.80", + "version": "0.0.81", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-dom": "*", @@ -12089,7 +12089,7 @@ }, "packages/lib/midi": { "name": "@moises-ai/lib-midi", - "version": "0.0.63", + "version": "0.0.64", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-dsp": "*", @@ -12102,7 +12102,7 @@ }, "packages/lib/runtime": { "name": "@moises-ai/lib-runtime", - "version": "0.0.76", + "version": "0.0.77", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-std": "*" @@ -12114,7 +12114,7 @@ }, "packages/lib/std": { "name": "@moises-ai/lib-std", - "version": "0.0.75", + "version": "0.0.76", "license": "LGPL-3.0-or-later", "devDependencies": { "@opendaw/eslint-config": "*", @@ -12124,7 +12124,7 @@ }, "packages/lib/xml": { "name": "@moises-ai/lib-xml", - "version": "0.0.61", + "version": "0.0.62", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-std": "*" @@ -12205,7 +12205,7 @@ } }, "packages/server/yjs-server": { - "version": "1.0.35", + "version": "1.0.36", "license": "ISC", "dependencies": { "lib0": "^0.2.114", @@ -12216,7 +12216,7 @@ }, "packages/studio/adapters": { "name": "@moises-ai/studio-adapters", - "version": "0.0.106", + "version": "0.0.107", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-box": "*", @@ -12235,7 +12235,7 @@ }, "packages/studio/boxes": { "name": "@moises-ai/studio-boxes", - "version": "0.0.88", + "version": "0.0.89", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-box": "*", @@ -12250,7 +12250,7 @@ }, "packages/studio/core": { "name": "@moises-ai/studio-core", - "version": "0.0.137", + "version": "0.0.138", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-box": "*", @@ -12286,7 +12286,7 @@ }, "packages/studio/core-processors": { "name": "@opendaw/studio-core-processors", - "version": "0.0.111", + "version": "0.0.112", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-box": "*", @@ -13188,7 +13188,7 @@ }, "packages/studio/core-workers": { "name": "@opendaw/studio-core-workers", - "version": "0.0.101", + "version": "0.0.102", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-box": "*", @@ -13206,7 +13206,7 @@ }, "packages/studio/enums": { "name": "@moises-ai/studio-enums", - "version": "0.0.72", + "version": "0.0.73", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-std": "*" @@ -13218,7 +13218,7 @@ }, "packages/studio/forge-boxes": { "name": "@opendaw/studio-forge-boxes", - "version": "0.0.88", + "version": "0.0.89", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-box": "*", @@ -13234,7 +13234,7 @@ }, "packages/studio/p2p": { "name": "@moises-ai/studio-p2p", - "version": "0.0.8", + "version": "0.0.9", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-dsp": "*", @@ -13250,7 +13250,7 @@ }, "packages/studio/scripting": { "name": "@moises-ai/studio-scripting", - "version": "0.0.59", + "version": "0.0.60", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-box": "*", @@ -13268,7 +13268,7 @@ }, "packages/studio/sdk": { "name": "@moises-ai/studio-sdk", - "version": "0.0.139", + "version": "0.0.140", "license": "LGPL-3.0-or-later", "dependencies": { "@moises-ai/lib-box": "*", diff --git a/packages/app/lab/CHANGELOG.md b/packages/app/lab/CHANGELOG.md index 751020ef3..dabbb3af8 100644 --- a/packages/app/lab/CHANGELOG.md +++ b/packages/app/lab/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## 0.0.138 (2026-05-11) + +**Note:** Version bump only for package @opendaw/lab + ## [0.0.137](https://github.com/andremichelle/openDAW/compare/@opendaw/lab@0.0.136...@opendaw/lab@0.0.137) (2026-05-04) **Note:** Version bump only for package @opendaw/lab diff --git a/packages/app/lab/package.json b/packages/app/lab/package.json index aaf4be795..4957dcd53 100644 --- a/packages/app/lab/package.json +++ b/packages/app/lab/package.json @@ -1,7 +1,7 @@ { "name": "@opendaw/lab", "private": true, - "version": "0.0.137", + "version": "0.0.138", "type": "module", "license": "LGPL-3.0-or-later", "scripts": { diff --git a/packages/app/nam-test/CHANGELOG.md b/packages/app/nam-test/CHANGELOG.md index d36ff9a78..675c082f2 100644 --- a/packages/app/nam-test/CHANGELOG.md +++ b/packages/app/nam-test/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## 0.0.13 (2026-05-11) + +**Note:** Version bump only for package @opendaw/nam-test + ## [0.0.12](https://github.com/andremichelle/openDAW/compare/@opendaw/nam-test@0.0.11...@opendaw/nam-test@0.0.12) (2026-04-15) **Note:** Version bump only for package @opendaw/nam-test diff --git a/packages/app/nam-test/package.json b/packages/app/nam-test/package.json index f58604b43..ebd8198f4 100644 --- a/packages/app/nam-test/package.json +++ b/packages/app/nam-test/package.json @@ -1,7 +1,7 @@ { "name": "@opendaw/nam-test", "private": true, - "version": "0.0.12", + "version": "0.0.13", "type": "module", "license": "LGPL-3.0-or-later", "scripts": { diff --git a/packages/app/studio/CHANGELOG.md b/packages/app/studio/CHANGELOG.md index 8c8b1b2a5..7fee17dfd 100644 --- a/packages/app/studio/CHANGELOG.md +++ b/packages/app/studio/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## 0.0.138 (2026-05-11) + +### Bug Fixes + +- prevent overlapping regions in RegionContentStartModifier ([dc15f99](https://github.com/andremichelle/openDAW/commit/dc15f99ab431e290e8d1e5bc12984580f41128ae)) + ## [0.0.137](https://github.com/andremichelle/openDAW/compare/@opendaw/app-studio@0.0.136...@opendaw/app-studio@0.0.137) (2026-05-04) **Note:** Version bump only for package @opendaw/app-studio diff --git a/packages/app/studio/package.json b/packages/app/studio/package.json index 9d3312db4..8c768a885 100644 --- a/packages/app/studio/package.json +++ b/packages/app/studio/package.json @@ -1,7 +1,7 @@ { "name": "@opendaw/app-studio", "private": true, - "version": "0.0.137", + "version": "0.0.138", "type": "module", "license": "LGPL-3.0-or-later", "scripts": { diff --git a/packages/app/studio/public/sponsors.json b/packages/app/studio/public/sponsors.json index 816463b20..bfaa67b97 100644 --- a/packages/app/studio/public/sponsors.json +++ b/packages/app/studio/public/sponsors.json @@ -1,5 +1,5 @@ { - "fetchedAt": "2026-05-04T07:14:52.259Z", + "fetchedAt": "2026-05-11T07:27:10.034Z", "totalCount": 10, "sponsors": [ { diff --git a/packages/app/studio/src/perf/SampleReadBenchmark.ts b/packages/app/studio/src/perf/SampleReadBenchmark.ts new file mode 100644 index 000000000..bf1ca2722 --- /dev/null +++ b/packages/app/studio/src/perf/SampleReadBenchmark.ts @@ -0,0 +1,236 @@ +// Stand-alone benchmark of the proposed block-copy optimisation for +// Playfield/SampleVoice's hot loop. Mirrors the per-output-sample shape of the +// real kernel (4 reads + 2 writes, linear interpolation, variable rate, stereo) +// without touching SampleVoice.ts or any device/audio-engine code. + +export type SampleReadKernel = "direct" | "blockcopy" +export type SampleReadThread = "main" | "worker" + +export type SampleReadTest = { + readonly id: string + readonly label: string + readonly sizeMB: number // total stereo source bytes per voice (e.g. 3.84 = 10 s @ 48 kHz stereo Float32) + readonly rateRatio: number // playback rate; >1 reads more source frames per output frame + readonly voices: number // simultaneous voices, each with its own source array +} + +export type SampleReadResult = { + readonly id: string + readonly label: string + readonly sizeMB: number + readonly rateRatio: number + readonly voices: number + readonly thread: SampleReadThread + readonly directNsPerSample: number + readonly blockcopyNsPerSample: number + readonly speedup: number + readonly directBestMs: number + readonly blockcopyBestMs: number +} + +// Each row gets run twice (direct + blockcopy) on each thread (main + worker). +// Mix of small-sample (SoundFont-like, plan predicts wash), medium-sample +// (Playfield default, where the plan claims a big win on Linux), and large- +// sample / many-voice cases that exceed L2/L3 (worst case for direct read). +export const SAMPLE_READ_TESTS: ReadonlyArray = [ + {id: "sf-1.0-1", label: "Small 50 KB · 1.0× · 1 voice", sizeMB: 0.05, rateRatio: 1.0, voices: 1}, + {id: "sf-2.0-1", label: "Small 50 KB · 2.0× · 1 voice", sizeMB: 0.05, rateRatio: 2.0, voices: 1}, + {id: "sf-4.0-4", label: "Small 50 KB · 4.0× · 4 voices", sizeMB: 0.05, rateRatio: 4.0, voices: 4}, + {id: "pf-0.5-1", label: "Playfield 3.84 MB · 0.5× · 1 voice", sizeMB: 3.84, rateRatio: 0.5, voices: 1}, + {id: "pf-1.0-1", label: "Playfield 3.84 MB · 1.0× · 1 voice", sizeMB: 3.84, rateRatio: 1.0, voices: 1}, + {id: "pf-1.0-2", label: "Playfield 3.84 MB · 1.0× · 2 voices", sizeMB: 3.84, rateRatio: 1.0, voices: 2}, + {id: "pf-1.0-4", label: "Playfield 3.84 MB · 1.0× · 4 voices", sizeMB: 3.84, rateRatio: 1.0, voices: 4}, + {id: "pf-2.0-1", label: "Playfield 3.84 MB · 2.0× · 1 voice", sizeMB: 3.84, rateRatio: 2.0, voices: 1}, + {id: "pf-4.0-1", label: "Playfield 3.84 MB · 4.0× · 1 voice", sizeMB: 3.84, rateRatio: 4.0, voices: 1}, + {id: "pf-4.0-4", label: "Playfield 3.84 MB · 4.0× · 4 voices", sizeMB: 3.84, rateRatio: 4.0, voices: 4}, + {id: "lg-1.0-1", label: "Large 16 MB · 1.0× · 1 voice", sizeMB: 16, rateRatio: 1.0, voices: 1}, + {id: "lg-1.0-4", label: "Large 16 MB · 1.0× · 4 voices", sizeMB: 16, rateRatio: 1.0, voices: 4}, + {id: "lg-4.0-4", label: "Large 16 MB · 4.0× · 4 voices", sizeMB: 16, rateRatio: 4.0, voices: 4} +] + +const QUANTUM = 128 +const WARMUP_RUNS = 3 +const TIMED_RUNS = 7 +const TARGET_MS_PER_RUN = 50 // each timed run targets ~50 ms of work +const MAX_LOCAL_FRAMES = 8192 // safety cap for local block-copy buffers + +let DCE_SINK = 0 + +const allocateSource = (sizeMB: number): { L: Float32Array, R: Float32Array, frames: number } => { + const totalBytes = Math.max(1024, Math.round(sizeMB * 1024 * 1024)) + const stereoFrames = Math.max(QUANTUM * 2, Math.floor(totalBytes / (2 * 4))) + const L = new Float32Array(stereoFrames) + const R = new Float32Array(stereoFrames) + // Cheap deterministic fill that doesn't compress to a constant in the JIT. + for (let i = 0; i < stereoFrames; i++) { + L[i] = ((i * 0.000123) % 1.0) - 0.5 + R[i] = ((i * 0.000231) % 1.0) - 0.5 + } + return {L, R, frames: stereoFrames} +} + +// Mirrors SampleVoice.ts:77 — direct reads from the (potentially multi-MB) +// source array each output sample. Envelope is folded into a constant `env` +// because the bench is about memory traffic, not branch-prediction. +const directKernel = ( + inpL: Float32Array, + inpR: Float32Array, + outL: Float32Array, + outR: Float32Array, + startPosition: number, + rateRatio: number, + span: number, + env: number +): number => { + let position = startPosition + let acc = 0.0 + for (let i = 0; i < span; i++) { + const intPosition = position | 0 + const frac = position - intPosition + const l = inpL[intPosition] * (1.0 - frac) + inpL[intPosition + 1] * frac + const r = inpR[intPosition] * (1.0 - frac) + inpR[intPosition + 1] * frac + outL[i] += l * env + outR[i] += r * env + acc += l + r + position += rateRatio + } + return acc +} + +// Same maths as directKernel, but the inner loop reads from L1-resident +// `localL` / `localR` after a single SIMD memcpy of the source window. +// See plans/optimise-memory-read.md for the rationale. +const blockcopyKernel = ( + inpL: Float32Array, + inpR: Float32Array, + outL: Float32Array, + outR: Float32Array, + localL: Float32Array, + localR: Float32Array, + startPosition: number, + rateRatio: number, + span: number, + env: number +): number => { + const startInt = startPosition | 0 + const samplesNeeded = Math.min(localL.length, Math.ceil(span * Math.abs(rateRatio)) + 2) + localL.set(inpL.subarray(startInt, startInt + samplesNeeded)) + localR.set(inpR.subarray(startInt, startInt + samplesNeeded)) + let localPos = startPosition - startInt + let acc = 0.0 + for (let i = 0; i < span; i++) { + const intPos = localPos | 0 + const frac = localPos - intPos + const l = localL[intPos] * (1.0 - frac) + localL[intPos + 1] * frac + const r = localR[intPos] * (1.0 - frac) + localR[intPos + 1] * frac + outL[i] += l * env + outR[i] += r * env + acc += l + r + localPos += rateRatio + } + return acc +} + +type Sources = ReadonlyArray<{ L: Float32Array, R: Float32Array, frames: number }> + +const positionsFor = (sources: Sources): Float64Array => { + const positions = new Float64Array(sources.length) + for (let v = 0; v < sources.length; v++) { + // Stagger so voices read different pages — reproduces the TLB pressure + // multiple simultaneous Playfield voices put on a shared sample. + positions[v] = (sources[v].frames * 0.13 * (v + 1)) | 0 + } + return positions +} + +const advanceWithWrap = ( + position: number, + rateRatio: number, + span: number, + sourceFrames: number +): number => { + const next = position + span * rateRatio + const safeMax = sourceFrames - Math.abs(span * rateRatio) - 4 + if (next < 1 || next > safeMax) {return 1.0} + return next +} + +const median = (sorted: ReadonlyArray): number => sorted[(sorted.length - 1) >> 1] + +const measureKernel = ( + kernel: SampleReadKernel, + sources: Sources, + rateRatio: number, + quantaPerRun: number +): { bestMs: number, medianMs: number, totalSamples: number } => { + const outL = new Float32Array(QUANTUM) + const outR = new Float32Array(QUANTUM) + const localFrames = Math.min(MAX_LOCAL_FRAMES, Math.ceil(QUANTUM * Math.abs(rateRatio)) + 8) + const localL = kernel === "blockcopy" ? new Float32Array(localFrames) : new Float32Array(0) + const localR = kernel === "blockcopy" ? new Float32Array(localFrames) : new Float32Array(0) + const env = 0.5 + const positions = positionsFor(sources) + const runOnce = (): number => { + let acc = 0.0 + for (let q = 0; q < quantaPerRun; q++) { + for (let v = 0; v < sources.length; v++) { + const src = sources[v] + let position = positions[v] + if (kernel === "direct") { + acc += directKernel(src.L, src.R, outL, outR, position, rateRatio, QUANTUM, env) + } else { + acc += blockcopyKernel(src.L, src.R, outL, outR, localL, localR, position, rateRatio, QUANTUM, env) + } + positions[v] = advanceWithWrap(position, rateRatio, QUANTUM, src.frames) + } + } + return acc + } + for (let w = 0; w < WARMUP_RUNS; w++) {DCE_SINK ^= runOnce() | 0} + const samples: Array = [] + for (let r = 0; r < TIMED_RUNS; r++) { + const start = performance.now() + const value = runOnce() + const elapsed = performance.now() - start + DCE_SINK ^= value | 0 + samples.push(elapsed) + } + samples.sort((a, b) => a - b) + const totalSamples = quantaPerRun * sources.length * QUANTUM + return {bestMs: samples[0], medianMs: median(samples), totalSamples} +} + +const calibrateQuanta = (sources: Sources, rateRatio: number): number => { + // Quick warm calibration so each timed run hits TARGET_MS_PER_RUN. We use + // the direct kernel's first few quanta to estimate, then scale. + const probe = measureKernel("direct", sources, rateRatio, Math.max(64, 1024 >> Math.max(0, sources.length - 1))) + const nsPerSample = (probe.bestMs * 1_000_000) / probe.totalSamples + const targetSamples = (TARGET_MS_PER_RUN * 1_000_000) / nsPerSample + const samplesPerQuantum = sources.length * QUANTUM + return Math.max(64, Math.ceil(targetSamples / samplesPerQuantum)) +} + +export const runSampleReadTest = (test: SampleReadTest, thread: SampleReadThread): SampleReadResult => { + const sources: Sources = Array.from({length: test.voices}, () => allocateSource(test.sizeMB)) + const quantaPerRun = calibrateQuanta(sources, test.rateRatio) + const direct = measureKernel("direct", sources, test.rateRatio, quantaPerRun) + const blockcopy = measureKernel("blockcopy", sources, test.rateRatio, quantaPerRun) + const directNsPerSample = (direct.bestMs * 1_000_000) / direct.totalSamples + const blockcopyNsPerSample = (blockcopy.bestMs * 1_000_000) / blockcopy.totalSamples + const speedup = blockcopyNsPerSample > 0 ? directNsPerSample / blockcopyNsPerSample : 0 + return { + id: test.id, + label: test.label, + sizeMB: test.sizeMB, + rateRatio: test.rateRatio, + voices: test.voices, + thread, + directNsPerSample, + blockcopyNsPerSample, + speedup, + directBestMs: direct.bestMs, + blockcopyBestMs: blockcopy.bestMs + } +} + +export const consumeSink = (): number => DCE_SINK diff --git a/packages/app/studio/src/perf/SampleReadRunner.ts b/packages/app/studio/src/perf/SampleReadRunner.ts new file mode 100644 index 000000000..d257c8acb --- /dev/null +++ b/packages/app/studio/src/perf/SampleReadRunner.ts @@ -0,0 +1,84 @@ +import {tryCatch} from "@moises-ai/lib-std" +import SampleReadWorker from "./sample-read-worker.ts?worker" +import {SAMPLE_READ_TESTS, SampleReadResult, runSampleReadTest} from "./SampleReadBenchmark" + +export type SampleReadProgress = { readonly current: string, readonly index: number, readonly total: number } +export type SampleReadWorkerError = { readonly kind: "worker-error", readonly message: string } + +type WorkerMessage = + | { readonly kind: "progress", readonly id: string, readonly label: string, readonly index: number, readonly total: number } + | { readonly kind: "result", readonly result: SampleReadResult } + | { readonly kind: "test-error", readonly id: string, readonly label: string, readonly message: string } + | { readonly kind: "done" } + +const yieldToEventLoop = (): Promise => new Promise(resolve => setTimeout(resolve, 0)) +const errorMessage = (error: unknown): string => error instanceof Error ? error.message : String(error) + +export const runSampleReadBenchmarks = async ( + onProgress: (progress: SampleReadProgress) => void, + onResult: (result: SampleReadResult) => void, + onWorkerError?: (error: SampleReadWorkerError) => void +): Promise => { + const total = SAMPLE_READ_TESTS.length * 2 + let step = 0 + for (const test of SAMPLE_READ_TESTS) { + onProgress({current: `${test.label} (main)`, index: step, total}) + await yieldToEventLoop() + const outcome = tryCatch(() => runSampleReadTest(test, "main")) + if (outcome.status === "success") { + onResult(outcome.value) + } else { + onWorkerError?.({kind: "worker-error", message: `${test.label} (main): ${errorMessage(outcome.error)}`}) + } + step++ + } + let worker: Worker + try { + worker = new SampleReadWorker() + } catch (error: unknown) { + const message = errorMessage(error) + console.warn("Sample-read worker construction failed:", message) + onWorkerError?.({kind: "worker-error", message: `worker construct: ${message}`}) + return + } + try { + await new Promise((resolve) => { + worker.onmessage = (event: MessageEvent) => { + const message = event.data + if (message.kind === "progress") { + onProgress({ + current: `${message.label} (worker)`, + index: step + message.index, + total + }) + } else if (message.kind === "result") { + onResult(message.result) + } else if (message.kind === "test-error") { + onWorkerError?.({kind: "worker-error", message: `${message.label} (worker): ${message.message}`}) + } else if (message.kind === "done") { + resolve() + } + } + worker.onerror = (event: ErrorEvent) => { + event.preventDefault() + const message = event.message || event.filename || "unknown worker error" + console.warn("Sample-read worker error:", message) + onWorkerError?.({kind: "worker-error", message: `worker error: ${message}`}) + resolve() + } + worker.onmessageerror = () => { + onWorkerError?.({kind: "worker-error", message: "worker messageerror (structured clone failed)"}) + resolve() + } + try { + worker.postMessage({kind: "run", tests: SAMPLE_READ_TESTS}) + } catch (error: unknown) { + const message = errorMessage(error) + onWorkerError?.({kind: "worker-error", message: `postMessage: ${message}`}) + resolve() + } + }) + } finally { + worker.terminate() + } +} diff --git a/packages/app/studio/src/perf/sample-read-worker.ts b/packages/app/studio/src/perf/sample-read-worker.ts new file mode 100644 index 000000000..186d26e08 --- /dev/null +++ b/packages/app/studio/src/perf/sample-read-worker.ts @@ -0,0 +1,23 @@ +import {tryCatch} from "@moises-ai/lib-std" +import {SampleReadTest, runSampleReadTest} from "./SampleReadBenchmark" + +type IncomingMessage = { readonly kind: "run", readonly tests: ReadonlyArray } + +const errorMessage = (error: unknown): string => error instanceof Error ? error.message : String(error) + +self.onmessage = (event: MessageEvent) => { + const data = event.data + if (data.kind !== "run") {return} + const tests = data.tests + for (let index = 0; index < tests.length; index++) { + const test = tests[index] + self.postMessage({kind: "progress", id: test.id, label: test.label, index, total: tests.length}) + const outcome = tryCatch(() => runSampleReadTest(test, "worker")) + if (outcome.status === "success") { + self.postMessage({kind: "result", result: outcome.value}) + } else { + self.postMessage({kind: "test-error", id: test.id, label: test.label, message: errorMessage(outcome.error)}) + } + } + self.postMessage({kind: "done"}) +} diff --git a/packages/app/studio/src/ui/App.tsx b/packages/app/studio/src/ui/App.tsx index bf55bcbc1..4ab4b3b7b 100644 --- a/packages/app/studio/src/ui/App.tsx +++ b/packages/app/studio/src/ui/App.tsx @@ -23,6 +23,7 @@ import {PreferencesPage} from "@/ui/pages/PreferencesPage" import {TestPage} from "@/ui/pages/TestPage" import {JoinRoomPage} from "@/ui/pages/JoinRoomPage" import {PerformancePage} from "@/ui/pages/PerformancePage" +import {SampleReadPage} from "@/ui/pages/SampleReadPage" export const App = (service: StudioService) => { const terminator = new Terminator() @@ -67,6 +68,7 @@ export const App = (service: StudioService) => { {path: "/open-bundle/*", factory: OpenBundlePage}, {path: "/test", factory: TestPage}, {path: "/performance", factory: PerformancePage}, + {path: "/performance/sample-read", factory: SampleReadPage}, {path: "/join/*", factory: JoinRoomPage} ]} /> diff --git a/packages/app/studio/src/ui/pages/CodeEditorPage.sass b/packages/app/studio/src/ui/pages/CodeEditorPage.sass index 187ffc396..0dfdef917 100644 --- a/packages/app/studio/src/ui/pages/CodeEditorPage.sass +++ b/packages/app/studio/src/ui/pages/CodeEditorPage.sass @@ -4,6 +4,12 @@ component flex: 1 position: relative + .content + display: flex + flex-direction: column + flex: 1 + overflow: hidden + header height: 30px padding: 1em @@ -21,8 +27,8 @@ component width: min-content white-space: nowrap - .monaco-editor + .monaco-host flex: 1 - width: 100% - height: 100% - position: absolute \ No newline at end of file + position: relative + overflow: hidden + min-height: 0 \ No newline at end of file diff --git a/packages/app/studio/src/ui/pages/CodeEditorPage.tsx b/packages/app/studio/src/ui/pages/CodeEditorPage.tsx index 680fd979b..bcfc000be 100644 --- a/packages/app/studio/src/ui/pages/CodeEditorPage.tsx +++ b/packages/app/studio/src/ui/pages/CodeEditorPage.tsx @@ -122,7 +122,7 @@ export const CodeEditorPage: PageFactory = ({lifecycle, service}: } } return ( -
+
+ + {statusEl} +
+ {headlineEl} + + + + + + + + + + + + {tbody} +
Testthreaddirect (ns/sample)blockcopy (ns/sample)Δ time %verdict
+
+ ) +} diff --git a/packages/app/studio/src/ui/pages/stats/DashboardPage.tsx b/packages/app/studio/src/ui/pages/stats/DashboardPage.tsx index 5573db4b4..63f9080bd 100644 --- a/packages/app/studio/src/ui/pages/stats/DashboardPage.tsx +++ b/packages/app/studio/src/ui/pages/stats/DashboardPage.tsx @@ -12,6 +12,7 @@ import { BuildInfo, DailySeries, DiscordStats, + dropPartialDay, ErrorStats, fetchBuildInfo, fetchDiscordStats, @@ -67,7 +68,12 @@ type StatsBodyProps = { tiles: LiveTiles } -const StatsBody = ({lifecycle, data, tiles}: StatsBodyProps) => { +const StatsBody = ({lifecycle, data: rawData, tiles}: StatsBodyProps) => { + const data: DashboardData = { + rooms: {count: dropPartialDay(rawData.rooms.count), duration: dropPartialDay(rawData.rooms.duration)}, + users: dropPartialDay(rawData.users), + visitors: dropPartialDay(rawData.visitors) + } const dates = unionDates(data) if (dates.length === 0) { return
No statistics available yet.
diff --git a/packages/app/studio/src/ui/pages/stats/data.ts b/packages/app/studio/src/ui/pages/stats/data.ts index ef700cbd1..f14e7b292 100644 --- a/packages/app/studio/src/ui/pages/stats/data.ts +++ b/packages/app/studio/src/ui/pages/stats/data.ts @@ -245,6 +245,12 @@ export const sumValues = (series: DailySeries): number => export const lastValue = (series: DailySeries): number => series.length === 0 ? 0 : series[series.length - 1][1] +// The most recent day in any DailySeries is still being written to, so its +// value is always partial. Drop it before charting/trending — otherwise the +// last point sits below the trend and skews any visual reading. +export const dropPartialDay = (series: DailySeries): DailySeries => + series.length > 0 ? series.slice(0, -1) : series + export const minutesToHours = (series: DailySeries): DailySeries => series.map(([date, minutes]) => [date, minutes / 60] as const) diff --git a/packages/app/studio/src/ui/shadertoy/ShadertoyEditor.sass b/packages/app/studio/src/ui/shadertoy/ShadertoyEditor.sass index f384dd29d..fabaa4f13 100644 --- a/packages/app/studio/src/ui/shadertoy/ShadertoyEditor.sass +++ b/packages/app/studio/src/ui/shadertoy/ShadertoyEditor.sass @@ -7,6 +7,12 @@ component position: relative @include colors.panel-background + .content + display: flex + flex-direction: column + flex: 1 + overflow: hidden + header height: 30px padding: 1em @@ -25,8 +31,8 @@ component width: min-content white-space: nowrap - .monaco-editor + .monaco-host flex: 1 - width: 100% - height: 100% - position: absolute \ No newline at end of file + position: relative + overflow: hidden + min-height: 0 \ No newline at end of file diff --git a/packages/app/studio/src/ui/shadertoy/ShadertoyEditor.tsx b/packages/app/studio/src/ui/shadertoy/ShadertoyEditor.tsx index 7093b3b0d..60f450f4e 100644 --- a/packages/app/studio/src/ui/shadertoy/ShadertoyEditor.tsx +++ b/packages/app/studio/src/ui/shadertoy/ShadertoyEditor.tsx @@ -147,7 +147,7 @@ export const ShadertoyEditor = ({service, lifecycle}: Construct) => { }, {capture: true}), ) return ( -
+