Repository navigation
Avoid building a MatchData in two string checks - #814
Conversation
0fdc426 to
a30f8c1
Compare
42c5cfd to
d2e7244
Compare
|
Thanks @tagliala. This looks good on review. The The description ends with "Draft." Is it ready to merge as it is, or is there more you want to add? It would go into 0.19.0, which we're about to cut. Two small notes for the description, not blockers:
Nothing renders from either kind of source, before or after. Generated by Claude Code |
Two checks only ask whether a pattern occurs in a string, and both asked
it with `=~`, which builds a MatchData neither of them reads and sets
$~:
- Renderer#process_source: whether a source given as a string already
contains a newline, so that a template string is not looked up as a
file. String#include? answers it without allocating.
- Engine's JSONP callback check: whether the request's callback
parameter is a valid name. Regexp#match? runs the same match and drops
the result.
Measured on an Apple M5 Pro, ruby 4.0.7 arm64-darwin23, median of 5 to
7 alternating runs of the same script against master and this branch,
with the two variants' output compared to check the work is the same.
The allocation counts are steady: Renderer.new with a multi-line source
10 -> 7 objects per call, a full render 56 -> 53, a render with
enable_json_callbacks 43 -> 40. The timings move less clearly, and only
in the first: Renderer.new 1.99M -> 2.33M i/s (1.17x), a full render
115.5k -> 117.8k i/s (1.02x, inside the noise), the callback render
1.00x.
One behaviour difference, only in the first check and only for a source
that is not valid in its own encoding: `=~` raised ArgumentError from
the regexp, include? compares bytes. The same ArgumentError ("invalid
byte sequence in UTF-8") still comes out of the render, from a
different frame: with a newline from String#present? in
Engine#eval_source, without one from String#split in
Partials#partialized during the template lookup. A source in an
encoding that can't be compared with the pattern, such as UTF-16LE,
still raises Encoding::CompatibilityError from the same check, now
with Ruby's "incompatible character encodings" message rather than
"incompatible encoding regexp match". rabl rescues ArgumentError
nowhere, so no caller loses the error it caught, and
test/renderer_test.rb records both cases. match? changes no behaviour:
the same names match, and nothing reads $~ after either check.
Written by DeepSeek Flash. Reviewed by Claude Opus 5.5 and Claude
Fable 5.1.
Reviewed-by: Claude Opus 5.5 <noreply@anthropic.com>
Reviewed-by: Claude Fable 5.1 <noreply@anthropic.com>
d2e7244 to
c1488e1
Compare
|
Since then:
Current head |
|
Merged. Thanks @tagliala, and thanks for folding those notes into the description and the CHANGELOG. It goes out in 0.19.0. Generated by Claude Code |
Two guards only ask a yes/no question about a string, and both asked it with
=~, which builds aMatchDataneither of them reads and sets$~:Renderer#process_source: does a source given as a string already contain a newline, so a template string isn't looked up as a file? →String#include?Engine's JSONP callback check: is the request'scallbackparameter a valid name? →Regexp#match?, which runs the same match without keeping the result.Benchmark
Apple M5 Pro, macOS 27.2, ruby 4.0.7 arm64-darwin23, YJIT off. Same script against
masterand this branch, 5–7 alternating runs, medians; the two variants' output is compared on every run to check they do the same work.The steady part is the allocations: 3 objects fewer per compile, per render, and per render with callbacks. The timings move clearly only on
Renderer.new; the full render's 2% and the callback path's 1.04x are inside the noise.Behaviour
match?changes nothing:ascii_only?still runs first, the same names match, and nothing reads$~after either check.include?differs for one input: a source that isn't valid in its own encoding.=~raised from the regexp,include?compares bytes, so the failure moves rather than disappears — the same class and message from a different frame:rabl rescues
ArgumentErrornowhere, so nothing that caught it stops catching it.test/renderer_test.rbnow records both cases, and the CHANGELOG entry says where the error comes from.Two further cases, from @nesquena's review. Nothing renders from either kind of source before or after, and both are verified on this branch:
Encoding::CompatibilityErrorat the same place,Renderer#process_source, and with or without a newline; only the message differs,incompatible character encodings: UTF-16LE and UTF-8instead ofincompatible encoding regexp match (US-ASCII regexp with UTF-16LE string).#renderinstead of fromRabl::Renderer.new:newsucceeds and theArgumentErrorcomes fromEngine#eval_sourceon render.The CHANGELOG entry records both the
ArgumentErrorframes and theEncoding::CompatibilityErrormessage change.Where this came from
rubocop-performance's
Performance/StringInclude.process_source, which is where this started, and finds nothing in the tree after this change.=~left,test/configuration_test.rb:33(to_s =~ /oj/i). The cop only rewrites a regexp written inline with a metacharacter-free pattern and no option flags;RABL_JSON_CALLBACK_NAMEis a named anchored constant and that test's pattern is case-insensitive. Neither was reported by any cop, andinclude?could not express them anyway; both were found by reading every=~in the tree.lib/,[:mode, :nan]→ a constant inConfiguration#oj_dump_options, is not taken: 0.99x, and slower in isolation (5.10M → 4.41M i/s), because CRuby compiles that literal toopt_duparray_sendalready.No RuboCop configuration is added here; that's a separate decision.
Attribution
eval_source(without a newline it moves to the template lookup). Those sentences and the test above are the result.CI is green on the current head: Ruby 2.7, 3.0-3.4, 4.0 and head, plus the generated Rails 7.2, Rails 8.1 and Sinatra apps.