screenpipe 内存泄漏狩猎指南:24/7 压力循环压测与诊断工具链深度解析
2026/9/13 18:57:22 网站建设 项目流程

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.pyrun(前台)模式下同时做两件事,对应两条并行的工作线:

  1. 采样线(sampler thread):每 30 秒(--sample-interval-sec,默认 30.0)采样一次screenpipe-appscreenpipescreenpipe-engine三者中内存占用最大的进程树的 RSS/CPU/句柄数/线程数/fd 数,并滚动写入samples.jsonlsamples.csv。实现见 leak_hunt.py 的 sampler_loop。
  2. 压力线(主线程):不断轮转 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):

  • 关键词screenpipemeetinggithubslackcustomererrormemoryaudiotodopricing
  • content_typeall/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)

先通过/searchcontent_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-enginebunmcp-helper等子进程),简单采样单一 PID 会严重漏计。leak_hunt.py实现了递归进程树聚合:以匹配进程为根,沿 PPID 关系收集全部后代,累计得到tree_rss_kbtree_private_kbtree_vsz_kbtree_pcputree_handle_counttree_thread_countdescendant_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>(线程列表);
  • WindowsGet-Process -Id <pid> | Format-List *Get-CimInstance Win32_Process进程树 JSON;
  • 每个快照附带snapshot.json元数据(时间戳、PID、触发原因、产物路径)。

这些产物对事后分析"内存在哪个子系统里涨"至关重要:vmmap -summary能定位到MALLOC_SMALLIOAccelerator(图形)等具体区域,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_FRAMES6000预置 OCR 帧数
SCREENPIPE_PRESSURE_AUDIO1000预置音频转写段数
SCREENPIPE_PRESSURE_UI4000预置 UI 事件数
SCREENPIPE_PRESSURE_MEETINGS60预置会议数(每个含 20 条转写段)
SCREENPIPE_PRESSURE_ROUNDS40读压力轮数
SCREENPIPE_PRESSURE_MAX_RSS_GROWTH_MB1024RSS 增长断言上限
SCREENPIPE_PRESSURE_CHURN_SECONDS60写读抖动持续秒数
SCREENPIPE_PRESSURE_READERS4并发读者数
SCREENPIPE_PRESSURE_WRITER_SLEEP_MS0写者每次插入后的睡眠毫秒数

两个用例各有侧重:

  1. 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

  2. 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)IOSurfaceMALLOC_SMALLMALLOC_LARGEWebKit MallocDefaultMallocZone等),输出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.jsonsummary.csv与各阶段前后vmmap-<phase>-before/after.txt。每阶段终端会打印rss 增量 / fd 增量 / ops / errors / 读取字节数及变化最大的 4 个 vmmap 桶——例如某阶段MALLOC_LARGE +42.0MB,就把怀疑范围从"全局泄漏"缩小到"大块分配路径"。

十、参数速查表与推荐实战流程

10.1 公共参数速查(run/start均支持)

参数默认值说明
--base-urlhttp://127.0.0.1:3030本地 screenpipe API 基址
--out-dir~/.screenpipe/diagnostics/memory-leak诊断输出目录
--process-namescreenpipe-appscreenpipescreenpipe-engine可重复指定,追加匹配名
--pid精确指定跟踪的根 PID(含后代)
--api-key无(或SCREENPIPE_API_KEY携带 Bearer 认证头
--sample-interval-sec30.0采样间隔
--scenario-duration-sec180.0每个场景持续时长
--concurrency8场景并发度(各场景内部还会二次钳制)
--rss-threshold-mb8192.0RSS 绝对阈值
--growth-threshold-mb-per-hour512.0小时增长斜率阈值
--snapshot-cooldown-sec3600.0快照最小间隔
--request-timeout-sec30.0单个请求超时
--include-frame-images关闭开启帧图片重压(并发降为 1、limit≤20)
--allow-audio-toggle关闭开启音频启停切换(会干扰录音)
--no-snapshots关闭关闭阈值自动取证

10.2 推荐实战流程

  1. 前台冒烟:先用run --duration-sec 900短跑 15 分钟,确认 screenpipe 正在运行、API 可达、场景无大量报错;
  2. 24/7 常驻start启动守护进程,部署成开机任务或服务;期间用status观察守护进程与进程树状态;
  3. 定期判读status --analyze检查斜率;斜率持续高于 512 MB/h 或 RSS 触及 8 GB 时,进入snapshots/阅读vmmap-summary.txtsample.txt定位增长区域与热点栈;
  4. 端点定位:用leak_probe.py对嫌疑端点族做分相增量探测,结合 vmmap 桶差值缩小范围;
  5. 库层复现:用第八节的memory_pressure_test定向变体在screenpipe-db层复现并验证修复,跑通repeated_search_timeline_meeting_reads_do_not_grow_unboundedconcurrent_write_read_churn_stays_bounded即回归通过。

十一、适用前提与平台说明

  • 压测对象是正在运行的本地 screenpipe 实例(桌面版或screenpipe-engine服务),默认 API 端口3030;使用前请确认进程与端口已就绪;
  • 进程采样在 Linux/macOS 依赖ps(fd 数 Linux 走/proc,macOS 回退lsof);Windows 依赖 PowerShell 的Get-Process/Get-CimInstance
  • vmmapsample等取证工具仅在 macOS 可用(代码按平台条件执行);Windows 下取证使用 PowerShell 命令;
  • 时间线流式场景的 WebSocket 连接仅支持http://基址(https下该场景直接返回失败);
  • 文章中的阈值(8 GB、512 MB/h)、默认数据规模(6000 帧、40 轮等)均为仓库当前代码的默认值,实际压测时应结合被测机器内存容量调整;
  • 工具链全部基于 Python 标准库(urllibsocketconcurrent.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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询