querysub 0.456.0 → 0.458.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.456.0",
3
+ "version": "0.458.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -516,8 +516,9 @@ export class ArchivesBackblaze {
516
516
  fnc: (api: B2Api) => Promise<T>,
517
517
  retries = 3
518
518
  ): Promise<T> {
519
- let api = await this.getBucketAPI();
519
+ let api: B2Api | undefined;
520
520
  try {
521
+ api = await this.getBucketAPI();
521
522
  return await fnc(api);
522
523
  } catch (err: any) {
523
524
  if (retries <= 0) throw err;
@@ -570,19 +571,21 @@ export class ArchivesBackblaze {
570
571
  }
571
572
 
572
573
  if (err.stack.includes(`getaddrinfo ENOTFOUND`)) {
573
- let urlObj = new URL(api.apiUrl);
574
- let hostname = urlObj.hostname;
575
- let lookupAddresses = await new Promise(resolve => {
576
- dns.lookup(hostname, (err, addresses) => {
577
- resolve(addresses);
574
+ if (api) {
575
+ let urlObj = new URL(api.apiUrl);
576
+ let hostname = urlObj.hostname;
577
+ let lookupAddresses = await new Promise(resolve => {
578
+ dns.lookup(hostname, (err, addresses) => {
579
+ resolve(addresses);
580
+ });
578
581
  });
579
- });
580
- let resolveAddresses = await new Promise(resolve => {
581
- dns.resolve4(hostname, (err, addresses) => {
582
- resolve(addresses);
582
+ let resolveAddresses = await new Promise(resolve => {
583
+ dns.resolve4(hostname, (err, addresses) => {
584
+ resolve(addresses);
585
+ });
583
586
  });
584
- });
585
- console.error(`[${context}] getaddrinfo ENOTFOUND ${hostname}`, { lookupAddresses, resolveAddresses, apiUrl: api.apiUrl, fullError: err.stack });
587
+ console.error(`[${context}] getaddrinfo ENOTFOUND ${hostname}`, { lookupAddresses, resolveAddresses, apiUrl: api.apiUrl, fullError: err.stack });
588
+ }
586
589
  }
587
590
 
588
591
  // NOTE: The AI thought case that happens when we run out of retries, that's stupid. This obviously isn't the case. This is the case when it's a normal error, as in the file doesn't exist, we need to throw. We absolutely should not warn here. Warning here wouldn't be anything. It would just be saying, oh, we checked if a file and it didn't, which is normal, which is why we check if a file exists.
@@ -997,23 +997,23 @@ class PathValueSerializer {
997
997
  public getPathValue(pathValue: PathValue | undefined, noMutate?: "noMutate"): unknown {
998
998
  if (!pathValue) return undefined;
999
999
  if (pathValue.isValueLazy) {
1000
- // NOTE: If this throws, it likely means you used atomicObjectWrite.
1001
- // Use atomicObjectWriteNoFreeze or doAtomicWrites instead.
1002
- if (!noMutate) {
1003
- try {
1004
- pathValue.isValueLazy = false;
1005
- } catch { }
1006
- }
1007
1000
  let buffer = this.lazyValues.get(pathValue.value as {});
1008
1001
  if (!buffer) {
1009
- console.error(`Expected lazy value to have a buffer, but it didn't. Lazy ref has a typeof ${typeof pathValue.value} (${String(pathValue.value)})`);
1010
- return pathValue.value;
1002
+ throw new Error(`Expected lazy value to have a buffer, but it didn't. Lazy ref has a typeof ${typeof pathValue.value} (${String(pathValue.value)})`);
1011
1003
  }
1012
1004
  let newValue = recursiveFreeze(cbor.decode(buffer));
1013
- if (!noMutate) {
1005
+ if (!noMutate && !pathValue.isValueLazy) {
1006
+ // NOTE: If this throws, it likely means you used atomicObjectWrite.
1007
+ // Use atomicObjectWriteNoFreeze or doAtomicWrites instead.
1014
1008
  try {
1009
+ pathValue.isValueLazy = false;
1015
1010
  pathValue.value = newValue;
1016
- } catch { }
1011
+ } catch {
1012
+ // In theory, it could be that you can set the is value lazy property but not the value property. In which case we want to reset the isValazy property back. Otherwise, we will no longer try to deserialize it, and we will emit the lazy placeholder instead of the actual value next time.
1013
+ try {
1014
+ pathValue.isValueLazy = true;
1015
+ } catch { }
1016
+ }
1017
1017
  }
1018
1018
  return newValue;
1019
1019
  }
@@ -1027,8 +1027,7 @@ class PathValueSerializer {
1027
1027
  // NOTE: Did you pass a raw PathValue and then try to use PathValueSerializer with it?
1028
1028
  // - Instead you should pass a buffer serialized with pathValueSerializer.serialize and
1029
1029
  // deserialized with pathValueSerializer.deserialize.
1030
- console.error(`Expected lazy value to have a buffer, but it didn't. Lazy ref has a typeof ${typeof pathValue.value} (${String(pathValue.value)})`);
1031
- return pathValue.value;
1030
+ throw new Error(`Expected lazy value to have a buffer, but it didn't. Lazy ref has a typeof ${typeof pathValue.value} (${String(pathValue.value)})`);
1032
1031
  }
1033
1032
  return buffer;
1034
1033
  }
@@ -1036,8 +1035,11 @@ class PathValueSerializer {
1036
1035
  }
1037
1036
 
1038
1037
  private getBuffer(pathValue: PathValue): Buffer {
1039
- let buffer = pathValue.isValueLazy && this.lazyValues.get(pathValue.value as {});
1040
- if (buffer) return buffer;
1038
+ if (pathValue.isValueLazy) {
1039
+ let buffer = this.lazyValues.get(pathValue.value as {});
1040
+ if (!buffer) throw new Error(`Expected lazy value to have a buffer, but it didn't. Lazy ref has a typeof ${typeof pathValue.value} (${String(pathValue.value)})`);
1041
+ return buffer;
1042
+ }
1041
1043
  return cborEncoder().encode(pathValue.value);
1042
1044
  }
1043
1045
  public compareValuePaths(a: PathValue | undefined, b: PathValue | undefined) {
@@ -23,6 +23,9 @@ import { debugGetAllCallFactories } from "socket-function/src/nodeCache";
23
23
  import { delay } from "socket-function/src/batching";
24
24
  export { pathValueCommitter };
25
25
 
26
+ let pathValueSendCount = 0;
27
+ export function getPathValueSendCount() { return pathValueSendCount; }
28
+
26
29
  // ONLY returns non-canGCValues that are valid
27
30
  export type AuditSnapshotEntry = {
28
31
  path: string;
@@ -47,6 +50,7 @@ export class PathValueControllerBase {
47
50
  const { Querysub } = await import("../4-querysub/Querysub");
48
51
  let { pathValues, nodeId } = config;
49
52
 
53
+ pathValueSendCount += pathValues.length;
50
54
  let serializedValues = await pathValueSerializer.serialize(pathValues, { compress: Querysub.COMPRESS_NETWORK });
51
55
  if (isDebugLogEnabled()) {
52
56
  for (let value of pathValues) {
@@ -70,6 +74,7 @@ export class PathValueControllerBase {
70
74
  nodeId: string;
71
75
  pathValues: PathValue[];
72
76
  initialTriggers?: { values: Set<string>; parentPaths: Set<string> },
77
+ reason: string;
73
78
  }) {
74
79
  let { nodeId, pathValues, initialTriggers } = config;
75
80
  if (isDebugLogEnabled()) {
@@ -88,6 +93,7 @@ export class PathValueControllerBase {
88
93
  initialTrigger: initialTriggers?.values.has(pathValue.path),
89
94
  triggerId: nextId(),
90
95
  totalChanges: pathValues.length,
96
+ reason: config.reason,
91
97
  });
92
98
  }
93
99
  }
@@ -96,14 +102,16 @@ export class PathValueControllerBase {
96
102
  nodeId: string;
97
103
  pathValues: PathValue[];
98
104
  initialTriggers?: { values: Set<string>; parentPaths: Set<string> },
105
+ reason: string;
99
106
  }) {
100
107
  let changes = config.pathValues;
101
108
  let { nodeId, initialTriggers } = config;
109
+ pathValueSendCount += changes.length;
102
110
  let buffers = await pathValueSerializer.serialize(changes, {
103
111
  noLocks: true,
104
112
  compress: getCompressNetwork(),
105
113
  });
106
- this.logSendValues({ nodeId, pathValues: changes, initialTriggers, });
114
+ this.logSendValues({ nodeId, pathValues: changes, initialTriggers, reason: config.reason });
107
115
  return await PathValueController.nodes[nodeId].sendData({
108
116
  valueBuffers: buffers,
109
117
  initialTriggers: initialTriggers,
@@ -201,6 +209,7 @@ export class PathValueControllerBase {
201
209
  await timeoutToError(timeInSecond * 15, PathValueControllerBase.sendValues({
202
210
  nodeId: otherAuthority,
203
211
  pathValues: values,
212
+ reason: "authorityShareValues",
204
213
  }), () => new Error(`Timeout forwarding shared values (${values.length} values) to authority ${otherAuthority}`));
205
214
  } catch (error: any) {
206
215
  console.error(error.message, { otherAuthority, count: values.length });
@@ -494,6 +494,7 @@ class PathWatcher {
494
494
  nodeId: watcher,
495
495
  pathValues: changes,
496
496
  initialTriggers: initialTriggers,
497
+ reason: "triggerLatestWatcher",
497
498
  });
498
499
  })());
499
500
  }
@@ -746,7 +746,7 @@ class AuthorityPathValueStorage {
746
746
 
747
747
  if (isDebugLogEnabled() && !config?.doNotArchive) {
748
748
  for (let value of newValues) {
749
- auditLog("INGEST VALUE", { path: value.path, timeId: value.time.time });
749
+ auditLog("INGEST VALUE", { path: value.path, timeId: value.time.time, batchTime: now });
750
750
  }
751
751
  }
752
752
 
@@ -26,6 +26,11 @@ import { getRoutingOverride, getRoutingOverridePart } from "../0-path-value-core
26
26
  import { PathRouter } from "../0-path-value-core/PathRouter";
27
27
  setImmediate(() => import("../4-querysub/Querysub"));
28
28
 
29
+ let functionCallOuterCount = 0;
30
+ let functionCallInnerCount = 0;
31
+ export function getFunctionCallOuterCount() { return functionCallOuterCount; }
32
+ export function getFunctionCallInnerCount() { return functionCallInnerCount; }
33
+
29
34
  export const functionSchema = rawSchema<{
30
35
  [domainName: string]: {
31
36
  PathFunctionRunner: {
@@ -520,6 +525,7 @@ export class PathFunctionRunner {
520
525
 
521
526
  stats.isInsideRunCall = true;
522
527
  stats.outerLoopCount++;
528
+ functionCallOuterCount++;
523
529
  try {
524
530
  await this.runCallBase(callSpec, functionSpec, stats);
525
531
  } finally {
@@ -647,6 +653,7 @@ export class PathFunctionRunner {
647
653
  runCount++;
648
654
  stats.lastInternalLoopCount = runCount;
649
655
  stats.totalInternalLoopCount++;
656
+ functionCallInnerCount++;
650
657
  if (PathFunctionRunner.DEBUG_CALLS) {
651
658
  console.log(`Evaluating (try count ${runCount}) ${debugNameColored}`);
652
659
  }
@@ -488,6 +488,7 @@ export class QuerysubControllerBase {
488
488
  values: new Set(undefinedValues.map(value => value.path)),
489
489
  parentPaths: new Set(newParentPathsNotAllowed)
490
490
  },
491
+ reason: "QuerysubController.watch disallowed paths (permissions)",
491
492
  }));
492
493
  }
493
494
 
@@ -0,0 +1,102 @@
1
+ // Gross cluster statistics — per-minute counters of high-level activity, kept for a
2
+ // week per-node and polled by the management UI. See managementPages.tsx GrossStatsInfo / GrossStatsPage.
3
+ //
4
+ // Sources of stats live in their own modules and just maintain monotonic counters; this file
5
+ // imports those counters and samples them once a minute, recording deltas against the
6
+ // previous sample. Sources never depend on this file.
7
+
8
+ import { SocketFunction } from "socket-function/SocketFunction";
9
+ import { runInfinitePoll } from "socket-function/src/batching";
10
+ import { timeInMinute } from "socket-function/src/misc";
11
+ import { isNode } from "typesafecss";
12
+ import { getFunctionCallInnerCount, getFunctionCallOuterCount } from "../../3-path-functions/PathFunctionRunner";
13
+ import { getPathValueSendCount } from "../../0-path-value-core/PathValueController";
14
+ import { getLogWriteCount } from "../../diagnostics/logs/diskLogger";
15
+
16
+ export const GROSS_STATS_FIELDS = [
17
+ "functionCallsInner",
18
+ "functionCallsOuter",
19
+ "pathValuesSent",
20
+ "socketCalls",
21
+ "socketUploadBytes",
22
+ "socketDownloadBytes",
23
+ "socketCallTimeMs",
24
+ "logsWritten",
25
+ ] as const;
26
+ export type GrossStatsField = typeof GROSS_STATS_FIELDS[number];
27
+
28
+ export type GrossStatsBucket = {
29
+ // Epoch ms of the END of the minute this bucket covers
30
+ time: number;
31
+ deltas: Record<GrossStatsField, number>;
32
+ };
33
+
34
+ const BUCKETS_PER_WEEK = 60 * 24 * 7;
35
+
36
+ function zeroCounts(): Record<GrossStatsField, number> {
37
+ let out = {} as Record<GrossStatsField, number>;
38
+ for (let field of GROSS_STATS_FIELDS) out[field] = 0;
39
+ return out;
40
+ }
41
+
42
+ // socketUploadBytes / socketDownloadBytes / socketCallTimeMs come from SocketFunction callbacks
43
+ // rather than a foreign module's counter, so we maintain monotonic running totals locally.
44
+ let socketTotals = { uploadBytes: 0, downloadBytes: 0, callTimeMs: 0 };
45
+ let socketHooksInstalled = false;
46
+ function installSocketHooks() {
47
+ if (socketHooksInstalled) return;
48
+ socketHooksInstalled = true;
49
+ SocketFunction.trackMessageSizes.upload.push(size => {
50
+ socketTotals.uploadBytes += size;
51
+ });
52
+ SocketFunction.trackMessageSizes.download.push(size => {
53
+ socketTotals.downloadBytes += size;
54
+ });
55
+ SocketFunction.trackMessageSizes.callTimes.push(({ start, end }) => {
56
+ socketTotals.callTimeMs += Math.max(0, end - start);
57
+ });
58
+ }
59
+
60
+ function readCurrentTotals(): Record<GrossStatsField, number> {
61
+ return {
62
+ functionCallsInner: getFunctionCallInnerCount(),
63
+ functionCallsOuter: getFunctionCallOuterCount(),
64
+ pathValuesSent: getPathValueSendCount(),
65
+ socketCalls: SocketFunction.TOTAL_CALLS,
66
+ socketUploadBytes: socketTotals.uploadBytes,
67
+ socketDownloadBytes: socketTotals.downloadBytes,
68
+ socketCallTimeMs: socketTotals.callTimeMs,
69
+ logsWritten: getLogWriteCount(),
70
+ };
71
+ }
72
+
73
+ let prevSnapshot = zeroCounts();
74
+ let buckets: GrossStatsBucket[] = [];
75
+
76
+ function sampleMinute() {
77
+ let snapshot = readCurrentTotals();
78
+ let deltas = {} as Record<GrossStatsField, number>;
79
+ for (let field of GROSS_STATS_FIELDS) {
80
+ deltas[field] = snapshot[field] - prevSnapshot[field];
81
+ }
82
+ prevSnapshot = snapshot;
83
+ buckets.push({ time: Date.now(), deltas });
84
+ if (buckets.length > BUCKETS_PER_WEEK) {
85
+ buckets.splice(0, buckets.length - BUCKETS_PER_WEEK);
86
+ }
87
+ }
88
+
89
+ if (isNode()) {
90
+ installSocketHooks();
91
+ prevSnapshot = readCurrentTotals();
92
+ void runInfinitePoll(timeInMinute, sampleMinute);
93
+ }
94
+
95
+ export function getGrossStatsBuckets(sinceTime?: number): GrossStatsBucket[] {
96
+ if (sinceTime === undefined) return buckets;
97
+ let i = 0;
98
+ while (i < buckets.length && buckets[i].time <= sinceTime) {
99
+ i++;
100
+ }
101
+ return i === 0 ? buckets : buckets.slice(i);
102
+ }
@@ -984,13 +984,7 @@ export async function machineApplyMain() {
984
984
  await Querysub.hostService("machine-apply");
985
985
  onServiceConfigChange(resyncServices);
986
986
 
987
- runInfinitePoll(DEBUGGER_WEDGE_POLL_INTERVAL, async () => {
988
- try {
989
- await unwedgeStuckDebuggerScreens();
990
- } catch (e: any) {
991
- console.error(`Error in debugger-wedge watcher: ${e.stack ?? e}`);
992
- }
993
- });
987
+ runInfinitePoll(DEBUGGER_WEDGE_POLL_INTERVAL, unwedgeStuckDebuggerScreens);
994
988
 
995
989
  runInfinitePoll(timeInMinute * 3, async () => {
996
990
  //console.log(magenta(`Quick outdated check at ${new Date().toISOString()}`));
@@ -184,7 +184,7 @@ export async function auditDiskValues(spec: AuthoritySpec) {
184
184
  if (valuesToSync.length > 0) {
185
185
  let valuesPerAuthority = PathRouter.getAllAuthoritiesForValues(valuesToSync);
186
186
  for (let [authorityNodeId, values] of valuesPerAuthority) {
187
- await PathValueControllerBase.sendValues({ nodeId: authorityNodeId, pathValues: values });
187
+ await PathValueControllerBase.sendValues({ nodeId: authorityNodeId, pathValues: values, reason: "auditDiskValues" });
188
188
  }
189
189
  }
190
190
  let timeSync = Date.now() - timeStartSync;
@@ -0,0 +1,111 @@
1
+ import { SocketFunction } from "socket-function/SocketFunction";
2
+ import { lazy } from "socket-function/src/caching";
3
+ import { runInfinitePollCallAtStart } from "socket-function/src/batching";
4
+ import { timeInHour, timeInMinute, timeInSecond } from "socket-function/src/misc";
5
+ import { assertIsManagementUser } from "../managementPages";
6
+ import {
7
+ getGrossStatsBuckets,
8
+ GrossStatsBucket,
9
+ GrossStatsField,
10
+ GROSS_STATS_FIELDS,
11
+ } from "../../5-diagnostics/gross-stats/grossStats";
12
+ import { getOwnNodeId, isOwnNodeId } from "../../-f-node-discovery/NodeDiscovery";
13
+ import { authorityLookup } from "../../0-path-value-core/AuthorityLookup";
14
+ import { timeoutToUndefinedSilent } from "../../errors";
15
+
16
+ const POLL_INTERVAL = timeInMinute;
17
+ const PER_NODE_TIMEOUT = 30 * timeInSecond;
18
+ const HEADER_WINDOW_MS = 60 * timeInMinute;
19
+ const RETENTION_MS = 7 * 24 * timeInHour;
20
+
21
+ let polledBuckets: Map<string, GrossStatsBucket[]> = new Map();
22
+
23
+ async function listClusterNodeIds(): Promise<string[]> {
24
+ let topology = await authorityLookup.getTopology();
25
+ let ids = new Set<string>();
26
+ for (let entry of topology) ids.add(entry.nodeId);
27
+ ids.add(getOwnNodeId());
28
+ return Array.from(ids);
29
+ }
30
+
31
+ async function pollOnce() {
32
+ let nodeIds = await listClusterNodeIds();
33
+ let cutoff = Date.now() - RETENTION_MS;
34
+ await Promise.all(nodeIds.map(async nodeId => {
35
+ if (isOwnNodeId(nodeId)) {
36
+ polledBuckets.set(nodeId, getGrossStatsBuckets().filter(b => b.time >= cutoff));
37
+ return;
38
+ }
39
+ let prev = polledBuckets.get(nodeId);
40
+ let sinceTime = prev && prev.length > 0 ? prev[prev.length - 1].time : undefined;
41
+
42
+ let result = await timeoutToUndefinedSilent(
43
+ PER_NODE_TIMEOUT,
44
+ GrossStatsController.nodes[nodeId].getGrossStatsLocal({ sinceTime }),
45
+ );
46
+ let merged = prev ? (result ? prev.concat(result.buckets) : prev) : (result?.buckets ?? []);
47
+ merged = merged.filter(b => b.time >= cutoff);
48
+ polledBuckets.set(nodeId, merged);
49
+ }));
50
+ }
51
+
52
+ let ensurePolling = lazy(() => {
53
+ void runInfinitePollCallAtStart(POLL_INTERVAL, pollOnce);
54
+ });
55
+
56
+ class GrossStatsControllerBase {
57
+ public async getGrossStatsLocal(config?: { sinceTime?: number }): Promise<{
58
+ nodeId: string;
59
+ buckets: GrossStatsBucket[];
60
+ }> {
61
+ return {
62
+ nodeId: getOwnNodeId(),
63
+ buckets: getGrossStatsBuckets(config?.sinceTime),
64
+ };
65
+ }
66
+
67
+ /** Trampoline: returns the latest polled buckets for every node in the cluster.
68
+ * Starts the background poller on first call. */
69
+ public async getGrossStats(): Promise<{
70
+ bucketsByNode: Map<string, GrossStatsBucket[]>;
71
+ }> {
72
+ ensurePolling();
73
+ return { bucketsByNode: new Map(polledBuckets) };
74
+ }
75
+
76
+ /** Trampoline: computes the cluster-wide per-second sum over the last hour from polled buckets.
77
+ * Cheap — at ~100 nodes × ~60 buckets/hour = ~6000 numbers per field. */
78
+ public async getGrossStatsHeaderSummary(): Promise<{
79
+ clusterSum: Record<GrossStatsField, number>;
80
+ }> {
81
+ ensurePolling();
82
+ let windowStart = Date.now() - HEADER_WINDOW_MS;
83
+ let windowSec = HEADER_WINDOW_MS / 1000;
84
+
85
+ let clusterSum = {} as Record<GrossStatsField, number>;
86
+ for (let f of GROSS_STATS_FIELDS) clusterSum[f] = 0;
87
+ for (let buckets of polledBuckets.values()) {
88
+ for (let bucket of buckets) {
89
+ if (bucket.time < windowStart) continue;
90
+ for (let f of GROSS_STATS_FIELDS) {
91
+ clusterSum[f] += bucket.deltas[f];
92
+ }
93
+ }
94
+ }
95
+ for (let f of GROSS_STATS_FIELDS) clusterSum[f] /= windowSec;
96
+ return { clusterSum };
97
+ }
98
+ }
99
+
100
+ export const GrossStatsController = SocketFunction.register(
101
+ "GrossStatsController-0193f8aa-7b91-7402-a4be-91a4b1f9c93d",
102
+ new GrossStatsControllerBase(),
103
+ () => ({
104
+ getGrossStatsLocal: { compress: true },
105
+ getGrossStats: { compress: true },
106
+ getGrossStatsHeaderSummary: {},
107
+ }),
108
+ () => ({
109
+ hooks: [assertIsManagementUser],
110
+ }),
111
+ );
@@ -0,0 +1,95 @@
1
+ import { qreact } from "../../4-dom/qreact";
2
+ import { css } from "../../4-dom/css";
3
+ import { formatNumber } from "socket-function/src/formatting/format";
4
+ import { SocketFunction } from "socket-function/SocketFunction";
5
+ import { timeInMinute } from "socket-function/src/misc";
6
+ import { isCurrentUserSuperUser } from "../../user-implementation/userData";
7
+ import { ATag } from "../../library-components/ATag";
8
+ import { managementPageURL, showingManagementURL } from "../managementPages";
9
+ import { getSyncedController } from "../../library-components/SyncedController";
10
+ import { GrossStatsController } from "./GrossStatsController";
11
+ import { GrossStatsField } from "../../5-diagnostics/gross-stats/grossStats";
12
+
13
+ module.hotreload = true;
14
+
15
+ const REFRESH_INTERVAL_MS = 5 * timeInMinute;
16
+
17
+ const HEADER_FIELDS: GrossStatsField[] = [
18
+ "functionCallsInner",
19
+ "pathValuesSent",
20
+ "socketCalls",
21
+ "socketUploadBytes",
22
+ "socketDownloadBytes",
23
+ "socketCallTimeMs",
24
+ "logsWritten",
25
+ ];
26
+ const FIELD_GLYPHS: Partial<Record<GrossStatsField, string>> = {
27
+ functionCallsInner: "⚡",
28
+ pathValuesSent: "📤",
29
+ socketCalls: "📞",
30
+ socketUploadBytes: "⬆",
31
+ socketDownloadBytes: "⬇",
32
+ socketCallTimeMs: "⏱",
33
+ logsWritten: "📝",
34
+ };
35
+ const FIELD_TITLES: Partial<Record<GrossStatsField, string>> = {
36
+ functionCallsInner: "Function calls per second, last hour",
37
+ pathValuesSent: "Path values sent per second, last hour",
38
+ socketCalls: "SocketFunction calls per second, last hour",
39
+ socketUploadBytes: "Upload bytes per second, last hour",
40
+ socketDownloadBytes: "Download bytes per second, last hour",
41
+ socketCallTimeMs: "SocketFunction call time ms per second, last hour",
42
+ logsWritten: "Log entries per second, last hour",
43
+ };
44
+
45
+ function formatField(field: GrossStatsField, value: number): string {
46
+ if (field === "socketUploadBytes" || field === "socketDownloadBytes") {
47
+ if (value >= 1024 * 1024) return `${(value / 1024 / 1024).toFixed(2)}MB`;
48
+ if (value >= 1024) return `${(value / 1024).toFixed(1)}KB`;
49
+ return `${value.toFixed(0)}B`;
50
+ }
51
+ if (field === "socketCallTimeMs") {
52
+ return `${value.toFixed(1)}ms`;
53
+ }
54
+ return formatNumber(value);
55
+ }
56
+
57
+ let grossStatsController = getSyncedController(GrossStatsController);
58
+
59
+ export class GrossStatsInfo extends qreact.Component {
60
+ refreshTimer: ReturnType<typeof setInterval> | undefined;
61
+ componentDidMount() {
62
+ this.refreshTimer = setInterval(() => {
63
+ grossStatsController.refreshAll();
64
+ }, REFRESH_INTERVAL_MS);
65
+ }
66
+ componentWillUnmount() {
67
+ if (this.refreshTimer) clearInterval(this.refreshTimer);
68
+ }
69
+ render() {
70
+ if (!isCurrentUserSuperUser()) return undefined;
71
+
72
+ let result = grossStatsController(SocketFunction.browserNodeId()).getGrossStatsHeaderSummary();
73
+ if (!result) return undefined;
74
+
75
+ let parts: preact.ComponentChild[] = [];
76
+ for (let field of HEADER_FIELDS) {
77
+ parts.push(
78
+ <span title={FIELD_TITLES[field]}>
79
+ {formatField(field, result.clusterSum[field])}{FIELD_GLYPHS[field]}
80
+ </span>
81
+ );
82
+ }
83
+
84
+ return <ATag
85
+ className={css.button.hbox(7).pad2(4)}
86
+ values={[
87
+ showingManagementURL.getOverride(true),
88
+ managementPageURL.getOverride("GrossStatsPage"),
89
+ ]}
90
+ title="Cluster activity per second over the past hour. Click for detail."
91
+ >
92
+ {parts}
93
+ </ATag>;
94
+ }
95
+ }