首页>>前端>>Node->performance.timerify 的 bugs

performance.timerify 的 bugs

时间:2023-11-29 本站 点击:0

Node.js实现W3C performance API已经有一段时间了,最近我发现Node.js还提供了方便的Histogram API,可得到平均值、最小值、最大值,中位数或指定的百分位、标准差等。对于常见的函数执行时间的统计需求,可以:

import {performance, createHistogram} from 'perf_hooks'const histogram = createHistogram()const wrapped_fn = performance.timerify(fn, {histogram})doSth(wrapped_fn) // 内部可能多次调用 wrapped_fnconsole.log(  histogram.count,          // 采样次数   histogram.min,            // 最小值  histogram.percentile(50), // 中位值  histogram.mean,           // 平均值  histogram.stddev,         // 标准差)

performance.timerify(fn, {histogram})(Node.js v16+)生成一个包装函数,每次调用会对fn的执行计时(单位为纳秒)并将耗时写入histogram。看上去这个API用来做microbenchmark还是很方便的。

然而我在使用的时候遇到了bug——fn的返回值如果是primitive值,包装函数的返回值会变成一个空对象。我当时写了个fn会返回null,它给我偷换成了个对象,自然把程序搞挂了。

研究了一番后,我发现如果fn是普通函数(即function fn() {}),会总是以new fn方式调用。

到Node.js仓库里查找了一番,已经有人发了Issue #40623。也有试图修复的PR #40625,但一直没有被合进去,因为其修复方式并不合理。

从讨论中可见,原作者的意图是,如果是构造器,那么就new之,于是写了类似IsConstructor(fn) ? new fn(...args) : fn(...args)的逻辑,但忘记了普通函数也是构造器。

【所以有个workaround就是写成箭头函数——箭头函数不是构造器。】

PR则改为了类似IsClass(fn)。但这导致传统的非class的构造器就不会以new方式调用了。尽管ES6之后绝大部分新代码都已经用class了,但总还是有老代码。另外还有一种情况是,代码本身是以class写的,但是可能发的包仍然是被编译成ES5了。

【此外,该PR的IsClass的判断是通过/^\s*class/.test(fn.toString())这样的hack方式,并不靠谱。比如内建构造器的toString()结果并不会以"class"开头;又比如,按照目前stage3的decorator提案,被decorator所修饰的class的toString()结果会包含decorator(也就是以"@deco class"开头);未来也可能包含其他修饰关键字(比如abstractasyncfinalstatic等)。 】

实际上,合理的逻辑并不是检查fn是否是构造器,而应是原样传递语义——包装函数在这里应该是一个代理。

假如用Proxy实现的话是很简单的,大体如下:

function timerify(fn) {  return new Proxy(fn, {    construct(...args) {      const start = now()      const result = Reflect.construct(...args)      processComplete(start)      return result    },    apply(...args) {      const start = now()      const result = Reflect.apply(...args)      processComplete(start)      return result    },  }}

不过我们可能并不想用proxy。(比如担心proxy的性能?可能阻止内联?)

如果直接写包装函数应该怎么写呢?

逻辑上是IsNew ? new fn(...args) : fn(...args),IsNew表示当前执行函数是否是以new调用的,但IsNew如何写?

传统上,我们可以用instanceof来判定:

function timerify(fn) {  return function timerified(...args) {    const start = now()    const result = this instanceof timerified      ? new fn(...args) : fn.call(this, ...args)    processComplete(start)    return result  }}

不过现在可以祭出更精确的new.target这个元属性(meta property):

function timerify(fn) {  return function (...args) {    const start = now()    const result = new.target      ? Reflect.construct(fn, args, new.target)      : Reflect.apply(fn, this, args)    processComplete(start)    return result  }}

【注意Reflect.construct的第三个参数,在当前实现中是没有传递的。这意味着当前实现也不能正确处理子类继承如class X extends timerify(Base)的情形。】

更进一步说,timerify最好和Function.prototype.bind一样,如果fn不是构造器,返回的包装函数也不是构造器。

【要返回一个非构造器的函数,可以使用一个偏门小技巧——简写形式方法不是构造器,所以可以写成:return {fn() { ... }}.fn。】

PS. 在研究这个bug时,我查看了timerify源码,并发现了另外两个bug ? ,于是去开了issue。

第一个issue是performance.timerify(fn, options) always return the same timerifed function · Issue #42742 · nodejs/node。

当前实现画蛇添足地做了缓存,即多次timerify(fn)的结果返回同一个函数。 然而我们可能有需求要为同一个fn产生多个包装函数,比如为相同函数在不同场景的使用生成不同的统计函数:

let h1 = perf_hooks.createHistogram()let h2 = perf_hooks.createHistogram()let f1 = perf_hooks.performance.timerify(f, {histogram: h1})let f2 = perf_hooks.performance.timerify(f, {histogram: h2})f1 !== f2 // expect true, actual false

结果调用f2的用时数据并不会写入h2,而是也写入了h1

第二个issue是performance.timerify(fn) behave inconsistently for sync/async functions · Issue #42743 · nodejs/node。

timerify对异步函数(或所有返回promise的函数)做了特殊处理,计时不是到函数调用结束(返回promise)之时,而是到promise完成之后。这符合大部分使用者的直觉。但当前实现不是使用then调用,而是再次画蛇添足地使用了finally调用。Promise.prototype.finally会确保无论成功失败总是调用,看上去似乎更「安全」,但实际上在这里使用finally,会导致异步函数和非异步函数调用结果不一致。因为包装函数调用fn时并没有使用try ... finally构造,如果throw,则并不会对本次调用完成计时。

为了确保一致,要么都不用finally,要么都用finally。事实上,之所以promise上的这个方法命名为finally,也是在提示这个方法和try ... finally的对应性。然而在本例中还是被无视了……

那么到底是否应该用finally呢?不应该用。因为我们计时是希望测量函数的运行时间,throw或reject表明并没有完成函数的正常计算逻辑,不符合我们的统计目标,不应该被计时。

【即使要用finally,当前实现中的逻辑if (result?.finally) result.finally(...)也是有问题的。因为promise或所谓thenable的标志是then方法而不是finally方法。依赖finally方法就和上面提到的依赖toString的结果一样不严谨。】

总结:写代码要做到严谨是不容易的。即使是Node.js这样的明星项目,即使是出自James M Snell这样的资深程序员之手,即使是一个并不算太复杂的API,即使只有100行代码……也可能潜藏各种问题。

【当然,我们可以喷Node.js的代码质量也不过尔尔;其实就算JS引擎代码,也经常出bug(如https://github.com/hax/hax.github.com/issues/51);JS标准规范,也有很多bug,比如前面提到的finally方法,就有和then方法存在行为不一致的bug:https://github.com/tc39/ecma262/issues/2222,而且因为涉及潜在的安全性问题,委员会还没就如何修这个bug达成一致意见……总之,是人类的产物,就会有bug。而且相比人类复杂系统中的各种bug——大到战争,小到团购,程序bug算是最容易处理了,一篇文章就能写清楚。】

【题图盗自《Measure execution times in browsers & Node.js》,是一篇不错的入门文章。】

【本文首发于我的知乎专栏:https://zhuanlan.zhihu.com/p/498708739 】

原文:https://juejin.cn/post/7096387106297085988


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/Node/885.html