diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 5f43018b3c..4f58415ced 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -38,6 +38,7 @@ import { logAndReturn } from '../Utility/Async/returns'; import { is } from '../Utility/System/guards'; import * as util from '../common'; import { isWindows } from '../constants'; +import { instrument, isInstrumentationEnabled } from '../instrumentation'; import { DebugProtocolParams, Logger, ShowWarningParams, getDiagnosticsChannel, getOutputChannelLogger, logDebugProtocol, logLocalized, showWarning } from '../logger'; import { localizedStringCount, lookupString } from '../nativeStrings'; import { SessionState } from '../sessionState'; @@ -855,6 +856,16 @@ export interface Client { } export function createClient(workspaceFolder?: vscode.WorkspaceFolder): Client { + if (isInstrumentationEnabled()) { + instrument(vscode.languages, { name: "languages" }); + instrument(vscode.window, { name: "window" }); + instrument(vscode.workspace, { name: "workspace" }); + instrument(vscode.commands, { name: "commands" }); + instrument(vscode.debug, { name: "debug" }); + instrument(vscode.env, { name: "env" }); + instrument(vscode.extensions, { name: "extensions" }); + return instrument(new DefaultClient(workspaceFolder), { ignore: ["enqueue", "onInterval", "logTelemetry"] }); + } return new DefaultClient(workspaceFolder); } @@ -1338,31 +1349,33 @@ export class DefaultClient implements Client { this.currentCopilotHoverEnabled = new PersistentWorkspaceState("cpp.copilotHover", settings.copilotHover); if (settings.copilotHover !== "disabled") { this.copilotHoverProvider = new CopilotHoverProvider(this); - this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, this.copilotHoverProvider)); + this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, instrument(this.copilotHoverProvider))); } + if (settings.copilotHover !== this.currentCopilotHoverEnabled.Value) { this.currentCopilotHoverEnabled.Value = settings.copilotHover; } - this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, this.hoverProvider)); - this.disposables.push(vscode.languages.registerInlayHintsProvider(util.documentSelector, this.inlayHintsProvider)); - this.disposables.push(vscode.languages.registerRenameProvider(util.documentSelector, new RenameProvider(this))); - this.disposables.push(vscode.languages.registerReferenceProvider(util.documentSelector, new FindAllReferencesProvider(this))); - this.disposables.push(vscode.languages.registerWorkspaceSymbolProvider(new WorkspaceSymbolProvider(this))); - this.disposables.push(vscode.languages.registerDocumentSymbolProvider(util.documentSelector, new DocumentSymbolProvider(), undefined)); - this.disposables.push(vscode.languages.registerCodeActionsProvider(util.documentSelector, new CodeActionProvider(this), undefined)); - this.disposables.push(vscode.languages.registerCallHierarchyProvider(util.documentSelector, new CallHierarchyProvider(this))); + this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, instrument(this.hoverProvider))); + this.disposables.push(vscode.languages.registerInlayHintsProvider(util.documentSelector, instrument(this.inlayHintsProvider))); + this.disposables.push(vscode.languages.registerRenameProvider(util.documentSelector, instrument(new RenameProvider(this)))); + this.disposables.push(vscode.languages.registerReferenceProvider(util.documentSelector, instrument(new FindAllReferencesProvider(this)))); + this.disposables.push(vscode.languages.registerWorkspaceSymbolProvider(instrument(new WorkspaceSymbolProvider(this)))); + this.disposables.push(vscode.languages.registerDocumentSymbolProvider(util.documentSelector, instrument(new DocumentSymbolProvider()), undefined)); + this.disposables.push(vscode.languages.registerCodeActionsProvider(util.documentSelector, instrument(new CodeActionProvider(this)), undefined)); + this.disposables.push(vscode.languages.registerCallHierarchyProvider(util.documentSelector, instrument(new CallHierarchyProvider(this)))); + // Because formatting and codeFolding can vary per folder, we need to register these providers once // and leave them registered. The decision of whether to provide results needs to be made on a per folder basis, // within the providers themselves. - this.documentFormattingProviderDisposable = vscode.languages.registerDocumentFormattingEditProvider(util.documentSelector, new DocumentFormattingEditProvider(this)); - this.formattingRangeProviderDisposable = vscode.languages.registerDocumentRangeFormattingEditProvider(util.documentSelector, new DocumentRangeFormattingEditProvider(this)); - this.onTypeFormattingProviderDisposable = vscode.languages.registerOnTypeFormattingEditProvider(util.documentSelector, new OnTypeFormattingEditProvider(this), ";", "}", "\n"); + this.documentFormattingProviderDisposable = vscode.languages.registerDocumentFormattingEditProvider(util.documentSelector, instrument(new DocumentFormattingEditProvider(this))); + this.formattingRangeProviderDisposable = vscode.languages.registerDocumentRangeFormattingEditProvider(util.documentSelector, instrument(new DocumentRangeFormattingEditProvider(this))); + this.onTypeFormattingProviderDisposable = vscode.languages.registerOnTypeFormattingEditProvider(util.documentSelector, instrument(new OnTypeFormattingEditProvider(this)), ";", "}", "\n"); this.codeFoldingProvider = new FoldingRangeProvider(this); - this.codeFoldingProviderDisposable = vscode.languages.registerFoldingRangeProvider(util.documentSelector, this.codeFoldingProvider); + this.codeFoldingProviderDisposable = vscode.languages.registerFoldingRangeProvider(util.documentSelector, instrument(this.codeFoldingProvider)); if (settings.isEnhancedColorizationEnabled && semanticTokensLegend) { - this.semanticTokensProvider = new SemanticTokensProvider(); + this.semanticTokensProvider = instrument(new SemanticTokensProvider()); this.semanticTokensProviderDisposable = vscode.languages.registerDocumentSemanticTokensProvider(util.documentSelector, this.semanticTokensProvider, semanticTokensLegend); } @@ -1745,8 +1758,7 @@ export class DefaultClient implements Client { const oldLoggingLevelLogged: boolean = this.loggingLevel > 1; this.loggingLevel = util.getNumericLoggingLevel(changedSettings.loggingLevel); if (oldLoggingLevelLogged || this.loggingLevel > 1) { - const out: Logger = getOutputChannelLogger(); - out.appendLine(localize({ key: "loggingLevel.changed", comment: ["{0} is the setting name 'loggingLevel', {1} is a string value such as 'Debug'"] }, "{0} has changed to: {1}", "loggingLevel", changedSettings.loggingLevel)); + getOutputChannelLogger().appendLine(localize({ key: "loggingLevel.changed", comment: ["{0} is the setting name 'loggingLevel', {1} is a string value such as 'Debug'"] }, "{0} has changed to: {1}", "loggingLevel", changedSettings.loggingLevel)); } } const settings: CppSettings = new CppSettings(); @@ -2763,12 +2775,7 @@ export class DefaultClient implements Client { const status: IntelliSenseStatus = { status: Status.IntelliSenseCompiling }; testHook.updateStatus(status); } else if (message.endsWith("IntelliSense done")) { - const settings: CppSettings = new CppSettings(); - if (util.getNumericLoggingLevel(settings.loggingLevel) >= 6) { - const out: Logger = getOutputChannelLogger(); - const duration: number = Date.now() - timeStamp; - out.appendLine(localize("update.intellisense.time", "Update IntelliSense time (sec): {0}", duration / 1000)); - } + getOutputChannelLogger().appendLineAtLevel(6, localize("update.intellisense.time", "Update IntelliSense time (sec): {0}", (Date.now() - timeStamp) / 1000)); this.model.isUpdatingIntelliSense.Value = false; const status: IntelliSenseStatus = { status: Status.IntelliSenseReady }; testHook.updateStatus(status); @@ -3200,11 +3207,8 @@ export class DefaultClient implements Client { return; } - const settings: CppSettings = new CppSettings(); const out: Logger = getOutputChannelLogger(); - if (util.getNumericLoggingLevel(settings.loggingLevel) >= 6) { - out.appendLine(localize("configurations.received", "Custom configurations received:")); - } + out.appendLineAtLevel(6, localize("configurations.received", "Custom configurations received:")); const sanitized: SourceFileConfigurationItemAdapter[] = []; configs.forEach(item => { if (this.isSourceFileConfigurationItem(item, providerVersion)) { @@ -3216,10 +3220,8 @@ export class DefaultClient implements Client { uri = item.uri.toString(); } this.configurationLogging.set(uri, JSON.stringify(item.configuration, null, 4)); - if (util.getNumericLoggingLevel(settings.loggingLevel) >= 6) { - out.appendLine(` uri: ${uri}`); - out.appendLine(` config: ${JSON.stringify(item.configuration, null, 2)}`); - } + out.appendLineAtLevel(6, ` uri: ${uri}`); + out.appendLineAtLevel(6, ` config: ${JSON.stringify(item.configuration, null, 2)}`); if (item.configuration.includePath.some(path => path.endsWith('**'))) { console.warn("custom include paths should not use recursive includes ('**')"); } @@ -3323,11 +3325,7 @@ export class DefaultClient implements Client { return; } - const settings: CppSettings = new CppSettings(); - if (util.getNumericLoggingLevel(settings.loggingLevel) >= 6) { - const out: Logger = getOutputChannelLogger(); - out.appendLine(localize("browse.configuration.received", "Custom browse configuration received: {0}", JSON.stringify(sanitized, null, 2))); - } + getOutputChannelLogger().appendLineAtLevel(6, localize("browse.configuration.received", "Custom browse configuration received: {0}", JSON.stringify(sanitized, null, 2))); // Separate compiler path and args before sending to language client if (util.isString(sanitized.compilerPath)) { diff --git a/Extension/src/LanguageServer/extension.ts b/Extension/src/LanguageServer/extension.ts index 52721a1fc7..a5abf69f2e 100644 --- a/Extension/src/LanguageServer/extension.ts +++ b/Extension/src/LanguageServer/extension.ts @@ -1280,10 +1280,8 @@ async function handleCrashFileRead(crashDirectory: string, crashFile: string, cr if (crashCallStack !== prevCppCrashCallStackData) { prevCppCrashCallStackData = crashCallStack; - const settings: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", null); - if (lines.length >= 6 && util.getNumericLoggingLevel(settings.get("loggingLevel")) >= 1) { - const out: vscode.OutputChannel = getCrashCallStacksChannel(); - out.appendLine(`\n${isCppToolsSrv ? "cpptools-srv" : "cpptools"}\n${crashDate.toLocaleString()}\n${signalType}${crashCallStack}`); + if (lines.length >= 6 && util.getLoggingLevel() >= 1) { + getCrashCallStacksChannel().appendLine(`\n${isCppToolsSrv ? "cpptools-srv" : "cpptools"}\n${crashDate.toLocaleString()}\n${signalType}${crashCallStack}`); } } diff --git a/Extension/src/common.ts b/Extension/src/common.ts index e4ae353b06..8817767b16 100644 --- a/Extension/src/common.ts +++ b/Extension/src/common.ts @@ -759,10 +759,7 @@ export interface ProcessReturnType { export async function spawnChildProcess(program: string, args: string[] = [], continueOn?: string, skipLogging?: boolean, cancellationToken?: vscode.CancellationToken): Promise { // Do not use CppSettings to avoid circular require() if (skipLogging === undefined || !skipLogging) { - const settings: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", null); - if (getNumericLoggingLevel(settings.get("loggingLevel")) >= 5) { - getOutputChannelLogger().appendLine(`$ ${program} ${args.join(' ')}`); - } + getOutputChannelLogger().appendLineAtLevel(5, `$ ${program} ${args.join(' ')}`); } const programOutput: ProcessOutput = await spawnChildProcessImpl(program, args, continueOn, skipLogging, cancellationToken); const exitCode: number | NodeJS.Signals | undefined = programOutput.exitCode; @@ -789,10 +786,6 @@ interface ProcessOutput { async function spawnChildProcessImpl(program: string, args: string[], continueOn?: string, skipLogging?: boolean, cancellationToken?: vscode.CancellationToken): Promise { const result = new ManualPromise(); - // Do not use CppSettings to avoid circular require() - const settings: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", null); - const loggingLevel: number = (skipLogging === undefined || !skipLogging) ? getNumericLoggingLevel(settings.get("loggingLevel")) : 0; - let proc: child_process.ChildProcess; if (await isExecutable(program)) { proc = child_process.spawn(`.${isWindows ? '\\' : '/'}${path.basename(program)}`, args, { shell: true, cwd: path.dirname(program) }); @@ -817,8 +810,8 @@ async function spawnChildProcessImpl(program: string, args: string[], continueOn if (proc.stdout) { proc.stdout.on('data', data => { const str: string = data.toString(); - if (loggingLevel > 0) { - getOutputChannelLogger().append(str); + if (skipLogging === undefined || !skipLogging) { + getOutputChannelLogger().appendAtLevel(1, str); } stdout += str; if (continueOn) { @@ -1576,6 +1569,10 @@ function isIntegral(str: string): boolean { return regex.test(str); } +export function getLoggingLevel() { + return getNumericLoggingLevel(vscode.workspace.getConfiguration("C_Cpp", null).get("loggingLevel")); +} + export function getNumericLoggingLevel(loggingLevel: string | undefined): number { if (!loggingLevel) { return 1; diff --git a/Extension/src/cppTools.ts b/Extension/src/cppTools.ts index ca6bb98613..21152f8bcd 100644 --- a/Extension/src/cppTools.ts +++ b/Extension/src/cppTools.ts @@ -9,9 +9,7 @@ import { CppToolsTestApi, CppToolsTestHook } from 'vscode-cpptools/out/testApi'; import * as nls from 'vscode-nls'; import { CustomConfigurationProvider1, CustomConfigurationProviderCollection, getCustomConfigProviders } from './LanguageServer/customProviders'; import * as LanguageServer from './LanguageServer/extension'; -import { CppSettings } from './LanguageServer/settings'; -import { getNumericLoggingLevel } from './common'; -import { getOutputChannel } from './logger'; +import { getOutputChannelLogger } from './logger'; import * as test from './testHook'; nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); @@ -61,10 +59,7 @@ export class CppTools implements CppToolsTestApi { if (providers.add(provider, this.version)) { const added: CustomConfigurationProvider1 | undefined = providers.get(provider); if (added) { - const settings: CppSettings = new CppSettings(); - if (getNumericLoggingLevel(settings.loggingLevel) >= 5) { - getOutputChannel().appendLine(localize("provider.registered", "Custom configuration provider '{0}' registered", added.name)); - } + getOutputChannelLogger().appendLineAtLevel(5, localize("provider.registered", "Custom configuration provider '{0}' registered", added.name)); this.providers.push(added); LanguageServer.getClients().forEach(client => void client.onRegisterCustomConfigurationProvider(added)); this.addNotifyReadyTimer(added); diff --git a/Extension/src/instrumentation.ts b/Extension/src/instrumentation.ts new file mode 100644 index 0000000000..2e77102742 --- /dev/null +++ b/Extension/src/instrumentation.ts @@ -0,0 +1,61 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +'use strict'; + +/* eslint @typescript-eslint/no-var-requires: "off" */ + +export interface PerfMessage | undefined> { + /** this is the 'name' of the event */ + name: string; + + /** this indicates the context or origin of the message */ + context: Record>; + + /** if the message contains complex data, it should be in here */ + data?: TInput; + + /** if the message is just a text message, this is the contents of the message */ + text?: string; + + /** the message can have a numeric value that indicates the 'level' or 'severity' etc */ + level?: number; +} + +const services = { + instrument: >(instance: T, _options?: { ignore?: string[]; name?: string }): T => instance, + message: (_message: PerfMessage) => { }, + init: (_vscode: any) => { }, + launchSettings: undefined as Record | undefined +}; + +/** Adds instrumentation to all the members of an object when instrumentation is enabled */ +export function instrument>(instance: T, options?: { ignore?: string[]; name?: string }): T { + return services.instrument(instance, options); +} + +/** sends a perf message to the monitor */ +export function sendInstrumentation(message: PerfMessage): void { + services.message(message); +} + +/** verifies that the instrumentation is loaded into the global namespace */ +export function isInstrumentationEnabled(): boolean { + return !!(global as any).instrumentation; +} + +// If the instrumentation object was *actually* loaded then we can set the services from the global object. +// Having this separate ensures that this module is wired up to the global object. +// It's not included in the previous block because if something else loads the instrumentation code first +// this is still needed so that *this* module is properly connected to the global object. +if (isInstrumentationEnabled()) { + // instrumentation services provided by the tool + services.instrument = (global as any).instrumentation.instrument; + services.message = (global as any).instrumentation.message; + services.init = (global as any).instrumentation.init; + + services.init(require('vscode')); +} + +(globalThis as any)["_vscode_"] = require('vscode'); diff --git a/Extension/src/logger.ts b/Extension/src/logger.ts index 4dba0e1d3e..d14bb531f6 100644 --- a/Extension/src/logger.ts +++ b/Extension/src/logger.ts @@ -7,7 +7,8 @@ import * as os from 'os'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import { getNumericLoggingLevel } from './common'; +import { getLoggingLevel } from './common'; +import { sendInstrumentation } from './instrumentation'; import { CppSourceStr } from './LanguageServer/extension'; import { getLocalizedString, LocalizeStringParams } from './LanguageServer/localization'; @@ -27,18 +28,26 @@ export class Logger { this.writer = writer; } - public append(message: string): void { - this.writer(message); - if (Subscriber) { - Subscriber(message); + public appendAtLevel(level: number, message: string): void { + if (getLoggingLevel() >= level) { + this.writer(message); + if (Subscriber) { + Subscriber(message); + } } + sendInstrumentation({ name: 'log', text: message, context: { channel: 'log', source: 'extension' }, level }); + } + + public append(message: string): void { + this.appendAtLevel(0, message); + } + + public appendLineAtLevel(level: number, message: string): void { + this.appendAtLevel(level, message + os.EOL); } public appendLine(message: string): void { - this.writer(message + os.EOL); - if (Subscriber) { - Subscriber(message + os.EOL); - } + this.appendAtLevel(0, message + os.EOL); } // We should not await on this function. @@ -83,9 +92,8 @@ export function getOutputChannel(): vscode.OutputChannel { if (!outputChannel) { outputChannel = vscode.window.createOutputChannel(CppSourceStr); // Do not use CppSettings to avoid circular require() - const settings: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("C_Cpp", null); - const loggingLevel: string | undefined = settings.get("loggingLevel"); - if (getNumericLoggingLevel(loggingLevel) > 1) { + const loggingLevel = getLoggingLevel(); + if (loggingLevel > 1) { outputChannel.appendLine(`loggingLevel: ${loggingLevel}`); } } @@ -130,10 +138,7 @@ export function getOutputChannelLogger(): Logger { } export function log(output: string): void { - if (!outputChannel) { - outputChannel = getOutputChannel(); - } - outputChannel.appendLine(`${output}`); + getOutputChannel().appendLine(`${output}`); } export interface DebugProtocolParams { diff --git a/Extension/src/main.ts b/Extension/src/main.ts index f8d5bcd33f..08c056b76b 100644 --- a/Extension/src/main.ts +++ b/Extension/src/main.ts @@ -23,6 +23,7 @@ import { CppSettings } from './LanguageServer/settings'; import { logAndReturn, returns } from './Utility/Async/returns'; import { CppTools1 } from './cppTools1'; import { logMachineIdMappings } from './id'; +import { sendInstrumentation } from './instrumentation'; import { disposeOutputChannels, log } from './logger'; import { PlatformInformation } from './platform'; @@ -35,6 +36,11 @@ let reloadMessageShown: boolean = false; const disposables: vscode.Disposable[] = []; export async function activate(context: vscode.ExtensionContext): Promise { + sendInstrumentation({ + name: "activate", + context: { cpptools: '', start: '' } + }); + util.setExtensionContext(context); Telemetry.activate(); util.setProgress(0); @@ -152,6 +158,11 @@ export async function activate(context: vscode.ExtensionContext): Promise