diff --git a/apps/example/src/Examples/API/List.tsx b/apps/example/src/Examples/API/List.tsx index 7b4200dc4f..405660d604 100644 --- a/apps/example/src/Examples/API/List.tsx +++ b/apps/example/src/Examples/API/List.tsx @@ -134,6 +134,10 @@ export const examples = [ screen: "StressTest4", title: "🔥 Stress Test 4", }, + { + screen: "PictureViewCrashTest", + title: "💥 PictureView Race Condition", + }, { screen: "FirstFrame", title: "🎬 First Frame", diff --git a/apps/example/src/Examples/API/PictureViewCrashTest.tsx b/apps/example/src/Examples/API/PictureViewCrashTest.tsx new file mode 100644 index 0000000000..737f0d46ef --- /dev/null +++ b/apps/example/src/Examples/API/PictureViewCrashTest.tsx @@ -0,0 +1,177 @@ +import React, { useState, useRef, useEffect } from "react"; +import { Button, ScrollView, StyleSheet, Text, View } from "react-native"; +import { Canvas, Fill } from "@shopify/react-native-skia"; +import { + useSharedValue, + withRepeat, + withTiming, + useDerivedValue, +} from "react-native-reanimated"; + +/** + * This stress test attempts to reproduce a race condition crash in RNSkPictureView. + * + * The crash occurs when: + * 1. A Canvas with animations is rapidly mounted/unmounted + * 2. The render thread may still be drawing while the view is being torn down + * 3. _picture could become invalid between the null check and drawPicture() call + * + * The crash manifests as: + * null pointer dereference: SIGSEGV + * at SkRecord::Record::visit (SkRecordDraw) + * + * This test rapidly mounts/unmounts animated Canvas components to maximize + * the chance of hitting this race condition. + */ + +const AnimatedCanvas = () => { + const progress = useSharedValue(0); + + useEffect(() => { + progress.value = withRepeat(withTiming(1, { duration: 16 }), -1, true); + }, [progress]); + + const color = useDerivedValue(() => { + const r = Math.floor(progress.value * 255); + const g = Math.floor((1 - progress.value) * 255); + const b = Math.floor(Math.abs(0.5 - progress.value) * 2 * 255); + return `rgb(${r}, ${g}, ${b})`; + }); + + return ( + + + + ); +}; + +export const PictureViewCrashTest = () => { + const [isRunning, setIsRunning] = useState(false); + const [iterations, setIterations] = useState(0); + const [canvasCount, setCanvasCount] = useState(0); + const intervalRef = useRef | null>(null); + const keyRef = useRef(0); + + useEffect(() => { + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, []); + + const startTest = () => { + if (isRunning) return; + setIsRunning(true); + setIterations(0); + keyRef.current = 0; + + // Rapidly mount/unmount animated Canvas components + // This creates a race between: + // - The animation mapper setting pictures on the UI thread + // - The view unregistering and potentially clearing state + // - The render thread drawing the picture + intervalRef.current = setInterval(() => { + keyRef.current += 1; + // Toggle between 0 and 5 canvases to force mount/unmount cycles + setCanvasCount((prev) => (prev > 0 ? 0 : 5)); + setIterations((prev) => prev + 1); + }, 50); // 50ms gives enough time for animations to start before unmounting + }; + + const stopTest = () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + setIsRunning(false); + setCanvasCount(0); + }; + + return ( + + + PictureView Race Condition Test + + This test rapidly mounts and unmounts animated Canvas components to + reproduce the race condition crash where the render thread tries to + draw a picture that has been cleared during unmount. + + + Mount/Unmount cycles: {iterations} + + Active canvases: {canvasCount} + {isRunning && ( + + Running... If the app crashes, the race condition was triggered. + + )} + + + + {Array.from({ length: canvasCount }).map((_, index) => ( + + ))} + + + +