From f87e7c076e14832526251b17c89c81f788fa4384 Mon Sep 17 00:00:00 2001 From: blacku Date: Thu, 30 Jul 2026 14:35:38 +0200 Subject: [PATCH] perf(keymap): reduce bind parser CPU cost (#157) * fix(keymap): reduce Hyprland bind ID CPU cost * perf(keymap): avoid bind hashing in all parsers --- keymap/mangowc_service.luau | 20 ++++++++++++-------- keymap/niri_service.luau | 14 ++++++++++++-- keymap/plugin.toml | 2 +- keymap/service.luau | 19 +++++++++++-------- keymap/tests/hypr_cpu_budget_test.lua | 2 +- keymap/tests/hypr_scale_test.lua | 26 +++++++++----------------- keymap/tests/mangowc_scale_test.lua | 24 +++++++++++++++++++----- keymap/tests/niri_cpu_budget_test.lua | 8 ++++++-- keymap/tests/niri_scale_test.lua | 9 ++++++--- 9 files changed, 77 insertions(+), 47 deletions(-) diff --git a/keymap/mangowc_service.luau b/keymap/mangowc_service.luau index eaa7134..f703a37 100644 --- a/keymap/mangowc_service.luau +++ b/keymap/mangowc_service.luau @@ -11,6 +11,7 @@ local SYSTEM_CONFIG = "/etc/mango/config.conf" local MAX_FILES = 64 local MAX_SOURCE_BYTES = 512 * 1024 local MAX_HIDDEN_BYTES = 2 * 1024 * 1024 +local EXACT_SOURCE_FINGERPRINT = "exact-v1" local refreshing = false local refreshQueued = false @@ -431,13 +432,16 @@ local function addCategory(context, name) return category end -local fingerprint - local function stableBindId(context, fields) - local identity = table.concat(fields, "|") + local encoded = {} + for _, value in ipairs(fields) do + local field = tostring(value or "") + encoded[#encoded + 1] = tostring(#field) .. ":" .. field + end + local identity = table.concat(encoded) local count = (context.bindIds[identity] or 0) + 1 context.bindIds[identity] = count - return "mango:" .. fingerprint(identity) .. (count > 1 and (":" .. tostring(count)) or "") + return "mango:" .. identity .. (count > 1 and (":" .. tostring(count)) or "") end -- FNV-1a over the exact source line. The editor recalculates this before a @@ -460,7 +464,7 @@ end local xorByteFast = type(bit32) == "table" and type(bit32.bxor) == "function" and bit32.bxor or xorByte -fingerprint = function(rawSnippet) +local function fingerprint(rawSnippet) local hash = 2166136261 for index = 1, #rawSnippet do local low = hash % 256 @@ -571,9 +575,9 @@ local function addBind( description = description or formatAction(action, args), dispatcher = trim(action), command = args, action = formatAction(action, args), mode = context.keymode, kind = kind, flags = parseFlags(suffix), activation = activation, - source = source, line = startLine or lineNumber, - start_line = startLine or lineNumber, end_line = lineNumber, - raw_snippet = rawSnippet, fingerprint = fingerprint(rawSnippet), + source = source, line = startLine or lineNumber, + start_line = startLine or lineNumber, end_line = lineNumber, + raw_snippet = rawSnippet, fingerprint = EXACT_SOURCE_FINGERPRINT, managed = source:match("([^/]+)$") == "keymap.conf", capabilities = editCapabilities(kind, action), _rawKey = kind == "bind" and trim(parts[2]) or key, diff --git a/keymap/niri_service.luau b/keymap/niri_service.luau index 0cb1471..06b380e 100644 --- a/keymap/niri_service.luau +++ b/keymap/niri_service.luau @@ -8,6 +8,7 @@ local REFRESH_REQUEST_KEY = "keymap.refresh_request" local MAX_FILES = 64 local MAX_SOURCE_BYTES = 512 * 1024 local MAX_HIDDEN_BYTES = 2 * 1024 * 1024 +local EXACT_SOURCE_FINGERPRINT = "exact-v1" local refreshing = false local refreshQueued = false @@ -468,6 +469,15 @@ local function stableFingerprint(value) return string.format("%08x", hash) end +local function stableBindId(fields) + local identity = {} + for _, value in ipairs(fields) do + local field = tostring(value or "") + identity[#identity + 1] = tostring(#field) .. ":" .. field + end + return "niri:" .. table.concat(identity) +end + local function sourceLines(source) local lines = {} for line in (source .. "\n"):gmatch("([^\n]*)\n") do lines[#lines + 1] = line end @@ -674,12 +684,12 @@ local function parseBind(combo, attributes, action, category, records, activeByC end local rawSnippet = provenance.raw_snippet or "" local bind = { - id = "niri:" .. stableFingerprint(signature .. "\0" .. verb .. "\0" .. normalizedAction), + id = stableBindId({ signature, verb, normalizedAction }), modifiers = modifiers, key = key, description = description, dispatcher = verb, activation = "press", command = command, action = normalizedAction, source = provenance.source, start_line = provenance.start_line, end_line = provenance.end_line, raw_snippet = rawSnippet, - fingerprint = stableFingerprint(rawSnippet), + fingerprint = EXACT_SOURCE_FINGERPRINT, managed = provenance.source:match("([^/]+)$") == "keymap.kdl", capabilities = { combo = true, category = true, description = true, diff --git a/keymap/plugin.toml b/keymap/plugin.toml index 9801db7..97dd27d 100644 --- a/keymap/plugin.toml +++ b/keymap/plugin.toml @@ -1,6 +1,6 @@ id = "blackbartblues/keymap" name = "Keymap" -version = "1.3.4" +version = "1.3.5" plugin_api = 9 author = "blackbartblues" license = "MIT" diff --git a/keymap/service.luau b/keymap/service.luau index 90100b3..0d56ca9 100644 --- a/keymap/service.luau +++ b/keymap/service.luau @@ -698,14 +698,17 @@ local function stableBindId(bind) dispatchIdentity = dispatcher .. ":" .. tostring(bind.arg or "") end local flags = boolFlag(bind.release) .. boolFlag(bind.mouse) .. boolFlag(bind.long_press) - local identity = table.concat({ - tostring(bind.submap or ""), - tostring(bind.modmask or 0), - tostring(bind.key or ""), - flags, - dispatchIdentity, - }, "|") - return "hypr:" .. fingerprint(identity) + local submap = tostring(bind.submap or "") + local modmask = tostring(bind.modmask or 0) + local key = tostring(bind.key or "") + return string.format( + "hypr:%d:%s%d:%s%d:%s%d:%s%d:%s", + #submap, submap, + #modmask, modmask, + #key, key, + #flags, flags, + #dispatchIdentity, dispatchIdentity + ) end -- Hyprland 0.56 can emit invalid JSON for native Lua binds while its plain diff --git a/keymap/tests/hypr_cpu_budget_test.lua b/keymap/tests/hypr_cpu_budget_test.lua index f9a4e5f..4d8d1e0 100644 --- a/keymap/tests/hypr_cpu_budget_test.lua +++ b/keymap/tests/hypr_cpu_budget_test.lua @@ -14,7 +14,7 @@ end files[rootPath] = table.concat(rootLines, "\n") local bindLines, bindDescriptions = {}, {} -for index = 1, 60 do +for index = 1, 120 do if index % 15 == 1 then bindLines[#bindLines + 1] = "-- " .. tostring(math.floor(index / 15) + 1) .. ". Group" end diff --git a/keymap/tests/hypr_scale_test.lua b/keymap/tests/hypr_scale_test.lua index a4a1bc9..94da58b 100644 --- a/keymap/tests/hypr_scale_test.lua +++ b/keymap/tests/hypr_scale_test.lua @@ -21,21 +21,7 @@ local values, watchers = { categories = { { name = "Previous", binds = { { id = "previous" } } } }, }, }, {} -local reads, xorCalls, loadingCategoryCount = 0, 0, -1 -local originalBit32 = bit32 -bit32 = { - bxor = function(left, right) - xorCalls = xorCalls + 1 - local result, place = 0, 1 - for _ = 1, 8 do - if left % 2 ~= right % 2 then result = result + place end - left = math.floor(left / 2) - right = math.floor(right / 2) - place = place * 2 - end - return result - end, -} +local reads, loadingCategoryCount = 0, -1 noctalia = { state = { @@ -74,7 +60,6 @@ noctalia = { } assert(loadfile("service.luau"))() -bit32 = originalBit32 local snapshot = values["keymap.snapshot"] assert(snapshot.status == "ready", "large Hyprland fixture did not parse") @@ -82,6 +67,13 @@ assert(snapshot.total == 93, "large Hyprland fixture lost binds") assert(#snapshot.categories == 5, "large Hyprland fixture lost category markers") assert(reads == 1, "Hyprland root config should only be read once per refresh") assert(loadingCategoryCount == 0, "loading snapshot should not reserialize the previous bind tree") -assert(xorCalls > 0, "Hyprland parser did not use the native-xor fingerprint path") +local seenIds = {} +for _, category in ipairs(snapshot.categories) do + for _, bind in ipairs(category.binds) do + assert(bind.id:match("^hypr:"), "invalid Hyprland bind ID") + assert(not seenIds[bind.id], "duplicate Hyprland bind ID") + seenIds[bind.id] = true + end +end print("hypr scale tests: ok") diff --git a/keymap/tests/mangowc_scale_test.lua b/keymap/tests/mangowc_scale_test.lua index 7478586..5a29360 100644 --- a/keymap/tests/mangowc_scale_test.lua +++ b/keymap/tests/mangowc_scale_test.lua @@ -1,5 +1,5 @@ local sourceLines = {} -for index = 1, 93 do +for index = 1, 120 do if index % 20 == 1 then sourceLines[#sourceLines + 1] = "# Group " .. tostring(math.floor(index / 20) + 1) end @@ -61,15 +61,29 @@ noctalia = { end, } +local instructionBlocks = 0 +debug.sethook(function() instructionBlocks = instructionBlocks + 1 end, "", 1000) assert(loadfile("mangowc_service.luau"))() +debug.sethook() bit32 = originalBit32 local snapshot = values["keymap.snapshot"] assert(snapshot.status == "ready", "large MangoWC fixture did not parse") -assert(snapshot.total == 93, "large MangoWC fixture lost binds") -assert(#snapshot.categories == 5, "large MangoWC fixture lost category markers") +assert(snapshot.total == 120, "large MangoWC fixture lost binds") +assert(#snapshot.categories == 6, "large MangoWC fixture lost category markers") assert(reads == 1, "MangoWC root config should only be read once per refresh") assert(loadingCategoryCount == 0, "loading snapshot should not reserialize the previous bind tree") -assert(xorCalls > 0, "MangoWC parser did not use the native-xor fingerprint path") +assert(xorCalls == 0, "MangoWC visible binds still use per-character fingerprinting") +assert(instructionBlocks < 250, "MangoWC parser exceeded its regression instruction budget") -print("MangoWC scale tests: ok") +local seenIds = {} +for _, category in ipairs(snapshot.categories) do + for _, bind in ipairs(category.binds) do + assert(bind.id:match("^mango:"), "invalid MangoWC bind ID") + assert(not seenIds[bind.id], "duplicate MangoWC bind ID") + assert(bind.fingerprint == "exact-v1", "MangoWC bind did not use exact source verification") + seenIds[bind.id] = true + end +end + +print(string.format("MangoWC scale tests: ok (%d blocks)", instructionBlocks)) diff --git a/keymap/tests/niri_cpu_budget_test.lua b/keymap/tests/niri_cpu_budget_test.lua index dc65ad7..70fe833 100644 --- a/keymap/tests/niri_cpu_budget_test.lua +++ b/keymap/tests/niri_cpu_budget_test.lua @@ -80,7 +80,10 @@ noctalia = { end, } +local instructionBlocks = 0 +debug.sethook(function() instructionBlocks = instructionBlocks + 1 end, "", 1000) assert(loadfile("niri_service.luau"))() +debug.sethook() bit32 = originalBit32 string.sub = originalStringSub @@ -91,10 +94,11 @@ assert(reads["/fixture/config.kdl"] == 1, "Niri root config should only be read assert(reads["/fixture/mine/binds.kdl"] == 1, "Niri bind include should only be read once") assert(reads["/fixture/mine/debug.kdl"] == 1, "Niri optional include should only be read once") assert(reads["/fixture/mine/theme.kdl"] == 1, "Niri non-bind include should only be read once") -assert(xorCalls > 0, "Niri parser did not use the native-xor fingerprint path") +assert(xorCalls == 0, "Niri visible binds still use per-character fingerprinting") +assert(instructionBlocks < 700, "Niri parser exceeded its regression instruction budget") assert( stringSubCalls < 45000, "Niri parser scanned an unrelated include character by character: " .. tostring(stringSubCalls) ) -print("niri CPU-budget regression tests: ok") +print(string.format("niri CPU-budget regression tests: ok (%d blocks)", instructionBlocks)) diff --git a/keymap/tests/niri_scale_test.lua b/keymap/tests/niri_scale_test.lua index 69a9299..915cd63 100644 --- a/keymap/tests/niri_scale_test.lua +++ b/keymap/tests/niri_scale_test.lua @@ -72,12 +72,15 @@ assert(snapshot.total == 93, "large Niri fixture lost binds") assert(#snapshot.categories == 5, "large Niri fixture lost category markers") assert(reads == 1, "Niri root config should only be read once per refresh") assert(loadingCategoryCount == 0, "loading snapshot should not reserialize the previous bind tree") -assert(xorCalls > 0, "Niri parser did not use the native-xor fingerprint path") +assert(xorCalls == 0, "Niri visible binds still use per-character fingerprinting") +local seenIds = {} for _, category in ipairs(snapshot.categories) do for _, bind in ipairs(category.binds) do - assert(bind.id:match("^niri:[0-9a-f]+$"), "invalid bind fingerprint") - assert(bind.fingerprint:match("^[0-9a-f]+$"), "invalid source fingerprint") + assert(bind.id:match("^niri:"), "invalid Niri bind ID") + assert(not seenIds[bind.id], "duplicate Niri bind ID") + assert(bind.fingerprint == "exact-v1", "Niri bind did not use exact source verification") + seenIds[bind.id] = true end end