Luxon: Moar speed

Created on 21 Nov 2017  路  16Comments  路  Source: moment/luxon

I would love some help with perf testing and fixes. I suspect there are a lot of CPU cycles to squeeze out of this thing, especially at instance creation.

performance up for grabs

Most helpful comment

First of all I love Luxon and have already started porting a bunch of my internal work off moment-timezone.

I know this may be super controversial however I have also started speeding up my projects by rewriting the most performance intensive parts in Rust and then compiling to WASM (and Asm.js for fallback) I鈥檓 wondering if you would be open to a PR that does this for some of luxons hot paths. If not however I totally understand.

All 16 comments

A big perf change: b6a7aed. Gives about 6x better perf on DateTime.local() at the cost of less safety; it's now much easier to mutate Luxon objects by messing with their members.

on this topic, for your reference

when we experimented luxon to replace moment-timezone, same operations:

  1. parse time string that does not come with a timezone, and assign a timezone for it
  2. assign a variable with its date (using .toFormat('yyyy-MM-dd')), another with its H.

moment-timezone version when parsing through 100 time-strings on average takes ~500ms on a 128MB instance
luxon on average takes ~10,000ms on the same instance

of course, there are other operations such as DB insertion and what not, but DB operations stayed the same between the two variants

Wait, it took 10 seconds to do 100 strings (or at best 9.5 seconds)? I do expect moment-timezone is probably a little faster at timezones, but that is an astronomical amount of time. I'll see if I can peek into that soon. In the meantime, optimization help is always wanted.

yes. the exact code:

// luxon version
const { DateTime } = require('luxon')
const format = 'yyyy-MM-dd HH:mm:ss.SSS'
const zone = 'America/Toronto'
// some array of data
for (let r of rows) {
  const logTime = DateTime.fromString(r.LogTime, format, { zone }).toUTC()
  r.date = logTime.toFormat('yyyy-MM-dd')
  r.hour = logTime.toFormat('H')
  // ...
}
// moment-timezone version
const moment = require('moment-timezone')
const format = 'YYYY-MM-DD HH:mm:ss.SSS'
const zone = 'America/Toronto'
// some array of data
for (let r of rows) {
  const logTime = moment.tz(r.LogTime, format, zone).utc()
  r.date = logTime.format('YYYY-MM-DD')
  r.hour = logTime.format('H')
  // ...
}

Using AWS Lambda 128MB instance, Node.js 6.10 environment.

@woozyking I wasn't able to generate anything like those numbers on my Mac, but definitely setting zones is slow. I've pushed a perf enhancement to master that provides significant speedup, though it's still slower than I'd like. Formatting is also not that fast and there's surely some perf to squeeze out of that.

@icambron thanks! I'll take some time to test it out again later using AWS Lambda.

First of all I love Luxon and have already started porting a bunch of my internal work off moment-timezone.

I know this may be super controversial however I have also started speeding up my projects by rewriting the most performance intensive parts in Rust and then compiling to WASM (and Asm.js for fallback) I鈥檓 wondering if you would be open to a PR that does this for some of luxons hot paths. If not however I totally understand.

@icambron sorry for the huge delay, I feel really bad for dragging such an simple task for this long.

I've set this up through a repo https://github.com/woozyking/test-datetime which has both local run through npm start as well as appropriate AWS Lambda setup through the serverless framework.

The test goes through 1000 timestamps iteratively.

Local result (npm start, early-2014 macbook air with latest macos, and 8GB ram):

LuxonParse: 1569.733ms
MomentParse: 227.736ms

AWS Lambda (128MB instances), initially timed out for luxon variant with default 6 sec timeout, adjusted to 30 sec.

-- luxon
START RequestId: xxx Version: $LATEST
2018-03-23T21:51:02.388Z    xxx LuxonParse: 17259.227ms
END RequestId: xxx
REPORT RequestId: xxx   Duration: 17446.99 ms   Billed Duration: 17500 ms Memory Size: 128 MB   Max Memory Used: 79 MB  
-- momenttz
START RequestId: yyy Version: $LATEST
2018-03-23T21:50:39.609Z    yyy MomentParse: 3220.308ms
END RequestId: yyy
REPORT RequestId: yyy   Duration: 3257.35 ms    Billed Duration: 3300 ms Memory Size: 128 MB    Max Memory Used: 34 MB  

edit: hid request ids, not sure whether they're sensitive

@corbinu I think there's a lot of lower hanging fruit on this tree. I bet we could get at least an order or two of magnitude out of the slow parts of Luxon before resorting to that.

@woozyking Thanks for putting that together; stuff like that is really helpful. On the issue itself: one thing I've begun to realize is how much the Intl library slows down performance. That realization allowed me to release two formatting fixes with huge perf gains, and there's more to come there, once I set up some more benchmarks and get the flamecharts. I'm willing to bet the parsing slowness is caused by the same (or similar) root issue. If so, some dramatic perf improvements should be relatively easy.

Ok no worries. I will probably try it in some of the other modules I depend on first

Looking forward to the perf gain, will run the same lambda tests with new releases.

Hello @woozyking,

I had the same issues with the performance of the library. I ran the Chrome profiler against the library, and found out that in my specific case (parsing a string from a specific format in a specific zone) over 80% of the time is spent inside the (native) Intl library (as mentioned above by @icambron).

I could get the performance of parsing up by a factor of 10 by caching the timezone. In fact, the code seems to run faster than the moment-timezone-equivalent.

Some benchmarking results:

Function | Run 1 | Run 2 | Run 3 | Run 4 | Average | Relative to momentjs
----- | ------- | ------ | ------ | ------- | ----- | ------
stringZone | 1.717 ms | 1.995 ms | 2.408 ms | 1.907 ms | 2.007 ms | 965% (not a typo)
iANAZone | 0.158 ms | 0.183 ms | 0.237 ms | 0.198 ms | 0.194 ms | 93%
momentTz | 0.164 ms | 0.267 ms | 0.193 ms | 0.206 ms | 0.208 ms | 100%

Benchmark code:

/* eslint import/no-extraneous-dependencies: off */
/* eslint no-console: off */
import DateTime from '../src/datetime';
import IANAZone from '../src/zones/IANAZone';
import * as moment from 'moment-timezone';

const dt = DateTime.local();

const zone = new IANAZone('America/Los_Angeles');
// ^^^^ this is 'caching the timezone'

function stringZone() {
  DateTime.fromFormat('1982/05/25 09:10:11.445', 'yyyy/MM/dd HH:mm:ss.SSS', {
    zone: 'America/Los_Angeles'
  });
}

function iANAZone() {
  DateTime.fromFormat('1982/05/25 09:10:11.445', 'yyyy/MM/dd HH:mm:ss.SSS', {
    zone
  });
}

function momentTz() {
  moment.tz('1982/05/25 09:10:11.445', 'yyyy/MM/dd HH:mm:ss.SSS', 'America/Los_Angeles');
}

console.time("iANAZone");
for (let i = 0; i < 1000; i++)
  iANAZone();
console.timeEnd("iANAZone");
console.time("momentTz");
for (let i = 0; i < 1000; i++)
  momentTz();
console.timeEnd("momentTz");
console.time("stringZone");
for (let i = 0; i < 1000; i++)
  stringZone();
console.timeEnd("stringZone");

tl;dr

Don't use timezone as a string if you need it multiple times, use the IANAZone class instead (for a 10x faster parsing).

Hi @mhverbakel

That's some wonderful insight! I think having the IANAZone cached internally would be a good enough boost for these cases.

Yup, I saw that in the flamechart I ran against the benchmarker. The IANAZone class should definitely maintain its own cache so that this is transparent.

Let's move the IANAZone discussion to https://github.com/moment/luxon/issues/226.

Minor update of the benchmarks, updated both Luxon and moment-timezone to-date (at the time of writing this), and added separate benchmark with "cached" IANA zone (created before the timer), one run with my good old early-2104 MacBook Air:

Test sample size: 1000 timezone-less timestamps
luxon: 1409.340ms
luxon zone cached: 129.252ms
moment-timezone: 83.667ms

It isn't too much effort to create a constant IANA zone object in real-world applications, but I still agree with @icambron that if this is seamlessly offered behind the scenes, users would be much more appreciated.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

UnsungHero97 picture UnsungHero97  路  5Comments

jonathanborges picture jonathanborges  路  6Comments

kaftand picture kaftand  路  4Comments

tamirvs picture tamirvs  路  5Comments

dnalborczyk picture dnalborczyk  路  3Comments