querysub 0.589.0 → 0.591.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 +2 -2
- 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 +179 -0
- package/src/deployManager/components/ServicesListPage.tsx +49 -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 +67 -1
- package/src/library-components/ATag.tsx +1 -0
- package/src/library-components/SyncedController.ts +8 -0
|
@@ -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,179 @@
|
|
|
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 { timeInSecond } from "socket-function/src/misc";
|
|
6
|
+
import { timeoutToError } from "../../errors";
|
|
7
|
+
import { StatsValue, getStatsTop } from "socket-function/src/profiling/stats";
|
|
8
|
+
import type { MeasureChunk, MeasureSummary } from "../../diagnostics/watchdog";
|
|
9
|
+
import { NodeCapabilitiesController } from "../../-g-core-values/NodeCapabilities";
|
|
10
|
+
import { getSyncedController } from "../../library-components/SyncedController";
|
|
11
|
+
import { assertIsManagementUser } from "../../diagnostics/managementPages";
|
|
12
|
+
|
|
13
|
+
const NODE_CALL_TIMEOUT = timeInSecond * 10;
|
|
14
|
+
const BAR_HEIGHT_PX = 6;
|
|
15
|
+
const MIN_BAR_WIDTH_PX = 30;
|
|
16
|
+
// Green at idle, red at fully busy.
|
|
17
|
+
const BAR_IDLE_HUE = 120;
|
|
18
|
+
const BAR_FLASH_THRESHOLD = 0.6;
|
|
19
|
+
const FLASH_CLASS_NAME = "ServiceMeasureBars-flash";
|
|
20
|
+
|
|
21
|
+
class ServiceMeasureControllerBase {
|
|
22
|
+
public async getMeasureSummary(nodeId: string): Promise<MeasureSummary | undefined> {
|
|
23
|
+
return await timeoutToError(NODE_CALL_TIMEOUT, NodeCapabilitiesController.nodes[nodeId].getMeasureSummary(), () => new Error(`Timed out asking ${nodeId} for its measure summary`));
|
|
24
|
+
}
|
|
25
|
+
public async getMeasureBreakdown(nodeId: string, range: { startTime: number; endTime: number }): Promise<{ chunk?: MeasureChunk }> {
|
|
26
|
+
let chunk = await timeoutToError(NODE_CALL_TIMEOUT, NodeCapabilitiesController.nodes[nodeId].getMeasureBreakdown(range), () => new Error(`Timed out asking ${nodeId} for its measure breakdown`));
|
|
27
|
+
return { chunk };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export const ServiceMeasureController = getSyncedController(SocketFunction.register(
|
|
31
|
+
"ServiceMeasureController-3f8c1de2-5b74-4e0f-a6c9-8d21f47b90a5",
|
|
32
|
+
new ServiceMeasureControllerBase(),
|
|
33
|
+
() => ({
|
|
34
|
+
getMeasureSummary: { hooks: [assertIsManagementUser], compress: true },
|
|
35
|
+
getMeasureBreakdown: { hooks: [assertIsManagementUser], compress: true },
|
|
36
|
+
}),
|
|
37
|
+
() => ({
|
|
38
|
+
})
|
|
39
|
+
));
|
|
40
|
+
|
|
41
|
+
function percent(value: number) {
|
|
42
|
+
return `${(value * 100).toFixed(2)}%`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class ServiceMeasureBars extends qreact.Component<{
|
|
46
|
+
serviceNodeIds: string[];
|
|
47
|
+
expandedNodeId: string;
|
|
48
|
+
onToggleNode: (nodeId: string) => void;
|
|
49
|
+
}> {
|
|
50
|
+
render() {
|
|
51
|
+
let controller = ServiceMeasureController(SocketFunction.browserNodeId());
|
|
52
|
+
|
|
53
|
+
return <div className={css.vbox(1).alignItems("stretch")}>
|
|
54
|
+
{this.props.serviceNodeIds.map(nodeId => {
|
|
55
|
+
let isExpanded = this.props.expandedNodeId === nodeId;
|
|
56
|
+
let summary: MeasureSummary | undefined;
|
|
57
|
+
try {
|
|
58
|
+
summary = controller.getMeasureSummary(nodeId);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return <div key={nodeId}
|
|
61
|
+
className={css.height(BAR_HEIGHT_PX).hsl(0, 70, 55)}
|
|
62
|
+
title={`Node did not respond (it likely doesn't have the measure API yet)\n${nodeId}\n${(err as Error).stack ?? err}`}
|
|
63
|
+
/>;
|
|
64
|
+
}
|
|
65
|
+
if (!summary) return undefined;
|
|
66
|
+
let timeRunFor = summary.endTime - summary.startTime;
|
|
67
|
+
if (timeRunFor <= 0) return undefined;
|
|
68
|
+
let fraction = summary.profiledTime / timeRunFor;
|
|
69
|
+
let clampedFraction = Math.min(1, Math.max(0, fraction));
|
|
70
|
+
let hue = BAR_IDLE_HUE * (1 - clampedFraction);
|
|
71
|
+
let isFlashing = fraction > BAR_FLASH_THRESHOLD;
|
|
72
|
+
return <div key={nodeId} className={css.hbox(0)}>
|
|
73
|
+
<div
|
|
74
|
+
className={
|
|
75
|
+
css.height(BAR_HEIGHT_PX)
|
|
76
|
+
.width(`${fraction * 100}%`)
|
|
77
|
+
.minWidth(MIN_BAR_WIDTH_PX)
|
|
78
|
+
.button
|
|
79
|
+
.hsl(hue, 70, isExpanded ? 40 : 55)
|
|
80
|
+
+ (isFlashing && (" " + FLASH_CLASS_NAME) || "")
|
|
81
|
+
}
|
|
82
|
+
title={`Profiled ${formatTime(summary.profiledTime)} of ${formatTime(timeRunFor)} (${percent(fraction)} CPU)\n${nodeId}`}
|
|
83
|
+
onClick={() => this.props.onToggleNode(nodeId)}
|
|
84
|
+
/>
|
|
85
|
+
{isFlashing && <style>{`
|
|
86
|
+
@keyframes ${FLASH_CLASS_NAME}-anim {
|
|
87
|
+
0%, 100% { opacity: 1; }
|
|
88
|
+
50% { opacity: 0.3; }
|
|
89
|
+
}
|
|
90
|
+
.${FLASH_CLASS_NAME} {
|
|
91
|
+
animation: ${FLASH_CLASS_NAME}-anim 0.8s infinite;
|
|
92
|
+
}
|
|
93
|
+
`}</style>}
|
|
94
|
+
</div>;
|
|
95
|
+
})}
|
|
96
|
+
</div>;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class ServiceMeasureBreakdown extends qreact.Component<{
|
|
101
|
+
nodeId: string;
|
|
102
|
+
}> {
|
|
103
|
+
render() {
|
|
104
|
+
let controller = ServiceMeasureController(SocketFunction.browserNodeId());
|
|
105
|
+
let summary: MeasureSummary | undefined;
|
|
106
|
+
let result: { chunk?: MeasureChunk } | undefined;
|
|
107
|
+
try {
|
|
108
|
+
summary = controller.getMeasureSummary(this.props.nodeId);
|
|
109
|
+
if (!summary) return undefined;
|
|
110
|
+
result = controller.getMeasureBreakdown(this.props.nodeId, { startTime: summary.startTime, endTime: summary.endTime });
|
|
111
|
+
} catch (err) {
|
|
112
|
+
return <div className={css.pad2(10).hsl(0, 70, 92).bord2(0, 0, 20)}>
|
|
113
|
+
<div className={css.boldStyle.colorhsl(0, 70, 35)}>Node did not respond (it likely doesn't have the measure API yet)</div>
|
|
114
|
+
<pre className={css.whiteSpace("pre-wrap").fontSize(12)}>{(err as Error).stack ?? String(err)}</pre>
|
|
115
|
+
</div>;
|
|
116
|
+
}
|
|
117
|
+
if (!result) {
|
|
118
|
+
return <div className={css.pad2(10).hsl(0, 0, 98).bord2(0, 0, 20)}>Loading breakdown...</div>;
|
|
119
|
+
}
|
|
120
|
+
let chunk = result.chunk;
|
|
121
|
+
if (!chunk) {
|
|
122
|
+
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>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let timeRunFor = chunk.endTime - chunk.startTime;
|
|
126
|
+
let cpuFraction = chunk.profiledTime / timeRunFor;
|
|
127
|
+
return <div className={css.vbox(6).pad2(10).hsl(0, 0, 98).bord2(0, 0, 20).fontFamily("monospace").fontSize(13)}>
|
|
128
|
+
<div className={css.boldStyle}>
|
|
129
|
+
Profiled {formatTime(chunk.profiledTime)} ({percent(cpuFraction)} CPU) (profile for {formatTime(timeRunFor)}, ending {formatVeryNiceDateTime(chunk.endTime)})
|
|
130
|
+
</div>
|
|
131
|
+
<div className={css.colorhsl(0, 0, 45).fontSize(12)}>{this.props.nodeId}</div>
|
|
132
|
+
<table className={css.borderCollapse("collapse")}>
|
|
133
|
+
{chunk.entries.map(entry => this.renderEntryRow(entry, chunk!))}
|
|
134
|
+
</table>
|
|
135
|
+
</div>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
renderEntryRow(entry: MeasureChunk["entries"][0], chunk: MeasureChunk) {
|
|
139
|
+
let fraction = entry.ownTime.sum / chunk.profiledTime;
|
|
140
|
+
let cellPad = css.pad2(6, 1).whiteSpace("pre");
|
|
141
|
+
return <tr key={entry.name}>
|
|
142
|
+
<td className={cellPad
|
|
143
|
+
.colorhsl(210, 70, 35)
|
|
144
|
+
.background(`linear-gradient(to right, hsla(210, 60%, 55%, 0.25) ${fraction * 100}%, transparent ${fraction * 100}%)`)
|
|
145
|
+
}>
|
|
146
|
+
{entry.name}
|
|
147
|
+
</td>
|
|
148
|
+
<td className={cellPad.textAlign("right")}>{percent(fraction)}</td>
|
|
149
|
+
<td className={cellPad.textAlign("right")}>{formatTime(entry.ownTime.sum)}</td>
|
|
150
|
+
<td className={cellPad}>=</td>
|
|
151
|
+
{this.renderEquationCell(entry.ownTime)}
|
|
152
|
+
<td className={cellPad}>
|
|
153
|
+
{entry.stillOpenCount > 0 && <span className={css.colorhsl(0, 70, 45)}>({entry.stillOpenCount} open)</span>}
|
|
154
|
+
</td>
|
|
155
|
+
</tr>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
renderEquationCell(stats: StatsValue) {
|
|
159
|
+
let cellPad = css.pad2(6, 1).whiteSpace("pre");
|
|
160
|
+
let top = getStatsTop(stats);
|
|
161
|
+
if (!top.topHeavy) {
|
|
162
|
+
return <td className={cellPad}>
|
|
163
|
+
{formatNumber(stats.count)} × {formatTime(stats.sum / stats.count)}
|
|
164
|
+
</td>;
|
|
165
|
+
}
|
|
166
|
+
let bottomCount = stats.count - top.count;
|
|
167
|
+
let bottomValue = stats.sum - top.value;
|
|
168
|
+
let splitAt = top.valueFraction * 100;
|
|
169
|
+
return <td className={cellPad
|
|
170
|
+
.background(`linear-gradient(to right, hsla(0, 70%, 55%, 0.2) ${splitAt}%, hsla(210, 60%, 55%, 0.15) ${splitAt}%)`)
|
|
171
|
+
}
|
|
172
|
+
title={`Top heavy: ${percent(top.valueFraction)} of the time is in ${percent(top.countFraction)} of the calls`}
|
|
173
|
+
>
|
|
174
|
+
<span className={css.colorhsl(0, 70, 40)}>{formatNumber(top.count)} × {formatTime(top.value / top.count)}</span>
|
|
175
|
+
{" + "}
|
|
176
|
+
<span>{formatNumber(bottomCount)} × {formatTime(bottomValue / bottomCount || 0)}</span>
|
|
177
|
+
</td>;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -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,20 @@ 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
|
+
|
|
66
85
|
return <div className={css.vbox(16)}>
|
|
67
86
|
<div className={css.hbox(12).wrap}>
|
|
68
87
|
<h2 className={css.flexGrow(1)}>Services</h2>
|
|
@@ -104,6 +123,14 @@ export class ServicesListPage extends qreact.Component {
|
|
|
104
123
|
</button>
|
|
105
124
|
<UpdateButtons services={services.map(x => x[1]).filter(isDefined)} />
|
|
106
125
|
</div>
|
|
126
|
+
<div
|
|
127
|
+
className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(0, 0, 95).alignSelf("flex-start")}
|
|
128
|
+
onClick={() => {
|
|
129
|
+
this.state.measurementsEnabled = !this.state.measurementsEnabled;
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
<span className={css.boldStyle}>{this.state.measurementsEnabled ? "☑" : "☐"} Measurements</span>
|
|
133
|
+
</div>
|
|
107
134
|
<Tools />
|
|
108
135
|
<div className={css.vbox(8)}>
|
|
109
136
|
{services.map(([serviceId, config]) => {
|
|
@@ -138,9 +165,13 @@ export class ServicesListPage extends qreact.Component {
|
|
|
138
165
|
}, 0);
|
|
139
166
|
let unknown = enabledMachineIds.length - runningMachines.length - failingMachines.length - missingMachines.length;
|
|
140
167
|
let duplicateKey = (keyCounts.get(config.parameters.key || "") || 0) > 1;
|
|
141
|
-
|
|
168
|
+
let measureNodeIds = serviceNodeIds.get(serviceId) || [];
|
|
169
|
+
let expandedMeasureNodeId = this.state.expandedMeasureNodes[serviceId] || "";
|
|
170
|
+
return <div className={css.vbox(4).alignItems("stretch")} key={serviceId}>
|
|
171
|
+
<div className={css.hbox(10)}>
|
|
142
172
|
<ServiceCategoryBadge config={config} />
|
|
143
|
-
<
|
|
173
|
+
<div className={css.vbox(4).alignItems("stretch")}>
|
|
174
|
+
<Anchor noStyles
|
|
144
175
|
values={[currentViewParam.getOverride("service-detail"), selectedServiceIdParam.getOverride(serviceId)]}
|
|
145
176
|
className={
|
|
146
177
|
css.pad2(12).button.bord2(0, 0, 20)
|
|
@@ -206,8 +237,24 @@ export class ServicesListPage extends qreact.Component {
|
|
|
206
237
|
</div>
|
|
207
238
|
</div>
|
|
208
239
|
</Anchor>
|
|
240
|
+
{this.state.measurementsEnabled && measureNodeIds.length > 0 && <ServiceMeasureBars
|
|
241
|
+
serviceNodeIds={measureNodeIds}
|
|
242
|
+
expandedNodeId={expandedMeasureNodeId}
|
|
243
|
+
onToggleNode={nodeId => {
|
|
244
|
+
if (this.state.expandedMeasureNodes[serviceId] === nodeId) {
|
|
245
|
+
delete this.state.expandedMeasureNodes[serviceId];
|
|
246
|
+
} else {
|
|
247
|
+
this.state.expandedMeasureNodes[serviceId] = nodeId;
|
|
248
|
+
}
|
|
249
|
+
}}
|
|
250
|
+
/>}
|
|
251
|
+
</div>
|
|
209
252
|
<UpdateServiceButtons service={config} />
|
|
210
253
|
<PendingDeployInfo service={config} />
|
|
254
|
+
</div>
|
|
255
|
+
{this.state.measurementsEnabled && expandedMeasureNodeId && <ServiceMeasureBreakdown
|
|
256
|
+
nodeId={expandedMeasureNodeId}
|
|
257
|
+
/>}
|
|
211
258
|
</div>
|
|
212
259
|
;
|
|
213
260
|
})}
|
|
@@ -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)}>
|