* test(api): measure historical image read costs * fix(test): resolve image benchmark paths from its config * fix(attachments): read stored images without scanning sessions * test(attachments): complete by-id fixtures and access assertions * fix(docs): implement image-id read on catalog fixture * docs(attachments): refresh generated read contracts * test(web): verify stored images across history and forks * docs(api): clarify image read metadata and permission scope * test(api): type benchmark instrumentation without unsafe returns * test(api): retire unknown-cast allowances with removed history fixtures * test(attachments): isolate browser cases and document store-wide access * test(web): intercept the by-id thumbnail read path * docs(perf): run generated diagnostic from its owning package * refactor(attachments): reuse reference reads for conversation images * docs(api): refresh complete image reference client contract * test(ui-chat): assert complete image reference forwarding * test(perf): measure image reads against current Session observations
337 lines
16 KiB
TypeScript
337 lines
16 KiB
TypeScript
/** Threshold-free, built-runtime measurements of uncached historical image requests. */
|
|
|
|
import assert from 'node:assert/strict'
|
|
import { execFileSync, spawnSync } from 'node:child_process'
|
|
import { createHash } from 'node:crypto'
|
|
import { once } from 'node:events'
|
|
import { mkdtemp, rm } from 'node:fs/promises'
|
|
import { createServer } from 'node:http'
|
|
import { arch, cpus, platform, release, tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { performance } from 'node:perf_hooks'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { crc32, deflateSync } from 'node:zlib'
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
import { LocalAttachmentStore } from '@deepseek-ai/dsh-attachment-local'
|
|
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
|
import { MessageId, ToolCallId } from '@deepseek-ai/dsh-llm'
|
|
import { AssistantStreamAccumulator } from '@deepseek-ai/dsh-llm/assistant-stream'
|
|
import SessionStore, { SessionId, type Session } from '@deepseek-ai/dsh-session'
|
|
import SessionQueryEngine from '@deepseek-ai/dsh-session-query'
|
|
import { ApiSessionAgentController } from '../src/agent.ts'
|
|
import { SessionCommandController } from '../src/commands.ts'
|
|
import type { SessionAttachmentValue } from '../src/types.ts'
|
|
|
|
const WORKLOADS = {
|
|
typical: { turns: 120, images: 8 },
|
|
tail: { turns: 2_000, images: 32 },
|
|
} as const
|
|
const SAMPLES = 5
|
|
const WIDTH = 1024
|
|
const HEIGHT = 768
|
|
|
|
interface Timings {
|
|
historyObservations: number
|
|
historyQueries: number
|
|
historySnapshots: number
|
|
observationMs: number
|
|
snapshotMs: number
|
|
beforeStoreMs: number
|
|
storeMs: number
|
|
storeFinished: number
|
|
started: number
|
|
}
|
|
|
|
/** Production point reads; full-text search is outside this diagnostic. */
|
|
class ImageReadSessionQuery extends SessionQueryEngine {
|
|
override searchSessions(): Promise<never> {
|
|
return Promise.reject(new Error('image read diagnostic does not search sessions'))
|
|
}
|
|
|
|
override searchEvents(): Promise<never> {
|
|
return Promise.reject(new Error('image read diagnostic does not search events'))
|
|
}
|
|
}
|
|
|
|
function image(ordinal: number): Uint8Array {
|
|
const pixels = Buffer.alloc((WIDTH * 3 + 1) * HEIGHT)
|
|
let seed = ordinal + 1
|
|
for (let y = 0; y < HEIGHT; y++) {
|
|
for (let x = 0; x < WIDTH; x++) {
|
|
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
|
|
const offset = y * (WIDTH * 3 + 1) + 1 + x * 3
|
|
const value = x < WIDTH / 4 ? seed >>> 24 : (Math.floor(x / 16) + Math.floor(y / 16) + ordinal) % 256
|
|
pixels[offset] = value
|
|
pixels[offset + 1] = (value + ordinal * 17) % 256
|
|
pixels[offset + 2] = (value + y) % 256
|
|
}
|
|
}
|
|
const chunk = (name: string, data: Buffer): Buffer => {
|
|
const output = Buffer.alloc(data.length + 12)
|
|
output.writeUInt32BE(data.length)
|
|
output.write(name, 4)
|
|
data.copy(output, 8)
|
|
output.writeUInt32BE(crc32(output.subarray(4, -4)), output.length - 4)
|
|
return output
|
|
}
|
|
const header = Buffer.alloc(13)
|
|
header.writeUInt32BE(WIDTH)
|
|
header.writeUInt32BE(HEIGHT, 4)
|
|
header[8] = 8
|
|
header[9] = 2
|
|
return Buffer.concat([
|
|
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
chunk('IHDR', header), chunk('IDAT', deflateSync(pixels)), chunk('IEND', Buffer.alloc(0)),
|
|
])
|
|
}
|
|
|
|
function populate(session: Session, turns: number, refs: readonly ImageAttachmentRef[]): void {
|
|
for (let turn = 1; turn <= turns; turn++) {
|
|
const imageIndex = refs.findIndex((_, ordinal) => Math.floor(ordinal * turns / refs.length) + 1 === turn)
|
|
const ref = refs[imageIndex]
|
|
session.append('turn/start', { turn })
|
|
session.append('step/start', { turn, step: 1 })
|
|
session.append('user/message', {
|
|
id: MessageId(`input-${turn}`), role: 'user', source: { kind: 'user' },
|
|
content: [
|
|
{ type: 'text', text: `Inspect synthetic module ${turn}. ` + 'Describe the visible result. '.repeat(8) },
|
|
...(ref === undefined ? [] : [{ type: 'image' as const, attachment: ref }]),
|
|
],
|
|
}, { surfaceOp: 'append' })
|
|
const callId = ToolCallId(`call-${turn}`)
|
|
const call = session.append('tool/call', { turn, step: 1, callId, name: 'synthetic_read', arguments: '{}' })
|
|
session.append('tool/result', {
|
|
turn, step: 1,
|
|
message: { id: MessageId(`result-${turn}`), role: 'tool', toolCallId: callId, isError: false,
|
|
source: { kind: 'tool', callId }, content: [{ type: 'text', text: `module ${turn}\n` + 'export const value = 42;\n'.repeat(80) }] },
|
|
}, { surfaceOp: 'append', sourceEventSeqs: [call.seq] })
|
|
const block = { type: 'text' as const, text: `Result ${turn}. ` + 'Synthetic response. '.repeat(40) }
|
|
const stream = new AssistantStreamAccumulator()
|
|
stream.push({ time: 1_700_000_000_000 + turn, chunk: { type: 'block-start', index: 0, blockType: 'text' } })
|
|
for (let offset = 0; offset < block.text.length; offset += 32) {
|
|
stream.push({ time: 1_700_000_000_001 + turn + offset, chunk: { type: 'text-delta', index: 0, text: block.text.slice(offset, offset + 32) } })
|
|
}
|
|
stream.push({ time: 1_700_000_001_000 + turn, chunk: { type: 'block-end', index: 0, block } })
|
|
stream.push({ time: 1_700_000_001_001 + turn, chunk: { type: 'finish', reason: { kind: 'stop' } } })
|
|
session.append('assistant/message', {
|
|
turn, step: 1, stream: [...stream.snapshot()],
|
|
message: { id: MessageId(`reply-${turn}`), role: 'assistant', content: [block], source: { kind: 'model', provider: 'synthetic', model: 'synthetic' } },
|
|
}, { surfaceOp: 'append' })
|
|
session.append('step/end', { turn, step: 1 })
|
|
session.append('turn/end', { turn, reason: { kind: 'completed' } })
|
|
}
|
|
}
|
|
|
|
function memory(): { heapUsedBytes: number; rssBytes: number; peakRssBytes: number } {
|
|
assert.ok(global.gc, 'run the diagnostic with --expose-gc')
|
|
global.gc()
|
|
const usage = process.memoryUsage()
|
|
return { heapUsedBytes: usage.heapUsed, rssBytes: usage.rss, peakRssBytes: process.resourceUsage().maxRSS * 1024 }
|
|
}
|
|
|
|
async function transfer(payloads: readonly string[]): Promise<readonly number[]> {
|
|
let index = 0
|
|
const server = createServer((_request, response) => {
|
|
response.setHeader('content-type', 'application/json')
|
|
response.end(payloads[index++])
|
|
})
|
|
try {
|
|
server.listen(0, '127.0.0.1')
|
|
await once(server, 'listening')
|
|
const address = server.address()
|
|
assert.ok(address !== null && typeof address !== 'string')
|
|
const samples: number[] = []
|
|
for (const payload of payloads) {
|
|
const start = performance.now()
|
|
const response: Response = await fetch(`http://127.0.0.1:${address.port}`, { signal: AbortSignal.timeout(15_000) })
|
|
const received: string = await response.text()
|
|
samples.push(performance.now() - start)
|
|
assert.equal(received, payload)
|
|
}
|
|
return samples
|
|
} finally {
|
|
server.closeAllConnections()
|
|
if (server.listening) await new Promise<void>((resolve, reject) => {
|
|
server.close((error) => { if (error === undefined) resolve(); else reject(error) })
|
|
})
|
|
}
|
|
}
|
|
|
|
async function measure(workload: keyof typeof WORKLOADS) {
|
|
const root = await mkdtemp(join(tmpdir(), 'dsh-image-read-perf-'))
|
|
const ctx = new Context()
|
|
try {
|
|
await ctx.plugin(SessionStore)
|
|
await ctx.plugin(ImageReadSessionQuery)
|
|
await ctx.plugin(LocalAttachmentStore, { dshHome: root })
|
|
const refs: ImageAttachmentRef[] = []
|
|
for (let ordinal = 0; ordinal < WORKLOADS[workload].images; ordinal++) {
|
|
refs.push(await ctx.attachments.saveImage({ data: image(ordinal), mediaType: 'image/png' }))
|
|
}
|
|
const session = ctx.sessions.create(SessionId('synthetic-image-history'), { meta: { cwd: '/synthetic-image-history' } })
|
|
populate(session, WORKLOADS[workload].turns, refs)
|
|
let timing: Timings = {
|
|
historyObservations: 0, historyQueries: 0, historySnapshots: 0,
|
|
observationMs: 0, snapshotMs: 0, beforeStoreMs: 0,
|
|
storeMs: 0, storeFinished: 0, started: 0,
|
|
}
|
|
let traversedEvents = 0
|
|
let countEvents = false
|
|
const observeSession = ctx.sessionQuery.observeSession.bind(ctx.sessionQuery)
|
|
ctx.sessionQuery.observeSession = async (...args) => {
|
|
timing.historyObservations++
|
|
const start = performance.now()
|
|
try {
|
|
return await observeSession(...args)
|
|
} finally {
|
|
timing.observationMs += performance.now() - start
|
|
}
|
|
}
|
|
const getSession = ctx.sessions.get.bind(ctx.sessions)
|
|
ctx.sessions.get = (id) => {
|
|
timing.historyQueries++
|
|
const found = getSession(id)
|
|
return found === undefined ? undefined : new Proxy(found, {
|
|
get(target, key) {
|
|
const member: unknown = Reflect.get(target, key, target)
|
|
if (key !== 'snapshotEvents' || typeof member !== 'function') return member
|
|
return (...args: unknown[]) => {
|
|
timing.historySnapshots++
|
|
const start = performance.now()
|
|
const events = Reflect.apply(member as (...args: unknown[]) => readonly unknown[], target, args)
|
|
timing.snapshotMs += performance.now() - start
|
|
if (!countEvents) return events
|
|
return new Proxy(events, { get(array, property) {
|
|
if (property !== Symbol.iterator) {
|
|
const value: unknown = Reflect.get(array, property, array)
|
|
return value
|
|
}
|
|
return function* () { for (const event of array) { traversedEvents++; yield event } }
|
|
} })
|
|
}
|
|
},
|
|
})
|
|
}
|
|
const attachments = ctx.attachments
|
|
const observedAttachments = new Proxy(attachments, {
|
|
get(target, key) {
|
|
const member: unknown = Reflect.get(target, key, target)
|
|
if (key !== 'readImage' || typeof member !== 'function') return member
|
|
return async (...args: unknown[]) => {
|
|
const start = performance.now()
|
|
timing.beforeStoreMs = start - timing.started
|
|
const result: unknown = await Reflect.apply(member, target, args)
|
|
timing.storeFinished = performance.now()
|
|
timing.storeMs = timing.storeFinished - start
|
|
return result
|
|
}
|
|
},
|
|
})
|
|
const observedContext = new Proxy(ctx, {
|
|
get(target, key) {
|
|
if (key === 'attachments') return observedAttachments
|
|
const value: unknown = Reflect.get(target, key, target)
|
|
return value
|
|
},
|
|
})
|
|
const controller = new SessionCommandController(observedContext, new ApiSessionAgentController(ctx), '/synthetic-image-history')
|
|
const baselineMemory = memory()
|
|
const outputs: SessionAttachmentValue[] = []
|
|
const samples = []
|
|
for (const ref of refs) {
|
|
timing = {
|
|
historyObservations: 0, historyQueries: 0, historySnapshots: 0,
|
|
observationMs: 0, snapshotMs: 0, beforeStoreMs: 0,
|
|
storeMs: 0, storeFinished: 0, started: performance.now(),
|
|
}
|
|
const cpu = process.cpuUsage()
|
|
const value = await controller.attachment({ sessionId: session.id, attachment: ref })
|
|
const completed = performance.now()
|
|
const usedCpu = process.cpuUsage(cpu)
|
|
assert.deepEqual(value.attachment, ref)
|
|
assert.equal(`sha256:${createHash('sha256').update(Buffer.from(value.data, 'base64')).digest('hex')}`, ref.attachmentId)
|
|
samples.push({
|
|
controllerMs: completed - timing.started,
|
|
controllerCpuMs: (usedCpu.user + usedCpu.system) / 1000,
|
|
historyObservations: timing.historyObservations,
|
|
historyQueries: timing.historyQueries, historySnapshots: timing.historySnapshots,
|
|
observationMs: timing.observationMs, snapshotMs: timing.snapshotMs,
|
|
scanAndDispatchMs: timing.beforeStoreMs - timing.observationMs - timing.snapshotMs,
|
|
storeMs: timing.storeMs, base64AndReturnMs: completed - timing.storeFinished,
|
|
})
|
|
outputs.push(value)
|
|
}
|
|
const retainedMemory = memory()
|
|
const jsonMs: number[] = []
|
|
const payloads = outputs.map((value) => {
|
|
const start = performance.now()
|
|
const text = JSON.stringify(value)
|
|
jsonMs.push(performance.now() - start)
|
|
return text
|
|
})
|
|
const loopbackTransferMs = await transfer(payloads)
|
|
countEvents = true
|
|
timing.historyObservations = 0
|
|
timing.historyQueries = 0
|
|
timing.historySnapshots = 0
|
|
for (const ref of refs) await controller.attachment({ sessionId: session.id, attachment: ref })
|
|
const workCounts = {
|
|
historyObservations: timing.historyObservations, historyQueries: timing.historyQueries,
|
|
historySnapshots: timing.historySnapshots, traversedEvents,
|
|
}
|
|
if (process.argv.includes('--expect-no-history')) {
|
|
assert.deepEqual(workCounts, { historyObservations: 0, historyQueries: 0, historySnapshots: 0, traversedEvents: 0 })
|
|
}
|
|
return {
|
|
workload, fixture: { ...WORKLOADS[workload], events: session.seq, width: WIDTH, height: HEIGHT,
|
|
imageBytes: refs.map(ref => ref.bytes), attachmentIds: refs.map(ref => ref.attachmentId),
|
|
fullReferenceRequestJsonBytes: refs.map(attachment => Buffer.byteLength(JSON.stringify({ sessionId: session.id, attachment }))),
|
|
idOnlyRequestJsonBytes: refs.map(ref => Buffer.byteLength(JSON.stringify({
|
|
sessionId: session.id, attachmentId: ref.attachmentId,
|
|
}))),
|
|
responseJsonBytes: payloads.map(text => Buffer.byteLength(text)) },
|
|
samples, jsonMs, loopbackTransferMs, workCounts, baselineMemory, retainedMemory,
|
|
}
|
|
} finally {
|
|
await ctx.fiber.dispose()
|
|
await rm(root, { recursive: true, force: true })
|
|
}
|
|
}
|
|
|
|
for (const specifier of [
|
|
'@deepseek-ai/cordis', '@deepseek-ai/dsh-attachment-local', '@deepseek-ai/dsh-session', '@deepseek-ai/dsh-session-query',
|
|
]) {
|
|
assert.match(import.meta.resolve(specifier), /\/lib\/.*\.js$/, `${specifier} must resolve to built JavaScript`)
|
|
}
|
|
assert.ok(import.meta.url.endsWith('.js'), 'compile this diagnostic before running it')
|
|
assert.ok(process.execArgv.every(value => !value.includes('tsx')), 'do not use a TypeScript loader')
|
|
|
|
const workload = process.argv[3]
|
|
if (process.argv[2] === '--sample') {
|
|
assert.ok(workload === 'typical' || workload === 'tail')
|
|
process.stdout.write(`${JSON.stringify(await measure(workload))}\n`)
|
|
} else {
|
|
const samples: unknown[] = []
|
|
for (const name of Object.keys(WORKLOADS)) {
|
|
for (let sample = 0; sample < SAMPLES; sample++) {
|
|
const child = spawnSync(process.execPath, ['--expose-gc', fileURLToPath(import.meta.url), '--sample', name,
|
|
...process.argv.includes('--expect-no-history') ? ['--expect-no-history'] : []], {
|
|
encoding: 'utf8', timeout: 60_000, maxBuffer: 16 * 1024 * 1024,
|
|
env: { ...process.env, NODE_OPTIONS: '', TSX_TSCONFIG_PATH: '' },
|
|
})
|
|
if (child.error !== undefined) throw child.error
|
|
assert.equal(child.signal, null, child.stderr)
|
|
assert.equal(child.status, 0, child.stderr)
|
|
const report: unknown = JSON.parse(child.stdout)
|
|
samples.push(report)
|
|
}
|
|
}
|
|
process.stdout.write(JSON.stringify({
|
|
revision: execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(),
|
|
node: process.version, platform: platform(), release: release(), arch: arch(), cpu: cpus()[0]?.model,
|
|
clock: 'Fresh child per sample; loaded Session; first uncached request per image; OS file cache may be warm from fixture writes. Store includes file I/O, digest and header validation. observationMs measures the production SessionQuery point observation; snapshotMs measures its lazy event-array acquisition. scanAndDispatchMs is the residual before store entry after subtracting observation and snapshot acquisition. JSON and loopback HTTP transfer are separate measurements over retained responses. Gateway, browser, remote network, upload and model inference are excluded.',
|
|
memory: 'GC after setup and after all image responses; Session and all base64 responses remain reachable. Process peak RSS includes fixture creation. No retained-memory budget.',
|
|
samples,
|
|
}, null, 2) + '\n')
|
|
}
|