mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-11-09 20:07:43 +08:00
fix(inspector): reserve record overhead in the Cordis snapshot budget
The collector pruned snapshots to maxFrameBytes - 4096, but setState also counts the retained record's JSON around the payload, so a pruned snapshot near the budget threw and crashed the process. Refs #7424
This commit is contained in:
@@ -7,6 +7,7 @@ import { INSPECTOR_BOOTSTRAP_ROUTE } from '../shared/web.ts'
|
||||
import { parseInspectorClientBootstrap } from '../shared/bridge/control-codec.ts'
|
||||
import { createInspectorService, type InspectorService as SharedInspectorService } from '../shared/service.ts'
|
||||
import { publishCordisTree } from './inspection/cordis.ts'
|
||||
import { maxStatePayloadBytes } from '../shared/bridge/buffer.ts'
|
||||
import { startInspectorClient } from './bridge/controller.ts'
|
||||
import { disposeInspectorResources } from '../shared/dispose.ts'
|
||||
|
||||
@@ -130,7 +131,7 @@ class InspectorClientSession {
|
||||
this.lifetime.signal.throwIfAborted()
|
||||
disposers.push(publishCordisTree(this.ctx, source, {
|
||||
maxNodes: bootstrap.maxCordisNodes,
|
||||
maxBytes: bootstrap.maxFrameBytes - 4_096,
|
||||
maxBytes: maxStatePayloadBytes(bootstrap.maxFrameBytes),
|
||||
}))
|
||||
disposers.push(this.ctx.provide('inspector', createInspectorService(source)))
|
||||
const panel = this.ctx.inject(['slots', 'shortcuts', 'locale'], async (ctx) => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { resolveInspectorOptions, startInspector, type InspectorHandle, type Ins
|
||||
import type { InspectorClientBootstrap } from '../shared/bridge/messages/control.ts'
|
||||
import { createInspectorService } from '../shared/service.ts'
|
||||
import { publishCordisTree } from './inspection/cordis.ts'
|
||||
import { maxStatePayloadBytes } from '../shared/bridge/buffer.ts'
|
||||
import { INSPECTOR_BOOTSTRAP_PATH, INSPECTOR_CLIENT_QUERY, readInspectorClientSelection } from '../shared/web.ts'
|
||||
import { disposeInspectorResources } from '../shared/dispose.ts'
|
||||
|
||||
@@ -79,7 +80,7 @@ export async function apply(ctx: Context, config: HostPluginConfig): Promise<voi
|
||||
const { default: serveStatic } = await import('serve-static')
|
||||
disposers.push(publishCordisTree(ctx, handle.source, {
|
||||
maxNodes: spec.maxCordisNodes,
|
||||
maxBytes: spec.maxSourceFrameBytes - 4_096,
|
||||
maxBytes: maxStatePayloadBytes(spec.maxSourceFrameBytes),
|
||||
}))
|
||||
disposers.push(ctx.provide('inspector', createInspectorService(handle.source)))
|
||||
disposers.push(ctx.on('webserver/index-inject', (table: IndexInjection[]) => {
|
||||
|
||||
@@ -6,6 +6,17 @@ import type { InspectorRecordInput, SourceAppendFrame, SourceReplaceFrame } from
|
||||
import { INSPECTOR_PROTOCOL_VERSION } from './version.ts'
|
||||
|
||||
const SOURCE_FRAME_OVERHEAD_BYTES = 4_096
|
||||
/** Upper bound on the JSON around one replacement state payload: array, `monotonicMs`, and a 128-code-unit escaped topic. */
|
||||
const STATE_RECORD_OVERHEAD_BYTES = 1_024
|
||||
|
||||
/**
|
||||
* Compute the payload budget of a source's single retained state topic.
|
||||
* @param maxFrameBytes - Source-frame byte limit.
|
||||
* @returns Encoded payload bytes that {@link InspectorSourceBuffer.setState} accepts when no other state topic is retained.
|
||||
*/
|
||||
export function maxStatePayloadBytes(maxFrameBytes: number): number {
|
||||
return maxFrameBytes - SOURCE_FRAME_OVERHEAD_BYTES - STATE_RECORD_OVERHEAD_BYTES
|
||||
}
|
||||
|
||||
/** Limits and declared topics shared by both source transports. */
|
||||
export interface InspectorSourceBufferOptions {
|
||||
|
||||
@@ -6,6 +6,7 @@ import WebSocket from 'ws'
|
||||
import { ClientInspectorSource } from '../../src/client/bridge/transport.ts'
|
||||
import { ClientSourceCatalog } from '../../src/client/cdp/sources.ts'
|
||||
import { publishCordisTree } from '../../src/client/inspection/cordis.ts'
|
||||
import { maxStatePayloadBytes } from '../../src/shared/bridge/buffer.ts'
|
||||
import { inspectorId } from '../../src/shared/bridge/ids.ts'
|
||||
import type { InspectorClientBootstrap } from '../../src/shared/bridge/messages/control.ts'
|
||||
import type { InspectorJsonValue } from '../../src/shared/json.ts'
|
||||
@@ -69,7 +70,7 @@ const sourceCatalog = input.sourceCatalog === undefined
|
||||
const source = new ClientInspectorSource(input.bootstrap, input.label, sourceCatalog)
|
||||
const disposeCordis = publishCordisTree(context, source, {
|
||||
maxNodes: input.bootstrap.maxCordisNodes,
|
||||
maxBytes: input.bootstrap.maxFrameBytes - 4_096,
|
||||
maxBytes: maxStatePayloadBytes(input.bootstrap.maxFrameBytes),
|
||||
})
|
||||
const service = createInspectorService(source)
|
||||
let addedFiber: Fiber | undefined
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
/** Worker-side source buffer behavior. */
|
||||
|
||||
import { MessageChannel } from 'node:worker_threads'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { HostBridgePublisher } from '../src/host/bridge/publisher.ts'
|
||||
import { inspectorId } from '../src/shared/bridge/ids.ts'
|
||||
import { InspectorSourceBuffer, type InspectorSourceBufferOptions } from '../src/shared/bridge/buffer.ts'
|
||||
import { InspectorSourceBuffer, maxStatePayloadBytes, type InspectorSourceBufferOptions } from '../src/shared/bridge/buffer.ts'
|
||||
import { CORDIS_TREE_TOPIC } from '../src/shared/bridge/messages/cordis.ts'
|
||||
import { CordisTreeCollector } from '../src/shared/cordis/collector.ts'
|
||||
import { jsonByteLength, type InspectorJsonValue } from '../src/shared/json.ts'
|
||||
import type { InspectorSourceDescriptor } from '../src/shared/bridge/messages/observation.ts'
|
||||
|
||||
const sourceId = inspectorId<'InspectorSourceId'>('source-buffer-test', 'sourceId')
|
||||
@@ -75,6 +79,32 @@ describe('Inspector source buffer', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('accepts a single state payload at its budget with the largest record envelope', () => {
|
||||
const maxFrameBytes = 8_192
|
||||
const budget = maxStatePayloadBytes(maxFrameBytes)
|
||||
const topic = '\u0001'.repeat(128)
|
||||
const payload = 'x'.repeat(budget - 2)
|
||||
expect(jsonByteLength(payload)).toBe(budget)
|
||||
const records = buffer(4, { maxFrameBytes })
|
||||
records.setState(topic, payload, -Number.MAX_VALUE)
|
||||
const frame = JSON.stringify(records.replacement(sourceId, generation))
|
||||
expect(new TextEncoder().encode(frame).byteLength).toBeLessThanOrEqual(maxFrameBytes)
|
||||
})
|
||||
|
||||
it('retains every pruned Cordis snapshot whose collector uses the state budget', async (test) => {
|
||||
const root = new Context()
|
||||
test.onTestFinished(async () => { await root.fiber.dispose() })
|
||||
for (let index = 0; index < 200; index++) await root.plugin({ name: `plugin-${String(index)}`, apply() {} })
|
||||
for (let maxFrameBytes = 8_192; maxFrameBytes < 8_448; maxFrameBytes++) {
|
||||
const collector = new CordisTreeCollector(root, { maxNodes: 1_000, maxBytes: maxStatePayloadBytes(maxFrameBytes) })
|
||||
const snapshot = collector.snapshot()
|
||||
collector.close()
|
||||
expect(snapshot.truncated).toBe(true)
|
||||
const payload = JSON.parse(JSON.stringify(snapshot)) as InspectorJsonValue
|
||||
buffer(4, { maxFrameBytes }).setState(CORDIS_TREE_TOPIC, payload, performance.now())
|
||||
}
|
||||
})
|
||||
|
||||
it('splits frames at record, byte, and sequence gaps and discards pending records', () => {
|
||||
const records = buffer(10, { maxRecordsPerFrame: 2, maxFrameBytes: 4_300 })
|
||||
expect(records.hasPending).toBe(false)
|
||||
|
||||
Reference in New Issue
Block a user