Skip to content

Instantly share code, notes, and snippets.

@misterdjules
Last active July 3, 2018 22:37
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save misterdjules/d7674cf6c42149552958 to your computer and use it in GitHub Desktop.
Save misterdjules/d7674cf6c42149552958 to your computer and use it in GitHub Desktop.

Challenges in using post-mortem debugging with generators

Introduction

Generators were introduced in EcmaScript 2015. They provide a way to iterate over a set of values that are produced by a generating function.

Here's an example of how to use a generator:

function* myGenerator() {
	yield 1;
	yield 2;
	yield 3;
}

var aGenerator = myGenerator();
console.log(aGenerator.next()); // outputs { value: 1, done: false }
console.log(aGenerator.next()); // outputs { value: 2, done: false }
console.log(aGenerator.next()); // outputs { value: 3, done: false }
console.log(aGenerator.next()); // outputs { value: undefined, done: true }

Potential issues identified so far with regards to post-mortem debugging

Generators catch exceptions implicitly/by default

Generators are stateful, and in order to be in a coherent state even in the face of uncaught exceptions, they need to catch them in two different situations:

  1. When next() is called on the generator.

  2. When the Generator's prototype's throw method is called.

The consequences is that the following sample code:

function boom() {
  throw new Error('boom');
}

function bar() {
  boom();
}

function foo() {
  bar();
}

function* generatorThatThrows() {
  yield foo();
}

var generator = generatorThatThrows();
console.log(generator.next());

when run with the --abort-on-uncaught-exception command line switch:

$ node --abort-on-uncaught-exception generator-that-throws.js

will generate a core file that does not have the functions foo, bar or boom active on the main thread's call stack:

[root@dev /home/jgilli]# node --abort-on-uncaught-exception generator-that-throws.js  
Uncaught Error: boom

FROM
Object.<anonymous> (/home/jgilli/generator-that-throws:18:23)
Module._compile (module.js:410:26)
Object.Module._extensions..js (module.js:417:10)
Module.load (module.js:344:32)
Function.Module._load (module.js:301:12)
Function.Module.runMain (module.js:442:10)
startup (node.js:136:18)
node.js:966:3
Illegal Instruction (core dumped)
[root@dev /home/jgilli]# ls cores/
core.node.10771
[root@dev /home/jgilli]# mdb cores/core.node.10771 
Loading modules: [ libumem.so.1 libc.so.1 ld.so.1 ]
> ::load /home/jgilli/mdb_v8/build/amd64/mdb_v8.so
mdb_v8 version: 1.1.2 (dev)
V8 version: 4.5.103.35
Autoconfigured V8 support from target
C++ symbol demangling enabled
> ::jsstack
native: v8::base::OS::Abort+9
native: v8::internal::Runtime_Throw+0x30
        (1 internal frame elided)
js:     next
        (1 internal frame elided)
js:     <anonymous> (as <anon>)
        (1 internal frame elided)
js:     <anonymous> (as Module._compile)
js:     <anonymous> (as Module._extensions..js)
js:     <anonymous> (as Module.load)
js:     <anonymous> (as Module._load)
js:     <anonymous> (as Module.runMain)
js:     startup
js:     <anonymous> (as <anon>)
        (1 internal frame elided)
        (1 internal frame elided)
native: v8::internal::Execution::Call+0x14f
native: v8::Function::Call+0xff
native: v8::Function::Call+0x41
native: node::LoadEnvironment+0x1e8
native: node::Start+0x508
native: _start+0x6c
>

Potential solutions

Remove implicit catch handlers when --abort-on-uncaught-exception is used

When --abort-on-uncaught-exception is used, if an error is thrown from within a generator and is not caught, there is no point in resetting the state of the generator for the following reasons:

  1. The process is going to abort anyway, so the generator won't be used to run any further code.

  2. When inspecting the resulting core dump, preserving the generator in the state when the uncaught error was thrown is actually desirable.

Here's a patch that implements this approach:

diff --git a/deps/v8/src/js/generator.js b/deps/v8/src/js/generator.js
index 2f61b3f..2f2f39d 100644
--- a/deps/v8/src/js/generator.js
+++ b/deps/v8/src/js/generator.js
@@ -38,11 +38,15 @@ function GeneratorObjectNext(value) {
   if (continuation > 0) {
     // Generator is suspended.
     DEBUG_PREPARE_STEP_IN_IF_STEPPING(this);
-    try {
+    if (%IsAbortOnUncaughtException()) {
       return %_GeneratorNext(this, value);
-    } catch (e) {
-      %GeneratorClose(this);
-      throw e;
+    } else {
+      try {
+        return %_GeneratorNext(this, value);
+      } catch (e) {
+        %GeneratorClose(this);
+        throw e;
+      }
     }
   } else if (continuation == 0) {
     // Generator is already closed.
@@ -63,11 +67,15 @@ function GeneratorObjectThrow(exn) {
   var continuation = %GeneratorGetContinuation(this);
   if (continuation > 0) {
     // Generator is suspended.
-    try {
+    if (%IsAbortOnUncaughtException()) {
       return %_GeneratorThrow(this, exn);
-    } catch (e) {
-      %GeneratorClose(this);
-      throw e;
+    } else {
+      try {
+        return %_GeneratorThrow(this, exn);
+      } catch (e) {
+        %GeneratorClose(this);
+        throw e;
+      }
     }
   } else if (continuation == 0) {
     // Generator is already closed.
diff --git a/deps/v8/src/runtime/runtime-internal.cc b/deps/v8/src/runtime/runtime-internal.cc
index 478a954..874dad1 100644
--- a/deps/v8/src/runtime/runtime-internal.cc
+++ b/deps/v8/src/runtime/runtime-internal.cc
@@ -187,6 +187,15 @@ RUNTIME_FUNCTION(Runtime_PromiseRejectEvent) {
   return isolate->heap()->undefined_value();
 }
 
+RUNTIME_FUNCTION(Runtime_IsAbortOnUncaughtException) {
+   HandleScope scope(isolate);
+   if (FLAG_abort_on_uncaught_exception) {
+     return isolate->heap()->true_value();
+   }
+
+   return isolate->heap()->false_value();
+}
 
 RUNTIME_FUNCTION(Runtime_PromiseRevokeReject) {
   DCHECK(args.length() == 1);
diff --git a/deps/v8/src/runtime/runtime.h b/deps/v8/src/runtime/runtime.h
index 23f9bdc..467d42c 100644
--- a/deps/v8/src/runtime/runtime.h
+++ b/deps/v8/src/runtime/runtime.h
@@ -311,6 +311,7 @@ namespace internal {
 
 
 #define FOR_EACH_INTRINSIC_INTERNAL(F)        \
+  F(IsAbortOnUncaughtException, 0, 1)         \
   F(CheckIsBootstrapping, 0, 1)               \
   F(ExportFromRuntime, 1, 1)                  \
   F(ExportExperimentalFromRuntime, 1, 1)      \

Building node's master with this patch and running the same sample code gives the following output:

jgilli@dev ~/node]$ ./node --abort-on-uncaught-exception generator-that-throws.js 
Uncaught Error: boom

FROM
boom (/home/jgilli/generators-js/generator-that-throws.js:2:5)
bar (/home/jgilli/generators-js/generator-that-throws.js:6:5)
foo (/home/jgilli/generators-js/generator-that-throws.js:10:5)
generatorThatThrows (/home/jgilli/generators-js/generator-that-throws.js:14:11)
Object.<anonymous> (/home/jgilli/generators-js/generator-that-throws.js:18:23)
Module._compile (module.js:417:34)
Object.Module._extensions..js (module.js:426:10)
Module.load (module.js:357:32)
Function.Module._load (module.js:314:12)
Function.Module.runMain (module.js:451:10)
startup (node.js:140:18)
node.js:1004:3
Illegal Instruction (core dumped)

And we can see that the core file was generated at the time the initial uncaught error was thrown by checking that all functions foo, bar and boom were on the stack at that moment:

[root@dev /home/jgilli/node]# mdb /home/jgilli/cores/core.node.19504 
Loading modules: [ libumem.so.1 libc.so.1 ld.so.1 ]
> ::load /home/jgilli/mdb_v8/build/amd64/mdb_v8.so
mdb_v8 version: 1.1.2 (dev)
V8 version: 4.8.271.17
Autoconfigured V8 support from target
C++ symbol demangling enabled
> ::jsstack
native: v8::base::OS::Abort+9
native: v8::internal::Runtime_Throw+0x30
        (1 internal frame elided)
js:     boom
js:     bar
js:     foo
js:     generatorThatThrows
js:     next
        (1 internal frame elided)
js:     <anonymous> (as <anon>)
        (1 internal frame elided)
js:     <anonymous> (as Module._compile)
js:     <anonymous> (as Module._extensions..js)
js:     <anonymous> (as Module.load)
js:     <anonymous> (as Module._load)
js:     <anonymous> (as Module.runMain)
js:     startup
js:     <anonymous> (as <anon>)
        (1 internal frame elided)
        (1 internal frame elided)
native: v8::internal::_GLOBAL__N_1::Invoke+0xb3
native: v8::internal::Execution::Call+0x62
native: v8::Function::Call+0xf6
native: v8::Function::Call+0x41
native: node::LoadEnvironment+0x1e8
native: node::Start+0x508
native: _start+0x6c
>

This change does not alter the semantics of throwing an uncaught exception from within a generator, or calling the generator's throw method, so it seems like it could be an uncontroversial change.

Catching the exception also has the same semantics with and without this change:

[jgilli@dev ~/node]$ cat ~/generators-js/test-generator-throws-catch.js 
function boom() {
    throw new Error('boom');
}

function bar() {
    boom();
}

function foo() {
    bar();
}

function* generatorThatThrows() {
    yield foo();
}

var generator = generatorThatThrows();

try {
  console.log(generator.next());
} catch (err) {
  console.log('Error:', err);
}
[jgilli@dev ~/node]$ node --version
v4.2.1
[jgilli@dev ~/node]$ ./node --version
v6.0.0-pre # This is a local development version with the patch presented above
[jgilli@dev ~/node]$ node --abort-on-uncaught-exception ~/generators-js/test-generator-throws-catch.js 
Error: [Error: boom]
[jgilli@dev ~/node]$ echo $?
0
[jgilli@dev ~/node]$ node ~/generators-js/test-generator-throws-catch.js 
Error: [Error: boom]
[jgilli@dev ~/node]$ echo $?
0
[jgilli@dev ~/node]$ ./node --abort-on-uncaught-exception ~/generators-js/test-generator-throws-catch.js 
Error: Error: boom
    at boom (/home/jgilli/generators-js/test-generator-throws-catch.js:2:11)
    at bar (/home/jgilli/generators-js/test-generator-throws-catch.js:6:5)
    at foo (/home/jgilli/generators-js/test-generator-throws-catch.js:10:5)
    at generatorThatThrows (/home/jgilli/generators-js/test-generator-throws-catch.js:14:11)
    at next (native)
    at Object.<anonymous> (/home/jgilli/generators-js/test-generator-throws-catch.js:20:25)
    at Module._compile (module.js:417:34)
    at Object.Module._extensions..js (module.js:426:10)
    at Module.load (module.js:357:32)
    at Function.Module._load (module.js:314:12)
[jgilli@dev ~/node]$ echo $?
0
[jgilli@dev ~/node]$ ./node ~/generators-js/test-generator-throws-catch.js 
Error: Error: boom
    at boom (/home/jgilli/generators-js/test-generator-throws-catch.js:2:11)
    at bar (/home/jgilli/generators-js/test-generator-throws-catch.js:6:5)
    at foo (/home/jgilli/generators-js/test-generator-throws-catch.js:10:5)
    at generatorThatThrows (/home/jgilli/generators-js/test-generator-throws-catch.js:14:11)
    at next (native)
    at Object.<anonymous> (/home/jgilli/generators-js/test-generator-throws-catch.js:20:25)
    at Module._compile (module.js:417:34)
    at Object.Module._extensions..js (module.js:426:10)
    at Module.load (module.js:357:32)
    at Function.Module._load (module.js:314:12)
[jgilli@dev ~/node]$ echo $?
0
[jgilli@dev ~/node]$

The only difference is in the way the Error object is displayed on the console, which I'm still investigating.

Internal machinery to pause/resume a generator hides internal state

Another (maybe less) potentially problematic characteristic of generators with regards to post-mortem debugging is that, even though they are stateful, their state cannot be inspected by post-mortem debugging tools.

Let's consider the following sample code:

function* myGenerator() {
	yield 1;
	yield 2;
	yield 3;
}

var aGenerator = myGenerator();
console.log(aGenerator.next()); // outputs { value: 1, done: false }
aGenerator.throw();

When loading the generator core file, there is no way to determine that the generator threw on the second yield statement:

$ node --abort-on-uncaught-exception generator-state.js 
{ value: 1, done: false }
Uncaught Error: Foo

FROM
Object.<anonymous> (/home/jgilli/generator-state.js:9:17)
Module._compile (module.js:435:26)
Object.Module._extensions..js (module.js:442:10)
Module.load (module.js:356:32)
Function.Module._load (module.js:311:12)
Function.Module.runMain (module.js:467:10)
startup (node.js:134:18)
node.js:961:3
Illegal Instruction (core dumped)
$ mdb /home/jgilli/cores/core.node.20332 
Loading modules: [ libumem.so.1 libc.so.1 ld.so.1 ]
> ::load /home/jgilli/mdb_v8/build/amd64/mdb_v8.so
mdb_v8 version: 1.1.2 (dev)
V8 version: 4.5.103.35
Autoconfigured V8 support from target
C++ symbol demangling enabled
> ::jsstack -van0
fffffd7fffdfedf0 v8::base::OS::Abort+9
fffffd7fffdff0d0 v8::internal::Runtime_ResumeJSGeneratorObject+0xe1
fffffd7fffdff0f0 0x38daaf40b61b internal (Code: 38daaf40b561)
fffffd7fffdff128 0x38daaf54cd6d myGenerator (JSFunction: 3e0fd3005721)
          file: /home/jgilli/generators-js/generator-state.js
          posn: line 1
          this: 3650e210b181 (JSGlobalProxy)
fffffd7fffdff158 0x38daaf5818b3 throw (JSFunction: 32f0102d23a1)
          file: native generator.js
          posn: position 748
          this: 3e0fd3005671 (JSGeneratorObject)
          arg1: 3e0fd308a2a9 (JSObject: Error)
fffffd7fffdff1a8 0x38daaf54befb <anonymous> (as <anon>) (JSFunction: 3e0fd3005769)
          file: /home/jgilli/generators-js/generator-state.js
          posn: line 1
          this: 3e0fd3005921 (JSObject: Object)
          arg1: 3e0fd3005921 (JSObject: Object)
          arg2: 3e0fd30058d9 (JSFunction)
          arg3: 3e0fd3005851 (JSObject: Module)
          arg4: 3e0fd30057d9 (SeqTwoByteString)
          arg5: 3e0fd30057b1 (ConsString)
fffffd7fffdff218 0x38daaf436163 <InternalFrame>
fffffd7fffdff290 0x38daaf54b2e1 <anonymous> (as Module._compile) (JSFunction: 9100af3d2d1)
          file: module.js
          posn: line 380
          this: 3e0fd3005851 (JSObject: Module)
          arg1: 3e0fd3005bb9 (SeqOneByteString)
          arg2: 3e0fd30057d9 (SeqTwoByteString)
fffffd7fffdff2d8 0x38daaf54470b <anonymous> (as Module._extensions..js) (JSFunction: 9100af3d379)
          file: module.js
          posn: line 424
          this: 3e0fd3005cc1 (JSObject: Object)
          arg1: 3e0fd3005851 (JSObject: Module)
          arg2: 3e0fd30057d9 (SeqTwoByteString)
fffffd7fffdff320 0x38daaf541a6f <anonymous> (as Module.load) (JSFunction: 9100af3d1a1)
          file: module.js
          posn: line 348
          this: 3e0fd3005851 (JSObject: Module)
          arg1: 3e0fd30057d9 (SeqTwoByteString)
fffffd7fffdff380 0x38daaf5379f2 <anonymous> (as Module._load) (JSFunction: 9100af3d019)
          file: module.js
          posn: line 285
          this: 3e0fd3005d91 (JSFunction)
          arg1: 3e0fd3005d19 (SeqTwoByteString)
          arg2: 32f010204101 (Oddball: "null")
          arg3: 32f010204231 (Oddball: "true")
fffffd7fffdff3c8 0x38daaf5373e6 <anonymous> (as Module.runMain) (JSFunction: 9100af3d571)
          file: module.js
          posn: line 449
          this: 3e0fd3005d91 (JSFunction)
fffffd7fffdff450 0x38daaf442d8a startup (JSFunction: 3650e210b1a9)
> 3e0fd3005671::jsprint
mdb: <unknown JavaScript object type "JSGeneratorObject">
> 3e0fd3005671::v8print               
3e0fd3005671 JSGeneratorObject {
    3e0fd3005671 JSObject {
        3e0fd3005671 JSReceiver {
            3e0fd3005671 HeapObject < Object  {
                3e0fd3005670 map = 72dca119449 (Map)
            }
        }
        3e0fd3005678 properties = 32f010204139 (FixedArray)
        3e0fd3005680 elements = 32f010204139 (FixedArray)
    }
    3e0fd3005688 function = 3e0fd3005721 (JSFunction)
    3e0fd3005690 context = 3e0fd3070fd1 (FixedArray)
    3e0fd3005698 receiver = 3650e210b181 (JSGlobalProxy)
    3e0fd30056a0 continuation = ffffffff00000000 (SMI: value = -1)
    3e0fd30056a8 operand_stack = 32f010204139 (FixedArray)
}
>

When a generator reaches a yield statement, it stores the current offset from the start of the generated generator function into the continuation property. Having access to that value would be a first step in understanding in what state the generator was when it threw an error.

With the ::v8print command, we can inspect the value of the continuation accessor. However we can see from the output above that it only indicates that the generator is executing (-1 === kGeneratorExecuting). This value is not sufficient to determine in what state the generator was when it threw an error.

Now consider the following similar code written without using ES6 generators:

function myGenerator() {
  var state = {value: undefined, done: false};

  return {
    next: function next() {
      if (state.value === undefined || state.value < 3) {
        state.value = (state.value !== undefined ? ++state.value : 1);
      } else {
        state.done = true
      }

      return state;
    },
    throw: function doThrow(err) {
      state.done = true;
       if (err === undefined) {
         err = new Error('Foo');
       }

      throw err;
    }
  }
}

var aGenerator = myGenerator();
console.log(aGenerator.next()); // outputs { value: 1, done: false }
aGenerator.throw();

When running this code with the --abort-on-uncaught-exception command line switch, it is possible to inspect the encapsulated state by accessing the generator's throw function's closure:

$ node --abort-on-uncaught-exception  custom-generator-state.js 
{ value: 1, done: false }
Uncaught Error: Foo

FROM
Object.doThrow [as throw] (/home/jgilli/generators-js/custom-generator-state.js:20:7)
Object.<anonymous> (/home/jgilli/generators-js/custom-generator-state.js:27:17)
Module._compile (module.js:435:26)
Object.Module._extensions..js (module.js:442:10)
Module.load (module.js:356:32)
Function.Module._load (module.js:311:12)
Function.Module.runMain (module.js:467:10)
startup (node.js:134:18)
node.js:961:3
Illegal Instruction (core dumped)
$ mdb /home/jgilli/cores/core.node.20197 
Loading modules: [ libumem.so.1 libc.so.1 ld.so.1 ]
> ::load /home/jgilli/mdb_v8/build/amd64/mdb_v8.so
mdb_v8 version: 1.1.2 (dev)
V8 version: 4.5.103.35
Autoconfigured V8 support from target
C++ symbol demangling enabled
> ::jsstack
native: v8::base::OS::Abort+9
native: v8::internal::Runtime_Throw+0x30
        (1 internal frame elided)
js:     doThrow
        (1 internal frame elided)
js:     <anonymous> (as <anon>)
        (1 internal frame elided)
js:     <anonymous> (as Module._compile)
js:     <anonymous> (as Module._extensions..js)
js:     <anonymous> (as Module.load)
js:     <anonymous> (as Module._load)
js:     <anonymous> (as Module.runMain)
js:     startup
js:     <anonymous> (as <anon>)
        (1 internal frame elided)
        (1 internal frame elided)
native: v8::internal::Execution::Call+0x107
native: v8::Function::Call+0xff
native: v8::Function::Call+0x41
native: node::LoadEnvironment+0x1e8
native: node::Start+0x4e8
native: _start+0x6c
> ::jsstack -a
fffffd7fffdff110 v8::base::OS::Abort+9
fffffd7fffdff140 v8::internal::Runtime_Throw+0x30
fffffd7fffdff168 0x1bd1b00060bb internal (Code: 1bd1b0006001)
fffffd7fffdff198 0x1bd1b00e3afd doThrow (JSFunction: 36afff087e19)
fffffd7fffdff1d0 0x1bd1b0019494 <ArgumentsAdaptorFrame>
fffffd7fffdff210 0x1bd1b00bf3d1 <anonymous> (as <anon>) (JSFunction: 36afff0879c9)
fffffd7fffdff278 0x1bd1b001f5c7 <InternalFrame>
fffffd7fffdff2e0 0x1bd1b00beab7 <anonymous> (as Module._compile) (JSFunction: fca96abb4c9)
fffffd7fffdff328 0x1bd1b00b710b <anonymous> (as Module._extensions..js) (JSFunction: fca96abb541)
fffffd7fffdff370 0x1bd1b00b3a72 <anonymous> (as Module.load) (JSFunction: fca96abb439)
fffffd7fffdff3d8 0x1bd1b00a9b30 <anonymous> (as Module._load) (JSFunction: fca96abb351)
fffffd7fffdff420 0x1bd1b00a9263 <anonymous> (as Module.runMain) (JSFunction: fca96abb649)
fffffd7fffdff4b0 0x1bd1b007a083 startup (JSFunction: fca96a5a7f9)
fffffd7fffdff4e8 0x1bd1b0075d92 <anonymous> (as <anon>) (JSFunction: 2122fe5b84f9)
fffffd7fffdff528 0x1bd1b0019f7d <InternalFrame>
fffffd7fffdff590 0x1bd1b00189e2 <EntryFrame>
fffffd7fffdff630 v8::internal::Execution::Call+0x107
fffffd7fffdff6d0 v8::Function::Call+0xff
fffffd7fffdff700 v8::Function::Call+0x41
fffffd7fffdff7f0 node::LoadEnvironment+0x1e8
fffffd7fffdff930 node::Start+0x4e8
fffffd7fffdff940 _start+0x6c
> 36afff087e19::jsclosure
    "state": 36afff087c31: {
        "value": 100000000: 1,
        "done": 2122fe5043f1: true,
    }
> 

From the output above we can determine in what state the generator was when it threw an error.

Potential solutions

Storing previous continuation data when calling next

TODO

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment