Skip to content

Commit

Permalink
Throttle printing repl output to output window
Browse files Browse the repository at this point in the history
When an excessive amount of output is produced by the repl (for example
as a result of a rogue loop that is printing to stdout) Calva can
sometimes hang while trying to write all the output to the output
window/file.

The only way to resolve is to restart/reload the VSCode window.

This commit introduces a new config entry `replOutputThrottleRate` which
when set to a non-0 number will throttle output from the repl
connection. If more output items are received than the throttle rate in
a 500ms window then they will just be dropped.

Addresses #942
Fixes #2010
  • Loading branch information
julienvincent committed Jan 8, 2023
1 parent 21304f0 commit ac022a4
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changes to Calva.

## [Unreleased]

- Fix: [Rogue loops that print output to stdout cause Calva to hang](https://github.com/BetterThanTomorrow/calva/issues/2010)
## [2.0.323] - 2023-01-07

- Fix: [Provider completions not handling errors gracefully](https://github.com/BetterThanTomorrow/calva/issues/2006)
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,11 @@
"lsp"
]
},
"calva.replOutputThrottleRate": {
"markdownDescription": "If the repl outputs too quickly then results will be dropped from the output window. Setting this to 0 will disable throttling.",
"type": "number",
"default": 100
},
"calva.depsEdnJackInExecutable": {
"markdownDescription": "Which executable should Calva Jack-in use for starting a deps.edn project? The default is to let Calva choose. It will choose `clojure` if that is installed and working. Otherwise `deps.clj`, which is bundled with Calva, will be used. (This settings has no effect on Windows, where `deps.clj` will always be used.)",
"enum": [
Expand Down
3 changes: 3 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const REPL_FILE_EXT = 'calva-repl';
const KEYBINDINGS_ENABLED_CONFIG_KEY = 'calva.keybindingsEnabled';
const KEYBINDINGS_ENABLED_CONTEXT_KEY = 'calva:keybindingsEnabled';

const REPL_OUTPUT_THROTTLE_RATE_CONFIG_KEY = 'calva.replOutputThrottleRate';

type ReplSessionType = 'clj' | 'cljs';

// include the 'file' and 'untitled' to the
Expand Down Expand Up @@ -234,6 +236,7 @@ export {
REPL_FILE_EXT,
KEYBINDINGS_ENABLED_CONFIG_KEY,
KEYBINDINGS_ENABLED_CONTEXT_KEY,
REPL_OUTPUT_THROTTLE_RATE_CONFIG_KEY,
documentSelector,
ReplSessionType,
getConfig,
Expand Down
37 changes: 37 additions & 0 deletions src/results-output/results-doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import { formatAsLineComments, splitEditQueueForTextBatching } from './util';

const RESULTS_DOC_NAME = `output.${config.REPL_FILE_EXT}`;

const REPL_OUTPUT_THROTTLE_RATE = vscode.workspace
.getConfiguration()
.get<number>(config.REPL_OUTPUT_THROTTLE_RATE_CONFIG_KEY);
const PROMPT_HINT = 'Use `alt+enter` to evaluate';

const START_GREETINGS = [
Expand Down Expand Up @@ -331,6 +334,16 @@ export interface OnAppendedCallback {

let resultsBuffer: ResultsBuffer = [];

type BufferThrottleState = {
count: number;
dropped: number;
timeout?: NodeJS.Timeout;
};
const throttleState: BufferThrottleState = {
count: 0,
dropped: 0,
};

async function writeNextOutputBatch() {
if (!resultsBuffer[0]) {
return;
Expand Down Expand Up @@ -366,6 +379,30 @@ async function flushOutput() {

/* If something must be done after a particular edit, use the onAppended callback. */
export function append(text: string, onAppended?: OnAppendedCallback): void {
if (REPL_OUTPUT_THROTTLE_RATE > 0) {
throttleState.count++;

if (!throttleState.timeout) {
throttleState.timeout = setTimeout(() => {
if (throttleState.dropped > 0) {
resultsBuffer.push({
text: `;; Dropped ${throttleState.dropped} items from output due to throttling\n`,
});
void flushOutput();
}

throttleState.timeout = undefined;
throttleState.count = 0;
throttleState.dropped = 0;
}, 500);
}

if (throttleState.count > REPL_OUTPUT_THROTTLE_RATE) {
throttleState.dropped++;
return;
}
}

resultsBuffer.push({ text, onAppended });
void flushOutput();
}
Expand Down

0 comments on commit ac022a4

Please sign in to comment.