diff --git a/.github/workflows/tarball-windows.yml b/.github/workflows/tarball-windows.yml index 0bd476b8092029..940c6db591871b 100644 --- a/.github/workflows/tarball-windows.yml +++ b/.github/workflows/tarball-windows.yml @@ -90,8 +90,12 @@ jobs: "VCPKG_BINARY_SOURCES=clear;nuget,$Env:FEED_URL,$Env:FEED_MODE" >> $Env:GITHUB_ENV - name: Install libraries with vcpkg + # The registry pinned by `builtin-baseline` can need a newer tool + # than the vcpkg.exe shipped in the runner image, so re-bootstrap + # it to the version the pulled registry asks for. run: | git -C "%VCPKG_INSTALLATION_ROOT%" pull --quiet + call "%VCPKG_INSTALLATION_ROOT%\bootstrap-vcpkg.bat" vcpkg install working-directory: ${{ inputs.archname }} diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 6b6bd7f0901e02..c7b9aef0396bd6 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -147,11 +147,13 @@ jobs: # assertion timeouts (e.g. assert_separately) flake. Scale them up, as # macos.yml does for slower/contended runners. SYNTAX_SUGGEST_TIMEOUT # covers syntax_suggest's own internal search timeout, which - # RUBY_TEST_TIMEOUT_SCALE does not affect. + # RUBY_TEST_TIMEOUT_SCALE does not affect. Note: setting SYNTAX_SUGGEST_TIMEOUT + # also activates the "does not timeout on massive files" integration test, so + # it must be large enough for the slower architecture to complete the search. - name: Set timeout scale for emulated runners run: | echo "RUBY_TEST_TIMEOUT_SCALE=10" >> $GITHUB_ENV - echo "SYNTAX_SUGGEST_TIMEOUT=10" >> $GITHUB_ENV + echo "SYNTAX_SUGGEST_TIMEOUT=60" >> $GITHUB_ENV if: ${{ endsWith(matrix.os, 'ppc64le') || endsWith(matrix.os, 's390x') }} - name: make ${{ matrix.test_task }} diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index c871faaa06cbd6..6547818299c9fc 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -109,8 +109,12 @@ jobs: "VCPKG_BINARY_SOURCES=clear;nuget,$Env:FEED_URL,$Env:FEED_MODE" >> $Env:GITHUB_ENV - name: Install libraries with vcpkg + # The registry pinned by `builtin-baseline` can need a newer tool + # than the vcpkg.exe shipped in the runner image, so re-bootstrap + # it to the version the pulled registry asks for. run: | git -C "%VCPKG_INSTALLATION_ROOT%" pull --quiet + call "%VCPKG_INSTALLATION_ROOT%\bootstrap-vcpkg.bat" vcpkg install working-directory: src diff --git a/NEWS.md b/NEWS.md index 5d3ec3a703d9c7..2501bfd1449a4e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -176,8 +176,8 @@ releases. * 0.3.0 to [v0.4.0][syslog-v0.4.0] * csv 3.3.6 * 3.3.5 to [v3.3.6][csv-v3.3.6] -* repl_type_completor 0.1.15 - * 0.1.12 to [v0.1.13][repl_type_completor-v0.1.13], [v0.1.14][repl_type_completor-v0.1.14], [v0.1.15][repl_type_completor-v0.1.15] +* repl_type_completor 0.1.16 + * 0.1.12 to [v0.1.13][repl_type_completor-v0.1.13], [v0.1.14][repl_type_completor-v0.1.14], [v0.1.15][repl_type_completor-v0.1.15], [v0.1.16][repl_type_completor-v0.1.16] * pstore 0.2.1 * 0.2.0 to [v0.2.1][pstore-v0.2.1] * rdoc 8.0.0 @@ -355,6 +355,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [repl_type_completor-v0.1.13]: https://github.com/ruby/repl_type_completor/releases/tag/v0.1.13 [repl_type_completor-v0.1.14]: https://github.com/ruby/repl_type_completor/releases/tag/v0.1.14 [repl_type_completor-v0.1.15]: https://github.com/ruby/repl_type_completor/releases/tag/v0.1.15 +[repl_type_completor-v0.1.16]: https://github.com/ruby/repl_type_completor/releases/tag/v0.1.16 [pstore-v0.2.1]: https://github.com/ruby/pstore/releases/tag/v0.2.1 [rdoc-v7.0.4]: https://github.com/ruby/rdoc/releases/tag/v7.0.4 [rdoc-v7.1.0]: https://github.com/ruby/rdoc/releases/tag/v7.1.0 diff --git a/compile.c b/compile.c index 0d4d6ee2002bdc..f654514256c404 100644 --- a/compile.c +++ b/compile.c @@ -3677,30 +3677,22 @@ iseq_peephole_optimize(rb_iseq_t *iseq, LINK_ELEMENT *list, const int do_tailcal */ INSN *nobj = (INSN *)get_destination_insn(iobj); - /* This is super nasty hack!!! - * - * This jump-jump optimization may ignore event flags of the jump - * instruction being skipped. Actually, Line 2 TracePoint event - * is never fired in the following code: + /* This jump-jump optimization may ignore line events on the jump + * instruction being skipped. For example, the Line 2 TracePoint + * event would otherwise never fire in the following code: * * 1: raise if 1 == 2 * 2: while true * 3: break * 4: end * - * This is critical for coverage measurement. [Bug #15980] - * - * This is a stopgap measure: stop the jump-jump optimization if - * coverage measurement is enabled and if the skipped instruction - * has any event flag. - * - * Note that, still, TracePoint Line event does not occur on Line 2. - * This should be fixed in future. + * Do not skip a jump that carries a line event. This applies even + * when coverage is disabled because TracePoint consumes the same + * event. [Bug #15980] */ int stop_optimization = - ISEQ_COVERAGE(iseq) && ISEQ_LINE_COVERAGE(iseq) && nobj->link.type == ISEQ_ELEMENT_INSN && - nobj->insn_info.events; + (nobj->insn_info.events & (RUBY_EVENT_LINE | RUBY_EVENT_COVERAGE_LINE)); if (!stop_optimization) { INSN *pobj = (INSN *)iobj->link.prev; int prev_dup = 0; diff --git a/gc/mmtk/mmtk.c b/gc/mmtk/mmtk.c index 60bbbfc9a1b610..d7228c43becbcf 100644 --- a/gc/mmtk/mmtk.c +++ b/gc/mmtk/mmtk.c @@ -906,6 +906,20 @@ obj_can_parallel_free_p(VALUE obj) } } +static bool +obj_need_obj_free_p(VALUE obj) +{ + switch (RB_BUILTIN_TYPE(obj)) { + case T_FLOAT: + case T_RATIONAL: + case T_COMPLEX: + case T_OBJECT: + return false; + default: + return true; + } +} + static void mmtk_flush_obj_free_buffer(struct MMTk_ractor_cache *cache) { @@ -1001,8 +1015,9 @@ rb_gc_impl_new_obj(void *objspace_ptr, void *cache_ptr, VALUE klass, VALUE flags mmtk_post_alloc_fast_immix(objspace, ractor_cache, (uintptr_t)alloc_obj); } - // TODO: only add when object needs obj_free to be called - mmtk_buffer_obj_free_candidate(ractor_cache, (VALUE)alloc_obj); + if (obj_need_obj_free_p((VALUE)alloc_obj)) { + mmtk_buffer_obj_free_candidate(ractor_cache, (VALUE)alloc_obj); + } objspace->total_allocated_objects++; diff --git a/gems/bundled_gems b/gems/bundled_gems index 70fc7e7997c27f..3e4355a07d92e7 100644 --- a/gems/bundled_gems +++ b/gems/bundled_gems @@ -32,7 +32,7 @@ drb 2.2.3 https://github.com/ruby/drb nkf 0.3.0 https://github.com/ruby/nkf syslog 0.4.0 https://github.com/ruby/syslog csv 3.3.6 https://github.com/ruby/csv -repl_type_completor 0.1.15 https://github.com/ruby/repl_type_completor +repl_type_completor 0.1.16 https://github.com/ruby/repl_type_completor ostruct 0.6.3 https://github.com/ruby/ostruct pstore 0.2.1 https://github.com/ruby/pstore benchmark 0.5.0 https://github.com/ruby/benchmark diff --git a/lib/bundler.rb b/lib/bundler.rb index 12dde90fc5d835..5cd92a7f27180c 100644 --- a/lib/bundler.rb +++ b/lib/bundler.rb @@ -26,7 +26,7 @@ # or Bundler.setup to setup environment where only specified gems and their # specified versions could be used. # -# See {Bundler website}[https://bundler.io/docs.html] for extensive documentation +# See {Bundler documentation}[https://guides.rubygems.org/command-reference/bundle/] for extensive documentation # on gemfiles creation and Bundler usage. # # As a standard library inside project, Bundler could be used for introspection diff --git a/lib/bundler/cli/issue.rb b/lib/bundler/cli/issue.rb index cbfb7da2d8723f..332e41afa04c51 100644 --- a/lib/bundler/cli/issue.rb +++ b/lib/bundler/cli/issue.rb @@ -13,10 +13,10 @@ def run https://github.com/ruby/rubygems/blob/master/doc/bundler/TROUBLESHOOTING.md 2. Instructions for common Bundler uses can be found on the documentation - site: https://bundler.io/ + site: https://guides.rubygems.org/ 3. Information about each Bundler command can be found in the Bundler - man pages: https://bundler.io/man/bundle.1.html + man pages: https://guides.rubygems.org/command-reference/bundle/ Hopefully the troubleshooting steps above resolved your problem! If things still aren't working the way you expect them to, please let us know so diff --git a/lib/bundler/man/bundle-add.1 b/lib/bundler/man/bundle-add.1 index 0956aa83f14f54..53fb689658885a 100644 --- a/lib/bundler/man/bundle-add.1 +++ b/lib/bundler/man/bundle-add.1 @@ -79,4 +79,4 @@ You can assign the gem to more than one group\. \fBbundle add rails \-\-group "development, test"\fR .IP "" 0 .SH "SEE ALSO" -Gemfile(5) \fIhttps://bundler\.io/man/gemfile\.5\.html\fR, bundle\-remove(1) \fIbundle\-remove\.1\.html\fR +Gemfile(5) \fIhttps://guides\.rubygems\.org/gemfile/\fR, bundle\-remove(1) \fIbundle\-remove\.1\.html\fR diff --git a/lib/bundler/man/bundle-add.1.ronn b/lib/bundler/man/bundle-add.1.ronn index 8c65af0cc00fd0..4b5648e440d4ee 100644 --- a/lib/bundler/man/bundle-add.1.ronn +++ b/lib/bundler/man/bundle-add.1.ronn @@ -91,5 +91,5 @@ Adds the named gem to the [`Gemfile(5)`][Gemfile(5)] and run `bundle install`. ## SEE ALSO -[Gemfile(5)](https://bundler.io/man/gemfile.5.html), +[Gemfile(5)](https://guides.rubygems.org/gemfile/), [bundle-remove(1)](bundle-remove.1.html) diff --git a/lib/bundler/man/bundle-console.1 b/lib/bundler/man/bundle-console.1 index 5d3f65365faaef..af58cb668ce0b7 100644 --- a/lib/bundler/man/bundle-console.1 +++ b/lib/bundler/man/bundle-console.1 @@ -8,7 +8,7 @@ .SH "DESCRIPTION" Starts an interactive Ruby console session in the context of the current bundle\. .P -If no \fBGROUP\fR is specified, all gems in the \fBdefault\fR group in the Gemfile(5) \fIhttps://bundler\.io/man/gemfile\.5\.html\fR are preliminarily loaded\. +If no \fBGROUP\fR is specified, all gems in the \fBdefault\fR group in the Gemfile(5) \fIhttps://guides\.rubygems\.org/gemfile/\fR are preliminarily loaded\. .P If \fBGROUP\fR is specified, all gems in the given group in the Gemfile in addition to the gems in \fBdefault\fR group are loaded\. Even if the given group does not exist in the Gemfile, IRB console starts without any warning or error\. .P @@ -30,4 +30,4 @@ Resolving dependencies\|\.\|\.\|\. [1] pry(main)> .fi .SH "SEE ALSO" -Gemfile(5) \fIhttps://bundler\.io/man/gemfile\.5\.html\fR +Gemfile(5) \fIhttps://guides\.rubygems\.org/gemfile/\fR diff --git a/lib/bundler/man/bundle-console.1.ronn b/lib/bundler/man/bundle-console.1.ronn index ed842ae1c36ecc..41a0b634333bba 100644 --- a/lib/bundler/man/bundle-console.1.ronn +++ b/lib/bundler/man/bundle-console.1.ronn @@ -9,7 +9,7 @@ bundle-console(1) -- Open an IRB session with the bundle pre-loaded Starts an interactive Ruby console session in the context of the current bundle. -If no `GROUP` is specified, all gems in the `default` group in the [Gemfile(5)](https://bundler.io/man/gemfile.5.html) are +If no `GROUP` is specified, all gems in the `default` group in the [Gemfile(5)](https://guides.rubygems.org/gemfile/) are preliminarily loaded. If `GROUP` is specified, all gems in the given group in the Gemfile in addition @@ -36,4 +36,4 @@ the shell from the following: ## SEE ALSO -[Gemfile(5)](https://bundler.io/man/gemfile.5.html) +[Gemfile(5)](https://guides.rubygems.org/gemfile/) diff --git a/lib/bundler/man/bundle-init.1 b/lib/bundler/man/bundle-init.1 index 63e2376c3fd3fb..52dd08171cf1e1 100644 --- a/lib/bundler/man/bundle-init.1 +++ b/lib/bundler/man/bundle-init.1 @@ -17,4 +17,4 @@ Use the specified name for the gemfile instead of \fBGemfile\fR .SH "FILES" Included in the default [\fBGemfile(5)\fR][Gemfile(5)] generated is the line \fB# frozen_string_literal: true\fR\. This is a magic comment supported for the first time in Ruby 2\.3\. The presence of this line results in all string literals in the file being implicitly frozen\. .SH "SEE ALSO" -Gemfile(5) \fIhttps://bundler\.io/man/gemfile\.5\.html\fR +Gemfile(5) \fIhttps://guides\.rubygems\.org/gemfile/\fR diff --git a/lib/bundler/man/bundle-init.1.ronn b/lib/bundler/man/bundle-init.1.ronn index ab3c427b52ce06..39238931b8fb91 100644 --- a/lib/bundler/man/bundle-init.1.ronn +++ b/lib/bundler/man/bundle-init.1.ronn @@ -29,4 +29,4 @@ results in all string literals in the file being implicitly frozen. ## SEE ALSO -[Gemfile(5)](https://bundler.io/man/gemfile.5.html) +[Gemfile(5)](https://guides.rubygems.org/gemfile/) diff --git a/lib/bundler/man/bundle-plugin.1 b/lib/bundler/man/bundle-plugin.1 index d182c7789ba12f..e200dcfbd1d1ba 100644 --- a/lib/bundler/man/bundle-plugin.1 +++ b/lib/bundler/man/bundle-plugin.1 @@ -71,6 +71,6 @@ Describe subcommands or one specific subcommand\. No options\. .SH "SEE ALSO" .IP "\(bu" 4 -How to write a Bundler plugin \fIhttps://bundler\.io/guides/bundler_plugins\.html\fR +How to write a Bundler plugin \fIhttps://guides\.rubygems\.org/bundler_plugins/\fR .IP "" 0 diff --git a/lib/bundler/man/bundle-plugin.1.ronn b/lib/bundler/man/bundle-plugin.1.ronn index b54e0c08b4e424..e5cf9eb1341ce9 100644 --- a/lib/bundler/man/bundle-plugin.1.ronn +++ b/lib/bundler/man/bundle-plugin.1.ronn @@ -81,4 +81,4 @@ No options. ## SEE ALSO -* [How to write a Bundler plugin](https://bundler.io/guides/bundler_plugins.html) +* [How to write a Bundler plugin](https://guides.rubygems.org/bundler_plugins/) diff --git a/lib/bundler/man/bundle.1 b/lib/bundler/man/bundle.1 index b8e8aefa66228f..be5d3960ad09df 100644 --- a/lib/bundler/man/bundle.1 +++ b/lib/bundler/man/bundle.1 @@ -8,7 +8,7 @@ .SH "DESCRIPTION" Bundler manages an \fBapplication's dependencies\fR through its entire life across many machines systematically and repeatably\. .P -See the bundler website \fIhttps://bundler\.io\fR for information on getting started, and Gemfile(5) for more information on the \fBGemfile\fR format\. +See the RubyGems guides \fIhttps://guides\.rubygems\.org/\fR for information on getting started, and Gemfile(5) for more information on the \fBGemfile\fR format\. .SH "OPTIONS" .TP \fB\-\-no\-color\fR diff --git a/lib/bundler/man/bundle.1.ronn b/lib/bundler/man/bundle.1.ronn index 3f993df1a5b6f0..bf322f79410d6b 100644 --- a/lib/bundler/man/bundle.1.ronn +++ b/lib/bundler/man/bundle.1.ronn @@ -10,7 +10,7 @@ bundle(1) -- Ruby Dependency Management Bundler manages an `application's dependencies` through its entire life across many machines systematically and repeatably. -See [the bundler website](https://bundler.io) for information on getting +See [the RubyGems guides](https://guides.rubygems.org/) for information on getting started, and Gemfile(5) for more information on the `Gemfile` format. ## OPTIONS diff --git a/lib/bundler/man/gemfile.5 b/lib/bundler/man/gemfile.5 index 20ae73f5db14d3..7af10fa00fc95f 100644 --- a/lib/bundler/man/gemfile.5 +++ b/lib/bundler/man/gemfile.5 @@ -166,7 +166,7 @@ bundle config set \-\-local without development test .P Also, calling \fBBundler\.setup\fR with no parameters, or calling \fBrequire "bundler/setup"\fR will setup all groups except for the ones you excluded via \fB\-\-without\fR (since they are not available)\. .P -Note that on \fBbundle install\fR, bundler downloads and evaluates all gems, in order to create a single canonical list of all of the required gems and their dependencies\. This means that you cannot list different versions of the same gems in different groups\. For more details, see Understanding Bundler \fIhttps://bundler\.io/rationale\.html\fR\. +Note that on \fBbundle install\fR, bundler downloads and evaluates all gems, in order to create a single canonical list of all of the required gems and their dependencies\. This means that you cannot list different versions of the same gems in different groups\. For more details, see Dependency management \fIhttps://guides\.rubygems\.org/dependency_management/\fR\. .SS "PLATFORMS" If a gem should only be used in a particular platform or set of platforms, you can specify them\. Platforms are essentially identical to groups, except that you do not need to use the \fB\-\-without\fR install\-time flag to exclude groups of gems for other platforms\. .P diff --git a/lib/bundler/man/gemfile.5.ronn b/lib/bundler/man/gemfile.5.ronn index a733ae866e040a..09fd1a46865804 100644 --- a/lib/bundler/man/gemfile.5.ronn +++ b/lib/bundler/man/gemfile.5.ronn @@ -200,7 +200,7 @@ are not available). Note that on `bundle install`, bundler downloads and evaluates all gems, in order to create a single canonical list of all of the required gems and their dependencies. This means that you cannot list different versions of the same gems in different -groups. For more details, see [Understanding Bundler](https://bundler.io/rationale.html). +groups. For more details, see [Dependency management](https://guides.rubygems.org/dependency_management/). ### PLATFORMS diff --git a/lib/rubygems/commands/help_command.rb b/lib/rubygems/commands/help_command.rb index 664f400561a481..3e77004d926d7b 100644 --- a/lib/rubygems/commands/help_command.rb +++ b/lib/rubygems/commands/help_command.rb @@ -59,7 +59,7 @@ class Gem::Commands::HelpCommand < Gem::Command compatible with Bundler's Gemfile format. You can see additional documentation on the format at: - https://bundler.io + https://guides.rubygems.org/ RubyGems automatically looks for these gem dependencies files: @@ -174,7 +174,7 @@ class Gem::Commands::HelpCommand < Gem::Command See the bundler Gemfile manual page for a list of platforms supported in a gem dependencies file.: - https://bundler.io/v2.5/man/gemfile.5.html + https://guides.rubygems.org/gemfile/ Ruby Version and Engine Dependency ================================== diff --git a/test/ruby/test_settracefunc.rb b/test/ruby/test_settracefunc.rb index 95fc7f45536217..86e6fdebeaa662 100644 --- a/test/ruby/test_settracefunc.rb +++ b/test/ruby/test_settracefunc.rb @@ -2808,6 +2808,62 @@ def test_while_in_while assert_equal [__LINE__ - 5, __LINE__ - 4, __LINE__ - 3], lines, 'Bug #17868' end + def test_line_event_after_guard_before_while + lines = [] + while_line = body_line = nil + + TracePoint.new(:line) {|tp| + next unless target_thread? + lines << tp.lineno + }.enable { + raise if 1 == 2 + while_line = __LINE__ + 1 + while true + body_line = __LINE__ + 1 + break + end + } + + assert_includes lines, while_line + assert_includes lines, body_line + assert_operator lines.index(while_line), :<, lines.index(body_line) + end + + def test_line_event_after_guard_before_while_predicate + parent = Class.new do + def read + @values.shift + end + end + + child = Class.new(parent) do + def initialize + @values = ["chunk", nil] + end + end + + start_line = __LINE__ + 2 + while_line = start_line + 2 + child.class_eval <<~RUBY, __FILE__, start_line + def read + return if @finished + while chunk = super + chunk.upcase + end + end + RUBY + + lines = [] + TracePoint.new(:line) {|tp| + next unless target_thread? + lines << tp.lineno + }.enable { + child.new.read + } + + assert_includes lines, while_line + end + def test_allow_reentry event_lines = [] _l1 = _l2 = _l3 = _l4 = nil diff --git a/zjit.rb b/zjit.rb index 4f8dabc4aaed4d..a579827bcd7062 100644 --- a/zjit.rb +++ b/zjit.rb @@ -116,6 +116,7 @@ def stats_string # use multiple complex features, a decrease in this number does not # necessarily mean an increase in number of optimized calls. print_counters_with_prefix(prefix: 'complex_arg_pass_', prompt: 'popular complex argument-parameter features not optimized', buf:, stats:, limit: 10) + print_counters_with_prefix(prefix: 'caller_splat_profile_', prompt: 'caller splat length profiles', buf:, stats:, limit: 10) # Show exit counters, ordered by the typical amount of exits for the prefix at the time print_counters_with_prefix(prefix: 'compile_error_', prompt: 'compile error reasons', buf:, stats:, limit: 20) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index e849f83e9da33e..861a6d0c26828b 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -3912,9 +3912,12 @@ impl Function { result } - fn count_complex_call_features(&mut self, block: BlockId, ci_flags: c_uint) { + fn count_complex_call_features(&mut self, block: BlockId, ci_flags: c_uint, state: InsnId) { use Counter::*; - if 0 != ci_flags & VM_CALL_ARGS_SPLAT { self.count(block, complex_arg_pass_caller_splat); } + if 0 != ci_flags & VM_CALL_ARGS_SPLAT { + self.count(block, complex_arg_pass_caller_splat); + self.count_caller_splat_profile(block, state); + } if 0 != ci_flags & VM_CALL_ARGS_BLOCKARG { self.count(block, complex_arg_pass_caller_blockarg); } if 0 != ci_flags & VM_CALL_KWARG { self.count(block, complex_arg_pass_caller_kwarg); } if 0 != ci_flags & VM_CALL_KW_SPLAT { self.count(block, complex_arg_pass_caller_kw_splat); } @@ -3924,6 +3927,21 @@ impl Function { if 0 != ci_flags & VM_CALL_FORWARDING { self.count(block, complex_arg_pass_caller_forwarding); } } + fn count_caller_splat_profile(&mut self, block: BlockId, state: InsnId) { + let state = self.frame_state(state); + let summary = get_or_create_iseq_payload(state.iseq).profile.get_splat_length_summary(state.insn_idx); + let counter = match summary { + None => Counter::caller_splat_profile_no_profiles, + Some(summary) if summary.is_monomorphic() => Counter::caller_splat_profile_monomorphic, + Some(summary) if summary.is_polymorphic() => Counter::caller_splat_profile_polymorphic, + Some(summary) if summary.is_skewed_polymorphic() => Counter::caller_splat_profile_skewed_polymorphic, + Some(summary) if summary.is_megamorphic() => Counter::caller_splat_profile_megamorphic, + Some(summary) if summary.is_skewed_megamorphic() => Counter::caller_splat_profile_skewed_megamorphic, + Some(_) => unreachable!(), + }; + self.count(block, counter); + } + fn rewrite_if_frozen(&mut self, block: BlockId, orig_insn_id: InsnId, self_val: InsnId, klass: u32, bop: u32, state: InsnId) { if !unsafe { rb_BASIC_OP_UNREDEFINED_P(bop, klass) } { // If the basic operation is already redefined, we cannot optimize it. @@ -4242,7 +4260,7 @@ impl Function { // Mask out ARGS_BLOCKARG only if we've already handled the nil block arg case above. let flags_for_check = if stripped_nil_block { flags & !VM_CALL_ARGS_BLOCKARG } else { flags }; if def_type != VM_METHOD_TYPE_OPTIMIZED && def_type != VM_METHOD_TYPE_CFUNC && unspecializable_call_type(flags_for_check) { - self.count_complex_call_features(block, flags); + self.count_complex_call_features(block, flags, state); self.set_dynamic_send_reason(insn_id, ComplexArgPass); self.push_insn_id(block, insn_id); continue; } @@ -4384,7 +4402,7 @@ impl Function { match (opt_type, args.as_slice()) { (OptimizedMethodType::Call, _) => { if flags & (VM_CALL_ARGS_SPLAT | VM_CALL_KWARG) != 0 { - self.count_complex_call_features(block, flags); + self.count_complex_call_features(block, flags, state); self.set_dynamic_send_reason(insn_id, ComplexArgPass); self.push_insn_id(block, insn_id); continue; } @@ -4403,7 +4421,7 @@ impl Function { } (OptimizedMethodType::StructAref, &[]) | (OptimizedMethodType::StructAset, &[_]) => { if unspecializable_call_type(flags) { - self.count_complex_call_features(block, flags); + self.count_complex_call_features(block, flags, state); self.set_dynamic_send_reason(insn_id, ComplexArgPass); self.push_insn_id(block, insn_id); continue; } @@ -4492,7 +4510,7 @@ impl Function { if unspecializable_c_call_type(ci_flags) { // Only count features NOT already counted in type_specialize. if !unspecializable_call_type(ci_flags) { - fun.count_complex_call_features(block, ci_flags); + fun.count_complex_call_features(block, ci_flags, state); } fun.set_dynamic_send_reason(send_insn_id, ComplexArgPass); return Err(()); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index ae4322702c02f1..739d382b3a5b1a 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -13654,6 +13654,84 @@ mod hir_opt_tests { "); } + #[test] + fn dont_specialize_call_to_iseq_with_monomorphic_caller_splat() { + enable_zjit_stats(); + eval(" + def foo(*args) = args + def test(args) = foo(*args) + test([1]) + test([2]) + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :args@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :args@1 + IncrCounterPtr + Jump bb3(v6, v7) + bb3(v10:BasicObject, v11:BasicObject): + IncrCounter zjit_insn_count + IncrCounter zjit_insn_count + IncrCounter zjit_insn_count + v20:ArrayExact = ToArray v11 + IncrCounter zjit_insn_count + IncrCounter complex_arg_pass_caller_splat + IncrCounter caller_splat_profile_monomorphic + v23:BasicObject = Send v10, :foo, v20 # SendFallbackReason: Complex argument passing + IncrCounter zjit_insn_count + CheckInterrupts + Return v23 + "); + } + + #[test] + fn dont_specialize_call_to_iseq_with_polymorphic_caller_splat() { + enable_zjit_stats(); + set_call_threshold(3); + eval(" + def foo(*args) = args + def test(args) = foo(*args) + test([1]) + test([1, 2]) + test([3]) + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :args@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :args@1 + IncrCounterPtr + Jump bb3(v6, v7) + bb3(v10:BasicObject, v11:BasicObject): + IncrCounter zjit_insn_count + IncrCounter zjit_insn_count + IncrCounter zjit_insn_count + v20:ArrayExact = ToArray v11 + IncrCounter zjit_insn_count + IncrCounter complex_arg_pass_caller_splat + IncrCounter caller_splat_profile_polymorphic + v23:BasicObject = Send v10, :foo, v20 # SendFallbackReason: Complex argument passing + IncrCounter zjit_insn_count + CheckInterrupts + Return v23 + "); + } + #[test] fn test_inline_symbol_to_sym() { eval(r#" diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs index a8df3b6f1e5f54..a118254ac21ad3 100644 --- a/zjit/src/profile.rs +++ b/zjit/src/profile.rs @@ -100,6 +100,7 @@ fn profile_insn_sample( let argc = num_arguments_on_stack(cd); // Profile all the arguments and self (+1). profile_operands(profiler, profile, argc + 1); + profile_splat_length(profiler, profile, unsafe { (*cd).ci }); } YARVINSN_splatkw => profile_operands(profiler, profile, 2), _ => return false, @@ -154,6 +155,14 @@ pub type TypeDistribution = Distribution; pub type TypeDistributionSummary = DistributionSummary; +pub type SplatLength = u32; + +/// `None` records an unknown length so this distribution covers the same +/// executions as the operand type profile. +pub type SplatLengthDistribution = Distribution, DISTRIBUTION_SIZE>; + +pub type SplatLengthDistributionSummary = DistributionSummary, DISTRIBUTION_SIZE>; + /// Profile the Type of top-`n` stack operands fn profile_operands(profiler: &mut Profiler, profile: &mut IseqProfile, n: usize) { let entry = profile.entry_mut(profiler.insn_idx); @@ -171,6 +180,30 @@ fn profile_operands(profiler: &mut Profiler, profile: &mut IseqProfile, n: usize } } +fn profile_splat_length(profiler: &mut Profiler, profile: &mut IseqProfile, ci: *const rb_callinfo) { + let flags = unsafe { rb_vm_ci_flag(ci) }; + // Only call sites with VM_CALL_ARGS_SPLAT have a splat array on the stack. + if flags & VM_CALL_ARGS_SPLAT == 0 { + return; + } + + let kwarg = unsafe { rb_vm_ci_kwarg(ci) }; + let caller_kw_count = if kwarg.is_null() { 0 } else { (unsafe { get_cikw_keyword_len(kwarg) }) as usize }; + // Starting at the top of the stack, skip the block argument, keyword-splat + // hash, and explicit keyword values to reach the splat array. + let splat_pos = usize::from(flags & VM_CALL_ARGS_BLOCKARG != 0) + + usize::from(flags & VM_CALL_KW_SPLAT != 0) + + caller_kw_count; + let splat_array = profiler.peek_at_stack(splat_pos as isize); + let length = if unsafe { RB_TYPE_P(splat_array, RUBY_T_ARRAY) } { + SplatLength::try_from(unsafe { rb_jit_array_len(splat_array) }).ok() + } else { + None + }; + profile.splat_lengths.entry(profiler.insn_idx) + .or_insert_with(SplatLengthDistribution::new).observe(length); +} + fn profile_self(profiler: &mut Profiler, profile: &mut IseqProfile) { let entry = profile.entry_mut(profiler.insn_idx); if entry.opnd_types.is_empty() { @@ -225,6 +258,7 @@ fn profile_invokesuper(profiler: &mut Profiler, profile: &mut IseqProfile) { // Profile all the arguments and self (+1). profile_operands(profiler, profile, (argc + 1) as usize); + profile_splat_length(profiler, profile, unsafe { (*cd).ci }); } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -286,38 +320,14 @@ impl ProfiledType { /// Profile the class and shape of the given object fn new(obj: VALUE) -> Self { - if obj == Qfalse { - return Self { class: unsafe { rb_cFalseClass }, - shape: INVALID_SHAPE_ID, - flags: Flags::immediate() }; - } - if obj == Qtrue { - return Self { class: unsafe { rb_cTrueClass }, - shape: INVALID_SHAPE_ID, - flags: Flags::immediate() }; - } - if obj == Qnil { - return Self { class: unsafe { rb_cNilClass }, - shape: INVALID_SHAPE_ID, - flags: Flags::immediate() }; - } - if obj.fixnum_p() { - return Self { class: unsafe { rb_cInteger }, - shape: INVALID_SHAPE_ID, - flags: Flags::immediate() }; - } - if obj.flonum_p() { - return Self { class: unsafe { rb_cFloat }, - shape: INVALID_SHAPE_ID, - flags: Flags::immediate() }; - } - if obj.static_sym_p() { - return Self { class: unsafe { rb_cSymbol }, + if obj.special_const_p() { + return Self { class: obj.class_of(), shape: INVALID_SHAPE_ID, flags: Flags::immediate() }; } let mut flags = Flags::none(); - if obj.shape_id_of().layout() == ShapeLayout::RObject { + let shape = obj.shape_id_of(); + if shape.layout() == ShapeLayout::RObject { flags.0 |= Flags::IS_EMBEDDED; } if obj.struct_embedded_p() { @@ -326,7 +336,7 @@ impl ProfiledType { if unsafe { RB_TYPE_P(obj, RUBY_T_OBJECT) } { flags.0 |= Flags::IS_T_OBJECT; } - Self { class: obj.class_of(), shape: obj.shape_id_of(), flags } + Self { class: obj.class_of(), shape, flags } } pub fn empty() -> Self { @@ -415,7 +425,10 @@ pub struct IseqProfile { entries: Vec, /// Method entries for `super` calls (stored as VALUE to be GC-safe) - super_cme: HashMap + super_cme: HashMap, + + /// Observed lengths of caller splat arrays for call instructions. + splat_lengths: HashMap, } impl IseqProfile { @@ -423,6 +436,7 @@ impl IseqProfile { Self { entries: Vec::new(), super_cme: HashMap::new(), + splat_lengths: HashMap::new(), } } @@ -459,6 +473,11 @@ impl IseqProfile { self.entry(insn_idx).map(|e| e.opnd_types.as_slice()).filter(|s| !s.is_empty()) } + pub fn get_splat_length_summary(&self, insn_idx: YarvInsnIdx) -> Option { + self.splat_lengths.get(&insn_idx) + .map(SplatLengthDistributionSummary::new) + } + pub fn get_super_method_entry(&self, insn_idx: YarvInsnIdx) -> Option<*const rb_callable_method_entry_t> { let Some(entry) = self.super_cme.get(&insn_idx) else { return None }; let summary = TypeDistributionSummary::new(entry); diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 7ebfb62309a0d9..8347246f2b77af 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -434,6 +434,14 @@ make_counters! { // Unsupported argument conversions complex_arg_pass_keyword_to_positional_hash, + // Caller splat length profile shapes + caller_splat_profile_no_profiles, + caller_splat_profile_monomorphic, + caller_splat_profile_polymorphic, + caller_splat_profile_skewed_polymorphic, + caller_splat_profile_megamorphic, + caller_splat_profile_skewed_megamorphic, + // Writes to the VM frame vm_write_jit_frame_count, vm_write_sp_count,