Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Language server startup time improvement #1299

Merged
merged 105 commits into from
Apr 5, 2018
Merged
Show file tree
Hide file tree
Changes from 102 commits
Commits
Show all changes
105 commits
Select commit Hold shift + click to select a range
a764bc9
Undo changes
Feb 13, 2018
9d1b2cc
Test fixes
Feb 13, 2018
a91291a
Increase timeout
Mar 2, 2018
bf266af
Remove double event listening
Mar 7, 2018
7bc6bd6
Remove test
Mar 7, 2018
8ce8b48
Revert "Remove test"
Mar 7, 2018
e3a549e
Revert "Remove double event listening"
Mar 7, 2018
c879aa6
Undo changes
Feb 13, 2018
716968f
Test fixes
Feb 13, 2018
63a1385
.NET Core check
Mar 5, 2018
7ea7717
Better find dotnet
Mar 7, 2018
75be0da
Fix pip test
Mar 13, 2018
2701768
Linting tests
Mar 13, 2018
a22e705
Undo accidental changes
Mar 15, 2018
5441644
Add clone and build PTVS
Mar 16, 2018
97175e1
Appveyor PTVS build
Mar 16, 2018
1e97e6a
Fix slashes
Mar 16, 2018
86ac269
Enable build
Mar 16, 2018
7b4e73c
Try absolute path
Mar 18, 2018
bd1fd77
Fix xcopy switch
Mar 18, 2018
72b91f1
Activate Analysis Engine test on Appveyor
Mar 18, 2018
e32320b
Temporary only run new tests
Mar 20, 2018
55bb349
Disable PEP hint tests
Mar 20, 2018
3826ffa
Test fix
Mar 20, 2018
b62ba25
Disable appveyor build and tests for PTVS for now
Mar 21, 2018
a13770b
Remove analysis engine test from the set
Mar 21, 2018
ac646c8
Remove VS image for now
Mar 21, 2018
f754812
Build/sign VSXI project
Mar 26, 2018
826dcee
Run vsce from cmd
Mar 27, 2018
8cb0c44
Rename
Mar 27, 2018
05d4d23
Abs path vsce
Mar 27, 2018
7fd6a8b
Path
Mar 27, 2018
bbcc65d
Move project
Mar 27, 2018
8d170c0
Ignore publishing project
Mar 27, 2018
869b38c
Try csproj
Mar 27, 2018
d8c80f6
Add framework
Mar 27, 2018
56e6b56
Ignore build output folder
Mar 27, 2018
ce6b360
Package before build
Mar 27, 2018
a7847d8
Try batch instead of PS
Mar 27, 2018
05c0bf0
Fix path quotes
Mar 27, 2018
92e8c1e
#1096 The if statement is automatically formatted incorrectly
Mar 27, 2018
b540a1d
Merge fix
Mar 27, 2018
7b0573e
Add more tests
Mar 27, 2018
facb106
More tests
Mar 27, 2018
f113881
Typo
Mar 27, 2018
3e76718
Test
Mar 28, 2018
6e85dc6
Also better handle multiline arguments
Mar 28, 2018
3e071e0
Changes lost on squash
Mar 28, 2018
818a46f
More lost changes
Mar 28, 2018
ac97532
Restore Jedi/PTVS setting
Mar 28, 2018
395b96a
Merge branch 'master' into vsc
Mar 28, 2018
54a090b
Update tests to new PTVS
Mar 28, 2018
3f74686
Signature tests
Mar 28, 2018
febb828
Merge master
Mar 28, 2018
355edfb
Add PTVS tests task
Mar 28, 2018
ccc1017
Analysis Engine contribution
Mar 28, 2018
5c87964
Add Mac/Linux info
Mar 28, 2018
68e3384
Disable csproj build
Mar 28, 2018
abb3daa
Add unzip to dependencies
Mar 28, 2018
78fd495
Minor fixes to doc
Mar 29, 2018
9b9b51c
Merge branch 'master' of https://github.com/Microsoft/vscode-python i…
Mar 29, 2018
84c8b0f
Change setting type to bool
Mar 29, 2018
7761a15
Report progress on status bar
Mar 30, 2018
240d26f
Simplify
Mar 30, 2018
736846a
CR feedback
Mar 30, 2018
1c73143
Merge branch 'vsc' of https://github.com/MikhailArkhipov/vscode-pytho…
Mar 30, 2018
f03c18a
Fix launching fx-independent code on Mac/Linux
Mar 30, 2018
70ce69b
Add title
Mar 30, 2018
c33ae8d
Proper download when .NET is missing
Mar 30, 2018
be268fb
Merge branch 'vsc' of https://github.com/MikhailArkhipov/vscode-pytho…
Mar 30, 2018
3ee75f4
PTVS startup time
Apr 3, 2018
d0ba56a
PTVS startup time
Apr 4, 2018
7748dde
Remove test
Mar 7, 2018
993db95
Revert "Remove test"
Mar 7, 2018
5130ee5
Undo changes
Feb 13, 2018
d32b9d4
Test fixes
Feb 13, 2018
54d364e
Merge master
Apr 4, 2018
5ff9836
Remove unused code
Apr 4, 2018
8c955a9
Add clone and build PTVS
Mar 16, 2018
5047d96
Fix slashes
Mar 16, 2018
b8c9edc
Disable PEP hint tests
Mar 20, 2018
965753c
Test fix
Mar 20, 2018
9345b9f
Remove analysis engine test from the set
Mar 21, 2018
82f9657
Build/sign VSXI project
Mar 26, 2018
8f16e9a
Run vsce from cmd
Mar 27, 2018
a15cedf
Rename
Mar 27, 2018
3245d56
Abs path vsce
Mar 27, 2018
35bf343
Path
Mar 27, 2018
2ba533e
Move project
Mar 27, 2018
3529ec2
Ignore publishing project
Mar 27, 2018
0e57ab7
Try csproj
Mar 27, 2018
1e0df43
Add framework
Mar 27, 2018
d7a5d78
Ignore build output folder
Mar 27, 2018
5bd5a71
Package before build
Mar 27, 2018
0414216
More lost changes
Mar 28, 2018
462a19d
Change setting type to bool
Mar 29, 2018
446faaf
Merge branch 'master' of https://github.com/Microsoft/vscode-python i…
Apr 4, 2018
3666dee
Merge issues
Apr 4, 2018
2e2d4e6
Merge issues
Apr 4, 2018
0a0e66c
Merge issues
Apr 4, 2018
d972ff3
Check search paths only if using cache
Apr 4, 2018
ccf8eaf
Undo change
Apr 4, 2018
5a21ea9
PR feedback
Apr 5, 2018
03b0bfd
Add async startup to PTVS
Apr 5, 2018
e1d2b15
Merge branch 'vsc' of https://github.com/MikhailArkhipov/vscode-pytho…
Apr 5, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1907,4 +1907,4 @@
"publisherDisplayName": "Microsoft",
"publisherId": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8"
}
}
}
151 changes: 36 additions & 115 deletions src/client/activation/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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;
Expand All @@ -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>(IPythonExecutionFactory);
this.configuration = this.services.get<IConfigurationService>(IConfigurationService);
this.appShell = this.services.get<IApplicationShell>(IApplicationShell);
this.output = this.services.get<OutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL);
Expand All @@ -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);
}

Expand Down Expand Up @@ -96,7 +88,7 @@ export class AnalysisExtensionActivator implements IExtensionActivator {
return false;
}

private async tryStartLanguageClient(context: ExtensionContext, lc: LanguageClient): Promise<Error> {
private async tryStartLanguageClient(context: ExtensionContext, lc: LanguageClient): Promise<Error | undefined> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, not sure how I missed this. Why are we returning a promise?
Shouldn't we return an error as part of the rejection. I.e. change this to Promise<void> and errors will be captured in catch?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you are right. Returning exception is questionable pattern.

let disposable: Disposable | undefined;
try {
disposable = lc.start();
Expand Down Expand Up @@ -135,45 +127,37 @@ export class AnalysisExtensionActivator implements IExtensionActivator {
const properties = new Map<string, any>();

// Microsoft Python code analysis engine needs full path to the interpreter
const interpreterService = this.services.get<IInterpreterService>(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>(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>(IApplicationShell);
const pythonPath = this.configuration.getSettings().pythonPath;
appShell.showErrorMessage(`Interpreter ${pythonPath} does not exist.`);
return;
properties['TestEnvironment'] = true;
}

const selector: string[] = [PYTHON];
Expand All @@ -188,80 +172,17 @@ export class AnalysisExtensionActivator implements IExtensionActivator {
initializationOptions: {
interpreter: {
properties
},
displayOptions: {
trimDocumentationLines: false,
maxDocumentationLineLength: 0,
trimDocumentationText: false,
maxDocumentationTextLength: 0
}
}
};
}

private async getInterpreterData(): Promise<InterpreterData> {
// 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) <<SOMETIMES NEW LINE HERE>>
// [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<string> {
// 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<boolean> {
// if (!await this.isDotNetInstalled()) {
// const appShell = this.services.get<IApplicationShell>(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<boolean> {
const ps = this.services.get<IProcessService>(IProcessService);
const result = await ps.exec('dotnet', ['--version']).catch(() => { return { stdout: '' }; });
Expand Down
145 changes: 145 additions & 0 deletions src/client/activation/interpreterDataService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// 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<InterpreterData | undefined> {
const executionFactory = this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory);
const execService = await executionFactory.create(resource);

const interpreterPath = await execService.getExecutablePath();
if (interpreterPath.length === 0) {
return;
}

let interpreterData = this.context.globalState.get(interpreterPath) as InterpreterData;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use another key for the cache storage. the python path would be too generic, easy to use this else where for other information related to the python path. Let use:

const cacheKey = `InterpreterData-${interpreterPath}`; 

My plan is to do something very similar for interpreter information displayed in the status bar. though I might use this same class (slightly altered). Either way, lets use a non-generic key.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. I was not too convinced about raw path myself

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.verifySearchPathsAsync(interpreterData.searchPaths, interpreterPath, execService);
}
return interpreterData;
}

private async getInterpreterDataFromPython(execService: IPythonExecutionService, interpreterPath: string): Promise<InterpreterData> {
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) <<SOMETIMES NEW LINE HERE>>
// [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<string> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a thought, no change necessary:
Looks like a generic function that can go into IFileSystem for as a method named getFileHash ?

const platform = this.serviceContainer.get<IPlatformService>(IPlatformService);
const pythonExecutable = path.join(path.dirname(interpreterPath), platform.isWindows ? 'python.exe' : 'python');
// Hash mod time and creation time
const deferred = createDeferred<string>();
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<string> {
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 verifySearchPathsAsync(currentPaths: string, interpreterPath: string, execService: IPythonExecutionService): void {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the Async suffix.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. This was mostly for the calling code to show that this is actually async and is not awaited for.

this.getSearchPaths(execService)
.then(async paths => {
if (paths !== currentPaths) {
this.context.globalState.update(interpreterPath, undefined);
const appShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell);
await appShell.showWarningMessage('Search paths have changed for this Python interpreter. Please reload the extension to ensure that the IntelliSense works correctly.');
}
}).ignoreErrors();
}
}
Loading