From acbc39b66307870480422ff0d5d0279de42ac728 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Mon, 31 Jan 2022 16:38:08 -0600 Subject: [PATCH] Improve human_attribute_name performance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ``` --- activemodel/lib/active_model/translation.rb | 28 +++++++++++---------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index 9a7c7ba95a..bf94e52217 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -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