V8 ленивое поколение следов стека, кажется, вызывает бесконечный цикл в библиотеке клятв

Я потратил некоторое время на отладку странной задачи бесконечного цикла в тестировании NodeJS. Это происходит только в редких условиях, но я могу воспроизвести его, когда я прикрепляю хром-отладчик.

Я думаю, что это связано с обработкой V8 трассировки стека в исключениях и расширением, которое предлагает библиотеку vows сделал объект 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)

Теперь программа заканчивается в бесконечном цикле: метод toString (добавленный библиотекой клятвы) вызывается снова и снова, когда this.stack обращается к регулярному выражению в строке 83.

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)
}

Когда я проверяю this в отладчике, он показывает, что это AssertionError, но его свойство stack undefined. Однако, когда я наводил указатель мыши на него, он показывает фактическую трассировку стека.

Я предполагаю, что это явление вызвано оптимизацией лень V8. Он только вычисляет трассировку стека по требованию. При этом он мешает добавленному методу toString клятв. Метод toString снова обращается к трассировке стека (this.stack), поэтому цикл продолжается.

Правильно ли это заключение? Если да, есть ли способ исправить код обета, поэтому он работает с V8 (или я могу хотя бы сообщить об этом как об ошибке в проекте обетах)?

Я использую node v0.10.28 под Ubuntu.

Обновление: упрощенный пример без обетов

Вот упрощенная версия. Это больше не зависит от обета, но вместо этого я скопировал соответствующие части расширения 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>

В режиме отладки рекурсия выполняется в операторе 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());
}

(Я также создаю пример jsfiddle, но я не могу воспроизвести бесконечный цикл там, только с node.)

Ответ 1

Поздравляем, вы нашли ошибку в V8!

Да, это определенно ошибка в версии V8 в этой версии node.

Код в версии V8 в вашей версии Node использует код, который выглядит примерно так:

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. В Node 0.11 эта ошибка уже исправлена ​​.

Вот фиксация, которая зафиксировала его, заработала полтора года назад Вячеслав Егоров.

Что вы можете с этим сделать?

Ну, ваши варианты ограничены, но поскольку это для отладки в любом случае, всегда можно предотвратить вторую итерацию и остановить цикл:

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>';
};

Не имеет такой же проблемы. Существует не намного больше, что вы могли бы сделать без доступа к основному коду.