Skip to content

Commit

Permalink
fix(evaluator): quiescence works
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
michaelfig committed Feb 11, 2020
1 parent db3acfd commit 15adc38
Show file tree
Hide file tree
Showing 21 changed files with 428 additions and 210 deletions.
7 changes: 5 additions & 2 deletions packages/evaluate/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import makeDefaultEvaluateOptions from '@agoric/default-evaluate-options';
export const makeEvaluators = (makerOptions = {}) => {
// Evaluate any shims, globally!
if (typeof globalThis === 'undefined') {
const myGlobal = typeof window === 'undefined' ? global : window;
// eslint-disable-next-line no-new-func
const myGlobal = Function('return this')();
myGlobal.globalThis = myGlobal;
}
// eslint-disable-next-line no-eval
Expand Down Expand Up @@ -93,16 +94,18 @@ export const makeEvaluators = (makerOptions = {}) => {
// The eval below is direct, so that we have access to the named endowments.
const scopedEval = `(function() {
with (arguments[0]) {
console.log('endow', arguments[0]);
return function() {
'use strict';
console.log('src', arguments[0]);
return eval(arguments[0]);
};
}
})`;

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

// We need to make this first so that it is available to the other evaluators.
Expand Down
90 changes: 56 additions & 34 deletions packages/tame-metering/src/tame.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,23 @@ const { get: wmGet, set: wmSet } = WeakMap.prototype;

const ObjectConstructor = Object;

let setGlobalMeter;
let replaceGlobalMeter;

export default function tameMetering() {
if (setGlobalMeter) {
if (replaceGlobalMeter) {
// Already installed.
return setGlobalMeter;
return replaceGlobalMeter;
}

let globalMeter;
let globalMeter = null;
const wrapped = new WeakMap();
const setWrapped = (...args) => apply(wmSet, wrapped, args);
const getWrapped = (...args) => apply(wmGet, wrapped, args);

/*
setWrapped(Error, Error); // FIGME: debugging
setWrapped(console, console); // FIGME
*/
const wrapDescriptor = desc => {
const newDesc = {};
for (const [k, v] of entries(desc)) {
Expand All @@ -34,16 +38,11 @@ export default function tameMetering() {
return newDesc;
};

function wrap(target, deepMeter = globalMeter) {
function wrap(target) {
if (ObjectConstructor(target) !== target) {
return target;
}

const meter = globalMeter;
if (target === meter) {
return target;
}

let wrapper = getWrapped(target);
if (wrapper) {
return wrapper;
Expand All @@ -52,33 +51,50 @@ export default function tameMetering() {
if (typeof target === 'function') {
// Meter the call to the function/constructor.
wrapper = function meterFunction(...args) {
// We first install no meter to make metering explicit.
const userMeter = setGlobalMeter(null);
// We're careful not to use the replaceGlobalMeter function as
// it may consume some stack.
// Instead, directly manipulate the globalMeter variable.
const savedMeter = globalMeter;
try {
userMeter && userMeter[c.METER_ENTER]();
// This is a common idiom to disable global metering so
// that the savedMeter can use builtins without
// recursively calling itself.

// Track the entry of the stack frame.
globalMeter = null;
// savedMeter && savedMeter[c.METER_ENTER](undefined, false);
let ret;
try {
// Temporarily install the deep meter.
setGlobalMeter(deepMeter);
const newTarget = new.target;
if (newTarget) {
ret = construct(target, args, newTarget);
} else {
ret = apply(target, this, args);
}
} finally {
// Resume explicit metering.
setGlobalMeter(null);

// Reinstall the saved meter for the actual function invocation.
globalMeter = savedMeter;
const newTarget = new.target;
if (newTarget) {
ret = construct(target, args, newTarget);
} else {
ret = apply(target, this, args);
}
userMeter && userMeter[c.METER_ALLOCATE](ret);

// Track the allocation of the return value.
globalMeter = null;
savedMeter && savedMeter[c.METER_ALLOCATE](ret, false);

return ret;
} catch (e) {
userMeter && userMeter[c.METER_ALLOCATE](e);
// Track the allocation of the exception value.
globalMeter = null;
savedMeter && savedMeter[c.METER_ALLOCATE](e, false);
throw e;
} finally {
// Resume the user meter.
userMeter && userMeter[c.METER_LEAVE]();
setGlobalMeter(userMeter);
// In case a try block consumes stack.
globalMeter = savedMeter;
try {
// Declare we left the stack frame.
globalMeter = null;
// savedMeter && savedMeter[c.METER_LEAVE](undefined, false);
} finally {
// Resume the saved meter, if there was one.
globalMeter = savedMeter;
}
}
};

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

// Override the globals with wrappers.
wrap(globalThis, null);
wrap(globalThis);

// Provide a way to set the meter.
setGlobalMeter = m => {
replaceGlobalMeter = m => {
const oldMeter = globalMeter;
globalMeter = m;
if (m !== undefined) {
// console.log('replacing', oldMeter, 'with', m, Error('here')); // FIGME
globalMeter = m;
}
/* if (oldMeter === null) {
console.log('returning', oldMeter, Error('here'));
} */
return oldMeter;
};
return setGlobalMeter;
return replaceGlobalMeter;
}
29 changes: 18 additions & 11 deletions packages/transform-metering/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,37 @@ This technique is not airtight, but it is at least is a best approximation in th
```js
import SES from 'ses';
import * as babelCore from '@babel/core';
import { makeMeteredEvaluator } from '@agoric/transform-metering';
import { makeMeter, makeMeteredEvaluator } from '@agoric/transform-metering';
import tameMetering from '@agoric/tame-metering';

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

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

// Now use the returned meteredEval: it should not throw.
const { exhausted, exceptionBox, returned } = meteredEval(untrustedSource, /* endowments */);
if (exhausted) {
console.log('the meter was exhausted');
}
if (exceptionBox) {
console.log('the source threw exception', exceptionBox[0]);
} else {
console.log('the source returned', returned);
// It also doesn't return until the code has quiesced.
const { meter } = makeMeter();
const { exhaustedP, exceptionBox, returned } = meteredEval(meter, untrustedSource, /* endowments */);
exhaustedP.then(exhausted =>
if (exhausted) {
console.log('the meter was exhausted');
}
if (exceptionBox) {
console.log('the source threw exception', exceptionBox[0]);
} else {
console.log('the source returned', returned);
}
}
```
Expand Down
2 changes: 2 additions & 0 deletions packages/transform-metering/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ export * from '@agoric/tame-metering/src/constants';
export const METER_COMBINED = '*';

export const DEFAULT_METER_ID = '$h\u200d_meter';
export const DEFAULT_GET_METER_ID = '$h\u200d_meter_get';
export const DEFAULT_SET_METER_ID = '$h\u200d_meter_set';
export const DEFAULT_REGEXP_ID_PREFIX = '$h\u200d_re_';

// Default metering values. These can easily be overridden in meter.js.
Expand Down
65 changes: 44 additions & 21 deletions packages/transform-metering/src/evaluator.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,62 @@
import { makeWithMeter } from './with';
import { makeMeterAndResetters } from './meter';
import { makeMeteringTransformer } from './transform';

export function makeMeteredEvaluator({
setGlobalMeter,
replaceGlobalMeter,
makeEvaluator,
babelCore,
maxima,
quiesceCallback = cb => cb(),
}) {
const [meter, reset] = makeMeterAndResetters(maxima);
const { meterId, meteringTransform } = makeMeteringTransformer(babelCore);
const meteringTransform = makeMeteringTransformer(babelCore);
const transforms = [meteringTransform];
const { withMeter } = makeWithMeter(setGlobalMeter, meter);

const ev = makeEvaluator({ transforms });

return (src, endowments = {}) => {
// Reset all meters to their defaults.
Object.values(reset).forEach(r => r());
endowments[meterId] = meter;
let exhausted = true;
return (meter, srcOrThunk, endowments = {}, whenQuiesced = undefined) => {
let returned;
let exceptionBox = false;

// Enable the specific meter.
const savedMeter = replaceGlobalMeter(null);
try {
// Evaluate the source with the meter.
returned = withMeter(() => ev.evaluate(src, { [meterId]: meter }));
exhausted = false;
if (whenQuiesced) {
// Install the quiescence callback.
quiesceCallback(() => {
// console.log('quiescer exited');
replaceGlobalMeter(savedMeter);
whenQuiesced({
exhausted: meter.isExhausted(),
returned,
exceptionBox,
});
});
}

if (typeof srcOrThunk === 'string') {
// Transform the source on our own budget, then evaluate against the meter.
endowments.getGlobalMeter = m =>
m === true ? meter : replaceGlobalMeter(m);
returned = ev.evaluate(srcOrThunk, endowments);
} else {
// Evaluate the thunk with the specified meter.
replaceGlobalMeter(meter);
returned = srcOrThunk();
}
} catch (e) {
exceptionBox = [e];
exhausted = reset.isExhausted();
}
return {
exhausted,
returned,
exceptionBox,
};
try {
replaceGlobalMeter(savedMeter);
const exhausted = meter.isExhausted();
return {
exhausted,
returned,
exceptionBox,
};
} finally {
if (whenQuiesced) {
// Keep going with the specified meter while we're quiescing.
replaceGlobalMeter(meter);
}
}
};
}
2 changes: 1 addition & 1 deletion packages/transform-metering/src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { makeMeteredEvaluator } from './evaluator';
export { makeMeter, makeMeterAndResetters } from './meter';
export { makeMeter } from './meter';
export { makeMeteringTransformer } from './transform';
export { makeWithMeter } from './with';
Loading

0 comments on commit 15adc38

Please sign in to comment.