mcpField
W
wherewent

wherewent

本地服务
github.com开发工具013 周前更新

关于 wherewent

wherewent 记录 Python 作业中的 SQLAlchemy 活动,并报告高成本查询模式、调用位置和修复建议,适合排查缓慢批处理作业,也支持分析未完成的运行。

主要功能

  1. 01无需改代码即可记录 SQL
  2. 02按查询形态分组并计数
  3. 03定位应用代码调用行
  4. 04基于规则生成发现与修复建议
  5. 05支持部分运行和异步 SQLAlchemy
  6. 06报告单元成本及变化趋势

适用场景

  1. 01查找拖慢批处理的重复快速查询
  2. 02发现逐行提交过于频繁
  3. 03不中断长时间作业并检查运行情况
  4. 04识别处理单元增加时上升的查询成本

MCP 相关

其 stdio MCP 服务提供 `analyze_job`,用于运行并分析作业;`explain_run` 可读取已保存 JSON 报告中的发现。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"habibafaisal-wherewent": {
"command": "uvx",
"args": [
"wherewent"
]
}
}
}
启动命令uvx wherewent

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
pypiwherewent0.4.1

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

wherewent

时间都去哪儿了?一条命令,马上查明。

零配置记录器,帮你解答“为什么这个 Python 批处理任务要跑这么久?”

在 shell 中运行——也可以将它用作 MCP 服务,由 AI Agent 直接调用。

PyPI 版本 Python 版本 许可证:MIT CI

wherewent run python your_job.py

耗时 5 分钟的 0.4ms 查询

单次查询可能很快——只要 0.4ms——但仍会拖垮你的任务,因为它在一行代码中被调用了 500,000 次。你的应用在往返通信上耗费了 300 秒,而 Postgres 本身只运行了 80 秒。你试过的每个性能分析器都只告诉你*“时间花在 psycopg 上”*,然后就没下文了。

wherewent 会展示调用模式。 它按查询形状分组,统计每种形状的运行次数,汇总总耗时,并指出你自己的代码中触发查询的确切 file:line——然后用通俗易懂的语言和清楚的计算过程告诉你该怎么做。

====================================================================================================
wherewent — SQL flight recorder
----------------------------------------------------------------------------------------------------
wall: 26.15s   cpu: 25.41s (97% CPU busy)   queries: 20,004   commits: 20,001   rollbacks: 1
in-DB time: 5.46s (20.9% of wall; app-observed: includes network+driver+server)
commit time: 9.06s   total rows: 20,000
recording added ~1.81s (~6.9% of wall)
====================================================================================================
QUERY GROUP                                         CALLS     TOTAL     MEDIAN  CALL SITE
----------------------------------------------------------------------------------------------------
INSERT INTO events (name, value) VALUES (?, ?)     20,000     5.46s     0.24ms  demo/naive_job.py:65 in main
SELECT count(*) AS count_1 FROM events                  1     0.00s     0.16ms  demo/naive_job.py:71 in main
====================================================================================================
FINDINGS
----------------------------------------------------------------------------------------------------
1. [R1+R2] commit-per-row loop
   20,000 calls x 0.24ms median ~= 5.5s = 21% of 26.1s wall, at demo/naive_job.py:65. Batch it.
   20,001 commits for 20,000 rows (1.0 rows/commit), 9.1s in commit = 35% of wall. Batch to 1,000+ rows/txn.
   ~= 14.5s attributable
====================================================================================================

与众不同之处

采样分析器APM / 链路追踪wherewent
无需修改代码✅❌✅
按查询形状分组❌⚠️✅
定位到你的调用位置⚠️⚠️✅
告诉你如何修复❌❌✅
随处运行,无需服务端✅❌✅
支持按 Ctrl-C 中断后的部分运行❌⚠️✅

安装

pip install wherewent

就这样——记录器只使用标准库。你只需要 SQLAlchemy,因为你的任务已经在用它了。

使用方法

用它包装任意命令。你的脚本会完全不做修改地运行——无需导入、装饰器或配置:

wherewent run python your_job.py --some arg
wherewent run python -m your_package
wherewent run --save run.json python your_job.py   # also dump machine-readable JSON
  • 报告会在退出时输出到 stderr;任务自身的 stdout/stderr 会原样通过。
  • 按 Ctrl-C 仍会生成报告。 采集一个 14 小时任务最初 5 分钟的数据是主要用例——重点就是获取部分数据。
  • 无需停止即可查看。 发送 SIGUSR1(kill -USR1 <pid>)可在运行期间获取部分快照;也可以设置 WHEREWENT_INTERVAL=30,每隔 30 秒输出一次。任务会继续运行。
  • 支持异步 SQLAlchemy。 查询在 greenlet 中运行,堆栈上没有用户帧,因此简单的堆栈遍历会无法定位;wherewent 仍能将其归因到实际调用位置(AsyncSession / AsyncConnection)。
  • 绝不会导致任务崩溃或数据损坏。 每个钩子函数体都经过包装,记录器即使失败也会静默处理,不会中断运行。
  • 绝不会记录你的数据。 只保留查询形状和计数——在存储任何内容之前,都会剥离字面值和绑定参数。
试试内置演示
git clone https://github.com/habibafaisal/wherewent && cd wherewent
pip install -e ".[dev]"
wherewent run python demo/naive_job.py     # watch the R1+R2 finding fire
python demo/benchmark.py                    # naive vs fixed, with the overhead gate

将其用作 MCP 服务(原生支持 Agent)

wherewent 提供 Model Context Protocol(MCP)服务,因此 AI Agent 可以在任务变慢时立即直接调用它,并获取机器可读的发现结果 ——无需读取原始查询日志并自行推理得出相同结论。它已列入官方 MCP Registry,名称为 io.github.habibafaisal/wherewent。

使用 [mcp] 附加项安装(它会引入 MCP SDK;核心记录器仍仅依赖标准库), 然后运行 stdio 服务:

uvx --from 'wherewent[mcp]' wherewent-mcp
# or:  pip install 'wherewent[mcp]'  &&  wherewent-mcp

传输方式: stdio。提供的工具:

MCP 工具功能
analyze_job(command, unit_function?, timeout_s=600)在 wherewent 下运行 Python/SQLAlchemy 任务,并以结构化字段返回任务变慢的原因——确切调用位置、查询数量和修复建议。超时后会返回部分结果(timed_out: true)。
explain_run(path)返回已由 wherewent run --save 生成的 JSON 文件中的增强版发现结果——无需重新运行。

每项发现都包含 fix、call_site、calls、wall_fraction,以及 Agent 可据此采取行动并引用的 evidence 对象。通过配置将其接入任意 MCP 客户端(例如 Claude Desktop):

{
  "mcpServers": {
    "wherewent": {
      "command": "uvx",
      "args": ["--from", "wherewent[mcp]", "wherewent-mcp"]
    }
  }
}

仓库根目录下的 Dockerfile 可为基于容器的 MCP 主机构建相同的 stdio 服务。

为工作单元命名

“81,749 次查询”很难判断。**“每个应收账款 135 次查询”**能让工程师 立刻看出架构存在过多请求。为任务处理的工作单元命名,wherewent 就会报告 单个工作单元的成本情况——中位时长、每个单元的查询数/提交数/行数,以及 成本随运行推进的变化趋势:

# Zero-config: name a function; every top-level call is one unit
wherewent run --unit-function myapp.jobs:process_receivable python run.py
# Or mark the unit in code (same machinery, same report)
import wherewent
for receivable in book:
    with wherewent.unit("receivable"):
        process(receivable)
UNIT: myapp.jobs:process_receivable   (1,203 units)
----------------------------------------------------------------------------------------------------
  median duration    341 ms         queries/unit    135 (median)
  commits/unit       1.0            rows/unit       46.0
  GROWTH
    units 1–100          220 ms/unit
    units (last 100)     379 ms/unit
    queries 1–100        98 queries/unit
    queries (last 100)   171 queries/unit
    trend                +72% slower over the run
    query trend          +74% more queries/unit over the run     ← R6 fires

R6 在任一趋势斜率触发时都会生效。这对计算密集型任务很重要:如果运行时间保持不变,但 每个单元的查询数不断增加,时长趋势会显示为 flat,只有查询趋势能揭示问题—— 因此 wherewent 会同时报告两种趋势,并明确指出这种模式存在可扩展性风险,而非当前的 运行时瓶颈。

增长趋势也说明了为什么采样运行更能反映真实情况:它会显示每个单元的成本不断上升, 因此你会知道完整运行的实际成本会高于线性外推结果——而仅统计总量的分析器 永远无法告诉你这一点。即使异步单元并发运行,每个单元的计数也都准确;记录的内容仅限 模式和计数。

工作原理

  1. 通过 PYTHONPATH 中的 sitecustomize 垫片将自身注入目标进程——无需 修改代码,也无需导入包装器。
  2. 在类级别监听——event.listen(sqlalchemy.engine.Engine, ...)——因此 会自动捕获应用创建的所有引擎,无需配置。
  3. 将每条语句规范化为一个查询组:字面量、绑定参数、IN 列表 和多行 VALUES 都会合并,因此一百万条不同的插入语句会汇总成一条如实的记录。
  4. 通过沿调用栈跳过库代码帧,将调用位置解析到你自己的代码中的首行——开销很低: 按文件名缓存,每个组仅对前 5 个样本收集完整调用栈,因此热路径开销足够低,能满足其预算。
  5. 根据三条规则触发确定性发现,每条规则都会展示其计算过程。
发现引擎
规则触发条件说明
R1 — 频繁调用组调用次数 > 1,000,耗时占墙钟时间 > 10%,中位数 < 5ms快速查询被调用得太频繁——将其批量处理(executemany / IN 列表 / JOIN)。
R2 — 每行提交提交次数 > 100,每次提交的行数 < 10,提交耗时占墙钟时间 > 5%你在逐行提交——将每个事务的批量大小增至 1,000 行以上。
R3 — 受数据库等待限制数据库内耗时占墙钟时间 > 60%,CPU 忙碌时间 < 30%该任务受往返延迟限制,而非计算能力限制。
R4 — 同时出现的模式≥ 2 个查询组由同一个函数触发,且该模式会扩展——在多次迭代中,每次迭代都有许多查询,或者排除一次性设置后耗时占墙钟时间 > 10%每次迭代都会一起触发多个查询(SELECT + UPDATE + INSERT)——将它们合并为一次往返。按函数而非行聚类,因此即使某个辅助函数在三行不同的代码中执行语句,也仍会被视为一个操作。排除只执行一次的语句(calls == 1)——它们属于固定开销,由 R5 检查。报告估算的每次迭代查询数,即使有界运行的计时掩盖了某些模式,也会标记可扩展的模式。
R5 — 一次性高开销语句单条 calls==1 语句耗时占墙钟时间 > 15% 或绝对耗时 > 10s一条语句就产生了巨大的固定开销。R1/R3/R4 都在查找频繁调用,因此会漏掉它——R5 则能捕获最值得优化的单行代码。绝对耗时下限很重要:无论 20s 占采样运行的 24%,还是占完整运行的 1%,都值得优化。
R6 — 单位成本上升从前 100 个单位到最后 100 个单位,每单位耗时 或 查询数/单位上升 ≥ 1.5 倍(需要 --unit-function/wherewent.unit())随着运行推进,每个项目的成本不断增长——可能是状态累积、历史读取未批量处理,或列表在每轮循环中不断增长。报告增长斜率(早期与后期的查询数/单位),因此即使任务受计算能力限制,只要查询成本在增长,也能被发现。

具有相同根本原因的发现会合并(例如 R1+R2);耗时占墙钟时间不足 5% 的发现会被隐藏,最多显示排名靠前的 3 项——按其所占用的秒数排序。R4 能捕获单组阈值无法发现的情况:N+1 模式分散在 SELECT + UPDATE + INSERT 中,单独来看都不显眼,却会在每次循环中作为一个整体触发。从 v0.3 开始,即使一次性设置开销让模式在短时采样运行中看起来很小,只要模式可扩展,R4 也会触发。

**每个数字都真实可信。**查询耗时会标注为应用观测值(包含网络、驱动程序和服务器耗时——不只是 Postgres)。任何无法测量的内容都会显示 —,绝不会猜测。wherewent 甚至会测量自身钩子的耗时,并报告它增加的开销。

路线图 — 欢迎贡献 🙌

wherewent 的设计目标是超越 SQLAlchemy。它的八个模块中,有七个—— 规范化、调用点解析、统计模型、规则引擎、报告、CLI 和注入垫片——已经与框架无关。它们处理的是纯粹的 RunSnapshot 查询事件快照。只有绑定 SQLAlchemy 事件系统的 recorder.py 与框架相关。

**这意味着新增后端是一项边界清晰的贡献:**从另一个驱动捕获查询 开始/结束/行数/事务事件,传入相同的 RunSnapshot,整个发现与报告流程便可直接复用。适合作为首个后端的选项:

  • Async SQLAlchemy — 通过 greenlet 边界进行调用点归因 (v0.2.0)
  • 感知工作单元的性能分析 — 按单元统计开销 + 增长趋势 (v0.3.0)
  • 执行模式发现 (下一个重要功能 — 欢迎贡献) — 目前 wherewent 会对每次迭代中一起触发的查询进行聚类(R4)。下一步:重建这些查询背后有序的、 可能嵌套的工作流并为其命名,例如
    For each receivable:
      For each audit event ×23:
        SELECT chain_state → SELECT payload → INSERT payload → INSERT audit_event → UPDATE chain_state
    Finding: serialized audit-append loop — 23 repetitions/receivable, ≈115 statements/receivable,
             58% of DB activity, at process_receivable → emit_firing → append_event.
    
    这是真正超越普通 N+1 检测的一步(Sentry/Scout 会发现重复的单一形态 查询;此功能则能发现跨越多种 SQL 形态和函数的多操作工作流): 读取→修改→写入循环、每个条目依次序列化→插入→提交、每次变更后都获取完整状态快照、不断增长的历史记录扫描,以及随条目位置上升的 CPU 开销。需要按单元记录有序事件日志 + 挖掘重复子序列,并将开销控制在门槛以内。
  • 原生 psycopg / psycopg2 — 游标子类或连接工厂钩子
  • 原生 asyncpg(SQLAlchemy 之外)— 异步执行路径
  • Django ORM — 通过 connection.execute_wrapper
  • 通用 DB-API 2.0 — 无需 monkeypatch 的 Cursor 代理
  • 更多发现规则(锁等待、顺序扫描启发式规则)

请参阅 CONTRIBUTING.md,了解后端接口约定,以及每条捕获路径都必须通过的 < 15% 开销 门槛。

当前的局限

  • SQLAlchemy 2.x(同步和异步 ORM/Core;1.4 可能也能用)。目前尚未归因 SQLAlchemy 之外的原始 asyncpg 调用。
  • 单进程——不支持多进程扇出。
  • 查询耗时按应用观察结果计算(网络 + 驱动程序 + 服务端),这是有意为之。
  • 提交耗时通过包装方言的提交操作获取;如果包装失败,则显示 —。
  • 每次迭代的比率是估算值(标记为 ≈),根据同时出现的查询数量推断——只有信号明确时才显示,绝不猜测。
  • 按单元统计的数量(--unit-function / wherewent.unit())即使在并发异步单元下也准确;按单元统计的耗时是墙钟时间,单元并发运行时可能重叠——常见的顺序循环场景下则准确。
  • R6 归因的秒数是确定性的下限估算值,不是测量值。每个单元的超额查询数按本次运行的单次查询平均 DB 耗时折算,因此,如果额外查询比平均值更快,真实成本就更高(反之亦然)。它根据准确的整数查询数量计算,而非读取时钟,因此每次运行的结果都可复现——但 R6 关注的是斜率,而不是秒数。
  • ORM flush 归因。 session.flush()/commit() 发出的所有查询都会归到同一个调用位置,因此 R4 可能会把无关写入归为同一个“工作流”。当某个集群的写入共用一行源码时,wherewent 会将其标记为可能来自单次 flush,而不会声称你可以合并它们——它不会建议你批量处理已经批量处理过的内容。
  • 每组中位数是有上限的样本中位数(每组最多保留 5,000 次执行),因此即使运行包含数百万次查询,内存占用也能保持稳定。calls 和 total_time 仍然是准确值。
  • 提交耗时和回滚耗时会分别报告。 SQLAlchemy 的连接池会在每次连接归还时执行回滚,因此回滚耗时会标记为*(包括连接池重置)*,绝不会计入提交耗时。
  • 分析结果会说明耗时所在位置及其扩展方式——如果运行受 CPU 限制,就会明确指出,而不是暗示修复 SQL 就能加快本次运行。

这些是验证原型需要坦诚面对的边界,并非永久限制——请参阅路线图。

贡献

非常欢迎贡献——包括新后端、新规则、文档和错误报告。请先阅读 CONTRIBUTING.md,对于任何较大的改动,请先提交 issue 讨论,并在推送前运行 pytest && python demo/benchmark.py。

许可证

MIT © 2026 Habiba Faisal

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。