开发工具与关键技术: Adobe Dreamweaver CC 2019
撰写时间:2021/5/7
它为什么是有用的?
作为一名JavaScript开发者,你可能经常发现自己处于代码覆盖可能有用的情景。例如:
· 对测试套件的质量感兴趣?重构一个大型的遗留项目?代码覆盖可以准确显示代码库中已覆盖了哪些部分。
· 想快速了解是否覆盖了代码库的特定部分?代码覆盖可以显示有关应用程序的哪些部分已被执行的实时信息,而不是使用console.log进行printf-风格的调试或手动执行代码。
· 或者你可能正在优化速度,并想知道要关注哪些点?执行次数可以指出关键函数和循环。
JavaScript在V8中的代码覆盖
今年早些时候,我们在V8上添加了对JavaScript代码覆盖的原生支持。5.9版本中的初始发布提供了函数粒度(显示已执行的函数)的覆盖范围,后来扩展为支持在v6.2中的块粒度覆盖(同样的,仅对于单独表达式有效)。
函数粒度(左侧)和块粒度(右侧)
对JavaScript开发者
目前访问覆盖信息有两种主要的方式。对于JavaScript开发者,Chrome DevTools的Coverage tab给出了JS (和CSS)覆盖率并在源码面板中指出了无用代码。
块覆盖coverage 在DevTools Coverage 面板中的块覆盖。覆盖的行使用绿色标注,未覆盖的行则使用红色。
基于V8覆盖数据的Istanbul.js报告
给嵌入式
嵌入式及框架作者可以通过直接hook到Inspector API上获得更大的灵活性。V8提供两种不同的覆盖模式:
1.尽力覆盖模式下收集覆盖信息,确保在运行时对性能的影响最小,但可能会丢失已被垃圾回收(GC)函数的数据。
2.精确覆盖确保不会因为GC而丢失任何数据,用户可以选择接收执行计数而不是二进制覆盖信息;但性能可能会受此额外开销的影响(有关详细信息,请参阅下一节)。精准覆盖可以按函数或块粒度收集信息。
精准覆盖的Inspector API如下:
· Profiler.startPreciseCoverage(callCount, detailed) 使能覆盖信息收集,可选调用次数(vs.二进制覆盖)以及块粒度(vs. 函数粒度);
· Profiler.takePreciseCoverage() 返回已收集的覆盖信息,其中包含源码范围列表以及相关的执行次数;
· Profiler.stopPreciseCoverage() 禁用收集并释放相关数据结构。
Inspector协议间的通信可能如下所示:
1234567891011121314151617181920212223242526272829303132333435363738 // The embedder directs V8 to begin collecting precise coverage.{ "id": 26, "method": "Profiler.startPreciseCoverage","params": { "callCount": false, "detailed": true}}// Embedder requests coverage data (delta since last request).{ "id": 32, "method":"Profiler.takePreciseCoverage"}// The reply contains collection of nested source ranges.{ "id": 32, "result": { "result": [{"functions": [{"functionName": "fib","isBlockCoverage": true, // Block granularity."ranges": [ // An array of nested ranges.{"startOffset": 50, // Byte offset, inclusive."endOffset": 224, // Byte offset, exclusive."count": 1}, {"startOffset": 97,"endOffset": 107,"count": 0}, {"startOffset": 134,"endOffset": 144,"count": 0}, {"startOffset": 192,"endOffset": 223,"count": 0},]},"scriptId": "199","url": "file:///coverage-fib.html"}]}}// Finally, the embedder directs V8 to end collection and// free related data structures.{"id":37,"method":"Profiler.stopPreciseCoverage"}
同理,尽力覆盖可以使用Profiler.getBestEffortCoverage() 。
尽力覆盖
尽力和精确覆盖模式都大量重用其它的V8机制,其中首数被称为调用计数器的机制。每次通过V8的Ignition解释器调用函数时,我们都会在函数的反馈向量上增加其调用计数器。随着函数后来变得愈加频繁并通过优化编译器做了提升,这个计数器用于帮助辅助关于内联函数的内联决策;现在,我们也依靠它报告代码覆盖情况。
第二种重用机制确立了函数的源码范围。报告代码覆盖时,调用计数需要与源文件中的相关范围作关联。例如,在下面的示例中,我们不仅需要报告函数f已经执行了一次,还包含f的源码范围从第1行开始到第3行结束。
1234 functionf() {console.log('Hello World');}f();
又一次我们是幸运的,我们能够重用 V8 中的现有信息。由于 Function.prototype.toString 需要知道函数在原文件中的位置以提取适当的子字符串,函数已经知道它们在源代码中的起始位置和结束位置。
在收集到最优的覆盖范围时,这两种机制简单地结合在一起:首先,我们通过遍历整个堆来找到所有存活的函数。对于每个可见的函数,我们报告调用次数(存储在反馈向量中,我们可以从函数中访问)和源范围(方便存储在函数本身)。
请注意,由于无论是否启用 coverage,都会维护调用计数,因此尽力服务的覆盖不会引入任何运行时开销。它也不使用专用的数据结构,因此既不需要显式启用也无需显式禁用。
那么为什么这种模式称为尽力服务(best-effort)呢,它的局限性是什么?超出范围的函数可能会被垃圾回收器释放掉。这意味着相关的调用计数将会丢失,事实上我们完全忘记了这些函数曾经存在过。因此“尽力服务”:即使我们尽力了,所收集的覆盖信息也可能不完整。
精准覆盖 (函数粒度)
与尽力服务模式相比,精确覆盖可确保所提供的覆盖信息是完整的。为实现这一目标,我们会在启用精准覆盖后将所有反馈向量添加到V8的根参考集中,从而阻止GC对其进行回收。虽然这确保了信息无丢失,但它通过人为地保持对象存活增加内存开销。
精准覆盖模式还可以提供执行计数。这为精准覆盖实施增加了另一个窍门。回想一下,每次通过V的解释器调用函数时,调用计数器都会递增,并且一旦函数访问频率过高,这些函数就可以升级并进行优化。但优化的函数不再增加其调用计数器,因此必须禁用优化编译器,以使其报告的执行次数保持准确。
精准覆盖(块粒度)
块粒度覆盖必须报告准确到独立表达式层级的覆盖范围。例如,在下面的一段代码中,块覆盖可以检测到条件表达式的else分支: c从不执行,而函数粒度覆盖只会知道函数 f(作为一个整体)被覆盖了。
1234 functionf(a) {returna ? b : c;}f(true);
你可能从前面的部分想起我们已经在 V8 中提供了函数调用次数和源码范围。不幸的是,这不适合块覆盖的场景,我们必须实现新的机制来收集执行次数和它们相应的源码范围。
第一个方面是源码范围:假设我们拥有一个特定块的执行计数,我们如何将它们映射到源代码的一部分呢?为此,我们需要在解析源文件时收集相关位置信息。在块覆盖之前,V8已经在某种程度上做到了这一点。一个示例是由如上所述的Function.prototype.toString而触发的函数范围的收集。