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

Perf: Summary of all perf changes. DO NOT MERGE. #1584

Open
wants to merge 16 commits into
base: master
Choose a base branch
from

Conversation

schleyfox
Copy link
Contributor

We have worked to improve luxon performance for our use case of parsing/formatting hundreds of thousands of dates at a time. These are represented as a series of independent PRs

perf

  1. Perf: Add benchmark for DateTime.local with zone #1574
  2. Perf: Use computed offset passed in DateTime constructor #1576
  3. Perf: Cache ts offset guesses for quickDT #1579
  4. Perf: Use hackyOffset if it parses date strings in the expected format #1580
  5. Perf: Memoize digitsRegex #1581
  6. Perf: Add DateTime.buildFormatParser and DateTime.fromFormatParser #1582

bug fix/quality of life

  1. Validate time zone in quickDT prior to guessing offset #1575
  2. Run buildAll in prepare, move husky install to postinstall #1583

While each perf-related PR improves perf on its own, we see the most benefit when they are all combined (especially for time zone offset handling).

Benchmark Comparison (name | before | after | after/before):

DateTime.fromObject with locale | 1,112,953 ±0.08% | 1,229,589 ±0.17% | 1.1x
DateTime.local with numbers | 844,898 ±0.15% | 965,500 ±0.15% | 1.14x
DateTime.local with numbers and zone | 50,913 ±0.18% | 183,955 ±0.17% | 3.61x
DateTime.fromFormat | 60,666 ±0.17% | 115,620 ±0.21% | 1.91x
DateTime.fromFormat with zone | 26,687 ±0.18% | 56,353 ±0.23% | 2.11x
DateTime.fromFormatParser |		| 418,416 ±0.12%
DateTime.fromFormatParser with zone |		| 98,700 ±0.24%
fromFormat vs. fromFormatParser | 60,666 ±0.17% | 418,416 ±0.12% | 6.9x
fromFormat vs. fromFormatParser with zone | 26,687 ±0.18% | 98,700 ±0.24% | 3.7x
DateTime#setZone | 175,791 ±0.29% | 301,550 ±0.34% | 1.72x

schleyfox and others added 16 commits January 19, 2024 17:01
We validate the zone in the constructor anyway, but the tests missed a
branch where we attempt to use an invalid zone to compute an offset,
which will result in an exception. Zone#isValid is calculated at
creation time (and checked in DateTime constructor already) so this will
not degrade performance in the happy case.
All calls to the datetime constructor that pass an offset (o), have just
computed that offset (quickDT, fromObject) except for clone(). Clone
passes an "old" option to determine whether previous offset can be
reused. If we have config.o, but config.old is not set, then we can use
o without computing it.

This saves an expensive call to zone.offset that duplicates work that
was done immediately prior.
Previously we guessed by calling zone.offset(tsNow) on every
construction of a date from date parts. This caches that result instead,
saving a call to offset
Profiles reveal that a substantial portion of fromFormat time is spent
in digitsRegex. It is only called from unitsForToken and it is always
called with 11 suffixes to match different numbers of digits.

There are 21 numbering systems, so a fully expanded cache would hold
11*21 = 231 regexes.

Benchmarks show about a 1.5x improvement on DateTime.fromFormat
This allows constructing a parser for a locale/format and reusing it
when parsing dates. Without this, DateTime.fromFormat constructs a new
parser on every call. When parsing large amounts of date strings, this
gets rather slow.

In benchmarks, this speeds up parsing by 4.4x
The time zone offset of a date can be computed on platforms that support
it (anything that's not super ancient) by using
Intl.DateTimeFormat.formatToParts with en-US to output an array of the
date components. For legacy reasons, you can also generate a date string
using Intl.DateTimeFormat.format which can be parsed into an array using
regexes. The string/regex approach (hackyOffset) is way faster (2-4x),
but much more susceptible to weird client configurations.

This detects whether hackyOffset is able to parse a known date
correctly, and uses it if it does.
Husky adds git hooks into the repo. We want the dev experience of

    $ git clone remote/luxon
    $ npm install # installs husky hooks
    $ ...develop develop develop...
    $ git commit
    _lint runs, everything works_

We also want to build luxon when we call npm pack or npm publish.

We also want to build luxon when it is installed somewhere else as a git
dependency
(https://docs.npmjs.com/cli/v10/configuring-npm/package-json#git-urls-as-dependencies)

Git dependencies work by running `npm install` from GitFetcher and then
depend on DirFetcher running `npm prepare` (but only prepare, not
prepack or postpack)

This change moves `husky install` to postinstall (which will,
unfortunately, still pointlessly run when we install luxon as a git
dependency). It moves the buildAll to prepare from prepack so that the
build happens when installed as a git dependency. This should preserve
existing behavior while enabling installation from git.

When installing from git and/or in CI environments, the husky command is
sometimes not available in postinstall or is available but bails out
when not run in the context of a git repo.
Copy link

linux-foundation-easycla bot commented Jan 22, 2024

CLA Signed

The committers listed above are authorized under a signed CLA.

@schleyfox
Copy link
Contributor Author

/easycla

@icambron
Copy link
Member

icambron commented Mar 9, 2024

@schleyfox Hey Ben -- these are great, and I really like the effort and depth here. I've merged most of them. The remaining items are:

@schleyfox
Copy link
Contributor Author

@icambron thanks!

for #1579 I'll get that fixed shortly

for #1580 I'll benchmark that and respond, should be straightforward.

for #1583 totally! It's been on my backlog to docker up an example to see how the npm hooks behave under different conditions (I'm quite confused), but I haven't had a chance yet.

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.

None yet

2 participants