From d213a79cee454fb2cfbec4be4b17cea7235a182b Mon Sep 17 00:00:00 2001 From: Mikhail Arkhipov Date: Thu, 5 Apr 2018 09:51:43 -0700 Subject: [PATCH] Language server startup time improvement (#1299) * Undo changes * Test fixes * Increase timeout * Remove double event listening * Remove test * Revert "Remove test" This reverts commit e240c3fd117c38b9e6fdcbdd1ba2715789fefe48. * Revert "Remove double event listening" This reverts commit af573be27372a79d5589e2134002cc753bb54f2a. * Undo changes * Test fixes * .NET Core check * Better find dotnet * Fix pip test * Linting tests * Undo accidental changes * Add clone and build PTVS * Appveyor PTVS build * Fix slashes * Enable build * Try absolute path * Fix xcopy switch * Activate Analysis Engine test on Appveyor * Temporary only run new tests * Disable PEP hint tests * Test fix * Disable appveyor build and tests for PTVS for now * Remove analysis engine test from the set * Remove VS image for now * Build/sign VSXI project * Run vsce from cmd * Rename * Abs path vsce * Path * Move project * Ignore publishing project * Try csproj * Add framework * Ignore build output folder * Package before build * Try batch instead of PS * Fix path quotes * #1096 The if statement is automatically formatted incorrectly * Merge fix * Add more tests * More tests * Typo * Test * Also better handle multiline arguments * Changes lost on squash * More lost changes * Restore Jedi/PTVS setting * Update tests to new PTVS * Signature tests * Add PTVS tests task * Analysis Engine contribution * Add Mac/Linux info * Disable csproj build * Add unzip to dependencies * Minor fixes to doc * Change setting type to bool * Report progress on status bar * Simplify * CR feedback * Fix launching fx-independent code on Mac/Linux * Add title * PTVS startup time * PTVS startup time * Remove test * Revert "Remove test" This reverts commit e240c3fd117c38b9e6fdcbdd1ba2715789fefe48. * Undo changes * Test fixes * Merge master * Remove unused code * Add clone and build PTVS * Fix slashes * Disable PEP hint tests * Test fix * Remove analysis engine test from the set * Build/sign VSXI project * Run vsce from cmd * Rename * Abs path vsce * Path * Move project * Ignore publishing project * Try csproj * Add framework * Ignore build output folder * Package before build * More lost changes * Change setting type to bool * Merge issues * Merge issues * Merge issues * Check search paths only if using cache * Undo change * PR feedback * Add async startup to PTVS --- package.json | 2 +- src/client/activation/analysis.ts | 182 +++++------------- .../activation/interpreterDataService.ts | 146 ++++++++++++++ src/client/common/configuration/service.ts | 30 +-- src/client/common/types.ts | 1 - 5 files changed, 201 insertions(+), 160 deletions(-) create mode 100644 src/client/activation/interpreterDataService.ts diff --git a/package.json b/package.json index 7925bb4a33f9..13d8ab4506b0 100644 --- a/package.json +++ b/package.json @@ -1907,4 +1907,4 @@ "publisherDisplayName": "Microsoft", "publisherId": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8" } -} +} \ No newline at end of file diff --git a/src/client/activation/analysis.ts b/src/client/activation/analysis.ts index 3590d52b951c..554daace8d96 100644 --- a/src/client/activation/analysis.ts +++ b/src/client/activation/analysis.ts @@ -6,14 +6,13 @@ import { ExtensionContext, OutputChannel } from 'vscode'; import { Disposable, LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient'; import { IApplicationShell } from '../common/application/types'; import { isTestExecution, STANDARD_OUTPUT_CHANNEL } from '../common/constants'; -import '../common/extensions'; import { IFileSystem, IPlatformService } from '../common/platform/types'; -import { IProcessService, IPythonExecutionFactory } from '../common/process/types'; +import { IProcessService } from '../common/process/types'; import { StopWatch } from '../common/stopWatch'; import { IConfigurationService, IOutputChannel, IPythonSettings } from '../common/types'; -import { IInterpreterService } from '../interpreter/contracts'; import { IServiceContainer } from '../ioc/types'; import { AnalysisEngineDownloader } from './downloader'; +import { InterpreterDataService } from './interpreterDataService'; import { PlatformData } from './platformData'; import { IExtensionActivator } from './types'; @@ -22,12 +21,7 @@ const dotNetCommand = 'dotnet'; const languageClientName = 'Python Tools'; const analysisEngineFolder = 'analysis'; -class InterpreterData { - constructor(public readonly version: string, public readonly prefix: string) { } -} - export class AnalysisExtensionActivator implements IExtensionActivator { - private readonly executionFactory: IPythonExecutionFactory; private readonly configuration: IConfigurationService; private readonly appShell: IApplicationShell; private readonly output: OutputChannel; @@ -37,7 +31,6 @@ export class AnalysisExtensionActivator implements IExtensionActivator { private languageClient: LanguageClient | undefined; constructor(private readonly services: IServiceContainer, pythonSettings: IPythonSettings) { - this.executionFactory = this.services.get(IPythonExecutionFactory); this.configuration = this.services.get(IConfigurationService); this.appShell = this.services.get(IApplicationShell); this.output = this.services.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); @@ -50,7 +43,6 @@ export class AnalysisExtensionActivator implements IExtensionActivator { if (!clientOptions) { return false; } - this.output.appendLine(`Options determined: ${this.sw.elapsedTime} ms`); return this.startLanguageServer(context, clientOptions); } @@ -68,16 +60,17 @@ export class AnalysisExtensionActivator implements IExtensionActivator { if (!await this.fs.fileExistsAsync(mscorlib)) { // Depends on .NET Runtime or SDK this.languageClient = this.createSimpleLanguageClient(context, clientOptions); - const e = await this.tryStartLanguageClient(context, this.languageClient); - if (!e) { + try { + await this.tryStartLanguageClient(context, this.languageClient); return true; + } catch (ex) { + if (await this.isDotNetInstalled()) { + this.appShell.showErrorMessage(`.NET Runtime appears to be installed but the language server did not start. Error ${ex}`); + return false; + } + // No .NET Runtime, no mscorlib - need to download self-contained package. + downloadPackage = true; } - if (await this.isDotNetInstalled()) { - this.appShell.showErrorMessage(`.NET Runtime appears to be installed but the language server did not start. Error ${e}`); - return false; - } - // No .NET Runtime, no mscorlib - need to download self-contained package. - downloadPackage = true; } if (downloadPackage) { @@ -88,15 +81,16 @@ export class AnalysisExtensionActivator implements IExtensionActivator { const serverModule = path.join(context.extensionPath, analysisEngineFolder, this.platformData.getEngineExecutableName()); // Now try to start self-contained app this.languageClient = this.createSelfContainedLanguageClient(context, serverModule, clientOptions); - const error = await this.tryStartLanguageClient(context, this.languageClient); - if (!error) { + try { + await this.tryStartLanguageClient(context, this.languageClient); return true; + } catch (ex) { + this.appShell.showErrorMessage(`Language server failed to start. Error ${ex}`); + return false; } - this.appShell.showErrorMessage(`Language server failed to start. Error ${error}`); - return false; } - private async tryStartLanguageClient(context: ExtensionContext, lc: LanguageClient): Promise { + private async tryStartLanguageClient(context: ExtensionContext, lc: LanguageClient): Promise { let disposable: Disposable | undefined; try { disposable = lc.start(); @@ -106,7 +100,7 @@ export class AnalysisExtensionActivator implements IExtensionActivator { } catch (ex) { if (disposable) { disposable.dispose(); - return ex; + throw ex; } } } @@ -135,45 +129,37 @@ export class AnalysisExtensionActivator implements IExtensionActivator { const properties = new Map(); // Microsoft Python code analysis engine needs full path to the interpreter - const interpreterService = this.services.get(IInterpreterService); - const interpreter = await interpreterService.getActiveInterpreter(); + const interpreterDataService = new InterpreterDataService(context, this.services); + const interpreterData = await interpreterDataService.getInterpreterData(); + if (!interpreterData) { + const appShell = this.services.get(IApplicationShell); + appShell.showErrorMessage('Unable to determine path to Python interpreter.'); + return; + } - if (interpreter) { - // tslint:disable-next-line:no-string-literal - properties['InterpreterPath'] = interpreter.path; - if (interpreter.displayName) { - // tslint:disable-next-line:no-string-literal - properties['Description'] = interpreter.displayName; + // tslint:disable-next-line:no-string-literal + properties['InterpreterPath'] = interpreterData.path; + // tslint:disable-next-line:no-string-literal + properties['Version'] = interpreterData.version; + // tslint:disable-next-line:no-string-literal + properties['PrefixPath'] = interpreterData.prefix; + // tslint:disable-next-line:no-string-literal + properties['DatabasePath'] = path.join(context.extensionPath, analysisEngineFolder); + + let searchPaths = interpreterData.searchPaths; + const settings = this.configuration.getSettings(); + if (settings.autoComplete) { + const extraPaths = settings.autoComplete.extraPaths; + if (extraPaths && extraPaths.length > 0) { + searchPaths = `${searchPaths};${extraPaths.join(';')}`; } - const interpreterData = await this.getInterpreterData(); - - // tslint:disable-next-line:no-string-literal - properties['Version'] = interpreterData.version; - // tslint:disable-next-line:no-string-literal - properties['PrefixPath'] = interpreterData.prefix; - // tslint:disable-next-line:no-string-literal - properties['DatabasePath'] = path.join(context.extensionPath, analysisEngineFolder); + } + // tslint:disable-next-line:no-string-literal + properties['SearchPaths'] = searchPaths; - let searchPaths = await this.getSearchPaths(); - const settings = this.configuration.getSettings(); - if (settings.autoComplete) { - const extraPaths = settings.autoComplete.extraPaths; - if (extraPaths && extraPaths.length > 0) { - searchPaths = `${searchPaths};${extraPaths.join(';')}`; - } - } + if (isTestExecution()) { // tslint:disable-next-line:no-string-literal - properties['SearchPaths'] = searchPaths; - - if (isTestExecution()) { - // tslint:disable-next-line:no-string-literal - properties['TestEnvironment'] = true; - } - } else { - const appShell = this.services.get(IApplicationShell); - const pythonPath = this.configuration.getSettings().pythonPath; - appShell.showErrorMessage(`Interpreter ${pythonPath} does not exist.`); - return; + properties['TestEnvironment'] = true; } const selector: string[] = [PYTHON]; @@ -188,80 +174,18 @@ export class AnalysisExtensionActivator implements IExtensionActivator { initializationOptions: { interpreter: { properties - } + }, + displayOptions: { + trimDocumentationLines: false, + maxDocumentationLineLength: 0, + trimDocumentationText: false, + maxDocumentationTextLength: 0 + }, + asyncStartup: true } }; } - private async getInterpreterData(): Promise { - // Not appropriate for multiroot workspaces. - // See https://github.com/Microsoft/vscode-python/issues/1149 - const execService = await this.executionFactory.create(); - const result = await execService.exec(['-c', 'import sys; print(sys.version_info); print(sys.prefix)'], {}); - // 2.7.14 (v2.7.14:84471935ed, Sep 16 2017, 20:19:30) <> - // [MSC v.1500 32 bit (Intel)] - // C:\Python27 - if (!result.stdout) { - throw Error('Unable to determine Python interpreter version and system prefix.'); - } - const output = result.stdout.splitLines({ removeEmptyEntries: true, trim: true }); - if (!output || output.length < 2) { - throw Error('Unable to parse version and and system prefix from the Python interpreter output.'); - } - const majorMatches = output[0].match(/major=(\d*?),/); - const minorMatches = output[0].match(/minor=(\d*?),/); - if (!majorMatches || majorMatches.length < 2 || !minorMatches || minorMatches.length < 2) { - throw Error('Unable to parse interpreter version.'); - } - const prefix = output[output.length - 1]; - return new InterpreterData(`${majorMatches[1]}.${minorMatches[1]}`, prefix); - } - - private async getSearchPaths(): Promise { - // Not appropriate for multiroot workspaces. - // See https://github.com/Microsoft/vscode-python/issues/1149 - const execService = await this.executionFactory.create(); - const result = await execService.exec(['-c', 'import sys; print(sys.path);'], {}); - if (!result.stdout) { - throw Error('Unable to determine Python interpreter search paths.'); - } - // tslint:disable-next-line:no-unnecessary-local-variable - const paths = result.stdout.split(',') - .filter(p => this.isValidPath(p)) - .map(p => this.pathCleanup(p)); - return paths.join(';'); - } - - private pathCleanup(s: string): string { - s = s.trim(); - if (s[0] === '\'') { - s = s.substr(1); - } - if (s[s.length - 1] === ']') { - s = s.substr(0, s.length - 1); - } - if (s[s.length - 1] === '\'') { - s = s.substr(0, s.length - 1); - } - return s; - } - - private isValidPath(s: string): boolean { - return s.length > 0 && s[0] !== '['; - } - - // private async checkNetCoreRuntime(): Promise { - // if (!await this.isDotNetInstalled()) { - // const appShell = this.services.get(IApplicationShell); - // if (await appShell.showErrorMessage('Python Tools require .NET Core Runtime. Would you like to install it now?', 'Yes', 'No') === 'Yes') { - // appShell.openUrl('https://www.microsoft.com/net/download/core#/runtime'); - // appShell.showWarningMessage('Please restart VS Code after .NET Runtime installation is complete.'); - // } - // return false; - // } - // return true; - // } - private async isDotNetInstalled(): Promise { const ps = this.services.get(IProcessService); const result = await ps.exec('dotnet', ['--version']).catch(() => { return { stdout: '' }; }); diff --git a/src/client/activation/interpreterDataService.ts b/src/client/activation/interpreterDataService.ts new file mode 100644 index 000000000000..45cf9749e6cf --- /dev/null +++ b/src/client/activation/interpreterDataService.ts @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { createHash } from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; +import { ExtensionContext, Uri } from 'vscode'; +import { IApplicationShell } from '../common/application/types'; +import '../common/extensions'; +import { createDeferred } from '../common/helpers'; +import { IPlatformService } from '../common/platform/types'; +import { IPythonExecutionFactory, IPythonExecutionService } from '../common/process/types'; +import { IServiceContainer } from '../ioc/types'; + +const DataVersion = 1; + +export class InterpreterData { + constructor( + public readonly dataVersion: number, + // tslint:disable-next-line:no-shadowed-variable + public readonly path: string, + public readonly version: string, + public readonly prefix: string, + public readonly searchPaths: string, + public readonly hash: string + ) { } +} + +export class InterpreterDataService { + constructor( + private readonly context: ExtensionContext, + private readonly serviceContainer: IServiceContainer) { } + + public async getInterpreterData(resource?: Uri): Promise { + const executionFactory = this.serviceContainer.get(IPythonExecutionFactory); + const execService = await executionFactory.create(resource); + + const interpreterPath = await execService.getExecutablePath(); + if (interpreterPath.length === 0) { + return; + } + + const cacheKey = `InterpreterData-${interpreterPath}`; + let interpreterData = this.context.globalState.get(cacheKey) as InterpreterData; + let interpreterChanged = false; + if (interpreterData) { + // Check if interpreter executable changed + if (interpreterData.dataVersion !== DataVersion) { + interpreterChanged = true; + } else { + const currentHash = await this.getInterpreterHash(interpreterPath); + interpreterChanged = currentHash !== interpreterData.hash; + } + } + + if (interpreterChanged || !interpreterData) { + interpreterData = await this.getInterpreterDataFromPython(execService, interpreterPath); + this.context.globalState.update(interpreterPath, interpreterData); + } else { + // Make sure we verify that search paths did not change. This must be done + // completely async so we don't delay Python language server startup. + this.verifySearchPaths(interpreterData.searchPaths, interpreterPath, execService); + } + return interpreterData; + } + + private async getInterpreterDataFromPython(execService: IPythonExecutionService, interpreterPath: string): Promise { + const result = await execService.exec(['-c', 'import sys; print(sys.version_info); print(sys.prefix)'], {}); + // 2.7.14 (v2.7.14:84471935ed, Sep 16 2017, 20:19:30) <> + // [MSC v.1500 32 bit (Intel)] + // C:\Python27 + if (!result.stdout) { + throw Error('Unable to determine Python interpreter version and system prefix.'); + } + const output = result.stdout.splitLines({ removeEmptyEntries: true, trim: true }); + if (!output || output.length < 2) { + throw Error('Unable to parse version and and system prefix from the Python interpreter output.'); + } + const majorMatches = output[0].match(/major=(\d*?),/); + const minorMatches = output[0].match(/minor=(\d*?),/); + if (!majorMatches || majorMatches.length < 2 || !minorMatches || minorMatches.length < 2) { + throw Error('Unable to parse interpreter version.'); + } + const prefix = output[output.length - 1]; + const hash = await this.getInterpreterHash(interpreterPath); + const searchPaths = await this.getSearchPaths(execService); + return new InterpreterData(DataVersion, interpreterPath, `${majorMatches[1]}.${minorMatches[1]}`, prefix, searchPaths, hash); + } + + private getInterpreterHash(interpreterPath: string): Promise { + const platform = this.serviceContainer.get(IPlatformService); + const pythonExecutable = path.join(path.dirname(interpreterPath), platform.isWindows ? 'python.exe' : 'python'); + // Hash mod time and creation time + const deferred = createDeferred(); + fs.lstat(pythonExecutable, (err, stats) => { + if (err) { + deferred.resolve(''); + } else { + const actual = createHash('sha512').update(`${stats.ctimeMs}-${stats.mtimeMs}`).digest('hex'); + deferred.resolve(actual); + } + }); + return deferred.promise; + } + + private async getSearchPaths(execService: IPythonExecutionService): Promise { + const result = await execService.exec(['-c', 'import sys; print(sys.path);'], {}); + if (!result.stdout) { + throw Error('Unable to determine Python interpreter search paths.'); + } + // tslint:disable-next-line:no-unnecessary-local-variable + const paths = result.stdout.split(',') + .filter(p => this.isValidPath(p)) + .map(p => this.pathCleanup(p)); + return paths.join(';'); // PTVS uses ; on all platforms + } + + private pathCleanup(s: string): string { + s = s.trim(); + if (s[0] === '\'') { + s = s.substr(1); + } + if (s[s.length - 1] === ']') { + s = s.substr(0, s.length - 1); + } + if (s[s.length - 1] === '\'') { + s = s.substr(0, s.length - 1); + } + return s; + } + + private isValidPath(s: string): boolean { + return s.length > 0 && s[0] !== '['; + } + + private verifySearchPaths(currentPaths: string, interpreterPath: string, execService: IPythonExecutionService): void { + this.getSearchPaths(execService) + .then(async paths => { + if (paths !== currentPaths) { + this.context.globalState.update(interpreterPath, undefined); + const appShell = this.serviceContainer.get(IApplicationShell); + await appShell.showWarningMessage('Search paths have changed for this Python interpreter. Please reload the extension to ensure that the IntelliSense works correctly.'); + } + }).ignoreErrors(); + } +} diff --git a/src/client/common/configuration/service.ts b/src/client/common/configuration/service.ts index e5d862930ca3..fe84a3de9d5c 100644 --- a/src/client/common/configuration/service.ts +++ b/src/client/common/configuration/service.ts @@ -1,19 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable } from 'inversify'; +import { injectable } from 'inversify'; import { ConfigurationTarget, Uri, workspace, WorkspaceConfiguration } from 'vscode'; -import { IServiceContainer } from '../../ioc/types'; -import { IApplicationShell } from '../application/types'; import { PythonSettings } from '../configSettings'; -import { IProcessService } from '../process/types'; import { IConfigurationService, IPythonSettings } from '../types'; @injectable() export class ConfigurationService implements IConfigurationService { - constructor(@inject(IServiceContainer) private services: IServiceContainer) { - } - public getSettings(resource?: Uri): IPythonSettings { return PythonSettings.getInstance(resource); } @@ -39,10 +33,6 @@ export class ConfigurationService implements IConfigurationService { return process.env.VSC_PYTHON_CI_TEST === '1'; } - public async checkDependencies(): Promise { - return this.checkDotNet(); - } - private async verifySetting(pythonConfig: WorkspaceConfiguration, target: ConfigurationTarget, settingName: string, value?: {}): Promise { if (this.isTestExecution()) { let retries = 0; @@ -66,22 +56,4 @@ export class ConfigurationService implements IConfigurationService { } while (retries < 20); } } - - private async checkDotNet(): Promise { - if (!await this.isDotNetInstalled()) { - const appShell = this.services.get(IApplicationShell); - if (await appShell.showErrorMessage('Python Tools require .NET Core Runtime. Would you like to install it now?', 'Yes', 'No') === 'Yes') { - appShell.openUrl('https://www.microsoft.com/net/download/core#/runtime'); - appShell.showWarningMessage('Please restart VS Code after .NET Runtime installation is complete.'); - } - return false; - } - return true; - } - - private async isDotNetInstalled(): Promise { - const ps = this.services.get(IProcessService); - const result = await ps.exec('dotnet', ['--version']); - return result.stdout.trim().startsWith('2.'); - } } diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 266d1a812cb0..f64617178288 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -219,7 +219,6 @@ export interface IConfigurationService { getSettings(resource?: Uri): IPythonSettings; isTestExecution(): boolean; updateSettingAsync(setting: string, value?: {}, resource?: Uri, configTarget?: ConfigurationTarget): Promise; - checkDependencies(): Promise; } export const ISocketServer = Symbol('ISocketServer');