diff --git a/packages/agoric-cli/package.json b/packages/agoric-cli/package.json index 5786b3594f5..b5780b31f46 100644 --- a/packages/agoric-cli/package.json +++ b/packages/agoric-cli/package.json @@ -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", diff --git a/packages/agoric-cli/scripts/stat-bundle.js b/packages/agoric-cli/scripts/stat-bundle.js new file mode 100755 index 00000000000..43de05222f7 --- /dev/null +++ b/packages/agoric-cli/scripts/stat-bundle.js @@ -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 '); + +await statBundle(filename); diff --git a/packages/agoric-cli/scripts/stat-plans.js b/packages/agoric-cli/scripts/stat-plans.js new file mode 100755 index 00000000000..34382985fba --- /dev/null +++ b/packages/agoric-cli/scripts/stat-plans.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node +import process from 'node:process'; +import { statPlans } from '../src/lib/bundles.js'; + +await statPlans(process.cwd()); diff --git a/packages/agoric-cli/src/lib/bundles.js b/packages/agoric-cli/src/lib/bundles.js new file mode 100644 index 00000000000..c9a8ef9d800 --- /dev/null +++ b/packages/agoric-cli/src/lib/bundles.js @@ -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 = /(?.*-v[\d.]+)\//; + +/** + * @typedef {{ name: string, label: string, location: string, modules: Record}} Compartment + */ + +/** + * @typedef CompartmentMap + * @property {string[]} tags + * @property {{compartment: string, module: string}} entry + * @property {Record} 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} */ + 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); + } + } +}; diff --git a/packages/agoric-cli/src/main.js b/packages/agoric-cli/src/main.js index ad008f927aa..18315708b24 100644 --- a/packages/agoric-cli/src/main.js +++ b/packages/agoric-cli/src/main.js @@ -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/'; @@ -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. @@ -280,29 +282,6 @@ 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 and port to connect to VM', - '127.0.0.1:8000', - ) - .option( - '--need ', - 'comma-separated names of subsystems to wait for', - 'local,agoric,wallet', - ) - .option( - '--provide ', - 'comma-separated names of subsystems this script initializes', - '', - ); - baseCmd('run