Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

Performance problem with DateTimeFormat polyfill #2807

Closed
andreialecu opened this issue Apr 9, 2021 · 5 comments
Closed

Performance problem with DateTimeFormat polyfill #2807

andreialecu opened this issue Apr 9, 2021 · 5 comments
Labels

Comments

@andreialecu
Copy link

andreialecu commented Apr 9, 2021

Which package?

It appears that using the DateTimeFormat polyfill to format dates can be extremely slow, especially on lower end Android devices.

We measured a single DateTimeFormat to 20ms on a real device. This blocks the main thread resulting in major performance problems.

Originally reported in: facebook/hermes#23 (comment) and comments around it.

Describe the bug

Not really a bug, but perhaps performance could be improved.

To Reproduce

delete Intl.getCanonicalLocales;
delete Intl.Locale;
delete Intl.PluralRules;
delete Intl.NumberFormat;
delete Intl.DateTimeFormat;

// Polyfills required to use Intl with Hermes engine
require("@formatjs/intl-getcanonicallocales/polyfill");
require("@formatjs/intl-locale/polyfill");
require("@formatjs/intl-pluralrules/polyfill");
require("@formatjs/intl-pluralrules/locale-data/en");
require("@formatjs/intl-numberformat/polyfill");
require("@formatjs/intl-numberformat/locale-data/en");
require("@formatjs/intl-datetimeformat/polyfill");
require("@formatjs/intl-datetimeformat/locale-data/en");
require("@formatjs/intl-datetimeformat/add-golden-tz");

for (let y = 0; y < 5; y++) {
  const start = Date.now();
  const dtf = new Intl.DateTimeFormat("en");
  for (let i = 0; i < 100; i++) {
    dtf.format(new Date());
  }
  console.log(Date.now() - start);
}

Run the above script with node --cpu-prof test.js to see a cpu trace.

Codesandbox URL

https://codesandbox.io/s/determined-nash-j9wug?file=/src/index.js

Expected behavior

DateTimeFormat polyfill should not be so slow.

Screenshots

Screenshot of profile session when using nodejs (slow paths can be seen there as well):

Screenshot 2021-04-09 at 10 02 05

Additional context

Takes around 0.2-0.5ms per format on an M1 CPU which has among the best single threaded performance in the world right now. Takes around 20ms on a mid level android CPU.

@andreialecu andreialecu added the bug label Apr 9, 2021
@andreialecu
Copy link
Author

andreialecu commented Apr 9, 2021

One thing to note is that many libraries (such as luxon), will initialize a new DateTimeFormat instance on every format.

If the constructor call is moved inside the loop in the example above, the time per format doubles. Caching, or improving the DateTimeFormat constructor's performance would also be great, if possible.

Profiling output when also constructing DateTimeFormat instances:
Screenshot 2021-04-09 at 10 43 16

@andreialecu
Copy link
Author

andreialecu commented Apr 9, 2021

Been poking at this out of curiosity.

It appears that there's a complex implementation here:

export function YearFromTime(t: number) {
const min = Math.ceil(t / MS_PER_DAY / 366)
let y = min
while (TimeFromYear(y) <= t) {
y++
}
return y - 1
}

Simply using new Date(t).getFullYear() results in a 40% speed up.

Before: 10,731 ops/sec ±0.92% (92 runs sampled)
After: 14,115 ops/sec ±1.33% (91 runs sampled)

Not sure why that logic is used instead of using the native Date.

DayFromYear could also be replaced with simply Date.UTC(year) / MS_PER_DAY for another modest speed-up.

@andreialecu
Copy link
Author

function isUnicodeScriptSubtag(script: string): boolean {
return ALPHA_4.test(script)
}

It appears the above function is called with a parameter of undefined a majority of the time. Adding a check like return script && ALPHA_4.test(script); results in another good speed-up:

Screenshot 2021-04-09 at 11 47 11

15,100 ops/sec ±1.12% (94 runs sampled)

@andreialecu
Copy link
Author

andreialecu commented Apr 9, 2021

Public repository with the patches mentioned above if a maintainer wants to take a look:
https://github.com/andreialecu/formatjs-datetimeformat-perf

Currently about 100% slower without the patches.

I think it can be made much much faster with memoization or other improvements. Most of the time is spent creating the same type of locale.

One example is that it calls removeLikelySubtags('en') unnecessarily, which does a lot of work to only return en again.

Screenshot 2021-04-09 at 12 10 29

If I make that function simply return tag (would be incorrect in the general sense, but it seems ok here), the performance increases further by around 20%: 17,782 ops/sec ±1.06% (91 runs sampled)

The original, non patched version only does 9k ops per second, so it's a 100% speed-up with only a few minor changes.

longlho added a commit that referenced this issue Apr 10, 2021
…2807

format (polyfill) x 4,801 ops/sec ±1.18% (81 runs sampled)
format (native) x 456,735 ops/sec ±1.41% (91 runs sampled)
@longlho
Copy link
Member

longlho commented Apr 10, 2021

Using new Date(t).getFullYear() would be wrong because Date is timezone-sensitive. As for DayFromYear it takes into account leap year for Gregorian calendar.
We don't memoize simply because that'd just be an unbounded memory leak. Most of those operations are done within the constructor so it's a common practice to memoize that when you use it.

The bottom line is that performance isn't a priority for us tbh since i18n is complex enough to be implemented correctly. This in combination with the fact that you have to strictly follow the spec to prevent security issues like object pollution or cross-Realm pollution.

@longlho longlho closed this as completed Apr 10, 2021
@formatjs formatjs locked and limited conversation to collaborators Apr 10, 2021

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
Projects
None yet
2 participants