mirror of https://github.com/microsoft/vscode.git
chore: log js stacks from unresponsive window (#241390)
* chore: log js stacks from unresponsive window
* chore: address review feedback
* chore: remove setting
* chore: remove unused setter
* chore: add link to tracing cpu profile
* chore: add cli support to control sample period
* 💄
* chore: upload interesting samples to error telemetry
---------
Co-authored-by: Benjamin Pasero <benjamin.pasero@microsoft.com>
This commit is contained in:
parent
f82be6d0f1
commit
395bddec8a
|
@ -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
|
||||
|
|
|
@ -358,7 +358,11 @@ class FileAccessImpl {
|
|||
export const FileAccess = new FileAccessImpl();
|
||||
|
||||
export const CacheControlheaders: Record<string, string> = Object.freeze({
|
||||
'Cache-Control': 'no-cache, no-store',
|
||||
'Cache-Control': 'no-cache, no-store'
|
||||
});
|
||||
|
||||
export const DocumentPolicyheaders: Record<string, string> = Object.freeze({
|
||||
'Document-Policy': 'include-js-call-stacks-in-crash-reports'
|
||||
});
|
||||
|
||||
export namespace COI {
|
||||
|
|
|
@ -121,8 +121,9 @@ export interface NativeParsedArgs {
|
|||
'profile-temp'?: boolean;
|
||||
'disable-chromium-sandbox'?: boolean;
|
||||
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;
|
||||
|
|
|
@ -182,8 +182,9 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
|
|||
'__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' },
|
||||
|
||||
// chromium flags
|
||||
'no-proxy-server': { type: 'boolean' },
|
||||
|
|
|
@ -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<string, string> | 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 });
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -540,6 +541,11 @@ export class CodeWindow extends BaseWindow implements ICodeWindow {
|
|||
private pendingLoadConfig: INativeWindowConfiguration | undefined;
|
||||
private wasLoaded = false;
|
||||
|
||||
private readonly jsCallStackMap: Map<string, number>;
|
||||
private readonly jsCallStackEffectiveSampleCount: number;
|
||||
private readonly jsCallStackCollector: Delayer<void>;
|
||||
private readonly jsCallStackCollectorStopScheduler: RunOnceScheduler;
|
||||
|
||||
constructor(
|
||||
config: IWindowCreationOptions,
|
||||
@ILogService logService: ILogService,
|
||||
|
@ -595,6 +601,25 @@ 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) {
|
||||
this.logService.warn(`Invalid unresponsive sample interval (${sampleInterval}ms) or period (${samplePeriod}ms), using defaults.`);
|
||||
sampleInterval = 1000;
|
||||
samplePeriod = 15000;
|
||||
}
|
||||
|
||||
this.jsCallStackMap = new Map<string, number>();
|
||||
this.jsCallStackEffectiveSampleCount = Math.round(sampleInterval / samplePeriod);
|
||||
this.jsCallStackCollector = this._register(new Delayer<void>(sampleInterval));
|
||||
this.jsCallStackCollectorStopScheduler = this._register(new RunOnceScheduler(() => {
|
||||
this.stopCollectingJScallStacks(); // Stop collecting after 15s max
|
||||
}, samplePeriod));
|
||||
|
||||
//#endregion
|
||||
|
||||
// respect configured menu bar visibility
|
||||
this.onConfigurationUpdated();
|
||||
|
||||
|
@ -655,6 +680,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 +756,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow {
|
|||
}
|
||||
|
||||
private async onWindowError(error: WindowError.UNRESPONSIVE): Promise<void>;
|
||||
private async onWindowError(error: WindowError.RESPONSIVE): Promise<void>;
|
||||
private async onWindowError(error: WindowError.PROCESS_GONE, details: { reason: string; exitCode: number }): Promise<void>;
|
||||
private async onWindowError(error: WindowError.LOAD, details: { reason: string; exitCode: number }): Promise<void>;
|
||||
private async onWindowError(type: WindowError, details?: { reason?: string; exitCode?: number }): Promise<void> {
|
||||
|
@ -741,6 +768,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 || '<unknown>'}, code: ${details?.exitCode || '<unknown>'})`);
|
||||
break;
|
||||
|
@ -799,6 +829,14 @@ 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
|
||||
this.jsCallStackCollectorStopScheduler.schedule();
|
||||
|
||||
// Show Dialog
|
||||
const { response, checkboxChecked } = await this.dialogMainService.showMessageBox({
|
||||
type: 'warning',
|
||||
|
@ -815,6 +853,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 +886,9 @@ export class CodeWindow extends BaseWindow implements ICodeWindow {
|
|||
await this.destroyWindow(reopen, checkboxChecked);
|
||||
}
|
||||
break;
|
||||
case WindowError.RESPONSIVE:
|
||||
this.stopCollectingJScallStacks();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1449,6 +1491,50 @@ export class CodeWindow extends BaseWindow implements ICodeWindow {
|
|||
return segments;
|
||||
}
|
||||
|
||||
private async startCollectingJScallStacks(): Promise<void> {
|
||||
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;
|
||||
// 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`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -1460,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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue