- 即时通讯
- 后端
【免费下载链接】WuKongIM
More than just IM 不只是即时通讯(IM)
本文整理 WuKongIM 仓库内部维护的插件(plugin)迁移微基准基线文档 PLUGIN_BENCHMARK_BASELINE.md。读完后你将掌握三件事:如何在改动插件分发、host RPC 映射、PersistAfter/Send/Receive 钩子、NoPersist 实时投递或插件指标之前,用仓库给定的go test -bench命令复现同一套基准;如何在 Apple M4、go1.25.0 环境下记录的一组完整基准数据作为回归参照;以及如何按仓库明确的规则区分“真实的内存/分配退化”与“单次计时噪声”,避免误报性能回归。
一、基线文档的适用范围
基线文档开头明确了它的使用时机:在改动触及以下任何路径之前和之后,都应运行并对照这组基准:
- 插件分发(plugin dispatch);
- host RPC 映射(插件宿主侧的 RPC 转发);
- PersistAfter 钩子(消息落库提交后触发);
- Send 钩子与 Receive 钩子;
- NoPersist 实时投递(不落盘的一次性实时消息);
- 离线接收者(offline recipient)探测;
- 插件指标(plugin metrics)。
从源码结构看,这七个范围恰好对应仓库中四组独立的基准测试文件,文档把它们统一纳入同一套回归参照体系:
| 基线覆盖范围 | 对应基准测试文件 |
|---|---|
| PersistAfter 映射、Send 钩子、host RPC 映射、候选集选择 | internal/usecase/plugin/benchmark_test.go |
| 异步钩子队列的入队/队列打满行为 | internal/runtime/pluginhook/benchmark_test.go |
| 插件事件的深拷贝成本 | internal/contracts/pluginevents/benchmark_test.go |
| NoPersist 实时投递与提交后插件入队开销 | internal/runtime/channelappend/benchmark_test.go |
| 大接收者规模下的在线投递离线分类 | internal/runtime/delivery/runtime_benchmark_test.go |
| 指标观察者(Observer)本身的开销 | internal/app/plugin_observer_benchmark_test.go |
二、基线环境:Commit、机器与 Go 版本
文档记录基准来自以下环境(这是判断数据可比性的前提):
- 基线 Commit:
c6e42412a9ce test: add plugin benchmark baselines - Receive hook 扩展数据:记录自 2026-06-22 工作区(working tree),时间点位于 Receive hook 提交之前,属于同一天的补充采样;
- 记录日期:2026-06-22;
- 硬件平台:Apple M4,
darwin/arm64; - Go 版本:
go1.25.0。
当前仓库 go.mod 声明go 1.25.0(toolchaingo1.25.11),与基线记录一致。需要特别注意:这组 ns/op 数字只在同一台 M4 机器上具有直接可比性。跨机器、跨架构(例如 x86_64 服务器)对比时,只能把基线当作量级参照,回归结论必须在改动前后的同一台机器上重新测量。这一点与文档“Interpreting Regressions”一节的规则互为呼应。
三、基准命令:如何完整复现
文档给出三组命令,覆盖了全部六个基准文件。所有命令都使用-run '^$'(不跑普通测试)、-benchmem(报告分配数据)和-benchtime=3s(每个基准运行 3 秒)以保证统计稳定性。
3.1 插件包主组
go test ./internal/usecase/plugin ./internal/runtime/pluginhook ./internal/contracts/pluginevents ./internal/app -run '^$' -bench 'Benchmark(PersistAfter|Receive|SendMessageFromPluginReq|ChannelMessagesFromPluginReq|ClusterConfigFromSnapshot|ClusterChannelsBelongNode|ConversationChannels|HTTPForward|ListPlugins|SendPluginCandidates|BeforeSend|PluginHook|PluginMetricsObserver)' -benchmem -benchtime=3s注意-bench参数使用的是交替正则:PersistAfter会同时匹配BenchmarkPersistAfterMessageBatchMapping、BenchmarkPersistAfterCandidates、BenchmarkPersistAfterCommittedClone;Receive会匹配BenchmarkReceiveOffline、BenchmarkReceivePluginCandidates、BenchmarkReceiveOfflineClone、BenchmarkPluginHookReceiveEnqueue等。这正是文档把多个包合并进一条命令执行的原因——插件链路上的每个环节都有对应前缀。
3.2 channelappend 插件相关子集
go test ./internal/runtime/channelappend -run '^$' -bench 'Benchmark(SubmitLocalNoPersistRealtimeScoped|ChannelAppendPostCommitPlugin)$' -benchmem -benchtime=3s注意这里用了$锚定精确匹配,说明 channelappend 包内其他更大的压测(如多频道并行提交)不属于本基线范围,刻意排除以保持回归信号聚焦。
3.3 大接收者 Online Delivery 规范负载
go test ./internal/runtime/delivery -run '^$' -bench '^BenchmarkRuntimeBounded100KRecipientWorkload$' -benchmem -benchtime=3s这是文档指定的 canonical(规范)大接收者工作负载。后文会解释为什么大群回归覆盖必须集中在这一个基准上,而不能另起炉灶。
四、基线数据(2026-06-22,Apple M4,darwin/arm64,go1.25.0)
以下数据完整继承自 基线文档,是改动前后对照的参照值。
4.1 规范 Online Delivery 离线分类
| Benchmark | ns/op | B/op | allocs/op | Extra |
|---|---|---|---|---|
BenchmarkRuntimeBounded100KRecipientWorkload | 2714780 | 11010771 | 5865 | 100000 offline_recipients/workload |
这个单次“op”处理 10 万个离线接收者:39 个投递 plan(每 plan 256 人),全部经 presence 解析后被判定为离线并走OfflineRecipientsObserver路径。Extra列的100000 offline_recipients/workload由b.ReportMetric输出,直接验证了离线分类结果的数量正确性(见 runtime_benchmark_test.go#L113-L168)。
4.2 Plugin usecase
| Benchmark | ns/op | B/op | allocs/op |
|---|---|---|---|
BenchmarkPersistAfterMessageBatchMapping/payload_128 | 71.60 | 392 | 4 |
BenchmarkPersistAfterMessageBatchMapping/payload_1024 | 155.1 | 1288 | 4 |
BenchmarkPersistAfterMessageBatchMapping/payload_16384 | 1528 | 16648 | 4 |
BenchmarkSendMessageFromPluginReq/payload_128 | 56.76 | 176 | 2 |
BenchmarkSendMessageFromPluginReq/payload_1024 | 137.1 | 1072 | 2 |
BenchmarkSendMessageFromPluginReq/payload_16384 | 1401 | 16432 | 2 |
BenchmarkChannelMessagesFromPluginReq/items_1 | 133.6 | 528 | 8 |
BenchmarkChannelMessagesFromPluginReq/items_16 | 1802 | 7488 | 98 |
BenchmarkChannelMessagesFromPluginReq/items_128 | 14259 | 59584 | 770 |
BenchmarkClusterConfigFromSnapshot | 9742 | 47384 | 526 |
BenchmarkClusterChannelsBelongNode/items_1 | 136.7 | 256 | 7 |
BenchmarkClusterChannelsBelongNode/items_16 | 819.2 | 1792 | 36 |
BenchmarkClusterChannelsBelongNode/items_128 | 4047 | 11648 | 157 |
BenchmarkConversationChannels/items_1 | 39.64 | 136 | 3 |
BenchmarkConversationChannels/items_16 | 280.7 | 1216 | 18 |
BenchmarkConversationChannels/items_128 | 2027 | 9408 | 130 |
BenchmarkConversationChannels/items_1000 | 16144 | 72256 | 1002 |
BenchmarkHTTPForward/local_payload_128 | 3149 | 4184 | 58 |
BenchmarkHTTPForward/remote_payload_128 | 328.2 | 736 | 6 |
BenchmarkHTTPForward/local_payload_1024 | 3234 | 7848 | 58 |
BenchmarkHTTPForward/remote_payload_1024 | 371.5 | 1632 | 6 |
BenchmarkHTTPForward/local_payload_16384 | 8936 | 71208 | 58 |
BenchmarkHTTPForward/remote_payload_16384 | 1752 | 16992 | 6 |
BenchmarkHTTPForwardFanoutDeferred | 68.42 | 200 | 3 |
BenchmarkPersistAfterCandidates/plugins_1 | 113.9 | 360 | 4 |
BenchmarkPersistAfterCandidates/plugins_16 | 1356 | 5864 | 21 |
BenchmarkPersistAfterCandidates/plugins_128 | 14248 | 45800 | 133 |
BenchmarkPersistAfterCandidates/plugins_1024 | 135253 | 344306 | 1029 |
BenchmarkReceivePluginCandidates/bindings_1 | 233.4 | 520 | 5 |
BenchmarkReceivePluginCandidates/bindings_16 | 3076 | 9488 | 25 |
BenchmarkReceivePluginCandidates/bindings_128 | 26636 | 74128 | 137 |
BenchmarkReceivePluginCandidates/bindings_1024 | 250413 | 562745 | 1035 |
BenchmarkReceiveOffline/payload_128 | 705.9 | 983 | 11 |
BenchmarkReceiveOffline/payload_1024 | 1013 | 2871 | 11 |
BenchmarkReceiveOffline/payload_16384 | 6088 | 35527 | 11 |
BenchmarkListPlugins/plugins_1 | 85.36 | 336 | 3 |
BenchmarkListPlugins/plugins_16 | 858.1 | 5632 | 18 |
BenchmarkListPlugins/plugins_256 | 11708 | 86017 | 258 |
BenchmarkListPlugins/plugins_1024 | 50208 | 344068 | 1026 |
BenchmarkSendPluginCandidates/plugins_1 | 107.2 | 360 | 4 |
BenchmarkSendPluginCandidates/plugins_16 | 1339 | 5864 | 21 |
BenchmarkSendPluginCandidates/plugins_256 | 28093 | 86249 | 261 |
BenchmarkSendPluginCandidates/plugins_1024 | 129268 | 344306 | 1029 |
BenchmarkBeforeSend/no_candidates | 50.76 | 24 | 1 |
BenchmarkBeforeSend/one_plugin | 419.0 | 768 | 11 |
BenchmarkBeforeSend/chain_4 | 1569 | 3384 | 37 |
几个值得注意的读数规律:
- 映射类基准的分配几乎与 payload 大小无关:
PersistAfterMessageBatchMapping在 128 B 到 16 KB 三档 payload 下都是 4 次分配,说明映射路径对 payload 只做切片搬运而不做深拷贝(拷贝成本单独由合同层 Clone 基准度量,见 4.4 节); - 候选集基准呈近似线性增长:
plugins_1→plugins_1024,ns/op 从约 114 增长到约 135 000,allocs/op 从 4 增长到 1029,约对应每个插件 1 次分配——这是评估“插件注册表/选择逻辑”改动的核心信号; - HTTP 转发的 local 与 remote 差距:local 路径(经 Invoker 真正走 HTTP 路由)固定约 58 次分配,而 remote 路径(转发到远端节点)恒定 6 次分配,两者成本结构完全不同,回归时要分开看。
4.3 Plugin hook runtime
| Benchmark | ns/op | B/op | allocs/op |
|---|---|---|---|
BenchmarkPluginHookEnqueue | 799.9 | 1331 | 5 |
BenchmarkPluginHookQueueFull | 2263497 | 257 | 4 |
BenchmarkPluginHookReceiveEnqueue | 1060 | 1352 | 5 |
BenchmarkPluginHookReceiveQueueFull | 2222822 | 257 | 4 |
4.4 Plugin event contracts
| Benchmark | ns/op | B/op | allocs/op |
|---|---|---|---|
BenchmarkPersistAfterCommittedClone/payload_128/scoped_0 | 22.88 | 128 | 1 |
BenchmarkPersistAfterCommittedClone/payload_128/scoped_10 | 51.56 | 288 | 2 |
BenchmarkPersistAfterCommittedClone/payload_128/scoped_1000 | 1243 | 16512 | 2 |
BenchmarkPersistAfterCommittedClone/payload_1024/scoped_0 | 87.18 | 1024 | 1 |
BenchmarkPersistAfterCommittedClone/payload_1024/scoped_10 | 109.4 | 1184 | 2 |
BenchmarkPersistAfterCommittedClone/payload_1024/scoped_1000 | 1182 | 17408 | 2 |
BenchmarkPersistAfterCommittedClone/payload_16384/scoped_0 | 1045 | 16384 | 1 |
BenchmarkPersistAfterCommittedClone/payload_16384/scoped_10 | 1073 | 16544 | 2 |
BenchmarkPersistAfterCommittedClone/payload_16384/scoped_1000 | 2190 | 32768 | 2 |
BenchmarkReceiveOfflineClone/payload_128/scoped_0 | 33.25 | 128 | 1 |
BenchmarkReceiveOfflineClone/payload_128/scoped_10 | 76.76 | 288 | 2 |
BenchmarkReceiveOfflineClone/payload_128/scoped_1000 | 1903 | 16512 | 2 |
BenchmarkReceiveOfflineClone/payload_1024/scoped_0 | 147.4 | 1024 | 1 |
BenchmarkReceiveOfflineClone/payload_1024/scoped_10 | 195.8 | 1184 | 2 |
BenchmarkReceiveOfflineClone/payload_1024/scoped_1000 | 1925 | 17408 | 2 |
BenchmarkReceiveOfflineClone/payload_16384/scoped_0 | 1775 | 16384 | 1 |
BenchmarkReceiveOfflineClone/payload_16384/scoped_10 | 1750 | 16544 | 2 |
BenchmarkReceiveOfflineClone/payload_16384/scoped_1000 | 3880 | 32768 | 2 |
4.5 App 插件指标观察者
| Benchmark | ns/op | B/op | allocs/op |
|---|---|---|---|
BenchmarkPluginMetricsObserverSendInvoke | 117.8 | 128 | 4 |
BenchmarkPluginMetricsObserverReceiveEnqueue | 168.6 | 128 | 4 |
BenchmarkPluginMetricsObserverReceiveInvoke | 161.7 | 128 | 4 |
4.6 Channelappend 插件与 NoPersist 边界
| Benchmark | ns/op | B/op | allocs/op | Extra |
|---|---|---|---|---|
BenchmarkSubmitLocalNoPersistRealtimeScoped | 3301 | 5185 | 43 | 3.000 goroutine-delta |
BenchmarkChannelAppendPostCommitPlugin/disabled | 101.6 | 320 | 1 | |
BenchmarkChannelAppendPostCommitPlugin/enabled_enqueue | 105.1 | 320 | 1 |
goroutine-delta是b.ReportMetric输出的 goroutine 数量差,3.000 说明该基准路径没有 goroutine 泄漏;disabled与enabled_enqueue的 ns/op 几乎持平(101.6 vs 105.1),说明提交后插件入队的固定开销非常小。
五、源码级解读:这些数字从哪来
5.1 usecase 层:映射、发送与钩子候选集
benchmark_test.go 中的基准通过NewApp(Options{...})注入recordingRuntime、recordingInvoker等 fake 端口,把测量焦点收敛到 usecase 层自身的逻辑。以BenchmarkSendMessageFromPluginReq(第 46-81 行)为例,它构造带NoPersist/SyncOnce/RedDot头的SendReq,每轮调用app.SendMessage并校验返回的MessageId非零——因此测的是“插件发起一条消息”的完整 usecase 路径,而不含真实 RPC 与存储。
BenchmarkBeforeSend定义了no_candidates、one_plugin、chain_4三档钩子链长度,正好覆盖“没有插件参与”“单插件”“四插件链式执行”的相对成本;BenchmarkHTTPForwardFanoutDeferred则专门验证ToNodeId: -1的广播转发被延迟处理并返回ErrHTTPForwardFanoutDeferred且响应为 nil(第 248-271 行),是 fanout 延迟语义的基准化契约。
5.2 合同层:事件 Clone 的深拷贝语义
异步插件 worker 消费的是入队时深拷贝的事件副本,PersistAfterCommitted.Clone()与ReceiveOffline.Clone()的实现(types.go#L34-L38、L69-L73)只复制两个可变部分:
e.Payload = append([]byte(nil), e.Payload...) e.MessageScopedUIDs = append([]string(nil), e.MessageScopedUIDs...)合同基准用payload大小(128/1024/16384)×MessageScopedUIDs数量(0/10/1000)九宫格展开,正是为了分别暴露“payload 拷贝”和“scoped UID 切片拷贝”两个成本维度:scoped_0时恒定 1 次分配(仅 payload),scoped_10/scoped_1000时恒定 2 次分配。这也解释了为什么 usecase 层映射基准的 allocs/op 与 payload 大小无关——深拷贝被有意推到了入队边界。
5.3 pluginhook 运行时:有界准入、fail-open 与“队列打满”基准
worker.go 实现了有界插件钩子 worker,关键常量(第 14-26 行):
defaultQueueSize = 1024 defaultWorkers = 1 defaultInvokeTimeout = time.Second enqueueAdmissionTimeout = 2 * time.MillisecondEnqueuePersistAfter/EnqueueReceive/EnqueueReceiveBatch三个入口在入队前统一调用event.Clone()(第 237-250 行),入队采用两段式:先尝试无锁快路径tryEnqueueFast,队列满时进入带 2ms 超时器的select,超时即记为full并放弃该事件,绝不阻塞或拖垮调用方(第 252-287 行)。这就是 4.3 节两组 QueueFull 基准的由来:benchmark_test.go#L96-L124 用QueueSize: 1, Workers: 1加一个阻塞至 context 结束的 usecase 制造“队列永久打满”场景,ns/op ≈ 2.26ms恰好对应enqueueAdmissionTimeout的 2ms 上限加少量开销。换句话说,这组基线锁定的是一项可用性契约:插件侧再慢,消息提交路径的入队成本也不会超过约 2ms,代价是事件按 fail-open 语义丢弃。任何改动若让 QueueFull 的 ns/op 显著偏离 2ms 量级,或让 Enqueue 的 allocs/op 上升(当前 PersistAfter/Receive 各 5 次),都应按回归处理。
此外,worker 的Observer接口(第 57-63 行)记录每次入队与调用的结果和耗时,其真实实现pluginHookMetricsObserver的成本由 4.5 节三组观察者基准锁定——指标上报路径本身约 120~170ns、恒定 4 次分配,不会反向成为热点。
5.4 delivery 规范负载:10 万离线接收者分类
BenchmarkRuntimeBounded100KRecipientWorkload 构造 39 个 plan(每 plan 256 个接收者,共 100 000 个 UID),presence 解析器统一返回空结果(即全部离线),再由benchmarkOfflineRecipientsObserver累计接收者与回调次数,并在结尾断言b.N * 100000的精确数量。基准还通过b.ReportMetric输出recipients/workload、offline_recipients/workload、recipient_bytes/workload三项自定义指标。文档特别规定:大接收者的 Online Delivery 回归覆盖归属于这一个基准,不要在 channelappend 里另建 recipient-processor 基准——从源码看,channelappend 侧已有BenchmarkVeryLargeGroupRecipientDispatch覆盖“分页→权威分组→投递计划准入”的上游段,两者分段协作而不重叠,各自负责自己边界上的 CPU 与分配成本。
5.5 channelappend:NoPersist 实时投递与提交后插件
BenchmarkSubmitLocalNoPersistRealtimeScoped(benchmark_test.go#L147-L177)构造NoPersist=true、SyncOnce=true且带MessageScopedUIDs: ["u1"]的实时消息,经OnlineDeliveryEnqueuer计数型 fake 完成“提交→实时投递计划入队”的闭环,并在计时结束后校验 goroutine 差。文档明确要求该基准必须保留在 channelappend 子集中,因为命令式 NoPersist 实时投递的职责归属在 channelappend,而不是 plugin usecase。BenchmarkChannelAppendPostCommitPlugin的 disabled/enabled 两档则量化了“提交效果中挂载插件入队端口”的边际成本(约 3.5ns),为“提交路径接入插件钩子”这一迁移决策提供了成本证据。
六、回归判定规则(文档原文的完整继承)
基线文档给出的判定规则是这份数据的“使用说明”,应逐条遵循:
- 以 B/op 与 allocs/op 的持续增长作为强于单次 ns/op 变慢的信号。计时样本受机器状态影响大,分配模式退化则是结构性的;
- 宣布计时回归之前,必须在同一台机器上重跑同一条命令,排除偶发干扰;
- 候选集与列表类基准刻意包含
plugins_1024:修改插件注册表或选择逻辑时,不要删除该用例——它是最容易暴露每插件固定开销(分配、字符串拼接)的规模档; - Receive 候选基准刻意包含
bindings_1024:修改 UID 绑定选择逻辑时保留该用例; - 大接收者回归覆盖只归属于
BenchmarkRuntimeBounded100KRecipientWorkload,不要在 channelappend 重建 recipient-processor 基准; BenchmarkSubmitLocalNoPersistRealtimeScoped必须留在 channelappend 子集,因为命令式 NoPersist 实时投递由 channelappend 负责;- 如果一次有意的迁移导致分配增长,必须在同一提交中更新本文档,并写明实测权衡。
第 7 条意味着这份基线文档是“活”的:任何经评审接受的性能形态变化都要把新数字与理由写回文档,保持“文档数字 = 当前主干实测”的对应关系。
七、实操建议小结
- 改动前:在当前机器上用第三节三条命令各跑一轮,记录完整输出(含
-benchmem的 B/op、allocs/op 与 Extra 指标列)作为本地基线; - 改动后:原样重跑,优先对照 allocs/op 与 B/op,再看 ns/op;
- 对照 基线表格 时:跨机器只做量级比较,同机器才下结论;
- 保留文档标记的用例:
plugins_1024、bindings_1024、100K 工作负载、NoPersist scoped 基准都是回归检测的“探针”,删除它们等价于放弃对最坏规模档的监控; - 有意变更:更新基线文档与改动同提交合入,写明新数字与权衡依据。
这套“命令 + 数据 + 判定规则 + 用例保护条款”的组合,把一个容易流于形式的性能回归流程,固化成了可复制、可审计的工程契约。
- 即时通讯
- 后端
【免费下载链接】WuKongIM
More than just IM 不只是即时通讯(IM)
相关推荐
WuKongIM pkg/client 批量发送路径性能基线:分配预算、微基准命令与回归判读方法
WuKongIM pkg/client 批量发送路径性能基线:分配预算、微基准命令与回归判读方法 本文基于 WuKongIM 仓库的 pkg/client Be
即时通讯后端react-spring 从 Yarn 迁移 pnpm:迁移基线(Migration Baseline)方法论与基准数据实录
react spring 从 Yarn 迁移 pnpm:迁移基线(Migration Baseline)方法论与基准数据实录 本指南以 react spring
前端PySpark 基准测试实战:用 ASV 编写、运行与回归对比 PySpark 微基准
PySpark 基准测试实战:用 ASV 编写、运行与回归对比 PySpark 微基准 本指南以 Apache Spark 仓库中 python/benchma
大数据数据分析批处理流处理机器学习图计算
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考