Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow

// memorySize hint: these wrappers look tiny to Hermes but each pins a JNI global ref, so
// without it the GC lets them pile up until the global reference table (cap 51200) overflows.
const val VIEW_MODEL_HYBRID_MEMORY_SIZE = 65_536L

Comment thread
mfazekas marked this conversation as resolved.
@Keep
@DoNotStrip
interface BaseHybridViewModelProperty<T> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import com.margelo.nitro.core.Promise
@Keep
@DoNotStrip
class HybridViewModel(private val viewModel: ViewModel) : HybridViewModelSpec() {
override val memorySize: Long
get() = VIEW_MODEL_HYBRID_MEMORY_SIZE

override val propertyCount: Double
get() = viewModel.propertyCount.toDouble()
override val instanceCount: Double
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import com.facebook.proguard.annotations.DoNotStrip
class HybridViewModelArtboardProperty(private val property: ViewModelArtboardProperty) :
HybridViewModelArtboardPropertySpec() {

override val memorySize: Long
get() = VIEW_MODEL_HYBRID_MEMORY_SIZE

override fun set(artboard: HybridBindableArtboardSpec?) {
val bindable = (artboard as? HybridBindableArtboard)?.bindableArtboard
property.set(bindable)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ import com.margelo.nitro.core.Promise
class HybridViewModelBooleanProperty(private val viewModelBoolean: ViewModelBooleanProperty) :
HybridViewModelBooleanPropertySpec(),
BaseHybridViewModelProperty<Boolean> by BaseHybridViewModelPropertyImpl() {
override fun dispose() {
removeListeners()
super<HybridViewModelBooleanPropertySpec>.dispose()
}

override val memorySize: Long
get() = VIEW_MODEL_HYBRID_MEMORY_SIZE

override var value: Boolean
get() = viewModelBoolean.value
set(value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ import com.margelo.nitro.core.Promise
class HybridViewModelColorProperty(private val viewModelColor: ViewModelColorProperty) :
HybridViewModelColorPropertySpec(),
BaseHybridViewModelProperty<Int> by BaseHybridViewModelPropertyImpl() {
override fun dispose() {
removeListeners()
super<HybridViewModelColorPropertySpec>.dispose()
}

override val memorySize: Long
get() = VIEW_MODEL_HYBRID_MEMORY_SIZE

override var value: Double
get() = viewModelColor.value.toDouble()
set(value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ import com.margelo.nitro.core.Promise
class HybridViewModelEnumProperty(private val viewModelEnum: ViewModelEnumProperty) :
HybridViewModelEnumPropertySpec(),
BaseHybridViewModelProperty<String> by BaseHybridViewModelPropertyImpl() {
override fun dispose() {
removeListeners()
super<HybridViewModelEnumPropertySpec>.dispose()
}

override val memorySize: Long
get() = VIEW_MODEL_HYBRID_MEMORY_SIZE

override var value: String
get() = viewModelEnum.value
set(value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ import kotlinx.coroutines.flow.map
class HybridViewModelImageProperty(private val viewModelImage: ViewModelImageProperty) :
HybridViewModelImagePropertySpec(),
BaseHybridViewModelProperty<Unit> by BaseHybridViewModelPropertyImpl() {
override fun dispose() {
removeListeners()
super<HybridViewModelImagePropertySpec>.dispose()
}

override val memorySize: Long
get() = VIEW_MODEL_HYBRID_MEMORY_SIZE

override fun set(image: HybridRiveImageSpec?) {
viewModelImage.set((image as? HybridRiveImage)?.renderImage)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import com.margelo.nitro.core.Promise
@Keep
@DoNotStrip
class HybridViewModelInstance(val viewModelInstance: ViewModelInstance) : HybridViewModelInstanceSpec() {
override val memorySize: Long
get() = VIEW_MODEL_HYBRID_MEMORY_SIZE

override val instanceName: String
get() = viewModelInstance.name

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ import kotlinx.coroutines.flow.map
class HybridViewModelListProperty(private val listProperty: ViewModelListProperty) :
HybridViewModelListPropertySpec(),
BaseHybridViewModelProperty<Unit> by BaseHybridViewModelPropertyImpl() {
override fun dispose() {
removeListeners()
super<HybridViewModelListPropertySpec>.dispose()
}

override val memorySize: Long
get() = VIEW_MODEL_HYBRID_MEMORY_SIZE

override val length: Double
get() = listProperty.size.toDouble()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ import kotlinx.coroutines.flow.map
class HybridViewModelNumberProperty(private val viewModelNumber: ViewModelNumberProperty) :
HybridViewModelNumberPropertySpec(),
BaseHybridViewModelProperty<Double> by BaseHybridViewModelPropertyImpl() {
override fun dispose() {
removeListeners()
super<HybridViewModelNumberPropertySpec>.dispose()
}

override val memorySize: Long
get() = VIEW_MODEL_HYBRID_MEMORY_SIZE

override var value: Double
get() = viewModelNumber.value.toDouble()
set(value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ import com.margelo.nitro.core.Promise
class HybridViewModelStringProperty(private val viewModelString: ViewModelStringProperty) :
HybridViewModelStringPropertySpec(),
BaseHybridViewModelProperty<String> by BaseHybridViewModelPropertyImpl() {
override fun dispose() {
removeListeners()
super<HybridViewModelStringPropertySpec>.dispose()
}

override val memorySize: Long
get() = VIEW_MODEL_HYBRID_MEMORY_SIZE

override var value: String
get() = viewModelString.value
set(value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ import com.facebook.proguard.annotations.DoNotStrip
class HybridViewModelTriggerProperty(private val viewModelTrigger: ViewModelTriggerProperty) :
HybridViewModelTriggerPropertySpec(),
BaseHybridViewModelProperty<ViewModelTriggerProperty.TriggerUnit> by BaseHybridViewModelPropertyImpl() {
override fun dispose() {
removeListeners()
super<HybridViewModelTriggerPropertySpec>.dispose()
}

override val memorySize: Long
get() = VIEW_MODEL_HYBRID_MEMORY_SIZE

override fun trigger() {
viewModelTrigger.trigger()
}
Expand Down
174 changes: 174 additions & 0 deletions example/src/reproducers/AndroidGlobalRefOverflow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/* eslint-disable @typescript-eslint/no-deprecated -- the repro needs the sync API */
import {
View,
Text,
StyleSheet,
ActivityIndicator,
Button,
} from 'react-native';
import { useCallback, useEffect, useRef, useState } from 'react';
import {
Fit,
RiveView,
useRiveFile,
useViewModelInstance,
type ViewModelInstance,
type RiveFile,
} from '@rive-app/react-native';
import { type Metadata } from '../shared/metadata';

/**
* Manual reproducer for the Android JNI global-reference overflow.
*
* Every view-model property/instance accessor returns a fresh HybridObject that pins one
* JNI global reference. On Android the global reference table is capped at 51200; without
* the `memorySize` hint Hermes can't see how heavy these tiny JS objects are, so it never
* collects them under churn and the process aborts with
* `JNI ERROR (app bug): global reference table overflow`.
*
* Toggle the stress: on an unfixed build the app dies within seconds; on a fixed build the
* op counter climbs indefinitely. (No-op on iOS — there is no JNI global reference table.)
*/
const CONCURRENCY = 64;

function StressRunner({
instance,
file,
}: {
instance: ViewModelInstance;
file: RiveFile;
}) {
const [running, setRunning] = useState(false);
const [ops, setOps] = useState(0);
const [errors, setErrors] = useState(0);

const runningRef = useRef(false);
const opsRef = useRef(0);
const errRef = useRef(0);

const tick = useCallback(async () => {
const asyncWork: Promise<unknown>[] = [];
for (let i = 0; i < CONCURRENCY; i++) {
const path = i % 2 === 0 ? 'vm1' : 'vm2';
asyncWork.push(
instance
.viewModelAsync(path)
.then((nested) => nested?.stringProperty('name')?.getValueAsync())
.catch(() => {
errRef.current += 1;
})
);
}

for (let i = 0; i < CONCURRENCY; i++) {
try {
const namePath = i % 2 === 0 ? 'vm1/name' : 'vm2/name';
const prop = instance.stringProperty(namePath);
prop?.set(`${prop?.value ?? ''}`.slice(0, 0) + 'x');
instance.viewModel(i % 2 === 0 ? 'vm1' : 'vm2');
} catch {
errRef.current += 1;
}
}

await Promise.all(asyncWork);
opsRef.current += CONCURRENCY * 2;
}, [instance]);

const loop = useCallback(async () => {
while (runningRef.current) {
await tick();
setOps(opsRef.current);
setErrors(errRef.current);
await new Promise((r) => setTimeout(r, 0));
}
}, [tick]);
Comment thread
mfazekas marked this conversation as resolved.

useEffect(() => {
return () => {
runningRef.current = false;
};
}, []);

const onToggle = () => {
if (runningRef.current) {
runningRef.current = false;
setRunning(false);
} else {
runningRef.current = true;
setRunning(true);
loop();
}
};

return (
<View style={styles.content}>
<RiveView
style={styles.rive}
autoPlay={true}
dataBind={instance}
fit={Fit.Contain}
file={file}
/>
<View style={styles.panel}>
<Text style={styles.title}>Android JNI global-ref overflow</Text>
<Text style={styles.stat}>property accessors: {ops}</Text>
<Text style={styles.stat}>errors: {errors}</Text>
<Button
title={running ? 'Stop stress' : 'Start stress'}
onPress={onToggle}
/>
</View>
</View>
);
}

function WithViewModelSetup({ file }: { file: RiveFile }) {
const { instance, error } = useViewModelInstance(file);

if (error) {
return <Text style={styles.errorText}>{error.message}</Text>;
}
if (!instance) {
return <ActivityIndicator size="large" color="#0000ff" />;
}
return <StressRunner instance={instance} file={file} />;
}

export default function AndroidGlobalRefOverflow() {
const { riveFile, isLoading, error } = useRiveFile(
require('../../assets/rive/viewmodelproperty.riv')
);

return (
<View style={styles.container}>
{isLoading ? (
<ActivityIndicator size="large" color="#0000ff" />
) : riveFile ? (
<WithViewModelSetup file={riveFile} />
) : (
<Text style={styles.errorText}>
{error?.message || 'Unexpected error'}
</Text>
)}
</View>
);
}

AndroidGlobalRefOverflow.metadata = {
name: 'Android JNI Global-Ref Overflow',
description:
'Churns view-model property/instance accessors to exhaust the Android JNI global ' +
'reference table (cap 51200). Crashes within seconds without the memorySize fix; ' +
'runs indefinitely with it.',
} satisfies Metadata;

const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#fff' },
content: { flex: 1 },
rive: { flex: 1, width: '100%' },
panel: { padding: 16, backgroundColor: '#fff', gap: 8 },
title: { fontSize: 16, fontWeight: 'bold' },
stat: { fontSize: 14, fontFamily: 'monospace', color: '#333' },
errorText: { color: 'red', textAlign: 'center', padding: 20 },
});
Loading