Skip to content

Commit f438262

Browse files
authored
Add support for a tensorboard experiment (microsoft#22215)
1 parent 1310bd6 commit f438262

16 files changed

+380
-208
lines changed

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -537,15 +537,17 @@
537537
"pythonPromptNewToolsExt",
538538
"pythonTerminalEnvVarActivation",
539539
"pythonTestAdapter",
540-
"pythonREPLSmartSend"
540+
"pythonREPLSmartSend",
541+
"pythonRecommendTensorboardExt"
541542
],
542543
"enumDescriptions": [
543544
"%python.experiments.All.description%",
544545
"%python.experiments.pythonSurveyNotification.description%",
545546
"%python.experiments.pythonPromptNewToolsExt.description%",
546547
"%python.experiments.pythonTerminalEnvVarActivation.description%",
547548
"%python.experiments.pythonTestAdapter.description%",
548-
"%python.experiments.pythonREPLSmartSend.description%"
549+
"%python.experiments.pythonREPLSmartSend.description%",
550+
"%python.experiments.pythonRecommendTensorboardExt.description%"
549551
]
550552
},
551553
"scope": "machine",

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"python.experiments.pythonTerminalEnvVarActivation.description": "Enables use of environment variables to activate terminals instead of sending activation commands.",
4343
"python.experiments.pythonTestAdapter.description": "Denotes the Python Test Adapter experiment.",
4444
"python.experiments.pythonREPLSmartSend.description": "Denotes the Python REPL Smart Send experiment.",
45+
"python.experiments.pythonRecommendTensorboardExt.description": "Denotes the Tensorboard Extension recommendation experiment.",
4546
"python.globalModuleInstallation.description": "Whether to install Python modules globally when not using an environment.",
4647
"python.languageServer.description": "Defines type of the language server.",
4748
"python.languageServer.defaultDescription": "Automatically select a language server: Pylance if installed and available, otherwise fallback to Jedi.",

src/client/common/experiments/groups.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,8 @@ export enum EnableTestAdapterRewrite {
2222
export enum EnableREPLSmartSend {
2323
experiment = 'pythonREPLSmartSend',
2424
}
25+
26+
// Experiment to recommend installing the tensorboard extension.
27+
export enum RecommendTensobardExtension {
28+
experiment = 'pythonRecommendTensorboardExt',
29+
}

src/client/tensorBoard/nbextensionCodeLensProvider.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,46 @@
33

44
import { inject, injectable } from 'inversify';
55
import { once } from 'lodash';
6-
import { CancellationToken, CodeLens, Command, languages, Position, Range, TextDocument } from 'vscode';
6+
import { CancellationToken, CodeLens, Command, Disposable, languages, Position, Range, TextDocument } from 'vscode';
77
import { IExtensionSingleActivationService } from '../activation/types';
88
import { Commands, NotebookCellScheme, PYTHON_LANGUAGE } from '../common/constants';
9-
import { IDisposableRegistry } from '../common/types';
9+
import { IDisposable, IDisposableRegistry } from '../common/types';
1010
import { TensorBoard } from '../common/utils/localize';
1111
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';
15+
import { TensorboardExperiment } from './tensorboarExperiment';
1616

1717
@injectable()
1818
export class TensorBoardNbextensionCodeLensProvider implements IExtensionSingleActivationService {
1919
public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false };
2020

21+
private readonly disposables: IDisposable[] = [];
22+
2123
private sendTelemetryOnce = once(
2224
sendTelemetryEvent.bind(this, EventName.TENSORBOARD_ENTRYPOINT_SHOWN, undefined, {
2325
trigger: TensorBoardEntrypointTrigger.nbextension,
2426
entrypoint: TensorBoardEntrypoint.codelens,
2527
}),
2628
);
2729

28-
constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {}
30+
constructor(
31+
@inject(IDisposableRegistry) disposables: IDisposableRegistry,
32+
@inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment,
33+
) {
34+
disposables.push(this);
35+
}
36+
37+
public dispose(): void {
38+
Disposable.from(...this.disposables).dispose();
39+
}
2940

3041
public async activate(): Promise<void> {
31-
if (useNewTensorboardExtension()) {
42+
if (TensorboardExperiment.isTensorboardExtensionInstalled) {
3243
return;
3344
}
45+
this.experiment.disposeOnInstallingTensorboard(this);
3446
this.activateInternal().ignoreErrors();
3547
}
3648

src/client/tensorBoard/serviceRegistry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { TensorBoardSessionProvider } from './tensorBoardSessionProvider';
1111
import { TensorBoardNbextensionCodeLensProvider } from './nbextensionCodeLensProvider';
1212
import { TerminalWatcher } from './terminalWatcher';
1313
import { TensorboardDependencyChecker } from './tensorboardDependencyChecker';
14+
import { TensorboardExperiment } from './tensorboarExperiment';
1415

1516
export function registerTypes(serviceManager: IServiceManager): void {
1617
serviceManager.addSingleton<TensorBoardSessionProvider>(TensorBoardSessionProvider, TensorBoardSessionProvider);
@@ -34,4 +35,5 @@ export function registerTypes(serviceManager: IServiceManager): void {
3435
serviceManager.addBinding(TensorBoardNbextensionCodeLensProvider, IExtensionSingleActivationService);
3536
serviceManager.addSingleton(IExtensionSingleActivationService, TerminalWatcher);
3637
serviceManager.addSingleton(TensorboardDependencyChecker, TensorboardDependencyChecker);
38+
serviceManager.addSingleton(TensorboardExperiment, TensorboardExperiment);
3739
}

src/client/tensorBoard/tensorBoardFileWatcher.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
// Licensed under the MIT License.
33

44
import { inject, injectable } from 'inversify';
5-
import { FileSystemWatcher, RelativePattern, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode';
5+
import { Disposable, FileSystemWatcher, RelativePattern, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode';
66
import { IExtensionSingleActivationService } from '../activation/types';
77
import { IWorkspaceService } from '../common/application/types';
8-
import { IDisposableRegistry } from '../common/types';
8+
import { IDisposable, IDisposableRegistry } from '../common/types';
99
import { TensorBoardEntrypointTrigger } from './constants';
1010
import { TensorBoardPrompt } from './tensorBoardPrompt';
11-
import { useNewTensorboardExtension } from './tensorboarExperiment';
11+
import { TensorboardExperiment } from './tensorboarExperiment';
1212

1313
@injectable()
1414
export class TensorBoardFileWatcher implements IExtensionSingleActivationService {
@@ -18,16 +18,26 @@ export class TensorBoardFileWatcher implements IExtensionSingleActivationService
1818

1919
private globPatterns = ['*tfevents*', '*/*tfevents*', '*/*/*tfevents*'];
2020

21+
private readonly disposables: IDisposable[] = [];
22+
2123
constructor(
2224
@inject(IWorkspaceService) private workspaceService: IWorkspaceService,
2325
@inject(TensorBoardPrompt) private tensorBoardPrompt: TensorBoardPrompt,
24-
@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry,
25-
) {}
26+
@inject(IDisposableRegistry) disposables: IDisposableRegistry,
27+
@inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment,
28+
) {
29+
disposables.push(this);
30+
}
31+
32+
public dispose(): void {
33+
Disposable.from(...this.disposables).dispose();
34+
}
2635

2736
public async activate(): Promise<void> {
28-
if (useNewTensorboardExtension()) {
37+
if (TensorboardExperiment.isTensorboardExtensionInstalled) {
2938
return;
3039
}
40+
this.experiment.disposeOnInstallingTensorboard(this);
3141
this.activateInternal().ignoreErrors();
3242
}
3343

src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33

44
import { inject, injectable } from 'inversify';
55
import { once } from 'lodash';
6-
import { CancellationToken, CodeLens, Command, languages, Position, Range, TextDocument } from 'vscode';
6+
import { CancellationToken, CodeLens, Command, Disposable, languages, Position, Range, TextDocument } from 'vscode';
77
import { IExtensionSingleActivationService } from '../activation/types';
88
import { Commands, PYTHON } from '../common/constants';
9-
import { IDisposableRegistry } from '../common/types';
9+
import { IDisposable, IDisposableRegistry } from '../common/types';
1010
import { TensorBoard } from '../common/utils/localize';
1111
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';
15+
import { TensorboardExperiment } from './tensorboarExperiment';
1616

1717
@injectable()
1818
export class TensorBoardImportCodeLensProvider implements IExtensionSingleActivationService {
@@ -25,12 +25,24 @@ export class TensorBoardImportCodeLensProvider implements IExtensionSingleActiva
2525
}),
2626
);
2727

28-
constructor(@inject(IDisposableRegistry) private disposables: IDisposableRegistry) {}
28+
private readonly disposables: IDisposable[] = [];
29+
30+
constructor(
31+
@inject(IDisposableRegistry) disposables: IDisposableRegistry,
32+
@inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment,
33+
) {
34+
disposables.push(this);
35+
}
36+
37+
public dispose(): void {
38+
Disposable.from(...this.disposables).dispose();
39+
}
2940

3041
public async activate(): Promise<void> {
31-
if (useNewTensorboardExtension()) {
42+
if (TensorboardExperiment.isTensorboardExtensionInstalled) {
3243
return;
3344
}
45+
this.experiment.disposeOnInstallingTensorboard(this);
3446
this.activateInternal().ignoreErrors();
3547
}
3648

src/client/tensorBoard/tensorBoardSessionProvider.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT License.
33

44
import { inject, injectable } from 'inversify';
5-
import { l10n, ViewColumn } from 'vscode';
5+
import { Disposable, l10n, ViewColumn } from 'vscode';
66
import { IExtensionSingleActivationService } from '../activation/types';
77
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types';
88
import { Commands } from '../common/constants';
@@ -14,6 +14,7 @@ import {
1414
IPersistentState,
1515
IPersistentStateFactory,
1616
IConfigurationService,
17+
IDisposable,
1718
} from '../common/types';
1819
import { IMultiStepInputFactory } from '../common/utils/multiStepInput';
1920
import { IInterpreterService } from '../interpreter/contracts';
@@ -22,7 +23,7 @@ import { sendTelemetryEvent } from '../telemetry';
2223
import { EventName } from '../telemetry/constants';
2324
import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants';
2425
import { TensorBoardSession } from './tensorBoardSession';
25-
import { useNewTensorboardExtension } from './tensorboarExperiment';
26+
import { TensorboardExperiment } from './tensorboarExperiment';
2627

2728
export const PREFERRED_VIEWGROUP = 'PythonTensorBoardWebviewPreferredViewGroup';
2829

@@ -36,18 +37,22 @@ export class TensorBoardSessionProvider implements IExtensionSingleActivationSer
3637

3738
private hasActiveTensorBoardSessionContext: ContextKey;
3839

40+
private readonly disposables: IDisposable[] = [];
41+
3942
constructor(
4043
@inject(IInstaller) private readonly installer: IInstaller,
4144
@inject(IInterpreterService) private readonly interpreterService: IInterpreterService,
4245
@inject(IApplicationShell) private readonly applicationShell: IApplicationShell,
4346
@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService,
4447
@inject(ICommandManager) private readonly commandManager: ICommandManager,
45-
@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry,
48+
@inject(IDisposableRegistry) disposables: IDisposableRegistry,
4649
@inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory,
4750
@inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory,
4851
@inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory,
4952
@inject(IConfigurationService) private readonly configurationService: IConfigurationService,
53+
@inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment,
5054
) {
55+
disposables.push(this);
5156
this.preferredViewGroupMemento = this.stateFactory.createGlobalPersistentState<ViewColumn>(
5257
PREFERRED_VIEWGROUP,
5358
ViewColumn.Active,
@@ -58,27 +63,36 @@ export class TensorBoardSessionProvider implements IExtensionSingleActivationSer
5863
);
5964
}
6065

66+
public dispose(): void {
67+
Disposable.from(...this.disposables).dispose();
68+
}
69+
6170
public async activate(): Promise<void> {
62-
if (useNewTensorboardExtension()) {
71+
if (TensorboardExperiment.isTensorboardExtensionInstalled) {
6372
return;
6473
}
74+
this.experiment.disposeOnInstallingTensorboard(this);
6575

6676
this.disposables.push(
6777
this.commandManager.registerCommand(
6878
Commands.LaunchTensorBoard,
6979
(
7080
entrypoint: TensorBoardEntrypoint = TensorBoardEntrypoint.palette,
7181
trigger: TensorBoardEntrypointTrigger = TensorBoardEntrypointTrigger.palette,
72-
) => {
82+
): void => {
7383
sendTelemetryEvent(EventName.TENSORBOARD_SESSION_LAUNCH, undefined, {
7484
trigger,
7585
entrypoint,
7686
});
77-
return this.createNewSession();
87+
if (this.experiment.recommendAndUseNewExtension() === 'continueWithPythonExtension') {
88+
void this.createNewSession();
89+
}
7890
},
7991
),
8092
this.commandManager.registerCommand(Commands.RefreshTensorBoard, () =>
81-
this.knownSessions.map((w) => w.refresh()),
93+
this.experiment.recommendAndUseNewExtension() === 'continueWithPythonExtension'
94+
? this.knownSessions.map((w) => w.refresh())
95+
: undefined,
8296
),
8397
);
8498
}

src/client/tensorBoard/tensorBoardUsageTracker.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import { inject, injectable } from 'inversify';
55
import * as path from 'path';
6-
import { TextEditor } from 'vscode';
6+
import { Disposable, TextEditor } from 'vscode';
77
import { IExtensionSingleActivationService } from '../activation/types';
88
import { IDocumentManager } from '../common/application/types';
99
import { isTestExecution } from '../common/constants';
@@ -12,7 +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';
15+
import { TensorboardExperiment } from './tensorboarExperiment';
1616

1717
const testExecution = isTestExecution();
1818

@@ -26,12 +26,20 @@ export class TensorBoardUsageTracker implements IExtensionSingleActivationServic
2626
@inject(IDocumentManager) private documentManager: IDocumentManager,
2727
@inject(IDisposableRegistry) private disposables: IDisposableRegistry,
2828
@inject(TensorBoardPrompt) private prompt: TensorBoardPrompt,
29-
) {}
29+
@inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment,
30+
) {
31+
disposables.push(this);
32+
}
33+
34+
public dispose(): void {
35+
Disposable.from(...this.disposables).dispose();
36+
}
3037

3138
public async activate(): Promise<void> {
32-
if (useNewTensorboardExtension()) {
39+
if (TensorboardExperiment.isTensorboardExtensionInstalled) {
3340
return;
3441
}
42+
this.experiment.disposeOnInstallingTensorboard(this);
3543
if (testExecution) {
3644
await this.activateInternal();
3745
} else {
Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,67 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
import { extensions } from 'vscode';
4+
import { Disposable, EventEmitter, commands, extensions, l10n, window } from 'vscode';
5+
import { inject, injectable } from 'inversify';
6+
import { IDisposable, IDisposableRegistry, IExperimentService } from '../common/types';
7+
import { RecommendTensobardExtension } from '../common/experiments/groups';
8+
import { TENSORBOARD_EXTENSION_ID } from '../common/constants';
59

6-
export function useNewTensorboardExtension(): boolean {
7-
return !!extensions.getExtension('ms-toolsai.tensorboard');
10+
@injectable()
11+
export class TensorboardExperiment {
12+
private readonly _onDidChange = new EventEmitter<void>();
13+
14+
public readonly onDidChange = this._onDidChange.event;
15+
16+
private readonly toDisposeWhenTensobardIsInstalled: IDisposable[] = [];
17+
18+
public static get isTensorboardExtensionInstalled(): boolean {
19+
return !!extensions.getExtension(TENSORBOARD_EXTENSION_ID);
20+
}
21+
22+
private readonly isExperimentEnabled: boolean;
23+
24+
constructor(
25+
@inject(IDisposableRegistry) disposables: IDisposableRegistry,
26+
@inject(IExperimentService) experiments: IExperimentService,
27+
) {
28+
this.isExperimentEnabled = experiments.inExperimentSync(RecommendTensobardExtension.experiment);
29+
disposables.push(this._onDidChange);
30+
extensions.onDidChange(
31+
() =>
32+
TensorboardExperiment.isTensorboardExtensionInstalled
33+
? Disposable.from(...this.toDisposeWhenTensobardIsInstalled).dispose()
34+
: undefined,
35+
this,
36+
disposables,
37+
);
38+
}
39+
40+
public recommendAndUseNewExtension(): 'continueWithPythonExtension' | 'usingTensorboardExtension' {
41+
if (!this.isExperimentEnabled) {
42+
return 'continueWithPythonExtension';
43+
}
44+
if (TensorboardExperiment.isTensorboardExtensionInstalled) {
45+
return 'usingTensorboardExtension';
46+
}
47+
const install = l10n.t('Install Tensorboard Extension');
48+
window
49+
.showInformationMessage(
50+
l10n.t(
51+
'Install the TensorBoard extension to use the this functionality. Once installed, select the command `Launch Tensorboard`.',
52+
),
53+
{ modal: true },
54+
install,
55+
)
56+
.then((result): void => {
57+
if (result === install) {
58+
void commands.executeCommand('workbench.extensions.installExtension', TENSORBOARD_EXTENSION_ID);
59+
}
60+
});
61+
return 'usingTensorboardExtension';
62+
}
63+
64+
public disposeOnInstallingTensorboard(disposabe: IDisposable): void {
65+
this.toDisposeWhenTensobardIsInstalled.push(disposabe);
66+
}
867
}

0 commit comments

Comments
 (0)