Skip to content

Commit e2eb92e

Browse files
committed
feat: introduce slogCallbacks for the host to handle slog calls
This means rather than making another subsystem for SwingSet to report metrics to the host at runtime, just intercept the existing slog events and collate their data to produce the metrics we need.
1 parent 6cc2e4f commit e2eb92e

File tree

6 files changed

+208
-30
lines changed

6 files changed

+208
-30
lines changed

packages/SwingSet/src/controller.js

+4
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export async function makeSwingsetController(
5656
const {
5757
verbose,
5858
debugPrefix = '',
59+
slogCallbacks,
5960
slogFile,
6061
testTrackDecref,
6162
defaultManagerType = env.WORKER_TYPE || 'local',
@@ -223,6 +224,7 @@ export async function makeSwingsetController(
223224
makeNodeWorker,
224225
startSubprocessWorkerNode,
225226
startXSnap,
227+
slogCallbacks,
226228
writeSlogObject,
227229
WeakRef,
228230
FinalizationRegistry,
@@ -320,13 +322,15 @@ export async function buildVatController(
320322
verbose,
321323
kernelBundles,
322324
debugPrefix,
325+
slogCallbacks,
323326
testTrackDecref,
324327
defaultManagerType,
325328
} = runtimeOptions;
326329
const actualRuntimeOptions = {
327330
verbose,
328331
debugPrefix,
329332
testTrackDecref,
333+
slogCallbacks,
330334
defaultManagerType,
331335
};
332336
const initializationOptions = { verbose, kernelBundles };

packages/SwingSet/src/kernel/kernel.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export default function buildKernel(
9494
hostStorage,
9595
debugPrefix,
9696
vatEndowments,
97+
slogCallbacks = {},
9798
makeConsole,
9899
replaceGlobalMeter,
99100
transformMetering,
@@ -119,8 +120,8 @@ export default function buildKernel(
119120
);
120121

121122
const kernelSlog = writeSlogObject
122-
? makeSlogger(writeSlogObject)
123-
: makeDummySlogger(makeConsole);
123+
? makeSlogger(slogCallbacks, writeSlogObject)
124+
: makeDummySlogger(slogCallbacks, makeConsole);
124125

125126
const kernelKeeper = makeKernelKeeper(enhancedCrankBuffer, kernelSlog);
126127

packages/SwingSet/src/kernel/metrics.js

+5
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ export const KERNEL_STATS_UPDOWN_METRICS = [
8383
name: 'swingset_clist_entries',
8484
description: 'Number of entries in the kernel c-list',
8585
},
86+
{
87+
key: 'vats',
88+
name: 'swingset_vats',
89+
description: 'Number of active vats',
90+
},
8691
];
8792

8893
export const KERNEL_STATS_METRICS = [

packages/SwingSet/src/kernel/slogger.js

+116-21
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,100 @@
1-
import { assert, details as X } from '@agoric/assert';
1+
import { assert, details as X, quote } from '@agoric/assert';
22

33
const IDLE = 'idle';
44
const STARTUP = 'startup';
55
const DELIVERY = 'delivery';
66

7-
export function makeDummySlogger(makeConsole) {
7+
function makeCallbackRegistry(callbacks) {
8+
const todo = new Set(Object.keys(callbacks));
89
return harden({
9-
addVat: () => 0,
10-
vatConsole: () => makeConsole('disabled slogger'),
11-
startup: () => () => 0, // returns nop finish() function
12-
delivery: () => () => 0,
13-
syscall: () => () => 0,
14-
changeCList: () => () => 0,
15-
terminateVat: () => () => 0,
10+
/**
11+
* Robustly wrap a method with a callbacks[method] function, if defined. We
12+
* incur no runtime overhead if the given callback method isn't defined.
13+
*
14+
* @param {string} method wrap with callbacks[method]
15+
* @param {(...args: Array<unknown>) => unknown} impl the original
16+
* implementation of the method
17+
* @returns {(...args: Array<unknown>) => unknown} the wrapped method if the
18+
* callback is defined, or original method if not
19+
*/
20+
registerCallback(method, impl) {
21+
todo.delete(method);
22+
const cb = callbacks[method];
23+
if (!cb) {
24+
// No registered callback, just use the implementation directly.
25+
console.error('no registered callback for', method);
26+
return impl;
27+
}
28+
29+
return (...args) => {
30+
// Invoke the implementation first.
31+
const ret = impl(...args);
32+
try {
33+
// Allow the callback to observe the call synchronously, and affect
34+
// the finisher function, but not to throw an exception.
35+
const cbRet = cb(method, args, ret);
36+
if (typeof ret === 'function') {
37+
// We wrap the finisher in the callback's return value.
38+
return (...finishArgs) => {
39+
try {
40+
return cbRet(...finishArgs);
41+
} catch (e) {
42+
console.error(
43+
`failed to call registered ${method}.finish function:`,
44+
e,
45+
);
46+
}
47+
return ret(...args);
48+
};
49+
}
50+
// We just return the callback's return value.
51+
return cbRet;
52+
} catch (e) {
53+
console.error('failed to call registered', method, 'callback:', e);
54+
}
55+
return ret;
56+
};
57+
},
58+
/**
59+
* Declare that all the methods have been registered.
60+
*
61+
* @param {string} errorUnusedMsg message to display if there are callback
62+
* names that don't correspond to a registration
63+
*/
64+
doneRegistering(errorUnusedMsg = `Unrecognized callback names:`) {
65+
const cbNames = [...todo.keys()];
66+
if (!cbNames.length) {
67+
return;
68+
}
69+
console.warn(
70+
errorUnusedMsg,
71+
cbNames
72+
.map(quote)
73+
.sort()
74+
.join(', '),
75+
);
76+
},
1677
});
1778
}
1879

19-
export function makeSlogger(writeObj) {
80+
export function makeDummySlogger(slogCallbacks, makeConsole) {
81+
const { registerCallback: reg, doneRegistering } = makeCallbackRegistry(
82+
slogCallbacks,
83+
);
84+
const dummySlogger = harden({
85+
addVat: reg('addVat', () => 0),
86+
vatConsole: reg('vatConsole', () => makeConsole('disabled slogger')),
87+
startup: reg('startup', () => () => 0), // returns nop finish() function
88+
delivery: reg('delivery', () => () => 0),
89+
syscall: reg('syscall', () => () => 0),
90+
changeCList: reg('changeCList', () => () => 0),
91+
terminateVat: reg('terminateVat', () => () => 0),
92+
});
93+
doneRegistering(`Unrecognized makeDummySlogger slogCallbacks names:`);
94+
return dummySlogger;
95+
}
96+
97+
export function makeSlogger(slogCallbacks, writeObj) {
2098
const write = writeObj ? e => writeObj(e) : () => 0;
2199

22100
const vatSlogs = new Map(); // vatID -> vatSlog
@@ -28,7 +106,7 @@ export function makeSlogger(writeObj) {
28106
let syscallNum;
29107

30108
function assertOldState(exp, msg) {
31-
assert(state === exp, X`vat ${vatID} in ${state}, not ${exp}: ${msg}`);
109+
assert.equal(state, exp, X`vat ${vatID} in ${state}, not ${exp}: ${msg}`);
32110
}
33111

34112
function vatConsole(origConsole) {
@@ -65,9 +143,9 @@ export function makeSlogger(writeObj) {
65143
syscallNum = 0;
66144

67145
// dr: deliveryResult
68-
function finish(dr) {
146+
function finish(dr, stats) {
69147
assertOldState(DELIVERY, 'delivery-finish called twice?');
70-
write({ type: 'deliver-result', ...when, dr });
148+
write({ type: 'deliver-result', ...when, dr, stats });
71149
state = IDLE;
72150
}
73151
return harden(finish);
@@ -126,13 +204,30 @@ export function makeSlogger(writeObj) {
126204
// write({ type: 'annotate-vat', vatID, data });
127205
// }
128206

129-
return harden({
130-
addVat,
131-
vatConsole: (vatID, ...args) => vatSlogs.get(vatID).vatConsole(...args),
132-
startup: (vatID, ...args) => vatSlogs.get(vatID).startup(...args),
133-
delivery: (vatID, ...args) => vatSlogs.get(vatID).delivery(...args),
134-
syscall: (vatID, ...args) => vatSlogs.get(vatID).syscall(...args),
135-
changeCList: (vatID, ...args) => vatSlogs.get(vatID).changeCList(...args),
136-
terminateVat: (vatID, ...args) => vatSlogs.get(vatID).terminateVat(...args),
207+
const { registerCallback: reg, doneRegistering } = makeCallbackRegistry(
208+
slogCallbacks,
209+
);
210+
const slogger = harden({
211+
addVat: reg('addVat', addVat),
212+
vatConsole: reg('vatConsole', (vatID, ...args) =>
213+
vatSlogs.get(vatID).vatConsole(...args),
214+
),
215+
startup: reg('startup', (vatID, ...args) =>
216+
vatSlogs.get(vatID).startup(...args),
217+
),
218+
delivery: reg('delivery', (vatID, ...args) =>
219+
vatSlogs.get(vatID).delivery(...args),
220+
),
221+
syscall: reg('syscall', (vatID, ...args) =>
222+
vatSlogs.get(vatID).syscall(...args),
223+
),
224+
changeCList: reg('changeCList', (vatID, ...args) =>
225+
vatSlogs.get(vatID).changeCList(...args),
226+
),
227+
terminateVat: reg('terminateVat', (vatID, ...args) =>
228+
vatSlogs.get(vatID).terminateVat(...args),
229+
),
137230
});
231+
doneRegistering(`Unrecognized makeSlogger slogCallbacks names:`);
232+
return slogger;
138233
}

packages/SwingSet/src/kernel/state/kernelKeeper.js

+2
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,7 @@ export default function makeKernelKeeper(storage, kernelSlog) {
457457
break;
458458
}
459459
}
460+
decStat('vats');
460461
}
461462

462463
return kernelPromisesToReject;
@@ -543,6 +544,7 @@ export default function makeKernelKeeper(storage, kernelSlog) {
543544

544545
function allocateUnusedVatID() {
545546
const nextID = Nat(BigInt(getRequired(`vat.nextID`)));
547+
incStat('vats');
546548
storage.set(`vat.nextID`, `${nextID + 1n}`);
547549
return makeVatID(nextID);
548550
}

packages/cosmic-swingset/lib/launch-chain.js

+78-7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
loadSwingsetConfigFile,
1616
} from '@agoric/swingset-vat';
1717
import { assert, details as X } from '@agoric/assert';
18+
import makeStore from '@agoric/store';
1819
import { getBestSwingStore } from './check-lmdb';
1920
import { exportKernelStats } from './kernel-stats';
2021

@@ -47,7 +48,7 @@ async function buildSwingset(
4748
storage,
4849
vatsDir,
4950
argv,
50-
debugName = undefined,
51+
{ debugName = undefined, slogCallbacks },
5152
) {
5253
const debugPrefix = debugName === undefined ? '' : `${debugName}:`;
5354
let config = loadSwingsetConfigFile(`${vatsDir}/chain-config.json`);
@@ -78,7 +79,9 @@ async function buildSwingset(
7879
if (!swingsetIsInitialized(storage)) {
7980
await initializeSwingset(config, argv, storage, { debugPrefix });
8081
}
81-
const controller = await makeSwingsetController(storage, deviceEndowments);
82+
const controller = await makeSwingsetController(storage, deviceEndowments, {
83+
slogCallbacks,
84+
});
8285

8386
// We DON'T want to run the kernel yet, only when we're in the scheduler at
8487
// endBlock!
@@ -106,20 +109,88 @@ export async function launch(
106109
// console.error('would outbound bridge', dstID, obj);
107110
return doOutboundBridge(dstID, obj);
108111
}
112+
113+
const wrapDeltaMS = (finisher, useDeltaMS) => {
114+
const startMS = Date.now();
115+
return (...finishArgs) => {
116+
try {
117+
return finisher(...finishArgs);
118+
} finally {
119+
const deltaMS = Date.now() - startMS;
120+
useDeltaMS(deltaMS, finishArgs);
121+
}
122+
};
123+
};
124+
125+
// Not to be confused with the gas model, this meter is for OpenTelemetry.
126+
const metricMeter = meterProvider.getMeter('ag-chain-cosmos');
127+
const METRIC_LABELS = { app: 'ag-chain-cosmos' };
128+
129+
const nameToBaseMetric = makeStore('metricName');
130+
nameToBaseMetric.init(
131+
'swingset_vat_startup',
132+
metricMeter.createValueRecorder('swingset_vat_startup', {
133+
description: 'Vat startup time (ms)',
134+
}),
135+
);
136+
nameToBaseMetric.init(
137+
'swingset_vat_delivery',
138+
metricMeter.createValueRecorder('swingset_vat_delivery', {
139+
description: 'Vat delivery time (ms)',
140+
}),
141+
);
142+
const vatToMetrics = makeStore();
143+
144+
const getVatMetric = (vatID, name) => {
145+
let nameToMetric;
146+
if (vatToMetrics.has(vatID)) {
147+
nameToMetric = vatToMetrics.get(vatID);
148+
} else {
149+
nameToMetric = makeStore('metricName');
150+
vatToMetrics.init(vatID, nameToMetric);
151+
}
152+
let metric;
153+
if (nameToMetric.has(name)) {
154+
metric = nameToMetric.get(name);
155+
} else {
156+
// Bind the base metric to the vatID label.
157+
metric = nameToBaseMetric.get(name).bind({ ...METRIC_LABELS, vatID });
158+
nameToMetric.init(name, metric);
159+
}
160+
return metric;
161+
};
162+
163+
/**
164+
* Measure some interesting stats. We currently do a per-vat recording of
165+
* time spent in the vat for startup and delivery.
166+
*/
167+
const slogCallbacks = {
168+
startup(_method, [vatID], finisher) {
169+
return wrapDeltaMS(finisher, deltaMS => {
170+
getVatMetric(vatID, 'swingset_vat_startup').record(deltaMS);
171+
});
172+
},
173+
delivery(_method, [vatID], finisher) {
174+
return wrapDeltaMS(finisher, (deltaMS, [_dr, _stats]) => {
175+
// console.info(vatID, 'delivery.finish', stats);
176+
getVatMetric(vatID, 'swingset_vat_delivery').record(deltaMS);
177+
});
178+
},
179+
};
180+
109181
log.debug(`buildSwingset`);
110182
const { controller, mb, bridgeInbound, timer } = await buildSwingset(
111183
mailboxStorage,
112184
bridgeOutbound,
113185
storage,
114186
vatsDir,
115187
argv,
116-
debugName,
188+
{
189+
debugName,
190+
slogCallbacks,
191+
},
117192
);
118193

119-
const METRIC_LABELS = { app: 'ag-chain-cosmos' };
120-
121-
// Not to be confused with the gas model, this meter is for OpenTelemetry.
122-
const metricMeter = meterProvider.getMeter('ag-chain-cosmos');
123194
exportKernelStats({ controller, metricMeter, log, labels: METRIC_LABELS });
124195

125196
const schedulerCrankTimeHistogram = metricMeter

0 commit comments

Comments
 (0)