[PR #2628] [MERGED] perf(frida): evict captured messages without copying the whole queue #5290

Closed
opened 2026-10-31 01:05:28 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/2628
Author: @kaoru0822-kitauji
Created: 10/13/2026
Status: ✅ Merged
Merged: 10/13/2026
Merged by: @kaoru0822-kitauji

Base: main ← Head: perf/frida-message-fifo


📝 Commits (3)

  • 281735c perf(frida): evict captured messages without copying the whole queue
  • 65cae1b Merge remote-tracking branch 'origin/main' into perf/frida-message-fifo
  • 671a8a2 Merge remote-tracking branch 'origin/main' into perf/frida-message-fifo

📊 Changes

2 files changed (+98 additions, -23 deletions)

View changed files

📝 src/application/frida/FridaInstrumentationManager.test.ts (+36 -0)
📝 src/frida/FridaInstrumentationManager.ts (+62 -23)

📄 Description

Problem

FridaInstrumentationManager keeps a session's script messages under a 16 MiB capture budget. Once the budget is full, every incoming message evicts the oldest one through #recordMessage, and each eviction does three linear-time steps:

  • managed.messages.shift() moves every retained message.
  • Buffer.byteLength(JSON.stringify(removed)) re-serializes the evicted message to recover the byte charge it was admitted with.
  • scriptMessages.indexOf(removed) followed by splice(index, 1) moves the per-script array.

With small send() payloads, about 188k messages fit in the budget. A standalone reproduction of the exact eviction logic (Node 26.5, Apple Silicon) measures:

per message
filling the buffer 0.14 µs
buffer full, before 32 µs (about 31k msg/s ceiling on the main thread)
buffer full, after 0.14 µs (200k messages in 28 ms)

Real manager path (fake Frida session through FridaInstrumentationManager#recordMessage, details in the review thread): at the budget, 100k messages take 13,099 ms (131 µs/msg, CPU 13,128 ms) on main versus 134 ms (1.34 µs/msg, CPU 151 ms) on this branch. Retained count (135,409) and truncation are identical.

A hook on a hot function (open, malloc, objc_msgSend) can exceed 31k messages per second. When that happens, the MCP/CLI process event loop is saturated by bookkeeping rather than serving requests.

Fix

  • Both the session queue and the per-script queues become a small MessageFifo. Eviction advances a head offset, and the queue compacts with one slice once half its slots are evicted, so eviction is amortized O(1). Evicted slots are cleared immediately so their messages can be collected.
  • Each queued entry records the charge computed when it was admitted, so eviction no longer re-serializes.
  • Both queues are in arrival order, so the globally oldest message is always the oldest retained message of its script. Per-script eviction becomes an identity check on the head instead of indexOf plus splice. A script that has been unloaded has no queue, as before.

Retained messages, their order, messagesTruncated, and the loadScript/status result shapes are unchanged. Agent tokens are unaffected because the output doesn't change. Peak memory adds at most one transient pointer array during compaction, about 3 MB at the 188k-message worst case. CPU time and elapsed time are the table above.

Validation

  • New test: retains the newest contiguous messages across repeated budget evictions. It sends 2,000 messages of about 32 KB through the fake Frida session and asserts that status().messages is exactly the newest contiguous suffix, in order, with truncation reported. That forces several evictions and compactions.
  • npm run test:focused: FridaInstrumentationManager.test.ts (26), fridaMcp.test.ts, fridaCli.test.ts; 33 tests pass in total. That includes the existing eviction tests, such as "keeps an active script message bucket after its last message is evicted".
  • npm run check:fast and format:check pass.
  • Not verified with real Frida: it can't attach on this macOS host (SIP). The benchmark runs the extracted eviction logic in isolation.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • When captured messages exceed the session’s storage limit, the oldest messages are removed across the session and their corresponding per-script records are kept in sync.
    • Recent messages remain available within the storage limit, and results report when message history has been truncated.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/2628 **Author:** [@kaoru0822-kitauji](https://github.com/kaoru0822-kitauji) **Created:** 10/13/2026 **Status:** ✅ Merged **Merged:** 10/13/2026 **Merged by:** [@kaoru0822-kitauji](https://github.com/kaoru0822-kitauji) **Base:** `main` ← **Head:** `perf/frida-message-fifo` --- ### 📝 Commits (3) - [`281735c`](https://github.com/morluto/rea/commit/281735ced582fb7e7439fdf67380a07024118e23) perf(frida): evict captured messages without copying the whole queue - [`65cae1b`](https://github.com/morluto/rea/commit/65cae1b879d8223f37503709a478a73a788471ad) Merge remote-tracking branch 'origin/main' into perf/frida-message-fifo - [`671a8a2`](https://github.com/morluto/rea/commit/671a8a24471d10d3f5629a3ffe82f773db31581f) Merge remote-tracking branch 'origin/main' into perf/frida-message-fifo ### 📊 Changes **2 files changed** (+98 additions, -23 deletions) <details> <summary>View changed files</summary> 📝 `src/application/frida/FridaInstrumentationManager.test.ts` (+36 -0) 📝 `src/frida/FridaInstrumentationManager.ts` (+62 -23) </details> ### 📄 Description ## Problem `FridaInstrumentationManager` keeps a session's script messages under a 16 MiB capture budget. Once the budget is full, every incoming message evicts the oldest one through `#recordMessage`, and each eviction does three linear-time steps: - `managed.messages.shift()` moves every retained message. - `Buffer.byteLength(JSON.stringify(removed))` re-serializes the evicted message to recover the byte charge it was admitted with. - `scriptMessages.indexOf(removed)` followed by `splice(index, 1)` moves the per-script array. With small `send()` payloads, about 188k messages fit in the budget. A standalone reproduction of the exact eviction logic (Node 26.5, Apple Silicon) measures: | | per message | | --- | --- | | filling the buffer | 0.14 µs | | buffer full, before | **32 µs** (about 31k msg/s ceiling on the main thread) | | buffer full, after | 0.14 µs (200k messages in 28 ms) | **Real manager path** (fake Frida session through `FridaInstrumentationManager#recordMessage`, details in the review thread): at the budget, 100k messages take 13,099 ms (131 µs/msg, CPU 13,128 ms) on main versus 134 ms (1.34 µs/msg, CPU 151 ms) on this branch. Retained count (135,409) and truncation are identical. A hook on a hot function (`open`, `malloc`, `objc_msgSend`) can exceed 31k messages per second. When that happens, the MCP/CLI process event loop is saturated by bookkeeping rather than serving requests. ## Fix - Both the session queue and the per-script queues become a small `MessageFifo`. Eviction advances a head offset, and the queue compacts with one `slice` once half its slots are evicted, so eviction is amortized O(1). Evicted slots are cleared immediately so their messages can be collected. - Each queued entry records the `charge` computed when it was admitted, so eviction no longer re-serializes. - Both queues are in arrival order, so the globally oldest message is always the oldest retained message of its script. Per-script eviction becomes an identity check on the head instead of `indexOf` plus `splice`. A script that has been unloaded has no queue, as before. Retained messages, their order, `messagesTruncated`, and the `loadScript`/`status` result shapes are unchanged. Agent tokens are unaffected because the output doesn't change. Peak memory adds at most one transient pointer array during compaction, about 3 MB at the 188k-message worst case. CPU time and elapsed time are the table above. ## Validation - New test: `retains the newest contiguous messages across repeated budget evictions`. It sends 2,000 messages of about 32 KB through the fake Frida session and asserts that `status().messages` is exactly the newest contiguous suffix, in order, with truncation reported. That forces several evictions and compactions. - `npm run test:focused`: `FridaInstrumentationManager.test.ts` (26), `fridaMcp.test.ts`, `fridaCli.test.ts`; 33 tests pass in total. That includes the existing eviction tests, such as "keeps an active script message bucket after its last message is evicted". - `npm run check:fast` and `format:check` pass. - Not verified with real Frida: it can't attach on this macOS host (SIP). The benchmark runs the extracted eviction logic in isolation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * When captured messages exceed the session’s storage limit, the oldest messages are removed across the session and their corresponding per-script records are kept in sync. * Recent messages remain available within the storage limit, and results report when message history has been truncated. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-31 01:05:28 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#5290
No description provided.