Skip to content
Draft
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
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,45 @@ jobs:
echo "=== System log for Metro/Node ==="
xcrun simctl spawn booted log show --predicate 'process CONTAINS "node" OR process CONTAINS "metro"' --last 5m --style compact 2>&1 | tail -50 || true

- name: Debug - Collect iOS crash reports
if: failure() || cancelled()
run: |
set +e
UDID=$(xcrun simctl list devices | awk -F '[()]' '/Booted/{print $2; exit}')
echo "Booted simulator UDID: ${UDID:-<none>}"
mkdir -p crash-reports
# Simulator app crashes are written to the host's DiagnosticReports by
# ReportCrash, and sometimes inside the simulator's own data dir. Grab
# RiveExample-named reports plus anything from the last 25 min as a fallback.
for dir in \
"$HOME/Library/Logs/DiagnosticReports" \
"$HOME/Library/Developer/CoreSimulator/Devices/$UDID/data/Library/Logs/DiagnosticReports"; do
[ -d "$dir" ] || continue
find "$dir" -type f \
\( -iname 'RiveExample-*' -o \( \( -iname '*.ips' -o -iname '*.crash' \) -mmin -25 \) \) \
-exec cp {} crash-reports/ \; 2>/dev/null
done
count=$(ls -1 crash-reports 2>/dev/null | wc -l | tr -d ' ')
echo "=== Collected ${count} crash report(s) ==="
for f in crash-reports/*; do
[ -e "$f" ] || continue
echo "---------------- ${f} ----------------"
cat "$f"
echo
done
if [ "${count}" = "0" ]; then
echo "No crash reports found (app may not have crashed, or reports were not flushed yet)."
fi
exit 0

- name: Upload iOS crash reports
if: failure() || cancelled()
uses: actions/upload-artifact@v4
with:
name: ios-harness-crash-reports
path: crash-reports/
if-no-files-found: ignore

test-harness-android:
runs-on: ubuntu-latest
timeout-minutes: 30
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
diff --git a/dist/bundler/bundle.js b/dist/bundler/bundle.js
index abc86b64d38265d8c605925ed71f6138f3b48e37..9e42cc97b2624e14992e705b241d458ca556259b 100644
--- a/dist/bundler/bundle.js
+++ b/dist/bundler/bundle.js
@@ -19,3 +19,18 @@ export const fetchModule = async (fileName) => {
}
return text;
};
+/**
+ * Ask Metro to drop this entry's dependency graph. Metro caches one graph per
+ * distinct `.bundle` entry (for delta updates), so fetching each test file as
+ * its own entry would otherwise retain one graph per file for the whole run and
+ * OOM the runner. Metro frees a graph on a DELETE to its bundle URL, so we reuse
+ * the exact fetch URL to match the graph id.
+ */
+export const releaseModule = async (fileName) => {
+ try {
+ await fetch(getModuleUrl(fileName), { method: 'DELETE' });
+ }
+ catch {
+ // Best-effort: a failed release only costs memory, never correctness.
+ }
+};
diff --git a/dist/bundler/factory.js b/dist/bundler/factory.js
index 49bf4d1d47a3ee17b51f876405dde860ebbdfc4e..086d040f6bd38895743a11a3fb63deaffe2ac08b 100644
--- a/dist/bundler/factory.js
+++ b/dist/bundler/factory.js
@@ -1,5 +1,5 @@
import { getEmitter } from '../utils/emitter.js';
-import { fetchModule } from './bundle.js';
+import { fetchModule, releaseModule } from './bundle.js';
import { BundlingFailedError } from './errors.js';
export const getBundler = () => {
const events = getEmitter();
@@ -32,5 +32,6 @@ export const getBundler = () => {
throw error;
}
},
+ releaseModule: (filePath) => releaseModule(filePath),
};
};
diff --git a/dist/client/factory.js b/dist/client/factory.js
index d5e29eaf166efa7bb68ece1faf21f9bc501b43ff..f7ee7d60afe742590d938641a1a72fd4f026a0bb 100644
--- a/dist/client/factory.js
+++ b/dist/client/factory.js
@@ -57,6 +57,8 @@ export const getClient = async () => {
});
}
finally {
+ // Free this file's Metro graph so graphs don't accumulate per file (OOM).
+ await bundler?.releaseModule(path);
collector?.dispose();
runner?.dispose();
events?.clearAllListeners();
diff --git a/src/bundler/bundle.ts b/src/bundler/bundle.ts
index 4adf52166f6c0369f4dab39838378251dbc76c38..68b8a3f6daf231f5345e729d35bc62e62b614837 100644
--- a/src/bundler/bundle.ts
+++ b/src/bundler/bundle.ts
@@ -24,3 +24,18 @@ export const fetchModule = async (fileName: string): Promise<string> => {

return text;
};
+
+/**
+ * Ask Metro to drop this entry's dependency graph. Metro caches one graph per
+ * distinct `.bundle` entry (for delta updates), so fetching each test file as
+ * its own entry would otherwise retain one graph per file for the whole run and
+ * OOM the runner. Metro frees a graph on a DELETE to its bundle URL, so we reuse
+ * the exact fetch URL to match the graph id.
+ */
+export const releaseModule = async (fileName: string): Promise<void> => {
+ try {
+ await fetch(getModuleUrl(fileName), { method: 'DELETE' });
+ } catch {
+ // Best-effort: a failed release only costs memory, never correctness.
+ }
+};
diff --git a/src/bundler/factory.ts b/src/bundler/factory.ts
index e793159849851e6e7c864bd41dade3422fa5db05..f3b4100c9c98f0bd5495150c04b081bf9bdf5727 100644
--- a/src/bundler/factory.ts
+++ b/src/bundler/factory.ts
@@ -1,7 +1,7 @@
import { BundlerEvents } from '@react-native-harness/bridge';
import { getEmitter } from '../utils/emitter.js';
import { Bundler } from './types.js';
-import { fetchModule } from './bundle.js';
+import { fetchModule, releaseModule } from './bundle.js';
import { BundlingFailedError } from './errors.js';

export const getBundler = (): Bundler => {
@@ -39,5 +39,6 @@ export const getBundler = (): Bundler => {
throw error;
}
},
+ releaseModule: (filePath) => releaseModule(filePath),
};
};
diff --git a/src/bundler/types.ts b/src/bundler/types.ts
index ca03fb1d1ff0168599ac612a18359fd7615552e5..9c5e0126e6937373d58096043668e4c9b4e89536 100644
--- a/src/bundler/types.ts
+++ b/src/bundler/types.ts
@@ -4,4 +4,5 @@ import { EventEmitter } from '../utils/emitter.js';
export type Bundler = {
events: EventEmitter<BundlerEvents>;
getModule: (filePath: string) => Promise<string>;
+ releaseModule: (filePath: string) => Promise<void>;
};
diff --git a/src/client/factory.ts b/src/client/factory.ts
index 14a05830b94397b9868d2f3e6c2c880a37a901ec..b17e47f4e4d559d2faf98c4a5834d703363be6cd 100644
--- a/src/client/factory.ts
+++ b/src/client/factory.ts
@@ -81,6 +81,8 @@ export const getClient = async (): Promise<HarnessHandle> => {
runner: options.runner,
});
} finally {
+ // Free this file's Metro graph so graphs don't accumulate per file (OOM).
+ await bundler?.releaseModule(path);
collector?.dispose();
runner?.dispose();
events?.clearAllListeners();
14 changes: 11 additions & 3 deletions example/__tests__/autoplay.harness.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -397,9 +397,17 @@ describe('Auto dataBind with no default ViewModel (issue #189)', () => {

expect(context.error).toBeNull();

const vmi = context.ref!.getViewModelInstance();
expect(vmi).not.toBeNull();
expect(vmi!.numberProperty('ypos')).toBeDefined();
// The default ViewModel instance and its properties resolve asynchronously a
// short time after the view ref is assigned, so poll until the property is
// available rather than reading it the instant the ref exists.
await waitFor(
() => {
expect(
context.ref!.getViewModelInstance()?.numberProperty('ypos')
).toBeDefined();
},
{ timeout: 5000 }
);

cleanup();
});
Expand Down
32 changes: 22 additions & 10 deletions example/__tests__/reconfigure.harness.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
Fit,
type RiveFile,
type RiveViewRef,
type ViewModelNumberProperty,
} from '@rive-app/react-native';

const BOUNCING_BALL = require('../assets/rive/bouncing_ball.riv');
Expand Down Expand Up @@ -74,11 +75,17 @@ describe('RiveView reconfigure (file switch)', () => {
await render(<Wrapper />);
await waitFor(() => expect(context.ref).not.toBeNull(), { timeout: 5000 });

// Confirm bouncing_ball is animating via its ypos ViewModel property
const vmi1 = context.ref!.getViewModelInstance();
expect(vmi1).not.toBeNull();
const ypos1 = vmi1!.numberProperty('ypos');
expect(ypos1).toBeDefined();
// The ViewModel instance and its properties resolve asynchronously a short
// time after the view ref is assigned, so poll until the property is
// available rather than reading it the instant the ref exists.
let ypos1: ViewModelNumberProperty | undefined;
await waitFor(
() => {
ypos1 = context.ref!.getViewModelInstance()?.numberProperty('ypos');
expect(ypos1).toBeDefined();
},
{ timeout: 5000 }
);
const valueBefore = ypos1!.value;
await delay(500);
expect(ypos1!.value).not.toBe(valueBefore);
Expand All @@ -93,11 +100,16 @@ describe('RiveView reconfigure (file switch)', () => {
await delay(600);
expect(context.error).toBeNull();

// Animation should still be running on the reconfigured view
const vmi2 = context.ref!.getViewModelInstance();
expect(vmi2).not.toBeNull();
const ypos2 = vmi2!.numberProperty('ypos');
expect(ypos2).toBeDefined();
// Animation should still be running on the reconfigured view. The
// reconfigured instance also resolves asynchronously, so poll for it too.
let ypos2: ViewModelNumberProperty | undefined;
await waitFor(
() => {
ypos2 = context.ref!.getViewModelInstance()?.numberProperty('ypos');
expect(ypos2).toBeDefined();
},
{ timeout: 5000 }
);
const valueAfterSwitch = ypos2!.value;
await delay(500);
expect(ypos2!.value).not.toBe(valueAfterSwitch);
Expand Down
4 changes: 2 additions & 2 deletions example/src/demos/DataBindingArtboardsExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
Fit,
RiveView,
useRiveFile,
useViewModelInstance,
useViewModelInstanceAsync,
type RiveFile,
type BindableArtboard,
} from '@rive-app/react-native';
Expand Down Expand Up @@ -78,7 +78,7 @@
mainFile: RiveFile;
assetsFile: RiveFile;
}) {
const { instance, error } = useViewModelInstance(mainFile);
const { instance, error } = useViewModelInstanceAsync(mainFile);
const [currentArtboard, setCurrentArtboard] = useState<string>('Dragon');
const initializedRef = useRef(false);

Expand All @@ -100,7 +100,7 @@

if (error) {
console.error(error.message);
return <Text style={{ color: 'red' }}>{error.message}</Text>;

Check warning on line 103 in example/src/demos/DataBindingArtboardsExample.tsx

View workflow job for this annotation

GitHub Actions / lint

Inline style: { color: 'red' }
}

// Map display names to actual artboard names
Expand Down
4 changes: 2 additions & 2 deletions example/src/demos/QuickStart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
useRiveFile,
useRiveNumber,
useRiveTrigger,
useViewModelInstance,
useViewModelInstanceAsync,
Fit,
} from '@rive-app/react-native';
import type { Metadata } from '../shared/metadata';
Expand All @@ -23,7 +23,7 @@ export default function QuickStart() {
require('../../assets/rive/quick_start.riv')
);
const { riveViewRef, setHybridRef } = useRive();
const { instance: viewModelInstance } = useViewModelInstance(riveFile, {
const { instance: viewModelInstance } = useViewModelInstanceAsync(riveFile, {
onInit: (vmi) => vmi.numberProperty('health')!.set(9),
});

Expand Down
4 changes: 2 additions & 2 deletions example/src/exercisers/FontFallbackExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
useRive,
useRiveFile,
useRiveString,
useViewModelInstance,
useViewModelInstanceAsync,
type FontSource,
type FallbackFont,
} from '@rive-app/react-native';
Expand Down Expand Up @@ -258,7 +258,7 @@ function MountedView({ text }: { text: string }) {
// https://rive.app/marketplace/26480-49641-simple-test-text-property/
require('../../assets/rive/font_fallback.riv')
);
const { instance } = useViewModelInstance(riveFile);
const { instance } = useViewModelInstanceAsync(riveFile);

const { setValue: setRiveText, error: textError } = useRiveString(
TEXT_PROPERTY,
Expand Down
4 changes: 2 additions & 2 deletions example/src/exercisers/MenuListExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
type RiveFile,
useRiveFile,
useRiveList,
useViewModelInstance,
useViewModelInstanceAsync,
} from '@rive-app/react-native';
import { type Metadata } from '../shared/metadata';

Expand All @@ -41,11 +41,11 @@
}

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

if (error) {
console.error(error.message);
return <Text style={{ color: 'red' }}>{error.message}</Text>;

Check warning on line 48 in example/src/exercisers/MenuListExample.tsx

View workflow job for this annotation

GitHub Actions / lint

Inline style: { color: 'red' }
}

if (!instance) {
Expand Down
4 changes: 2 additions & 2 deletions example/src/exercisers/NestedViewModelExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
RiveView,
useRiveFile,
useRiveString,
useViewModelInstance,
useViewModelInstanceAsync,
type ViewModelInstance,
type RiveFile,
type RiveViewRef,
Expand Down Expand Up @@ -41,11 +41,11 @@
}

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

if (error) {
console.error(error.message);
return <Text style={{ color: 'red' }}>{error.message}</Text>;

Check warning on line 48 in example/src/exercisers/NestedViewModelExample.tsx

View workflow job for this annotation

GitHub Actions / lint

Inline style: { color: 'red' }
}

if (!instance) {
Expand Down
4 changes: 2 additions & 2 deletions example/src/exercisers/RiveDataBindingExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Fit,
RiveView,
useRiveNumber,
useViewModelInstance,
useViewModelInstanceAsync,
type ViewModelInstance,
type RiveFile,
useRiveString,
Expand Down Expand Up @@ -37,11 +37,11 @@
}

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

if (error) {
console.error(error.message);
return <Text style={{ color: 'red' }}>{error.message}</Text>;

Check warning on line 44 in example/src/exercisers/RiveDataBindingExample.tsx

View workflow job for this annotation

GitHub Actions / lint

Inline style: { color: 'red' }
}

if (!instance) {
Expand Down
4 changes: 2 additions & 2 deletions example/src/reproducers/Issue297ThreadRace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
Fit,
RiveView,
useRiveFile,
useViewModelInstance,
useViewModelInstanceAsync,
type ViewModelInstance,
type RiveFile,
} from '@rive-app/react-native';
Expand Down Expand Up @@ -130,7 +130,7 @@ function StressRunner({
}

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

if (error) {
return <Text style={styles.errorText}>{error.message}</Text>;
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@
},
"resolutions": {
"core-js-compat": "^3.40.0",
"browserslist": "^4.24.4"
"browserslist": "^4.24.4",
"@react-native-harness/runtime@1.4.0-rc.1": "patch:@react-native-harness/runtime@npm%3A1.4.0-rc.1#./.yarn/patches/@react-native-harness-runtime-npm-1.4.0-rc.1-5f3d78d6f0.patch"
}
}
Loading
Loading