qwen-code/packages/cli/index.ts
易良 5048f5f314
feat(cli): add CPU profiling support for Chrome DevTools analysis (#4620)
* feat(cli): add CPU profiling support for Chrome DevTools analysis (#4617)

Add a cpuProfiler module that generates .cpuprofile files loadable in
Chrome DevTools Performance tab. Three trigger modes:

- Environment variable: QWEN_CODE_CPU_PROFILE=1 records from start to exit
- Signal toggle: SIGUSR1 starts/stops recording (Linux/macOS)
- Command: /doctor cpu-profile [--duration N] records for N seconds

Output written to ~/.qwen/cpu-profiles/ with safety guards (rate limit,
max 5 files, disk space check). Complements existing heap snapshot and
memory diagnostics infrastructure.

* fix(cli): address cpu-profile review feedback and CI failures

- Platform-aware SIGUSR1 hint (suppress on Windows)
- Log warning when statfsSync is unavailable instead of silent swallow
- Improve error messages for stopping state and lost session
- Add user feedback on abort (report saved file path)
- Fix non-deterministic sort in cleanupOldProfiles on stat failure
- Add initCpuProfiler() ordering comment
- Unify copyright year to 2025
- Add InspectorSession interface rationale comment
- Fix doctorCommand.test.ts assertions for new cpu-profile subcommand
- Add zh-CN/zh-TW translations for cpu-profile command description

* fix(i18n): add missing CPU profile key to en.js

The i18n check failed because zh.js and zh-TW.js had the
"Record a CPU profile for Chrome DevTools analysis" key but
en.js did not. Add the identity mapping to en.js.

* fix(cli): reset profiler state on rate-limit and tailor Windows messages

- When enforceRateLimit() throws in stopCpuProfile(), explicitly stop the
  V8 profiler, disconnect the session, and reset state to 'idle' so the
  user is not stuck and can start a fresh recording.
- Tailor the "stopped externally" info message to omit SIGUSR1 on Windows
  where the signal does not exist.
- Add test verifying state recovery after rate-limit rejection.
2026-06-03 03:01:29 +00:00

117 lines
3.4 KiB
JavaScript

#!/usr/bin/env node
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { initStartupProfiler } from './src/utils/startupProfiler.js';
// Must run before any other imports to capture the earliest possible T0.
initStartupProfiler();
import { initCpuProfiler } from './src/utils/cpuProfiler.js';
// Initialize early to register SIGUSR1 handler and start recording when
// QWEN_CODE_CPU_PROFILE=1, capturing as much of the startup as possible.
initCpuProfiler();
import './src/gemini.js';
import { main } from './src/gemini.js';
import { FatalError } from '@qwen-code/qwen-code-core';
import { AlreadyReportedError } from './src/utils/errors.js';
import { writeStderrLine } from './src/utils/stdioHelpers.js';
// --- Global Entry Point ---
// Suppress known race conditions in @lydell/node-pty.
//
// PTY errors that are expected due to timing races between process exit
// and I/O operations. These should not crash the app.
//
// References:
// - https://github.com/microsoft/node-pty/issues/178 (EIO on macOS/Linux)
// - https://github.com/microsoft/node-pty/issues/827 (resize on Windows)
const getErrnoCode = (error: unknown): string | undefined => {
if (!error || typeof error !== 'object') {
return undefined;
}
const code = (error as { code?: unknown }).code;
return typeof code === 'string' ? code : undefined;
};
const isExpectedPtyRaceError = (error: unknown): boolean => {
if (!(error instanceof Error)) {
return false;
}
const message = error.message;
const code = getErrnoCode(error);
// EIO: PTY read race on macOS/Linux - code + PTY context required
// https://github.com/microsoft/node-pty/issues/178
if (
(code === 'EIO' && message.includes('read')) ||
message.includes('read EIO')
) {
return true;
}
// EAGAIN: transient non-blocking read error from PTY fd
if (
(code === 'EAGAIN' && message.includes('read')) ||
message.includes('read EAGAIN')
) {
return true;
}
// PTY-specific resize/exit race errors - require PTY context in message
if (
message.includes('ioctl(2) failed, EBADF') ||
message.includes('Cannot resize a pty that has already exited')
) {
return true;
}
return false;
};
process.on('uncaughtException', (error) => {
if (isExpectedPtyRaceError(error)) {
return;
}
if (error instanceof Error) {
writeStderrLine(error.stack ?? error.message);
} else {
writeStderrLine(String(error));
}
process.exit(1);
});
main().catch((error) => {
if (error instanceof FatalError) {
let errorMessage = error.message;
if (!process.env['NO_COLOR']) {
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
}
console.error(errorMessage);
process.exit(error.exitCode);
}
// AlreadyReportedError means an upstream layer (e.g. the non-interactive
// stream-error handler) has already written the user-facing message to
// stderr and just wants to surface a non-zero exit code. Don't print
// "An unexpected critical error occurred:" with a stack trace — that
// framing is for genuinely unexpected, programmer-level bugs, and a
// routine 4xx from an upstream API does not qualify.
if (error instanceof AlreadyReportedError) {
process.exit(error.exitCode);
}
console.error('An unexpected critical error occurred:');
if (error instanceof Error) {
console.error(error.stack);
} else {
console.error(String(error));
}
process.exit(1);
});