Improve human_attribute_name performance

This reduces allocations and improves performance by ~35% when a
translation is defined and ~50% when a translation is not defined.

Benchmark script:

```ruby
require "benchmark/memory"
require "benchmark/ips"

class BaseModel
  extend ActiveModel::Translation
end

Person = Class.new(BaseModel)

module A
  Person = Class.new(BaseModel)

  module B
    Person = Class.new(BaseModel)
  end
end

I18n.backend.store_translations "en",
  activemodel: { attributes: { person: { has_translation: "translated" } } }

Benchmark.memory do |x|
  x.report("warmup") do
    Person.human_attribute_name("first_name")
    A::Person.human_attribute_name("first_name")
    A::B::Person.human_attribute_name("first_name")
    Person.human_attribute_name("has_translation")
  end

  x.report("no namespace") { Person.human_attribute_name("first_name") }
  x.report("1 namespace")  { A::Person.human_attribute_name("first_name") }
  x.report("2 namespaces") { A::B::Person.human_attribute_name("first_name") }

  x.report("has translation") { Person.human_attribute_name("has_translation") }
end

Benchmark.ips do |x|
  x.report("no namespace") { Person.human_attribute_name("first_name") }
  x.report("1 namespace")  { A::Person.human_attribute_name("first_name") }
  x.report("2 namespaces") { A::B::Person.human_attribute_name("first_name") }

  x.report("has translation") { Person.human_attribute_name("has_translation") }
end
```

Before:

```
Calculating -------------------------------------
              warmup   988.923k memsize (    24.587k retained)
                         2.441k objects (   339.000  retained)
                        50.000  strings (    50.000  retained)
        no namespace     6.416k memsize (     0.000  retained)
                        58.000  objects (     0.000  retained)
                        18.000  strings (     0.000  retained)
         1 namespace     6.416k memsize (     0.000  retained)
                        58.000  objects (     0.000  retained)
                        18.000  strings (     0.000  retained)
        2 namespaces     6.416k memsize (     0.000  retained)
                        58.000  objects (     0.000  retained)
                        18.000  strings (     0.000  retained)
     has translation     4.501k memsize (     0.000  retained)
                        46.000  objects (     0.000  retained)
                        18.000  strings (     0.000  retained)

Warming up --------------------------------------
        no namespace   567.000  i/100ms
         1 namespace   563.000  i/100ms
        2 namespaces   565.000  i/100ms
     has translation   839.000  i/100ms
Calculating -------------------------------------
        no namespace      5.642k (± 0.9%) i/s -     28.350k in   5.025255s
         1 namespace      5.652k (± 0.9%) i/s -     28.713k in   5.080325s
        2 namespaces      5.662k (± 1.1%) i/s -     28.815k in   5.090226s
     has translation      8.391k (± 1.6%) i/s -     42.789k in   5.100484s
```

After:

```
Calculating -------------------------------------
              warmup   982.803k memsize (    24.587k retained)
                         2.385k objects (   339.000  retained)
                        50.000  strings (    50.000  retained)
        no namespace     4.712k memsize (     0.000  retained)
                        44.000  objects (     0.000  retained)
                        13.000  strings (     0.000  retained)
         1 namespace     4.712k memsize (     0.000  retained)
                        44.000  objects (     0.000  retained)
                        12.000  strings (     0.000  retained)
        2 namespaces     4.712k memsize (     0.000  retained)
                        44.000  objects (     0.000  retained)
                        12.000  strings (     0.000  retained)
     has translation     3.493k memsize (     0.000  retained)
                        32.000  objects (     0.000  retained)
                        11.000  strings (     0.000  retained)

Warming up --------------------------------------
        no namespace   850.000  i/100ms
         1 namespace   846.000  i/100ms
        2 namespaces   842.000  i/100ms
     has translation     1.127k i/100ms
Calculating -------------------------------------
        no namespace      8.389k (± 0.9%) i/s -     42.500k in   5.066296s
         1 namespace      8.412k (± 0.6%) i/s -     42.300k in   5.028401s
        2 namespaces      8.423k (± 0.6%) i/s -     42.942k in   5.098322s
     has translation     11.303k (± 1.1%) i/s -     57.477k in   5.085568s
```
This commit is contained in:
Jonathan Hefner 2022-01-31 16:38:08 -06:00
parent abde74c118
commit acbc39b663

@ -35,6 +35,8 @@ def lookup_ancestors
ancestors.select { |x| x.respond_to?(:model_name) }
end
MISSING_TRANSLATION = Object.new # :nodoc:
# Transforms attribute names into a more human format, such as "First name"
# instead of "first_name".
#
@ -42,29 +44,29 @@ def lookup_ancestors
#
# Specify +options+ with additional translating options.
def human_attribute_name(attribute, options = {})
options = { count: 1 }.merge!(options)
parts = attribute.to_s.split(".")
attribute = parts.pop
namespace = parts.join("/") unless parts.empty?
attributes_scope = "#{i18n_scope}.attributes"
attribute = attribute.to_s
if attribute.include?(".")
namespace, _, attribute = attribute.rpartition(".")
namespace.tr!(".", "/")
if namespace
defaults = lookup_ancestors.map do |klass|
:"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}"
:"#{i18n_scope}.attributes.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}"
end
defaults << :"#{attributes_scope}.#{namespace}.#{attribute}"
defaults << :"#{i18n_scope}.attributes.#{namespace}.#{attribute}"
else
defaults = lookup_ancestors.map do |klass|
:"#{attributes_scope}.#{klass.model_name.i18n_key}.#{attribute}"
:"#{i18n_scope}.attributes.#{klass.model_name.i18n_key}.#{attribute}"
end
end
defaults << :"attributes.#{attribute}"
defaults << options.delete(:default) if options[:default]
defaults << attribute.humanize
defaults << options[:default] if options[:default]
defaults << MISSING_TRANSLATION
options[:default] = defaults
I18n.translate(defaults.shift, **options)
translation = I18n.translate(defaults.shift, count: 1, **options, default: defaults)
translation = attribute.humanize if translation == MISSING_TRANSLATION
translation
end
end
end