budget.spec.ts 3.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. /** Host budget decisions use controlled clocks and ELU samples; worker execution and binding transport stay real. */
  2. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
  3. import { Context } from '@deepseek-ai/cordis'
  4. import { WorkerThreadCodeRuntime } from '@deepseek-ai/dsh-code-runtime-worker-thread'
  5. import type { CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
  6. const meter = vi.hoisted(() => ({ sample: vi.fn() }))
  7. vi.mock('node:worker_threads', async (importOriginal) => {
  8. const original = await importOriginal<typeof import('node:worker_threads')>()
  9. return {
  10. ...original,
  11. Worker: class extends original.Worker {
  12. constructor(...args: ConstructorParameters<typeof original.Worker>) {
  13. super(...args)
  14. this.performance.eventLoopUtilization = meter.sample
  15. }
  16. },
  17. }
  18. })
  19. describe('worker budgets with controlled ELU samples and real binding transport', () => {
  20. let ctx: Context
  21. let controller: AbortController
  22. let run: Promise<CodeRunResult> | undefined
  23. let release: (() => void) | undefined
  24. beforeEach(() => {
  25. ctx = new Context()
  26. controller = new AbortController()
  27. run = undefined
  28. release = undefined
  29. meter.sample.mockReset().mockReturnValue({ active: 10, idle: 0, utilization: 1 })
  30. // Worker bootstrap and scheduling contribute to ELU active time; only the
  31. // measured input and host deadlines are controlled, not worker execution.
  32. vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'] })
  33. })
  34. afterEach(async () => {
  35. const owned = { ctx, controller, run, release }
  36. try {
  37. owned.controller.abort('test cleanup')
  38. owned.release?.()
  39. } finally {
  40. vi.useRealTimers()
  41. }
  42. try {
  43. await owned.run
  44. } finally {
  45. await owned.ctx.fiber.dispose()
  46. }
  47. })
  48. async function pendingBinding(): Promise<void> {
  49. await ctx.plugin(WorkerThreadCodeRuntime, { computeMs: 1_000, maxWallMs: 30_000 })
  50. let entered!: () => void
  51. const ready = new Promise<void>((resolve) => { entered = resolve })
  52. const binding = new Promise<string>((resolve) => { release = () => { resolve('slow-done') } })
  53. run = ctx.codeRuntime.run({
  54. program: 'return await tools.slow({})',
  55. bindings: [{ global: 'tools', functions: { slow: () => { entered(); return binding } } }],
  56. signal: controller.signal,
  57. })
  58. await Promise.race([
  59. ready,
  60. run.then((result) => { throw new Error('Worker settled before binding entry: ' + JSON.stringify(result)) }),
  61. ])
  62. }
  63. it('does not charge a binding wait longer than the compute budget', async () => {
  64. await pendingBinding()
  65. const settled = vi.fn()
  66. void run!.then(settled, settled)
  67. meter.sample.mockReturnValue({ active: 10, idle: 1_500, utilization: 10 / 1_510 })
  68. await vi.advanceTimersByTimeAsync(1_500)
  69. expect(meter.sample).toHaveBeenCalled()
  70. expect(settled).not.toHaveBeenCalled()
  71. release!()
  72. expect(await run).toEqual({ logs: [], value: 'slow-done' })
  73. })
  74. it('expires active time even while a binding is pending', async () => {
  75. await pendingBinding()
  76. meter.sample.mockReturnValue({ active: 1_001, idle: 1_500, utilization: 1_001 / 2_501 })
  77. await vi.advanceTimersByTimeAsync(25)
  78. expect(await run).toEqual({ logs: [], error: { kind: 'timeout', message: 'compute budget exhausted (1000ms busy)' } })
  79. })
  80. it('expires the wall ceiling while active time remains below the compute budget', async () => {
  81. await pendingBinding()
  82. meter.sample.mockReturnValue({ active: 10, idle: 30_000, utilization: 10 / 30_010 })
  83. await vi.advanceTimersByTimeAsync(30_000)
  84. expect(await run).toEqual({ logs: [], error: { kind: 'timeout', message: 'wall-clock ceiling reached (30000ms)' } })
  85. })
  86. })