Skip to content

Commit d1e4562

Browse files
authored
Move tensorboard support into a separate extension (#22197)
* No need of experiments (if users install extension, then it works) * If tensorboard extension is installed the we rely on tensorboard extension to handle everything * For final deplayment we can decide whether to just remove this feature altogether or prompt users to install tensorboard extension or to go with an experiment, for now I wanted to keep this super simple (this shoudl not affect anyone as no one will have a tensorboard extension except us) * Simple private API for tensorboard extension, untill Python ext exposes a stable API * API is similar to Jupyter, scoped to Tensorboard ext
1 parent 1dd8a4b commit d1e4562

18 files changed

+256
-33
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1836,15 +1836,15 @@
18361836
"category": "Python",
18371837
"command": "python.launchTensorBoard",
18381838
"title": "%python.command.python.launchTensorBoard.title%",
1839-
"when": "!virtualWorkspace && shellExecutionSupported"
1839+
"when": "!virtualWorkspace && shellExecutionSupported && !python.tensorboardExtInstalled"
18401840
},
18411841
{
18421842
"category": "Python",
18431843
"command": "python.refreshTensorBoard",
18441844
"enablement": "python.hasActiveTensorBoardSession",
18451845
"icon": "$(refresh)",
18461846
"title": "%python.command.python.refreshTensorBoard.title%",
1847-
"when": "!virtualWorkspace && shellExecutionSupported"
1847+
"when": "!virtualWorkspace && shellExecutionSupported && !python.tensorboardExtInstalled"
18481848
},
18491849
{
18501850
"category": "Python",

src/client/api.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { IDiscoveryAPI } from './pythonEnvironments/base/locator';
2121
import { buildEnvironmentApi } from './environmentApi';
2222
import { ApiForPylance } from './pylanceApi';
2323
import { getTelemetryReporter } from './telemetry';
24+
import { TensorboardExtensionIntegration } from './tensorBoard/tensorboardIntegration';
2425

2526
export function buildApi(
2627
ready: Promise<void>,
@@ -31,7 +32,14 @@ export function buildApi(
3132
const configurationService = serviceContainer.get<IConfigurationService>(IConfigurationService);
3233
const interpreterService = serviceContainer.get<IInterpreterService>(IInterpreterService);
3334
serviceManager.addSingleton<JupyterExtensionIntegration>(JupyterExtensionIntegration, JupyterExtensionIntegration);
35+
serviceManager.addSingleton<TensorboardExtensionIntegration>(
36+
TensorboardExtensionIntegration,
37+
TensorboardExtensionIntegration,
38+
);
3439
const jupyterIntegration = serviceContainer.get<JupyterExtensionIntegration>(JupyterExtensionIntegration);
40+
const tensorboardIntegration = serviceContainer.get<TensorboardExtensionIntegration>(
41+
TensorboardExtensionIntegration,
42+
);
3543
const outputChannel = serviceContainer.get<ILanguageServerOutputChannel>(ILanguageServerOutputChannel);
3644

3745
const api: PythonExtension & {
@@ -41,6 +49,12 @@ export function buildApi(
4149
jupyter: {
4250
registerHooks(): void;
4351
};
52+
/**
53+
* Internal API just for Tensorboard, hence don't include in the official types.
54+
*/
55+
tensorboard: {
56+
registerHooks(): void;
57+
};
4458
} & {
4559
/**
4660
* @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an
@@ -92,6 +106,9 @@ export function buildApi(
92106
jupyter: {
93107
registerHooks: () => jupyterIntegration.integrateWithJupyterExtension(),
94108
},
109+
tensorboard: {
110+
registerHooks: () => tensorboardIntegration.integrateWithTensorboardExtension(),
111+
},
95112
debug: {
96113
async getRemoteLauncherCommand(
97114
host: string,

src/client/common/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const PYTHON_NOTEBOOKS = [
2323
export const PVSC_EXTENSION_ID = 'ms-python.python';
2424
export const PYLANCE_EXTENSION_ID = 'ms-python.vscode-pylance';
2525
export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter';
26+
export const TENSORBOARD_EXTENSION_ID = 'ms-toolsai.tensorboard';
2627
export const AppinsightsKey = '0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255';
2728

2829
export type Channel = 'stable' | 'insiders';

src/client/tensorBoard/nbextensionCodeLensProvider.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { sendTelemetryEvent } from '../telemetry';
1212
import { EventName } from '../telemetry/constants';
1313
import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants';
1414
import { containsNotebookExtension } from './helpers';
15+
import { useNewTensorboardExtension } from './tensorboarExperiment';
1516

1617
@injectable()
1718
export class TensorBoardNbextensionCodeLensProvider implements IExtensionSingleActivationService {
@@ -27,6 +28,9 @@ export class TensorBoardNbextensionCodeLensProvider implements IExtensionSingleA
2728
constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {}
2829

2930
public async activate(): Promise<void> {
31+
if (useNewTensorboardExtension()) {
32+
return;
33+
}
3034
this.activateInternal().ignoreErrors();
3135
}
3236

src/client/tensorBoard/serviceRegistry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { TensorBoardPrompt } from './tensorBoardPrompt';
1010
import { TensorBoardSessionProvider } from './tensorBoardSessionProvider';
1111
import { TensorBoardNbextensionCodeLensProvider } from './nbextensionCodeLensProvider';
1212
import { TerminalWatcher } from './terminalWatcher';
13+
import { TensorboardDependencyChecker } from './tensorboardDependencyChecker';
1314

1415
export function registerTypes(serviceManager: IServiceManager): void {
1516
serviceManager.addSingleton<TensorBoardSessionProvider>(TensorBoardSessionProvider, TensorBoardSessionProvider);
@@ -32,4 +33,5 @@ export function registerTypes(serviceManager: IServiceManager): void {
3233
);
3334
serviceManager.addBinding(TensorBoardNbextensionCodeLensProvider, IExtensionSingleActivationService);
3435
serviceManager.addSingleton(IExtensionSingleActivationService, TerminalWatcher);
36+
serviceManager.addSingleton(TensorboardDependencyChecker, TensorboardDependencyChecker);
3537
}

src/client/tensorBoard/tensorBoardFileWatcher.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { IWorkspaceService } from '../common/application/types';
88
import { IDisposableRegistry } from '../common/types';
99
import { TensorBoardEntrypointTrigger } from './constants';
1010
import { TensorBoardPrompt } from './tensorBoardPrompt';
11+
import { useNewTensorboardExtension } from './tensorboarExperiment';
1112

1213
@injectable()
1314
export class TensorBoardFileWatcher implements IExtensionSingleActivationService {
@@ -24,6 +25,9 @@ export class TensorBoardFileWatcher implements IExtensionSingleActivationService
2425
) {}
2526

2627
public async activate(): Promise<void> {
28+
if (useNewTensorboardExtension()) {
29+
return;
30+
}
2731
this.activateInternal().ignoreErrors();
2832
}
2933

src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { sendTelemetryEvent } from '../telemetry';
1212
import { EventName } from '../telemetry/constants';
1313
import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants';
1414
import { containsTensorBoardImport } from './helpers';
15+
import { useNewTensorboardExtension } from './tensorboarExperiment';
1516

1617
@injectable()
1718
export class TensorBoardImportCodeLensProvider implements IExtensionSingleActivationService {
@@ -27,6 +28,9 @@ export class TensorBoardImportCodeLensProvider implements IExtensionSingleActiva
2728
constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {}
2829

2930
public async activate(): Promise<void> {
31+
if (useNewTensorboardExtension()) {
32+
return;
33+
}
3034
this.activateInternal().ignoreErrors();
3135
}
3236

src/client/tensorBoard/tensorBoardPrompt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export class TensorBoardPrompt {
8484
}
8585
}
8686

87-
private isPromptEnabled(): boolean {
87+
public isPromptEnabled(): boolean {
8888
return this.state.value;
8989
}
9090

src/client/tensorBoard/tensorBoardSession.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,10 @@ export class TensorBoardSession {
100100
private readonly globalMemento: IPersistentState<ViewColumn>,
101101
private readonly multiStepFactory: IMultiStepInputFactory,
102102
private readonly configurationService: IConfigurationService,
103-
) {}
103+
) {
104+
this.disposables.push(this.onDidChangeViewStateEventEmitter);
105+
this.disposables.push(this.onDidDisposeEventEmitter);
106+
}
104107

105108
public get onDidDispose(): Event<TensorBoardSession> {
106109
return this.onDidDisposeEventEmitter.event;
@@ -189,10 +192,10 @@ export class TensorBoardSession {
189192
// to start a TensorBoard session. If the user has a torch import in
190193
// any of their open documents, also try to install the torch-tb-plugin
191194
// package, but don't block if installing that fails.
192-
private async ensurePrerequisitesAreInstalled() {
195+
public async ensurePrerequisitesAreInstalled(resource?: Uri): Promise<boolean> {
193196
traceVerbose('Ensuring TensorBoard package is installed into active interpreter');
194197
const interpreter =
195-
(await this.interpreterService.getActiveInterpreter()) ||
198+
(await this.interpreterService.getActiveInterpreter(resource)) ||
196199
(await this.commandManager.executeCommand('python.setInterpreter'));
197200
if (!interpreter) {
198201
return false;

src/client/tensorBoard/tensorBoardSessionProvider.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ import { sendTelemetryEvent } from '../telemetry';
2222
import { EventName } from '../telemetry/constants';
2323
import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants';
2424
import { TensorBoardSession } from './tensorBoardSession';
25+
import { useNewTensorboardExtension } from './tensorboarExperiment';
2526

26-
const PREFERRED_VIEWGROUP = 'PythonTensorBoardWebviewPreferredViewGroup';
27+
export const PREFERRED_VIEWGROUP = 'PythonTensorBoardWebviewPreferredViewGroup';
2728

2829
@injectable()
2930
export class TensorBoardSessionProvider implements IExtensionSingleActivationService {
@@ -58,6 +59,10 @@ export class TensorBoardSessionProvider implements IExtensionSingleActivationSer
5859
}
5960

6061
public async activate(): Promise<void> {
62+
if (useNewTensorboardExtension()) {
63+
return;
64+
}
65+
6166
this.disposables.push(
6267
this.commandManager.registerCommand(
6368
Commands.LaunchTensorBoard,

src/client/tensorBoard/tensorBoardUsageTracker.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { getDocumentLines } from '../telemetry/importTracker';
1212
import { TensorBoardEntrypointTrigger } from './constants';
1313
import { containsTensorBoardImport } from './helpers';
1414
import { TensorBoardPrompt } from './tensorBoardPrompt';
15+
import { useNewTensorboardExtension } from './tensorboarExperiment';
1516

1617
const testExecution = isTestExecution();
1718

@@ -28,6 +29,9 @@ export class TensorBoardUsageTracker implements IExtensionSingleActivationServic
2829
) {}
2930

3031
public async activate(): Promise<void> {
32+
if (useNewTensorboardExtension()) {
33+
return;
34+
}
3135
if (testExecution) {
3236
await this.activateInternal();
3337
} else {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { extensions } from 'vscode';
5+
6+
export function useNewTensorboardExtension(): boolean {
7+
return !!extensions.getExtension('ms-toolsai.tensorboard');
8+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { inject, injectable } from 'inversify';
5+
import { Uri, ViewColumn } from 'vscode';
6+
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types';
7+
import { IPythonExecutionFactory } from '../common/process/types';
8+
import {
9+
IInstaller,
10+
IPersistentState,
11+
IPersistentStateFactory,
12+
IConfigurationService,
13+
IDisposable,
14+
} from '../common/types';
15+
import { IMultiStepInputFactory } from '../common/utils/multiStepInput';
16+
import { IInterpreterService } from '../interpreter/contracts';
17+
import { TensorBoardSession } from './tensorBoardSession';
18+
import { disposeAll } from '../common/utils/resourceLifecycle';
19+
import { PREFERRED_VIEWGROUP } from './tensorBoardSessionProvider';
20+
21+
@injectable()
22+
export class TensorboardDependencyChecker {
23+
private preferredViewGroupMemento: IPersistentState<ViewColumn>;
24+
25+
constructor(
26+
@inject(IInstaller) private readonly installer: IInstaller,
27+
@inject(IInterpreterService) private readonly interpreterService: IInterpreterService,
28+
@inject(IApplicationShell) private readonly applicationShell: IApplicationShell,
29+
@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService,
30+
@inject(ICommandManager) private readonly commandManager: ICommandManager,
31+
@inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory,
32+
@inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory,
33+
@inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory,
34+
@inject(IConfigurationService) private readonly configurationService: IConfigurationService,
35+
) {
36+
this.preferredViewGroupMemento = this.stateFactory.createGlobalPersistentState<ViewColumn>(
37+
PREFERRED_VIEWGROUP,
38+
ViewColumn.Active,
39+
);
40+
}
41+
42+
public async ensureDependenciesAreInstalled(resource?: Uri): Promise<boolean> {
43+
const disposables: IDisposable[] = [];
44+
const newSession = new TensorBoardSession(
45+
this.installer,
46+
this.interpreterService,
47+
this.workspaceService,
48+
this.pythonExecFactory,
49+
this.commandManager,
50+
disposables,
51+
this.applicationShell,
52+
this.preferredViewGroupMemento,
53+
this.multiStepFactory,
54+
this.configurationService,
55+
);
56+
const result = await newSession.ensurePrerequisitesAreInstalled(resource);
57+
disposeAll(disposables);
58+
return result;
59+
}
60+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/* eslint-disable comma-dangle */
2+
3+
/* eslint-disable implicit-arrow-linebreak */
4+
// Copyright (c) Microsoft Corporation. All rights reserved.
5+
// Licensed under the MIT License.
6+
7+
import { inject, injectable } from 'inversify';
8+
import { Extension, Uri, commands } from 'vscode';
9+
import { IWorkspaceService } from '../common/application/types';
10+
import { TENSORBOARD_EXTENSION_ID } from '../common/constants';
11+
import { IDisposableRegistry, IExtensions, Resource } from '../common/types';
12+
import { IEnvironmentActivationService } from '../interpreter/activation/types';
13+
import { TensorBoardPrompt } from './tensorBoardPrompt';
14+
import { TensorboardDependencyChecker } from './tensorboardDependencyChecker';
15+
16+
type PythonApiForTensorboardExtension = {
17+
/**
18+
* Gets activated env vars for the active Python Environment for the given resource.
19+
*/
20+
getActivatedEnvironmentVariables(resource: Resource): Promise<NodeJS.ProcessEnv | undefined>;
21+
/**
22+
* Ensures that the dependencies required for TensorBoard are installed in Active Environment for the given resource.
23+
*/
24+
ensureDependenciesAreInstalled(resource?: Uri): Promise<boolean>;
25+
/**
26+
* Whether to allow displaying tensorboard prompt.
27+
*/
28+
isPromptEnabled(): boolean;
29+
};
30+
31+
type TensorboardExtensionApi = {
32+
/**
33+
* Registers python extension specific parts with the tensorboard extension
34+
*/
35+
registerPythonApi(interpreterService: PythonApiForTensorboardExtension): void;
36+
};
37+
38+
@injectable()
39+
export class TensorboardExtensionIntegration {
40+
private tensorboardExtension: Extension<TensorboardExtensionApi> | undefined;
41+
42+
constructor(
43+
@inject(IExtensions) private readonly extensions: IExtensions,
44+
@inject(IEnvironmentActivationService) private readonly envActivation: IEnvironmentActivationService,
45+
@inject(IWorkspaceService) private workspaceService: IWorkspaceService,
46+
@inject(TensorboardDependencyChecker) private readonly dependencyChcker: TensorboardDependencyChecker,
47+
@inject(TensorBoardPrompt) private readonly tensorBoardPrompt: TensorBoardPrompt,
48+
@inject(IDisposableRegistry) disposables: IDisposableRegistry,
49+
) {
50+
this.hideCommands();
51+
extensions.onDidChange(this.hideCommands, this, disposables);
52+
}
53+
54+
public registerApi(tensorboardExtensionApi: TensorboardExtensionApi): TensorboardExtensionApi | undefined {
55+
this.hideCommands();
56+
if (!this.workspaceService.isTrusted) {
57+
this.workspaceService.onDidGrantWorkspaceTrust(() => this.registerApi(tensorboardExtensionApi));
58+
return undefined;
59+
}
60+
tensorboardExtensionApi.registerPythonApi({
61+
getActivatedEnvironmentVariables: async (resource: Resource) =>
62+
this.envActivation.getActivatedEnvironmentVariables(resource, undefined, true),
63+
ensureDependenciesAreInstalled: async (resource?: Uri): Promise<boolean> =>
64+
this.dependencyChcker.ensureDependenciesAreInstalled(resource),
65+
isPromptEnabled: () => this.tensorBoardPrompt.isPromptEnabled(),
66+
});
67+
return undefined;
68+
}
69+
70+
public hideCommands(): void {
71+
if (this.extensions.getExtension(TENSORBOARD_EXTENSION_ID)) {
72+
console.error('TensorBoard extension is installed');
73+
void commands.executeCommand('setContext', 'python.tensorboardExtInstalled', true);
74+
} else {
75+
console.error('TensorBoard extension not installed');
76+
}
77+
}
78+
79+
public async integrateWithTensorboardExtension(): Promise<void> {
80+
const api = await this.getExtensionApi();
81+
if (api) {
82+
this.registerApi(api);
83+
}
84+
}
85+
86+
private async getExtensionApi(): Promise<TensorboardExtensionApi | undefined> {
87+
if (!this.tensorboardExtension) {
88+
const extension = this.extensions.getExtension<TensorboardExtensionApi>(TENSORBOARD_EXTENSION_ID);
89+
if (!extension) {
90+
return undefined;
91+
}
92+
await extension.activate();
93+
if (extension.isActive) {
94+
this.tensorboardExtension = extension;
95+
return this.tensorboardExtension.exports;
96+
}
97+
} else {
98+
return this.tensorboardExtension.exports;
99+
}
100+
return undefined;
101+
}
102+
}

0 commit comments

Comments
 (0)