Skip to content

Skip requiring cross compiled .so on non-Windows platform #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from

Conversation

amatsuda
Copy link
Member

@amatsuda amatsuda commented Jun 4, 2019

require "io/console" takes so much time on my machine, and here's an attempt to fix that situation.

Currently, console.rb tries to load the cross-compiled .so file first, then falls back to the plain extension on LoadError.
However, it needs to scan through all installed gem's lib directories to make sure if something with this name exists, and that whole process results in significant overhead.

For instance, while simply running a ruby process takes 0.17 second,

% time ruby -ep
ruby -ep  0.17s user 0.05s system 95% cpu 0.238 total

just requiring 'io/console' adds extra 0.1+ sec on my MacBook Pro having 656 gems installed.

% time ruby -e "require 'io/console'"
ruby -e "require 'io/console'"  0.29s user 0.08s system 97% cpu 0.383 total

For me, 0.1+ second is unignorable overhead for this kind of library.
As I wrote above, this delay is because require "#{RUBY_VERSION[/\d+\.\d+/]}/io/console.so" searches all locally installed gems for a matching .so file, and so with more gems installed, the more delay it would cause.

The following tiny oneliner shows what's actually happening here. You see, thousands of method calls are occurring inside RubyGems.

% ruby -e 'methods=[];TracePoint.new(:call) {|t| methods << "#{t.defined_class}##{t.method_id}"}.enable; require "io/console"; p methods.length, methods.tally(&:itself).sort_by {|_, c| -c}'
54530
[["Gem::StubSpecification#data", 18553], ["Gem::StubSpecification#name", 13201], ["Gem::Version#canonical_segments", 1456], ["Gem::StubSpecification#full_name", 1393], ["Gem::StubSpecification#extensions", 1269], ["#<Class:Gem::Version>#correct?", 980], ["#<Class:Gem::Deprecate>#skip", 980], ["Gem::StubSpecification#version", 723], ["#<Class:Gem::Version>#new", 671], ["Gem::Version#version", 662], ["Gem::BasicSpecification#internal_init", 661], ["Gem::BasicSpecification#initialize", 661], ["Gem::StubSpecification#valid?", 660], ["Gem::StubSpecification#initialize", 660], ["Gem::StubSpecification::StubLine#initialize", 660], ["#<Class:Gem::Platform>#new", 660], ["#<Class:Gem::Platform>#match", 658], ["#<Class:Gem>#platforms", 658], ["#<Class:Gem::BundlerVersionFinder>#compatible?", 656], ["Gem::StubSpecification#platform", 655], ["Gem::BasicSpecification#have_extensions?", 655], ["#<Class:Gem>#suffixes", 654], ["Gem::BasicSpecification#contains_requirable_file?", 654], ["Gem::BasicSpecification#have_file?", 654], ["Gem::StubSpecification#default_gem?", 652], ["Gem::StubSpecification#missing_extensions?", 652], ["Gem::StubSpecification#raw_require_paths", 652], ["#<Class:Gem::StubSpecification>#gemspec_stub", 621], ["Gem::Version#_segments", 440], ["Gem::Version#<=>", 364], ["Gem::Version#_version", 364], ["Gem::Version#initialize", 320], ["Gem::Version#segments", 223], ["Gem::Version#_split_segments", 217], ["Gem::BasicSpecification#extension_dir", 125], ["Gem::Platform#to_a", 77], ["Gem::Platform#to_s", 71], ["#<Class:Gem::Platform>#local", 68], ["Gem::BasicSpecification#extensions_dir", 68], ["#<Class:Gem>#ruby_api_version", 68], ["#<Class:Gem>#extension_api_version", 68], ["#<Class:Gem>#default_ext_dir_for", 68], ["Gem::BasicSpecification#gem_build_complete_path", 56], ["#<Class:Gem::StubSpecification>#default_gemspec_stub", 39], ["Gem::Platform#==", 9], ["#<Class:Gem>#default_dir", 8], ["#<Class:Gem::Requirement>#parse", 7], ["Gem::Specification#full_name", 7], ["Gem::Requirement#initialize", 7], ["#<Class:Gem::Specification>#gemspec_stubs_in", 6], ["#<Class:Gem::BasicSpecification>#default_specifications_dir", 6], ["#<Class:Gem::Util>#glob_files_in_dir", 6], ["#<Class:Gem::Requirement>#create", 6], ["Gem::Specification#extensions", 5], ["#<Class:Gem::Specification>#unresolved_deps", 4], ["Gem::Specification#dependencies", 4], ["Gem::Specification#platform", 4], ["Gem::Dependency#runtime?", 4], ["Gem::BasicSpecification#default_gem?", 4], ["MonitorMixin#mon_check_owner", 4], ["MonitorMixin#mon_enter", 4], ["MonitorMixin#mon_exit", 4], ["Gem::Platform#===", 3], ["#<Class:Gem>#find_unresolved_default_spec", 3], ["Gem::Dependency#initialize", 3], ["#<Class:Gem::Specification>#_resort!", 3], ["Gem::Requirement#satisfied_by?", 3], ["Gem::Specification#default_value", 3], ["Gem::Specification#raw_require_paths", 3], ["Gem::Specification#gems_dir", 3], ["Gem::Specification#base_dir", 3], ["Gem::Platform#initialize", 3], ["Gem::Platform#=~", 3], ["Kernel#require", 3], ["#<Class:Gem::BundlerVersionFinder>#bundler_version", 3], ["#<Class:Gem::BundlerVersionFinder>#bundler_version_with_reason", 3], ["#<Class:Gem::BundlerVersionFinder>#bundle_update_bundler_version", 3], ["#<Class:Gem::BundlerVersionFinder>#lockfile_version", 3], ["#<Class:Gem::BundlerVersionFinder>#lockfile_contents", 3], ["#<Class:Gem::Util>#traverse_parents", 3], ["Gem::Specification#add_development_dependency", 2], ["#<Class:Gem::Specification>#installed_stubs", 2], ["Gem::Specification#missing_extensions?", 2], ["#<Class:Gem::Specification>#dirs", 2], ["Gem::Version#prerelease?", 2], ["Gem::Specification#add_dependency_with_type", 2], ["#<Class:Gem>#env_requirement", 2], ["Gem::Dependency#requirement", 2], ["#<Class:Gem::Specification>#uniq_by", 2], ["#<Class:Gem::Specification>#default_stubs", 2], ["#<Class:Gem::Specification>#map_stubs", 2], ["Gem::Specification#add_self_to_load_path", 1], ["Gem::Specification#set_nil_attributes_to_nil", 1], ["Gem::BasicSpecification#full_require_paths", 1], ["#<Class:Gem::Requirement>#default", 1], ["Gem::BasicSpecification#full_gem_path", 1], ["Gem::BasicSpecification#find_full_gem_path", 1], ["Gem::Specification#add_bindir", 1], ["Gem::Specification#files", 1], ["Gem::BasicSpecification#full_name", 1], ["Gem::Specification#internal_init", 1], ["Gem::Specification#initialize", 1], ["#<Class:Gem::Specification>#load", 1], ["Gem::StubSpecification#to_spec", 1], ["#<Class:Gem>#load_path_insert_index", 1], ["#<Class:Gem::Specification>#stubs_for", 1], ["#<Class:Gem>#try_activate", 1], ["#<Class:Gem::Specification>#find_by_path", 1], ["#<Class:Gem::Specification>#stubs", 1], ["#<Class:Gem>#remove_unresolved_default_spec", 1], ["Gem::Dependency#matching_specs", 1], ["Gem::Specification#installed_by_version", 1], ["Gem::Dependency#to_specs", 1], ["Gem::Dependency#to_spec", 1], ["Kernel#gem", 1], ["Gem::Requirement#as_list", 1], ["Gem::Requirement#to_s", 1], ["Gem::Specification#invalidate_memoized_attributes", 1], ["Gem::Specification#required_rubygems_version=", 1], ["Gem::Specification#require_paths=", 1], ["Gem::Specification#authors=", 1], ["Gem::Specification#date=", 1], ["Gem::Specification#description=", 1], ["Gem::Specification#extensions=", 1], ["Gem::Specification#files=", 1], ["Gem::Specification#licenses=", 1], ["Gem::Specification#required_ruby_version=", 1], ["Gem::Specification#summary=", 1], ["Gem::Specification#installed_by_version=", 1], ["#<Class:Gem::Version>#create", 1], ["Gem::Dependency#prerelease?", 1], ["Gem::Requirement#prerelease?", 1], ["Gem::Specification#activate", 1], ["Gem::Specification#raise_if_conflicts", 1], ["Gem::Specification#has_conflicts?", 1], ["Gem::Specification#version=", 1], ["Gem::Specification#activate_dependencies", 1], ["Gem::Specification#set_not_nil_attributes_to_default_values", 1], ["Gem::Specification#runtime_dependencies", 1]]

Obviously, we'd better avoid raising and rescuing a LoadError where not necessary.
This PR drastically reduces these method calls by not trying to require the cross-compiled .so file where not needed,

% ruby -e 'methods=[];TracePoint.new(:call) {|t| methods << "#{t.defined_class}##{t.method_id}"}.enable; require "io/console"; p methods.length, methods.tally(&:itself).sort_by {|_, c| -c}'
235
[["Gem::Version#canonical_segments", 16], ["#<Class:Gem::Version>#new", 9], ["Gem::StubSpecification#data", 9], ["Gem::Requirement#initialize", 7], ["#<Class:Gem::Requirement>#parse", 7], ["#<Class:Gem::Requirement>#create", 6], ["Gem::Specification#dependencies", 4], ["Gem::Dependency#runtime?", 4], ["Gem::Version#version", 4], ["Gem::Version#_segments", 4], ["Gem::Version#<=>", 4], ["Gem::Version#_version", 4], ["#<Class:Gem>#default_dir", 4], ["Gem::Requirement#satisfied_by?", 3], ["Gem::BasicSpecification#initialize", 3], ["Gem::BasicSpecification#internal_init", 3], ["#<Class:Gem::Version>#correct?", 3], ["#<Class:Gem::Deprecate>#skip", 3], ["#<Class:Gem::BasicSpecification>#default_specifications_dir", 3], ["Gem::Specification#full_name", 3], ["#<Class:Gem::Specification>#unresolved_deps", 3], ["#<Class:Gem::Util>#glob_files_in_dir", 3], ["Gem::Specification#base_dir", 3], ["Gem::Dependency#initialize", 3], ["Gem::Specification#default_value", 3], ["#<Class:Gem::Specification>#gemspec_stubs_in", 3], ["Gem::Version#prerelease?", 2], ["#<Class:Gem::Specification>#_resort!", 2], ["Gem::BasicSpecification#default_gem?", 2], ["Gem::Dependency#requirement", 2], ["Gem::StubSpecification#version", 2], ["MonitorMixin#mon_check_owner", 2], ["Gem::Specification#add_development_dependency", 2], ["#<Class:Gem>#platforms", 2], ["Kernel#require", 2], ["#<Class:Gem::Platform>#match", 2], ["Gem::Version#_split_segments", 2], ["#<Class:Gem::Platform>#new", 2], ["Gem::Version#segments", 2], ["Gem::StubSpecification::StubLine#initialize", 2], ["#<Class:Gem>#env_requirement", 2], ["Gem::StubSpecification#valid?", 2], ["Gem::Specification#platform", 2], ["MonitorMixin#mon_exit", 2], ["MonitorMixin#mon_enter", 2], ["#<Class:Gem>#find_unresolved_default_spec", 2], ["Gem::StubSpecification#initialize", 2], ["Gem::Specification#add_dependency_with_type", 2], ["Gem::StubSpecification#full_name", 2], ["Gem::StubSpecification#name", 2], ["Gem::Specification#raw_require_paths", 1], ["Gem::BasicSpecification#full_gem_path", 1], ["Gem::BasicSpecification#find_full_gem_path", 1], ["Gem::Specification#gems_dir", 1], ["Gem::BasicSpecification#full_name", 1], ["Gem::BasicSpecification#have_extensions?", 1], ["Gem::Specification#extensions", 1], ["Gem::BasicSpecification#extension_dir", 1], ["Gem::BasicSpecification#extensions_dir", 1], ["#<Class:Gem>#default_ext_dir_for", 1], ["#<Class:Gem::Platform>#local", 1], ["Gem::Platform#to_s", 1], ["Gem::Platform#to_a", 1], ["#<Class:Gem>#extension_api_version", 1], ["#<Class:Gem>#ruby_api_version", 1], ["#<Class:Gem>#load_path_insert_index", 1], ["#<Class:Gem>#win_platform?", 1], ["#<Class:Gem>#remove_unresolved_default_spec", 1], ["Gem::Specification#files", 1], ["Gem::Specification#add_bindir", 1], ["#<Class:Gem::Requirement>#default", 1], ["Gem::Requirement#to_s", 1], ["Gem::Requirement#as_list", 1], ["Kernel#gem", 1], ["Gem::Version#initialize", 1], ["Gem::Dependency#to_spec", 1], ["Gem::Dependency#to_specs", 1], ["Gem::Dependency#matching_specs", 1], ["#<Class:Gem::Specification>#stubs_for", 1], ["#<Class:Gem::Specification>#dirs", 1], ["#<Class:Gem::Specification>#installed_stubs", 1], ["#<Class:Gem::Specification>#map_stubs", 1], ["#<Class:Gem::StubSpecification>#gemspec_stub", 1], ["Gem::StubSpecification#platform", 1], ["#<Class:Gem::Specification>#default_stubs", 1], ["#<Class:Gem::StubSpecification>#default_gemspec_stub", 1], ["#<Class:Gem::Specification>#uniq_by", 1], ["Gem::StubSpecification#to_spec", 1], ["#<Class:Gem::Specification>#load", 1], ["Gem::Specification#initialize", 1], ["Gem::Specification#internal_init", 1], ["Gem::Specification#set_nil_attributes_to_nil", 1], ["Gem::Specification#set_not_nil_attributes_to_default_values", 1], ["Gem::Specification#version=", 1], ["#<Class:Gem::Version>#create", 1], ["Gem::Specification#invalidate_memoized_attributes", 1], ["Gem::Specification#required_rubygems_version=", 1], ["Gem::Specification#require_paths=", 1], ["Gem::Specification#authors=", 1], ["Gem::Specification#date=", 1], ["Gem::Specification#description=", 1], ["Gem::Specification#extensions=", 1], ["Gem::Specification#files=", 1], ["Gem::Specification#licenses=", 1], ["Gem::Specification#required_ruby_version=", 1], ["Gem::Specification#summary=", 1], ["Gem::Specification#installed_by_version=", 1], ["Gem::Dependency#prerelease?", 1], ["Gem::Requirement#prerelease?", 1], ["Gem::Specification#activate", 1], ["Gem::Specification#raise_if_conflicts", 1], ["Gem::Specification#has_conflicts?", 1], ["Gem::Specification#activate_dependencies", 1], ["Gem::Specification#runtime_dependencies", 1], ["Gem::Specification#add_self_to_load_path", 1], ["Gem::BasicSpecification#full_require_paths", 1]]

and reduces the require overhead to be almost zero second on non-Windows platforms.

% time ruby -e "require 'io/console'"
ruby -e "require 'io/console'"  0.18s user 0.05s system 95% cpu 0.244 total

I'm not 100% confident about my approach though. There could be a better way to detect the existence of a cross-compiled extension, or we could use require_relative or something, maybe? I'm not a regular user of rake-compiler, so I'm open to any advice. :)

So let's search it only on Windows platforms, and let's not scan through all installed gems on other platforms.
@nobu
Copy link
Member

nobu commented Jun 4, 2019

@josenk
Copy link

josenk commented Jun 4, 2019

Just to be clear.... Are you requesting this change to save 0.1 seconds for the compile?

@amatsuda
Copy link
Member Author

amatsuda commented Jun 5, 2019

@nobu Wow, I very much love the idea of using require_paths for this!
Maybe I'm missing something, but doesn't this mean we're looking up for the stub for all platforms?
Shouldn't we add spec.require_paths.insert(0, *%w[stub]) inside the ext.cross_compiling block?

@amatsuda
Copy link
Member Author

amatsuda commented Jun 5, 2019

@josenk No.

@nobu
Copy link
Member

nobu commented Jun 5, 2019

The stub file is included in the ordinary (non-binary?, I'm not sure how it is called) gem, and will be installed certainly. But the gemspec file for that gem comes from the gemspec file directly, not the loaded with eval and modified spec, so the stub directory will not be added in $LOAD_PATH installed from the ordinary gem.

@amatsuda
Copy link
Member Author

amatsuda commented Jun 5, 2019

Thank you for the explanation. All clear now!
Could you push that commit please?

Also, it's nicer if we had a released package with this stub thing, so I could benchmark again with it. A beta release is totally fine.

@amatsuda
Copy link
Member Author

@nobu Thank you so much for the updates and the release.
Loading io-console became 50x-100x faster on my machine now! 🤘

ruby -e "now = Time.now; gem 'io-console', '0.4.7'; require 'io/console'; p '0.4.7' => Time.now - now"
ruby -e "now = Time.now; gem 'io-console', '0.4.8'; require 'io/console'; p '0.4.8' => Time.now - now"

{"0.4.7"=>0.16418}
{"0.4.8"=>0.003289}

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