perf(keymap): reduce bind parser CPU cost (#157)
* fix(keymap): reduce Hyprland bind ID CPU cost * perf(keymap): avoid bind hashing in all parsers
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
id = "blackbartblues/keymap"
|
||||
name = "Keymap"
|
||||
version = "1.3.4"
|
||||
version = "1.3.5"
|
||||
plugin_api = 9
|
||||
author = "blackbartblues"
|
||||
license = "MIT"
|
||||
|
||||
+11
-8
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user