Skip to content

[Perf]: Optimize nvm_tree_contains_path by eliminating subshell loop - #3912

Merged
ljharb merged 1 commit into
nvm-sh:masterfrom
iam-saiteja:perf/optimize-nvm-tree-contains-path
Sep 9, 2026
Merged

ljharb merged 1 commit into
nvm-sh:masterfrom
iam-saiteja:perf/optimize-nvm-tree-contains-path

Conversation

@iam-saiteja

Copy link
Copy Markdown
Contributor

What usually happens

The function nvm_tree_contains_path checks if a target file path resides within a parent directory tree. To do this, it runs a while loop that calls dirname repeatedly to move up directory levels one component at a time until it matches the root tree path or reaches the root directory.

Why is that a bottleneck

Calling dirname inside a subshell $(dirname ...) spawns a new subshell process on every loop iteration. For paths that are 7 to 10 directory levels deep, a single call to nvm_tree_contains_path creates 7 to 10 process forks. Running this function across multiple version checks accumulates process creation overhead during shell initialization and commands like nvm current, nvm use, and nvm ls.

How we are solving it

We replace the dirname subshell loop with pure POSIX in-memory parameter expansion and pattern matching.

Why does it work

Checking whether a path is inside a directory tree is equivalent to checking if the path starts with that directory path followed by a slash boundary. Removing trailing slashes and performing string pattern matching produces the exact same containment result as stepping up directory levels with dirname.

How does it work

  1. We strip any trailing slashes from the parent directory path in memory using a POSIX parameter expansion loop: clean_tree="${clean_tree%/}".
  2. We match the target path against two patterns using a POSIX case statement: exact match "${clean_tree}" or child match "${clean_tree}"/*.
  3. Quoting "${clean_tree}" inside the case pattern ensures characters like * or ? in directory names are matched literally and not treated as wildcards.
  4. If the parent tree is the root directory /, stripping the slash reduces clean_tree to empty string "", which matches /* for any absolute path.

What is the result

Subshell process creation drops from N subshells to 0. In benchmark testing of 50 calls:

  • Before: 16,274 ms (~350 process forks)
  • After: 115 ms (0 process forks)
    This speedup for this function, making shell initialization and version checks faster and lag-free.

How to reproduce and verify

Run the unit test suite:

sh "test/fast/Unit tests/nvm_tree_contains_path"

Copilot AI lite review requested due to automatic review settings September 2, 2026 15:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new case-pattern containment check can misbehave for directory names containing glob metacharacters (e.g., *, ?, []), causing incorrect containment results.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR optimizes nvm_tree_contains_path in nvm.sh, aiming to remove repeated dirname subprocess calls and speed up path containment checks that are used in common nvm operations.

Changes:

  • Replaced the prior dirname-driven parent-walk with an in-memory string/pattern-based containment check.
  • Added unit tests for tree paths with trailing slashes and for / containing an absolute descendant path.
File summaries
File Description
nvm.sh Refactors nvm_tree_contains_path to avoid per-iteration subprocesses.
test/fast/Unit tests/nvm_tree_contains_path Extends unit coverage for trailing-slash trees and the root tree case.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread nvm.sh Outdated
Comment thread test/fast/Unit tests/nvm_tree_contains_path
@iam-saiteja
iam-saiteja force-pushed the perf/optimize-nvm-tree-contains-path branch 2 times, most recently from ebed861 to f4dbc6a Compare September 2, 2026 15:53
@iam-saiteja

Copy link
Copy Markdown
Contributor Author

Updated the branch addressing the review feedback:

  1. Switched from case pattern matching to a pure in-memory POSIX parameter-expansion parent walk (${pathdir%/*}) with exact string equality (=). This guarantees that directory paths containing glob metacharacters (*, ?, []) are matched literally with zero wildcard expansion risk.
  2. Restored negative test assertions and added unit test coverage for glob metacharacter paths.
  3. Preserves zero subshell process forks (dirname), retaining the full performance speedup.
  4. Added comments in the code for readability

@schnare22

This comment was marked as spam.

@iam-saiteja
iam-saiteja force-pushed the perf/optimize-nvm-tree-contains-path branch 2 times, most recently from 2d5f3d7 to 13eaa28 Compare September 2, 2026 16:03
@iam-saiteja

Copy link
Copy Markdown
Contributor Author

Updated nvm_tree_contains_path with a case-guarded in-memory POSIX parent walk (case "${pathdir}" in */*) pathdir="${pathdir%/*}" ;; *) break ;; esac):

  1. Zsh Compatibility: case "${pathdir}" in */*) verifies a slash is present before parameter expansion runs, preventing Zsh from throwing no matches found errors on relative paths.
  2. Glob Metacharacter Safety: Uses literal POSIX string comparison [ "${pathdir}" = "${clean_tree}" ], ensuring paths with glob characters (*, ?, []) are matched 100% literally with zero wildcard expansion risk.
  3. 0 Subshells: Completely eliminates $(dirname) process creation overhead, maintaining the 154x performance speedup.

Replace looping subshell process forks (dirname)
in nvm_tree_contains_path with case-guarded in-memory POSIX parameter expansion () and exact string equality comparisons.

Ensures literal string matching for glob metacharacters (*, ?, [])
and full zsh compatibility across all platforms.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Approval recommended

The updated containment logic removes subprocess overhead while preserving expected behavior, and the added unit tests cover the key new edge cases.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@schnare22

Copy link
Copy Markdown

🟡 Changes recommended

The new case-pattern containment check can misbehave for directory names containing glob metacharacters (e.g., *, ?, []), causing incorrect containment results.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR optimizes nvm_tree_contains_path in nvm.sh, aiming to remove repeated dirname subprocess calls and speed up path containment checks that are used in common nvm operations.

Changes:

  • Replaced the prior dirname-driven parent-walk with an in-memory string/pattern-based containment check.
  • Added unit tests for tree paths with trailing slashes and for / containing an absolute descendant path.
File summaries

| File | Description |

| ---- | ----------- |

| nvm.sh | Refactors nvm_tree_contains_path to avoid per-iteration subprocesses. |

| test/fast/Unit tests/nvm_tree_contains_path | Extends unit coverage for trailing-slash trees and the root tree case. |

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@iam-saiteja

Copy link
Copy Markdown
Contributor Author

@schnare22 That was the review for the initial draft.

Copilot AI has already re-reviewed the updated commit (c56e953) and recommended approval (🟢 Approval recommended). The current implementation uses an in-memory parent walk with exact string equality (=), fully resolving the glob metacharacter concern.

@iam-saiteja
iam-saiteja force-pushed the perf/optimize-nvm-tree-contains-path branch from c56e953 to f7f4263 Compare September 5, 2026 16:26
@iam-saiteja

iam-saiteja commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Freshly rebased the PR branch on top of the latest master branch.

schnare22

This comment was marked as spam.

@ljharb
ljharb force-pushed the perf/optimize-nvm-tree-contains-path branch from f7f4263 to cd84623 Compare September 8, 2026 23:43
@ljharb
ljharb merged commit cd84623 into nvm-sh:master Sep 9, 2026
318 of 321 checks passed
@iam-saiteja

Copy link
Copy Markdown
Contributor Author

Thank you @ljharb for the review and for merging this. Glad to contribute to nvm.

ljharb added a commit that referenced this pull request Sep 21, 2026
Bug Fixes

 - `nvm_alias`, `nvm_version_path`: reject `..` path components
 - `nvm_get_make_jobs`: count cores on platforms the `case` does not name
 - `nvm-exec`: improve the no-version failure message
 - `nvm use`: do not clobber `man`'s default search path (#3890)
 - `nvm_err`, `nvm_err_with_colors`: do not fail when stderr is closed (#3907)
 - `nvm_hash_reset`: only run `hash -r` where the shell supports it
 - `nvm_alias`: unset zsh's `extendedglob` while reading an alias file (#3891)
 - zsh: restore `nomatch` and `markdirs` rather than clobbering them
 - `nvm --help`: render the set-colors legend in color (#3915)
 - `nvm unload`: drop the undefined `nvm_format_help_message_colors`
 - `nvm_get_mirror`: allow ports, userinfo, IPv6 hosts, and percent-encoding
 - `nvm_sanitize_auth_header`: allow `~`, completing RFC 7235 `token68` (#3751)

Refactors

 - `nvm --help`: extract the set-colors legend into `nvm_print_color_legend`

Performance

 - Optimize nvm_tree_contains_path with in-memory POSIX parent-walk (#3912)

Docs

 - [readme] document platforms with no official node binaries
 - `install.sh`: Fix missing words in bash/zsh completion message (#3887)
 - Remove stray closing HTML tag (#3919)

Misc

 - [meta] remove CLAUDE.md symlink in favor of AGENTS.md (#3928)
 - [meta] add `release-notes.sh`, the release notes generator

Tests

 - homebrew/actions changed their default branch
 - [actions] WSL: pin every distribution, and test the newest of each line
 - `nvm_print_color_legend`: look up the function with `PATH` emptied
 - `nvm_print_color_legend`: check for the function, not a same-named file
 - `nvm_print_color_legend`: fix the `fast` suite on GNU userlands
 - [actions] `release.yml`: provide `GH_TOKEN` so release-notes PR lookup can run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants