Skip to content

Commit b92a5c2

Browse files
authored
fix: Reset Kotlin C++ state in dispose() (#1326)
* fix: Reset C++ state in `dispose()` * fix: Must call super * fix: Run memory leak 51200 times to test JNI as well * test 55k separately
1 parent c870f9d commit b92a5c2

3 files changed

Lines changed: 72 additions & 40 deletions

File tree

example/src/getTests.ts

Lines changed: 59 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,54 +1532,76 @@ export function getTests(
15321532
.didNotThrow()
15331533
.equals(10_000)
15341534
),
1535-
createTest('HybridObjects dont leak memory', () =>
1536-
it(() => {
1537-
const baselineAllocations =
1538-
NitroModules.debug_getTotalAllocatedHybridObjects()
1535+
createTest(
1536+
'HybridObjects dont leak memory with automatic YoungGen GC',
1537+
() =>
1538+
it(() => {
1539+
const baselineAllocations =
1540+
NitroModules.debug_getTotalAllocatedHybridObjects()
1541+
const TOTAL_ALLOCATIONS = 10_000
1542+
const BATCH_SIZE = 1000
1543+
1544+
const objects: Array<TestObjectCpp | TestObjectSwiftKotlin> = []
1545+
for (let i = 0; i < TOTAL_ALLOCATIONS; i++) {
1546+
const object = testObject.newTestObject()
1547+
object.numberValue = i
1548+
objects.push(object)
1549+
1550+
if (objects.length >= BATCH_SIZE) {
1551+
objects.length = 0
1552+
gc()
1553+
}
1554+
}
1555+
1556+
objects.length = 0
1557+
gc()
1558+
gc()
1559+
gc()
15391560

1561+
const currentAllocations =
1562+
NitroModules.debug_getTotalAllocatedHybridObjects()
1563+
const remainingAllocations = currentAllocations - baselineAllocations
1564+
// make sure that less than 10% of the total allocations are remaining, indicating GC ran for most of it.
1565+
const didDeleteMostObjects =
1566+
remainingAllocations < TOTAL_ALLOCATIONS * 0.1
1567+
const result: {
1568+
baselineAllocations: number
1569+
currentAllocations: number
1570+
isEqual?: boolean
1571+
} = {
1572+
baselineAllocations: baselineAllocations,
1573+
currentAllocations: currentAllocations,
1574+
isEqual: didDeleteMostObjects,
1575+
}
1576+
if (!didDeleteMostObjects) {
1577+
delete result.isEqual
1578+
}
1579+
return result
1580+
})
1581+
.didNotThrow()
1582+
.toContain('isEqual')
1583+
),
1584+
createTest('HybridObjects dont leak memory with manual dispose()', () =>
1585+
it(() => {
1586+
// We test 55_000 allocations, because JNI's max global_ref count
1587+
// is 51200, so if this test goes green, it properly deletes
1588+
// jni::global_refs. If there would be a memory leak, this test
1589+
// will likely crash in C++.
1590+
const TOTAL_ALLOCATIONS = 55_000
15401591
const BATCH_SIZE = 1000
1541-
const LOOP_COUNT = 10
1542-
const TOTAL_ALLOCATIONS = BATCH_SIZE * LOOP_COUNT
15431592

1544-
const objects: Array<TestObjectCpp | TestObjectSwiftKotlin> = []
15451593
for (let i = 0; i < TOTAL_ALLOCATIONS; i++) {
15461594
const object = testObject.newTestObject()
1547-
object.numberValue = i
1548-
objects.push(object)
1595+
object.dispose()
15491596

1550-
if (objects.length >= BATCH_SIZE) {
1551-
objects.length = 0
1597+
if ((i + 1) % BATCH_SIZE === 0) {
15521598
gc()
15531599
}
15541600
}
15551601

1556-
objects.length = 0
15571602
gc()
1558-
gc()
1559-
gc()
1560-
1561-
const currentAllocations =
1562-
NitroModules.debug_getTotalAllocatedHybridObjects()
1563-
const remainingAllocations = currentAllocations - baselineAllocations
1564-
// make sure that less than 10% of the total allocations are remaining, indicating GC ran for most of it.
1565-
const didDeleteMostObjects =
1566-
remainingAllocations < TOTAL_ALLOCATIONS * 0.1
1567-
const result: {
1568-
baselineAllocations: number
1569-
currentAllocations: number
1570-
isEqual?: boolean
1571-
} = {
1572-
baselineAllocations: baselineAllocations,
1573-
currentAllocations: currentAllocations,
1574-
isEqual: didDeleteMostObjects,
1575-
}
1576-
if (!didDeleteMostObjects) {
1577-
delete result.isEqual
1578-
}
1579-
return result
1580-
})
1581-
.didNotThrow()
1582-
.toContain('isEqual')
1603+
return TOTAL_ALLOCATIONS
1604+
}).didNotThrow()
15831605
),
15841606
createTest('callWithOptional(undefined)', async () =>
15851607
(

packages/react-native-nitro-modules/android/src/main/java/com/margelo/nitro/core/HybridObject.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.margelo.nitro.core
22

3+
import androidx.annotation.CallSuper
34
import androidx.annotation.Keep
45
import com.facebook.jni.HybridData
56
import com.facebook.proguard.annotations.DoNotStrip
@@ -39,13 +40,17 @@ abstract class HybridObject {
3940
* If this method is never manually called, a `HybridObject` is expected to disposes its
4041
* resources as usual via the object's destructor (`~HybridObject()`, `deinit` or `finalize()`).
4142
*
42-
* By default, this method does nothing. It can be overridden to perform actual disposing/cleanup
43-
* if required.
43+
* By default, this method only destroys the connected C++ part.
44+
* It can be overridden to perform actual disposing/cleanup if required.
4445
* This method must not throw.
4546
*/
4647
@DoNotStrip
4748
@Keep
48-
open fun dispose() { }
49+
@CallSuper
50+
open fun dispose() {
51+
cxxPartCache?.get()?.destroy()
52+
cxxPartCache = null
53+
}
4954

5055
/**
5156
* Get a string representation of this `HybridObject` - useful for logging or debugging.
@@ -96,6 +101,10 @@ abstract class HybridObject {
96101
mHybridData = initHybrid()
97102
}
98103

104+
fun destroy() {
105+
mHybridData.resetNative()
106+
}
107+
99108
protected open external fun initHybrid(): HybridData
100109
}
101110
}

packages/react-native-nitro-test/android/src/main/java/com/margelo/nitro/test/HybridTestObjectKotlin.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,7 @@ class HybridTestObjectKotlin : HybridTestObjectSwiftKotlinSpec() {
628628
}
629629

630630
override fun dispose() {
631+
super.dispose()
631632
this.optionalCallback?.let { callback ->
632633
callback(13.0)
633634
}

0 commit comments

Comments
 (0)