Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize I18n::Locale::Fallbacks#[] for recursive locale mappings #692

Merged
merged 3 commits into from
May 6, 2024

Conversation

uiur
Copy link
Contributor

@uiur uiur commented Apr 27, 2024

I found this performance issue when running our service.

Problem

The service had a big locale mapping that is recursive.
This takes ~1.5s before cache.

hash = {
  :ja=>[:en, :"zh-CN", :"zh-TW", :"zh-HK", :fr, :ko, :th, :vi],
  :en=>[:ja, :"zh-CN", :"zh-TW", :"zh-HK", :fr, :ko, :th, :vi],
  :"zh-CN"=>[:en, :ja, :"zh-TW", :"zh-HK", :fr, :ko, :th, :vi],
  :"zh-TW"=>[:en, :ja, :"zh-CN", :"zh-HK", :fr, :ko, :th, :vi],
  :"zh-HK"=>[:en, :ja, :"zh-CN", :"zh-TW", :fr, :ko, :th, :vi],
  :fr=>[:en, :ja, :"zh-CN", :"zh-TW", :"zh-HK", :ko, :th, :vi],
  :ko=>[:en, :ja, :"zh-CN", :"zh-TW", :"zh-HK", :fr, :th, :vi],
  :th=>[:en, :ja, :"zh-CN", :"zh-TW", :"zh-HK", :fr, :ko, :vi],
  :vi=>[:en, :ja, :"zh-CN", :"zh-TW", :"zh-HK", :fr, :ko, :th]
}
fallbacks = I18n::Locale::Fallbacks.new(hash)
puts Benchmark.realtime { fallbacks[:en] }
#=> 1.5359459999017417

The cause is from I18n::Locale::Fallbacks#compute.
It computes locale fallbacks in a recursive way.

But it can be very slow for a recursive mapping like the above example.

Benchmark

original: 1.571336
new: 0.000546
ratio: 2876x faster
Code

require 'i18n'
require 'benchmark'

class Fallbacks < ::I18n::Locale::Fallbacks
  def compute(tags, include_defaults = true, exclude = [])
    result = []
    Array(tags).each do |tag|
      tags = I18n::Locale::Tag.tag(tag).self_and_parents.map! { |t| t.to_sym } - exclude
      result += tags
      tags.each { |_tag| result += compute(@map[_tag], false, exclude + result) if @map[_tag] }
    end

    result.push(*defaults) if include_defaults
    result.uniq!
    result.compact!
    result
  end
end

hash = {
  :ja=>[:en, :"zh-CN", :"zh-TW", :"zh-HK", :fr, :ko, :th, :vi],
  :en=>[:ja, :"zh-CN", :"zh-TW", :"zh-HK", :fr, :ko, :th, :vi],
  :"zh-CN"=>[:en, :ja, :"zh-TW", :"zh-HK", :fr, :ko, :th, :vi],
  :"zh-TW"=>[:en, :ja, :"zh-CN", :"zh-HK", :fr, :ko, :th, :vi],
  :"zh-HK"=>[:en, :ja, :"zh-CN", :"zh-TW", :fr, :ko, :th, :vi],
  :fr=>[:en, :ja, :"zh-CN", :"zh-TW", :"zh-HK", :ko, :th, :vi],
  :ko=>[:en, :ja, :"zh-CN", :"zh-TW", :"zh-HK", :fr, :th, :vi],
  :th=>[:en, :ja, :"zh-CN", :"zh-TW", :"zh-HK", :fr, :ko, :vi],
  :vi=>[:en, :ja, :"zh-CN", :"zh-TW", :"zh-HK", :fr, :ko, :th]
}

original_durations = []
new_durations = []

10.times do
  original_fallbacks = ::I18n::Locale::Fallbacks.new(hash)
  new_fallbacks = ::Fallbacks.new(hash)

  locale = :en
  original_durations << Benchmark.realtime { original_fallbacks[locale] }
  new_durations << Benchmark.realtime { new_fallbacks[locale] }
end

original_duration = original_durations.sum.to_f / original_durations.size
new_duration = new_durations.sum.to_f / new_durations.size
ratio = original_duration / new_duration
puts "original: #{original_duration.round(6)}"
puts "new: #{new_duration.round(6)}"
puts "ratio: #{ratio.round}x faster"

tags.each { |_tag| tags += compute(@map[_tag], false, exclude + tags) if @map[_tag] }
tags
result += tags
tags.each { |_tag| result += compute(@map[_tag], false, exclude + result) if @map[_tag] }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is prune and exclude more when calling recursion.
result is already computed so it can exclude result within following recursions.

@uiur uiur marked this pull request as ready for review April 27, 2024 01:36
@radar
Copy link
Collaborator

radar commented May 6, 2024

Thank you! Looks good to me.

@radar radar merged commit 9b84d1d into ruby-i18n:master May 6, 2024
27 checks passed
@uiur uiur deleted the optimize-fallbacks-compute branch May 8, 2024 07:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants