Skip to content

Commit

Permalink
report bundle sizing in agoric run (#9503)
Browse files Browse the repository at this point in the history
refs: #1656

## Description

Spike on endojs/endo#1656 @kriskowal , similar to https://github.com/Agoric/agoric-sdk/discussions/8416 @dckc 

Makes the core-eval metadata available to `agoric run` so it can report more to the user. cc @michaelfig 

Adds a `--verbose` flag to report:

<img width="687" alt="Screenshot 2024-06-13 at 1 46 28 PM" src="https://github.com/Agoric/agoric-sdk/assets/21505/b76b4d14-2fbd-45c2-b7a5-d94f964f2461">

Also a `scripts/stat-bundle.js` to stat a known bundle path.

### Security Considerations


### Scaling Considerations


### Documentation Considerations


### Testing Considerations


### Upgrade Considerations
  • Loading branch information
mergify[bot] authored Jun 17, 2024
2 parents 0850f10 + 22b43da commit 7b30169
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 48 deletions.
1 change: 1 addition & 0 deletions packages/agoric-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"@endo/nat": "^5.0.7",
"@endo/patterns": "^1.4.0",
"@endo/promise-kit": "^1.1.2",
"@endo/zip": "^1.0.5",
"@iarna/toml": "^2.2.3",
"anylogger": "^0.21.0",
"chalk": "^5.2.0",
Expand Down
9 changes: 9 additions & 0 deletions packages/agoric-cli/scripts/stat-bundle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env node
import assert from 'node:assert';
import process from 'node:process';
import { statBundle } from '../src/lib/bundles.js';

const filename = process.argv[2];
assert(filename, 'usage: stat-bundle.js <filename>');

await statBundle(filename);
5 changes: 5 additions & 0 deletions packages/agoric-cli/scripts/stat-plans.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node
import process from 'node:process';
import { statPlans } from '../src/lib/bundles.js';

await statPlans(process.cwd());
102 changes: 102 additions & 0 deletions packages/agoric-cli/src/lib/bundles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// @ts-check

/* global Buffer */

import assert from 'node:assert/strict';
import fs from 'node:fs';
import { join } from 'node:path';

import { ZipReader } from '@endo/zip';

/** @import {Bundle} from '@agoric/swingset-vat'; */
/** @import {CoreEvalPlan} from '@agoric/deploy-script-support/src/writeCoreEvalParts.js' */

const PACKAGE_NAME_RE = /(?<packageName>.*-v[\d.]+)\//;

/**
* @typedef {{ name: string, label: string, location: string, modules: Record<string, {compartment: string, module: string}>}} Compartment
*/

/**
* @typedef CompartmentMap
* @property {string[]} tags
* @property {{compartment: string, module: string}} entry
* @property {Record<string, Compartment>} compartments
*/

/** @param {Bundle} bundleObj*/
export const extractBundleInfo = async bundleObj => {
if (bundleObj.moduleFormat !== 'endoZipBase64') {
throw new Error('only endoZipBase64 is supported');
}

const contents = Buffer.from(bundleObj.endoZipBase64, 'base64');

const zipReader = new ZipReader(contents);
const { files } = zipReader;

const cmapEntry = files.get('compartment-map.json');
/** @type {CompartmentMap} */
const compartmentMap = JSON.parse(Buffer.from(cmapEntry.content).toString());

// XXX mapIter better but requires SES
const fileSizes = Object.fromEntries(
Array.from(files.values()).map(f => [
f.name,
// bundle contents are not compressed
f.content.length,
]),
);

return { compartmentMap, fileSizes };
};

// UNTIL https://github.com/endojs/endo/issues/1656
/** @param {string} bundleFilename */
export const statBundle = async bundleFilename => {
const bundle = fs.readFileSync(bundleFilename, 'utf8');
/** @type {Bundle} */
const bundleObj = JSON.parse(bundle);
console.log('\nBUNDLE', bundleObj.moduleFormat, bundleFilename);

const info = await extractBundleInfo(bundleObj);
assert(info, 'no bundle info');

/** @type {Record<string, number>} */
const byPackage = {};
let totalSize = 0;
for (const [filename, size] of Object.entries(info.fileSizes)) {
totalSize += size;
if (filename === 'compartment-map.json') {
continue;
}
const { packageName } = filename.match(PACKAGE_NAME_RE)?.groups ?? {};
assert(packageName, `invalid filename ${filename}`);
byPackage[packageName] ||= 0;
byPackage[packageName] += size;
}

console.log('Sum of file sizes in each package:');
console.table(byPackage);

console.log('total size:', totalSize);
console.log('\nTo explore the contents:\n');
console.log(
` DIR=$(mktemp -d); cat ${bundleFilename} | jq -r .endoZipBase64 | base64 -d | tar xC $DIR; open $DIR`,
);
};

/** @param {string} path */
export const statPlans = async path => {
const files = await fs.promises.readdir(path);
const planfiles = files.filter(f => f.endsWith('plan.json'));

for (const planfile of planfiles) {
/** @type {CoreEvalPlan} */
const plan = JSON.parse(fs.readFileSync(join(path, planfile), 'utf8'));
console.log('\n**\nPLAN', plan.name);
for (const bundle of plan.bundles) {
await statBundle(bundle.fileName);
}
}
};
96 changes: 49 additions & 47 deletions packages/agoric-cli/src/main.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
/* global process */
import { Command } from 'commander';
import path from 'path';
import url from 'url';
import { assert, details as X } from '@agoric/assert';
import {
DEFAULT_KEEP_POLLING_SECONDS,
DEFAULT_JITTER_SECONDS,
DEFAULT_KEEP_POLLING_SECONDS,
} from '@agoric/casting';
import { Command } from 'commander';
import path from 'path';
import url from 'url';
import { makeWalletCommand } from './commands/wallet.js';
import cosmosMain from './cosmos.js';
import deployMain from './deploy.js';
import runMain from './run.js';
import publishMain from './main-publish.js';
import followMain from './follow.js';
import initMain from './init.js';
import installMain from './install.js';
import { statPlans } from './lib/bundles.js';
import publishMain from './main-publish.js';
import walletMain from './open.js';
import runMain from './run.js';
import setDefaultsMain from './set-defaults.js';
import startMain from './start.js';
import followMain from './follow.js';
import walletMain from './open.js';
import { makeWalletCommand } from './commands/wallet.js';

const DEFAULT_DAPP_TEMPLATE = 'dapp-offer-up';
const DEFAULT_DAPP_URL_BASE = 'https://github.com/Agoric/';
Expand Down Expand Up @@ -46,6 +47,7 @@ const main = async (progname, rawArgs, powers) => {
return true;
}

// XXX exits process when fn resolves
function subMain(fn, args, options) {
return fn(progname, args, powers, options).then(
// This seems to be the only way to propagate the exit code.
Expand Down Expand Up @@ -280,53 +282,53 @@ const main = async (progname, rawArgs, powers) => {
return subMain(followMain, ['follow', ...pathSpecs], opts);
});

const addRunOptions = cmd =>
cmd
.option(
'--allow-unsafe-plugins',
`CAREFUL: installed Agoric VM plugins will also have all your user's privileges`,
false,
)
.option(
'--hostport <host:port>',
'host and port to connect to VM',
'127.0.0.1:8000',
)
.option(
'--need <subsystems>',
'comma-separated names of subsystems to wait for',
'local,agoric,wallet',
)
.option(
'--provide <subsystems>',
'comma-separated names of subsystems this script initializes',
'',
);

baseCmd('run <script> [script-args...]')
.description(
'run a script with all your user privileges and some Agoric endowments',
)
.passThroughOptions(true)
.action(async (script, scriptArgs, _options, cmd) => {
const opts = { ...program.opts(), ...cmd.opts(), ...cmdOpts, scriptArgs };
return subMain(runMain, ['run', script], opts);
await runMain(progname, ['run', script], powers, opts);

if (opts.verbose) {
await statPlans(process.cwd());
}
});

addRunOptions(
baseCmd('deploy [script...]')
.option(
'--target <target>',
'One of agoric, local, cosmos, or sim',
'agoric',
)
.description(
'run multiple scripts with all your user privileges against the local Agoric VM',
),
).action(async (scripts, _options, cmd) => {
const opts = { ...program.opts(), ...cmd.opts() };
return subMain(deployMain, ['deploy', ...scripts], opts);
});
baseCmd('deploy [script...]')
.option(
'--target <target>',
'One of agoric, local, cosmos, or sim',
'agoric',
)
.option(
'--allow-unsafe-plugins',
`CAREFUL: installed Agoric VM plugins will also have all your user's privileges`,
false,
)
.option(
'--hostport <host:port>',
'host and port to connect to VM',
'127.0.0.1:8000',
)
.option(
'--need <subsystems>',
'comma-separated names of subsystems to wait for',
'local,agoric,wallet',
)
.option(
'--provide <subsystems>',
'comma-separated names of subsystems this script initializes',
'',
)
.description(
'run multiple scripts with all your user privileges against the local Agoric VM',
)
.action(async (scripts, _options, cmd) => {
const opts = { ...program.opts(), ...cmd.opts() };
return subMain(deployMain, ['deploy', ...scripts], opts);
});

baseCmd('publish [bundle...]')
.option(
Expand Down
12 changes: 11 additions & 1 deletion packages/deploy-script-support/src/writeCoreEvalParts.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,21 @@ import {
* @import {CoreEvalDescriptor} from './externalTypes.js';
*/

/**
* @typedef CoreEvalPlan
* @property {string} name
* @property {string} permit
* @property {string} script
* @property {{entrypoint: string, bundleID: string, fileName: string}[]} bundles
*/

/**
* @callback WriteCoreEval write to disk the files needed for a CoreEval (js code to`${filePrefix}.js`, permits to `${filePrefix}-permit.json`, an overall
* summary to `${filePrefix}-plan.json), plus whatever bundles bundles the code loads)
* see CoreEval in {@link '/golang/cosmos/x/swingset/types/swingset.pb.go'}
* @param {string} filePrefix name on disk
* @param {import('./externalTypes.js').CoreEvalBuilder} builder
* @returns {Promise<void>}
* @returns {Promise<CoreEvalPlan>}
*/

/**
Expand Down Expand Up @@ -189,6 +197,7 @@ behavior;
log(`creating ${codeFile}`);
await writeFile(codeFile, trimmed);

/** @type {CoreEvalPlan} */
const plan = {
name: filePrefix,
script: codeFile,
Expand All @@ -209,6 +218,7 @@ You can now run a governance submission command like:
Remember to install bundles before submitting the proposal:
${cmds.join('\n ')}
`);
return plan;
};

return writeCoreEval;
Expand Down

0 comments on commit 7b30169

Please sign in to comment.