perf(startup): load canvas, node-pty, ssh2, html-to-text and the OpenCode SDK on first use

Main module evaluation drops from ~420 ms to ~330 ms. Types stay as import type; the DeepSeek Harness chunks stay static because the protocol chunk shares code with Main startup paths.
This commit is contained in:
mesalogo
2026-10-03 17:15:13 +08:00
parent 68cc0792e1
commit 010aafb6f4
12 changed files with 104 additions and 26 deletions
+24 -7
View File
@@ -1,11 +1,11 @@
import { runtimeChecklistSchema, type RuntimeChecklist } from '../../shared/runtime-checklist';
import {
import type {
createOpencodeClient,
type AssistantMessage,
type OpencodeClient,
type PermissionRequest,
type PermissionRuleset,
type QuestionRequest,
AssistantMessage,
OpencodeClient,
PermissionRequest,
PermissionRuleset,
QuestionRequest,
} from "@opencode-ai/sdk/v2";
import spawn from "cross-spawn";
import { createHash, randomBytes } from "node:crypto";
@@ -740,11 +740,27 @@ async function defaultValidateBinary(
});
}
type OpencodeSdk = typeof import("@opencode-ai/sdk/v2");
let opencodeSdk: OpencodeSdk | undefined;
// The SDK is loaded when an OpenCode client is first created, not at Main
// startup (P6). initializeClient awaits this before any default factory runs.
async function loadOpencodeSdk(): Promise<OpencodeSdk> {
opencodeSdk ??= await import("@opencode-ai/sdk/v2");
return opencodeSdk;
}
const defaultCreateClient: typeof createOpencodeClient = (config) => {
if (!opencodeSdk) throw new Error("OpenCode SDK is not loaded");
return opencodeSdk.createOpencodeClient(config);
};
async function defaultCheckServerHealth(
url: string,
authorization: string,
signal: AbortSignal,
): Promise<boolean> {
const { createOpencodeClient } = await loadOpencodeSdk();
const client = createOpencodeClient({
baseUrl: url,
headers: { Authorization: authorization },
@@ -798,7 +814,7 @@ export class OpenCodeRuntime implements AgentRuntime {
spawn,
detectBinary: defaultDetectBinary,
validateBinary: defaultValidateBinary,
createClient: createOpencodeClient,
createClient: defaultCreateClient,
checkServerHealth: defaultCheckServerHealth,
platform: process.platform,
startupTimeoutMs: STARTUP_TIMEOUT_MS,
@@ -1483,6 +1499,7 @@ export class OpenCodeRuntime implements AgentRuntime {
if (baseUrl && this.options.modelProfile) {
throw new Error("OpenCode 独立模型连接仅支持由 GoodBuddy 启动的本机服务");
}
await loadOpencodeSdk();
if (!baseUrl && this.options.embedded) {
this.server = await this.launchEmbedded(signal);
baseUrl = this.server.url;
+9 -2
View File
@@ -1,8 +1,15 @@
import { convert } from 'html-to-text'
import { createLazyRequire } from './lazy-require'
/**
* Loads html-to-text on first use instead of at Main startup (P6, ~45 ms).
* Synchronous so the text checks keep their signatures.
*/
export const loadHtmlToText =
createLazyRequire<typeof import('html-to-text')>('html-to-text')
export function hasExtractedDocumentText(content: string): boolean {
const text = content
.replace(/!\[[^\]]*\]\([^)]*\)/gu, '')
.replace(/\[第 \d+ 页图片未保存:[^\]]*\]/gu, '')
return Boolean(convert(text, { wordwrap: false }).trim())
return Boolean(loadHtmlToText().convert(text, { wordwrap: false }).trim())
}
+2 -1
View File
@@ -1,7 +1,6 @@
import { randomUUID } from 'node:crypto'
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
import { basename, dirname, extname, join } from 'node:path'
import { createCanvas, loadImage } from '@napi-rs/canvas'
import { documentResultSchema, type DocumentResult } from '../shared/document-result-contracts'
import type { DocumentParsingSettings } from '../shared/document-parsing-contracts'
import type { ParsedDocument } from './knowledge/document-parser'
@@ -96,6 +95,8 @@ export class DocumentResultStorage {
if (!image) throw new Error('解析图片不存在')
const data = await readFile(join(this.require(id).directory, 'images', imageId))
if (thumbnail) {
// Native canvas loads on first use (P6).
const { createCanvas, loadImage } = await import('@napi-rs/canvas')
const source = await loadImage(data)
const scale = Math.min(1, 640 / Math.max(source.width, source.height))
const canvas = createCanvas(Math.max(1, Math.round(source.width * scale)), Math.max(1, Math.round(source.height * scale)))
+2 -1
View File
@@ -1,5 +1,4 @@
import { randomUUID } from 'node:crypto'
import { loadImage } from '@napi-rs/canvas'
import { z } from 'zod'
import {
maximumDocumentExtractedCharacters,
@@ -131,6 +130,8 @@ export class HttpDocumentOcr {
: bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) ? 'image/png'
: bytes.toString('ascii', 0, 4) === 'RIFF' && bytes.toString('ascii', 8, 12) === 'WEBP' ? 'image/webp' : undefined
if (!mimeType) throw new Error('图片格式不支持')
// Native canvas loads on first use (P6).
const { loadImage } = await import('@napi-rs/canvas')
const image = await loadImage(bytes)
if (!image.width || !image.height || image.width * image.height > 40_000_000) throw new Error('图片尺寸无效或超过 4000 万像素')
imageBytes += bytes.length
+2 -2
View File
@@ -1,4 +1,4 @@
import { convert } from 'html-to-text'
import { loadHtmlToText } from '../document-extracted-text'
import { unzipSync } from 'fflate'
import { extname } from 'node:path'
import type { ParsedDocumentImage } from '../http-document-ocr'
@@ -701,7 +701,7 @@ export async function parseDocument(
} else if (['.docx', '.xlsx', '.pptx'].includes(extension)) {
sections = parseOfficeArchive(buffer, extension)
} else if (['.html', '.htm'].includes(extension)) {
const content = convert(decodeText(buffer), {
const content = loadHtmlToText().convert(decodeText(buffer), {
wordwrap: false,
selectors: [
{ selector: 'script', format: 'skip' },
+24
View File
@@ -0,0 +1,24 @@
// @vitest-environment node
import { describe, expect, it } from 'vitest'
import { createLazyRequire } from './lazy-require'
import { loadHtmlToText } from './document-extracted-text'
import { loadSsh2 } from './ssh/ssh2-module'
describe('createLazyRequire', () => {
it('loads a module on first call and returns the cached instance afterwards', () => {
const load = createLazyRequire<typeof import('node:path')>('node:path')
const first = load()
expect(typeof first.join).toBe('function')
expect(load()).toBe(first)
})
it('surfaces a missing module at first use, not at import time', () => {
const load = createLazyRequire('goodbuddy-module-that-does-not-exist')
expect(load).toThrow()
})
it('resolves the deferred Main dependencies', () => {
expect(typeof loadHtmlToText().convert).toBe('function')
expect(typeof loadSsh2().Client).toBe('function')
})
})
+14
View File
@@ -0,0 +1,14 @@
import { createRequire } from 'node:module'
/**
* Synchronously loads a dependency on first use instead of at module
* evaluation (P6). Main is ESM, but some test fixtures bundle the same code as
* CommonJS, where `import.meta.url` is empty and `__filename` exists instead.
*/
export function createLazyRequire<T>(specifier: string): () => T {
let loaded: T | undefined
return () => {
loaded ??= createRequire(import.meta.url || __filename)(specifier) as T
return loaded
}
}
+5 -2
View File
@@ -1,4 +1,4 @@
import { createCanvas } from '@napi-rs/canvas'
// @napi-rs/canvas is a native module (~85 ms to load); load it on first OCR render (P6).
export async function* renderOcrPdf(
buffer: Buffer,
@@ -6,7 +6,10 @@ export async function* renderOcrPdf(
signal?: AbortSignal
): AsyncGenerator<{ pageNumber: number; data: Buffer; width: number; height: number }> {
signal?.throwIfAborted()
const { getDocument } = await import('pdfjs-dist/legacy/build/pdf.mjs')
const [{ getDocument }, { createCanvas }] = await Promise.all([
import('pdfjs-dist/legacy/build/pdf.mjs'),
import('@napi-rs/canvas')
])
const task = getDocument({
data: Uint8Array.from(buffer), disableFontFace: true,
isOffscreenCanvasSupported: false, useSystemFonts: false, useWorkerFetch: false
+3 -3
View File
@@ -1,7 +1,7 @@
import { createHmac, getCiphers, randomBytes } from "node:crypto";
import { connect as connectLoopback, type Socket } from "node:net";
import { Client } from "ssh2";
import type { ClientChannel, ConnectConfig, SFTPWrapper } from "ssh2";
import type { Client, ClientChannel, ConnectConfig, SFTPWrapper } from "ssh2";
import { loadSsh2 } from "./ssh2-module";
import {
BoundedStagedSftp,
type BoundedSftpLimits,
@@ -344,7 +344,7 @@ export class Ssh2AuthenticatedConnection implements AuthenticatedSshConnection {
validatePoolTarget(target);
validateConnectionIdentity(target, identity);
const resolvedDependencies: SshConnectionDependencies = {
createClient: () => new Client(),
createClient: () => new (loadSsh2().Client)(),
supportedOpenSslCiphers: getCiphers,
systemAgent: defaultSystemAgent,
controlPlanePackageInstaller: Buffer.alloc(0),
+7 -6
View File
@@ -1,11 +1,12 @@
import { createHash, getCiphers } from 'node:crypto'
import {
import type {
Client,
type Algorithms,
type CipherAlgorithm,
type ConnectConfig,
type ServerHostKeyAlgorithm
Algorithms,
CipherAlgorithm,
ConnectConfig,
ServerHostKeyAlgorithm
} from 'ssh2'
import { loadSsh2 } from './ssh2-module'
import type {
SshHostConnectionTestResult
} from '../../shared/ssh-host-contracts'
@@ -336,7 +337,7 @@ export class Ssh2Transport implements SshTransport {
dependencies: Partial<SshTransportDependencies> = {}
) {
this.dependencies = {
createClient: () => new Client(),
createClient: () => new (loadSsh2().Client)(),
now: Date.now,
supportedOpenSslCiphers: getCiphers,
systemAgent: defaultSystemAgent,
+8
View File
@@ -0,0 +1,8 @@
import { createLazyRequire } from '../lazy-require'
/**
* Loads ssh2 on the first SSH connection instead of at Main startup (P6):
* evaluating it costs ~65 ms and most sessions never open a remote host.
* Synchronous so existing client factories keep their signatures.
*/
export const loadSsh2 = createLazyRequire<typeof import('ssh2')>('ssh2')
+4 -2
View File
@@ -3,7 +3,7 @@ import { constants as fileSystemConstants } from 'node:fs'
import { access, stat } from 'node:fs/promises'
import { homedir } from 'node:os'
import { posix, win32 } from 'node:path'
import * as nodePty from 'node-pty'
import type * as nodePty from 'node-pty'
import type { IDisposable, IPty } from 'node-pty'
import {
TERMINAL_LIMITS,
@@ -393,7 +393,9 @@ export class LocalTerminalSession {
: undefined,
this.dependencies
)
const spawn = this.dependencies.spawn ?? nodePty.spawn
// node-pty is native; load it when the first terminal starts (P6).
const spawn = this.dependencies.spawn ??
(await import('node-pty')).spawn
this.pty = spawn(this.launch.shell, spec?.args ?? [], {
name: 'xterm-256color',
cols: this.size.cols,