深入了解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 sudoku 数独智力游戏生成代码
Mar 27 Javascript
jquery ui resizable bug解决方法
Oct 26 Javascript
js 程序执行与顺序实现详解
May 13 Javascript
仿谷歌主页js动画效果实现代码
Jul 14 Javascript
css如何让浮动元素水平居中
Aug 07 Javascript
js下拉选择框与输入框联动实现添加选中值到输入框的方法
Aug 17 Javascript
jquery UI Datepicker时间控件的使用及问题解决
Apr 28 Javascript
浅谈jQuery效果函数
Sep 16 Javascript
javascript获取图片的top N主色值方法详解
Jan 26 Javascript
vue-cli 3.0 自定义vue.config.js文件,多页构建的方法
Sep 19 Javascript
在react项目中使用antd的form组件,动态设置input框的值
Oct 24 Javascript
vue实现登录、注册、退出、跳转等功能
Dec 23 Vue.js
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利用COM对象访问SQLServer、Access
2006/10/09 PHP
PHP详细彻底学习Smarty
2008/03/27 PHP
利用PHP制作简单的内容采集器的原理分析
2008/10/01 PHP
两种设置php载入页面时编码的方法
2014/07/29 PHP
php数组添加元素方法小结
2014/12/20 PHP
微信公众平台开发关注及取消关注事件的方法
2014/12/23 PHP
php如何修改SESSION的生存存储时间的实例代码
2017/07/05 PHP
一款JavaScript压缩工具:X2JSCompactor
2007/06/13 Javascript
JavaScript 组件之旅(一)分析和设计
2009/10/28 Javascript
菜鸟javascript基础资料整理3 正则
2010/12/06 Javascript
JavaScript使用IEEE 标准进行二进制浮点运算产生莫名错误的解决方法
2011/05/28 Javascript
如何将JS的变量值传递给ASP变量
2012/12/10 Javascript
JS动态添加option和删除option(附实例代码)
2013/04/01 Javascript
JQuery中SetTimeOut传参问题探讨
2013/05/10 Javascript
js操作输入框提示信息且响应鼠标事件
2014/03/25 Javascript
使用Node.js实现一个简单的FastCGI服务器实例
2014/06/09 Javascript
javascript跨域总结之window.name实现的跨域数据传输
2015/11/01 Javascript
jquery的ajax提交form表单的两种方法小结(推荐)
2016/05/25 Javascript
js 声明数组和向数组中添加对象变量的简单实例
2016/07/28 Javascript
Angular实现预加载延迟模块的示例
2017/10/12 Javascript
利用npm 安装删除模块的方法
2018/05/15 Javascript
webpack4 CSS Tree Shaking的使用
2018/09/03 Javascript
手挽手带你学React之React-router4.x的使用
2019/02/14 Javascript
深入解析vue 源码目录及构建过程分析
2019/04/24 Javascript
微信小程序wx.getUserInfo授权获取用户信息(头像、昵称)的实现
2020/08/19 Javascript
原生js+css实现tab切换功能
2020/09/17 Javascript
python3中的md5加密实例
2018/05/29 Python
python 获取键盘输入,同时有超时的功能示例
2018/11/13 Python
Python列表(List)知识点总结
2019/02/18 Python
python 普通克里金(Kriging)法的实现
2019/12/19 Python
pytorch查看torch.Tensor和model是否在CUDA上的实例
2020/01/03 Python
2015年综治维稳工作总结
2015/04/07 职场文书
盗窃案辩护词
2015/05/21 职场文书
致运动员加油稿
2015/07/21 职场文书
2019幼儿园感恩节活动策划书
2019/11/28 职场文书
Java使用JMeter进行高并发测试
2021/11/23 Java/Android