feat: WebFrameMain.collectJavaScriptCallStack() (#44937)
* feat: WebFrameMain.unresponsiveDocumentJSCallStack Co-authored-by: Samuel Maddock <smaddock@slack-corp.com> * Revert "feat: WebFrameMain.unresponsiveDocumentJSCallStack" This reverts commit e0612bc1a00a5282cba5df97da3c9c90e96ef244. Co-authored-by: Samuel Maddock <smaddock@slack-corp.com> * feat: frame.collectJavaScriptCallStack() Co-authored-by: Samuel Maddock <smaddock@slack-corp.com> * feat: frame.collectJavaScriptCallStack() Co-authored-by: Samuel Maddock <smaddock@slack-corp.com> * Update web-frame-main.md Co-authored-by: Sam Maddock <smaddock@slack-corp.com> --------- Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Samuel Maddock <smaddock@slack-corp.com>
This commit is contained in:
parent
7d2d5240bd
commit
e1d9df224c
5 changed files with 131 additions and 1 deletions
|
@ -142,6 +142,29 @@ ipcRenderer.on('port', (e, msg) => {
|
|||
})
|
||||
```
|
||||
|
||||
#### `frame.collectJavaScriptCallStack()` _Experimental_
|
||||
|
||||
Returns `Promise<string> | Promise<void>` - A promise that resolves with the currently running JavaScript call
|
||||
stack. If no JavaScript runs in the frame, the promise will never resolve. In cases where the call stack is
|
||||
otherwise unable to be collected, it will return `undefined`.
|
||||
|
||||
This can be useful to determine why the frame is unresponsive in cases where there's long-running JavaScript.
|
||||
For more information, see the [proposed Crash Reporting API.](https://wicg.github.io/crash-reporting/)
|
||||
|
||||
```js
|
||||
const { app } = require('electron')
|
||||
|
||||
app.commandLine.appendSwitch('enable-features', 'DocumentPolicyIncludeJSCallStacksInCrashReports')
|
||||
|
||||
app.on('web-contents-created', (_, webContents) => {
|
||||
webContents.on('unresponsive', async () => {
|
||||
// Interrupt execution and collect call stack from unresponsive renderer
|
||||
const callStack = await webContents.mainFrame.collectJavaScriptCallStack()
|
||||
console.log('Renderer unresponsive\n', callStack)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Instance Properties
|
||||
|
||||
#### `frame.ipc` _Readonly_
|
||||
|
|
|
@ -9,9 +9,11 @@
|
|||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "base/feature_list.h"
|
||||
#include "base/logging.h"
|
||||
#include "base/no_destructor.h"
|
||||
#include "content/browser/renderer_host/render_frame_host_impl.h" // nogncheck
|
||||
#include "content/browser/renderer_host/render_process_host_impl.h" // nogncheck
|
||||
#include "content/public/browser/frame_tree_node_id.h"
|
||||
#include "content/public/browser/render_frame_host.h"
|
||||
#include "content/public/common/isolated_world_ids.h"
|
||||
|
@ -429,6 +431,61 @@ std::vector<content::RenderFrameHost*> WebFrameMain::FramesInSubtree() const {
|
|||
return frame_hosts;
|
||||
}
|
||||
|
||||
v8::Local<v8::Promise> WebFrameMain::CollectDocumentJSCallStack(
|
||||
gin::Arguments* args) {
|
||||
gin_helper::Promise<base::Value> promise(args->isolate());
|
||||
v8::Local<v8::Promise> handle = promise.GetHandle();
|
||||
|
||||
if (render_frame_disposed_) {
|
||||
promise.RejectWithErrorMessage(
|
||||
"Render frame was disposed before WebFrameMain could be accessed");
|
||||
return handle;
|
||||
}
|
||||
|
||||
if (!base::FeatureList::IsEnabled(
|
||||
blink::features::kDocumentPolicyIncludeJSCallStacksInCrashReports)) {
|
||||
promise.RejectWithErrorMessage(
|
||||
"DocumentPolicyIncludeJSCallStacksInCrashReports is not enabled");
|
||||
return handle;
|
||||
}
|
||||
|
||||
content::RenderProcessHostImpl* rph_impl =
|
||||
static_cast<content::RenderProcessHostImpl*>(render_frame_->GetProcess());
|
||||
|
||||
rph_impl->GetJavaScriptCallStackGeneratorInterface()
|
||||
->CollectJavaScriptCallStack(
|
||||
base::BindOnce(&WebFrameMain::CollectedJavaScriptCallStack,
|
||||
weak_factory_.GetWeakPtr(), std::move(promise)));
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
void WebFrameMain::CollectedJavaScriptCallStack(
|
||||
gin_helper::Promise<base::Value> promise,
|
||||
const std::string& untrusted_javascript_call_stack,
|
||||
const std::optional<blink::LocalFrameToken>& remote_frame_token) {
|
||||
if (render_frame_disposed_) {
|
||||
promise.RejectWithErrorMessage(
|
||||
"Render frame was disposed before call stack was received");
|
||||
return;
|
||||
}
|
||||
|
||||
const blink::LocalFrameToken& frame_token = render_frame_->GetFrameToken();
|
||||
if (remote_frame_token == frame_token) {
|
||||
base::Value base_value(untrusted_javascript_call_stack);
|
||||
promise.Resolve(base_value);
|
||||
} else if (!remote_frame_token) {
|
||||
// Failed to collect call stack. See logic in:
|
||||
// third_party/blink/renderer/controller/javascript_call_stack_collector.cc
|
||||
promise.Resolve(base::Value());
|
||||
} else {
|
||||
// Requests for call stacks can be initiated on an old RenderProcessHost
|
||||
// then be received after a frame swap.
|
||||
LOG(ERROR) << "Received call stack from old RPH";
|
||||
promise.Resolve(base::Value());
|
||||
}
|
||||
}
|
||||
|
||||
void WebFrameMain::DOMContentLoaded() {
|
||||
Emit("dom-ready");
|
||||
}
|
||||
|
@ -461,6 +518,8 @@ void WebFrameMain::FillObjectTemplate(v8::Isolate* isolate,
|
|||
v8::Local<v8::ObjectTemplate> templ) {
|
||||
gin_helper::ObjectTemplateBuilder(isolate, templ)
|
||||
.SetMethod("executeJavaScript", &WebFrameMain::ExecuteJavaScript)
|
||||
.SetMethod("collectJavaScriptCallStack",
|
||||
&WebFrameMain::CollectDocumentJSCallStack)
|
||||
.SetMethod("reload", &WebFrameMain::Reload)
|
||||
.SetMethod("isDestroyed", &WebFrameMain::IsDestroyed)
|
||||
.SetMethod("_send", &WebFrameMain::Send)
|
||||
|
|
|
@ -36,6 +36,11 @@ template <typename T>
|
|||
class Handle;
|
||||
} // namespace gin
|
||||
|
||||
namespace gin_helper {
|
||||
template <typename T>
|
||||
class Promise;
|
||||
} // namespace gin_helper
|
||||
|
||||
namespace electron::api {
|
||||
|
||||
class WebContents;
|
||||
|
@ -128,6 +133,12 @@ class WebFrameMain final : public gin::Wrappable<WebFrameMain>,
|
|||
std::vector<content::RenderFrameHost*> Frames() const;
|
||||
std::vector<content::RenderFrameHost*> FramesInSubtree() const;
|
||||
|
||||
v8::Local<v8::Promise> CollectDocumentJSCallStack(gin::Arguments* args);
|
||||
void CollectedJavaScriptCallStack(
|
||||
gin_helper::Promise<base::Value> promise,
|
||||
const std::string& untrusted_javascript_call_stack,
|
||||
const std::optional<blink::LocalFrameToken>& remote_frame_token);
|
||||
|
||||
void DOMContentLoaded();
|
||||
|
||||
mojo::Remote<mojom::ElectronRenderer> renderer_api_;
|
||||
|
|
|
@ -21,8 +21,16 @@ describe('webFrameMain module', () => {
|
|||
type Server = { server: http.Server, url: string, crossOriginUrl: string }
|
||||
|
||||
/** Creates an HTTP server whose handler embeds the given iframe src. */
|
||||
const createServer = async (): Promise<Server> => {
|
||||
const createServer = async (options: {
|
||||
headers?: Record<string, string>
|
||||
} = {}): Promise<Server> => {
|
||||
const server = http.createServer((req, res) => {
|
||||
if (options.headers) {
|
||||
for (const [k, v] of Object.entries(options.headers)) {
|
||||
res.setHeader(k, v);
|
||||
}
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(new URL(req.url || '', `http://${req.headers.host}`).search || '');
|
||||
if (params.has('frameSrc')) {
|
||||
res.end(`<iframe src="${params.get('frameSrc')}"></iframe>`);
|
||||
|
@ -444,6 +452,29 @@ describe('webFrameMain module', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('webFrameMain.collectJavaScriptCallStack', () => {
|
||||
let server: Server;
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
headers: {
|
||||
'Document-Policy': 'include-js-call-stacks-in-crash-reports'
|
||||
}
|
||||
});
|
||||
});
|
||||
after(() => {
|
||||
server.server.close();
|
||||
});
|
||||
|
||||
it('collects call stack during JS execution', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
await w.loadURL(server.url);
|
||||
const callStackPromise = w.webContents.mainFrame.collectJavaScriptCallStack();
|
||||
w.webContents.mainFrame.executeJavaScript('"run a lil js"');
|
||||
const callStack = await callStackPromise;
|
||||
expect(callStack).to.be.a('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('"frame-created" event', () => {
|
||||
it('emits when the main frame is created', async () => {
|
||||
const w = new BrowserWindow({ show: false });
|
||||
|
|
|
@ -33,6 +33,12 @@ app.commandLine.appendSwitch('host-resolver-rules', [
|
|||
'MAP notfound.localhost2 ~NOTFOUND'
|
||||
].join(', '));
|
||||
|
||||
// Enable features required by tests.
|
||||
app.commandLine.appendSwitch('enable-features', [
|
||||
// spec/api-web-frame-main-spec.ts
|
||||
'DocumentPolicyIncludeJSCallStacksInCrashReports'
|
||||
].join(','));
|
||||
|
||||
global.standardScheme = 'app';
|
||||
global.zoomScheme = 'zoom';
|
||||
global.serviceWorkerScheme = 'sw';
|
||||
|
|
Loading…
Reference in a new issue