深入了解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 相关文章推荐
用JS实现一个页面多个css样式实现
May 29 Javascript
juqery 学习之三 选择器 可见性 元素属性
Nov 25 Javascript
jquery动态增加删除表格行的小例子
Nov 14 Javascript
jquery cookie的用法总结
Nov 18 Javascript
node.js开发中使用Node Supervisor实现监测文件修改并自动重启应用
Nov 04 Javascript
AngularJS中的Directive实现延迟加载
Jan 25 Javascript
用file标签实现多图文件上传预览
Feb 14 Javascript
JavaScript判断浏览器和hack滚动条的写法
Jul 23 Javascript
vue.js开发实现全局调用的MessageBox组件实例代码
Nov 22 Javascript
前端防止用户重复提交js实现代码示例
Sep 07 Javascript
通过vue-cli3构建一个SSR应用程序的方法
Sep 13 Javascript
详解微信小程序图片地扯转base64解决方案
Aug 18 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
雄兵连:天使彦天使彦为爱折翼,彦和炙心同时念动的誓言!
2020/03/02 国漫
php mysql数据库操作分页类
2008/06/04 PHP
php后台如何避免用户直接进入方法实例
2013/10/15 PHP
PHP中isset()和unset()函数的用法小结
2014/03/11 PHP
PHP+Mysql树型结构(无限分类)数据库设计的2种方式实例
2014/07/15 PHP
PHP使用array_merge重新排列数组下标的方法
2015/07/22 PHP
php实现可逆加密的方法
2015/08/11 PHP
PHP中empty,isset,is_null用法和区别
2017/02/19 PHP
摘自启点的main.js
2008/04/20 Javascript
学习ExtJS 访问容器对象
2009/10/07 Javascript
js对象的比较
2011/02/26 Javascript
JS判断客服QQ号在线还是离线状态的方法
2015/01/13 Javascript
ES6中如何使用Set和WeakSet
2016/03/10 Javascript
jQuery实现将div中滚动条滚动到指定位置的方法
2016/08/10 Javascript
手机端实现Bootstrap简单图片轮播效果
2016/10/13 Javascript
Vue报错:Uncaught TypeError: Cannot assign to read only property’exports‘ of object’#‘的解决方法
2017/06/17 Javascript
聊聊JS动画库 Velocity.js的使用
2018/03/13 Javascript
vue2.0+vuex+localStorage代办事项应用实现详解
2018/05/31 Javascript
从局部变量和全局变量开始全面解析Python中变量的作用域
2016/06/16 Python
Python实现购物评论文本情感分析操作【基于中文文本挖掘库snownlp】
2018/08/07 Python
linux安装python修改默认python版本方法
2019/03/31 Python
python生成特定分布数的实例
2019/12/05 Python
Python sys模块常用方法解析
2020/02/20 Python
ubuntu16.04升级Python3.5到Python3.7的方法步骤
2020/08/20 Python
Python读写csv文件流程及异常解决
2020/10/20 Python
MAC Cosmetics官方网站:魅可专业艺术彩妆
2019/04/10 全球购物
公司门卫管理制度
2014/02/01 职场文书
产品质量承诺书范文
2014/03/27 职场文书
小班评语大全
2014/05/04 职场文书
党员教师群众路线对照检查材料思想汇报
2014/09/29 职场文书
先进集体事迹材料范文
2014/12/25 职场文书
给老婆的检讨书
2015/01/27 职场文书
邹越演讲观后感
2015/06/15 职场文书
使用Redis实现秒杀功能的简单方法
2021/05/08 Redis
mybatis3中@SelectProvider传递参数方式
2021/08/04 Java/Android
Java 深入探究讲解简单工厂模式
2022/04/07 Java/Android