From 15adc38228fe14dfac4a52a647b47d3013818aec Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Sun, 9 Feb 2020 01:08:04 -0600 Subject: [PATCH] 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. --- packages/evaluate/src/index.js | 7 +- packages/tame-metering/src/tame.js | 90 +++++---- packages/transform-metering/README.md | 29 +-- packages/transform-metering/src/constants.js | 2 + packages/transform-metering/src/evaluator.js | 65 ++++--- packages/transform-metering/src/index.js | 2 +- packages/transform-metering/src/meter.js | 104 +++++------ packages/transform-metering/src/transform.js | 80 +++++--- packages/transform-metering/src/with.js | 6 +- .../transform-metering/test/test-meter.js | 24 +-- packages/transform-metering/test/test-tame.js | 12 +- .../transform-metering/test/test-transform.js | 16 +- .../transform-metering/test/test-zzz-eval.js | 171 +++++++++++++++--- .../testdata/arrow-function-block/rewrite.js | 3 +- .../arrow-function-expression/rewrite.js | 3 +- .../testdata/classes/rewrite.js | 5 +- .../testdata/concise-method/rewrite.js | 5 +- .../testdata/for-loops/rewrite.js | 3 +- .../testdata/function-expression/rewrite.js | 3 +- .../testdata/regexp-literal/rewrite.js | 3 +- .../testdata/while-loops/rewrite.js | 5 +- 21 files changed, 428 insertions(+), 210 deletions(-) diff --git a/packages/evaluate/src/index.js b/packages/evaluate/src/index.js index 13ceed5a094..be76b4814a3 100644 --- a/packages/evaluate/src/index.js +++ b/packages/evaluate/src/index.js @@ -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 @@ -93,8 +94,10 @@ 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]); }; } @@ -102,7 +105,7 @@ export const makeEvaluators = (makerOptions = {}) => { // 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. diff --git a/packages/tame-metering/src/tame.js b/packages/tame-metering/src/tame.js index 9be41010157..08892e4692c 100644 --- a/packages/tame-metering/src/tame.js +++ b/packages/tame-metering/src/tame.js @@ -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)) { @@ -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; @@ -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; + } } }; @@ -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; } diff --git a/packages/transform-metering/README.md b/packages/transform-metering/README.md index b4ddb9a9da9..a4311bba15b 100644 --- a/packages/transform-metering/README.md +++ b/packages/transform-metering/README.md @@ -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); + } } ``` diff --git a/packages/transform-metering/src/constants.js b/packages/transform-metering/src/constants.js index eec8e5c13e6..bd9c0363012 100644 --- a/packages/transform-metering/src/constants.js +++ b/packages/transform-metering/src/constants.js @@ -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. diff --git a/packages/transform-metering/src/evaluator.js b/packages/transform-metering/src/evaluator.js index 31518964635..dd2dc295345 100644 --- a/packages/transform-metering/src/evaluator.js +++ b/packages/transform-metering/src/evaluator.js @@ -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); + } + } }; } diff --git a/packages/transform-metering/src/index.js b/packages/transform-metering/src/index.js index ea041fdda72..7fd51510ad2 100644 --- a/packages/transform-metering/src/index.js +++ b/packages/transform-metering/src/index.js @@ -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'; diff --git a/packages/transform-metering/src/meter.js b/packages/transform-metering/src/meter.js index f2f011bbb93..0645b973bf9 100644 --- a/packages/transform-metering/src/meter.js +++ b/packages/transform-metering/src/meter.js @@ -1,6 +1,4 @@ /* global BigInt */ -import harden from '@agoric/harden'; - import * as c from './constants'; const { isArray } = Array; @@ -28,43 +26,46 @@ const makeCounter = initBalance => { export function makeAborter() { let abortReason; - const maybeAbort = (reason = undefined) => { + const maybeAbort = (reason = undefined, throwForever = true) => { if (reason !== undefined) { // Set a new reason. - abortReason = harden(reason); + abortReason = reason; } - if (abortReason !== undefined) { + if (abortReason !== undefined && throwForever) { // Keep throwing the same reason. throw abortReason; } + return abortReason; }; maybeAbort.reset = () => (abortReason = undefined); - return harden(maybeAbort); + return maybeAbort; } export function makeComputeMeter(maybeAbort, meter, computeCounter = null) { if (computeCounter === null) { - return maybeAbort; + return (_cost = 1, throwForever = true) => { + maybeAbort(undefined, throwForever); + }; } - return (cost = 1) => { - maybeAbort(); - if (computeCounter(-cost) <= 0) { - throw maybeAbort(RangeError(`Compute meter exceeded`)); + return (cost = 1, throwForever = true) => { + const already = maybeAbort(undefined, throwForever); + if (!already && computeCounter(-cost) <= 0) { + maybeAbort(RangeError(`Compute meter exceeded`), throwForever); } }; } export function makeAllocateMeter(maybeAbort, meter, allocateCounter = null) { if (allocateCounter === null) { - return value => { - maybeAbort(); + return (value, throwForever = true) => { + maybeAbort(undefined, throwForever); return value; }; } - return value => { + return (value, throwForever = true) => { try { - meter[c.METER_ENTER](); - maybeAbort(); + const already = maybeAbort(undefined, throwForever); + // meter[c.METER_ENTER](undefined, throwForever); let cost = 1; if (value && ObjectConstructor(value) === value) { // Either an array or an object with properties. @@ -75,7 +76,7 @@ export function makeAllocateMeter(maybeAbort, meter, allocateCounter = null) { // Compute the number of own properties. // eslint-disable-next-line guard-for-in, no-unused-vars for (const p in getOwnPropertyDescriptors(value)) { - meter[c.METER_COMPUTE](); + meter[c.METER_COMPUTE](undefined, throwForever); cost += 1; } } @@ -94,7 +95,7 @@ export function makeAllocateMeter(maybeAbort, meter, allocateCounter = null) { remaining = -remaining; } while (remaining > bigIntZero) { - meter[c.METER_COMPUTE](); + meter[c.METER_COMPUTE](undefined, throwForever); remaining /= bigIntWord; cost += 1; } @@ -104,6 +105,7 @@ export function makeAllocateMeter(maybeAbort, meter, allocateCounter = null) { if (value !== null) { throw maybeAbort( TypeError(`Allocate meter found unexpected non-null object`), + throwForever, ); } // Otherwise, minimum cost. @@ -117,56 +119,59 @@ export function makeAllocateMeter(maybeAbort, meter, allocateCounter = null) { default: throw maybeAbort( TypeError(`Allocate meter found unrecognized type ${t}`), + throwForever, ); } } - if (allocateCounter(-cost) <= 0) { - throw maybeAbort(RangeError(`Allocate meter exceeded`)); + if (!already && allocateCounter(-cost, throwForever) <= 0) { + maybeAbort(RangeError(`Allocate meter exceeded`), throwForever); } return value; } finally { - meter[c.METER_LEAVE](); + // meter[c.METER_LEAVE](undefined, throwForever); } }; } export function makeStackMeter(maybeAbort, meter, stackCounter = null) { if (stackCounter === null) { - return _ => maybeAbort(); + return (_cost, throwForever = true) => { + maybeAbort(undefined, throwForever); + }; } - return (cost = 1) => { + return (cost = 1, throwForever = true) => { try { - maybeAbort(); - if (stackCounter(-cost) <= 0) { - throw maybeAbort(RangeError(`Stack meter exceeded`)); + meter[c.METER_COMPUTE](undefined, throwForever); + const already = maybeAbort(undefined, throwForever); + if (!already && stackCounter(-cost, throwForever) <= 0) { + maybeAbort(RangeError(`Stack meter exceeded`), throwForever); } - meter[c.METER_COMPUTE](); } catch (e) { - throw maybeAbort(e); + throw maybeAbort(e, throwForever); } }; } -export function makeMeterAndResetters(maxima = {}) { +export function makeMeter(budgets = {}) { let combinedCounter; const counter = (vname, dflt) => { - const max = vname in maxima ? maxima[vname] : c[dflt]; - if (max === true) { + const budget = vname in budgets ? budgets[vname] : c[dflt]; + if (budget === true) { if (!combinedCounter) { throw TypeError( - `A maxCombined value must be set to use the combined meter for ${vname}`, + `A budgetCombined value must be set to use the combined meter for ${vname}`, ); } return combinedCounter; } - return max === null ? null : makeCounter(max); + return budget === null ? null : makeCounter(budget); }; - combinedCounter = counter('maxCombined', 'DEFAULT_COMBINED_METER'); - const allocateCounter = counter('maxAllocate', 'DEFAULT_ALLOCATE_METER'); - const computeCounter = counter('maxCompute', 'DEFAULT_COMPUTE_METER'); - const stackCounter = counter('maxStack', 'DEFAULT_STACK_METER'); + combinedCounter = counter('budgetCombined', 'DEFAULT_COMBINED_METER'); + const allocateCounter = counter('budgetAllocate', 'DEFAULT_ALLOCATE_METER'); + const computeCounter = counter('budgetCompute', 'DEFAULT_COMPUTE_METER'); + const stackCounter = counter('budgetStack', 'DEFAULT_STACK_METER'); // Link all the meters together with the same aborter. const maybeAbort = makeAborter(); @@ -184,15 +189,12 @@ export function makeMeterAndResetters(maxima = {}) { cnt.reset(newBalance); } }; - const resetters = { - isExhausted() { - try { - maybeAbort(); - return undefined; - } catch (e) { - return e; - } - }, + const isExhausted = () => { + return maybeAbort(undefined, false); + }; + + const adminFacet = { + isExhausted, allocate: makeResetter(allocateCounter), stack: makeResetter(stackCounter), compute: makeResetter(computeCounter), @@ -204,12 +206,12 @@ export function makeMeterAndResetters(maxima = {}) { meter[c.METER_COMPUTE] = meterCompute; meter[c.METER_ENTER] = meterStack; meter[c.METER_LEAVE] = () => meterStack(-1); + meter.isExhausted = isExhausted; // Export the allocate meter with other meters as properties. Object.assign(meterAllocate, meter); - return [harden(meterAllocate), harden(resetters)]; -} - -export function makeMeter(maxima = {}) { - return makeMeterAndResetters(maxima)[0]; + return { + meter: meterAllocate, + adminFacet, + }; } diff --git a/packages/transform-metering/src/transform.js b/packages/transform-metering/src/transform.js index 9b19c6b4424..18e2dbd617d 100644 --- a/packages/transform-metering/src/transform.js +++ b/packages/transform-metering/src/transform.js @@ -1,5 +1,3 @@ -import harden from '@agoric/harden'; - import * as c from './constants'; // We'd like to import this, but RE2 is cjs @@ -11,8 +9,10 @@ export function makeMeteringTransformer( babelCore, { overrideParser = undefined, - overrideRegExp = harden(RE2), + overrideRegExp = RE2, overrideMeterId = c.DEFAULT_METER_ID, + overrideGetMeterId = c.DEFAULT_GET_METER_ID, + overrideSetMeterId = c.DEFAULT_SET_METER_ID, overrideRegExpIdPrefix = c.DEFAULT_REGEXP_ID_PREFIX, } = {}, ) { @@ -20,17 +20,32 @@ export function makeMeteringTransformer( ? overrideParser.parse || overrideParser : babelCore.parseSync; const meterId = overrideMeterId; + const getMeterId = overrideGetMeterId; + const setMeterId = overrideSetMeterId; const regexpIdPrefix = overrideRegExpIdPrefix; let regexpNumber = 0; const meteringPlugin = regexpList => ({ types: t }) => { - // Call [[meterId]][idString](...args) + // const [[meterId]] = [[getMeterId]](); + const getMeterDecl = () => { + const emid = t.Identifier(getMeterId); + const mid = t.Identifier(meterId); + emid[METER_GENERATED] = true; + mid[METER_GENERATED] = true; + return t.variableDeclaration('const', [ + t.variableDeclarator(mid, t.CallExpression(emid, [])), + ]); + }; + + // [[meterId]] && [[meterId]][idString](...args) const meterCall = (idString, args = []) => { const mid = t.Identifier(meterId); mid[METER_GENERATED] = true; - return t.CallExpression( - t.MemberExpression(mid, t.Identifier(idString)), - args, + + return t.logicalExpression( + '&&', + mid, + t.CallExpression(t.MemberExpression(mid, t.Identifier(idString)), args), ); }; @@ -58,15 +73,16 @@ export function makeMeteringTransformer( // Transform a body into a stack-metered try...finally block. const wrapWithStackMeter = tryBlock => { - tryBlock.body.unshift(t.ExpressionStatement(meterCall(c.METER_ENTER))); const finalizer = t.BlockStatement([ t.ExpressionStatement(meterCall(c.METER_LEAVE)), ]); finalizer[METER_GENERATED] = true; - const tryStatement = t.BlockStatement([ + const newBlock = t.BlockStatement([ + getMeterDecl(), + t.ExpressionStatement(meterCall(c.METER_ENTER)), t.TryStatement(tryBlock, null, finalizer), ]); - return tryStatement; + return newBlock; }; // Transform a body into a compute-metered block. @@ -79,7 +95,10 @@ export function makeMeteringTransformer( // Ensure meter identifiers are generated by us, or abort. Identifier(path) { if ( - (path.node.name === meterId || + (// FIGME path.node.name === meterId || + path.node.name === getMeterId || + path.node.name === 'getGlobalMeter' || + path.node.name === setMeterId || path.node.name.startsWith(regexpIdPrefix)) && !path.node[METER_GENERATED] ) { @@ -117,10 +136,12 @@ const ${reid}=RegExp(${JSON.stringify(pattern)},${JSON.stringify(flags)});`); // To prevent interception after exhaustion, wrap catch and finally. TryStatement(path) { if (path.node.handler) { - path.node.handler = wrapWithComputeMeter(path.node.handler); + path.node.handler.body = wrapWithComputeMeter(path.node.handler.body); } if (path.node.finalizer && !path.node.finalizer[METER_GENERATED]) { - path.node.finalizer = wrapWithComputeMeter(path.node.finalizer); + path.node.finalizer.body = wrapWithComputeMeter( + path.node.finalizer.body, + ); } }, // Function definitions need a stack meter, too. @@ -146,12 +167,16 @@ const ${reid}=RegExp(${JSON.stringify(pattern)},${JSON.stringify(flags)});`); const meteringTransform = { rewrite(ss) { const { src: source, endowments } = ss; - if (!endowments[meterId]) { + + if (!endowments.getGlobalMeter) { + // This flag turns on the metering. return ss; } - // Meter how much source code they want to use. - endowments[meterId][c.METER_COMPUTE](source.length); + // Bill the sources to the meter we'll use later. + const meter = endowments.getGlobalMeter(true); + // console.log('got meter from endowments', meter); + meter && meter[c.METER_COMPUTE](source.length); // Do the actual transform. const ast = parser(source); @@ -166,14 +191,18 @@ const ${reid}=RegExp(${JSON.stringify(pattern)},${JSON.stringify(flags)});`); }); // Meter by the regular expressions in use. - const reSource = regexpList.join(''); + const regexpSource = regexpList.join(''); + const preSource = `const ${getMeterId}=getGlobalMeter;const ${meterId}=${setMeterId}(true);\ +${meterId}&&${meterId}.${c.METER_ENTER}();\ +try{${regexpSource}`; + const postSource = `\n}finally{${setMeterId}(false);${meterId} && ${meterId}.${c.METER_LEAVE}();}`; // Force into an IIFE, if necessary. const maybeSource = output.code; const actualSource = ss.sourceType === 'expression' - ? `(function(){${reSource}return ${maybeSource}})()` - : `${reSource}${maybeSource}`; + ? `(function(){${preSource}return ${maybeSource}${postSource}})()` + : `${preSource}${maybeSource}${postSource}`; if (overrideRegExp) { // By default, override with RE2, which protects against @@ -181,7 +210,16 @@ const ${reid}=RegExp(${JSON.stringify(pattern)},${JSON.stringify(flags)});`); endowments.RegExp = overrideRegExp; } - // console.log('metered source:', actualSource); + // console.log('metered source:', `\n${actualSource}`); + + // Install the specified user meter. + // console.log('installing', meter); + const savedMeter = endowments.getGlobalMeter(); + endowments[setMeterId] = set => { + endowments.getGlobalMeter(set ? meter : savedMeter); + return meter; + }; + return { ...ss, ast, @@ -191,5 +229,5 @@ const ${reid}=RegExp(${JSON.stringify(pattern)},${JSON.stringify(flags)});`); }, }; - return { meterId, meteringTransform }; + return meteringTransform; } diff --git a/packages/transform-metering/src/with.js b/packages/transform-metering/src/with.js index 57dbe4cb665..657862fc2a0 100644 --- a/packages/transform-metering/src/with.js +++ b/packages/transform-metering/src/with.js @@ -1,11 +1,11 @@ -export function makeWithMeter(setGlobalMeter, defaultMeter = null) { +export function makeWithMeter(replaceGlobalMeter, defaultMeter = null) { const withMeter = (thunk, newMeter = defaultMeter) => { let oldMeter; try { - oldMeter = setGlobalMeter(newMeter); + oldMeter = replaceGlobalMeter(newMeter); return thunk(); } finally { - setGlobalMeter(oldMeter); + replaceGlobalMeter(oldMeter); } }; const withoutMeter = thunk => withMeter(thunk, null); diff --git a/packages/transform-metering/test/test-meter.js b/packages/transform-metering/test/test-meter.js index c3f2d482d1e..9e40db45928 100644 --- a/packages/transform-metering/test/test-meter.js +++ b/packages/transform-metering/test/test-meter.js @@ -29,12 +29,12 @@ const testAllExhausted = (t, meter, desc) => { test('meter running', async t => { try { - const meter = makeMeter({ maxCompute: 10 }); + const { meter } = makeMeter({ budgetCompute: 10 }); meter[c.METER_COMPUTE](9); t.throws(() => meter[c.METER_COMPUTE](), RangeError, 'compute exhausted'); testAllExhausted(t, meter, 'compute meter'); - const meter2 = makeMeter({ maxAllocate: 10 }); + const { meter: meter2 } = makeMeter({ budgetAllocate: 10 }); meter2[c.METER_ALLOCATE](new Array(8)); t.throws( () => meter2[c.METER_ALLOCATE]([]), @@ -43,7 +43,7 @@ test('meter running', async t => { ); testAllExhausted(t, meter2, 'allocate meter'); - const meter3 = makeMeter({ maxStack: 10 }); + const { meter: meter3 } = makeMeter({ budgetStack: 10 }); for (let i = 0; i < 9; i += 1) { meter3[c.METER_ENTER](); } @@ -65,29 +65,29 @@ test('meter running', async t => { test('meter running', async t => { try { t.throws( - () => makeMeter({ maxAllocate: true, maxCombined: null }), + () => makeMeter({ budgetAllocate: true, budgetCombined: null }), TypeError, 'missing combined allocate', ); t.throws( - () => makeMeter({ maxCompute: true, maxCombined: null }), + () => makeMeter({ budgetCompute: true, budgetCombined: null }), TypeError, 'missing combined compute', ); t.throws( - () => makeMeter({ maxStack: true, maxCombined: null }), + () => makeMeter({ budgetStack: true, budgetCombined: null }), TypeError, 'missing combined stack', ); // Try a combined meter. - const meter = makeMeter({ - maxAllocate: true, - maxCompute: true, - maxStack: true, - maxCombined: 10, + const { meter } = makeMeter({ + budgetAllocate: true, + budgetCompute: true, + budgetStack: true, + budgetCombined: 10, }); t.throws( () => { @@ -97,7 +97,7 @@ test('meter running', async t => { meter[c.METER_COMPUTE](); meter[c.METER_ENTER](); }, - RangeError, + /RangeError/, 'combined meter exhausted', ); testAllExhausted(t, meter, 'combined meter'); diff --git a/packages/transform-metering/test/test-tame.js b/packages/transform-metering/test/test-tame.js index ffaff672d5d..f3b7290ca5a 100644 --- a/packages/transform-metering/test/test-tame.js +++ b/packages/transform-metering/test/test-tame.js @@ -1,15 +1,15 @@ /* eslint-disable no-await-in-loop */ -import setGlobalMeter from '@agoric/tame-metering/src/install-global-metering'; +import replaceGlobalMeter from '@agoric/tame-metering/src/install-global-metering'; // eslint-disable-next-line import/order import test from 'tape-promise/tape'; -import { makeMeterAndResetters, makeWithMeter } from '../src/index'; +import { makeMeter, makeWithMeter } from '../src/index'; test('meter running', async t => { try { - const [meter, resetters] = makeMeterAndResetters({ maxCombined: 10 }); - const { withMeter, withoutMeter } = makeWithMeter(setGlobalMeter, meter); + const { meter, adminFacet } = makeMeter({ budgetCombined: 10 }); + const { withMeter, withoutMeter } = makeWithMeter(replaceGlobalMeter, meter); const withMeterFn = (thunk, newMeter = meter) => () => withMeter(thunk, newMeter); @@ -26,14 +26,14 @@ test('meter running', async t => { `withMeter works`, ); - resetters.combined(10); + adminFacet.combined(10); t.throws( withMeterFn(() => new Array(10)), RangeError, 'new Array exhausted', ); - resetters.combined(20); + adminFacet.combined(20); withMeter(() => { const a = new Array(10); withoutMeter(() => diff --git a/packages/transform-metering/test/test-transform.js b/packages/transform-metering/test/test-transform.js index 6cf7112087f..37eab6a4ae6 100644 --- a/packages/transform-metering/test/test-transform.js +++ b/packages/transform-metering/test/test-transform.js @@ -8,27 +8,29 @@ import * as c from '../src/constants'; test('meter transform', async t => { try { - const { meterId, meteringTransform } = makeMeteringTransformer(babelCore, { + let getGlobalMeter; + const meteringTransform = makeMeteringTransformer(babelCore, { overrideMeterId: '$m', overrideRegExpIdPrefix: '$re_', }); const rewrite = (source, testName) => { let cMeter; + getGlobalMeter = () => ({ + [c.METER_COMPUTE]: units => (cMeter = units), + }); + const ss = meteringTransform.rewrite({ src: source, - endowments: { - [meterId]: { - [c.METER_COMPUTE]: units => (cMeter = units), - }, - }, + endowments: { getGlobalMeter }, sourceType: 'script', }); + t.equals(cMeter, source.length, `compute meter updated ${testName}`); return ss.src; }; t.throws( - () => rewrite(`${meterId}.l()`, 'blacklisted meterId'), + () => rewrite(`$m.l()`, 'blacklisted meterId'), SyntaxError, 'meterId cannot appear in source', ); diff --git a/packages/transform-metering/test/test-zzz-eval.js b/packages/transform-metering/test/test-zzz-eval.js index 3ac39e47f61..c2da3465005 100644 --- a/packages/transform-metering/test/test-zzz-eval.js +++ b/packages/transform-metering/test/test-zzz-eval.js @@ -1,73 +1,184 @@ -// eslint-disable-next-line import/order -import setGlobalMeter from '@agoric/tame-metering/src/install-global-metering'; - +/* global globalThis */ /* eslint-disable no-await-in-loop */ + import test from 'tape-promise/tape'; import * as babelCore from '@babel/core'; import SES from 'ses'; +import { makeEvaluators } from '@agoric/evaluate'; + +import replaceGlobalMeter from '@agoric/tame-metering/src/install-global-metering'; +import { makeMeter, makeMeteredEvaluator } from '../src/index'; + +export const makeSESEvaluator = opts => + SES.makeSESRootRealm({ ...opts, consoleMode: 'allow' }); + +export const makeAgoricEvaluator = opts => { + const { evaluateProgram } = makeEvaluators(opts); + return { + evaluate(src, endowments = {}) { + return evaluateProgram(src, endowments); + }, + }; +}; + +export const makeLocalEvaluator = opts => ({ + evaluate(src, endowments = {}) { + // console.log('evaluating src', src); + const ss = opts.transforms.reduce( + (prior, t) => (t.rewrite ? t.rewrite(prior) : prior), + { src, endowments, sourceType: 'script' }, + ); -import { makeMeteredEvaluator } from '../src/index'; + globalThis.getGlobalMeter = ss.endowments.getGlobalMeter; + // eslint-disable-next-line global-require + globalThis.RegExp = require('re2'); + + // eslint-disable-next-line no-eval + return (1, eval)(ss.src); + }, +}); test('metering evaluator', async t => { + const rejectionHandler = (_e, _promise) => { + // console.log('have', e); + }; try { + process.on('unhandledRejection', rejectionHandler); + const { meter, adminFacet } = makeMeter(); + const makeEvaluator = makeSESEvaluator; // makeSESEvaluator; // ideal const meteredEval = makeMeteredEvaluator({ - setGlobalMeter, + replaceGlobalMeter, babelCore, - makeEvaluator: SES.makeSESRootRealm, + makeEvaluator, + quiesceCallback: cb => setTimeout(cb), }); // Destructure the output of the meteredEval. let exhaustedTimes = 0; + let expectedExhaustedTimes = 0; const myEval = src => { - const { exhausted, exceptionBox, returned } = meteredEval(src); - if (exhausted) { - exhaustedTimes += 1; - } - if (exceptionBox) { - throw exceptionBox[0]; - } - return returned; + Object.values(adminFacet).forEach(r => r()); + let whenQuiesced; + const whenQuiescedP = new Promise(res => (whenQuiesced = res)).then( + ({ exhausted, returned, exceptionBox }) => { + // console.log('quiesced returned', exhausted, exceptionBox, returned); + if (exhausted) { + exhaustedTimes += 1; + throw exhausted; + } + if (exceptionBox) { + throw exceptionBox[0]; + } + return returned; + }, + ); + // Defer the evaluation for another turn. + Promise.resolve() + .then(_ => meteredEval(meter, src, {}, whenQuiesced)) + .catch(_ => {}); + return whenQuiescedP; }; const src1 = `123; 456;`; - t.equals(myEval(src1), 456, 'trivial source succeeds'); + t.equals(await myEval(src1), 456, 'trivial source succeeds'); + + const failedToRejectUnderSES = () => { + // FIXME: This skips tests that aren't currently working under SES. + const times = makeEvaluator === makeSESEvaluator ? 0 : 1; + expectedExhaustedTimes += times; + return times === 0; + }; + + const src3a = `\ +Promise.resolve().then( + () => { + while(true) {} + }); +0`; + expectedExhaustedTimes += 1; + await t.rejects( + myEval(src3a), + /Compute meter exceeded/, + 'promised infinite loop exhausts', + ); + + const src5a = `\ +('x'.repeat(1e8), 0) +`; + failedToRejectUnderSES() || + (await t.rejects( + myEval(src5a), + /Allocate meter exceeded/, + 'big string exhausts', + )); + + const src5 = `\ +(new Array(1e8), 0) +`; + failedToRejectUnderSES() || + (await t.rejects( + myEval(src5), + /Allocate meter exceeded/, + 'big array exhausts', + )); const src2 = `\ -function f() { - f(); - return 1; +function f(a) { + return f(a + 1) + f(a + 2); } -f(); +f(1); `; - t.throws( - () => myEval(src2), + expectedExhaustedTimes += 1; + await t.rejects( + myEval(src2), /Stack meter exceeded/, - 'stack overflow fails', + 'stack overflow exhausts', ); const src3 = `\ while (true) {} `; - t.throws( - () => myEval(src3), + expectedExhaustedTimes += 1; + await t.rejects( + myEval(src3), /Compute meter exceeded/, - 'infinite loop fails', + 'infinite loop exhausts', + ); + + const src3b = `\ +(() => { while(true) {} })(); +`; + expectedExhaustedTimes += 1; + await t.rejects( + myEval(src3b), + /Compute meter exceeded/, + 'nested loop exhausts', ); const src4 = `\ /(x+x+)+y/.test('x'.repeat(10000)); `; - t.equals(myEval(src4), false, `catastrophic backtracking doesn't happen`); + t.equals( + await myEval(src4), + false, + `catastrophic backtracking doesn't happen`, + ); - const src5 = `\ -new Array(1e6).map(Object.create) + const src6 = `\ +new Array(1e8).map(Object.create); 0 `; - t.throws(() => myEval(src5), /Allocate meter exceeded/, 'long map fails'); + failedToRejectUnderSES() || + await t.rejects(myEval(src6), /Allocate meter exceeded/, 'long map exhausts'); - t.equals(exhaustedTimes, 3, `meter was exhausted as expected`); + t.equals( + exhaustedTimes, + expectedExhaustedTimes, + `meter was exhausted ${expectedExhaustedTimes} times`, + ); } catch (e) { t.isNot(e, e, 'unexpected exception'); } finally { + process.off('unhandledRejection', rejectionHandler); t.end(); } }); diff --git a/packages/transform-metering/testdata/arrow-function-block/rewrite.js b/packages/transform-metering/testdata/arrow-function-block/rewrite.js index 45cf0847546..ec6175c8853 100644 --- a/packages/transform-metering/testdata/arrow-function-block/rewrite.js +++ b/packages/transform-metering/testdata/arrow-function-block/rewrite.js @@ -1,2 +1,3 @@ -() => {try {$m.e(); +const $m=$h‍_enterMeter();try{() => {const $m = $h‍_enterMeter();try { f();} finally {$m.l();}}; +}finally{$m.l()} diff --git a/packages/transform-metering/testdata/arrow-function-expression/rewrite.js b/packages/transform-metering/testdata/arrow-function-expression/rewrite.js index 22ea97891d9..2be92f23662 100644 --- a/packages/transform-metering/testdata/arrow-function-expression/rewrite.js +++ b/packages/transform-metering/testdata/arrow-function-expression/rewrite.js @@ -1,2 +1,3 @@ -() => {try {$m.e();return ( +const $m=$h‍_enterMeter();try{() => {const $m = $h‍_enterMeter();try {return ( f());} finally {$m.l();}}; +}finally{$m.l()} diff --git a/packages/transform-metering/testdata/classes/rewrite.js b/packages/transform-metering/testdata/classes/rewrite.js index 02c66e60fa3..d2259919e19 100644 --- a/packages/transform-metering/testdata/classes/rewrite.js +++ b/packages/transform-metering/testdata/classes/rewrite.js @@ -1,4 +1,5 @@ -class Abc { - f() {try {$m.e(); +const $m=$h‍_enterMeter();try{class Abc { + f() {const $m = $h‍_enterMeter();try { return doit(); } finally {$m.l();}}} +}finally{$m.l()} diff --git a/packages/transform-metering/testdata/concise-method/rewrite.js b/packages/transform-metering/testdata/concise-method/rewrite.js index cb374fcfb77..0a9e7e5898d 100644 --- a/packages/transform-metering/testdata/concise-method/rewrite.js +++ b/packages/transform-metering/testdata/concise-method/rewrite.js @@ -1,4 +1,5 @@ -a = { - f() {try {$m.e(); +const $m=$h‍_enterMeter();try{a = { + f() {const $m = $h‍_enterMeter();try { doit(); } finally {$m.l();}} }; +}finally{$m.l()} diff --git a/packages/transform-metering/testdata/for-loops/rewrite.js b/packages/transform-metering/testdata/for-loops/rewrite.js index 27d136ec1f9..c24e7a1e22a 100644 --- a/packages/transform-metering/testdata/for-loops/rewrite.js +++ b/packages/transform-metering/testdata/for-loops/rewrite.js @@ -1,4 +1,4 @@ -for (const f of b) {$m.c(); +const $m=$h‍_enterMeter();try{for (const f of b) {$m.c(); doit(f);} for (const p in bar) {$m.c(); @@ -6,3 +6,4 @@ for (const p in bar) {$m.c(); for (let i = 0; i < 3; i++) {$m.c(); doit(i);} +}finally{$m.l()} diff --git a/packages/transform-metering/testdata/function-expression/rewrite.js b/packages/transform-metering/testdata/function-expression/rewrite.js index e5f1c7e6123..e16ba6a702a 100644 --- a/packages/transform-metering/testdata/function-expression/rewrite.js +++ b/packages/transform-metering/testdata/function-expression/rewrite.js @@ -1,3 +1,4 @@ -(function () {try {$m.e(); +const $m=$h‍_enterMeter();try{(function () {const $m = $h‍_enterMeter();try { f(); } finally {$m.l();}}); +}finally{$m.l()} diff --git a/packages/transform-metering/testdata/regexp-literal/rewrite.js b/packages/transform-metering/testdata/regexp-literal/rewrite.js index 62ab8a93a9b..ce032e44d23 100644 --- a/packages/transform-metering/testdata/regexp-literal/rewrite.js +++ b/packages/transform-metering/testdata/regexp-literal/rewrite.js @@ -1,3 +1,4 @@ -const $re_0=RegExp("^my-favourite-regexp","");if ($re_0.test('myf')) { +const $m=$h‍_enterMeter();try{const $re_0=RegExp("^my-favourite-regexp","");if ($re_0.test('myf')) { doit(); } +}finally{$m.l()} diff --git a/packages/transform-metering/testdata/while-loops/rewrite.js b/packages/transform-metering/testdata/while-loops/rewrite.js index da06dc6bfc7..634747267fe 100644 --- a/packages/transform-metering/testdata/while-loops/rewrite.js +++ b/packages/transform-metering/testdata/while-loops/rewrite.js @@ -1,4 +1,4 @@ -while (a) {$m.c();} +const $m=$h‍_enterMeter();try{while (a) {$m.c();} while (a) {$m.c();doit();} @@ -8,4 +8,5 @@ while (a) {$m.c(); do {$m.c(); doit();} while ( -a); \ No newline at end of file +a); +}finally{$m.l()}