1、单测
chromium 原生的单测程序能尽早地暴露我们在对网络库进行定制化开发过程中会引发的 Bug,因此认真对待单测可为网络库定制化开发保驾护航。
1.1 确认代码迁移引起的 case
跑 NQE、QUIC 迁移后的统一网络库的单测程序 net_unittests,出现较多 fail 和 少数 crash 的 case; 从 fail case 看,与迁移的代码看起来无直接关联,因此直接阅读源码来定位原因显得较为困难且耗时可能比较长;
那么首先要做的是,确认这些 case 是因为代码迁移引起的,所以需要对比迁移前后 net_unittests 的 case 的差异,只分析确实是迁移代码引起的 case,避免将时间花在不是代码迁移引起 fail/crash 的 case 上;
1.2 输出可单步调试的 net_unittests
chromium 项目输出的可执行文件默认都是没有调试符号的,需要将输出目录下的 args.gn 中的 symbol_level 的值改为 2,这样所有输出的可执行文件就带有了调试符号,从而可以使用 gdb 进行单步调试:
Ubuntu 下使用 gdb 调试 net_unittests 的效果:
1.3 选择性执行单测
当跑完 net_unittests 的全部单测时,它会汇总出一个 fail/crash 的信息(考虑版面大小,删除了下图部分 case),可以看到全部单测跑完需要差不多 半个小时,而当它给出汇总信息后,我们只需重新执行这些 fail/crash 的 case 就行了:
如何选择性执行单测?我们可以先看下 net_unittests 的帮助内容, “./net_unittests --help”,可以看到 --gtest_filter 应该就是我们需要的:
Test Selection:
--gtest_list_tests
List the names of all tests instead of running them. The name of
TEST(Foo, Bar) is "Foo.Bar".
--gtest_filter=POSTIVE_PATTERNS[-NEGATIVE_PATTERNS]
Run only the tests whose name matches one of the positive patterns but
none of the negative patterns. '?' matches any single character; '*'
matches any substring; ':' separates two patterns.
--gtest_also_run_disabled_tests
Run all disabled tests too.
测试发现 --gtest_filter 的值需要在前后加上星号 “*” ,以便正则匹配,如果不加则是完全匹配:
./net_unittests --gtest_filter="*NetworkQualityEstimatorTest*" // 测试包含 NetworkQualityEstimatorTest 的所有 case
./net_unittests --gtest_filter="NetworkQualityEstimatorTest.OnUpdatedSocketValues" // 测试完全匹配的 case
下图是执行精确匹配的执行情况:
1.4 vpython 引起的 fail case
net_unittests 执行过程会通过 vpython 启动本地的 server,因此需要将 vpython 的路径添加到 PATH 环境变量中,否则会提示找不到 vpython 而造成 case 失败,如下:
vpython 的路径为:src/third_party/depot_tools/vpython
export PATH=$PATH:/path/to/chromium/src/third_party/depot_tools/vpython
2、实际单测失败 case 定位过程
2.1 crash message
34827 QuicConnectionTests/QuicConnectionTest.CoalescerHandlesInitialKeyDiscard/T051_defer_NoStopWaiting (../../net/third_party/quiche/src/quic/core/quic_connection_test.cc:1461)
34828 QuicConnectionTests/QuicConnectionTest.CoalescerHandlesInitialKeyDiscard/T051_immediate_NoStopWaiting (../../net/third_party/quiche/src/quic/core/quic_connection_test.cc:1461)
34829 QuicConnectionTests/QuicConnectionTest.CoalescerHandlesInitialKeyDiscard/draft29_defer_NoStopWaiting (../../net/third_party/quiche/src/quic/core/quic_connection_test.cc:1461)
34830 QuicConnectionTests/QuicConnectionTest.CoalescerHandlesInitialKeyDiscard/draft29_immediate_NoStopWaiting (../../net/third_party/quiche/src/quic/core/quic_connection_test.cc:1461)
其中 T051 版本的两条 crash 位置:
33909 [ RUN ] QuicConnectionTests/QuicConnectionTest.CoalescerHandlesInitialKeyDiscard/T051_defer_NoStopWaiting
33910 [17958:17958:0617/114206.744933:4977804956469:FATAL:quic_framer.cc(1031)] AppendCryptoFrame failed
draft29 的两条 crash 位置:
33870 [ RUN ] QuicConnectionTests/QuicConnectionTest.CoalescerHandlesInitialKeyDiscard/draft29_immediate_NoStopWaiting
33871 [17948:17948:0617/114205.183844:4977803395380:FATAL:quic_framer.cc(1200)] AppendCryptoFrame failed:
虽然 crash 位置不同,但 crash 的代码一样:
1198 case CRYPTO_FRAME:
1199 if (!AppendCryptoFrame(*frame.crypto_frame, writer)) {
1200 QUIC_BUG << "AppendCryptoFrame failed: " << detailed_error();
1201 return 0;
1202 }
2.2 定位过程:
同时使用 gdb 单步执行 crash 的单测 case,分析两者的执行路径有什么不同;同时分析路径上是否涉及到本次代码迁移;
测试发现,未迁移版本只调用了一次,而迁移版本调用了两次;迁移版本是在第二次出现了 crash;
目前看是从要传输的数据量上有差别,调用两次次是因为数据没传完,调用一次是因为数据已经传完了;
为什么传不完?为什么要传这么多数据?为什么再次调用时写进 producer 中的 offset 是 0,而 length 是 76,但 AppendCryptoFrame 时 frame 的信息则是 offset 为 1286,length 为 76?
可以看下 BuildDataPacket 的入参 frame 在两次调用中调用栈传入时的差别:
在 QuicPacketCreator::ConsumeCryptoData 运行过程汇总 while 循环了两次,产出了两个 crypto frame;
未 crash 的情况 write_lenght 等于 total_bytes_consumed 即 1300 字节,而 crash 情况 total_bytes_consumed 为 1246;
因此在未 crash 的调用栈第一次调用后,在 quic_packet_creator.cc 1360 行加上断点,看看 frame 的创建过程与 crash 的有什么区别;
发现两者的区别在于 QuicPacketCreator 类的 max_plaintext_size_ 成员的值,未 crash 的为 1338,而 crash 的只有 1268;
现在要分析这个成员的值是怎么赋值的;为什么有这样的差异;它的赋值有三处:
153 max_plaintext_size_ = framer_->GetMaxPlaintextSize(max_packet_length_);
...
173 max_plaintext_size_ = framer_->GetMaxPlaintextSize(max_packet_length_);
...
213 max_plaintext_size_ = framer_->GetMaxPlaintextSize(length);
发现实际调用的是 173 这行代码,是在该类构造方法中被调用,而导致差异的原因是下面的修改:
// (Before Modified)
// const QuicByteCount kDefaultMaxPacketSize = 1350;
// (After Modified)
const QuicByteCount kDefaultMaxPacketSize = 1280;
至此可知 crash 原因与这个值的修改有关,由于该值的变化导致原生代码的逻辑改变进而导致 crash,且该值的修改目前看并无特殊作用,因此恢复了该值为 1350,重新编译 net_unittests 后执行发现其他 Bbr2、QuicConnectionTests 相关的 fail case (见 1.3 小节图)都解决了。