From 3a70b17745cd9de5d633a9d16647d77dbcc4be88 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Wed, 15 Jul 2026 21:32:40 +0200 Subject: [PATCH 1/2] :wrench: --- apps/example/ios/Podfile.lock | 140 +++++------ apps/example/src/Examples/API/List.tsx | 4 + .../Examples/API/PictureRaceStressTest.tsx | 224 ++++++++++++++++++ apps/example/src/Examples/API/Routes.ts | 1 + apps/example/src/Examples/API/index.tsx | 8 + apps/example/src/Examples/API/linking.ts | 1 + 6 files changed, 308 insertions(+), 70 deletions(-) create mode 100644 apps/example/src/Examples/API/PictureRaceStressTest.tsx diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index dba6f8cd75..91b6a07c4e 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -3208,88 +3208,88 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 11b010917f5f15150b2c015abddef1573d2bb05d - RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 + RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f RCTDeprecation: a41bbdd9af30bf2e5715796b313e44ec43eefff1 RCTRequired: 7be34aabb0b77c3cefe644528df0fa0afad4e4d0 RCTSwiftUI: a6c7271c39098bf00dbdad8f8ed997a59bbfbe44 - RCTSwiftUIWrapper: ff9098ccf7727e58218f2f8ea110349863f43438 + RCTSwiftUIWrapper: 5ec163e8fde163d3fba714a992b50a266e1ece37 RCTTypeSafety: 27927d0ca04e419ed9467578b3e6297e37210b5c React: 4bc1f928568ad4bcfd147260f907b4ea5873a03b React-callinvoker: 87f8728235a0dc62e9dc19b3851c829d9347d015 - React-Core: 76bed73b02821e5630e7f2cb2e82432ee964695d - React-CoreModules: 752dbfdaeb096658aa0adc4a03ba6214815a08df - React-cxxreact: b6798528aa601c6db66e6adc7e2da2b059c8be74 + React-Core: 19e0183e28d7a6613ecacebd7525fe6650efa3b6 + React-CoreModules: 73cc86f2a0ff84b93d6325073ad2e4874d21ad40 + React-cxxreact: 4bf734645c77c9b86e2f3e933e0411cf2f14d1ba React-debug: 8978deb306f6f38c28b5091e52b0ac9f942b157e - React-defaultsnativemodule: 682b77ef4acfb298017be15f4f93c1d998deb174 - React-domnativemodule: 4c4b44f7eb68dbc3a2218db088bef318a7302017 - React-Fabric: b6f82a4d8498ce4475586f71ca8397a771fe292d - React-FabricComponents: c8695f4b11918a127c4560d66f7d3fdb01a17986 - React-FabricImage: d64f48830f63830e8ffaaf69fa487116856fbbf1 - React-featureflags: 2a46b229903e906d33dbaf9207ce57c59306c369 - React-featureflagsnativemodule: cba6c0814051a0934f8bcee4a436ee2a6bcc9754 - React-graphics: 3d0435051e1ab8904d065f8ffbe981a9fc202841 - React-hermes: 32fc9c231c1aa5c2fcfe851b0d19ee9269f88f4c - React-idlecallbacksnativemodule: f8ee42581795c4844d97147596bcc2d824c0f188 - React-ImageManager: e8f7377ef0585fd2df05559a17e01a03e187d5cf - React-intersectionobservernativemodule: b1bea12ca29accdd2eda60c87605a6030b894eb9 - React-jserrorhandler: 1a86df895b4eaf4e771abe8cf34cbb26d821f771 - React-jsi: adf8527fec197ad9d0480cc5b8945eb56de627f0 - React-jsiexecutor: 315fa2f879b43e3a9ca08f5f4b733472f7e4e8a4 - React-jsinspector: b4fd1933666bcb2549b566b40656c1e45e9d7632 - React-jsinspectorcdp: 80141710f2668e5b8f00417298d9b76b4abf90fa - React-jsinspectornetwork: 1d3ea717dbbec316cd8c21a0af53928a7bf74901 - React-jsinspectortracing: 4ce745374d4b2bfbd164cce9f8de8383d3d818a0 - React-jsitooling: fc4ac4c3b1f3f9f7fedf0c777c6ff3f244f568bd - React-jsitracing: bff08a6faeef4a9bd286487da191f5e5329e21a9 - React-logger: b8483fa08e0d62e430c76d864309d90576ca2f68 - React-Mapbuffer: 7b72a669e94662359dad4f42b5af005eb24b4e83 - React-microtasksnativemodule: cdc02da075f2857803ed63f24f5f72fc40e094c0 - react-native-safe-area-context: c00143b4823773bba23f2f19f85663ae89ceb460 - react-native-skia: 82260e6ce556ff33eb29360905eb775383d01a8c - React-NativeModulesApple: a2c3d2cbec893956a5b3e4060322db2984fff75b - React-networking: 3f98bd96893a294376e7e03730947a08d474c380 + React-defaultsnativemodule: 724eb9ec388d494f1e2057d83355ee8fe6f1d780 + React-domnativemodule: 9068f41092f725acd09950233d2847364c731947 + React-Fabric: 945cc8abf08d9d0966acef605bffce7b501c49d9 + React-FabricComponents: 4c4ad6f0d16c964a68f945e029505e2eeec6654b + React-FabricImage: a8b628fd98db21b9f8588e06f14a9194dda11b40 + React-featureflags: 0937601c1af1cc125851ec5bbf4654285d47a3e7 + React-featureflagsnativemodule: ac1a3e0353e1a6e15411b17ed6c7122adb0468a4 + React-graphics: cca521e06463608be46207a4aa160f8a7f725f8b + React-hermes: ec50b9fcea2c3bfdd42f8cec845eac3f35888572 + React-idlecallbacksnativemodule: effcae5b7b4473211adb154aaa321d5d9e2fbcc9 + React-ImageManager: b38459e538f1840fa5c3e7612a4bcb0029a3c366 + React-intersectionobservernativemodule: 8d33366661971200cf2e151727f6fe007b62ae7b + React-jserrorhandler: f94c688a0dbe2e045b91b992722b92e97d56f77f + React-jsi: 3216c876cd4c571a57909e22d77c8fd9530aa067 + React-jsiexecutor: 475563c0042841a85930a455d3199f6b1483a5fe + React-jsinspector: bc484fb32bf1b9fed80afe8793e614eba4f7b39e + React-jsinspectorcdp: 5a574d1d35016968a67e78e6b8a7917473ffbb77 + React-jsinspectornetwork: dce3a5a1351b527ee8c28ad4a8bdd211507e1a45 + React-jsinspectortracing: 65f6b166bd67e5adc31eba027e1570bacf7a3cc7 + React-jsitooling: d5463f5489a31640b0fa0ec4e31566ca8aa86c13 + React-jsitracing: 3c7fc18821aba64855acb8658aa857ca6a7fddf6 + React-logger: 6ac901f5c7f7321d2be1a40b203bccc2e23411e3 + React-Mapbuffer: 2e0e7cc5b7064eaed9c8b8afc3a87621cb7ef5cd + React-microtasksnativemodule: dd4d33b251b57e5027c572c6d0b45cbfbcfaa386 + react-native-safe-area-context: 54d812805f3c4e08a4580ad086cbde1d8780c2e4 + react-native-skia: b28747dc1cf8c344ad041f14a618eda7f429bfeb + React-NativeModulesApple: 7f2f2fed3f6c858889eb61d09941be965d52df58 + React-networking: 43e5e6773ac2ca2a93261a1388fed269c9fce092 React-oscompat: 80166b66da22e7af7fad94474e9997bd52d4c8c6 - React-perflogger: d6797918d2b1031e91a9d8f5e7fdd2c8728fb390 - React-performancecdpmetrics: 5570be61e2f97c4741c5d432c91570e8e5a39892 - React-performancetimeline: 5763499ae1991fc18dcf416e340ce7bc829bb298 + React-perflogger: 63c90e0d8c24df87ffa14dad01aeafc352847dd0 + React-performancecdpmetrics: 5a9b81c08f75045635127d626440d9ada01e774b + React-performancetimeline: 31cebfff69ec9174b3fb54b0606fcb12ef91cbad React-RCTActionSheet: 3bd5f5db9f983cf38d51bb9a7a198e2ebea94821 - React-RCTAnimation: 46a9978f27dc434dbeed16afa7b82619b690a9af - React-RCTAppDelegate: 62ecd60a2b2a8cae26ce6a066bfa59cfde97af01 - React-RCTBlob: 8285c859513023ee3cc8c806d9b59d4da078c4ba - React-RCTFabric: 05ed09347e938de985052f791a6a0698816d5761 - React-RCTFBReactNativeSpec: 83ba579fca9a51e774ac32578ef5dd3262edd7e2 - React-RCTImage: a5364d0f098692cfbf5bef1e8a63e7712ecb14b7 - React-RCTLinking: 34b63b0aa0e92d30f5d7aa2c255a8f95fa75ee8f - React-RCTNetwork: 1ef88b7a5310b8f915d3556b5b247def113191ed - React-RCTRuntime: ed29cf68a46782fec891e5afe1d8d758ca6ccd9b - React-RCTSettings: 2c45623d6c0f30851a123f621eb9d32298bcbb0c - React-RCTText: 0ee70f5dc18004b4d81b2c214267c6cbec058587 - React-RCTVibration: 88557e21e7cc3fe76b5b174cba28ff45c6def997 + React-RCTAnimation: 346865a809fa5132f6c594c8b376c6cf46b44e88 + React-RCTAppDelegate: b2d1e0d3663c987f49f45094883b9e36fcbf0181 + React-RCTBlob: 74759ebb7ff9077d19f60c301782c1f8c3eb2813 + React-RCTFabric: 7b4b14dad21ca99333ebcbc0bf5c205647a315a8 + React-RCTFBReactNativeSpec: 39151968adb68b8c59f29a8bd4223d4d7780a793 + React-RCTImage: 60763f56e8a5e45d861d7c4777e428bb820ec52a + React-RCTLinking: 52aee78b0b3163167c7fcf58f80a42943c03a056 + React-RCTNetwork: f5e1e8ae5eff6982efff6289b06ec0a76d0a6ac2 + React-RCTRuntime: 0e99199322afd372e74b95ae5c58f4e074cc2855 + React-RCTSettings: 298bb40d3412bf32e0b4f0797e48416b0b7278a1 + React-RCTText: dfb74800e27d792d1188fa975a3b9807c3362e3e + React-RCTVibration: ffe5fd4f50a835e353a3b6869eb005dab11eea44 React-rendererconsistency: d280314a3e7f0097152f89e815b4de821c2be8b9 - React-renderercss: f8cbf83d95c2c2bbf893d37fe50c73f046584411 - React-rendererdebug: 37216ddfcd38e49d1e92bf9052ea4bc9d7b932e5 - React-RuntimeApple: 1c0e7cb8e1c2c5775585afcaaa666ec151629a8d - React-RuntimeCore: 925fe2ca24cf8e6ed87586dbb92827306b93b83f - React-runtimeexecutor: 962dae024f6df760d029512a7d99e3f73d570935 - React-RuntimeHermes: 19a7c59ec1bc9908516f0bbc29b49425f6ec64ba - React-runtimescheduler: 62f21127cd97f4d8f164eee5150d3ce53dd36f66 - React-timing: 8757bf6fb96227c264f2d1609f4ba5c68217b8ce - React-utils: 8ab26781c2f5c2f7fafb2022c8ab39d39f231b80 - React-webperformancenativemodule: 7953b7fe519f76fa595111fe18ff3d5de131bfe9 - ReactAppDependencyProvider: 0eb286cc274abb059ee601b862ebddac2e681d01 - ReactCodegen: b8e56b780fffe6edd6405be0af4a1e3049a937f7 - ReactCommon: ac934cb340aee91282ecd6f273a26d24d4c55cae - ReactNativeHost: eef98ec49b55d88ad4cabf5a4378a12b42b551ee - ReactTestApp-DevSupport: ea18f446cff64b6c9a3e28788600c82ecf51bde6 + React-renderercss: 8a1a346f3665fd5ea7a7be7b3b9f95d4743e1180 + React-rendererdebug: af74afdfb3d6c5382ebab35562efd8eb9e690473 + React-RuntimeApple: 06e33d291e72fd0c73ac47046c3536d77d5aeedd + React-RuntimeCore: 99273d2af072062eb07f0b2d2d4a0f2de697ea14 + React-runtimeexecutor: 2063c03c18810ee57939d138142e6493333360ef + React-RuntimeHermes: 2253a7f4c8d56b449230b330b0b15383ed4b3df4 + React-runtimescheduler: ff37ac6720a943da91645c06274282ac46b71f23 + React-timing: 831d7e081ba4c332ca5cccf389b88e363f13f2b4 + React-utils: 25db6c17598c4fed22b5956d7551bb8bddf1f95b + React-webperformancenativemodule: 57e41e6193cfb815bde0b5534bef68673f1270eb + ReactAppDependencyProvider: bfb12ead469222b022a2024f32aba47ce50de512 + ReactCodegen: 9ca1bd49eee1eccf6e427e406d2163f49e9c48c0 + ReactCommon: 05ad684db7d88e194272ae26baddf6300e30b8b7 + ReactNativeHost: e7e0a518b0120f0070b3e1f13c7006d3e0e8ee13 + ReactTestApp-DevSupport: 6994b53b5b81139a8ce63e0776c726c95de079a1 ReactTestApp-Resources: 1bd9ff10e4c24f2ad87101a32023721ae923bccf - RNGestureHandler: cd4be101cfa17ea6bbd438710caa02e286a84381 - RNReanimated: c26dfcd831add485c2ed93de9d7bfb90b035eeaa - RNScreens: 714e10b6b554f7dc7ad9f78dcf36dc8e3fc73415 - RNSVG: 11354d28dd6cb71a59570b68c91ba6772a2d781d - RNWorklets: b89b501d37972e6419d6f87effe41d6d76157648 + RNGestureHandler: 77eecab5fd636666ca73a55bb61e2f1a685b7e84 + RNReanimated: d1a7a4c20eefc371e062990ce1debeaff4f1b9be + RNScreens: b2a5c76af24a02a2fd71bfce42780fdd9c79cc6d + RNSVG: ea9cbf6dcdbebdfff5822b0ad9311bbc4510a0b7 + RNWorklets: 5f6e5664c1819eac103ca75cc2f36191f55aa110 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - Yoga: 5456bb010373068fc92221140921b09d126b116e + Yoga: 5bd0956bf9cb16f75101e78b5e852c7577bc5a45 PODFILE CHECKSUM: dca89d921c9f2a2d3d405a5fca0bfb60b30b5022 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 diff --git a/apps/example/src/Examples/API/List.tsx b/apps/example/src/Examples/API/List.tsx index 305fa8f203..c1d682b742 100644 --- a/apps/example/src/Examples/API/List.tsx +++ b/apps/example/src/Examples/API/List.tsx @@ -146,6 +146,10 @@ export const examples = [ screen: "PictureViewCrashTest", title: "💥 PictureView Race Condition", }, + { + screen: "PictureRaceStressTest", + title: "💥 Picture Swap Race (#3925)", + }, { screen: "FirstFrame", title: "🎬 First Frame", diff --git a/apps/example/src/Examples/API/PictureRaceStressTest.tsx b/apps/example/src/Examples/API/PictureRaceStressTest.tsx new file mode 100644 index 0000000000..a351009cb1 --- /dev/null +++ b/apps/example/src/Examples/API/PictureRaceStressTest.tsx @@ -0,0 +1,224 @@ +import React, { useEffect, useRef, useState } from "react"; +import { Button, ScrollView, StyleSheet, Text, View } from "react-native"; +import type { SkImage, SkPicture } from "@shopify/react-native-skia"; +import { + createPicture, + FilterMode, + MipmapMode, + Skia, + SkiaPictureView, + TileMode, +} from "@shopify/react-native-skia"; +import { SkiaViewApi } from "@shopify/react-native-skia/src/views/api"; + +/** + * Stress test for https://github.com/Shopify/react-native-skia/issues/3925 + * + * RNSkPictureRenderer::_picture is written from the JS thread (setPicture, + * via setJsiProperty) and read from the main thread (performDraw) without + * synchronization. The sk_sp copy in performDraw reads the raw pointer and + * then increments the refcount in two separate steps, so if the JS thread + * swaps the picture in between — and the renderer held the last reference — + * the main thread refs/draws a destroyed SkPicture: + * + * - EXC_BREAKPOINT (SIGTRAP) in SkRefCntBase::ref() / SkSafeRef + * - EXC_BAD_ACCESS (SIGSEGV) in SkCanvas::drawPicture + * + * To make the renderer the sole owner at swap time (in real apps this is + * done by the Hermes GC finalizing the previous frame's JsiSkPicture), the + * test dispose()s the previous picture right before setting the new one. + * It then swaps pictures on several views as fast as the JS thread allows, + * while every swap also schedules a main-thread redraw — maximizing the + * number of swap-vs-copy encounters. + * + * Running the app with Thread Sanitizer enabled flags the race on the very + * first concurrent swap, without needing an actual crash. + */ + +const NUM_VIEWS = 6; +const SWAPS_PER_TICK = 10; +const SIZE = 96; + +// A shared GPU-backed image referenced by every picture (matches the +// reporter's setup: one decoded image used as a shader by every frame). +const makeSharedImage = (): SkImage | null => { + const surface = Skia.Surface.MakeOffscreen(64, 64); + if (!surface) { + return null; + } + const canvas = surface.getCanvas(); + const paint = Skia.Paint(); + paint.setShader( + Skia.Shader.MakeLinearGradient( + { x: 0, y: 0 }, + { x: 64, y: 64 }, + [Skia.Color("cyan"), Skia.Color("magenta")], + null, + TileMode.Clamp + ) + ); + canvas.drawRect(Skia.XYWHRect(0, 0, 64, 64), paint); + surface.flush(); + return surface.makeImageSnapshot(); +}; + +const buildPicture = (image: SkImage | null, seed: number) => + createPicture( + (canvas) => { + const paint = Skia.Paint(); + if (image) { + paint.setShader( + image.makeShaderOptions( + TileMode.Repeat, + TileMode.Repeat, + FilterMode.Linear, + MipmapMode.None + ) + ); + } else { + paint.setColor(Skia.Color("magenta")); + } + for (let i = 0; i < 20; i++) { + const x = (seed * 13 + i * 37) % SIZE; + const y = (seed * 7 + i * 53) % SIZE; + canvas.drawCircle(x, y, 8 + (i % 12), paint); + } + }, + { width: SIZE, height: SIZE } + ); + +export const PictureRaceStressTest = () => { + const [running, setRunning] = useState(false); + const [displayedSwaps, setDisplayedSwaps] = useState(0); + const viewRefs = useRef<(SkiaPictureView | null)[]>([]); + const swapCount = useRef(0); + + useEffect(() => { + if (!running) { + return; + } + const image = makeSharedImage(); + const previous: (SkPicture | null)[] = new Array(NUM_VIEWS).fill(null); + let alive = true; + let seed = 0; + + const tick = () => { + if (!alive) { + return; + } + for (let v = 0; v < NUM_VIEWS; v++) { + const view = viewRefs.current[v]; + if (!view) { + continue; + } + for (let k = 0; k < SWAPS_PER_TICK; k++) { + const picture = buildPicture(image, seed++); + // Drop the JS-side reference to the picture the renderer currently + // holds, so the swap below performs the destroying unref on the JS + // thread — racing the sk_sp copy in performDraw on the main thread. + previous[v]?.dispose(); + SkiaViewApi.setJsiProperty(view.nativeId, "picture", picture); + previous[v] = picture; + swapCount.current++; + } + } + setImmediate(tick); + }; + tick(); + + const display = setInterval( + () => setDisplayedSwaps(swapCount.current), + 500 + ); + + return () => { + alive = false; + clearInterval(display); + }; + }, [running]); + + return ( + + + Picture Swap Race (issue #3925) + + Replaces the picture of {NUM_VIEWS} picture views as fast as possible + from the JS thread while the main thread redraws them, disposing the + previous picture before each swap. A crash (SIGTRAP in + SkRefCntBase::ref or SIGSEGV in SkCanvas::drawPicture) means the + unsynchronized picture swap in RNSkPictureRenderer was hit. + + Swaps: {displayedSwaps} + {running && ( + + Running... leave it on — the window is small, this can take a while. + Thread Sanitizer catches it immediately. + + )} + + + {Array.from({ length: NUM_VIEWS }).map((_, index) => ( + { + viewRefs.current[index] = ref; + }} + style={styles.smallCanvas} + /> + ))} + + +