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

Speed up toLocaleString ~2.5x by reducing creation of Intl.DateTimeFormat instances #12

Merged
merged 1 commit into from
Jul 16, 2021

Conversation

justingrant
Copy link
Contributor

This PR speeds up toLocaleString and other operations that depend on the polyfilled Intl.DateTimeFormat. The fix was to prevent unnecessary creation of built-in Intl.DateTimeFormat objects, because the constructor of that built-in class is slooooooow. For more details about the underlying issue see: https://bugs.chromium.org/p/v8/issues/detail?id=6528

In local testing, speedup is about 2.5x for ZDT toLocaleString calls. The sample code below runs in about 1800ms in the playground of the current main branch, and about 700ms in the playground of the PR branch.

(() => {
  start = Date.now();
  for (i = 0; i < 1000; i++) {
    Temporal.ZonedDateTime.from('2020-01-01[America/Los_Angeles]').toLocaleString();
  }
  total = Date.now()-start;
  console.log(`${total}ms`);
}) ()

@justingrant justingrant changed the title Speed up toLocaleString ~2.5x by reducing creation of Intl.DateTimeFormat instances Speed up toLocaleString ~2.5x by reducing creation of Intl.DateTimeFormat instances Jul 9, 2021
lib/intl.mjs Outdated Show resolved Hide resolved
Copy link
Contributor

@ptomato ptomato left a comment

Choose a reason for hiding this comment

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

Thanks, this looks good!

lib/intl.mjs Outdated
if (!(this instanceof DateTimeFormat)) return new DateTimeFormat(locale, options);
const original = new IntlDateTimeFormat(locale, options);
const ro = original.resolvedOptions();
options = typeof options === 'undefined' ? {} : ObjectAssign({}, options);
Copy link
Contributor

Choose a reason for hiding this comment

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

options would no longer be undefined here unless the caller explicitly passed undefined, I think? Maybe we should have options = undefined as the function default argument. It doesn't really matter much though. I believe this is correct as is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, I made exactly this change in my latest checkin. I also realized that shallow-cloning of options was not enough, because if options property values or locale were not primitives, then they could also be mutated by the caller before lazy-init happens. Take a look at my last checkin and let me know if the hacky JSON-based deep clone is sufficient.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I also added tests to verify that mutating constructor inputs after the constructor returns won't break anything.

Still not sure that using a JSON round-trip is the right way to deep-clone these inputs.

lib/intl.mjs Outdated Show resolved Hide resolved
@justingrant justingrant force-pushed the speedup-tolocalestring branch 3 times, most recently from 0092f38 to b268b7a Compare July 12, 2021 23:18
Copy link
Contributor

@ptomato ptomato left a comment

Choose a reason for hiding this comment

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

Similarly to you, I am feeling a bit iffy about the deep-cloning, but maybe it can be avoided altogether?

lib/intl.mjs Outdated
return val;
}

function deepClone(val) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be possible to avoid the deep cloning altogether, and store ro instead of options for lazily creating new instances? (and use ro.locale as the locale?)

I ask because either deep- or shallow-cloning the object is going to be very tricky to do while matching the observable behaviour in ToDateTimeOptions. But it seems like no matter whether we create a DateTimeFormat with the originally-given or the resolved options/locale, it should have the same result.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ryzokuken @sffc - @ptomato's idea above sounds like a good one. Will there be any difference in behavior between i1 and i2 below?

i1 = new DateTimeFormat(locale, options);
ro = i1.resolvedOptions();
i2 = new DateTimeFormat(ro.locale, ro);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Update: using resolvedOptions() output to replace options didn't work, because the polyfill adds additional options if the user's options don't contain unit options like year or hour.

What does seem to work is a small variation of this approach: clone resolvedOptions()and delete all properties that weren't present in the original options object.

@ptomato - take a look at my latest commit and LMK if you think this is a promising approach.

Copy link
Contributor

@ptomato ptomato left a comment

Choose a reason for hiding this comment

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

I think this solution is good, though maybe one of the Intl folks could chip in?

lib/intl.mjs Outdated
if (typeof options !== 'undefined') {
const clonedOptions = ObjectAssign({}, ro);
for (const prop in clonedOptions) {
if (typeof options[prop] === 'undefined') delete clonedOptions[prop];
Copy link
Contributor

Choose a reason for hiding this comment

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

I would use ES.HasOwnProperty() here, to avoid calling getters a second time. That will still not quite be spec-compliant, since Proxy objects would be able to observe the HasProperty operation, but it's probably fine. Although that might mean we shouldn't backport this to proposal-temporal.

Copy link
Contributor Author

@justingrant justingrant Jul 15, 2021

Choose a reason for hiding this comment

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

I would use ES.HasOwnProperty() here, to avoid calling getters a second time. That will still not quite be spec-compliant, since Proxy objects would be able to observe the HasProperty operation, but it's probably fine. Although that might mean we shouldn't backport this to proposal-temporal.

@ptomato I looked at the current polyfill code and I think that it has the multi-getter-call problem too. Look at the amend function. I don't think the current polyfill has the Proxy problem because it ObjectAssigns the options object before doing anything with it.

Anyway, I just pushed a new commit that does the options cloning (like the old polyfill) before reading, and also uses ES.HasOwnProperty() to avoid making the proxy multi-getter problem any worse than it already is. I'll leave it for someone else to fix amend later. Is this OK?

Also, I think this should be safe for back-porting because it doesn't make the current polyfill worse. What do you think?

LMK if you (or @ryzokuken @sffc) have any ideas how to improve this further.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I was planning to take a look at the handling in the spec polyfill at some point. I guess we can accept this as "don't pass silly arguments" here, though.

@justingrant justingrant force-pushed the speedup-tolocalestring branch 3 times, most recently from f065e51 to e646297 Compare July 15, 2021 03:43
@justingrant
Copy link
Contributor Author

I think this PR is ready to merge, but I'm going to hold off on merging it for a day or so so that we can port and merge the now=>Now rename (see tc39/proposal-temporal#1645) first. As soon as that's merged in the old repo, I'll prepare a port of it for this repo.

justingrant added a commit to justingrant/proposal-temporal that referenced this pull request Jul 16, 2021
Speed up toLocaleString ~2.5x by optimizing creation of
Intl.DateTimeFormat instances.

Port of js-temporal/temporal-polyfill#12
justingrant added a commit to justingrant/proposal-temporal that referenced this pull request Jul 16, 2021
Speed up toLocaleString ~2.5x by optimizing creation of
Intl.DateTimeFormat instances.

Port of js-temporal/temporal-polyfill#12
This commit speeds up toLocaleString and other operations that depend
on the polyfilled Intl.DateTimeFormat. The fix was to prevent
unnecessary creation of built-in Intl.DateTimeFormat objects, because
the constructor of that built-in class is slooooooow.

For more details about the underlying issue see:
https://bugs.chromium.org/p/v8/issues/detail?id=6528

In local testing, speedup is about 2.5x for ZDT toLocaleString calls.
@justingrant justingrant merged commit f67e312 into js-temporal:main Jul 16, 2021
Ms2ger pushed a commit to tc39/proposal-temporal that referenced this pull request Jul 16, 2021
Speed up toLocaleString ~2.5x by optimizing creation of
Intl.DateTimeFormat instances.

Port of js-temporal/temporal-polyfill#12
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.

4 participants