-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Changes from 102 commits
a764bc9
9d1b2cc
a91291a
bf266af
7bc6bd6
8ce8b48
e3a549e
c879aa6
716968f
63a1385
7ea7717
75be0da
2701768
a22e705
5441644
97175e1
1e97e6a
86ac269
7b4e73c
bd1fd77
72b91f1
e32320b
55bb349
3826ffa
b62ba25
a13770b
ac646c8
f754812
826dcee
8cb0c44
05d4d23
7fd6a8b
bbcc65d
8d170c0
869b38c
d8c80f6
56e6b56
ce6b360
a7847d8
05c0bf0
92e8c1e
b540a1d
7b0573e
facb106
f113881
3e76718
6e85dc6
3e071e0
818a46f
ac97532
395b96a
54a090b
3f74686
febb828
355edfb
ccc1017
5c87964
68e3384
abb3daa
78fd495
9b9b51c
84c8b0f
7761a15
240d26f
736846a
1c73143
f03c18a
70ce69b
c33ae8d
be268fb
3ee75f4
d0ba56a
7748dde
993db95
5130ee5
d32b9d4
54d364e
5ff9836
8c955a9
5047d96
b8c9edc
965753c
9345b9f
82f9657
8f16e9a
a15cedf
3245d56
35bf343
2ba533e
3529ec2
0e57ab7
1e0df43
d7a5d78
5bd5a71
0414216
462a19d
446faaf
3666dee
2e2d4e6
0a0e66c
d972ff3
ccf8eaf
5a21ea9
03b0bfd
e1d2b15
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1907,4 +1907,4 @@ | |
"publisherDisplayName": "Microsoft", | ||
"publisherId": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8" | ||
} | ||
} | ||
} |
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a thought, no change necessary: |
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please remove the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
} | ||
} |
There was a problem hiding this comment.
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 incatch
?There was a problem hiding this comment.
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.