From de988ff18185532dfc944ada09fc5d4c94eb8d36 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 19 Dec 2024 11:19:04 +1100 Subject: [PATCH] Remove dead tensorboard code (#24641) Fixes https://github.com/microsoft/vscode-python/issues/23093 --- package.json | 26 +- src/client/common/application/commands.ts | 3 - src/client/common/configSettings.ts | 11 - src/client/common/constants.ts | 2 - src/client/common/experiments/groups.ts | 5 - src/client/common/types.ts | 5 - src/client/common/utils/localize.ts | 2 - src/client/telemetry/constants.ts | 8 - src/client/telemetry/index.ts | 121 +---- src/client/tensorBoard/helpers.ts | 27 - .../nbextensionCodeLensProvider.ts | 83 --- src/client/tensorBoard/serviceRegistry.ts | 28 - .../tensorBoard/tensorBoardFileWatcher.ts | 96 ---- .../tensorBoardImportCodeLensProvider.ts | 76 --- src/client/tensorBoard/tensorBoardPrompt.ts | 71 +-- src/client/tensorBoard/tensorBoardSession.ts | 496 +---------------- .../tensorBoard/tensorBoardSessionProvider.ts | 146 ----- .../tensorBoard/tensorBoardUsageTracker.ts | 72 --- .../tensorBoard/tensorboarExperiment.ts | 67 --- .../tensorboardDependencyChecker.ts | 38 +- .../tensorBoard/tensorboardIntegration.ts | 17 +- src/client/tensorBoard/terminalWatcher.ts | 50 -- src/client/tensorBoard/types.ts | 14 - src/test/tensorBoard/helpers.ts | 18 - .../nbextensionCodeLensProvider.unit.test.ts | 60 --- .../tensorBoardFileWatcher.test.ts | 94 ---- ...orBoardImportCodeLensProvider.unit.test.ts | 72 --- .../tensorBoardPrompt.unit.test.ts | 65 --- .../tensorBoard/tensorBoardSession.test.ts | 510 ------------------ .../tensorBoardUsageTracker.unit.test.ts | 117 ---- 30 files changed, 16 insertions(+), 2384 deletions(-) delete mode 100644 src/client/tensorBoard/nbextensionCodeLensProvider.ts delete mode 100644 src/client/tensorBoard/tensorBoardFileWatcher.ts delete mode 100644 src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts delete mode 100644 src/client/tensorBoard/tensorBoardSessionProvider.ts delete mode 100644 src/client/tensorBoard/tensorBoardUsageTracker.ts delete mode 100644 src/client/tensorBoard/tensorboarExperiment.ts delete mode 100644 src/client/tensorBoard/terminalWatcher.ts delete mode 100644 src/client/tensorBoard/types.ts delete mode 100644 src/test/tensorBoard/helpers.ts delete mode 100644 src/test/tensorBoard/nbextensionCodeLensProvider.unit.test.ts delete mode 100644 src/test/tensorBoard/tensorBoardFileWatcher.test.ts delete mode 100644 src/test/tensorBoard/tensorBoardImportCodeLensProvider.unit.test.ts delete mode 100644 src/test/tensorBoard/tensorBoardPrompt.unit.test.ts delete mode 100644 src/test/tensorBoard/tensorBoardSession.test.ts delete mode 100644 src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts diff --git a/package.json b/package.json index c8fe74cc8255..72e05327d8d4 100644 --- a/package.json +++ b/package.json @@ -332,18 +332,6 @@ "command": "python.execInREPL", "title": "%python.command.python.execInREPL.title%" }, - { - "category": "Python", - "command": "python.launchTensorBoard", - "title": "%python.command.python.launchTensorBoard.title%" - }, - { - "category": "Python", - "command": "python.refreshTensorBoard", - "enablement": "python.hasActiveTensorBoardSession", - "icon": "$(refresh)", - "title": "%python.command.python.refreshTensorBoard.title%" - }, { "category": "Python", "command": "python.reportIssue", @@ -461,8 +449,7 @@ "pythonTerminalEnvVarActivation", "pythonDiscoveryUsingWorkers", "pythonTestAdapter", - "pythonREPLSmartSend", - "pythonRecommendTensorboardExt" + "pythonREPLSmartSend" ], "enumDescriptions": [ "%python.experiments.All.description%", @@ -471,8 +458,7 @@ "%python.experiments.pythonTerminalEnvVarActivation.description%", "%python.experiments.pythonDiscoveryUsingWorkers.description%", "%python.experiments.pythonTestAdapter.description%", - "%python.experiments.pythonREPLSmartSend.description%", - "%python.experiments.pythonRecommendTensorboardExt.description%" + "%python.experiments.pythonREPLSmartSend.description%" ] }, "scope": "window", @@ -604,14 +590,6 @@ "scope": "machine-overridable", "type": "string" }, - "python.tensorBoard.logDirectory": { - "default": "", - "description": "%python.tensorBoard.logDirectory.description%", - "scope": "resource", - "type": "string", - "markdownDeprecationMessage": "%python.tensorBoard.logDirectory.markdownDeprecationMessage%", - "deprecationMessage": "%python.tensorBoard.logDirectory.deprecationMessage%" - }, "python.terminal.activateEnvInCurrentTerminal": { "default": false, "description": "%python.terminal.activateEnvInCurrentTerminal.description%", diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index a145d3410033..2195fe09aabf 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -5,7 +5,6 @@ import { CancellationToken, Position, TextDocument, Uri } from 'vscode'; import { Commands as LSCommands } from '../../activation/commands'; -import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from '../../tensorBoard/constants'; import { Channel, Commands, CommandSource } from '../constants'; import { CreateEnvironmentOptions } from '../../pythonEnvironments/creation/proposed.createEnvApis'; @@ -41,7 +40,6 @@ interface ICommandNameWithoutArgumentTypeMapping { [Commands.ClearStorage]: []; [Commands.CreateNewFile]: []; [Commands.ReportIssue]: []; - [Commands.RefreshTensorBoard]: []; [LSCommands.RestartLS]: []; } @@ -100,7 +98,6 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu [Commands.Debug_In_Terminal]: [Uri]; [Commands.Tests_Configure]: [undefined, undefined | CommandSource, undefined | Uri]; [Commands.Tests_CopilotSetup]: [undefined | Uri]; - [Commands.LaunchTensorBoard]: [TensorBoardEntrypoint, TensorBoardEntrypointTrigger]; ['workbench.view.testing.focus']: []; ['cursorMove']: [ { diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 875e4b1baa6c..58c41587c4f8 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -30,7 +30,6 @@ import { IInterpreterSettings, IPythonSettings, IREPLSettings, - ITensorBoardSettings, ITerminalSettings, Resource, } from './types'; @@ -107,8 +106,6 @@ export class PythonSettings implements IPythonSettings { public autoComplete!: IAutoCompleteSettings; - public tensorBoard: ITensorBoardSettings | undefined; - public testing!: ITestingSettings; public terminal!: ITerminalSettings; @@ -386,14 +383,6 @@ export class PythonSettings implements IPythonSettings { optInto: [], optOutFrom: [], }; - - const tensorBoardSettings = systemVariables.resolveAny( - pythonSettings.get('tensorBoard'), - )!; - this.tensorBoard = tensorBoardSettings || { logDirectory: '' }; - if (this.tensorBoard.logDirectory) { - this.tensorBoard.logDirectory = getAbsolutePath(this.tensorBoard.logDirectory, workspaceRoot); - } } // eslint-disable-next-line class-methods-use-this diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index 11729e460efa..5ffa775bf04a 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -55,9 +55,7 @@ export namespace Commands { export const InstallPython = 'python.installPython'; export const InstallPythonOnLinux = 'python.installPythonOnLinux'; export const InstallPythonOnMac = 'python.installPythonOnMac'; - export const LaunchTensorBoard = 'python.launchTensorBoard'; export const PickLocalProcess = 'python.pickLocalProcess'; - export const RefreshTensorBoard = 'python.refreshTensorBoard'; export const ReportIssue = 'python.reportIssue'; export const Set_Interpreter = 'python.setInterpreter'; export const Set_ShebangInterpreter = 'python.setShebangInterpreter'; diff --git a/src/client/common/experiments/groups.ts b/src/client/common/experiments/groups.ts index d43f376ddc87..12f4ef89018b 100644 --- a/src/client/common/experiments/groups.ts +++ b/src/client/common/experiments/groups.ts @@ -19,8 +19,3 @@ export enum DiscoveryUsingWorkers { export enum EnableTestAdapterRewrite { experiment = 'pythonTestAdapter', } - -// Experiment to recommend installing the tensorboard extension. -export enum RecommendTensobardExtension { - experiment = 'pythonRecommendTensorboardExt', -} diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 71813c71904e..cec297f8329a 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -177,15 +177,10 @@ export interface IPythonSettings { readonly languageServer: LanguageServerType; readonly languageServerIsDefault: boolean; readonly defaultInterpreterPath: string; - readonly tensorBoard: ITensorBoardSettings | undefined; readonly REPL: IREPLSettings; register(): void; } -export interface ITensorBoardSettings { - logDirectory: string | undefined; -} - export interface IInterpreterSettings { infoVisibility: 'never' | 'onPythonRelated' | 'always'; } diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index f670fe493a1b..1e5d28d778dc 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -138,8 +138,6 @@ export namespace TensorBoard { export const upgradePrompt = l10n.t( 'Integrated TensorBoard support is only available for TensorBoard >= 2.4.1. Would you like to upgrade your copy of TensorBoard?', ); - export const launchNativeTensorBoardSessionCodeLens = l10n.t('▶ Launch TensorBoard Session'); - export const launchNativeTensorBoardSessionCodeAction = l10n.t('Launch TensorBoard session'); export const missingSourceFile = l10n.t( 'The Python extension could not locate the requested source file on disk. Please manually specify the file.', ); diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index b5da8fcc96b7..53420c275e8a 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -90,19 +90,11 @@ export enum EventName { JEDI_LANGUAGE_SERVER_READY = 'JEDI_LANGUAGE_SERVER.READY', JEDI_LANGUAGE_SERVER_REQUEST = 'JEDI_LANGUAGE_SERVER.REQUEST', - TENSORBOARD_SESSION_LAUNCH = 'TENSORBOARD.SESSION_LAUNCH', - TENSORBOARD_SESSION_DURATION = 'TENSORBOARD.SESSION_DURATION', - TENSORBOARD_SESSION_DAEMON_STARTUP_DURATION = 'TENSORBOARD.SESSION_DAEMON_STARTUP_DURATION', - TENSORBOARD_LAUNCH_PROMPT_SELECTION = 'TENSORBOARD.LAUNCH_PROMPT_SELECTION', - TENSORBOARD_SESSION_E2E_STARTUP_DURATION = 'TENSORBOARD.SESSION_E2E_STARTUP_DURATION', - TENSORBOARD_ENTRYPOINT_SHOWN = 'TENSORBOARD.ENTRYPOINT_SHOWN', TENSORBOARD_INSTALL_PROMPT_SHOWN = 'TENSORBOARD.INSTALL_PROMPT_SHOWN', TENSORBOARD_INSTALL_PROMPT_SELECTION = 'TENSORBOARD.INSTALL_PROMPT_SELECTION', TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL = 'TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL', TENSORBOARD_PACKAGE_INSTALL_RESULT = 'TENSORBOARD.PACKAGE_INSTALL_RESULT', TENSORBOARD_TORCH_PROFILER_IMPORT = 'TENSORBOARD.TORCH_PROFILER_IMPORT', - TENSORBOARD_JUMP_TO_SOURCE_REQUEST = 'TENSORBOARD_JUMP_TO_SOURCE_REQUEST', - TENSORBOARD_JUMP_TO_SOURCE_FILE_NOT_FOUND = 'TENSORBOARD_JUMP_TO_SOURCE_FILE_NOT_FOUND', ENVIRONMENT_CREATING = 'ENVIRONMENT.CREATING', ENVIRONMENT_CREATED = 'ENVIRONMENT.CREATED', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index ba7e60bd6763..37ae9328c546 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -11,12 +11,7 @@ import { isPromise } from '../common/utils/async'; import { StopWatch } from '../common/utils/stopWatch'; import { ConsoleType, TriggerType } from '../debugger/types'; import { EnvironmentType, PythonEnvironment } from '../pythonEnvironments/info'; -import { - TensorBoardEntrypoint, - TensorBoardEntrypointTrigger, - TensorBoardPromptSelection, - TensorBoardSessionStartResult, -} from '../tensorBoard/constants'; +import { TensorBoardPromptSelection } from '../tensorBoard/constants'; import { EventName } from './constants'; import type { TestTool } from './types'; @@ -2577,101 +2572,6 @@ export interface IEventNamePropertyMapping { }; // TensorBoard integration events - /** - * Telemetry event sent after the user has clicked on an option in the prompt we display - * asking them if they want to launch an integrated TensorBoard session. - * `selection` is one of 'yes', 'no', or 'do not ask again'. - */ - /* __GDPR__ - "tensorboard.launch_prompt_selection" : { "owner": "donjayamanne" } - */ - - [EventName.TENSORBOARD_LAUNCH_PROMPT_SELECTION]: { - selection: TensorBoardPromptSelection; - }; - /** - * Telemetry event sent after the python.launchTensorBoard command has been executed. - * The `entrypoint` property indicates whether the command was executed directly by the - * user from the command palette or from a codelens or the user clicking 'yes' - * on the launch prompt we display. - * The `trigger` property indicates whether the entrypoint was triggered by the user - * importing tensorboard, using tensorboard in a notebook, detected tfevent files in - * the workspace. For the palette entrypoint, the trigger is also 'palette'. - */ - /* __GDPR__ - "tensorboard.session_launch" : { - "entrypoint" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, - "trigger": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" } - } - */ - [EventName.TENSORBOARD_SESSION_LAUNCH]: { - entrypoint: TensorBoardEntrypoint; - trigger: TensorBoardEntrypointTrigger; - }; - /** - * Telemetry event sent after we have attempted to create a tensorboard program instance - * by spawning a daemon to run the tensorboard_launcher.py script. The event is sent with - * `duration` which should never exceed 60_000ms. Depending on the value of `result`, `duration` means: - * 1. 'success' --> the total amount of time taken for the execObservable daemon to report successful TB session launch - * 2. 'canceled' --> the total amount of time that the user waited for the daemon to start before canceling launch - * 3. 'error' --> 60_000ms, i.e. we timed out waiting for the daemon to launch - * In the first two cases, `duration` should not be more than 60_000ms. - */ - /* __GDPR__ - "tensorboard.session_daemon_startup_duration" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, - "result" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" } - } - */ - [EventName.TENSORBOARD_SESSION_DAEMON_STARTUP_DURATION]: { - result: TensorBoardSessionStartResult; - }; - /** - * Telemetry event sent after the webview framing the TensorBoard website has been successfully shown. - * This event is sent with `duration` which represents the total time to create a TensorBoardSession. - * Note that this event is only sent if an integrated TensorBoard session is successfully created in full. - * This includes checking whether the tensorboard package is installed and installing it if it's not already - * installed, requesting the user to select a log directory, starting the tensorboard - * program instance in a daemon, and showing the TensorBoard UI in a webpanel, in that order. - */ - /* __GDPR__ - "tensorboard.session_e2e_startup_duration" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" } - } - */ - [EventName.TENSORBOARD_SESSION_E2E_STARTUP_DURATION]: never | undefined; - /** - * Telemetry event sent after the user has closed a TensorBoard webview panel. This event is - * sent with `duration` specifying the total duration of time that the TensorBoard session - * ran for before the user terminated the session. - */ - /* __GDPR__ - "tensorboard.session_duration" : { - "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" } - } - */ - [EventName.TENSORBOARD_SESSION_DURATION]: never | undefined; - /** - * Telemetry event sent when an entrypoint is displayed to the user. This event is sent once - * per entrypoint per session to minimize redundant events since codelenses - * can be displayed multiple times per file. - * The `entrypoint` property indicates whether the command was executed directly by the - * user from the command palette or from a codelens or the user clicking 'yes' - * on the launch prompt we display. - * The `trigger` property indicates whether the entrypoint was triggered by the user - * importing tensorboard, using tensorboard in a notebook, detected tfevent files in - * the workspace. For the palette entrypoint, the trigger is also 'palette'. - */ - /* __GDPR__ - "tensorboard.entrypoint_shown" : { - "entrypoint" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, - "trigger": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" } - } - */ - [EventName.TENSORBOARD_ENTRYPOINT_SHOWN]: { - entrypoint: TensorBoardEntrypoint; - trigger: TensorBoardEntrypointTrigger; - }; /** * Telemetry event sent when the user is prompted to install Python packages that are * dependencies for launching an integrated TensorBoard session. @@ -2732,25 +2632,6 @@ export interface IEventNamePropertyMapping { "tensorboard.torch_profiler_import" : { "owner": "donjayamanne" } */ [EventName.TENSORBOARD_TORCH_PROFILER_IMPORT]: never | undefined; - /** - * Telemetry event sent when the extension host receives a message from the - * TensorBoard webview containing a valid jump to source payload from the - * PyTorch profiler TensorBoard plugin. - */ - /* __GDPR__ - "tensorboard_jump_to_source_request" : { "owner": "donjayamanne" } - */ - [EventName.TENSORBOARD_JUMP_TO_SOURCE_REQUEST]: never | undefined; - /** - * Telemetry event sent when the extension host receives a message from the - * TensorBoard webview containing a valid jump to source payload from the - * PyTorch profiler TensorBoard plugin, but the source file does not exist - * on the machine currently running TensorBoard. - */ - /* __GDPR__ - "tensorboard_jump_to_source_file_not_found" : { "owner": "donjayamanne" } - */ - [EventName.TENSORBOARD_JUMP_TO_SOURCE_FILE_NOT_FOUND]: never | undefined; [EventName.TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL]: never | undefined; /** * Telemetry event sent before creating an environment. diff --git a/src/client/tensorBoard/helpers.ts b/src/client/tensorBoard/helpers.ts index 3efb6aca04f9..8da3ef6a38f2 100644 --- a/src/client/tensorBoard/helpers.ts +++ b/src/client/tensorBoard/helpers.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { noop } from '../common/utils/misc'; - // While it is uncommon for users to `import tensorboard`, TensorBoard is frequently // included as a submodule of other packages, e.g. torch.utils.tensorboard. // This is a modified version of the regex from src/client/telemetry/importTracker.ts @@ -11,28 +9,3 @@ import { noop } from '../common/utils/misc'; // RegEx to match `import torch.profiler` or `from torch import profiler` export const TorchProfilerImportRegEx = /^\s*(?:import (?:(\w+, )*torch\.profiler(, \w+)*))|(?:from torch import (?:(\w+, )*profiler(, \w+)*))/; -// RegEx to match `from torch.utils import tensorboard`, `import torch.utils.tensorboard`, `import tensorboardX`, `import tensorboard` -const TensorBoardImportRegEx = /^\s*(?:from torch\.utils\.tensorboard import \w+)|(?:from torch\.utils import (?:(\w+, )*tensorboard(, \w+)*))|(?:from tensorboardX import \w+)|(?:import (\w+, )*((torch\.utils\.tensorboard)|(tensorboardX)|(tensorboard))(, \w+)*)/; - -export function containsTensorBoardImport(lines: (string | undefined)[]): boolean { - try { - for (const s of lines) { - if (s && (TensorBoardImportRegEx.test(s) || TorchProfilerImportRegEx.test(s))) { - return true; - } - } - } catch { - // Don't care about failures. - noop(); - } - return false; -} - -export function containsNotebookExtension(lines: (string | undefined)[]): boolean { - for (const s of lines) { - if (s?.startsWith('%tensorboard') || s?.startsWith('%load_ext tensorboard')) { - return true; - } - } - return false; -} diff --git a/src/client/tensorBoard/nbextensionCodeLensProvider.ts b/src/client/tensorBoard/nbextensionCodeLensProvider.ts deleted file mode 100644 index afaaf116851a..000000000000 --- a/src/client/tensorBoard/nbextensionCodeLensProvider.ts +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { once } from 'lodash'; -import { CancellationToken, CodeLens, Command, Disposable, languages, Position, Range, TextDocument } from 'vscode'; -import { IExtensionSingleActivationService } from '../activation/types'; -import { Commands, NotebookCellScheme, PYTHON_LANGUAGE } from '../common/constants'; -import { IDisposable, IDisposableRegistry } from '../common/types'; -import { TensorBoard } from '../common/utils/localize'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; -import { containsNotebookExtension } from './helpers'; -import { TensorboardExperiment } from './tensorboarExperiment'; - -@injectable() -export class TensorBoardNbextensionCodeLensProvider implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - private readonly disposables: IDisposable[] = []; - - private sendTelemetryOnce = once( - sendTelemetryEvent.bind(this, EventName.TENSORBOARD_ENTRYPOINT_SHOWN, undefined, { - trigger: TensorBoardEntrypointTrigger.nbextension, - entrypoint: TensorBoardEntrypoint.codelens, - }), - ); - - constructor( - @inject(IDisposableRegistry) disposables: IDisposableRegistry, - @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, - ) { - disposables.push(this); - } - - public dispose(): void { - Disposable.from(...this.disposables).dispose(); - } - - public async activate(): Promise { - if (TensorboardExperiment.isTensorboardExtensionInstalled) { - return; - } - this.experiment.disposeOnInstallingTensorboard(this); - this.activateInternal().ignoreErrors(); - } - - private async activateInternal() { - this.disposables.push( - languages.registerCodeLensProvider( - [ - { scheme: NotebookCellScheme, language: PYTHON_LANGUAGE }, - { scheme: 'vscode-notebook', language: PYTHON_LANGUAGE }, - ], - this, - ), - ); - } - - public provideCodeLenses(document: TextDocument, cancelToken: CancellationToken): CodeLens[] { - const command: Command = { - title: TensorBoard.launchNativeTensorBoardSessionCodeLens, - command: Commands.LaunchTensorBoard, - arguments: [ - { trigger: TensorBoardEntrypointTrigger.nbextension, entrypoint: TensorBoardEntrypoint.codelens }, - ], - }; - const codelenses: CodeLens[] = []; - for (let index = 0; index < document.lineCount; index += 1) { - if (cancelToken.isCancellationRequested) { - return codelenses; - } - const line = document.lineAt(index); - if (containsNotebookExtension([line.text])) { - const range = new Range(new Position(line.lineNumber, 0), new Position(line.lineNumber, 1)); - codelenses.push(new CodeLens(range, command)); - this.sendTelemetryOnce(); - } - } - return codelenses; - } -} diff --git a/src/client/tensorBoard/serviceRegistry.ts b/src/client/tensorBoard/serviceRegistry.ts index 5fedb7b6abf5..9f53af72053e 100644 --- a/src/client/tensorBoard/serviceRegistry.ts +++ b/src/client/tensorBoard/serviceRegistry.ts @@ -1,39 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; -import { TensorBoardImportCodeLensProvider } from './tensorBoardImportCodeLensProvider'; -import { TensorBoardFileWatcher } from './tensorBoardFileWatcher'; -import { TensorBoardUsageTracker } from './tensorBoardUsageTracker'; import { TensorBoardPrompt } from './tensorBoardPrompt'; -import { TensorBoardSessionProvider } from './tensorBoardSessionProvider'; -import { TensorBoardNbextensionCodeLensProvider } from './nbextensionCodeLensProvider'; -import { TerminalWatcher } from './terminalWatcher'; import { TensorboardDependencyChecker } from './tensorboardDependencyChecker'; -import { TensorboardExperiment } from './tensorboarExperiment'; export function registerTypes(serviceManager: IServiceManager): void { - serviceManager.addSingleton(TensorBoardSessionProvider, TensorBoardSessionProvider); - serviceManager.addBinding(TensorBoardSessionProvider, IExtensionSingleActivationService); - serviceManager.addSingleton(TensorBoardFileWatcher, TensorBoardFileWatcher); - serviceManager.addBinding(TensorBoardFileWatcher, IExtensionSingleActivationService); serviceManager.addSingleton(TensorBoardPrompt, TensorBoardPrompt); - serviceManager.addSingleton( - IExtensionSingleActivationService, - TensorBoardUsageTracker, - ); - serviceManager.addSingleton( - TensorBoardImportCodeLensProvider, - TensorBoardImportCodeLensProvider, - ); - serviceManager.addBinding(TensorBoardImportCodeLensProvider, IExtensionSingleActivationService); - serviceManager.addSingleton( - TensorBoardNbextensionCodeLensProvider, - TensorBoardNbextensionCodeLensProvider, - ); - serviceManager.addBinding(TensorBoardNbextensionCodeLensProvider, IExtensionSingleActivationService); - serviceManager.addSingleton(IExtensionSingleActivationService, TerminalWatcher); serviceManager.addSingleton(TensorboardDependencyChecker, TensorboardDependencyChecker); - serviceManager.addSingleton(TensorboardExperiment, TensorboardExperiment); } diff --git a/src/client/tensorBoard/tensorBoardFileWatcher.ts b/src/client/tensorBoard/tensorBoardFileWatcher.ts deleted file mode 100644 index f2f9344d7365..000000000000 --- a/src/client/tensorBoard/tensorBoardFileWatcher.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { Disposable, FileSystemWatcher, RelativePattern, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; -import { IExtensionSingleActivationService } from '../activation/types'; -import { IWorkspaceService } from '../common/application/types'; -import { IDisposable, IDisposableRegistry } from '../common/types'; -import { TensorBoardEntrypointTrigger } from './constants'; -import { TensorBoardPrompt } from './tensorBoardPrompt'; -import { TensorboardExperiment } from './tensorboarExperiment'; - -@injectable() -export class TensorBoardFileWatcher implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - private fileSystemWatchers = new Map(); - - private globPatterns = ['*tfevents*', '*/*tfevents*', '*/*/*tfevents*']; - - private readonly disposables: IDisposable[] = []; - - constructor( - @inject(IWorkspaceService) private workspaceService: IWorkspaceService, - @inject(TensorBoardPrompt) private tensorBoardPrompt: TensorBoardPrompt, - @inject(IDisposableRegistry) disposables: IDisposableRegistry, - @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, - ) { - disposables.push(this); - } - - public dispose(): void { - Disposable.from(...this.disposables).dispose(); - } - - public async activate(): Promise { - if (TensorboardExperiment.isTensorboardExtensionInstalled) { - return; - } - this.experiment.disposeOnInstallingTensorboard(this); - this.activateInternal().ignoreErrors(); - } - - private async activateInternal() { - const folders = this.workspaceService.workspaceFolders; - if (!folders) { - return; - } - - // If the user creates or changes tfevent files, listen for those too - for (const folder of folders) { - this.createFileSystemWatcher(folder); - } - - // If workspace folders change, ensure we update our FileSystemWatchers - this.disposables.push( - this.workspaceService.onDidChangeWorkspaceFolders((e) => this.updateFileSystemWatchers(e)), - ); - } - - private async updateFileSystemWatchers(event: WorkspaceFoldersChangeEvent) { - for (const added of event.added) { - this.createFileSystemWatcher(added); - } - for (const removed of event.removed) { - const fileSystemWatchers = this.fileSystemWatchers.get(removed); - if (fileSystemWatchers) { - fileSystemWatchers.forEach((fileWatcher) => fileWatcher.dispose()); - this.fileSystemWatchers.delete(removed); - } - } - } - - private createFileSystemWatcher(folder: WorkspaceFolder) { - const fileWatchers = []; - for (const pattern of this.globPatterns) { - const relativePattern = new RelativePattern(folder, pattern); - const fileSystemWatcher = this.workspaceService.createFileSystemWatcher(relativePattern); - - // When a file is created or changed that matches `this.globPattern`, try to show our prompt - this.disposables.push( - fileSystemWatcher.onDidCreate(() => - this.tensorBoardPrompt.showNativeTensorBoardPrompt(TensorBoardEntrypointTrigger.tfeventfiles), - ), - ); - this.disposables.push( - fileSystemWatcher.onDidChange(() => - this.tensorBoardPrompt.showNativeTensorBoardPrompt(TensorBoardEntrypointTrigger.tfeventfiles), - ), - ); - this.disposables.push(fileSystemWatcher); - fileWatchers.push(fileSystemWatcher); - } - this.fileSystemWatchers.set(folder, fileWatchers); - } -} diff --git a/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts b/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts deleted file mode 100644 index 585b9151922a..000000000000 --- a/src/client/tensorBoard/tensorBoardImportCodeLensProvider.ts +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { once } from 'lodash'; -import { CancellationToken, CodeLens, Command, Disposable, languages, Position, Range, TextDocument } from 'vscode'; -import { IExtensionSingleActivationService } from '../activation/types'; -import { Commands, PYTHON } from '../common/constants'; -import { IDisposable, IDisposableRegistry } from '../common/types'; -import { TensorBoard } from '../common/utils/localize'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; -import { containsTensorBoardImport } from './helpers'; -import { TensorboardExperiment } from './tensorboarExperiment'; - -@injectable() -export class TensorBoardImportCodeLensProvider implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - private sendTelemetryOnce = once( - sendTelemetryEvent.bind(this, EventName.TENSORBOARD_ENTRYPOINT_SHOWN, undefined, { - trigger: TensorBoardEntrypointTrigger.fileimport, - entrypoint: TensorBoardEntrypoint.codelens, - }), - ); - - private readonly disposables: IDisposable[] = []; - - constructor( - @inject(IDisposableRegistry) disposables: IDisposableRegistry, - @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, - ) { - disposables.push(this); - } - - public dispose(): void { - Disposable.from(...this.disposables).dispose(); - } - - public async activate(): Promise { - if (TensorboardExperiment.isTensorboardExtensionInstalled) { - return; - } - this.experiment.disposeOnInstallingTensorboard(this); - this.activateInternal().ignoreErrors(); - } - - // eslint-disable-next-line class-methods-use-this - public provideCodeLenses(document: TextDocument, cancelToken: CancellationToken): CodeLens[] { - const command: Command = { - title: TensorBoard.launchNativeTensorBoardSessionCodeLens, - command: Commands.LaunchTensorBoard, - arguments: [ - { trigger: TensorBoardEntrypointTrigger.fileimport, entrypoint: TensorBoardEntrypoint.codelens }, - ], - }; - const codelenses: CodeLens[] = []; - for (let index = 0; index < document.lineCount; index += 1) { - if (cancelToken.isCancellationRequested) { - return codelenses; - } - const line = document.lineAt(index); - if (containsTensorBoardImport([line.text])) { - const range = new Range(new Position(line.lineNumber, 0), new Position(line.lineNumber, 1)); - codelenses.push(new CodeLens(range, command)); - this.sendTelemetryOnce(); - } - } - return codelenses; - } - - private async activateInternal() { - this.disposables.push(languages.registerCodeLensProvider(PYTHON, this)); - } -} diff --git a/src/client/tensorBoard/tensorBoardPrompt.ts b/src/client/tensorBoard/tensorBoardPrompt.ts index d42101cb51d6..563419bd4ea6 100644 --- a/src/client/tensorBoard/tensorBoardPrompt.ts +++ b/src/client/tensorBoard/tensorBoardPrompt.ts @@ -2,14 +2,7 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { once } from 'lodash'; -import { IApplicationShell, ICommandManager } from '../common/application/types'; -import { Commands } from '../common/constants'; import { IPersistentState, IPersistentStateFactory } from '../common/types'; -import { Common, TensorBoard } from '../common/utils/localize'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger, TensorBoardPromptSelection } from './constants'; enum TensorBoardPromptStateKeys { ShowNativeTensorBoardPrompt = 'showNativeTensorBoardPrompt', @@ -19,76 +12,14 @@ enum TensorBoardPromptStateKeys { export class TensorBoardPrompt { private state: IPersistentState; - private enabled: boolean; - - private enabledInCurrentSession = true; - - private waitingForUserSelection = false; - - private sendTelemetryOnce = once((trigger) => { - sendTelemetryEvent(EventName.TENSORBOARD_ENTRYPOINT_SHOWN, undefined, { - entrypoint: TensorBoardEntrypoint.prompt, - trigger, - }); - }); - - constructor( - @inject(IApplicationShell) private applicationShell: IApplicationShell, - @inject(ICommandManager) private commandManager: ICommandManager, - @inject(IPersistentStateFactory) private persistentStateFactory: IPersistentStateFactory, - ) { + constructor(@inject(IPersistentStateFactory) private persistentStateFactory: IPersistentStateFactory) { this.state = this.persistentStateFactory.createWorkspacePersistentState( TensorBoardPromptStateKeys.ShowNativeTensorBoardPrompt, true, ); - this.enabled = this.isPromptEnabled(); - } - - public async showNativeTensorBoardPrompt(trigger: TensorBoardEntrypointTrigger): Promise { - if (this.enabled && this.enabledInCurrentSession && !this.waitingForUserSelection) { - const yes = Common.bannerLabelYes; - const no = Common.bannerLabelNo; - const doNotAskAgain = Common.doNotShowAgain; - const options = [yes, no, doNotAskAgain]; - this.waitingForUserSelection = true; - this.sendTelemetryOnce(trigger); - const selection = await this.applicationShell.showInformationMessage( - TensorBoard.nativeTensorBoardPrompt, - ...options, - ); - this.waitingForUserSelection = false; - this.enabledInCurrentSession = false; - let telemetrySelection = TensorBoardPromptSelection.None; - switch (selection) { - case yes: - telemetrySelection = TensorBoardPromptSelection.Yes; - await this.commandManager.executeCommand( - Commands.LaunchTensorBoard, - TensorBoardEntrypoint.prompt, - trigger, - ); - break; - case doNotAskAgain: - telemetrySelection = TensorBoardPromptSelection.DoNotAskAgain; - await this.disablePrompt(); - break; - case no: - telemetrySelection = TensorBoardPromptSelection.No; - break; - default: - break; - } - sendTelemetryEvent(EventName.TENSORBOARD_LAUNCH_PROMPT_SELECTION, undefined, { - selection: telemetrySelection, - }); - } } public isPromptEnabled(): boolean { return this.state.value; } - - private async disablePrompt() { - await this.state.updateValue(false); - } } diff --git a/src/client/tensorBoard/tensorBoardSession.ts b/src/client/tensorBoard/tensorBoardSession.ts index 13e5e66e0a30..b18202810e45 100644 --- a/src/client/tensorBoard/tensorBoardSession.ts +++ b/src/client/tensorBoard/tensorBoardSession.ts @@ -1,57 +1,18 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { ChildProcess } from 'child_process'; -import * as path from 'path'; -import { - CancellationToken, - CancellationTokenSource, - env, - Event, - EventEmitter, - l10n, - Position, - Progress, - ProgressLocation, - ProgressOptions, - QuickPickItem, - Selection, - TextEditorRevealType, - Uri, - ViewColumn, - WebviewPanel, - WebviewPanelOnDidChangeViewStateEvent, - window, - workspace, -} from 'vscode'; -import * as fs from '../common/platform/fs-paths'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; +import { CancellationTokenSource, Uri } from 'vscode'; +import { IApplicationShell, ICommandManager } from '../common/application/types'; import { createPromiseFromCancellation } from '../common/cancellation'; -import { tensorboardLauncher } from '../common/process/internal/scripts'; -import { IPythonExecutionFactory, ObservableExecutionResult } from '../common/process/types'; -import { - IDisposableRegistry, - IInstaller, - InstallerResponse, - ProductInstallStatus, - Product, - IPersistentState, - IConfigurationService, -} from '../common/types'; -import { createDeferred, sleep } from '../common/utils/async'; +import { IInstaller, InstallerResponse, ProductInstallStatus, Product } from '../common/types'; import { Common, TensorBoard } from '../common/utils/localize'; -import { StopWatch } from '../common/utils/stopWatch'; import { IInterpreterService } from '../interpreter/contracts'; import { sendTelemetryEvent } from '../telemetry'; import { EventName } from '../telemetry/constants'; import { ImportTracker } from '../telemetry/importTracker'; -import { TensorBoardPromptSelection, TensorBoardSessionStartResult } from './constants'; -import { IMultiStepInputFactory } from '../common/utils/multiStepInput'; +import { TensorBoardPromptSelection } from './constants'; import { ModuleInstallFlags } from '../common/installer/types'; import { traceError, traceVerbose } from '../logging'; -enum Messages { - JumpToSource = 'jump_to_source', -} const TensorBoardSemVerRequirement = '>= 2.4.1'; const TorchProfilerSemVerRequirement = '>= 0.2.0'; @@ -66,87 +27,12 @@ const TorchProfilerSemVerRequirement = '>= 0.2.0'; * - shuts down the TensorBoard process when the webview is closed */ export class TensorBoardSession { - public get panel(): WebviewPanel | undefined { - return this.webviewPanel; - } - - public get daemon(): ChildProcess | undefined { - return this.process; - } - - private _active = false; - - private webviewPanel: WebviewPanel | undefined; - - private url: string | undefined; - - private process: ChildProcess | undefined; - - private onDidChangeViewStateEventEmitter = new EventEmitter(); - - private onDidDisposeEventEmitter = new EventEmitter(); - - // This tracks the total duration of time that the user kept the TensorBoard panel open - private sessionDurationStopwatch: StopWatch | undefined; - constructor( private readonly installer: IInstaller, private readonly interpreterService: IInterpreterService, - private readonly workspaceService: IWorkspaceService, - private readonly pythonExecFactory: IPythonExecutionFactory, private readonly commandManager: ICommandManager, - private readonly disposables: IDisposableRegistry, private readonly applicationShell: IApplicationShell, - private readonly globalMemento: IPersistentState, - private readonly multiStepFactory: IMultiStepInputFactory, - private readonly configurationService: IConfigurationService, - ) { - this.disposables.push(this.onDidChangeViewStateEventEmitter); - this.disposables.push(this.onDidDisposeEventEmitter); - } - - public get onDidDispose(): Event { - return this.onDidDisposeEventEmitter.event; - } - - public get onDidChangeViewState(): Event { - return this.onDidChangeViewStateEventEmitter.event; - } - - public get active(): boolean { - return this._active; - } - - public async refresh(): Promise { - if (!this.webviewPanel) { - return; - } - this.webviewPanel.webview.html = ''; - this.webviewPanel.webview.html = await this.getHtml(); - } - - public async initialize(): Promise { - const e2eStartupDurationStopwatch = new StopWatch(); - const tensorBoardWasInstalled = await this.ensurePrerequisitesAreInstalled(); - if (!tensorBoardWasInstalled) { - return; - } - const logDir = await this.getLogDirectory(); - if (!logDir) { - return; - } - const startedSuccessfully = await this.startTensorboardSession(logDir); - if (startedSuccessfully) { - await this.showPanel(); - // Not using captureTelemetry on this method as we only want to send - // this particular telemetry event if the whole session creation succeeded - sendTelemetryEvent( - EventName.TENSORBOARD_SESSION_E2E_STARTUP_DURATION, - e2eStartupDurationStopwatch.elapsedTime, - ); - } - this.sessionDurationStopwatch = new StopWatch(); - } + ) {} private async promptToInstall( tensorBoardInstallStatus: ProductInstallStatus, @@ -294,376 +180,4 @@ export class TensorBoardSession { } return tensorboardInstallStatus === ProductInstallStatus.Installed; } - - private async showFilePicker(): Promise { - const selection = await this.applicationShell.showOpenDialog({ - canSelectFiles: false, - canSelectFolders: true, - canSelectMany: false, - }); - // If the user selected a folder, return the uri.fsPath - // There will only be one selection since canSelectMany: false - if (selection) { - return selection[0].fsPath; - } - return undefined; - } - - // eslint-disable-next-line class-methods-use-this - private getQuickPickItems(logDir: string | undefined) { - const items = []; - - if (logDir) { - const useCwd = { - label: TensorBoard.useCurrentWorkingDirectory, - detail: TensorBoard.useCurrentWorkingDirectoryDetail, - }; - const selectAnotherFolder = { - label: TensorBoard.selectAnotherFolder, - detail: TensorBoard.selectAnotherFolderDetail, - }; - items.push(useCwd, selectAnotherFolder); - } else { - const selectAFolder = { - label: TensorBoard.selectAFolder, - detail: TensorBoard.selectAFolderDetail, - }; - items.push(selectAFolder); - } - - items.push({ - label: TensorBoard.enterRemoteUrl, - detail: TensorBoard.enterRemoteUrlDetail, - }); - - return items; - } - - // Display a quickpick asking the user to acknowledge our autopopulated log directory or - // select a new one using the file picker. Default this to the folder that is open in - // the editor, if any, then the directory that the active text editor is in, if any. - private async getLogDirectory(): Promise { - // See if the user told us to always use a specific log directory - const settings = this.configurationService.getSettings(); - const settingValue = settings.tensorBoard?.logDirectory; - if (settingValue) { - traceVerbose(`Using log directory resolved by python.tensorBoard.logDirectory setting: ${settingValue}`); - return settingValue; - } - // No log directory in settings. Ask the user which directory to use - const logDir = this.autopopulateLogDirectoryPath(); - const { useCurrentWorkingDirectory } = TensorBoard; - const { selectAFolder } = TensorBoard; - const { selectAnotherFolder } = TensorBoard; - const { enterRemoteUrl } = TensorBoard; - const items: QuickPickItem[] = this.getQuickPickItems(logDir); - const item = await this.applicationShell.showQuickPick(items, { - canPickMany: false, - ignoreFocusOut: false, - placeHolder: logDir ? l10n.t('Current: {0}', logDir) : undefined, - }); - switch (item?.label) { - case useCurrentWorkingDirectory: - return logDir; - case selectAFolder: - case selectAnotherFolder: - return this.showFilePicker(); - case enterRemoteUrl: - return this.applicationShell.showInputBox({ - prompt: TensorBoard.enterRemoteUrlDetail, - }); - default: - return undefined; - } - } - - // Spawn a process which uses TensorBoard's Python API to start a TensorBoard session. - // Times out if it hasn't started up after 1 minute. - // Hold on to the process so we can kill it when the webview is closed. - private async startTensorboardSession(logDir: string): Promise { - const interpreter = await this.interpreterService.getActiveInterpreter(); - if (!interpreter) { - return false; - } - - // Timeout waiting for TensorBoard to start after 60 seconds. - // This is the same time limit that TensorBoard itself uses when waiting for - // its webserver to start up. - const timeout = 60_000; - - // Display a progress indicator as TensorBoard takes at least a couple seconds to launch - const progressOptions: ProgressOptions = { - title: TensorBoard.progressMessage, - location: ProgressLocation.Notification, - cancellable: true, - }; - - const processService = await this.pythonExecFactory.createActivatedEnvironment({ - allowEnvironmentFetchExceptions: true, - interpreter, - }); - const args = tensorboardLauncher([logDir]); - const sessionStartStopwatch = new StopWatch(); - const observable = processService.execObservable(args, {}); - - const result = await this.applicationShell.withProgress( - progressOptions, - (_progress: Progress, token: CancellationToken) => { - traceVerbose(`Starting TensorBoard with log directory ${logDir}...`); - - const spawnTensorBoard = this.waitForTensorBoardStart(observable); - const userCancellation = createPromiseFromCancellation({ - token, - cancelAction: 'resolve', - defaultValue: 'canceled', - }); - - return Promise.race([sleep(timeout), spawnTensorBoard, userCancellation]); - }, - ); - - switch (result) { - case 'canceled': - traceVerbose('Canceled starting TensorBoard session.'); - sendTelemetryEvent( - EventName.TENSORBOARD_SESSION_DAEMON_STARTUP_DURATION, - sessionStartStopwatch.elapsedTime, - { - result: TensorBoardSessionStartResult.cancel, - }, - ); - observable.dispose(); - return false; - case 'success': - this.process = observable.proc; - sendTelemetryEvent( - EventName.TENSORBOARD_SESSION_DAEMON_STARTUP_DURATION, - sessionStartStopwatch.elapsedTime, - { - result: TensorBoardSessionStartResult.success, - }, - ); - return true; - case timeout: - sendTelemetryEvent( - EventName.TENSORBOARD_SESSION_DAEMON_STARTUP_DURATION, - sessionStartStopwatch.elapsedTime, - { - result: TensorBoardSessionStartResult.error, - }, - ); - throw new Error(`Timed out after ${timeout / 1000} seconds waiting for TensorBoard to launch.`); - default: - // We should never get here - throw new Error(`Failed to start TensorBoard, received unknown promise result: ${result}`); - } - } - - private async waitForTensorBoardStart(observable: ObservableExecutionResult) { - const urlThatTensorBoardIsRunningAt = createDeferred(); - - observable.out.subscribe({ - next: (output) => { - if (output.source === 'stdout') { - const match = output.out.match(/TensorBoard started at (.*)/); - if (match && match[1]) { - // eslint-disable-next-line prefer-destructuring - this.url = match[1]; - urlThatTensorBoardIsRunningAt.resolve('success'); - } - traceVerbose(output.out); - } else if (output.source === 'stderr') { - traceError(output.out); - } - }, - error: (err) => { - traceError(err); - }, - }); - - return urlThatTensorBoardIsRunningAt.promise; - } - - private async showPanel() { - traceVerbose('Showing TensorBoard panel'); - const panel = this.webviewPanel || (await this.createPanel()); - panel.reveal(); - this._active = true; - this.onDidChangeViewStateEventEmitter.fire(); - } - - private async createPanel() { - const webviewPanel = window.createWebviewPanel('tensorBoardSession', 'TensorBoard', this.globalMemento.value, { - enableScripts: true, - retainContextWhenHidden: true, - }); - webviewPanel.webview.html = await this.getHtml(); - this.webviewPanel = webviewPanel; - this.disposables.push( - webviewPanel.onDidDispose(() => { - this.webviewPanel = undefined; - // Kill the running TensorBoard session - this.process?.kill(); - sendTelemetryEvent(EventName.TENSORBOARD_SESSION_DURATION, this.sessionDurationStopwatch?.elapsedTime); - this.process = undefined; - this._active = false; - this.onDidDisposeEventEmitter.fire(this); - }), - ); - this.disposables.push( - webviewPanel.onDidChangeViewState(async (args: WebviewPanelOnDidChangeViewStateEvent) => { - // The webview has been moved to a different viewgroup if it was active before and remains active now - if (this.active && args.webviewPanel.active) { - await this.globalMemento.updateValue(webviewPanel.viewColumn ?? ViewColumn.Active); - } - this._active = args.webviewPanel.active; - this.onDidChangeViewStateEventEmitter.fire(); - }), - ); - this.disposables.push( - webviewPanel.webview.onDidReceiveMessage((message) => { - // Handle messages posted from the webview - switch (message.command) { - case Messages.JumpToSource: - void this.jumpToSource(message.args.filename, message.args.line); - break; - default: - break; - } - }), - ); - return webviewPanel; - } - - private autopopulateLogDirectoryPath(): string | undefined { - if (this.workspaceService.rootPath) { - return this.workspaceService.rootPath; - } - const { activeTextEditor } = window; - if (activeTextEditor) { - return path.dirname(activeTextEditor.document.uri.fsPath); - } - return undefined; - } - - private async jumpToSource(fsPath: string, line: number) { - sendTelemetryEvent(EventName.TENSORBOARD_JUMP_TO_SOURCE_REQUEST); - let uri: Uri | undefined; - if (fs.existsSync(fsPath)) { - uri = Uri.file(fsPath); - } else { - sendTelemetryEvent(EventName.TENSORBOARD_JUMP_TO_SOURCE_FILE_NOT_FOUND); - traceError( - `Requested jump to source filepath ${fsPath} does not exist. Prompting user to select source file...`, - ); - // Prompt the user to pick the file on disk - const items: QuickPickItem[] = [ - { - label: TensorBoard.selectMissingSourceFile, - description: TensorBoard.selectMissingSourceFileDescription, - }, - ]; - // Using a multistep so that we can add a title to the quickpick - const multiStep = this.multiStepFactory.create(); - await multiStep.run(async (input) => { - const selection = await input.showQuickPick({ - items, - title: TensorBoard.missingSourceFile, - placeholder: fsPath, - }); - switch (selection?.label) { - case TensorBoard.selectMissingSourceFile: { - const filePickerSelection = await this.applicationShell.showOpenDialog({ - canSelectFiles: true, - canSelectFolders: false, - canSelectMany: false, - }); - if (filePickerSelection !== undefined) { - [uri] = filePickerSelection; - } - break; - } - default: - break; - } - }, {}); - } - if (uri === undefined) { - return; - } - const document = await workspace.openTextDocument(uri); - const editor = await window.showTextDocument(document, ViewColumn.Beside); - // Select the line if it exists in the document - if (line < editor.document.lineCount) { - const position = new Position(line, 0); - const selection = new Selection(position, editor.document.lineAt(line).range.end); - editor.selection = selection; - editor.revealRange(selection, TextEditorRevealType.InCenterIfOutsideViewport); - } - } - - private async getHtml() { - // We cannot cache the result of calling asExternalUri, so regenerate - // it each time. From docs: "Note that extensions should not cache the - // result of asExternalUri as the resolved uri may become invalid due - // to a system or user action — for example, in remote cases, a user may - // close a port forwarding tunnel that was opened by asExternalUri." - const fullWebServerUri = await env.asExternalUri(Uri.parse(this.url!)); - return ` - - - - - - TensorBoard - - - - - - - `; - } } diff --git a/src/client/tensorBoard/tensorBoardSessionProvider.ts b/src/client/tensorBoard/tensorBoardSessionProvider.ts deleted file mode 100644 index ec52b9ef94dc..000000000000 --- a/src/client/tensorBoard/tensorBoardSessionProvider.ts +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { Disposable, l10n, ViewColumn } from 'vscode'; -import { IExtensionSingleActivationService } from '../activation/types'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; -import { Commands } from '../common/constants'; -import { ContextKey } from '../common/contextKey'; -import { IPythonExecutionFactory } from '../common/process/types'; -import { - IDisposableRegistry, - IInstaller, - IPersistentState, - IPersistentStateFactory, - IConfigurationService, - IDisposable, -} from '../common/types'; -import { IMultiStepInputFactory } from '../common/utils/multiStepInput'; -import { IInterpreterService } from '../interpreter/contracts'; -import { traceError, traceVerbose } from '../logging'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from './constants'; -import { TensorBoardSession } from './tensorBoardSession'; -import { TensorboardExperiment } from './tensorboarExperiment'; - -export const PREFERRED_VIEWGROUP = 'PythonTensorBoardWebviewPreferredViewGroup'; - -@injectable() -export class TensorBoardSessionProvider implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - private knownSessions: TensorBoardSession[] = []; - - private preferredViewGroupMemento: IPersistentState; - - private hasActiveTensorBoardSessionContext: ContextKey; - - private readonly disposables: IDisposable[] = []; - - constructor( - @inject(IInstaller) private readonly installer: IInstaller, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, - @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(IDisposableRegistry) disposables: IDisposableRegistry, - @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, - @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory, - @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, - ) { - disposables.push(this); - this.preferredViewGroupMemento = this.stateFactory.createGlobalPersistentState( - PREFERRED_VIEWGROUP, - ViewColumn.Active, - ); - this.hasActiveTensorBoardSessionContext = new ContextKey( - 'python.hasActiveTensorBoardSession', - this.commandManager, - ); - } - - public dispose(): void { - Disposable.from(...this.disposables).dispose(); - } - - public async activate(): Promise { - if (TensorboardExperiment.isTensorboardExtensionInstalled) { - return; - } - this.experiment.disposeOnInstallingTensorboard(this); - - this.disposables.push( - this.commandManager.registerCommand( - Commands.LaunchTensorBoard, - ( - entrypoint: TensorBoardEntrypoint = TensorBoardEntrypoint.palette, - trigger: TensorBoardEntrypointTrigger = TensorBoardEntrypointTrigger.palette, - ): void => { - sendTelemetryEvent(EventName.TENSORBOARD_SESSION_LAUNCH, undefined, { - trigger, - entrypoint, - }); - if (this.experiment.recommendAndUseNewExtension() === 'continueWithPythonExtension') { - void this.createNewSession(); - } - }, - ), - this.commandManager.registerCommand(Commands.RefreshTensorBoard, () => - this.experiment.recommendAndUseNewExtension() === 'continueWithPythonExtension' - ? this.knownSessions.map((w) => w.refresh()) - : undefined, - ), - ); - } - - private async updateTensorBoardSessionContext() { - let hasActiveTensorBoardSession = false; - this.knownSessions.forEach((viewer) => { - if (viewer.active) { - hasActiveTensorBoardSession = true; - } - }); - await this.hasActiveTensorBoardSessionContext.set(hasActiveTensorBoardSession); - } - - private async didDisposeSession(session: TensorBoardSession) { - this.knownSessions = this.knownSessions.filter((s) => s !== session); - this.updateTensorBoardSessionContext(); - } - - private async createNewSession(): Promise { - traceVerbose('Starting new TensorBoard session...'); - try { - const newSession = new TensorBoardSession( - this.installer, - this.interpreterService, - this.workspaceService, - this.pythonExecFactory, - this.commandManager, - this.disposables, - this.applicationShell, - this.preferredViewGroupMemento, - this.multiStepFactory, - this.configurationService, - ); - newSession.onDidChangeViewState(() => this.updateTensorBoardSessionContext(), this, this.disposables); - newSession.onDidDispose((e) => this.didDisposeSession(e), this, this.disposables); - this.knownSessions.push(newSession); - await newSession.initialize(); - return newSession; - } catch (e) { - traceError(`Encountered error while starting new TensorBoard session: ${e}`); - await this.applicationShell.showErrorMessage( - l10n.t( - 'We failed to start a TensorBoard session due to the following error: {0}', - (e as Error).message, - ), - ); - } - return undefined; - } -} diff --git a/src/client/tensorBoard/tensorBoardUsageTracker.ts b/src/client/tensorBoard/tensorBoardUsageTracker.ts deleted file mode 100644 index d1b21473677f..000000000000 --- a/src/client/tensorBoard/tensorBoardUsageTracker.ts +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Disposable, TextEditor } from 'vscode'; -import { IExtensionSingleActivationService } from '../activation/types'; -import { IDocumentManager } from '../common/application/types'; -import { isTestExecution } from '../common/constants'; -import { IDisposableRegistry } from '../common/types'; -import { getDocumentLines } from '../telemetry/importTracker'; -import { TensorBoardEntrypointTrigger } from './constants'; -import { containsTensorBoardImport } from './helpers'; -import { TensorBoardPrompt } from './tensorBoardPrompt'; -import { TensorboardExperiment } from './tensorboarExperiment'; - -const testExecution = isTestExecution(); - -// Prompt the user to start an integrated TensorBoard session whenever the active Python file or Python notebook -// contains a valid TensorBoard import. -@injectable() -export class TensorBoardUsageTracker implements IExtensionSingleActivationService { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - constructor( - @inject(IDocumentManager) private documentManager: IDocumentManager, - @inject(IDisposableRegistry) private disposables: IDisposableRegistry, - @inject(TensorBoardPrompt) private prompt: TensorBoardPrompt, - @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, - ) {} - - public dispose(): void { - Disposable.from(...this.disposables).dispose(); - } - - public async activate(): Promise { - if (TensorboardExperiment.isTensorboardExtensionInstalled) { - return; - } - this.experiment.disposeOnInstallingTensorboard(this); - if (testExecution) { - await this.activateInternal(); - } else { - this.activateInternal().ignoreErrors(); - } - } - - private async activateInternal() { - // Process currently active text editor - this.onChangedActiveTextEditor(this.documentManager.activeTextEditor); - // Process changes to active text editor as well - this.documentManager.onDidChangeActiveTextEditor( - (e) => this.onChangedActiveTextEditor(e), - this, - this.disposables, - ); - } - - private onChangedActiveTextEditor(editor: TextEditor | undefined): void { - if (!editor || !editor.document) { - return; - } - const { document } = editor; - const extName = path.extname(document.fileName).toLowerCase(); - if (extName === '.py' || (extName === '.ipynb' && document.languageId === 'python')) { - const lines = getDocumentLines(document); - if (containsTensorBoardImport(lines)) { - this.prompt.showNativeTensorBoardPrompt(TensorBoardEntrypointTrigger.fileimport).ignoreErrors(); - } - } - } -} diff --git a/src/client/tensorBoard/tensorboarExperiment.ts b/src/client/tensorBoard/tensorboarExperiment.ts deleted file mode 100644 index 3cf4cb3c779a..000000000000 --- a/src/client/tensorBoard/tensorboarExperiment.ts +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { Disposable, EventEmitter, commands, extensions, l10n, window } from 'vscode'; -import { inject, injectable } from 'inversify'; -import { IDisposable, IDisposableRegistry, IExperimentService } from '../common/types'; -import { RecommendTensobardExtension } from '../common/experiments/groups'; -import { TENSORBOARD_EXTENSION_ID } from '../common/constants'; - -@injectable() -export class TensorboardExperiment { - private readonly _onDidChange = new EventEmitter(); - - public readonly onDidChange = this._onDidChange.event; - - private readonly toDisposeWhenTensobardIsInstalled: IDisposable[] = []; - - public static get isTensorboardExtensionInstalled(): boolean { - return !!extensions.getExtension(TENSORBOARD_EXTENSION_ID); - } - - private readonly isExperimentEnabled: boolean; - - constructor( - @inject(IDisposableRegistry) disposables: IDisposableRegistry, - @inject(IExperimentService) experiments: IExperimentService, - ) { - this.isExperimentEnabled = experiments.inExperimentSync(RecommendTensobardExtension.experiment); - disposables.push(this._onDidChange); - extensions.onDidChange( - () => - TensorboardExperiment.isTensorboardExtensionInstalled - ? Disposable.from(...this.toDisposeWhenTensobardIsInstalled).dispose() - : undefined, - this, - disposables, - ); - } - - public recommendAndUseNewExtension(): 'continueWithPythonExtension' | 'usingTensorboardExtension' { - if (!this.isExperimentEnabled) { - return 'continueWithPythonExtension'; - } - if (TensorboardExperiment.isTensorboardExtensionInstalled) { - return 'usingTensorboardExtension'; - } - const install = l10n.t('Install Tensorboard Extension'); - window - .showInformationMessage( - l10n.t( - 'Install the TensorBoard extension to use the this functionality. Once installed, select the command `Launch Tensorboard`.', - ), - { modal: true }, - install, - ) - .then((result): void => { - if (result === install) { - void commands.executeCommand('workbench.extensions.installExtension', TENSORBOARD_EXTENSION_ID); - } - }); - return 'usingTensorboardExtension'; - } - - public disposeOnInstallingTensorboard(disposabe: IDisposable): void { - this.toDisposeWhenTensobardIsInstalled.push(disposabe); - } -} diff --git a/src/client/tensorBoard/tensorboardDependencyChecker.ts b/src/client/tensorBoard/tensorboardDependencyChecker.ts index 5c377e1d2455..995344284eec 100644 --- a/src/client/tensorBoard/tensorboardDependencyChecker.ts +++ b/src/client/tensorBoard/tensorboardDependencyChecker.ts @@ -2,59 +2,29 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { Uri, ViewColumn } from 'vscode'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; -import { IPythonExecutionFactory } from '../common/process/types'; -import { - IInstaller, - IPersistentState, - IPersistentStateFactory, - IConfigurationService, - IDisposable, -} from '../common/types'; -import { IMultiStepInputFactory } from '../common/utils/multiStepInput'; +import { Uri } from 'vscode'; +import { IApplicationShell, ICommandManager } from '../common/application/types'; +import { IInstaller } from '../common/types'; import { IInterpreterService } from '../interpreter/contracts'; import { TensorBoardSession } from './tensorBoardSession'; -import { disposeAll } from '../common/utils/resourceLifecycle'; -import { PREFERRED_VIEWGROUP } from './tensorBoardSessionProvider'; @injectable() export class TensorboardDependencyChecker { - private preferredViewGroupMemento: IPersistentState; - constructor( @inject(IInstaller) private readonly installer: IInstaller, @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, - @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory, - @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - ) { - this.preferredViewGroupMemento = this.stateFactory.createGlobalPersistentState( - PREFERRED_VIEWGROUP, - ViewColumn.Active, - ); - } + ) {} public async ensureDependenciesAreInstalled(resource?: Uri): Promise { - const disposables: IDisposable[] = []; const newSession = new TensorBoardSession( this.installer, this.interpreterService, - this.workspaceService, - this.pythonExecFactory, this.commandManager, - disposables, this.applicationShell, - this.preferredViewGroupMemento, - this.multiStepFactory, - this.configurationService, ); const result = await newSession.ensurePrerequisitesAreInstalled(resource); - disposeAll(disposables); return result; } } diff --git a/src/client/tensorBoard/tensorboardIntegration.ts b/src/client/tensorBoard/tensorboardIntegration.ts index 22d590d6ee65..f3cbad59977b 100644 --- a/src/client/tensorBoard/tensorboardIntegration.ts +++ b/src/client/tensorBoard/tensorboardIntegration.ts @@ -5,10 +5,10 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { Extension, Uri, commands } from 'vscode'; +import { Extension, Uri } from 'vscode'; import { IWorkspaceService } from '../common/application/types'; import { TENSORBOARD_EXTENSION_ID } from '../common/constants'; -import { IDisposableRegistry, IExtensions, Resource } from '../common/types'; +import { IExtensions, Resource } from '../common/types'; import { IEnvironmentActivationService } from '../interpreter/activation/types'; import { TensorBoardPrompt } from './tensorBoardPrompt'; import { TensorboardDependencyChecker } from './tensorboardDependencyChecker'; @@ -45,14 +45,9 @@ export class TensorboardExtensionIntegration { @inject(IWorkspaceService) private workspaceService: IWorkspaceService, @inject(TensorboardDependencyChecker) private readonly dependencyChcker: TensorboardDependencyChecker, @inject(TensorBoardPrompt) private readonly tensorBoardPrompt: TensorBoardPrompt, - @inject(IDisposableRegistry) disposables: IDisposableRegistry, - ) { - this.hideCommands(); - extensions.onDidChange(this.hideCommands, this, disposables); - } + ) {} public registerApi(tensorboardExtensionApi: TensorboardExtensionApi): TensorboardExtensionApi | undefined { - this.hideCommands(); if (!this.workspaceService.isTrusted) { this.workspaceService.onDidGrantWorkspaceTrust(() => this.registerApi(tensorboardExtensionApi)); return undefined; @@ -67,12 +62,6 @@ export class TensorboardExtensionIntegration { return undefined; } - public hideCommands(): void { - if (this.extensions.getExtension(TENSORBOARD_EXTENSION_ID)) { - void commands.executeCommand('setContext', 'python.tensorboardExtInstalled', true); - } - } - public async integrateWithTensorboardExtension(): Promise { const api = await this.getExtensionApi(); if (api) { diff --git a/src/client/tensorBoard/terminalWatcher.ts b/src/client/tensorBoard/terminalWatcher.ts deleted file mode 100644 index 5f48def54e43..000000000000 --- a/src/client/tensorBoard/terminalWatcher.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { inject, injectable } from 'inversify'; -import { window } from 'vscode'; -import { IExtensionSingleActivationService } from '../activation/types'; -import { IDisposable, IDisposableRegistry } from '../common/types'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { TensorboardExperiment } from './tensorboarExperiment'; - -// Every 5 min look, through active terminals to see if any are running `tensorboard` -@injectable() -export class TerminalWatcher implements IExtensionSingleActivationService, IDisposable { - public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; - - private handle: NodeJS.Timeout | undefined; - - constructor( - @inject(IDisposableRegistry) private disposables: IDisposableRegistry, - @inject(TensorboardExperiment) private readonly experiment: TensorboardExperiment, - ) { - disposables.push(this); - } - - public async activate(): Promise { - if (TensorboardExperiment.isTensorboardExtensionInstalled) { - return; - } - this.experiment.disposeOnInstallingTensorboard(this); - const handle = setInterval(() => { - // When user runs a command in VSCode terminal, the terminal's name - // becomes the program that is currently running. Since tensorboard - // stays running in the terminal while the webapp is running and - // until the user kills it, the terminal with the updated name should - // stick around for long enough that we only need to run this check - // every 5 min or so - const matches = window.terminals.filter((terminal) => terminal.name === 'tensorboard'); - if (matches.length > 0) { - sendTelemetryEvent(EventName.TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL); - clearInterval(handle); // Only need telemetry sent once per VS Code session - } - }, 300_000); - this.handle = handle; - this.disposables.push(this); - } - - public dispose(): void { - if (this.handle) { - clearInterval(this.handle); - } - } -} diff --git a/src/client/tensorBoard/types.ts b/src/client/tensorBoard/types.ts deleted file mode 100644 index a11659015da8..000000000000 --- a/src/client/tensorBoard/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Event, Uri } from 'vscode'; - -export const ITensorBoardImportTracker = Symbol('ITensorBoardImportTracker'); -export interface ITensorBoardImportTracker { - onDidImportTensorBoard: Event; -} - -export const ITensorboardDependencyChecker = Symbol('ITensorboardDependencyChecker'); -export interface ITensorboardDependencyChecker { - ensureDependenciesAreInstalled(resource?: Uri): Promise; -} diff --git a/src/test/tensorBoard/helpers.ts b/src/test/tensorBoard/helpers.ts deleted file mode 100644 index b9f90226b28e..000000000000 --- a/src/test/tensorBoard/helpers.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as TypeMoq from 'typemoq'; -import { IApplicationShell, ICommandManager } from '../../client/common/application/types'; -import { IPersistentStateFactory } from '../../client/common/types'; -import { TensorBoardPrompt } from '../../client/tensorBoard/tensorBoardPrompt'; -import { MockState } from '../interpreters/mocks'; - -export function createTensorBoardPromptWithMocks(): TensorBoardPrompt { - const appShell = TypeMoq.Mock.ofType(); - const commandManager = TypeMoq.Mock.ofType(); - const persistentStateFactory = TypeMoq.Mock.ofType(); - const persistentState = new MockState(true); - persistentStateFactory - .setup((factory) => { - factory.createWorkspacePersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny()); - }) - .returns(() => persistentState); - return new TensorBoardPrompt(appShell.object, commandManager.object, persistentStateFactory.object); -} diff --git a/src/test/tensorBoard/nbextensionCodeLensProvider.unit.test.ts b/src/test/tensorBoard/nbextensionCodeLensProvider.unit.test.ts deleted file mode 100644 index d4339a4af61b..000000000000 --- a/src/test/tensorBoard/nbextensionCodeLensProvider.unit.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as sinon from 'sinon'; -import { assert } from 'chai'; -import { CancellationTokenSource } from 'vscode'; -import { instance, mock } from 'ts-mockito'; -import { TensorBoardNbextensionCodeLensProvider } from '../../client/tensorBoard/nbextensionCodeLensProvider'; -import { MockDocument } from '../mocks/mockDocument'; -import { TensorboardExperiment } from '../../client/tensorBoard/tensorboarExperiment'; - -[true, false].forEach((tbExtensionInstalled) => { - suite(`Tensorboard Extension is ${tbExtensionInstalled ? 'installed' : 'not installed'}`, () => { - suite('TensorBoard nbextension code lens provider', () => { - let experiment: TensorboardExperiment; - let codeLensProvider: TensorBoardNbextensionCodeLensProvider; - let cancelTokenSource: CancellationTokenSource; - - setup(() => { - sinon.stub(TensorboardExperiment, 'isTensorboardExtensionInstalled').returns(tbExtensionInstalled); - experiment = mock(); - codeLensProvider = new TensorBoardNbextensionCodeLensProvider([], instance(experiment)); - cancelTokenSource = new CancellationTokenSource(); - }); - teardown(() => { - sinon.restore(); - cancelTokenSource.dispose(); - }); - - test('Provide code lens for Python notebook loading tensorboard nbextension', async () => { - const document = new MockDocument('a=1\n%load_ext tensorboard', 'foo.ipynb', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length > 0, 'Failed to provide code lens for file loading tensorboard nbextension'); - }); - test('Provide code lens for Python notebook launching tensorboard nbextension', async () => { - const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.ipynb', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length > 0, 'Failed to provide code lens for file loading tensorboard nbextension'); - }); - test('Fails when cancellation is signaled', () => { - const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.ipynb', async () => true); - cancelTokenSource.cancel(); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length === 0, 'Provided codelens even after cancellation was requested'); - }); - // Can't verify these cases without running in vscode as we depend on vscode to not call us - // based on the DocumentSelector we provided. See nbExtensionCodeLensProvider.test.ts for that. - // test('Does not provide code lens for Python file loading tensorboard nbextension', async () => { - // const document = new MockDocument('a=1\n%load_ext tensorboard', 'foo.py', async () => true); - // const codeLens = codeLensProvider.provideCodeLenses(document); - // assert.ok(codeLens.length === 0, 'Provided code lens for Python file loading tensorboard nbextension'); - // }); - // test('Does not provide code lens for Python file launching tensorboard nbextension', async () => { - // const document = new MockDocument('a=1\n%tensorboard --logdir logs/fit', 'foo.py', async () => true); - // const codeLens = codeLensProvider.provideCodeLenses(document); - // assert.ok(codeLens.length === 0, 'Provided code lens for Python file loading tensorboard nbextension'); - // }); - }); - }); -}); diff --git a/src/test/tensorBoard/tensorBoardFileWatcher.test.ts b/src/test/tensorBoard/tensorBoardFileWatcher.test.ts deleted file mode 100644 index 3ad9ada21bdb..000000000000 --- a/src/test/tensorBoard/tensorBoardFileWatcher.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { assert } from 'chai'; -import * as path from 'path'; -import * as sinon from 'sinon'; -import * as fse from '../../client/common/platform/fs-paths'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { IExperimentService } from '../../client/common/types'; -import { TensorBoardFileWatcher } from '../../client/tensorBoard/tensorBoardFileWatcher'; -import { TensorBoardPrompt } from '../../client/tensorBoard/tensorBoardPrompt'; -import { waitForCondition } from '../common'; -import { initialize } from '../initialize'; - -suite('TensorBoard file system watcher', async () => { - const tfeventfileName = 'events.out.tfevents.1606887221.24672.162.v2'; - const currentDirectory = process.env.CODE_TESTS_WORKSPACE ?? path.join(__dirname, '..', '..', '..', 'src', 'test'); - let showNativeTensorBoardPrompt: sinon.SinonSpy; - const sandbox = sinon.createSandbox(); - let eventFile: string | undefined; - let eventFileDirectory: string | undefined; - - async function createFiles(directory: string) { - eventFileDirectory = directory; - await fse.ensureDir(directory); - eventFile = path.join(directory, tfeventfileName); - await fse.writeFile(eventFile, ''); - } - - async function configureStubsAndActivate() { - const { serviceManager } = await initialize(); - // Stub the prompt show method so we can verify that it was called - const prompt = serviceManager.get(TensorBoardPrompt); - showNativeTensorBoardPrompt = sandbox.stub(prompt, 'showNativeTensorBoardPrompt'); - serviceManager.rebindInstance(TensorBoardPrompt, prompt); - const experimentService = serviceManager.get(IExperimentService); - sandbox.stub(experimentService, 'inExperiment').resolves(true); - const fileWatcher = serviceManager.get(TensorBoardFileWatcher); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (fileWatcher as any).activateInternal(); - } - - teardown(async () => { - sandbox.restore(); - if (eventFile) { - await fse.unlink(eventFile); - eventFile = undefined; - } - }); - - suiteTeardown(async () => { - if (eventFileDirectory && eventFileDirectory !== currentDirectory) { - await fse.rmdir(eventFileDirectory); - eventFileDirectory = undefined; - } - }); - - test('Creating tfeventfile one directory down results in prompt being shown', async () => { - const dir1 = path.join(currentDirectory, '1'); - await configureStubsAndActivate(); - await createFiles(dir1); - await waitForCondition(async () => showNativeTensorBoardPrompt.called, 5000, 'Prompt not shown'); - }); - - test('Creating tfeventfile two directories down results in prompt being called', async () => { - const dir2 = path.join(currentDirectory, '1', '2'); - await configureStubsAndActivate(); - await createFiles(dir2); - await waitForCondition(async () => showNativeTensorBoardPrompt.called, 5000, 'Prompt not shown'); - }); - - test('Creating tfeventfile three directories down does not result in prompt being called', async () => { - const dir3 = path.join(currentDirectory, '1', '2', '3'); - await configureStubsAndActivate(); - await createFiles(dir3); - await waitForCondition(async () => showNativeTensorBoardPrompt.notCalled, 5000, 'Prompt shown'); - }); - - test('No workspace folder open, prompt is not called', async () => { - const { serviceManager } = await initialize(); - - // Stub the prompt show method so we can verify that it was called - const prompt = serviceManager.get(TensorBoardPrompt); - showNativeTensorBoardPrompt = sandbox.stub(prompt, 'showNativeTensorBoardPrompt'); - serviceManager.rebindInstance(TensorBoardPrompt, prompt); - - // Pretend there are no open folders - const workspaceService = serviceManager.get(IWorkspaceService); - sandbox.stub(workspaceService, 'workspaceFolders').get(() => undefined); - serviceManager.rebindInstance(IWorkspaceService, workspaceService); - const fileWatcher = serviceManager.get(TensorBoardFileWatcher); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (fileWatcher as any).activateInternal(); - - assert.ok(showNativeTensorBoardPrompt.notCalled); - }); -}); diff --git a/src/test/tensorBoard/tensorBoardImportCodeLensProvider.unit.test.ts b/src/test/tensorBoard/tensorBoardImportCodeLensProvider.unit.test.ts deleted file mode 100644 index 8b16301753a6..000000000000 --- a/src/test/tensorBoard/tensorBoardImportCodeLensProvider.unit.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as sinon from 'sinon'; -import { assert } from 'chai'; -import { CancellationTokenSource } from 'vscode'; -import { instance, mock } from 'ts-mockito'; -import { TensorBoardImportCodeLensProvider } from '../../client/tensorBoard/tensorBoardImportCodeLensProvider'; -import { MockDocument } from '../mocks/mockDocument'; -import { TensorboardExperiment } from '../../client/tensorBoard/tensorboarExperiment'; - -[true, false].forEach((tbExtensionInstalled) => { - suite(`Tensorboard Extension is ${tbExtensionInstalled ? 'installed' : 'not installed'}`, () => { - suite('TensorBoard import code lens provider', () => { - let experiment: TensorboardExperiment; - let codeLensProvider: TensorBoardImportCodeLensProvider; - let cancelTokenSource: CancellationTokenSource; - - setup(() => { - sinon.stub(TensorboardExperiment, 'isTensorboardExtensionInstalled').returns(tbExtensionInstalled); - experiment = mock(); - codeLensProvider = new TensorBoardImportCodeLensProvider([], instance(experiment)); - cancelTokenSource = new CancellationTokenSource(); - }); - teardown(() => { - sinon.restore(); - cancelTokenSource.dispose(); - }); - [ - 'import tensorboard', - 'import foo, tensorboard', - 'import foo, tensorboard, bar', - 'import tensorboardX', - 'import tensorboardX, bar', - 'import torch.profiler', - 'import foo, torch.profiler', - 'from torch.utils import tensorboard', - 'from torch.utils import foo, tensorboard', - 'import torch.utils.tensorboard, foo', - 'from torch import profiler', - ].forEach((importStatement) => { - test(`Provides code lens for Python files containing ${importStatement}`, () => { - const document = new MockDocument(importStatement, 'foo.py', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok( - codeLens.length > 0, - `Failed to provide code lens for file containing ${importStatement} import`, - ); - }); - test(`Provides code lens for Python ipynbs containing ${importStatement}`, () => { - const document = new MockDocument(importStatement, 'foo.ipynb', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok( - codeLens.length > 0, - `Failed to provide code lens for ipynb containing ${importStatement} import`, - ); - }); - test('Fails when cancellation is signaled', () => { - const document = new MockDocument(importStatement, 'foo.py', async () => true); - cancelTokenSource.cancel(); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length === 0, 'Provided codelens even after cancellation was requested'); - }); - }); - test('Does not provide code lens if no matching import', () => { - const document = new MockDocument('import foo', 'foo.ipynb', async () => true); - const codeLens = codeLensProvider.provideCodeLenses(document, cancelTokenSource.token); - assert.ok(codeLens.length === 0, 'Provided code lens for file without tensorboard import'); - }); - }); - }); -}); diff --git a/src/test/tensorBoard/tensorBoardPrompt.unit.test.ts b/src/test/tensorBoard/tensorBoardPrompt.unit.test.ts deleted file mode 100644 index 6f096e560d70..000000000000 --- a/src/test/tensorBoard/tensorBoardPrompt.unit.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { ApplicationShell } from '../../client/common/application/applicationShell'; -import { CommandManager } from '../../client/common/application/commandManager'; -import { Commands } from '../../client/common/constants'; -import { PersistentState, PersistentStateFactory } from '../../client/common/persistentState'; -import { Common } from '../../client/common/utils/localize'; -import { TensorBoardEntrypointTrigger } from '../../client/tensorBoard/constants'; -import { TensorBoardPrompt } from '../../client/tensorBoard/tensorBoardPrompt'; - -suite('TensorBoard prompt', () => { - let applicationShell: ApplicationShell; - let commandManager: CommandManager; - let persistentState: PersistentState; - let persistentStateFactory: PersistentStateFactory; - let prompt: TensorBoardPrompt; - - async function setupPromptWithOptions(persistentStateValue = true, selection = 'Yes') { - applicationShell = mock(ApplicationShell); - when(applicationShell.showInformationMessage(anything(), anything(), anything(), anything())).thenReturn( - Promise.resolve(selection), - ); - - commandManager = mock(CommandManager); - when(commandManager.executeCommand(Commands.LaunchTensorBoard, anything(), anything())).thenResolve(); - - persistentStateFactory = mock(PersistentStateFactory); - persistentState = mock(PersistentState) as PersistentState; - when(persistentState.value).thenReturn(persistentStateValue); - when(persistentState.updateValue(anything())).thenResolve(); - when(persistentStateFactory.createWorkspacePersistentState(anything(), anything())).thenReturn( - instance(persistentState), - ); - - prompt = new TensorBoardPrompt( - instance(applicationShell), - instance(commandManager), - instance(persistentStateFactory), - ); - await prompt.showNativeTensorBoardPrompt(TensorBoardEntrypointTrigger.palette); - } - - test('Show prompt if user is in experiment, and prompt has not previously been disabled or shown', async () => { - await setupPromptWithOptions(); - verify(applicationShell.showInformationMessage(anything(), anything(), anything(), anything())).once(); - verify(commandManager.executeCommand(Commands.LaunchTensorBoard, anything(), anything())).once(); - }); - - test('Disable prompt if user selects "Do not show again"', async () => { - await setupPromptWithOptions(true, Common.doNotShowAgain); - verify(persistentState.updateValue(false)).once(); - }); - - test('Do not show prompt if user has previously disabled prompt', async () => { - await setupPromptWithOptions(false); - verify(applicationShell.showInformationMessage(anything(), anything(), anything(), anything())).never(); - verify(commandManager.executeCommand(Commands.LaunchTensorBoard, anything(), anything())).never(); - }); - - test('Do not show prompt more than once per session', async () => { - await setupPromptWithOptions(); - verify(applicationShell.showInformationMessage(anything(), anything(), anything(), anything())).once(); - await prompt.showNativeTensorBoardPrompt(TensorBoardEntrypointTrigger.palette); - verify(applicationShell.showInformationMessage(anything(), anything(), anything(), anything())).once(); - }); -}); diff --git a/src/test/tensorBoard/tensorBoardSession.test.ts b/src/test/tensorBoard/tensorBoardSession.test.ts deleted file mode 100644 index 626740f4f530..000000000000 --- a/src/test/tensorBoard/tensorBoardSession.test.ts +++ /dev/null @@ -1,510 +0,0 @@ -import * as path from 'path'; -import { assert } from 'chai'; -import Sinon, * as sinon from 'sinon'; -import { SemVer } from 'semver'; -import { Uri, ViewColumn, window, workspace, WorkspaceConfiguration } from 'vscode'; -import { - IExperimentService, - IInstaller, - InstallerResponse, - Product, - ProductInstallStatus, -} from '../../client/common/types'; -import { Common, TensorBoard } from '../../client/common/utils/localize'; -import { IApplicationShell, ICommandManager } from '../../client/common/application/types'; -import { IServiceManager } from '../../client/ioc/types'; -import { TensorBoardEntrypoint, TensorBoardEntrypointTrigger } from '../../client/tensorBoard/constants'; -import { TensorBoardSession } from '../../client/tensorBoard/tensorBoardSession'; -import { closeActiveWindows, EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../initialize'; -import { IInterpreterService } from '../../client/interpreter/contracts'; -import { Architecture } from '../../client/common/utils/platform'; -import { PythonEnvironment, EnvironmentType } from '../../client/pythonEnvironments/info'; -import { PYTHON_PATH } from '../common'; -import { ImportTracker } from '../../client/telemetry/importTracker'; -import { IMultiStepInput, IMultiStepInputFactory } from '../../client/common/utils/multiStepInput'; -import { ModuleInstallFlags } from '../../client/common/installer/types'; - -// Class methods exposed just for testing purposes -interface ITensorBoardSessionTestAPI { - jumpToSource(fsPath: string, line: number): Promise; -} - -const info: PythonEnvironment = { - architecture: Architecture.Unknown, - companyDisplayName: '', - displayName: '', - envName: '', - path: '', - envType: EnvironmentType.Unknown, - version: new SemVer('0.0.0-alpha'), - sysPrefix: '', - sysVersion: '', -}; - -const interpreter: PythonEnvironment = { - ...info, - envType: EnvironmentType.Unknown, - path: PYTHON_PATH, -}; - -suite('TensorBoard session creation', async () => { - let serviceManager: IServiceManager; - let errorMessageStub: Sinon.SinonStub; - let sandbox: Sinon.SinonSandbox; - let applicationShell: IApplicationShell; - let commandManager: ICommandManager; - let experimentService: IExperimentService; - let installer: IInstaller; - let initialValue: string | undefined; - let workspaceConfiguration: WorkspaceConfiguration; - - suiteSetup(function () { - if (process.env.CI_PYTHON_VERSION === '2.7') { - // TensorBoard 2.4.1 not available for Python 2.7 - this.skip(); - } - - // See: https://github.com/microsoft/vscode-python/issues/18130 - this.skip(); - }); - - setup(async () => { - sandbox = sinon.createSandbox(); - ({ serviceManager } = await initialize()); - - experimentService = serviceManager.get(IExperimentService); - const interpreterService = serviceManager.get(IInterpreterService); - sandbox.stub(interpreterService, 'getActiveInterpreter').resolves(interpreter); - - applicationShell = serviceManager.get(IApplicationShell); - commandManager = serviceManager.get(ICommandManager); - installer = serviceManager.get(IInstaller); - workspaceConfiguration = workspace.getConfiguration('python.tensorBoard'); - initialValue = workspaceConfiguration.get('logDirectory'); - await workspaceConfiguration.update('logDirectory', undefined, true); - }); - - teardown(async () => { - await workspaceConfiguration.update('logDirectory', initialValue, true); - await closeActiveWindows(); - sandbox.restore(); - }); - - function configureStubs( - hasTorchImports: boolean, - tensorBoardInstallStatus: ProductInstallStatus, - torchProfilerPackageInstallStatus: ProductInstallStatus, - installPromptSelection: 'Yes' | 'No', - ) { - sandbox.stub(ImportTracker, 'hasModuleImport').withArgs('torch').returns(hasTorchImports); - const isProductVersionCompatible = sandbox.stub(installer, 'isProductVersionCompatible'); - isProductVersionCompatible - .withArgs(Product.tensorboard, '>= 2.4.1', interpreter) - .resolves(tensorBoardInstallStatus); - isProductVersionCompatible - .withArgs(Product.torchProfilerImportName, '>= 0.2.0', interpreter) - .resolves(torchProfilerPackageInstallStatus); - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - errorMessageStub.resolves(installPromptSelection); - } - async function createSession() { - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - // Stub user selections - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - - const session = (await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - )) as TensorBoardSession; - - assert.ok(session.panel?.viewColumn === ViewColumn.One, 'Panel opened in wrong group'); - assert.ok(session.panel?.visible, 'Webview panel not shown on session creation golden path'); - assert.ok(errorMessageStub.notCalled, 'Error message shown on session creation golden path'); - return session; - } - suite('Core functionality', async () => { - test('Golden path: TensorBoard session starts successfully and webview is shown', async () => { - await createSession(); - }); - test('When webview is closed, session is killed', async () => { - const session = await createSession(); - const { daemon, panel } = session; - assert.ok(panel?.visible, 'Webview panel not shown'); - panel?.dispose(); - assert.ok(session.panel === undefined, 'Webview still visible'); - assert.ok(daemon?.killed, 'TensorBoard session process not killed after webview closed'); - }); - test('When user selects file picker, display file picker', async () => { - // Stub user selections - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.selectAnotherFolder }); - const filePickerStub = sandbox.stub(applicationShell, 'showOpenDialog'); - - // Create session - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok(filePickerStub.called, 'User requests to select another folder and file picker was not shown'); - }); - test('When user selects remote URL, display input box', async () => { - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.enterRemoteUrl }); - const inputBoxStub = sandbox.stub(applicationShell, 'showInputBox'); - - // Create session - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok( - inputBoxStub.called, - 'User requested to enter remote URL and input box to enter URL was not shown', - ); - }); - }); - suite('Installation prompt message', async () => { - async function createSessionAndVerifyMessage(message: string) { - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - assert.ok( - errorMessageStub.calledOnceWith(message, Common.bannerLabelYes, Common.bannerLabelNo), - 'Wrong error message shown', - ); - } - suite('Install profiler package + upgrade tensorboard', async () => { - async function runTest(expectTensorBoardUpgrade: boolean) { - const installStub = sandbox.stub(installer, 'install').resolves(InstallerResponse.Installed); - await createSessionAndVerifyMessage(TensorBoard.installTensorBoardAndProfilerPluginPrompt); - assert.ok(installStub.calledTwice, `Expected 2 installs but got ${installStub.callCount} calls`); - assert.ok(installStub.calledWith(Product.torchProfilerInstallName)); - assert.ok( - installStub.calledWith( - Product.tensorboard, - sinon.match.any, - sinon.match.any, - expectTensorBoardUpgrade ? ModuleInstallFlags.upgrade : undefined, - ), - ); - } - test('Has torch imports: true, is profiler package installed: false, TensorBoard needs upgrade', async () => { - configureStubs(true, ProductInstallStatus.NeedsUpgrade, ProductInstallStatus.NotInstalled, 'Yes'); - await runTest(true); - }); - test('Has torch imports: true, is profiler package installed: false, TensorBoard not installed', async () => { - configureStubs(true, ProductInstallStatus.NotInstalled, ProductInstallStatus.NotInstalled, 'Yes'); - await runTest(false); - }); - }); - suite('Install profiler only', async () => { - test('Has torch imports: true, is profiler package installed: false, TensorBoard installed', async () => { - configureStubs(true, ProductInstallStatus.Installed, ProductInstallStatus.NotInstalled, 'Yes'); - sandbox - .stub(applicationShell, 'showQuickPick') - .resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - // Ensure we ask to install the profiler package and that it resolves to a cancellation - sandbox - .stub(installer, 'install') - .withArgs(Product.torchProfilerInstallName, sinon.match.any, sinon.match.any) - .resolves(InstallerResponse.Ignore); - - const session = (await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - )) as TensorBoardSession; - - assert.ok(session.panel?.visible, 'Webview panel not shown, expected successful session creation'); - assert.ok( - errorMessageStub.calledOnceWith( - TensorBoard.installProfilerPluginPrompt, - Common.bannerLabelYes, - Common.bannerLabelNo, - ), - 'Wrong error message shown', - ); - }); - }); - suite('Install tensorboard only', async () => { - [false, true].forEach(async (hasTorchImports) => { - [ - ProductInstallStatus.Installed, - ProductInstallStatus.NotInstalled, - ProductInstallStatus.NeedsUpgrade, - ].forEach(async (torchProfilerInstallStatus) => { - const isTorchProfilerPackageInstalled = - torchProfilerInstallStatus === ProductInstallStatus.Installed; - if (!(hasTorchImports && !isTorchProfilerPackageInstalled)) { - test(`Has torch imports: ${hasTorchImports}, is profiler package installed: ${isTorchProfilerPackageInstalled}, TensorBoard not installed`, async () => { - configureStubs( - hasTorchImports, - ProductInstallStatus.NotInstalled, - torchProfilerInstallStatus, - 'No', - ); - await createSessionAndVerifyMessage(TensorBoard.installPrompt); - }); - } - }); - }); - }); - suite('Upgrade tensorboard only', async () => { - async function runTest() { - const installStub = sandbox.stub(installer, 'install').resolves(InstallerResponse.Installed); - await createSessionAndVerifyMessage(TensorBoard.upgradePrompt); - - assert.ok(installStub.calledOnce, `Expected 1 install but got ${installStub.callCount} installs`); - assert.ok(installStub.args[0][0] === Product.tensorboard, 'Did not install tensorboard'); - assert.ok( - installStub.args.filter((argsList) => argsList[0] === Product.torchProfilerInstallName).length === - 0, - 'Unexpected attempt to install profiler package', - ); - } - - [false, true].forEach(async (hasTorchImports) => { - [ - ProductInstallStatus.Installed, - ProductInstallStatus.NotInstalled, - ProductInstallStatus.NeedsUpgrade, - ].forEach(async (torchProfilerInstallStatus) => { - const isTorchProfilerPackageInstalled = - torchProfilerInstallStatus === ProductInstallStatus.Installed; - if (!(hasTorchImports && !isTorchProfilerPackageInstalled)) { - test(`Has torch imports: ${hasTorchImports}, is profiler package installed: ${isTorchProfilerPackageInstalled}, TensorBoard needs upgrade`, async () => { - configureStubs( - hasTorchImports, - ProductInstallStatus.NeedsUpgrade, - torchProfilerInstallStatus, - 'Yes', - ); - await runTest(); - }); - } - }); - }); - }); - suite('No prompt', async () => { - async function runTest() { - sandbox - .stub(applicationShell, 'showQuickPick') - .resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - assert.ok(errorMessageStub.notCalled, 'Prompt was unexpectedly shown'); - } - - [false, true].forEach(async (hasTorchImports) => { - [ - ProductInstallStatus.Installed, - ProductInstallStatus.NotInstalled, - ProductInstallStatus.NeedsUpgrade, - ].forEach(async (torchProfilerInstallStatus) => { - const isTorchProfilerPackageInstalled = - torchProfilerInstallStatus === ProductInstallStatus.Installed; - if (!(hasTorchImports && !isTorchProfilerPackageInstalled)) { - test(`Has torch imports: ${hasTorchImports}, is profiler package installed: ${isTorchProfilerPackageInstalled}, TensorBoard installed`, async () => { - configureStubs( - hasTorchImports, - ProductInstallStatus.Installed, - torchProfilerInstallStatus, - 'Yes', - ); - await runTest(); - }); - } - }); - }); - }); - }); - suite('Error messages', async () => { - test('If user cancels starting TensorBoard session, do not show error', async () => { - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - sandbox.stub(applicationShell, 'withProgress').resolves('canceled'); - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok(errorMessageStub.notCalled, 'User canceled session start and error was shown'); - }); - test('If existing install of TensorBoard is outdated and user cancels installation, do not show error', async () => { - sandbox.stub(experimentService, 'inExperiment').resolves(true); - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - sandbox.stub(installer, 'isProductVersionCompatible').resolves(ProductInstallStatus.NeedsUpgrade); - sandbox.stub(installer, 'install').resolves(InstallerResponse.Ignore); - const quickPickStub = sandbox.stub(applicationShell, 'showQuickPick'); - - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok(quickPickStub.notCalled, 'User opted not to upgrade and we proceeded to create session'); - }); - test('If TensorBoard is not installed and user chooses not to install, do not show error', async () => { - configureStubs(true, ProductInstallStatus.NotInstalled, ProductInstallStatus.NotInstalled, 'Yes'); - sandbox.stub(installer, 'install').resolves(InstallerResponse.Ignore); - - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok( - errorMessageStub.calledOnceWith( - TensorBoard.installTensorBoardAndProfilerPluginPrompt, - Common.bannerLabelYes, - Common.bannerLabelNo, - ), - 'User opted not to install and error was shown', - ); - }); - test('If user does not select a logdir, do not show error', async () => { - sandbox.stub(experimentService, 'inExperiment').resolves(true); - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - // Stub user selections - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.selectAFolder }); - sandbox.stub(applicationShell, 'showOpenDialog').resolves(undefined); - - // Create session - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok(errorMessageStub.notCalled, 'User opted not to select a logdir and error was shown'); - }); - test('If starting TensorBoard times out, show error', async () => { - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - sandbox.stub(applicationShell, 'withProgress').resolves(60_000); - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - - await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - ); - - assert.ok(errorMessageStub.called, 'TensorBoard timed out but no error was shown'); - }); - test('If installing the profiler package fails, do not show error, continue to create session', async () => { - configureStubs(true, ProductInstallStatus.Installed, ProductInstallStatus.NotInstalled, 'Yes'); - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - // Ensure we ask to install the profiler package and that it resolves to a cancellation - sandbox - .stub(installer, 'install') - .withArgs(Product.torchProfilerInstallName, sinon.match.any, sinon.match.any) - .resolves(InstallerResponse.Ignore); - - const session = (await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - )) as TensorBoardSession; - - assert.ok(session.panel?.visible, 'Webview panel not shown, expected successful session creation'); - }); - test('If user opts not to install profiler package and tensorboard is already installed, continue to create session', async () => { - configureStubs(true, ProductInstallStatus.Installed, ProductInstallStatus.NotInstalled, 'No'); - sandbox.stub(applicationShell, 'showQuickPick').resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - const session = (await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - )) as TensorBoardSession; - assert.ok(session.panel?.visible, 'Webview panel not shown, expected successful session creation'); - }); - }); - test('If python.tensorBoard.logDirectory is provided, do not prompt user to pick a log directory', async () => { - const selectDirectoryStub = sandbox - .stub(applicationShell, 'showQuickPick') - .resolves({ label: TensorBoard.useCurrentWorkingDirectory }); - errorMessageStub = sandbox.stub(applicationShell, 'showErrorMessage'); - await workspaceConfiguration.update('logDirectory', 'logs/fit', true); - - const session = (await commandManager.executeCommand( - 'python.launchTensorBoard', - TensorBoardEntrypoint.palette, - TensorBoardEntrypointTrigger.palette, - )) as TensorBoardSession; - - assert.ok(session.panel?.visible, 'Expected successful session creation but webpanel not shown'); - assert.ok(errorMessageStub.notCalled, 'Expected successful session creation but error message was shown'); - assert.ok( - selectDirectoryStub.notCalled, - 'Prompted user to select log directory although setting was specified', - ); - }); - suite('Jump to source', async () => { - // We can't test a full E2E scenario with the TB profiler plugin because we can't - // accurately target simulated clicks at iframed content. This only tests - // code from the moment that the VS Code webview posts a message back - // to the extension. - const fsPath = path.join( - EXTENSION_ROOT_DIR_FOR_TESTS, - 'src', - 'test', - 'python_files', - 'tensorBoard', - 'sourcefile.py', - ); - teardown(() => { - sandbox.restore(); - }); - function setupStubsForMultiStepInput() { - // Stub the factory to return our stubbed multistep input when it's asked to create one - const multiStepFactory = serviceManager.get(IMultiStepInputFactory); - const inputInstance = multiStepFactory.create(); - // Create a multistep input with stubs for methods - const showQuickPickStub = sandbox.stub(inputInstance, 'showQuickPick').resolves({ - label: TensorBoard.selectMissingSourceFile, - description: TensorBoard.selectMissingSourceFileDescription, - }); - const createInputStub = sandbox - .stub(multiStepFactory, 'create') - .returns(inputInstance as IMultiStepInput); - // Stub the system file picker - const filePickerStub = sandbox.stub(applicationShell, 'showOpenDialog').resolves([Uri.file(fsPath)]); - return [showQuickPickStub, createInputStub, filePickerStub]; - } - test('Resolves filepaths without displaying prompt', async () => { - const session = ((await createSession()) as unknown) as ITensorBoardSessionTestAPI; - const stubs = setupStubsForMultiStepInput(); - await session.jumpToSource(fsPath, 0); - assert.ok(window.activeTextEditor !== undefined, 'Source file not resolved'); - assert.ok(window.activeTextEditor?.document.uri.fsPath === fsPath, 'Wrong source file opened'); - assert.ok( - stubs.reduce((prev, current) => current.notCalled && prev, true), - 'Stubs were called when file is present', - ); - }); - test('Display quickpick to user if filepath is not on disk', async () => { - const session = ((await createSession()) as unknown) as ITensorBoardSessionTestAPI; - const stubs = setupStubsForMultiStepInput(); - await session.jumpToSource('/nonexistent/file/path.py', 0); - assert.ok(window.activeTextEditor !== undefined, 'Source file not resolved'); - assert.ok(window.activeTextEditor?.document.uri.fsPath === fsPath, 'Wrong source file opened'); - assert.ok( - stubs.reduce((prev, current) => current.calledOnce && prev, true), - 'Stubs called an unexpected number of times', - ); - }); - }); -}); diff --git a/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts b/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts deleted file mode 100644 index 7eba1805c8bf..000000000000 --- a/src/test/tensorBoard/tensorBoardUsageTracker.unit.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { assert } from 'chai'; -import * as sinon from 'sinon'; -import { anything, instance, mock, reset, when } from 'ts-mockito'; -import { TensorBoardUsageTracker } from '../../client/tensorBoard/tensorBoardUsageTracker'; -import { TensorBoardPrompt } from '../../client/tensorBoard/tensorBoardPrompt'; -import { MockDocumentManager } from '../mocks/mockDocumentManager'; -import { createTensorBoardPromptWithMocks } from './helpers'; -import { mockedVSCodeNamespaces } from '../vscode-mock'; -import { TensorboardExperiment } from '../../client/tensorBoard/tensorboarExperiment'; - -[true, false].forEach((tbExtensionInstalled) => { - suite(`Tensorboard Extension is ${tbExtensionInstalled ? 'installed' : 'not installed'}`, () => { - suite('TensorBoard usage tracker', () => { - let experiment: TensorboardExperiment; - let documentManager: MockDocumentManager; - let tensorBoardImportTracker: TensorBoardUsageTracker; - let prompt: TensorBoardPrompt; - let showNativeTensorBoardPrompt: sinon.SinonSpy; - - suiteSetup(() => { - reset(mockedVSCodeNamespaces.extensions); - when(mockedVSCodeNamespaces.extensions?.getExtension(anything())).thenReturn(undefined); - }); - suiteTeardown(() => reset(mockedVSCodeNamespaces.extensions)); - setup(() => { - sinon.stub(TensorboardExperiment, 'isTensorboardExtensionInstalled').returns(tbExtensionInstalled); - experiment = mock(); - documentManager = new MockDocumentManager(); - prompt = createTensorBoardPromptWithMocks(); - showNativeTensorBoardPrompt = sinon.spy(prompt, 'showNativeTensorBoardPrompt'); - tensorBoardImportTracker = new TensorBoardUsageTracker( - documentManager, - [], - prompt, - instance(experiment), - ); - }); - - test('Simple tensorboard import in Python file', async () => { - const document = documentManager.addDocument('import tensorboard', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('Simple tensorboardX import in Python file', async () => { - const document = documentManager.addDocument('import tensorboardX', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('Simple tensorboard import in Python ipynb', async () => { - const document = documentManager.addDocument('import tensorboard', 'foo.ipynb'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`from x.y.tensorboard import z` import', async () => { - const document = documentManager.addDocument( - 'from torch.utils.tensorboard import SummaryWriter', - 'foo.py', - ); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`from x.y import tensorboard` import', async () => { - const document = documentManager.addDocument('from torch.utils import tensorboard', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`from tensorboardX import x` import', async () => { - const document = documentManager.addDocument('from tensorboardX import SummaryWriter', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`import x, y` import', async () => { - const document = documentManager.addDocument('import tensorboard, tensorflow', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('`import pkg as _` import', async () => { - const document = documentManager.addDocument('import tensorboard as tb', 'foo.py'); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('Show prompt on changed text editor', async () => { - await tensorBoardImportTracker.activate(); - const document = documentManager.addDocument('import tensorboard as tb', 'foo.py'); - await documentManager.showTextDocument(document); - assert.ok(showNativeTensorBoardPrompt.calledOnce); - }); - test('Do not show prompt if no tensorboard import', async () => { - const document = documentManager.addDocument( - 'import tensorflow as tf\nfrom torch.utils import foo', - 'foo.py', - ); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.notCalled); - }); - test('Do not show prompt if language is not Python', async () => { - const document = documentManager.addDocument( - 'import tensorflow as tf\nfrom torch.utils import foo', - 'foo.cpp', - 'cpp', - ); - await documentManager.showTextDocument(document); - await tensorBoardImportTracker.activate(); - assert.ok(showNativeTensorBoardPrompt.notCalled); - }); - }); - }); -});