diff --git a/packages/SwingSet/src/controller/initializeSwingset.js b/packages/SwingSet/src/controller/initializeSwingset.js index 89989358724..e3f48c3950a 100644 --- a/packages/SwingSet/src/controller/initializeSwingset.js +++ b/packages/SwingSet/src/controller/initializeSwingset.js @@ -237,10 +237,7 @@ export async function loadSwingsetConfigFile(configPath) { await null; try { const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - const referrer = new URL( - configPath, - `file:///${process.cwd()}/`, - ).toString(); + const referrer = new URL(configPath, `file://${process.cwd()}/`).toString(); await normalizeConfigDescriptor(config.vats, referrer, true); await normalizeConfigDescriptor(config.bundles, referrer, false); // await normalizeConfigDescriptor(config.devices, referrer, true); // TODO: represent devices diff --git a/packages/SwingSet/src/kernel/kernel.js b/packages/SwingSet/src/kernel/kernel.js index 99a0b22533c..c99e95d0d75 100644 --- a/packages/SwingSet/src/kernel/kernel.js +++ b/packages/SwingSet/src/kernel/kernel.js @@ -1208,7 +1208,8 @@ export default function buildKernel( */ async function processDeliveryMessage(message) { kdebug(''); - kdebug(`processQ ${JSON.stringify(message)}`); + // prettier-ignore + kdebug(`processQ crank ${kernelKeeper.getCrankNumber()} ${JSON.stringify(message)}`); kdebug(legibilizeMessage(message)); kernelSlog.write({ type: 'crank-start', @@ -1366,8 +1367,9 @@ export default function buildKernel( */ async function processAcceptanceMessage(message) { kdebug(''); - kdebug(`processAcceptanceQ ${JSON.stringify(message)}`); - kdebug(legibilizeMessage(message)); + // prettier-ignore + kdebug(`processAcceptanceQ crank ${kernelKeeper.getCrankNumber()} ${message.type}`); + // kdebug(legibilizeMessage(message)); kernelSlog.write({ type: 'crank-start', crankType: 'routing', diff --git a/packages/boot/test/bootstrapTests/bench-vaults-performance.js b/packages/boot/test/bootstrapTests/bench-vaults-performance.js index 391a09c8c69..5b54dd38a17 100644 --- a/packages/boot/test/bootstrapTests/bench-vaults-performance.js +++ b/packages/boot/test/bootstrapTests/bench-vaults-performance.js @@ -70,7 +70,7 @@ const collateralBrandKey = 'ATOM'; const makeDefaultTestContext = async t => { console.time('DefaultTestContext'); - const swingsetTestKit = await makeSwingsetTestKit(t); + const swingsetTestKit = await makeSwingsetTestKit(t, 'bundles/vaults'); const { runUtils, storage } = swingsetTestKit; console.timeLog('DefaultTestContext', 'swingsetTestKit'); @@ -129,6 +129,15 @@ async function stressVaults(t, dumpHeap) { const { walletFactoryDriver } = t.context; const wd = await walletFactoryDriver.provideSmartWallet('agoric1open'); + await walletFactoryDriver.provideSmartWallet( + 'agoric1ldmtatp24qlllgxmrsjzcpe20fvlkp448zcuce', + ); + await walletFactoryDriver.provideSmartWallet( + 'agoric140dmkrz2e42ergjj7gyvejhzmjzurvqeq82ang', + ); + await walletFactoryDriver.provideSmartWallet( + 'agoric1w8wktaur4zf8qmmtn3n7x3r0jhsjkjntcm3u6h', + ); /** * @param {number} i diff --git a/packages/boot/test/bootstrapTests/bench-vaults-stripped.js b/packages/boot/test/bootstrapTests/bench-vaults-stripped.js new file mode 100644 index 00000000000..9f97b21aad7 --- /dev/null +++ b/packages/boot/test/bootstrapTests/bench-vaults-stripped.js @@ -0,0 +1,85 @@ +// This is a version of bench-vaults-performance.js that has had all intrinsic +// performance measurement code stripped out of it, purely implementing the +// vault benchmark test code in expectation of extrinsic measurement. + +import { test as unknownTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; + +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; +import { makeAgoricNamesRemotesFromFakeStorage } from '@agoric/vats/tools/board-utils.js'; +import { makeSwingsetTestKit } from './supports.js'; +import { makeWalletFactoryDriver } from './drivers.js'; + +// presently all these tests use one collateral manager +const collateralBrandKey = 'ATOM'; + +const makeDefaultTestContext = async t => { + const swingsetTestKit = await makeSwingsetTestKit(t); + + const { runUtils, storage: chainStorage } = swingsetTestKit; + const { EV } = runUtils; + + // Wait for ATOM to make it into agoricNames + // E(bootstrap).consumeItem('vaultFactoryKit'); + await EV.vat('bootstrap').consumeItem('vaultFactoryKit'); + + // has to be late enough for agoricNames data to have been published + const agoricNamesRemotes = + makeAgoricNamesRemotesFromFakeStorage(chainStorage); + + const walletFactoryDriver = await makeWalletFactoryDriver( + runUtils, + chainStorage, + agoricNamesRemotes, + ); + + return { ...swingsetTestKit, walletFactoryDriver }; +}; + +/** @type {import('ava').TestFn>>} */ +const test = unknownTest; + +test.before(async t => { + t.context = await makeDefaultTestContext(t); +}); +test.after.always(t => t.context.shutdown()); + +test.serial('stress vaults', async t => { + const { walletFactoryDriver } = t.context; + await walletFactoryDriver.provideSmartWallet( + 'agoric1ldmtatp24qlllgxmrsjzcpe20fvlkp448zcuce', + ); + await walletFactoryDriver.provideSmartWallet( + 'agoric140dmkrz2e42ergjj7gyvejhzmjzurvqeq82ang', + ); + await walletFactoryDriver.provideSmartWallet( + 'agoric1w8wktaur4zf8qmmtn3n7x3r0jhsjkjntcm3u6h', + ); + + const wd = await walletFactoryDriver.provideSmartWallet('agoric1open'); + + const openVault = async (i, n) => { + const offerId = `open-vault-${i}-of-${n}`; + await wd.executeOfferMaker(Offers.vaults.OpenVault, { + offerId, + collateralBrandKey, + wantMinted: 5, + giveCollateral: 1.0, + }); + + t.like(wd.getLatestUpdateRecord(), { + updated: 'offerStatus', + status: { id: offerId, numWantsSatisfied: 1 }, + }); + }; + + const openN = async n => { + const range = [...Array(n)].map((_, i) => i + 1); + await Promise.all(range.map(i => openVault(i, n))); + }; + + await openN(1); + await eventLoopIteration(); + t.pass(); +}); diff --git a/packages/boot/test/bootstrapTests/supports.js b/packages/boot/test/bootstrapTests/supports.js index 03363b8bd96..f1c8b4fef53 100644 --- a/packages/boot/test/bootstrapTests/supports.js +++ b/packages/boot/test/bootstrapTests/supports.js @@ -426,7 +426,7 @@ export const makeSwingsetTestKit = async ( bridgeOutbound, kernelStorage, configPath, - {}, + [], {}, { debugName: 'TESTBOOT' }, ); diff --git a/packages/inter-protocol/src/proposals/addAssetToVault.js b/packages/inter-protocol/src/proposals/addAssetToVault.js index 3b632b27f92..cbe39b02ff3 100644 --- a/packages/inter-protocol/src/proposals/addAssetToVault.js +++ b/packages/inter-protocol/src/proposals/addAssetToVault.js @@ -293,9 +293,7 @@ export const getManifestForAddAssetToVault = ( consume: { bankManager: true, agoricNamesAdmin: true, - bankMints: true, reserveKit: true, - vBankKits: true, startUpgradable: true, }, produce: { bankMints: true, vBankKits: true }, @@ -310,7 +308,6 @@ export const getManifestForAddAssetToVault = ( startUpgradable: true, priceAuthorityAdmin: true, priceAuthority: true, - scaledPriceAuthorityKits: true, }, produce: { scaledPriceAuthorityKits: true, diff --git a/packages/inter-protocol/src/proposals/committee-proposal.js b/packages/inter-protocol/src/proposals/committee-proposal.js index ea61923f639..8ebe07367f9 100644 --- a/packages/inter-protocol/src/proposals/committee-proposal.js +++ b/packages/inter-protocol/src/proposals/committee-proposal.js @@ -30,12 +30,10 @@ export const inviteCommitteeMembers = async ( const distributeInvitations = async addrInvitations => { await Promise.all( addrInvitations.map(async ([addr, invitationP]) => { - await reserveThenDeposit( - `econ committee member ${addr}`, - namesByAddressAdmin, - addr, - [invitationP], - ); + const debugName = `econ committee member ${addr}`; + await reserveThenDeposit(debugName, namesByAddressAdmin, addr, [ + invitationP, + ]).catch(err => console.error(`failed deposit to ${debugName}`, err)); if (highPrioritySendersManager) { await E(highPrioritySendersManager).add( EC_HIGH_PRIORITY_SENDERS_NAMESPACE, @@ -46,7 +44,9 @@ export const inviteCommitteeMembers = async ( ); }; - await distributeInvitations(zip(values(voterAddresses), invitations)); + // This doesn't resolve until the committee members create their smart wallets. + // Don't block bootstrap on it. + void distributeInvitations(zip(values(voterAddresses), invitations)); }; harden(inviteCommitteeMembers); @@ -135,15 +135,15 @@ export const inviteToEconCharter = async ( ) => { const { creatorFacet } = E.get(econCharterKit); - await Promise.all( - values(voterAddresses).map(async addr => - reserveThenDeposit( - `econ charter member ${addr}`, - namesByAddressAdmin, - addr, - [E(creatorFacet).makeCharterMemberInvitation()], - ), - ), + // This doesn't resolve until the committee members create their smart wallets. + // Don't block bootstrap on it. + void Promise.all( + values(voterAddresses).map(async addr => { + const debugName = `econ charter member ${addr}`; + reserveThenDeposit(debugName, namesByAddressAdmin, addr, [ + E(creatorFacet).makeCharterMemberInvitation(), + ]).catch(err => console.error(`failed deposit to ${debugName}`, err)); + }), ); }; @@ -176,8 +176,6 @@ export const getManifestForInviteCommittee = async ( [addGovernorsToEconCharter.name]: { consume: { auctioneerKit: t, - reserveGovernorCreatorFacet: t, - vaultFactoryGovernorCreator: t, econCharterKit: t, zoe: t, agoricNames: t, diff --git a/packages/inter-protocol/src/proposals/price-feed-proposal.js b/packages/inter-protocol/src/proposals/price-feed-proposal.js index 4a38110d6b2..5919c231d89 100644 --- a/packages/inter-protocol/src/proposals/price-feed-proposal.js +++ b/packages/inter-protocol/src/proposals/price-feed-proposal.js @@ -218,16 +218,16 @@ export const createPriceFeed = async ( */ const addOracle = async addr => { const invitation = await E(faKit.creatorFacet).makeOracleInvitation(addr); - await reserveThenDeposit( - `${AGORIC_INSTANCE_NAME} member ${addr}`, - namesByAddressAdmin, - addr, - [invitation], - ); + const debugName = `${AGORIC_INSTANCE_NAME} member ${addr}`; + await reserveThenDeposit(debugName, namesByAddressAdmin, addr, [ + invitation, + ]).catch(err => console.error(`failed deposit to ${debugName}`, err)); }; trace('distributing invitations', oracleAddresses); - await Promise.all(oracleAddresses.map(addOracle)); + // This doesn't resolve until oracle operators create their smart wallets. + // Don't block bootstrap on it. + void Promise.all(oracleAddresses.map(addOracle)); trace('createPriceFeed complete'); }; @@ -252,7 +252,6 @@ export const getManifestForPriceFeed = async ( chainStorage: t, chainTimerService: t, client: t, - contractGovernor: t, econCharterKit: t, economicCommitteeCreatorFacet: t, highPrioritySendersManager: t, diff --git a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js index f1c974ee26d..7850e7a531a 100644 --- a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js +++ b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js @@ -263,6 +263,7 @@ test.serial('admin price', async t => { [operatorAddress], ); const wallet = oracleWallets[operatorAddress]; + await eventLoopIteration(); const adminOfferId = await acceptInvitation(wallet, governedPriceAggregator); // Push a new price result ///////////////////////// @@ -300,6 +301,7 @@ test.serial('errors', async t => { const { oracleWallets, governedPriceAggregator: priceAggregator } = await setupFeedWithWallets(t, [operatorAddress]); const wallet = oracleWallets[operatorAddress]; + await eventLoopIteration(); const adminOfferId = await acceptInvitation(wallet, priceAggregator); // TODO move to smart-wallet package when it has sufficient test supports diff --git a/packages/swingset-runner/demo/vaultPerfTest/vat-benchmark.js b/packages/swingset-runner/demo/vaultPerfTest/vat-benchmark.js new file mode 100644 index 00000000000..69b82ba7dc8 --- /dev/null +++ b/packages/swingset-runner/demo/vaultPerfTest/vat-benchmark.js @@ -0,0 +1,108 @@ +// In-swingset implementation of the vault performance benchmark from +// packages/boot/test/bootstrapTests/bench-vaults-performance.js +// +// Run with: +// +// runner --usebundlecache --chain --sbench demo/vaultPerfTest/vat-benchmark.js --benchmark ${ROUNDS} --config ../vm-config/decentral-itest-vaults-config.json run +// +// NOTE: The above incantation assumes running from the root directory of the +// swingset-runner package. If running from elsewhere, adjust the paths to +// vat-benchmark.js driver and the JSON config file (and possibly the runner +// itself, if it's not on your $PATH) accordingly. + +import { E } from '@endo/eventual-send'; +import { Far } from '@endo/marshal'; +import { assert } from '@agoric/assert'; + +import { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; + +const log = console.log; + +const collateralBrandKey = 'ATOM'; + +export function buildRootObject() { + let benchmarkRounds = 0; + let wallet; + let walletOffersFacet; + let brand; + let subscriber; + + return Far('root', { + async setup(vats, devices) { + // XXX TODO: Some of this setup logic will be common to perhaps many + // benchmarks. The common portions should be factored out into a + // benchmark support library as we learn from experience which are the + // common parts and which are the benchmark-specific parts + + log( + `benchmark setup(${JSON.stringify(vats)}, ${JSON.stringify(devices)})`, + ); + await E(vats.bootstrap).consumeItem('vaultFactoryKit'); + + const walletFactoryStartResult = await E(vats.bootstrap).consumeItem( + 'walletFactoryStartResult', + ); + const agoricNames = await E(vats.bootstrap).consumeItem('agoricNames'); + const atomBrand = await E(agoricNames).lookup('brand', 'ATOM'); + const istBrand = await E(agoricNames).lookup('brand', 'IST'); + brand = { + ATOM: atomBrand, + IST: istBrand, + }; + + const bankManager = await E(vats.bootstrap).consumeItem('bankManager'); + const namesByAddressAdmin = await E(vats.bootstrap).consumeItem( + 'namesByAddressAdmin', + ); + + async function provideSmartWallet(walletAddress) { + const bank = await E(bankManager).getBankForAddress(walletAddress); + return E(walletFactoryStartResult.creatorFacet) + .provideSmartWallet(walletAddress, bank, namesByAddressAdmin) + .then(([walletPresence]) => walletPresence); + } + + await provideSmartWallet('agoric1ldmtatp24qlllgxmrsjzcpe20fvlkp448zcuce'); + await provideSmartWallet('agoric140dmkrz2e42ergjj7gyvejhzmjzurvqeq82ang'); + await provideSmartWallet('agoric1w8wktaur4zf8qmmtn3n7x3r0jhsjkjntcm3u6h'); + wallet = await provideSmartWallet('agoric1open'); + walletOffersFacet = await E(wallet).getOffersFacet(); + + const topics = await E(wallet).getPublicTopics(); + subscriber = topics.updates.subscriber; + }, + async runBenchmarkRound() { + benchmarkRounds += 1; + log(`runBenchmarkRound #${benchmarkRounds}`); + + const openVault = async (i, n) => { + const offerId = `open-vault-${i}-of-${n}-round-${benchmarkRounds}`; + const offer = Offers.vaults.OpenVault( + { brand }, + { + offerId, + collateralBrandKey, + wantMinted: 5, + giveCollateral: 1.0, + }, + ); + + await E(walletOffersFacet).executeOffer(offer); + + const upd = await E(subscriber).getUpdateSince(); + assert( + upd.value.updated === 'offerStatus' && + upd.value.status.id === offerId && + upd.value.status.numWantsSatisfied === 1, + ); + }; + + const openN = async n => { + const range = [...Array(n)].map((_, i) => i + 1); + await Promise.all(range.map(i => openVault(i, n))); + }; + + await openN(1); + }, + }); +} diff --git a/packages/swingset-runner/package.json b/packages/swingset-runner/package.json index 9fa9237983c..da523992125 100644 --- a/packages/swingset-runner/package.json +++ b/packages/swingset-runner/package.json @@ -20,8 +20,10 @@ }, "dependencies": { "@agoric/assert": "^0.6.0", + "@agoric/deploy-script-support": "^0.10.3", "@agoric/ertp": "^0.16.2", "@agoric/internal": "^0.3.2", + "@agoric/inter-protocol": "^0.16.1", "@agoric/stat-logger": "^0.4.28", "@agoric/store": "^0.9.2", "@agoric/swing-store": "^0.9.1", diff --git a/packages/swingset-runner/src/chain.js b/packages/swingset-runner/src/chain.js new file mode 100644 index 00000000000..d957f5bee63 --- /dev/null +++ b/packages/swingset-runner/src/chain.js @@ -0,0 +1,190 @@ +import { buildBridge } from '@agoric/swingset-vat'; +import { BridgeId, VBankAccount } from '@agoric/internal'; +import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js'; +import * as STORAGE_PATH from '@agoric/internal/src/chain-storage-paths.js'; +import { extractCoreProposalBundles } from '@agoric/deploy-script-support/src/extract-proposal.js'; + +const { Fail, quote: q } = assert; + +/** + * Export any specified storage subtrees, then delete the ones marked to clear. + * + * @param {(method: string, args: unknown[]) => any} batchChainStorage send a + * batch command over the chain storage bridge + * @param {string[]} [exportStorageSubtrees] chain storage paths identifying + * roots of subtrees for which data should be exported into bootstrap vat + * parameter `chainStorageEntries` (e.g., `exportStorageSubtrees: ['c.o']` + * might result in vatParameters including `chainStorageEntries: [ ['c.o', + * '"top"'], ['c.o.i'], ['c.o.i.n', '42'], ['c.o.w', '"moo"'] ]`). + * @param {string[]} [clearStorageSubtrees] chain storage paths identifying + * roots of subtrees for which data should be deleted (TODO: including + * overlaps with exportStorageSubtrees, which are *not* preserved). + */ +function exportStorage( + batchChainStorage, + exportStorageSubtrees = [], + clearStorageSubtrees = [], +) { + const chainStorageEntries = []; + + const isInSubtree = (path, root) => + path === root || path.startsWith(`${root}.`); + + const singleChainStorage = (method, path) => + batchChainStorage(method, [path]); + + { + // Disallow exporting internal details like bundle contents and the action queue. + const exportRoot = STORAGE_PATH.CUSTOM; + const badPaths = exportStorageSubtrees.filter( + path => !isInSubtree(path, exportRoot), + ); + badPaths.length === 0 || + // prettier-ignore + Fail`Exported chain storage paths ${q(badPaths)} must start with ${q(exportRoot)}`; + + const makeExportEntry = (path, value) => + value == null ? [path] : [path, value]; + + // Preserve the ordering of each subtree via depth-first traversal. + let pendingEntries = exportStorageSubtrees.map(path => { + const value = singleChainStorage('get', path); + return makeExportEntry(path, value); + }); + while (pendingEntries.length > 0) { + const entry = /** @type {[string, string?]} */ (pendingEntries.shift()); + chainStorageEntries.push(entry); + const [path, _value] = entry; + const childEntryData = singleChainStorage('entries', path); + const childEntries = childEntryData.map(([pathSegment, value]) => { + return makeExportEntry(`${path}.${pathSegment}`, value); + }); + pendingEntries = [...childEntries, ...pendingEntries]; + } + } + + { + // Clear other chain storage data as configured. + // NOTE THAT WE DO NOT LIMIT THIS TO THE CUSTOM PATH! + // USE AT YOUR OWN RISK! + const pathsToClear = []; + const batchThreshold = 100; + const sendBatch = () => { + // Consume pathsToClear and map each path to a no-value entry. + const args = pathsToClear.splice(0).map(path => [path]); + batchChainStorage('setWithoutNotify', args); + }; + let pathsToCheck = [...clearStorageSubtrees]; + while (pathsToCheck.length > 0) { + const path = pathsToCheck.shift(); + pathsToClear.push(path); + if (pathsToClear.length >= batchThreshold) { + sendBatch(); + } + const childPaths = singleChainStorage('children', path).map( + segment => `${path}.${segment}`, + ); + pathsToCheck = [...childPaths, ...pathsToCheck]; + } + if (pathsToClear.length > 0) { + sendBatch(); + } + } + + return chainStorageEntries; +} + +export async function initEmulatedChain(config, configPath) { + const chainStorage = makeFakeStorageKit('swingset-runner'); + let lastNonce = 0n; + + // cribbed from packages/vats/test/bootstrapTests/support.js + function bridgeOutbound(bridgeId, obj) { + switch (bridgeId) { + case BridgeId.BANK: { + // bridgeOutbound bank : { + // moduleName: 'vbank/reserve', + // type: 'VBANK_GET_MODULE_ACCOUNT_ADDRESS' + // } + switch (obj.type) { + case 'VBANK_GET_MODULE_ACCOUNT_ADDRESS': { + const { moduleName } = obj; + const moduleDescriptor = Object.values(VBankAccount).find( + ({ module }) => module === moduleName, + ); + if (!moduleDescriptor) { + return 'undefined'; + } + return moduleDescriptor.address; + } + + // Observed message: + // address: 'agoric1megzytg65cyrgzs6fvzxgrcqvwwl7ugpt62346', + // denom: 'ibc/toyatom', + // type: 'VBANK_GET_BALANCE' + case 'VBANK_GET_BALANCE': { + // TODO consider letting config specify vbank assets + // empty balances for test. + return '0'; + } + + case 'VBANK_GRAB': + case 'VBANK_GIVE': { + lastNonce += 1n; + // Also empty balances. + return harden({ + type: 'VBANK_BALANCE_UPDATE', + nonce: `${lastNonce}`, + updated: [], + }); + } + + default: { + return 'undefined'; + } + } + } + case BridgeId.CORE: + case BridgeId.DIBC: + case BridgeId.PROVISION: + case BridgeId.PROVISION_SMART_WALLET: + case BridgeId.WALLET: + console.warn('Bridge returning undefined for', bridgeId, ':', obj); + return undefined; + case BridgeId.STORAGE: + return chainStorage.toStorage(obj); + default: + throw Error(`unknown bridgeId ${bridgeId}`); + } + } + + const { + coreProposals, + clearStorageSubtrees, + exportStorageSubtrees = [], + } = config; + + const bootVat = config.vats[config.bootstrap]; + await null; + if (coreProposals) { + const { bundles, code } = await extractCoreProposalBundles( + coreProposals, + configPath, + ); + config.bundles = { ...config.bundles, ...bundles }; + bootVat.parameters = { ...bootVat.parameters, coreProposalCode: code }; + } + + const batchChainStorage = (method, args) => + bridgeOutbound(BridgeId.STORAGE, { method, args }); + + // Extract data from chain storage as [path, value?] pairs. + const chainStorageEntries = exportStorage( + batchChainStorage, + exportStorageSubtrees, + clearStorageSubtrees, + ); + bootVat.parameters = { ...bootVat.parameters, chainStorageEntries }; + + return buildBridge(bridgeOutbound); +} diff --git a/packages/swingset-runner/src/main.js b/packages/swingset-runner/src/main.js index 45238fe3b22..e6a20d196a3 100644 --- a/packages/swingset-runner/src/main.js +++ b/packages/swingset-runner/src/main.js @@ -6,6 +6,7 @@ import util from 'util'; import { makeStatLogger } from '@agoric/stat-logger'; import { + buildTimer, loadSwingsetConfigFile, loadBasedir, initializeSwingset, @@ -19,6 +20,7 @@ import { makeSlogSender } from '@agoric/telemetry'; import { dumpStore } from './dumpstore.js'; import { auditRefCounts } from './auditstore.js'; +import { initEmulatedChain } from './chain.js'; import { organizeBenchmarkStats, printBenchmarkStats, @@ -43,11 +45,12 @@ Command line: runner [FLAGS...] CMD [{BASEDIR|--} [ARGS...]] FLAGS may be: - --init - discard any existing saved state at startup + --resume - resume execution using existing saved state --initonly - initialize the swingset but exit without running it --sqlite - runs using Sqlite3 as the data store (default) --memdb - runs using the non-persistent in-memory data store --usexs - run vats using the the XS engine + --usebundlecache - cache bundles created by swingset loader --dbdir DIR - specify where the data store should go (default BASEDIR) --blockmode - run in block mode (checkpoint every BLOCKSIZE blocks) --blocksize N - set BLOCKSIZE to N cranks (default 200) @@ -71,6 +74,8 @@ FLAGS may be: --stats - print a performance stats report at the end of a run --statsfile FILE - output performance stats to FILE as a JSON object --benchmark N - perform an N round benchmark after the initial run + --sbench FILE - run a whole-swingset benchmark with the driver vat in FILE + --chain - emulate the behavior of the Cosmos-based chain --indirect - launch swingset from a vat instead of launching directly --config FILE - read swingset config from FILE instead of inferring it @@ -82,10 +87,15 @@ CMD is one of: shell - starts a simple CLI allowing the swingset to be run or stepped or interrogated interactively. -BASEDIR is the base directory for locating the swingset's vat definitions. - If BASEDIR is omitted or '--' it defaults to the current working directory. +BASEDIR is the base directory for locating the swingset's vat definitions and + for storing the swingset's state. If BASEDIR is omitted or '--' it defaults + to, in order of preference: + - the directory of the benchmark driver, if there is one + - the directory of the config file, if there is one + - the current working directory -Any remaining args are passed to the swingset's bootstrap vat. +Any remaining command line args are passed to the swingset's bootstrap vat in +the 'argv' vat parameter. `); } @@ -97,6 +107,72 @@ function fail(message, printUsage) { process.exit(1); } +/** + * In swingset benchmark mode, the configured swingset is loaded indirectly, + * interposing a benchmark controller vat we provide here as the swingset's + * bootstrap vat. In addition, the benchmark author is expected to provide a + * benchmark driver vat from a file specified on the command line. This + * benchmark driver vat is expected to expose the methods `setup` and + * `runBenchmarkRound`. The controller vat's `bootstrap` method invokes the + * swingset's "real" `bootstrap` method and then tells the benchmark driver vat + * to perform setup for the benchmark. The controller vat then orchestrates the + * execution of the directed number of bootstrap rounds. + * + * In order to perform the controller interposition and benchmark orchestration, + * this function generates a new swingset configuration with selective + * modifications and changes to the original swingset configuration. + * + * @param {*} baseConfig The original configuration being adapted for benchmark use + * @param {string} swingsetBenchmarkDriverPath Path to the benchmark driver vat source + * @param {string[]} bootstrapArgv Bootstrap args from the swingset-runner command line + * + * @returns {*} a new configuration that is a copy of `baseConfig` with modifications applied. + */ +function generateSwingsetBenchmarkConfig( + baseConfig, + swingsetBenchmarkDriverPath, + bootstrapArgv, +) { + if (baseConfig.vats.benchmarkBootstrap) { + fail( + `you can't have a vat named benchmarkBootstrap in a benchmark swingset`, + ); + } + if (baseConfig.vats.benchmarkDriver) { + fail(`you can't have a vat named benchmarkDriver in a benchmark swingset`); + } + // eslint-disable-next-line prefer-const + let { benchmarkDriver, ...baseConfigOptions } = baseConfig; + if (!benchmarkDriver) { + benchmarkDriver = { + sourceSpec: swingsetBenchmarkDriverPath, + }; + } + const config = { + ...baseConfigOptions, + bootstrap: 'benchmarkBootstrap', + defaultManagerType: 'local', + vats: { + benchmarkBootstrap: { + sourceSpec: new URL('vat-benchmarkBootstrap.js', import.meta.url) + .pathname, + parameters: { + config: { + bootstrap: baseConfig.bootstrap, + }, + }, + }, + benchmarkDriver, + ...baseConfig.vats, + }, + }; + if (!config.vats[baseConfig.bootstrap].parameters) { + config.vats[baseConfig.bootstrap].parameters = {}; + } + config.vats[baseConfig.bootstrap].parameters.argv = bootstrapArgv; + return config; +} + function generateIndirectConfig(baseConfig) { const config = { bootstrap: 'launcher', @@ -152,7 +228,7 @@ function generateIndirectConfig(baseConfig) { export async function main() { const argv = process.argv.slice(2); - let forceReset = false; + let forceReset = true; let dbMode = '--sqlite'; let blockSize = 200; let batchSize = 200; @@ -179,7 +255,10 @@ export async function main() { let dbDir = null; let initOnly = false; let useXS = false; + let useBundleCache = false; let activityHash = false; + let emulateChain = false; + let swingsetBenchmarkDriverPath = null; while (argv[0] && argv[0].startsWith('-')) { const flag = argv.shift(); @@ -190,6 +269,7 @@ export async function main() { case '--init': forceReset = true; break; + case '--resume': case '--noinit': forceReset = false; break; @@ -280,6 +360,16 @@ export async function main() { case '--useXS': useXS = true; break; + case '--usebundlecache': + case '--useBundleCache': + useBundleCache = true; + break; + case '--chain': + emulateChain = true; + break; + case '--sbench': + swingsetBenchmarkDriverPath = argv.shift(); + break; case '-v': case '--verbose': verbose = true; @@ -312,7 +402,7 @@ export async function main() { // Prettier demands that the conditional not be parenthesized. Prettier is wrong. // prettier-ignore - let basedir = (argv[0] === '--' || argv[0] === undefined) ? '.' : argv.shift(); + let basedir = (argv[0] === '--' || argv[0] === undefined) ? undefined : argv.shift(); const bootstrapArgv = argv[0] === '--' ? argv.slice(1) : argv; let config; @@ -322,24 +412,53 @@ export async function main() { if (config === null) { fail(`config file ${configPath} not found`); } - basedir = path.dirname(configPath); + if (!basedir) { + basedir = swingsetBenchmarkDriverPath + ? path.dirname(swingsetBenchmarkDriverPath) + : path.dirname(configPath); + } } else { + if (!basedir) { + basedir = swingsetBenchmarkDriverPath + ? path.dirname(swingsetBenchmarkDriverPath) + : '.'; + } config = loadBasedir(basedir); } - const deviceEndowments = {}; + if (config.bundleCachePath) { + const base = new URL(configPath, `file://${process.cwd()}/`); + config.bundleCachePath = new URL(config.bundleCachePath, base).pathname; + } else if (useBundleCache) { + config.bundleCachePath = path.join(basedir, 'bundles'); + } + + const timer = buildTimer(); + config.devices = { + timer: { + sourceSpec: timer.srcPath, + }, + }; + const deviceEndowments = { + timer: { ...timer.endowments }, + }; if (config.loopboxSenders) { const { loopboxSrcPath, loopboxEndowments } = buildLoopbox('immediate'); - config.devices = { - loopbox: { - sourceSpec: loopboxSrcPath, - parameters: { - senders: config.loopboxSenders, - }, + config.devices.loopbox = { + sourceSpec: loopboxSrcPath, + parameters: { + senders: config.loopboxSenders, }, }; delete config.loopboxSenders; deviceEndowments.loopbox = { ...loopboxEndowments }; } + if (emulateChain) { + const bridge = await initEmulatedChain(config, configPath); + config.devices.bridge = { + sourceSpec: bridge.srcPath, + }; + deviceEndowments.bridge = { ...bridge.endowments }; + } if (!config.defaultManagerType) { if (useXS) { config.defaultManagerType = 'xs-worker'; @@ -347,6 +466,15 @@ export async function main() { config.defaultManagerType = 'local'; } } + config.pinBootstrapRoot = true; + + if (swingsetBenchmarkDriverPath) { + config = generateSwingsetBenchmarkConfig( + config, + swingsetBenchmarkDriverPath, + bootstrapArgv, + ); + } if (launchIndirectly) { config = generateIndirectConfig(config); } @@ -445,7 +573,7 @@ export async function main() { if (benchmarkRounds > 0) { // Pin the vat root that will run benchmark rounds so it'll still be there // when it comes time to run them. - controller.pinVatRoot(launchIndirectly ? 'launcher' : 'bootstrap'); + controller.pinVatRoot(config.bootstrap); } let blockNumber = 0; @@ -592,7 +720,7 @@ export async function main() { await null; for (let i = 0; i < rounds; i += 1) { const roundResult = controller.queueToVatRoot( - launchIndirectly ? 'launcher' : 'bootstrap', + config.bootstrap, 'runBenchmarkRound', [], 'ignore', @@ -654,7 +782,7 @@ export async function main() { log(`activityHash: ${controller.getActivityhash()}`); } if (verbose) { - log(`===> end of crank ${crankNumber}`); + log(`===> end of crank ${crankNumber - 1}`); } } const commitStartTime = readClock(); diff --git a/packages/swingset-runner/src/vat-benchmarkBootstrap.js b/packages/swingset-runner/src/vat-benchmarkBootstrap.js new file mode 100644 index 00000000000..2439c293d18 --- /dev/null +++ b/packages/swingset-runner/src/vat-benchmarkBootstrap.js @@ -0,0 +1,38 @@ +import { E } from '@endo/eventual-send'; +import { Far } from '@endo/marshal'; + +/* + * Vat to bootstrap a benchmark swingset. + * + * On boot, removes itself and the benchmark driver vat from the visible vat + * collection and then forwards the bootstrap message (without these) to the + * original bootstrap vat. + * + * It then tells the benchmark driver to do its setup and thereafter just + * forwards `runBenchmarkRound` messages to the benchmark driver. + */ +export function buildRootObject(_vatPowers, vatParameters) { + let benchmarkDriverVat; + + return Far('root', { + async bootstrap(vatsExtended, devices) { + const { + benchmarkBootstrap: _benchmarkBootstrap, + benchmarkDriver, + ...vats + } = vatsExtended; + benchmarkDriverVat = benchmarkDriver; + const originalBootstrapVat = vatsExtended[vatParameters.config.bootstrap]; + harden(vats); + const bootstrapResult = await E(originalBootstrapVat).bootstrap( + vats, + devices, + ); + await E(benchmarkDriverVat).setup(vats, devices); + return bootstrapResult; + }, + runBenchmarkRound() { + return E(benchmarkDriverVat).runBenchmarkRound(); + }, + }); +} diff --git a/packages/swingset-runner/test/test-demo.js b/packages/swingset-runner/test/test-demo.js index 940ff9de35a..25ebbc498d6 100644 --- a/packages/swingset-runner/test/test-demo.js +++ b/packages/swingset-runner/test/test-demo.js @@ -30,7 +30,7 @@ async function innerTest(t, extraFlags, dbdir) { dbdir = `${appDir}/${dbdir}`; extraFlags += ` --dbdir ${dbdir}`; } - const proc = spawn(`node bin/runner --init ${extraFlags} run ${appDir}`, { + const proc = spawn(`node bin/runner ${extraFlags} run ${appDir}`, { cwd: path.resolve(dirname, '..'), shell: true, stdio: ['ignore', 'pipe', 'inherit'], diff --git a/packages/swingset-runner/whacker b/packages/swingset-runner/whacker index 492595c7cf5..1550246f0b7 100755 --- a/packages/swingset-runner/whacker +++ b/packages/swingset-runner/whacker @@ -2,11 +2,11 @@ t=`jot -p 3 -r 1 3 15` #echo killing after ${t} secs -bin/runner --init --blockmode run demo/megapong 10000 > /dev/null & +bin/runner --blockmode run demo/megapong 10000 > /dev/null & pid=$! sleep ${t} kill -9 ${pid} >& /dev/null wait ${pid} >& /dev/null sleep 1 #echo @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -bin/runner --blockmode run demo/megapong 10000 | grep 'alice contact' | tail -1 +bin/runner --resume --blockmode run demo/megapong 10000 | grep 'alice contact' | tail -1