diff --git a/src/main.ts b/src/main.ts index 7e2ab209c01a9..ffe9478b9a879 100644 --- a/src/main.ts +++ b/src/main.ts @@ -286,6 +286,12 @@ function configureCommandlineSwitchesSync(cliArgs: NativeParsedArgs) { } }); + // Following features are enabled from the runtime: + // `DocumentPolicyIncludeJSCallStacksInCrashReports` - https://www.electronjs.org/docs/latest/api/web-frame-main#framecollectjavascriptcallstack-experimental + const featuresToEnable = + `DocumentPolicyIncludeJSCallStacksInCrashReports, ${app.commandLine.getSwitchValue('enable-features')}`; + app.commandLine.appendSwitch('enable-features', featuresToEnable); + // Following features are disabled from the runtime: // `CalculateNativeWinOcclusion` - Disable native window occlusion tracker (https://groups.google.com/a/chromium.org/g/embedder-dev/c/ZF3uHHyWLKw/m/VDN2hDXMAAAJ) // `PlzDedicatedWorker` - Refs https://github.com/microsoft/vscode/issues/233060#issuecomment-2523212427 diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index 16bbfe74115ec..d3383e2adc217 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -358,7 +358,11 @@ class FileAccessImpl { export const FileAccess = new FileAccessImpl(); export const CacheControlheaders: Record = Object.freeze({ - 'Cache-Control': 'no-cache, no-store', + 'Cache-Control': 'no-cache, no-store' +}); + +export const DocumentPolicyheaders: Record = Object.freeze({ + 'Document-Policy': 'include-js-call-stacks-in-crash-reports' }); export namespace COI { diff --git a/src/vs/platform/protocol/electron-main/protocolMainService.ts b/src/vs/platform/protocol/electron-main/protocolMainService.ts index 2f0c61b8a81db..b8d25e12da405 100644 --- a/src/vs/platform/protocol/electron-main/protocolMainService.ts +++ b/src/vs/platform/protocol/electron-main/protocolMainService.ts @@ -5,7 +5,7 @@ import { session } from 'electron'; import { Disposable, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; -import { COI, FileAccess, Schemas, CacheControlheaders } from '../../../base/common/network.js'; +import { COI, FileAccess, Schemas, CacheControlheaders, DocumentPolicyheaders } from '../../../base/common/network.js'; import { basename, extname, normalize } from '../../../base/common/path.js'; import { isLinux } from '../../../base/common/platform.js'; import { TernarySearchTree } from '../../../base/common/ternarySearchTree.js'; @@ -113,6 +113,16 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ }; } + // Document-policy header is needed for collecting + // JavaScript callstacks via https://www.electronjs.org/docs/latest/api/web-frame-main#framecollectjavascriptcallstack-experimental + // until https://github.com/electron/electron/issues/45356 is resolved. + if (extname(path).toLowerCase() === '.html') { + headers = { + ...headers, + ...DocumentPolicyheaders + }; + } + // first check by validRoots if (this.validRoots.findSubstr(path)) { return callback({ path, headers }); diff --git a/src/vs/platform/window/electron-main/window.ts b/src/vs/platform/window/electron-main/window.ts index af926cf5b513d..11c6040803637 100644 --- a/src/vs/platform/window/electron-main/window.ts +++ b/src/vs/platform/window/electron-main/window.ts @@ -196,5 +196,10 @@ export const enum WindowError { /** * Maps to the `did-fail-load` event on a `WebContents`. */ - LOAD = 3 + LOAD = 3, + + /** + * Maps to the `responsive` event on a `BrowserWindow`. + */ + RESPONSIVE = 4, } diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 7ea47d522f8e7..fc47ddb6f3c2c 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -540,6 +540,8 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { private pendingLoadConfig: INativeWindowConfiguration | undefined; private wasLoaded = false; + private JScallStackCollector: NodeJS.Timeout | undefined; + constructor( config: IWindowCreationOptions, @ILogService logService: ILogService, @@ -653,6 +655,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { // Window error conditions to handle this._register(Event.fromNodeEventEmitter(this._win, 'unresponsive')(() => this.onWindowError(WindowError.UNRESPONSIVE))); + this._register(Event.fromNodeEventEmitter(this._win, 'responsive')(() => this.onWindowError(WindowError.RESPONSIVE))); this._register(Event.fromNodeEventEmitter(this._win.webContents, 'render-process-gone', (event, details) => details)(details => this.onWindowError(WindowError.PROCESS_GONE, { ...details }))); this._register(Event.fromNodeEventEmitter(this._win.webContents, 'did-fail-load', (event, exitCode, reason) => ({ exitCode, reason }))(({ exitCode, reason }) => this.onWindowError(WindowError.LOAD, { reason, exitCode }))); @@ -728,6 +731,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { } private async onWindowError(error: WindowError.UNRESPONSIVE): Promise; + private async onWindowError(error: WindowError.RESPONSIVE): Promise; private async onWindowError(error: WindowError.PROCESS_GONE, details: { reason: string; exitCode: number }): Promise; private async onWindowError(error: WindowError.LOAD, details: { reason: string; exitCode: number }): Promise; private async onWindowError(type: WindowError, details?: { reason?: string; exitCode?: number }): Promise { @@ -739,6 +743,9 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { case WindowError.UNRESPONSIVE: this.logService.error('CodeWindow: detected unresponsive'); break; + case WindowError.RESPONSIVE: + this.logService.error('CodeWindow: recovered from unresponsive'); + break; case WindowError.LOAD: this.logService.error(`CodeWindow: failed to load (reason: ${details?.reason || ''}, code: ${details?.exitCode || ''})`); break; @@ -797,6 +804,9 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { return; } + // Interrupt V8 and collect JavaScript stack + this.startCollectingJScallStacks(); + // Show Dialog const { response, checkboxChecked } = await this.dialogMainService.showMessageBox({ type: 'warning', @@ -813,6 +823,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { // Handle choice if (response !== 2 /* keep waiting */) { const reopen = response === 0; + this.stopCollectingJScallStacks(); await this.destroyWindow(reopen, checkboxChecked); } } @@ -845,6 +856,9 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { await this.destroyWindow(reopen, checkboxChecked); } break; + case WindowError.RESPONSIVE: + this.stopCollectingJScallStacks(); + break; } } @@ -958,6 +972,23 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { } } + private async startCollectingJScallStacks(): Promise { + if (this.JScallStackCollector) { + return; + } + this.JScallStackCollector = setInterval(async () => { + const stack = await this._win.webContents.mainFrame.collectJavaScriptCallStack(); + this.logService.error('CodeWindow unresponsive : ', stack); + }, 1000); + } + + private async stopCollectingJScallStacks(): Promise { + if (this.JScallStackCollector) { + clearInterval(this.JScallStackCollector); + this.JScallStackCollector = undefined; + } + } + addTabbedWindow(window: ICodeWindow): void { if (isMacintosh && window.win) { this._win.addTabbedWindow(window.win);