hermes: Date 800x slower than JSC
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 cleanand 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
- Create initial app with latest RN version.
- 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.
About this issue
- Original URL
- State: open
- Created a year ago
- Reactions: 28
- Comments: 58 (21 by maintainers)
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?
Any updates on this? Still blocking us from switching to hermes
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:Results:
tzset(), either called explicitly by DateUtil here, or indirectly by C stdlib’slocaltime().tzset()as it’s called anyway bylocaltime()(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:
Results:
Benchmark results: 66.47054199874401 387.2435830011964JavascriptCoredoesn’t calllocaltime()(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 disabletzset()from being called each time.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)
Here is a preview:
<div>And then I tried to use isSame function form dayjs and it took ~15500ms!!! to complete sorting
Hello 👋🏻 @tmikov, following our discussion during React Native Europe, I’d love to keep this moving. Can you clarify if:
@Titozzz thanks for posting here! Here are my initial comments:
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:
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.
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…
@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 you can’t control how many calls / sec are out there. Most modern apps use translation and text formatting libraries which rely heavily on
DateAPIs. UUID, crypto key generation or time stamping rely onDateAPIs. 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
Dateneeds to be blazing fast. It is used everywhere.@tmikov can’t speak for others but it seems ok for our case.
@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.
FYI, we are working on this. Asking questions and commenting here doesn’t mean we are waiting.
@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
@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.
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.
In our case DayJS was the problem, so I made some quick performance improvments and it now runs 1000x faster on iOS simulator:
@rrebase I was able to simplify your example to the following:
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 whenIntlis enabled is provided byIntl. FWIW, MDN says that when callingDate.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:
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.The “solution” in our case is indeed not using Hermes atm.
@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.@burakgormek Standalone examples like this are exactly what we need, thank you!
Now, I tried your test and got somewhat different results:
Hermes is slower, but not 70 times. When I increase the loop count from 1000 to 100,000, the difference becomes more pronounced:
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.
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 i think nobody is having issues with hermes on pc. Only on react native.
Testing shows that creating spacetime objects is whats taking 95% of the time in hermes, so I tested it using node locally and here is the same test pretty much.
Just to show you the difference.
Ill accept that the 85ms is in big reason because we are running in development mode.
But here we can compare creation of 600 spacetime objects in hermes takes 4 seconds. Creating 600 spacetime objects in node locally takes 1.5 ms
Thats 2600x slower.
I hope this is enough to show how extremely slow hermes is with date calculations
Listening for
NSSystemClockDidChangeNotificationorNSSystemTimeZoneDidChangecan be done directly fromPlatformIntlApple.mmusing objective-c blocks. That should work without any side effects from C++Don’t forget to remove the subscription for
NSSystemTimeZoneDidChangeevent on destructor. https://developer.apple.com/documentation/foundation/nsnotificationcenter/1411723-addobserverforname?language=objcThis should allow you to update the cached timezone data only when
NSSystemTimeZoneDidChangeevent fires.@tmikov you are correct.
spacetimeis just one of many. I believe nowadaysdate-fnsandmoment.js(deprecated)are the most popular. As for my benchmark,spacetimewas on average 30% slower with hermes than it was with JSC.As a non-synthetic example, displaying hundreds of data points on a graph. The Intl options passed to toLocaleTimeString might are also different in case of a more complex batched group of data points. So having to workaround all the logic in UTC only due to Hermes is not ideal.
On my machine the rough numbers I’m getting with code below, show almost a 10x difference:
This is only amplified with additional Intl API calls for every data point so these 1000x numbers can be realistic.
Here is the same function, and results using just Date objects.
Which means this takes 4-5 frames to calculate, which is incredibly slow even that
Here you go:
Doing date operations on about 600 objects in an array takes 4.2s, which is extremely slow
Spacetime is this lib: https://github.com/spencermountain/spacetime
@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 simple
isSameDaycalculation can be 2600x slower on hermes.