V8懒惰的堆栈跟踪生成似乎会导致誓言库中的无限循环

我花了一些时间在NodeJStesting套件中debugging一个奇怪的无限循环问题。 它只发生在罕见的情况下,但我可以重现它,当我连接到铬debugging器。

我认为它必须处理V8处理exception中的堆栈跟踪,以及发誓库对AssertionError对象做出的扩展(发誓添加了toString方法)。 我也可能弄错了,所以我想问一下,我对V8的实现的理解是否正确。

这是一个重现错误的最简单的例子:

 $ git clone https://github.com/flatiron/vows.git $ cd vows && npm install && npm install should $ cat > example.js var should = require('should'); var error = require('./lib/assert/error.js'); try { 'x'.should.be.json; } catch (e) { console.log(e.toString()); } // without debug, it should fail as expected $ node example.js expected 'x' to have property 'headers' // should.js:61 // now with debug $ node-inspector & $ node --debug-brk example.js // 1) open http://127.0.0.1:8080/debug?port=5858 in Chrome // 2) set breakpoint at lib/assert/error.js#79 (the toString method) // 3) Resume script execution (F8) 

现在程序结束了一个无限循环:当在83行的regexp中访问this.stack时, toString方法(由this.stack库添加)被再次调用。

 require('assert').AssertionError.prototype.toString = function () { var that = this, // line 79: breakpoint source; if (this.stack) { source = this.stack.match(/([a-zA-Z0-9._-]+\.(?:js|coffee))(:\d+):\d+/); // line 83: infinite loop takes place here (however, this.stack is undefined) } 

当我在debugging器中检查this时,它显示它是一个AssertionError但是它的stack属性是undefined 。 但是,当我将鼠标hover在上面时,它会显示实际的堆栈跟踪。

我认为这种现象是由V8的懒惰优化造成的。 它只根据需要计算堆栈跟踪。 这样做会干扰添加的toString方法的誓言。 toString方法再次访问堆栈跟踪( this.stack ),所以循环继续。

这个结论是否正确? 如果是这样,是否有办法补丁誓言代码,所以它适用于V8(或者我可以至less报告它在誓言项目中的错误)?

我在Ubuntu下使用节点v0.10.28。

更新:没有誓言的简单例子

这是一个简化的版本。 它不再依赖于誓言,而是我只复制了toString扩展的相关部分:

 var should = require('should'); require('assert').AssertionError.prototype.toString = function () { var that = this, source; if (this.stack) { source = this.stack.match(/([a-zA-Z0-9._-]+\.(?:js|coffee))(:\d+):\d+/); } return '<dummy-result>'; }; try { 'x'.should.be.json; } catch (e) { console.log(e.toString()); } // expected result (without debug mode) $ node example.js <dummy-result> 

在debugging模式下,recursion发生在if语句中。

更新:甚至与ReferenceError更简单的版本

 ReferenceError.prototype.toString = function () { var that = this, source; if (this.stack) { source = this.stack.match(/([a-zA-Z0-9._-]+\.(?:js|coffee))(:\d+):\d+/); } return '<dummy-result>'; }; try { throw new ReferenceError('ABC'); } catch (e) { console.log(e.toString()); } 

(我也创build了一个jsfiddle的例子,但是我不能在那里重现无限循环,只能用节点。)

恭喜,你在V8中发现了一个bug!

是的,这个版本的节点中的V8版本肯定是个bug。

V8版本中的代码使用的代码如下所示:

 function FormatStackTrace(error, frames) { var lines = []; try { lines.push(error.toString()); } catch (e) { try { lines.push("<error: " + e + ">"); } catch (ee) { lines.push("<error>"); } } 

以下是NodeJS使用版本的实际代码。

事实上,它正在做error.toString()本身导致循环, this.stack访问FormatStackTrace这反过来正在做.toString() 。 你的观察是正确的。

如果这样做有什么好处的话,那么在新版本的V8中,这个代码看起来有很大的不同。 在节点0.11中, 这个bug已经被修复了

这是维亚切斯拉夫·叶戈罗夫 ( Vyacheslav Egorov)在一年半以前提交的那个提交 。

你可以做些什么呢?

那么,你的select是有限的,但因为这是用于debugging,总是可以防止第二次迭代,并停止循环:

 ReferenceError.prototype.toString = function () { var source; if(this.alreadyCalled) return "ReferenceError"; this.alreadyCalled = true; if (this.stack) { source = this.stack.match(/([a-zA-Z0-9._-]+\.(?:js|coffee))(:\d+):\d+/); } return '<dummy-result>'; }; 

不performance出同样的问题。 没有访问核心代码就没有更多的事情可做。

Interesting Posts