Address console rendering and scrollback performance#13201
Conversation
|
E2E Tests 🚀 |
juliasilge
left a comment
There was a problem hiding this comment.
I built this and tested it out with the approach I outlined in #9852 (comment), and this is just massively, massively improved. Amazing! 🏆
This is a pretty sensitive area of the product, so Brian and I chatted about running more tests. They are kicked off here:
https://github.com/posit-dev/positron/actions/runs/24916997294
Other questions I have:
- Shouldn't the new unit test file be switched over to use Vitest? Related to #13033
- Should we add an additional test on
PositronConsoleInstance.trimScrollbackabout cleanup logic? IIUC from reading the code, it is at least a little tricky (deleting from_runtimeItemActivities, clearing_runtimeItemPendingInputwhen it gets spliced) and might be possible to break moving forward
|
If this work was branched from |
I rebased this branch on main a little while before creating the PR, so I think we're all set in this regard. |
|
I'm not sure about that; it looks like it is written as a Mocha test instead of with Vitest. Can you check out what we now say at: https://github.com/posit-dev/positron/blob/main/.claude/rules/core-tests.md |
jmcphers
left a comment
There was a problem hiding this comment.
This is working great for me. It's SUCH a big improvement and was so fast even when I pushed it heavily.
The only real perf issue I found is that with the new cap for console items, the rendering can get expensive on resize. Here's a repo ... run this R statement in your console:
library(crayon); for(i in 1:10000) cat(i, sapply(runif(3), function(x) do.call(crayon::make_style(sample(colors(),1)), list(formatC(x, digits=4, format="f")))), "\n")
Then drag the vertical separator between the console and the console tab list back and forth. Even on my M4 without any throttling it's very sluggish. We might consider debouncing the redraw during resize events so that resizing is smooth.
That's a pathological case, though, that we could address on followup. Epic PR!
| StorageTarget.MACHINE | ||
| ); | ||
| }, err => { | ||
| this._logService.warn(`Failed to reset ${scrollbackSizeSettingId} user override: ${err}`); |
There was a problem hiding this comment.
Do you think we should set the migration reset key here so we don't keep trying over and over on subsequent runs? In my experience automated settings JSON edits fail for a lot of reasons (e.g. user has a pre-existing JSON error in their file, etc.).
There was a problem hiding this comment.
I considered this for a while. I also asked Claude what it thought about this idea. It's answer was:
Q: Perhaps we should set the migration reset key in the error path so we don't keep trying over and over on subsequent runs when there's a failure. Do you agree?
A: I'd push back on this, actually. The current behavior is intentional and documented in the JSDoc above the
method — and I think it's right:
Setting the flag on failure means abandoning the migration. If the user keeps their (now-obsolete) override forever because of a transient write failure on first startup, that's a worse outcome than logging a warning a few times. The flag is a "we did the work" marker, not a "we tried" marker — flipping that semantic loses information.
Most plausible failure modes are transient:
- Settings file lock / write contention with another process
- Brief I/O hiccup
- Race during profile bootstrapping
For these, retrying is the recovery path.
For genuinely permanent failures (e.g. malformed settings.json, read-only filesystem), the warning is the user-visible signal that something needs attention. Suppressing it by stashing the flag would hide a real problem.
The cost of the current behavior is one async no-op + one log line per startup until the underlying issue is fixed. That seems cheap compared to silently abandoning a migration.
If you want to bound the retries, the right shape would be a counter (e.g. give up after 5 attempts), not flipping the success flag. But honestly I think the current behavior is fine as-is.
I think it might be fine as is.
81071fc to
7c1c5e0
Compare
juliasilge
left a comment
There was a problem hiding this comment.
Such a huge improvement!

Description
Addresses #9852.
Console performance improvements: memoized rendering + scrollback trimming
The console had two related performance problems. First, every item re-rendered on every append, so as the console grew, each new line got more expensive to show. Second, the existing scrollback cap wasn't actually freeing memory or work: the old code flipped a hidden flag on items that fell off the edge but left them in the list, so every render still walked the full history and every streamed output item kept its full ANSI state alive. The cap could also only act at the whole-item level, so a single long-running streamed activity (which accumulates many thousands of output lines into one item) was untouched regardless of its size.
This branch memoizes every runtime item the console can render. For static items that means a default memo comparison is enough; for mutable ones (notably the activity item that accumulates streamed output) a monotonic version counter is bumped whenever its content actually changes, and React uses that to decide whether to re-render. Items that didn't change are skipped entirely.
On top of that, every runtime item now participates in a unified scrollback trim. When the configured cap is exceeded the console walks its items from newest to oldest, letting each one report how much scrollback it takes and whether it trimmed itself in place, and splices off anything that fell out of scope. The trim runs on every append and also when the user lowers the scrollback setting.
Because rendering is now a lot cheaper, the default
console.scrollbackSizehas been raised to 10,000 items (with a new range of 1,000–20,000). A one-time-per-profile migration clears any stale user override so everyone picks up the new default, unless they explicitly set their own value from here on.New unit tests cover the trim walk, the version-bump semantics for activity-item mutations, and the new ANSI-output helper used when trimming streamed output.
Release Notes
New Features
Bug Fixes
QA Notes
Tests:
@:console
I've added additional tests in this PR.
I tested this on Windows and macOS.