@operato/twin-kernel 0.7.9 → 0.7.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/forecast.d.ts +19 -0
- package/dist/forecast.js +56 -6
- package/dist-cjs/index.cjs +30 -6
- package/package.json +1 -1
package/dist/forecast.d.ts
CHANGED
|
@@ -35,3 +35,22 @@ export interface MonteCarloOptions {
|
|
|
35
35
|
* run 마다 fork(현재 보존) + scenario.load(seed+i)(미래 변주) + horizon 까지 구동. 원본 무간섭.
|
|
36
36
|
*/
|
|
37
37
|
export declare function monteCarloForecast(twin: ForecastTwin, opts: MonteCarloOptions): MonteCarloResult;
|
|
38
|
+
/**
|
|
39
|
+
* 같은 예측을, **회차 사이에 자리를 내주면서** 계산한다.
|
|
40
|
+
*
|
|
41
|
+
* ── 왜 필요한가 (2026-08-17 실측) ───────────────────────────────────────────
|
|
42
|
+
* 이 예측은 호출한 쪽의 이벤트 루프에서 **동기로** 돈다. 실측으로 트윈 하나의 예측 한 번이
|
|
43
|
+
* 30회차 × 300틱 = 9,000틱, 한 틱 3.2ms → **28.5초**였다. 그동안 그 프로세스의 라이브 트윈도,
|
|
44
|
+
* HTTP 도, 구독도 전부 멈춘다 — 예측 한 번에 시스템 전체가 정지한다.
|
|
45
|
+
*
|
|
46
|
+
* 총 시간은 이 함수로 줄지 않는다(그건 틱 수를 줄이는 다른 일이다). 줄어드는 것은 **한 번에
|
|
47
|
+
* 붙잡는 시간**이다. 회차 하나가 끝날 때마다 자리를 내주면 그 사이에 틱과 요청이 지나간다.
|
|
48
|
+
*
|
|
49
|
+
* 양보 방법은 **호출자가 준다.** 커널은 실행 환경을 모른다(`setImmediate` 는 서버 런타임의 것이다).
|
|
50
|
+
* 주지 않으면 마이크로태스크로 떨어지는데, 그것은 I/O 에 자리를 내주지 못하므로 **얼어붙는 증상은
|
|
51
|
+
* 그대로**다 — 그래서 기본값에 기대지 말고 호출자가 명시하는 것이 맞다.
|
|
52
|
+
*/
|
|
53
|
+
export declare function monteCarloForecastAsync(twin: ForecastTwin, opts: MonteCarloOptions & {
|
|
54
|
+
yieldFn?: () => Promise<void>;
|
|
55
|
+
yieldEveryTicks?: number;
|
|
56
|
+
}): Promise<MonteCarloResult>;
|
package/dist/forecast.js
CHANGED
|
@@ -18,14 +18,9 @@ function clockOf(twin) {
|
|
|
18
18
|
*/
|
|
19
19
|
export function monteCarloForecast(twin, opts) {
|
|
20
20
|
const now = clockOf(twin);
|
|
21
|
-
const step = opts.tickMs ?? 1000;
|
|
22
|
-
const baseSeed = opts.scenario.seed ?? 1;
|
|
23
21
|
const samples = [];
|
|
24
22
|
for (let i = 0; i < opts.runs; i++) {
|
|
25
|
-
const fc = twin
|
|
26
|
-
fc.scenario.load({ ...opts.scenario, seed: baseSeed + i }); // 미래만 변주(현재 상태는 fork 로 보존)
|
|
27
|
-
fc.scenario.start();
|
|
28
|
-
const target = now + opts.horizonMs;
|
|
23
|
+
const { fc, step, target } = startRun(twin, opts, now, i);
|
|
29
24
|
let guard = 0;
|
|
30
25
|
while (clockOf(fc) < target && guard++ < 1_000_000)
|
|
31
26
|
fc.tick(step);
|
|
@@ -33,6 +28,61 @@ export function monteCarloForecast(twin, opts) {
|
|
|
33
28
|
}
|
|
34
29
|
return summarize(opts.runs, samples);
|
|
35
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* 한 회차의 **출발점** — fork(현재 보존) + 그 회차의 미래(seed 변주).
|
|
33
|
+
*
|
|
34
|
+
* 동기·비동기 두 진입점이 이것을 함께 쓴다. 구동 루프는 각자 쓰지만(하나는 중간에 await 한다),
|
|
35
|
+
* **표본이 무엇이 되는지를 정하는 규칙**(어디서 갈라져 어떤 seed 로 도는가)은 여기 한 곳이다.
|
|
36
|
+
* 그것이 갈라지면 같은 seed 가 다른 수를 내고, 그때는 어느 쪽이 옳은지 가릴 방법이 없다.
|
|
37
|
+
*/
|
|
38
|
+
function startRun(twin, opts, nowMs, i) {
|
|
39
|
+
const fc = twin.fork();
|
|
40
|
+
fc.scenario.load({ ...opts.scenario, seed: (opts.scenario.seed ?? 1) + i }); // 미래만 변주(현재 상태는 fork 로 보존)
|
|
41
|
+
fc.scenario.start();
|
|
42
|
+
return { fc, step: opts.tickMs ?? 1000, target: nowMs + opts.horizonMs };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 같은 예측을, **회차 사이에 자리를 내주면서** 계산한다.
|
|
46
|
+
*
|
|
47
|
+
* ── 왜 필요한가 (2026-08-17 실측) ───────────────────────────────────────────
|
|
48
|
+
* 이 예측은 호출한 쪽의 이벤트 루프에서 **동기로** 돈다. 실측으로 트윈 하나의 예측 한 번이
|
|
49
|
+
* 30회차 × 300틱 = 9,000틱, 한 틱 3.2ms → **28.5초**였다. 그동안 그 프로세스의 라이브 트윈도,
|
|
50
|
+
* HTTP 도, 구독도 전부 멈춘다 — 예측 한 번에 시스템 전체가 정지한다.
|
|
51
|
+
*
|
|
52
|
+
* 총 시간은 이 함수로 줄지 않는다(그건 틱 수를 줄이는 다른 일이다). 줄어드는 것은 **한 번에
|
|
53
|
+
* 붙잡는 시간**이다. 회차 하나가 끝날 때마다 자리를 내주면 그 사이에 틱과 요청이 지나간다.
|
|
54
|
+
*
|
|
55
|
+
* 양보 방법은 **호출자가 준다.** 커널은 실행 환경을 모른다(`setImmediate` 는 서버 런타임의 것이다).
|
|
56
|
+
* 주지 않으면 마이크로태스크로 떨어지는데, 그것은 I/O 에 자리를 내주지 못하므로 **얼어붙는 증상은
|
|
57
|
+
* 그대로**다 — 그래서 기본값에 기대지 말고 호출자가 명시하는 것이 맞다.
|
|
58
|
+
*/
|
|
59
|
+
export async function monteCarloForecastAsync(twin, opts) {
|
|
60
|
+
const now = clockOf(twin);
|
|
61
|
+
const yieldFn = opts.yieldFn ?? (() => Promise.resolve());
|
|
62
|
+
/*
|
|
63
|
+
* **회차 사이만 내주면 부족하다.** 회차 하나가 300틱 × 3.2ms ≈ 1초라, 그동안 들어온 요청은 그만큼
|
|
64
|
+
* 기다린다(실측 응답 3초). 그래서 틱 몇 개마다도 끊는다 — 기본값은 라이브 틱 간격(1초)보다 훨씬
|
|
65
|
+
* 짧게 잡아, 트윈이 한 박자도 밀리지 않을 크기로 둔다.
|
|
66
|
+
*/
|
|
67
|
+
const everyTicks = Math.max(1, opts.yieldEveryTicks ?? 50);
|
|
68
|
+
const samples = [];
|
|
69
|
+
for (let i = 0; i < opts.runs; i++) {
|
|
70
|
+
const { fc, step, target } = startRun(twin, opts, now, i);
|
|
71
|
+
let guard = 0;
|
|
72
|
+
let sinceYield = 0;
|
|
73
|
+
while (clockOf(fc) < target && guard++ < 1_000_000) {
|
|
74
|
+
fc.tick(step);
|
|
75
|
+
if (++sinceYield >= everyTicks) {
|
|
76
|
+
sinceYield = 0;
|
|
77
|
+
await yieldFn();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
samples.push(opts.metric(fc.getSnapshot()));
|
|
81
|
+
if (i < opts.runs - 1)
|
|
82
|
+
await yieldFn();
|
|
83
|
+
}
|
|
84
|
+
return summarize(opts.runs, samples);
|
|
85
|
+
}
|
|
36
86
|
function summarize(runs, samples) {
|
|
37
87
|
const sorted = [...samples].sort((a, b) => a - b);
|
|
38
88
|
const pct = (p) => sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))];
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -122,6 +122,7 @@ __export(index_exports, {
|
|
|
122
122
|
meetsTests: () => meetsTests,
|
|
123
123
|
minuteOfDayAt: () => minuteOfDayAt,
|
|
124
124
|
monteCarloForecast: () => monteCarloForecast,
|
|
125
|
+
monteCarloForecastAsync: () => monteCarloForecastAsync,
|
|
125
126
|
objectEvent: () => objectEvent,
|
|
126
127
|
offCalendarAt: () => offCalendarAt,
|
|
127
128
|
offCalendarReasonAt: () => offCalendarReasonAt,
|
|
@@ -730,20 +731,42 @@ function clockOf2(twin) {
|
|
|
730
731
|
}
|
|
731
732
|
function monteCarloForecast(twin, opts) {
|
|
732
733
|
const now = clockOf2(twin);
|
|
733
|
-
const step = opts.tickMs ?? 1e3;
|
|
734
|
-
const baseSeed = opts.scenario.seed ?? 1;
|
|
735
734
|
const samples = [];
|
|
736
735
|
for (let i = 0; i < opts.runs; i++) {
|
|
737
|
-
const fc = twin
|
|
738
|
-
fc.scenario.load({ ...opts.scenario, seed: baseSeed + i });
|
|
739
|
-
fc.scenario.start();
|
|
740
|
-
const target = now + opts.horizonMs;
|
|
736
|
+
const { fc, step, target } = startRun(twin, opts, now, i);
|
|
741
737
|
let guard = 0;
|
|
742
738
|
while (clockOf2(fc) < target && guard++ < 1e6) fc.tick(step);
|
|
743
739
|
samples.push(opts.metric(fc.getSnapshot()));
|
|
744
740
|
}
|
|
745
741
|
return summarize(opts.runs, samples);
|
|
746
742
|
}
|
|
743
|
+
function startRun(twin, opts, nowMs, i) {
|
|
744
|
+
const fc = twin.fork();
|
|
745
|
+
fc.scenario.load({ ...opts.scenario, seed: (opts.scenario.seed ?? 1) + i });
|
|
746
|
+
fc.scenario.start();
|
|
747
|
+
return { fc, step: opts.tickMs ?? 1e3, target: nowMs + opts.horizonMs };
|
|
748
|
+
}
|
|
749
|
+
async function monteCarloForecastAsync(twin, opts) {
|
|
750
|
+
const now = clockOf2(twin);
|
|
751
|
+
const yieldFn = opts.yieldFn ?? (() => Promise.resolve());
|
|
752
|
+
const everyTicks = Math.max(1, opts.yieldEveryTicks ?? 50);
|
|
753
|
+
const samples = [];
|
|
754
|
+
for (let i = 0; i < opts.runs; i++) {
|
|
755
|
+
const { fc, step, target } = startRun(twin, opts, now, i);
|
|
756
|
+
let guard = 0;
|
|
757
|
+
let sinceYield = 0;
|
|
758
|
+
while (clockOf2(fc) < target && guard++ < 1e6) {
|
|
759
|
+
fc.tick(step);
|
|
760
|
+
if (++sinceYield >= everyTicks) {
|
|
761
|
+
sinceYield = 0;
|
|
762
|
+
await yieldFn();
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
samples.push(opts.metric(fc.getSnapshot()));
|
|
766
|
+
if (i < opts.runs - 1) await yieldFn();
|
|
767
|
+
}
|
|
768
|
+
return summarize(opts.runs, samples);
|
|
769
|
+
}
|
|
747
770
|
function summarize(runs, samples) {
|
|
748
771
|
const sorted = [...samples].sort((a, b) => a - b);
|
|
749
772
|
const pct = (p) => sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))];
|
|
@@ -6516,6 +6539,7 @@ function retiredVocabularyIn(line) {
|
|
|
6516
6539
|
meetsTests,
|
|
6517
6540
|
minuteOfDayAt,
|
|
6518
6541
|
monteCarloForecast,
|
|
6542
|
+
monteCarloForecastAsync,
|
|
6519
6543
|
objectEvent,
|
|
6520
6544
|
offCalendarAt,
|
|
6521
6545
|
offCalendarReasonAt,
|
package/package.json
CHANGED