qwen-code/packages
Shaojin Wen 609b4324f6
perf(core): cut runtime sync I/O on tool hot path by 91% (#3581)
* perf(core): make chat recording writes async

Every recorded chat event (user message, assistant turn, tool call,
tool result, slash command, etc.) was issuing 4 sync fs syscalls on
the main event loop: existsSync(dir) + mkdirSync(dir) + existsSync(file)
+ appendFileSync(file). For a tool-heavy prompt this added ~88 sync
I/O calls per session, blocking the UI render and keypress handler
during each one.

- chatRecordingService.appendRecord: cache ensure-flags so dir/file
  creation runs once per session, then enqueue the actual write on a
  per-instance promise chain (writeChain). lastRecordUuid is updated
  synchronously so chained createBaseRecord still sees the right
  parentUuid without waiting for the previous write.
- chatRecordingService.flush: drains the chain — wired into
  Config.shutdown so no records are lost on exit.
- jsonl-utils.writeLine: now actually async (fs.promises.mkdir +
  fs.promises.appendFile) with per-dir mkdir cache. The existing
  per-file mutex still serializes writes correctly.
- Tests updated to await flush() before assertions.

Trace measurement on a single tool-heavy prompt: 110 → 20 sync I/O
calls (-82%), with chatRecordingService dropping from 88 to 0.

* perf(core): cache repeated fs lookups on tool hot path

Each tool invocation went through validatePath → isPathWithinWorkspace
→ fullyResolvedPath, plus its own existence/dir checks. The same paths
got re-resolved across back-to-back tool calls, and ripGrep re-
discovered .qwenignore on every Grep.

- workspaceContext.fullyResolvedPath: bounded LRU on input path
  (1024, FIFO). Failed resolutions are NOT cached so retries work.
- paths.validatePath: cache positive isDirectory results; ENOENT
  falls through every time so a freshly created file is picked up
  immediately.
- ripGrep: module-level caches for searchPath-is-dir and per-dir
  .qwenignore presence (256 each, FIFO).
- fileUtils.processSingleFileContent: drop the existsSync gate;
  let fs.promises.stat throw ENOENT and convert to FILE_NOT_FOUND
  in catch.

Trace: 20 → 10 sync I/O calls. Cumulative reduction since the
chat-recording change: 110 → 10, -91%. All 6057 core tests pass.

* test(core): cache reset hooks + regression-guards from audit

Self-review pass on the previous two perf commits surfaced a few
follow-ups worth pinning down before they bite:

- Module-level caches (paths.isDirectoryCache, ripGrep dirIsDir/qwen-
  Ignore, jsonl-utils.ensuredDirs) persisted across vitest cases
  silently. Added underscore-prefixed `_reset*ForTest` exports and
  wired one into the validatePath describe block so future cases
  mutating the same absolute paths can't pass by accident.
- Documented the parentUuid-chain tradeoff on chatRecordingService
  .appendRecord: when the async write rejects, lastRecordUuid was
  already set sync, so subsequent records reference an absent
  ancestor — readers like sessionService.reconstructHistory then
  silently drop those descendants. Same observable failure mode as
  the prior sync code's caught-and-logged throw.
- Documented the dir<->file mutation and mid-session .qwenignore
  staleness windows for the validatePath / ripGrep caches.
- Added regression tests:
    * validatePath does NOT cache ENOENT (Edit-then-Read works)
    * validatePath skips re-stat on cache hit (perf assertion)
    * flush() resolves immediately on a fresh service
    * a rejected writeLine does not block the next record

Full core suite: 6061 pass, 2 skipped — no regressions.

* fix(core): cache chatsDirEnsured only on mkdir success

Pre-fix, the flag flipped to true even when mkdirSync threw, so a single
transient failure (NFS EACCES, sandbox mount race, parent dir briefly
missing) would short-circuit every subsequent appendRecord and silently
drop the rest of the session's transcript with no error surfaced.

Reported by zhangxy-zju on #3581.

* fix(cli): destroy stdout instead of process.exit on EPIPE

Routine CLI patterns like `qwen -p ... | head -1` / `| less` / `| grep -m1`
close the downstream pipe and trigger EPIPE. The previous handler called
process.exit(0), which bypassed the caller's runExitCleanup -> Config
.shutdown -> chat-recording flush() chain and silently dropped queued
JSONL writes (most recent assistant turn + tool results).

Destroying stdout instead lets writes fail fast and the natural function
return drive cleanup. We deliberately do not also abortController.abort()
here: the abort path runs handleCancellationError which itself calls
process.exit(130), re-introducing the same bypass.

Reported by zhangxy-zju on #3581.

* fix(cli): bound runExitCleanup with per-fn + wall-clock timeouts

Pre-fix, runExitCleanup was an unbounded series of awaits. After the
async-jsonl change moved chat-recording writes off the calling thread
(Config.shutdown now `await flush()`s the queue), any hung syscall
(slow disk, dead NFS mount, stuck MCP socket, telemetry HTTP stall)
would hang process exit indefinitely — sync writes were inherently
bounded by syscall return; async writes are not.

Adds per-cleanup 2s + overall 5s wall-clock failsafes on the same
shape as Claude Code's gracefulShutdown.ts. Also replaces dead
test-isolation code (`global['cleanupFunctions']` was never on global,
the array is module-private) with a `_resetCleanupFunctionsForTest`
hook matching the convention from d6485964c.

Follow-up flagged by zhangxy-zju on #3581.

---------

Co-authored-by: wenshao <wenshao@U-K7F6PQY3-2157.local>
2026-04-24 21:17:51 +08:00
..
channels chore(release): bump version to 0.15.2 (#3596) 2026-04-24 19:55:12 +08:00
cli perf(core): cut runtime sync I/O on tool hot path by 91% (#3581) 2026-04-24 21:17:51 +08:00
core perf(core): cut runtime sync I/O on tool hot path by 91% (#3581) 2026-04-24 21:17:51 +08:00
sdk-java fix(sdk-java): pass custom env to CLI process (#3543) 2026-04-24 10:37:52 +08:00
sdk-typescript feat(web-search): remove built-in web_search tool, replace with MCP-based approach (#3502) 2026-04-24 11:29:02 +08:00
vscode-ide-companion feat(vscode): add native context menu copy actions for webview chat (#3477) 2026-04-24 20:26:56 +08:00
web-templates chore(release): bump version to 0.15.2 (#3596) 2026-04-24 19:55:12 +08:00
webui chore(release): bump version to 0.15.2 (#3596) 2026-04-24 19:55:12 +08:00
zed-extension chore(zed-extension): update package version to 0.10.0 2026-02-06 14:26:01 +08:00