Skip to content

Commit b20faf0

Browse files
authored
Detect tfevent files in workspace and prompt to launch TensorBoard (#14752)
1 parent f9838bf commit b20faf0

File tree

8 files changed

+189
-10
lines changed

8 files changed

+189
-10
lines changed

news/1 Enhancements/14807.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Detect tfevent files in workspace and prompt to launch native TensorBoard session.

package.nls.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,9 @@
222222
"StartPage.badWebPanelFormatString": "<html><body><h1>{0} is not a valid file name</h1></body></html>",
223223
"Jupyter.extensionRequired": "The Jupyter extension is required to perform that task. Click Yes to open the Jupyter extension installation page.",
224224
"TensorBoard.logDirectoryPrompt" : "Please select a log directory to start TensorBoard with.",
225-
"TensorBoard.installPrompt" : "The package TensorBoard is required in order to launch a TensorBoard session. Would you like to install it?",
226-
"TensorBoard.failedToStartSessionError" : "We failed to start a TensorBoard session due to the following error: {0}",
227225
"TensorBoard.progressMessage" : "Starting TensorBoard session...",
226+
"TensorBoard.failedToStartSessionError" : "We failed to start a TensorBoard session due to the following error: {0}",
227+
"TensorBoard.nativeTensorBoardPrompt" : "VS Code now has native TensorBoard support. Would you like to launch TensorBoard?",
228228
"TensorBoard.usingCurrentWorkspaceFolder": "We are using the current workspace folder as the log directory for your TensorBoard session.",
229229
"TensorBoard.selectAFolder": "Select a folder"
230230
}

src/client/common/application/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -795,7 +795,8 @@ export interface IWorkspaceService {
795795
* will be matched against the file paths of resulting matches relative to their workspace. Use a [relative pattern](#RelativePattern)
796796
* to restrict the search results to a [workspace folder](#WorkspaceFolder).
797797
* @param exclude A [glob pattern](#GlobPattern) that defines files and folders to exclude. The glob pattern
798-
* will be matched against the file paths of resulting matches relative to their workspace.
798+
* will be matched against the file paths of resulting matches relative to their workspace. If `undefined` is passed,
799+
* the glob patterns excluded in the `search.exclude` setting will be applied.
799800
* @param maxResults An upper-bound for the result.
800801
* @param token A token that can be used to signal cancellation to the underlying search engine.
801802
* @return A thenable that resolves to an array of resource identifiers. Will return no results if no

src/client/common/application/workspace.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,13 @@ export class WorkspaceService implements IWorkspaceService {
5252
}
5353
public createFileSystemWatcher(
5454
globPattern: GlobPattern,
55-
_ignoreCreateEvents?: boolean,
55+
ignoreCreateEvents?: boolean,
5656
ignoreChangeEvents?: boolean,
5757
ignoreDeleteEvents?: boolean
5858
): FileSystemWatcher {
5959
return workspace.createFileSystemWatcher(
6060
globPattern,
61-
ignoreChangeEvents,
61+
ignoreCreateEvents,
6262
ignoreChangeEvents,
6363
ignoreDeleteEvents
6464
);
@@ -69,7 +69,8 @@ export class WorkspaceService implements IWorkspaceService {
6969
maxResults?: number,
7070
token?: CancellationToken
7171
): Thenable<Uri[]> {
72-
return workspace.findFiles(include, exclude, maxResults, token);
72+
const excludePattern = exclude === undefined ? this.searchExcludes : exclude;
73+
return workspace.findFiles(include, excludePattern, maxResults, token);
7374
}
7475
public getWorkspaceFolderIdentifier(resource: Resource, defaultValue: string = ''): string {
7576
const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined;
@@ -79,4 +80,10 @@ export class WorkspaceService implements IWorkspaceService {
7980
)
8081
: defaultValue;
8182
}
83+
84+
private get searchExcludes() {
85+
const searchExcludes = this.getConfiguration('search.exclude');
86+
const enabledSearchExcludes = Object.keys(searchExcludes).filter((key) => searchExcludes.get(key) === true);
87+
return `{${enabledSearchExcludes.join(',')}}`;
88+
}
8289
}

src/client/common/utils/localize.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,14 @@ export namespace TensorBoard {
139139
'Please select a log directory to start TensorBoard with.'
140140
);
141141
export const progressMessage = localize('TensorBoard.progressMessage', 'Starting TensorBoard session...');
142-
export const installTensorBoardPrompt = localize(
143-
'TensorBoard.installPrompt',
144-
'The package TensorBoard is required in order to launch a TensorBoard session. Would you like to install it?'
145-
);
146142
export const failedToStartSessionError = localize(
147143
'TensorBoard.failedToStartSessionError',
148144
'We failed to start a TensorBoard session due to the following error: {0}'
149145
);
146+
export const nativeTensorBoardPrompt = localize(
147+
'TensorBoard.nativeTensorBoardPrompt',
148+
'VS Code now has native TensorBoard support. Would you like to launch TensorBoard?'
149+
);
150150
export const usingCurrentWorkspaceFolder = localize(
151151
'TensorBoard.usingCurrentWorkspaceFolder',
152152
'We are using the current workspace folder as the log directory for your TensorBoard session.'

src/client/tensorBoard/serviceRegistry.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@
33

44
import { IExtensionSingleActivationService } from '../activation/types';
55
import { IServiceManager } from '../ioc/types';
6+
import { TensorBoardFileWatcher } from './tensorBoardFileWatcher';
7+
import { TensorBoardPrompt } from './tensorBoardPrompt';
68
import { TensorBoardSessionProvider } from './tensorBoardSessionProvider';
79

810
export function registerTypes(serviceManager: IServiceManager) {
911
serviceManager.addSingleton<IExtensionSingleActivationService>(
1012
IExtensionSingleActivationService,
1113
TensorBoardSessionProvider
1214
);
15+
serviceManager.addSingleton<IExtensionSingleActivationService>(
16+
IExtensionSingleActivationService,
17+
TensorBoardFileWatcher
18+
);
19+
serviceManager.addSingleton<TensorBoardPrompt>(TensorBoardPrompt, TensorBoardPrompt);
1320
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { inject, injectable } from 'inversify';
5+
import { FileSystemWatcher, RelativePattern, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode';
6+
import { IExtensionSingleActivationService } from '../activation/types';
7+
import { IWorkspaceService } from '../common/application/types';
8+
import { NativeTensorBoard } from '../common/experiments/groups';
9+
import { traceError } from '../common/logger';
10+
import { IDisposableRegistry, IExperimentService } from '../common/types';
11+
import { TensorBoardPrompt } from './tensorBoardPrompt';
12+
13+
@injectable()
14+
export class TensorBoardFileWatcher implements IExtensionSingleActivationService {
15+
private fileSystemWatchers = new Map<WorkspaceFolder, FileSystemWatcher[]>();
16+
private globPattern1 = '*tfevents*';
17+
private globPattern2 = '*/*tfevents*';
18+
19+
constructor(
20+
@inject(IWorkspaceService) private workspaceService: IWorkspaceService,
21+
@inject(TensorBoardPrompt) private tensorBoardPrompt: TensorBoardPrompt,
22+
@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry,
23+
@inject(IExperimentService) private experimentService: IExperimentService
24+
) {}
25+
26+
public async activate() {
27+
if (!(await this.experimentService.inExperiment(NativeTensorBoard.experiment))) {
28+
return;
29+
}
30+
31+
const folders = this.workspaceService.workspaceFolders;
32+
if (!folders) {
33+
return;
34+
}
35+
36+
// Look for pre-existing tfevent files, as the file watchers will only pick up files
37+
// created or changed after they have been registered and hooked up. Just one will do.
38+
await this.promptIfWorkspaceHasPreexistingFiles();
39+
40+
// If the user creates or changes tfevent files, listen for those too
41+
for (const folder of folders) {
42+
this.createFileSystemWatcher(folder);
43+
}
44+
45+
// If workspace folders change, ensure we update our FileSystemWatchers
46+
this.disposables.push(
47+
this.workspaceService.onDidChangeWorkspaceFolders((e) => this.updateFileSystemWatchers(e))
48+
);
49+
}
50+
51+
private async promptIfWorkspaceHasPreexistingFiles() {
52+
try {
53+
for (const pattern of [this.globPattern1, this.globPattern2]) {
54+
const matches = await this.workspaceService.findFiles(pattern, undefined, 1);
55+
if (matches.length > 0) {
56+
await this.tensorBoardPrompt.showNativeTensorBoardPrompt();
57+
return;
58+
}
59+
}
60+
} catch (e) {
61+
traceError(
62+
`Failed to prompt to launch TensorBoard session based on preexisting tfevent files in workspace: ${e}`
63+
);
64+
}
65+
}
66+
67+
private async updateFileSystemWatchers(event: WorkspaceFoldersChangeEvent) {
68+
for (const added of event.added) {
69+
this.createFileSystemWatcher(added);
70+
}
71+
for (const removed of event.removed) {
72+
const fileSystemWatchers = this.fileSystemWatchers.get(removed);
73+
if (fileSystemWatchers) {
74+
fileSystemWatchers.forEach((fileWatcher) => fileWatcher.dispose());
75+
this.fileSystemWatchers.delete(removed);
76+
}
77+
}
78+
}
79+
80+
private createFileSystemWatcher(folder: WorkspaceFolder) {
81+
const fileWatchers = [];
82+
for (const pattern of [this.globPattern1, this.globPattern2]) {
83+
const relativePattern = new RelativePattern(folder, pattern);
84+
const fileSystemWatcher = this.workspaceService.createFileSystemWatcher(relativePattern);
85+
86+
// When a file is created or changed that matches `this.globPattern`, try to show our prompt
87+
this.disposables.push(
88+
fileSystemWatcher.onDidCreate((_uri) => this.tensorBoardPrompt.showNativeTensorBoardPrompt())
89+
);
90+
this.disposables.push(
91+
fileSystemWatcher.onDidChange((_uri) => this.tensorBoardPrompt.showNativeTensorBoardPrompt())
92+
);
93+
this.disposables.push(fileSystemWatcher);
94+
fileWatchers.push(fileSystemWatcher);
95+
}
96+
this.fileSystemWatchers.set(folder, fileWatchers);
97+
}
98+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { inject, injectable } from 'inversify';
5+
import { IApplicationShell, ICommandManager } from '../common/application/types';
6+
import { Commands } from '../common/constants';
7+
import { IPersistentState, IPersistentStateFactory } from '../common/types';
8+
import { Common, TensorBoard } from '../common/utils/localize';
9+
10+
enum TensorBoardPromptStateKeys {
11+
ShowNativeTensorBoardPrompt = 'showNativeTensorBoardPrompt'
12+
}
13+
14+
@injectable()
15+
export class TensorBoardPrompt {
16+
private state: IPersistentState<boolean>;
17+
private enabled: Promise<boolean> | undefined;
18+
private waitingForUserSelection: boolean = false;
19+
20+
constructor(
21+
@inject(IApplicationShell) private applicationShell: IApplicationShell,
22+
@inject(ICommandManager) private commandManager: ICommandManager,
23+
@inject(IPersistentStateFactory) private persistentStateFactory: IPersistentStateFactory
24+
) {
25+
this.state = this.persistentStateFactory.createWorkspacePersistentState<boolean>(
26+
TensorBoardPromptStateKeys.ShowNativeTensorBoardPrompt,
27+
true
28+
);
29+
this.enabled = this.isPromptEnabled();
30+
}
31+
32+
public async showNativeTensorBoardPrompt() {
33+
if ((await this.enabled) && !this.waitingForUserSelection) {
34+
const yes = Common.bannerLabelYes();
35+
const no = Common.bannerLabelNo();
36+
const doNotAskAgain = Common.doNotShowAgain();
37+
const options = [yes, no, doNotAskAgain];
38+
this.waitingForUserSelection = true;
39+
const selection = await this.applicationShell.showInformationMessage(
40+
TensorBoard.nativeTensorBoardPrompt(),
41+
...options
42+
);
43+
this.waitingForUserSelection = false;
44+
switch (selection) {
45+
case yes:
46+
await this.commandManager.executeCommand(Commands.LaunchTensorBoard);
47+
await this.disablePrompt();
48+
break;
49+
case doNotAskAgain:
50+
await this.disablePrompt();
51+
break;
52+
default:
53+
break;
54+
}
55+
}
56+
}
57+
58+
private async isPromptEnabled() {
59+
return this.state.value;
60+
}
61+
62+
private async disablePrompt() {
63+
await this.state.updateValue(false);
64+
}
65+
}

0 commit comments

Comments
 (0)