querysub 0.43.0 → 0.45.0
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/package.json +60 -60
- package/src/-a-archives/archivesBackBlaze.ts +3 -0
- package/src/-g-core-values/NodeCapabilities.ts +6 -4
- package/src/0-path-value-core/NodePathAuthorities.ts +24 -2
- package/src/0-path-value-core/pathValueCore.ts +2221 -2216
- package/src/2-proxy/PathValueProxyWatcher.ts +41 -3
- package/src/3-path-functions/PathFunctionRunner.ts +19 -6
- package/src/3-path-functions/pathFunctionLoader.ts +7 -1
- package/src/4-querysub/QuerysubController.ts +0 -10
- package/src/5-diagnostics/GenericFormat.tsx +3 -1
- package/src/5-diagnostics/TimeGrouper.tsx +2 -0
- package/src/5-diagnostics/nodeMetadata.ts +137 -11
- package/src/diagnostics/NodeViewer.tsx +19 -8
- package/src/diagnostics/logs/DiskLoggerPage.tsx +36 -16
- package/src/diagnostics/logs/diskLogger.ts +12 -1
- package/src/diagnostics/periodic.ts +2 -2
- package/src/diagnostics/trackResources.ts +4 -3
- package/src/diagnostics/watchdog.ts +20 -6
- package/src/library-components/Input.tsx +1 -1
- package/src/zip.ts +1 -0
|
@@ -32,6 +32,8 @@ import { DEPTH_TO_DATA, MODULE_INDEX, getCurrentCall, getCurrentCallObj } from "
|
|
|
32
32
|
import { inlineNestedCalls } from "../3-path-functions/syncSchema";
|
|
33
33
|
import { interceptCalls, runCall } from "../3-path-functions/PathFunctionHelpers";
|
|
34
34
|
import { deepCloneCborx } from "../misc/cloneHelpers";
|
|
35
|
+
import { addStatPeriodic, addTimeProfileDistribution, onTimeProfile } from "../5-diagnostics/nodeMetadata";
|
|
36
|
+
import { formatPercent } from "socket-function/src/formatting/format";
|
|
35
37
|
|
|
36
38
|
// TODO: Break this into two parts:
|
|
37
39
|
// 1) Run and get accesses
|
|
@@ -158,7 +160,19 @@ interface WatcherOptions<Result> {
|
|
|
158
160
|
trackTriggers?: boolean;
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
|
|
163
|
+
let harvestableReadyLoopCount = 0;
|
|
164
|
+
let harvestableWaitingLoopCount = 0;
|
|
165
|
+
let lastZombieCount = 0;
|
|
166
|
+
// NOTE: Only the % waiting for active watchers. Which... is fine
|
|
167
|
+
addStatPeriodic("Watcher % Waiting", () => {
|
|
168
|
+
let totalLoop = harvestableReadyLoopCount + harvestableWaitingLoopCount;
|
|
169
|
+
if (totalLoop === 0) return 0;
|
|
170
|
+
let frac = harvestableWaitingLoopCount / totalLoop;
|
|
171
|
+
harvestableWaitingLoopCount = 0;
|
|
172
|
+
harvestableReadyLoopCount = 0;
|
|
173
|
+
return frac;
|
|
174
|
+
}, formatPercent);
|
|
175
|
+
addStatPeriodic("Stalled Watchers", () => lastZombieCount);
|
|
162
176
|
|
|
163
177
|
// Used to prevent local rejections on remote values (as local functions aren't rerun)
|
|
164
178
|
// Also used for security on servers, so they can read from untrusted domains, but can't have values
|
|
@@ -966,7 +980,19 @@ export class PathValueProxyWatcher {
|
|
|
966
980
|
watcher.dispose = dispose;
|
|
967
981
|
this.allWatchers.add(watcher);
|
|
968
982
|
|
|
969
|
-
|
|
983
|
+
let baseFunction = options.watchFunction;
|
|
984
|
+
baseFunction = measureWrap(baseFunction, watcher.debugName);
|
|
985
|
+
{
|
|
986
|
+
let base = baseFunction;
|
|
987
|
+
baseFunction = (...args: unknown[]) => {
|
|
988
|
+
let time = Date.now();
|
|
989
|
+
try {
|
|
990
|
+
return (base as any)(...args);
|
|
991
|
+
} finally {
|
|
992
|
+
onTimeProfile("Watcher", time);
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
}
|
|
970
996
|
|
|
971
997
|
watcher.setCurrentReadTime = (time, code) => {
|
|
972
998
|
let prev = watcher.currentReadTime;
|
|
@@ -1430,13 +1456,24 @@ export class PathValueProxyWatcher {
|
|
|
1430
1456
|
this.runningWatcher = undefined;
|
|
1431
1457
|
}
|
|
1432
1458
|
|
|
1459
|
+
const nonLooping = options.runImmediately || options.allowUnsyncedReads;
|
|
1460
|
+
|
|
1433
1461
|
const readyToCommit = (
|
|
1434
|
-
|
|
1462
|
+
nonLooping || (
|
|
1435
1463
|
watcher.lastUnsyncedAccesses.size === 0
|
|
1436
1464
|
&& watcher.lastUnsyncedParentAccesses.size === 0
|
|
1437
1465
|
&& !watcher.lastSpecialPromiseUnsynced
|
|
1438
1466
|
)
|
|
1439
1467
|
);
|
|
1468
|
+
|
|
1469
|
+
if (!nonLooping) {
|
|
1470
|
+
if (readyToCommit) {
|
|
1471
|
+
harvestableReadyLoopCount++;
|
|
1472
|
+
} else {
|
|
1473
|
+
harvestableWaitingLoopCount++;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1440
1477
|
// Commit writes BEFORE we watch. This prevents us from triggering ourself, in our first watch
|
|
1441
1478
|
// (on subsequent watches ClientWatcher will prevent self watches. It can't for the first
|
|
1442
1479
|
// watch, as it wasn't the triggerer).
|
|
@@ -1881,6 +1918,7 @@ registerPeriodic(function checkForZombieWatches() {
|
|
|
1881
1918
|
}
|
|
1882
1919
|
|
|
1883
1920
|
let zombies = Array.from(proxyWatcher.getAllWatchers()).filter(isZombieWatch);
|
|
1921
|
+
lastZombieCount = zombies.length;
|
|
1884
1922
|
if (zombies.length > 0) {
|
|
1885
1923
|
console.warn(red(`Found ${zombies.length} zombie watchers`));
|
|
1886
1924
|
let namesSet = new Set(zombies.map(x => x.debugName));
|
|
@@ -542,11 +542,14 @@ export class PathFunctionRunner {
|
|
|
542
542
|
}
|
|
543
543
|
|
|
544
544
|
let evalTimeStart = Date.now();
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
545
|
+
try {
|
|
546
|
+
let args = parseArgs(callPath);
|
|
547
|
+
overrideCurrentCall({ spec: callPath, fnc: syncedSpec, }, () => {
|
|
548
|
+
baseFunction(...args);
|
|
549
|
+
});
|
|
550
|
+
} finally {
|
|
551
|
+
evalTime += Date.now() - evalTimeStart;
|
|
552
|
+
}
|
|
550
553
|
|
|
551
554
|
// NOTE: The results are only temporarily stored, but serve as a way to let clients
|
|
552
555
|
// know figure out the dependencies for the call, which makes function call prediction
|
|
@@ -573,6 +576,7 @@ export class PathFunctionRunner {
|
|
|
573
576
|
});
|
|
574
577
|
proxyWatcher.setEventPath(() => syncedModule.Results[callPath.CallId]);
|
|
575
578
|
});
|
|
579
|
+
|
|
576
580
|
},
|
|
577
581
|
}, {
|
|
578
582
|
onWritesCommitted(writes) {
|
|
@@ -631,7 +635,16 @@ export class PathFunctionRunner {
|
|
|
631
635
|
if (PathFunctionRunner.DEBUG_CALLS) {
|
|
632
636
|
console.log(`FINISHED${nooped ? " (skipped)" : ""} ${getDebugName(callPath, functionSpec, true)}, writes: ${finalWrites?.length}`);
|
|
633
637
|
}
|
|
634
|
-
|
|
638
|
+
|
|
639
|
+
let wallTime = Date.now() - startTime;
|
|
640
|
+
let syncTime = wallTime - evalTime;
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
diskLog(`Finished FunctionRunner function`, {
|
|
644
|
+
...callPath, argsEncoded: "", functionSpec,
|
|
645
|
+
wallTime, syncTime, evalTime,
|
|
646
|
+
loops: runCount,
|
|
647
|
+
});
|
|
635
648
|
PathFunctionRunner.RUN_FINISH_COUNT++;
|
|
636
649
|
}
|
|
637
650
|
}
|
|
@@ -22,6 +22,7 @@ import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
|
|
|
22
22
|
import { getControllerNodeId, getControllerNodeIdList } from "../-g-core-values/NodeCapabilities";
|
|
23
23
|
import { sha256 } from "js-sha256";
|
|
24
24
|
import os from "os";
|
|
25
|
+
import { formatTime } from "socket-function/src/formatting/format";
|
|
25
26
|
|
|
26
27
|
// Get localPathRemappings using yargs, so it is easy to configure in multiple entry points
|
|
27
28
|
let yargObj = isNodeTrue() && yargs(process.argv)
|
|
@@ -158,6 +159,8 @@ let moduleResolver = async (spec: FunctionSpec) => {
|
|
|
158
159
|
await getFileLock(lockPath, async () => {
|
|
159
160
|
if (fs.existsSync(repoPath + loadTimeIndicatorFileName)) return;
|
|
160
161
|
|
|
162
|
+
let time = Date.now();
|
|
163
|
+
|
|
161
164
|
// Remove any previous attempt to sync it
|
|
162
165
|
if (fs.existsSync(repoPath)) {
|
|
163
166
|
await fs.promises.rename(repoPath, repoPath + "_" + Date.now());
|
|
@@ -171,6 +174,9 @@ let moduleResolver = async (spec: FunctionSpec) => {
|
|
|
171
174
|
|
|
172
175
|
// Mark it as loaded. If we don't reach this point we will move the folder and try again next time
|
|
173
176
|
await fs.promises.writeFile(repoPath + loadTimeIndicatorFileName, Date.now() + "");
|
|
177
|
+
|
|
178
|
+
time = Date.now() - time;
|
|
179
|
+
console.log(blue(`Cloned and yarn installed repo`), { gitURL, time });
|
|
174
180
|
});
|
|
175
181
|
}
|
|
176
182
|
|
|
@@ -415,7 +421,7 @@ let everLoadedRepos = new Set<string>();
|
|
|
415
421
|
export async function preloadFunctions(specs: FunctionSpec[]) {
|
|
416
422
|
let nodeIds = await getControllerNodeIdList(functionPreloadController);
|
|
417
423
|
await Promise.all(nodeIds.map(async nodeId => {
|
|
418
|
-
let controller = functionPreloadController.nodes[nodeId];
|
|
424
|
+
let controller = functionPreloadController.nodes[nodeId.nodeId];
|
|
419
425
|
console.log(blue(`Preloading functions on ${nodeId}`));
|
|
420
426
|
await errorToUndefined(controller.preloadFunctions(specs));
|
|
421
427
|
console.log(blue(`Finished preloading functions on ${nodeId}`));
|
|
@@ -125,16 +125,6 @@ interface PermissionsObj {
|
|
|
125
125
|
let permissionsPerNode = new Map<string, PermissionsObj>();
|
|
126
126
|
registerResource("querysub|permissionsPerNode", permissionsPerNode);
|
|
127
127
|
|
|
128
|
-
const lastCallTime = cache((nodeId: string): { value: Time } => {
|
|
129
|
-
SocketFunction.onNextDisconnect(nodeId, () => {
|
|
130
|
-
setTimeout(() => {
|
|
131
|
-
if (SocketFunction.isNodeConnected(nodeId)) return;
|
|
132
|
-
lastCallTime.clear(nodeId);
|
|
133
|
-
}, 1000 * 60);
|
|
134
|
-
});
|
|
135
|
-
return { value: epochTime };
|
|
136
|
-
});
|
|
137
|
-
|
|
138
128
|
export const isTrustedByDomain = cache(async function isTrustedByDomain(domain: string): Promise<boolean> {
|
|
139
129
|
// NOTE: When getControllerNodeId accepts a domain we will pass them the domain we are using.
|
|
140
130
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatDateTime, formatNiceDateTime, formatNumber, formatTime, formatVeryNiceDateTime } from "socket-function/src/formatting/format";
|
|
1
|
+
import { formatDateTime, formatNiceDateTime, formatNumber, formatPercent, formatTime, formatVeryNiceDateTime } from "socket-function/src/formatting/format";
|
|
2
2
|
import { css } from "typesafecss";
|
|
3
3
|
import { canHaveChildren } from "socket-function/src/types";
|
|
4
4
|
import { qreact } from "../4-dom/qreact";
|
|
@@ -22,6 +22,7 @@ export type JSXFormatter<T = unknown, RowT extends RowType = RowType> = (
|
|
|
22
22
|
type StringFormatters = (
|
|
23
23
|
"guess"
|
|
24
24
|
| "string" | "number"
|
|
25
|
+
| "percent"
|
|
25
26
|
| "timeSpan" | "date"
|
|
26
27
|
| "error" | "link"
|
|
27
28
|
| "toSpaceCase"
|
|
@@ -38,6 +39,7 @@ const endGuessDateRange = +new Date(2040, 0, 1).getTime();
|
|
|
38
39
|
let formatters: { [formatter in StringFormatters]: (value: unknown) => preact.ComponentChild } = {
|
|
39
40
|
string: (value) => d(value, String(value || "")),
|
|
40
41
|
number: (value) => d(value, formatNumber(Number(value))),
|
|
42
|
+
percent: (value) => d(value, formatPercent(Number(value))),
|
|
41
43
|
timeSpan: (value) => d(value, formatTime(Number(value))),
|
|
42
44
|
date: (value) => d(value, formatVeryNiceDateTime(Number(value))),
|
|
43
45
|
error: (value) => d(value, <span class={errorMessage}>{String(value)}</span>),
|
|
@@ -93,6 +93,7 @@ export class TimeGrouper {
|
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/** For sampled values, ex, % CPU usage */
|
|
96
97
|
public getStateSummary(format: (value: number) => string): string {
|
|
97
98
|
return this.getSummary().map(bucket => {
|
|
98
99
|
if (bucket.duration === 0 || bucket.min === bucket.max) {
|
|
@@ -102,6 +103,7 @@ export class TimeGrouper {
|
|
|
102
103
|
return `(${bucket.name}) ${format(bucket.min)} to ${format(bucket.max)}`;
|
|
103
104
|
}).filter(isDefined).join("\n");
|
|
104
105
|
}
|
|
106
|
+
/** Sums up the values. Ex, for time waiting. */
|
|
105
107
|
public getEventSummary(format: (value: number) => string): string {
|
|
106
108
|
return this.getSummary().map(bucket => {
|
|
107
109
|
if (bucket.duration === 0) {
|
|
@@ -5,13 +5,16 @@ import { SocketFunction } from "socket-function/SocketFunction";
|
|
|
5
5
|
import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
|
|
6
6
|
import { cache } from "socket-function/src/caching";
|
|
7
7
|
import { TimeGrouper } from "./TimeGrouper";
|
|
8
|
-
import { runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
9
|
-
import { throttleFunction, timeInMinute } from "socket-function/src/misc";
|
|
8
|
+
import { runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
9
|
+
import { sort, throttleFunction, timeInMinute } from "socket-function/src/misc";
|
|
10
10
|
import { formatNumber, formatPercent, formatTime } from "socket-function/src/formatting/format";
|
|
11
11
|
import os from "os";
|
|
12
12
|
import { isNode } from "typesafecss";
|
|
13
13
|
import { diskLog } from "../diagnostics/logs/diskLogger";
|
|
14
14
|
|
|
15
|
+
|
|
16
|
+
const POLL_INTERVAL = timeInMinute * 5;
|
|
17
|
+
|
|
15
18
|
// Undefined means we infer the column
|
|
16
19
|
// Null means the column is removed
|
|
17
20
|
export type ColumnType = undefined | null | {
|
|
@@ -39,14 +42,14 @@ export function registerNodeMetadata(getter: ExtraMetadata) {
|
|
|
39
42
|
extraMetadatas.push(getter);
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
// IMPORTANT! We no longer diskLog here! diskLogging is not ideal for metrics like these. You almost only
|
|
46
|
+
// care about the current metrics OR past trends. A single value in the logs from the past is just
|
|
47
|
+
// NOT useful.
|
|
48
|
+
|
|
42
49
|
let stateValues = cache((title: string, format: (value: number) => string): { (value: number): void } => {
|
|
43
50
|
let timeGrouper = new TimeGrouper();
|
|
44
|
-
let logToDisk = throttleFunction(timeInMinute, (value: number) => {
|
|
45
|
-
diskLog(title, { value, formatted: format(value) });
|
|
46
|
-
});
|
|
47
51
|
function onValue(value: number) {
|
|
48
52
|
timeGrouper.onValueChanged(value);
|
|
49
|
-
void logToDisk(value);
|
|
50
53
|
}
|
|
51
54
|
registerNodeMetadata({
|
|
52
55
|
columnName: title,
|
|
@@ -59,12 +62,8 @@ let stateValues = cache((title: string, format: (value: number) => string): { (v
|
|
|
59
62
|
});
|
|
60
63
|
let eventValues = cache((title: string, format: (value: number) => string): { (value: number): void } => {
|
|
61
64
|
let timeGrouper = new TimeGrouper();
|
|
62
|
-
let logToDisk = throttleFunction(timeInMinute, (value: number) => {
|
|
63
|
-
diskLog(title, { value, formatted: format(value) });
|
|
64
|
-
});
|
|
65
65
|
function onValue(value: number) {
|
|
66
66
|
timeGrouper.onValueChanged(value);
|
|
67
|
-
void logToDisk(value);
|
|
68
67
|
}
|
|
69
68
|
registerNodeMetadata({
|
|
70
69
|
columnName: title,
|
|
@@ -76,15 +75,142 @@ let eventValues = cache((title: string, format: (value: number) => string): { (v
|
|
|
76
75
|
return onValue;
|
|
77
76
|
});
|
|
78
77
|
|
|
78
|
+
/** For values that should be summed to get the value. */
|
|
79
79
|
export function logNodeStats(title: string, format: (value: number) => string): { (value: number): void } {
|
|
80
80
|
return eventValues(title, format);
|
|
81
81
|
}
|
|
82
|
+
/** For state values (ex, % CPU usage), for which summing wouldn't make sense. */
|
|
82
83
|
export function logNodeStateStats(title: string, format: (value: number) => string): { (value: number): void } {
|
|
83
84
|
return stateValues(title, format);
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
let existingPeriodic = new Set<string>();
|
|
88
|
+
/** For state values (ex, % CPU usage), for which summing wouldn't make sense. */
|
|
89
|
+
export function addStatPeriodic(
|
|
90
|
+
title: string,
|
|
91
|
+
getValue: () => number,
|
|
92
|
+
format: (value: number) => string = formatNumber,
|
|
93
|
+
) {
|
|
94
|
+
let hash = JSON.stringify({ title, getValue: getValue.toString(), format: format.toString() });
|
|
95
|
+
// Ignoring duplicates, otherwise we end up logging way too much
|
|
96
|
+
if (globalThis.isHotReloading?.() && existingPeriodic.has(hash)) return;
|
|
97
|
+
|
|
98
|
+
let timeGrouper = new TimeGrouper();
|
|
99
|
+
void runInfinitePollCallAtStart(POLL_INTERVAL, () => {
|
|
100
|
+
timeGrouper.onValueChanged(getValue());
|
|
101
|
+
});
|
|
102
|
+
registerNodeMetadata({
|
|
103
|
+
columnName: title,
|
|
104
|
+
column: {},
|
|
105
|
+
getValue() {
|
|
106
|
+
return timeGrouper.getStateSummary(format);
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/** For values that should be summed to get the value. */
|
|
111
|
+
export function addStatSumPeriodic(
|
|
112
|
+
title: string,
|
|
113
|
+
getValue: () => number,
|
|
114
|
+
format: (value: number) => string = formatNumber,
|
|
115
|
+
) {
|
|
116
|
+
let hash = JSON.stringify({ title, getValue: getValue.toString(), format: format.toString() });
|
|
117
|
+
// Ignoring duplicates, otherwise we end up logging way too much
|
|
118
|
+
if (globalThis.isHotReloading?.() && existingPeriodic.has(hash)) return;
|
|
119
|
+
|
|
120
|
+
let timeGrouper = new TimeGrouper();
|
|
121
|
+
void runInfinitePollCallAtStart(POLL_INTERVAL, () => {
|
|
122
|
+
timeGrouper.onValueChanged(getValue());
|
|
123
|
+
});
|
|
124
|
+
registerNodeMetadata({
|
|
125
|
+
columnName: title,
|
|
126
|
+
column: {},
|
|
127
|
+
getValue() {
|
|
128
|
+
return timeGrouper.getEventSummary(format);
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let pendingTimeProfiles = new Map<string, { start: number; end: number }[]>();
|
|
134
|
+
export function onTimeProfile(title: string, startTime: number) {
|
|
135
|
+
let profiles = pendingTimeProfiles.get(title);
|
|
136
|
+
if (!profiles) {
|
|
137
|
+
profiles = [];
|
|
138
|
+
pendingTimeProfiles.set(title, profiles);
|
|
139
|
+
addTimeProfileDistribution(title, () => {
|
|
140
|
+
let values = pendingTimeProfiles.get(title);
|
|
141
|
+
pendingTimeProfiles.set(title, []);
|
|
142
|
+
return values || [];
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
profiles.push({ start: startTime, end: Date.now() });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Ex, for tracking the time distribution of API calls, or watchFunction evaluation, etc.
|
|
149
|
+
* - Specifically for time, so we can handle overlap cases (such as waiting for 10 calls at once),
|
|
150
|
+
* which requires specially handling otherwise the values will be meaningless (because
|
|
151
|
+
* waiting from time 0 to 100ms for 10 calls is VERY different than waiting
|
|
152
|
+
* from time 0 to 1000ms for 1 call).
|
|
153
|
+
*
|
|
154
|
+
* NOTE: Usually you should call onTimeProfile, which will wrap this call. But, sometimes
|
|
155
|
+
* you need to harvest values from an external library, in which case, you will need
|
|
156
|
+
* this function.
|
|
157
|
+
*/
|
|
158
|
+
export function addTimeProfileDistribution(
|
|
159
|
+
title: string,
|
|
160
|
+
/** It's expected that this will return all values since the last call to harvestValues
|
|
161
|
+
* (basically, copy your value buffer and clear the old one).
|
|
162
|
+
*/
|
|
163
|
+
harvestTimeRanges: () => { start: number; end: number; }[],
|
|
164
|
+
) {
|
|
165
|
+
let hash = JSON.stringify({ title, getValues: harvestTimeRanges.toString() });
|
|
166
|
+
// Ignoring duplicates, otherwise we end up logging way too much
|
|
167
|
+
if (globalThis.isHotReloading?.() && existingPeriodic.has(hash)) return;
|
|
168
|
+
|
|
169
|
+
let fractionTimeWaitingGroup = new TimeGrouper();
|
|
170
|
+
let ratePerSecondGroup = new TimeGrouper();
|
|
171
|
+
let lastPollTime = Date.now();
|
|
172
|
+
runInfinitePoll(POLL_INTERVAL, () => {
|
|
173
|
+
let originalStartTime = lastPollTime;
|
|
174
|
+
let values = harvestTimeRanges();
|
|
175
|
+
values = values.slice();
|
|
176
|
+
sort(values, x => x.start);
|
|
177
|
+
let freeTime = 0;
|
|
178
|
+
for (let { start, end } of values) {
|
|
179
|
+
if (start > lastPollTime) {
|
|
180
|
+
freeTime += start - lastPollTime;
|
|
181
|
+
}
|
|
182
|
+
lastPollTime = end;
|
|
183
|
+
}
|
|
184
|
+
let now = Date.now();
|
|
185
|
+
freeTime += now - lastPollTime;
|
|
186
|
+
lastPollTime = now;
|
|
187
|
+
|
|
188
|
+
let duration = lastPollTime - originalStartTime;
|
|
189
|
+
let waitingTime = duration - freeTime;
|
|
190
|
+
let ratePerSecond = values.length / (duration / 1000);
|
|
191
|
+
|
|
192
|
+
fractionTimeWaitingGroup.onValueChanged(waitingTime / duration);
|
|
193
|
+
ratePerSecondGroup.onValueChanged(ratePerSecond);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
registerNodeMetadata({
|
|
197
|
+
columnName: `% Time in ${title}`,
|
|
198
|
+
column: {},
|
|
199
|
+
getValue() {
|
|
200
|
+
return fractionTimeWaitingGroup.getStateSummary(formatPercent);
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
registerNodeMetadata({
|
|
204
|
+
columnName: `${title}/S`,
|
|
205
|
+
column: {},
|
|
206
|
+
getValue() {
|
|
207
|
+
return ratePerSecondGroup.getStateSummary(formatNumber);
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
86
212
|
if (isNode()) {
|
|
87
|
-
void runInfinitePollCallAtStart(
|
|
213
|
+
void runInfinitePollCallAtStart(POLL_INTERVAL, async () => {
|
|
88
214
|
// Get system memory usage
|
|
89
215
|
|
|
90
216
|
const free = os.freemem();
|
|
@@ -185,13 +185,22 @@ export class NodeViewer extends qreact.Component {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
let builtinGroups = {
|
|
188
|
-
"Default": ["buttons", "devToolsURL", "nodeId", "ip", "uptime", "loadTime", "
|
|
188
|
+
"Default": ["buttons", "devToolsURL", "nodeId", "ip", "uptime", "loadTime", "% Profiled", "All Memory", "port", "threadId", "machineId", "apiError", "live_entryPoint"],
|
|
189
|
+
"Performance": [
|
|
190
|
+
"All Memory",
|
|
191
|
+
"Heap", "Buffers",
|
|
192
|
+
"% Profiled",
|
|
193
|
+
"Blocking Lag",
|
|
194
|
+
"Top Profiled",
|
|
195
|
+
],
|
|
189
196
|
};
|
|
190
197
|
// Column => group
|
|
191
|
-
let builtInGroupsLookup = new Map<string, string>();
|
|
198
|
+
let builtInGroupsLookup = new Map<string, string[]>();
|
|
192
199
|
for (let [group, columns] of Object.entries(builtinGroups)) {
|
|
193
200
|
for (let column of columns) {
|
|
194
|
-
builtInGroupsLookup.
|
|
201
|
+
let groups = builtInGroupsLookup.get(column) || [];
|
|
202
|
+
groups.push(group);
|
|
203
|
+
builtInGroupsLookup.set(column, groups);
|
|
195
204
|
}
|
|
196
205
|
}
|
|
197
206
|
let groupedTables: {
|
|
@@ -206,12 +215,14 @@ export class NodeViewer extends qreact.Component {
|
|
|
206
215
|
for (let table of tableList) {
|
|
207
216
|
let row = table.table?.row || {};
|
|
208
217
|
for (let column in row) {
|
|
209
|
-
let
|
|
210
|
-
|
|
211
|
-
|
|
218
|
+
let groupTitles = builtInGroupsLookup.get(column) || ["Other"];
|
|
219
|
+
for (let groupTitle of groupTitles) {
|
|
220
|
+
if (column.includes("|")) {
|
|
221
|
+
groupTitle = column.split("|")[0];
|
|
222
|
+
}
|
|
223
|
+
groupedTables[groupTitle] = groupedTables[groupTitle] || {};
|
|
224
|
+
groupedTables[groupTitle][column] = true;
|
|
212
225
|
}
|
|
213
|
-
groupedTables[groupTitle] = groupedTables[groupTitle] || {};
|
|
214
|
-
groupedTables[groupTitle][column] = true;
|
|
215
226
|
}
|
|
216
227
|
}
|
|
217
228
|
}
|
|
@@ -4,7 +4,7 @@ import { SocketFunction } from "socket-function/SocketFunction";
|
|
|
4
4
|
import { getSourceVSCodeLink, qreact } from "../../4-dom/qreact";
|
|
5
5
|
import { assertIsManagementUser } from "../managementPages";
|
|
6
6
|
import { LogFile, LogObj, getLogBuffer, getLogFiles, parseLogBuffer } from "./diskLogger";
|
|
7
|
-
import { t } from "../../4-querysub/Querysub";
|
|
7
|
+
import { Querysub, t } from "../../4-querysub/Querysub";
|
|
8
8
|
import { URLParam } from "../../library-components/URLParam";
|
|
9
9
|
import { getSyncedController } from "../../library-components/SyncedController";
|
|
10
10
|
import { getBrowserUrlNode } from "../../-f-node-discovery/NodeDiscovery";
|
|
@@ -16,7 +16,7 @@ import { DropdownSelector } from "../../library-components/DropdownSelector";
|
|
|
16
16
|
import { Table, TableType } from "../../5-diagnostics/Table";
|
|
17
17
|
import { LogType } from "../errorLogs/ErrorLogCore";
|
|
18
18
|
import { canHaveChildren } from "socket-function/src/types";
|
|
19
|
-
import { measureBlock } from "socket-function/src/profiling/measure";
|
|
19
|
+
import { measureBlock, measureWrap } from "socket-function/src/profiling/measure";
|
|
20
20
|
import { binarySearchBasic, list, sort, timeInDay, timeInHour } from "socket-function/src/misc";
|
|
21
21
|
import { NodeViewerController, nodeViewerController } from "../NodeViewer";
|
|
22
22
|
import { red } from "socket-function/src/formatting/logColors";
|
|
@@ -50,6 +50,10 @@ import { Zip } from "../../zip";
|
|
|
50
50
|
// - Probably by injecting ids? Although how would they be persistent? Hmm... Maybe line numbers
|
|
51
51
|
// is fine? I think we map the typescript (I hope we do), so they should be correct?
|
|
52
52
|
|
|
53
|
+
// TODO: Use a binary format, but faster parsing.
|
|
54
|
+
// - If we could batch it, it would probably be very fast. We ARE just dealing with KVPs though,
|
|
55
|
+
// so... maybe it wouldn't be that much faster...
|
|
56
|
+
|
|
53
57
|
module.hotreload = true;
|
|
54
58
|
module.hotreload = true;
|
|
55
59
|
|
|
@@ -66,7 +70,7 @@ let inspectTimeURL = new URLParam("inspectTime", "");
|
|
|
66
70
|
|
|
67
71
|
export class DiskLoggerPage extends qreact.Component {
|
|
68
72
|
state = t.state({
|
|
69
|
-
|
|
73
|
+
downloadFilesHash: t.string,
|
|
70
74
|
});
|
|
71
75
|
render() {
|
|
72
76
|
let selectedNodesIds = selectedNodeId.value.split("|").filter(x => x);
|
|
@@ -75,8 +79,8 @@ export class DiskLoggerPage extends qreact.Component {
|
|
|
75
79
|
|
|
76
80
|
if (selectedNodesIds.length === 0) {
|
|
77
81
|
// Group by machineId
|
|
78
|
-
let grouped = new Map<string, string[]>();
|
|
79
|
-
for (let nodeId of nodeIds) {
|
|
82
|
+
let grouped = new Map<string, { nodeId: string; entryPoint: string }[]>();
|
|
83
|
+
for (let { nodeId, entryPoint } of nodeIds) {
|
|
80
84
|
let obj = decodeNodeId(nodeId);
|
|
81
85
|
if (!obj) continue;
|
|
82
86
|
let { machineId } = obj;
|
|
@@ -84,7 +88,7 @@ export class DiskLoggerPage extends qreact.Component {
|
|
|
84
88
|
if (!list) {
|
|
85
89
|
grouped.set(machineId, list = []);
|
|
86
90
|
}
|
|
87
|
-
list.push(nodeId);
|
|
91
|
+
list.push({ nodeId, entryPoint });
|
|
88
92
|
}
|
|
89
93
|
return (
|
|
90
94
|
<div class={css.pad2(10).vbox(10)}>
|
|
@@ -95,12 +99,12 @@ export class DiskLoggerPage extends qreact.Component {
|
|
|
95
99
|
<div class={css.vbox(10).wrap.maxWidth("30vw").pad2(5).hsla(0, 0, 0, 0.1)}>
|
|
96
100
|
<div class={css.fontSize(30)}>{getMachineId(machineId)}</div>
|
|
97
101
|
<div class={css.hbox(20, 5).wrap}>
|
|
98
|
-
{list.map(nodeId =>
|
|
102
|
+
{list.map(({ nodeId, entryPoint }) =>
|
|
99
103
|
<Anchor
|
|
100
104
|
class={css.hbox(10)}
|
|
101
105
|
values={[selectedNodeId.getOverride(nodeId)]}
|
|
102
106
|
>
|
|
103
|
-
{nodeId}
|
|
107
|
+
{nodeId} ({entryPoint.replaceAll("\\", "/").split("/").pop()})
|
|
104
108
|
</Anchor>
|
|
105
109
|
)}
|
|
106
110
|
</div>
|
|
@@ -136,17 +140,23 @@ export class DiskLoggerPage extends qreact.Component {
|
|
|
136
140
|
let firstTime = Number.MAX_SAFE_INTEGER;
|
|
137
141
|
let lastTime = Number.MIN_SAFE_INTEGER;
|
|
138
142
|
|
|
143
|
+
let filesHash = JSON.stringify(files.map(x => x.path));
|
|
144
|
+
|
|
139
145
|
for (let file of files) {
|
|
140
146
|
try {
|
|
141
147
|
if (file.startTime < firstTime) firstTime = file.startTime;
|
|
142
148
|
if (file.endTime > lastTime) lastTime = file.endTime;
|
|
149
|
+
if (filesHash !== this.state.downloadFilesHash) {
|
|
150
|
+
loadingCount++;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
143
153
|
let buffer = controller.getRemoteLogBuffer(file.nodeId, file.path);
|
|
144
|
-
if (buffer) {
|
|
145
|
-
buffer = Zip.gunzipSync(buffer);
|
|
146
|
-
buffers.push(buffer);
|
|
147
|
-
} else {
|
|
154
|
+
if (!buffer) {
|
|
148
155
|
loadingCount++;
|
|
156
|
+
continue;
|
|
149
157
|
}
|
|
158
|
+
buffer = unzipCached(buffer);
|
|
159
|
+
buffers.push(buffer);
|
|
150
160
|
} catch (e) {
|
|
151
161
|
console.log("Error reading buffer", file, e);
|
|
152
162
|
}
|
|
@@ -279,9 +289,8 @@ export class DiskLoggerPage extends qreact.Component {
|
|
|
279
289
|
picked={selectedNodesIds}
|
|
280
290
|
addPicked={x => selectedNodeId.value = [...selectedNodesIds, x].join("|")}
|
|
281
291
|
removePicked={x => selectedNodeId.value = selectedNodesIds.filter(y => y !== x).join("|")}
|
|
282
|
-
options={nodeIds.map(
|
|
292
|
+
options={nodeIds.map(({ nodeId, entryPoint }) => ({ value: nodeId, label: `${nodeId} (${entryPoint.replaceAll("\\", "/").split("/").pop()})` }))}
|
|
283
293
|
/>
|
|
284
|
-
{loadingCount > 0 && <h1>Loading {loadingCount} files</h1>}
|
|
285
294
|
<div className={css.fontSize(28).boldStyle}>
|
|
286
295
|
{selectedNodesIds.length} nodes, {files.length} files, filtered logs {formatNumber(filteredCount)} / {formatNumber(totalCount)}
|
|
287
296
|
</div>
|
|
@@ -296,6 +305,13 @@ export class DiskLoggerPage extends qreact.Component {
|
|
|
296
305
|
)}
|
|
297
306
|
</div>
|
|
298
307
|
</ShowMore>
|
|
308
|
+
{filesHash !== this.state.downloadFilesHash && <div>
|
|
309
|
+
<button onClick={() => {
|
|
310
|
+
this.state.downloadFilesHash = filesHash;
|
|
311
|
+
}}>
|
|
312
|
+
Download Files
|
|
313
|
+
</button>
|
|
314
|
+
</div>}
|
|
299
315
|
{!inspectTime && <>
|
|
300
316
|
<TimeRangeSelector
|
|
301
317
|
start={startTimeURL} end={endTimeURL}
|
|
@@ -351,6 +367,7 @@ export class DiskLoggerPage extends qreact.Component {
|
|
|
351
367
|
number
|
|
352
368
|
/>
|
|
353
369
|
)}
|
|
370
|
+
{loadingCount > 0 && <h1>Loading {loadingCount} files</h1>}
|
|
354
371
|
<Table {...table} initialLimit={30} />
|
|
355
372
|
{!!inspectTime && (
|
|
356
373
|
<InputLabel
|
|
@@ -365,6 +382,8 @@ export class DiskLoggerPage extends qreact.Component {
|
|
|
365
382
|
}
|
|
366
383
|
}
|
|
367
384
|
|
|
385
|
+
const unzipCached = cacheLimited(100, Zip.gunzipSync);
|
|
386
|
+
|
|
368
387
|
|
|
369
388
|
let remainingCacheSize = 1024 * 1024 * 1024 * 2;
|
|
370
389
|
let logBufferCache = new Map<string, {
|
|
@@ -372,7 +391,8 @@ let logBufferCache = new Map<string, {
|
|
|
372
391
|
logs: LogObj[];
|
|
373
392
|
lastAccess: number;
|
|
374
393
|
}>();
|
|
375
|
-
|
|
394
|
+
|
|
395
|
+
const parseLogBufferCached = measureWrap(function parseLogBufferCached(hash: string, buffers: Buffer[]): LogObj[] {
|
|
376
396
|
let cached = logBufferCache.get(hash);
|
|
377
397
|
if (cached) {
|
|
378
398
|
cached.lastAccess = Date.now();
|
|
@@ -392,7 +412,7 @@ function parseLogBufferCached(hash: string, buffers: Buffer[]): LogObj[] {
|
|
|
392
412
|
lastAccess: Date.now()
|
|
393
413
|
});
|
|
394
414
|
return logs;
|
|
395
|
-
}
|
|
415
|
+
});
|
|
396
416
|
|
|
397
417
|
|
|
398
418
|
|
|
@@ -9,6 +9,7 @@ import { SocketFunction } from "socket-function/SocketFunction";
|
|
|
9
9
|
import { isNode } from "typesafecss";
|
|
10
10
|
import { logGitHashes } from "./logGitHashes";
|
|
11
11
|
import { Zip } from "../../zip";
|
|
12
|
+
import { formatNumber } from "socket-function/src/formatting/format";
|
|
12
13
|
|
|
13
14
|
if (isNode()) {
|
|
14
15
|
// Delayed setup, as we depend on diskLogger early, and we don't want to force high level
|
|
@@ -297,21 +298,31 @@ if (isNode()) {
|
|
|
297
298
|
}
|
|
298
299
|
});
|
|
299
300
|
// Wait a random time, so we hopefully don't synchronize with any other services on this machine
|
|
300
|
-
|
|
301
|
+
runInfinitePoll(timeInHour + (1 + Math.random()), async function compressOldLogs() {
|
|
301
302
|
let logFiles = await fs.promises.readdir(folder);
|
|
302
303
|
let compressTime = Date.now() - LOG_FILE_DURATION * 2;
|
|
304
|
+
let filesCompressed = 0;
|
|
303
305
|
for (let file of logFiles) {
|
|
304
306
|
if (!file.endsWith(".log")) continue;
|
|
307
|
+
if (filesCompressed === 0) {
|
|
308
|
+
console.log("Compressing old logs");
|
|
309
|
+
}
|
|
310
|
+
filesCompressed++;
|
|
305
311
|
let path = folder + file;
|
|
306
312
|
if (decodeLogFileName(path).endTime > compressTime) continue;
|
|
307
313
|
let basePath = path.split(".").slice(0, -1).join(".");
|
|
308
314
|
let buffer = await fs.promises.readFile(path);
|
|
315
|
+
let beforeSize = buffer.length;
|
|
309
316
|
buffer = await Zip.gzip(buffer);
|
|
317
|
+
console.log(`Compressed ${file} from ${formatNumber(beforeSize)}B to ${formatNumber(buffer.length)}B`);
|
|
310
318
|
let tempPath = basePath + Math.random() + ".temp";
|
|
311
319
|
await fs.promises.writeFile(tempPath, buffer);
|
|
312
320
|
await fs.promises.rename(tempPath, basePath + ".zip");
|
|
313
321
|
await fs.promises.unlink(path);
|
|
314
322
|
}
|
|
323
|
+
if (filesCompressed > 0) {
|
|
324
|
+
console.log(`Compressed ${filesCompressed} old log files`);
|
|
325
|
+
}
|
|
315
326
|
});
|
|
316
327
|
}
|
|
317
328
|
|