最好是好人的敌人

图片12

本文是关于一旦我们决定略微改进用于检查PVS-Studio分析仪质量的内部工具SelfTester的方式。 改进很简单并且看起来很有用,但是它给我们带来了很多问题,后来发现如果我们不这样做会更好。

自测器


我们开发和推广适用于C,C ++,C#和Java的PVS-Studio静态代码分析器。 为了检查分析仪的质量,我们使用内部工具,这些工具统称为SelfTester。 每种受支持的语言都有其自己的SelfTester版本。 这是由于测试的功能,而且更加方便。 因此,目前,我们公司针对C \ C ++,C#和Java使用了三个内部SelfTester工具。 接下来,我将讨论Windows版本的C \ C ++ Visual Studio项目的SelfTester,简称为SelfTester。 该测试器是此类内部工具中的第一款,它是所有工具中最先进,最复杂的。

SelfTester如何工作? 这个想法很简单:收集一组测试项目(我们使用带有开放源代码的真实项目)并使用PVS-Studio对其进行分析。 结果,为每个项目生成了一个分析器警告日志。 将此日志与同一项目的参考日志进行比较。 比较日志时,SelfTester会以便于开发人员理解的形式创建日志比较日志。

在研究了日志之后,开发人员得出有关分析仪行为变化的结论:警告的数量和性质,操作速度,内部分析仪错误等。 所有这些信息非常重要,它使您能够了解分析仪的工作状况。

根据日志比较日志,开发人员对分析器核心进行更改(例如,在创建新的诊断规则时),从而立即控制其编辑的效果。 如果开发人员不再对日志的下一个比较有疑问,则可以将项目的当前警告日志作为参考 。 否则,工作将继续。

因此,SelfTester的任务是使用一组测试项目(顺便说一下,对于C / C ++,已经有120多个测试项目)。 选择该池的项目作为Visual Studio解决方案。 这样做是为了在分析器支持的不同版本的Visual Studio上额外测试分析器(从当前的Visual Studio 2010到Visual Studio 2019)。

注意 :我将进一步分离解决方案项目的概念,将项目理解为解决方案的一部分,这是Visual Studio中的惯例。

SelfTester界面如下所示:

图片3

左侧是解决方案列表,右侧是每个版本的Visual Studio的测试结果。

灰色的“不支持”标记表示该解决方案不支持Visual Studio的所选版本或尚未转换为该版本。 池中的某些解决方案具有一个设置,该设置指示要检查的特定Visual Studio版本。 如果未指定版本,则解决方案将更新为Visual Studio的所有后续版本。 屏幕快照中的此类解决方案的一个示例是“ smart_ptr_check.sln”(已对所有版本的Visual Studio进行了验证)。

绿色的“ OK”标记表示下一次检查未发现与参考日志有任何差异。 红色的“差异”标记表示差异。 开发人员应注意这些标签上的内容。 为此,他需要双击所需的标签。 所选的解决方案将在所需的Visual Studio版本中打开,并在其中打开带有警告日志的窗口。 使用下面的控制按钮,您可以重新开始对所选或所有决策的分析,将所选日志(或一次全部)分配给标准日志,等等。

SelfTester工作的显示结果始终在html报告(差异日志)中重复。

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

为什么速度很重要:

  1. 要在夜间测试期间运行,完成每个步骤所花费的时间至关重要。 显然,测试通过得越快越好。 目前,SelfTester的平均运行时间超过2小时;
  2. 白天启动SelfTester时,开发人员必须减少等待结果的时间,从而提高了劳动生产率。

渴望加快SelfTester的工作是这次的改进。

SelfTester中的多线程


SelfTester最初是作为多线程应用程序创建的,能够并行检查多个解决方案。 唯一的限制是您不能同时为不同版本的Visual Studio检查同一解决方案,因为在检查之前必须将许多解决方案更新到Visual Studio的某些版本。 在此期间,将直接对.vcxproj项目文件进行更改,这将导致在并行运行时出错。

为了使工作更有效率,SelfTester使用了智能任务计划程序,它使您可以为并行线程设置严格限制的值并对其进行维护。

调度程序在两个级别上使用。 第一个是解决方案级别,用于使用PVS-Studio_Cmd.exe实用工具开始检查.sln解决方案。 在PVS-Studio_Cmd.exe内部(在检查源代码文件的级别),使用了相同的调度程序,但是并行度设置不同。

并行度是一个参数,实际上指示应该同时运行多少个并行线程。 对于决策层和文件的并行度值,分别选择了默认值48 。 因此,此实现的并行线程数应等于32(四个同时测试的解决方案和八个文件)。 在我们看来,此设置似乎是分析仪在八核处理器上工作的最佳选择。

开发人员可以独立设置并行度的其他值,专注于计算机的性能或当前任务。 如果他未设置此参数,则默认情况下将选择系统的逻辑处理器数。

注意 :进一步,我们将考虑使用默认的并行度值进行工作。

LimitedConcurrencyLevelTask​​Scheduler调度程序是System.Threading.Tasks.TaskScheduler继承的,并且经过精炼以在ThreadPool上工作时提供最大的并行度。 继承层次结构:

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

PausableTaskScheduler允许暂停任务的执行,并且LimitedConcurrencyLevelTask​​Scheduler还可提供对任务队列的智能控制和执行计划,同时考虑并行度,已调度任务的数量和其他因素。 启动System.Threading.Tasks.Task任务时使用调度程序。

改进的先决条件


上述工作的实现有一个缺点:在使用不同大小的解决方案时,它不是最佳的。 测试池中解决方案的大小有很大不同:解决方案文件夹的大小从8 KB到4 GB,每个源代码文件从一到数千个。

调度程序仅按顺序对决策进行排队,而无需任何智能组件。 让我提醒您,默认情况下,不能同时检查四个以上的解决方案。 如果目前正在检查四个大型解决方案(每个文件中的文件数量都超过八个),则假定我们正在高效地工作,因为我们使用了最大可能的线程数(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继承并添加“ weight”字段。 要设置解决方案的权重,使用一种简单的算法:如果解决方案中的文件数少于八个,则将权重设置为等于此值(例如5),如果文件数大于或等于八,则将权重选择为等于八。

还需要优化调度程序:教他选择权重合适的解决方案,以实现32个线程的最大值。 还必须允许分配四个以上的线程以同时验证解决方案。

最后,它采取了初步步骤来分析所有池解决方案(使用MSBuild API进行评估),以计算和设置解决方案权重(获取带有源代码的文件数)。

结果


我认为,经过这么长时间的介绍,您已经猜到结果为零。

图片15

改进起来既简单又迅速,这很好。

好吧,现在,实际上,文章的“为我们带来了很多问题”部分开始了,仅此而已。

副作用


因此,负面结果也是结果。 事实证明,池中大型解决方案的数量大大超过小型解决方案(少于八个文件)的数量。 在这种情况下,所做的改进几乎没有什么效果,因为它们实际上是不可见的:与大型项目相比,它们的验证需要花费很短的时间。

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

然后...

图片5

一名开发人员抱怨SelfTester的“崩溃”。 好吧,它发生了。 为了防止丢失此错误,启动了内部事件(故障单),名称为“使用SelfTester时发生异常”。 在项目评估期间发生错误。 没错,在错误处理程序中,如此丰富的Windows也证明了该问题。 但这很快就消除了,在接下来的一周里什么也没发生。 突然,另一个用户抱怨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错误的工作。 有时,会有不同的人联系在一起(在整个事件发生期间,有6个人从事此问题的工作,其中包括两名实习生)。 然而,任务必须分心。

我们的第一个错误。 实际上,这里已经可以一劳永逸地解决问题。 怎么了 很明显,该错误是由新的优化引起的。 毕竟,在此之前,一切都运行良好,并且重用的代码显然不会那么糟糕。 而且,这种优化没有带来任何好处。 那该怎么办呢? 删除此优化 如您所知,这还没有完成。 我们继续致力于解决我们自己创造的问题。 搜索继续寻找问题的答案:“ HOW ???” 它如何下降? 但它似乎写得正确。

我们的第二个错误。 其他人与问题的解决有关。 一个非常非常大的错误。 不幸的是,这不仅不能解决问题,而且花费了更多的资源。 是的,新人们带来了新的想法,但是对于他们的实施,却花了很多时间(绝对浪费了)。 在某个阶段,(由相同的受训人员)编写了测试程序,这些程序模拟了在不同线程中对同一项目的评估,并在另一个线程中对该项目进行了并行修改。 它没有帮助。 除了我们之前已经知道的内容之外,MSBuild API还具有线程安全性,他们没有发现任何新的东西。 并且在SelfTester中,当抛出XmlException时添加了一个小型转储。 然后所有这一切都让人惊恐,恐惧。 进行了讨论,完成了许多其他不必要的事情。

最后,我们的第三个错误 。 您是否知道自SelfTester出现问题到解决该问题已经过去了多少时间? 虽然没有,但请数一数。 该事件创建于2018年9月17日,并于02/20/2019关闭,那里有40多个(四十个!)消息。 伙计们,这是很多时间! 我们允许自己从事IT 工作五个月。 同时(并行),我们参与了对Visual Studio 2019的支持,添加了Java语言,开始实施MISRA C / C ++标准,改进了C#分析器,积极参加了会议,撰写了大量文章等。 由于愚蠢的错误SelfTester,所有这些作品都没有给开发人员带来时间。

公民,从我们的错误中学习,永远不要那样做。 而且我们不会。

我有一切

图片17

当然,这是个玩笑,我将告诉您SelfTester的问题是什么:)

宾果!


幸运的是,我们当中有一个意识最模糊的人(我的同事谢尔盖·瓦西里耶夫(Sergey Vasiliev)),他只是从完全不同的角度看问题(他也很幸运)。 如果SelfTester内部确实可以正常工作,并且项目从外部破坏了某些东西怎么办? 与SelfTester并行运行时,通常什么也没有启动,在某些情况下,我们严格控制运行时间。 在这种情况下,此“内容”只能是SelfTester本身,而可以是它的另一个实例。

退出SelfTester后,从标准中恢复流的项目文件将继续工作一段时间。 此时,您可以重新启动测试仪。 后来增加了防止同时运行多个SelfTester实例的保护,现在看起来像这样:

图片16

但是后来她走了。

令人难以置信的是,在将近半年的酷刑中,没有人注意到它。 从标准还原项目是一个足够快的后台过程,但不幸的是,它还不够快,以免干扰SelfTester的重启。 在启动时会发生什么? 没错,计算决策权重。 一个过程将覆盖.vcxproj文件,而另一个则尝试读取它们。 嗨, XmlException

当Sergey向测试人员添加了切换到使用另一组标准日志的模式的功能时,他发现了所有这一切。 在将MISRA规则集添加到分析器后,就需要进行此操作。 您可以在用户看到窗口的同时直接在界面中切换:

图片14

之后,SelfTester 重新启动 。 好吧,很显然,以前,用户以某种方式自己模拟了问题,从而再次启动了测试仪。

汇报和结论


当然,我们删除或禁用了先前创建的优化。 此外,这比在测试仪其余部分之间进行某种同步要容易得多。 和以前一样,一切开始正常。 作为一项附加措施,增加了上述针对同时启动测试仪的保护措施。

我已经在上面写过关于在寻找问题期间遇到的主要错误的信息,因此自我鞭ation就足够了。 我们也是人,因此我们是错误的。 重要的是要从错误中吸取教训并得出结论。 这里的结论很简单:

  • 有必要跟踪和评估任务复杂性的增长;
  • 及时停止;
  • 尝试更广泛地看待问题,因为随着时间的流逝,视图会“模糊”,并且视角会变窄。
  • 不要害怕删除旧的或不必要的代码。

现在确定-就是这样。 感谢您的阅读。 对于所有绝望的代码!



如果您想与讲英语的读者分享这篇文章,请使用以下链接:Sergey Khrenov。 最好是善良的敌人

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


All Articles