screenpipe 内存泄漏狩猎指南:24/7 压力循环压测与诊断工具链深度解析
【免费下载链接】screenpipeYC (S26) | Open Computer History | Record your screen continuously locally and provide context to your agents (Claude, Codex, Openclaw, Hermes, Runner...)项目地址: https://gitcode.com/GitHub_Trending/sc/screenpipe
导读:本文围绕 scripts/memory-leak/README.md 展开,系统讲解 screenpipe 项目为根治"连续运行一周后进程内存涨到 20 GB"这类长驻内存泄漏问题而构建的黑盒压力测试工具链。你将掌握
leak_hunt.py的采样与场景轮转原理、leak_probe.py的端点增量探测法、screenpipe-db中被忽略(#[ignore])的 DB 层压力测试,以及完整的命令参数、阈值判定与诊断产物解读方法,可直接用于自建桌面/服务型应用的长期内存压测。
一、为什么要为长驻进程做 24/7 内存压测
screenpipe 是一个持续在本地录制屏幕、音频与无障碍事件并提供给 AI Agent 上下文的桌面应用。这类进程有一个共同的顽疾:单次运行内无法暴露、但连续运行数小时到数天后必然出现的渐进式内存增长——常见诱因包括 FTS 全文搜索缓存未释放、WebSocket 连接不关闭、base64 响应缓冲滞留、数据库读游标泄漏等。这类问题在短时单测里几乎不可能复现,只有"长时间 + 高并发 API 压力"才能把它逼出来。
为此,screenpipe 在 scripts/memory-leak/ 下维护了一套完整的"内存泄漏狩猎"工具链,其核心设计哲学在 leak_hunt.py 的模块注释中写得很明确:
The harness is intentionally black-box: it keeps pressure on the local screenpipe API while sampling the OS process RSS. This catches leaks that only show up in the real desktop/server process after hours or days.
即:刻意采用黑盒方式,不侵入被测进程内部,而是持续对本地 screenpipe API 施压,同时从操作系统层面采样进程 RSS。这正是针对"只有真实桌面/服务进程运行数小时乃至数天后才出现"的那类泄漏而设计的。
二、整体架构:采样器 + 场景轮转 双线程
leak_hunt.py在run(前台)模式下同时做两件事,对应两条并行的工作线:
- 采样线(sampler thread):每 30 秒(
--sample-interval-sec,默认 30.0)采样一次screenpipe-app、screenpipe、screenpipe-engine三者中内存占用最大的进程树的 RSS/CPU/句柄数/线程数/fd 数,并滚动写入samples.jsonl与samples.csv。实现见 leak_hunt.py 的 sampler_loop。 - 压力线(主线程):不断轮转 8 类压力场景,每个场景持续
--scenario-duration-sec(默认 180 秒)后切换到下一个,循环往复。场景实现见 leak_hunt.py 的场景编排。
默认情况下采样间隔为 30 秒,进程名匹配集合为("screenpipe-app", "screenpipe", "screenpipe-engine"),API 基址为http://127.0.0.1:3030(默认常量定义)。若本地 API 启用了认证,可通过SCREENPIPE_API_KEY环境变量或--api-key参数传入,请求会携带Authorization: Bearer <key>头。
所有诊断产物只写入~/.screenpipe/diagnostics/memory-leak目录,不会污染被测进程的工作目录:
python3 scripts/memory-leak/leak_hunt.py start python3 scripts/memory-leak/leak_hunt.py status --analyze python3 scripts/memory-leak/leak_hunt.py stop python3 scripts/memory-leak/leak_hunt.py run --duration-sec 900 --scenario-duration-sec 60 --concurrency 8四个子命令的分工如下:
| 子命令 | 作用 | 关键参数 |
|---|---|---|
run | 前台运行压力循环,--duration-sec控制时长(默认 3600 秒),--forever无限运行 | --duration-sec、--forever |
start | 以后台守护进程方式启动run --forever,PID 写入leak-hunt.pid | 全部公共参数 |
status | 打印守护进程运行状态与当前 screenpipe 进程树信息,--analyze附加分析 | --analyze、--since-hours |
stop | 向守护进程发SIGTERM,10 秒未退出则升级为SIGKILL | --out-dir |
start的实现细节值得注意(start_daemon):它会把完整的run --forever子进程以start_new_session=True方式脱离终端,stdout/stderr 重定向到daemon.log,并把子进程 PID 写入leak-hunt.pid;如果检测到已有存活守护进程则直接退出并提示。stop则读取 PID 文件,先SIGTERM优雅停止,超时 10 秒后SIGKILL兜底。
三、压力场景全景:9 类 API 攻击面
run模式的场景轮转队列默认包含 8 个场景(场景列表),开启--allow-audio-toggle后追加第 9 个。每个场景由fanout函数用ThreadPoolExecutor以固定并发度持续轰炸,并逐次记录成功/失败计数(fanout 实现)。
3.1 健康轮询(health_poll)
随机请求 17 个轻量状态端点(端点清单):/health、/vision/status、/vision/metrics、/audio/metrics、/audio/device/status、/audio/list、/meetings/status、/capture/hd、/data/storage-preview、/data/device-storage、/sync/status、/archive/status、/retention/status、/vault/status、/browser/status、/tags/autocomplete、/speakers/unnamed。用于制造持续的短请求流量,暴露连接/上下文对象泄漏。
3.2 搜索扇出(search_fanout)
对/search端点做高基数参数组合轰炸(build_search_fanout_params):
- 关键词:
screenpipe、meeting、github、slack、customer、error、memory、audio、todo、pricing; - content_type:
all/ocr/audio/input/accessibility/memory; - limit:默认模式下取
[10, 25, 50, 100, 250](开启帧图片时改为[5, 10, 20]); - offset:
[0, 10, 50, 100, 250]; - max_content_length:
[256, 1024, 4096]; - focused:随机
None/true/false。
这组参数刻意覆盖"大返回体 + 深分页 + 长文本截断"的组合,专门检验 FTS 搜索路径的内存回收。
3.3 时间线流式推送(timeline_stream)
通过真实的 WebSocket 协议连接/stream/frames,随机选取 1/6/24/72/168 小时时间窗、ascending/descending排序、limit取[100, 500, 2000, 10000],每个连接保持 1~5 秒后断开(实现)。注意这里的并发被钳制在max(1, min(concurrency, 4)),因为长连接流式场景并发过高会失真。
值得一提:脚本没有依赖第三方 WS 库,而是手写了完整的 RFC 6455 客户端——包括Sec-WebSocket-Key握手、掩码帧构造(ws_text_frame)、短/长帧长度编码,见 leak_hunt.py。这保证了它不需要任何 Python 依赖即可运行(纯标准库)。
3.4 帧元数据行走(frame_walk)
先通过/search(content_type=ocr, limit=100)发现一批真实存在的frame_id(递归遍历响应 JSON 收集frame_id字段,discover_frame_ids),随后随机对/frames/{id}/metadata、/frames/{id}/text、/frames/{id}/context发起读取(开启帧图片时追加裸/frames/{id}图像端点),并发上限 8。
3.5 会议行走(meeting_walk)
随机在list/status/query/transcript/detail五种操作间切换(实现):列表分页、状态查询、关键词过滤、单会议详情与全文转写读取,并发上限 8。重点考察转写分段读路径。
3.6 记忆/工件列表(memory_artifact_lists)
轮询/memories(含分页 offset 0/100)、/memories/tags、/artifacts、/tags/autocomplete、/activity-summary(端点清单)。
3.7 音频只读(audio_readonly)
只读轰炸/audio/list、/audio/device/status、/audio/metrics、/audio/reconciliation/backlog,并发上限 4(实现)。
3.8 WebSocket 抖动(websocket_churn)
对/ws/health、/ws/metrics、/ws/meeting-status、/ws/events反复建立/保持/断开 WebSocket 连接,每个连接持有 1~5 秒,并发上限 16(实现)。这是检验"连接未关闭/订阅未注销"类泄漏的经典手段。
3.9 音频启停切换(audio_toggle,可选)
仅当传入--allow-audio-toggle时启用:循环POST /audio/stop→ 等待 2 秒 →POST /audio/start→ 等待 10 秒(实现)。它会真实干扰正常录音,因此默认关闭。
四、采样与泄漏判定:RSS 阈值 + 时均斜率双闸门
4.1 进程树聚合采样
由于 screenpipe 是典型的多进程应用(screenpipe-app壳进程会派生screenpipe-engine、bun、mcp-helper等子进程),简单采样单一 PID 会严重漏计。leak_hunt.py实现了递归进程树聚合:以匹配进程为根,沿 PPID 关系收集全部后代,累计得到tree_rss_kb、tree_private_kb、tree_vsz_kb、tree_pcpu、tree_handle_count、tree_thread_count与descendant_count(aggregate_process_tree);若存在多个候选根,选取整树 RSS 最大者作为采样对象(find_screenpipe_process)。也可用--pid指定精确的根 PID。
进程数据来源分平台:Linux/macOS 用ps -axo pid=,ppid=,rss=,vsz=,pcpu=,comm=,Windows 用 PowerShell 的Get-Process+Get-CimInstance Win32_Process(process_rows);fd 数在 Linux 下直接读/proc/{pid}/fd,其他平台回退到lsof -nP -p <pid>。
4.2 增长斜率:一小时内线性回归
growth_mb_per_hour对最近 1 小时窗口内的(时间, RSS)样本做最小二乘线性回归,取斜率即为"每小时内存增长速率"(growth_mb_per_hour)。窗口内样本不足 3 个时返回None。
4.3 双阈值与自动取证快照
当满足以下任一条件时触发取证(sampler_loop 判定逻辑):
| 闸门 | 默认值 | 触发原因标记 |
|---|---|---|
| 进程树 RSS 绝对值 | 8 GB(--rss-threshold-mb,默认 8192.0) | rss_over_8192mb |
| 一小时增长斜率 | 512 MB/h(--growth-threshold-mb-per-hour,默认 512.0) | growth_over_512mb_per_hour |
触发后(受--snapshot-cooldown-sec默认 3600 秒冷却限制),会捕获一份快照到<run-dir>/snapshots/<时间戳>-<原因>-pid<pid>/(capture_snapshot):
- macOS(工具可用时):
vmmap -summary(内存区域摘要)、sample <pid> 20(20 秒采样堆栈)、lsof -nP -p <pid>(打开文件清单)、ps -M -p <pid>(线程列表); - Windows:
Get-Process -Id <pid> | Format-List *与Get-CimInstance Win32_Process进程树 JSON; - 每个快照附带
snapshot.json元数据(时间戳、PID、触发原因、产物路径)。
这些产物对事后分析"内存在哪个子系统里涨"至关重要:vmmap -summary能定位到MALLOC_SMALL、IOAccelerator(图形)等具体区域,sample则给出热点堆栈。
五、诊断产物与 status/analyze 解读
每次run会话在~/.screenpipe/diagnostics/memory-leak/下创建一个以时间戳命名的运行目录,内含:
| 文件 | 内容 |
|---|---|
config.json | 本次运行全部参数快照(base_url、时长、并发、阈值、进程名、PID 等) |
samples.jsonl | 每 30 秒一行的全字段采样(JSON Lines,含 scenario、rss_mb、growth_mb_per_hour、snapshot_reason 等) |
samples.csv | 与 jsonl 同源的 CSV 版,便于表格软件/脚本分析 |
summary.json | 各场景 ok/err 计数汇总 |
snapshots/ | 触发阈值后的取证快照 |
latest | 指向最近一次运行目录的符号链接 |
analyze(实现)汇总--since-hours(默认 24 小时)内的样本,输出:样本数、首/末/最大 RSS、总增长量与MB/h斜率、按场景统计的 RSS min/max/样本数,以及最近 10 次快照路径。其退出码有明确语义:
0:未越过任何泄漏阈值(status: no leak threshold crossed);1:未找到样本;2:判定为疑似泄漏(status: suspect leak)。
这使status --analyze可以无缝接入 CI 或告警脚本。status还会额外打印守护进程存活状态与当前 screenpipe 进程树信息(RSS、私有内存、VSZ、后代进程数)。
六、可选重压模式:帧图片与音频启停的设计权衡
默认压测刻意不包含两类高成本操作,README 明确说明原因:帧图片读取与音频启停更重,且可能干扰正常录制。需要聚焦复现时才显式开启:
python3 scripts/memory-leak/leak_hunt.py start --include-frame-images python3 scripts/memory-leak/leak_hunt.py start --allow-audio-toggle--include-frame-images背后的权衡在 search_fanout 场景注释 中写得很直白:内联帧图片会让服务端触发 ffmpeg 转码并在内存中滞留大块 base64 响应缓冲。因此开启后代码强制把该场景并发降为 1,limit上限缩到 20,且仅随机部分请求携带图片——确保压测工具本身不会成为它试图测量的那个内存压力源。这个"测量者不能污染被测对象"的原则,是整套工具设计上最值得借鉴的地方。
七、源码级守卫测试:防止压测器自身退化
scripts/memory-leak/test_leak_hunt.py 用unittest锁定了上述两个关键行为:
test_default_search_pressure_never_includes_frames:连续 500 次生成搜索参数,断言默认模式下include_frames恒为"false";test_opt_in_frame_pressure_uses_small_limits:开启帧图片时,凡是携带帧的请求limit必须<= 20;ProcessTreeAccountingTests系列:验证递归后代统计(含 3 层嵌套子树)、多候选根时按整树内存选根、--pid精确指定子树三种行为,保证采样口径不会随重构漂移。
八、DB 层压力测试:内存压力测试的 Rust 侧补充
黑盒 API 压测之外,crates/screenpipe-db/tests/memory_pressure_test.rs 提供了一组被忽略(#[ignore])的 Rust 压力测试,在库层直接复现最易分配内存的操作:FTS/搜索、时间线帧 join、会议转写读取,以及并发写读抖动。它们默认被#[ignore]标注,保证常规测试套件保持快速;需要时显式运行:
cargo test -p screenpipe-db --test memory_pressure_test -- --ignored --nocapture测试由env_usize辅助函数驱动,全部规模参数可通过环境变量调整(默认值定义):
| 环境变量 | 默认值 | 语义 |
|---|---|---|
SCREENPIPE_PRESSURE_FRAMES | 6000 | 预置 OCR 帧数 |
SCREENPIPE_PRESSURE_AUDIO | 1000 | 预置音频转写段数 |
SCREENPIPE_PRESSURE_UI | 4000 | 预置 UI 事件数 |
SCREENPIPE_PRESSURE_MEETINGS | 60 | 预置会议数(每个含 20 条转写段) |
SCREENPIPE_PRESSURE_ROUNDS | 40 | 读压力轮数 |
SCREENPIPE_PRESSURE_MAX_RSS_GROWTH_MB | 1024 | RSS 增长断言上限 |
SCREENPIPE_PRESSURE_CHURN_SECONDS | 60 | 写读抖动持续秒数 |
SCREENPIPE_PRESSURE_READERS | 4 | 并发读者数 |
SCREENPIPE_PRESSURE_WRITER_SLEEP_MS | 0 | 写者每次插入后的睡眠毫秒数 |
两个用例各有侧重:
repeated_search_timeline_meeting_reads_do_not_grow_unbounded(实现):先通过seed_mixed_corpus灌入混合语料(多窗口 OCR 帧、音频转写、UI 事件、会议及转写段),再循环执行"读压力轮"——对 6 种ContentType全量搜索、拉取视频块、读取前 100 帧的 metadata/text/accessibility、读取会议转写;测试通过ps -o rss=(Windows 为Get-Process)自测 RSS,断言峰值增长不超过SCREENPIPE_PRESSURE_MAX_RSS_GROWTH_MB。concurrent_write_read_churn_stays_bounded(实现):一个写者协程持续批量插入 OCR 帧与 UI 事件(可选WRITER_SLEEP_MS节流),多个读者协程同时跑读压力轮,持续CHURN_SECONDS后断言 RSS 增长有界——模拟真实桌面场景中"一边录制写入、一边查询"的并发形态。
README 还给出了三组针对性变体,方便聚焦特定怀疑路径:
# 只做读/搜索/时间线/会议压力(缩小数据规模) SCREENPIPE_PRESSURE_FRAMES=1000 \ SCREENPIPE_PRESSURE_AUDIO=200 \ SCREENPIPE_PRESSURE_UI=1000 \ SCREENPIPE_PRESSURE_MEETINGS=10 \ SCREENPIPE_PRESSURE_ROUNDS=8 \ cargo test -p screenpipe-db --test memory_pressure_test repeated_search_timeline_meeting_reads_do_not_grow_unbounded -- --ignored --nocapture # 纯写抖动(读者为 0,写者睡眠 1ms) SCREENPIPE_PRESSURE_CHURN_SECONDS=10 \ SCREENPIPE_PRESSURE_READERS=0 \ SCREENPIPE_PRESSURE_WRITER_SLEEP_MS=1 \ cargo test -p screenpipe-db --test memory_pressure_test concurrent_write_read_churn_stays_bounded -- --ignored --nocapture # 混合写读抖动(4 读者 + 1ms 写者节流) SCREENPIPE_PRESSURE_CHURN_SECONDS=10 \ SCREENPIPE_PRESSURE_READERS=4 \ SCREENPIPE_PRESSURE_WRITER_SLEEP_MS=1 \ cargo test -p screenpipe-db --test memory_pressure_test concurrent_write_read_churn_stays_bounded -- --ignored --nocapture这三条命令分别对应"读路径排查""写路径排查""读写并发排查"三种场景,是缩小泄漏根因范围的高效手段。
九、进阶工具:leak_probe.py 端点增量探测
当 24/7 压测确认"确实在涨"之后,下一步是定位是哪个端点族在涨。scripts/memory-leak/leak_probe.py 与leak_hunt.py刻意互补(模块注释):
- 一次只跑一个端点族(health / meetings / search_no_frames / search_with_frames / memory_lists / timeline_past_1h / timeline_past_7d / timeline_live_today 等 12 个阶段),而不是混合轮转;
- 使用真实
/stream/framesWebSocket 协议(含 SHA-1 accept key 校验,见 ws_connect); - 每个阶段前后各拍一次快照,记录vmmap 内存桶增量(
TRACKED_VMMAP_BUCKETS包括IOAccelerator (graphics)、IOSurface、MALLOC_SMALL、MALLOC_LARGE、WebKit Malloc、DefaultMallocZone等),输出Physical footprint与各桶前后差值; - 绝不写入线上数据库、绝不重启 screenpipe(纯只读)。
基本用法:
python3 scripts/memory-leak/leak_probe.py --phases "search_ocr_no_frames,search_with_frames,timeline_past_7d" \ --phase-seconds 25 --cooldown-seconds 8 --concurrency 2主要参数:--base-url(默认http://127.0.0.1:3030)、--pid(默认自动探测screenpipe-app进程)、--phase-seconds(默认 25)、--cooldown-seconds(默认 8,留给 GC 稳定期)、--concurrency(默认 2)、--phases(逗号分隔阶段名)。产物为<out-dir>/results.json、summary.csv与各阶段前后vmmap-<phase>-before/after.txt。每阶段终端会打印rss 增量 / fd 增量 / ops / errors / 读取字节数及变化最大的 4 个 vmmap 桶——例如某阶段MALLOC_LARGE +42.0MB,就把怀疑范围从"全局泄漏"缩小到"大块分配路径"。
十、参数速查表与推荐实战流程
10.1 公共参数速查(run/start均支持)
| 参数 | 默认值 | 说明 |
|---|---|---|
--base-url | http://127.0.0.1:3030 | 本地 screenpipe API 基址 |
--out-dir | ~/.screenpipe/diagnostics/memory-leak | 诊断输出目录 |
--process-name | screenpipe-app、screenpipe、screenpipe-engine | 可重复指定,追加匹配名 |
--pid | 无 | 精确指定跟踪的根 PID(含后代) |
--api-key | 无(或SCREENPIPE_API_KEY) | 携带 Bearer 认证头 |
--sample-interval-sec | 30.0 | 采样间隔 |
--scenario-duration-sec | 180.0 | 每个场景持续时长 |
--concurrency | 8 | 场景并发度(各场景内部还会二次钳制) |
--rss-threshold-mb | 8192.0 | RSS 绝对阈值 |
--growth-threshold-mb-per-hour | 512.0 | 小时增长斜率阈值 |
--snapshot-cooldown-sec | 3600.0 | 快照最小间隔 |
--request-timeout-sec | 30.0 | 单个请求超时 |
--include-frame-images | 关闭 | 开启帧图片重压(并发降为 1、limit≤20) |
--allow-audio-toggle | 关闭 | 开启音频启停切换(会干扰录音) |
--no-snapshots | 关闭 | 关闭阈值自动取证 |
10.2 推荐实战流程
- 前台冒烟:先用
run --duration-sec 900短跑 15 分钟,确认 screenpipe 正在运行、API 可达、场景无大量报错; - 24/7 常驻:
start启动守护进程,部署成开机任务或服务;期间用status观察守护进程与进程树状态; - 定期判读:
status --analyze检查斜率;斜率持续高于 512 MB/h 或 RSS 触及 8 GB 时,进入snapshots/阅读vmmap-summary.txt与sample.txt定位增长区域与热点栈; - 端点定位:用
leak_probe.py对嫌疑端点族做分相增量探测,结合 vmmap 桶差值缩小范围; - 库层复现:用第八节的
memory_pressure_test定向变体在screenpipe-db层复现并验证修复,跑通repeated_search_timeline_meeting_reads_do_not_grow_unbounded与concurrent_write_read_churn_stays_bounded即回归通过。
十一、适用前提与平台说明
- 压测对象是正在运行的本地 screenpipe 实例(桌面版或
screenpipe-engine服务),默认 API 端口3030;使用前请确认进程与端口已就绪; - 进程采样在 Linux/macOS 依赖
ps(fd 数 Linux 走/proc,macOS 回退lsof);Windows 依赖 PowerShell 的Get-Process/Get-CimInstance; vmmap、sample等取证工具仅在 macOS 可用(代码按平台条件执行);Windows 下取证使用 PowerShell 命令;- 时间线流式场景的 WebSocket 连接仅支持
http://基址(https下该场景直接返回失败); - 文章中的阈值(8 GB、512 MB/h)、默认数据规模(6000 帧、40 轮等)均为仓库当前代码的默认值,实际压测时应结合被测机器内存容量调整;
- 工具链全部基于 Python 标准库(
urllib、socket、concurrent.futures等),无需额外 pip 依赖即可运行。
十二、小结
整套 memory-leak 工具链给出了一个可复制的"长驻进程内存泄漏"治理闭环:黑盒 24/7 压测(leak_hunt.py)→ 阈值触发取证(vmmap/sample/lsof)→ 端点增量定位(leak_probe.py)→ 库层确定性复现(memory_pressure_test.rs)→ 修复后回归。其中"测量者不得污染被测对象"(帧图片默认关闭且并发受限)、"进程树聚合采样"(不遗漏子进程内存)、"双闸门判定"(绝对阈值 + 斜率)这三个设计点,对任何需要长期守护内存健康的桌面/服务型项目都具有直接的移植价值。相关实现均可直接在 scripts/memory-leak/、crates/screenpipe-db/tests/memory_pressure_test.rs 中继续深入阅读。
【免费下载链接】screenpipeYC (S26) | Open Computer History | Record your screen continuously locally and provide context to your agents (Claude, Codex, Openclaw, Hermes, Runner...)项目地址: https://gitcode.com/GitHub_Trending/sc/screenpipe
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考