🔍🕵️♂️📈
- Theryn Groetken
The Chrome DevTools heap profiler shows memory distribution by your application's JavaScript objects and related DOM nodes.
Fancier Explanation - JS heap snapshots can be used to analyze memory graphs, compare snapshots, and find memory leaks.
TL;DR - Heap Snapshots let you see what is happening with your app's JavaScript, in memory.
Shallow size column displays the sum of "shallow sizes" of all objects created by a certain constructor function.
The shallow size is the size of memory held by an object itself (generally, arrays and strings have larger shallow sizes).
Retained size column displays the maximum retained size among the same set of objects.
The size of memory that can be freed once an object is deleted (and this its dependents made no longer reachable) is called the retained size.
Distance displays the distance to the root using the shortest simple path of nodes.
A case study with Jest 🃏
Mobile has been collecting test coverage on Pull Request for about ~8 months now. But the github action being used was faulty.
So we decided to move back to one that has had a recent rewrite and added cool new features like "annotations of where coverage could be added" on changed files in a PR.
Funny enough - there really shouldn't have been one. But low and behold - you can't just upgrade something without something else breaking - can you? 😅
<--- Last few GCs --->
[3140:0x5772ae0] 200367 ms: Scavenge 1840.0 (2074.6) -> 1833.6 (2074.6) MB, 8.1 / 0.2 ms (average mu = 0.389, current mu = 0.489) allocation failure
[3140:0x5772ae0] 200466 ms: Scavenge 1841.2 (2074.6) -> 1836.8 (2080.4) MB, 13.8 / 0.1 ms (average mu = 0.389, current mu = 0.489) allocation failure
[3140:0x5772ae0] 201606 ms: Mark-sweep 1852.6 (2082.3) -> 1840.4 (2085.6) MB, 975.2 / 0.2 ms (average mu = 0.393, current mu = 0.398) allocation failure scavenge might not succeed
<--- JS stacktrace --->
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
1: 0xb09980 node::Abort() [/usr/local/bin/node]
2: 0xa1c235 node::FatalError(char const*, char const*) [/usr/local/bin/node]
3: 0xcf77be v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/usr/local/bin/node]
4: 0xcf7b37 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/usr/local/bin/node]
5: 0xeaf3d5 [/usr/local/bin/node]
6: 0xeafeb6 [/usr/local/bin/node]
7: 0xebe3de [/usr/local/bin/node]
8: 0xebee20 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/usr/local/bin/node]
9: 0xec1d9e v8::internal::Heap::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/usr/local/bin/node]
10: 0xe83012 v8::internal::Factory::AllocateRaw(int, v8::internal::AllocationType, v8::internal::AllocationAlignment) [/usr/local/bin/node]
11: 0xe7b624 v8::internal::FactoryBase<v8::internal::Factory>::AllocateRawWithImmortalMap(int, v8::internal::AllocationType, v8::internal::Map, v8::internal::AllocationAlignment) [/usr/local/bin/node]
12: 0xe7d330 v8::internal::FactoryBase<v8::internal::Factory>::NewRawOneByteString(int, v8::internal::AllocationType) [/usr/local/bin/node]
13: 0x110e678 v8::internal::String::SlowFlatten(v8::internal::Isolate*, v8::internal::Handle<v8::internal::ConsString>, v8::internal::AllocationType) [/usr/local/bin/node]
14: 0x110ede4 v8::internal::String::SlowEquals(v8::internal::Isolate*, v8::internal::Handle<v8::internal::String>, v8::internal::Handle<v8::internal::String>) [/usr/local/bin/node]
15: 0x121c12a v8::internal::Runtime_StringEqual(int, unsigned long*, v8::internal::Isolate*) [/usr/local/bin/node]
16: 0x15f0a99 [/usr/local/bin/node]Run after run - OOMs were the cause of failure.
✅ Correct! There were a few things that were tried before moving onto heap snapshots.
node --inspect-brk --expose-gc ./node_modules/.bin/jest --runInBand --logHeapUsage
From here - it is just applying the knowledge we just covered.
Since we're looking for a memory leak - we can record a couple of snapshots (in the `memory tab`) and use the `compare` view to diff between two snapshots. Which is exactly what we'll do.
The heap snapshots will definitely help us - but it's important to call out that this was a clear memory leak.
Looking at the heap logging - it's apparent that there is a leak and that the leak is linear in nature. As each test passes, the heap size increases.
PASS App/1.spec.tsx (375 MB heap size)
PASS App/2.test.tsx (413 MB heap size)
PASS App/3.test.tsx (460 MB heap size)
PASS App/4.spec.ts (500 MB heap size)
PASS App/5.spec.tsx (540 MB heap size)
PASS App/6.spec.tsx (581 MB heap size)
PASS App/7.spec.tsx (617 MB heap size)
PASS App/8.spec.tsx (750 MB heap size)
PASS App/9.tsx (786 MB heap size)
PASS App/10.spec.tsx (824 MB heap size)
PASS App/11.tsx (898 MB heap size)(note the above is an aggregate of a few tests - so it looks potentially exponential but it isn't - at least not from my observations it wasn't)
Before we do that its worth mentioning...
A license? This looks like a node_module being loaded in... and with every test? 🤔
Oh dear 🦌 - is the entire TypeScript library being imported on every test?!
> typescript - jest - memory leak
https://github.com/kulshekhar/ts-jest/issues/1967
Turns out - there is a significant memory leak in Jest. But the neat part is - no one can agree on whose fault it is. Jest, ts-jest, node? 🤷♂️
The only thing we do know is that this problem appears after upgrading to anything after `v16.10.0`.
In React Native land - not much at all. There were attempts to integrate with newer build packages like `@swc/jest` and `@esbuild/jest` but neither of them have good support for React Native. (Web may be able to use these packages with some success - and added benefit of much faster test times).
For mobile - we have to pin our actions to `node v16.10.0` as well as our `engines` in our package.json.
Since we can't change our build tooling - we'll have to pin our version.
Theoretically - this should mitigate our CI issues until the Open Source teams at play have time to resolve the issue.