Skip to content

Package resolution (resolve_attached_packages) spawns a fresh R subprocess on nearly every keystroke in large Quarto/R Markdown documents, causing sustained high CPU #754

Description

@mkthalmann

Environment

  • languageserver: 0.3.18
  • callr: 3.8.0
  • R: 4.6.1 (macOS arm64)
  • vscode-R extension: 2.8.8
  • Quarto VS Code extension: 1.135.0
  • Quarto CLI: 1.10.18
  • VS Code: 1.133.0
  • OS: macOS 26.6.2 (Apple Silicon)

Summary

Editing a large .qmd file (prose-heavy, ~20KB, with a handful of embedded R chunks) causes sustained high CPU / fan noise while typing, even when typing in plain prose sections far from any R chunk. Process monitoring shows the language server spawning a brand-new R interpreter subprocess (via callr) on nearly every edit event, each spiking 40-90%+ CPU for roughly a second — dozens of these in a 15-second span of continuous typing.

Root cause (traced locally)

Tracing through the installed languageserver namespace:

  • textDocument/didChange calls self$text_sync(..., parse = TRUE, delay = 0.5) unconditionally (this is not gated by diagnostics/r.lsp.diagnostics — only the separate lintr pass is).
  • This schedules a parse_task, whose callback parse_callback compares parse_data$packages against the previous parse's package list with !identical(old_parse_data$packages, parse_data$packages).
  • Whenever that comparison reads as "changed," it calls resolve_task, which calls create_task(target = resolve_attached_packages, ...). This spawns a new R subprocess via callr just to run:
function (pkgs = NULL) {
    for (pkg in pkgs) {
        tryCatch(library(pkg, character.only = TRUE), error = function(e) NULL)
    }
    rev(.packages())
}

In this document, the packages field appears to be recomputed as "changed" on nearly every edit — including edits in prose text nowhere near an R chunk — which suggests the derived package list is not stable/order-independent across reparses of the whole compound document (e.g. identical() on a vector recomputed via .packages()/set-like logic is order-sensitive; re-scanning the whole document on every change may not guarantee stable ordering even when the actual set of used packages hasn't changed).

Confirmed via direct captures on this machine: dozens of distinct R --slave --no-save --no-restore -f .../callr-scr-* child processes spawned within a 12-15 second typing burst, each the child of the running languageServer.R process. Deserializing the corresponding callr-fun-*.rds payloads consistently showed the target function to be resolve_attached_packages.

Impact

  • A brand-new R process incurs real interpreter-startup cost (loading base packages, etc.), so this isn't a cheap no-op — it's a genuine, repeated, avoidable cost.
  • For large documents (common with Quarto books/long-form .qmd reports), this makes editing prose sections generate continuous CPU/thermal load with no R code involved.
  • There's currently no setting to disable just this behavior — r.lsp.diagnostics only gates the lintr pass, not the parse/package-resolution path. The only way to stop it is r.lsp.enabled = false, which also removes completion/hover/diagnostics/go-to-definition entirely.

Happy to provide the full process-monitoring logs / captured callr-fun payloads if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions