feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env node
|
||||
// Headless Playwright benchmark that creates a Monaco editor in a page and
|
||||
// runs a high-frequency update scenario to collect timing and long-task info.
|
||||
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
async function run({ updates = 2000, freqHz = 200, append = false } = {}) {
|
||||
const browser = await chromium.launch({ headless: true })
|
||||
const page = await browser.newPage()
|
||||
|
||||
// minimal HTML that loads Monaco from CDN and creates a container
|
||||
const html = `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<style>html,body,#editor{height:100%;margin:0;padding:0}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="editor"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/loader.js"></script>
|
||||
<script>
|
||||
require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs' } });
|
||||
window.__benchReady = new Promise((res) => {
|
||||
require(['vs/editor/editor.main'], function () {
|
||||
window.editor = monaco.editor.create(document.getElementById('editor'), {
|
||||
value: '',
|
||||
language: 'javascript',
|
||||
automaticLayout: true,
|
||||
});
|
||||
res(true)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
await page.setContent(html, { waitUntil: 'load' })
|
||||
await page.evaluate(() => window.__benchReady)
|
||||
|
||||
// run the benchmark inside the page
|
||||
const result = await page.evaluate(async ({ updates, freqHz, append, payloadSize, random }) => {
|
||||
const editor = window.editor
|
||||
const model = editor.getModel()
|
||||
|
||||
// observe long tasks
|
||||
let longTasks = 0
|
||||
try {
|
||||
const obs = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
if (entry.duration && entry.duration > 50)
|
||||
longTasks++
|
||||
}
|
||||
})
|
||||
obs.observe({ type: 'longtask', buffered: true })
|
||||
}
|
||||
catch { }
|
||||
|
||||
const interval = 1000 / freqHz
|
||||
const start = performance.now()
|
||||
|
||||
for (let i = 0; i < updates; i++) {
|
||||
const text = 'a'.repeat(i % 1000)
|
||||
if (append) {
|
||||
// append via executeEdits
|
||||
const lastLine = model.getLineCount()
|
||||
const lastCol = model.getLineMaxColumn(lastLine)
|
||||
model.applyEdits([{ range: new monaco.Range(lastLine, lastCol, lastLine, lastCol), text }])
|
||||
}
|
||||
else {
|
||||
model.setValue(text)
|
||||
}
|
||||
// busy wait to emulate frequency
|
||||
const t0 = performance.now()
|
||||
while (performance.now() - t0 < interval) {}
|
||||
}
|
||||
|
||||
// allow observers to flush
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
const end = performance.now()
|
||||
return { updates, freqHz, elapsedMs: Math.round(end - start), longTasks }
|
||||
}, { updates, freqHz, append })
|
||||
|
||||
await browser.close()
|
||||
return result
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}` || (process.argv[1] && process.argv[1].endsWith('playwright-bench.mjs'))) {
|
||||
// Robust CLI parsing: support running directly or via `pnpm run bench:playwright -- ...`
|
||||
// process.argv can contain additional npm/pnpm wrapper args, so parse slice(2)
|
||||
const raw = process.argv.slice(2)
|
||||
// Accept: <updates> <freqHz> [append]
|
||||
let updates = 2000
|
||||
let freqHz = 200
|
||||
let append = false
|
||||
|
||||
if (raw.length >= 1) {
|
||||
const n = Number(raw[0])
|
||||
if (!Number.isNaN(n) && isFinite(n))
|
||||
updates = n
|
||||
}
|
||||
if (raw.length >= 2) {
|
||||
const n = Number(raw[1])
|
||||
if (!Number.isNaN(n) && isFinite(n))
|
||||
freqHz = n
|
||||
}
|
||||
// allow append to appear anywhere in args
|
||||
if (raw.some(a => a === 'append' || a === '--append'))
|
||||
append = true
|
||||
|
||||
// If caller asked for 'compare', run three benches with different throttle values
|
||||
const rawArgs = raw
|
||||
if (rawArgs.includes('compare') || rawArgs.includes('--compare')) {
|
||||
// parse optional payload size and random flag from args
|
||||
// payloadSize: any numeric arg after the first two (updates, freqHz)
|
||||
let payloadSize = 1000
|
||||
const numericArgs = rawArgs.map(a => Number(a)).filter(n => !Number.isNaN(n) && isFinite(n))
|
||||
if (numericArgs.length >= 3)
|
||||
payloadSize = numericArgs[2]
|
||||
const random = rawArgs.includes('random') || rawArgs.includes('--random')
|
||||
|
||||
// throttle values to compare (ms)
|
||||
const throttleValues = [0, 50, 100]
|
||||
const results = []
|
||||
for (const t of throttleValues) {
|
||||
try {
|
||||
const r = await runWithThrottle({ updates, freqHz, append, throttle: t, payloadSize, random })
|
||||
results.push({ throttle: t, payloadSize, random, result: r })
|
||||
}
|
||||
catch (err) {
|
||||
console.error('compare run failed for throttle', t, err)
|
||||
results.push({ throttle: t, payloadSize, random, error: String(err) })
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify({ compare: true, updates, freqHz, append, payloadSize, random, runs: results }, null, 2))
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
run({ updates, freqHz, append }).then(r => console.log(JSON.stringify(r, null, 2))).catch((err) => { console.error(err); process.exit(1) })
|
||||
}
|
||||
|
||||
// Helper that loads the page, sets up a throttled update wrapper in-page, and runs the bench
|
||||
async function runWithThrottle({ updates = 2000, freqHz = 200, append = false, throttle = 50, payloadSize = 1000, random = false } = {}) {
|
||||
const browser = await chromium.launch({ headless: true })
|
||||
const page = await browser.newPage()
|
||||
|
||||
const html = `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<style>html,body,#editor{height:100%;margin:0;padding:0}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="editor"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/loader.js"></script>
|
||||
<script>
|
||||
require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs' } });
|
||||
window.__benchReady = new Promise((res) => {
|
||||
require(['vs/editor/editor.main'], function () {
|
||||
window.editor = monaco.editor.create(document.getElementById('editor'), {
|
||||
value: '',
|
||||
language: 'javascript',
|
||||
automaticLayout: true,
|
||||
});
|
||||
// create a throttled updateCode wrapper that batches setValue calls
|
||||
(function(){
|
||||
let pending = null
|
||||
let timer = null
|
||||
const throttle = ${throttle}
|
||||
const payloadSize = ${payloadSize}
|
||||
const random = ${random}
|
||||
|
||||
function makePayload(i) {
|
||||
if (random) {
|
||||
// simple pseudo-random string of payloadSize
|
||||
let s = ''
|
||||
for (let k = 0; k < payloadSize; k++) s += String.fromCharCode(97 + (Math.floor(Math.random() * 26)))
|
||||
return s
|
||||
}
|
||||
return 'x'.repeat(payloadSize)
|
||||
}
|
||||
window.__updateWithThrottle = function(text){
|
||||
if (throttle === 0) {
|
||||
try { window.editor.getModel().setValue(text) } catch(e) {}
|
||||
return
|
||||
}
|
||||
pending = text
|
||||
if (timer != null) return
|
||||
timer = setTimeout(() => {
|
||||
try { window.editor.getModel().setValue(pending || '') } catch(e) {}
|
||||
pending = null
|
||||
timer = null
|
||||
}, throttle)
|
||||
}
|
||||
})()
|
||||
res(true)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
await page.setContent(html, { waitUntil: 'load' })
|
||||
await page.evaluate(() => window.__benchReady)
|
||||
|
||||
const result = await page.evaluate(async ({ updates, freqHz, append, payloadSize, random }) => {
|
||||
const editor = window.editor
|
||||
const model = editor.getModel()
|
||||
|
||||
// observe long tasks
|
||||
let longTasks = 0
|
||||
try {
|
||||
const obs = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
if (entry.duration && entry.duration > 50)
|
||||
longTasks++
|
||||
}
|
||||
})
|
||||
obs.observe({ type: 'longtask', buffered: true })
|
||||
}
|
||||
catch { }
|
||||
|
||||
const interval = 1000 / freqHz
|
||||
const start = performance.now()
|
||||
|
||||
for (let i = 0; i < updates; i++) {
|
||||
let text
|
||||
if (random) {
|
||||
// simple deterministic-ish random per iteration
|
||||
const arr = new Array(payloadSize)
|
||||
for (let k = 0; k < payloadSize; k++) arr[k] = String.fromCharCode(97 + ((i + k) % 26))
|
||||
text = arr.join('')
|
||||
}
|
||||
else {
|
||||
text = 'x'.repeat(payloadSize)
|
||||
}
|
||||
if (append) {
|
||||
const lastLine = model.getLineCount()
|
||||
const lastCol = model.getLineMaxColumn(lastLine)
|
||||
model.applyEdits([{ range: new monaco.Range(lastLine, lastCol, lastLine, lastCol), text }])
|
||||
}
|
||||
else {
|
||||
// use the throttled API if present
|
||||
if (typeof window.__updateWithThrottle === 'function') {
|
||||
window.__updateWithThrottle(text)
|
||||
}
|
||||
else {
|
||||
model.setValue(text)
|
||||
}
|
||||
}
|
||||
const t0 = performance.now()
|
||||
while (performance.now() - t0 < interval) {}
|
||||
}
|
||||
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
const end = performance.now()
|
||||
return { updates, freqHz, elapsedMs: Math.round(end - start), longTasks }
|
||||
}, { updates, freqHz, append, payloadSize, random })
|
||||
|
||||
await browser.close()
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user