Skip to content

Commit 839ed37

Browse files
committed
Throttle printing repl output to output window
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
1 parent 21304f0 commit 839ed37

File tree

4 files changed

+46
-0
lines changed

4 files changed

+46
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Changes to Calva.
44

55
## [Unreleased]
66

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

910
- Fix: [Provider completions not handling errors gracefully](https://github.com/BetterThanTomorrow/calva/issues/2006)

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,11 @@
770770
"lsp"
771771
]
772772
},
773+
"calva.replOutputThrottleRate": {
774+
"markdownDescription": "If the repl outputs too quickly then results will be dropped from the output window. Setting this to 0 will disable throttling.",
775+
"type": "number",
776+
"default": 100
777+
},
773778
"calva.depsEdnJackInExecutable": {
774779
"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.)",
775780
"enum": [

src/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const REPL_FILE_EXT = 'calva-repl';
1414
const KEYBINDINGS_ENABLED_CONFIG_KEY = 'calva.keybindingsEnabled';
1515
const KEYBINDINGS_ENABLED_CONTEXT_KEY = 'calva:keybindingsEnabled';
1616

17+
const REPL_OUTPUT_THROTTLE_RATE_CONFIG_KEY = 'calva.replOutputThrottleRate';
18+
1719
type ReplSessionType = 'clj' | 'cljs';
1820

1921
// include the 'file' and 'untitled' to the
@@ -234,6 +236,7 @@ export {
234236
REPL_FILE_EXT,
235237
KEYBINDINGS_ENABLED_CONFIG_KEY,
236238
KEYBINDINGS_ENABLED_CONTEXT_KEY,
239+
REPL_OUTPUT_THROTTLE_RATE_CONFIG_KEY,
237240
documentSelector,
238241
ReplSessionType,
239242
getConfig,

src/results-output/results-doc.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import { formatAsLineComments, splitEditQueueForTextBatching } from './util';
1717

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

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

2225
const START_GREETINGS = [
@@ -331,6 +334,16 @@ export interface OnAppendedCallback {
331334

332335
let resultsBuffer: ResultsBuffer = [];
333336

337+
type BufferThrottleState = {
338+
count: number;
339+
dropped: number;
340+
timeout?: NodeJS.Timeout;
341+
};
342+
const throttleState: BufferThrottleState = {
343+
count: 0,
344+
dropped: 0,
345+
};
346+
334347
async function writeNextOutputBatch() {
335348
if (!resultsBuffer[0]) {
336349
return;
@@ -366,6 +379,30 @@ async function flushOutput() {
366379

367380
/* If something must be done after a particular edit, use the onAppended callback. */
368381
export function append(text: string, onAppended?: OnAppendedCallback): void {
382+
if (REPL_OUTPUT_THROTTLE_RATE > 0) {
383+
throttleState.count++;
384+
385+
if (!throttleState.timeout) {
386+
throttleState.timeout = setTimeout(() => {
387+
if (throttleState.dropped > 0) {
388+
resultsBuffer.push({
389+
text: `;; Dropped ${throttleState.dropped} items from output due to throttling\n`,
390+
});
391+
flushOutput();
392+
}
393+
394+
throttleState.timeout = undefined;
395+
throttleState.count = 0;
396+
throttleState.dropped = 0;
397+
}, 500);
398+
}
399+
400+
if (throttleState.count > REPL_OUTPUT_THROTTLE_RATE) {
401+
throttleState.dropped++;
402+
return;
403+
}
404+
}
405+
369406
resultsBuffer.push({ text, onAppended });
370407
void flushOutput();
371408
}

0 commit comments

Comments
 (0)