二分浏览器错误

当大规模运行WebRTC时,最终会以遇到问题和频繁的回归结束。能够快速识别出问题所在是防止Chrome Stable中的归档降级或调整自己代码以避免问题的关键。Chrome的bisect-builds.py工具使这个过程比你想象中容易得多。来自appear.in的Arne为你提供了一个示例,说明他是如何使用它解决最近出现的问题。

本文中,我将会逐步解释Chrome的更改是如何触发appear.in中的错误,以及我们如何确定更改的确切内容。

在WebRTC的基础上构建应用程序有好处也有弊端。显而易见的好处是你可以利用大量的使WebRTC成为现在样子的工作成果。然而,这也有弊端。应用程序的正确运行取决于支持WebRTC技术的正确操作,以及应用程序代码与技术之间的正确交互。当这种交互产生缺陷时,这些缺陷可能是非常明显,非常模糊,或者介于两者之间。这是一个既不明显也不介于两者之间的情形。

问题所在

这绝不应该发生

故事开始于我们的支持专家Ashley去讨论遇到奇怪事情的客户的问题。他们报告说,最近他们在通过公司防火墙传输视频时遇到了问题。音频很好,视频通过防火墙进入公司网络就好了。但是,防火墙外的参与者只能听到防火墙内部的参与者,而不能看到它们。更奇怪的是,如果防火墙内的参与者共享屏幕,它将通过防火墙传输出去。然后外部参与者可以通过屏幕共享观看到内部参与者的第一视角,但主视频反馈仍为空白。

了解WebRTC的架构,这是一种奇怪的故障模式。如果防火墙让音频数据包通过,它阻止附带视频数据包的可能性很小。至少它需要一些非常有侵略性的(D)TLS中间人设置才能知道两者之间的差异。所以我们开始深度挖掘。

第一个有关线索是该用户正在使用我们的付费SFU会议室,这意味着来自各个参与者的视频馈送同时作为几个不同的空间分辨率馈送(同时联播)。我们的校友fippo在我们调查此事时提供旁观的一些评论,并且很早就提出联播作为触发条件。事实证明,非常正确。

扭曲

第二个相关线索是受影响的连接也通过我们的TURN网络转换。据推测,这是因为所讨论的防火墙在允许流量通过的出站端口方面具有相当大的限制。由于我们正在努力了解客户运行的确切网络条件,工程师Hans Christian证明他简单的通过强制TURN用于我们的SFU来复制行为。还有一些细微之处,例如不同参与者加入房间的顺序,但是一旦可以重现问题,这些就容易了。尽管如此,基于这两条线索还是没有任何见解。TURN旨在不加选择的传递流量,并且随着我们处理的TURN流量(很多),如果不按预期运行,则会引起更加强烈的反应。

进一步缩小

如果能够重现,我们就可以快速确定:

  1. 接收参与者使用什么设备无关紧要和
  2. 视频丢失的发送者需要在Chrome73或更高版本上运行in。

我们的注意力转向了Chrome73的发行说明和更新日志,但仍然没有什么进展。是时候查看包痕迹了。

我们的SFU能够在开发模式下清除RTP数据包,以便进行调试。但是,这种情况下我们要检查TURN服务器两侧的数据包。即使SFU能够提供它在剥离DTLS后看到的数据包日志,也不能立即将这些数据包与TURN服务器的数据包跟踪进行比较。在这里,有点模糊的Chrome Canary命令行标志–disable-webrtc-encryption就派上用场了。在向我们的SFU代码库添加类似功能之后,我们能从Chrome73TURN服务器会话的入站和出站端获得明文分组跟踪。然后迅速将其作为错误来源消除。TURN服务器正如所希望的那样忠实的传输数据。

通过手中的明文数据包跟踪,是时候开始深入研究RTP流的实际内容了。很明显,我们的SFU没有收到Chrome73期望的关键帧。这很奇怪,因为我们在Chrome的方向发送了大量PLI(图片丢失指标)。

在这一点上,我们觉得我们有一些有型的东西向Chrome团队报告,并且那样做了。然而,失败似乎仍然与我们的特定设置密切相关,因此我们也在继续调查。

使用Bisect调试Chrome

分治

在调查一个棘手的问题时,能够在受控制环境中重现客户的问题会使世界发生变化。首先,我们可以轻易改变参数,来查看具体哪个属性引发了问题。此外,它还增加了你可以改变的参数数量。用于运行应用程序的浏览器的精确版本是你可以在调试环境中改变的一件事,因为很难让客户完成。如果你可以尝试任何想要的浏览器版本,这将启用一种称为“二等分”的强大技术。

当对软件进行故障排除时,你经常知道有一个版本的代码可以显示要避免的问题而另一个不能。如果你想确定软件代码库的哪个更改引发了问题,一种方法是重放从第一个版本到另一个版本的所有更改,在每个步骤测试软件看问题是否显现出来。尽管可行,但是如果你只是在软件代码库发生重大变化后才意识到问题,这可能是个繁琐费时的过程,因为回溯的步骤数量可能会非常多。

在二等分时,你假设问题在某个特定点引入,然后保留所有后续版本。这允许你在已知的好坏版本之间通过迭代选择一个版本进行优化,以测试它是否表现出问题。接下来,这个中途版本要么成为你的好版本,要么成为糟糕的版本,然后你重新选择一个新的中间点。最终,好坏版本足够接近,你可以推断它们之间的所有改变,并希望发现引发问题的改变。

Bisection受到了git的欢迎,它引入了一个名为“git bisect”的专用命令来自动化这个过程。Git中好坏版本之间的区别表示为从一个到另一个提交的提交路径。中间点被选为最靠近此路径中间点的提交。当你最近发现的好版本是最近发现的坏版本的父类时,过程将会终止。然后由你来检查错误提交引入的更改,以便确定实际问题是什么。为了充分利用此过程,原始好坏版本之间的提交路径应该是线性的,路径上的所有提交应该代表软件的可运行版本,并且每次提交引入的更改应该足够小以至于一旦二分过程终止,它们就可以被有效的分析。

Sam,再次播放

理论上听起来不错,但我们如何将其应用于Chrome和应用程序呢?我们的前提是在Chrome72下,应用程序没有表现出我们试图消除的行为,但是在Chrome73下表现出来了。如果我们上一个已知的好版本是例如Chrome62,那么将Chrome主要版本二分就有意义,但是这种情况下我们已经知道调查的问题是在主要版本Chrome73中引入的。我们想深入一级讨论。我们需要将Chrome72和73之间的差异分解为更小的步骤,然后尝试将二分法应用于步骤列表。幸运的是,作为Chromium构建基础架构的一部分,Google维护了一个非常漂亮的变更集查找工具omahaproxy。我们使用它来查找Chrome版本“72.0.3626”和“73.0.3683.90”,它们分别产生了“基本分支位置”612437和625896。因此,Chrome版本72和73之间有超过13000个基本分支步骤(对应于主要Chrome git存储库的Git提交(小块)),这很多。一步一步编译测试这些问题是否显现出来显然是站不住脚的。但是,由于二分法通过每次将迭代的搜索空间减半工作,我们应该能够在log2(13459)=14步之内将其缩小到一个基本分支步骤。尽管如此,这是一项重大任务,从头编译Chrome需要花费大量时间,并且第一次迭代将产生足够远的步骤,迭代编译的好处微乎其微。

隐藏的宝石

在这一点上,我们感到非常惊喜。事实证明Google实际上为所有这些基本分支步骤提供了预编译二进制文件。他们甚至提供了一个python脚本来使用这些二进制文件运行二分法。在Chrome开发人员页面上详细说明了如何获得和使用它。安装完成后,根据上面找到的基本分支步骤,我们现在可以轻松搜索完整的步骤列表。通过给bisect-builds.py我们想要搜索的范围,它将向我们展示不同的Chrome候选者进行测试。我们告诉bisect-builds想要运行的Chrome参数,它会为我们自动启动候选人。然后由我们执行必要的步骤来重现问题,终止候选Chrome并报告问题是否显现。下一个候选者将根据报告选出。实际上,此过程看起来像:

$ python bisect-builds.py -a linux64 -g 612437 -b 625896 --use-local-cache -- --no-first-run --user-data-dir=/tmp 'https://appearin.appear.in/[..]'
Downloading list of known revisions...
Loaded revisions 41523-646438 from /home/argggh/appearin/src/google/.bisect-builds-cache.json
Downloading revision 619030...
Received 112276811 of 112276811 bytes, 100.00%
Bisecting range [612442 (good), 625894 (bad)], roughly 13 steps left.
Trying revision 619030…
#
# Try to reproduce error condition, exit candidate Chrome
#
Revision 619030 is [(g)ood/(b)ad/(r)etry/(u)nknown/(s)tdout/(q)uit]: g
Downloading revision 622012...
Bisecting range [619030 (good), 625894 (bad)], roughly 12 steps left.
Trying revision 622012...
#
# Try to reproduce error condition, exit candidate Chrome
#
Revision 622012 is [(g)ood/(b)ad/(r)etry/(u)nknown/(s)tdout/(q)uit]: g
Downloading revision 624029...
Received 112735549 of 112735549 bytes, 100.00%
Bisecting range [622012 (good), 625894 (bad)], roughly 11 steps left.
Trying revision 624029...
#
# Repeat for 10 more steps...
#
[..]
Revision 624768 is [(g)ood/(b)ad/(r)etry/(u)nknown/(s)tdout/(q)uit]: b
You are probably looking for a change made after 624767 (known good), but no later than 624768 (first known bad).
CHANGELOG URL:
https://chromium.googlesource.com/chromium/src/+log/cdb1b2073f12c6edf89fb4c518d6dd4fa018c66f..94cbf3f56197bcd83ba7a9830dcfcd0e1fcf2699
$

 

整个过程效率惊人,总共需要大约十分钟。

再一次,加上体验

坏消息是,已识别的更改是依赖项更新,从而破坏了捆绑的WebRTC代码库的版本。版本包含对WebRTC存储库的53个不同提交,从74ba99062c 到 71b5a7df77。查看提交列表,我们可以推测哪些可能涉及到问题。我们也可以从阅读代码差异开始,以了解行为的变化。如果更幸运,有问题的交互现在已经很明显了,我们可以称之为一天。

然而,当我们越来越接近回答“什么”引发了我们的问题时,“为什么”仍然在逃避我们。我们得出结论,我们需要检测已更改的代码路径,以更好地了解它们与应用程序的交互方式。幸运的是,Chrome(或更确切地说,Chromium)源代码是公开可用的,Google提供了自行构建它的良好说明。按照构建说明中描述的步骤,我们很快就会在本地计算机上安装Chromium源代码和相关的Google工具。此时,我们略微偏离官方构建说明。由于我们想要构建Chromium的历史版本而不是master的顶端,我们使用git checkout 94cbf3f56将树定位在正确的版本中。然后,我们确保使用gclient sync将所有依赖项同步到此Chromium版本。从现在开始,大部分操作都在子目录third_party / webrtc中。这是一个来自主Chromium仓库的独立git存储库,在上面的同步之后,gclient将我们定位在修订版71b5a7df77这里

在确定了可能违规的提交之后,我们现在可以推测性地将其退出,以查看问题是否消失。但是,只要我们不得不从头开始编译Chromium,我们选择彻底并通过这53次提交将我们的方式分成两部分。我们通过在之前指出的WebRTC存储库更改范围上运行传统的git bisect进程来实现此目的。这个过程的基本原理类似于我们经历的第一个二等分过程,但之前在幕后发生的一些比特现在由我们来完成。在每一步,我们现在必须明确地编译和启动我们的Chromium候选者。

$ cd chromium/src/third_party/webrtc
$ git bisect good 74ba99062c48
You need to start by "git bisect start"
Do you want me to do it for you [Y/n]? y
$ git bisect bad 71b5a7df7794
Bisecting: 26 revisions left to test after this (roughly 5 steps)
[9dac02d93974ee836f78bee33300872602bd5ee7] Adding text log on actual opus bitrate.
$ cd ../..
$ gn gen out/Default
Done. Made 12023 targets from 1885 files in 4263ms
$ autoninja -C out/Default chrome
/home/argggh/appearin/src/google/google/depot_tools/ninja -C out/Default chrome
ninja: Entering directory `out/Default'
[38425/38425] LINK ./chrome
$ out/Default/chrome https://appearin.appear.in/[..]
#
# Try to reproduce error condition, exit candidate Chrome
#
$ cd -
/home/argggh/appearin/src/google/chromium/src/third_party/webrtc
$ git bisect good
Bisecting: 13 revisions left to test after this (roughly 4 steps)
[5a6ae02e90dd02193b69129da025d9812d95dd2d] Reland "Trim down FileWrapper class to be merely a wrapper owning a FILE*"
$ cd ../..
$ autoninja -C out/Default chrome
/home/argggh/appearin/src/google/google/depot_tools/ninja -C out/Default chrome
ninja: Entering directory `out/Default'
[1/1] Regenerating ninja files
[380/380] LINK ./chrome
$ out/Default/chrome https://appearin.appear.in/[..]
#
# Try to reproduce error condition, exit candidate Chrome
#
$ cd -
/home/argggh/appearin/src/google/chromium/src/third_party/webrtc
$ git bisect good
Bisecting: 6 revisions left to test after this (roughly 3 steps)
[443760d4baa80753b148ab5571628c3d80d2a896] Android: Add option to print native stack traces in PeerConnectionFactory API
#
# Repeat for 3 more steps...
#
[..]
$ git bisect bad
1b761ca21ac76513d3abe3790fb4c2f73a81e127 is the first bad commit
commit 1b761ca21ac76513d3abe3790fb4c2f73a81e127
Author: Florent Castelli <orphis@webrtc.org>
Date:   Mon Jan 21 14:33:02 2019 +0100
 
    Remove simulcast constraints in SimulcastEncoderAdapter
    
    The lowest and highest resolution layers are also identified instead
    of assuming they are the first and last ones.
    
    Bug: webrtc:10069
    Change-Id: If9c76d647415c5065b79dc71850709db6bf16f61
    Reviewed-on: https://webrtc-review.googlesource.com/c/114429
[..]
$

 

在新树中第一次编译Chromium需要花费大量的时间(比如两位数的小时数),所以我们将其留在一夜之间并在第二天早上返回任务。后续构建需要大约5分钟来准备每个步骤,并且该过程的其余部分或多或少地以交互方式运行。这比使用预先构建的二进制文件平分要慢得多,但对于我们剩余的5-6步可以容忍。如果我们的步骤进一步分开,每次构建的时间会显着增加,接近完整构建的时间。这只是强调了第一次二分过程的效率。

在彩虹的尽头

最终,如上所示,此过程确认https://webrtc-review.googlesource.com/c/src/+/114429是导致我们的应用程序行为异常的更改。这只是124行添加的代码,虽然我们仍然处于实际问题的黑暗中,但此时可以检测Chromium中所有已更改的代码流,以了解它们如何与我们的应用程序代码进行交互。

在我们这样做的时候,罪魁祸首变得逐渐明显。 Chrome更改可确保联播层始终按正确的顺序排序,因为有时(显然)可以按相反顺序给出图层(分配给底层的最高带宽,而不是顶层)。无论如何,层总是从最低到最高带宽排序。

查看调试打印输出显示Chrome已按顺序[1(640×360),0(320×180),2(1280×720)]排序我们的图层 – 所以既不是升序也不是降序,而是某种乱七八糟的混乱。进一步挖掘,第0层设置的带宽上限为768kbps,高于第1层给出的上限,低于第2层给出的上限。因此,当只有一层可用带宽时,Chrome只会发送第1层上的视频,而不是我们SFU期望的第0层。此时,我们可以连接到appear.in应用程序代码。

如前所述,我们推出了大量的TURN数据。为了控制转发此数据所花费的成本,当我们确定ICE使用“中继”候选时,我们使用RTCRtpSender.setParameters将带宽目标限制为768kbps。这种逻辑早于我们使用联播。不幸的结果是,当使用SFU,TURN和同时广播时,我们有效地将第0层的目标带宽设置为比预期的通常150kbps高得多的值。结果,该层被降低优先级。我们的SFU将在第1层接收视频流,但由于它期望第0层是最初活动的,因此它试图获得第一个关键帧。这使用户只有音频而没有视频流,正如报告的那样。 修复此问题的更改是7行或8行代码。

小贴士

那么,你应该从这个故事中学习什么?以下几点可供选择:

  • 良好的支持人才是宝贵的。认真对待他们,倾听他们并帮助他们进行有效的分流。
  • 调查“不可能发生”的事情
  • 复制客户端条件允许您在受控环境中重现报告的错误 – 确保您具有现场诊断功能以及可用于执行此操作的所需产品旋钮和控制杆。
  • Bisection是一种强大的技术,可以在您自己的应用程序和依赖项中查明问题
  • 即使您无法修复该错误,在请求上游帮助时能够报告有问题的更改也会增加获得帮助的几率
  • 良好的git使用习惯(提交尽可能小,每次提交可编译/可测试,(大致)线性提交历史)将确保二分法可以充分利用您自己的代码
  • 谷歌有一些令人惊讶的工具,需要注意
  • 你总是可以更深入一级
  • 开源很棒!

 

原文标题:Bisecting Browser Bugs

作者:“Arne Georg Gisnås Gleditsch

填写常用邮箱,接收社区更新

WebRTC 中文社区由

运营