深入了解JavaScript代码覆盖


Posted in Javascript onJune 13, 2019

它为什么是有用的?

作为一名JavaScript开发者,你可能经常发现自己处于代码覆盖可能有用的情景。例如:

  • 对测试套件的质量感兴趣? 重构一个大型的遗留项目? 代码覆盖可以准确显示代码库中已覆盖了哪些部分。
  • 想快速了解是否覆盖了代码库的特定部分? 代码覆盖可以显示有关应用程序的哪些部分已被执行的实时信息,而不是使用console.log进行printf-风格的调试或手动执行代码。
  • 或者你可能正在优化速度,并想知道要关注哪些点? 执行次数可以指出关键函数和循环。

JavaScript在V8中的代码覆盖

今年早些时候,我们在V8上添加了对JavaScript代码覆盖的原生支持。5.9版本中的初始发布提供了函数粒度(显示已执行的函数)的覆盖范围,后来扩展为支持在v6.2中的块粒度覆盖(同样的,仅对于单独表达式有效)。

深入了解JavaScript代码覆盖

函数粒度(左侧)和块粒度(右侧)

对JavaScript开发者

目前访问覆盖信息有两种主要的方式。对于JavaScript开发者,Chrome DevTools的Coverage tab给出了JS (和CSS)覆盖率并在源码面板中指出了无用代码。

深入了解JavaScript代码覆盖

块覆盖coverage 在DevTools Coverage 面板中的块覆盖。覆盖的行使用绿色标注,未覆盖的行则使用红色。

深入了解JavaScript代码覆盖

基于V8覆盖数据的Istanbul.js报告

给嵌入式

嵌入式及框架作者可以通过直接hook到Inspector API上获得更大的灵活性。V8提供两种不同的覆盖模式:

1.尽力覆盖模式下收集覆盖信息,确保在运行时对性能的影响最小,但可能会丢失已被垃圾回收(GC)函数的数据。

2.精确覆盖确保不会因为GC而丢失任何数据,用户可以选择接收执行计数而不是二进制覆盖信息;但性能可能会受此额外开销的影响(有关详细信息,请参阅下一节)。精准覆盖可以按函数或块粒度收集信息。

精准覆盖的Inspector API如下:

  • Profiler.startPreciseCoverage(callCount, detailed) 使能覆盖信息收集,可选调用次数(vs.二进制覆盖)以及块粒度(vs. 函数粒度);
  • Profiler.takePreciseCoverage() 返回已收集的覆盖信息,其中包含源码范围列表以及相关的执行次数;
  • Profiler.stopPreciseCoverage() 禁用收集并释放相关数据结构。

Inspector协议间的通信可能如下所示:

// 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机制,其中首数被称为调用计数器的机制。每次通过V8的Ignition解释器调用函数时,我们都会在函数的反馈向量上增加其调用计数器。随着函数后来变得愈加频繁并通过优化编译器做了提升,这个计数器用于帮助辅助关于内联函数的内联决策;现在,我们也依靠它报告代码覆盖情况。

第二种重用机制确立了函数的源码范围。报告代码覆盖时,调用计数需要与源文件中的相关范围作关联。例如,在下面的示例中,我们不仅需要报告函数f已经执行了一次,还包含f的源码范围从第1行开始到第3行结束。

function f() {
console.log('Hello World');
}
f();

又一次我们是幸运的,我们能够重用 V8 中的现有信息。由于 Function.prototype.toString 需要知道函数在原文件中的位置以提取适当的子字符串,函数已经知道它们在源代码中的起始位置和结束位置。

在收集到最优的覆盖范围时,这两种机制简单地结合在一起:首先,我们通过遍历整个堆来找到所有存活的函数。对于每个可见的函数,我们报告调用次数(存储在反馈向量中,我们可以从函数中访问)和源范围(方便存储在函数本身)。

请注意,由于无论是否启用 coverage,都会维护调用计数,因此尽力服务的覆盖不会引入任何运行时开销。它也不使用专用的数据结构,因此既不需要显式启用也无需显式禁用。

那么为什么这种模式称为尽力服务(best-effort)呢,它的局限性是什么? 超出范围的函数可能会被垃圾回收器释放掉。这意味着相关的调用计数将会丢失,事实上我们完全忘记了这些函数曾经存在过。 因此“尽力服务”:即使我们尽力了,所收集的覆盖信息也可能不完整。

精准覆盖 (函数粒度)

与尽力服务模式相比,精确覆盖可确保所提供的覆盖信息是完整的。为实现这一目标,我们会在启用精准覆盖后将所有反馈向量添加到V8的根参考集中,从而阻止GC对其进行回收。虽然这确保了信息无丢失,但它通过人为地保持对象存活增加内存开销。

精准覆盖模式还可以提供执行计数。这为精准覆盖实施增加了另一个窍门。回想一下,每次通过V的解释器调用函数时,调用计数器都会递增,并且一旦函数访问频率过高,这些函数就可以升级并进行优化。 但优化的函数不再增加其调用计数器,因此必须禁用优化编译器,以使其报告的执行次数保持准确。

精准覆盖(块粒度)

块粒度覆盖必须报告准确到独立表达式层级的覆盖范围。例如,在下面的一段代码中,块覆盖可以检测到条件表达式的else分支: c从不执行,而函数粒度覆盖只会知道函数 f(作为一个整体)被覆盖了。

function f(a) {
return a ? b : c;
}
f(true);

你可能从前面的部分想起我们已经在 V8 中提供了函数调用次数和源码范围。不幸的是,这不适合块覆盖的场景,我们必须实现新的机制来收集执行次数和它们相应的源码范围。

第一个方面是源码范围:假设我们拥有一个特定块的执行计数,我们如何将它们映射到源代码的一部分呢? 为此,我们需要在解析源文件时收集相关位置信息。在块覆盖之前,V8已经在某种程度上做到了这一点。一个示例是由如上所述的Function.prototype.toString而触发的函数范围的收集。

另一个例子是用于构造Error对象的回溯的源码位置。但这些都不足以支持块覆盖; 前者仅适用于函数,而后者仅保存位置信息(例如if-else语句的if标记的位置),而不是源码范围。

因此,我们必须扩展解析器以收集源码范围。为了演示,假设我们正在使用if-else语句:

if (cond) {
/* Then branch. */
} else {
/* Else branch. */
}

当启用块覆盖时,我们收集 then 和 else 分支的源码范围,并将它们与已解析的 IfStatement AST 节点相关联。其他相关语言结构也是如此处理。

在解析过程中收集完源码范围集之后,第二个方面是在运行时跟踪执行计数。 这是通过在生成的字节码数组的关键位置插入新的专用 IncBlockCounter 字节码来完成的。在运行时,IncBlockCounter 字节码处理程序只是增加对应的计数器接口(可通过函数对象访问)。

在 if-else 语句的上述示例中,这样的字节码将被插入在三个位置:紧接在 then 分支的主体之前,在 else 分支的主体之前,紧接在 if-else 语句之后(由于分支内可能存在非本地控制,因此需要连续的计数器)。

最后,报告块粒度覆盖与函数粒度报告类似。但除了调用计数(来自反馈向量)之外,我们现在还报告了感兴趣的源范围的集合以及它们的块计数(存储在挂起该函数的辅助数据结构中)。

如果您想了解V8中代码覆盖之后相关技术细节的更多信息,请参阅coverage和block coverage设计文档。

总结

我们希望你喜欢本文中对V8原生代码覆盖支持的简要介绍。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
Javascript继承(上)——对象构建介绍
Nov 08 Javascript
js实现收缩菜单效果实例代码
Oct 30 Javascript
JavaScript数值转换的三种方式总结
Jul 31 Javascript
JavaScript中Date.toSource()方法的使用教程
Jun 12 Javascript
原生js实现jquery函数animate()动画效果的简单实例
Aug 21 Javascript
Vuejs第一篇之入门教程详解(单向绑定、双向绑定、列表渲染、响应函数)
Sep 09 Javascript
基于JQuery实现的跑马灯效果(文字无缝向上翻动)
Dec 02 Javascript
基于HTML5+JS实现本地图片裁剪并上传功能
Mar 24 Javascript
详解Angular模板引用变量及其作用域
Nov 23 Javascript
node中实现删除目录的几种方法
Jun 24 Javascript
JS实现的雪花飘落特效示例
Dec 03 Javascript
jstree中的checkbox默认选中和隐藏示例代码
Dec 29 Javascript
js使用cookie实现记住用户名功能示例
Jun 13 #Javascript
探索JavaScript中私有成员的相关知识
Jun 13 #Javascript
详解vue中的父子传值双向绑定及数据更新问题
Jun 13 #Javascript
基于Vue实现平滑过渡的拖拽排序功能
Jun 12 #Javascript
Vue + Elementui实现多标签页共存的方法
Jun 12 #Javascript
JavaScript使用面向对象实现的拖拽功能详解
Jun 12 #Javascript
JS实现点击生成UUID的方法完整实例【基于jQuery】
Jun 12 #jQuery
You might like
PHP的简易冒泡法代码分享
2012/08/28 PHP
php excel reader读取excel内容存入数据库实现代码
2012/12/06 PHP
php求两个目录的相对路径示例(php获取相对路径)
2014/03/27 PHP
phpmailer简单发送邮件的方法(附phpmailer源码下载)
2016/06/13 PHP
php mysql PDO 查询操作的实例详解
2017/09/23 PHP
PHP PDOStatement::columnCount讲解
2019/01/30 PHP
Laravel创建数据库表结构的例子
2019/10/09 PHP
用javascript自动显示最后更新时间
2007/03/15 Javascript
JavaScript监测ActiveX控件是否已经安装过的代码
2008/09/02 Javascript
javascript Array数组对象的扩展函数代码
2010/05/22 Javascript
Extjs显示从数据库取出时间转换JSON后的出现问题
2012/11/20 Javascript
web基于浏览器的本地存储方法应用
2012/11/27 Javascript
js中事件的处理与浏览器对象示例介绍
2013/11/29 Javascript
jQuery通过改变input的type属性实现密码显示隐藏切换功能
2017/02/08 Javascript
详解webpack3如何正确引用并使用jQuery库
2017/08/26 jQuery
浅入深出Vue之自动化路由
2019/08/06 Javascript
[02:07]DOTA2超级联赛专访BBC:难忘网吧超神经历
2013/06/09 DOTA
Python strip lstrip rstrip使用方法
2008/09/06 Python
python实现批量改文件名称的方法
2015/05/25 Python
Python判断Abundant Number的方法
2015/06/15 Python
Python3.4学习笔记之类型判断,异常处理,终止程序操作小结
2019/03/01 Python
Python高级特性 切片 迭代解析
2019/08/23 Python
Python实现链表反转的方法分析【迭代法与递归法】
2020/02/22 Python
python 中的9个实用技巧,助你提高开发效率
2020/08/30 Python
python3访问字典里的值实例方法
2020/11/18 Python
使用css实现android系统的loading加载动画
2019/07/25 HTML / CSS
深入剖析webstorage[html5的本地数据处理]
2016/07/11 HTML / CSS
使用canvas压缩图片大小的方法示例
2019/08/02 HTML / CSS
Stokke美国官方网店:高级儿童家具、推车、汽车座椅和配件
2020/06/06 全球购物
电话销售经理岗位职责
2013/12/07 职场文书
公司司机岗位职责
2014/02/07 职场文书
大学生评语大全
2014/04/18 职场文书
会计主管岗位职责
2015/04/02 职场文书
2015年学校党建工作总结
2015/05/19 职场文书
python数据分析之单因素分析线性拟合及地理编码
2022/06/25 Python
全网非常详细的pytest配置文件
2022/07/15 Python