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.
@@ -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
- const startTime = Date.now();
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
- const baseFunction = measureWrap(options.watchFunction, watcher.debugName);
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
- options.runImmediately || options.allowUnsyncedReads || (
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
- let args = parseArgs(callPath);
546
- overrideCurrentCall({ spec: callPath, fnc: syncedSpec, }, () => {
547
- baseFunction(...args);
548
- });
549
- evalTime += Date.now() - evalTimeStart;
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
- diskLog(`Finished FunctionRunner funciton`, { ...callPath, argsEncoded: "", functionSpec });
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(timeInMinute, async () => {
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", "Est % Usage", "Heap", "Buffers", "All Memory", "Blocking Lag", "port", "threadId", "machineId", "apiError", "live_entryPoint"],
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.set(column, group);
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 groupTitle = builtInGroupsLookup.get(column) || "Other";
210
- if (column.includes("|")) {
211
- groupTitle = column.split("|")[0];
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
- x: t.number
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(x => ({ value: x, label: x }))}
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
- function parseLogBufferCached(hash: string, buffers: Buffer[]): LogObj[] {
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
- void runInfinitePollCallAtStart(timeInHour + (1 + Math.random()), async function compressOldLogs() {
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