ruby-i18n / i18n

Internationalization (i18n) library for Ruby
MIT License
976 stars 408 forks source link

Calls to translate are extremely slow #672

Closed VirgilThinks closed 1 year ago

VirgilThinks commented 1 year ago

I've been trying to work out why my application is slow and I've traced it back to the translate function of this gem.

After running some profiling tests, I can see that the translate function (t()) takes 20ms to run, on average.

There are 6 YAML files in the load_path, each about 100 lines long. The translation calls are not complex, they just call a key, that's it.

Is this the expected speed of translation? Unfortunately it means I can't continue using the gem, because the 20ms quickly builds up to a ridiculously long execution time as many things must be translated across many pages.

Obviously not asking you to debug for me, and I haven't provided enough info for that anyway. I am just keen to understand if 20ms indicates something is wrong, or if that's just the typical speed of the code?

radar commented 1 year ago

1st: Shopify and plenty of other larger Rails application use this gem for translation. If they felt like this gem was too slow, they would've patched it already. They have a track record of doing so in the past. 2nd: Please provide an MVCE: https://stackoverflow.com/help/minimal-reproducible-example 3rd: At least mentioning the version that you're using would've been useful information here. And the Ruby version you're using might have an effect too. 4th: As with any performance-related issue: It depends on so many factors. Running I18n on a potato / raspberry pi will result in slower than expected execution times vs a server-racked maxed-to-the-max EC2 instance. Running it on a computer that's running tons of other tasks would also have an effect.

Those things out of the way...


Running a quick benchmark locally on my 2021 M1 MacBook Pro, inside a Rails application with the rails-i18n gem installed (that has a whole heap of translations provided).

require 'benchmark/ips'

Benchmark.ips do |x|
  x.report("I18n.t") do
    I18n.t(:hello)
  end
end

Results in:

Warming up --------------------------------------
              I18n.t    37.047k i/100ms
Calculating -------------------------------------
              I18n.t    371.014k (± 0.8%) i/s -      1.889M in   5.092891s

~370k calls per second. If we were seeing your kind of speed, we'd see 50 calls a second.

There's a huge disparity here between what I'm seeing and what you're reporting.

If you can provide an MVCE to reproduce this issue then I would happily spend my time investigating it further.

VirgilThinks commented 1 year ago

Thanks @radar

I think I worked it out, just trial and error.

Just to answer your questions:

Anyway, in my code, shortly before each call to t(), I was ensuring the load_path was correct with:

I18n.load_path |= Dir[...]

I assumed that this would set it to the correct files on the first run, and then basically have no effect thereafter (as it would union the same paths with the existing paths). Basically I assumed that calling I18n.load_path didn't have much more effect than accessing and updating an array. I think this was my mistake.

When I refactored the code to ensure that the load path was touched only once, it suddenly ran at an acceptable speed. Like, 50 times faster.