fix(keybind-cheatsheet): keep runtime refreshes within the host CPU budget - #333
Open
rylos wants to merge 1 commit into
Open
fix(keybind-cheatsheet): keep runtime refreshes within the host CPU budget#333rylos wants to merge 1 commit into
rylos wants to merge 1 commit into
Conversation
…udget
Requesting a refresh at runtime aborted mid-parse with "script callback
exceeded its CPU budget", and because the aborted callback never got to
clear `refreshing`, every later refresh was swallowed as "already
running": the cheatsheet then showed stale bindings until the plugin was
reloaded. Measured on a 32 KB niri config, four consecutive refresh
requests updated the cache 0 times.
The niri parser now does markedly less work for the same result:
- the tokenizer locates whitespace runs, string bodies and bare words
with one string.find each instead of walking a character at a time;
- only `binds { … }` blocks are tokenized, since a real config spends
most of its bytes on outputs, layout and window rules;
- includes are collected with a single pattern pass rather than one
match per line.
Standalone, parsing that config drops from 4.18 ms to 1.32 ms, and the
bindings it produces are byte-identical: same count, keys, modifiers,
actions, descriptions, categories and source lines, verified against the
fixtures, a 32 KB real-world config, and cases covering commented-out
`binds` blocks, braces inside strings, multiple blocks, CRLF and
unterminated strings.
An in-flight refresh older than 15 seconds is now treated as lost, so an
abort can no longer wedge the service permanently.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Plugin
kenn/keybind-cheatsheetplugin.toml)@kenn — this touches your plugin, so it needs your sign-off. Happy to change
the approach or drop any part of it.
What it does
Fixes a refresh that dies halfway and takes the service with it.
Asking the plugin to refresh at runtime aborted with
script callback 'state watch callback' exceeded its CPU budget, part-way through parsing theniri config. The aborted callback never reached the line that clears
refreshing, so every later request was coalesced away as "already running"and the cheatsheet kept showing stale bindings until the plugin was reloaded.
Startup was never affected, which is why this only shows up after editing a
keybind: the panel simply never catches up.
Measured on my 32 KB / 1100-line
config.kdl(134 bindings), four refreshrequests spaced 18 s apart updated
bindings-cache.json0 times beforethis change and 2 times after. The remaining failures are the honest part
of this PR — see the last section.
The niri parser now does the same job with far fewer VM instructions:
bare words are each located with one
string.findrather than acharacter-at-a-time loop over tens of kilobytes.
binds { … }is tokenized. A real config spends most of its byteson outputs, layout, window rules and animations. The block scan is anchored
per line and skips block comments, so a commented-out
// binds {example isnot mistaken for the real thing; if no block is found, the whole file is
parsed exactly as before.
Standalone (
luau, same config),parseNiriContentdrops from 4.18 ms to1.32 ms. The bindings are byte-identical — same count, keys, modifiers,
actions, descriptions, categories and source lines.
Separately, an in-flight refresh older than 15 s is now treated as lost. A
callback the host aborts can no longer wedge the service permanently, which is
what turned a single failed refresh into a dead cheatsheet.
No behaviour, setting, translation or UI change.
plugin_apistays at 9.External dependencies
None added. The plugin still declares
hyprctl, which only the Hyprland Luapath invokes.
Testing
Ran the bundled fixture suite in Noctalia
(
noctalia msg plugin kenn/keybind-cheatsheet:data all self-test):mango 9/9, hypr_conf 5/5, hypr_lua 4/4, niri 5/5,
passed: true.Differential test of old vs new
parseNiriContentunderluau, comparingevery field of every binding including
sourceLine. Identical on: thebundled niri fixtures, a 32 KB real-world config (134 bindings), and hand-made
cases for commented-out
bindsblocks (//and/* */), a brace inside astring, two
bindsblocks in one file,bindson the first line, indentedand commented
includelines, CRLF, an unterminated string and an unclosedblock.
Token-level differential of the old and new tokenizer on the same inputs:
same tokens, values and line numbers.
Panel opened and refreshed from the bar widget, from the panel's refresh
button, and over IPC; bindings, categories, descriptions and source lines
render as before.
python3 .github/workflows/scripts/validate-plugins.pyandnoctalia plugins lint keybind-cheatsheetare clean.Tested on Niri
Tested on Hyprland
Tested on Sway
Tested on another compositor:
Noctalia version tested against: 5.0.0 (97917d9ca07e)
Plugin API level: 9
Screenshots / Videos
No visual change: same panel, same widget, same bindings. The bug and the fix
are both in
service.luau, and the differential test above is the evidencethat the rendered content is unchanged.
Where this is still short
Being straight about it: this reduces the cost, it does not put the parse
safely under the limit. Probing the host with a throwaway plugin,
onIpc,updateand state-watch callbacks all abort at roughly the same point — around13–15 ms of work — and parsing my config still lands close to that line. So a
refresh now sometimes succeeds instead of never succeeding, and a failure is no
longer permanent, but a larger config will still lose some refreshes.
The real fix is to make parsing incremental — carry a cursor across several
update()ticks and yield at token or binding boundaries — which is astructural change to your service that I did not want to make unilaterally. If
you would like it, I am happy to write it as a follow-up PR.
Checklist
idafter the/inplugin.tomlexactly.plugin.toml,README.md,thumbnail.webp, andtranslations/en.json.README.mdfollows theREADME template, documents
every entry id and dependency, and includes exact panel IPC commands and launcher prefixes where applicable.
thumbnail.webpwith the thumbnail generator.versionfollows semver and is bumped in this PR;plugin_apiis the oldest API level this plugin requires.understand that language well enough to review and maintain it (no unreviewed machine/LLM translations).
catalog.toml; CI generates it.Code review attestation
Plugins run as trusted, unsandboxed Luau in the user's session. Confirm:
licensedeclared inplugin.toml.