[heft] Faster startup, same API (v2) - #6097
Sean Larkin (TheLarkInn) wants to merge 1 commit into
Conversation
Heft loads less at startup. Config, CLI, plugin framework and shared libraries load lazily. Common paths skip ajv, heft-config-file, argparse and fast-glob. Plugins load task code when the task runs. Node's compile cache is enabled at startup. heft --help: 407 ms -> 86 ms. First task: 406 ms -> 90 ms. Peak RSS: -31%. API reports unchanged. CLI output byte-identical. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| // which avoids loading fs-extra, and only use fs-extra to retry in the EMFILE/ENFILE case. | ||
| const { data, error } = await new Promise( | ||
| (resolve: (result: { data?: Buffer; error?: NodeJS.ErrnoException }) => void) => { | ||
| fs.readFile(filePath, (readError: NodeJS.ErrnoException | null, readData: Buffer) => |
| throw error; | ||
| } | ||
|
|
||
| return await _getFsx().readFile(filePath); |
| // Enable the V8 compile cache before anything else is loaded (Node.js >= 22.8). It honors the | ||
| // NODE_COMPILE_CACHE and NODE_DISABLE_COMPILE_CACHE environment variables and fails silently. | ||
| try { | ||
| require('../lib-commonjs/bootstrap/CompileCache.js').tryEnableCompileCache(); | ||
| } catch {} |
There was a problem hiding this comment.
Node compile cache is a double-edged sword. It adds a lot of extra file system operations unless the implementation has changed, so is really only effective for low numbers of large files.
| } | ||
|
|
||
| function defineParameter(provider: CommandLineParameterProvider, definition: IDefinition): CommandLineParameter { | ||
| const { kind, ...rest } = definition; |
There was a problem hiding this comment.
Does passing kind to the define calls throw an error? Otherwise switch(definition.kind) should be able to act as a discriminant and just pass definition to each of the methods.
| // operations. | ||
| const executionManager: IOperationExecutionManager = this.#action.watch | ||
| ? new (await import('@rushstack/operation-graph')).OperationExecutionManager(operations) | ||
| : new (await import('../operations/OperationExecutionManager')).OperationExecutionManager(operations); |
There was a problem hiding this comment.
If you want a forked operation manager for watch vs. not, do it in operation-graph
| * byte-identical results (help, errors, unusual syntax, etc.) is handled by the full ts-command-line based | ||
| * implementation in `HeftFullCommandLineParser`. | ||
| */ | ||
| export class HeftCommandLineParser { |
There was a problem hiding this comment.
TL;DR, ts-command-line is overly expensive and needs a rewrite/optimization?
| } | ||
|
|
||
| class SchemaAnalyzer { | ||
| private readonly _root: ISchemaObject; |
There was a problem hiding this comment.
Use ECMAScript private class fields, not typescript private
| /** | ||
| * node:child_process (which also loads the net, dgram and stream implementations) is only loaded when needed. | ||
| */ | ||
| function _getChildProcess(): typeof child_process { |
There was a problem hiding this comment.
This sort of thing should be done by transpiling with SWC and setting lazy: true
Heft starts faster. Same API.
Before → after
heft 1.3.1 → this PR. Linux x64 (WSL2), Node 22, 50 interleaved runs, pinned CPUs.
heft --help--helpStill 3–4× faster than 1.3.1 with
NODE_COMPILE_CACHEon.What changed
Compatibility
Notes