Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

report bundle sizing in agoric run #9503

Merged
merged 5 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading