Skip to content
Closed
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
25 changes: 25 additions & 0 deletions benchmark/benchmark_resolve_rails_env.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
require_relative './utils'

require 'benchmark/ips'

# `resolve_type_names` absolutizes every type name in the environment. The
# first resolve must do that work, but long-running clients such as Steep
# re-resolve on every edit: they unload the edited source, re-add it, and
# resolve again. This benchmark reproduces that edit cycle -- unload one
# source, add it back, then resolve -- which is the path that benefits from
# reusing declarations whose type names did not change.

tmpdir = prepare_collection!

base_env = new_rails_env(tmpdir)
sample_source = base_env.each_rbs_source.first or raise

Benchmark.ips do |x|
x.time = 10

x.report("resolve_type_names") do
env = base_env.unload([sample_source.buffer])
env.add_source(sample_source)
env.resolve_type_names
end
end
23 changes: 23 additions & 0 deletions benchmark/memory_resolve_rails_env.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
require_relative './utils'

require 'memory_profiler'

# See benchmark_resolve_type_names.rb for the scenario. Here we profile the
# allocations of a single edit-cycle resolve: one source is unloaded and added
# back (not profiled), then `resolve_type_names` is profiled on its own.

tmpdir = prepare_collection!

base_env = new_rails_env(tmpdir)
sample_source = base_env.each_rbs_source.first or raise

env = base_env.unload([sample_source.buffer])
env.add_source(sample_source)

_ = resolved = nil

r = MemoryProfiler.report do
resolved = env.resolve_type_names
end

r.pretty_print
1 change: 1 addition & 0 deletions benchmark/utils.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "rbs"
require 'tmpdir'
require 'pathname'

def prepare_collection!
tmpdir = Pathname(Dir.mktmpdir)
Expand Down
46 changes: 46 additions & 0 deletions lib/rbs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,51 @@ def print_warning()
logger.warn { message }
end
end

# Internal helper for `map_type_name` / `map_type` / `resolve_*` paths
# in this gem. The given block is invoked for every element. Returns
# the input array unchanged (the same object) when every mapped result
# is `equal?` to its source; otherwise returns a fresh array with the
# changed elements substituted in. Callers detect a no-op by comparing
# the return value with the input via `equal?`, which avoids
# allocating a `[mapped, changed]` tuple on every invocation.
def map_if_changed(array, &)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure where the best location for these utility methods is.

return array if array.empty?

result = array
changed = false
array.each_with_index do |element, i|
new_element = yield(element)
next if new_element.equal?(element)

unless changed
result = array.dup
changed = true
end
result[i] = new_element
end
result
end

# Hash counterpart of `map_if_changed`: transforms values through the
# block and returns the receiver unchanged when every value identity
# is preserved.
def transform_values_if_changed(hash, &)
return hash if hash.empty?

result = hash
changed = false
hash.each do |key, value|
new_value = yield(value)
next if new_value.equal?(value)

unless changed
result = hash.dup
changed = true
end
result[key] = new_value
end
result
end
end
end
22 changes: 10 additions & 12 deletions lib/rbs/ast/type_param.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,25 +67,23 @@ def to_json(state = JSON::State.new)
end

def map_type(&block)
if b = upper_bound_type
_upper_bound_type = yield(b)
end

if b = lower_bound_type
_lower_bound_type = yield(b)
end
new_upper_bound_type = upper_bound_type ? yield(upper_bound_type) : nil
new_lower_bound_type = lower_bound_type ? yield(lower_bound_type) : nil
new_default_type = default_type ? yield(default_type) : nil

if dt = default_type
_default_type = yield(dt)
if new_upper_bound_type.equal?(upper_bound_type) &&
new_lower_bound_type.equal?(lower_bound_type) &&
new_default_type.equal?(default_type)
return self
end

TypeParam.new(
name: name,
variance: variance,
upper_bound: _upper_bound_type,
lower_bound: _lower_bound_type,
upper_bound: new_upper_bound_type,
lower_bound: new_lower_bound_type,
location: location,
default_type: _default_type
default_type: new_default_type
).unchecked!(unchecked?)
end

Expand Down
Loading
Loading