From b4c8b9975d168824b7eec72d7865de4ac78070d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lek=C3=AB=20Mula?= Date: Thu, 4 Sep 2025 23:18:11 +0200 Subject: [PATCH 1/4] Add solargraph profile command --- lib/solargraph/shell.rb | 94 +++++++++++++++++++++++++++++++++++++++++ solargraph.gemspec | 1 + 2 files changed, 95 insertions(+) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index a005f600b..a8a8337f9 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -241,6 +241,100 @@ def list puts "#{workspace.filenames.length} files total." end + desc 'profile [FILE]', 'Profile go-to-definition performance using vernier' + option :directory, type: :string, aliases: :d, desc: 'The workspace directory', default: '.' + option :output_dir, type: :string, aliases: :o, desc: 'The output directory for profiles', default: './tmp/profiles' + option :line, type: :numeric, aliases: :l, desc: 'Line number (0-based)', default: 4 + option :column, type: :numeric, aliases: :c, desc: 'Column number', default: 10 + # @param file [String, nil] + # @return [void] + def profile(file = nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + begin + require 'vernier' + rescue LoadError + STDERR.puts "vernier gem not found. Install with: gem install vernier" + return + end + + directory = File.realpath(options[:directory]) + FileUtils.mkdir_p(options[:output_dir]) + + host = Solargraph::LanguageServer::Host.new + host.client_capabilities.merge!({ 'window' => { 'workDoneProgress' => true } }) + def host.send_notification method, params + puts "Notification: #{method} - #{params}" + end + + puts "Parsing and mapping source files..." + prepare_start = Time.now + Vernier.profile(out: "#{options[:output_dir]}/parse_benchmark.json.gz") do + puts "Mapping libraries" + host.prepare(directory) + sleep 0.2 until host.libraries.all?(&:mapped?) + end + prepare_time = Time.now - prepare_start + + puts "Building the catalog..." + catalog_start = Time.now + Vernier.profile(out: "#{options[:output_dir]}/catalog_benchmark.json.gz") do + host.catalog + end + catalog_time = Time.now - catalog_start + + # Determine test file + if file + test_file = File.join(directory, file) + else + test_file = File.join(directory, 'lib', 'other.rb') + unless File.exist?(test_file) + # Fallback to any Ruby file in the workspace + workspace = Solargraph::Workspace.new(directory) + test_file = workspace.filenames.find { |f| f.end_with?('.rb') } + unless test_file + STDERR.puts "No Ruby files found in workspace" + return + end + end + end + + file_uri = Solargraph::LanguageServer::UriHelpers.file_to_uri(File.absolute_path(test_file)) + + puts "Profiling go-to-definition for #{test_file}" + puts "Position: line #{options[:line]}, column #{options[:column]}" + + definition_start = Time.now + Vernier.profile(out: "#{options[:output_dir]}/definition_benchmark.json.gz") do + message = Solargraph::LanguageServer::Message::TextDocument::Definition.new( + host, { + 'params' => { + 'textDocument' => { 'uri' => file_uri }, + 'position' => { 'line' => options[:line], 'character' => options[:column] } + } + } + ) + puts "Processing go-to-definition request..." + result = message.process + + puts "Result: #{result.inspect}" + end + definition_time = Time.now - definition_start + + puts "\n=== Timing Results ===" + puts "Parsing & mapping: #{(prepare_time * 1000).round(2)}ms" + puts "Catalog building: #{(catalog_time * 1000).round(2)}ms" + puts "Go-to-definition: #{(definition_time * 1000).round(2)}ms" + total_time = prepare_time + catalog_time + definition_time + puts "Total time: #{(total_time * 1000).round(2)}ms" + + puts "\nProfiles saved to:" + puts " - #{File.expand_path('parse_benchmark.json.gz', options[:output_dir])}" + puts " - #{File.expand_path('catalog_benchmark.json.gz', options[:output_dir])}" + puts " - #{File.expand_path('definition_benchmark.json.gz', options[:output_dir])}" + + puts "\nUpload the JSON files to https://vernier.prof/ to view the profiles." + puts "Or use https://rubygems.org/gems/profile-viewer to view them locally." + end + private # @param pin [Solargraph::Pin::Base] diff --git a/solargraph.gemspec b/solargraph.gemspec index 42d80dae2..1c694034b 100755 --- a/solargraph.gemspec +++ b/solargraph.gemspec @@ -59,6 +59,7 @@ Gem::Specification.new do |s| s.add_development_dependency 'undercover', '~> 0.7' s.add_development_dependency 'overcommit', '~> 0.68.0' s.add_development_dependency 'webmock', '~> 3.6' + s.add_development_dependency 'vernier' # work around missing yard dependency needed as of Ruby 3.5 s.add_development_dependency 'irb', '~> 1.15' end From d67d580d84bc73728245afb3d319b0d7c57e98bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lek=C3=AB=20Mula?= Date: Fri, 5 Sep 2025 11:09:23 +0200 Subject: [PATCH 2/4] Add memory usage counter --- lib/solargraph/shell.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index a8a8337f9..2c07a94e0 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -246,6 +246,7 @@ def list option :output_dir, type: :string, aliases: :o, desc: 'The output directory for profiles', default: './tmp/profiles' option :line, type: :numeric, aliases: :l, desc: 'Line number (0-based)', default: 4 option :column, type: :numeric, aliases: :c, desc: 'Column number', default: 10 + option :memory, type: :boolean, aliases: :m, desc: 'Include memory usage counter', default: true # @param file [String, nil] # @return [void] def profile(file = nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength @@ -256,6 +257,9 @@ def profile(file = nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength return end + hooks = [] + hooks << :memory_usage if options[:memory] + directory = File.realpath(options[:directory]) FileUtils.mkdir_p(options[:output_dir]) @@ -267,7 +271,7 @@ def host.send_notification method, params puts "Parsing and mapping source files..." prepare_start = Time.now - Vernier.profile(out: "#{options[:output_dir]}/parse_benchmark.json.gz") do + Vernier.profile(out: "#{options[:output_dir]}/parse_benchmark.json.gz", hooks: hooks) do puts "Mapping libraries" host.prepare(directory) sleep 0.2 until host.libraries.all?(&:mapped?) @@ -276,7 +280,7 @@ def host.send_notification method, params puts "Building the catalog..." catalog_start = Time.now - Vernier.profile(out: "#{options[:output_dir]}/catalog_benchmark.json.gz") do + Vernier.profile(out: "#{options[:output_dir]}/catalog_benchmark.json.gz", hooks: hooks) do host.catalog end catalog_time = Time.now - catalog_start @@ -303,7 +307,7 @@ def host.send_notification method, params puts "Position: line #{options[:line]}, column #{options[:column]}" definition_start = Time.now - Vernier.profile(out: "#{options[:output_dir]}/definition_benchmark.json.gz") do + Vernier.profile(out: "#{options[:output_dir]}/definition_benchmark.json.gz", hooks: hooks) do message = Solargraph::LanguageServer::Message::TextDocument::Definition.new( host, { 'params' => { From c0455fae9be835a4eb288eb626f92b058be75cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lek=C3=AB=20Mula?= Date: Thu, 11 Sep 2025 23:16:10 +0200 Subject: [PATCH 3/4] Fix CI (typechecking) and pin the gem version Co-authored-by: Vince Broz --- lib/solargraph/parser/parser_gem/node_processors/defs_node.rb | 3 ++- solargraph.gemspec | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/solargraph/parser/parser_gem/node_processors/defs_node.rb b/lib/solargraph/parser/parser_gem/node_processors/defs_node.rb index 5f40457e9..ce807cd47 100644 --- a/lib/solargraph/parser/parser_gem/node_processors/defs_node.rb +++ b/lib/solargraph/parser/parser_gem/node_processors/defs_node.rb @@ -15,7 +15,8 @@ def process closure = region.closure else closure = Solargraph::Pin::Namespace.new( - name: unpack_name(node.children[0]) + name: unpack_name(node.children[0]), + source: :parser, ) end pins.push Solargraph::Pin::Method.new( diff --git a/solargraph.gemspec b/solargraph.gemspec index 1c694034b..091daa093 100755 --- a/solargraph.gemspec +++ b/solargraph.gemspec @@ -59,7 +59,7 @@ Gem::Specification.new do |s| s.add_development_dependency 'undercover', '~> 0.7' s.add_development_dependency 'overcommit', '~> 0.68.0' s.add_development_dependency 'webmock', '~> 3.6' - s.add_development_dependency 'vernier' + s.add_development_dependency 'vernier', '< 2' # work around missing yard dependency needed as of Ruby 3.5 s.add_development_dependency 'irb', '~> 1.15' end From 431dd3263b2e9449b7fa1a0a133fe2e8b38cd2b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lek=C3=AB=20Mula?= Date: Fri, 12 Sep 2025 00:02:18 +0200 Subject: [PATCH 4/4] Make overcommit happy? --- lib/solargraph/shell.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index 2c07a94e0..6a335bc9a 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -265,6 +265,9 @@ def profile(file = nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength host = Solargraph::LanguageServer::Host.new host.client_capabilities.merge!({ 'window' => { 'workDoneProgress' => true } }) + # @param method [String] The message method + # @param params [Hash] The method parameters + # @return [void] def host.send_notification method, params puts "Notification: #{method} - #{params}" end