前言

最近因为需求,需要测量JavaScript单个函数的执行时间,但由于精度问题,遇到了各种各样的问题,在此做简单记录。

 

JavaScript高精度时间函数

首先得明确,JavaScript单个函数执行时间应该是微秒级的,所以必须需要高精度。那么第一反应自然是查找是否有相关函数,以往的时间函数new Date().getTime()肯定是不够了,因为其精度为毫秒级,测量函数执行时间,得到的结果必然为0。
这里我们查阅手册,将目光定位到performance.now()
我们不妨做个测试

let n = 0;
while (n<10000){
  console.log((new Date()).getTime());
  n = n+1;
}

得到结果

而对于

let n = 0;
while (n<10000){
  console.log(performance.now());
  n = n+1;
}

得到结果

我们可以轻易的看出,这里进行一次循环大概是0.04ms,而精度在ms级的new Date().getTime()已经无能为力

 

精度失灵

既然确定了performance.now(),不妨在Chrome进行测试

我们可以轻松测量出crypto.getRandomValues(new Uint8Array(10))的运行时间大概为0.1ms

但由于可能存在误差,我尝试运行1000次,却出现了问题

竟然出现了大量的0

我又在我虚拟机里的Chrome运行

这是什么原因?对比之后发现

虚拟机

而物理机

查阅Chrome的Updates (2018)

由于高精度的时间可应用于重大漏洞speculative execution side-channel attack(https://spectreattack.com/)

所以各大主流浏览器做出了安全措施

例如FireFox、Safari,将performance.now()精度降低为1ms

而Chrome改为100微秒并加入了抖动

所以这也不难解释,为什么Chrome 71版本得到这么多0,相比FireFox、Safari,能得到数据,已经算是仁慈了

 

柳暗花明

那么怎么进行高精度测量呢?不能因为浏览器的不支持,我们就不完成需求吧~

这里查阅文章发现

https://link.springer.com/chapter/10.1007/978-3-319-70972-7_13

一文中进行了JavaScript侧信道测量时间的介绍

由于精度问题,例如

var start = performance.now();
func()
var end = performance.now();

会使得start = end,这样测量出来只能为0,而作者很巧妙的使用了wait_edge()

function wait_edge()
{
    var next,last = performance.now();
    while((next = performance.now()) == last)
    {}
    return next;
}

这样一来就可以到下一次performance.now()的时候再继续

那么问题又来了,中间空转的时间怎么办呢?

作者又使用了count_edge()进行了空转次数探测

function count_edge()
{
    var last = performance.now(),count = 0;
    while(performance.now() == last) count++;
    return count;
}

那么怎么把空转次数的单次时间测量出来呢?这里作者又设计了calibrate()

function calibrate()
{
    var counter = 0,next;
    for(var i=0;i<10;i++)
    {
        next = wait_edge();
        counter += count_edge();
    }
    next = wait_edge();
    return (wait_edge() - next)/(counter/10.0);
}

假设我们要测量函数fuc(),即可如下编写即可

function measure()
{
    var start = wait_edge();
    fuc();
    var count = count_edge();
    return (performance.now()-start)-count*calibrate();
}

即结束减去开始的时间,再减去中间空转的时间。

我们再来用chrome 71测试一下

和之前的performance.now()对比

显然误差已经控制在了0.01ms,即10微秒内,这是我们能接受的

当然,在FireFox这种ms级的更有成就感,因为之前的结果都是0,但是用这样的方法,可以测量了

FireFox:

 

测试与结论

我以crypto.getRandomValues(new Uint8Array(n));为例测试

performance.now()的结果和measure()进行做差比较,不难发现

Chrome

在Chrome 57版本下,差异仅在10微秒以内。(注:结果由performance.now()经过进制转换输出)

而在Chrome 71版本下,差异却达到了50微秒以内(注:结果由performance.now()经过进制转换输出)

原因也很明显,因为71版本的performance.now()降低了精度,并且加入了抖动,导致许多end-start的值为0

那么我们在71版本下直接测试侧信道方式得到的时间

不难发现,其实在71版本下计算差是没有意义的,因为performance.now()的精度已经变为100微秒

所以做差得到的值基本是侧信道方式测得的结果。

所以我们基本可以确定,这样的方式在目前chrome版本可以得到比performance.now()更高精度的时间测量

但我们的目的肯定不局限于Chrome,我们再看看Firefox

Firefox

对于Firefox就更过分了,通过performance.now()测量高精度时间直接变成了不可能,因为精度被调整成了毫秒级,所以end-start的值都变为了0

而对于侧信道测量方式

我们依旧还是可以测量出许多微秒级的时间

 

后记

这样的方式可以有效突破浏览器的高精度函数毫秒级的限制,甚至对于一些特定攻击依旧奏效。

若有更好的方式还请大佬不吝赐教~

 

参考链接

https://zhuanlan.zhihu.com/p/32629875

https://link.springer.com/chapter/10.1007/978-3-319-70972-7_13

源链接

Hacking more

...