Skip to content

LiveText: announce large bursts via a pumped generator#20177

Merged
seanbudd merged 20 commits into
nvaccess:masterfrom
ethindp:live-text-queue-fixes
May 26, 2026
Merged

LiveText: announce large bursts via a pumped generator#20177
seanbudd merged 20 commits into
nvaccess:masterfrom
ethindp:live-text-queue-fixes

Conversation

@ethindp

@ethindp ethindp commented May 19, 2026

Copy link
Copy Markdown
Contributor

Link to issue number:

Fixes #6291. Partially addresses #15786, #15850, #11002, and #14189. All of these describe the same general class of "NVDA stops responding during console flood" problem but also include symptoms (UIA event flooding, NVDA crashes destroying the console window, etc.) that are out of scope for this PR and were partly addressed in #14888 and #14067 anyway. Related to #2977 and #875.

Most of these are already closed, but the underlying root cause (which is the main thread blocking during line announcement) was never actually addressed.

Summary of the issue:

When a live-text region (such as a terminal) produces a large burst of new text in a short window, NVDA's main thread blocks announcing every line sequentially, becoming unresponsive to keyboard input until the burst finishes. In some cases this lasts tens of seconds. In other (more drastic) cases, it can last so long that the user has to sign out, restart, or force-kill NVDA.

Description of user facing changes:

NVDA no longer freezes when large bursts of text are reported in a live region.

Description of developer facing changes:

LiveText._reportNewLines is no longer guaranteed to report lines synchronously. Internally, it now registers a generator with queueHandler which yields between batches of lines so the main thread can service input during long announcements.

A new class attribute LiveText.MAX_LINES caps the number of lines announced per burst. We drop everything before that on the floor.

When a new burst arrives while a previous one is still being announced, the previous generator is cancelled and the new burst supersedes it.

Description of development approach:

Live-text regions announce new text via LiveText._reportNewLines, which was invoked synchronously from the main thread's event queue. For workloads producing substantial amounts of new lines in a short window, every line traverses the full speech pipeline sequentially without yielding, which can leave the main thread unresponsive to keyboard input for tens of seconds or longer.

This PR addresses the responsiveness problem at the announcement layer rather than at the diff or event-source layer, which were previously addressed by #14888 and #14067. Specifically:

(1) Bursts larger than MAX_LINES are truncated to the most recent N lines. Skipping the oldest lines was chosen over skipping the newest because most flooding scenarios carry their actionable content at the end.

(2) Reporting now happens via a generator registered with queueHandler.registerGeneratorObject, yielding every 5 lines. This is the same primitive previously used by speech.speakSpelling and the original sayAll. Between yields, the main thread services the event queue, which keeps NVDA responsive.

(3) A handler is registered with pre_speechCanceled so that pressing control (or any other speech-cancel trigger) also cancels the in-flight generator. Perceptibly this may not cancel immediately and up to 5 lines may still be announced past the cancel, but internally the generator won't proceed past that iteration.

Testing strategy:

Reproducing this bug is rather tricky and I'm not sure what the most reliable reproducer is. However, here are some things that trigger it for me:

  1. In terminal, find a python project you haven't updated in a while and run uv sync --upgrade. Another alternative is to just uv add a bunch of packages. UV constantly sends ASCII control sequences and redraw commands to the terminal, and NVDA will freeze trying to process every single one.
  2. In power shell, dump the system event log with Get-EventLog -LogName System. Unless PowerShell has changed this command, this should freeze NVDA for at least a second.
  3. In an SSH session on a server or WSL2 instance, either:
    1. Dump the journal with journalctl -xfe, or
    2. On some systems like fedora, install a huge number of packages (> 5000) with a command like dnf install texlive-*.

I've no doubt there are many other ways this bug can be triggered. Essentially, any action that sends a huge number of speak requests to NVDA extremely quickly should do it.

Known issues with pull request:

This PR does not address the case where a single line in an editable text control is extremely long (e.g., the Notepad/Notepad++ scenario in #13432). The bottleneck there is getTextWithFields in NVDAObjects/window/edit.py, which does COM round-trips to extract the text and per-character formatting from the host process. In theory it could be addressed by chunking the speech and adding a fast path for uniform-format ranges, but that's a different PR with its own design questions.

The supersession behavior silently drops content from the previous burst when a new one arrives. In practice this is what users want for the common flooding scenarios (most recent state is most relevant), but I'm open to feedback if reviewers think a different policy is appropriate.

Code Review Checklist:

  • Documentation:
    • Change log entry
    • User Documentation
    • Developer / Technical Documentation
    • Context sensitive help for GUI changes
  • Testing:
    • Unit tests
    • System (end to end) tests
    • Manual testing
  • UX of all users considered:
    • Speech
    • Braille
    • Low Vision
    • Different web browsers
    • Localization in other languages / culture than English
  • API is compatible with existing add-ons.
  • Security precautions taken.

@cary-rowen

Copy link
Copy Markdown
Contributor

This looks like a super useful fix for anyone regularly running CLI-based AI agents. Might be worth taking this PR for a spin with a similar test case right away.

@ethindp

ethindp commented May 19, 2026

Copy link
Copy Markdown
Contributor Author

@cary-rowen I don't use agents myself, but I'd love more testing of it since I don't have a lot of cases to test this on my system atm. One candidate I'm 99 percent sure this solves is things like journalctl -xfe (or similar) where system logs get dumped to your terminal and you have to wade through the output.

@ethindp

ethindp commented May 19, 2026

Copy link
Copy Markdown
Contributor Author

@cary-rowen Just confirmed: this fixes the console flood problem with jorunalctl at least.

@ethindp ethindp marked this pull request as ready for review May 19, 2026 15:10
@ethindp ethindp requested a review from a team as a code owner May 19, 2026 15:10
@ethindp ethindp requested a review from SaschaCowley May 19, 2026 15:10
@seanbudd seanbudd requested review from michaelDCurran and seanbudd and removed request for SaschaCowley May 19, 2026 23:55
Comment thread source/NVDAObjects/behaviors.py
Comment thread source/NVDAObjects/behaviors.py Outdated
Comment thread source/NVDAObjects/behaviors.py Outdated
Comment thread source/NVDAObjects/behaviors.py Outdated
ethindp and others added 4 commits May 19, 2026 19:02
Comment thread source/NVDAObjects/behaviors.py Outdated
Comment thread source/NVDAObjects/behaviors.py Outdated
Comment thread source/NVDAObjects/behaviors.py Outdated
Comment thread source/NVDAObjects/behaviors.py Outdated
Comment thread source/NVDAObjects/behaviors.py Outdated
Comment thread source/NVDAObjects/behaviors.py Outdated
Co-authored-by: Sean Budd <seanbudd123@gmail.com>
@seanbudd

Copy link
Copy Markdown
Member

can this please get a change log entry and more clearer testing steps in the PR description?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR targets a long-standing responsiveness issue where large bursts of live-region text (notably terminal output) can block NVDA’s main thread while announcing many lines. It changes LiveText line reporting to truncate very large bursts and to announce lines via a queued, yielding generator so the main loop can continue pumping input/events.

Changes:

  • Add a per-burst cap on announced live-text lines (default 100) and announce how many lines were skipped when truncation occurs.
  • Replace synchronous per-line reporting with a queueHandler-registered generator that yields every N lines to keep NVDA responsive.
  • Cancel any in-flight live-text reporting generator when speech is canceled or when a new burst supersedes it.

Comment thread source/NVDAObjects/behaviors.py
Comment thread source/NVDAObjects/behaviors.py Outdated
@ethindp

ethindp commented May 20, 2026

Copy link
Copy Markdown
Contributor Author

@seanbudd I can do the change log at least, but I'm not really sure what a "better testing strategy" entails. Like, you can validate it by SSH'ing into a server that's been up for a month or something and doing journalctl -xfe, but in general all the bugs I've looked at typically encounter this through a variety of circumstances.

@seanbudd

Copy link
Copy Markdown
Member

I was asking for a clearer testing strategy, not a better one.

The section currently states

Tested with specific ls commands on my computer in directories with very large numbers of files. In NVDA 2026.1, this lagged for a couple seconds before recovering. Admittedly I need to test this more thoroughly, hence the draft.

Can you provide brief step by step instructions on what you did to test?

@ethindp ethindp requested a review from a team as a code owner May 22, 2026 01:49
@ethindp ethindp requested a review from Qchristensen May 22, 2026 01:49
@wmhn1872265132

Copy link
Copy Markdown
Contributor

Your merging method seems to be problematic. The differences in this PR are very large; please do not use the squash method to merge.

@ethindp

ethindp commented May 22, 2026

Copy link
Copy Markdown
Contributor Author

@wmhn1872265132 I mean, okay, if that's what you want...

@ethindp

ethindp commented May 23, 2026

Copy link
Copy Markdown
Contributor Author

Is there anything else that is required for this PR? Just checking since I'm pretty sure people are clamoring for this to get merged. :D I hope all the testing has worked out well (it seems to work fantastically for me).

Comment thread source/NVDAObjects/behaviors.py Outdated
Comment thread source/NVDAObjects/behaviors.py Outdated
@cary-rowen

Copy link
Copy Markdown
Contributor

Thanks @ethindp
Could you update the PR description to match the latest changes?

ethindp and others added 2 commits May 23, 2026 09:15
Co-authored-by: wencong <manchen_0528@outlook.com>
Co-authored-by: wencong <manchen_0528@outlook.com>
@ethindp

ethindp commented May 23, 2026

Copy link
Copy Markdown
Contributor Author

Will do

@ethindp

ethindp commented May 23, 2026

Copy link
Copy Markdown
Contributor Author

@cary-rowen All done.

@ethindp

ethindp commented May 25, 2026

Copy link
Copy Markdown
Contributor Author

I'm not entirely certain why the system checks are failing. Two of the runs can't find the Chrome window and the third fails with a speech timeout. are they blockers to this getting merged?

@seanbudd seanbudd merged commit 5344d4b into nvaccess:master May 26, 2026
38 of 42 checks passed
@github-actions github-actions Bot added this to the 2026.3 milestone May 26, 2026
@jcsteh

jcsteh commented May 26, 2026

Copy link
Copy Markdown
Contributor

Thanks for working on this. Something still doesn't make sense for me here, though. When we use LiveText, we should only be diffing a full screen at maximum, which means, say, 60 lines at an absolute maximum. _reportNewLines is called once for each diff, and since it is queued to the main thread from a background thread, that should mean the main thread naturally yields to input between batches of, say, 60 lines. This leads me to wonder: why was the main thread being blocked such that it couldn't process intervening input? Or was the queue eventually just so backed up that it just couldn't handle input fast enough for the user to perceive it as useful? Regardless, it definitely makes sense to reduce the contention on the main thread, especially as there's no benefit to throwing all of this at the synth if the user is very likely to interrupt it anyway, but it does leave me a little puzzled.

Just in case this helps anyone, another thing I discovered a while back when looking into this is that having logging set to i/o or lower can cause severe lag in this case because each line of speech gets logged and the log is then flushed to disk. This PR should help that too because we're ultimately logging less lines, but the only real solution there is to avoid setting the log level like this if you're doing a lot of spammy terminal stuff.

@codeofdusk

Copy link
Copy Markdown
Contributor

When we use LiveText, we should only be diffing a full screen at maximum

Not any more: the UIATextRange for Windows Terminal and Conhost points at the entire buffer, not just the visible text. I can't remember exactly why I didn't limit diffing to the visible text (GetVisibleRanges) in UIA now, but I suspect it was to do with issues with the alt buffer or weird behaviour when the window was minimized or maximized.

@codeofdusk

Copy link
Copy Markdown
Contributor

Yep fairly certain it was alt buffer related, see #12669 and microsoft/terminal#5406.

@jcsteh

jcsteh commented May 26, 2026

Copy link
Copy Markdown
Contributor

I can't remember exactly why I didn't limit diffing to the visible text (GetVisibleRanges) in UIA now

That feels like something we should look at fixing, even if we just limit it to the last 100 lines or similar. Diffing that many lines is much more expensive and it's ultimately pointless, since even a sighted user can only perceive what's changing on screen, not what's changing outside of it.

@codeofdusk

Copy link
Copy Markdown
Contributor

That feels like something we should look at fixing, even if we just limit it to the last 100 lines or similar.

That was done in this PR.

@jcsteh

jcsteh commented May 26, 2026

Copy link
Copy Markdown
Contributor

This PR limits what we report, not what we diff. We're still potentially (expensively) calculating diffs which we later throw away.

@jcsteh

jcsteh commented May 26, 2026

Copy link
Copy Markdown
Contributor

I'm guessing the calculation of diffs on larger blocks of text is probably also more expensive even if the actual diff is minimal, though I can't substantiate that with any data, so I could be wrong. Still, it might be premature optimisation unless proven otherwise.

@codeofdusk

codeofdusk commented May 26, 2026

Copy link
Copy Markdown
Contributor

This PR limits what we report, not what we diff. We're still potentially (expensively) calculating diffs which we later throw away.

Yes, you're right, and I did try bounding the diff at say the last 100 lines ages ago. (See a quick implementation at the limit-diffs branch of my fork). The problem is that if a large block (say 10,000 lines) is printed, NVDA will drop all but the last 100 at first, but subsequent commands in the session sometimes get stuck reading very old output because DMP doesn't have enough context. I'll see if I can come up with clear repro steps.

@ethindp

ethindp commented May 26, 2026

Copy link
Copy Markdown
Contributor Author

Why was the main thread being blocked such that it couldn't process intervening input?

Diffing and such may have been done on a background thread, but the actual line update reporting was not. Instead if you diff against this PR and a commit before it was merged, you'll see that _reportNewLines was literally a for loop that just reported each and every line in sequence until complete. Given that (per back-of-the-napkin-math) each speech.speak function takes maybe 10-50ms, this would lead to thousands upon thousands of updates trying to be synthesized on the main thread. Since no yielding of the thread was done, NVDA would hang until the function had completed and all speech had been synthesized correctly.

@jcsteh

jcsteh commented May 26, 2026

Copy link
Copy Markdown
Contributor

this would lead to thousands upon thousands of updates trying to be synthesized on the main thread. Since no yielding of the thread was done

To be more concrete, were you seeing (and still seeing) calls to _reportNewLines where len(lines) > several thousand? If so, UIA consoles are the only consumer that could have caused that, since they diff the entire buffer, where everything else only diffs the screen. We just shouldn't be calling _reportNewLines with that many lines in the first place. 60 maybe, since that's a full screen. Anything larger than that is pointless. Of course, fixing that would involve figuring out how to solve the UIA console diffing problem @codeofdusk notes above.

That said, I'm fairly sure I've heard of complaints about this from folks with the UIA console disabled. That case could not have been sending more than 60 or so lines at a time. There would have been a huge number of calls, but each would have naturally yielded to the queue after 60 lines or so.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NVDA hangs up in terminal, when a large piece of text is loaded

7 participants