Method delegation with `...` argument is known to be slow because it allocates
redundant Array and Hash objects each time when being called.
see: https://bugs.ruby-lang.org/issues/19165
Current implementation of `delegate` defines all delegation methods in this
slow form regardless of the original method's arity, but in case the delegation
target is a Class or a Module, we can investigate the arity of the original
method in the definition timing, then we can define the delegation in proper
minimal arity.
This makes 3.5 times faster zero arity method delegation as follows:
Warming up --------------------------------------
old 811.534k i/100ms
new 1.807M i/100ms
Calculating -------------------------------------
old 9.809M (± 3.4%) i/s - 49.504M in 5.053355s
new 34.360M (± 0.8%) i/s - 173.465M in 5.048692s
Comparison:
new: 34360408.4 i/s
old: 9809157.4 i/s - 3.50x (± 0.00) slower
This avoids an extra allocation from `map`.
__Benchmark__
```ruby
# frozen_string_literal: true
require "benchmark/ips"
require "active_support/all"
Hash.alias_method(:old_values_at, :values_at)
Hash.alias_method(:new_values_at, :values_at)
class ActiveSupport::HashWithIndifferentAccess
def old_values_at(*keys)
super(*keys.map { |key| convert_key(key) })
end
def new_values_at(*keys)
keys.map! { |key| convert_key(key) }
super
end
end
hwia = { foo: 1, bar: 2, baz: 3, qux: 4 }.with_indifferent_access
splat_keys = [:bar, :baz]
Benchmark.ips do |x|
x.report("old values_at 1") do
hwia.old_values_at(:bar)
end
x.report("new values_at 1") do
hwia.new_values_at(:bar)
end
x.compare!
end
Benchmark.ips do |x|
x.report("old values_at splat") do
hwia.old_values_at(*splat_keys)
end
x.report("new values_at splat") do
hwia.new_values_at(*splat_keys)
end
x.compare!
end
```
__Results__
```
Warming up --------------------------------------
old values_at 1 150.881k i/100ms
new values_at 1 163.731k i/100ms
Calculating -------------------------------------
old values_at 1 1.509M (± 1.3%) i/s - 7.695M in 5.099286s
new values_at 1 1.646M (± 1.1%) i/s - 8.350M in 5.072959s
Comparison:
new values_at 1: 1646260.9 i/s
old values_at 1: 1509283.6 i/s - 1.09x (± 0.00) slower
Warming up --------------------------------------
old values_at splat 110.815k i/100ms
new values_at splat 118.871k i/100ms
Calculating -------------------------------------
old values_at splat 1.118M (± 1.3%) i/s - 5.652M in 5.057480s
new values_at splat 1.194M (± 0.9%) i/s - 6.062M in 5.077104s
Comparison:
new values_at splat: 1194171.4 i/s
old values_at splat: 1117670.4 i/s - 1.07x (± 0.00) slower
```
This clarifies the `ActiveSupport::ParameterFilter` documentation, and
tweaks the example code to be more friendly to the syntax highlighter
(similar to the tweaks made for `ActionDispatch::Http::FilterParameters`
in 782bed5d450363b302e0e6aa28b7ea0aef306d9f).
This also trims the `ActionDispatch::Http::FilterParameters`
documentation, and links it to `ActiveSupport::ParameterFilter`, since
`ActiveSupport::ParameterFilter` is responsible for filter behavior.
This is in line with transitioning away from the global
`ActiveSupport::Deprecation` instance, towards individual
`ActiveSupport::Deprecation` instances.
This matches the indentation used in generated code, such as code from
`railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt`.
Unless you're very good at math, this test fail message is not the easiest to debug:
```
"User.count" didn't change by 32.
Expected: 1611
Actual: 1579
```
It's not obvious from the error, but in this case, it actually changed by 0. This is a pretty strong clue as to what went wrong, but the message doesn't tell us that.
This PR improves the message to make debugging easier:
```
"User.count" didn't change by 32 (changed by 0).
Expected: 1611
Actual: 1579
```
because splatting the argument allocates an extra Array object.
benchmark
```ruby
s = 'a'.html_safe
Benchmark.ips do |x|
x.report('') { s * 1 }
end
```
result
```
before
Warming up --------------------------------------
216.816k i/100ms
Calculating -------------------------------------
2.341M (± 2.0%) i/s - 11.708M in 5.002555s
after
Warming up --------------------------------------
315.118k i/100ms
Calculating -------------------------------------
3.704M (± 1.5%) i/s - 18.592M in 5.020261s
```
because Hash#transform_values! takes no argument and so raises when delegating
with the given arguments.
{}.with_indifferent_access.transform_values!(1) { p :hello }
=> wrong number of arguments (given 1, expected 0) (ArgumentError)
Prior to this commit, there were several places to potentially add a new
message metadata test:
* `SharedMessageMetadataTests` module
* `MessageEncryptorMetadataTest` class
(includes `SharedMessageMetadataTests`)
* `MessageEncryptorMetadataMarshalTest` class
(subclasses `MessageEncryptorMetadataTest`)
* `MessageEncryptorMetadataJSONTest` class
(subclasses `MessageEncryptorMetadataTest`)
* `MessageEncryptorMetadataJsonWithMarshalFallbackTest` class
(subclasses `MessageEncryptorMetadataTest`)
* `MessageVerifierMetadataTest` class
(includes `SharedMessageMetadataTests`)
* `MessageVerifierMetadataMarshalTest` class
(subclasses `MessageVerifierMetadataTest`)
* `MessageVerifierMetadataJsonWithMarshalFallbackTest` class
(subclasses `MessageVerifierMetadataTest`)
* `MessageVerifierMetadataJsonTest` class
(subclasses `MessageVerifierMetadataTest`)
* `MessageVerifierMetadataCustomJSONTest` class
(subclasses `MessageVerifierMetadataTest`)
* `MessageEncryptorMetadataNullSerializerTest` class
(subclasses `MessageVerifierMetadataTest`)
This commit refactors the message metadata tests, reducing the list to:
* `MessageMetadataTests` module
* `MessageEncryptorMetadataTest` class (includes `MessageMetadataTests`)
* `MessageVerifierMetadataTest` class (includes `MessageMetadataTests`)
This makes it easier to add new tests, as well as new testing scenarios
(e.g. new encryptor / verifier configurations).
Additionally, this commit fixes two tests that were ineffective:
* The `test_passing_expires_in_less_than_a_second_is_not_expired` test
(which is now simply a part of the "message expires with :expires_in"
test) did not use the `with_usec: true` option. Therefore, the time
resulting from `travel 0.5.seconds` was truncated, effectively
traveling 0 seconds.
* The `test_verify_with_use_standard_json_time_format_as_false` test
(which is now named "expiration works with
ActiveSupport.use_standard_json_time_format = false") did not specify
an expiration time. Therefore, the conditional branch that it was
supposed to test was never exercised.
It sounds like a default timeout of 1 second can sometimes not be enough.
In normal operations, this should be fine (will result in a cache miss),
but in these tests, we always expect the cache to return the value, hence doing this change for these tests only.
Co-authored-by: Matthew Draper <matthew@trebex.net>
In #46612, a check was added to only attempt metadata extraction if the
message looks like a JSON object (i.e. starts with "{"), thus avoiding
an unnecessary JSON parse and possible exception.
This commit extends the check to only attempt metadata extraction if the
message looks like a metadata wrapper object (i.e. has the "_rails"
key). This avoids an unnecessary JSON parse of JSON object messages
that don't have metadata.
**Benchmark**
```ruby
require "benchmark/ips"
require "active_support/all"
verifier = ActiveSupport::MessageVerifier.new("secret", serializer: JSON)
message_100 = verifier.generate({ content: "x" * 100 })
message_1m = verifier.generate({ content: "x" * 1_000_000 })
Benchmark.ips do |x|
x.report("100 chars") do
verifier.verify(message_100)
end
x.report("1m chars") do
verifier.verify(message_1m)
end
end
```
**Before**
```
Warming up --------------------------------------
100 chars 2.803k i/100ms
1m chars 6.000 i/100ms
Calculating -------------------------------------
100 chars 27.762k (± 1.6%) i/s - 140.150k in 5.049649s
1m chars 83.516 (±16.8%) i/s - 402.000 in 5.037269s
```
**After**
```
Warming up --------------------------------------
100 chars 3.360k i/100ms
1m chars 9.000 i/100ms
Calculating -------------------------------------
100 chars 33.480k (± 1.7%) i/s - 168.000k in 5.019311s
1m chars 113.373 (±15.0%) i/s - 549.000 in 5.023443s
```
* Add Rails.env.local?
So many checks against Rails.env is whether we're running test or development, so combine into just one.
* Add CHANGELOG
* Prevent 'local' from being used as an environment name
Now that we have it as a combined predicate for dev + test.
ExecutionContext was added as a regular autoload when it was introduced
in 6bad959. However, the class is not currently referenced anywhere on
the boot path. This means that the file will currently be required
during the first request/job/query (currently its loaded when the to_run
callback defined in active_support.reset_execution_context is executed).
To maximize CoW and ensure that the first request/job/query doesn't have
any extra latency, ExecutionContext should be eager autoloaded instead.
This commit adds a block form of `ActiveSupport::MessageEncryptors#rotate`
and `ActiveSupport::MessageVerifiers#rotate`, which supports
fine-grained per-salt rotation options. The block will receive a salt,
and should return an appropriate options Hash. The block may also
return `nil` to indicate that the rotation does not apply to the given
salt. For example:
```ruby
verifiers = ActiveSupport::MessageVerifiers.new { ... }
verifiers.rotate(serializer: JSON, url_safe: true)
verifiers.rotate do |salt|
case salt
when :foo
{ serializer: Marshal, url_safe: true }
when :bar
{ serializer: Marshal, url_safe: false }
end
end
# Uses `serializer: JSON, url_safe: true`.
# Falls back to `serializer: Marshal, url_safe: true`.
verifiers[:foo]
# Uses `serializer: JSON, url_safe: true`.
# Falls back to `serializer: Marshal, url_safe: false`.
verifiers[:bar]
# Uses `serializer: JSON, url_safe: true`.
verifiers[:baz]
```
This can be particularly useful when migrating older configurations to a
unified configuration.