Skip to content

Conversation

@mfazekas
Copy link
Collaborator

@mfazekas mfazekas commented Nov 27, 2025

Keep RiveFile alive during async asset loading operations to prevent factory invalidation. The cachedRiveFile reference is retained until the next load or view
deallocation, ensuring the RiveFactory remains valid for background decode operations.

Simplified version of: rive-app/rive-nitro-react-native#45

Reproducer:

import * as React from 'react';
import {
  ActivityIndicator,
  Button,
  StyleSheet,
  Text,
  View,
} from 'react-native';
import Rive, { Fit, RNRiveError } from 'rive-react-native';

const VIEW_DISPOSAL_TIME_MS = 200;
const PAUSE_BETWEEN_TESTS_MS = 200;
const TEST_DELAYS = [200, 201, 202, 203, 204, 205, 206, 207, 208, 209];
const TOTAL_TEST_ROUNDS = 200;

function RiveContent({ delayMs }: { delayMs: number }) {
  const imageUrl = `https://app.requestly.io/delay/${delayMs}/https://picsum.photos/id/374/500/500`;

  React.useEffect(() => {
    console.log(
      `[RACE-TEST] RiveContent mounted at ${Date.now()} with delay ${delayMs}ms`
    );
    return () => {
      console.log(
        `[RACE-TEST] RiveContent unmounting at ${Date.now()} (delay was ${delayMs}ms)`
      );
    };
  }, [delayMs]);

  return (
    <Rive
      autoplay={true}
      fit={Fit.Contain}
      style={styles.rive}
      stateMachineName="State Machine 1"
      artboardName="Artboard"
      resourceName={'out_of_band'}
      referencedAssets={{
        'Inter-594377': {
          source: require('../../assets/fonts/Inter-594377.ttf'),
        },
        'referenced-image-2929282': {
          source: { uri: imageUrl },
        },
        'referenced_audio-2929340': {
          source: require('../../assets/audio/referenced_audio-2929340.wav'),
        },
      }}
      onError={(riveError: RNRiveError) => {
        console.log('[RACE-TEST] Rive error:', riveError);
      }}
    />
  );
}

export default function RaceConditionTest() {
  const [showView, setShowView] = React.useState(false);
  const [testRunning, setTestRunning] = React.useState(false);
  const [currentIteration, setCurrentIteration] = React.useState(0);
  const [currentDelay, setCurrentDelay] = React.useState<number | null>(null);

  const runSingleTest = React.useCallback(
    async (delayMs: number, iteration: number) => {
      console.log(
        `[RACE-TEST] ========== Test ${iteration + 1}/${TOTAL_TEST_ROUNDS} started at ${Date.now()} ==========`
      );
      console.log(
        `[RACE-TEST] Download delay: ${delayMs}ms, View disposal: ${VIEW_DISPOSAL_TIME_MS}ms`
      );

      setCurrentIteration(iteration);
      setCurrentDelay(delayMs);
      setShowView(true);

      await new Promise((resolve) =>
        setTimeout(() => {
          console.log(
            `[RACE-TEST] Disposing view at ${Date.now()} (iteration ${iteration + 1})`
          );
          setShowView(false);
          resolve(undefined);
        }, VIEW_DISPOSAL_TIME_MS)
      );

      await new Promise((resolve) =>
        setTimeout(() => {
          console.log(
            `[RACE-TEST] Test ${iteration + 1} completed at ${Date.now()}`
          );
          console.log(
            `[RACE-TEST] ========== End of test ${iteration + 1}/${TOTAL_TEST_ROUNDS} ==========\n`
          );
          resolve(undefined);
        }, PAUSE_BETWEEN_TESTS_MS)
      );
    },
    []
  );

  const startTest = React.useCallback(async () => {
    console.log(`[RACE-TEST] ====================================`);
    console.log(
      `[RACE-TEST] Starting automated race condition test suite at ${Date.now()}`
    );
    console.log(
      `[RACE-TEST] Running ${TOTAL_TEST_ROUNDS} iterations cycling through delays: ${TEST_DELAYS.join(', ')}ms`
    );
    console.log(`[RACE-TEST] ====================================\n`);

    setTestRunning(true);

    for (let i = 0; i < TOTAL_TEST_ROUNDS; i++) {
      const delayIndex = i % TEST_DELAYS.length;
      const delay = TEST_DELAYS[delayIndex]!;
      await runSingleTest(delay, i);
    }

    setTestRunning(false);
    setCurrentIteration(0);
    setCurrentDelay(null);
    console.log(`[RACE-TEST] ====================================`);
    console.log(`[RACE-TEST] All tests completed at ${Date.now()}`);
    console.log(`[RACE-TEST] ====================================`);
  }, [runSingleTest]);

  const resetTest = React.useCallback(() => {
    setShowView(false);
    setTestRunning(false);
    setCurrentIteration(0);
    setCurrentDelay(null);
    console.log(`[RACE-TEST] Test reset at ${Date.now()}`);
  }, []);

  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.title}>Race Condition Test</Text>
        <Text style={styles.description}>
          Automated test running {TOTAL_TEST_ROUNDS} iterations, cycling through
          delays {TEST_DELAYS[0]}ms - {TEST_DELAYS[TEST_DELAYS.length - 1]}ms.
        </Text>
        <Text style={styles.description}>
          View disposal: {VIEW_DISPOSAL_TIME_MS}ms, Pause between tests:{' '}
          {PAUSE_BETWEEN_TESTS_MS}ms
        </Text>
        {testRunning && currentDelay !== null && (
          <Text style={styles.statusText}>
            Running Test {currentIteration + 1}/{TOTAL_TEST_ROUNDS} - Delay:{' '}
            {currentDelay}ms
          </Text>
        )}
        <Text style={styles.description}>
          Check the logs for [RACE-TEST] messages to see the timing of events.
        </Text>
      </View>

      <View style={styles.viewContainer}>
        {showView && currentDelay !== null && (
          <RiveContent key={currentIteration} delayMs={currentDelay} />
        )}
      </View>

      <View style={styles.controls}>
        <Button
          title={testRunning ? 'Tests Running...' : 'Start Automated Tests'}
          onPress={startTest}
          disabled={testRunning}
        />
        <View style={styles.buttonSpacer} />
        <Button title="Reset" onPress={resetTest} disabled={testRunning} />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
  header: {
    padding: 16,
    backgroundColor: '#f5f5f5',
  },
  title: {
    fontSize: 20,
    fontWeight: 'bold',
    marginBottom: 8,
  },
  description: {
    fontSize: 14,
    color: '#666',
    marginBottom: 8,
  },
  statusText: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#007AFF',
    marginTop: 8,
    marginBottom: 8,
  },
  viewContainer: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  rive: {
    width: '100%',
    height: 400,
  },
  loadingContainer: {
    width: '100%',
    height: 400,
    justifyContent: 'center',
    alignItems: 'center',
  },
  loadingText: {
    marginTop: 16,
    fontSize: 16,
    color: '#666',
  },
  controls: {
    padding: 16,
    gap: 12,
  },
  buttonSpacer: {
    height: 8,
  },
});

Crash:

Thread 34 Queue : com.apple.NSURLSession-delegate (serial)
#0	0x000000010375d29c in rive::gpu::RenderContext::decodeImage ()
#1	0x0000000103556ae4 in -[RiveFactory decodeImage:] at /Users/admin/actions-runner/_work/rive-ios/rive-ios/Source/Renderer/RiveFactory.mm:135
#2	0x000000010355317c in __63-[CDNFileAssetLoader loadContentsWithAsset:andData:andFactory:]_block_invoke at /Users/admin/actions-runner/_work/rive-ios/rive-ios/Source/Renderer/CDNFileAssetLoader.mm:52
#3	0x000000018492dcac in __53-[__NSURLSessionLocal _downloadTaskWithTaskForClass:]_block_invoke ()
#4	0x0000000184949bc0 in __58-[__NSCFLocalDownloadTask _private_completionAfterMetrics]_block_invoke_3 ()
#5	0x0000000102483ec8 in _dispatch_call_block_and_release ()
#6	0x000000010249d798 in _dispatch_client_callout ()
#7	0x000000010248c2a4 in _dispatch_lane_serial_drain ()
#8	0x000000010248cf08 in _dispatch_lane_invoke ()
#9	0x000000010249903c in _dispatch_root_queue_drain_deferred_wlh ()
#10	0x00000001024985b4 in _dispatch_workloop_worker_thread ()
#11	0x0000000102522bcc in _pthread_wqthread ()
Enqueued from com.apple.NSURLSession-work (Thread 44) Queue : com.apple.NSURLSession-work (serial)
#0	0x00000001024893f8 in dispatch_async ()
#1	0x000000018494964c in -[__NSCFLocalDownloadTask _private_completionAfterMetrics] ()
#2	0x00000001849347a0 in -[__NSCFURLSessionDelegateWrapper task:didFinishCollectingMetrics:completionHandler:] ()
#3	0x00000001849346d0 in -[__NSCFURLSessionDelegateWrapper task:didFinishCollectingMetrics:completionHandler:] ()
#4	0x00000001849490e4 in __50-[__NSCFLocalDownloadTask _private_fileCompletion]_block_invoke.50 ()
#5	0x0000000184b4bee0 in -[NSURLSessionTaskMetrics collectWithCompletionHandler:queue:] ()
#6	0x0000000184948fa8 in -[__NSCFLocalDownloadTask _private_fileCompletion] ()
#7	0x0000000184949060 in __37-[__NSCFLocalDownloadTask checkWrite]_block_invoke ()
#8	0x0000000102483ec8 in _dispatch_call_block_and_release ()
#9	0x000000010249d798 in _dispatch_client_callout ()
#10	0x000000010248c2a4 in _dispatch_lane_serial_drain ()
#11	0x000000010248cf08 in _dispatch_lane_invoke ()
#12	0x000000010249903c in _dispatch_root_queue_drain_deferred_wlh ()
#13	0x00000001024985b4 in _dispatch_workloop_worker_thread ()
#14	0x0000000102522bcc in _pthread_wqthread ()
#15	0x000000010252198c in start_wqthread ()
Enqueued from com.apple.__NSCFLocalDownloadFile (Thread 44) Queue : com.apple.__NSCFLocalDownloadFile (serial)
#0	0x00000001024893f8 in dispatch_async ()
#1	0x0000000184a72d24 in __52-[__NSCFLocalDownloadFile finishOnQueue:completion:]_block_invoke ()
#2	0x0000000184a729e4 in __36-[__NSCFLocalDownloadFile ioChannel]_block_invoke ()
#3	0x0000000102483ec8 in _dispatch_call_block_and_release ()
#4	0x000000010249d798 in _dispatch_client_callout ()
#5	0x000000010248c2a4 in _dispatch_lane_serial_drain ()
#6	0x000000010248cf08 in _dispatch_lane_invoke ()
#7	0x000000010249903c in _dispatch_root_queue_drain_deferred_wlh ()
#8	0x00000001024985b4 in _dispatch_workloop_worker_thread ()
#9	0x0000000102522bcc in _pthread_wqthread ()
#10	0x000000010252198c in start_wqthread ()

Keep RiveFile alive during async asset loading operations
to prevent factory invalidation. The cachedRiveFile
reference is retained until the next load or view
deallocation, ensuring the RiveFactory remains valid for
background decode operations.
@mfazekas mfazekas requested a review from HayesGordon November 27, 2025 14:58
Copy link
Contributor

@HayesGordon HayesGordon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@mfazekas mfazekas merged commit 9f8b0bf into main Dec 9, 2025
5 checks passed
@mfazekas mfazekas deleted the mfazekas/fix-file-release-on-asset-load-is-pending branch December 9, 2025 14:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants