@cesarus777/opencode-cgp 0.4.1 → 0.4.2
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/CHANGELOG.md +8 -0
- package/README.md +7 -7
- package/dist/cli.js +415 -180
- package/dist/core.js +88 -59
- package/dist/index.js +710 -166
- package/dist/tui.js +3 -2
- package/dist/types/archive.d.ts +40 -0
- package/dist/types/core.d.ts +1 -1
- package/dist/types/dashboard/controller.d.ts +1 -0
- package/dist/types/dashboard/inventory.d.ts +1 -1
- package/dist/types/dashboard/terminal.d.ts +6 -0
- package/dist/types/dashboard/terminalview.d.ts +2 -0
- package/dist/types/dashboard/view.d.ts +1 -1
- package/dist/types/history.d.ts +39 -2
- package/dist/types/historyserver.d.ts +2 -1
- package/dist/types/modelanalytics.d.ts +11 -0
- package/dist/types/modelanalyticsserver.d.ts +2 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -473,15 +473,109 @@ async function segments(directory) {
|
|
|
473
473
|
return (await Promise.all(names.filter((n) => filename.test(n)).map(async (name) => ({
|
|
474
474
|
name,
|
|
475
475
|
time: Number(filename.exec(name)?.[1]),
|
|
476
|
-
|
|
476
|
+
...fileIdentity(await stat(join(directory, name)))
|
|
477
477
|
})))).sort((a, b) => a.time - b.time || a.name.localeCompare(b.name));
|
|
478
478
|
}
|
|
479
|
+
function fileIdentity(info) {
|
|
480
|
+
return {
|
|
481
|
+
bytes: info.size,
|
|
482
|
+
dev: info.dev,
|
|
483
|
+
ino: info.ino,
|
|
484
|
+
mtimeMs: info.mtimeMs,
|
|
485
|
+
ctimeMs: info.ctimeMs
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function scanScheduler(signal, options = {}) {
|
|
489
|
+
const now = options.now ?? (() => performance.now());
|
|
490
|
+
const pause = options.yield ?? (() => new Promise((done) => setTimeout(done, 0)));
|
|
491
|
+
let started = now();
|
|
492
|
+
return () => {
|
|
493
|
+
signal?.throwIfAborted();
|
|
494
|
+
if (now() - started >= 4)
|
|
495
|
+
return pause().then(() => {
|
|
496
|
+
signal?.throwIfAborted();
|
|
497
|
+
started = now();
|
|
498
|
+
});
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function matchesHistory(value, filter) {
|
|
502
|
+
return !(filter.sessionId !== undefined && value.sessionId !== filter.sessionId && value.sessionId !== "__history__" || value.type !== "gap" && filter.agent !== undefined && (value.performance?.requestAgent ?? value.agent) !== filter.agent || value.type !== "gap" && filter.model !== undefined && (value.performance?.requestModel ?? value.model) !== filter.model || filter.from !== undefined && value.time < filter.from || filter.to !== undefined && value.time > filter.to);
|
|
503
|
+
}
|
|
504
|
+
async function scanHistorySegment(handle, size, filter = {}, options = {}, start = 0) {
|
|
505
|
+
const records = [];
|
|
506
|
+
let malformedRecords = 0, position = start, boundary = start;
|
|
507
|
+
let pending = Buffer.alloc(0);
|
|
508
|
+
const checkpoint = scanScheduler(filter.signal, options);
|
|
509
|
+
function parse(line) {
|
|
510
|
+
if (!line.length)
|
|
511
|
+
return;
|
|
512
|
+
options.onParse?.();
|
|
513
|
+
let value;
|
|
514
|
+
try {
|
|
515
|
+
value = JSON.parse(line.toString("utf8"));
|
|
516
|
+
} catch {
|
|
517
|
+
malformedRecords++;
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (!isHistoryRecord(value)) {
|
|
521
|
+
malformedRecords++;
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (matchesHistory(value, filter))
|
|
525
|
+
records.push(value);
|
|
526
|
+
}
|
|
527
|
+
while (position < size) {
|
|
528
|
+
{
|
|
529
|
+
const waiting = checkpoint();
|
|
530
|
+
if (waiting)
|
|
531
|
+
await waiting;
|
|
532
|
+
}
|
|
533
|
+
const buffer = Buffer.alloc(Math.min(options.chunkBytes ?? 65536, size - position));
|
|
534
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
|
|
535
|
+
if (!bytesRead)
|
|
536
|
+
throw new Error("Archive changed during read");
|
|
537
|
+
options.onRead?.(bytesRead);
|
|
538
|
+
position += bytesRead;
|
|
539
|
+
const chunk = pending.length ? Buffer.concat([pending, buffer.subarray(0, bytesRead)]) : buffer.subarray(0, bytesRead);
|
|
540
|
+
let offset = 0;
|
|
541
|
+
for (;; ) {
|
|
542
|
+
const end = chunk.indexOf(10, offset);
|
|
543
|
+
if (end < 0)
|
|
544
|
+
break;
|
|
545
|
+
{
|
|
546
|
+
const waiting = checkpoint();
|
|
547
|
+
if (waiting)
|
|
548
|
+
await waiting;
|
|
549
|
+
}
|
|
550
|
+
parse(chunk.subarray(offset, end));
|
|
551
|
+
offset = end + 1;
|
|
552
|
+
}
|
|
553
|
+
pending = Buffer.from(chunk.subarray(offset));
|
|
554
|
+
boundary = position - pending.length;
|
|
555
|
+
}
|
|
556
|
+
const completeRecords = records.length, completeMalformed = malformedRecords;
|
|
557
|
+
{
|
|
558
|
+
const waiting = checkpoint();
|
|
559
|
+
if (waiting)
|
|
560
|
+
await waiting;
|
|
561
|
+
}
|
|
562
|
+
parse(pending);
|
|
563
|
+
filter.signal?.throwIfAborted();
|
|
564
|
+
return {
|
|
565
|
+
records,
|
|
566
|
+
malformedRecords,
|
|
567
|
+
boundary,
|
|
568
|
+
completeRecords,
|
|
569
|
+
completeMalformed
|
|
570
|
+
};
|
|
571
|
+
}
|
|
479
572
|
|
|
480
573
|
class PerformanceHistory {
|
|
481
574
|
path;
|
|
482
575
|
config;
|
|
483
576
|
now;
|
|
484
577
|
report;
|
|
578
|
+
changed;
|
|
485
579
|
pending = [];
|
|
486
580
|
working;
|
|
487
581
|
active;
|
|
@@ -491,11 +585,13 @@ class PerformanceHistory {
|
|
|
491
585
|
dropped = 0;
|
|
492
586
|
droppedTotal = 0;
|
|
493
587
|
constructor(path, config, now = Date.now, report = () => {
|
|
588
|
+
}, changed = () => {
|
|
494
589
|
}) {
|
|
495
590
|
this.path = path;
|
|
496
591
|
this.config = config;
|
|
497
592
|
this.now = now;
|
|
498
593
|
this.report = report;
|
|
594
|
+
this.changed = changed;
|
|
499
595
|
}
|
|
500
596
|
emit(record2) {
|
|
501
597
|
if (!this.config.enabled || this.disabled)
|
|
@@ -544,6 +640,7 @@ class PerformanceHistory {
|
|
|
544
640
|
if (file.time >= this.now() - this.config.retentionDays * 86400000 && total + incoming <= this.config.maxBytes)
|
|
545
641
|
break;
|
|
546
642
|
await unlink(join(this.path, file.name));
|
|
643
|
+
this.changed(file.name);
|
|
547
644
|
if (file.name === this.active) {
|
|
548
645
|
this.active = undefined;
|
|
549
646
|
this.bytes = 0;
|
|
@@ -594,6 +691,7 @@ class PerformanceHistory {
|
|
|
594
691
|
const file = await open(join(this.path, this.active), "a", 384);
|
|
595
692
|
try {
|
|
596
693
|
await file.writeFile(line);
|
|
694
|
+
this.changed(this.active);
|
|
597
695
|
} finally {
|
|
598
696
|
await file.close();
|
|
599
697
|
}
|
|
@@ -648,7 +746,8 @@ function scalarPerformance(p) {
|
|
|
648
746
|
}))
|
|
649
747
|
};
|
|
650
748
|
}
|
|
651
|
-
async function readPerformanceHistory(directory, filter = {}) {
|
|
749
|
+
async function readPerformanceHistory(directory, filter = {}, options = {}) {
|
|
750
|
+
filter.signal?.throwIfAborted();
|
|
652
751
|
const records = [];
|
|
653
752
|
const files = await segments(directory);
|
|
654
753
|
filter.signal?.throwIfAborted();
|
|
@@ -666,36 +765,16 @@ async function readPerformanceHistory(directory, filter = {}) {
|
|
|
666
765
|
for (const file of selected) {
|
|
667
766
|
filter.signal?.throwIfAborted();
|
|
668
767
|
const handle = await open(join(directory, file.name), "r");
|
|
669
|
-
let source;
|
|
670
768
|
try {
|
|
671
|
-
const
|
|
672
|
-
const
|
|
673
|
-
|
|
769
|
+
const result = await scanHistorySegment(handle, file.bytes, filter, options);
|
|
770
|
+
for (const record2 of result.records)
|
|
771
|
+
records.push(record2);
|
|
772
|
+
malformedRecords += result.malformedRecords;
|
|
674
773
|
} finally {
|
|
675
774
|
await handle.close();
|
|
676
775
|
}
|
|
677
|
-
let scannedLines = 0;
|
|
678
|
-
for (const line of source.split("\n")) {
|
|
679
|
-
if (filter.signal && ++scannedLines % 256 === 0) {
|
|
680
|
-
await new Promise((done) => setTimeout(done, 0));
|
|
681
|
-
filter.signal.throwIfAborted();
|
|
682
|
-
}
|
|
683
|
-
if (!line)
|
|
684
|
-
continue;
|
|
685
|
-
try {
|
|
686
|
-
const value = JSON.parse(line);
|
|
687
|
-
if (!isHistoryRecord(value)) {
|
|
688
|
-
malformedRecords++;
|
|
689
|
-
continue;
|
|
690
|
-
}
|
|
691
|
-
if (filter.sessionId !== undefined && value.sessionId !== filter.sessionId && value.sessionId !== "__history__" || value.type !== "gap" && filter.agent !== undefined && (value.performance?.requestAgent ?? value.agent) !== filter.agent || value.type !== "gap" && filter.model !== undefined && (value.performance?.requestModel ?? value.model) !== filter.model || filter.from !== undefined && value.time < filter.from || filter.to !== undefined && value.time > filter.to)
|
|
692
|
-
continue;
|
|
693
|
-
records.push(value);
|
|
694
|
-
} catch {
|
|
695
|
-
malformedRecords++;
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
776
|
}
|
|
777
|
+
filter.signal?.throwIfAborted();
|
|
699
778
|
const metadata = {
|
|
700
779
|
schema: 1,
|
|
701
780
|
...files[0] ? { retainedFrom: files[0].time } : {},
|
|
@@ -713,7 +792,13 @@ function isHistoryRecord(value) {
|
|
|
713
792
|
if (!isRecord(value))
|
|
714
793
|
return false;
|
|
715
794
|
const r = value;
|
|
716
|
-
|
|
795
|
+
return Object.keys(r).every((k) => historyFields.has(k)) && r.schema === 1 && typeof r.recordId === "string" && typeof r.time === "number" && Number.isSafeInteger(r.time) && r.time >= 0 && typeof r.sessionId === "string" && typeof r.type === "string" && ["sample", "request", "lifecycle", "gap", "terminal"].includes(r.type) && ["parentId", "agent", "model", "event"].every((k) => r[k] === undefined || typeof r[k] === "string") && (r.performance === undefined || isPerformanceSnapshot(r.performance)) && (r.droppedRecords === undefined || typeof r.droppedRecords === "number" && Number.isSafeInteger(r.droppedRecords) && r.droppedRecords >= 0);
|
|
796
|
+
}
|
|
797
|
+
var filename, historyFields;
|
|
798
|
+
var init_history = __esm(() => {
|
|
799
|
+
init_rpc();
|
|
800
|
+
filename = /^([0-9]+)-[a-f0-9-]+\.jsonl$/;
|
|
801
|
+
historyFields = new Set([
|
|
717
802
|
"schema",
|
|
718
803
|
"recordId",
|
|
719
804
|
"time",
|
|
@@ -726,12 +811,6 @@ function isHistoryRecord(value) {
|
|
|
726
811
|
"performance",
|
|
727
812
|
"droppedRecords"
|
|
728
813
|
]);
|
|
729
|
-
return Object.keys(r).every((k) => allowed.has(k)) && r.schema === 1 && typeof r.recordId === "string" && typeof r.time === "number" && Number.isSafeInteger(r.time) && r.time >= 0 && typeof r.sessionId === "string" && typeof r.type === "string" && ["sample", "request", "lifecycle", "gap", "terminal"].includes(r.type) && ["parentId", "agent", "model", "event"].every((k) => r[k] === undefined || typeof r[k] === "string") && (r.performance === undefined || isPerformanceSnapshot(r.performance)) && (r.droppedRecords === undefined || typeof r.droppedRecords === "number" && Number.isSafeInteger(r.droppedRecords) && r.droppedRecords >= 0);
|
|
730
|
-
}
|
|
731
|
-
var filename;
|
|
732
|
-
var init_history = __esm(() => {
|
|
733
|
-
init_rpc();
|
|
734
|
-
filename = /^([0-9]+)-[a-f0-9-]+\.jsonl$/;
|
|
735
814
|
});
|
|
736
815
|
|
|
737
816
|
// src/modelanalytics.ts
|
|
@@ -778,6 +857,11 @@ function reconcile(a, b) {
|
|
|
778
857
|
};
|
|
779
858
|
}
|
|
780
859
|
function analyzeModelRequests(records, options) {
|
|
860
|
+
const accumulator = createModelAccumulator(options);
|
|
861
|
+
accumulator.append(records);
|
|
862
|
+
return accumulator.result();
|
|
863
|
+
}
|
|
864
|
+
function createModelAccumulator(options) {
|
|
781
865
|
const maxRequests = options.maxRequests ?? 1e5;
|
|
782
866
|
if (!Number.isSafeInteger(maxRequests) || maxRequests < 1 || maxRequests > 1e6)
|
|
783
867
|
throw new Error("maxRequests must be an integer from 1 to 1000000");
|
|
@@ -788,7 +872,7 @@ function analyzeModelRequests(records, options) {
|
|
|
788
872
|
throw new Error("Invalid analytics timestamp");
|
|
789
873
|
if (options.from !== undefined && options.to !== undefined && options.from > options.to)
|
|
790
874
|
throw new Error("--from must precede --to");
|
|
791
|
-
const
|
|
875
|
+
const counters = {
|
|
792
876
|
source: "retained-request-records",
|
|
793
877
|
scannedRecords: 0,
|
|
794
878
|
unscannedRecords: 0,
|
|
@@ -802,74 +886,98 @@ function analyzeModelRequests(records, options) {
|
|
|
802
886
|
partial: false
|
|
803
887
|
};
|
|
804
888
|
const requests = new Map;
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
}
|
|
811
|
-
if (record2.type !== "request")
|
|
812
|
-
continue;
|
|
813
|
-
const fact = requestFact(record2);
|
|
814
|
-
if (!fact) {
|
|
815
|
-
coverage.missingIdentityRecords++;
|
|
816
|
-
continue;
|
|
817
|
-
}
|
|
818
|
-
fact.location = options.location;
|
|
819
|
-
const key = JSON.stringify([
|
|
820
|
-
fact.sessionId,
|
|
821
|
-
fact.requestId,
|
|
822
|
-
fact.startedAt
|
|
823
|
-
]);
|
|
824
|
-
const previous = requests.get(key);
|
|
825
|
-
if (previous) {
|
|
826
|
-
coverage.duplicateRecords++;
|
|
827
|
-
const merged = reconcile(previous.fact, fact);
|
|
828
|
-
if (!merged)
|
|
829
|
-
previous.conflict = true;
|
|
830
|
-
else
|
|
831
|
-
previous.fact = merged;
|
|
832
|
-
} else {
|
|
833
|
-
if (requests.size === maxRequests) {
|
|
834
|
-
coverage.scannedRecords--;
|
|
835
|
-
coverage.unscannedRecords = records.length - coverage.scannedRecords;
|
|
889
|
+
let totalRecords = 0, stopped = false;
|
|
890
|
+
function append(records) {
|
|
891
|
+
totalRecords += records.length;
|
|
892
|
+
for (const record2 of records) {
|
|
893
|
+
if (stopped)
|
|
836
894
|
break;
|
|
895
|
+
counters.scannedRecords++;
|
|
896
|
+
if (record2.type === "gap") {
|
|
897
|
+
counters.gapRecords++;
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
if (record2.type !== "request")
|
|
901
|
+
continue;
|
|
902
|
+
const fact = requestFact(record2);
|
|
903
|
+
if (!fact) {
|
|
904
|
+
counters.missingIdentityRecords++;
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
fact.location = options.location;
|
|
908
|
+
const key = JSON.stringify([
|
|
909
|
+
fact.sessionId,
|
|
910
|
+
fact.requestId,
|
|
911
|
+
fact.startedAt
|
|
912
|
+
]);
|
|
913
|
+
const previous = requests.get(key);
|
|
914
|
+
if (previous) {
|
|
915
|
+
counters.duplicateRecords++;
|
|
916
|
+
const merged = reconcile(previous.fact, fact);
|
|
917
|
+
if (!merged)
|
|
918
|
+
previous.conflict = true;
|
|
919
|
+
else
|
|
920
|
+
previous.fact = merged;
|
|
921
|
+
} else {
|
|
922
|
+
if (requests.size === maxRequests) {
|
|
923
|
+
counters.scannedRecords--;
|
|
924
|
+
stopped = true;
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
requests.set(key, { fact, conflict: false });
|
|
837
928
|
}
|
|
838
|
-
requests.set(key, { fact, conflict: false });
|
|
839
929
|
}
|
|
840
930
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
931
|
+
function* steps() {
|
|
932
|
+
const coverage = {
|
|
933
|
+
...counters,
|
|
934
|
+
unscannedRecords: totalRecords - counters.scannedRecords
|
|
935
|
+
};
|
|
936
|
+
const accepted = [];
|
|
937
|
+
for (const { fact: r, conflict } of requests.values()) {
|
|
938
|
+
yield;
|
|
939
|
+
if (conflict) {
|
|
940
|
+
coverage.conflictingRequests++;
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
if (r.completedAt === undefined) {
|
|
944
|
+
coverage.incompleteRequests++;
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
const duration = r.completedAt - r.startedAt;
|
|
948
|
+
if (!nonnegative2(r.completedAt) || !Number.isFinite(duration) || duration <= 0) {
|
|
949
|
+
coverage.invalidTimingRequests++;
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (options.from !== undefined && r.completedAt < options.from || options.to !== undefined && r.completedAt >= options.to || options.model !== undefined && r.model !== options.model || options.agent !== undefined && r.agent !== options.agent)
|
|
953
|
+
continue;
|
|
954
|
+
accepted.push({ ...r, completedAt: r.completedAt });
|
|
855
955
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
956
|
+
const models = yield* summarizeModelRequestSteps(accepted);
|
|
957
|
+
coverage.partial = coverage.unscannedRecords > 0 || coverage.conflictingRequests > 0 || coverage.missingIdentityRecords > 0 || coverage.incompleteRequests > 0 || coverage.invalidTimingRequests > 0 || coverage.gapRecords > 0 || models.some((s) => s.partialRequests > 0 || s.legacyAttributionRequests > 0 || s.excludedTpsRequests > 0 || s.excludedTtftRequests > 0 || !s.model);
|
|
958
|
+
return {
|
|
959
|
+
schemaVersion: 1,
|
|
960
|
+
location: options.location,
|
|
961
|
+
models,
|
|
962
|
+
coverage,
|
|
963
|
+
requests: accepted
|
|
964
|
+
};
|
|
859
965
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
requests: accepted
|
|
868
|
-
};
|
|
966
|
+
return { append, steps, result: () => finishSteps(steps()) };
|
|
967
|
+
}
|
|
968
|
+
function finishSteps(steps) {
|
|
969
|
+
let step = steps.next();
|
|
970
|
+
while (!step.done)
|
|
971
|
+
step = steps.next();
|
|
972
|
+
return step.value;
|
|
869
973
|
}
|
|
870
974
|
function summarizeModelRequests(requests) {
|
|
975
|
+
return finishSteps(summarizeModelRequestSteps(requests));
|
|
976
|
+
}
|
|
977
|
+
function* summarizeModelRequestSteps(requests) {
|
|
871
978
|
const groups = new Map;
|
|
872
979
|
for (const r of requests) {
|
|
980
|
+
yield;
|
|
873
981
|
const duration = r.completedAt - r.startedAt;
|
|
874
982
|
let group = groups.get(r.model);
|
|
875
983
|
if (!group) {
|
|
@@ -936,6 +1044,324 @@ var nonnegative2 = (n) => n !== undefined && Number.isFinite(n) && n >= 0;
|
|
|
936
1044
|
// src/index.ts
|
|
937
1045
|
import { Plugin } from "@opencode/plugin";
|
|
938
1046
|
|
|
1047
|
+
// src/archive.ts
|
|
1048
|
+
init_history();
|
|
1049
|
+
import { open as open2 } from "fs/promises";
|
|
1050
|
+
import { join as join2 } from "path";
|
|
1051
|
+
var signature = (file) => JSON.stringify(file);
|
|
1052
|
+
var sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino;
|
|
1053
|
+
function freezeRecord(record2) {
|
|
1054
|
+
if (record2.performance)
|
|
1055
|
+
Object.freeze(record2.performance);
|
|
1056
|
+
return Object.freeze(record2);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
class PerformanceArchive {
|
|
1060
|
+
path;
|
|
1061
|
+
options;
|
|
1062
|
+
entries = new Map;
|
|
1063
|
+
generation = 0;
|
|
1064
|
+
stopped = false;
|
|
1065
|
+
dirty = new Map;
|
|
1066
|
+
forcedGeneration = 0;
|
|
1067
|
+
job;
|
|
1068
|
+
stats = {
|
|
1069
|
+
readBytes: 0,
|
|
1070
|
+
parsedRecords: 0,
|
|
1071
|
+
cacheBytes: 0,
|
|
1072
|
+
cacheRecords: 0
|
|
1073
|
+
};
|
|
1074
|
+
constructor(path, options = {}) {
|
|
1075
|
+
this.path = path;
|
|
1076
|
+
this.options = options;
|
|
1077
|
+
}
|
|
1078
|
+
invalidate(name) {
|
|
1079
|
+
this.generation++;
|
|
1080
|
+
if (name) {
|
|
1081
|
+
if (this.dirty.size >= 256 && !this.dirty.has(name)) {
|
|
1082
|
+
this.entries.clear();
|
|
1083
|
+
this.dirty.clear();
|
|
1084
|
+
this.forcedGeneration = this.generation;
|
|
1085
|
+
}
|
|
1086
|
+
this.dirty.set(name, this.generation);
|
|
1087
|
+
} else {
|
|
1088
|
+
this.entries.clear();
|
|
1089
|
+
this.dirty.clear();
|
|
1090
|
+
this.forcedGeneration = this.generation;
|
|
1091
|
+
}
|
|
1092
|
+
this.account();
|
|
1093
|
+
}
|
|
1094
|
+
dispose() {
|
|
1095
|
+
this.stopped = true;
|
|
1096
|
+
this.job?.controller.abort();
|
|
1097
|
+
this.entries.clear();
|
|
1098
|
+
this.dirty.clear();
|
|
1099
|
+
this.account();
|
|
1100
|
+
}
|
|
1101
|
+
account() {
|
|
1102
|
+
this.stats.cacheBytes = [...this.entries.values()].reduce((n, e) => n + e.file.bytes, 0);
|
|
1103
|
+
this.stats.cacheRecords = [...this.entries.values()].reduce((n, e) => n + e.records.length, 0);
|
|
1104
|
+
}
|
|
1105
|
+
retain(name, entry) {
|
|
1106
|
+
this.entries.delete(name);
|
|
1107
|
+
this.entries.set(name, entry);
|
|
1108
|
+
this.account();
|
|
1109
|
+
while (this.entries.size > (this.options.maxCacheSegments ?? 256) || this.stats.cacheBytes > (this.options.maxCacheBytes ?? 64 * 1024 * 1024) || this.stats.cacheRecords > (this.options.maxCacheRecords ?? 200000)) {
|
|
1110
|
+
const oldest = this.entries.keys().next().value;
|
|
1111
|
+
if (oldest === undefined)
|
|
1112
|
+
break;
|
|
1113
|
+
this.entries.delete(oldest);
|
|
1114
|
+
this.dirty.delete(oldest);
|
|
1115
|
+
this.account();
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
validCapture(expected, actual, generation) {
|
|
1119
|
+
return signature(expected) === signature(actual) || sameFile(expected, actual) && actual.bytes > expected.bytes && (this.dirty.get(expected.name) ?? -1) > generation;
|
|
1120
|
+
}
|
|
1121
|
+
async scan(maxBytes, signal) {
|
|
1122
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
1123
|
+
try {
|
|
1124
|
+
signal.throwIfAborted();
|
|
1125
|
+
const generation = this.generation;
|
|
1126
|
+
const files = await segments(this.path);
|
|
1127
|
+
signal.throwIfAborted();
|
|
1128
|
+
const names = new Set(files.map((f) => f.name));
|
|
1129
|
+
for (const name of this.entries.keys())
|
|
1130
|
+
if (!names.has(name)) {
|
|
1131
|
+
this.entries.delete(name);
|
|
1132
|
+
this.dirty.delete(name);
|
|
1133
|
+
}
|
|
1134
|
+
this.account();
|
|
1135
|
+
const selected = [];
|
|
1136
|
+
let bytes = 0, skippedBytes = 0, malformedRecords = 0;
|
|
1137
|
+
for (const file of files.slice().reverse()) {
|
|
1138
|
+
if (bytes + file.bytes > maxBytes) {
|
|
1139
|
+
skippedBytes += file.bytes;
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
selected.unshift(file);
|
|
1143
|
+
bytes += file.bytes;
|
|
1144
|
+
}
|
|
1145
|
+
const parts = [];
|
|
1146
|
+
let unstable = false;
|
|
1147
|
+
for (const file of selected) {
|
|
1148
|
+
signal.throwIfAborted();
|
|
1149
|
+
const handle = await open2(join2(this.path, file.name), "r");
|
|
1150
|
+
try {
|
|
1151
|
+
const captured = {
|
|
1152
|
+
name: file.name,
|
|
1153
|
+
time: file.time,
|
|
1154
|
+
...fileIdentity(await handle.stat())
|
|
1155
|
+
};
|
|
1156
|
+
if (!this.validCapture(file, captured, generation)) {
|
|
1157
|
+
unstable = true;
|
|
1158
|
+
break;
|
|
1159
|
+
}
|
|
1160
|
+
const previous = this.entries.get(file.name);
|
|
1161
|
+
let entry;
|
|
1162
|
+
if (previous && previous.generation >= this.forcedGeneration && !this.dirty.has(file.name) && signature(previous.file) === signature(file)) {
|
|
1163
|
+
entry = previous;
|
|
1164
|
+
} else {
|
|
1165
|
+
const append = previous && previous.generation >= this.forcedGeneration && sameFile(previous.file, file) && file.bytes > previous.file.bytes && this.dirty.has(file.name);
|
|
1166
|
+
const parsed = await scanHistorySegment(handle, file.bytes, { signal }, {
|
|
1167
|
+
...this.options,
|
|
1168
|
+
onRead: (n) => {
|
|
1169
|
+
this.stats.readBytes += n;
|
|
1170
|
+
this.options.onRead?.(n);
|
|
1171
|
+
},
|
|
1172
|
+
onParse: () => {
|
|
1173
|
+
this.stats.parsedRecords++;
|
|
1174
|
+
this.options.onParse?.();
|
|
1175
|
+
}
|
|
1176
|
+
}, append ? previous.boundary : 0);
|
|
1177
|
+
entry = {
|
|
1178
|
+
...parsed,
|
|
1179
|
+
file,
|
|
1180
|
+
generation,
|
|
1181
|
+
sessions: new Map,
|
|
1182
|
+
minTime: Infinity,
|
|
1183
|
+
maxTime: -Infinity
|
|
1184
|
+
};
|
|
1185
|
+
if (append) {
|
|
1186
|
+
entry.records = [
|
|
1187
|
+
...previous.records.slice(0, previous.completeRecords),
|
|
1188
|
+
...parsed.records
|
|
1189
|
+
];
|
|
1190
|
+
entry.malformedRecords += previous.completeMalformed;
|
|
1191
|
+
entry.completeRecords += previous.completeRecords;
|
|
1192
|
+
entry.completeMalformed += previous.completeMalformed;
|
|
1193
|
+
}
|
|
1194
|
+
const checkpoint = scanScheduler(signal, this.options);
|
|
1195
|
+
for (const [index, record2] of entry.records.entries()) {
|
|
1196
|
+
if (index % 64 === 0) {
|
|
1197
|
+
const waiting = checkpoint();
|
|
1198
|
+
if (waiting)
|
|
1199
|
+
await waiting;
|
|
1200
|
+
}
|
|
1201
|
+
freezeRecord(record2);
|
|
1202
|
+
entry.minTime = Math.min(entry.minTime, record2.time);
|
|
1203
|
+
entry.maxTime = Math.max(entry.maxTime, record2.time);
|
|
1204
|
+
const list = entry.sessions.get(record2.sessionId) ?? [];
|
|
1205
|
+
list.push(index);
|
|
1206
|
+
entry.sessions.set(record2.sessionId, list);
|
|
1207
|
+
}
|
|
1208
|
+
Object.freeze(entry.records);
|
|
1209
|
+
}
|
|
1210
|
+
const after = {
|
|
1211
|
+
name: file.name,
|
|
1212
|
+
time: file.time,
|
|
1213
|
+
...fileIdentity(await handle.stat())
|
|
1214
|
+
};
|
|
1215
|
+
if (!this.validCapture(file, after, generation)) {
|
|
1216
|
+
unstable = true;
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1219
|
+
signal.throwIfAborted();
|
|
1220
|
+
this.retain(file.name, entry);
|
|
1221
|
+
parts.push(entry);
|
|
1222
|
+
malformedRecords += entry.malformedRecords;
|
|
1223
|
+
} finally {
|
|
1224
|
+
await handle.close();
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
signal.throwIfAborted();
|
|
1228
|
+
const current = await segments(this.path);
|
|
1229
|
+
const currentByName = new Map(current.map((file) => [file.name, file]));
|
|
1230
|
+
const originalNames = new Set(files.map((file) => file.name));
|
|
1231
|
+
if (unstable || this.forcedGeneration > generation || files.some((file) => {
|
|
1232
|
+
const actual = currentByName.get(file.name);
|
|
1233
|
+
return !actual || !this.validCapture(file, actual, generation);
|
|
1234
|
+
}) || current.some((file) => !originalNames.has(file.name) && (this.dirty.get(file.name) ?? -1) <= generation))
|
|
1235
|
+
continue;
|
|
1236
|
+
for (const [name, changedAt] of this.dirty)
|
|
1237
|
+
if (changedAt <= generation)
|
|
1238
|
+
this.dirty.delete(name);
|
|
1239
|
+
const metadata = {
|
|
1240
|
+
schema: 1,
|
|
1241
|
+
...files[0] ? { retainedFrom: files[0].time } : {},
|
|
1242
|
+
retainedBytes: files.reduce((n, f) => n + f.bytes, 0),
|
|
1243
|
+
skippedBytes,
|
|
1244
|
+
malformedRecords,
|
|
1245
|
+
completeness: "bounded-retention"
|
|
1246
|
+
};
|
|
1247
|
+
Object.freeze(metadata);
|
|
1248
|
+
let flattened;
|
|
1249
|
+
return Object.freeze({
|
|
1250
|
+
revision: JSON.stringify([generation, maxBytes, files]),
|
|
1251
|
+
metadata,
|
|
1252
|
+
parts,
|
|
1253
|
+
get records() {
|
|
1254
|
+
if (!flattened) {
|
|
1255
|
+
flattened = [];
|
|
1256
|
+
for (const part of parts)
|
|
1257
|
+
for (const record2 of part.records)
|
|
1258
|
+
flattened.push(record2);
|
|
1259
|
+
Object.freeze(flattened);
|
|
1260
|
+
}
|
|
1261
|
+
return flattened;
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
signal.throwIfAborted();
|
|
1266
|
+
if (!(error instanceof Error && ("code" in error) && error.code === "ENOENT"))
|
|
1267
|
+
throw error;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
throw new Error("Archive changed repeatedly during read; retry later");
|
|
1271
|
+
}
|
|
1272
|
+
async read(filter = {}) {
|
|
1273
|
+
filter.signal?.throwIfAborted();
|
|
1274
|
+
if (this.stopped)
|
|
1275
|
+
throw new Error("Archive stopped");
|
|
1276
|
+
const key = filter.maxBytes ?? 64 * 1024 * 1024;
|
|
1277
|
+
if (this.job && this.job.key !== key) {
|
|
1278
|
+
await this.wait(this.job, filter.signal);
|
|
1279
|
+
return this.read(filter);
|
|
1280
|
+
}
|
|
1281
|
+
if (this.job?.controller.signal.aborted) {
|
|
1282
|
+
await this.job.promise.catch(() => {
|
|
1283
|
+
});
|
|
1284
|
+
return this.read(filter);
|
|
1285
|
+
}
|
|
1286
|
+
if (!this.job) {
|
|
1287
|
+
const controller = new AbortController;
|
|
1288
|
+
const job = {
|
|
1289
|
+
key,
|
|
1290
|
+
controller,
|
|
1291
|
+
waiters: 0,
|
|
1292
|
+
promise: Promise.resolve().then(() => this.scan(key, controller.signal))
|
|
1293
|
+
};
|
|
1294
|
+
job.promise = job.promise.finally(() => {
|
|
1295
|
+
if (this.job === job)
|
|
1296
|
+
this.job = undefined;
|
|
1297
|
+
});
|
|
1298
|
+
this.job = job;
|
|
1299
|
+
}
|
|
1300
|
+
const snapshot = await this.wait(this.job, filter.signal);
|
|
1301
|
+
const parts = snapshot.parts;
|
|
1302
|
+
const source = { revision: snapshot.revision, metadata: snapshot.metadata };
|
|
1303
|
+
if (filter.sessionId === undefined && filter.agent === undefined && filter.model === undefined && filter.from === undefined && filter.to === undefined)
|
|
1304
|
+
return Object.freeze({
|
|
1305
|
+
...source,
|
|
1306
|
+
get records() {
|
|
1307
|
+
return snapshot.records;
|
|
1308
|
+
},
|
|
1309
|
+
*iterateRecords() {
|
|
1310
|
+
for (const part of parts)
|
|
1311
|
+
for (const record2 of part.records)
|
|
1312
|
+
yield record2;
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
const records = [];
|
|
1316
|
+
const checkpoint = scanScheduler(filter.signal, this.options);
|
|
1317
|
+
for (const part of parts) {
|
|
1318
|
+
if (filter.from !== undefined && part.maxTime < filter.from || filter.to !== undefined && part.minTime > filter.to)
|
|
1319
|
+
continue;
|
|
1320
|
+
const offsets = filter.sessionId === undefined ? undefined : [
|
|
1321
|
+
...part.sessions.get(filter.sessionId) ?? [],
|
|
1322
|
+
...filter.sessionId === "__history__" ? [] : part.sessions.get("__history__") ?? []
|
|
1323
|
+
].sort((a, b) => a - b);
|
|
1324
|
+
const candidates = offsets ? offsets.map((i) => part.records[i]).filter((r) => r !== undefined) : part.records;
|
|
1325
|
+
for (const record2 of candidates) {
|
|
1326
|
+
{
|
|
1327
|
+
const waiting = checkpoint();
|
|
1328
|
+
if (waiting)
|
|
1329
|
+
await waiting;
|
|
1330
|
+
}
|
|
1331
|
+
if (matchesHistory(record2, filter))
|
|
1332
|
+
records.push(record2);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
filter.signal?.throwIfAborted();
|
|
1336
|
+
Object.freeze(records);
|
|
1337
|
+
return Object.freeze({ ...source, records });
|
|
1338
|
+
}
|
|
1339
|
+
async wait(job, signal) {
|
|
1340
|
+
job.waiters++;
|
|
1341
|
+
let onAbort = () => {
|
|
1342
|
+
};
|
|
1343
|
+
try {
|
|
1344
|
+
return await Promise.race([
|
|
1345
|
+
job.promise,
|
|
1346
|
+
new Promise((_resolve, reject) => {
|
|
1347
|
+
onAbort = () => reject(signal?.reason ?? job.controller.signal.reason ?? new Error("Archive read cancelled"));
|
|
1348
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1349
|
+
job.controller.signal.addEventListener("abort", onAbort, {
|
|
1350
|
+
once: true
|
|
1351
|
+
});
|
|
1352
|
+
if (signal?.aborted || job.controller.signal.aborted)
|
|
1353
|
+
onAbort();
|
|
1354
|
+
})
|
|
1355
|
+
]);
|
|
1356
|
+
} finally {
|
|
1357
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1358
|
+
job.controller.signal.removeEventListener("abort", onAbort);
|
|
1359
|
+
if (--job.waiters === 0)
|
|
1360
|
+
job.controller.abort();
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
939
1365
|
// src/performance.ts
|
|
940
1366
|
var valid2 = (n) => Number.isFinite(n) && n >= 0;
|
|
941
1367
|
var rate = (tokens, ms) => valid2(tokens) && ms > 0 ? tokens * 1000 / ms : undefined;
|
|
@@ -1322,10 +1748,11 @@ function buildChartData(records, options = {}) {
|
|
|
1322
1748
|
// src/historyserver.ts
|
|
1323
1749
|
init_history();
|
|
1324
1750
|
init_rpc();
|
|
1325
|
-
async function serveHistory(rpc, path, enabled, now = Date.now, authorize = async () => true) {
|
|
1751
|
+
async function serveHistory(rpc, path, enabled, now = Date.now, authorize = async () => true, reader = readPerformanceHistory) {
|
|
1326
1752
|
if (!rpc)
|
|
1327
1753
|
return () => {
|
|
1328
1754
|
};
|
|
1755
|
+
const shutdown = new AbortController;
|
|
1329
1756
|
let reading = false, stopped = false;
|
|
1330
1757
|
const registration = await rpc.register(historyRpc, {
|
|
1331
1758
|
read: async (input, context) => {
|
|
@@ -1333,12 +1760,15 @@ async function serveHistory(rpc, path, enabled, now = Date.now, authorize = asyn
|
|
|
1333
1760
|
throw new Error("Invalid history request (maximum window: seven days)");
|
|
1334
1761
|
if (reading || stopped)
|
|
1335
1762
|
throw new Error("History service busy or stopped; retry later");
|
|
1763
|
+
const signal = AbortSignal.any([context.signal, shutdown.signal]);
|
|
1764
|
+
signal.throwIfAborted();
|
|
1336
1765
|
reading = true;
|
|
1337
1766
|
try {
|
|
1338
|
-
const allowed = !enabled || await authorize(input.sessionId,
|
|
1767
|
+
const allowed = !enabled || await authorize(input.sessionId, signal);
|
|
1339
1768
|
if (!allowed)
|
|
1340
1769
|
throw new Error("Session is outside this location");
|
|
1341
|
-
const result = enabled ? await
|
|
1770
|
+
const result = enabled ? await reader(path, {
|
|
1771
|
+
signal,
|
|
1342
1772
|
sessionId: input.sessionId,
|
|
1343
1773
|
from: input.from,
|
|
1344
1774
|
to: input.to
|
|
@@ -1381,6 +1811,7 @@ async function serveHistory(rpc, path, enabled, now = Date.now, authorize = asyn
|
|
|
1381
1811
|
});
|
|
1382
1812
|
return async () => {
|
|
1383
1813
|
stopped = true;
|
|
1814
|
+
shutdown.abort();
|
|
1384
1815
|
await registration.dispose();
|
|
1385
1816
|
};
|
|
1386
1817
|
}
|
|
@@ -1422,15 +1853,62 @@ var metadata = {
|
|
|
1422
1853
|
malformedRecords: 0,
|
|
1423
1854
|
completeness: "bounded-retention"
|
|
1424
1855
|
};
|
|
1856
|
+
async function responsive(steps, signal) {
|
|
1857
|
+
const checkpoint = scanScheduler(signal);
|
|
1858
|
+
let step = steps.next(), iterations = 0;
|
|
1859
|
+
while (!step.done) {
|
|
1860
|
+
if (++iterations % 64 === 0) {
|
|
1861
|
+
const waiting = checkpoint();
|
|
1862
|
+
if (waiting)
|
|
1863
|
+
await waiting;
|
|
1864
|
+
}
|
|
1865
|
+
step = steps.next();
|
|
1866
|
+
}
|
|
1867
|
+
signal.throwIfAborted();
|
|
1868
|
+
return step.value;
|
|
1869
|
+
}
|
|
1870
|
+
function* collectRecords(values) {
|
|
1871
|
+
const records = [];
|
|
1872
|
+
for (const record2 of values) {
|
|
1873
|
+
yield;
|
|
1874
|
+
records.push(record2);
|
|
1875
|
+
}
|
|
1876
|
+
return records;
|
|
1877
|
+
}
|
|
1878
|
+
function* sortRequests(requests) {
|
|
1879
|
+
const compare = (a, b) => b.completedAt - a.completedAt || a.sessionId.localeCompare(b.sessionId) || a.requestId.localeCompare(b.requestId) || a.startedAt - b.startedAt;
|
|
1880
|
+
let source = requests, target = requests.slice();
|
|
1881
|
+
for (let size = 1;size < requests.length; size *= 2) {
|
|
1882
|
+
for (let start = 0;start < requests.length; start += 2 * size) {
|
|
1883
|
+
const middle = Math.min(start + size, requests.length), end = Math.min(start + 2 * size, requests.length);
|
|
1884
|
+
let left = start, right = middle;
|
|
1885
|
+
for (let output = start;output < end; output++) {
|
|
1886
|
+
yield;
|
|
1887
|
+
const a = source[left], b = source[right];
|
|
1888
|
+
if (left < middle && a && (right >= end || !b || compare(a, b) <= 0)) {
|
|
1889
|
+
target[output] = a;
|
|
1890
|
+
left++;
|
|
1891
|
+
} else if (b) {
|
|
1892
|
+
target[output] = b;
|
|
1893
|
+
right++;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
[source, target] = [target, source];
|
|
1898
|
+
}
|
|
1899
|
+
return source;
|
|
1900
|
+
}
|
|
1425
1901
|
function createAnalyticsService(path, location, enabled, options = {}) {
|
|
1426
1902
|
const now = options.now ?? Date.now;
|
|
1427
1903
|
const reader = options.read ?? readPerformanceHistory;
|
|
1428
1904
|
const snapshots = new Map;
|
|
1429
|
-
|
|
1905
|
+
const shutdown = new AbortController;
|
|
1906
|
+
let stopped = false, projecting = 0;
|
|
1430
1907
|
let archive;
|
|
1431
1908
|
let scan;
|
|
1909
|
+
let incremental;
|
|
1432
1910
|
async function load(signal) {
|
|
1433
|
-
if (archive && now() - archive.at < 30000)
|
|
1911
|
+
if (archive && !options.revisioned && now() - archive.at < 30000)
|
|
1434
1912
|
return archive;
|
|
1435
1913
|
if (!scan || scan.controller.signal.aborted) {
|
|
1436
1914
|
const controller = new AbortController;
|
|
@@ -1442,12 +1920,35 @@ function createAnalyticsService(path, location, enabled, options = {}) {
|
|
|
1442
1920
|
controller.signal.throwIfAborted();
|
|
1443
1921
|
if (stopped)
|
|
1444
1922
|
throw new Error("Analytics service stopped");
|
|
1445
|
-
const
|
|
1923
|
+
const revision = "revision" in source && typeof source.revision === "string" ? source.revision : undefined;
|
|
1924
|
+
if (archive && revision !== undefined && archive.revision === revision) {
|
|
1925
|
+
archive = { ...archive, at: now() };
|
|
1926
|
+
return archive;
|
|
1927
|
+
}
|
|
1928
|
+
const records = source.iterateRecords ? await responsive(collectRecords(source.iterateRecords()), controller.signal) : source.records;
|
|
1929
|
+
const previous = incremental;
|
|
1930
|
+
incremental = undefined;
|
|
1931
|
+
const append = previous && records.length >= previous.prefix.length && previous.prefix.every((record2, index) => records[index] === record2);
|
|
1932
|
+
const accumulator = append ? previous.accumulator : createModelAccumulator({ location });
|
|
1933
|
+
const checkpoint = scanScheduler(controller.signal);
|
|
1934
|
+
for (let index = append ? previous.prefix.length : 0;index < records.length; index += 256) {
|
|
1935
|
+
{
|
|
1936
|
+
const waiting = checkpoint();
|
|
1937
|
+
if (waiting)
|
|
1938
|
+
await waiting;
|
|
1939
|
+
}
|
|
1940
|
+
accumulator.append(records.slice(index, index + 256));
|
|
1941
|
+
}
|
|
1942
|
+
const analysis = await responsive(accumulator.steps(), controller.signal);
|
|
1943
|
+
controller.signal.throwIfAborted();
|
|
1944
|
+
if (records.length <= 1e5 && records.every((r) => Object.isFrozen(r) && (!r.performance || Object.isFrozen(r.performance))))
|
|
1945
|
+
incremental = { prefix: records.slice(), accumulator };
|
|
1446
1946
|
const result = {
|
|
1447
1947
|
at: now(),
|
|
1948
|
+
...revision === undefined ? {} : { revision },
|
|
1448
1949
|
source: {
|
|
1449
1950
|
metadata: source.metadata,
|
|
1450
|
-
records:
|
|
1951
|
+
records: records.filter((r) => r.type === "gap")
|
|
1451
1952
|
},
|
|
1452
1953
|
analysis
|
|
1453
1954
|
};
|
|
@@ -1505,48 +2006,82 @@ function createAnalyticsService(path, location, enabled, options = {}) {
|
|
|
1505
2006
|
signal.throwIfAborted();
|
|
1506
2007
|
if (stopped)
|
|
1507
2008
|
throw new Error("Analytics service stopped");
|
|
2009
|
+
signal = AbortSignal.any([signal, shutdown.signal]);
|
|
1508
2010
|
for (const [id, item] of snapshots)
|
|
1509
2011
|
if (item.expiresAt <= now())
|
|
1510
2012
|
snapshots.delete(id);
|
|
1511
|
-
|
|
2013
|
+
const current = options.revisioned && !input.snapshotId ? await load(signal) : undefined;
|
|
2014
|
+
let snapshot = input.snapshotId ? snapshots.get(input.snapshotId) : [...snapshots.values()].find((s) => s.key === queryKey(input) && (current ? s.revision === current.revision : now() - s.generatedAt < 30000));
|
|
1512
2015
|
if (input.snapshotId && (!snapshot || snapshot.key !== queryKey(input)))
|
|
1513
2016
|
throw new Error("Analytics snapshot expired; refresh Models");
|
|
1514
2017
|
if (!snapshot) {
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
2018
|
+
if (projecting >= 2)
|
|
2019
|
+
throw new Error("Analytics service busy; retry later");
|
|
2020
|
+
projecting++;
|
|
2021
|
+
try {
|
|
2022
|
+
const data = current ?? await load(signal);
|
|
2023
|
+
signal.throwIfAborted();
|
|
2024
|
+
let requests2 = data.analysis.requests.filter((r) => r.completedAt >= input.from && r.completedAt < input.to && (!input.agent || r.agent === input.agent));
|
|
2025
|
+
requests2 = await responsive(sortRequests(requests2), signal);
|
|
2026
|
+
const gaps = data.source.records.filter((r) => r.time >= input.from && r.time < input.to).map((r) => r.time);
|
|
2027
|
+
snapshot = {
|
|
2028
|
+
id: crypto.randomUUID(),
|
|
2029
|
+
key: queryKey(input),
|
|
2030
|
+
generatedAt: data.at,
|
|
2031
|
+
expiresAt: now() + 90000,
|
|
2032
|
+
requests: requests2,
|
|
2033
|
+
models: await responsive(summarizeModelRequestSteps(requests2), signal),
|
|
2034
|
+
coverage: data.analysis.coverage,
|
|
2035
|
+
metadata: data.source.metadata,
|
|
2036
|
+
gaps: gaps.slice(-128),
|
|
2037
|
+
omittedGaps: Math.max(0, gaps.length - 128),
|
|
2038
|
+
...data.revision === undefined ? {} : { revision: data.revision },
|
|
2039
|
+
sorted: new Map,
|
|
2040
|
+
variants: new Map
|
|
2041
|
+
};
|
|
2042
|
+
signal.throwIfAborted();
|
|
2043
|
+
snapshots.set(snapshot.id, snapshot);
|
|
2044
|
+
if (snapshots.size > 2) {
|
|
2045
|
+
const oldest = snapshots.keys().next().value;
|
|
2046
|
+
if (oldest)
|
|
2047
|
+
snapshots.delete(oldest);
|
|
2048
|
+
}
|
|
2049
|
+
} finally {
|
|
2050
|
+
projecting--;
|
|
1537
2051
|
}
|
|
1538
2052
|
}
|
|
1539
2053
|
const sort = input.sort ?? "requests";
|
|
1540
|
-
const models = [...snapshot.models].sort((a, b) => {
|
|
2054
|
+
const models = snapshot.sorted.get(sort) ?? [...snapshot.models].sort((a, b) => {
|
|
1541
2055
|
const key = sort === "tps" ? "averageTps" : sort === "ttft" ? "p95TtftMs" : "completedRequests";
|
|
1542
2056
|
return (sort === "identity" ? 0 : (b[key] ?? -Infinity) - (a[key] ?? -Infinity)) || (a.model ?? "").localeCompare(b.model ?? "");
|
|
1543
2057
|
});
|
|
2058
|
+
snapshot.sorted.set(sort, models);
|
|
1544
2059
|
const modelOffset = pageOffset(input.modelCursor, snapshot, "models", sort);
|
|
1545
2060
|
const selected = input.unknownModel ? models.find((m) => m.model === undefined) : input.model ? models.find((m) => m.model === input.model) : models[modelOffset];
|
|
1546
2061
|
const selection = selected?.model ?? "";
|
|
1547
|
-
const
|
|
2062
|
+
const variantKey = JSON.stringify([
|
|
2063
|
+
selected?.model ?? null,
|
|
2064
|
+
!!selected,
|
|
2065
|
+
input.buckets
|
|
2066
|
+
]);
|
|
2067
|
+
let variant = snapshot.variants.get(variantKey);
|
|
2068
|
+
if (!variant) {
|
|
2069
|
+
const requests2 = selected ? snapshot.requests.filter((r) => r.model === selected.model) : [];
|
|
2070
|
+
variant = {
|
|
2071
|
+
requests: requests2,
|
|
2072
|
+
buckets: selected ? bucketModelRequests(requests2, input.from, input.to, input.buckets) : []
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
snapshot.variants.delete(variantKey);
|
|
2076
|
+
snapshot.variants.set(variantKey, variant);
|
|
2077
|
+
if (snapshot.variants.size > 8) {
|
|
2078
|
+
const oldest = snapshot.variants.keys().next().value;
|
|
2079
|
+
if (oldest !== undefined)
|
|
2080
|
+
snapshot.variants.delete(oldest);
|
|
2081
|
+
}
|
|
2082
|
+
const requests = variant.requests;
|
|
1548
2083
|
const requestOffset = pageOffset(input.requestCursor, snapshot, "requests", selection);
|
|
1549
|
-
const buckets =
|
|
2084
|
+
const buckets = structuredClone(variant.buckets);
|
|
1550
2085
|
const result = {
|
|
1551
2086
|
schemaVersion: 1,
|
|
1552
2087
|
generatedAt: snapshot.generatedAt,
|
|
@@ -1584,17 +2119,19 @@ function createAnalyticsService(path, location, enabled, options = {}) {
|
|
|
1584
2119
|
},
|
|
1585
2120
|
dispose() {
|
|
1586
2121
|
stopped = true;
|
|
2122
|
+
shutdown.abort();
|
|
1587
2123
|
scan?.controller.abort();
|
|
1588
2124
|
snapshots.clear();
|
|
1589
2125
|
archive = undefined;
|
|
2126
|
+
incremental = undefined;
|
|
1590
2127
|
}
|
|
1591
2128
|
};
|
|
1592
2129
|
}
|
|
1593
|
-
async function serveModelAnalytics(port, path, location, enabled) {
|
|
2130
|
+
async function serveModelAnalytics(port, path, location, enabled, options = {}) {
|
|
1594
2131
|
if (!port)
|
|
1595
2132
|
return () => {
|
|
1596
2133
|
};
|
|
1597
|
-
const service = createAnalyticsService(path, location, enabled);
|
|
2134
|
+
const service = createAnalyticsService(path, location, enabled, options);
|
|
1598
2135
|
const registration = await port.register(analyticsRpc, {
|
|
1599
2136
|
read: async (input, context) => {
|
|
1600
2137
|
if (!isAnalyticsRead(input))
|
|
@@ -1847,10 +2384,10 @@ class SnapshotStore {
|
|
|
1847
2384
|
const s = resident.get(id);
|
|
1848
2385
|
if (!s)
|
|
1849
2386
|
continue;
|
|
1850
|
-
const
|
|
2387
|
+
const performance2 = this.performanceSnapshot?.(id);
|
|
1851
2388
|
const next = freezeSnapshot({
|
|
1852
2389
|
...project(s, this.config, this.now()),
|
|
1853
|
-
...
|
|
2390
|
+
...performance2 ? { performance: performance2 } : {}
|
|
1854
2391
|
});
|
|
1855
2392
|
const old = this.rows.get(id);
|
|
1856
2393
|
if (old && JSON.stringify({ ...old, revision: 0 }) === JSON.stringify(next))
|
|
@@ -1919,7 +2456,7 @@ function freezeSnapshot(row2) {
|
|
|
1919
2456
|
|
|
1920
2457
|
// src/snapshotserver.ts
|
|
1921
2458
|
init_rpc();
|
|
1922
|
-
async function serveSnapshots(rpc, store,
|
|
2459
|
+
async function serveSnapshots(rpc, store, performance2 = false, prepareRead) {
|
|
1923
2460
|
if (!rpc)
|
|
1924
2461
|
return () => {
|
|
1925
2462
|
};
|
|
@@ -1927,15 +2464,15 @@ async function serveSnapshots(rpc, store, performance = false, prepareRead) {
|
|
|
1927
2464
|
...batch,
|
|
1928
2465
|
upserts: batch.upserts.map(({ performance: _performance, ...row2 }) => row2)
|
|
1929
2466
|
});
|
|
1930
|
-
const batchValid =
|
|
1931
|
-
const resultValid =
|
|
1932
|
-
const registration = await rpc.register(
|
|
2467
|
+
const batchValid = performance2 ? isPerformanceBatch : isBatch;
|
|
2468
|
+
const resultValid = performance2 ? isPerformanceResult : isResult;
|
|
2469
|
+
const registration = await rpc.register(performance2 ? performanceRpc : monitorRpc, {
|
|
1933
2470
|
read: async (input, context) => {
|
|
1934
2471
|
if (!isRead(input))
|
|
1935
2472
|
throw new Error("Invalid snapshot request");
|
|
1936
2473
|
await prepareRead?.(input.sessionIds, context.signal);
|
|
1937
2474
|
const raw = store.read(input);
|
|
1938
|
-
const result =
|
|
2475
|
+
const result = performance2 ? raw : stripPerformance(raw);
|
|
1939
2476
|
if (!resultValid(result))
|
|
1940
2477
|
throw new Error("Invalid snapshot projection");
|
|
1941
2478
|
return result;
|
|
@@ -1966,7 +2503,7 @@ async function serveSnapshots(rpc, store, performance = false, prepareRead) {
|
|
|
1966
2503
|
});
|
|
1967
2504
|
}
|
|
1968
2505
|
const unlisten = store.listen((raw) => {
|
|
1969
|
-
const batch =
|
|
2506
|
+
const batch = performance2 ? raw : stripPerformance(raw);
|
|
1970
2507
|
if (!batchValid(batch))
|
|
1971
2508
|
return;
|
|
1972
2509
|
queue.push(batch);
|
|
@@ -2498,13 +3035,13 @@ function encodeRecovery(rows) {
|
|
|
2498
3035
|
}
|
|
2499
3036
|
|
|
2500
3037
|
// src/telemetry.ts
|
|
2501
|
-
import { mkdir as mkdir2, open as
|
|
2502
|
-
import { dirname, join as
|
|
3038
|
+
import { mkdir as mkdir2, open as open3, rename, stat as stat2 } from "fs/promises";
|
|
3039
|
+
import { dirname, join as join3 } from "path";
|
|
2503
3040
|
import { homedir } from "os";
|
|
2504
3041
|
import { createHash as createHash2 } from "crypto";
|
|
2505
3042
|
function defaultPath(directory) {
|
|
2506
3043
|
const key = createHash2("sha256").update(directory).digest("hex").slice(0, 16);
|
|
2507
|
-
return
|
|
3044
|
+
return join3(process.env.XDG_STATE_HOME || join3(homedir(), ".local/state"), "opencode-context-guard", `${key}.jsonl`);
|
|
2508
3045
|
}
|
|
2509
3046
|
|
|
2510
3047
|
class JsonlTelemetrySink {
|
|
@@ -2563,7 +3100,7 @@ class JsonlTelemetrySink {
|
|
|
2563
3100
|
});
|
|
2564
3101
|
this.bytes = 0;
|
|
2565
3102
|
}
|
|
2566
|
-
const file = await
|
|
3103
|
+
const file = await open3(this.path, "a", 384);
|
|
2567
3104
|
try {
|
|
2568
3105
|
await file.writeFile(line);
|
|
2569
3106
|
} finally {
|
|
@@ -2588,7 +3125,7 @@ async function readJournal(path, maxBytes = 16 * 1024 * 1024) {
|
|
|
2588
3125
|
const size = await stat2(name).then((s) => s.size).catch(() => 0);
|
|
2589
3126
|
if (!size)
|
|
2590
3127
|
continue;
|
|
2591
|
-
const file = await
|
|
3128
|
+
const file = await open3(name, "r");
|
|
2592
3129
|
const buffer = Buffer.alloc(Math.min(size, maxBytes));
|
|
2593
3130
|
try {
|
|
2594
3131
|
await file.read(buffer, 0, buffer.length, Math.max(0, size - maxBytes));
|
|
@@ -2615,8 +3152,8 @@ async function setup(ctx, injectedSink) {
|
|
|
2615
3152
|
const config = parseConfig(ctx.options);
|
|
2616
3153
|
const disk = new JsonlTelemetrySink(config.telemetry.path ?? defaultPath(ctx.location.directory), config.telemetry);
|
|
2617
3154
|
const diagnostics = injectedSink ?? disk;
|
|
2618
|
-
const
|
|
2619
|
-
const store = new SnapshotStore(config, () => monitor?.sessions ?? new Map, Date.now, crypto.randomUUID(), config.metrics.enabled ? (id) =>
|
|
3155
|
+
const performance2 = new PerformanceTracker(config.safety.maxSessions, config.metrics.rollingWindowMs);
|
|
3156
|
+
const store = new SnapshotStore(config, () => monitor?.sessions ?? new Map, Date.now, crypto.randomUUID(), config.metrics.enabled ? (id) => performance2.snapshot(id) : undefined);
|
|
2620
3157
|
let monitor;
|
|
2621
3158
|
const sink = {
|
|
2622
3159
|
emit(event) {
|
|
@@ -2662,15 +3199,17 @@ async function setup(ctx, injectedSink) {
|
|
|
2662
3199
|
return () => {
|
|
2663
3200
|
};
|
|
2664
3201
|
});
|
|
3202
|
+
const archive = new PerformanceArchive(config.history.path ?? `${defaultPath(ctx.location.directory)}.metrics`);
|
|
3203
|
+
const archiveRead = (_path, filter) => archive.read(filter);
|
|
2665
3204
|
const stopHistory = await serveHistory(ctx.historyRpc, config.history.path ?? `${defaultPath(ctx.location.directory)}.metrics`, config.enabled && config.metrics.enabled && config.history.enabled, Date.now, async (sessionId, signal) => {
|
|
2666
3205
|
const info = await ctx.session.get({ sessionID: sessionId }, { signal });
|
|
2667
3206
|
return info.location.directory === ctx.location.directory;
|
|
2668
|
-
}).catch(() => {
|
|
3207
|
+
}, archiveRead).catch(() => {
|
|
2669
3208
|
diagnostics.emit({ time: Date.now(), type: "history-service-unavailable" });
|
|
2670
3209
|
return () => {
|
|
2671
3210
|
};
|
|
2672
3211
|
});
|
|
2673
|
-
const stopAnalytics = await serveModelAnalytics(ctx.analyticsRpc, config.history.path ?? `${defaultPath(ctx.location.directory)}.metrics`, ctx.location.directory, config.enabled && config.metrics.enabled && config.history.enabled).catch(() => {
|
|
3212
|
+
const stopAnalytics = await serveModelAnalytics(ctx.analyticsRpc, config.history.path ?? `${defaultPath(ctx.location.directory)}.metrics`, ctx.location.directory, config.enabled && config.metrics.enabled && config.history.enabled, { read: archiveRead, revisioned: true }).catch(() => {
|
|
2674
3213
|
diagnostics.emit({
|
|
2675
3214
|
time: Date.now(),
|
|
2676
3215
|
type: "analytics-service-unavailable"
|
|
@@ -2679,8 +3218,13 @@ async function setup(ctx, injectedSink) {
|
|
|
2679
3218
|
};
|
|
2680
3219
|
});
|
|
2681
3220
|
const stopSnapshots = async () => {
|
|
2682
|
-
await stopAnalytics();
|
|
2683
|
-
|
|
3221
|
+
const archives = await Promise.allSettled([stopAnalytics(), stopHistory()]);
|
|
3222
|
+
archive.dispose();
|
|
3223
|
+
if (archives.some((result) => result.status === "rejected"))
|
|
3224
|
+
diagnostics.emit({
|
|
3225
|
+
time: Date.now(),
|
|
3226
|
+
type: "registration-cleanup-failed"
|
|
3227
|
+
});
|
|
2684
3228
|
await stopV2();
|
|
2685
3229
|
await stopV1();
|
|
2686
3230
|
};
|
|
@@ -2694,10 +3238,10 @@ async function setup(ctx, injectedSink) {
|
|
|
2694
3238
|
const history = new PerformanceHistory(config.history.path ?? `${defaultPath(ctx.location.directory)}.metrics`, {
|
|
2695
3239
|
...config.history,
|
|
2696
3240
|
enabled: config.history.enabled && config.metrics.enabled
|
|
2697
|
-
}, Date.now, (type) => diagnostics.emit({ time: Date.now(), type }));
|
|
3241
|
+
}, Date.now, (type) => diagnostics.emit({ time: Date.now(), type }), (name) => archive.invalidate(name));
|
|
2698
3242
|
await history.initialize();
|
|
2699
3243
|
if (config.metrics.enabled) {
|
|
2700
|
-
|
|
3244
|
+
performance2.restore(await ctx.storage.get("performance.v1").catch(() => {
|
|
2701
3245
|
return;
|
|
2702
3246
|
}));
|
|
2703
3247
|
}
|
|
@@ -2705,7 +3249,7 @@ async function setup(ctx, injectedSink) {
|
|
|
2705
3249
|
const recordPerformance = (id, type, event) => {
|
|
2706
3250
|
if (!config.metrics.enabled)
|
|
2707
3251
|
return;
|
|
2708
|
-
const state = monitor?.sessions.get(id), snapshot =
|
|
3252
|
+
const state = monitor?.sessions.get(id), snapshot = performance2.snapshot(id);
|
|
2709
3253
|
if (!state || !snapshot)
|
|
2710
3254
|
return;
|
|
2711
3255
|
history.emit({
|
|
@@ -2724,7 +3268,7 @@ async function setup(ctx, injectedSink) {
|
|
|
2724
3268
|
const persistPerformance = async () => {
|
|
2725
3269
|
if (!config.metrics.enabled)
|
|
2726
3270
|
return;
|
|
2727
|
-
await ctx.storage.set("performance.v1",
|
|
3271
|
+
await ctx.storage.set("performance.v1", performance2.checkpoint().map((row2) => ({ ...row2 }))).catch(() => {
|
|
2728
3272
|
diagnostics.emit({
|
|
2729
3273
|
time: Date.now(),
|
|
2730
3274
|
type: "performance-persistence-unavailable"
|
|
@@ -2907,12 +3451,12 @@ async function setup(ctx, injectedSink) {
|
|
|
2907
3451
|
recoveries.delete(sessionId);
|
|
2908
3452
|
}
|
|
2909
3453
|
if (config.metrics.enabled) {
|
|
2910
|
-
if (!
|
|
2911
|
-
const dormant = [...
|
|
3454
|
+
if (!performance2.snapshot(sessionId) && [...performance2.ids()].length >= config.safety.maxSessions) {
|
|
3455
|
+
const dormant = [...performance2.ids()].find((id) => !performanceResidents.has(id));
|
|
2912
3456
|
if (dormant)
|
|
2913
|
-
|
|
3457
|
+
performance2.remove(dormant);
|
|
2914
3458
|
}
|
|
2915
|
-
|
|
3459
|
+
performance2.observe(sessionId, info.time.created, outputTokens(info.tokens));
|
|
2916
3460
|
performanceResidents.add(sessionId);
|
|
2917
3461
|
}
|
|
2918
3462
|
s.usage.cumulative = info.tokens;
|
|
@@ -3015,7 +3559,7 @@ async function setup(ctx, injectedSink) {
|
|
|
3015
3559
|
return;
|
|
3016
3560
|
if (event.type === "session.text.delta" || event.type === "session.reasoning.delta") {
|
|
3017
3561
|
if (config.metrics.enabled)
|
|
3018
|
-
|
|
3562
|
+
performance2.delta(event.data.sessionID, event.data.assistantMessageID, event.id, event.created, event.data.delta.length);
|
|
3019
3563
|
return;
|
|
3020
3564
|
}
|
|
3021
3565
|
if (!relevant.has(event.type) || !("sessionID" in event.data))
|
|
@@ -3025,7 +3569,7 @@ async function setup(ctx, injectedSink) {
|
|
|
3025
3569
|
return;
|
|
3026
3570
|
if (event.type === "session.deleted") {
|
|
3027
3571
|
recordPerformance(id, "terminal", "session.deleted");
|
|
3028
|
-
|
|
3572
|
+
performance2.remove(id);
|
|
3029
3573
|
performanceResidents.delete(id);
|
|
3030
3574
|
await persistPerformance();
|
|
3031
3575
|
store.remove(id);
|
|
@@ -3041,11 +3585,11 @@ async function setup(ctx, injectedSink) {
|
|
|
3041
3585
|
return;
|
|
3042
3586
|
if ("durable" in event && event.durable && !activeMonitor.accept(s, event.durable.seq))
|
|
3043
3587
|
return;
|
|
3044
|
-
const perfAccepted = config.metrics.enabled && (!("durable" in event) || !event.durable ||
|
|
3588
|
+
const perfAccepted = config.metrics.enabled && (!("durable" in event) || !event.durable || performance2.accept(id, event.durable.seq));
|
|
3045
3589
|
switch (event.type) {
|
|
3046
3590
|
case "session.step.started":
|
|
3047
3591
|
if (perfAccepted)
|
|
3048
|
-
|
|
3592
|
+
performance2.start(id, event.data.assistantMessageID, event.data.agent, `${event.data.model.providerID}/${event.data.model.id}`, event.data.started);
|
|
3049
3593
|
activeMonitor.touch({
|
|
3050
3594
|
sessionId: id,
|
|
3051
3595
|
agent: event.data.agent,
|
|
@@ -3060,12 +3604,12 @@ async function setup(ctx, injectedSink) {
|
|
|
3060
3604
|
break;
|
|
3061
3605
|
case "session.step.streamed":
|
|
3062
3606
|
if (perfAccepted)
|
|
3063
|
-
|
|
3607
|
+
performance2.streamed(id, event.data.assistantMessageID, event.created);
|
|
3064
3608
|
break;
|
|
3065
3609
|
case "session.step.ended":
|
|
3066
3610
|
case "session.step.failed": {
|
|
3067
3611
|
if (perfAccepted)
|
|
3068
|
-
|
|
3612
|
+
performance2.settle(id, event.data.assistantMessageID, event.data.tokens ? outputTokens(event.data.tokens) : undefined);
|
|
3069
3613
|
const tokens = event.data.tokens && promptTokens(event.data.tokens);
|
|
3070
3614
|
const request = requests.get(id);
|
|
3071
3615
|
if (tokens !== undefined && request?.id === event.data.assistantMessageID && request.model === s.model)
|
|
@@ -3104,8 +3648,8 @@ async function setup(ctx, injectedSink) {
|
|
|
3104
3648
|
case "session.execution.failed":
|
|
3105
3649
|
case "session.execution.interrupted":
|
|
3106
3650
|
s.active = false;
|
|
3107
|
-
if (
|
|
3108
|
-
|
|
3651
|
+
if (performance2.snapshot(id)?.tokenSource === "estimated")
|
|
3652
|
+
performance2.gap(id);
|
|
3109
3653
|
s.lastExecutionEnd = event.created;
|
|
3110
3654
|
break;
|
|
3111
3655
|
case "session.tool.called":
|
|
@@ -3126,7 +3670,7 @@ async function setup(ctx, injectedSink) {
|
|
|
3126
3670
|
lastPerformanceSample = now;
|
|
3127
3671
|
for (const id of performanceResidents) {
|
|
3128
3672
|
store.mark(id);
|
|
3129
|
-
const snapshot =
|
|
3673
|
+
const snapshot = performance2.snapshot(id);
|
|
3130
3674
|
if (now - lastHistorySample >= 1000 && snapshot && (snapshot.status === "waiting" || snapshot.status === "streaming"))
|
|
3131
3675
|
recordPerformance(id, "sample");
|
|
3132
3676
|
}
|
|
@@ -3144,7 +3688,7 @@ async function setup(ctx, injectedSink) {
|
|
|
3144
3688
|
store.recover("capacity-exceeded");
|
|
3145
3689
|
for (const id of performanceResidents)
|
|
3146
3690
|
if (!activeMonitor.sessions.has(id)) {
|
|
3147
|
-
|
|
3691
|
+
performance2.remove(id);
|
|
3148
3692
|
performanceResidents.delete(id);
|
|
3149
3693
|
}
|
|
3150
3694
|
for (const id of requests.keys())
|
|
@@ -3168,7 +3712,7 @@ async function setup(ctx, injectedSink) {
|
|
|
3168
3712
|
} catch {
|
|
3169
3713
|
}
|
|
3170
3714
|
if (!stopped) {
|
|
3171
|
-
|
|
3715
|
+
performance2.gap();
|
|
3172
3716
|
for (const id of performanceResidents) {
|
|
3173
3717
|
store.mark(id);
|
|
3174
3718
|
recordPerformance(id, "gap", "event-stream-gap");
|