1.performance.timerify的源码bugs
2.performance.timerifyçbugs
performance.timerify的bugs
Node.js实现W3CperformanceAPI已经有一段时间了,最近我发现Node.js还提供了方便的解析HistogramAPI,可得到平均值、源码最小值、解析最大值,源码中位数或指定的解析内存监视器源码百分位、标准差等。源码对于常见的解析函数执行时间的统计需求,可以: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(),?//?中位值histogram.mean,?//?平均值histogram.stddev,?//?标准差)performance.timerify(fn,{ histogram})(Node.jsv+)生成一个包装函数,每次调用会对fn的解析执行计时(单位为纳秒)并将耗时写入histogram。看上去这个API用来做microbenchmark还是源码很方便的。
然而我在使用的解析时候遇到了bug——fn的返回值如果是primitive值,包装函数的源码捕获主力指标源码返回值会变成一个空对象。我当时写了个fn会返回null,解析它给我偷换成了个对象,源码自然把程序搞挂了。
研究了一番后,我发现如果fn是普通函数(即functionfn(){ }),会总是以newfn方式调用。
到Node.js仓库里查找了一番,已经有人发了Issue#。也有试图修复的PR#,但一直没有被合进去,因为其修复方式并不合理。
从讨论中可见,电子协作白板源码原作者的意图是,如果是构造器,那么就new之,于是写了类似IsConstructor(fn)?newfn(...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(也就是以"@decoclass"开头);未来也可能包含其他修饰关键字(比如abstract、async、final、static等)。
实际上,合理的福州台源码头逻辑并不是检查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?newfn(...args):fn(...args),IsNew表示当前执行函数是否是以new调用的,但IsNew如何写?
传统上,我们可以用instanceof来判定:
function?timerify(fn)?{ return?function?timerified(...args)?{ const?start?=?now()const?result?=?this?instanceof?timerifiednew?fn(...args)?:?fn.call(this,?...args)processComplete(start)return?result}}不过现在可以祭出更精确的new.target这个元属性(metaproperty):
function?timerify(fn)?{ return?function?(...args)?{ const?start?=?now()const?result?=?new.targetReflect.construct(fn,?args,?new.target):?Reflect.apply(fn,?this,?args)processComplete(start)return?result}}注意Reflect.construct的第三个参数,在当前实现中是没有传递的。这意味着当前实现也不能正确处理子类继承如classXextendstimerify(Base)的情形。
更进一步说,timerify最好和Function.prototype.bind一样,如果fn不是构造器,返回的包装函数也不是构造器。
要返回一个非构造器的函数,可以使用一个偏门小技巧——简写形式方法不是构造器,所以可以写成:return{ fn(){ ...}}.fn。
PS.在研究这个bug时,我查看了timerify源码,并发现了另外两个bug?,于是去开了issue。
第一个issue是performance.timerify(fn,options)alwaysreturnthesametimerifedfunction·Issue#·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)behaveinconsistentlyforsync/asyncfunctions·Issue#·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这样的明星项目,即使是出自JamesMSnell这样的资深程序员之手,即使是一个并不算太复杂的API,即使只有行代码……也可能潜藏各种问题。
当然,我们可以喷Node.js的代码质量也不过尔尔;其实就算JS引擎代码,也经常出bug(如/post/
performance.timerifyçbugs
Node.jså®ç°W3CperformanceAPIå·²ç»æä¸æ®µæ¶é´äºï¼æè¿æåç°Node.jsè¿æä¾äºæ¹ä¾¿çHistogramAPIï¼å¯å¾å°å¹³åå¼ãæå°å¼ãæ大å¼ï¼ä¸ä½æ°ææå®çç¾åä½ãæ åå·®çã对äºå¸¸è§çå½æ°æ§è¡æ¶é´çç»è®¡éæ±ï¼å¯ä»¥ï¼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(),?//?ä¸ä½å¼?histogram.mean,//?å¹³åå¼?histogram.stddev,?//?æ åå·®)performance.timerify(fn,{ histogram})ï¼Node.jsv+ï¼çæä¸ä¸ªå è£ å½æ°ï¼æ¯æ¬¡è°ç¨ä¼å¯¹fnçæ§è¡è®¡æ¶ï¼åä½ä¸ºçº³ç§ï¼å¹¶å°èæ¶åå ¥histogramãçä¸å»è¿ä¸ªAPIç¨æ¥åmicrobenchmarkè¿æ¯å¾æ¹ä¾¿çã
ç¶èæå¨ä½¿ç¨çæ¶åéå°äºbugââfnçè¿åå¼å¦ææ¯primitiveå¼ï¼å è£ å½æ°çè¿åå¼ä¼åæä¸ä¸ªç©ºå¯¹è±¡ãæå½æ¶åäºä¸ªfnä¼è¿ånullï¼å®ç»æå·æ¢æäºä¸ªå¯¹è±¡ï¼èªç¶æç¨åºææäºã
ç 究äºä¸çªåï¼æåç°å¦æfnæ¯æ®éå½æ°ï¼å³functionfn(){ }ï¼ï¼ä¼æ»æ¯ä»¥newfnæ¹å¼è°ç¨ã
å°Node.jsä»åºéæ¥æ¾äºä¸çªï¼å·²ç»æ人åäºIssue#ãä¹æè¯å¾ä¿®å¤çPR#ï¼ä½ä¸ç´æ²¡æ被åè¿å»ï¼å ä¸ºå ¶ä¿®å¤æ¹å¼å¹¶ä¸åçã
ä»è®¨è®ºä¸å¯è§ï¼åä½è çæå¾æ¯ï¼å¦ææ¯æé å¨ï¼é£ä¹å°±newä¹ï¼äºæ¯åäºç±»ä¼¼IsConstructor(fn)?newfn(...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ï¼ä¹å°±æ¯ä»¥"@decoclass"å¼å¤´ï¼ï¼æªæ¥ä¹å¯è½å å«å ¶ä»ä¿®é¥°å ³é®åï¼æ¯å¦abstractãasyncãfinalãstaticçï¼ãã
å®é ä¸ï¼åççé»è¾å¹¶ä¸æ¯æ£æ¥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?newfn(...args):fn(...args)ï¼IsNew表示å½åæ§è¡å½æ°æ¯å¦æ¯ä»¥newè°ç¨çï¼ä½IsNewå¦ä½åï¼
ä¼ ç»ä¸ï¼æ们å¯ä»¥ç¨instanceofæ¥å¤å®ï¼
function?timerify(fn)?{ ?return?function?timerified(...args)?{ const?start?=?now()const?result?=?this?instanceof?timerifiednew?fn(...args)?:?fn.call(this,?...args)processComplete(start)return?result?}}ä¸è¿ç°å¨å¯ä»¥ç¥åºæ´ç²¾ç¡®çnew.targetè¿ä¸ªå å±æ§ï¼metapropertyï¼ï¼
function?timerify(fn)?{ ?return?function?(...args)?{ const?start?=?now()const?result?=?new.targetReflect.construct(fn,?args,?new.target)?:?Reflect.apply(fn,?this,?args)processComplete(start)return?result?}}ã注æReflect.constructç第ä¸ä¸ªåæ°ï¼å¨å½åå®ç°ä¸æ¯æ²¡æä¼ éçãè¿æå³çå½åå®ç°ä¹ä¸è½æ£ç¡®å¤çå类继æ¿å¦classXextendstimerify(Base)çæ å½¢ãã
æ´è¿ä¸æ¥è¯´ï¼timerifyæ好åFunction.prototype.bindä¸æ ·ï¼å¦æfnä¸æ¯æé å¨ï¼è¿åçå è£ å½æ°ä¹ä¸æ¯æé å¨ã
ãè¦è¿åä¸ä¸ªéæé å¨çå½æ°ï¼å¯ä»¥ä½¿ç¨ä¸ä¸ªåé¨å°æå·§ââç®åå½¢å¼æ¹æ³ä¸æ¯æé å¨ï¼æ以å¯ä»¥åæï¼return{ fn(){ ...}}.fnãã
PS.å¨ç 究è¿ä¸ªbugæ¶ï¼ææ¥çäºtimerifyæºç ï¼å¹¶åç°äºå¦å¤ä¸¤ä¸ªbug?ï¼äºæ¯å»å¼äºissueã
第ä¸ä¸ªissueæ¯performance.timerify(fn,options)alwaysreturnthesametimerifedfunction·Issue#·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)behaveinconsistentlyforsync/asyncfunctions·Issue#·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è¿æ ·çææ项ç®ï¼å³ä½¿æ¯åºèªJamesMSnellè¿æ ·çèµæ·±ç¨åºåä¹æï¼å³ä½¿æ¯ä¸ä¸ªå¹¶ä¸ç®å¤ªå¤æçAPIï¼å³ä½¿åªæè¡ä»£ç â¦â¦ä¹å¯è½æ½èåç§é®é¢ã
ãå½ç¶ï¼æ们å¯ä»¥å·Node.jsç代ç è´¨éä¹ä¸è¿å°å°ï¼å ¶å®å°±ç®JSå¼æ代ç ï¼ä¹ç»å¸¸åºbugï¼å¦/post/