forked from boos/BoosAPI
e0a5f57793
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.
147 lines
4.6 KiB
JavaScript
147 lines
4.6 KiB
JavaScript
import { performance } from 'node:perf_hooks'
|
|
|
|
function sleep(ms) {
|
|
return new Promise(r => setTimeout(r, ms))
|
|
}
|
|
|
|
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) {
|
|
value += e.text
|
|
}
|
|
},
|
|
getOption() { return false },
|
|
}
|
|
}
|
|
|
|
function createWrapper(opts = {}) {
|
|
const appendBuffer = []
|
|
let appendBufferScheduled = false
|
|
let pendingUpdate = null
|
|
let lastKnownCode = ''
|
|
const throttleMs = opts.updateThrottleMs ?? 0
|
|
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
|
|
|
|
// Build prevCode including buffered (mirror recent EditorManager fix)
|
|
const buffered = appendBuffer.length > 0 ? appendBuffer.join('') : ''
|
|
const prevCode = (appendBuffer.length > 0) ? (editorView.getValue() + buffered) : (lastKnownCode || editorView.getValue())
|
|
|
|
if (prevCode === code)
|
|
return
|
|
if (code.startsWith(prevCode) && prevCode.length < code.length) {
|
|
const suffix = code.slice(prevCode.length)
|
|
if (suffix)
|
|
appendCode(suffix)
|
|
lastKnownCode = code
|
|
return
|
|
}
|
|
// fallback full replace
|
|
model.setValue(code)
|
|
lastKnownCode = code
|
|
}
|
|
|
|
function flushAppendBuffer() {
|
|
if (appendBuffer.length === 0)
|
|
return
|
|
const parts = appendBuffer.splice(0)
|
|
const text = parts.join('')
|
|
appendBuffer.length = 0
|
|
editorView.executeEdits('append', [{ range: null, text }])
|
|
// sync lastKnownCode
|
|
try { lastKnownCode = editorView.getModel().getValue() } catch {}
|
|
}
|
|
|
|
function scheduleAppendFlush() {
|
|
if (appendBufferScheduled)
|
|
return
|
|
appendBufferScheduled = true
|
|
// emulate raf frame
|
|
setTimeout(() => {
|
|
appendBufferScheduled = false
|
|
flushAppendBuffer()
|
|
}, 0)
|
|
}
|
|
|
|
function appendCode(appendText) {
|
|
if (!appendText) return
|
|
appendBuffer.push(appendText)
|
|
scheduleAppendFlush()
|
|
}
|
|
|
|
function updateCode(code) {
|
|
pendingUpdate = { code }
|
|
// emulate RAF-like immediate schedule
|
|
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, appendCode, flushAppendBuffer, editorView }
|
|
}
|
|
|
|
async function run({ iterations = 200, blockSize = 50, intervalMs = 1, throttleMs = 0 }) {
|
|
const wrapper = createWrapper({ updateThrottleMs: throttleMs })
|
|
let expected = ''
|
|
const block = 'x'.repeat(blockSize)
|
|
const start = performance.now()
|
|
for (let i = 0; i < iterations; i++) {
|
|
expected += block
|
|
wrapper.updateCode(expected)
|
|
await sleep(intervalMs)
|
|
}
|
|
// wait for pending timers and flushes
|
|
await sleep(500 + throttleMs)
|
|
const end = performance.now()
|
|
const finalValue = wrapper.editorView.getModel().getValue()
|
|
const ok = finalValue === expected
|
|
console.log(JSON.stringify({ iterations, blockSize, intervalMs, throttleMs, ok, expectedLength: expected.length, finalLength: finalValue.length, elapsedMs: Math.round(end - start) }, null, 2))
|
|
if (!ok) {
|
|
// print small diff info
|
|
let i = 0
|
|
while (i < Math.min(finalValue.length, expected.length) && finalValue[i] === expected[i]) i++
|
|
console.log('first mismatch at index', i)
|
|
console.log('expected slice', expected.slice(Math.max(0, i - 40), i + 40))
|
|
console.log('actual slice', finalValue.slice(Math.max(0, i - 40), i + 40))
|
|
}
|
|
}
|
|
|
|
if (process.argv.includes('--help')) {
|
|
console.log('Usage: node scripts/stream-benchmark-aggressive.mjs [iterations] [blockSize] [intervalMs] [throttleMs]')
|
|
process.exit(0)
|
|
}
|
|
|
|
const iterations = Number(process.argv[2] || 200)
|
|
const blockSize = Number(process.argv[3] || 50)
|
|
const intervalMs = Number(process.argv[4] || 1)
|
|
const throttleMs = Number(process.argv[5] || 0)
|
|
|
|
run({ iterations, blockSize, intervalMs, throttleMs }).catch(err => { console.error(err); process.exit(1) })
|