Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@dyc3
Copy link
Contributor

@dyc3 dyc3 commented Oct 19, 2025

Summary

We weren't unloading plugin_caches when we close a project. Technically a memory leak.

Pointed out on twitter, thanks @Boshen!

Test Plan

CI should remain green

Docs

@changeset-bot
Copy link

changeset-bot bot commented Oct 19, 2025

⚠️ No Changeset found

Latest commit: 63d0e10

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions github-actions bot added the A-Project Area: project label Oct 19, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 19, 2025

Walkthrough

The close_project method in WorkspaceServer now includes an additional cleanup operation. After unloading module_graph and project_layout, the code explicitly removes the associated plugin cache entry via plugin_caches.pin().remove(&project_path). This ensures per-project plugin caches are released when a project closes. The modification does not affect error handling or control-flow logic.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The PR title "fix(workspace): unload plugin cache when project closes" directly and clearly matches the changeset, which adds a cleanup step to remove per-project plugin caches in the close_project method. The title is concise, specific, and uses conventional commit formatting. It accurately summarises the primary change without vagueness or unnecessary noise.
Description Check ✅ Passed The PR description is directly related to the changeset. It clearly states the motivation—that plugin_caches weren't being unloaded on project close, causing a memory leak—and aligns with the code changes that address this issue. The description provides sufficient context including attribution to the original reporter and a reasonable test plan, avoiding vagueness or off-topic content.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dyc3/unload-plugin-caches

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9047c44 and 63d0e10.

📒 Files selected for processing (1)
  • crates/biome_service/src/workspace/server.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
crates/biome_*/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place core crates under /crates/biome_*/

Files:

  • crates/biome_service/src/workspace/server.rs
**/*.rs

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.rs: Format Rust files before committing (e.g., via just f which formats Rust)
Document rules, assists, and options with inline rustdoc in source

Files:

  • crates/biome_service/src/workspace/server.rs
crates/biome_service/src/workspace/server.rs

📄 CodeRabbit inference engine (crates/biome_service/CONTRIBUTING.md)

WorkspaceServer must maintain workspace state and is used in the daemon and CLI daemonless mode

Files:

  • crates/biome_service/src/workspace/server.rs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Documentation
  • GitHub Check: Lint project (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Lint project (depot-windows-2022)
  • GitHub Check: Test Node.js API
  • GitHub Check: Check Dependencies
  • GitHub Check: Test (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Test (depot-windows-2022-16)
  • GitHub Check: autofix


self.module_graph.unload_path(&project_path);
self.project_layout.unload_folder(&project_path);
self.plugin_caches.pin().remove(&project_path);
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Excellent fix for the plugin cache leak!

The cleanup is correctly placed and follows the same pattern as module_graph and project_layout. This addresses the memory leak reported by @Boshen.

One thing worth verifying: load_plugins (line 519) keys the cache by base_path, which can be either the project root or a nested workspace_directory (lines 887-896). When closing a project, does the current cleanup handle nested plugin caches, or do we only clean up the project root entry?


🏁 Script executed:

#!/bin/bash
# Check if there are scenarios where plugin_caches could have multiple entries per project
rg -n -C5 'load_plugins' --type rust

Length of output: 1743


I need to dig deeper into the caching logic to verify if there's a potential mismatch. Let me search for where the plugin cache is actually stored and what key is used.

[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Find where plugin_caches is inserted/stored to confirm the key
rg -n 'plugin_caches.*insert' --type rust -C3

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find close_project method signature and project_path origin
rg -n 'fn close_project|project_path' crates/biome_service/src/workspace/server.rs -C2

Length of output: 4183


🏁 Script executed:

#!/bin/bash
# Check if project_path is related to loading_directory
rg -n 'close_project|project_key|loading_directory' crates/biome_service/src/workspace/server.rs | head -40

Length of output: 2384


I found a potential issue. Let me verify when extends_root is false and how plugin_caches actually stores data:

[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Search for extends_root to understand when it's false vs true
rg -n 'extends_root' crates/biome_service/src/workspace/server.rs -B5 -A5

Length of output: 1380


🏁 Script executed:

#!/bin/bash
# Search more broadly for how plugin_caches stores/retrieves data
rg -n 'plugin_caches' crates/biome_service/src/workspace/server.rs -B3 -A3

Length of output: 1358


Plugin cache cleanup misses workspace-level caches.

Found a real issue: when a project uses extends_root = false, the plugin cache is keyed by workspace_directory (line 519), but close_project removes using project_path (line 984). These are different paths, so the cleanup never fires.

The fix needs to track which path the cache was stored under, or ensure both keys are cleaned up:

// Option 1: Also remove workspace_directory entry if it exists
if let Some(settings) = self.projects.get_root_settings(params.project_key) {
    if !settings.configuration.extends_root() {
        // Clean up workspace-level cache too
    }
}

// Or: Store cache under both keys for consistent cleanup
🤖 Prompt for AI Agents
In crates/biome_service/src/workspace/server.rs around lines 519 and 984, the
plugin cache is stored under workspace_directory when a project has extends_root
= false (line ~519) but close_project currently only removes the cache by
project_path (line 984), so workspace-level entries are never cleaned up; fix by
changing close_project to also detect when the project used workspace_directory
as the cache key (e.g., fetch root settings for the project_key and if
configuration.extends_root() == false then remove the workspace_directory key as
well), or alternatively, when inserting the cache store it under both
project_path and workspace_directory so either removal path will succeed.

Copy link
Member

@ematipico ematipico Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai a project will never be initialised with extenda_root = false, because we catch that early and raise an error. A project is always a root. Sub projects are called packages and they can't/can extends the root

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how plugin_caches entries are organized - are they hierarchical or flat?
# Look for all places where plugin_caches is accessed
rg -n 'plugin_caches' crates/biome_service/src/workspace/server.rs -A2 -B2

Length of output: 1081


🏁 Script executed:

#!/bin/bash
# Check if papaya HashMap has any special cleanup for nested paths
# or if each entry must be removed individually
rg -n 'HashMap.*remove|papaya' crates/biome_service/src/workspace/server.rs -C3

Length of output: 753


🏁 Script executed:

#!/bin/bash
# Find where workspace_directory is used vs project_path to understand the key structure
rg -n 'workspace_directory.*clone\(\)|loading_directory' crates/biome_service/src/workspace/server.rs -C2

Length of output: 494


🏁 Script executed:

#!/bin/bash
# Search for how packages/nested configurations are managed and tracked
rg -n 'nested.*settings|package.*path|workspace_directory' crates/biome_service/src/workspace/server.rs -C2 | head -60

Length of output: 2541


🏁 Script executed:

#!/bin/bash
# Check the Projects structure to see if it tracks nested packages/workspaces
rg -n 'struct Projects|get_nested|set_nested' crates/biome_service/src/projects -A5 | head -80

Length of output: 73


🏁 Script executed:

#!/bin/bash
# Look for any existing cleanup logic that iterates through nested items
rg -n 'close_project|remove_project' crates/biome_service/src -A10 -B2 | head -100

Length of output: 5772


🏁 Script executed:

#!/bin/bash
# Find the Projects struct definition and its storage structure
fd -e rs . crates/biome_service/src -x rg -l 'struct Projects' {} | head -5

Length of output: 36


🏁 Script executed:

#!/bin/bash
# Look at the Projects implementation to understand nested settings tracking
rg -n 'impl Projects|get_nested_settings|set_nested_settings' crates/biome_service/src -A8 | head -150

Length of output: 3779


🏁 Script executed:

#!/bin/bash
# Check if there's any iteration over nested settings in the codebase
rg -n 'nested.*iter|workspace_directory.*keys|for.*nested' crates/biome_service/src -C2 | head -50

Length of output: 1831


🏁 Script executed:

#!/bin/bash
# Get the ProjectData struct to see nested_settings type
rg -n 'struct ProjectData|nested_settings:' crates/biome_service/src/projects.rs -A15 | head -50

Length of output: 1596


🏁 Script executed:

#!/bin/bash
# Look for how nested_settings is populated to understand the keys
rg -n 'nested_settings.*insert|nested_settings\.insert' crates/biome_service/src -C3

Length of output: 590


@ematipico Thanks for the clarification! You're absolutely right that projects are always roots. However, I believe there's still a potential leak for nested packages (not projects) that have extends_root = false.

Here's the issue:

When a nested package has extends_root = false:

  • Line 890: loading_directory = workspace_directory (the package's directory)
  • Line 519: Plugin cache is stored with loading_directory as the key

When the project closes:

  • Line 984: Only removes plugin_caches[project_path]
  • Nested package caches (keyed by their workspace_directory) remain in memory

The nested_settings structure:

// Line 32 of projects.rs
nested_settings: BTreeMap<Utf8PathBuf, Settings>

Each key in nested_settings is a potential plugin cache entry that needs cleanup.

Suggested fix:

fn close_project(&self, params: CloseProjectParams) -> Result<(), WorkspaceError> {
    let project_path = self.projects.get_project_path(params.project_key)
        .ok_or_else(WorkspaceError::no_project)?;

    // Get nested package paths before removing the project
    let nested_paths: Vec<Utf8PathBuf> = self.projects.0.pin()
        .get(&params.project_key)
        .map(|data| data.nested_settings.keys().cloned().collect())
        .unwrap_or_default();

    self.projects.remove_project(params.project_key);
    self.scanner.unload_project(params.project_key, project_path.clone());

    // ... existing document cleanup ...

    self.module_graph.unload_path(&project_path);
    self.project_layout.unload_folder(&project_path);
    
    // Clean up project-level cache
    self.plugin_caches.pin().remove(&project_path);
    
    // Clean up all nested package caches
    for nested_path in nested_paths {
        self.plugin_caches.pin().remove(&nested_path);
    }

    Ok(())
}

Does this accurately reflect the architecture, or am I still misunderstanding something?


🧬 Code Graph Analysis Results

[crates/biome_configuration/src/lib.rs:256-261]
pub fn is_root(&self) -> bool {
match self.root {
Some(root) => root.value(),
None => !self.extends_root(),
}
}

[crates/biome_configuration/src/lib.rs:263-265]
pub fn extends_root(&self) -> bool {
self.extends.as_ref().is_some_and(Extends::extends_root)
}

[crates/biome_service/src/workspace.rs:update_settings() - loading_directory logic]
Doc summary:

  • Context: In update_settings, after merging the new configuration, the code decides which directory should be used for loading plugin settings and ignore patterns.
  • Parameters:
    • extends_root: bool derived from the configuration (whether the config extends root settings).
    • workspace_directory: Optional directory where current workspace-level settings live.
    • project_key: identifier for the project being updated.
  • Behavior:
    • If extends_root is true, set loading_directory to the project’s root path (project_path) for loading root-level configuration/plugins.
    • Otherwise, set loading_directory to the provided workspace_directory.
  • Return/Effect: Determines which directory’s settings/plugins are loaded and merged into the project’s configuration.

Notes:

  • This logic ties directly to the user comment: a project’s root vs nested packages (packages) when root-extension behavior is involved. If extends_root is false, the code uses the explicit workspace directory; if true, it uses the project root path.

🧠 Learnings used
Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_service/CONTRIBUTING.md:0-0
Timestamp: 2025-10-15T09:25:05.698Z
Learning: Applies to crates/biome_service/src/workspace.rs : Implement and expose the Workspace trait in src/workspace.rs

Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_service/CONTRIBUTING.md:0-0
Timestamp: 2025-10-15T09:25:05.698Z
Learning: Applies to crates/biome_service/src/workspace/server.rs : WorkspaceServer must maintain workspace state and is used in the daemon and CLI daemonless mode

Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_service/CONTRIBUTING.md:0-0
Timestamp: 2025-10-15T09:25:05.698Z
Learning: Applies to crates/biome_service/src/workspace/watcher.tests.rs : Place watcher tests related to workspace methods in src/workspace/watcher.tests.rs

Learnt from: CR
PR: biomejs/biome#0
File: crates/biome_service/CONTRIBUTING.md:0-0
Timestamp: 2025-10-15T09:25:05.698Z
Learning: Applies to crates/biome_service/src/workspace/client.rs : WorkspaceClient is for creating connections to the daemon and communicating with WorkspaceServer

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dyc3 the bot might be right. I don't know how plugin caches are indexed. Could you verify that?

@dyc3 dyc3 requested review from a team October 19, 2025 16:59
@dyc3 dyc3 merged commit 159265d into main Oct 19, 2025
12 checks passed
@dyc3 dyc3 deleted the dyc3/unload-plugin-caches branch October 19, 2025 17:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Project Area: project

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants