Skip to content

OOM: [RNBridgeless] reason = Max heap size was exceeded #445

Description

@yannis-winzana

Hello, I have a crash using react-native-webgpu with navigation

After repeatedly opening and closing Three.js scenes in browser tabs, I’ve encountered a crash and this error message

HermesGC: OOM: [RNBridgeless] reason = Max heap size was exceeded (1 from category: vm_allocate_category), numCollections = 2296, heapSize = 33554432, allocated = 31563808, va = 33554432, external = 3371365863. LLVM ERROR: OOM: error_code(value = 1, category = vm_allocate_category, message = Max heap size was exceeded)

Each scene has a fixed number of openings depending on its size; I realised this with one scene that crashes after the fifth time, but with another it’s after the twelfth time.

Environment :
"react": "19.1.0",
"react-native": "0.81.5",
"three": "^0.184.0"
"@react-three/fiber": "^9.6.1"
"@react-navigation/native": "^7.1.11",
"@react-navigation/native-stack": "^7.3.16",
Platform: iOS
Device: iPad 3

After extensive research, I realised that the js_externalBytes value returned by HermesInternal.getInstrumentedStats() does not seem to decrease when a scene is closed, which, in my case with a scene that grows to 800 MB, leads to a crash very quickly. However, this issue occurs regardless of the scene’s size, and I have observed the same problem in the example repository here.

Step to reproduce :

  • Clone this repository
  • Go into FiberCanvas.tsx and replace the content with the code bellow (I only added logs)
  • Run the app, go to Three.js > Fiber
  • open and close the scene and check the logs to see js_externalBytes increase on every opening
import type { ReconcilerRoot, RootState } from "@react-three/fiber";
import {
  createRoot,
  events,
  extend,
  unmountComponentAtNode,
} from "@react-three/fiber";
import React, { useEffect, useRef } from "react";
import type { ViewProps } from "react-native";
import { PixelRatio } from "react-native";
import type { CanvasRef } from "react-native-webgpu";
import { Canvas } from "react-native-webgpu";
import * as THREE from "three";

import { makeWebGPURenderer } from "./makeWebGPURenderer";

//global.THREE = global.THREE || THREE;

interface FiberCanvasProps {
  children: React.ReactNode;
  style?: ViewProps["style"];
  camera?: THREE.PerspectiveCamera;
  scene?: THREE.Scene;
}

declare const HermesInternal:
  | { getInstrumentedStats?: () => Record<string, number> }
  | undefined;

let openCount = 0;
let closeCount = 0;

const logExternalBytes = (label: string) => {
  const stats =
    typeof HermesInternal !== "undefined"
      ? HermesInternal?.getInstrumentedStats?.()
      : undefined;
  console.log(label, {
    js_externalBytes: stats?.js_externalBytes,
    js_heapSize: stats?.js_heapSize,
  });
};

export const FiberCanvas = ({
  children,
  style,
  scene,
  camera,
}: FiberCanvasProps) => {
  const root = useRef<ReconcilerRoot<OffscreenCanvas>>(null!);
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-expect-error
  React.useMemo(() => extend(THREE), []);
  const canvasRef = useRef<CanvasRef>(null);
  useEffect(() => {
    const context = canvasRef.current!.getContext("webgpu")!;
    const renderer = makeWebGPURenderer(context);

    const canvas = context.canvas as unknown as HTMLCanvasElement;
    canvas.width = canvas.clientWidth * PixelRatio.get();
    canvas.height = canvas.clientHeight * PixelRatio.get();
    const size = {
      top: 0,
      left: 0,
      width: canvas.clientWidth,
      height: canvas.clientHeight,
    };

    if (!root.current) {
      root.current = createRoot(canvas);
    }
    root.current.configure({
      size,
      events,
      scene,
      camera,
      gl: renderer,
      frameloop: "always",
      dpr: 1, //PixelRatio.get(),
      onCreated: async (state: RootState) => {
        await state.gl.init();
        const renderFrame = state.gl.render.bind(state.gl);
        state.gl.render = (s: THREE.Scene, c: THREE.Camera) => {
          renderFrame(s, c);
          context.present();
        };
        openCount += 1;
        logExternalBytes(`[open #${openCount}] scene opened`);
      },
    });
    root.current.render(children);
    return () => {
      closeCount += 1;
      logExternalBytes(`[close #${closeCount}] scene start`);
      if (canvas != null) {
        unmountComponentAtNode(canvas!);
      }
    };
  });

  return <Canvas ref={canvasRef} style={style} />;
};
 (NOBRIDGE) LOG  [open #1] scene opened {"js_externalBytes": 11589887, "js_heapSize": 20971520}
 (NOBRIDGE) LOG  [close #1] scene start {"js_externalBytes": 85933905, "js_heapSize": 25165824}
 (NOBRIDGE) LOG  [open #2] scene opened {"js_externalBytes": 86326305, "js_heapSize": 25165824}
 (NOBRIDGE) LOG  [close #2] scene start {"js_externalBytes": 159344810, "js_heapSize": 25165824}
 (NOBRIDGE) LOG  [open #3] scene opened {"js_externalBytes": 159896250, "js_heapSize": 25165824}
 (NOBRIDGE) LOG  [close #3] scene start {"js_externalBytes": 233057918, "js_heapSize": 25165824}
 (NOBRIDGE) LOG  [open #4] scene opened {"js_externalBytes": 233463710, "js_heapSize": 25165824}
 (NOBRIDGE) LOG  [close #4] scene start {"js_externalBytes": 307577342, "js_heapSize": 29360128}
...

For the example I tried to do the same on the Helmet :

 (NOBRIDGE) LOG  [open #1] scene opened {"js_externalBytes": 100241837, "js_heapSize": 16777216}
 (NOBRIDGE) LOG  [close #1] scene start {"js_externalBytes": 325081896, "js_heapSize": 20971520}
 (NOBRIDGE) LOG  [open #2] scene opened {"js_externalBytes": 413379661, "js_heapSize": 20971520}
 (NOBRIDGE) LOG  [close #2] scene start {"js_externalBytes": 638207944, "js_heapSize": 20971520}
 (NOBRIDGE) LOG  [open #3] scene opened {"js_externalBytes": 738823875, "js_heapSize": 20971520}
 (NOBRIDGE) LOG  [close #3] scene start {"js_externalBytes": 951805688, "js_heapSize": 25165824}
...
 (NOBRIDGE) LOG  [open #8] scene opened {"js_externalBytes": 2323505937, "js_heapSize": 25165824}
Unfortunately, I can’t go any further to look at the OOM because of another memory issue.
So I just added a "renderer.dispose();" at the end of the useEffect in Helmet.tsx to clean memory on each destroy.
And finally I got the crash on the 
'[open #12] scene opened', { js_externalBytes: 3316571485, js_heapSize: 29360128 }
HermesGC: OOM: [RNBridgeless] reason = Max heap size was exceeded (1 from category: vm_allocate_category), numCollections = 2343, heapSize = 16777216, allocated = 15835072, va = 16777216, external = 3423283937.
LLVM ERROR: OOM: error_code(value = 1, category = vm_allocate_category, message = Max heap size was exceeded)

I hope this will help to understand and resolve the problem; thank you in advance for your help

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions