Files
readest/apps/readest-app/.claude/memory/pdf-oom-range-flood-3470.md
T
Huang Xin 23d1ef6f13 fix(rsvp): restore in-flow control bar layout reverted by #4589 (#4671)
* fix(rsvp): restore in-flow control bar layout reverted by #4589

PR #4585 fixed the mobile RSVP control bar overlap by laying the audio
toggle and settings gear in a single in-flow flex row flanking the
centered transport. PR #4589 branched from main about five minutes
before #4585 merged and merged about ten hours later without rebasing,
so its squash carried the stale pre-#4585 file and reverted the entire
fix, including the regression test #4585 had added.

On narrow phones (360px) the audio and settings icons again overlapped
the right end of the transport, hiding the "skip forward 15" control.

Restore the #4585 layout and re-add a structural guard test asserting
the audio toggle and settings share the transport row and live in no
absolutely positioned cluster. Verified on a Xiaomi 13 (360px) via
on-device CDP: no overlap, play button stays centered.

Also stage the project-memory note for this regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(rsvp): hide Faster/Slower buttons at 350px or below

On very narrow phones (width 350px or less) the control row has no room
for every control. Collapse the Faster/Slower speed buttons via a
max-[350px]:hidden variant (matching the existing 350px tightening tier)
so the transport, audio toggle and settings never overflow. Speed stays
adjustable from the WPM dropdown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 02:58:47 +02:00

4.0 KiB

name, description, metadata
name description metadata
pdf-oom-range-flood-3470 Android/iOS large-PDF import/open OOM (#3470) = unthrottled pdf.js range-request flood, not whole-file load; fix = concurrency cap in foliate makePDF
node_type type originSessionId
memory project 1f5ecad5-076c-4170-939a-c80438c37f64

Large-PDF OOM on Android/iOS (#3470)

Symptom: importing/opening a 50 MB+ PDF crashes (no message) with java.lang.OutOfMemoryError ... target footprint 536870912 (512 MB Java heap) at RustWebViewClient.handleRequestshouldInterceptRequest. Same file is fine in the official pdf.js viewer on Android Chrome. Repro file: 100个句子记完7000个雅思单词.pdf (67 MB, 970 pages).

Root cause (NOT whole-file load): opening the PDF makes pdf.js fire ~759 small 64 KB range reads to parse scattered xref/object streams. foliate-js makePDF fulfilled every requestDataRange with an un-awaited file.slice(begin,end).arrayBuffer() → all dispatched at once (measured maxInFlight 753). On Android each read is a fetch() to the rangefile scheme → shouldInterceptRequest allocates a Rust Vec<u8> + a Java byte[] per request; ~750 simultaneous intercepted requests exhaust the 512 MB Java heap. The official pdf.js viewer survives because the browser caps ~6 connections/host; the custom rangefile (and iOS native-file) scheme has no such cap. Explains "50 MB+" (bigger PDF → more scattered objects → bigger flood) and "crashes on some devices only" (heap/WebView threshold).

Fix (RESOLVED — foliate-js#31 squash e098bc3 + readest#4670, both merged): packages/foliate-js/pdf.js makePDF — queue + pump bounding range reads to MAX_CONCURRENT_RANGES = 6 (mimics the browser's per-host limit). One spot covers Android RemoteFile, iOS NativeFile, web File. Throttling is free on speed (6 parallel fetches saturate throughput). foliate-js is a git submodule → commit + push to readest fork, then bump pointer. Test: src/__tests__/foliate-pdf-range-concurrency.test.tsvi.mock('@pdfjs/pdf.min.mjs') installs a fake globalThis.pdfjsLib whose getDocument fires a 200-call flood; asserts maxInFlight ≤ 6 and all served. Fails (200) before, passes after.

On-device CDP verification recipe (no rebuild)

Release Readest 0.11.10 ships a debuggable WebView (socket webview_devtools_remote_<pid>), so CDP attaches without run-as.

  • adb forward tcp:9222 localabstract:webview_devtools_remote_$PID; page WS from curl :9222/json.
  • Push file where asset scope allows: /sdcard/Readest/Books/ matches scope glob **/Readest/**/*; app has MANAGE_EXTERNAL_STORAGE → readable. Canonical path /storage/emulated/0/Readest/Books/x.pdf.
  • rangefile URL: http://rangefile.localhost/?path=<encodeURIComponent(abs)>&start=&end= (end inclusive, omit=EOF, 8 MB cap, returns 200 + X-Total-Size).
  • Faithfully replicate makePDF: await import('http://tauri.localhost/vendor/pdfjs/pdf.min.mjs') (sets globalThis.pdfjsLib, same vendored 5.7.284), a file-like {size, slice(b,e)→{arrayBuffer:()=>fetchRangePart(b,e-1)}}, new pdfjsLib.PDFDataRangeTransport(size,[]), instrument requestDataRange, getDocument({range,wasmUrl:'/vendor/pdfjs/',cMapUrl,standardFontDataUrl,isEvalSupported:false}) then getPage(1)/getViewport/getMetadata.
  • Java heap via adb shell dumpsys meminfo com.bilingify.readest (Dalvik Heap line).

Verified on Xiaomi 13 (fuxi) / Android 16 / WebView 147 / 8 GB: this device does NOT OOM (newer WebView; Dalvik only +9 MB) but the flood reproduces: 753 → 6 concurrent, open time 1446 → 1479 ms (no penalty), 970 pages/title/viewport identical. Gotcha: package installs (installPackageLI in logcat) kill the app mid-session → re-discover the devtools socket PID. The makePDF flood alone did NOT crash this device — can't get a live OOM here; rely on the user's WebView-145 log + the bounded-concurrency proof.

Related: android-nativefile-remotefile-io (rangefile vs asset-protocol Range bug), webtoon-mode-3647 (foliate-js submodule fork-push).