react / react/react-native

[Android] Heap snapshot via Hermes CDP fails mid-stream: OkHttp WebSocket closes with code 1001 due to MAX_QUEUE_SIZE overflow in CxxInspectorPackagerConnection

Aperta
#56,471 4 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

🌐Networking 📦Bundler Debugging Flow Needs: Attention Needs: Repro Platform: Android
Lingua principale
C++
Stelle
127k
Fork
25.3k
Merge medio
1g 23h
PR unite (30g)
4

Descrizione

Description

The following issue was found using the Claude Opus 4.6 LLM and verified by me manually. I've used Claude to come up with a fix in the React Native Java code, then monkey patched the RN source files and built an app with that fix.

Before the fix whenever I tried to get the snapshot through the Debugger, Chrome Dev Tools or even a custom Node script it would fail. When using the node script I would get around 70MB of snapshot and then the WS connection would close leaving me with an incomplete snapshot file. By inspecting the internal metro code during that I found that the socket connection to the device was closing with the 1001 code.

After applying the fix Claude came up with both the Debugger and the Node script were able to get the snapshot without any issues.

The final snapshot I extracted from the app was around 120 MB.

I've confirmed this issue to be present in the 0.83. which is the latest version I am able to upgrade the project in which I ran into this issue. The fix that ended up helping I only validated on 0.79.6

Summary generated by Claude

React Native version: 0.79.6 (unpatched as of main branch)

Platform: Android only

Description

Taking a JS heap snapshot from a React Native Android app via Hermes CDP always fails for large heaps (>100MB snapshot size). The WebSocket connection between the app and Metro closes with code 1001 partway through the snapshot — typically after a portion of chunks are delivered — causing the snapshot to be incomplete or the tooling to throw WS closed before snapshot read was completed.

Root cause

CxxInspectorPackagerConnection.java uses OkHttp to maintain the WebSocket from the app to Metro. OkHttp's RealWebSocket has a hardcoded MAX_QUEUE_SIZE = 16 * 1024 * 1024 (16 MiB). When HeapProfiler.takeHeapSnapshot is triggered, Hermes streams chunks to the inspector faster than TCP delivers them to Metro. The outgoing OkHttp queue fills past 16 MiB, and OkHttp closes the connection with code 1001.

A second issue compounds it: the OkHttpClient is configured with writeTimeout(10, TimeUnit.SECONDS). Since heap snapshots can take more than 10 seconds to fully flush, the write timeout can also trigger disconnection independently.

Relevant file: ReactAndroid/src/main/java/com/facebook/react/devsupport/CxxInspectorPackagerConnection.java

// Current code (unfixed):

private final OkHttpClient mHttpClient =                                                                                               
      new OkHttpClient.Builder()
          .connectTimeout(10, TimeUnit.SECONDS)                                                                                          
          .writeTimeout(10, TimeUnit.SECONDS)  // ← kills long snapshots
          .readTimeout(0, TimeUnit.MINUTES)                                                                                              
          .build();                                         

// ...in connectWebSocket():

  return new IWebSocket() {
      @Override                                                                                                                          
      public void send(String message) {
          webSocket.send(message);  // ← no backpressure, overflows MAX_QUEUE_SIZE                                                       
      }                                                                                                                                  
      // ...
  };                                                                                                                                     

Fix

Two changes in connectWebSocket():

  1. Set writeTimeout(0, TimeUnit.SECONDS) to disable the write timeout (matching how readTimeout is already disabled).
  2. Introduce an unbounded local queue with a sender thread that throttles sends to keep OkHttp's internal queue below its 16 MiB hard limit:
  .writeTimeout(0, TimeUnit.SECONDS) // was 10s — disabled to match readTimeout                                                          
                                                                                                                                         
  // In connectWebSocket(), replace direct webSocket.send() with:
  final BlockingQueue<String> localQueue = new LinkedBlockingQueue<>();                                                                  
  final AtomicBoolean closing = new AtomicBoolean(false);                                                                                
  final long OKHTTP_QUEUE_HIGH_WATERMARK = 8 * 1024 * 1024; // stay under 16 MiB hard limit                                              
                                                                                                                                         
  Thread senderThread = new Thread(() -> {                                                                                               
      while (!closing.get()) {                                                                                                           
          try {                                             
              String message = localQueue.poll(1, TimeUnit.SECONDS);
              if (message == null) continue;                                                                                             
              while (webSocket.queueSize() > OKHTTP_QUEUE_HIGH_WATERMARK) {
                  Thread.sleep(5);                                                                                                       
              }                                                                                                                          
              webSocket.send(message);
          } catch (InterruptedException e) {                                                                                             
              Thread.currentThread().interrupt();           
              break;
          }
      }
  }, "InspectorWebSocketSender");
  senderThread.setDaemon(true);                                                                                                          
  senderThread.start();
                                                                                                                                         
  return new IWebSocket() {                                 
      @Override public void send(String message) { localQueue.offer(message); }
      @Override public void close() {                                                                                                    
          closing.set(true);
          webSocket.close(1000, "End of session");                                                                                       
      }                                                                                                                                  
  };
Steps to reproduce
  1. Run a React Native Android app in debug mode with a large heap
  2. Connect to Metro via CDP and call HeapProfiler.takeHeapSnapshot
  3. Observe connection closes before the whole snapshot is delivered. code 1001 is given by the socket (node_modules/@react-native/dev-middleware/dist/inspector-proxy/InspectorProxy.js:232)
React Native Version

0.83.0

Output of npx @react-native-community/cli info
System:
  OS: Linux 6.18 Manjaro Linux
  CPU: (8) x64 Intel(R) Core(TM) i7-10610U CPU @ 1.80GHz
  Memory: 2.76 GB / 30.97 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 22.22.0
    path: /home/owner/.nvm/versions/node/v22.22.0/bin/node
  Yarn:
    version: 1.22.22
    path: /usr/local/bin/yarn
  npm:
    version: 10.9.4
    path: /home/owner/.nvm/versions/node/v22.22.0/bin/npm
  Watchman: Not Found
SDKs:
  Android SDK:
    Android NDK: 29.0.14206865
IDEs:
  Android Studio: AI-253.30387.90.2532.14935130
Languages:
  Java:
    version: 25.0.2
    path: /usr/bin/javac
  Ruby:
    version: 3.4.8
    path: /usr/bin/ruby
npmPackages:
  "@react-native-community/cli": Not Found
  react:
    installed: 19.2.0
    wanted: 19.2.0
  react-native:
    installed: 0.83.0
    wanted: 0.83.0
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: Not found
  newArchEnabled: Not found
iOS:
  hermesEnabled: Not found
  newArchEnabled: Not found
Screenshots and Videos

No response

Maybe related to
https://github.com/facebook/react-native/issues/39651
https://github.com/facebook/react-native/issues/49158

Repro

This would likely be possible to be reproduced with this: https://github.com/abbasvlb/heapdumpissue/

although I would use a higher value than 20MB

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Direzione di ricerca

Inizia da ReactAndroid/src/main/java/com/facebook/react/devsupport/CxxInspectorPackagerConnection.java, in particolare dalla configurazione di OkHttpClient e da connectWebSocket(). Riproduci il problema con il progetto heapdumpissue collegato chiamando HeapProfiler.takeHeapSnapshot su un heap di grandi dimensioni. Il lavoro è completato quando la connessione CDP Android rimane aperta e restituisce lo snapshot completo invece di chiudersi con il codice 1001.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
android, java
Ambito
mobile-dev
Tipo di issue
Bug
Difficoltà
4/5
Tempo stimato
3-5 giorni
Stato di attività
Tranquilla
Chiarezza
Specificata chiaramente
Idoneità per principianti
55/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.