querysub 0.525.0 → 0.527.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/.claude/settings.local.json +2 -1
- package/appSecrets.ts +23 -0
- package/bin/storageserve.js +8 -0
- package/package.json +4 -3
- package/src/-b-authorities/cdnAuthority.ts +1 -1
- package/src/-b-authorities/cloudflareHelpers.ts +2 -45
- package/src/-b-authorities/dnsAuthority.ts +44 -185
- package/src/-c-identity/IdentityController.ts +4 -2
- package/src/-d-trust/NetworkTrust2.ts +1 -1
- package/src/-f-node-discovery/LatencyTracking.ts +18 -1
- package/src/-f-node-discovery/NodeDiscovery.ts +1 -1
- package/src/-h-path-value-serialize/PathValueSerializer.ts +35 -18
- package/src/0-path-value-core/AuthorityLookup.ts +4 -2
- package/src/0-path-value-core/PathRouter.ts +32 -10
- package/src/1-path-client/RemoteWatcher.ts +46 -2
- package/src/4-querysub/FunctionRunnerTracking.ts +73 -3
- package/src/config.ts +5 -0
- package/src/diagnostics/managementPages.tsx +0 -6
- package/src/diagnostics/misc-pages/RoutingTablePage.tsx +39 -4
- package/src/diagnostics/periodic.ts +2 -1
- package/src/diagnostics/watchdog.ts +11 -0
- package/src/library-components/EdgeNodeSelector.tsx +2 -1
- package/src/library-components/LatencyGraph.tsx +27 -8
- package/src/server.ts +7 -0
- package/src/diagnostics/misc-pages/DNSPage.tsx +0 -354
|
@@ -5,7 +5,7 @@ import { PathValue } from "./pathValueCore";
|
|
|
5
5
|
import { shuffle } from "../misc/random";
|
|
6
6
|
import { fastHash } from "../misc/hash";
|
|
7
7
|
import { getOwnNodeId, isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
|
|
8
|
-
import {
|
|
8
|
+
import { getNodeLatencyMedian } from "../-f-node-discovery/LatencyTracking";
|
|
9
9
|
import { unique } from "../misc";
|
|
10
10
|
import { measureFnc } from "socket-function/src/profiling/measure";
|
|
11
11
|
import { getRoutingOverride, hasPrefixHash } from "./PathRouterRouteOverride";
|
|
@@ -22,15 +22,18 @@ export { LOCAL_DOMAIN, LOCAL_DOMAIN_PATH };
|
|
|
22
22
|
// fine (two nearby servers just talk to each other). Replaces the old uniform-random pick between candidates.
|
|
23
23
|
const LATENCY_CANDIDATE_LIMIT = 5;
|
|
24
24
|
const LATENCY_WEIGHT_OFFSET = 10;
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
// How many latency samples we use per node (median of the last N). The more we actually got, the more we trust the number, and the more aggressively we cut slow candidates.
|
|
26
|
+
const LATENCY_HISTORY_COUNT = 10;
|
|
27
|
+
// A candidate whose weight is below (the current max weight / (this - its historyUsed)) is dropped to zero probability. So with full history the factor is 2 (2x the effective latency of the best is never picked), and with a single sample it is 11 (only extreme outliers are cut).
|
|
28
|
+
const LATENCY_CUTOFF_BASE_FACTOR = 12;
|
|
28
29
|
// Latency assumed for a node we haven't measured yet, so new/unreached nodes stay eligible but aren't preferred.
|
|
29
30
|
const UNKNOWN_LATENCY_MS = 50;
|
|
30
31
|
|
|
31
|
-
function getRoutingLatency(nodeId: string): number {
|
|
32
|
-
if (isOwnNodeId(nodeId)) return 0;
|
|
33
|
-
|
|
32
|
+
function getRoutingLatency(nodeId: string): { latency: number; historyUsed: number } {
|
|
33
|
+
if (isOwnNodeId(nodeId)) return { latency: 0, historyUsed: LATENCY_HISTORY_COUNT };
|
|
34
|
+
let median = getNodeLatencyMedian({ nodeId, historyCount: LATENCY_HISTORY_COUNT });
|
|
35
|
+
if (!median) return { latency: UNKNOWN_LATENCY_MS, historyUsed: 0 };
|
|
36
|
+
return median;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
// In-place reorder of candidates by a latency-weighted random draw over the lowest LATENCY_CANDIDATE_LIMIT
|
|
@@ -39,16 +42,22 @@ function getRoutingLatency(nodeId: string): number {
|
|
|
39
42
|
// since an overloaded server's latency spikes, which already makes it less likely to be chosen here.
|
|
40
43
|
function latencyWeightedShuffle<T extends { nodeId: string }>(arr: T[]) {
|
|
41
44
|
if (arr.length <= 1) return;
|
|
42
|
-
let withLatency = arr.map(x =>
|
|
45
|
+
let withLatency = arr.map(x => {
|
|
46
|
+
let routingLatency = getRoutingLatency(x.nodeId);
|
|
47
|
+
return { x, latency: routingLatency.latency, historyUsed: routingLatency.historyUsed };
|
|
48
|
+
});
|
|
43
49
|
sort(withLatency, e => e.latency);
|
|
44
50
|
let pool = withLatency.slice(0, LATENCY_CANDIDATE_LIMIT);
|
|
45
51
|
let rest = withLatency.slice(LATENCY_CANDIDATE_LIMIT);
|
|
46
52
|
let ordered: T[] = [];
|
|
47
53
|
while (pool.length > 0) {
|
|
48
54
|
let weights = pool.map(e => 1 / (e.latency + LATENCY_WEIGHT_OFFSET));
|
|
49
|
-
let
|
|
55
|
+
let maxWeight = Math.max(...weights);
|
|
50
56
|
for (let i = 0; i < weights.length; i++) {
|
|
51
|
-
|
|
57
|
+
let cutoffFactor = LATENCY_CUTOFF_BASE_FACTOR - Math.min(pool[i].historyUsed, LATENCY_HISTORY_COUNT);
|
|
58
|
+
if (weights[i] < maxWeight / cutoffFactor) {
|
|
59
|
+
weights[i] = 0;
|
|
60
|
+
}
|
|
52
61
|
}
|
|
53
62
|
let total = 0;
|
|
54
63
|
for (let w of weights) {
|
|
@@ -90,6 +99,7 @@ export type AuthoritySpec = {
|
|
|
90
99
|
routeEnd: number;
|
|
91
100
|
// If the path.startsWith(prefix), but prefix !== path, then we hash getPathIndex(path, hashIndex)
|
|
92
101
|
// - For now, let's just never add overlapping prefixes.
|
|
102
|
+
// - Sorted by originalPrefix (authorityLookup.setOurSpec sorts before publishing), so specs can be compared element-wise.
|
|
93
103
|
prefixes: PrefixMatcher[];
|
|
94
104
|
// - Make sure to set this if you just want the prefix values, otherwise you will get all that that prefixes don't match (the prefixes exclude prefix match but where route does not match).
|
|
95
105
|
excludeDefault?: boolean;
|
|
@@ -162,6 +172,18 @@ export function parsePrefixMatcher(prefixPath: string): PrefixMatcher {
|
|
|
162
172
|
};
|
|
163
173
|
}
|
|
164
174
|
|
|
175
|
+
// Full-overlap equivalence: same range, same default matching, and the same prefixes (prefixes are sorted, so element-wise comparison works).
|
|
176
|
+
export function areAuthoritySpecsEquivalent(a: AuthoritySpec, b: AuthoritySpec): boolean {
|
|
177
|
+
if (a.routeStart !== b.routeStart) return false;
|
|
178
|
+
if (a.routeEnd !== b.routeEnd) return false;
|
|
179
|
+
if (!a.excludeDefault !== !b.excludeDefault) return false;
|
|
180
|
+
if (a.prefixes.length !== b.prefixes.length) return false;
|
|
181
|
+
for (let [i, prefix] of a.prefixes.entries()) {
|
|
182
|
+
if (prefix.originalPrefix !== b.prefixes[i].originalPrefix) return false;
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
165
187
|
export function debugSpec(spec: AuthoritySpec) {
|
|
166
188
|
return {
|
|
167
189
|
info: `${spec.routeStart}-${spec.routeEnd} (${spec.prefixes.length} prefixes${spec.excludeDefault ? " excluding default" : ""})`,
|
|
@@ -6,7 +6,9 @@ import { isNode, sort, timeInMinute, timeInSecond } from "socket-function/src/mi
|
|
|
6
6
|
import { measureFnc, measureBlock } from "socket-function/src/profiling/measure";
|
|
7
7
|
import { getOwnNodeId, isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
|
|
8
8
|
import { PathValueController } from "../0-path-value-core/PathValueController";
|
|
9
|
-
import { AuthoritySpec, PathRouter } from "../0-path-value-core/PathRouter";
|
|
9
|
+
import { AuthoritySpec, PathRouter, areAuthoritySpecsEquivalent } from "../0-path-value-core/PathRouter";
|
|
10
|
+
import { getNodeLatencyMedian } from "../-f-node-discovery/LatencyTracking";
|
|
11
|
+
import { formatTime } from "socket-function/src/formatting/format";
|
|
10
12
|
import { WatchConfig, authorityStorage } from "../0-path-value-core/pathValueCore";
|
|
11
13
|
import { pathWatcher } from "../0-path-value-core/PathWatcher";
|
|
12
14
|
import { ActionsHistory } from "../diagnostics/ActionsHistory";
|
|
@@ -29,9 +31,15 @@ setImmediate(() => import("../4-querysub/Querysub"));
|
|
|
29
31
|
const STOP_KEYS_DOUBLE_SENDS = true;
|
|
30
32
|
|
|
31
33
|
// How close to an authority's scheduled shutdown we start treating it like a disconnect (rehoming all the paths we watch on it).
|
|
32
|
-
const SHUTDOWN_REHOME_WINDOW = timeInMinute *
|
|
34
|
+
const SHUTDOWN_REHOME_WINDOW = timeInMinute * 3;
|
|
33
35
|
const SHUTDOWN_REHOME_POLL_INTERVAL = timeInSecond * 30;
|
|
34
36
|
|
|
37
|
+
// Rehome the watches on an authority when an equivalent authority has this factor lower latency. At 2 the router's full-confidence cutoff factor (also 2) then reliably routes everything to the better node on the rewatch.
|
|
38
|
+
const LATENCY_REHOME_FACTOR = 2;
|
|
39
|
+
// Both latencies must be medians over this many samples — with fewer we don't trust either number enough to move watches over it.
|
|
40
|
+
const LATENCY_REHOME_HISTORY_COUNT = 10;
|
|
41
|
+
const LATENCY_REHOME_POLL_INTERVAL = timeInMinute;
|
|
42
|
+
|
|
35
43
|
// NOTE: If a parent watch is broken up between multiple nodes, we generally watch everything on all those nodes and then filter when we receive the data. This isn't efficient, and we should probably change it. However, in practice, it's probably fine, as it's unlikely for the watches to be broken up to a much finer granularity than the network (it would require very strange partially overlapping sharding cases which no reasonable sharding setup would ever satisfy).
|
|
36
44
|
export class RemoteWatcher {
|
|
37
45
|
public static DEBUG = false;
|
|
@@ -152,6 +160,40 @@ export class RemoteWatcher {
|
|
|
152
160
|
}
|
|
153
161
|
}
|
|
154
162
|
}
|
|
163
|
+
|
|
164
|
+
private latencyRehomeLoop = runInfinitePoll(LATENCY_REHOME_POLL_INTERVAL, () => this.rehomeHighLatencyAuthorities());
|
|
165
|
+
private rehomeHighLatencyAuthorities() {
|
|
166
|
+
let authorityIds = new Set<string>(this.remoteWatchPaths.values());
|
|
167
|
+
for (let watchObj of this.remoteWatchParents2.values()) {
|
|
168
|
+
for (let range of watchObj.ranges) {
|
|
169
|
+
authorityIds.add(range.authorityId);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Also guards getTopologySync — if we watch anything remotely, the authority lookup has synced.
|
|
173
|
+
if (authorityIds.size === 0) return;
|
|
174
|
+
let topology = authorityLookup.getTopologySync();
|
|
175
|
+
for (let authorityId of authorityIds) {
|
|
176
|
+
if (isOwnNodeId(authorityId)) continue;
|
|
177
|
+
let entry = topology.find(candidate => candidate.nodeId === authorityId);
|
|
178
|
+
if (!entry) continue;
|
|
179
|
+
let current = getNodeLatencyMedian({ nodeId: authorityId, historyCount: LATENCY_REHOME_HISTORY_COUNT });
|
|
180
|
+
if (!current || current.historyUsed < LATENCY_REHOME_HISTORY_COUNT) continue;
|
|
181
|
+
for (let candidate of topology) {
|
|
182
|
+
if (candidate.nodeId === authorityId) continue;
|
|
183
|
+
if (isOwnNodeId(candidate.nodeId)) continue;
|
|
184
|
+
if (!candidate.isReady) continue;
|
|
185
|
+
if (authorityLookup.nodeIsShuttingDownSoon(candidate.nodeId)) continue;
|
|
186
|
+
if (!areAuthoritySpecsEquivalent(entry.authoritySpec, candidate.authoritySpec)) continue;
|
|
187
|
+
let candidateLatency = getNodeLatencyMedian({ nodeId: candidate.nodeId, historyCount: LATENCY_REHOME_HISTORY_COUNT });
|
|
188
|
+
if (!candidateLatency || candidateLatency.historyUsed < LATENCY_REHOME_HISTORY_COUNT) continue;
|
|
189
|
+
if (candidateLatency.latency * LATENCY_REHOME_FACTOR > current.latency) continue;
|
|
190
|
+
console.log(yellow(`Authority ${authorityId} has high latency (${formatTime(current.latency)}), and the equivalent authority ${candidate.nodeId} is much faster (${formatTime(candidateLatency.latency)}), so we are rehoming all paths watched on it`));
|
|
191
|
+
logErrors(this.refreshAllWatches(authorityId));
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
155
197
|
private async tryToReconnectNow() {
|
|
156
198
|
if (!this.disconnectedPaths.size && !this.disconnectedParents.size) return;
|
|
157
199
|
|
|
@@ -709,6 +751,8 @@ export class RemoteWatcher {
|
|
|
709
751
|
}
|
|
710
752
|
let paths = Array.from(pathsToWatch);
|
|
711
753
|
|
|
754
|
+
console.log(yellow(`Refreshing all watches on ${authorityNodeId}: ${pathsToWatch.size} paths, ${parentPathsToRewatch.size} parent paths (${parentRemotePathsToUnwatch.size} remote parent ranges)`));
|
|
755
|
+
|
|
712
756
|
logErrors(RemoteWatcher.REMOTE_UNWATCH_FUNCTION({
|
|
713
757
|
paths,
|
|
714
758
|
parentPaths: Array.from(parentRemotePathsToUnwatch)
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
8
8
|
import { timeInMinute, timeInSecond, sort } from "socket-function/src/misc";
|
|
9
9
|
import { lazy } from "socket-function/src/caching";
|
|
10
|
-
import { delay, runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
10
|
+
import { delay, runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
11
11
|
import { isClient } from "../config2";
|
|
12
12
|
import { t } from "../2-proxy/schema2";
|
|
13
13
|
import { createLocalSchema } from "./schemaHelpers";
|
|
14
14
|
import { Querysub } from "./Querysub";
|
|
15
15
|
import { getAllNodeIds, getOwnNodeId, watchDeltaNodeIds } from "../-f-node-discovery/NodeDiscovery";
|
|
16
|
-
import { getNodeLatencyInfo } from "../-f-node-discovery/LatencyTracking";
|
|
16
|
+
import { getNodeLatencyInfo, getNodeLatencyMedian } from "../-f-node-discovery/LatencyTracking";
|
|
17
17
|
import { FunctionRunnerInfoController, FunctionStatsSummary } from "../3-path-functions/PathFunctionRunner";
|
|
18
18
|
import { URLParam } from "../library-components/URLParam";
|
|
19
19
|
import { logErrors, timeoutToUndefined, timeoutToUndefinedSilent } from "../errors";
|
|
@@ -37,6 +37,12 @@ const CALL_TIME_WEIGHT_BASE = 1000;
|
|
|
37
37
|
// If every runner on a network will be shut down within this window, the network is dying and we switch off of it.
|
|
38
38
|
const NETWORK_DYING_WINDOW = timeInMinute * 2;
|
|
39
39
|
|
|
40
|
+
// Once selected we stick with a network — only switching when another network's best node has this factor lower latency than our network's best node (comparing medians, both with full history, so we never switch on noisy data).
|
|
41
|
+
const NETWORK_SWITCH_LATENCY_FACTOR = 2;
|
|
42
|
+
const NETWORK_SWITCH_HISTORY_COUNT = 10;
|
|
43
|
+
// The latency data is already collected by LatencyTracking, so checking is cheap and can run often.
|
|
44
|
+
const NETWORK_SWITCH_POLL_INTERVAL = timeInMinute;
|
|
45
|
+
|
|
40
46
|
// The forced network is a URL parameter, so it survives reloads and is shareable / obvious in the URL.
|
|
41
47
|
export const forcedNetworkURL = new URLParam("network", "");
|
|
42
48
|
|
|
@@ -306,11 +312,75 @@ export function getAutoSelectedNetwork(): string | undefined {
|
|
|
306
312
|
return candidates[0]?.network;
|
|
307
313
|
}
|
|
308
314
|
|
|
315
|
+
// The lowest full-history median latency among the network's runners (the best node is what we compare networks by).
|
|
316
|
+
function getNetworkBestNodeLatency(network: string): number | undefined {
|
|
317
|
+
let index = getFunctionRunnerIndex();
|
|
318
|
+
if (!index) return undefined;
|
|
319
|
+
let best: number | undefined;
|
|
320
|
+
for (let node of index.nodes) {
|
|
321
|
+
if (!node.networks.includes(network)) continue;
|
|
322
|
+
let median = getNodeLatencyMedian({ nodeId: node.nodeId, historyCount: NETWORK_SWITCH_HISTORY_COUNT });
|
|
323
|
+
if (!median || median.historyUsed < NETWORK_SWITCH_HISTORY_COUNT) continue;
|
|
324
|
+
if (best === undefined || median.latency < best) {
|
|
325
|
+
best = median.latency;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return best;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let stickyNetwork: string | undefined;
|
|
332
|
+
function checkNetworkSwitch() {
|
|
333
|
+
if (!stickyNetwork) return;
|
|
334
|
+
let currentLatency = getNetworkBestNodeLatency(stickyNetwork);
|
|
335
|
+
if (currentLatency === undefined) return;
|
|
336
|
+
|
|
337
|
+
let candidates = getNetworkSelectionInfos().filter(x => !x.dying && x.network !== stickyNetwork);
|
|
338
|
+
if (isPublic()) {
|
|
339
|
+
candidates = candidates.filter(x => x.isPublic);
|
|
340
|
+
}
|
|
341
|
+
let upLongEnough = candidates.filter(x => x.upLongEnough);
|
|
342
|
+
if (upLongEnough.length > 0) {
|
|
343
|
+
candidates = upLongEnough;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let bestNetwork: string | undefined;
|
|
347
|
+
let bestLatency: number | undefined;
|
|
348
|
+
for (let candidate of candidates) {
|
|
349
|
+
let latency = getNetworkBestNodeLatency(candidate.network);
|
|
350
|
+
if (latency === undefined) continue;
|
|
351
|
+
if (bestLatency === undefined || latency < bestLatency) {
|
|
352
|
+
bestLatency = latency;
|
|
353
|
+
bestNetwork = candidate.network;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (bestNetwork === undefined || bestLatency === undefined) return;
|
|
357
|
+
if (bestLatency * NETWORK_SWITCH_LATENCY_FACTOR > currentLatency) return;
|
|
358
|
+
|
|
359
|
+
console.log(yellow(`Switching the selected function network from ${stickyNetwork} (best node latency ${formatTime(currentLatency)}) to ${bestNetwork} (best node latency ${formatTime(bestLatency)}), so all new function calls use the faster network`));
|
|
360
|
+
stickyNetwork = bestNetwork;
|
|
361
|
+
}
|
|
362
|
+
const startNetworkSwitchPoll = lazy(() => {
|
|
363
|
+
logErrors(runInfinitePoll(NETWORK_SWITCH_POLL_INTERVAL, checkNetworkSwitch));
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Sticky: the score-based pick only initializes (or replaces a dead/dying selection); after that only checkNetworkSwitch changes it, so calls don't bounce between networks on small score changes.
|
|
367
|
+
function getStickyNetwork(): string | undefined {
|
|
368
|
+
void startNetworkSwitchPoll();
|
|
369
|
+
if (stickyNetwork) {
|
|
370
|
+
let info = getNetworkSelectionInfos().find(x => x.network === stickyNetwork);
|
|
371
|
+
if (info && !info.dying && (!isPublic() || info.isPublic)) {
|
|
372
|
+
return stickyNetwork;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
stickyNetwork = getAutoSelectedNetwork();
|
|
376
|
+
return stickyNetwork;
|
|
377
|
+
}
|
|
378
|
+
|
|
309
379
|
/** Safe to call from any context. Returns the network new function calls should be put on. Throws if no network is available (calls without a network can never run), unless noThrow is set. */
|
|
310
380
|
export function getSelectedNetwork(): string;
|
|
311
381
|
export function getSelectedNetwork(config: { noThrow: boolean }): string | undefined;
|
|
312
382
|
export function getSelectedNetwork(config?: { noThrow?: boolean }): string | undefined {
|
|
313
|
-
let network = forcedNetworkURL.value ||
|
|
383
|
+
let network = forcedNetworkURL.value || getStickyNetwork();
|
|
314
384
|
if (!network && !config?.noThrow) {
|
|
315
385
|
throw new Error(`No function runner discovered during discovery, so this call cannot be given a network and would never run (we keep polling for function runners, every ${formatTime(POLL_INTERVAL)} when tracking, every ${formatTime(INDEX_POLL_INTERVAL)} for the client index)`);
|
|
316
386
|
}
|
package/src/config.ts
CHANGED
|
@@ -35,6 +35,7 @@ let yargObj = parseArgsFactory()
|
|
|
35
35
|
.option("slowdown", { type: "number", desc: "Delay all input data values by this amount of time, pretending like we didn't even receive it until this time is up." })
|
|
36
36
|
.option("network", { type: "array", desc: `The networks this node is on (pass multiple arguments to be on multiple, ex: --network test --network default). Authorities only satisfy paths on their networks ("default" if unset). Function calls are put on the first network in the list. If no FunctionRunner is on a call's network, the call will fail to run.` })
|
|
37
37
|
.option("networkfile", { type: "string", desc: `The same as --network, except the networks are read from the given file (one per line). Supports "~/" for the home directory. If the file doesn't exist, the process exits with an error.` })
|
|
38
|
+
.option("port", { type: "number", desc: "The storage port for `yarn storageserve`" })
|
|
38
39
|
.argv
|
|
39
40
|
;
|
|
40
41
|
|
|
@@ -51,6 +52,10 @@ export function expandHomePath(path: string): string {
|
|
|
51
52
|
return path;
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
export function getPort(): number | undefined {
|
|
56
|
+
return yargObj.port;
|
|
57
|
+
}
|
|
58
|
+
|
|
54
59
|
let networkFileNetworks = lazy((): string[] => {
|
|
55
60
|
if (!yargObj.networkfile) return [];
|
|
56
61
|
let path = expandHomePath(String(yargObj.networkfile));
|
|
@@ -128,12 +128,6 @@ export async function registerManagementPages2(config: {
|
|
|
128
128
|
controllerName: "SnapshotViewerController",
|
|
129
129
|
getModule: () => import("./misc-pages/SnapshotViewer"),
|
|
130
130
|
});
|
|
131
|
-
inputPages.push({
|
|
132
|
-
title: "DNS",
|
|
133
|
-
componentName: "DNSPage",
|
|
134
|
-
controllerName: "DNSPageController",
|
|
135
|
-
getModule: () => import("./misc-pages/DNSPage"),
|
|
136
|
-
});
|
|
137
131
|
inputPages.push({
|
|
138
132
|
title: "Fnc Capture",
|
|
139
133
|
componentName: "FunctionCapturePage",
|
|
@@ -36,7 +36,7 @@ const MACHINE_LATENCY_WIDTH_PX = 235;
|
|
|
36
36
|
|
|
37
37
|
// Clicking a machine node in the graph (or a row) sorts that machine's nodes to the top of the table.
|
|
38
38
|
const selectedMachineParam = new URLParam("rtSelectedMachine", "", { reset: [mainResets] });
|
|
39
|
-
const trafficWindowParam = new URLParam<number>("rtTrafficWindow", timeInMinute *
|
|
39
|
+
const trafficWindowParam = new URLParam<number>("rtTrafficWindow", timeInMinute * 5, { reset: [mainResets] });
|
|
40
40
|
|
|
41
41
|
// The per-node table columns, in order. `color` tints the function/path-value/querysub columns to match the graph;
|
|
42
42
|
// `perMachine` columns (the IP and machine id) only print on the first row of each machine group.
|
|
@@ -433,6 +433,35 @@ class NodeInfoTable extends qreact.Component<{
|
|
|
433
433
|
}
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
+
// Nodes that never reported their latencies — all we have is their nodeId (and possibly a DNS-resolved machine IP), so this is a much narrower table than NodeInfoTable, still grouped by machine.
|
|
437
|
+
class UnresponsiveNodesTable extends qreact.Component<{ nodeIds: string[]; machineIp: Map<string, string> }> {
|
|
438
|
+
render() {
|
|
439
|
+
let rows = this.props.nodeIds.map(nodeId => ({ nodeId, machineId: machineIdOf(nodeId) }));
|
|
440
|
+
let sep = String.fromCharCode(1);
|
|
441
|
+
sort(rows, row => `${row.machineId}${sep}${threadLabel(row.nodeId)}`);
|
|
442
|
+
return <div className={css.vbox(0).fillWidth.overflowAuto.bord2(0, 0, 85)}>
|
|
443
|
+
<div className={css.hbox(0).hsl(0, 0, 96).colorhsl(0, 0, 20).boldStyle}>
|
|
444
|
+
<div className={css.width(130).flexShrink0.pad2(6).ellipsis}>IP</div>
|
|
445
|
+
<div className={css.width(96).flexShrink0.pad2(6).ellipsis}>Machine</div>
|
|
446
|
+
<div className={css.width(130).flexShrink0.pad2(6).ellipsis}>Thread:Port</div>
|
|
447
|
+
<div className={css.width(520).flexShrink0.pad2(6).ellipsis}>Node Id</div>
|
|
448
|
+
</div>
|
|
449
|
+
{rows.map((row, i) => {
|
|
450
|
+
let firstOfMachine = i === 0 || rows[i - 1].machineId !== row.machineId;
|
|
451
|
+
return <div
|
|
452
|
+
className={css.hbox(0).fillWidth.hsl(0, 0, 99)
|
|
453
|
+
.borderTop(firstOfMachine ? "1px solid hsl(0, 0%, 80%)" : "1px solid hsl(0, 0%, 93%)")}
|
|
454
|
+
>
|
|
455
|
+
<div className={css.width(130).flexShrink0.pad2(6).ellipsis}>{firstOfMachine && this.props.machineIp.get(row.machineId) || ""}</div>
|
|
456
|
+
<div className={css.width(96).flexShrink0.pad2(6).ellipsis}>{firstOfMachine && row.machineId.slice(0, ID_CHARS) || ""}</div>
|
|
457
|
+
<div className={css.width(130).flexShrink0.pad2(6).ellipsis}>{threadLabel(row.nodeId)}</div>
|
|
458
|
+
<div className={css.width(520).flexShrink0.pad2(6).ellipsis.colorhsl(0, 0, 45)} title={row.nodeId}>{row.nodeId}</div>
|
|
459
|
+
</div>;
|
|
460
|
+
})}
|
|
461
|
+
</div>;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
436
465
|
class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: NodeAuthorityInfo[] }> {
|
|
437
466
|
render() {
|
|
438
467
|
let nodeIds = this.props.nodeIds;
|
|
@@ -609,7 +638,7 @@ class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: N
|
|
|
609
638
|
return {
|
|
610
639
|
id: machineId,
|
|
611
640
|
labelLines: machineLabelLines({ machineId, threads: allThreads, trafficMaps, ip: machineIp.get(machineId), networks: machineNetworks.get(machineId) || [] }),
|
|
612
|
-
weight: totals.
|
|
641
|
+
weight: totals.valuesSent + totals.valuesReceived,
|
|
613
642
|
};
|
|
614
643
|
});
|
|
615
644
|
let links: LatencyGraphLink[] = [];
|
|
@@ -620,16 +649,18 @@ class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: N
|
|
|
620
649
|
if (pv && (pv.fwd || pv.back)) {
|
|
621
650
|
extraLabel = { text: `↑${formatNumber(pv.fwd)}/s ↓${formatNumber(pv.back)}/s values`, color: PATHVALUE_COLOR };
|
|
622
651
|
}
|
|
623
|
-
links.push({ source, destination, latencyMs, weight: machinePairTraffic.get(key) || 0, extraLabel });
|
|
652
|
+
links.push({ source, destination, latencyMs, weight: machinePairTraffic.get(key) || 0, valueWeight: pv && pv.fwd + pv.back || 0, extraLabel });
|
|
624
653
|
}
|
|
625
654
|
|
|
626
655
|
// One row per thread node, grouped so a machine's threads sit together; the selected machine sorts to the top.
|
|
627
656
|
// Single composite key: selected-first, then machine, then thread — the low separator keeps segments ordered.
|
|
628
|
-
let rows = nodeIds.map(nodeId => buildNodeRow({ nodeId, runner: runnerByNode.get(nodeId), info: infoByNode.get(nodeId), traffic: trafficMaps.get(nodeId), machineIp, machineNetworks }));
|
|
657
|
+
let rows = nodeIds.filter(nodeId => respondedSet.has(nodeId)).map(nodeId => buildNodeRow({ nodeId, runner: runnerByNode.get(nodeId), info: infoByNode.get(nodeId), traffic: trafficMaps.get(nodeId), machineIp, machineNetworks }));
|
|
629
658
|
let selected = selectedMachineParam.value;
|
|
630
659
|
let sep = String.fromCharCode(1);
|
|
631
660
|
sort(rows, row => `${row.machineId === selected ? 0 : 1}${sep}${row.machineId}${sep}${row.threadPort}`);
|
|
632
661
|
|
|
662
|
+
let unresponsiveNodeIds = nodeIds.filter(nodeId => !respondedSet.has(nodeId));
|
|
663
|
+
|
|
633
664
|
return <div className={css.vbox(8).fillWidth}>
|
|
634
665
|
<div className={css.hbox(14)}>
|
|
635
666
|
<h2 className={css.margin(0)}>Latency Graph ({machineRespondedThreads.size} machines · {respondedSet.size}/{nodeIds.length} nodes reported)</h2>
|
|
@@ -656,6 +687,10 @@ class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: N
|
|
|
656
687
|
<div className={css.colorhsl(0, 0, 50)}>Click a node to sort its information to the top of the table below.</div>
|
|
657
688
|
<h2 className={css.margin(0)}>Nodes ({rows.length})</h2>
|
|
658
689
|
<NodeInfoTable rows={rows} selectedMachine={selected} machineColumns={machineColumns} nodeMachineLatency={nodeMachineLatency} />
|
|
690
|
+
{unresponsiveNodeIds.length > 0 && <>
|
|
691
|
+
<h2 className={css.margin(0)}>Unresponsive Nodes ({unresponsiveNodeIds.length})</h2>
|
|
692
|
+
<UnresponsiveNodesTable nodeIds={unresponsiveNodeIds} machineIp={machineIp} />
|
|
693
|
+
</>}
|
|
659
694
|
</div>;
|
|
660
695
|
}
|
|
661
696
|
}
|
|
@@ -116,6 +116,17 @@ function logProfileMeasuresTimingsNow() {
|
|
|
116
116
|
thresholdInTable: 0
|
|
117
117
|
});
|
|
118
118
|
};
|
|
119
|
+
export function logUnfiltered(depth = 2) {
|
|
120
|
+
let profile = measureObj.finish();
|
|
121
|
+
measureObj = startMeasure();
|
|
122
|
+
logMeasureTable(profile, {
|
|
123
|
+
name: `all logs at ${new Date().toLocaleString()}`,
|
|
124
|
+
mergeDepth: depth,
|
|
125
|
+
minTimeToLog: 0,
|
|
126
|
+
maxTableEntries: 10000000,
|
|
127
|
+
thresholdInTable: 0
|
|
128
|
+
});
|
|
129
|
+
};
|
|
119
130
|
|
|
120
131
|
|
|
121
132
|
registerPeriodic(logProfileMeasuresTimingsNow);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { css } from "typesafecss";
|
|
2
2
|
import { qreact } from "../4-dom/qreact";
|
|
3
3
|
import { Querysub } from "../4-querysub/Querysub";
|
|
4
|
-
import { timeInSecond } from "socket-function/src/misc";
|
|
4
|
+
import { sort, timeInSecond } from "socket-function/src/misc";
|
|
5
5
|
import { formatTime } from "socket-function/src/formatting/format";
|
|
6
6
|
import { isCurrentUserSuperUser } from "../user-implementation/userData";
|
|
7
7
|
import type { EdgeNodeConfig, EdgeNodeStat } from "../4-deploy/edgeNodes";
|
|
@@ -43,6 +43,7 @@ export class EdgeNodeSelector extends qreact.Component<{}> {
|
|
|
43
43
|
nodes = [...nodes, { host: booted.host, nodeId: booted.nodeId, public: booted.public, live: true, finished: false }];
|
|
44
44
|
}
|
|
45
45
|
let autoStat = nodes.find(x => x.host === stats?.autoPickedHost);
|
|
46
|
+
sort(nodes, x => x.latency);
|
|
46
47
|
|
|
47
48
|
return <div className={css.hbox(6)}>
|
|
48
49
|
<span className={css.opacity(0.7)}>Edge</span>
|
|
@@ -9,7 +9,7 @@ import { formatTime } from "socket-function/src/formatting/format";
|
|
|
9
9
|
|
|
10
10
|
export type LatencyGraphLabelLine = { text: string; color?: string; };
|
|
11
11
|
export type LatencyGraphNode = { id: string; label?: string; latitude?: number; longitude?: number; labelLines?: LatencyGraphLabelLine[]; weight?: number; };
|
|
12
|
-
export type LatencyGraphLink = { source: string; destination: string; latencyMs: number; weight?: number; extraLabel?: LatencyGraphLabelLine; };
|
|
12
|
+
export type LatencyGraphLink = { source: string; destination: string; latencyMs: number; weight?: number; valueWeight?: number; extraLabel?: LatencyGraphLabelLine; };
|
|
13
13
|
export type LatencyGraphProps = {
|
|
14
14
|
nodes: LatencyGraphNode[];
|
|
15
15
|
links: LatencyGraphLink[];
|
|
@@ -59,7 +59,7 @@ const HIGHLIGHT_COLOR = "hsl(40, 90%, 60%)";
|
|
|
59
59
|
const NODE_WEIGHT_MULT = 2.5;
|
|
60
60
|
const LINE_BASE_WIDTH = 1.5;
|
|
61
61
|
const LINE_MIN_WIDTH = 1;
|
|
62
|
-
const LINE_MAX_WIDTH =
|
|
62
|
+
const LINE_MAX_WIDTH = 35;
|
|
63
63
|
const LABEL_LINE_HEIGHT = 13;
|
|
64
64
|
const ZOOM_STEP = 1.1;
|
|
65
65
|
const MIN_SCALE = 0.05;
|
|
@@ -84,6 +84,8 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
84
84
|
maxNodeWeight = 0;
|
|
85
85
|
pairWeight = new Map<number, number>();
|
|
86
86
|
maxPairWeight = 0;
|
|
87
|
+
pairValueWeight = new Map<number, number>();
|
|
88
|
+
maxPairValueWeight = 0;
|
|
87
89
|
pairExtraLabel = new Map<number, LatencyGraphLabelLine>();
|
|
88
90
|
// Cached from props in render() so draw() (a rAF callback, not synced) never reads this.props.
|
|
89
91
|
formatWeight: ((weight: number) => string) | undefined = undefined;
|
|
@@ -156,15 +158,23 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
156
158
|
}
|
|
157
159
|
this.pairWeight = new Map();
|
|
158
160
|
this.maxPairWeight = 0;
|
|
161
|
+
this.pairValueWeight = new Map();
|
|
162
|
+
this.maxPairValueWeight = 0;
|
|
159
163
|
for (let link of this.props.links) {
|
|
160
|
-
if (!link.weight) continue;
|
|
161
164
|
let a = index.get(link.source);
|
|
162
165
|
let b = index.get(link.destination);
|
|
163
166
|
if (a === undefined || b === undefined || a === b) continue;
|
|
164
167
|
let pk = Math.min(a, b) * n + Math.max(a, b);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
+
if (link.weight) {
|
|
169
|
+
let combined = (this.pairWeight.get(pk) || 0) + link.weight;
|
|
170
|
+
this.pairWeight.set(pk, combined);
|
|
171
|
+
this.maxPairWeight = Math.max(this.maxPairWeight, combined);
|
|
172
|
+
}
|
|
173
|
+
if (link.valueWeight) {
|
|
174
|
+
let combined = (this.pairValueWeight.get(pk) || 0) + link.valueWeight;
|
|
175
|
+
this.pairValueWeight.set(pk, combined);
|
|
176
|
+
this.maxPairValueWeight = Math.max(this.maxPairValueWeight, combined);
|
|
177
|
+
}
|
|
168
178
|
}
|
|
169
179
|
}
|
|
170
180
|
|
|
@@ -554,10 +564,19 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
554
564
|
this.drawHover(ctx, toScreenX, toScreenY);
|
|
555
565
|
}
|
|
556
566
|
|
|
567
|
+
// Opacity encodes the values sent back and forth on the connection (normalized to the busiest pair). Latency-based opacity is only the fallback when no link has value data.
|
|
568
|
+
edgeOpacity(a: number, b: number, latency: number) {
|
|
569
|
+
if (this.maxPairValueWeight <= 0) {
|
|
570
|
+
return this.opacityFor(latency, this.renderMin, this.renderMax);
|
|
571
|
+
}
|
|
572
|
+
let valueWeight = this.pairValueWeight.get(Math.min(a, b) * this.nodes.length + Math.max(a, b)) || 0;
|
|
573
|
+
return Math.max(MIN_OPACITY, valueWeight / this.maxPairValueWeight);
|
|
574
|
+
}
|
|
575
|
+
|
|
557
576
|
drawEdges(ctx: CanvasRenderingContext2D, toScreenX: (wx: number) => number, toScreenY: (wy: number) => number) {
|
|
558
577
|
for (let edge of this.renderEdges) {
|
|
559
578
|
ctx.lineWidth = this.lineWidthFor(edge.a, edge.b);
|
|
560
|
-
ctx.strokeStyle = `hsla(${NODE_HUE}, 70%, 62%, ${this.
|
|
579
|
+
ctx.strokeStyle = `hsla(${NODE_HUE}, 70%, 62%, ${this.edgeOpacity(edge.a, edge.b, edge.latency)})`;
|
|
561
580
|
ctx.beginPath();
|
|
562
581
|
ctx.moveTo(toScreenX(this.positionsX[edge.a]), toScreenY(this.positionsY[edge.a]));
|
|
563
582
|
ctx.lineTo(toScreenX(this.positionsX[edge.b]), toScreenY(this.positionsY[edge.b]));
|
|
@@ -836,7 +855,7 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
836
855
|
weightSig += node.weight || 0;
|
|
837
856
|
}
|
|
838
857
|
for (let link of this.props.links) {
|
|
839
|
-
weightSig += link.weight || 0;
|
|
858
|
+
weightSig += (link.weight || 0) + (link.valueWeight || 0);
|
|
840
859
|
}
|
|
841
860
|
if (sig !== this.builtSig || geoParam.value !== this.builtGeo) {
|
|
842
861
|
// Switching layout mode (geographic vs solved) changes the whole coordinate space, so re-fit the view.
|
package/src/server.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { disableMeasurements } from "socket-function/src/profiling/measure";
|
|
2
|
+
// NOTE: Profiling seems to make us use about twice as much memory, at least in the bad case where we have many tiny watchers.
|
|
3
|
+
if (typeof document === "undefined" && (process.argv.includes("--noprofile") || process.argv.includes("--nprofile"))) {
|
|
4
|
+
disableMeasurements();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
|
|
1
8
|
import "./forceProduction";
|
|
2
9
|
import "./inject";
|
|
3
10
|
|