最好是好人的敌人

图片6

本文讲述了我们曾经决定如何改进内部的SelfTester工具,该工具可用于测试PVS-Studio分析仪的质量。 改进很简单,似乎很有用,但使我们陷入了麻烦。 后来发现我们最好放弃这个主意。

自测器


我们开发和推广适用于C,C ++,C#和Java的PVS-Studio静态代码分析器。 为了测试分析仪的质量,我们使用内部工具,通常称为SelfTester。 我们为每种受支持的语言创建了一个单独的SelfTester版本。 这是由于测试的细节所致,更加方便。 因此,目前我们公司拥有三个内部的SelfTester工具,分别用于C \ C ++,C#和Java。 此外,我将向您介绍Windows C / C ++ Visual Studio项目的SelfTester版本,简称为SelfTester。 该测试器是同类内部工具中的首款产品,它是所有工具中最先进和最复杂的。

SelfTester如何工作? 这个想法很简单:收集一组测试项目(我们正在使用真正的开源项目),然后使用PVS-Studio对其进行分析。 结果,为每个项目生成了一个分析器日志。 将此日志与同一项目的参考日志进行比较。 比较日志时,SelfTester会以方便开发人员的方式创建日志摘要 ,以进行比较。

在研究了摘要之后,开发人员根据警告的数量和类型,工作速度,内部分析仪错误等得出有关分析仪行为变化的结论。 所有这些信息非常重要:它使您知道分析仪如何应对其工作。

基于日志比较的摘要,开发人员在分析器核心中进行了更改(例如,在创建新的诊断规则时),并立即控制其编辑结果。 如果开发人员与常规日志比较没有更多问题,那么他将为项目提供当前的警告日志参考 。 否则,工作将继续。

因此,SelfTester的任务是使用一组测试项目(顺便说一下,其中有120多个用于C / C ++)。 池的项目以Visual Studio解决方案的形式选择。 这样做是为了额外检查分析器在支持分析器的各种Visual Studio版本上的工作(此时从Visual Studio 2010到Visual Studio 2019)。

注意:进一步,我将概念解决方案项目分开,将项目视为解决方案的一部分。

SelfTester的界面如下所示:

图片3

左边是解决方案的列表,右边是对每个Visual Studio版本的检查结果。

灰色标签“不支持”表示解决方案不支持所选的Visual Studio版本或未针对该版本进行转换。 某些解决方案在池中具有配置,该配置指示要检查的特定Visual Studio版本。 如果未指定版本,则将为所有后续Visual Studio版本更新解决方案。 屏幕截图上有一个这样的解决方案的示例-“ smart_ptr_check.sln”(对所有Visual Studio版本都进行了检查)。

绿色标签“确定”表示常规检查未检测到与参考日志的差异。 红色标签“ Diff”表示差异。 这些标签必须特别注意。 在所需标签上单击两次后,将在相关的Visual Studio版本中打开选定的解决方案。 带有警告日志的窗口也将在那里打开。 底部的控制按钮使您可以重新运行所选解决方案或所有解决方案的分析,使所选日志(或一次全部)成为参考,等等。

SelfTester的结果始终在html报告(差异报告)中重复

除GUI外,SelfTester还具有夜间构建运行的自动化模式。 但是,通常的使用模式是在工作日内重复由开发人员运行。 因此,最重要的SelfTester特征之一就是工作速度。

为什么速度很重要:

  1. 就夜间试运行而言,每个步骤的性能都至关重要。 显然,测试通过得越快越好。 目前,SelfTester的平均执行时间超过2小时;
  2. 白天运行SelfTester时,开发人员必须减少等待结果的时间,从而提高了工作效率。

正是性能提升成为这次改进的原因。

SelfTester中的多线程


SelfTester最初是作为多线程应用程序创建的,能够同时测试多个解决方案。 唯一的限制是您不能同时为不同的Visual Studio版本检查同一解决方案,因为在测试之前,许多解决方案需要更新为Visual Studio的某些版本。 在此过程中,更改直接引入.vcxproj项目的文件中,这会导致并行运行时出错。

为了使工作更有效率,SelfTester使用智能任务计划程序来设置并限制并行线程的严格限制值。

该计划程序有两个级别。 第一个是解决方案级别,它用于开始使用PVS-Studio_Cmd.exe实用工具测试.sln解决方案。 在PVS-Studio_Cmd.exe内部使用相同的调度程序,但具有另一个并行度设置(在源文件测试级别)。

并行度是一个参数,指示必须同时运行多少个并行线程。 为解决方案和文件级别的并行度分别选择了四个八个默认值。 因此,此实现中的并行线程数必须为32(4个同时测试的解决方案和8个文件)。 对于我们的分析仪在八核处理器上工作而言,此设置对我们而言是最佳的。

开发人员可以根据自己的计算机性能或当前任务来设置并行度的其他值。 如果开发人员未指定此参数,则默认情况下将选择逻辑系统处理器的数量。

注意:让我们进一步假设我们处理默认的并行度。

调度程序LimitedConcurrencyLevelTask​​Scheduler继承自System.Threading.Tasks.TaskScheduler,并经过改进以在使用ThreadPool时提供最大的并行度。 继承层次结构:

LimitedConcurrencyLevelTaskScheduler : PausableTaskScheduler { .... } PausableTaskScheduler: TaskScheduler { .... } 

PausableTaskScheduler允许您暂停任务性能,此外, LimitedConcurrencyLevelTask​​Scheduler还提供了对任务队列的智能控制,并考虑了并行度,已调度任务的范围和其他因素来调度其性能。 运行LimitedConcurrencyLevelTask​​Scheduler任务时使用调度程序。

完善的理由


上述过程有一个缺点:在处理不同大小的解决方案时不是最佳的。 测试池中解决方案的大小非常不同:从8KB到4GB-包含解决方案的文件夹的大小,每个文件夹中从1到数千个源代码文件。

调度程序将解决方案简单地一个接一个地放入队列中,而无需任何智能组件。 让我提醒您,默认情况下最多可以同时测试四个解决方案。 如果当前测试了四个大型解决方案(每个解决方案中的文件数都超过八个),则可以认为我们有效地工作是因为我们使用了尽可能多的线程(32个)。

但是,让我们想象一下在测试几个小型解决方案时出现的一种相当频繁的情况。 例如,一个解决方案很大,包含50个文件(将使用最大线程数),而其他三个解决方案则分别包含三个,四个,五个文件。 在这种情况下,我们将仅使用20个线程(8 + 3 + 4 + 5)。 我们无法充分利用处理器时间并降低整体性能。

注意 :实际上,瓶颈通常是磁盘子系统,而不是处理器。

改进之处


在这种情况下不言而喻的改进是测试解决方案列表的排名。 通过传递给具有正确数量文件的测试项目,我们需要对一定数量的同时执行线程(32)进行最佳利用。

让我们再次考虑测试四个具有以下文件数量的解决方案的示例:50、3、4和5。检查三个文件的解决方案的任务可能最快。 最好添加一个包含八个或更多文件的解决方案,而不是添加文件(以便为此解决方案使用最大可用线程数)。 这样,我们将一次利用25个线程(8 + 8 + 4 + 5)。 还不错 但是,仍然不涉及七个线程。 接下来是另一种改进的想法,即消除测试解决方案的四个线程限制。 因为我们现在可以利用32个线程添加一个解决方案,而不是几个解决方案。 让我们想象一下,我们还有两个分别有三个和四个文件的解决方案。 添加这些任务将完全消除未使用线程的“间隙”,并且将有32(8 + 8 + 4 + 5 + 3 + 4 )个线程。

希望这个想法很明确。 实际上,实施这些改进也不需要太多的努力。 一切都在一天之内完成。

我们需要重做任务类:从System.Threading.Tasks.Task继承并分配“权重”字段。 我们使用一种简单的算法为解决方案设置权重:如果文件数少于八个,则权重等于该数字(例如5)。 如果数字大于或等于8,则权重将等于8。

我们还必须详细说明调度程序:教它选择具有所需权重的解决方案,以达到32个线程的最大值。 我们还必须允许四个以上的线程同时进行解决方案测试。

最后,我们需要一个初步步骤来分析池中的所有解决方案(使用MSBuild API进行评估),以评估和设置解决方案的权重(使用源代码获取文件数量)。

结果


我认为经过这么长时间的介绍,您已经猜不到一切。

图片12

很好的是,这些改进既简单又快速。

这是文章的这一部分,在这里,我将告诉您什么“使我们陷入许多麻烦”以及与之相关的所有事情。

副作用


因此,负面结果也是结果。 事实证明,池中大型解决方案的数量远远超过小型解决方案(少于八个文件)的数量。 在这种情况下,这些改进不会产生非常明显的效果,因为它们几乎是看不见的:与大型项目相比,测试小型项目所花费的时间要少得多。

但是,我们决定将新的改进保留为“无干扰”且可能有用。 此外,不断补充测试解决方案的资源,因此在将来,情况可能会改变。

然后...

图片5

一名开发人员抱怨SelfTester崩溃。 好吧,生活发生了。 为避免丢失此错误,我们创建了一个内部事件(故障单),名称为“使用SelfTester时发生异常”。 评估项目时发生错误。 尽管出现错误的窗口很多,但在错误处理程序中却指出了问题。 但这很快就消除了,在接下来的一周中没有任何崩溃。 突然,另一个用户抱怨SelfTester。 同样,项目评估的错误:

图片8

这次堆栈包含许多有用的信息-错误是xml格式。 处理Proto_IRC.vcxproj项目的文件(以xml表示)时,文件本身可能发生了某些情况,这就是XmlTextReader无法处理它的原因。

在相当短的时间内出现两个错误使我们仔细研究了这个问题。 另外,正如我上面所说,SelfTester被开发人员非常积极地使用。

首先,我们分析了最后一次崩溃。 可悲的是,我们没有发现任何可疑的。 以防万一我们要求开发人员(SelfTester用户)保持警惕并报告可能的错误。

要点:错误代码已在SelfTester中重用。 它最初用于评估分析仪本身中的项目( PVS-Studio_Cmd.exe )。 这就是为什么人们越来越关注这个问题的原因。 但是,分析仪中没有此类崩溃。

同时,有关SelfTester问题的票证还添加了新的错误:

图片9

再次XmlException 。 显然,某些地方存在竞争线程,这些线程可用于读写项目文件。 在以下情况下,SelfTester可以处理项目:

  1. 在初步计算解决方案权重的过程中进行项目评估:最初引起怀疑的新步骤;
  2. 将项目更新到所需的Visual Studio版本:是在测试之前(项目不会干扰)执行的,并且它一定不会影响工作过程。
  3. 测试期间的项目评估:完善的线程安全机制,可从PVS-Studio_Cmd.exe重用;
  4. 从SelfTester退出时,还原项目文件(用初始参考文件替换已修改的.vcxproj文件),因为在工作过程中项目文件可以更新为所需的Visual Studio版本。 这是最后一步,对其他机制没有影响。

怀疑落在为优化(权重计算)而添加的新代码上。 但是其代码调查表明,如果用户在SelfTester启动后立即运行分析,则测试人员始终可以正确地等待直到预评估结束。 这个地方看起来很安全。

同样,我们无法确定问题的根源。


下个月,SelfTester继续崩溃一次又一次。 凭单不断填充数据,但不清楚如何处理该数据。 大多数崩溃都带有相同的XmlException。 有时还有其他东西,但是在PVS-Studio_Cmd.exe的相同重用代码上。

图片1

传统上,内部工具的要求不是很高,因此我们一直在以残留原则来混淆SelfTester的错误。 有时,会有不同的人参与进来(整个事件中,有六个人在解决问题,包括两名实习生)。 但是,我们必须分心这项任务。

我们的第一个错误。 实际上,到目前为止,我们可以彻底解决此问题。 怎么了 很明显,该错误是由新的优化引起的。 毕竟,在一切正常之前,重用的代码显然不会那么糟糕。 此外,这种优化没有带来任何好处。 那该怎么办呢? 删除此优化。 正如您可能理解的那样,它尚未完成。 我们继续致力于解决我们自己创造的问题。 我们继续寻找答案:“ HOW ???” 它如何崩溃? 它似乎写得正确。

我们的第二个错误。 其他人也参与解决问题 这是一个非常非常大的错误。 不仅不能解决问题,还需要额外的资源浪费。 是的,新人们带来了新的想法,但要花很多时间才能实施(毫无用处)这些想法。 在某个时候,我们让实习生编写测试程序,以模拟对一个项目和同一项目在不同线程中的评估,并在另一个项目中并行修改一个项目。 它没有帮助。 我们仅发现MSBuild API在内部是线程安全的,这一点我们已经知道。 当XmlException异常发生时,我们还添加了小型转储自动保存功能。 我们有一个调试这一切的人。 可怜的家伙! 经过讨论,我们做了其他不必要的事情。

最后,找出第三个错误。 您是否知道从SelfTester问题发生到解决这一问题已经过去了多少时间? 好吧,你可以数一数。 门票于2018年9月17日创建,并于02/20/2019关闭。 有40多个评论! 伙计们,这是很多时间! 我们让自己在THIS忙了五个月。 同时,我们忙于支持Visual Studio 2019,添加Java语言支持,引入MISRA C / C ++标准,改进C#分析器,积极参加会议,撰写大量文章等。 由于SelfTester中的一个愚蠢错误,所有这些活动都减少了开发人员的时间。

亲朋好友,要从我们的错误中吸取教训,从不这样做。 我们也不会。

就是这样,我完成了。

图片15

好的,这是个玩笑,我会告诉你SelfTester出了什么问题:)

宾果!


幸运的是,我们中间有一个眼睛清晰的人(我的同事谢尔盖·瓦西里耶夫(Sergey Vasiliev)),他只是从非常不同的角度看问题(而且-他很幸运)。 如果SelfTester内部没问题,但是外面的东西会使项目崩溃,该怎么办? 通常,我们没有使用SelfTester启动任何东西,在某些情况下,我们严格控制执行环境。 在这种情况下,这个“东西”可能是SelfTester本身,但是是不同的实例。

退出SelfTester时,从引用还原项目文件的线程将继续工作一段时间。 此时,测试器可能会再次启动。 稍后添加了针对多个SelfTester实例同时运行的保护,现在看起来如下所示:

图片16

但是那时我们还没有。

坚果,但确实如此-在遭受酷刑的六个月中,没有人关注它。 从引用还原项目是一个相当快的后台过程,但是不幸的是,它的速度还不够快,不会干扰SelfTester的重新启动。 当我们启动它时会发生什么? 没错,计算解决方案的权重。 一个进程重写.vcxproj文件,而另一个进程尝试读取它们。 向XmlException打个招呼

Sergey在向测试仪添加切换到另一组参考日志的功能时发现了所有这些问题。 在分析器中添加了一组MISRA规则后,变得很有必要。 您可以在用户看到此窗口的同时直接在界面中切换:

图片14

之后, SelfTester重新启动。 显然,更早之前,用户以某种方式自己模拟了问题,然后再次运行测试仪。

谴责和结论


当然,我们删除了(即禁用了)先前创建的优化。 此外,这比在重新启动测试仪本身之间进行某种同步要容易得多。 和以前一样,一切开始正常运行。 作为一项附加措施,我们增加了防止同时启动测试仪的上述保护措施。

我已经在上面写过关于搜索问题时的主要错误的信息,这足以引起自我鞭ation。 我们是人类,所以我们可能错了。 重要的是要从自己的错误中吸取教训并得出结论。 这个案例的结论很简单:

  • 我们应该监视和估计任务的复杂性;
  • 有时我们需要在某个时候停下来;
  • 尝试更广泛地看问题。 随着时间的流逝,人们可以获得对案件的洞察力,而它需要新的视角。
  • 不要害怕删除旧的或不必要的代码。

就是这样,这一次我肯定完成了。 感谢您的阅读。 希望您的代码没有错误!

Source: https://habr.com/ru/post/zh-CN466739/


All Articles