OBS Studio Source Profiler(source_profiler)深入解析:libobs 中诊断单个 Source 性能的完整 API 指南
本文以 OBS Studio 官方文档 docs/sphinx/reference-libobs-util-source-profiler.rst 为核心,结合 libobs/util/source-profiler.h 与 libobs/util/source-profiler.c 的实现源码,系统讲解 source profiler 的数据结构、四个公开 API 函数、5 秒采样窗口的产生机制以及 GPU 计时的底层原理。读完本文后,你可以在自研插件或前端中按 source 粒度定位 tick/render 耗时瓶颈,并正确理解每个结果字段的含义与精度限制。
1. Source Profiler 是什么:按 Source 粒度的性能诊断
OBS Studio 的 libobs 内置两套性能分析设施:
- 通用 Profiler(
util/profiler.h):面向整个程序的性能与效率分析,提供 profile 节点树、快照与 CSV 导出,见 Profiler 文档; - Source Profiler(
util/source-profiler.h):本文主题。它专门回答一个更细的问题——某个具体 source(场景项)自身的 tick 与 render 函数花了多少时间,以及异步 source(如媒体源)的输入/渲染帧率是否稳定。
官方文档给出的定位是:
The source profiler is used to get information about individual source's performance.
它的价值在于:当预览掉帧或推流卡顿发生时,通用 profiler 只能告诉你“某帧整体偏慢”,而 source profiler 能指出是哪个 source 的 tick 或渲染 pass 拖慢了一帧。
头文件位于 source-profiler.h,实现位于 source-profiler.c。使用方只需:
#include <util/source-profiler.h>
2. 核心数据结构 profiler_result_t
文档定义的结果结构 profiler_result(即 profiler_result_t)是所有 API 的输出载体。官方文档对每个字段组的说明如下,这里逐组继承并结合当前头文件源码(source-profiler.h)补全:
| 字段组 | 文档语义 | 源码注释与补充 |
|---|---|---|
tick_avg / render_avg |
采样时间窗(5 秒)内该 source tick 与 render 函数执行的平均耗时(ns) | 注意:tick 每帧只执行一次,但 render 可能一帧执行多次(source 被多个 display/输出同时绘制时) |
tick_max / render_max |
采样窗内 tick 与 render 的最大耗时(ns) | 用于发现偶发尖峰,比平均值更能暴露卡顿 |
render_gpu_avg / render_gpu_max |
GPU 渲染执行的平均/最大耗时 | macOS 不支持 GPU 计时;且由于 GPU 负载/时钟频率波动,精度有限 |
render_sum / render_gpu_sum |
一帧内该 source 所有 CPU/GPU 渲染 pass 耗时的总和,再对采样窗取平均 | 文档示例:若某 source 每帧被渲染 2 次、render_avg 恒为 1000000(1 ms),则 render_sum 为 2000000(2 ms) |
async_fps(文档名) |
由 obs_source_output_video2() 提交的异步帧时间差计算出的帧率 |
仅对异步 source 有效(如 Media Source) |
需要指出的一点:文档中的 double async_fps 字段在当前源码中已扩展为更细粒度的四个字段(见 source-profiler.h):
typedef struct profiler_result {
/* Tick times in ns */
uint64_t tick_avg;
uint64_t tick_max;
/* Average and max render times for CPU and GPU in ns */
uint64_t render_avg;
uint64_t render_max;
uint64_t render_gpu_avg;
uint64_t render_gpu_max;
/* Average of the sum of all render passes in a frame in ns
* (a source can be rendered more than once per frame). */
uint64_t render_sum;
uint64_t render_gpu_sum;
/* FPS of submitted async input */
double async_input;
/* Actually rendered async frames */
double async_rendered;
/* Best and worst frame times of input/output in ns */
uint64_t async_input_best;
uint64_t async_input_worst;
uint64_t async_rendered_best;
uint64_t async_rendered_worst;
} profiler_result_t;
即当前实现同时给出输入侧帧率(async_input,由提交帧的时间戳差计算)与实际被渲染侧帧率(async_rendered,由渲染时读到的最后异步时间戳计算),以及各自的最好/最差帧间隔。这一区分能帮你判断:媒体源是“解码/解码提交掉帧”,还是“提交正常但渲染侧丢帧”。
关于所有耗时字段:单位为纳秒(ns),采样窗口固定为 5 秒(见下一节源码说明)。
3. 四个公开 API 函数
文档定义了四个 EXPORT 函数,签名与 source-profiler.h 完全一致:
3.1 source_profiler_enable(bool enable)
开启/关闭 source profiler。从源码 source-profiler.c#L223-L226 可以看到,该调用只是设置 volatile 标志 enable_next,真正的开关在下一帧由图形线程的 source_profiler_frame_begin() 应用:
void source_profiler_enable(bool enable)
{
enable_next = enable;
}
因此文档强调 “The profiler will then start or stop collecting data with the next rendered frame”,并且开启后会有轻微性能开销(每个 source 的 tick/render 都会多一次 os_gettime_ns() 采样与哈希表读写)。
3.2 source_profiler_gpu_enable(bool enable)
开启/关闭 GPU 计时(macOS 不可用)。注意源码中的联动逻辑(source-profiler.c#L228-L231):
void source_profiler_gpu_enable(bool enable)
{
gpu_enable_next = enable && enable_next;
}
GPU 计时的前提是主 profiler 已启用——先调用 source_profiler_enable(true),GPU 计时才会随之在下一帧生效。文档也明确 GPU profiling 的性能影响更大(需要创建/同步 GPU timer,见第 5 节)。
3.3 source_profiler_get_result(obs_source_t *source)
返回指定 source 的 profiler_result_t *;结果必须由调用者用 bfree() 释放,无数据时返回 NULL。实现(source-profiler.c#L622-L630)就是分配一块内存后转发给 source_profiler_fill_result:
profiler_result_t *source_profiler_get_result(obs_source_t *source)
{
profiler_result_t *ret = bmalloc(sizeof(profiler_result_t));
if (!source_profiler_fill_result(source, ret)) {
bfree(ret);
return NULL;
}
return ret;
}
3.4 source_profiler_fill_result(obs_source_t *source, profiler_result_t *result)
将结果填充进调用者预先准备好的结构体,返回 true 表示该 source 存在数据,false 表示不存在。文档明确指出这个函数存在的原因:避免每次查询都分配新内存。对于需要高频轮询(例如每 500 ms 刷新一次面板)的前端,推荐做法是常驻一个 profiler_result_t 对象反复填充。实现见 source-profiler.c#L594-L620:内部持读锁(pthread_rwlock_rdlock)遍历哈希表,调用 calculate_tick / calculate_render / calculate_fps 三个静态函数计算平均值与最大值,异步 source 额外计算两组 fps。
4. 采样窗口为什么是 5 秒:从 source_profiler_reset_video 看实现
文档中反复出现的 “sampled timeframe (5 seconds)” 并非魔法常数,而是由视频帧率推导的。调用链是 obs_reset_video()(obs.c#L1594)→ source_profiler_reset_video():
void source_profiler_reset_video(struct obs_video_info *ovi)
{
double fps = ceil((double)ovi->fps_num / (double)ovi->fps_den);
profiler_samples = (uint64_t)(fps * 5);
/* This is fine because the video thread won't be running at this point */
profiler_shutdown();
}
(source-profiler.c#L233-L240)即:采样容量 = ceil(帧率) × 5 帧。以 60 fps 输出为例,每个 source 的统计环形缓冲(ucirclebuf)恰好容纳 300 帧的历史数据,因此结果天然是一个“最近 5 秒”的滚动窗口。这也意味着:
- 切换视频设置(
obs_reset_video)会清空全部采样数据,需要重新积累最多 5 秒才有完整统计; - 窗口是滚动式的,源刚创建的前几秒内平均值只基于已有样本(源码中
num < capacity时照常求平均)。
5. 数据是如何被采集的:渲染主循环中的六个挂点
Source profiler 不依赖你手动埋点,它的采样点直接织入 libobs 的渲染主循环。梳理 obs-video.c 中 obs_graphics_thread_loop(obs-video.c#L1094-L1144)可以还原完整流程:
- 帧开始:
source_profiler_frame_begin()(L1102)——应用enable_next/gpu_enable_next状态,并按FRAME_BUFFER_SIZE轮转 GPU timer range 索引; - Tick 阶段:
tick_sources()中逐个 source 调用source_profiler_source_tick_start()/source_profiler_source_tick_end(s, start)(obs-video.c#L81-L83),每次 tick 用os_gettime_ns()掐表。由于每帧每个 source 只 tick 一次,这正对应文档“tick 每帧仅一次”的说明; - 渲染阶段:
source_profiler_render_begin()(L1120)启动帧级 GPU timer range;每个 source 实际绘制时,obs_source_render_async_video等路径调用source_profiler_source_render_begin(&timer)创建并启动一个gs_timer_t,绘制结束后由source_profiler_source_render_end()收尾(obs-source.c#L2602、obs-source.c#L2699)。一次渲染一帧可能多次,这正是render_sum与render_avg存在差异的原因; - 帧收尾:
source_profiler_render_end()(L1128)结束帧级 timer range;source_profiler_frame_collect()(L1134)把上一帧的原始样本压入各 source 的统计环形缓冲。
第 4 步内部还有两个值得注意的机制(source-profiler.c#L284-L396):
- 两级缓冲:原始逐帧样本(
struct source_samples)以FRAME_BUFFER_SIZE帧为周期环形存放,而FRAME_BUFFER_SIZE直接取自渲染纹理缓冲NUM_TEXTURES(定义为 2,见 obs-internal.h#L49),注释写明“Buffer frame data collection to give GPU time to finish rendering”——GPU 是异步的,本帧的 timer 数据要等 1~2 帧后才能安全读取,因此处理延迟FRAME_BUFFER_SIZE - 1帧; - disjoint 检测:DirectX 计时器在 GPU 负载剧变时会报告 disjoint,此时源码会打印警告并丢弃该轮 GPU 样本(source-profiler.c#L303-L305):
blog(LOG_WARNING, "GPU Timers were disjoint, discarding samples.")。这是文档所说“GPU timing ... of limited accuracy”的具体来源之一。
另外两处与生命周期相关的挂点:
- 异步帧提交:
obs_source_output_video_internal(obs_source_output_video()/obs_source_output_video2()的公共入口,见 obs.h#L1418-L1422)一收到帧就调用source_profiler_async_frame_received(source)记录时间戳(obs-source.c#L3614),这是async_inputfps 的数据来源; - source 释放:
obs_source_release将对象销毁时,source_profiler_remove_source会在图形线程上调度删除任务(obs_queue_task(OBS_TASK_GRAPHICS, ...),source-profiler.c#L500-L506),避免在非图形线程碰 GPU 资源。
整个模块用 uthash 哈希表 + pthread_rwlock 组织:hm_samples 存逐帧原始样本,hm_entries 存统计环形缓冲,查询走读锁、采集走写锁,因此查询可以在任意线程安全调用,这为“前端定时轮询”的用法提供了基础。
6. 实战用法:在插件/前端中轮询单个 Source 的性能
综合上述 API 约束(下一帧生效、bfree 释放、可复用结构体),一个最小可用示例如下(以 C 写,C++ 前端同理):
#include <util/source-profiler.h>
#include <util/base.h>
#include <math.h>
/* 第一步:启用(可在任意线程调用,下一帧生效) */
source_profiler_enable(true);
source_profiler_gpu_enable(true); /* 仅在非 macOS 且确认承受得住开销时 */
/* 第二步:周期性(例如每 1~2 秒)查询某个 source */
profiler_result_t result;
if (source_profiler_fill_result(my_source, &result)) {
uint64_t ms = (uint64_t)(result.render_avg / 1000000.0);
blog(LOG_INFO, "render_avg=%.0f ms, render_max=%" PRIu64 " ms, "
"tick_avg=%" PRIu64 " ms, async_input_fps=%.1f",
(double)result.render_avg / 1e6, result.render_max / 1e6,
result.tick_avg / 1e6, result.async_input);
}
/* fill_result 复用同一对象,无需 bfree */
/* 若改用 get_result,则必须: */
profiler_result_t *r = source_profiler_get_result(my_source);
if (r) {
/* ... 使用 r ... */
bfree(r); /* 文档明确要求:result must be freed with bfree() */
}
使用时的解读建议:
render_avg稳定、render_max周期性尖峰 → 存在间歇性卡顿(如媒体源解码抖动);render_sum明显大于render_avg→ 该 source 一帧被渲染多次(多个预览/输出场景),总 GPU 负担应按 sum 评估;- 异步源:
async_input正常而async_rendered偏低 → 帧提交没问题,是渲染侧丢弃了帧;反之则上游供给不足; - 数据需等待约 5 秒采样窗积累后才稳定;
obs_reset_video后需重新等待。
7. 限制与适用前提
综合文档说明与源码事实,使用 source profiler 需注意:
- 平台限制:GPU 计时在 macOS 上不可用(文档明示);帧级 GPU timer range 相关实现注释也标注 “only required for DirectX”(source-profiler.c#L80-L82),即 GPU 字段主要在 Windows/D3D11 路径下有意义;
- 精度限制:GPU 计时精度受 GPU 负载/时钟频率波动影响,disjoint 时样本被整体丢弃,因此
render_gpu_*适合做趋势对比而非绝对测量; - 性能代价:CPU 路径每次 tick/render 都多一次时间戳读取与哈希操作(代价小);GPU 路径每帧创建/同步 timer(代价较大),文档均如实标注;
- 生效时机:两个 enable 接口都“下一帧生效”,且关闭 profiler 时会执行
profiler_shutdown()释放全部缓冲——关闭后再查询fill_result会直接返回false; - 采样窗语义:所有 avg/max 都是“最近约 5 秒”滚动窗口的统计(
fps × 5个样本),不是自启用以来的累计值。
8. 小结
OBS Studio 的 source profiler 是 libobs 提供的一套轻量级、按 source 粒度的性能诊断设施:4 个 API(source_profiler_enable / source_profiler_gpu_enable / source_profiler_get_result / source_profiler_fill_result)+ 一个 profiler_result_t 结果结构,覆盖 tick、CPU 渲染、GPU 渲染与异步帧率四类指标,采样窗口由视频帧率自动推导为 5 秒。结合本文给出的源码挂点(obs-video.c、source-profiler.c),你可以既按文档正确使用它,也能解释每个字段的来龙去脉,把“哪一帧慢了”进一步定位到“哪个 source 慢了”。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00