Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
101 changes: 101 additions & 0 deletions lib/solargraph/shell.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have based the host setup, based on this spec:

Is there any critical step missing that should be included in the profile?

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
Comment on lines +271 to +273
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought I'd never find a use-case for this Ruby feature, but I finally found one 😍

Copy link
Contributor

Choose a reason for hiding this comment

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

Hey! This is the line that triggered that assert - looks like Solargraph has never run through that code in its tests or typechecking in CI, either!

https://github.com/apiology/solargraph/blob/master/lib/solargraph/parser/parser_gem/node_processors/defs_node.rb creates some pins, but doesn't pass the 'source' kwarg in. If you throw "source: :parser" into that "Solargraph::Pin::Namespace.new" call, it should get past that error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks again, @apiology, for coming to the rescue! 🙇‍♂️

For some reason, I thought the typechecker does not cover the shell area.


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]
Expand Down
1 change: 1 addition & 0 deletions solargraph.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading