Skip to content

Commit d0e5cf6

Browse files
committed
Add CLI startup profiling feature to diagnose Windows performance issues
1 parent ba9a4f4 commit d0e5cf6

File tree

4 files changed

+142
-4
lines changed

4 files changed

+142
-4
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { CommandModule } from 'yargs';
2+
import { SharedOptions } from '../options.js';
3+
4+
export const command: CommandModule<object, SharedOptions> = {
5+
command: 'test-profile',
6+
describe: 'Test the profiling feature',
7+
handler: async () => {
8+
console.log('Profile test completed successfully');
9+
// Profiling report will be automatically displayed by the main function
10+
11+
// Force a delay to simulate some processing
12+
await new Promise(resolve => setTimeout(resolve, 100));
13+
},
14+
};

packages/cli/src/index.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,47 @@ import { hideBin } from 'yargs/helpers';
88
import { command as defaultCommand } from './commands/$default.js';
99
import { command as configCommand } from './commands/config.js';
1010
import { command as testSentryCommand } from './commands/test-sentry.js';
11+
import { command as testProfileCommand } from './commands/test-profile.js';
1112
import { command as toolsCommand } from './commands/tools.js';
1213
import { sharedOptions } from './options.js';
1314
import { initSentry, captureException } from './sentry/index.js';
14-
initSentry();
15+
import { enableProfiling, mark, reportTimings } from './utils/performance.js';
16+
17+
mark('After imports');
1518

1619
import type { PackageJson } from 'type-fest';
1720

1821
// Add global declaration for our patched toolAgent
1922

23+
mark('Before sourceMapSupport install');
2024
sourceMapSupport.install();
25+
mark('After sourceMapSupport install');
2126

2227
const main = async () => {
28+
// Parse argv early to check for profiling flag
29+
const parsedArgv = await yargs(hideBin(process.argv)).options(sharedOptions).parse();
30+
31+
// Enable profiling if --profile flag is set
32+
enableProfiling(Boolean(parsedArgv.profile));
33+
34+
mark('Main function start');
35+
2336
dotenv.config();
24-
37+
mark('After dotenv config');
38+
39+
// Only initialize Sentry if needed
40+
if (process.env.NODE_ENV !== 'development' || process.env.ENABLE_SENTRY === 'true') {
41+
initSentry();
42+
mark('After Sentry init');
43+
}
44+
45+
mark('Before package.json load');
2546
const require = createRequire(import.meta.url);
2647
const packageInfo = require('../package.json') as PackageJson;
27-
48+
mark('After package.json load');
49+
2850
// Set up yargs with the new CLI interface
51+
mark('Before yargs setup');
2952
await yargs(hideBin(process.argv))
3053
.scriptName(packageInfo.name!)
3154
.version(packageInfo.version!)
@@ -35,16 +58,23 @@ const main = async () => {
3558
.command([
3659
defaultCommand,
3760
testSentryCommand,
61+
testProfileCommand,
3862
toolsCommand,
3963
configCommand,
4064
] as CommandModule[])
4165
.strict()
4266
.showHelpOnFail(true)
4367
.help().argv;
68+
mark('After yargs setup');
69+
70+
// Report timings if profiling is enabled
71+
await reportTimings();
4472
};
4573

46-
await main().catch((error) => {
74+
await main().catch(async (error) => {
4775
console.error(error);
76+
// Report profiling data even if there's an error
77+
await reportTimings();
4878
// Capture the error with Sentry
4979
captureException(error);
5080
process.exit(1);

packages/cli/src/options.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type SharedOptions = {
99
readonly sentryDsn?: string;
1010
readonly modelProvider?: string;
1111
readonly modelName?: string;
12+
readonly profile?: boolean;
1213
};
1314

1415
export const sharedOptions = {
@@ -19,6 +20,11 @@ export const sharedOptions = {
1920
default: 'info',
2021
choices: ['debug', 'verbose', 'info', 'warn', 'error'],
2122
} as const,
23+
profile: {
24+
type: 'boolean',
25+
description: 'Enable performance profiling of CLI startup',
26+
default: false,
27+
} as const,
2228
modelProvider: {
2329
type: 'string',
2430
description: 'AI model provider to use',
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { performance } from 'perf_hooks';
2+
3+
// Store start time as soon as this module is imported
4+
const cliStartTime = performance.now();
5+
const timings: Record<string, number> = {};
6+
let isEnabled = false;
7+
8+
/**
9+
* Enable or disable performance tracking
10+
*/
11+
export function enableProfiling(enabled: boolean): void {
12+
isEnabled = enabled;
13+
}
14+
15+
/**
16+
* Mark a timing point in the application
17+
*/
18+
export function mark(label: string): void {
19+
if (!isEnabled) return;
20+
timings[label] = performance.now() - cliStartTime;
21+
}
22+
23+
/**
24+
* Log all collected performance metrics
25+
*/
26+
export async function reportTimings(): Promise<void> {
27+
if (!isEnabled) return;
28+
29+
console.log('\n📊 Performance Profile:');
30+
console.log('=======================');
31+
32+
// Sort timings by time value
33+
const sortedTimings = Object.entries(timings)
34+
.sort((a, b) => a[1] - b[1]);
35+
36+
// Calculate durations between steps
37+
let previousTime = 0;
38+
for (const [label, time] of sortedTimings) {
39+
const duration = time - previousTime;
40+
console.log(`${label}: ${time.toFixed(2)}ms (${duration.toFixed(2)}ms)`);
41+
previousTime = time;
42+
}
43+
44+
console.log(`Total startup time: ${previousTime.toFixed(2)}ms`);
45+
console.log('=======================\n');
46+
47+
// Report platform-specific information if on Windows
48+
if (process.platform === 'win32') {
49+
await reportPlatformInfo();
50+
}
51+
}
52+
53+
/**
54+
* Collect and report platform-specific information
55+
*/
56+
async function reportPlatformInfo(): Promise<void> {
57+
if (!isEnabled) return;
58+
59+
console.log('\n🖥️ Platform Information:');
60+
console.log('=======================');
61+
console.log(`Platform: ${process.platform}`);
62+
console.log(`Architecture: ${process.arch}`);
63+
console.log(`Node.js version: ${process.version}`);
64+
65+
// Windows-specific information
66+
if (process.platform === 'win32') {
67+
console.log('Windows-specific details:');
68+
console.log(`- Current working directory: ${process.cwd()}`);
69+
console.log(`- Path length: ${process.cwd().length} characters`);
70+
71+
// Check for antivirus markers by measuring file read time
72+
try {
73+
// Using dynamic import to avoid require
74+
const fs = await import('fs');
75+
const startTime = performance.now();
76+
fs.readFileSync(process.execPath);
77+
console.log(`- Time to read Node.js executable: ${(performance.now() - startTime).toFixed(2)}ms`);
78+
} catch (error: unknown) {
79+
const errorMessage = error instanceof Error ? error.message : String(error);
80+
console.log(`- Error reading Node.js executable: ${errorMessage}`);
81+
}
82+
}
83+
84+
console.log('=======================\n');
85+
}
86+
87+
// Initial mark for module load time
88+
mark('Module initialization');

0 commit comments

Comments
 (0)