From 68c69af4870e1607ebf298097479ab963b9f6592 Mon Sep 17 00:00:00 2001 From: deepak1556 Date: Thu, 20 Feb 2025 22:54:29 +0900 Subject: [PATCH 1/8] chore: log js stacks from unresponsive window --- src/main.ts | 6 ++ src/vs/base/common/async.ts | 4 + src/vs/base/common/network.ts | 6 +- .../electron-main/protocolMainService.ts | 14 +++- src/vs/platform/window/common/window.ts | 5 ++ .../platform/window/electron-main/window.ts | 7 +- .../windows/electron-main/windowImpl.ts | 79 ++++++++++++++++++- .../browser/workbench.contribution.ts | 19 ++++- 8 files changed, 134 insertions(+), 6 deletions(-) diff --git a/src/main.ts b/src/main.ts index fdc424e10875e..28fc2af863375 100644 --- a/src/main.ts +++ b/src/main.ts @@ -311,6 +311,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/async.ts b/src/vs/base/common/async.ts index bd772e5d2e922..4de45785260de 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -420,6 +420,10 @@ export class Delayer implements IDisposable { } } + set delay(value: number) { + this.defaultDelay = value; + } + private cancelTimeout(): void { this.deferred?.dispose(); this.deferred = null; diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index f476d8b140952..193f687da2443 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..f5c2e3a9dd709 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'; @@ -93,10 +93,10 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ private handleResourceRequest(request: Electron.ProtocolRequest, callback: ProtocolCallback): void { const path = this.requestToNormalizedFilePath(request); + const pathBasename = basename(path); let headers: Record | undefined; if (this.environmentService.crossOriginIsolated) { - const pathBasename = basename(path); if (pathBasename === 'workbench.html' || pathBasename === 'workbench-dev.html') { headers = COI.CoopAndCoep; } else { @@ -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 (pathBasename === 'workbench.html' || pathBasename === 'workbench-dev.html') { + headers = { + ...headers, + ...DocumentPolicyheaders + }; + } + // first check by validRoots if (this.validRoots.findSubstr(path)) { return callback({ path, headers }); diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index ddf6d46f7db68..e60448a6e7729 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -429,3 +429,8 @@ export function zoomLevelToZoomFactor(zoomLevel = 0): number { export const DEFAULT_WINDOW_SIZE = { width: 1200, height: 800 } as const; export const DEFAULT_AUX_WINDOW_SIZE = { width: 1024, height: 768 } as const; + +export interface IUnresponsiveWindowSettings { + readonly sampleInterval: number; + readonly samplePeriod: number; +} 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 8e80df8051970..1a3e208519b56 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import electron, { BrowserWindowConstructorOptions } from 'electron'; -import { DeferredPromise, RunOnceScheduler, timeout } from '../../../base/common/async.js'; +import { DeferredPromise, RunOnceScheduler, timeout, Delayer } from '../../../base/common/async.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { toErrorMessage } from '../../../base/common/errorMessage.js'; import { Emitter, Event } from '../../../base/common/event.js'; @@ -540,6 +540,10 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { private pendingLoadConfig: INativeWindowConfiguration | undefined; private wasLoaded = false; + private _jsCallStackCollector: Delayer; + private _jsCallStackMap: Map; + private _jsCallStackCollectorStopScheduler: RunOnceScheduler; + constructor( config: IWindowCreationOptions, @ILogService logService: ILogService, @@ -595,6 +599,18 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { } //#endregion + + const unresponsiveWindowSettings = + this.configurationService.getValue('window.unresponsive') || + { sampleInterval: 1000, samplePeriod: 15000 }; + this._jsCallStackCollector = this._register( + new Delayer(unresponsiveWindowSettings.sampleInterval)); + this._jsCallStackMap = new Map(); + // Stop collecting after 15s max + this._jsCallStackCollectorStopScheduler = this._register(new RunOnceScheduler(() => { + this.stopCollectingJScallStacks(); + }, unresponsiveWindowSettings.samplePeriod)); + // respect configured menu bar visibility this.onConfigurationUpdated(); @@ -655,6 +671,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 }))); @@ -730,6 +747,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 { @@ -741,6 +759,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; @@ -799,6 +820,16 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { return; } + // Interrupt V8 and collect JavaScript stack + this._jsCallStackCollector.trigger(() => this.startCollectingJScallStacks()); + // Stack collection will stop under any of the following conditions: + // - The window becomes responsive again + // - The window is destroyed i-e reopen or closed + // - sampling period is complete, default is 15s + if (!this._jsCallStackCollectorStopScheduler.isScheduled()) { + this._jsCallStackCollectorStopScheduler.schedule(); + } + // Show Dialog const { response, checkboxChecked } = await this.dialogMainService.showMessageBox({ type: 'warning', @@ -815,6 +846,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); } } @@ -847,6 +879,9 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { await this.destroyWindow(reopen, checkboxChecked); } break; + case WindowError.RESPONSIVE: + this.stopCollectingJScallStacks(); + break; } } @@ -958,6 +993,48 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { electron.app.setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); } } + + // JS callstack period and frequency + if (e && e.affectsConfiguration('window.unresponsive')) { + const unresponsiveWindowSettings = + this.configurationService.getValue('window.unresponsive'); + // Settings cannot affect current unresponsive window, + // so avoid rescheduling here. + this._jsCallStackCollector.dispose(); + this._jsCallStackCollector.delay = unresponsiveWindowSettings.sampleInterval; + this._jsCallStackCollectorStopScheduler.cancel(); + this._jsCallStackCollectorStopScheduler.delay = unresponsiveWindowSettings.samplePeriod; + } + } + + private async startCollectingJScallStacks(): Promise { + if (!this._jsCallStackCollector.isTriggered()) { + const stack = await this._win.webContents.mainFrame.collectJavaScriptCallStack(); + // Increment the count for this stack trace + if (stack) { + const count = this._jsCallStackMap.get(stack) || 0; + this._jsCallStackMap.set(stack, count + 1); + } + this._jsCallStackCollector.trigger(() => this.startCollectingJScallStacks()); + } + } + + private async stopCollectingJScallStacks(): Promise { + this._jsCallStackCollectorStopScheduler.cancel(); + this._jsCallStackCollector.dispose(); + if (this._jsCallStackMap.size) { + let logMessage = `CodeWindow unresponsive samples:\n`; + let samples = 0; + const sortedEntries = Array.from(this._jsCallStackMap.entries()) + .sort((a, b) => b[1] - a[1]); + for (const [stack, count] of sortedEntries) { + samples += count; + logMessage += `<${count}> ${stack}\n`; + } + logMessage += `Total Samples: ${samples}\n`; + this.logService.error(logMessage); + } + this._jsCallStackMap.clear(); } addTabbedWindow(window: ICodeWindow): void { diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 5e785e81b938d..738e024229d12 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -789,7 +789,24 @@ const registry = Registry.as(ConfigurationExtensions.Con localize('confirmBeforeCloseWeb', "Controls whether to show a confirmation dialog before closing the browser tab or window. Note that even if enabled, browsers may still decide to close a tab or window without confirmation and that this setting is only a hint that may not work in all cases.") : localize('confirmBeforeClose', "Controls whether to show a confirmation dialog before closing a window or quitting the application."), 'scope': ConfigurationScope.APPLICATION - } + }, + 'window.unresponsive': { + type: 'object', + description: localize('unresponsive', "Configures the sample interval and sample period to collect JS stacks from unresponsive window."), + additionalProperties: false, + default: { sampleInterval: 1000, samplePeriod: 15000 }, + properties: { + sampleInterval: { + type: 'number', + description: localize('window.unresponsive.sampleInterval', "Frequency in milliseconds at which to collect samples."), + }, + samplePeriod: { + type: 'number', + description: localize('window.unresponsive.samplePeriod', "Duration in milliseconds for which samples are collected."), + } + }, + scope: ConfigurationScope.APPLICATION + }, } }); From d92b9ed328ac77f6d8d171edfe3487703f310801 Mon Sep 17 00:00:00 2001 From: deepak1556 Date: Tue, 18 Mar 2025 14:54:41 +0900 Subject: [PATCH 2/8] chore: address review feedback --- src/vs/platform/window/common/window.ts | 5 -- .../windows/electron-main/windowImpl.ts | 51 ++++++++++--------- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index e60448a6e7729..ddf6d46f7db68 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -429,8 +429,3 @@ export function zoomLevelToZoomFactor(zoomLevel = 0): number { export const DEFAULT_WINDOW_SIZE = { width: 1200, height: 800 } as const; export const DEFAULT_AUX_WINDOW_SIZE = { width: 1024, height: 768 } as const; - -export interface IUnresponsiveWindowSettings { - readonly sampleInterval: number; - readonly samplePeriod: number; -} diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 1a3e208519b56..fb825dad93e97 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -60,6 +60,11 @@ interface ILoadOptions { readonly disableExtensions?: boolean; } +interface IUnresponsiveWindowSettings { + readonly sampleInterval: number; + readonly samplePeriod: number; +} + const enum ReadyState { /** @@ -540,9 +545,9 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { private pendingLoadConfig: INativeWindowConfiguration | undefined; private wasLoaded = false; - private _jsCallStackCollector: Delayer; - private _jsCallStackMap: Map; - private _jsCallStackCollectorStopScheduler: RunOnceScheduler; + private jsCallStackCollector: Delayer; + private jsCallStackMap: Map; + private jsCallStackCollectorStopScheduler: RunOnceScheduler; constructor( config: IWindowCreationOptions, @@ -603,11 +608,11 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { const unresponsiveWindowSettings = this.configurationService.getValue('window.unresponsive') || { sampleInterval: 1000, samplePeriod: 15000 }; - this._jsCallStackCollector = this._register( + this.jsCallStackCollector = this._register( new Delayer(unresponsiveWindowSettings.sampleInterval)); - this._jsCallStackMap = new Map(); + this.jsCallStackMap = new Map(); // Stop collecting after 15s max - this._jsCallStackCollectorStopScheduler = this._register(new RunOnceScheduler(() => { + this.jsCallStackCollectorStopScheduler = this._register(new RunOnceScheduler(() => { this.stopCollectingJScallStacks(); }, unresponsiveWindowSettings.samplePeriod)); @@ -821,14 +826,12 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { } // Interrupt V8 and collect JavaScript stack - this._jsCallStackCollector.trigger(() => this.startCollectingJScallStacks()); + this.jsCallStackCollector.trigger(() => this.startCollectingJScallStacks()); // Stack collection will stop under any of the following conditions: // - The window becomes responsive again // - The window is destroyed i-e reopen or closed // - sampling period is complete, default is 15s - if (!this._jsCallStackCollectorStopScheduler.isScheduled()) { - this._jsCallStackCollectorStopScheduler.schedule(); - } + this.jsCallStackCollectorStopScheduler.schedule(); // Show Dialog const { response, checkboxChecked } = await this.dialogMainService.showMessageBox({ @@ -1000,32 +1003,32 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { this.configurationService.getValue('window.unresponsive'); // Settings cannot affect current unresponsive window, // so avoid rescheduling here. - this._jsCallStackCollector.dispose(); - this._jsCallStackCollector.delay = unresponsiveWindowSettings.sampleInterval; - this._jsCallStackCollectorStopScheduler.cancel(); - this._jsCallStackCollectorStopScheduler.delay = unresponsiveWindowSettings.samplePeriod; + this.jsCallStackCollector.cancel(); + this.jsCallStackCollector.delay = unresponsiveWindowSettings.sampleInterval; + this.jsCallStackCollectorStopScheduler.cancel(); + this.jsCallStackCollectorStopScheduler.delay = unresponsiveWindowSettings.samplePeriod; } } private async startCollectingJScallStacks(): Promise { - if (!this._jsCallStackCollector.isTriggered()) { + if (!this.jsCallStackCollector.isTriggered()) { const stack = await this._win.webContents.mainFrame.collectJavaScriptCallStack(); // Increment the count for this stack trace if (stack) { - const count = this._jsCallStackMap.get(stack) || 0; - this._jsCallStackMap.set(stack, count + 1); + const count = this.jsCallStackMap.get(stack) || 0; + this.jsCallStackMap.set(stack, count + 1); } - this._jsCallStackCollector.trigger(() => this.startCollectingJScallStacks()); + this.jsCallStackCollector.trigger(() => this.startCollectingJScallStacks()); } } - private async stopCollectingJScallStacks(): Promise { - this._jsCallStackCollectorStopScheduler.cancel(); - this._jsCallStackCollector.dispose(); - if (this._jsCallStackMap.size) { + private stopCollectingJScallStacks(): void { + this.jsCallStackCollectorStopScheduler.cancel(); + this.jsCallStackCollector.cancel(); + if (this.jsCallStackMap.size) { let logMessage = `CodeWindow unresponsive samples:\n`; let samples = 0; - const sortedEntries = Array.from(this._jsCallStackMap.entries()) + const sortedEntries = Array.from(this.jsCallStackMap.entries()) .sort((a, b) => b[1] - a[1]); for (const [stack, count] of sortedEntries) { samples += count; @@ -1034,7 +1037,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { logMessage += `Total Samples: ${samples}\n`; this.logService.error(logMessage); } - this._jsCallStackMap.clear(); + this.jsCallStackMap.clear(); } addTabbedWindow(window: ICodeWindow): void { From 677b5eeac39f0879cfbb082dd4dba54ef1a83050 Mon Sep 17 00:00:00 2001 From: deepak1556 Date: Tue, 18 Mar 2025 15:41:18 +0900 Subject: [PATCH 3/8] chore: remove setting --- .../windows/electron-main/windowImpl.ts | 24 ++----------------- .../browser/workbench.contribution.ts | 19 +-------------- 2 files changed, 3 insertions(+), 40 deletions(-) diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index fb825dad93e97..d6c32eaaf5ad9 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -60,11 +60,6 @@ interface ILoadOptions { readonly disableExtensions?: boolean; } -interface IUnresponsiveWindowSettings { - readonly sampleInterval: number; - readonly samplePeriod: number; -} - const enum ReadyState { /** @@ -605,16 +600,13 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { //#endregion - const unresponsiveWindowSettings = - this.configurationService.getValue('window.unresponsive') || - { sampleInterval: 1000, samplePeriod: 15000 }; this.jsCallStackCollector = this._register( - new Delayer(unresponsiveWindowSettings.sampleInterval)); + new Delayer(1000)); this.jsCallStackMap = new Map(); // Stop collecting after 15s max this.jsCallStackCollectorStopScheduler = this._register(new RunOnceScheduler(() => { this.stopCollectingJScallStacks(); - }, unresponsiveWindowSettings.samplePeriod)); + }, 15000)); // respect configured menu bar visibility this.onConfigurationUpdated(); @@ -996,18 +988,6 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { electron.app.setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); } } - - // JS callstack period and frequency - if (e && e.affectsConfiguration('window.unresponsive')) { - const unresponsiveWindowSettings = - this.configurationService.getValue('window.unresponsive'); - // Settings cannot affect current unresponsive window, - // so avoid rescheduling here. - this.jsCallStackCollector.cancel(); - this.jsCallStackCollector.delay = unresponsiveWindowSettings.sampleInterval; - this.jsCallStackCollectorStopScheduler.cancel(); - this.jsCallStackCollectorStopScheduler.delay = unresponsiveWindowSettings.samplePeriod; - } } private async startCollectingJScallStacks(): Promise { diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 738e024229d12..5e785e81b938d 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -789,24 +789,7 @@ const registry = Registry.as(ConfigurationExtensions.Con localize('confirmBeforeCloseWeb', "Controls whether to show a confirmation dialog before closing the browser tab or window. Note that even if enabled, browsers may still decide to close a tab or window without confirmation and that this setting is only a hint that may not work in all cases.") : localize('confirmBeforeClose', "Controls whether to show a confirmation dialog before closing a window or quitting the application."), 'scope': ConfigurationScope.APPLICATION - }, - 'window.unresponsive': { - type: 'object', - description: localize('unresponsive', "Configures the sample interval and sample period to collect JS stacks from unresponsive window."), - additionalProperties: false, - default: { sampleInterval: 1000, samplePeriod: 15000 }, - properties: { - sampleInterval: { - type: 'number', - description: localize('window.unresponsive.sampleInterval', "Frequency in milliseconds at which to collect samples."), - }, - samplePeriod: { - type: 'number', - description: localize('window.unresponsive.samplePeriod', "Duration in milliseconds for which samples are collected."), - } - }, - scope: ConfigurationScope.APPLICATION - }, + } } }); From f63146b94417299702e3d09119c926f3ac53179f Mon Sep 17 00:00:00 2001 From: deepak1556 Date: Tue, 18 Mar 2025 15:43:48 +0900 Subject: [PATCH 4/8] chore: remove unused setter --- src/vs/base/common/async.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 4de45785260de..bd772e5d2e922 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -420,10 +420,6 @@ export class Delayer implements IDisposable { } } - set delay(value: number) { - this.defaultDelay = value; - } - private cancelTimeout(): void { this.deferred?.dispose(); this.deferred = null; From be988c7c558fa28de834ceaeb84f4de4782b8596 Mon Sep 17 00:00:00 2001 From: deepak1556 Date: Tue, 18 Mar 2025 16:14:11 +0900 Subject: [PATCH 5/8] chore: add link to tracing cpu profile --- src/vs/platform/windows/electron-main/windowImpl.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index d6c32eaaf5ad9..4b0caa172a9e5 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -1015,6 +1015,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { logMessage += `<${count}> ${stack}\n`; } logMessage += `Total Samples: ${samples}\n`; + logMessage += 'For full overview of the unresponsive period, capture cpu profile via https://aka.ms/vscode-tracing-cpu-profile'; this.logService.error(logMessage); } this.jsCallStackMap.clear(); From bd46144cf5422b10e69b28c2ff563aae5365c110 Mon Sep 17 00:00:00 2001 From: deepak1556 Date: Tue, 18 Mar 2025 19:25:10 +0900 Subject: [PATCH 6/8] chore: add cli support to control sample period --- src/vs/platform/environment/common/argv.ts | 2 ++ src/vs/platform/environment/node/argv.ts | 2 ++ src/vs/platform/windows/electron-main/windowImpl.ts | 12 +++++++++--- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index 1efc1b5de457b..d3cf2fd272707 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -123,6 +123,8 @@ export interface NativeParsedArgs { sandbox?: boolean; 'enable-coi'?: boolean; + 'unresponsive-sample-interval'?: string; + 'unresponsive-sample-period'?: string; // chromium command line args: https://electronjs.org/docs/all#supported-chrome-command-line-switches 'no-proxy-server'?: boolean; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 0025e4b597394..6595f128a857d 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -184,6 +184,8 @@ export const OPTIONS: OptionDescriptions> = { 'continueOn': { type: 'string' }, 'enable-coi': { type: 'boolean' }, + 'unresponsive-sample-interval': { type: 'string' }, + 'unresponsive-sample-period': { type: 'string' }, // chromium flags 'no-proxy-server': { type: 'boolean' }, diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 4b0caa172a9e5..a33f53cc1ea2d 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -599,14 +599,20 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { } //#endregion - + let sampleInterval = parseInt(this.environmentMainService.args['unresponsive-sample-interval'] || '1000'); + let samplePeriod = parseInt(this.environmentMainService.args['unresponsive-sample-period'] || '15000'); + if (sampleInterval <= 0 || samplePeriod <= 0 || sampleInterval > samplePeriod) { + this.logService.warn(`Invalid unresponsive sample interval (${sampleInterval}ms) or period (${samplePeriod}ms), using defaults.`); + sampleInterval = 1000; + samplePeriod = 15000; + } this.jsCallStackCollector = this._register( - new Delayer(1000)); + new Delayer(sampleInterval)); this.jsCallStackMap = new Map(); // Stop collecting after 15s max this.jsCallStackCollectorStopScheduler = this._register(new RunOnceScheduler(() => { this.stopCollectingJScallStacks(); - }, 15000)); + }, samplePeriod)); // respect configured menu bar visibility this.onConfigurationUpdated(); From 3753076563c8f9b9febadd09e133b6a22c0e845f Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 18 Mar 2025 14:00:09 +0100 Subject: [PATCH 7/8] :lipstick: --- src/vs/platform/environment/common/argv.ts | 1 - src/vs/platform/environment/node/argv.ts | 1 - .../windows/electron-main/windowImpl.ts | 86 +++++++++++-------- 3 files changed, 48 insertions(+), 40 deletions(-) diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index d3cf2fd272707..e4522f6492eb7 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -121,7 +121,6 @@ export interface NativeParsedArgs { 'profile-temp'?: boolean; 'disable-chromium-sandbox'?: boolean; sandbox?: boolean; - 'enable-coi'?: boolean; 'unresponsive-sample-interval'?: string; 'unresponsive-sample-period'?: string; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 6595f128a857d..a205483c48cf3 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -182,7 +182,6 @@ export const OPTIONS: OptionDescriptions> = { '__enable-file-policy': { type: 'boolean' }, 'editSessionId': { type: 'string' }, 'continueOn': { type: 'string' }, - 'enable-coi': { type: 'boolean' }, 'unresponsive-sample-interval': { type: 'string' }, 'unresponsive-sample-period': { type: 'string' }, diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index a33f53cc1ea2d..fd36aae182ac7 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -540,9 +540,9 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { private pendingLoadConfig: INativeWindowConfiguration | undefined; private wasLoaded = false; - private jsCallStackCollector: Delayer; - private jsCallStackMap: Map; - private jsCallStackCollectorStopScheduler: RunOnceScheduler; + private readonly jsCallStackMap: Map; + private readonly jsCallStackCollector: Delayer; + private readonly jsCallStackCollectorStopScheduler: RunOnceScheduler; constructor( config: IWindowCreationOptions, @@ -599,6 +599,8 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { } //#endregion + //#region JS Callstack Collector + let sampleInterval = parseInt(this.environmentMainService.args['unresponsive-sample-interval'] || '1000'); let samplePeriod = parseInt(this.environmentMainService.args['unresponsive-sample-period'] || '15000'); if (sampleInterval <= 0 || samplePeriod <= 0 || sampleInterval > samplePeriod) { @@ -606,14 +608,15 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { sampleInterval = 1000; samplePeriod = 15000; } - this.jsCallStackCollector = this._register( - new Delayer(sampleInterval)); + this.jsCallStackMap = new Map(); - // Stop collecting after 15s max + this.jsCallStackCollector = this._register(new Delayer(sampleInterval)); this.jsCallStackCollectorStopScheduler = this._register(new RunOnceScheduler(() => { - this.stopCollectingJScallStacks(); + this.stopCollectingJScallStacks(); // Stop collecting after 15s max }, samplePeriod)); + //#endregion + // respect configured menu bar visibility this.onConfigurationUpdated(); @@ -996,37 +999,6 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { } } - private async startCollectingJScallStacks(): Promise { - if (!this.jsCallStackCollector.isTriggered()) { - const stack = await this._win.webContents.mainFrame.collectJavaScriptCallStack(); - // Increment the count for this stack trace - if (stack) { - const count = this.jsCallStackMap.get(stack) || 0; - this.jsCallStackMap.set(stack, count + 1); - } - this.jsCallStackCollector.trigger(() => this.startCollectingJScallStacks()); - } - } - - private stopCollectingJScallStacks(): void { - this.jsCallStackCollectorStopScheduler.cancel(); - this.jsCallStackCollector.cancel(); - if (this.jsCallStackMap.size) { - let logMessage = `CodeWindow unresponsive samples:\n`; - let samples = 0; - const sortedEntries = Array.from(this.jsCallStackMap.entries()) - .sort((a, b) => b[1] - a[1]); - for (const [stack, count] of sortedEntries) { - samples += count; - logMessage += `<${count}> ${stack}\n`; - } - logMessage += `Total Samples: ${samples}\n`; - logMessage += 'For full overview of the unresponsive period, capture cpu profile via https://aka.ms/vscode-tracing-cpu-profile'; - this.logService.error(logMessage); - } - this.jsCallStackMap.clear(); - } - addTabbedWindow(window: ICodeWindow): void { if (isMacintosh && window.win) { this._win.addTabbedWindow(window.win); @@ -1516,6 +1488,44 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { return segments; } + private async startCollectingJScallStacks(): Promise { + if (!this.jsCallStackCollector.isTriggered()) { + const stack = await this._win.webContents.mainFrame.collectJavaScriptCallStack(); + + // Increment the count for this stack trace + if (stack) { + const count = this.jsCallStackMap.get(stack) || 0; + this.jsCallStackMap.set(stack, count + 1); + } + + this.jsCallStackCollector.trigger(() => this.startCollectingJScallStacks()); + } + } + + private stopCollectingJScallStacks(): void { + this.jsCallStackCollectorStopScheduler.cancel(); + this.jsCallStackCollector.cancel(); + + if (this.jsCallStackMap.size) { + let logMessage = `CodeWindow unresponsive samples:\n`; + let samples = 0; + + const sortedEntries = Array.from(this.jsCallStackMap.entries()) + .sort((a, b) => b[1] - a[1]); + + for (const [stack, count] of sortedEntries) { + samples += count; + logMessage += `<${count}> ${stack}\n`; + } + + logMessage += `Total Samples: ${samples}\n`; + logMessage += 'For full overview of the unresponsive period, capture cpu profile via https://aka.ms/vscode-tracing-cpu-profile'; + this.logService.error(logMessage); + } + + this.jsCallStackMap.clear(); + } + matches(webContents: electron.WebContents): boolean { return this._win?.webContents.id === webContents.id; } From ff1bc9ba2b19ec1d3b6fda9d879fd50f8aba6fdd Mon Sep 17 00:00:00 2001 From: deepak1556 Date: Mon, 24 Mar 2025 20:04:57 +0900 Subject: [PATCH 8/8] chore: upload interesting samples to error telemetry --- .../windows/electron-main/windowImpl.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index fd36aae182ac7..1aa19e7d76a22 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -44,6 +44,7 @@ import { IUserDataProfilesMainService } from '../../userDataProfile/electron-mai import { ILoggerMainService } from '../../log/electron-main/loggerService.js'; import { IInstantiationService } from '../../instantiation/common/instantiation.js'; import { VSBuffer } from '../../../base/common/buffer.js'; +import { errorHandler } from '../../../base/common/errors.js'; export interface IWindowCreationOptions { readonly state: IWindowState; @@ -541,6 +542,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { private wasLoaded = false; private readonly jsCallStackMap: Map; + private readonly jsCallStackEffectiveSampleCount: number; private readonly jsCallStackCollector: Delayer; private readonly jsCallStackCollectorStopScheduler: RunOnceScheduler; @@ -610,6 +612,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { } this.jsCallStackMap = new Map(); + this.jsCallStackEffectiveSampleCount = Math.round(sampleInterval / samplePeriod); this.jsCallStackCollector = this._register(new Delayer(sampleInterval)); this.jsCallStackCollectorStopScheduler = this._register(new RunOnceScheduler(() => { this.stopCollectingJScallStacks(); // Stop collecting after 15s max @@ -1515,6 +1518,12 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { for (const [stack, count] of sortedEntries) { samples += count; + // If the stack appears more than 20 percent of the time, log it + // to the error telemetry as UnresponsiveSampleError. + if (Math.round((count * 100) / this.jsCallStackEffectiveSampleCount) > 20) { + const fakeError = new UnresponsiveError(stack, this.id, this.win?.webContents.getOSProcessId()); + errorHandler.onUnexpectedError(fakeError); + } logMessage += `<${count}> ${stack}\n`; } @@ -1537,3 +1546,12 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { this.loggerMainService.deregisterLoggers(this.id); } } + +class UnresponsiveError extends Error { + + constructor(sample: string, windowId: number, pid: number = 0) { + super(`UnresponsiveSampleError: by ${windowId} from ${pid}`); + this.name = 'UnresponsiveSampleError'; + this.stack = sample; + } +}