提交 92183e82 编写于 作者: J Johannes Rieken

query for existing issues before filing new issues, #70492

上级 faf2a1dc
......@@ -10,17 +10,17 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { onUnexpectedError } from 'vs/base/common/errors';
import { tmpdir } from 'os';
import * as os from 'os';
import { join } from 'vs/base/common/path';
import { writeFile } from 'vs/base/node/pfs';
import { IExtensionHostProfileService, ReportExtensionIssueAction } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor';
import { IExtensionHostProfileService } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { localize } from 'vs/nls';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsInput';
import { generateUuid } from 'vs/base/common/uuid';
import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { createSlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions';
export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchContribution {
......@@ -30,11 +30,11 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont
constructor(
@IExtensionService private readonly _extensionService: IExtensionService,
@IExtensionHostProfileService private readonly _extensionProfileService: IExtensionHostProfileService,
@IExtensionsWorkbenchService private readonly _anotherExtensionService: IExtensionsWorkbenchService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@ILogService private readonly _logService: ILogService,
@INotificationService private readonly _notificationService: INotificationService,
@IEditorService private readonly _editorService: IEditorService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
) {
super();
this._register(_extensionService.onDidChangeResponsiveChange(this._onDidChangeResponsiveChange, this));
......@@ -132,16 +132,12 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont
return;
}
// add to running extensions view
this._extensionProfileService.setUnresponsiveProfile(extension.identifier, profile);
// print message to log
const path = join(tmpdir(), `exthost-${Math.random().toString(16).slice(2, 8)}.cpuprofile`);
const path = join(os.tmpdir(), `exthost-${Math.random().toString(16).slice(2, 8)}.cpuprofile`);
await writeFile(path, JSON.stringify(profile.data));
this._logService.warn(`UNRESPONSIVE extension host, '${top.id}' took ${top!.percentage}% of ${duration / 1e3}ms, saved PROFILE here: '${path}'`, data);
// send telemetry
const id = generateUuid();
/* __GDPR__
"exthostunresponsive" : {
......@@ -151,24 +147,22 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont
}
*/
this._telemetryService.publicLog('exthostunresponsive', {
id,
duration,
data,
});
// add to running extensions view
this._extensionProfileService.setUnresponsiveProfile(extension.identifier, profile);
// prompt: when really slow/greedy
if (!(top.percentage >= 99 && top.total >= 5e6)) {
return;
}
// prompt: only when you can file an issue
const reportAction = new ReportExtensionIssueAction({
marketplaceInfo: this._anotherExtensionService.local.filter(value => ExtensionIdentifier.equals(value.identifier.id, extension.identifier))[0],
description: extension,
unresponsiveProfile: profile,
status: undefined,
});
if (!reportAction.enabled) {
const action = await this._instantiationService.invokeFunction(createSlowExtensionAction, extension, profile);
if (!action) {
// cannot report issues against this extension...
return;
}
......@@ -190,18 +184,8 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont
label: localize('show', 'Show Extensions'),
run: () => this._editorService.openEditor(new RuntimeExtensionsInput())
},
{
label: localize('report', "Report Issue"),
run: () => {
/* __GDPR__
"exthostunresponsive/report" : {
"id" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }
}
*/
this._telemetryService.publicLog('exthostunresponsive/report', { id });
return reportAction.run();
}
}],
action
],
{ silent: true }
);
}
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import pkg from 'vs/platform/product/node/package';
import { Action } from 'vs/base/common/actions';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { URI } from 'vs/base/common/uri';
import { IExtensionHostProfile } from 'vs/workbench/services/extensions/common/extensions';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { localize } from 'vs/nls';
import { IRequestService } from 'vs/platform/request/node/request';
import { CancellationToken } from 'vs/base/common/cancellation';
import { asText } from 'vs/base/node/request';
import { join } from 'path';
import { onUnexpectedError } from 'vs/base/common/errors';
abstract class RepoInfo {
readonly base: string;
readonly owner: string;
readonly repo: string;
static fromExtension(desc: IExtensionDescription): RepoInfo | undefined {
let result: RepoInfo | undefined;
// scheme:auth/OWNER/REPO/issues/
if (desc.bugs && typeof desc.bugs.url === 'string') {
const base = URI.parse(desc.bugs.url);
const match = /\/([^/]+)\/([^/]+)\/issues\/?$/.exec(desc.bugs.url);
if (match) {
result = {
base: base.with({ path: null, fragment: null, query: null }).toString(true),
owner: match[1],
repo: match[2]
};
}
}
// scheme:auth/OWNER/REPO.git
if (!result && desc.repository && typeof desc.repository.url === 'string') {
const base = URI.parse(desc.repository.url);
const match = /\/([^/]+)\/([^/]+)(\.git)?$/.exec(desc.repository.url);
if (match) {
result = {
base: base.with({ path: null, fragment: null, query: null }).toString(true),
owner: match[1],
repo: match[2]
};
}
}
// for now only GH is supported
if (result && result.base.indexOf('github') === -1) {
result = undefined;
}
return result;
}
}
export class SlowExtensionAction extends Action {
constructor(
readonly extension: IExtensionDescription,
readonly profile: IExtensionHostProfile,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
) {
super('report.slow', localize('cmd.reportOrShow', "Performance Issue"), 'extension-action report-issue');
this.enabled = Boolean(RepoInfo.fromExtension(extension));
}
async run(): Promise<void> {
const action = await this._instantiationService.invokeFunction(createSlowExtensionAction, this.extension, this.profile);
if (action) {
await action.run();
}
}
}
export async function createSlowExtensionAction(
accessor: ServicesAccessor,
extension: IExtensionDescription,
profile: IExtensionHostProfile
): Promise<Action | undefined> {
const info = RepoInfo.fromExtension(extension);
if (!info) {
return undefined;
}
const requestService = accessor.get(IRequestService);
const url = `https://api.github.com/search/issues?q=is:issue+state:open+in:title+repo:${info.owner}/${info.repo}+%22Extension+causes+high+cpu+load%22`;
const res = await requestService.request({ url }, CancellationToken.None);
const rawText = await asText(res);
if (!rawText) {
return undefined;
}
const data = <{ total_count: number; }>JSON.parse(rawText);
if (!data || typeof data.total_count !== 'number') {
return undefined;
} else if (data.total_count === 0) {
return new ReportExtensionSlowAction(extension, info, profile);
} else {
return new ShowExtensionSlowAction(extension, info);
}
}
class ReportExtensionSlowAction extends Action {
constructor(
readonly extension: IExtensionDescription,
readonly repoInfo: RepoInfo,
readonly profile: IExtensionHostProfile,
) {
super('report.slow', localize('cmd.report', "Report Issue"));
}
async run(): Promise<void> {
// rewrite pii (paths) and store on disk
const profiler = await import('v8-inspect-profiler');
const data = profiler.rewriteAbsolutePaths({ profile: <any>this.profile.data }, 'pii_removed');
const path = join(os.homedir(), `${this.extension.identifier.value}-unresponsive.cpuprofile.txt`);
await profiler.writeProfile(data, path).then(undefined, onUnexpectedError);
// build issue
const title = encodeURIComponent('Extension causes high cpu load');
const osVersion = `${os.type()} ${os.arch()} ${os.release()}`;
const message = `:warning: Make sure to **attach** this file from your *home*-directory:\n:warning:\`${path}\`\n\nFind more details here: https://github.com/Microsoft/vscode/wiki/Explain:-extension-causes-high-cpu-load`;
const body = encodeURIComponent(`- Issue Type: \`Performance\`
- Extension Name: \`${this.extension.name}\`
- Extension Version: \`${this.extension.version}\`
- OS Version: \`${osVersion}\`
- VSCode version: \`${pkg.version}\`\n\n${message}`);
const url = `${this.repoInfo.base}/${this.repoInfo.owner}/${this.repoInfo.repo}/issues/new/?body=${body}&title=${title}`;
window.open(url);
}
}
class ShowExtensionSlowAction extends Action {
constructor(
readonly extension: IExtensionDescription,
readonly repoInfo: RepoInfo,
) {
super('show.slow', localize('cmd.show', "Show Issues"));
}
async run(): Promise<void> {
const url = `${this.repoInfo.base}/${this.repoInfo.owner}/${this.repoInfo.repo}/issues?utf8=✓&q=is%3Aissue+state%3Aopen+%22Extension+causes+high+cpu+load%22`;
window.open(url);
}
}
......@@ -40,10 +40,9 @@ import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/cont
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ILabelService } from 'vs/platform/label/common/label';
import { renderOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel';
import { join } from 'vs/base/common/path';
import { onUnexpectedError } from 'vs/base/common/errors';
import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { SlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions';
export const IExtensionHostProfileService = createDecorator<IExtensionHostProfileService>('extensionHostProfileService');
export const CONTEXT_PROFILE_SESSION_STATE = new RawContextKey<string>('profileSessionState', 'none');
......@@ -308,7 +307,10 @@ export class RuntimeExtensionsEditor extends BaseEditor {
data.activationTime.textContent = activationTimes.startup ? `Startup Activation: ${syncTime}ms` : `Activation: ${syncTime}ms`;
data.actionbar.clear();
if (element.unresponsiveProfile || isNonEmptyArray(element.status.runtimeErrors)) {
if (element.unresponsiveProfile) {
data.actionbar.push(this._instantiationService.createInstance(SlowExtensionAction, element.description, element.unresponsiveProfile), { icon: true, label: true });
}
if (isNonEmptyArray(element.status.runtimeErrors)) {
data.actionbar.push(new ReportExtensionIssueAction(element), { icon: true, label: true });
}
......@@ -471,7 +473,6 @@ export class ReportExtensionIssueAction extends Action {
private static _label = nls.localize('reportExtensionIssue', "Report Issue");
private readonly _url: string;
private readonly _task?: () => Promise<any>;
constructor(extension: {
description: IExtensionDescription;
......@@ -484,15 +485,10 @@ export class ReportExtensionIssueAction extends Action {
&& extension.marketplaceInfo.type === ExtensionType.User
&& !!extension.description.repository && !!extension.description.repository.url;
const { url, task } = ReportExtensionIssueAction._generateNewIssueUrl(extension);
this._url = url;
this._task = task;
this._url = ReportExtensionIssueAction._generateNewIssueUrl(extension);
}
async run(): Promise<void> {
if (this._task) {
await this._task();
}
window.open(this._url);
}
......@@ -501,9 +497,9 @@ export class ReportExtensionIssueAction extends Action {
marketplaceInfo: IExtension;
status?: IExtensionsStatus;
unresponsiveProfile?: IExtensionHostProfile
}): { url: string, task?: () => Promise<any> } {
}): string {
let task: (() => Promise<any>) | undefined;
let baseUrl = extension.marketplaceInfo && extension.marketplaceInfo.type === ExtensionType.User && extension.description.repository ? extension.description.repository.url : undefined;
if (!!baseUrl) {
baseUrl = `${baseUrl.indexOf('.git') !== -1 ? baseUrl.substr(0, baseUrl.length - 4) : baseUrl}/issues/new/`;
......@@ -511,28 +507,10 @@ export class ReportExtensionIssueAction extends Action {
baseUrl = product.reportIssueUrl;
}
let title: string;
let message: string;
let reason: string;
if (extension.unresponsiveProfile) {
// unresponsive extension host caused
reason = 'Performance';
title = 'Extension causes high cpu load';
let path = join(os.homedir(), `${extension.description.identifier.value}-unresponsive.cpuprofile.txt`);
task = async () => {
const profiler = await import('v8-inspect-profiler');
const data = profiler.rewriteAbsolutePaths({ profile: <any>extension.unresponsiveProfile!.data }, 'pii_removed');
profiler.writeProfile(data, path).then(undefined, onUnexpectedError);
};
message = `:warning: Make sure to **attach** this file from your *home*-directory:\n:warning:\`${path}\`\n\nFind more details here: https://github.com/Microsoft/vscode/wiki/Explain:-extension-causes-high-cpu-load`;
} else {
// generic
reason = 'Bug';
title = 'Extension issue';
message = ':warning: We have written the needed data into your clipboard. Please paste! :warning:';
clipboard.writeText('```json \n' + JSON.stringify(extension.status, null, '\t') + '\n```');
}
let reason = 'Bug';
let title = 'Extension issue';
let message = ':warning: We have written the needed data into your clipboard. Please paste! :warning:';
clipboard.writeText('```json \n' + JSON.stringify(extension.status, null, '\t') + '\n```');
const osVersion = `${os.type()} ${os.arch()} ${os.release()}`;
const queryStringPrefix = baseUrl.indexOf('?') === -1 ? '?' : '&';
......@@ -544,10 +522,7 @@ export class ReportExtensionIssueAction extends Action {
- VSCode version: \`${pkg.version}\`\n\n${message}`
);
return {
url: `${baseUrl}${queryStringPrefix}body=${body}&title=${encodeURIComponent(title)}`,
task
};
return `${baseUrl}${queryStringPrefix}body=${body}&title=${encodeURIComponent(title)}`;
}
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册