Skip to content

Conversation

@pkondzior
Copy link
Contributor

Motivation

Related #2660

This simple caching of uri path, file_name and dependency check on entry level provides significant performance improvements and on workspace/symbol makes it faster by 60.616 %

Implementation

This commit introduces a new method Entry#in_dependencies? to encapsulate dependency path checks within the Entry class and refactors existing dependency checks across the codebase to use it. The logic for determining whether a file is inside dependencies was moved from RubyLsp::Requests::Support::Common into RubyLsp::Utils.

Automated Tests

I wasn't sure if I should introduce any entry testing above what is being used now. If this will be required and change is considering useful for the project looking forward for recommendation how this should be tested on unit level (entry.rb does have its direct tests)

@pkondzior pkondzior requested a review from a team as a code owner October 29, 2025 07:13
@graphite-app
Copy link

graphite-app bot commented Oct 29, 2025

How to use the Graphite Merge Queue

Add the label graphite-merge to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

@pkondzior pkondzior force-pushed the caching-for-entry-processing branch from 08bf508 to a1b70cf Compare October 29, 2025 07:14
@pkondzior pkondzior force-pushed the caching-for-entry-processing branch from a1b70cf to 5f8d8bb Compare October 29, 2025 07:16
Copy link
Member

@vinistock vinistock left a comment

Choose a reason for hiding this comment

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

@janko does this change achieve similar results as #3792?

If so, I think we can move forward with this one

end

#: -> bool?
def in_dependencies?
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we can invert the logic here since we almost always want to check if something is defined in the workspace and end up negating the result everywhere.

How about we make this into in_workspace? and then we can use it more directly?

#: -> String
def file_name
if @uri.scheme == "untitled"
@file_name ||= if @uri.scheme == "untitled"
Copy link
Member

Choose a reason for hiding this comment

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

Does the file name have significant impact on performance? I can see Uri#full_path being slow, but I'm surprised that File.basename introduces significant overhead.

@janko
Copy link
Contributor

janko commented Nov 6, 2025

@vinistock I'm not noticing any speedup in workspace symbol searches with these changes alone. This is because fuzzy matching still runs before filtering, so dependency checks will run only on a small subset of entries, while fuzzy matching will run on all entries (and is the bottleneck).

We could combine flipping the order of fuzzy matching and filtering from #3792 with the dependency check speedup from this PR. When I try it locally, then workspace symbol search is about 20% faster than #3792.

Changes
diff --git a/lib/ruby_indexer/lib/ruby_indexer/index.rb b/lib/ruby_indexer/lib/ruby_indexer/index.rb
index ca4b18f2..deb91532 100644
--- a/lib/ruby_indexer/lib/ruby_indexer/index.rb
+++ b/lib/ruby_indexer/lib/ruby_indexer/index.rb
@@ -196,11 +196,12 @@ module RubyIndexer
 
     # Fuzzy searches index entries based on Jaro-Winkler similarity. If no query is provided, all entries are returned
     #: (String? query) -> Array[Entry]
-    def fuzzy_search(query)
+    def fuzzy_search(query, &condition)
       unless query
         entries = @entries.filter_map do |_name, entries|
           next if entries.first.is_a?(Entry::SingletonClass)
 
+          entries = entries.select(&condition) if condition
           entries
         end
 
@@ -212,6 +213,9 @@ module RubyIndexer
       results = @entries.filter_map do |name, entries|
         next if entries.first.is_a?(Entry::SingletonClass)
 
+        entries = entries.select(&condition) if condition
+        next if entries.empty?
+
         similarity = DidYouMean::JaroWinkler.distance(name.gsub("::", "").downcase, normalized_query)
         [entries, -similarity] if similarity > ENTRY_SIMILARITY_THRESHOLD
       end
diff --git a/lib/ruby_lsp/requests/workspace_symbol.rb b/lib/ruby_lsp/requests/workspace_symbol.rb
index 7c6186e4..b1654db8 100644
--- a/lib/ruby_lsp/requests/workspace_symbol.rb
+++ b/lib/ruby_lsp/requests/workspace_symbol.rb
@@ -20,15 +20,17 @@ module RubyLsp
       # @override
       #: -> Array[Interface::WorkspaceSymbol]
       def perform
-        @index.fuzzy_search(@query).filter_map do |entry|
-          uri = entry.uri
-
+        @index.fuzzy_search(@query) do |entry|
           # We only show symbols declared in the workspace
           next if entry.in_dependencies?
 
           # We should never show private symbols when searching the entire workspace
           next if entry.private?
 
+          true
+        end.map do |entry|
+          uri = entry.uri
+
           kind = kind_for_entry(entry)
           loc = entry.location

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.

3 participants