querysub 0.42.0 → 0.44.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";
@@ -31,6 +31,7 @@ import { decodeNodeId, encodeNodeId, getMachineId } from "../../-a-auth/certs";
31
31
  import { Button } from "../../library-components/Button";
32
32
  import { TimeRangeSelector } from "../../library-components/TimeRangeSelector";
33
33
  import { BrowserLargeFileCache, cacheCalls } from "./BrowserLargeFileCache";
34
+ import { Zip } from "../../zip";
34
35
 
35
36
  // TODO: Realtime log mode, by reading from the previous length forward, to add buffers
36
37
  // to what we already read.
@@ -49,6 +50,10 @@ import { BrowserLargeFileCache, cacheCalls } from "./BrowserLargeFileCache";
49
50
  // - Probably by injecting ids? Although how would they be persistent? Hmm... Maybe line numbers
50
51
  // is fine? I think we map the typescript (I hope we do), so they should be correct?
51
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
+
52
57
  module.hotreload = true;
53
58
  module.hotreload = true;
54
59
 
@@ -65,7 +70,7 @@ let inspectTimeURL = new URLParam("inspectTime", "");
65
70
 
66
71
  export class DiskLoggerPage extends qreact.Component {
67
72
  state = t.state({
68
- x: t.number
73
+ downloadFilesHash: t.string,
69
74
  });
70
75
  render() {
71
76
  let selectedNodesIds = selectedNodeId.value.split("|").filter(x => x);
@@ -74,8 +79,8 @@ export class DiskLoggerPage extends qreact.Component {
74
79
 
75
80
  if (selectedNodesIds.length === 0) {
76
81
  // Group by machineId
77
- let grouped = new Map<string, string[]>();
78
- for (let nodeId of nodeIds) {
82
+ let grouped = new Map<string, { nodeId: string; entryPoint: string }[]>();
83
+ for (let { nodeId, entryPoint } of nodeIds) {
79
84
  let obj = decodeNodeId(nodeId);
80
85
  if (!obj) continue;
81
86
  let { machineId } = obj;
@@ -83,7 +88,7 @@ export class DiskLoggerPage extends qreact.Component {
83
88
  if (!list) {
84
89
  grouped.set(machineId, list = []);
85
90
  }
86
- list.push(nodeId);
91
+ list.push({ nodeId, entryPoint });
87
92
  }
88
93
  return (
89
94
  <div class={css.pad2(10).vbox(10)}>
@@ -94,12 +99,12 @@ export class DiskLoggerPage extends qreact.Component {
94
99
  <div class={css.vbox(10).wrap.maxWidth("30vw").pad2(5).hsla(0, 0, 0, 0.1)}>
95
100
  <div class={css.fontSize(30)}>{getMachineId(machineId)}</div>
96
101
  <div class={css.hbox(20, 5).wrap}>
97
- {list.map(nodeId =>
102
+ {list.map(({ nodeId, entryPoint }) =>
98
103
  <Anchor
99
104
  class={css.hbox(10)}
100
105
  values={[selectedNodeId.getOverride(nodeId)]}
101
106
  >
102
- {nodeId}
107
+ {nodeId} ({entryPoint.replaceAll("\\", "/").split("/").pop()})
103
108
  </Anchor>
104
109
  )}
105
110
  </div>
@@ -135,16 +140,23 @@ export class DiskLoggerPage extends qreact.Component {
135
140
  let firstTime = Number.MAX_SAFE_INTEGER;
136
141
  let lastTime = Number.MIN_SAFE_INTEGER;
137
142
 
143
+ let filesHash = JSON.stringify(files.map(x => x.path));
144
+
138
145
  for (let file of files) {
139
146
  try {
140
147
  if (file.startTime < firstTime) firstTime = file.startTime;
141
148
  if (file.endTime > lastTime) lastTime = file.endTime;
149
+ if (filesHash !== this.state.downloadFilesHash) {
150
+ loadingCount++;
151
+ continue;
152
+ }
142
153
  let buffer = controller.getRemoteLogBuffer(file.nodeId, file.path);
143
- if (buffer) {
144
- buffers.push(buffer);
145
- } else {
154
+ if (!buffer) {
146
155
  loadingCount++;
156
+ continue;
147
157
  }
158
+ buffer = unzipCached(buffer);
159
+ buffers.push(buffer);
148
160
  } catch (e) {
149
161
  console.log("Error reading buffer", file, e);
150
162
  }
@@ -277,9 +289,8 @@ export class DiskLoggerPage extends qreact.Component {
277
289
  picked={selectedNodesIds}
278
290
  addPicked={x => selectedNodeId.value = [...selectedNodesIds, x].join("|")}
279
291
  removePicked={x => selectedNodeId.value = selectedNodesIds.filter(y => y !== x).join("|")}
280
- options={nodeIds.map(x => ({ value: x, label: x }))}
292
+ options={nodeIds.map(({ nodeId, entryPoint }) => ({ value: nodeId, label: `${nodeId} (${entryPoint.replaceAll("\\", "/").split("/").pop()})` }))}
281
293
  />
282
- {loadingCount > 0 && <h1>Loading {loadingCount} files</h1>}
283
294
  <div className={css.fontSize(28).boldStyle}>
284
295
  {selectedNodesIds.length} nodes, {files.length} files, filtered logs {formatNumber(filteredCount)} / {formatNumber(totalCount)}
285
296
  </div>
@@ -294,6 +305,13 @@ export class DiskLoggerPage extends qreact.Component {
294
305
  )}
295
306
  </div>
296
307
  </ShowMore>
308
+ {filesHash !== this.state.downloadFilesHash && <div>
309
+ <button onClick={() => {
310
+ this.state.downloadFilesHash = filesHash;
311
+ }}>
312
+ Download Files
313
+ </button>
314
+ </div>}
297
315
  {!inspectTime && <>
298
316
  <TimeRangeSelector
299
317
  start={startTimeURL} end={endTimeURL}
@@ -349,6 +367,7 @@ export class DiskLoggerPage extends qreact.Component {
349
367
  number
350
368
  />
351
369
  )}
370
+ {loadingCount > 0 && <h1>Loading {loadingCount} files</h1>}
352
371
  <Table {...table} initialLimit={30} />
353
372
  {!!inspectTime && (
354
373
  <InputLabel
@@ -363,6 +382,8 @@ export class DiskLoggerPage extends qreact.Component {
363
382
  }
364
383
  }
365
384
 
385
+ const unzipCached = cacheLimited(100, Zip.gunzipSync);
386
+
366
387
 
367
388
  let remainingCacheSize = 1024 * 1024 * 1024 * 2;
368
389
  let logBufferCache = new Map<string, {
@@ -370,7 +391,8 @@ let logBufferCache = new Map<string, {
370
391
  logs: LogObj[];
371
392
  lastAccess: number;
372
393
  }>();
373
- function parseLogBufferCached(hash: string, buffers: Buffer[]): LogObj[] {
394
+
395
+ const parseLogBufferCached = measureWrap(function parseLogBufferCached(hash: string, buffers: Buffer[]): LogObj[] {
374
396
  let cached = logBufferCache.get(hash);
375
397
  if (cached) {
376
398
  cached.lastAccess = Date.now();
@@ -390,7 +412,7 @@ function parseLogBufferCached(hash: string, buffers: Buffer[]): LogObj[] {
390
412
  lastAccess: Date.now()
391
413
  });
392
414
  return logs;
393
- }
415
+ });
394
416
 
395
417
 
396
418
 
@@ -542,7 +564,14 @@ class DiskLoggerControllerBase {
542
564
  return await getLogFiles();
543
565
  }
544
566
  public async getLogBuffer(path: string): Promise<Buffer | undefined> {
545
- return await getLogBuffer(path);
567
+ // Always compress it. If it is in progress it can't be compressed on disk, but
568
+ // the compression ratio is so high it's worth it to compress it dynamically now,
569
+ // to speed up the network transfer.
570
+ let buffer = await getLogBuffer(path);
571
+ if (buffer && path.endsWith(".log")) {
572
+ buffer = await Zip.gzip(buffer);
573
+ }
574
+ return buffer;
546
575
  }
547
576
  }
548
577
 
@@ -552,14 +581,11 @@ export const DiskLoggerController = SocketFunction.register(
552
581
  () => ({
553
582
  getRemoteLogFiles: {},
554
583
  getRemoteLogBuffer: {
555
- compress: true,
556
584
  clientHooks: [cacheCalls]
557
585
  },
558
586
 
559
587
  getLogFiles: {},
560
- getLogBuffer: {
561
- compress: true,
562
- },
588
+ getLogBuffer: {},
563
589
  }),
564
590
  () => ({
565
591
  hooks: [assertIsManagementUser],