conversation-fold.worker.client.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. /** Compiled worker for the cold Client conversation-fold benchmark. */
  2. import { performance } from 'node:perf_hooks'
  3. import { AssistantStreamAccumulator } from '@deepseek-ai/dsh-llm/assistant-stream'
  4. import type { StreamChunk } from '@deepseek-ai/dsh-llm'
  5. import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
  6. import type { ChatSnapshot } from '@deepseek-ai/dsh-client-ui-chat/client'
  7. import type { SessionEventLikeEntry } from '@deepseek-ai/dsh-api-session-controller/client'
  8. // These Client-only fold modules have no plain-Node package export and are compiled into this worker.
  9. import { ConversationNodeAssembler } from '../../packages/client/ui-conversation/src/client/conversation/assembler.ts'
  10. import { inspectRequestPrompt } from '../../packages/client/ui-conversation/src/client/contract/request-inspection.ts'
  11. import type {
  12. ConversationNodeDefinition,
  13. ConversationViewDefinition,
  14. } from '../../packages/client/ui-conversation/src/client/contract/conversation.ts'
  15. import { assistantDefinition } from '../../packages/client/ui-chat/src/client/conversation-nodes/assistant.ts'
  16. import { chatViewDefinition } from '../../packages/client/ui-chat/src/client/conversation-nodes/chat-snapshot-builder.ts'
  17. import { commandDefinition } from '../../packages/client/ui-chat/src/client/conversation-nodes/command.ts'
  18. import { compactionDefinition } from '../../packages/client/ui-chat/src/client/conversation-nodes/compaction.ts'
  19. import { unknownFallbackDefinition } from '../../packages/client/ui-chat/src/client/conversation-nodes/fallback.ts'
  20. import { nextStepInboxDefinition } from '../../packages/client/ui-chat/src/client/conversation-nodes/inbox.ts'
  21. import { messageDefinition } from '../../packages/client/ui-chat/src/client/conversation-nodes/message.ts'
  22. import { requestPromptDefinition } from '../../packages/client/ui-chat/src/client/conversation-nodes/request-prompt.ts'
  23. import { retryDefinition } from '../../packages/client/ui-chat/src/client/conversation-nodes/retry.ts'
  24. import { toolDefinition } from '../../packages/client/ui-chat/src/client/conversation-nodes/tool.ts'
  25. import { turnErrorDefinition } from '../../packages/client/ui-chat/src/client/conversation-nodes/turn-error.ts'
  26. import { turnMaxTokensDefinition } from '../../packages/client/ui-chat/src/client/conversation-nodes/turn-max-tokens.ts'
  27. import { turnProcessDefinition } from '../../packages/client/ui-chat/src/client/conversation-nodes/turn-process.ts'
  28. import { turnTailDefinition } from '../../packages/client/ui-chat/src/client/conversation-nodes/turn-tail.ts'
  29. import { assertBuiltBenchmarkRuntime } from '../support/built-worker.ts'
  30. const TIME_ZERO = 1_700_000_000_000
  31. /** Result emitted by the compiled conversation-fold worker. */
  32. export interface ConversationFoldWorkerReport {
  33. readonly events: number
  34. readonly compactRecords: number
  35. readonly streamedDeltas: number
  36. readonly chatNodes: number
  37. readonly smallFoldMs: number
  38. readonly largeFoldMs: number
  39. readonly scaling: number
  40. }
  41. class BenchEventDefinitions {
  42. readonly definitions: readonly ConversationNodeDefinition[] = [
  43. nextStepInboxDefinition,
  44. messageDefinition,
  45. requestPromptDefinition(inspectRequestPrompt),
  46. assistantDefinition,
  47. turnProcessDefinition,
  48. toolDefinition,
  49. commandDefinition,
  50. compactionDefinition,
  51. retryDefinition,
  52. turnErrorDefinition,
  53. turnMaxTokensDefinition,
  54. turnTailDefinition,
  55. ]
  56. entries(): readonly ConversationNodeDefinition[] {
  57. return this.definitions
  58. }
  59. fallbackEntry(): ConversationNodeDefinition {
  60. return unknownFallbackDefinition
  61. }
  62. }
  63. class BenchViewDefinitions {
  64. entries(): readonly ConversationViewDefinition[] {
  65. return [chatViewDefinition]
  66. }
  67. }
  68. function entry(seq: number, type: string, data: unknown, extra: Record<string, unknown> = {}): SessionEventLikeEntry {
  69. return {
  70. type: 'event',
  71. event: { seq, time: TIME_ZERO + seq, type, data, ...extra } as unknown as SessionEvent,
  72. }
  73. }
  74. function synthesizeWindow(
  75. turns: number,
  76. deltas: number,
  77. ): { readonly entries: readonly SessionEventLikeEntry[]; readonly records: number } {
  78. const entries: SessionEventLikeEntry[] = []
  79. let seq = 0
  80. let records = 0
  81. const push = (type: string, data: unknown, extra: Record<string, unknown> = {}): void => {
  82. entries.push(entry(seq, type, data, extra))
  83. seq += 1
  84. }
  85. const reasoningDeltas = Math.floor(deltas / 4)
  86. for (let turn = 1; turn <= turns; turn += 1) {
  87. push('turn/start', { turn })
  88. push('user/message', {
  89. id: `user-${String(turn)}`,
  90. role: 'user',
  91. content: [{ type: 'text', text: `prompt ${String(turn)}` }],
  92. source: { kind: 'user' },
  93. }, { surfaceOp: 'append' })
  94. push('step/start', { turn, step: 1 })
  95. const accumulator = new AssistantStreamAccumulator()
  96. let time = TIME_ZERO + seq * 1_000
  97. const stream = (chunk: StreamChunk): void => {
  98. accumulator.push({ time, chunk })
  99. time += 1
  100. }
  101. stream({ type: 'block-start', index: 0, blockType: 'reasoning' })
  102. let reasoning = ''
  103. for (let index = 0; index < reasoningDeltas; index += 1) {
  104. const delta = `r${String(index)} `
  105. reasoning += delta
  106. stream({ type: 'reasoning-delta', index: 0, text: delta })
  107. }
  108. stream({ type: 'block-end', index: 0, block: { type: 'reasoning', text: reasoning } })
  109. stream({ type: 'block-start', index: 1, blockType: 'text' })
  110. let text = ''
  111. for (let index = 0; index < deltas; index += 1) {
  112. const delta = `w${String(index)} `
  113. text += delta
  114. stream({ type: 'text-delta', index: 1, text: delta })
  115. }
  116. stream({ type: 'block-end', index: 1, block: { type: 'text', text } })
  117. const usage = { inputTokens: 100, outputTokens: deltas }
  118. stream({ type: 'usage', usage })
  119. stream({ type: 'finish', reason: { kind: 'stop' } })
  120. const snapshot = accumulator.snapshot()
  121. records += snapshot.length
  122. push('assistant/message', {
  123. turn,
  124. step: 1,
  125. message: {
  126. id: `assistant-${String(turn)}`,
  127. role: 'assistant',
  128. content: [{ type: 'reasoning', text: reasoning }, { type: 'text', text }],
  129. source: { kind: 'model', provider: 'bench', model: 'bench' },
  130. },
  131. usage,
  132. stream: snapshot,
  133. }, { surfaceOp: 'append' })
  134. push('step/end', { turn, step: 1 })
  135. push('turn/end', { turn, reason: { kind: 'completed' } })
  136. }
  137. return { entries, records }
  138. }
  139. function foldOnce(entries: readonly SessionEventLikeEntry[]): { readonly ms: number; readonly nodes: number } {
  140. const started = performance.now()
  141. const assembler = new ConversationNodeAssembler(new BenchEventDefinitions(), new BenchViewDefinitions())
  142. assembler.replaceWindow(entries, false)
  143. assembler.activateTarget('chat')
  144. const snapshot = assembler.snapshot('chat') as ChatSnapshot | undefined
  145. return { ms: performance.now() - started, nodes: snapshot?.order.length ?? 0 }
  146. }
  147. function bestOf(
  148. entries: readonly SessionEventLikeEntry[],
  149. attempts: number,
  150. ): { readonly ms: number; readonly nodes: number } {
  151. let best = foldOnce(entries)
  152. for (let attempt = 1; attempt < attempts; attempt += 1) {
  153. const next = foldOnce(entries)
  154. if (next.ms < best.ms) best = next
  155. }
  156. return best
  157. }
  158. function positiveInteger(value: string | undefined, label: string): number {
  159. const parsed = Number(value)
  160. if (!Number.isSafeInteger(parsed) || parsed <= 0) throw new Error(`${label} must be a positive integer`)
  161. return parsed
  162. }
  163. assertBuiltBenchmarkRuntime(import.meta.url, {
  164. '@deepseek-ai/dsh-client-store': import.meta.resolve('@deepseek-ai/dsh-client-store'),
  165. '@deepseek-ai/dsh-llm/assistant-stream': import.meta.resolve('@deepseek-ai/dsh-llm/assistant-stream'),
  166. '@deepseek-ai/dsh-session/surface': import.meta.resolve('@deepseek-ai/dsh-session/surface'),
  167. '@deepseek-ai/dsh-token-meter/client': import.meta.resolve('@deepseek-ai/dsh-token-meter/client'),
  168. })
  169. const [turnsValue, smallDeltasValue, largeDeltasValue, attemptsValue] = process.argv.slice(2)
  170. const turns = positiveInteger(turnsValue, 'turns')
  171. const smallDeltas = positiveInteger(smallDeltasValue, 'small deltas')
  172. const largeDeltas = positiveInteger(largeDeltasValue, 'large deltas')
  173. const attempts = positiveInteger(attemptsValue, 'attempts')
  174. const small = synthesizeWindow(turns, smallDeltas)
  175. const large = synthesizeWindow(turns, largeDeltas)
  176. if (large.entries.length !== small.entries.length || large.records !== small.records) {
  177. throw new Error('conversation-fold workloads must have matching event and compact-record counts')
  178. }
  179. const smallFold = bestOf(small.entries, attempts)
  180. const largeFold = bestOf(large.entries, attempts)
  181. const report: ConversationFoldWorkerReport = {
  182. events: large.entries.length,
  183. compactRecords: large.records,
  184. streamedDeltas: turns * (largeDeltas + Math.floor(largeDeltas / 4)),
  185. chatNodes: largeFold.nodes,
  186. smallFoldMs: Math.round(smallFold.ms * 10) / 10,
  187. largeFoldMs: Math.round(largeFold.ms * 10) / 10,
  188. scaling: Math.round((largeFold.ms / Math.max(smallFold.ms, 1)) * 100) / 100,
  189. }
  190. process.stdout.write(`${JSON.stringify(report)}\n`)