皮皮网

【全站vip源码】【qt-faststart 源码】【yii 模块源码分析】microbench源码解析

来源:神奇突破指标源码 时间:2024-12-24 08:27:07

1.performance.timerify的源码bugs
2.performance.timerify的bugs

microbench源码解析

performance.timerify的bugs

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

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值,包装函数的源码qt-faststart 源码返回值会变成一个空对象。我当时写了个fn会返回null,解析它给我偷换成了个对象,源码自然把程序搞挂了。

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

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

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

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

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

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

       实际上,合理的thinkphp app商城源码逻辑并不是检查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/