mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
141 lines
5.4 KiB
JavaScript
141 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// Phase 0 baseline: walk the packaged output and report a per-directory file
|
|
// count and byte size for the heavy resource dirs we ship, plus a grand total.
|
|
// This is the number we measure Phase 6 (python-env/node_modules slimming) and
|
|
// Phase 7 (Squirrel vs NSIS) against, so it must be deterministic: same tree in
|
|
// => same numbers out, regardless of OS or which arch was packaged.
|
|
//
|
|
// Usage:
|
|
// node scripts/perf/file-count.js [--root <dir>] [--json]
|
|
//
|
|
// With no --root it auto-detects the packaged resources dir for this platform,
|
|
// falling back to build-staging/ (the pre-package staging tree). Exits non-zero
|
|
// if it can't find anything to count so a CI gate notices a broken build.
|
|
|
|
'use strict';
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Top-level resource dirs we care about. Anything else in the root is folded
|
|
// into "other" so the grand total always reconciles with a raw walk.
|
|
const DIRS_OF_INTEREST = ['python-env', 'node', 'node_modules', 'mcp-servers', 'backend', 'frontend', 'router', 'debugger'];
|
|
|
|
function parseArgs(argv) {
|
|
const out = { root: null, json: false };
|
|
for (let i = 0; i < argv.length; i++) {
|
|
if (argv[i] === '--root') out.root = argv[++i];
|
|
else if (argv[i] === '--json') out.json = true;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// Candidate locations for the packaged tree, most-specific first. We resolve
|
|
// relative to the repo root (two levels up from this file) so it works from any
|
|
// cwd. The mac .app Resources and the Windows win-unpacked/resources are the
|
|
// real shipped trees; build-staging is the pre-electron-builder snapshot.
|
|
function autodetectRoot(repoRoot) {
|
|
const dist = path.join(repoRoot, 'electron', 'dist');
|
|
const candidates = [
|
|
path.join(dist, 'win-unpacked', 'resources'),
|
|
path.join(dist, 'mac-arm64', 'OpenSwarm.app', 'Contents', 'Resources'),
|
|
path.join(dist, 'mac', 'OpenSwarm.app', 'Contents', 'Resources'),
|
|
path.join(dist, 'mac-universal', 'OpenSwarm.app', 'Contents', 'Resources'),
|
|
path.join(repoRoot, 'electron', 'build-staging'),
|
|
path.join(repoRoot, 'build-staging'),
|
|
];
|
|
return candidates.find((c) => safeIsDir(c)) || null;
|
|
}
|
|
|
|
function safeIsDir(p) {
|
|
try { return fs.statSync(p).isDirectory(); } catch { return false; }
|
|
}
|
|
|
|
// Recursive walk. Counts regular files (not directories), sums byte size.
|
|
// Symlinks are counted as files without following them, so a symlink loop can
|
|
// never hang the walk and the count stays deterministic.
|
|
function walk(dir) {
|
|
let files = 0;
|
|
let bytes = 0;
|
|
let entries;
|
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return { files, bytes }; }
|
|
for (const ent of entries) {
|
|
const full = path.join(dir, ent.name);
|
|
if (ent.isDirectory()) {
|
|
const sub = walk(full);
|
|
files += sub.files;
|
|
bytes += sub.bytes;
|
|
} else {
|
|
files += 1;
|
|
try { bytes += fs.statSync(full).size; } catch { /* vanished mid-walk */ }
|
|
}
|
|
}
|
|
return { files, bytes };
|
|
}
|
|
|
|
function fmtBytes(n) {
|
|
const units = ['B', 'KB', 'MB', 'GB'];
|
|
let i = 0;
|
|
let v = n;
|
|
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
|
|
return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
}
|
|
|
|
function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
const repoRoot = path.resolve(__dirname, '..', '..');
|
|
const root = args.root ? path.resolve(args.root) : autodetectRoot(repoRoot);
|
|
|
|
if (!root || !safeIsDir(root)) {
|
|
process.stderr.write('file-count: no packaged tree found. Build first or pass --root.\n');
|
|
process.exit(2);
|
|
}
|
|
|
|
const report = { root, dirs: {}, total: { files: 0, bytes: 0 }, generatedAt: new Date().toISOString() };
|
|
const topEntries = fs.readdirSync(root, { withFileTypes: true });
|
|
const seen = new Set();
|
|
|
|
for (const name of DIRS_OF_INTEREST) {
|
|
const full = path.join(root, name);
|
|
if (safeIsDir(full)) {
|
|
report.dirs[name] = walk(full);
|
|
seen.add(name);
|
|
} else {
|
|
report.dirs[name] = { files: 0, bytes: 0, absent: true };
|
|
}
|
|
}
|
|
|
|
// "other" = everything in the root not already counted, so the printed total
|
|
// is the real total of the tree and can be cross-checked by an independent walk.
|
|
let other = { files: 0, bytes: 0 };
|
|
for (const ent of topEntries) {
|
|
if (seen.has(ent.name)) continue;
|
|
const full = path.join(root, ent.name);
|
|
if (ent.isDirectory()) { const s = walk(full); other.files += s.files; other.bytes += s.bytes; }
|
|
else { other.files += 1; try { other.bytes += fs.statSync(full).size; } catch { /* gone */ } }
|
|
}
|
|
report.dirs.other = other;
|
|
|
|
for (const k of Object.keys(report.dirs)) {
|
|
report.total.files += report.dirs[k].files;
|
|
report.total.bytes += report.dirs[k].bytes;
|
|
}
|
|
|
|
if (args.json) {
|
|
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
return;
|
|
}
|
|
|
|
process.stdout.write(`\nPackaged file count (root: ${root})\n`);
|
|
process.stdout.write(' ' + '-'.repeat(52) + '\n');
|
|
const rows = Object.keys(report.dirs).sort((a, b) => report.dirs[b].files - report.dirs[a].files);
|
|
for (const name of rows) {
|
|
const d = report.dirs[name];
|
|
const note = d.absent ? ' (absent)' : '';
|
|
process.stdout.write(` ${name.padEnd(16)} ${String(d.files).padStart(8)} files ${fmtBytes(d.bytes).padStart(10)}${note}\n`);
|
|
}
|
|
process.stdout.write(' ' + '-'.repeat(52) + '\n');
|
|
process.stdout.write(` ${'TOTAL'.padEnd(16)} ${String(report.total.files).padStart(8)} files ${fmtBytes(report.total.bytes).padStart(10)}\n\n`);
|
|
}
|
|
|
|
main();
|