Serialisation in the page context and handling exceptions

22 views
Skip to first unread message

Maksim Sadym

unread,
Sep 24, 2021, 10:10:28 AM9/24/21
to bidi-dev
Hi all!

I started implementing `script.evaluate`, and figured out there will be no way to get a stacktrace if we evaluate script and serialise the results in the page context. In case of enabled `Debugger` domain in CDP, the stacktrace, col and raw will be there CDP result, but there is no way to get a stacktrace of a caught exception from a script itself.

I can see 2 ways:
1. (worse) make `stacktrace, col and raw` optional and don't provide it at all.
2. (better) serialise based on the CDP data in the BiDi mapper context.
3. (hybrid) evaluate script using CDP, and after wards serialise results in the page context.

WDYT?

Mathias Bynens

unread,
Sep 27, 2021, 2:56:36 AM9/27/21
to Maksim Sadym, bidi-dev
 Could you give some more details, and perhaps an example of each? What are the pros/cons of each approach? It might be easier to review a small design doc.

--
You received this message because you are subscribed to the Google Groups "bidi-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to bidi-dev+u...@chromium.org.
To view this discussion on the web visit https://groups.google.com/a/chromium.org/d/msgid/bidi-dev/4aa66980-a508-4ff9-b99c-2db7731a486an%40chromium.org.
For more options, visit https://groups.google.com/a/chromium.org/d/optout.

Maksim Sadym

unread,
Sep 27, 2021, 3:35:44 AM9/27/21
to bidi-dev, Mathias Bynens, bidi-dev, Maksim Sadym
Serialisation has a recursive nature, and making serialisation from the mapper context ends up with multiple CDP requests. The initial serialisation + eval implementation. The approach wraps the requested script into another script, which evaluate and serialise the result. Simplified it work like this:
BiDi request 
```
    script.evaluate(`throw {a: "b"}`)
```
is converted to CDP command:
```
    try{
      throw {a: "b"};
    } catch(e){
      return SerializeRecursively(e);
    }
```

The problem is in the fact there is no way of getting exception stack-trace using this approach, as a JS exception doesn't have stacktrace.

Possible solutions I can think of:

  1. Don't provide any stack-trace. I don't consider it's an option.
  2. Run request script as-is, without wrapping, with `returnByValue: false`, which returns an `objectId` of the result or of the exception. 
    1. Serialise the `objectId` recursively by making CDP requests making each object described, like in old prototype: https://github.com/sadym-chromium/WebDriverBiDiServerExperiments/blob/main/bidiServer/server.js#L129
    2. Serialise the result on the page context, by calling the like in PR #42, by evaluating the serialisation script + objectId as a parameter.
In the PR Initial implementation of script.evaluate #44, I started implementation of the approach 2.1
On Monday, September 27, 2021 at 8:56:36 AM UTC+2 Mathias Bynens wrote:
 Could you give some more details, and perhaps an example of each? What are the pros/cons of each approach? It might be easier to review a small design doc.

On Fri, Sep 24, 2021 at 4:10 PM Maksim Sadym <sa...@chromium.org> wrote:
Hi all!

I started implementing `script.evaluate`, and figured out there will be no way to get a stacktrace if we evaluate script and serialise the results in the page context. In case of enabled `Debugger` domain in CDP, the stacktrace, col and raw will be there CDP result, but there is no way to get a stacktrace of a caught exception from a script itself.

I can see 2 ways:
1. (worse) make `stacktrace, col and raw` optional and don't provide it at all.
2. (better) serialise based on the CDP data in the BiDi mapper context.
3. (hybrid) evaluate script using CDP, and after wards serialise results in the page context.

WDYT?

--
You received this message because you are subscribed to the Google Groups "bidi-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to bidi-dev+unsubscribe@chromium.org.

Philip Jägenstedt

unread,
Sep 27, 2021, 4:13:11 AM9/27/21
to Maksim Sadym, bidi-dev, Mathias Bynens
Hi Maksim,

I'm not sure if you've tried this and already know, but note the difference between these two things:

try { throw 'bla'; } catch (e) { console.log(e.stack) }

try { throw new Error('bla'); } catch (e) { console.log(e.stack) }

When JS throws some value that isn't an Error, then you're right that there is no stacktrace. But when an Error instance is thrown there is, so in many cases we would have a stacktrace. How does CDP get the stacktrace when the thrown value isn't an Error, and is that something that content script could do too with some magic bindings injected?

One trick I've seen to get a stacktrace at any point is to simply read `new Error('bla').stack`. It would have to be rewritten to remove the 'bla' and perhaps one frame of the stack, but maybe that's doable?

Best regards,
Philip

To unsubscribe from this group and stop receiving emails from it, send an email to bidi-dev+u...@chromium.org.

--
You received this message because you are subscribed to the Google Groups "bidi-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to bidi-dev+u...@chromium.org.
To view this discussion on the web visit https://groups.google.com/a/chromium.org/d/msgid/bidi-dev/cb8794ef-c76c-4960-9816-86ec2c315accn%40chromium.org.

Maksim Sadym

unread,
Sep 27, 2021, 6:40:05 AM9/27/21
to bidi-dev, Philip Jägenstedt, bidi-dev, Mathias Bynens, Maksim Sadym
Thanks Philip for your suggestion!

I considered catching not only Errors, but rather anything was thrown.
Do you think we can provide stacktrace in case of having only `Error` thrown?

Talking about wrapping anything thrown into error, the experiment shows the stacktrace is dropped to the point of the catch. Meaning it'll be always only the stacktrace to the point of the serialisation script:
```
  const throwException = ()=>{
      throw {a:'a'};
  }
  const callDelegateAndWrapException = (delegate) => {
    try{
      delegate();
    } catch(e) {
      return {
        originalException: e,
        stack: new Error(e).stack
      }
    }
  }
  callDelegateAndWrapException(throwException)
```
Output stacktrace:
```
  Error: [object Object]
    at callDelegateAndWrapException (<anonymous>:10:16)
    at <anonymous>:14:3
```


> How does CDP get the stacktrace when the thrown value isn't an Error, and is that something that content script could do too with some magic bindings injected?
It's a `Debugger` domain enriches exceptions with stacktraces in case of any throw.

To unsubscribe from this group and stop receiving emails from it, send an email to bidi-dev+unsubscribe@chromium.org.

--
You received this message because you are subscribed to the Google Groups "bidi-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to bidi-dev+unsubscribe@chromium.org.

Philip Jägenstedt

unread,
Sep 27, 2021, 10:56:07 AM9/27/21
to Maksim Sadym, bidi-dev, Mathias Bynens
On Mon, Sep 27, 2021 at 12:40 PM Maksim Sadym <sa...@chromium.org> wrote:
Thanks Philip for your suggestion!

I considered catching not only Errors, but rather anything was thrown.
Do you think we can provide stacktrace in case of having only `Error` thrown?

I was hoping there was a workaround to handle non-Error values, but I was wrong. It makes sense that `new Error(e).stack` just points out the stack of that line of code, which of course doesn't include the line that threw the exception.

It doesn't seem good enough to only provide stack traces if an Error is thrown. If we do that as "good enough for now" it seems like it will never be important enough to fix it. So finding a proper solution for it up front seems better to me.
 
Talking about wrapping anything thrown into error, the experiment shows the stacktrace is dropped to the point of the catch. Meaning it'll be always only the stacktrace to the point of the serialisation script:
```
  const throwException = ()=>{
      throw {a:'a'};
  }
  const callDelegateAndWrapException = (delegate) => {
    try{
      delegate();
    } catch(e) {
      return {
        originalException: e,
        stack: new Error(e).stack
      }
    }
  }
  callDelegateAndWrapException(throwException)
```
Output stacktrace:
```
  Error: [object Object]
    at callDelegateAndWrapException (<anonymous>:10:16)
    at <anonymous>:14:3
```

> How does CDP get the stacktrace when the thrown value isn't an Error, and is that something that content script could do too with some magic bindings injected?
It's a `Debugger` domain enriches exceptions with stacktraces in case of any throw.

How does that work internally, could a magic method be exposed to JS to allow getting the same information? I would guess not, just checking.

Best regards,
Philip
 
To unsubscribe from this group and stop receiving emails from it, send an email to bidi-dev+u...@chromium.org.

--
You received this message because you are subscribed to the Google Groups "bidi-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to bidi-dev+u...@chromium.org.

Mathias Bynens

unread,
Sep 28, 2021, 3:36:13 AM9/28/21
to Philip Jägenstedt, Simon Zünd, Maksim Sadym, bidi-dev
+Simon Zünd who might have more context/thoughts from the V8 perspective.

Simon Zünd

unread,
Oct 1, 2021, 1:53:25 AM10/1/21
to Mathias Bynens, Philip Jägenstedt, Maksim Sadym, bidi-dev
Just to document the offline discussion that Maksim and I had:

Uncaught exceptions are reported via `Runtime.exceptionThrown`. For these events to be sent, we have to enable the runtime domain via `Runtime.enable` (not Debugger!). These events have stack traces associated with them regardless whether it's error objects or something else.

Implementation wise, this happens on the V8 side. The isolate has a flag that can be set called "SetCaptureStackTraceForUncaughtExceptions". This flag is responsible for augmenting non-error exceptions with a stack trace. `Runtime.enable` sets this flag for the inspected V8 isolate.

Given that `Runtime.enable` is rather low overhead (we keep it on e.g. for profiling) we should be fine using Runtime.exceptionThrown for bidi.

Mathias Bynens

unread,
Oct 1, 2021, 1:55:53 AM10/1/21
to Simon Zünd, Philip Jägenstedt, Maksim Sadym, bidi-dev
Nice, thanks Simon!
Reply all
Reply to author
Forward
0 new messages