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,142 @@
|
||||
// Lightweight benchmark for updateCode throttling and batching logic.
|
||||
// This script mocks a minimal subset of Monaco editor API used by useMonaco
|
||||
// and exercises updateCode at high frequency to compare CPU/time behavior.
|
||||
|
||||
import { performance } from 'node:perf_hooks'
|
||||
|
||||
// Load the compiled source if available, otherwise load TS source via ts-node is out of scope.
|
||||
// We'll directly import the source file (ESM) assuming Node can load it; however the module
|
||||
// depends on 'monaco-shim' and browser APIs. Instead, we'll directly benchmark the
|
||||
// core flush/update logic by copying minimal parts into this script as a function.
|
||||
|
||||
// For simplicity, we simulate a minimal "editor wrapper" with similar logic to useMonaco
|
||||
// focusing on updateCode/flushPendingUpdate/append buffering behavior.
|
||||
|
||||
function createMockEditor() {
|
||||
let value = ''
|
||||
return {
|
||||
getModel() {
|
||||
return {
|
||||
getValue() { return value },
|
||||
setValue(v) { value = v },
|
||||
getLineCount() { return value.split('\n').length },
|
||||
getLanguageId() { return 'javascript' },
|
||||
getLineMaxColumn(line) { return (value.split('\n')[line - 1] || '').length + 1 },
|
||||
}
|
||||
},
|
||||
getValue() { return value },
|
||||
executeEdits(_, edits) {
|
||||
for (const e of edits) {
|
||||
// append-only benchmark: assume appends only
|
||||
value += e.text
|
||||
}
|
||||
},
|
||||
getOption() { return false },
|
||||
}
|
||||
}
|
||||
|
||||
// Minimal wrapper implementing only the paths used by the benchmark
|
||||
function createWrapper(opts = {}) {
|
||||
const appendBuffer = []
|
||||
const appendBufferScheduled = false
|
||||
let pendingUpdate = null
|
||||
let lastKnownCode = ''
|
||||
const throttleMs = opts.updateThrottleMs ?? 50
|
||||
let lastFlushTime = 0
|
||||
let throttleTimer = null
|
||||
const editorView = createMockEditor()
|
||||
|
||||
function flushPendingUpdate() {
|
||||
lastFlushTime = Date.now()
|
||||
if (!pendingUpdate)
|
||||
return
|
||||
const model = editorView.getModel()
|
||||
const { code } = pendingUpdate
|
||||
pendingUpdate = null
|
||||
const prev = lastKnownCode || model.getValue()
|
||||
if (prev === code)
|
||||
return
|
||||
if (code.startsWith(prev)) {
|
||||
const suffix = code.slice(prev.length)
|
||||
if (suffix) {
|
||||
appendBuffer.push(suffix)
|
||||
}
|
||||
lastKnownCode = code
|
||||
return
|
||||
}
|
||||
// fallback full replace
|
||||
model.setValue(code)
|
||||
lastKnownCode = code
|
||||
}
|
||||
|
||||
function flushAppendBuffer() {
|
||||
if (appendBuffer.length === 0)
|
||||
return
|
||||
const text = appendBuffer.join('')
|
||||
appendBuffer.length = 0
|
||||
editorView.executeEdits('append', [{ range: null, text }])
|
||||
}
|
||||
|
||||
function updateCode(code) {
|
||||
pendingUpdate = { code }
|
||||
// schedule via RAF-like (here immediate) then apply throttle
|
||||
// emulate raf by setImmediate
|
||||
setImmediate(() => {
|
||||
if (!throttleMs) {
|
||||
flushPendingUpdate(); return
|
||||
}
|
||||
const now = Date.now()
|
||||
const since = now - lastFlushTime
|
||||
if (since >= throttleMs) { flushPendingUpdate(); return }
|
||||
if (throttleTimer != null)
|
||||
return
|
||||
const wait = throttleMs - since
|
||||
throttleTimer = setTimeout(() => { throttleTimer = null; setImmediate(flushPendingUpdate) }, wait)
|
||||
})
|
||||
}
|
||||
|
||||
return { updateCode, flushAppendBuffer, editorView }
|
||||
}
|
||||
|
||||
// Run benchmark: send N updates at freq Hz
|
||||
async function run({ updates = 2000, freqHz = 200, throttleMs = 50 }) {
|
||||
const wrapper = createWrapper({ updateThrottleMs: throttleMs })
|
||||
const intervalMs = 1000 / freqHz
|
||||
const sent = 0
|
||||
const start = performance.now()
|
||||
|
||||
for (let i = 0; i < updates; i++) {
|
||||
wrapper.updateCode(''.padEnd(i + 1, 'a'))
|
||||
// busy wait to respect frequency
|
||||
const t0 = performance.now()
|
||||
while (performance.now() - t0 < intervalMs) { /* noop */ }
|
||||
}
|
||||
|
||||
// allow pending timers to flush
|
||||
await new Promise(r => setTimeout(r, throttleMs + 100))
|
||||
const end = performance.now()
|
||||
const finalValue = wrapper.editorView.getModel().getValue()
|
||||
const elapsed = end - start
|
||||
const avgMsPerUpdate = elapsed / updates
|
||||
|
||||
const out = {
|
||||
updates,
|
||||
freqHz,
|
||||
throttleMs,
|
||||
elapsedMs: Math.round(elapsed),
|
||||
avgMsPerUpdate: avgMsPerUpdate.toFixed(3),
|
||||
finalLength: finalValue.length,
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(out, null, 2))
|
||||
}
|
||||
|
||||
if (process.argv.includes('--help')) {
|
||||
console.log('Usage: node scripts/stream-benchmark.mjs [updates] [freqHz] [throttleMs]')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const updates = Number(process.argv[2] || 2000)
|
||||
const freqHz = Number(process.argv[3] || 200)
|
||||
const throttleMs = Number(process.argv[4] || 50)
|
||||
run({ updates, freqHz, throttleMs }).catch((err) => { console.error(err); process.exit(1) })
|
||||
Reference in New Issue
Block a user