querysub 0.522.0 → 0.523.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.522.0",
3
+ "version": "0.523.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",
@@ -2,9 +2,10 @@
2
2
  Per-server, in-memory traffic counters (never written to the database). Everything is just an integer increment
3
3
  into a 5-minute bucket; we keep the last 60 minutes (12 buckets) and drop older ones, so memory is bounded.
4
4
 
5
- Reads return PER-SECOND RATES, not totals. Each bucket also records the timestamp it first received data, so the
6
- rate window runs from the oldest retained bucket's first data up to now which stays accurate right after startup
7
- (when only a partial window of data exists) instead of dividing a small burst by the full 60-minute window.
5
+ Reads return PER-SECOND RATES: the window's total divided by how long data has ACTUALLY been collecting in that
6
+ window from the later of the window's start and when we first saw any data, up to now (so it's dynamic, and on a
7
+ server that just started it divides by the real short duration, not the full window). Under a second of data falls
8
+ back to the window length so we never divide by ~0.
8
9
 
9
10
  Byte traffic comes from SocketFunction.trackMessageSizes (upload/download per connection). The connection nodeId is
10
11
  sometimes a raw client-connection id, so we map it to the nice node id (debugNodeId) lazily at read time — the hot
@@ -19,10 +20,8 @@ import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
19
20
  import { getCachedNodeIds } from "./NodeDiscovery";
20
21
  import { debugNodeId } from "../-c-identity/IdentityController";
21
22
 
22
- const BUCKET_MS = timeInMinute * 5;
23
- const BUCKET_COUNT = 12;
24
- // Floor on the rate window so a burst in the first fraction of a second after startup can't divide out to an absurd rate.
25
- const MIN_WINDOW_SECONDS = 1;
23
+ const BUCKET_SIZE = timeInMinute * 5;
24
+ const BUCKET_HISTORY = BUCKET_SIZE * 20;
26
25
  // Sentinel key for traffic that isn't a network node (client connections, etc.). Aggregated so memory stays O(nodes),
27
26
  // not O(client connections) — clients churn through many raw ids per hour and we never want a bucket per client.
28
27
  const OUTSIDE_KEY = "";
@@ -39,16 +38,8 @@ export type TrafficStats = {
39
38
  outside: NodeDataTraffic;
40
39
  };
41
40
 
42
- // count = amount accumulated in the bucket; firstMs = when the bucket first received data (for the rate window).
43
- type Bucket = { count: number; firstMs: number; };
44
- type BucketMap = Map<number, Bucket>;
45
-
46
- function currentBucket() {
47
- return Math.floor(Date.now() / BUCKET_MS);
48
- }
49
- function oldestBucket() {
50
- return currentBucket() - BUCKET_COUNT + 1;
51
- }
41
+ // bucket index => amount accumulated in that bucket
42
+ type BucketMap = Map<number, number>;
52
43
 
53
44
  let functionsExecuted: BucketMap = new Map();
54
45
  let pathValuesSent: BucketMap = new Map();
@@ -59,13 +50,8 @@ let bytesSent = new Map<string, BucketMap>();
59
50
  let bytesReceived = new Map<string, BucketMap>();
60
51
 
61
52
  function addSimple(buckets: BucketMap, amount: number) {
62
- let bucket = currentBucket();
63
- let entry = buckets.get(bucket);
64
- if (!entry) {
65
- entry = { count: 0, firstMs: Date.now() };
66
- buckets.set(bucket, entry);
67
- }
68
- entry.count += amount;
53
+ let bucket = Math.floor(Date.now() / BUCKET_SIZE) * BUCKET_SIZE;
54
+ buckets.set(bucket, (buckets.get(bucket) || 0) + amount);
69
55
  }
70
56
  function addKeyed(map: Map<string, BucketMap>, key: string, amount: number) {
71
57
  let buckets = map.get(key);
@@ -99,26 +85,30 @@ function trafficKey(nodeId: string): string {
99
85
  SocketFunction.trackMessageSizes.upload.push((size, nodeId) => addKeyed(bytesSent, trafficKey(nodeId), size));
100
86
  SocketFunction.trackMessageSizes.download.push((size, nodeId) => addKeyed(bytesReceived, trafficKey(nodeId), size));
101
87
 
102
- // Per-second rate over the retained window: total accumulated / (now - oldest retained bucket's first-data time).
103
- // Also prunes expired buckets as it goes.
104
- function bucketRate(buckets: BucketMap): number {
105
- let oldest = oldestBucket();
88
+ // Per-second rate: total within the window divided by the actual span the matched buckets cover (oldest bucket's start
89
+ // time → now). Also prunes expired buckets as it goes.
90
+ function bucketRate(buckets: BucketMap, windowSize: number): number {
91
+ let now = Date.now();
92
+ let retentionOldest = now - BUCKET_HISTORY;
93
+ let targetTime = now - windowSize;
106
94
  let total = 0;
107
- let firstMs = Infinity;
108
- for (let [bucket, entry] of Array.from(buckets)) {
109
- if (bucket < oldest) {
95
+ let oldestTime = now;
96
+ for (let [bucket, count] of Array.from(buckets)) {
97
+ if (bucket < retentionOldest) {
110
98
  buckets.delete(bucket);
111
99
  continue;
112
100
  }
113
- total += entry.count;
114
- if (entry.firstMs < firstMs) firstMs = entry.firstMs;
101
+ if (bucket < targetTime) continue;
102
+ oldestTime = Math.min(oldestTime, bucket);
103
+ total += count;
104
+ }
105
+ if (oldestTime === now) {
106
+ oldestTime = targetTime;
115
107
  }
116
- if (total <= 0 || !Number.isFinite(firstMs)) return 0;
117
- let windowSeconds = Math.max(MIN_WINDOW_SECONDS, (Date.now() - firstMs) / 1000);
118
- return total / windowSeconds;
108
+ return total / ((now - oldestTime) / 1000);
119
109
  }
120
110
 
121
- function aggregateData(): { perNode: { [nodeId: string]: NodeDataTraffic }; outside: NodeDataTraffic; } {
111
+ function aggregateData(windowSize: number): { perNode: { [nodeId: string]: NodeDataTraffic }; outside: NodeDataTraffic; } {
122
112
  let known = new Set(getCachedNodeIds());
123
113
  let perNode: { [nodeId: string]: NodeDataTraffic } = {};
124
114
  let outside: NodeDataTraffic = { sent: 0, received: 0 };
@@ -143,31 +133,31 @@ function aggregateData(): { perNode: { [nodeId: string]: NodeDataTraffic }; outs
143
133
  }
144
134
  };
145
135
  for (let [rawNodeId, buckets] of Array.from(bytesSent)) {
146
- attribute(rawNodeId, "sent", bucketRate(buckets));
136
+ attribute(rawNodeId, "sent", bucketRate(buckets, windowSize));
147
137
  if (buckets.size === 0) bytesSent.delete(rawNodeId);
148
138
  }
149
139
  for (let [rawNodeId, buckets] of Array.from(bytesReceived)) {
150
- attribute(rawNodeId, "received", bucketRate(buckets));
140
+ attribute(rawNodeId, "received", bucketRate(buckets, windowSize));
151
141
  if (buckets.size === 0) bytesReceived.delete(rawNodeId);
152
142
  }
153
143
  return { perNode, outside };
154
144
  }
155
145
 
156
- export function getTrafficStats(): TrafficStats {
157
- let data = aggregateData();
146
+ export function getTrafficStats(windowSize: number): TrafficStats {
147
+ let data = aggregateData(windowSize);
158
148
  return {
159
- functionsExecuted: bucketRate(functionsExecuted),
160
- pathValuesSent: bucketRate(pathValuesSent),
161
- pathValuesReceived: bucketRate(pathValuesReceived),
162
- querysubCalls: bucketRate(querysubCalls),
149
+ functionsExecuted: bucketRate(functionsExecuted, windowSize),
150
+ pathValuesSent: bucketRate(pathValuesSent, windowSize),
151
+ pathValuesReceived: bucketRate(pathValuesReceived, windowSize),
152
+ querysubCalls: bucketRate(querysubCalls, windowSize),
163
153
  perNode: data.perNode,
164
154
  outside: data.outside,
165
155
  };
166
156
  }
167
157
 
168
158
  class TrafficControllerBase {
169
- public async getTrafficStats(): Promise<TrafficStats> {
170
- return getTrafficStats();
159
+ public async getTrafficStats(windowSize: number): Promise<TrafficStats> {
160
+ return getTrafficStats(windowSize);
171
161
  }
172
162
  }
173
163
  export const TrafficController = SocketFunction.register(
@@ -34,7 +34,7 @@ export async function startupAuthority(spec: AuthoritySpec) {
34
34
  pathValues: values,
35
35
  parentSyncs: [],
36
36
  initialTriggers: { values: new Set(), parentPaths: new Set() },
37
- doNotArchive: new Set(values),
37
+ doNotArchive: true,
38
38
  });
39
39
  console.log(blue(`Finished ingesting values from source ${source.nodeId} (values: ${formatNumber(values.length)}`), { nodeId: source.nodeId, spec: debugSpec(source), values: values.length });
40
40
  }
@@ -62,7 +62,7 @@ export async function startupAuthority(spec: AuthoritySpec) {
62
62
  let flat = Object.values(snapshot.values).flat();
63
63
  console.log(blue(`Loaded snapshot in memory, found ${formatNumber(flat.length)} values`), { spec: debugSpec(spec), values: flat.length });
64
64
  // NOTE: If it's on disk, it can't be rejected, so there's no point in passing it through our validStateComputer (and passing it through is VERY slow).
65
- authorityStorage.ingestValues(flat, { doNotArchive: new Set(flat) });
65
+ authorityStorage.ingestValues(flat, { doNotArchive: true });
66
66
  console.log(blue(`Finished ingesting values from snapshot, found ${formatNumber(flat.length)} values`), { spec: debugSpec(spec), values: flat.length });
67
67
  })();
68
68
 
@@ -308,7 +308,7 @@ function predictCallBase(config: {
308
308
  pathValues: predictions.writes,
309
309
  parentSyncs: [],
310
310
  initialTriggers: { values: new Set(), parentPaths: new Set() },
311
- doNotArchive: new Set(predictions.writes),
311
+ doNotArchive: true,
312
312
  });
313
313
 
314
314
  if (Querysub.DEBUG_PREDICTIONS) {
@@ -337,7 +337,7 @@ function predictCallBase(config: {
337
337
  pathValues: rejectedWrites,
338
338
  parentSyncs: [],
339
339
  initialTriggers: { values: new Set(), parentPaths: new Set() },
340
- doNotArchive: new Set(rejectedWrites),
340
+ doNotArchive: true,
341
341
  });
342
342
  }
343
343
 
@@ -14,7 +14,7 @@ import { getSyncedController } from "../../library-components/SyncedController";
14
14
  import { assertIsManagementUser } from "../managementPages";
15
15
  import { NodeCapabilitiesController } from "../../-g-core-values/NodeCapabilities";
16
16
  import { timeoutToUndefinedSilent } from "../../errors";
17
- import { sort } from "socket-function/src/misc";
17
+ import { sort, timeInMinute } from "socket-function/src/misc";
18
18
  import type { AuthoritySpec } from "../../0-path-value-core/PathRouter";
19
19
  import { getFunctionRunnerIndex, FunctionRunnerNodeInfo } from "../../4-querysub/FunctionRunnerTracking";
20
20
  import { formatTime, formatNumber } from "socket-function/src/formatting/format";
@@ -34,6 +34,7 @@ const LATENCY_GRAPH_HEIGHT_PX = 720;
34
34
 
35
35
  // Clicking a machine node in the graph (or a row) sorts that machine's nodes to the top of the table.
36
36
  const selectedMachineParam = new URLParam("rtSelectedMachine", "");
37
+ const trafficWindowParam = new URLParam<number>("rtTrafficWindow", timeInMinute * 60);
37
38
 
38
39
  // The per-node table columns, in order. `color` tints the function/path-value/querysub columns to match the graph;
39
40
  // `perMachine` columns (the IP and machine id) only print on the first row of each machine group.
@@ -106,9 +107,10 @@ class RoutingTablePageControllerBase {
106
107
  public async getNodeLatencies(nodeId: string): Promise<{ [nodeId: string]: number } | undefined> {
107
108
  return timeoutToUndefinedSilent(PROBE_TIMEOUT_MS, LatencyController.nodes[nodeId].getLatencies());
108
109
  }
109
- // Renamed (was getNodeTraffic) because the returned shape changed a new name busts any stale synced-call cache.
110
- public async getNodeTrafficStats(nodeId: string): Promise<TrafficStats | undefined> {
111
- return timeoutToUndefinedSilent(PROBE_TIMEOUT_MS, TrafficController.nodes[nodeId].getTrafficStats());
110
+ // Values are now per-second rates over the window (were near-totals). Renamed so a stale synced-call cache from the
111
+ // old meaning is dropped instead of being served forever.
112
+ public async getNodeTrafficRates(nodeId: string, windowSize: number): Promise<TrafficStats | undefined> {
113
+ return timeoutToUndefinedSilent(PROBE_TIMEOUT_MS, TrafficController.nodes[nodeId].getTrafficStats(windowSize));
112
114
  }
113
115
  }
114
116
 
@@ -118,7 +120,7 @@ export const RoutingTablePageController = SocketFunction.register(
118
120
  () => ({
119
121
  getAllNodeAuthoritySpecs: {},
120
122
  getNodeLatencies: {},
121
- getNodeTrafficStats: {},
123
+ getNodeTrafficRates: {},
122
124
  getNodeIps: {},
123
125
  }),
124
126
  () => ({
@@ -387,12 +389,13 @@ class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: N
387
389
  let infoByNode = new Map(this.props.infos.map(info => [info.nodeId, info]));
388
390
 
389
391
  // One synced call per node for each data source; each resolves independently so the graph fills in progressively.
392
+ let trafficWindow = trafficWindowParam.value;
390
393
  let latencyMaps = new Map<string, { [nodeId: string]: number }>();
391
394
  let trafficMaps = new Map<string, TrafficStats>();
392
395
  for (let nodeId of nodeIds) {
393
396
  let latencies = synced.getNodeLatencies(nodeId);
394
397
  if (latencies) latencyMaps.set(nodeId, latencies);
395
- let traffic = synced.getNodeTrafficStats(nodeId);
398
+ let traffic = synced.getNodeTrafficRates(nodeId, trafficWindow);
396
399
  if (traffic) trafficMaps.set(nodeId, traffic);
397
400
  }
398
401
  // Only graph nodes that reported their latencies — ones that never respond probably don't exist.
@@ -512,7 +515,19 @@ class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: N
512
515
  sort(rows, row => `${row.machineId === selected ? 0 : 1}${sep}${row.machineId}${sep}${row.threadPort}`);
513
516
 
514
517
  return <div className={css.vbox(8).fillWidth}>
515
- <h2 className={css.margin(0)}>Latency Graph ({machineRespondedThreads.size} machines · {respondedSet.size}/{nodeIds.length} nodes reported)</h2>
518
+ <div className={css.hbox(14)}>
519
+ <h2 className={css.margin(0)}>Latency Graph ({machineRespondedThreads.size} machines · {respondedSet.size}/{nodeIds.length} nodes reported)</h2>
520
+ <div className={css.hbox(0).bord2(0, 0, 70)}>
521
+ {([timeInMinute * 5, timeInMinute * 60]).map(windowSize => {
522
+ let selected = trafficWindow === windowSize;
523
+ return <div
524
+ className={css.pad2(12, 6).cursor("pointer")
525
+ .hsl(210, selected ? 70 : 0, selected ? 45 : 96).colorhsl(0, 0, selected ? 100 : 30)}
526
+ onMouseDown={() => trafficWindowParam.value = windowSize}
527
+ >{formatTime(windowSize)}</div>;
528
+ })}
529
+ </div>
530
+ </div>
516
531
  <div className={css.relative.fillWidth.height(LATENCY_GRAPH_HEIGHT_PX).bord2(0, 0, 85)}>
517
532
  <LatencyGraph
518
533
  nodes={nodes}
@@ -821,15 +821,20 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
821
821
  this.builtSig = sig;
822
822
  this.builtWeightSig = weightSig;
823
823
  this.build();
824
- } else if (weightSig !== this.builtWeightSig) {
825
- // Traffic weights changed but the node/link set didn't just refresh sizing, no re-layout.
826
- this.builtWeightSig = weightSig;
827
- this.computeWeights();
828
- } else if (latencyExponentParam.value !== this.builtLatencyExponent) {
829
- this.computeSolveEdges();
830
- this.applyLayout();
831
- } else if (Math.max(1, Math.floor(connectionsParam.value)) !== this.builtConnections) {
832
- this.computeRenderEdges();
824
+ } else {
825
+ // Same node/link counts (same order): refresh the node objects so label text that changed as data streamed
826
+ // in — or when the traffic window toggled — shows up, without disturbing the settled layout/positions.
827
+ this.nodes = this.props.nodes.slice();
828
+ if (weightSig !== this.builtWeightSig) {
829
+ this.builtWeightSig = weightSig;
830
+ this.computeWeights();
831
+ }
832
+ if (latencyExponentParam.value !== this.builtLatencyExponent) {
833
+ this.computeSolveEdges();
834
+ this.applyLayout();
835
+ } else if (Math.max(1, Math.floor(connectionsParam.value)) !== this.builtConnections) {
836
+ this.computeRenderEdges();
837
+ }
833
838
  }
834
839
  this.scheduleFrame();
835
840