mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
## What Problem This Solves
`pnpm tsgo:profile constructor` treated an inherited object property as a configured graph, created artifact directories, and failed with an unrelated TypeError.
## User Impact
Inherited names now receive the existing `Unknown graph` diagnostic before artifacts or compiler work. Valid graph names, flags, and output remain unchanged.
## Why This Change Was Made
Require an own property in the graph registry instead of accepting JavaScript's prototype chain.
## Evidence
- Blacksmith Testbox `tbx_01m3v0m906fej4qb20j8wy64gd`: the CLI regression fails on the original source with the misleading profiling/error output, then passes with the fix. It exercises the supported CLI preload and verifies exit 1, the existing diagnostic, no stdout, no artifacts, and no compiler launch.
- Single-worker regression suite wall time: 1.63s. One CLI process is necessary to cover admission before the private parser's side effects; there is no production test hook, timer, sleep, or polling.
- Changed checks and SDK surface passed. Madge and runtime import-cycle checks both reported zero.
- Independent isolated Codex review completed with no P0–P2 findings.
No configuration option, schema, dependency, or protocol change.
### Hosted CI evidence
Exact-head run https://github.com/openclaw/openclaw/actions/runs/36825865133 failed `check-lint-core-1` and `check-lint-core-2`; fail-fast cancelled sibling coverage. Security checks and independent review passed. Cancelled checks remain incomplete, not green.
All twenty root diagnostics have independent executed CI witnesses. Nineteen match current unrelated PR CI: core-1 in https://github.com/openclaw/openclaw/actions/runs/36823091648 (head `488d5300b95fa56568284d014f43c44d3fdc2143`), and core-2 in https://github.com/openclaw/openclaw/actions/runs/36822965519 (head `2ca2ff4326be38e6c890f2c80dddb03c84e06c79`). The remaining `update-candidate-canary.test.ts` failure is independently witnessed by scheduled main run https://github.com/openclaw/openclaw/actions/runs/36827062752, head `867a82ce61`, job `check-lint-core-5` (`110255224713`): `File has too many lines (1006). Maximum allowed is 1000.` The executed job annotation names the same file at line 1035. All root failures therefore qualify for the explicitly approved inherited-failure exception; cancelled coverage remains incomplete. This PR changes only the profile CLI and its own regression; none of the listed core files or their lint configuration is changed.
| File | Rule | Diagnostic |
| --- | --- | --- |
| `src/skills/library/store.ts` | `typescript(no-redundant-type-constituents)` | 'StateDatabase' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/agents/harness/native-hook-relay-store.kernel.ts` | `typescript(no-redundant-type-constituents)` | 'OpenClawStateKyselyDatabase' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/gateway/github-publication-store.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/gateway/github-personal-publication-store.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/gateway/github-publication-recovery.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/gateway/github-repository-publication-store.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/gateway/github-repository-publication.kernel.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/gateway/github-publication-coordinator-methods.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/gateway/github-repository-publication-recovery.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/gateway/github-publication.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/gateway/github-publication-personal-pending.test.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/gateway/github-publication-shared-read.kernel.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/proxy-capture/store.kernel.ts` | `typescript(no-redundant-type-constituents)` | 'OpenClawStateKyselyDatabase' is an 'error' type that acts as 'any' and overrides all other types in this intersection type. |
| `src/channels/message/ingress-queue.test.ts` | `typescript(no-unnecessary-type-parameters)` | Type parameter TColumn is used only once in the function signature. |
| `src/config/sessions/session-transcript-projection-rebuild.ts` | `typescript(no-redundant-type-constituents)` | 'OpenClawAgentKyselyDatabase' is an 'error' type that acts as 'any' and overrides all other types in this intersection type. |
| `src/state/openclaw-state-read.types.ts` | `typescript(no-redundant-type-constituents)` | 'DB' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/infra/update-candidate-canary.test.ts` | `eslint(max-lines)` | File has too many lines (1006). |
| `src/infra/delivery-queue-sqlite.ts` | `typescript(no-redundant-type-constituents)` | 'OpenClawStateKyselyDatabase' is an 'error' type that acts as 'any' and overrides all other types in this union type. |
| `src/infra/deferred-plugin-migrations.test.ts` | `typescript(require-array-sort-compare)` | Require 'compare' argument. |
| `src/infra/update-repair-agent.self-lease.test.ts` | `typescript(require-array-sort-compare)` | Require 'compare' argument. |
467 lines
15 KiB
TypeScript
467 lines
15 KiB
TypeScript
#!/usr/bin/env node
|
|
|
|
// Profiles selected tsgo graphs and writes diagnostics/trace artifacts for
|
|
// TypeScript graph size and performance investigations.
|
|
import { spawnSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { applyLocalTsgoPolicy, resolveRepoToolBinPath } from "./lib/local-check-runtime.mts";
|
|
import { createManagedCommandInvocation } from "./lib/managed-child-process.mts";
|
|
import { resolveRepoRoot } from "./lib/repo-root.mjs";
|
|
import { TSGO_CORE_TEST_SHARDS, type TsgoCoreTestShard } from "./lib/tsgo-core-test-shards.mts";
|
|
const repoRoot = resolveRepoRoot(import.meta.url);
|
|
const artifactRoot = path.resolve(repoRoot, ".artifacts/tsgo-profile");
|
|
const tsgoPath = resolveRepoToolBinPath("tsgo", { cwd: repoRoot });
|
|
|
|
type GraphDefinition = { config: string; description: string };
|
|
type CoreTestGraphName = `core-test-${TsgoCoreTestShard["name"]}`;
|
|
const CORE_TEST_GRAPH_DEFINITIONS = Object.fromEntries(
|
|
TSGO_CORE_TEST_SHARDS.map((shard) => [
|
|
`core-test-${shard.name}`,
|
|
{
|
|
config: shard.config,
|
|
description: `bounded core test shard: ${shard.name}`,
|
|
},
|
|
]),
|
|
) as Record<CoreTestGraphName, GraphDefinition>;
|
|
|
|
const GRAPH_DEFINITIONS = {
|
|
core: {
|
|
config: "tsconfig.core.json",
|
|
description: "core production graph",
|
|
},
|
|
ui: {
|
|
config: "tsconfig.ui.json",
|
|
description: "UI production graph",
|
|
},
|
|
...CORE_TEST_GRAPH_DEFINITIONS,
|
|
extensions: {
|
|
config: "tsconfig.extensions.json",
|
|
description: "bundled extension production graph",
|
|
},
|
|
"extensions-test": {
|
|
config: "test/tsconfig/tsconfig.extensions.test.json",
|
|
description: "bundled extension colocated test graph",
|
|
},
|
|
} as const;
|
|
|
|
type GraphName = keyof typeof GRAPH_DEFINITIONS;
|
|
const DEFAULT_GRAPHS = [
|
|
...TSGO_CORE_TEST_SHARDS.map((shard) => `core-test-${shard.name}` as CoreTestGraphName),
|
|
"extensions-test",
|
|
] satisfies GraphName[];
|
|
type ProfileOptions = {
|
|
all: boolean;
|
|
deep: boolean;
|
|
explain: boolean;
|
|
json: boolean;
|
|
reuse: boolean;
|
|
outDir: string;
|
|
};
|
|
type Diagnostics = Record<string, number>;
|
|
type ProfileGraphResult = ReturnType<typeof profileGraph>;
|
|
type ProfileReport = {
|
|
generatedAt: string;
|
|
options: { graphs: GraphName[]; deep: boolean; explain: boolean; reuse: boolean };
|
|
graphs: ProfileGraphResult[];
|
|
paths: { json?: string; text?: string };
|
|
};
|
|
|
|
function usage(): string {
|
|
return [
|
|
"Usage: pnpm tsgo:profile [graph...] [options]",
|
|
"",
|
|
"Graphs:",
|
|
...Object.entries(GRAPH_DEFINITIONS).map(
|
|
([name, graph]) => ` ${name.padEnd(26)} ${graph.description}`,
|
|
),
|
|
"",
|
|
"Options:",
|
|
" --all Profile all graphs",
|
|
" --reuse Reuse profile tsbuildinfo files instead of forcing fresh checks",
|
|
" --deep Also write --generateTrace and --pprofDir artifacts",
|
|
" --explain Also write list-only --explainFiles artifacts",
|
|
" --out=<dir> Output directory (default: .artifacts/tsgo-profile)",
|
|
" --json Print JSON report to stdout",
|
|
" --help Show this help",
|
|
"",
|
|
"Default graphs: all bounded core-test shards and extensions-test",
|
|
].join("\n");
|
|
}
|
|
|
|
function parseArgs(argv: string[]): { options: ProfileOptions; selectedGraphs: GraphName[] } {
|
|
const graphNames: GraphName[] = [];
|
|
const options: ProfileOptions = {
|
|
all: false,
|
|
deep: false,
|
|
explain: false,
|
|
json: false,
|
|
reuse: false,
|
|
outDir: artifactRoot,
|
|
};
|
|
|
|
for (const arg of argv) {
|
|
if (arg === "--help" || arg === "-h") {
|
|
throw new Error(usage());
|
|
}
|
|
const flag = arg.startsWith("--") ? arg.slice(2) : undefined;
|
|
if (
|
|
flag === "all" ||
|
|
flag === "deep" ||
|
|
flag === "explain" ||
|
|
flag === "json" ||
|
|
flag === "reuse"
|
|
) {
|
|
options[flag] = true;
|
|
continue;
|
|
}
|
|
if (arg.startsWith("--out=")) {
|
|
options.outDir = path.resolve(repoRoot, arg.slice("--out=".length));
|
|
continue;
|
|
}
|
|
if (!Object.hasOwn(GRAPH_DEFINITIONS, arg)) {
|
|
throw new Error(`Unknown graph: ${arg}\n\n${usage()}`);
|
|
}
|
|
graphNames.push(arg as GraphName);
|
|
}
|
|
|
|
const selectedGraphs = options.all
|
|
? (Object.keys(GRAPH_DEFINITIONS) as GraphName[])
|
|
: graphNames.length > 0
|
|
? graphNames
|
|
: DEFAULT_GRAPHS;
|
|
|
|
return { options, selectedGraphs };
|
|
}
|
|
|
|
function ensureDirs(outDir: string): void {
|
|
fs.mkdirSync(outDir, { recursive: true });
|
|
fs.mkdirSync(path.join(outDir, "cache"), { recursive: true });
|
|
}
|
|
|
|
function runTsgo(
|
|
label: string,
|
|
args: string[],
|
|
params: { maxBuffer?: number } = {},
|
|
): { elapsedMs: number; stdout: string; stderr: string } {
|
|
const { args: finalArgs, env } = applyLocalTsgoPolicy(args, process.env, {
|
|
logicalCpuCount:
|
|
typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length,
|
|
totalMemoryBytes: os.totalmem(),
|
|
});
|
|
const startedAt = Date.now();
|
|
const tsgo = createManagedCommandInvocation({
|
|
args: finalArgs,
|
|
bin: tsgoPath,
|
|
env,
|
|
});
|
|
const result = spawnSync(tsgo.command, tsgo.args, {
|
|
cwd: repoRoot,
|
|
env,
|
|
encoding: "utf8",
|
|
maxBuffer: params.maxBuffer ?? 128 * 1024 * 1024,
|
|
shell: tsgo.shell,
|
|
windowsVerbatimArguments: tsgo.windowsVerbatimArguments,
|
|
});
|
|
const elapsedMs = Date.now() - startedAt;
|
|
const stdout = result.stdout ?? "";
|
|
const stderr = result.stderr ?? "";
|
|
if (result.error) {
|
|
throw result.error;
|
|
}
|
|
if ((result.status ?? 1) !== 0) {
|
|
const output = [stdout, stderr].filter(Boolean).join("\n");
|
|
throw new Error(`${label} failed with exit code ${result.status ?? 1}\n${output}`);
|
|
}
|
|
return { elapsedMs, stdout, stderr };
|
|
}
|
|
|
|
function parseDiagnostics(output: string): Diagnostics {
|
|
const diagnostics: Diagnostics = {};
|
|
for (const line of output.split(/\r?\n/u)) {
|
|
const match = /^(.+?):\s+([0-9.]+)(K|s)?\s*$/u.exec(line.trim());
|
|
if (!match) {
|
|
continue;
|
|
}
|
|
const [, rawKey, rawValue, unit] = match;
|
|
const key = rawKey!.trim().replaceAll(/\s+/gu, " ");
|
|
const value = Number(rawValue);
|
|
diagnostics[key] = unit === "K" ? value * 1024 : value;
|
|
}
|
|
return diagnostics;
|
|
}
|
|
|
|
function normalizeFilePath(filePath: string): string {
|
|
const normalized = filePath.trim().replaceAll("\\", "/");
|
|
const normalizedRoot = repoRoot.replaceAll("\\", "/");
|
|
if (normalized.startsWith(`${normalizedRoot}/`)) {
|
|
return normalized.slice(normalizedRoot.length + 1);
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
function packageNameFromNodeModule(parts: string[], startIndex: number): string {
|
|
const first = parts[startIndex + 1];
|
|
if (!first) {
|
|
return "node_modules";
|
|
}
|
|
if (first.startsWith("@")) {
|
|
return `${first}/${parts[startIndex + 2] ?? ""}`.replace(/\/$/u, "");
|
|
}
|
|
return first;
|
|
}
|
|
|
|
function classifyFile(relativePath: string): string {
|
|
const parts = relativePath.split("/");
|
|
const first = parts[0];
|
|
if (relativePath.includes("/node_modules/") || first === "node_modules") {
|
|
const nodeModulesIndex = parts.indexOf("node_modules");
|
|
return `node_modules/${packageNameFromNodeModule(parts, nodeModulesIndex)}`;
|
|
}
|
|
if (first && ["extensions", "packages", "src", "ui", "test"].includes(first)) {
|
|
return `${first}/${parts[1] ?? "(root)"}`;
|
|
}
|
|
if (first?.startsWith("/") || (first !== undefined && /^[A-Za-z]:/u.test(first))) {
|
|
return "(external)";
|
|
}
|
|
return first || "(unknown)";
|
|
}
|
|
|
|
function countBy<T>(values: T[], keyFn: (value: T) => string) {
|
|
const counts = new Map<string, number>();
|
|
for (const value of values) {
|
|
const key = keyFn(value);
|
|
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
}
|
|
return [...counts.entries()]
|
|
.map(([key, count]) => ({ key, count }))
|
|
.toSorted((left, right) => right.count - left.count || left.key.localeCompare(right.key));
|
|
}
|
|
|
|
function summarizeFiles(stdout: string) {
|
|
const files = stdout
|
|
.split(/\r?\n/u)
|
|
.map(normalizeFilePath)
|
|
.filter(Boolean)
|
|
.filter((line) => !line.startsWith("Files:"));
|
|
|
|
const projectRelativeFiles = files.filter(
|
|
(file) => !path.isAbsolute(file) && !/^[A-Za-z]:/u.test(file),
|
|
);
|
|
const testFiles = projectRelativeFiles.filter((file) => /\.test\.[cm]?[tj]sx?$/u.test(file));
|
|
return {
|
|
totalFiles: files.length,
|
|
projectRelativeFiles: projectRelativeFiles.length,
|
|
testFiles: testFiles.length,
|
|
groups: countBy(projectRelativeFiles, classifyFile).slice(0, 40),
|
|
};
|
|
}
|
|
|
|
function diffDiagnostics(check: Diagnostics, noCheck: Diagnostics) {
|
|
const totalDelta = (check["Total time"] ?? 0) - (noCheck["Total time"] ?? 0);
|
|
const checkTime = check["Check time"] ?? 0;
|
|
return {
|
|
checkTimeSeconds: checkTime,
|
|
totalDeltaSeconds: totalDelta,
|
|
typeShareOfTotal:
|
|
check["Total time"] && checkTime ? Number((checkTime / check["Total time"]).toFixed(3)) : 0,
|
|
};
|
|
}
|
|
|
|
function formatSeconds(value: number): string {
|
|
return `${value.toFixed(2)}s`;
|
|
}
|
|
|
|
function renderTextReport(report: ProfileReport): string {
|
|
const lines = [
|
|
"# tsgo profile",
|
|
"",
|
|
`Generated: ${report.generatedAt}`,
|
|
`Fresh profile caches: ${report.options.reuse ? "no" : "yes"}`,
|
|
"",
|
|
];
|
|
|
|
for (const graph of report.graphs) {
|
|
const check = graph.check.diagnostics;
|
|
const noCheck = graph.noCheck.diagnostics;
|
|
lines.push(`## ${graph.name}`);
|
|
lines.push(`Config: ${graph.config}`);
|
|
lines.push(
|
|
`Check: wall ${formatSeconds(graph.check.elapsedMs / 1000)}, compiler total ${formatSeconds(
|
|
check["Total time"] ?? 0,
|
|
)}, check ${formatSeconds(check["Check time"] ?? 0)}, memory ${Math.round(
|
|
(check["Memory used"] ?? 0) / 1024 / 1024,
|
|
)} MiB`,
|
|
);
|
|
lines.push(
|
|
`NoCheck: wall ${formatSeconds(
|
|
graph.noCheck.elapsedMs / 1000,
|
|
)}, compiler total ${formatSeconds(noCheck["Total time"] ?? 0)}`,
|
|
);
|
|
lines.push(
|
|
`Files: compiler ${check.Files ?? "?"}, listed ${graph.files.totalFiles}, project-relative ${graph.files.projectRelativeFiles}, tests ${graph.files.testFiles}`,
|
|
);
|
|
lines.push(`File list: ${graph.files.artifact}`);
|
|
lines.push(
|
|
`Type cost: check ${formatSeconds(graph.typeCost.checkTimeSeconds)}, total delta ${formatSeconds(
|
|
graph.typeCost.totalDeltaSeconds,
|
|
)}, share ${(graph.typeCost.typeShareOfTotal * 100).toFixed(1)}%`,
|
|
);
|
|
lines.push("Top file groups:");
|
|
for (const group of graph.files.groups.slice(0, 15)) {
|
|
lines.push(`- ${group.key}: ${group.count}`);
|
|
}
|
|
if (graph.deep) {
|
|
lines.push(`Deep artifacts: ${graph.deep.traceDir}, ${graph.deep.profileDir}`);
|
|
}
|
|
if (graph.explain) {
|
|
lines.push(`Explain: ${graph.explain.artifact}`);
|
|
}
|
|
lines.push("");
|
|
}
|
|
|
|
lines.push(`JSON: ${report.paths.json ?? ""}`);
|
|
lines.push("");
|
|
return `${lines.join("\n")}\n`;
|
|
}
|
|
|
|
function profileGraph(name: GraphName, options: ProfileOptions) {
|
|
const graph = GRAPH_DEFINITIONS[name];
|
|
const outDir = options.outDir;
|
|
const graphCacheRoot = path.join(outDir, "cache");
|
|
const checkBuildInfo = path.join(graphCacheRoot, `${name}-check.tsbuildinfo`);
|
|
const noCheckBuildInfo = path.join(graphCacheRoot, `${name}-nocheck.tsbuildinfo`);
|
|
const configPath = graph.config;
|
|
|
|
if (!options.reuse) {
|
|
for (const filePath of [checkBuildInfo, noCheckBuildInfo]) {
|
|
fs.rmSync(filePath, { force: true });
|
|
}
|
|
}
|
|
|
|
const baseArgs = ["-p", configPath, "--pretty", "false"];
|
|
const listFiles = runTsgo(`${name}:listFilesOnly`, [...baseArgs, "--listFilesOnly"], {
|
|
maxBuffer: 256 * 1024 * 1024,
|
|
});
|
|
const filesArtifact = path.join(outDir, `${name}.files.txt`);
|
|
fs.writeFileSync(filesArtifact, listFiles.stdout);
|
|
const noCheck = runTsgo(`${name}:noCheck`, [
|
|
...baseArgs,
|
|
"--noCheck",
|
|
"--incremental",
|
|
"--tsBuildInfoFile",
|
|
noCheckBuildInfo,
|
|
"--extendedDiagnostics",
|
|
]);
|
|
|
|
const checkArgs = [
|
|
...baseArgs,
|
|
"--incremental",
|
|
"--tsBuildInfoFile",
|
|
checkBuildInfo,
|
|
"--extendedDiagnostics",
|
|
];
|
|
let deep: { profileDir: string; traceDir: string } | undefined;
|
|
if (options.deep) {
|
|
const traceDir = path.join(outDir, `${name}-trace`);
|
|
const profileDir = path.join(outDir, `${name}-pprof`);
|
|
fs.rmSync(traceDir, { force: true, recursive: true });
|
|
fs.rmSync(profileDir, { force: true, recursive: true });
|
|
fs.mkdirSync(profileDir, { recursive: true });
|
|
checkArgs.push("--generateTrace", traceDir, "--pprofDir", profileDir);
|
|
deep = {
|
|
traceDir: path.relative(repoRoot, traceDir),
|
|
profileDir: path.relative(repoRoot, profileDir),
|
|
};
|
|
}
|
|
const check = runTsgo(`${name}:check`, checkArgs);
|
|
let explain: { artifact: string; elapsedMs: number } | undefined;
|
|
if (options.explain) {
|
|
const explainArtifact = path.join(outDir, `${name}.explain.txt`);
|
|
const explainResult = runTsgo(
|
|
`${name}:explainFiles`,
|
|
[...baseArgs, "--listFilesOnly", "--explainFiles"],
|
|
{
|
|
maxBuffer: 256 * 1024 * 1024,
|
|
},
|
|
);
|
|
fs.writeFileSync(explainArtifact, `${explainResult.stdout}${explainResult.stderr}`);
|
|
explain = {
|
|
artifact: path.relative(repoRoot, explainArtifact),
|
|
elapsedMs: explainResult.elapsedMs,
|
|
};
|
|
}
|
|
|
|
const checkDiagnostics = parseDiagnostics(`${check.stdout}\n${check.stderr}`);
|
|
const noCheckDiagnostics = parseDiagnostics(`${noCheck.stdout}\n${noCheck.stderr}`);
|
|
return {
|
|
name,
|
|
config: configPath,
|
|
description: graph.description,
|
|
files: {
|
|
...summarizeFiles(listFiles.stdout),
|
|
artifact: path.relative(repoRoot, filesArtifact),
|
|
},
|
|
noCheck: {
|
|
elapsedMs: noCheck.elapsedMs,
|
|
diagnostics: noCheckDiagnostics,
|
|
},
|
|
check: {
|
|
elapsedMs: check.elapsedMs,
|
|
diagnostics: checkDiagnostics,
|
|
},
|
|
typeCost: diffDiagnostics(checkDiagnostics, noCheckDiagnostics),
|
|
...(deep ? { deep } : {}),
|
|
...(explain ? { explain } : {}),
|
|
};
|
|
}
|
|
|
|
async function main(argv: string[]): Promise<void> {
|
|
const { options, selectedGraphs } = parseArgs(argv);
|
|
ensureDirs(options.outDir);
|
|
const report: ProfileReport = {
|
|
generatedAt: new Date().toISOString(),
|
|
options: {
|
|
graphs: selectedGraphs,
|
|
deep: options.deep,
|
|
explain: options.explain,
|
|
reuse: options.reuse,
|
|
},
|
|
graphs: [],
|
|
paths: {},
|
|
};
|
|
|
|
for (const graphName of selectedGraphs) {
|
|
process.stderr.write(`[tsgo-profile] profiling ${graphName}\n`);
|
|
report.graphs.push(profileGraph(graphName, options));
|
|
}
|
|
|
|
const timestamp = new Date()
|
|
.toISOString()
|
|
.replaceAll(":", "")
|
|
.replaceAll(".", "")
|
|
.replace("T", "-")
|
|
.replace("Z", "");
|
|
const jsonPath = path.join(options.outDir, `tsgo-profile-${timestamp}.json`);
|
|
const textPath = path.join(options.outDir, `tsgo-profile-${timestamp}.md`);
|
|
report.paths = {
|
|
json: path.relative(repoRoot, jsonPath),
|
|
text: path.relative(repoRoot, textPath),
|
|
};
|
|
|
|
const json = `${JSON.stringify(report, null, 2)}\n`;
|
|
const text = renderTextReport(report);
|
|
fs.writeFileSync(jsonPath, json);
|
|
fs.writeFileSync(textPath, text);
|
|
fs.writeFileSync(path.join(options.outDir, "latest.json"), json);
|
|
fs.writeFileSync(path.join(options.outDir, "latest.md"), text);
|
|
process.stdout.write(options.json ? json : text);
|
|
}
|
|
|
|
try {
|
|
await main(process.argv.slice(2));
|
|
} catch (error) {
|
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
process.exit(1);
|
|
}
|