codexmeter 1.0.18 → 1.0.20
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/bin/codexmeter.js +38 -0
- package/dist/assets/index-BBdnQ5aF.css +1 -0
- package/dist/assets/index-HV8wVWe1.js +34 -0
- package/dist/index.html +16 -16
- package/package.json +1 -1
- package/server/export-replay.js +2 -2
- package/server/ingest.js +183 -157
- package/server/live-state.js +19 -0
- package/server/rollout-reader.js +185 -26
- package/server/rollout-worker-pool.js +103 -4
- package/server/rollout-worker.js +2 -2
- package/src/utils/animationsDefault.js +4 -6
- package/dist/assets/index-Bbiu1QZp.js +0 -34
- package/dist/assets/index-CVvcsIkn.css +0 -1
package/server/rollout-reader.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
import { existsSync } from 'fs';
|
|
1
|
+
import { existsSync, statSync } from 'fs';
|
|
2
2
|
import { readFile } from 'fs/promises';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
3
4
|
import { createDayKeyFormatter } from './day-key.js';
|
|
4
5
|
|
|
5
6
|
const ACTIVE_GAP_CAP_MS = 15 * 60 * 1000;
|
|
7
|
+
const LINE_HEADER_SCAN_CHARS = 512;
|
|
8
|
+
const TIMESTAMP_RE = /"timestamp"\s*:\s*"([^"]+)"/;
|
|
9
|
+
const DEFAULT_RG_MIN_BYTES = 10 * 1024 * 1024;
|
|
10
|
+
const RG_RELEVANT_PATTERN =
|
|
11
|
+
'"type"\\s*:\\s*"session_meta"|' +
|
|
12
|
+
'"type"\\s*:\\s*"turn_context"|' +
|
|
13
|
+
'"type"\\s*:\\s*"token_count"';
|
|
14
|
+
const RG_TOKEN_COUNT_PATTERN = '"type"\\s*:\\s*"token_count"';
|
|
15
|
+
const RG_TIMESTAMP_PATTERN = '^\\{[^{}]*"timestamp"\\s*:\\s*"[^"]+"';
|
|
6
16
|
|
|
7
17
|
export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
8
18
|
if (!rolloutPath || !existsSync(rolloutPath)) {
|
|
@@ -11,6 +21,9 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
|
11
21
|
|
|
12
22
|
const tz = opts.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
13
23
|
const toDayKey = opts.toDayKey || createDayKeyFormatter(tz);
|
|
24
|
+
const fastScan = opts.fastScan === true;
|
|
25
|
+
const rgScan = opts.rgScan === true;
|
|
26
|
+
const rgMinBytes = Math.max(0, Number(opts.rgMinBytes ?? DEFAULT_RG_MIN_BYTES) || 0);
|
|
14
27
|
const result = {
|
|
15
28
|
model_name: null,
|
|
16
29
|
reasoning_effort: null,
|
|
@@ -27,7 +40,20 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
|
27
40
|
};
|
|
28
41
|
|
|
29
42
|
try {
|
|
30
|
-
|
|
43
|
+
let lines = null;
|
|
44
|
+
let activeFromRgTimestamps = false;
|
|
45
|
+
if (rgScan && shouldUseRipgrep(rolloutPath, rgMinBytes)) {
|
|
46
|
+
const rgResult = await readRolloutLinesWithRipgrep(rolloutPath);
|
|
47
|
+
if (rgResult) {
|
|
48
|
+
lines = rgResult.relevantLines;
|
|
49
|
+
applyTimestampMatches(result, rgResult.timestampMatches, toDayKey);
|
|
50
|
+
activeFromRgTimestamps = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!lines) {
|
|
54
|
+
const content = await readFile(rolloutPath, 'utf8');
|
|
55
|
+
lines = content.split(/\r?\n/);
|
|
56
|
+
}
|
|
31
57
|
|
|
32
58
|
let prevTimestamp = null;
|
|
33
59
|
let activeMs = 0;
|
|
@@ -39,21 +65,17 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
|
39
65
|
let lastReasoningOutputTokens = 0;
|
|
40
66
|
let lastTotalTokens = 0;
|
|
41
67
|
let hasSeenUsage = false;
|
|
42
|
-
for (const line of
|
|
68
|
+
for (const line of lines) {
|
|
43
69
|
if (!line.trim()) continue;
|
|
44
70
|
|
|
45
71
|
try {
|
|
46
|
-
|
|
72
|
+
let obj = null;
|
|
73
|
+
let ts = null;
|
|
47
74
|
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
if (!
|
|
51
|
-
|
|
52
|
-
result.first_timestamp = ts;
|
|
53
|
-
}
|
|
54
|
-
if (!result.last_timestamp || ts > result.last_timestamp) {
|
|
55
|
-
result.last_timestamp = ts;
|
|
56
|
-
}
|
|
75
|
+
if (fastScan || activeFromRgTimestamps) {
|
|
76
|
+
ts = extractTimestampMs(line);
|
|
77
|
+
if (ts !== null && !activeFromRgTimestamps) {
|
|
78
|
+
updateTimestampBounds(result, ts);
|
|
57
79
|
if (prevTimestamp !== null && ts >= prevTimestamp) {
|
|
58
80
|
const deltaMs = Math.min(ts - prevTimestamp, ACTIVE_GAP_CAP_MS);
|
|
59
81
|
activeMs += deltaMs;
|
|
@@ -64,6 +86,25 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
|
64
86
|
}
|
|
65
87
|
prevTimestamp = ts;
|
|
66
88
|
}
|
|
89
|
+
if (!activeFromRgTimestamps && !isRolloutLineWorthParsing(line)) continue;
|
|
90
|
+
obj = JSON.parse(line);
|
|
91
|
+
} else {
|
|
92
|
+
obj = JSON.parse(line);
|
|
93
|
+
if (obj.timestamp) {
|
|
94
|
+
ts = new Date(obj.timestamp).getTime();
|
|
95
|
+
if (!isNaN(ts)) {
|
|
96
|
+
updateTimestampBounds(result, ts);
|
|
97
|
+
if (prevTimestamp !== null && ts >= prevTimestamp) {
|
|
98
|
+
const deltaMs = Math.min(ts - prevTimestamp, ACTIVE_GAP_CAP_MS);
|
|
99
|
+
activeMs += deltaMs;
|
|
100
|
+
if (deltaMs > 0) {
|
|
101
|
+
const dayKey = toDayKey(prevTimestamp);
|
|
102
|
+
activeByDay.set(dayKey, (activeByDay.get(dayKey) || 0) + deltaMs);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
prevTimestamp = ts;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
67
108
|
}
|
|
68
109
|
|
|
69
110
|
if (obj.type === 'event_msg' && obj.payload?.type === 'token_count') {
|
|
@@ -121,16 +162,13 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
|
121
162
|
lastOutputTokens = outputTokens;
|
|
122
163
|
lastReasoningOutputTokens = reasoningOutputTokens;
|
|
123
164
|
lastTotalTokens = totalTokens;
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const dayKey = toDayKey(ts);
|
|
132
|
-
mergeUsageTotals(usageByDay, dayKey, usageDelta);
|
|
133
|
-
}
|
|
165
|
+
if (ts !== null && hasUsageBoundarySignal(usageDelta)) {
|
|
166
|
+
if (!result.first_usage_timestamp || ts < result.first_usage_timestamp) {
|
|
167
|
+
result.first_usage_timestamp = ts;
|
|
168
|
+
}
|
|
169
|
+
if (hasUsage(usageDelta)) {
|
|
170
|
+
const dayKey = toDayKey(ts);
|
|
171
|
+
mergeUsageTotals(usageByDay, dayKey, usageDelta);
|
|
134
172
|
}
|
|
135
173
|
}
|
|
136
174
|
}
|
|
@@ -179,19 +217,30 @@ export async function enrichFromRollout(rolloutPath, opts = {}) {
|
|
|
179
217
|
return result;
|
|
180
218
|
}
|
|
181
219
|
|
|
182
|
-
export async function readUsageTimeline(rolloutPath) {
|
|
220
|
+
export async function readUsageTimeline(rolloutPath, opts = {}) {
|
|
183
221
|
if (!rolloutPath || !existsSync(rolloutPath)) {
|
|
184
222
|
return [];
|
|
185
223
|
}
|
|
186
224
|
|
|
187
225
|
try {
|
|
188
|
-
const
|
|
226
|
+
const fastScan = opts.fastScan === true;
|
|
227
|
+
const rgScan = opts.rgScan === true;
|
|
228
|
+
const rgMinBytes = Math.max(0, Number(opts.rgMinBytes ?? DEFAULT_RG_MIN_BYTES) || 0);
|
|
229
|
+
let lines = null;
|
|
230
|
+
if (rgScan && shouldUseRipgrep(rolloutPath, rgMinBytes)) {
|
|
231
|
+
lines = await readMatchingLinesWithRipgrep(RG_TOKEN_COUNT_PATTERN, rolloutPath);
|
|
232
|
+
}
|
|
233
|
+
if (!lines) {
|
|
234
|
+
const content = await readFile(rolloutPath, 'utf8');
|
|
235
|
+
lines = content.split(/\r?\n/);
|
|
236
|
+
}
|
|
189
237
|
const timeline = [];
|
|
190
238
|
let segmentId = 0;
|
|
191
239
|
let lastTotalTokens = 0;
|
|
192
240
|
let hasSeenUsage = false;
|
|
193
|
-
for (const line of
|
|
241
|
+
for (const line of lines) {
|
|
194
242
|
if (!line.trim()) continue;
|
|
243
|
+
if (fastScan && !isTokenCountEventLine(line)) continue;
|
|
195
244
|
try {
|
|
196
245
|
const obj = JSON.parse(line);
|
|
197
246
|
if (obj.type !== 'event_msg' || obj.payload?.type !== 'token_count') continue;
|
|
@@ -241,6 +290,116 @@ export function findUsageAtOrBefore(timeline, timestampMs) {
|
|
|
241
290
|
return findUsageEntryAtOrBefore(timeline, timestampMs)?.usage || null;
|
|
242
291
|
}
|
|
243
292
|
|
|
293
|
+
function shouldUseRipgrep(rolloutPath, minBytes) {
|
|
294
|
+
try {
|
|
295
|
+
return statSync(rolloutPath).size >= minBytes;
|
|
296
|
+
} catch {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function readRolloutLinesWithRipgrep(rolloutPath) {
|
|
302
|
+
const [relevantLines, timestampMatches] = await Promise.all([
|
|
303
|
+
readMatchingLinesWithRipgrep(RG_RELEVANT_PATTERN, rolloutPath),
|
|
304
|
+
readMatchingLinesWithRipgrep(RG_TIMESTAMP_PATTERN, rolloutPath, ['--only-matching']),
|
|
305
|
+
]);
|
|
306
|
+
if (!relevantLines || !timestampMatches) return null;
|
|
307
|
+
return { relevantLines, timestampMatches };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function readMatchingLinesWithRipgrep(pattern, rolloutPath, extraArgs = []) {
|
|
311
|
+
return new Promise((resolve) => {
|
|
312
|
+
const args = [
|
|
313
|
+
'--no-heading',
|
|
314
|
+
'--no-line-number',
|
|
315
|
+
'--color',
|
|
316
|
+
'never',
|
|
317
|
+
...extraArgs,
|
|
318
|
+
pattern,
|
|
319
|
+
rolloutPath,
|
|
320
|
+
];
|
|
321
|
+
const child = spawn('rg', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
322
|
+
let stdout = '';
|
|
323
|
+
|
|
324
|
+
child.stdout.setEncoding('utf8');
|
|
325
|
+
child.stdout.on('data', (chunk) => {
|
|
326
|
+
stdout += chunk;
|
|
327
|
+
});
|
|
328
|
+
child.on('error', () => resolve(null));
|
|
329
|
+
child.on('close', (code) => {
|
|
330
|
+
if (code !== 0 && code !== 1) {
|
|
331
|
+
resolve(null);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
resolve(stdout ? stdout.split(/\r?\n/).filter(Boolean) : []);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function applyTimestampMatches(result, timestampMatches, toDayKey) {
|
|
340
|
+
let prevTimestamp = null;
|
|
341
|
+
let activeMs = 0;
|
|
342
|
+
const activeByDay = new Map();
|
|
343
|
+
|
|
344
|
+
for (const line of timestampMatches || []) {
|
|
345
|
+
const ts = extractTimestampMs(line);
|
|
346
|
+
if (ts === null) continue;
|
|
347
|
+
updateTimestampBounds(result, ts);
|
|
348
|
+
if (prevTimestamp !== null && ts >= prevTimestamp) {
|
|
349
|
+
const deltaMs = Math.min(ts - prevTimestamp, ACTIVE_GAP_CAP_MS);
|
|
350
|
+
activeMs += deltaMs;
|
|
351
|
+
if (deltaMs > 0) {
|
|
352
|
+
const dayKey = toDayKey(prevTimestamp);
|
|
353
|
+
activeByDay.set(dayKey, (activeByDay.get(dayKey) || 0) + deltaMs);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
prevTimestamp = ts;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (activeMs > 0) {
|
|
360
|
+
const activeByDaySeconds = Object.fromEntries(
|
|
361
|
+
[...activeByDay.entries()].map(([dayKey, ms]) => [dayKey, Math.round(ms / 1000)])
|
|
362
|
+
);
|
|
363
|
+
result.active_by_day = activeByDaySeconds;
|
|
364
|
+
result.active_seconds = Object.values(activeByDaySeconds).reduce((sum, seconds) => sum + seconds, 0);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function updateTimestampBounds(result, ts) {
|
|
369
|
+
if (!result.first_timestamp || ts < result.first_timestamp) {
|
|
370
|
+
result.first_timestamp = ts;
|
|
371
|
+
}
|
|
372
|
+
if (!result.last_timestamp || ts > result.last_timestamp) {
|
|
373
|
+
result.last_timestamp = ts;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function extractTimestampMs(line) {
|
|
378
|
+
const head = line.length > LINE_HEADER_SCAN_CHARS
|
|
379
|
+
? line.slice(0, LINE_HEADER_SCAN_CHARS)
|
|
380
|
+
: line;
|
|
381
|
+
const match = TIMESTAMP_RE.exec(head);
|
|
382
|
+
if (!match) return null;
|
|
383
|
+
const ts = Date.parse(match[1]);
|
|
384
|
+
return Number.isNaN(ts) ? null : ts;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function isRolloutLineWorthParsing(line) {
|
|
388
|
+
const head = line.length > LINE_HEADER_SCAN_CHARS
|
|
389
|
+
? line.slice(0, LINE_HEADER_SCAN_CHARS)
|
|
390
|
+
: line;
|
|
391
|
+
return head.includes('"type":"token_count"') ||
|
|
392
|
+
head.includes('"type":"turn_context"') ||
|
|
393
|
+
head.includes('"type":"session_meta"');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function isTokenCountEventLine(line) {
|
|
397
|
+
const head = line.length > LINE_HEADER_SCAN_CHARS
|
|
398
|
+
? line.slice(0, LINE_HEADER_SCAN_CHARS)
|
|
399
|
+
: line;
|
|
400
|
+
return head.includes('"type":"token_count"');
|
|
401
|
+
}
|
|
402
|
+
|
|
244
403
|
function normalizeUsageTotals(usage) {
|
|
245
404
|
return {
|
|
246
405
|
input_tokens: usage.input_tokens || 0,
|
|
@@ -3,8 +3,9 @@ import { Worker } from 'worker_threads';
|
|
|
3
3
|
|
|
4
4
|
export function createRolloutWorkerPool(opts = {}) {
|
|
5
5
|
const size = normalizePoolSize(opts.size);
|
|
6
|
+
const readerOptions = opts.readerOptions || {};
|
|
6
7
|
if (size <= 1) {
|
|
7
|
-
return createInlinePool();
|
|
8
|
+
return createInlinePool({ readerOptions });
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
const workers = new Set();
|
|
@@ -89,6 +90,7 @@ export function createRolloutWorkerPool(opts = {}) {
|
|
|
89
90
|
id,
|
|
90
91
|
rolloutPath: task.rolloutPath,
|
|
91
92
|
timezone: task.timezone,
|
|
93
|
+
readerOptions,
|
|
92
94
|
});
|
|
93
95
|
}
|
|
94
96
|
}
|
|
@@ -107,6 +109,70 @@ export function createRolloutWorkerPool(opts = {}) {
|
|
|
107
109
|
return Promise.all(rolloutPaths.map((rolloutPath) => runTask(rolloutPath, timezone)));
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
async function mapRolloutsInChunks(rolloutPaths, timezone, { chunkSize = 100, onChunk } = {}) {
|
|
113
|
+
const results = new Array(rolloutPaths.length);
|
|
114
|
+
const completed = new Array(rolloutPaths.length).fill(false);
|
|
115
|
+
const safeChunkSize = Math.max(1, Number(chunkSize) || 1);
|
|
116
|
+
const maxInFlight = Math.max(safeChunkSize, size * 4);
|
|
117
|
+
let nextStartIndex = 0;
|
|
118
|
+
let nextFlushIndex = 0;
|
|
119
|
+
let activeCount = 0;
|
|
120
|
+
let completedCount = 0;
|
|
121
|
+
let flushChain = Promise.resolve();
|
|
122
|
+
let rejected = false;
|
|
123
|
+
|
|
124
|
+
const scheduleFlush = (force = false) => {
|
|
125
|
+
if (typeof onChunk !== 'function') return;
|
|
126
|
+
const chunk = [];
|
|
127
|
+
while (nextFlushIndex < results.length && completed[nextFlushIndex]) {
|
|
128
|
+
chunk.push({ index: nextFlushIndex, result: results[nextFlushIndex] });
|
|
129
|
+
nextFlushIndex += 1;
|
|
130
|
+
if (!force && chunk.length >= safeChunkSize) break;
|
|
131
|
+
}
|
|
132
|
+
if (!chunk.length) return;
|
|
133
|
+
flushChain = flushChain.then(() => onChunk(chunk));
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
await new Promise((resolve, reject) => {
|
|
137
|
+
const launchNext = () => {
|
|
138
|
+
if (rejected) return;
|
|
139
|
+
while (activeCount < maxInFlight && nextStartIndex < rolloutPaths.length) {
|
|
140
|
+
const index = nextStartIndex;
|
|
141
|
+
const rolloutPath = rolloutPaths[index];
|
|
142
|
+
nextStartIndex += 1;
|
|
143
|
+
activeCount += 1;
|
|
144
|
+
runTask(rolloutPath, timezone)
|
|
145
|
+
.then((result) => {
|
|
146
|
+
results[index] = result;
|
|
147
|
+
completed[index] = true;
|
|
148
|
+
completedCount += 1;
|
|
149
|
+
activeCount -= 1;
|
|
150
|
+
scheduleFlush(false);
|
|
151
|
+
if (completedCount >= rolloutPaths.length && activeCount === 0) {
|
|
152
|
+
resolve();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
launchNext();
|
|
156
|
+
})
|
|
157
|
+
.catch((error) => {
|
|
158
|
+
rejected = true;
|
|
159
|
+
reject(error);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (completedCount >= rolloutPaths.length && activeCount === 0) {
|
|
164
|
+
resolve();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
launchNext();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
scheduleFlush(true);
|
|
172
|
+
await flushChain;
|
|
173
|
+
return results;
|
|
174
|
+
}
|
|
175
|
+
|
|
110
176
|
async function close() {
|
|
111
177
|
closed = true;
|
|
112
178
|
while (queuedTasks.length > 0) {
|
|
@@ -122,10 +188,10 @@ export function createRolloutWorkerPool(opts = {}) {
|
|
|
122
188
|
idleWorkers.length = 0;
|
|
123
189
|
}
|
|
124
190
|
|
|
125
|
-
return { mapRollouts, close, size };
|
|
191
|
+
return { mapRollouts, mapRolloutsInChunks, close, size };
|
|
126
192
|
}
|
|
127
193
|
|
|
128
|
-
function createInlinePool() {
|
|
194
|
+
function createInlinePool({ readerOptions = {} } = {}) {
|
|
129
195
|
return {
|
|
130
196
|
size: 1,
|
|
131
197
|
async mapRollouts(rolloutPaths, timezone) {
|
|
@@ -133,7 +199,7 @@ function createInlinePool() {
|
|
|
133
199
|
return Promise.all(
|
|
134
200
|
rolloutPaths.map(async (rolloutPath) => {
|
|
135
201
|
try {
|
|
136
|
-
const data = await enrichFromRollout(rolloutPath, { timezone });
|
|
202
|
+
const data = await enrichFromRollout(rolloutPath, { timezone, ...readerOptions });
|
|
137
203
|
return { ok: true, data, error: null };
|
|
138
204
|
} catch (error) {
|
|
139
205
|
return {
|
|
@@ -145,6 +211,39 @@ function createInlinePool() {
|
|
|
145
211
|
})
|
|
146
212
|
);
|
|
147
213
|
},
|
|
214
|
+
async mapRolloutsInChunks(rolloutPaths, timezone, { chunkSize = 100, onChunk } = {}) {
|
|
215
|
+
const { enrichFromRollout } = await import('./rollout-reader.js');
|
|
216
|
+
const results = [];
|
|
217
|
+
const completed = [];
|
|
218
|
+
const safeChunkSize = Math.max(1, Number(chunkSize) || 1);
|
|
219
|
+
|
|
220
|
+
for (let index = 0; index < rolloutPaths.length; index += 1) {
|
|
221
|
+
try {
|
|
222
|
+
const data = await enrichFromRollout(rolloutPaths[index], { timezone, ...readerOptions });
|
|
223
|
+
const result = { ok: true, data, error: null };
|
|
224
|
+
results[index] = result;
|
|
225
|
+
completed.push({ index, result });
|
|
226
|
+
} catch (error) {
|
|
227
|
+
const result = {
|
|
228
|
+
ok: false,
|
|
229
|
+
data: null,
|
|
230
|
+
error: error instanceof Error ? error.message : String(error),
|
|
231
|
+
};
|
|
232
|
+
results[index] = result;
|
|
233
|
+
completed.push({ index, result });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (typeof onChunk === 'function' && completed.length >= safeChunkSize) {
|
|
237
|
+
await onChunk(completed.splice(0, completed.length));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (typeof onChunk === 'function' && completed.length) {
|
|
242
|
+
await onChunk(completed.splice(0, completed.length));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return results;
|
|
246
|
+
},
|
|
148
247
|
async close() {},
|
|
149
248
|
};
|
|
150
249
|
}
|
package/server/rollout-worker.js
CHANGED
|
@@ -6,10 +6,10 @@ if (!parentPort) {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
parentPort.on('message', async (message) => {
|
|
9
|
-
const { id, rolloutPath, timezone } = message || {};
|
|
9
|
+
const { id, rolloutPath, timezone, readerOptions } = message || {};
|
|
10
10
|
|
|
11
11
|
try {
|
|
12
|
-
const result = await enrichFromRollout(rolloutPath, { timezone });
|
|
12
|
+
const result = await enrichFromRollout(rolloutPath, { timezone, ...(readerOptions || {}) });
|
|
13
13
|
parentPort.postMessage({ id, ok: true, data: result });
|
|
14
14
|
} catch (error) {
|
|
15
15
|
parentPort.postMessage({
|
|
@@ -6,7 +6,7 @@ const AUTO = 'auto';
|
|
|
6
6
|
* This is the single source of truth for:
|
|
7
7
|
* - Overview client-side presentation timing
|
|
8
8
|
* - Overview ECharts timing defaults
|
|
9
|
-
* - Overview
|
|
9
|
+
* - Overview live snapshot transport cadence
|
|
10
10
|
*
|
|
11
11
|
* `speed` scales the main timings:
|
|
12
12
|
* - `1` = baseline
|
|
@@ -21,8 +21,7 @@ export const OVERVIEW_INGEST_ANIMATION = {
|
|
|
21
21
|
speed: 1,
|
|
22
22
|
live: {
|
|
23
23
|
frameIntervalMs: 33,
|
|
24
|
-
|
|
25
|
-
dayKeysPerEmit: 1,
|
|
24
|
+
snapshotHz: 12,
|
|
26
25
|
},
|
|
27
26
|
main: {
|
|
28
27
|
presentationDurationMs: 220,
|
|
@@ -35,15 +34,13 @@ export const OVERVIEW_INGEST_ANIMATION = {
|
|
|
35
34
|
// Master switch for the end-of-ingest slowdown behavior.
|
|
36
35
|
enabled: true,
|
|
37
36
|
// Progress threshold where the tail mode starts, expressed from 0..1.
|
|
38
|
-
startPercent: 0.
|
|
37
|
+
startPercent: 0.95,
|
|
39
38
|
// Fixed presentation duration in ms during the tail; use AUTO to derive it from main.durationScale.
|
|
40
39
|
durationMs: AUTO,
|
|
41
40
|
// Multiplier applied to the normal presentation duration when durationMs is AUTO.
|
|
42
41
|
durationScale: 10,
|
|
43
42
|
// Shared easing applied by the Overview presentation animator during the tail.
|
|
44
43
|
easing: 'cubicOut',
|
|
45
|
-
// Backend Overview live-update cadence during the tail, in patches per second.
|
|
46
|
-
overviewHz: 10,
|
|
47
44
|
},
|
|
48
45
|
daily: {
|
|
49
46
|
chartAppearDurationMs: AUTO,
|
|
@@ -106,6 +103,7 @@ export const OVERVIEW_INGEST_ANIMATION = {
|
|
|
106
103
|
|
|
107
104
|
// Final Flash Settings
|
|
108
105
|
finalFlashDurationMs: 1000,
|
|
106
|
+
finalFlashDelayMs: 200,
|
|
109
107
|
finalFlashMaxOpacity: 0.35,
|
|
110
108
|
|
|
111
109
|
// Display Tween Settings
|