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/lib/solargraph/shell.rb b/lib/solargraph/shell.rb index a005f600b..6a335bc9a 100755 --- a/lib/solargraph/shell.rb +++ b/lib/solargraph/shell.rb @@ -241,6 +241,107 @@ 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 + 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 + begin + require 'vernier' + rescue LoadError + STDERR.puts "vernier gem not found. Install with: gem install vernier" + return + end + + hooks = [] + hooks << :memory_usage if options[:memory] + + directory = File.realpath(options[:directory]) + FileUtils.mkdir_p(options[:output_dir]) + + 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 + + puts "Parsing and mapping source files..." + prepare_start = Time.now + 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?) + 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", hooks: hooks) 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", hooks: hooks) 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..091daa093 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', '< 2' # work around missing yard dependency needed as of Ruby 3.5 s.add_development_dependency 'irb', '~> 1.15' end