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

Đang mở
#56,471 4 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

🌐Networking 📦Bundler Debugging Flow Needs: Attention Needs: Repro Platform: Android
Ngôn ngữ chính
C++
Star
127k
Fork
25.3k
Merge trung bình
1 ngày 23 giờ
Pull request đã merge (30 ngày)
4

Mô tả

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

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Bắt đầu với ReactAndroid/src/main/java/com/facebook/react/devsupport/CxxInspectorPackagerConnection.java, đặc biệt là cấu hình OkHttpClient và connectWebSocket(). Tái hiện vấn đề bằng dự án heapdumpissue được liên kết, gọi HeapProfiler.takeHeapSnapshot trên một heap lớn. Được xem là hoàn tất khi kết nối CDP của Android vẫn mở và gửi đầy đủ snapshot thay vì đóng với mã 1001.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
android, java
Lĩnh vực
mobile-dev
Loại issue
Lỗi
Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức độ hoạt động
Ít trao đổi
Độ rõ ràng
Đặc tả rõ ràng
Mức phù hợp với người mới
55/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.