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

Date 800x slower than JSC #930

Open
1 task done
burakgormek opened this issue Mar 3, 2023 · 58 comments
Open
1 task done

Date 800x slower than JSC #930

burakgormek opened this issue Mar 3, 2023 · 58 comments
Labels
enhancement New feature or request

Comments

@burakgormek
Copy link

burakgormek commented Mar 3, 2023

Bug Description

Date so slow that it can be freeze app. Impossible to use third party date library(much worse perf, 1800x).
I found that local date is a problem. UTC date still slow but not 800x, just 8x slower than JSC which can be ignored.

The code outputs:
JSC: 0.2605330003425479
Hermes: 166.44708999991417
Hermes UTC: 1.6460870001465082

  • I have run gradle clean and confirmed this bug does not occur with JSC

Hermes version: Bundled version with RN 0.71.3
React Native version (if any): 0.71.3
OS version (if any): iPhone 14 (simulator)
Platform (most likely one of arm64-v8a, armeabi-v7a, x86, x86_64):

Steps To Reproduce

  1. Create initial app with latest RN version.
  2. Add somewhere the code that down below. Look console to see the numbers.

code example:

var a = performance.now();
Array(1000)
  .fill(0)
  .map(() => {
    new Date(new Date().setMonth(1));
    // fast utc version 
    // new Date(new Date().setUTCMonth(1));
  });
var b = performance.now();

console.log(b - a);

The Expected Behavior

Fast Date operations.

@burakgormek burakgormek added the bug Something isn't working label Mar 3, 2023
@burakgormek burakgormek closed this as not planned Won't fix, can't repro, duplicate, stale Mar 4, 2023
@burakgormek burakgormek reopened this Mar 4, 2023
@tmikov
Copy link
Contributor

tmikov commented Mar 4, 2023

Thank you for reporting this. It looks like an actual problem and is likely caused by us not caching the timezone information.

As an experiment, we changed the system timezone while the JS process was running to see whether it would invalidate the timezone. Hermes displayed the correct new timezone, while JSC and v8 displayed the original one. It is not entirely clear which behavior is preferable.

Could you try a similar experiment in your mobile app?

@ljukas
Copy link

ljukas commented Mar 6, 2023

I can confirm this, I previously used spacetime js to simplify date comparisons in my expo managed app. With the newest release of expo 48 we transitioned to hermes engine and had to remove all spactime js uses because just 150 items slowed down the app to a standstill for several seconds whilst running date comparisons.

This is now our code:

image

And this is also slower than it was using jsc, by a large margin.

@rpopovici
Copy link

@tmikov this is a major problem for anything which relies on dates to generate unique identifiers or localized translations..

Both iOS and android have mechanisms to notify the app when there is a timezone change.
https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622992-applicationsignificanttimechange
https://developer.android.com/reference/android/content/Intent#ACTION_TIMEZONE_CHANGED

IMO, caching the timeszone info and then updating that info only when there is an actual timezone change could be a potential solution here.

@jpporto jpporto added enhancement New feature or request and removed bug Something isn't working labels Mar 16, 2023
@DePavlenko
Copy link

DePavlenko commented Apr 27, 2023

We have exactly the same issue. Because of it we cannot switch to Hermes engine unfortunately.

I run this function in a sorting loop (~500 items in a list)

const isSameDay = (date1: Date, date2: Date) =>
  date1.getFullYear() === date2.getFullYear() &&
  date1.getMonth() === date2.getMonth() &&
  date1.getDate() === date2.getDate();

Here is a preview:


And then I tried to use isSame function form dayjs and it took ~15500ms!!! to complete sorting

@tmikov
Copy link
Contributor

tmikov commented Apr 27, 2023

We agree that this needs to be fixed to match the behavior of the other engines. Unfortunately we don't have concrete plans on when exactly we can work on the fix yet. Meanwhile a community contribution would be welcome.

@DePavlenko
Copy link

Thank you for the quick response @tmikov!
I just wonder how people handle it in their projects at the moment (don't use Hermes?). It's quite hard to find any opened issues or articles about the problem but I think almost every project has some interactions with dates

@harrigee
Copy link

Thank you for the quick response @tmikov!
I just wonder how people handle it in their projects at the moment (don't use Hermes?). It's quite hard to find any opened issues or articles about the problem but I think almost every project has some interactions with dates

The "solution" in our case is indeed not using Hermes atm.

@rpopovici
Copy link

@tmikov The simplest most straight forward solution here would be to expose an API from hermes which will cache the timezone info on demand instead of doing it automatically on every call.

@tmikov
Copy link
Contributor

tmikov commented May 18, 2023

@rpopovici are there APIs for updating the cached timezone information in JSC? In other words, if a RN app is running in JSC and a system timezone change occurs, how can you force it to refresh its cache? If there is no such API, how can the app handle timezone changes at all?

@rpopovici
Copy link

@tmikov if relying on timezone change events is a deviation from the standard behaviour, then trying to debounce the timezone info every second could be an acceptable compromise solution which could alleviate the perf bottleneck and is better than JSC or v8 since these two won't update the timezone data at all.

Also the timezone debouncing solution can be easily exposed through a hermes runtime config flag and then you can keep both behaviours and let the user decide if they need super accurate timezone info or they can settle for something less accurate but more performant

@rpopovici
Copy link

rpopovici commented May 20, 2023

Listening for NSSystemClockDidChangeNotification or NSSystemTimeZoneDidChange can be done directly from PlatformIntlApple.mm using objective-c blocks. That should work without any side effects from C++

id __block token = [
    [NSNotificationCenter defaultCenter] addObserverForName: NSSystemTimeZoneDidChangeNotification
    object: nil
    queue: nil
    usingBlock: ^ (NSNotification * note) {
        // do stuff here, like calling a C++ method
    }
];

Don't forget to remove the subscription for NSSystemTimeZoneDidChange event on destructor. https://developer.apple.com/documentation/foundation/nsnotificationcenter/1411723-addobserverforname?language=objc

[[NSNotificationCenter defaultCenter] removeObserver:token];

This should allow you to update the cached timezone data only when NSSystemTimeZoneDidChange event fires.

@tmikov
Copy link
Contributor

tmikov commented May 22, 2023

@rpopovici this indeed looks like an attractive solution. What is the thread safety of the callback, i.e. in what thread will it be invoked?

I suspect the safest thing to do here is to set a per-runtime atomic flag and check it later.

@rpopovici
Copy link

@tmikov When queue: nil, the block runs synchronously on the posting thread, but than can be accommodated to run on different NSOperationQueue if that's necessary.

I suspect the safest thing to do here is to set a per-runtime atomic flag and check it later.
Yes

@tmikov
Copy link
Contributor

tmikov commented May 22, 2023

Hmm, unfortunately Hermes doesn't own a thread or an NSOperationQueue. It executes in the context of an arbitrary thread provided by an integrator. Adding functionality like this is never as simple as it appears.

So, let's clarify the scope of the problem. Other engines like v8 and JSC cache the timezone permanently with no ability to update it, even if the system timezone changes. Apparently RN developers find that behavior acceptable. It seems like we can duplicate that behavior in Hermes. Is that a reasonable conclusion?

@rpopovici
Copy link

@tmikov can't speak for others but it seems ok for our case.

@morgan-cromell
Copy link

Any updates on this? Still blocking us from switching to hermes

@evelant
Copy link

evelant commented Jul 9, 2023

I think this seems like the most likely culprit destroying my app performance with hermes. We have a fair amount of date logic since part of the app is a todo list. The app runs 4-6x slower on hermes than on JSC or v8.

Hopefully this can get bumped up in priority. It seems like it's impacting a fair number of projects. There are probably many projects using hermes by default now that are suffering poor performance because of this.

Perhaps some basic performance tests should be added to the hermes test suite to hopefully catch app breaking performance regressions like this before they get out to production apps.

@tmikov
Copy link
Contributor

tmikov commented Jul 10, 2023

@evelant do you have a repro of your problem or is this just a guess?

How many date calculation per second is your app performing and what kind of calculation? What does "runs 4-6x slower" mean? Is the update rate 6 times slower? Is your app performing non-stop date calculations in a loop?

@evelant
Copy link

evelant commented Jul 10, 2023

Unfortunately I don't have a repro, it's just a guess.

By 4-6x slower I mean that user-perceived responsiveness is 4-6x slower. Everything in the app takes 4-6x longer when running on hermes.

My best guesses so far at likely culprits are:

  • Date logic performance. Depending on the user the app could be performing hundreds or more date calculations on loading and some interactions.
  • Proxy performance. The app relies heavily on mobx which uses proxies to make objects observable for efficient react renders and derived computations.
  • JSI performance. The app uses react-native-skia and react-native-mmkv heavily. Those are both JSI libraries.
  • JSON parse/stringify performance. Some users have a lot of data, if serializing/deserializing that is slower with hermes that would add to the issue.

Sorry I don't have more than a guess at the moment. I don't currently have the time to really pick apart the app to narrow it down. We're just going to run on JSC or v8 until there's more room to investigate.

@rpopovici
Copy link

@tmikov you can't control how many calls / sec are out there. Most modern apps use translation and text formatting libraries which rely heavily on Date APIs. UUID, crypto key generation or time stamping rely on Date APIs. Just a simple sorting algorithm for a small item list can freeze your app with hermes.js when date comparison is involved.

This is why Date needs to be blazing fast. It is used everywhere.

@evelant
Copy link

evelant commented Jul 10, 2023

@rpopovici Agreed. For example our app by default groups tasks by day for a calendar view. If someone has a couple hundred tasks we're tripping over exactly the issue @DePavlenko described above where a simpleisSameDay calculation can be 2600x slower on hermes.

@tmikov
Copy link
Contributor

tmikov commented Jul 12, 2023

@evelant help me understand, why do you need to perform local time calculations for hundreds of tasks? Ordinarily, local time would only be used for display, and all logic would use UTC?

@evelant
Copy link

evelant commented Jul 12, 2023

@tmikov Most of the logic uses UTC, but we also use libraries like date-fns that might or might not use Date instances under the hood. As others have said however, it doesn't really matter -- it's not an app problem since the app is fine on JSC and v8, it's a hermes problem. If using particular date libraries or accidentally doing some date calculations using Date objects instead of UTC timestamps can cause such a massive slowdown that's a serious bug in hermes, you can't expect app/library developers to work around that just for hermes.

@tmikov
Copy link
Contributor

tmikov commented Jul 19, 2023

Can someone please provide a real example (not a synthetic one) demonstrating the problem? The already synthetic examples are illustrative, but for real optimization work we need examples of actual useful code that needs its performance improved.

@rpopovici
Copy link

rpopovici commented Jul 22, 2023

@tmikov I re-run your tests locally and indeed hermes is on par with v8 for this test scenario, but when you run the same test in react-native, that's a very different story..

  • v8(node.js on mac M1) - 56 ms
  • hermes cli(on mac M1) - 47 ms
  • react-native(hermes release build on iOS simulator) - 3410 ms
  • react-native(JSC on iOS simulator) - 158 ms

@ljukas
Copy link

ljukas commented Jul 22, 2023

Ok, this time Ive done some more test, with focus on running them as one would use hermes and dates and not sythetically via cmd.

Reproduction repo can be found here: https://github.com/ljukas/hermes-date-test

Its a fresh expo project that includes spacetime and runs two similar calculations.
One to calculate adding days to a Date object and one using spacetime.

I created 4 different builds via eas. 2 preview builds and 2 development builds, one of each with jsc and hermes. Results are as following.

Development/Hermes: spacetime - ~188ms | date - ~0.123ms
Development/Jsc: spacetime - ~135ms | date - ~0.066ms
Preview/Hermes: spacetime - ~158ms | date - ~0.12ms
Preview/Jsc: spacetime - ~139ms | date - ~0.08ms

I just did one simple calculation here, if you scale it little bit more and do several date calculations in each loop the difference between jsc and hermes grows.

Id also like to refer to @rpopovici answer above. And Id also like to say that I do not think we should compare what happens via command line like your test @tmikov and instead run it inside react-native where hermes is supposed to be used.

@morgan-cromell
Copy link

@tmikov i think nobody is having issues with hermes on pc. Only on react native.

@tmikov
Copy link
Contributor

tmikov commented Jul 22, 2023

First some background.

I hope everyone understands that running Hermes in "React Native" and running on desktop should be exactly the same thing, especially when using Arm64 devices. The desktop is faster than a phone, sometimes 20x faster, but the relative performance differences between engines are the same. If Hermes is 2x slower than v8 on desktop, it will be about 2x slower than v8 on a mobile device.

When we are debugging an issue, be it performance or correctness, we don't want to debug React Native, which is large and complex. This repository is for the Hermes JavaScript engine, not for React Native, the Hermes team consists of compiler engineers with little expertise in React Native, so we don't really have the ability to debug React Native problems. Thus we need isolated examples, not entire RN apps.

Since most people don't know how to run only Hermes without React Native on device, we are asking for reproduction on desktop CLI (if possible, of course).

Now, @rpopovici, your results from running Hermes in iOS simulator are extremely puzzling and point to an additional problem. As you know, the iOS simulator runs software directly on the host CPU or under Rosetta, but in either case the performance of CPU-bound tasks should be close to the host (in my experience Rosetta is shockingly fast).

Still, it is worth checking, are you running am arm64 or x86-64 image in your simulator?

If you are seeing something so dramatically different, this might mean that something is broken in the RN packaging of Hermes. Perhaps it is compiled in debug mode, or even in "slow debug" mode, which is even slower.

(Note that we fully acknowledge that Hermes has a performance problem with localized dates, but perhaps we are additionally looking at something different here)

@tmikov
Copy link
Contributor

tmikov commented Jul 22, 2023

@burakgormek Standalone examples like this are exactly what we need, thank you!

Now, I tried your test and got somewhat different results:

$ v8 --jitless index.js
Warning: disabling flag --expose_wasm due to conflicting flags
ms 3

$ hermes-rel index.js
ms 12

Hermes is slower, but not 70 times. When I increase the loop count from 1000 to 100,000, the difference becomes more pronounced:

$ v8 --jitless index.js
Warning: disabling flag --expose_wasm due to conflicting flags
ms 74

$ hermes index.js
ms 497

So, Hermes is about 7x slower than jitless v8 on this code, which is pretty bad. I think this is something that we can work with, I will examine what is going on closer and report back here.

@rpopovici
Copy link

rpopovici commented Jul 22, 2023

@tmikov I re-run the spacetime test with an arm64 build and the result is 30% faster but still a lot slower than JSC

  • v8(node.js on mac M1) - 41 ms
  • v8(node.js --jitless on mac M1) - 56 ms
  • hermes cli(on mac M1) - 47 ms
  • react-native(hermes release build on iOS simulator x86-64) - 3410 ms
  • react-native(hermes release build on iOS simulator arm64) - 2626 ms
  • react-native(JSC on iOS simulator x86-64) - 158 ms
  • react-native(JSC on iOS simulator arm64) - 50 ms
  • react-native(hermes release build on iPhone XR) - 180 - 227 ms
  • react-native(JSC release build on iPhone XR) - 123 - 160 ms

@tmikov
Copy link
Contributor

tmikov commented Jul 23, 2023

@rpopovici can you try the following benchmark on your Mac and in iOS simulator running on the same Mac?

var logger = typeof print === "undefined"
    ? console.log
    : print;

function bench (lc, fc) {
    var n, fact;
    var res = 0;
    while (--lc >= 0) {
        n = fc;
        fact = n;
        while (--n > 1)
            fact *= n;
        res += fact;
    }
    return res;
}

var t1 = Date.now();
logger(bench(4e6, 100))
logger(Date.now() - t1, "ms");

This is code with very well understood performance, which will help us establish a baseline.

@rpopovici
Copy link

rpopovici commented Jul 23, 2023

@tmikov bench test results:

  • v8(node.js on mac M1) - 393 ms
  • v8(node.js --jitless on mac M1) - 4394 ms
  • hermes cli(on mac M1) - 2177 ms
  • react-native(hermes release build on iOS simulator arm64) - 2261 ms
  • react-native(JSC on iOS simulator arm64) - 313 ms
  • react-native(hermes release build on iPhone XR) - 3292 ms
  • react-native(JSC release build on iPhone XR) - 4082 ms

@tmikov
Copy link
Contributor

tmikov commented Jul 23, 2023

BTW, I want to note that the JSC performance in iOS simulator is not representative, because it uses JIT, while JIT is disabled on real devices because of Apple policies. So, it is pointless to compare JSC in simulator to Hermes in simulator.

@tmikov
Copy link
Contributor

tmikov commented Jul 23, 2023

@rpopovici the latest perf results are unexpected. So, normal CPU-bound code executes with the same performance in the simulator and on the host, but localized date() operations in the same engine are much slower in the simulator.

This is so surprising that we need to confirm it.

But if it is true, it must mean that for some reason standard libraries on iOS throttle the implementation of functions like std::localtime().

@tmikov
Copy link
Contributor

tmikov commented Jul 23, 2023

@rrebase I was able to simplify your example to the following:

const t0 = Date.now();
for (let i = 0; i < 500; i++) {
  new Date('2021-09-15T18:30:00.000Z').toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
const t1 = Date.now();
(typeof print !== "undefined" ? print: console.log)(`Call took ${t1 - t0} milliseconds.`);

Running this simplified test, v8 takes about 30 ms, while Hermes takes 230 ms.

This test exercises only the performance of Date.prototype.toLocareTimeString(), which when Intl is enabled is provided by Intl. FWIW, MDN says that when calling Date.prototype.toLocareTimeString() repeatedly, "it is better to create an Intl.DateTimeFormat object and use its format() method".

So, I modified the example in the following way:

const t0 = Date.now();
const df = new Intl.DateTimeFormat('en-US', { hour: '2-digit', minute: '2-digit' });
for (let i = 0; i < 500; i++) {
  df.format(new Date('2021-09-15T18:30:00.000Z'));
}
const t1 = Date.now();
(typeof print !== "undefined" ? print: console.log)(`Call took ${t1 - t0} milliseconds.`);

With this change both v8 and Hermes take less than 15ms.

My conclusion is that Date.prototype.toLocaleTimeString() in Hermes can indeed benefit from caching some Intl structures, but it seems like the same result can be achieved manually.

@tmikov
Copy link
Contributor

tmikov commented Jul 24, 2023

FYI, we are working on this. Asking questions and commenting here doesn't mean we are waiting.

@rpopovici
Copy link

rpopovici commented Jul 24, 2023

@tmikov I tested again both spacetime and your bench test on iPhone XR. You might be right about the throttling of std::localtime() on simulator
Tested on IPhone XR
spacetime:

  • react-native(hermes release build on iPhone XR) - 180 - 227 ms
  • react-native(JSC release build on iPhone XR) - 123 - 160 ms

bench:

  • react-native(hermes release build on iPhone XR) - 3292 ms
  • react-native(JSC release build on iPhone XR) - 4082 ms

@tmikov
Copy link
Contributor

tmikov commented Jul 24, 2023

@rpopovici oh great, this is becoming more and more bizarre (to be fair most interesting real world problems are). So, it seems the throttling is only on iOS Simulator. and performance is much better on a physical device?

How bad is the 227ms number that you are getting from device? Can you compare it to JSC?

@rpopovici
Copy link

@tmikov I edited the previous post #930 (comment)

@tmikov
Copy link
Contributor

tmikov commented Jul 25, 2023

@rpopovici your latest results make it look like Hermes is in about the same performance ballpark as JSC when using Spacetime on a physical iOS device. Is my interpretation of your numbers correct?

It seems that we have proven two things:

  1. Creating Spacetime objects in a loop is not a good benchmark to illustrate the Hermes performance problem with localized Date.
  2. iOS Simulator can be extremely slow in some cases and cannot be trusted for performance measurements.

Perhaps we should focus on the Dayjs benchmark which was posted earlier and where I was able to reproduce the slowness locally.

@rpopovici
Copy link

@tmikov you are correct. spacetime is just one of many. I believe nowadays date-fns and moment.js(deprecated) are the most popular.
As for my benchmark, spacetime was on average 30% slower with hermes than it was with JSC.

@Titozzz
Copy link
Contributor

Titozzz commented Sep 17, 2023

Hello 👋🏻 @tmikov, following our discussion during React Native Europe, I'd love to keep this moving. Can you clarify if:

  • This is being worked on internally
  • This needs to be contributed from community
  • You mentioned opening a discussion, should we still do it?
    Also I'm happy to provide more reproductions if that would help, so, let me know 😃 .

@tmikov
Copy link
Contributor

tmikov commented Sep 18, 2023

@Titozzz thanks for posting here! Here are my initial comments:

  • This is not being worked on currently, though we are fixing a bug in that area (Daylight savings time not handled properly in America/Santigo.  #1121) and are planning to eventually work on it. It is a real problem that must be fixed.
  • It would be great if it could be contributed by the community.
  • I think this issue is a good place for a discussion. I know I mentioned it, but I am not sure whether a discussion provides advantages compared to just using the existing issue.

If this could be a community contribution, we should discuss the approach in detail ahead of time.

As I mentioned before, the main problem here is caching. Caching of the time range with a fixed offset surrounding a given timestamp. So for example if we are working with a timestamp on Aug 15th 2010 and DST starts on Sep 15th and ends June 1st, then the range is from June 1st 2010 to Sep 15th 2010 (with hours and minutes, of course). Once we have that, we can convert times in that range to UTC and back very efficiently. Of course we should cache multiple of these for multiple timezones. I am simplifying a bit, but I hope this gives enough of initial context. Plus, keep in mind that I am not an expert on this, not even a little bit!

Additionally there should be probably be another layer of cache for a certain number of timestamps, since it is likely that the same timestamp will be operated on multiple times, for example when sorting. This is an optional improvement that can be added later.

So, there seem to be two approaches for achieving this:

  1. Use the system APIs on every platform to determine the start and end of these time ranges. We believe (or may be hope would be a more accurate word) that there exist platform APIs on each of our target platforms that can provide this data. Determining that requires some research. If the APIs are dramatically different between platforms, it may be impossible to abstract and may require separate implementations. It is hard to say at this moment.

  2. Package a part of the Olson timezone database inside Hermes. The database contains this information, so we would not need platform APIs. There however are a few complications with this approach:
    -- The Olson db is large and we can't package all of it with Hermes. That means that we would have to resort to the existing slow algorithm for dates that are beyond the packaged range. Or perhaps we can provide the option of a separate .so that contains the entire DB and users can add it to Hermes if they need it.
    -- This runs the risk of having subtle incompatibilities with the platform APIs and the Intl library which uses those.
    -- The Olson DB changes all the time (multiple times per year), so the process of obtaining and incorporating it in Hermes should be automated.

I hope this gives you an idea of what is necessary and why we haven't fixed it yet...

@PS-Soundwave
Copy link

I'm pretty sure iOS does not provide an API for time zone information, only supporting queries for dates. This is not per se disqualifying of method 1: I would be surprised if allocation-free use of the iOS API was not performant, but it does prevent abstracting the platform away at the level of time zones.

Method 2 is to my knowledge the typical choice. Risk of incompatibility from partial repacking can be mitigated by preferring dropping time zones to packing partial data for a time zone (and in general I don't think strong guarantees are or should be expected with regards to localized times). I did notice that tzdb is distributed in plaintext with substantial commentary, I'm wondering if after compilation it's more viable to pack?

@simontreny
Copy link

simontreny commented Oct 9, 2023

I ran into the same performance issues when dealing with dates on the iOS simulator and investigated a bit.
I'll share some of my learnings:

tzset() is the culprit

I've reproduced the issue by making a native app that calls Hermes' localTime() in a loop.
I've used the following code to measure performance:

#import "DateUtil.h"

double getTime(void) {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return (double)(tv.tv_sec) * 1000 + (double)(tv.tv_usec) / 1000;
}

int main(int argc, char * argv[]) {
    double t1 = getTime();
    for (int i = 0; i < 100000; i++) {
        localTime(getTime());
    }
    double t2 = getTime();
    printf("Duration: %lf\n", t2 - t1);
}

Results:

  • When executed as a macOS app, it takes 43ms to complete
  • When executed as an iOS app on a simulator, it takes 5011ms to complete (116x slower)
  • Nearly all that time is spent running tzset(), either called explicitly by DateUtil here, or indirectly by C stdlib's localtime().
  • Optimizations:
    • we don't need to explicitly call tzset() as it's called anyway by localtime() (see here). It should make the code run twice as fast.
    • localTZA() can easily be cached as well as it doesn't rely on a timestamp. It should lead to an additional 25% improvement.

On caching

Javascript Core implements caching of local time offsets here. The cache is invalidated if consecutive dates are separated by more than one month from each other. I've benchmarked the impact on cache using this code, executed on an iOS simulator from a React Native app with JSC enabled:

    const iters = 1000000;
    const msPerHour = 60 * 60 * 1000;
    const msPer2Months = 2 * 30 * 24 * msPerHour;
    const t1 = performance.now();
    for (let i = 0; i < iters; i++) {
      // Cache will be effective as the dates are separated by just one hour
      const date = new Date(1696830297000 + i * msPerHour);
      date.getHours();
    }
    const t2 = performance.now();
    for (let i = 0; i < iters; i++) {
      // Cache will be invalidated each time as the dates are separated by 2 months
      const date = new Date(1696830297000 + i * msPer2Months);
      date.getHours();
    }
    const t3 = performance.now();
    console.log('Benchmark results:', t2 - t1, t3 - t2);

Results:

  • Benchmark results: 66.47054199874401 387.2435830011964
  • Cache results in 6x improved performances, in the best case scenario where dates are close enough to each other.
  • Even uncached performance is far faster than Hermes. It's because JavascriptCore doesn't call localtime() (confirmed by adding breakpoints) but relies on ICU for timezones instead.

Conclusion:
Even if a cache can definitely help, I think the way to go is to ditch localtime() for timezone calculation as it is very slow on the iOS simulator. Or we need to find a way to disable tzset() from being called each time.

@TiStyle
Copy link

TiStyle commented Dec 8, 2023

@simontreny
Is there a way to bypass this in the meantime? As I've been running into performance issues with simple date comparisons on Android devices. iOS and simulators for both iOS and Android are performing well enough, but once the app is installed on an actual device on Android, the drama starts.

The funny thing is that when I run my apps with JSC with a debugger, all performance issues go away...

I do have a simple example of a situation where I setup a scheduler for a week overview without any library for date handeling. It is setup in expo though..

https://github.com/TiStyle/Expo-Scheduler

@evelant
Copy link

evelant commented Dec 8, 2023

@TiStyle I refactored all of my code to avoid Date instances (except for display in the UI). I just use plain numbers and do simple math when I need to add/subtract/calculate things. Use Date.now() to get a number instead of a Date.

@TiStyle
Copy link

TiStyle commented Dec 11, 2023

@evelant I was hoping to avoid such things, but it seems there is no other solution but for now(). Haha, get it?.. Thank you for your advice, I'll just have to start this painstakingly task.

@tj-mc
Copy link

tj-mc commented Mar 26, 2024

I wanted to share that we are also facing this issue in our React Native app.

We have an infinite list of messages, each of which render a date, which must be calculated by the difference between sendTime and now.

To "solve" this, we can eagerly render the list, then defer the date calculations inside InteractionManager. This greatly sped up the initial paint of the list.

/**
 * DateTime calculation are currently very slow in Hermes https://github.com/facebook/hermes/issues/930
 * This means each MessageTime would add about 20ms to the list item render.
 *
 * To 'resolve' this, we start with empty string, then wrap the formatFunction in InteractionManager,
 * so that it does block the first paint. This tradeoff is that the timestamp loads slightly after the message,
 * but the message list renders in 1/3rd the time.
 *
 * This optimisation can be removed when the above issue is solved.
 *
 */
export const MessageTime = ({ timestamp, prefix }: Props) => {
  const [text, setText] = useState('');

  useEffect(() => {
    const calculateStamp = () =>
      InteractionManager.runAfterInteractions(() => {
        setText(formatTimestamp(new Date(), timestamp));
      });

    calculateStamp();

    const interval = setInterval(() => {
      calculateStamp();
    }, oneMinute);

    return () => clearInterval(interval);
  }, [timestamp]);

  return (
    <View style={styles.container}>
      <Text muted fontSize={'sm'} numberOfLines={1}>
        {prefix}
        {text}
      </Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    display: 'flex',
    flexDirection: 'row',
    flexShrink: 1,
  },
});

@gaberogan
Copy link

In our case DayJS was the problem, so I made some quick performance improvments and it now runs 1000x faster on iOS simulator:

diff --git a/node_modules/dayjs/esm/index.js b/node_modules/dayjs/esm/index.js
index a82986b..1fb8898 100644
--- a/node_modules/dayjs/esm/index.js
+++ b/node_modules/dayjs/esm/index.js
@@ -117,17 +117,101 @@ var Dayjs = /*#__PURE__*/function () {
 
   _proto.init = function init() {
     var $d = this.$d;
-    this.$y = $d.getFullYear();
-    this.$M = $d.getMonth();
-    this.$D = $d.getDate();
-    this.$W = $d.getDay();
-    this.$H = $d.getHours();
-    this.$m = $d.getMinutes();
-    this.$s = $d.getSeconds();
-    this.$ms = $d.getMilliseconds();
+    // this.$y = $d.getFullYear();
+    // this.$M = $d.getMonth();
+    // this.$D = $d.getDate();
+    // this.$W = $d.getDay();
+    // this.$H = $d.getHours();
+    // this.$m = $d.getMinutes();
+    // this.$s = $d.getSeconds();
+    // this.$ms = $d.getMilliseconds();
   } // eslint-disable-next-line class-methods-use-this
   ;
 
+  // Avoid running native Date functions like getFullYear until needed because these
+  // functions are super slow in Hermes. Then run once and cache since Dayjs is immutable.
+
+  Object.defineProperty(Dayjs.prototype, '$y', {
+    get() {
+      if (!this.cached_y) {
+        this.cached_y = this.$d.getFullYear();
+      }
+      return this.cached_y;
+    }
+  });
+
+  Object.defineProperty(Dayjs.prototype, '$M', {
+    get() {
+      if (!this.cached_M) {
+        this.cached_M = this.$d.getMonth();
+      }
+      return this.cached_M;
+    }
+  });
+
+  Object.defineProperty(Dayjs.prototype, '$D', {
+    get() {
+      if (!this.cached_D) {
+        this.cached_D = this.$d.getDate();
+      }
+      return this.cached_D;
+    }
+  });
+
+  Object.defineProperty(Dayjs.prototype, '$W', {
+    get() {
+      if (!this.cached_W) {
+        this.cached_W = this.$d.getDay();
+      }
+      return this.cached_W;
+    }
+  });
+
+  Object.defineProperty(Dayjs.prototype, '$H', {
+    get() {
+      if (!this.cached_H) {
+        this.cached_H = this.$d.getHours();
+      }
+      return this.cached_H;
+    }
+  });
+
+  Object.defineProperty(Dayjs.prototype, '$m', {
+    get() {
+      if (!this.cached_m) {
+        this.cached_m = this.$d.getMinutes();
+      }
+      return this.cached_m;
+    }
+  });
+
+  Object.defineProperty(Dayjs.prototype, '$s', {
+    get() {
+      if (!this.cached_s) {
+        this.cached_s = this.$d.getSeconds();
+      }
+      return this.cached_s;
+    }
+  });
+
+  Object.defineProperty(Dayjs.prototype, '$ms', {
+    get() {
+      if (!this.cached_ms) {
+        this.cached_ms = this.$d.getMilliseconds();
+      }
+      return this.cached_ms;
+    }
+  });
+
+  Object.defineProperty(Dayjs.prototype, '$timezoneOffset', {
+    get() {
+      if (!this.cached_timezoneOffset) {
+        this.cached_timezoneOffset = this.$d.getTimezoneOffset();
+      }
+      return this.cached_timezoneOffset
+    }
+  });
+
   _proto.$utils = function $utils() {
     return Utils;
   };
@@ -137,15 +221,27 @@ var Dayjs = /*#__PURE__*/function () {
   };
 
   _proto.isSame = function isSame(that, units) {
+    if (units === undefined) {
+      return this.toISOString() === dayjs(that).toISOString()
+    }
+
     var other = dayjs(that);
     return this.startOf(units) <= other && other <= this.endOf(units);
   };
 
   _proto.isAfter = function isAfter(that, units) {
+    if (units === undefined) {
+      return this.toISOString() > dayjs(that).toISOString()
+    }
+
     return dayjs(that) < this.startOf(units);
   };
 
   _proto.isBefore = function isBefore(that, units) {
+    if (units === undefined) {
+      return this.toISOString() < dayjs(that).toISOString()
+    }
+
     return this.endOf(units) < dayjs(that);
   };
 
@@ -408,7 +504,7 @@ var Dayjs = /*#__PURE__*/function () {
   _proto.utcOffset = function utcOffset() {
     // Because a bug at FF24, we're rounding the timezone offset around 15 minutes
     // https://github.com/moment/moment/pull/1871
-    return -Math.round(this.$d.getTimezoneOffset() / 15) * 15;
+    return -Math.round(this.$timezoneOffset / 15) * 15;
   };
 
   _proto.diff = function diff(input, units, _float) {

@efstathiosntonas
Copy link

I noticed 150ms increase in render time with dayjs, without the date the component rendered in 50ms, converted to Date and it increased to 70ms which is way faster than the 200ms with dayjs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests