深入了解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 相关文章推荐
jquery 插件 web2.0分格的分页脚本,可用于ajax无刷新分页
Dec 25 Javascript
JQuery slideshow的一个小问题(如何发现及解决过程)
Feb 06 Javascript
javascript获取form里的表单元素的示例代码
Feb 14 Javascript
AngularJS转换响应内容
Jan 27 Javascript
JS实现太极旋转思路分析
Dec 09 Javascript
微信小程序之MaterialDesign--input组件详解
Feb 15 Javascript
layui的table中显示图片方法
Aug 17 Javascript
webpack之引入图片的实现及问题
Oct 08 Javascript
创建Vue项目以及引入Iview的方法示例
Dec 03 Javascript
Elasticsearch实现复合查询高亮结果功能
Sep 10 Javascript
详解前端任务构建利器Gulp.js使用指南
Apr 30 Javascript
React自定义hook的方法
Jun 25 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
第十三节--对象串行化
2006/11/16 PHP
PHP临时文件的安全性分析
2014/07/04 PHP
jQuery插件开发全解析
2012/10/10 Javascript
javascript实现点击按钮让DIV层弹性移动的方法
2015/02/24 Javascript
JS实现进入页面时渐变背景色的方法
2015/02/25 Javascript
HTML5+jQuery插件Quicksand实现超酷的星际争霸2兵种分类展示效果(附demo源码下载)
2016/05/25 Javascript
JavaScript制作简单分页插件
2016/09/11 Javascript
jquery 正整数数字校验正则表达式
2017/01/10 Javascript
Javascript中构造函数要注意的一些坑
2017/01/23 Javascript
nodeJS实现路由功能实例代码
2017/06/08 NodeJs
jquery获取transform里的值实现方法
2017/12/12 jQuery
jQuery模拟12306城市选择框功能简单实现方法示例
2018/08/13 jQuery
JavaScript简单实现动态改变HTML内容的方法示例
2018/12/25 Javascript
vue中v-text / v-html使用实例代码详解
2019/04/02 Javascript
JS获取当前时间的年月日时分秒及时间的格式化的方法
2019/12/18 Javascript
[03:37]2015国际邀请赛第四日现场精彩集锦
2015/08/08 DOTA
[44:15]DOTA2上海特级锦标赛主赛事日 - 5 败者组决赛Liquid VS EG第二局
2016/03/06 DOTA
python删除列表内容
2015/08/04 Python
Python 3中的yield from语法详解
2017/01/18 Python
详谈Python基础之内置函数和递归
2017/06/21 Python
Python使用当前时间、随机数产生一个唯一数字的方法
2017/09/18 Python
python爬虫系列Selenium定向爬取虎扑篮球图片详解
2017/11/15 Python
python 去除txt文本中的空格、数字、特定字母等方法
2018/07/24 Python
python使用Plotly绘图工具绘制气泡图
2019/04/01 Python
在Python函数中输入任意数量参数的实例
2019/07/16 Python
Python lambda表达式filter、map、reduce函数用法解析
2019/09/11 Python
python orm 框架中sqlalchemy用法实例详解
2020/02/02 Python
详解torch.Tensor的4种乘法
2020/09/03 Python
英国领先的鞋类零售商和顶级品牌的官方零售商:Wynsors
2020/02/17 全球购物
酒店门卫岗位职责
2013/12/29 职场文书
锦旗标语大全
2014/06/23 职场文书
一年级语文上册复习计划
2015/01/17 职场文书
莫言诺贝尔获奖感言(全文)
2015/07/31 职场文书
2019学校运动会开幕词
2019/05/13 职场文书
简历上的自我评价,该怎么写呢?
2019/06/13 职场文书
SQL Server表分区删除详情
2021/10/16 SQL Server