Skip to content

Commit

Permalink
Merge pull request #2121 from posit-dev/sagerb-debounce-on-watchers
Browse files Browse the repository at this point in the history
UX: Add debounce and throttling to main query methods
  • Loading branch information
sagerb authored Aug 21, 2024
2 parents 8959587 + 542d177 commit c11819d
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 46 deletions.
16 changes: 16 additions & 0 deletions extensions/vscode/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions extensions/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@
"dependencies": {
"@hypersphere/omnibus": "0.1.6",
"@vscode/codicons": "^0.0.36",
"async-mutex": "^0.5.0",
"axios": "^1.7.4",
"debounce": "^2.1.0",
"eventsource": "^2.0.2",
Expand Down
6 changes: 6 additions & 0 deletions extensions/vscode/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,9 @@ export const enum Views {
HelpAndFeedback = "posit.publisher.helpAndFeedback",
Logs = "posit.publisher.logs",
}

export const DebounceDelaysMS = {
file: 1000,
refreshRPackages: 1000,
refreshPythonPackages: 1000,
};
42 changes: 42 additions & 0 deletions extensions/vscode/src/utils/throttle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (C) 2024 by Posit Software, PBC.

import { Mutex, MutexInterface } from "async-mutex";

/**
* This method allows throttling of a method (typically an API call), where only
* one call is active, and the last one which is requested while an existing call
* is still active, is then executed after the current call completes.
*
* We do not need any intermediate calls to be executed, because we're using this functionality
* with pure functions, which we simply need to execute the latest request.
*
* For example, if all of these come in sequence very quickly, and the API call is long:
*
* call() // A
* call() // B
* call() // C
* call() // D
*
* Call A will be executed and if it takes a long time, only Call D will be executed. Under the
* covers, when Call B comes in, it is queued up using the mutex, but then Call C cancels Call B
* (using mutex.cancel()), and Call C waits on the mutex, then Call D cancels Call C and waits on
* the mutex, which is then executed when Call A releases the mutex.
*/
export const throttleWithLastPending = async (
mutex: Mutex,
fn: () => Promise<void>,
) => {
if (mutex.isLocked()) {
// we have calls waiting, so cancel them, since we're here now and taking their place
mutex.cancel();
}
let release: MutexInterface.Releaser | undefined;
try {
release = await mutex.acquire();
return await fn();
} finally {
if (release) {
release();
}
}
};
7 changes: 2 additions & 5 deletions extensions/vscode/src/utils/webviewConduit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,11 @@ export class WebviewConduit {
};

public sendMsg = (msg: HostToWebviewMessage) => {
const e = JSON.stringify(msg);
// don't send messages if the Webview hasn't initialized yet
if (!this.target) {
console.warn(
`Warning: WebviewConduit::sendMsg queueing up msg called before webview reference established with init(): ${e}`,
);
this.pendingMsgs.push(msg);
return;
}
const e = JSON.stringify(msg);
this.target.postMessage(e);
};
}
108 changes: 67 additions & 41 deletions extensions/vscode/src/views/homeView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
workspace,
} from "vscode";
import { isAxiosError } from "axios";
import { Mutex } from "async-mutex";

import {
Configuration,
Expand Down Expand Up @@ -68,18 +69,17 @@ import { selectNewOrExistingConfig } from "src/multiStepInputs/selectNewOrExisti
import { RPackage, RVersionConfig } from "src/api/types/packages";
import { calculateTitle } from "src/utils/titles";
import { ConfigWatcherManager, WatcherManager } from "src/watchers";
import { Commands, Contexts, Views } from "src/constants";
import { Commands, Contexts, DebounceDelaysMS, Views } from "src/constants";
import { showProgress } from "src/utils/progress";
import { newCredential } from "src/multiStepInputs/newCredential";
import { PublisherState } from "src/state";
import { throttleWithLastPending } from "src/utils/throttle";

enum HomeViewInitialized {
initialized = "initialized",
uninitialized = "uninitialized",
}

const fileEventDebounce = 200;

export class HomeViewProvider implements WebviewViewProvider, Disposable {
private disposables: Disposable[] = [];

Expand Down Expand Up @@ -137,8 +137,8 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable {
"activeConfigChanged",
(cfg: Configuration | ConfigurationError | undefined) => {
this.sendRefreshedFilesLists();
this.onRefreshPythonPackages();
this.onRefreshRPackages();
this.refreshPythonPackages();
this.refreshRPackages();

this.configWatchers?.dispose();
if (cfg && isConfigurationError(cfg)) {
Expand All @@ -147,33 +147,33 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable {
this.configWatchers = new ConfigWatcherManager(cfg);

this.configWatchers.configFile?.onDidChange(
this.sendRefreshedFilesLists,
this.debounceSendRefreshedFilesLists,
this,
);

this.configWatchers.pythonPackageFile?.onDidCreate(
this.onRefreshPythonPackages,
this.debounceRefreshPythonPackages,
this,
);
this.configWatchers.pythonPackageFile?.onDidChange(
this.onRefreshPythonPackages,
this.debounceRefreshPythonPackages,
this,
);
this.configWatchers.pythonPackageFile?.onDidDelete(
this.onRefreshPythonPackages,
this.debounceRefreshPythonPackages,
this,
);

this.configWatchers.rPackageFile?.onDidCreate(
this.onRefreshRPackages,
this.debounceRefreshRPackages,
this,
);
this.configWatchers.rPackageFile?.onDidChange(
this.onRefreshRPackages,
this.debounceRefreshRPackages,
this,
);
this.configWatchers.rPackageFile?.onDidDelete(
this.onRefreshRPackages,
this.debounceRefreshRPackages,
this,
);
},
Expand All @@ -197,9 +197,9 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable {
case WebviewToHostMessageType.SAVE_SELECTION_STATE:
return await this.onSaveSelectionState(msg);
case WebviewToHostMessageType.REFRESH_PYTHON_PACKAGES:
return await this.onRefreshPythonPackages();
return await this.debounceRefreshPythonPackages();
case WebviewToHostMessageType.REFRESH_R_PACKAGES:
return await this.onRefreshRPackages();
return await this.debounceRefreshRPackages();
case WebviewToHostMessageType.VSCODE_OPEN_RELATIVE:
return await this.onRelativeOpenVSCode(msg);
case WebviewToHostMessageType.SCAN_PYTHON_PACKAGE_REQUIREMENTS:
Expand All @@ -209,7 +209,7 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable {
case WebviewToHostMessageType.VSCODE_OPEN:
return await this.onVSCodeOpen(msg);
case WebviewToHostMessageType.REQUEST_FILES_LISTS:
return this.sendRefreshedFilesLists();
return this.debounceSendRefreshedFilesLists();
case WebviewToHostMessageType.INCLUDE_FILE:
return this.updateFileList(msg.content.path, FileAction.INCLUDE);
case WebviewToHostMessageType.EXCLUDE_FILE:
Expand Down Expand Up @@ -470,7 +470,12 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable {
);
}

private async onRefreshPythonPackages() {
public debounceRefreshPythonPackages = debounce(
this.refreshPythonPackages,
DebounceDelaysMS.refreshPythonPackages,
);

private async refreshPythonPackages() {
const activeConfiguration = await this.state.getSelectedConfiguration();
let pythonProject = true;
let packages: string[] = [];
Expand Down Expand Up @@ -512,7 +517,7 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable {
pythonProject = false;
} else {
const summary = getSummaryStringFromError(
"homeView::onRefreshPythonPackages",
"homeView::refreshPythonPackages",
error,
);
window.showInformationMessage(summary);
Expand All @@ -532,7 +537,12 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable {
});
}

private async onRefreshRPackages() {
public debounceRefreshRPackages = debounce(
this.refreshRPackages,
DebounceDelaysMS.refreshRPackages,
);

private async refreshRPackages() {
const activeConfiguration = await this.state.getSelectedConfiguration();
let rProject = true;
let packages: RPackage[] = [];
Expand Down Expand Up @@ -575,7 +585,7 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable {
rProject = false;
} else {
const summary = getSummaryStringFromError(
"homeView::onRefreshRPackages",
"homeView::refreshRPackages",
error,
);
window.showInformationMessage(summary);
Expand Down Expand Up @@ -1251,21 +1261,33 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable {
}
};

public refreshContentRecords = async () => {
await this.refreshContentRecordData();
this.updateWebViewViewContentRecords();
useBus().trigger(
"activeContentRecordChanged",
await this.state.getSelectedContentRecord(),
private refreshContentRecordsMutex = new Mutex();
public refreshContentRecords = () => {
return throttleWithLastPending(
this.refreshContentRecordsMutex,
async () => {
await this.refreshContentRecordData();
this.updateWebViewViewContentRecords();
useBus().trigger(
"activeContentRecordChanged",
await this.state.getSelectedContentRecord(),
);
},
);
};

public refreshConfigurations = async () => {
await this.refreshConfigurationData();
this.updateWebViewViewConfigurations();
useBus().trigger(
"activeConfigChanged",
await this.state.getSelectedConfiguration(),
private refreshConfigurationsMutex = new Mutex();
public refreshConfigurations = () => {
return throttleWithLastPending(
this.refreshConfigurationsMutex,
async () => {
await this.refreshConfigurationData();
this.updateWebViewViewConfigurations();
useBus().trigger(
"activeConfigChanged",
await this.state.getSelectedConfiguration(),
);
},
);
};

Expand Down Expand Up @@ -1299,6 +1321,10 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable {
}
};

public debounceSendRefreshedFilesLists = debounce(async () => {
return await this.sendRefreshedFilesLists();
}, DebounceDelaysMS.file);

public initiatePublish(target: PublishProcessParams) {
this.initiateDeployment(
target.deploymentName,
Expand Down Expand Up @@ -1612,7 +1638,7 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable {
),
commands.registerCommand(
Commands.PythonPackages.Refresh,
this.onRefreshPythonPackages,
this.refreshPythonPackages,
this,
),
commands.registerCommand(
Expand Down Expand Up @@ -1641,7 +1667,7 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable {
),
commands.registerCommand(
Commands.RPackages.Refresh,
this.onRefreshRPackages,
this.refreshRPackages,
this,
),
commands.registerCommand(
Expand All @@ -1651,6 +1677,7 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable {
),
);

// directories
watchers.positDir?.onDidDelete(() => {
this.refreshContentRecords();
this.refreshConfigurations();
Expand All @@ -1661,19 +1688,18 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable {
}, this);
watchers.contentRecordsDir?.onDidDelete(this.refreshContentRecords, this);

// configurations
watchers.configurations?.onDidCreate(this.refreshConfigurations, this);
watchers.configurations?.onDidDelete(this.refreshConfigurations, this);
watchers.configurations?.onDidChange(this.refreshConfigurations, this);
watchers.configurations?.onDidDelete(this.refreshConfigurations, this);

// content records
watchers.contentRecords?.onDidCreate(this.refreshContentRecords, this);
watchers.contentRecords?.onDidDelete(this.refreshContentRecords, this);
watchers.contentRecords?.onDidChange(this.refreshContentRecords, this);
watchers.contentRecords?.onDidDelete(this.refreshContentRecords, this);

const fileEventCallback = debounce(
this.sendRefreshedFilesLists,
fileEventDebounce,
);
watchers.allFiles?.onDidCreate(fileEventCallback, this);
watchers.allFiles?.onDidDelete(fileEventCallback, this);
// all files
watchers.allFiles?.onDidCreate(this.debounceSendRefreshedFilesLists, this);
watchers.allFiles?.onDidDelete(this.debounceSendRefreshedFilesLists, this);
}
}

0 comments on commit c11819d

Please sign in to comment.