querysub 0.588.0 → 0.590.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 +3 -3
- package/src/-a-archives/archiveCache.ts +9 -7
- package/src/-a-archives/archiveCache2.ts +153 -0
- package/src/-a-archives/archives.ts +4 -2
- package/src/-a-archives/archives2.ts +25 -9
- package/src/-a-archives/archivesJSONT.ts +20 -5
- package/src/-g-core-values/NodeCapabilities.ts +13 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +57 -19
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +17 -13
- package/src/0-path-value-core/pathValueArchives.ts +9 -9
- package/src/2-proxy/PathValueProxyWatcher.ts +6 -6
- package/src/4-dom/qreact.tsx +4 -4
- package/src/4-querysub/Querysub.ts +8 -1
- package/src/deployManager/components/RouteConfigView.tsx +10 -1
- package/src/deployManager/components/ServiceMeasureBars.tsx +169 -0
- package/src/deployManager/components/ServicesListPage.tsx +52 -2
- package/src/deployManager/components/StoragePage.tsx +43 -22
- package/src/diagnostics/logs/errorNotifications2/errorNotifications.ts +16 -47
- package/src/diagnostics/managementPages.tsx +3 -0
- package/src/diagnostics/misc-pages/SnapshotViewer.tsx +32 -9
- package/src/diagnostics/watchdog.ts +72 -1
- package/src/library-components/ATag.tsx +1 -0
- package/src/library-components/SyncedController.ts +8 -0
|
@@ -9,11 +9,12 @@ import { sort } from "socket-function/src/misc";
|
|
|
9
9
|
import { lazy } from "socket-function/src/caching";
|
|
10
10
|
import { decodeCborx, encodeCborx } from "../../misc/cloneHelpers";
|
|
11
11
|
import { runInParallel } from "socket-function/src/batching";
|
|
12
|
-
import { formatNumber } from "socket-function/src/formatting/format";
|
|
12
|
+
import { formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
13
|
+
import { getArchives2 } from "../../-a-archives/archives2";
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
// Snapshot is just a newline delimitted list of files. The name contains the metadata and time of it.
|
|
16
|
-
const snapshots = lazy(() =>
|
|
17
|
+
const snapshots = lazy(() => getArchives2("snapshots"));
|
|
17
18
|
|
|
18
19
|
export type ArchiveSnapshotOverview = {
|
|
19
20
|
// If live, it isn't a real file that can be loaded
|
|
@@ -89,23 +90,26 @@ function fileNameToOverview(fileName: string): ArchiveSnapshotOverview {
|
|
|
89
90
|
};
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
export type ArchiveSnapshotList = {
|
|
94
|
+
loadTime: number;
|
|
95
|
+
snapshots: ArchiveSnapshotOverview[];
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export async function getSavedSnapshotList(): Promise<ArchiveSnapshotList> {
|
|
99
|
+
let startTime = Date.now();
|
|
94
100
|
let snapshotFiles = await snapshots().find("");
|
|
101
|
+
let overview = snapshotFiles.map(file => fileNameToOverview(file));
|
|
102
|
+
sort(overview, x => -x.time);
|
|
103
|
+
return { loadTime: Date.now() - startTime, snapshots: overview };
|
|
104
|
+
}
|
|
95
105
|
|
|
96
|
-
|
|
106
|
+
export async function getLiveSnapshotList(): Promise<ArchiveSnapshotList> {
|
|
107
|
+
let startTime = Date.now();
|
|
97
108
|
let locker = await pathValueArchives.getArchiveLocker();
|
|
98
109
|
let allFiles = await locker.getAllValidFiles();
|
|
99
|
-
|
|
100
110
|
let liveOverview = getSnapshotOverview(allFiles.map(x => x.file));
|
|
101
111
|
liveOverview.file = "live";
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
overview.push(...snapshotFiles.map(file => fileNameToOverview(file)));
|
|
105
|
-
|
|
106
|
-
sort(overview, x => -x.time);
|
|
107
|
-
|
|
108
|
-
return overview;
|
|
112
|
+
return { loadTime: Date.now() - startTime, snapshots: [liveOverview] };
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
export async function getSnapshot(snapshotFile: string | "live"): Promise<ArchiveSnapshotRead> {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { blue, green, magenta, red, yellow } from "socket-function/src/formatting/logColors";
|
|
2
2
|
import { measureFnc } from "socket-function/src/profiling/measure";
|
|
3
3
|
import { ARCHIVE_FLUSH_LIMIT, ARCHIVE_FLUSH_LIMIT_START, compareTime, debugPathValuePath, getCompressNetwork, isCoreQuiet, PathValue, PathValueSnapshot, registerGetCompressNetwork, Time } from "./pathValueCore";
|
|
4
|
-
import {
|
|
4
|
+
import { getArchives2 } from "../-a-archives/archives2";
|
|
5
5
|
import { cache, cacheLimited, lazy } from "socket-function/src/caching";
|
|
6
6
|
import { getOwnNodeId, getOwnNodeIdAssert } from "../-f-node-discovery/NodeDiscovery";
|
|
7
7
|
import { pathValueSerializer } from "../-h-path-value-serialize/PathValueSerializer";
|
|
@@ -11,7 +11,7 @@ import { list } from "socket-function/src/misc";
|
|
|
11
11
|
import { NodeIdParts, decodeNodeId, decodeNodeIdAssert, encodeNodeId } from "sliftutils/misc/https/certs";
|
|
12
12
|
import { createArchiveLocker2 } from "./archiveLocks/ArchiveLocks2";
|
|
13
13
|
import { devDebugbreak, isNoNetwork, getDomain } from "../config";
|
|
14
|
-
import {
|
|
14
|
+
import { wrapArchivesWithCache2 } from "../-a-archives/archiveCache2";
|
|
15
15
|
import { AuthoritySpec, PathRouter, debugSpec } from "./PathRouter";
|
|
16
16
|
import { authorityLookup } from "./AuthorityLookup";
|
|
17
17
|
import { delay, retryFunctional } from "socket-function/src/batching";
|
|
@@ -21,10 +21,10 @@ import { shutdown } from "../diagnostics/periodic";
|
|
|
21
21
|
|
|
22
22
|
// Kept separate from the cache-wrapped `archives` so we can probe the underlying archives
|
|
23
23
|
// directly (without the cache layer in the way) when diagnosing missing-file failures.
|
|
24
|
-
const archivesBase = lazy(() =>
|
|
25
|
-
export const archives = lazy(() =>
|
|
26
|
-
export const archivesLocks = lazy(() =>
|
|
27
|
-
export const archivesRecycleBin = lazy(() =>
|
|
24
|
+
const archivesBase = lazy(() => getArchives2("path-values/"));
|
|
25
|
+
export const archives = lazy(() => wrapArchivesWithCache2(archivesBase()));
|
|
26
|
+
export const archivesLocks = lazy(() => getArchives2("path-values-locks/"));
|
|
27
|
+
export const archivesRecycleBin = lazy(() => wrapArchivesWithCache2(getArchives2("path-values-recycle-bin/")));
|
|
28
28
|
|
|
29
29
|
// If getInfo reports a just-written file as missing, recheck a few times before treating it
|
|
30
30
|
// as a fatal "written too slowly" condition.
|
|
@@ -314,7 +314,7 @@ export class PathValueArchives {
|
|
|
314
314
|
while (pendingDataPaths.length > 0) {
|
|
315
315
|
let dataPath = pendingDataPaths.pop()!;
|
|
316
316
|
if (readCache.has(dataPath)) continue;
|
|
317
|
-
let data = await archives().get(dataPath
|
|
317
|
+
let data = await archives().get(dataPath);
|
|
318
318
|
if (!data) continue;
|
|
319
319
|
readCache.set(dataPath, data);
|
|
320
320
|
}
|
|
@@ -469,7 +469,7 @@ export class PathValueArchives {
|
|
|
469
469
|
const BATCH_SIZE = 16;
|
|
470
470
|
for (let i = 0; i < dataPath.length; i += BATCH_SIZE) {
|
|
471
471
|
let cur = dataPath.slice(i, i + BATCH_SIZE);
|
|
472
|
-
let curResults = await Promise.all(cur.map(x => archives().get(x
|
|
472
|
+
let curResults = await Promise.all(cur.map(x => archives().get(x)));
|
|
473
473
|
results.push(...curResults);
|
|
474
474
|
}
|
|
475
475
|
if (config?.includeRecycleBin) {
|
|
@@ -483,7 +483,7 @@ export class PathValueArchives {
|
|
|
483
483
|
for (let i = 0; i < notFoundList.length; i += BATCH_SIZE) {
|
|
484
484
|
let curIndexes = notFoundList.slice(i, i + BATCH_SIZE);
|
|
485
485
|
let cur = curIndexes.map(x => dataPath[x]);
|
|
486
|
-
let curResults = await Promise.all(cur.map(x => archivesRecycleBin().get(x
|
|
486
|
+
let curResults = await Promise.all(cur.map(x => archivesRecycleBin().get(x)));
|
|
487
487
|
for (let j = 0; j < curResults.length; j++) {
|
|
488
488
|
results[curIndexes[j]] = curResults[j];
|
|
489
489
|
}
|
|
@@ -664,7 +664,7 @@ export class PathValueProxyWatcher {
|
|
|
664
664
|
return pathValue;
|
|
665
665
|
};
|
|
666
666
|
public getCallback = (pathStr: string, syncParentKeys?: "parentKeys", readTransparent?: "readTransparent"): { value: unknown } | undefined => {
|
|
667
|
-
if (PathValueProxyWatcher.BREAK_ON_READS.size > 0 && (proxyWatcher.isAllSynced() || this)) {
|
|
667
|
+
if (PathValueProxyWatcher.BREAK_ON_READS.size > 0 && (proxyWatcher.isAllSynced({ checkCommitAllRunsFlag: true }) || this)) {
|
|
668
668
|
// NOTE: We can't do a recursive match, as the parent paths include the
|
|
669
669
|
// root, which is constantly read, but not relevant.
|
|
670
670
|
if (PathValueProxyWatcher.BREAK_ON_READS.has(pathStr)) {
|
|
@@ -750,7 +750,7 @@ export class PathValueProxyWatcher {
|
|
|
750
750
|
};
|
|
751
751
|
|
|
752
752
|
public setCallback = (pathStr: string, value: unknown, inRecursion = false, allowSpecial = false): void => {
|
|
753
|
-
if (PathValueProxyWatcher.BREAK_ON_WRITES.size > 0 && proxyWatcher.isAllSynced()) {
|
|
753
|
+
if (PathValueProxyWatcher.BREAK_ON_WRITES.size > 0 && proxyWatcher.isAllSynced({ checkCommitAllRunsFlag: true })) {
|
|
754
754
|
if (isRecursiveMatch(PathValueProxyWatcher.BREAK_ON_WRITES, pathStr)) {
|
|
755
755
|
let unwatch = () => removeMatches(PathValueProxyWatcher.BREAK_ON_WRITES, pathStr);
|
|
756
756
|
debugger;
|
|
@@ -758,7 +758,7 @@ export class PathValueProxyWatcher {
|
|
|
758
758
|
}
|
|
759
759
|
}
|
|
760
760
|
|
|
761
|
-
if (PathValueProxyWatcher.SET_FUNCTION_WATCH_ON_WRITES.size > 0 && proxyWatcher.isAllSynced()) {
|
|
761
|
+
if (PathValueProxyWatcher.SET_FUNCTION_WATCH_ON_WRITES.size > 0 && proxyWatcher.isAllSynced({ checkCommitAllRunsFlag: true })) {
|
|
762
762
|
if (isRecursiveMatch(PathValueProxyWatcher.SET_FUNCTION_WATCH_ON_WRITES, pathStr)) {
|
|
763
763
|
let unwatch = () => removeMatches(PathValueProxyWatcher.SET_FUNCTION_WATCH_ON_WRITES, pathStr);
|
|
764
764
|
PathValueProxyWatcher.BREAK_ON_CALL.add(
|
|
@@ -767,7 +767,7 @@ export class PathValueProxyWatcher {
|
|
|
767
767
|
}
|
|
768
768
|
}
|
|
769
769
|
|
|
770
|
-
if (PathValueProxyWatcher.LOG_WRITES_INCLUDES.size > 0 && proxyWatcher.isAllSynced()) {
|
|
770
|
+
if (PathValueProxyWatcher.LOG_WRITES_INCLUDES.size > 0 && proxyWatcher.isAllSynced({ checkCommitAllRunsFlag: true })) {
|
|
771
771
|
if (isRecursiveMatch(PathValueProxyWatcher.LOG_WRITES_INCLUDES, pathStr)) {
|
|
772
772
|
let unwatch = () => removeMatches(PathValueProxyWatcher.LOG_WRITES_INCLUDES, pathStr);
|
|
773
773
|
console.log(`Write path "${pathStr}" = ${value}`);
|
|
@@ -2138,9 +2138,9 @@ export class PathValueProxyWatcher {
|
|
|
2138
2138
|
}
|
|
2139
2139
|
|
|
2140
2140
|
public isAllSynced(config?: {
|
|
2141
|
-
|
|
2141
|
+
checkCommitAllRunsFlag?: boolean;
|
|
2142
2142
|
}) {
|
|
2143
|
-
if (
|
|
2143
|
+
if (config?.checkCommitAllRunsFlag && this.runningWatcher?.options.commitAllRuns) {
|
|
2144
2144
|
return true;
|
|
2145
2145
|
}
|
|
2146
2146
|
return !this.getTriggeredWatcherMaybeUndefined()?.hasAnyUnsyncedAccesses();
|
package/src/4-dom/qreact.tsx
CHANGED
|
@@ -744,7 +744,7 @@ class QRenderClass {
|
|
|
744
744
|
} else {
|
|
745
745
|
// We check here, and not in the caller, so the props are always up to date,
|
|
746
746
|
// as the objects might be mutated or special (or have callback functions).
|
|
747
|
-
if (statics.jsonComparePropUpdates && Querysub.isAllSynced()) {
|
|
747
|
+
if (statics.jsonComparePropUpdates && Querysub.isAllSynced({ checkCommitAllRunsFlag: true })) {
|
|
748
748
|
|
|
749
749
|
let props = self.data().props;
|
|
750
750
|
|
|
@@ -809,7 +809,7 @@ class QRenderClass {
|
|
|
809
809
|
// ALSO, this stops infinite loops caused by self triggering, which is really useful.
|
|
810
810
|
if (!QRenderClass.areVNodesEqual(comparePrevVNode, vNode)) {
|
|
811
811
|
self.data().vNodeForRender = frozen;
|
|
812
|
-
if (Querysub.isAllSynced()) {
|
|
812
|
+
if (Querysub.isAllSynced({ checkCommitAllRunsFlag: true })) {
|
|
813
813
|
comparePrevVNode = frozen.value;
|
|
814
814
|
}
|
|
815
815
|
} else {
|
|
@@ -1886,7 +1886,7 @@ class QRenderClass {
|
|
|
1886
1886
|
export const __INTERNAL__QRenderClass = QRenderClass;
|
|
1887
1887
|
|
|
1888
1888
|
let defaultErrorHandler: ErrorHandler = ({ error, debugName }) => {
|
|
1889
|
-
if (Querysub.isAllSynced()) {
|
|
1889
|
+
if (Querysub.isAllSynced({ checkCommitAllRunsFlag: true })) {
|
|
1890
1890
|
console.error(`Render error in ${debugName}`, error);
|
|
1891
1891
|
// Throw, so we get a good callstack
|
|
1892
1892
|
setImmediate(() => { throw error; });
|
|
@@ -2673,7 +2673,7 @@ function watchUnsyncedComponents(): Set<ExternalRenderClass> {
|
|
|
2673
2673
|
return unsyncedComponents;
|
|
2674
2674
|
}
|
|
2675
2675
|
function componentRendered(component: QRenderClass) {
|
|
2676
|
-
if (component.disposed || Querysub.isAllSynced(
|
|
2676
|
+
if (component.disposed || Querysub.isAllSynced()) {
|
|
2677
2677
|
if (unsyncedComponents.has(component)) {
|
|
2678
2678
|
unsyncedComponents.delete(component);
|
|
2679
2679
|
triggerUnsyncedNow();
|
|
@@ -422,7 +422,7 @@ export class Querysub {
|
|
|
422
422
|
public static anyUnsynced() {
|
|
423
423
|
return !Querysub.allSynced();
|
|
424
424
|
}
|
|
425
|
-
public static allSynced(config?: {
|
|
425
|
+
public static allSynced(config?: { checkCommitAllRunsFlag?: boolean }) {
|
|
426
426
|
return proxyWatcher.isAllSynced(config);
|
|
427
427
|
}
|
|
428
428
|
public static fullySynced = Querysub.allSynced;
|
|
@@ -528,6 +528,13 @@ export class Querysub {
|
|
|
528
528
|
});
|
|
529
529
|
}
|
|
530
530
|
|
|
531
|
+
public static triggerOnPromiseFinish(promise: MaybePromise<unknown>, config: {
|
|
532
|
+
waitReason: string;
|
|
533
|
+
noWait?: boolean;
|
|
534
|
+
}) {
|
|
535
|
+
proxyWatcher.triggerOnPromiseFinish(promise, config);
|
|
536
|
+
}
|
|
537
|
+
|
|
531
538
|
/** Returns true if any predictions are running. In which case, we will correctly rerun the function and consider it non-synced until the predictions finish. This allows you to check for predictions of the in your function and return if you have any. This is useful for functions that need a stable state before they run. However, if many functions use this and you trigger them at once, it might result in an n-squared situation, so this should be used with caution.
|
|
532
539
|
* - The best use case is if something is triggering maybe on focus and on blur, and you want to make sure the on blur changes happen before your on focus changes happen.
|
|
533
540
|
*/
|
|
@@ -183,9 +183,18 @@ export function getRouteConfigGroups(servers: StorageServerBuckets[]): RouteConf
|
|
|
183
183
|
let byBucket = new Map<string, Map<string, ConfigVariant>>();
|
|
184
184
|
for (let server of servers) {
|
|
185
185
|
for (let bucket of server.buckets || []) {
|
|
186
|
+
// An inactive bucket is not serving anything, so whatever routing it happens to hold says nothing about where values actually come from - and letting it vote would put a stale config up against the live ones as a conflict
|
|
187
|
+
if (!bucket.active) continue;
|
|
186
188
|
let remoteConfig: RemoteConfig | undefined = bucket.config?.remoteConfig;
|
|
187
189
|
if (!remoteConfig) continue;
|
|
188
|
-
let sources
|
|
190
|
+
let sources: Source[];
|
|
191
|
+
try {
|
|
192
|
+
sources = remoteConfig.sources.map(normalizeSource);
|
|
193
|
+
} catch (e: any) {
|
|
194
|
+
// One server's unreadable config must not cost us the grouping for every other bucket - it simply does not get a say in the consensus
|
|
195
|
+
console.error(`Ignoring the routing config ${server.url} reports for ${bucket.bucketName}, which could not be read:`, e.stack ?? e);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
189
198
|
let variants = byBucket.get(bucket.bucketName);
|
|
190
199
|
if (!variants) {
|
|
191
200
|
variants = new Map();
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
|
+
import { qreact } from "../../4-dom/qreact";
|
|
3
|
+
import { css } from "typesafecss";
|
|
4
|
+
import { formatNumber, formatTime, formatVeryNiceDateTime } from "socket-function/src/formatting/format";
|
|
5
|
+
import { timeoutToUndefinedSilent, timeInSecond } from "socket-function/src/misc";
|
|
6
|
+
import { StatsValue, getStatsTop } from "socket-function/src/profiling/stats";
|
|
7
|
+
import type { MeasureChunk, MeasureSummary } from "../../diagnostics/watchdog";
|
|
8
|
+
import { NodeCapabilitiesController } from "../../-g-core-values/NodeCapabilities";
|
|
9
|
+
import { getSyncedController } from "../../library-components/SyncedController";
|
|
10
|
+
import { assertIsManagementUser } from "../../diagnostics/managementPages";
|
|
11
|
+
|
|
12
|
+
const NODE_CALL_TIMEOUT = timeInSecond * 10;
|
|
13
|
+
const BAR_HEIGHT_PX = 10;
|
|
14
|
+
const MIN_SEGMENT_WIDTH_PX = 30;
|
|
15
|
+
|
|
16
|
+
class ServiceMeasureControllerBase {
|
|
17
|
+
public async getMeasureSummaries(nodeIds: string[]): Promise<Record<string, MeasureSummary | undefined>> {
|
|
18
|
+
let result: Record<string, MeasureSummary | undefined> = {};
|
|
19
|
+
await Promise.all(nodeIds.map(async nodeId => {
|
|
20
|
+
result[nodeId] = await timeoutToUndefinedSilent(NODE_CALL_TIMEOUT, NodeCapabilitiesController.nodes[nodeId].getMeasureSummary());
|
|
21
|
+
}));
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
public async getMeasureBreakdown(nodeId: string, range: { startTime: number; endTime: number }): Promise<{ chunk?: MeasureChunk }> {
|
|
25
|
+
let chunk = await timeoutToUndefinedSilent(NODE_CALL_TIMEOUT, NodeCapabilitiesController.nodes[nodeId].getMeasureBreakdown(range));
|
|
26
|
+
return { chunk };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export const ServiceMeasureController = getSyncedController(SocketFunction.register(
|
|
30
|
+
"ServiceMeasureController-3f8c1de2-5b74-4e0f-a6c9-8d21f47b90a5",
|
|
31
|
+
new ServiceMeasureControllerBase(),
|
|
32
|
+
() => ({
|
|
33
|
+
getMeasureSummaries: { hooks: [assertIsManagementUser], compress: true },
|
|
34
|
+
getMeasureBreakdown: { hooks: [assertIsManagementUser], compress: true },
|
|
35
|
+
}),
|
|
36
|
+
() => ({
|
|
37
|
+
})
|
|
38
|
+
));
|
|
39
|
+
|
|
40
|
+
function percent(value: number) {
|
|
41
|
+
return `${(value * 100).toFixed(2)}%`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function nameHue(name: string) {
|
|
45
|
+
let hash = 0;
|
|
46
|
+
for (let i = 0; i < name.length; i++) {
|
|
47
|
+
hash = (hash * 31 + name.charCodeAt(i)) | 0;
|
|
48
|
+
}
|
|
49
|
+
return Math.abs(hash) % 360;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class ServiceMeasureBars extends qreact.Component<{
|
|
53
|
+
serviceNodeIds: string[];
|
|
54
|
+
/** Every node id on the page, so all rows share one cached call. */
|
|
55
|
+
allNodeIds: string[];
|
|
56
|
+
expandedNodeId: string;
|
|
57
|
+
onToggleNode: (nodeId: string) => void;
|
|
58
|
+
}> {
|
|
59
|
+
render() {
|
|
60
|
+
let controller = ServiceMeasureController(SocketFunction.browserNodeId());
|
|
61
|
+
let summaries = controller.getMeasureSummaries(this.props.allNodeIds);
|
|
62
|
+
if (!summaries) return undefined;
|
|
63
|
+
|
|
64
|
+
return <div className={css.vbox(2)}>
|
|
65
|
+
{this.props.serviceNodeIds.map(nodeId => {
|
|
66
|
+
let summary = summaries[nodeId];
|
|
67
|
+
if (!summary) return undefined;
|
|
68
|
+
let timeRunFor = summary.endTime - summary.startTime;
|
|
69
|
+
if (timeRunFor <= 0) return undefined;
|
|
70
|
+
let isExpanded = this.props.expandedNodeId === nodeId;
|
|
71
|
+
return <div key={nodeId}
|
|
72
|
+
className={
|
|
73
|
+
css.hbox(1).wrap.button
|
|
74
|
+
+ (isExpanded && css.outline("1px solid hsl(210, 60%, 50%)") || "")
|
|
75
|
+
}
|
|
76
|
+
onClick={() => this.props.onToggleNode(nodeId)}
|
|
77
|
+
>
|
|
78
|
+
{summary.entries.map(entry => {
|
|
79
|
+
let fraction = entry.ownTime / timeRunFor;
|
|
80
|
+
return <div key={entry.name}
|
|
81
|
+
className={css
|
|
82
|
+
.height(BAR_HEIGHT_PX)
|
|
83
|
+
.width(`${fraction * 100}%`)
|
|
84
|
+
.minWidth(MIN_SEGMENT_WIDTH_PX)
|
|
85
|
+
.background(`hsl(${nameHue(entry.name)}, 60%, 60%)`)
|
|
86
|
+
}
|
|
87
|
+
title={`${entry.name}\n${formatTime(entry.ownTime)} (${percent(fraction)} of ${formatTime(timeRunFor)} profiled)\n${nodeId}`}
|
|
88
|
+
/>;
|
|
89
|
+
})}
|
|
90
|
+
</div>;
|
|
91
|
+
})}
|
|
92
|
+
</div>;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class ServiceMeasureBreakdown extends qreact.Component<{
|
|
97
|
+
nodeId: string;
|
|
98
|
+
allNodeIds: string[];
|
|
99
|
+
}> {
|
|
100
|
+
render() {
|
|
101
|
+
let controller = ServiceMeasureController(SocketFunction.browserNodeId());
|
|
102
|
+
let summaries = controller.getMeasureSummaries(this.props.allNodeIds);
|
|
103
|
+
let summary = summaries?.[this.props.nodeId];
|
|
104
|
+
if (!summary) return undefined;
|
|
105
|
+
|
|
106
|
+
let result = controller.getMeasureBreakdown(this.props.nodeId, { startTime: summary.startTime, endTime: summary.endTime });
|
|
107
|
+
if (!result) {
|
|
108
|
+
return <div className={css.pad2(10).hsl(0, 0, 98).bord2(0, 0, 20)}>Loading breakdown...</div>;
|
|
109
|
+
}
|
|
110
|
+
let chunk = result.chunk;
|
|
111
|
+
if (!chunk) {
|
|
112
|
+
return <div className={css.pad2(10).hsl(0, 0, 98).bord2(0, 0, 20)}>Breakdown is no longer available on the node (it only keeps the last hour, and restarts clear it).</div>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let timeRunFor = chunk.endTime - chunk.startTime;
|
|
116
|
+
let cpuFraction = chunk.profiledTime / timeRunFor;
|
|
117
|
+
return <div className={css.vbox(6).pad2(10).hsl(0, 0, 98).bord2(0, 0, 20).fontFamily("monospace").fontSize(13)}>
|
|
118
|
+
<div className={css.boldStyle}>
|
|
119
|
+
Profiled {formatTime(chunk.profiledTime)} ({percent(cpuFraction)} CPU) (profile for {formatTime(timeRunFor)}, ending {formatVeryNiceDateTime(chunk.endTime)})
|
|
120
|
+
</div>
|
|
121
|
+
<div className={css.colorhsl(0, 0, 45).fontSize(12)}>{this.props.nodeId}</div>
|
|
122
|
+
<table className={css.borderCollapse("collapse")}>
|
|
123
|
+
{chunk.entries.map(entry => this.renderEntryRow(entry, chunk!))}
|
|
124
|
+
</table>
|
|
125
|
+
</div>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
renderEntryRow(entry: MeasureChunk["entries"][0], chunk: MeasureChunk) {
|
|
129
|
+
let fraction = entry.ownTime.sum / chunk.profiledTime;
|
|
130
|
+
let cellPad = css.pad2(6, 1).whiteSpace("pre");
|
|
131
|
+
return <tr key={entry.name}>
|
|
132
|
+
<td className={cellPad
|
|
133
|
+
.colorhsl(210, 70, 35)
|
|
134
|
+
.background(`linear-gradient(to right, hsla(210, 60%, 55%, 0.25) ${fraction * 100}%, transparent ${fraction * 100}%)`)
|
|
135
|
+
}>
|
|
136
|
+
{entry.name}
|
|
137
|
+
</td>
|
|
138
|
+
<td className={cellPad.textAlign("right")}>{percent(fraction)}</td>
|
|
139
|
+
<td className={cellPad.textAlign("right")}>{formatTime(entry.ownTime.sum)}</td>
|
|
140
|
+
<td className={cellPad}>=</td>
|
|
141
|
+
{this.renderEquationCell(entry.ownTime)}
|
|
142
|
+
<td className={cellPad}>
|
|
143
|
+
{entry.stillOpenCount > 0 && <span className={css.colorhsl(0, 70, 45)}>({entry.stillOpenCount} open)</span>}
|
|
144
|
+
</td>
|
|
145
|
+
</tr>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
renderEquationCell(stats: StatsValue) {
|
|
149
|
+
let cellPad = css.pad2(6, 1).whiteSpace("pre");
|
|
150
|
+
let top = getStatsTop(stats);
|
|
151
|
+
if (!top.topHeavy) {
|
|
152
|
+
return <td className={cellPad}>
|
|
153
|
+
{formatNumber(stats.count)} × {formatTime(stats.sum / stats.count)}
|
|
154
|
+
</td>;
|
|
155
|
+
}
|
|
156
|
+
let bottomCount = stats.count - top.count;
|
|
157
|
+
let bottomValue = stats.sum - top.value;
|
|
158
|
+
let splitAt = top.valueFraction * 100;
|
|
159
|
+
return <td className={cellPad
|
|
160
|
+
.background(`linear-gradient(to right, hsla(0, 70%, 55%, 0.2) ${splitAt}%, hsla(210, 60%, 55%, 0.15) ${splitAt}%)`)
|
|
161
|
+
}
|
|
162
|
+
title={`Top heavy: ${percent(top.valueFraction)} of the time is in ${percent(top.countFraction)} of the calls`}
|
|
163
|
+
>
|
|
164
|
+
<span className={css.colorhsl(0, 70, 40)}>{formatNumber(top.count)} × {formatTime(top.value / top.count)}</span>
|
|
165
|
+
{" + "}
|
|
166
|
+
<span>{formatNumber(bottomCount)} × {formatTime(bottomValue / bottomCount || 0)}</span>
|
|
167
|
+
</td>;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -17,6 +17,7 @@ import { PendingDeployInfo, UpdateButtons, UpdateServiceButtons } from "./deploy
|
|
|
17
17
|
import { isDefined } from "../../misc";
|
|
18
18
|
import { formatDateJSX } from "../../misc/formatJSX";
|
|
19
19
|
import { Tools } from "./Tools";
|
|
20
|
+
import { ServiceMeasureBars, ServiceMeasureBreakdown } from "./ServiceMeasureBars";
|
|
20
21
|
|
|
21
22
|
module.hotreload = true;
|
|
22
23
|
|
|
@@ -41,6 +42,10 @@ class ServiceCategoryBadge extends qreact.Component<{ config: ServiceConfig }> {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export class ServicesListPage extends qreact.Component {
|
|
45
|
+
state = t.state({
|
|
46
|
+
measurementsEnabled: t.atomic<boolean>(true),
|
|
47
|
+
expandedMeasureNodes: t.lookup(t.string),
|
|
48
|
+
});
|
|
44
49
|
|
|
45
50
|
render() {
|
|
46
51
|
let controller = MachineServiceController(SocketFunction.browserNodeId());
|
|
@@ -63,6 +68,21 @@ export class ServicesListPage extends qreact.Component {
|
|
|
63
68
|
keyCounts.set(key, (keyCounts.get(key) || 0) + 1);
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
let serviceNodeIds = new Map<string, string[]>();
|
|
72
|
+
if (this.state.measurementsEnabled) {
|
|
73
|
+
for (let [serviceId, config] of services) {
|
|
74
|
+
if (!config) continue;
|
|
75
|
+
let nodeIds: string[] = [];
|
|
76
|
+
for (let machineId of getMachineIdList(config.parameters)) {
|
|
77
|
+
if (getMachineConfig(machineId)?.disabled) continue;
|
|
78
|
+
let serviceInfo = getMachineInfo(machineId)?.services[serviceId];
|
|
79
|
+
nodeIds.push(...Object.values(serviceInfo?.nodeIds || {}));
|
|
80
|
+
}
|
|
81
|
+
serviceNodeIds.set(serviceId, nodeIds);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
let allNodeIds = sort(Array.from(new Set(Array.from(serviceNodeIds.values()).flat())), x => x);
|
|
85
|
+
|
|
66
86
|
return <div className={css.vbox(16)}>
|
|
67
87
|
<div className={css.hbox(12).wrap}>
|
|
68
88
|
<h2 className={css.flexGrow(1)}>Services</h2>
|
|
@@ -104,6 +124,14 @@ export class ServicesListPage extends qreact.Component {
|
|
|
104
124
|
</button>
|
|
105
125
|
<UpdateButtons services={services.map(x => x[1]).filter(isDefined)} />
|
|
106
126
|
</div>
|
|
127
|
+
<div
|
|
128
|
+
className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(0, 0, 95).alignSelf("flex-start")}
|
|
129
|
+
onClick={() => {
|
|
130
|
+
this.state.measurementsEnabled = !this.state.measurementsEnabled;
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
<span className={css.boldStyle}>{this.state.measurementsEnabled ? "☑" : "☐"} Measurements</span>
|
|
134
|
+
</div>
|
|
107
135
|
<Tools />
|
|
108
136
|
<div className={css.vbox(8)}>
|
|
109
137
|
{services.map(([serviceId, config]) => {
|
|
@@ -138,9 +166,13 @@ export class ServicesListPage extends qreact.Component {
|
|
|
138
166
|
}, 0);
|
|
139
167
|
let unknown = enabledMachineIds.length - runningMachines.length - failingMachines.length - missingMachines.length;
|
|
140
168
|
let duplicateKey = (keyCounts.get(config.parameters.key || "") || 0) > 1;
|
|
141
|
-
|
|
169
|
+
let measureNodeIds = serviceNodeIds.get(serviceId) || [];
|
|
170
|
+
let expandedMeasureNodeId = this.state.expandedMeasureNodes[serviceId] || "";
|
|
171
|
+
return <div className={css.vbox(4)} key={serviceId}>
|
|
172
|
+
<div className={css.hbox(10)}>
|
|
142
173
|
<ServiceCategoryBadge config={config} />
|
|
143
|
-
<
|
|
174
|
+
<div className={css.vbox(4)}>
|
|
175
|
+
<Anchor noStyles
|
|
144
176
|
values={[currentViewParam.getOverride("service-detail"), selectedServiceIdParam.getOverride(serviceId)]}
|
|
145
177
|
className={
|
|
146
178
|
css.pad2(12).button.bord2(0, 0, 20)
|
|
@@ -206,8 +238,26 @@ export class ServicesListPage extends qreact.Component {
|
|
|
206
238
|
</div>
|
|
207
239
|
</div>
|
|
208
240
|
</Anchor>
|
|
241
|
+
{this.state.measurementsEnabled && measureNodeIds.length > 0 && <ServiceMeasureBars
|
|
242
|
+
serviceNodeIds={measureNodeIds}
|
|
243
|
+
allNodeIds={allNodeIds}
|
|
244
|
+
expandedNodeId={expandedMeasureNodeId}
|
|
245
|
+
onToggleNode={nodeId => {
|
|
246
|
+
if (this.state.expandedMeasureNodes[serviceId] === nodeId) {
|
|
247
|
+
delete this.state.expandedMeasureNodes[serviceId];
|
|
248
|
+
} else {
|
|
249
|
+
this.state.expandedMeasureNodes[serviceId] = nodeId;
|
|
250
|
+
}
|
|
251
|
+
}}
|
|
252
|
+
/>}
|
|
253
|
+
</div>
|
|
209
254
|
<UpdateServiceButtons service={config} />
|
|
210
255
|
<PendingDeployInfo service={config} />
|
|
256
|
+
</div>
|
|
257
|
+
{this.state.measurementsEnabled && expandedMeasureNodeId && <ServiceMeasureBreakdown
|
|
258
|
+
nodeId={expandedMeasureNodeId}
|
|
259
|
+
allNodeIds={allNodeIds}
|
|
260
|
+
/>}
|
|
211
261
|
</div>
|
|
212
262
|
;
|
|
213
263
|
})}
|
|
@@ -41,6 +41,8 @@ export type StorageServerBuckets = StorageServer & {
|
|
|
41
41
|
/** Still waiting on this server's own endpoint - each server loads independently */
|
|
42
42
|
loading?: boolean;
|
|
43
43
|
buckets?: ServerBucketInfo[];
|
|
44
|
+
/** This server could not be read at all. Kept per server rather than thrown, so one bad server costs its own row and nothing else. */
|
|
45
|
+
error?: string;
|
|
44
46
|
};
|
|
45
47
|
|
|
46
48
|
function getStorageUrls(parameters: ServiceParameters): string[] {
|
|
@@ -59,7 +61,7 @@ function getStorageUrls(parameters: ServiceParameters): string[] {
|
|
|
59
61
|
export function getStorageServers(configs: ServiceConfig[]): StorageServer[] {
|
|
60
62
|
let serviceByUrl = new Map<string, StorageServer>();
|
|
61
63
|
for (let config of configs) {
|
|
62
|
-
let allParameters = [getLiveServiceParameters(config)
|
|
64
|
+
let allParameters = [getLiveServiceParameters(config)];
|
|
63
65
|
for (let parameters of allParameters) {
|
|
64
66
|
if (!parameters) continue;
|
|
65
67
|
for (let url of getStorageUrls(parameters)) {
|
|
@@ -214,6 +216,10 @@ function getBucketRows(servers: StorageServerBuckets[]): BucketRow[] {
|
|
|
214
216
|
syncing: undefined,
|
|
215
217
|
error: "",
|
|
216
218
|
};
|
|
219
|
+
if (server.error) {
|
|
220
|
+
rows.push({ ...baseRow, error: server.error });
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
217
223
|
if (server.loading) {
|
|
218
224
|
rows.push({ ...baseRow, bucket: "Loading..." });
|
|
219
225
|
continue;
|
|
@@ -226,24 +232,34 @@ function getBucketRows(servers: StorageServerBuckets[]): BucketRow[] {
|
|
|
226
232
|
}
|
|
227
233
|
for (let [index, bucket] of buckets.entries()) {
|
|
228
234
|
let config = bucket.config;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
235
|
+
// Per bucket for the same reason it is per server: a single unparseable config costs its own row, not the table
|
|
236
|
+
try {
|
|
237
|
+
rows.push({
|
|
238
|
+
...baseRow,
|
|
239
|
+
server: index === 0 && server.url || "",
|
|
240
|
+
bucket: bucket.bucketName,
|
|
241
|
+
state: bucket.active && ACTIVE_STATE || INACTIVE_STATE,
|
|
242
|
+
files: config?.index && formatNumber(config.index.fileCount) || "",
|
|
243
|
+
bytes: config?.index && formatNumber(config.index.byteCount) + "B" || "",
|
|
244
|
+
flags: getBucketFlags(server.url, bucket.bucketName, config?.remoteConfig),
|
|
245
|
+
indexSources: config?.indexSources,
|
|
246
|
+
readerDiskLimit: config?.readerDiskLimit && formatNumber(config.readerDiskLimit) + "B" || "",
|
|
247
|
+
writes: bucket.writeStats && formatNumber(bucket.writeStats.originalWrites) || "",
|
|
248
|
+
written: bucket.writeStats && formatNumber(bucket.writeStats.originalBytes) + "B" || "",
|
|
249
|
+
writeGain: getWriteGain(bucket.writeStats),
|
|
250
|
+
disk: bucket.disk,
|
|
251
|
+
diskError: bucket.diskError || "",
|
|
252
|
+
syncing: config?.syncing,
|
|
253
|
+
error: bucket.error || "",
|
|
254
|
+
});
|
|
255
|
+
} catch (e: any) {
|
|
256
|
+
rows.push({
|
|
257
|
+
...baseRow,
|
|
258
|
+
server: index === 0 && server.url || "",
|
|
259
|
+
bucket: bucket.bucketName,
|
|
260
|
+
error: e.stack ?? String(e),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
247
263
|
}
|
|
248
264
|
}
|
|
249
265
|
return rows;
|
|
@@ -537,9 +553,14 @@ export class StoragePage extends qreact.Component {
|
|
|
537
553
|
const configs = (serviceList || []).map(serviceId => machineController.getServiceConfig(serviceId)).filter(isDefined);
|
|
538
554
|
const storageServers = getStorageServers(configs);
|
|
539
555
|
const servers: StorageServerBuckets[] = storageServers.map(server => {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
556
|
+
// Every other server still has something worth showing, so one that cannot be read becomes a row saying so instead of an empty page
|
|
557
|
+
try {
|
|
558
|
+
let buckets = controller.getServerBuckets(server.url);
|
|
559
|
+
if (!buckets) return { ...server, loading: true };
|
|
560
|
+
return { ...server, buckets };
|
|
561
|
+
} catch (e: any) {
|
|
562
|
+
return { ...server, error: e.stack ?? String(e) };
|
|
563
|
+
}
|
|
543
564
|
});
|
|
544
565
|
const header = <>
|
|
545
566
|
<div className={css.hbox(12)}>
|