perf: optimize startup time by using dynamic imports for heavy dependencies

This commit is contained in:
Sehoon Shon
2026-03-24 00:21:43 -04:00
parent 1c1416678d
commit baeff5b9dd
6 changed files with 80 additions and 51 deletions
@@ -21,8 +21,6 @@ import {
RestTransportFactory,
createAuthenticatingFetchWithRetry,
} from '@a2a-js/sdk/client';
import { GrpcTransportFactory } from '@a2a-js/sdk/client/grpc';
import * as grpc from '@grpc/grpc-js';
import { v4 as uuidv4 } from 'uuid';
import { Agent as UndiciAgent, ProxyAgent } from 'undici';
import { normalizeAgentCard } from './a2aUtils.js';
@@ -129,6 +127,8 @@ export class A2AClientManager {
agentCard.additionalInterfaces?.find((i) => i.transport === 'GRPC')
?.url ?? agentCard.url;
const { GrpcTransportFactory } = await import('@a2a-js/sdk/client/grpc');
const grpc = await import('@grpc/grpc-js');
const clientOptions = ClientFactoryOptions.createFrom(
ClientFactoryOptions.default,
{
+13 -5
View File
@@ -8,10 +8,16 @@ import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { isNodeError } from '../utils/errors.js';
import { spawnAsync } from '../utils/shell-utils.js';
import { simpleGit, CheckRepoActions, type SimpleGit } from 'simple-git';
import type { SimpleGit } from 'simple-git';
import type { Storage } from '../config/storage.js';
import { debugLogger } from '../utils/debugLogger.js';
async function getSimpleGit() {
const { simpleGit, CheckRepoActions } = await import('simple-git');
return { simpleGit, CheckRepoActions };
}
export class GitService {
private projectRoot: string;
private storage: Storage;
@@ -79,6 +85,7 @@ export class GitService {
const shadowRepoEnv = this.getShadowRepoEnv(repoDir);
await fs.writeFile(shadowRepoEnv.GIT_CONFIG_SYSTEM, '');
const { simpleGit, CheckRepoActions } = await getSimpleGit();
const repo = simpleGit(repoDir).env(shadowRepoEnv);
let isRepoDefined = false;
try {
@@ -114,8 +121,9 @@ export class GitService {
await fs.writeFile(shadowGitIgnorePath, userGitIgnoreContent);
}
private get shadowGitRepository(): SimpleGit {
private async shadowGitRepository(): Promise<SimpleGit> {
const repoDir = this.getHistoryDir();
const { simpleGit } = await getSimpleGit();
return simpleGit(this.projectRoot).env({
GIT_DIR: path.join(repoDir, '.git'),
GIT_WORK_TREE: this.projectRoot,
@@ -124,13 +132,13 @@ export class GitService {
}
async getCurrentCommitHash(): Promise<string> {
const hash = await this.shadowGitRepository.raw('rev-parse', 'HEAD');
const hash = await (await this.shadowGitRepository()).raw('rev-parse', 'HEAD');
return hash.trim();
}
async createFileSnapshot(message: string): Promise<string> {
try {
const repo = this.shadowGitRepository;
const repo = await this.shadowGitRepository();
await repo.add('.');
const status = await repo.status();
if (status.isClean()) {
@@ -149,7 +157,7 @@ export class GitService {
}
async restoreProjectFromSnapshot(commitHash: string): Promise<void> {
const repo = this.shadowGitRepository;
const repo = await this.shadowGitRepository();
await repo.raw(['restore', '--source', commitHash, '.']);
// Removes any untracked files that were introduced post snapshot.
await repo.clean('f', ['-d']);
@@ -6,7 +6,6 @@
import { createHash } from 'node:crypto';
import * as os from 'node:os';
import si from 'systeminformation';
import { HttpsProxyAgent } from 'https-proxy-agent';
import type {
StartSessionEvent,
@@ -262,6 +261,7 @@ let cachedGpuInfo: string | undefined;
async function refreshGpuInfo(): Promise<void> {
try {
const si = (await import('systeminformation')).default;
const graphics = await si.graphics();
if (graphics.controllers && graphics.controllers.length > 0) {
cachedGpuInfo = graphics.controllers.map((c) => c.model).join(', ');
+1 -5
View File
@@ -24,11 +24,7 @@ export {
parseBooleanEnvFlag,
parseTelemetryTargetValue,
} from './config.js';
export {
GcpTraceExporter,
GcpMetricExporter,
GcpLogExporter,
} from './gcp-exporters.js';
export {
logCliConfiguration,
logUserPrompt,
+41 -32
View File
@@ -12,29 +12,28 @@ import {
metrics,
propagation,
} from '@opentelemetry/api';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
import { OTLPTraceExporter as OTLPTraceExporterHttp } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPLogExporter as OTLPLogExporterHttp } from '@opentelemetry/exporter-logs-otlp-http';
import { OTLPMetricExporter as OTLPMetricExporterHttp } from '@opentelemetry/exporter-metrics-otlp-http';
import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { resourceFromAttributes } from '@opentelemetry/resources';
import {
import type { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import type { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc';
import type { OTLPTraceExporter as OTLPTraceExporterHttp } from '@opentelemetry/exporter-trace-otlp-http';
import type { OTLPLogExporter as OTLPLogExporterHttp } from '@opentelemetry/exporter-logs-otlp-http';
import type { NodeSDK } from '@opentelemetry/sdk-node';
import type {
BatchSpanProcessor,
ConsoleSpanExporter,
} from '@opentelemetry/sdk-trace-node';
import {
import type {
BatchLogRecordProcessor,
ConsoleLogRecordExporter,
} from '@opentelemetry/sdk-logs';
import {
ConsoleMetricExporter,
import type {
PeriodicExportingMetricReader,
} from '@opentelemetry/sdk-metrics';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import type { JWTInput } from 'google-auth-library';
import type { Config } from '../config/config.js';
import { SERVICE_NAME } from './constants.js';
@@ -45,11 +44,7 @@ import {
FileMetricExporter,
FileSpanExporter,
} from './file-exporters.js';
import {
GcpTraceExporter,
GcpMetricExporter,
GcpLogExporter,
} from './gcp-exporters.js';
import type { GcpTraceExporter , GcpLogExporter } from './gcp-exporters.js';
import { TelemetryTarget } from './index.js';
import { debugLogger } from '../utils/debugLogger.js';
import { authEvents } from '../code_assist/oauth2.js';
@@ -205,6 +200,8 @@ export async function initializeTelemetry(
return;
}
const { resourceFromAttributes } = await import('@opentelemetry/resources');
const { SemanticResourceAttributes } = await import('@opentelemetry/semantic-conventions');
const resource = resourceFromAttributes({
[SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME,
[SemanticResourceAttributes.SERVICE_VERSION]: process.version,
@@ -267,10 +264,12 @@ export async function initializeTelemetry(
'using',
credentials ? 'provided credentials' : 'ADC',
);
const { GcpTraceExporter } = await import('./gcp-exporters.js');
spanExporter = new GcpTraceExporter(gcpProjectId, credentials);
const { GcpLogExporter } = await import('./gcp-exporters.js');
logExporter = new GcpLogExporter(gcpProjectId, credentials);
metricReader = new PeriodicExportingMetricReader({
exporter: new GcpMetricExporter(gcpProjectId, credentials),
metricReader = new (await import('@opentelemetry/sdk-metrics')).PeriodicExportingMetricReader({
exporter: new (await import('./gcp-exporters.js')).GcpMetricExporter(gcpProjectId, credentials),
exportIntervalMillis: 30000,
});
} else if (useOtlp) {
@@ -281,32 +280,36 @@ export async function initializeTelemetry(
url.pathname = [url.pathname.replace(/\/$/, ''), path].join('/');
return url.href;
};
const { OTLPTraceExporter: OTLPTraceExporterHttp } = await import('@opentelemetry/exporter-trace-otlp-http');
spanExporter = new OTLPTraceExporterHttp({
url: buildUrl('v1/traces'),
});
const { OTLPLogExporter: OTLPLogExporterHttp } = await import('@opentelemetry/exporter-logs-otlp-http');
logExporter = new OTLPLogExporterHttp({
url: buildUrl('v1/logs'),
});
metricReader = new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporterHttp({
metricReader = new (await import('@opentelemetry/sdk-metrics')).PeriodicExportingMetricReader({
exporter: new (await import('@opentelemetry/exporter-metrics-otlp-http')).OTLPMetricExporter({
url: buildUrl('v1/metrics'),
}),
exportIntervalMillis: 10000,
});
} else {
// grpc
const { OTLPTraceExporter } = await import('@opentelemetry/exporter-trace-otlp-grpc');
spanExporter = new OTLPTraceExporter({
url: parsedEndpoint,
compression: CompressionAlgorithm.GZIP,
compression: (await import('@opentelemetry/otlp-exporter-base')).CompressionAlgorithm.GZIP,
});
const { OTLPLogExporter } = await import('@opentelemetry/exporter-logs-otlp-grpc');
logExporter = new OTLPLogExporter({
url: parsedEndpoint,
compression: CompressionAlgorithm.GZIP,
compression: (await import('@opentelemetry/otlp-exporter-base')).CompressionAlgorithm.GZIP,
});
metricReader = new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
metricReader = new (await import('@opentelemetry/sdk-metrics')).PeriodicExportingMetricReader({
exporter: new (await import('@opentelemetry/exporter-metrics-otlp-grpc')).OTLPMetricExporter({
url: parsedEndpoint,
compression: CompressionAlgorithm.GZIP,
compression: (await import('@opentelemetry/otlp-exporter-base')).CompressionAlgorithm.GZIP,
}),
exportIntervalMillis: 10000,
});
@@ -314,23 +317,29 @@ export async function initializeTelemetry(
} else if (telemetryOutfile) {
spanExporter = new FileSpanExporter(telemetryOutfile);
logExporter = new FileLogExporter(telemetryOutfile);
metricReader = new PeriodicExportingMetricReader({
metricReader = new (await import('@opentelemetry/sdk-metrics')).PeriodicExportingMetricReader({
exporter: new FileMetricExporter(telemetryOutfile),
exportIntervalMillis: 10000,
});
} else {
const { ConsoleSpanExporter } = await import('@opentelemetry/sdk-trace-node');
spanExporter = new ConsoleSpanExporter();
const { ConsoleLogRecordExporter } = await import('@opentelemetry/sdk-logs');
logExporter = new ConsoleLogRecordExporter();
metricReader = new PeriodicExportingMetricReader({
exporter: new ConsoleMetricExporter(),
metricReader = new (await import('@opentelemetry/sdk-metrics')).PeriodicExportingMetricReader({
exporter: new (await import('@opentelemetry/sdk-metrics')).ConsoleMetricExporter(),
exportIntervalMillis: 10000,
});
}
// Store processor references for manual flushing
const { BatchSpanProcessor } = await import('@opentelemetry/sdk-trace-node');
spanProcessor = new BatchSpanProcessor(spanExporter);
const { BatchLogRecordProcessor } = await import('@opentelemetry/sdk-logs');
logRecordProcessor = new BatchLogRecordProcessor(logExporter);
const { NodeSDK } = await import('@opentelemetry/sdk-node');
const { HttpInstrumentation } = await import('@opentelemetry/instrumentation-http');
sdk = new NodeSDK({
resource,
spanProcessors: [spanProcessor],
+22 -6
View File
@@ -14,7 +14,7 @@ import {
type SpawnOptionsWithoutStdio,
} from 'node:child_process';
import * as readline from 'node:readline';
import { Language, Parser, Query, type Node, type Tree } from 'web-tree-sitter';
import type { Language, Parser, Node, Tree, QueryCapture } from 'web-tree-sitter';
import { loadWasmBinary } from './fileUtils.js';
import { debugLogger } from './debugLogger.js';
import type { SandboxManager } from '../services/sandboxManager.js';
@@ -70,6 +70,8 @@ export async function resolveExecutable(
return undefined;
}
let _Parser: any = null;
let _Query: any = null;
let bashLanguage: Language | null = null;
let treeSitterInitialization: Promise<void> | null = null;
let treeSitterInitializationError: Error | null = null;
@@ -113,8 +115,20 @@ async function loadBashLanguage(): Promise<void> {
),
]);
await Parser.init({ wasmBinary: treeSitterBinary });
bashLanguage = await Language.load(bashBinary);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
const wts: any = await import('web-tree-sitter');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const ParserImpl = wts.default || wts;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const LanguageImpl = wts.Language || ParserImpl.Language;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
_Query = wts.Query || ParserImpl.Query;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
_Parser = ParserImpl;
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
await ParserImpl.init({ wasmBinary: treeSitterBinary });
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
bashLanguage = await LanguageImpl.load(bashBinary);
} catch (error) {
bashLanguage = null;
const normalized = toError(error);
@@ -213,7 +227,8 @@ function createParser(): Parser | null {
}
try {
const parser = new Parser();
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const parser = new _Parser() as Parser;
parser.setLanguage(bashLanguage);
return parser;
} catch {
@@ -404,9 +419,10 @@ function parseBashCommandDetails(command: string): CommandParseResult | null {
if (hasError) {
let query = null;
try {
query = new Query(bashLanguage, '(ERROR) @error (MISSING) @missing');
query = // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
new _Query(bashLanguage, '(ERROR) @error (MISSING) @missing');
const captures = query.captures(tree.rootNode);
const syntaxErrors = captures.map((capture) => {
const syntaxErrors = captures.map((capture: QueryCapture) => {
const { node, name } = capture;
const type = name === 'missing' ? 'Missing' : 'Error';
return `${type} node: "${node.text}" at ${node.startPosition.row}:${node.startPosition.column}`;