/* * 루프 지체 계기 — **원인을 지어내지 않기 위한 값**이 정확한지 본다 (2026-08-21). * * 지키는 것: * ① 늦게 깨어난 만큼만 지체다 — 제때 깨어난 표본은 0 이고, 0 을 정체로 세지 않는다. * ② 창이 닫히기 전에는 `recent` 가 **null** 이다(채우는 중인 창을 「지금」으로 읽으면 과소 집계다). * ③ 가장 길게 막힌 **시각**을 든다 — 언제였는지가 원인을 가리는 근거다. * ④ 문턱 이하의 흔들림은 정체로 세지 않는다. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { LoopLagSampler } from '../server/engine/loop-lag.ts' /** 시계를 손으로 밀어 넣는다 — 실제 타이머로 재면 기계 상태가 결과를 정한다. */ function sampler(sampleMs = 500, stallMs = 200, windowMs = 10_000) { let now = 1_000_000 const s = new LoopLagSampler(sampleMs, stallMs, windowMs, () => now) return { s, advance: (ms: number) => (now += ms), at: () => now } } test('제때 깨어난 표본은 지체가 0 이고 정체로 세지 않는다', () => { const { s, advance, at } = sampler() for (let i = 0; i < 4; i++) { advance(500) assert.equal(s.sample(at()), 0) } assert.equal(s.view().total.stalls, 0) assert.equal(s.view().total.maxMs, 0) assert.equal(s.view().total.samples, 4) }) test('늦게 깨어난 만큼이 지체다 — 그 초과분만', () => { const { s, advance, at } = sampler() advance(500) s.sample(at()) advance(4_600) // 예정보다 4,100ms 늦게 assert.equal(s.sample(at()), 4_100) assert.equal(s.view().total.maxMs, 4_100) assert.equal(s.view().total.stalls, 1) }) test('문턱 이하의 흔들림은 정체가 아니다 — 값은 남기고 세지는 않는다', () => { const { s, advance, at } = sampler(500, 200) advance(650) // 150ms 지체 — 문턱 200 미만 s.sample(at()) const v = s.view() assert.equal(v.total.maxMs, 150) assert.equal(v.total.stalls, 0, '문턱 이하를 세면 정상 상태가 정체로 보인다') }) test('창이 닫히기 전에는 recent 가 null 이다 — 채우는 중인 창을 「지금」으로 읽지 않는다', () => { const { s, advance, at } = sampler(500, 200, 2_000) advance(500) s.sample(at()) assert.equal(s.view().recent, null) /* 창 길이를 넘기면 그 창이 닫혀 `recent` 가 된다. */ advance(2_000) s.sample(at()) const v = s.view() assert.ok(v.recent, '창 길이를 넘겼으면 닫힌 창이 있어야 한다') assert.equal(v.recent!.stalls, 1, '1,500ms 지체는 정체 한 번이다') }) test('가장 길게 막힌 시각을 든다 — 언제였는지가 원인을 가린다', () => { const { s, advance, at } = sampler() advance(500) s.sample(at()) advance(3_000) const worst = at() s.sample(worst) advance(500) s.sample(at()) assert.equal(s.view().worstAtMs, worst) }) test('한 번도 재지 않았으면 정체 시각은 null 이다 — 0 으로 적지 않는다', () => { const { s } = sampler() assert.equal(s.view().worstAtMs, null) assert.equal(s.view().recent, null) })