Skip to content

Commit 15adc38

Browse files
committed
fix(evaluator): quiescence works
Had to disable rethrown exceptions in the builtin wrappers. This is because just one throw is sufficient. Transformed code still throws at every meter call once any meter is exhausted.
1 parent db3acfd commit 15adc38

File tree

21 files changed

+428
-210
lines changed

21 files changed

+428
-210
lines changed

packages/evaluate/src/index.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import makeDefaultEvaluateOptions from '@agoric/default-evaluate-options';
55
export const makeEvaluators = (makerOptions = {}) => {
66
// Evaluate any shims, globally!
77
if (typeof globalThis === 'undefined') {
8-
const myGlobal = typeof window === 'undefined' ? global : window;
8+
// eslint-disable-next-line no-new-func
9+
const myGlobal = Function('return this')();
910
myGlobal.globalThis = myGlobal;
1011
}
1112
// eslint-disable-next-line no-eval
@@ -93,16 +94,18 @@ export const makeEvaluators = (makerOptions = {}) => {
9394
// The eval below is direct, so that we have access to the named endowments.
9495
const scopedEval = `(function() {
9596
with (arguments[0]) {
97+
console.log('endow', arguments[0]);
9698
return function() {
9799
'use strict';
100+
console.log('src', arguments[0]);
98101
return eval(arguments[0]);
99102
};
100103
}
101104
})`;
102105

103106
// The eval below is indirect, so that we are only in the global scope.
104107
// eslint-disable-next-line no-eval
105-
return (1, eval)(scopedEval)(sourceState.endowments)(src);
108+
return (1, eval)(scopedEval)(sourceState.endowments)(src);
106109
};
107110

108111
// We need to make this first so that it is available to the other evaluators.

packages/tame-metering/src/tame.js

+56-34
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,23 @@ const { get: wmGet, set: wmSet } = WeakMap.prototype;
1212

1313
const ObjectConstructor = Object;
1414

15-
let setGlobalMeter;
15+
let replaceGlobalMeter;
1616

1717
export default function tameMetering() {
18-
if (setGlobalMeter) {
18+
if (replaceGlobalMeter) {
1919
// Already installed.
20-
return setGlobalMeter;
20+
return replaceGlobalMeter;
2121
}
2222

23-
let globalMeter;
23+
let globalMeter = null;
2424
const wrapped = new WeakMap();
2525
const setWrapped = (...args) => apply(wmSet, wrapped, args);
2626
const getWrapped = (...args) => apply(wmGet, wrapped, args);
2727

28+
/*
29+
setWrapped(Error, Error); // FIGME: debugging
30+
setWrapped(console, console); // FIGME
31+
*/
2832
const wrapDescriptor = desc => {
2933
const newDesc = {};
3034
for (const [k, v] of entries(desc)) {
@@ -34,16 +38,11 @@ export default function tameMetering() {
3438
return newDesc;
3539
};
3640

37-
function wrap(target, deepMeter = globalMeter) {
41+
function wrap(target) {
3842
if (ObjectConstructor(target) !== target) {
3943
return target;
4044
}
4145

42-
const meter = globalMeter;
43-
if (target === meter) {
44-
return target;
45-
}
46-
4746
let wrapper = getWrapped(target);
4847
if (wrapper) {
4948
return wrapper;
@@ -52,33 +51,50 @@ export default function tameMetering() {
5251
if (typeof target === 'function') {
5352
// Meter the call to the function/constructor.
5453
wrapper = function meterFunction(...args) {
55-
// We first install no meter to make metering explicit.
56-
const userMeter = setGlobalMeter(null);
54+
// We're careful not to use the replaceGlobalMeter function as
55+
// it may consume some stack.
56+
// Instead, directly manipulate the globalMeter variable.
57+
const savedMeter = globalMeter;
5758
try {
58-
userMeter && userMeter[c.METER_ENTER]();
59+
// This is a common idiom to disable global metering so
60+
// that the savedMeter can use builtins without
61+
// recursively calling itself.
62+
63+
// Track the entry of the stack frame.
64+
globalMeter = null;
65+
// savedMeter && savedMeter[c.METER_ENTER](undefined, false);
5966
let ret;
60-
try {
61-
// Temporarily install the deep meter.
62-
setGlobalMeter(deepMeter);
63-
const newTarget = new.target;
64-
if (newTarget) {
65-
ret = construct(target, args, newTarget);
66-
} else {
67-
ret = apply(target, this, args);
68-
}
69-
} finally {
70-
// Resume explicit metering.
71-
setGlobalMeter(null);
67+
68+
// Reinstall the saved meter for the actual function invocation.
69+
globalMeter = savedMeter;
70+
const newTarget = new.target;
71+
if (newTarget) {
72+
ret = construct(target, args, newTarget);
73+
} else {
74+
ret = apply(target, this, args);
7275
}
73-
userMeter && userMeter[c.METER_ALLOCATE](ret);
76+
77+
// Track the allocation of the return value.
78+
globalMeter = null;
79+
savedMeter && savedMeter[c.METER_ALLOCATE](ret, false);
80+
7481
return ret;
7582
} catch (e) {
76-
userMeter && userMeter[c.METER_ALLOCATE](e);
83+
// Track the allocation of the exception value.
84+
globalMeter = null;
85+
savedMeter && savedMeter[c.METER_ALLOCATE](e, false);
7786
throw e;
7887
} finally {
79-
// Resume the user meter.
80-
userMeter && userMeter[c.METER_LEAVE]();
81-
setGlobalMeter(userMeter);
88+
// In case a try block consumes stack.
89+
globalMeter = savedMeter;
90+
try {
91+
// Declare we left the stack frame.
92+
globalMeter = null;
93+
// savedMeter && savedMeter[c.METER_LEAVE](undefined, false);
94+
} finally {
95+
// Resume the saved meter, if there was one.
96+
globalMeter = savedMeter;
97+
}
8298
}
8399
};
84100

@@ -105,13 +121,19 @@ export default function tameMetering() {
105121
}
106122

107123
// Override the globals with wrappers.
108-
wrap(globalThis, null);
124+
wrap(globalThis);
109125

110126
// Provide a way to set the meter.
111-
setGlobalMeter = m => {
127+
replaceGlobalMeter = m => {
112128
const oldMeter = globalMeter;
113-
globalMeter = m;
129+
if (m !== undefined) {
130+
// console.log('replacing', oldMeter, 'with', m, Error('here')); // FIGME
131+
globalMeter = m;
132+
}
133+
/* if (oldMeter === null) {
134+
console.log('returning', oldMeter, Error('here'));
135+
} */
114136
return oldMeter;
115137
};
116-
return setGlobalMeter;
138+
return replaceGlobalMeter;
117139
}

packages/transform-metering/README.md

+18-11
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,37 @@ This technique is not airtight, but it is at least is a best approximation in th
99
```js
1010
import SES from 'ses';
1111
import * as babelCore from '@babel/core';
12-
import { makeMeteredEvaluator } from '@agoric/transform-metering';
12+
import { makeMeter, makeMeteredEvaluator } from '@agoric/transform-metering';
1313
import tameMetering from '@agoric/tame-metering';
1414

1515
// Override all the global objects with metered versions.
16-
const setGlobalMeter = tameMetering();
16+
const replaceGlobalMeter = tameMetering();
17+
// TODO: Here is where `lockdown` would be run.
1718

1819
const meteredEval = makeMeteredEvaluator({
1920
// Needed for enabling metering of the global builtins.
20-
setGlobalMeter,
21+
replaceGlobalMeter,
2122
// Needed for source transforms that prevent runaways.
2223
babelCore,
2324
// ({ transforms }) => { evaluate(src, endowments = {}) { [eval function] } }
2425
makeEvaluator: SES.makeSESRootRealm, // TODO: change to new SES/Compartment API
26+
// Resolve a promise when the code inside the eval function is done evaluating.
27+
makeQuiescenceP: () => new Promise(res => setImmediate(() => res())),
2528
});
2629

2730
// Now use the returned meteredEval: it should not throw.
28-
const { exhausted, exceptionBox, returned } = meteredEval(untrustedSource, /* endowments */);
29-
if (exhausted) {
30-
console.log('the meter was exhausted');
31-
}
32-
if (exceptionBox) {
33-
console.log('the source threw exception', exceptionBox[0]);
34-
} else {
35-
console.log('the source returned', returned);
31+
// It also doesn't return until the code has quiesced.
32+
const { meter } = makeMeter();
33+
const { exhaustedP, exceptionBox, returned } = meteredEval(meter, untrustedSource, /* endowments */);
34+
exhaustedP.then(exhausted =>
35+
if (exhausted) {
36+
console.log('the meter was exhausted');
37+
}
38+
if (exceptionBox) {
39+
console.log('the source threw exception', exceptionBox[0]);
40+
} else {
41+
console.log('the source returned', returned);
42+
}
3643
}
3744
```
3845

packages/transform-metering/src/constants.js

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ export * from '@agoric/tame-metering/src/constants';
33
export const METER_COMBINED = '*';
44

55
export const DEFAULT_METER_ID = '$h\u200d_meter';
6+
export const DEFAULT_GET_METER_ID = '$h\u200d_meter_get';
7+
export const DEFAULT_SET_METER_ID = '$h\u200d_meter_set';
68
export const DEFAULT_REGEXP_ID_PREFIX = '$h\u200d_re_';
79

810
// Default metering values. These can easily be overridden in meter.js.
+44-21
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,62 @@
1-
import { makeWithMeter } from './with';
2-
import { makeMeterAndResetters } from './meter';
31
import { makeMeteringTransformer } from './transform';
42

53
export function makeMeteredEvaluator({
6-
setGlobalMeter,
4+
replaceGlobalMeter,
75
makeEvaluator,
86
babelCore,
9-
maxima,
7+
quiesceCallback = cb => cb(),
108
}) {
11-
const [meter, reset] = makeMeterAndResetters(maxima);
12-
const { meterId, meteringTransform } = makeMeteringTransformer(babelCore);
9+
const meteringTransform = makeMeteringTransformer(babelCore);
1310
const transforms = [meteringTransform];
14-
const { withMeter } = makeWithMeter(setGlobalMeter, meter);
1511

1612
const ev = makeEvaluator({ transforms });
1713

18-
return (src, endowments = {}) => {
19-
// Reset all meters to their defaults.
20-
Object.values(reset).forEach(r => r());
21-
endowments[meterId] = meter;
22-
let exhausted = true;
14+
return (meter, srcOrThunk, endowments = {}, whenQuiesced = undefined) => {
2315
let returned;
2416
let exceptionBox = false;
17+
18+
// Enable the specific meter.
19+
const savedMeter = replaceGlobalMeter(null);
2520
try {
26-
// Evaluate the source with the meter.
27-
returned = withMeter(() => ev.evaluate(src, { [meterId]: meter }));
28-
exhausted = false;
21+
if (whenQuiesced) {
22+
// Install the quiescence callback.
23+
quiesceCallback(() => {
24+
// console.log('quiescer exited');
25+
replaceGlobalMeter(savedMeter);
26+
whenQuiesced({
27+
exhausted: meter.isExhausted(),
28+
returned,
29+
exceptionBox,
30+
});
31+
});
32+
}
33+
34+
if (typeof srcOrThunk === 'string') {
35+
// Transform the source on our own budget, then evaluate against the meter.
36+
endowments.getGlobalMeter = m =>
37+
m === true ? meter : replaceGlobalMeter(m);
38+
returned = ev.evaluate(srcOrThunk, endowments);
39+
} else {
40+
// Evaluate the thunk with the specified meter.
41+
replaceGlobalMeter(meter);
42+
returned = srcOrThunk();
43+
}
2944
} catch (e) {
3045
exceptionBox = [e];
31-
exhausted = reset.isExhausted();
3246
}
33-
return {
34-
exhausted,
35-
returned,
36-
exceptionBox,
37-
};
47+
try {
48+
replaceGlobalMeter(savedMeter);
49+
const exhausted = meter.isExhausted();
50+
return {
51+
exhausted,
52+
returned,
53+
exceptionBox,
54+
};
55+
} finally {
56+
if (whenQuiesced) {
57+
// Keep going with the specified meter while we're quiescing.
58+
replaceGlobalMeter(meter);
59+
}
60+
}
3861
};
3962
}
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export { makeMeteredEvaluator } from './evaluator';
2-
export { makeMeter, makeMeterAndResetters } from './meter';
2+
export { makeMeter } from './meter';
33
export { makeMeteringTransformer } from './transform';
44
export { makeWithMeter } from './with';

0 commit comments

Comments
 (0)