From 839ed3715153a8276a64e850650825e628565fb7 Mon Sep 17 00:00:00 2001 From: Julien Vincent Date: Sun, 8 Jan 2023 22:12:07 +0000 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + package.json | 5 +++++ src/config.ts | 3 +++ src/results-output/results-doc.ts | 37 +++++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b496252d6..e26f622f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/package.json b/package.json index 55340976e..67818a754 100644 --- a/package.json +++ b/package.json @@ -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": [ diff --git a/src/config.ts b/src/config.ts index c61b7d911..c8745413b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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 @@ -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, diff --git a/src/results-output/results-doc.ts b/src/results-output/results-doc.ts index 480e78953..952416787 100644 --- a/src/results-output/results-doc.ts +++ b/src/results-output/results-doc.ts @@ -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(config.REPL_OUTPUT_THROTTLE_RATE_CONFIG_KEY); const PROMPT_HINT = 'Use `alt+enter` to evaluate'; const START_GREETINGS = [ @@ -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; @@ -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`, + }); + 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(); }