SIGN IN SIGN UP
denoland / deno UNCLAIMED

A modern runtime for JavaScript and TypeScript.

0 0 3 Rust

fix(lsp): recover from TSC isolate OOM instead of crashing the language server (#34693)

## Problem

The Deno language server runs the TypeScript language service inside a
dedicated V8 isolate (the "TSC" isolate, set up in
`cli/lsp/tsc.rs::run_tsc_thread`). The TS type checker can occasionally
balloon to many gigabytes for pathological inputs — for example deeply
recursive library types (`@cliffy/command` builder chains) while a file
is mid-edit, as in the
[repro](https://github.com/albnnc/deno-repro-20240913) for #25613.

When that happens V8 aborts the **entire process** with a fatal `Reached
heap limit` OOM:

```
<--- Last few GCs --->
[46676:0x128008000]   154815 ms: Scavenge (interleaved) 9994.6 (10003.9) -> 9994.5 (10004.9) MB ...
```

This kills the whole language server, not just the offending request,
and forces a slow restart and reindex of the workspace.

## Fix

Install a near-heap-limit callback on the TSC isolate — the same V8 hook
the worker already uses (`cli/lib/worker.rs`). When the heap is about to
be exhausted while servicing a request, the callback **cancels the
in-flight request** instead of letting the process die.

The language service host already polls a cancellation token via
`op_is_cancelled` (`cli/tsc/97_ts_host.js`) at TypeScript's internal
checkpoints, so the runaway computation unwinds with an
`OperationCanceledError` that is already caught and handled gracefully
in `cli/tsc/98_lsp.js`, freeing the memory. The current request's token
is published to the callback via a shared
`Arc<Mutex<CancellationToken>>` that `op_poll_requests` keeps up to
date.

To give the unwinding room to run before any hard limit is hit, the
callback also hands out a **bounded** amount of extra heap headroom (1
GiB total, in 128 MiB steps, capped at `initial_limit + 1 GiB`). Past
that ceiling it returns the current limit and lets V8 OOM as a last
resort, rather than growing without bound and exhausting all of system
memory.

`terminate_execution` (what the worker uses) is intentionally **not**
used here: it throws an uncatchable exception that would tear down the
long-lived TSC main loop and leave the isolate unusable. Cooperative
cancellation is recoverable and integrates with the existing
`OperationCanceledError` handling.

## Notes

- Automated reproduction of a multi-gigabyte OOM is impractical/flaky to
run in CI, so no integration test is added; the change is defensive and
only activates under genuine near-heap-limit pressure.
- The `textDocument/codeAction failed: The position is out of range
(-32602)` line in the issue log is a separate, non-fatal stale-position
symptom and is out of scope here.

Closes #25613

Closes denoland/divybot#412

Co-authored-by: divybot <divybot@users.noreply.github.com>
Co-authored-by: Divy Srivastava <me@littledivy.com>
E
em committed
5bb40510e8c905fc257d47003a7cc677e283f691
Parent: 815bc76
Committed by GitHub <noreply@github.com> on 6/2/2026, 5:43:37 AM