Skip to content

Implement a per interpreter language server cache #8815

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

Merged
merged 28 commits into from
Dec 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c5c5ccd
Rework original idea to have smaller impact.
rchiodo Nov 22, 2019
6585f6d
Add ref counting for language server
rchiodo Nov 25, 2019
7dab6ad
Ref count idea
rchiodo Nov 25, 2019
23a9a16
Fix intellisense to work on restart of a jupyter server
rchiodo Nov 25, 2019
2c35a87
Disconnect from the language server when activating
rchiodo Nov 26, 2019
1ecf961
Fix linter issues
rchiodo Nov 26, 2019
97d05a8
Make jedi per interpreter as well
rchiodo Nov 26, 2019
0cbeacd
Fix up some static command registrations
rchiodo Nov 26, 2019
8df275c
Get all the unit tests to pass
rchiodo Nov 26, 2019
62f9f9f
Fix unit tests for interpreter changed
rchiodo Nov 26, 2019
2923d32
Add some functional tests
rchiodo Nov 26, 2019
1ae1972
Merge remote-tracking branch 'origin/master' into rchiodo/ls_per_kern…
rchiodo Nov 26, 2019
9fffeda
Get functional tests to have the right services
rchiodo Nov 26, 2019
4221c1e
Fix other functional tests to pass
rchiodo Nov 26, 2019
0aa62aa
Get functional test working for having a different interpreter
rchiodo Nov 27, 2019
677f239
Add news entry
rchiodo Nov 27, 2019
68135db
Fix linter problems.
rchiodo Nov 27, 2019
e25e802
Fix reconnect issue when Notebook is 'holding' a language server
rchiodo Nov 27, 2019
4433b05
Merge remote-tracking branch 'origin/master' into rchiodo/ls_per_kern…
rchiodo Nov 27, 2019
a1bff7f
Change connect/disconnect to activate/deactivate
rchiodo Nov 27, 2019
24d3b19
Fix build error
rchiodo Nov 27, 2019
cfc346d
Remove test only failure logic
rchiodo Nov 27, 2019
1bc9927
Fix notebook functional failure from second active interpreter
rchiodo Nov 27, 2019
fc88336
Review feedback
rchiodo Nov 27, 2019
da18351
Fix intellisense unit tests and linter problem
rchiodo Nov 27, 2019
5d42532
Fix service registry unit tests
rchiodo Nov 27, 2019
2f9b1dd
Fix linter problems
rchiodo Nov 27, 2019
03c9869
Fix smoke test for language server.
rchiodo Nov 27, 2019
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
24 changes: 24 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,30 @@
},
"skipFiles": ["<node_internals>/**"]
},
{
// Note, for the smoke test you want to debug, you may need to copy the file,
// rename it and remove a check for only smoke tests.
"name": "Tests (Smoke, VS Code, *.test.ts)",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"${workspaceFolder}/src/testMultiRootWkspc/smokeTests",
"--disable-extensions",
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test"
],
"env": {
"VSC_PYTHON_CI_TEST_GREP": "Smoke Test"
},
"stopOnEntry": false,
"sourceMaps": true,
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"preLaunchTask": "Compile",
"skipFiles": ["<node_internals>/**"]
},
{
"name": "Tests (Single Workspace, VS Code, *.test.ts)",
"type": "extensionHost",
Expand Down
1 change: 1 addition & 0 deletions news/1 Enhancements/8206.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support a per interpreter language server so that notebooks that aren't using the currently selected python can still have intellisense.
195 changes: 134 additions & 61 deletions src/client/activation/activationService.ts
Original file line number Diff line number Diff line change
@@ -1,106 +1,131 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';
import '../common/extensions';

import { inject, injectable } from 'inversify';
import { ConfigurationChangeEvent, Disposable, OutputChannel, Uri } from 'vscode';

import { LSNotSupportedDiagnosticServiceId } from '../application/diagnostics/checks/lsNotSupported';
import { IDiagnosticsService } from '../application/diagnostics/types';
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types';
import { STANDARD_OUTPUT_CHANNEL } from '../common/constants';
import { LSControl, LSEnabled } from '../common/experimentGroups';
import '../common/extensions';
import { traceError } from '../common/logger';
import { IConfigurationService, IDisposableRegistry, IExperimentsManager, IOutputChannel, IPersistentStateFactory, IPythonSettings, Resource } from '../common/types';
import {
IConfigurationService,
IDisposableRegistry,
IExperimentsManager,
IOutputChannel,
IPersistentStateFactory,
IPythonSettings,
Resource
} from '../common/types';
import { swallowExceptions } from '../common/utils/decorators';
import { noop } from '../common/utils/misc';
import { IInterpreterService, PythonInterpreter } from '../interpreter/contracts';
import { IServiceContainer } from '../ioc/types';
import { sendTelemetryEvent } from '../telemetry';
import { EventName } from '../telemetry/constants';
import { IExtensionActivationService, ILanguageServerActivator, LanguageServerActivator } from './types';
import { Commands } from './languageServer/constants';
import { RefCountedLanguageServer } from './refCountedLanguageServer';
import {
IExtensionActivationService,
ILanguageServerActivator,
ILanguageServerCache,
LanguageServerActivator
} from './types';

const jediEnabledSetting: keyof IPythonSettings = 'jediEnabled';
const workspacePathNameForGlobalWorkspaces = '';
type ActivatorInfo = { jedi: boolean; activator: ILanguageServerActivator };

interface IActivatedServer {
key: string;
server: ILanguageServerActivator;
jedi: boolean;
}

@injectable()
export class LanguageServerExtensionActivationService implements IExtensionActivationService, Disposable {
private lsActivatedWorkspaces = new Map<string, ILanguageServerActivator>();
private currentActivator?: ActivatorInfo;
private jediActivatedOnce: boolean = false;
export class LanguageServerExtensionActivationService implements IExtensionActivationService, ILanguageServerCache, Disposable {
private cache = new Map<string, Promise<RefCountedLanguageServer>>();
private activatedServer?: IActivatedServer;
private readonly workspaceService: IWorkspaceService;
private readonly output: OutputChannel;
private readonly appShell: IApplicationShell;
private readonly lsNotSupportedDiagnosticService: IDiagnosticsService;
private readonly interpreterService: IInterpreterService;
private resource!: Resource;

constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer,
@inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory,
@inject(IExperimentsManager) private readonly abExperiments: IExperimentsManager) {
this.workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService);
this.interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService);
this.output = this.serviceContainer.get<OutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL);
this.appShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell);
this.lsNotSupportedDiagnosticService = this.serviceContainer.get<IDiagnosticsService>(
IDiagnosticsService,
LSNotSupportedDiagnosticServiceId
);
const commandManager = this.serviceContainer.get<ICommandManager>(ICommandManager);
const disposables = serviceContainer.get<IDisposableRegistry>(IDisposableRegistry);
disposables.push(this);
disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this)));
disposables.push(this.workspaceService.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this));
disposables.push(this.interpreterService.onDidChangeInterpreter(this.onDidChangeInterpreter.bind(this)));
disposables.push(commandManager.registerCommand(Commands.ClearAnalyisCache, this.onClearAnalysisCaches.bind(this)));
}

public async activate(resource: Resource): Promise<void> {
// Get a new server and dispose of the old one (might be the same one)
this.resource = resource;
let jedi = this.useJedi();
if (!jedi) {
if (this.lsActivatedWorkspaces.has(this.getWorkspacePathKey(resource))) {
return;
}
const diagnostic = await this.lsNotSupportedDiagnosticService.diagnose(undefined);
this.lsNotSupportedDiagnosticService.handle(diagnostic).ignoreErrors();
if (diagnostic.length) {
sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED, undefined, { supported: false });
jedi = true;
}
} else {
if (this.jediActivatedOnce) {
return;
}
this.jediActivatedOnce = true;
const interpreter = await this.interpreterService.getActiveInterpreter(resource);
const key = await this.getKey(resource, interpreter);

// If we have an old server with a different key, then deactivate it as the
// creation of the new server may fail if this server is still connected
if (this.activatedServer && this.activatedServer.key !== key) {
this.activatedServer.server.deactivate();
}

await this.logStartup(jedi);
let activatorName = jedi ? LanguageServerActivator.Jedi : LanguageServerActivator.DotNet;
let activator = this.serviceContainer.get<ILanguageServerActivator>(ILanguageServerActivator, activatorName);
this.currentActivator = { jedi, activator };
// Get the new item
const result = await this.get(resource, interpreter);

try {
await activator.activate(resource);
if (!jedi) {
this.lsActivatedWorkspaces.set(this.getWorkspacePathKey(resource), activator);
}
} catch (ex) {
if (jedi) {
return;
}
//Language server fails, reverting to jedi
if (this.jediActivatedOnce) {
return;
}
this.jediActivatedOnce = true;
jedi = true;
await this.logStartup(jedi);
activatorName = LanguageServerActivator.Jedi;
activator = this.serviceContainer.get<ILanguageServerActivator>(ILanguageServerActivator, activatorName);
this.currentActivator = { jedi, activator };
await activator.activate(resource);
// Now we dispose. This ensures the object stays alive if it's the same object because
// we dispose after we increment the ref count.
if (this.activatedServer) {
this.activatedServer.server.dispose();
}

// Save our active server.
this.activatedServer = { key, server: result, jedi: result.type === LanguageServerActivator.Jedi };

// Force this server to reconnect (if disconnected) as it should be the active
// language server for all of VS code.
this.activatedServer.server.activate();
}

public async get(resource: Resource, interpreter?: PythonInterpreter): Promise<RefCountedLanguageServer> {
// See if we already have it or not
const key = await this.getKey(resource, interpreter);
let result: Promise<RefCountedLanguageServer> | undefined = this.cache.get(key);
if (!result) {
// Create a special ref counted result so we don't dispose of the
// server too soon.
result = this.createRefCountedServer(resource, interpreter, key);
this.cache.set(key, result);
} else {
// Increment ref count if already exists.
result = result.then(r => {
r.increment();
return r;
});
}
return result;
}

public dispose() {
if (this.currentActivator) {
this.currentActivator.activator.dispose();
if (this.activatedServer) {
this.activatedServer.server.dispose();
}
}
@swallowExceptions('Send telemetry for Language Server current selection')
Expand Down Expand Up @@ -149,19 +174,60 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv
return enabled;
}

protected onWorkspaceFoldersChanged() {
protected async onWorkspaceFoldersChanged() {
//If an activated workspace folder was removed, dispose its activator
const workspaceKeys = this.workspaceService.workspaceFolders!.map(workspaceFolder => this.getWorkspacePathKey(workspaceFolder.uri));
const activatedWkspcKeys = Array.from(this.lsActivatedWorkspaces.keys());
const workspaceKeys = await Promise.all(this.workspaceService.workspaceFolders!.map(workspaceFolder => this.getKey(workspaceFolder.uri)));
const activatedWkspcKeys = Array.from(this.cache.keys());
const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter(item => workspaceKeys.indexOf(item) < 0);
if (activatedWkspcFoldersRemoved.length > 0) {
for (const folder of activatedWkspcFoldersRemoved) {
this.lsActivatedWorkspaces.get(folder)!.dispose();
this.lsActivatedWorkspaces!.delete(folder);
const server = await this.cache.get(folder);
server?.dispose(); // This should remove it from the cache if this is the last instance.
}
}
}

private async onDidChangeInterpreter() {
// Reactivate the resource. It should destroy the old one if it's different.
return this.activate(this.resource);
}

private async createRefCountedServer(resource: Resource, interpreter: PythonInterpreter | undefined, key: string): Promise<RefCountedLanguageServer> {
let jedi = this.useJedi();
if (!jedi) {
const diagnostic = await this.lsNotSupportedDiagnosticService.diagnose(undefined);
this.lsNotSupportedDiagnosticService.handle(diagnostic).ignoreErrors();
if (diagnostic.length) {
sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED, undefined, { supported: false });
jedi = true;
}
}

await this.logStartup(jedi);
let serverName = jedi ? LanguageServerActivator.Jedi : LanguageServerActivator.DotNet;
let server = this.serviceContainer.get<ILanguageServerActivator>(ILanguageServerActivator, serverName);
try {
await server.start(resource, interpreter);
} catch (ex) {
if (jedi) {
throw ex;
}
await this.logStartup(jedi);
serverName = LanguageServerActivator.Jedi;
server = this.serviceContainer.get<ILanguageServerActivator>(ILanguageServerActivator, serverName);
await server.start(resource, interpreter);
}

// Wrap the returned server in something that ref counts it.
return new RefCountedLanguageServer(server, serverName, () => {
// When we finally remove the last ref count, remove from the cache
this.cache.delete(key);

// Dispose of the actual server.
server.dispose();
});
}

private async logStartup(isJedi: boolean): Promise<void> {
const outputLine = isJedi
? 'Starting Jedi Python language engine.'
Expand All @@ -177,10 +243,9 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv
return;
}
const jedi = this.useJedi();
if (this.currentActivator && this.currentActivator.jedi === jedi) {
if (this.activatedServer && this.activatedServer.jedi === jedi) {
return;
}

const item = await this.appShell.showInformationMessage(
'Please reload the window switching between language engines.',
'Reload'
Expand All @@ -189,7 +254,15 @@ export class LanguageServerExtensionActivationService implements IExtensionActiv
this.serviceContainer.get<ICommandManager>(ICommandManager).executeCommand('workbench.action.reloadWindow');
}
}
private getWorkspacePathKey(resource: Resource): string {
return this.workspaceService.getWorkspaceFolderIdentifier(resource, workspacePathNameForGlobalWorkspaces);
private async getKey(resource: Resource, interpreter?: PythonInterpreter): Promise<string> {
const resourcePortion = this.workspaceService.getWorkspaceFolderIdentifier(resource, workspacePathNameForGlobalWorkspaces);
interpreter = interpreter ? interpreter : await this.interpreterService.getActiveInterpreter(resource);
const interperterPortion = interpreter ? `${interpreter.path}-${interpreter.envName}` : '';
return `${resourcePortion}-${interperterPortion}`;
}

private async onClearAnalysisCaches() {
const values = await Promise.all([...this.cache.values()]);
values.forEach(v => v.clearAnalysisCache ? v.clearAnalysisCache() : noop());
}
}
Loading