// Focused micro-benchmarks for Space Lua interpreter performance. // Benchmarks are weighted toward the real SilverBullet workload: // ~40% API/syscall patterns (LuaNativeJSFunction, LuaBuiltinFunction) // ~30% data structure traversal (tables, linked structures, iteration) // ~15% string manipulation (format, find, split, concat, interpolation) // ~15% function calls, closures, control flow // // Each snippet is pre-parsed to exclude parsing cost from measurements. // All benchmarks create fresh Lua environments for isolation. import { bench } from "vitest"; import { evalStatement } from "../client/space_lua/eval.ts"; import { LuaBuiltinFunction, LuaEnv, LuaNativeJSFunction, LuaStackFrame, LuaTable, luaTypeOf, } from "../client/space_lua/runtime.ts"; import { parse as parseLua } from "../client/space_lua/parse.ts"; import { luaBuildStandardEnv } from "../client/space_lua/stdlib.ts"; const LOOP = 100_000; const SMALL = 10_000; // --- Helpers --- function makeMinimalEnv(): { global: LuaEnv; sf: LuaStackFrame } { const global = new LuaEnv(); global.setLocal("type", new LuaBuiltinFunction((_sf, v) => luaTypeOf(v))); const sf = LuaStackFrame.createWithGlobalEnv(global); return { global, sf }; } function makeStdEnv(): { global: LuaEnv; sf: LuaStackFrame } { const G = luaBuildStandardEnv(); const env = new LuaEnv(G); const sf = LuaStackFrame.createWithGlobalEnv(env); return { global: env, sf }; } async function runMinimal(ast: any) { const { global, sf } = makeMinimalEnv(); const r = evalStatement(ast, global, sf, false); if (r instanceof Promise) await r; } async function runStd(ast: any) { const { global, sf } = makeStdEnv(); const r = evalStatement(ast, global, sf, false); if (r instanceof Promise) await r; } // Environment with sync/async API stubs simulating real syscall patterns function makeApiEnv(): { global: LuaEnv; sf: LuaStackFrame } { const G = luaBuildStandardEnv(); const env = new LuaEnv(G); // Simulate sync API call (like space.readPage returning cached data) env.setLocal( "syncGet", new LuaBuiltinFunction((_sf, key) => `value_for_${key}`), ); // Simulate async API call (like space.readPage hitting storage) env.setLocal( "asyncGet", new LuaNativeJSFunction((key: string) => Promise.resolve(`value_for_${key}`), ), ); // Simulate sync API returning a table (like space.listPages) env.setLocal( "syncGetRecord", new LuaBuiltinFunction((_sf, id: number) => { const t = new LuaTable(); void t.set("id", id); void t.set("name", `item_${id}`); void t.set( "tags", (() => { const tags = new LuaTable(); void tags.set(1, "tag_a"); void tags.set(2, "tag_b"); return tags; })(), ); return t; }), ); // Simulate async API returning a table env.setLocal( "asyncGetRecord", new LuaNativeJSFunction((id: number) => Promise.resolve({ id, name: `item_${id}`, tags: ["tag_a", "tag_b"] }), ), ); // Simulate sync predicate API (like checking config) env.setLocal( "syncCheck", new LuaBuiltinFunction((_sf, val) => val !== null && val !== undefined), ); // Simulate async write API (like space.writePage) env.setLocal( "asyncSet", new LuaNativeJSFunction((_key: string, _value: string) => Promise.resolve(true), ), ); // Simulate async page read returning multi-line content with dates env.setLocal( "asyncGetPage", new LuaNativeJSFunction((id: number) => Promise.resolve( `2024-03-${String((id % 28) + 1).padStart(2, "0")} Task alpha for page ${id}\n` + `2023-12-${String((id % 28) + 1).padStart(2, "0")} Task beta for page ${id}\n` + `2024-01-${String((id % 28) + 1).padStart(2, "0")} Task gamma for page ${id}\n`, ), ), ); const sf = LuaStackFrame.createWithGlobalEnv(env); return { global: env, sf }; } async function runApi(ast: any) { const { global, sf } = makeApiEnv(); const r = evalStatement(ast, global, sf, false); if (r instanceof Promise) await r; } // ===================================================== // 1. API Call Patterns (sync and async) // The primary workload in SilverBullet: Lua scripts // call into TypeScript APIs via LuaNativeJSFunction // and LuaBuiltinFunction. // ===================================================== const API_LOOP = 1_000; const luaSyncApiCalls = ` local results = {} for i = 1, ${API_LOOP} do results[i] = syncGet("key_" .. i) end `; const luaAsyncApiCalls = ` local results = {} for i = 1, ${API_LOOP} do results[i] = asyncGet("key_" .. i) end `; const luaMixedApiCalls = ` local results = {} for i = 1, ${API_LOOP} do if i % 10 == 0 then results[i] = asyncGet("key_" .. i) else results[i] = syncGet("key_" .. i) end end `; // Simulate query-like pattern: fetch record, filter, collect const luaApiFilterCollect = ` local results = {} local count = 0 for i = 1, ${API_LOOP} do local rec = syncGetRecord(i) if rec.id % 3 == 0 then count = count + 1 results[count] = rec end end `; // Async record fetch with field access and filtering const luaAsyncApiFilterCollect = ` local results = {} local count = 0 for i = 1, ${API_LOOP} do local rec = asyncGetRecord(i) if syncCheck(rec.name) then count = count + 1 results[count] = rec end end `; // Chained async calls (read then write pattern) const luaAsyncChainedCalls = ` for i = 1, ${API_LOOP} do local val = asyncGet("key_" .. i) asyncSet("out_" .. i, val .. "_processed") end `; // Sync API with table construction (building query results) const luaSyncApiTableBuild = ` local results = {} for i = 1, ${API_LOOP} do local name = syncGet("name_" .. i) local status = syncGet("status_" .. i) results[i] = { name = name, status = status, index = i } end `; // ===================================================== // 2. Data Structure Traversal // Tables are the core data type in Space Lua. // Queries produce tables, configs are tables, // page metadata is tables. // ===================================================== const luaTableConstructor = ` local tables = {} for i = 1, ${SMALL} do tables[i] = { x = i, y = i * 2, z = "tag_" .. i } end `; const luaTableInsertLoop = ` local t = {} for i = 1, ${LOOP} do t[#t + 1] = i end `; const luaIpairsIteration = ` local t = {} for i = 1, 1000 do t[i] = i end local s = 0 for round = 1, 100 do for _, v in ipairs(t) do s = s + v end end `; const luaPairsIteration = ` local t = {} for i = 1, 100 do t["key" .. i] = i end local s = 0 for round = 1, 1000 do for k, v in pairs(t) do s = s + v end end `; // Nested table access (common in page metadata traversal) const luaNestedTableAccess = ` local pages = {} for i = 1, 200 do pages[i] = { name = "page_" .. i, meta = { tags = { "tag1", "tag2" }, priority = i % 5 } } end local count = 0 for _, page in ipairs(pages) do if page.meta.priority > 2 then for _, tag in ipairs(page.meta.tags) do count = count + 1 end end end `; // Table used as a map: insert, lookup, delete const luaTableAsMap = ` local map = {} for i = 1, 5000 do map["key_" .. i] = "value_" .. i end local found = 0 for i = 1, 5000 do if map["key_" .. i] ~= nil then found = found + 1 end end for i = 1, 5000, 2 do map["key_" .. i] = nil end `; // Linked list traversal (common pattern for tree/graph data) const luaLinkedList = ` local head = nil for i = 1000, 1, -1 do head = { value = i, next = head } end local total = 0 for round = 1, 100 do local node = head while node do total = total + node.value node = node.next end end `; // table.sort with custom comparator (query ORDER BY) const luaTableSort = ` local items = {} for i = 1, 1000 do items[i] = { name = "item_" .. (1000 - i), priority = i % 7 } end for round = 1, 20 do table.sort(items, function(a, b) if a.priority == b.priority then return a.name < b.name end return a.priority < b.priority end) end `; // table.concat (building output strings) const luaTableConcat = ` local parts = {} for i = 1, 1000 do parts[i] = "segment_" .. i end local result for round = 1, 100 do result = table.concat(parts, ", ") end `; // ===================================================== // 3. String Manipulation // Template interpolation, string matching, and // building output are core SilverBullet patterns. // ===================================================== const luaStringFormat = ` local s for i = 1, ${SMALL} do s = string.format("hello %s world %d", "test", i) end `; const luaStringFind = ` local text = "the quick brown fox jumps over the lazy dog" local count = 0 for i = 1, ${SMALL} do if string.find(text, "fox") then count = count + 1 end end `; const luaStringSub = ` local text = "abcdefghijklmnopqrstuvwxyz" local s for i = 1, ${LOOP} do s = string.sub(text, 5, 15) end `; // String concatenation building (template-like patterns) const luaStringConcat = ` local result = "" for i = 1, 1000 do result = result .. "item " .. i .. ", " end `; // String split + rejoin (common in tag processing) const luaStringSplitProcess = ` local input = "tag1,tag2,tag3,tag4,tag5" local results = {} for round = 1, 2000 do local parts = string.split(input, ",") local upper = {} for i, v in ipairs(parts) do upper[i] = string.upper(v) end results[round] = table.concat(upper, ";") end `; // String matching (regex-like patterns in content processing) const luaStringGsub = ` local text = "Hello [world](link1) and [foo](link2) and [bar](link3)" local result for i = 1, 5000 do result = string.gsub(text, "%[(.-)%]%((.-)%)", "%1") end `; // ===================================================== // 4. Function Calls, Closures, Control Flow // Function call overhead matters because every API // call, iterator, and callback goes through luaCall. // ===================================================== const luaFibonacci = ` local function fib(n) if n < 2 then return n end return fib(n - 1) + fib(n - 2) end local r = fib(20) `; const luaClosureCreateCall = ` local s = 0 for i = 1, ${SMALL} do local function add(x) return x + i end s = s + add(i) end `; const luaMethodCall = ` local obj = {} obj.value = 0 function obj:inc(n) self.value = self.value + n end for i = 1, ${LOOP} do obj:inc(1) end `; const luaVarargs = ` local function sum(...) local args = {...} local s = 0 for i = 1, #args do s = s + args[i] end return s end local total = 0 for i = 1, ${SMALL} do total = total + sum(1, 2, 3, 4, 5) end `; // Higher-order functions (filter/map pattern used in queries) const luaHigherOrderFunctions = ` local function filter(t, pred) local result = {} local n = 0 for _, v in ipairs(t) do if pred(v) then n = n + 1 result[n] = v end end return result end local function map(t, fn) local result = {} for i, v in ipairs(t) do result[i] = fn(v) end return result end local data = {} for i = 1, 1000 do data[i] = i end for round = 1, 50 do local evens = filter(data, function(x) return x % 2 == 0 end) local doubled = map(evens, function(x) return x * 2 end) end `; // ===================================================== // 5. Realistic Mixed Workloads // Patterns that combine multiple operations the way // real SilverBullet Lua code does. // ===================================================== // Simulate query: fetch pages, filter by tag, sort, build result const luaQuerySimulation = ` local pages = {} for i = 1, 500 do pages[i] = { name = "page_" .. i, tags = { "tag_" .. (i % 5) }, modified = 1000000 - i, } end -- filter: only tag_1 and tag_3 local filtered = {} local n = 0 for _, p in ipairs(pages) do local tag = p.tags[1] if tag == "tag_1" or tag == "tag_3" then n = n + 1 filtered[n] = p end end -- sort by modified desc table.sort(filtered, function(a, b) return a.modified > b.modified end) -- build output table local result = {} for i, p in ipairs(filtered) do result[i] = { name = p.name, tag = p.tags[1] } end `; // Simulate config/metadata processing const luaConfigProcessing = ` local config = { theme = "dark", plugins = { "markdown", "lua", "query", "template" }, shortcuts = {}, } for i = 1, 100 do config.shortcuts["ctrl_" .. i] = "action_" .. i end -- Process: iterate config, build summary string local parts = {} local n = 0 for round = 1, 100 do n = 0 for k, v in pairs(config.shortcuts) do n = n + 1 parts[n] = k .. "=" .. v end end local summary = table.concat(parts, "; ") `; // Simulate template rendering: string building from structured data const luaTemplateRendering = ` local items = {} for i = 1, 200 do items[i] = { title = "Item " .. i, done = i % 3 == 0, priority = i % 5 } end local output = {} for round = 1, 20 do local n = 0 for _, item in ipairs(items) do n = n + 1 local status = item.done and "x" or " " local prio = "" if item.priority > 3 then prio = " (!)" end output[n] = string.format("- [%s] %s%s", status, item.title, prio) end end local result = table.concat(output, "; ") `; // ===================================================== // 6. Async Patterns // Async API calls mixed with sync table/string work. // ===================================================== // Async loop with sync re-entry (the key optimization from Phase 3) const luaAsyncLoopReentry = ` local results = {} for i = 1, ${API_LOOP} do local val = asyncGet("key_" .. i) -- sync work between async calls results[i] = string.upper(val) .. "_done" end `; // While loop with async body const luaAsyncWhileLoop = ` local i = 0 local results = {} while i < ${API_LOOP} do i = i + 1 results[i] = asyncGet("item_" .. i) end `; // Async __index metamethod (lazy loading pattern) const luaAsyncMetamethod = ` local mt = {} mt.__index = function(t, k) return asyncGet(k) end local proxy = setmetatable({}, mt) local results = {} for i = 1, ${API_LOOP} do results[i] = proxy["field_" .. i] end `; // Async record fetch + sync processing (realistic query pattern) const luaAsyncQueryPattern = ` local results = {} local count = 0 for i = 1, ${API_LOOP} do local rec = asyncGetRecord(i) -- sync filtering and string work if syncCheck(rec) then count = count + 1 results[count] = { id = i, label = "item_" .. i } end end `; // ===================================================== // 7. Community-Driven Gap Coverage // Benchmarks derived from analyzing 163 community // Lua scripts (~40K lines) to cover patterns that // are heavily used but previously unbenchmarked. // ===================================================== // --- pcall overhead (51 occurrences in community scripts, 0 benchmarks) --- const luaPcallSuccess = ` local function safe_read(key) return syncGet(key) end local results = {} for i = 1, 1000 do local ok, val = pcall(safe_read, "key_" .. i) if ok then results[i] = val end end `; const luaPcallError = ` local function failing_read(key) error("not found: " .. key) end local errors = 0 for i = 1, 1000 do local ok, msg = pcall(failing_read, "key_" .. i) if not ok then errors = errors + 1 end end `; const luaPcallAsyncApi = ` local results = {} local errors = 0 for i = 1, 1000 do local ok, val = pcall(asyncGet, "key_" .. i) if ok then results[#results + 1] = val else errors = errors + 1 end end `; // --- string.match / string.gmatch (595+ occurrences, only gsub was benchmarked) --- const luaStringMatchDate = ` local text = "Due: 2024-03-15, Created: 2023-12-01, Modified: 2024-01-30" local results = {} for i = 1, 5000 do local y, m, d = string.match(text, "(%d%d%d%d)%-(%d%d)%-(%d%d)") results[1] = y end `; const luaStringGmatchMulti = ` local text = "2024-03-15 task1\\n2024-03-16 task2\\n2024-03-17 task3\\n2024-03-18 task4\\n2024-03-19 task5" local results = {} for round = 1, 2000 do local n = 0 for date, task in string.gmatch(text, "(%d+%-%d+%-%d+) ([^\\n]+)") do n = n + 1 results[n] = date end end `; const luaStringMatchComplex = ` local text = "[[Page Link]] and #tag and [markdown](http://url.com)" local count = 0 for i = 1, 2000 do if string.match(text, "%[%[(.-)%]%]") then count = count + 1 end if string.match(text, "#(%w+)") then count = count + 1 end if string.match(text, "%[(.-)%]%((.-)%)") then count = count + 1 end end `; // --- String building: table.insert + table.concat (THE idiomatic output pattern) --- const luaTableInsertConcat = ` for round = 1, 50 do local parts = {} for i = 1, 1000 do table.insert(parts, "- item " .. i .. ": " .. string.format("value_%d", i)) end local output = table.concat(parts, "\\n") end `; // --- SilverBullet string extensions (595+ for split/trim, thin JS wrappers) --- const luaStringSplitHot = ` local input = "one/two/three/four/five/six/seven/eight" local count = 0 for i = 1, 5000 do local parts = string.split(input, "/") count = count + #parts end `; const luaStringTrimStartsEnds = ` local inputs = { " hello ", " world ", " test ", " foo ", " bar " } local count = 0 for i = 1, 5000 do for _, s in ipairs(inputs) do local trimmed = string.trim(s) if string.startsWith(trimmed, "h") then count = count + 1 end if string.endsWith(trimmed, "d") then count = count + 1 end end end `; // --- type() checking (723+ occurrences, most-called builtin, 0 benchmarks) --- const luaTypeChecking = ` local values = { 1, "hello", true, nil, {}, 3.14 } local counts = { number = 0, string = 0, boolean = 0, table = 0 } for round = 1, 20000 do for _, v in ipairs(values) do local t = type(v) if t == "number" then counts.number = counts.number + 1 elseif t == "string" then counts.string = counts.string + 1 end end end `; // --- os.time / os.date (167+ occurrences, no perf benchmark) --- const luaOsDateFormat = ` local results = {} for i = 1, 2000 do local t = os.time() results[i] = os.date("%Y-%m-%d", t) end `; const luaOsDateTable = ` local base = os.time() local results = {} for i = 1, 2000 do local dt = os.date("*t", base + i * 86400) results[i] = dt.year .. "-" .. dt.month .. "-" .. dt.day end `; // --- tostring/tonumber conversions --- const luaTypeConversions = ` local results = {} for i = 1, ${SMALL} do local s = tostring(i) local n = tonumber(s) results[i] = s end `; // --- Deep nested table access 5 levels (community scripts access config 3-5 deep) --- const luaDeepNestedAccess = ` local root = { app = { config = { editor = { theme = { name = "dark", fontSize = 14 } } } } } local count = 0 for i = 1, ${SMALL} do local name = root.app.config.editor.theme.name root.app.config.editor.theme.fontSize = 14 + (i % 10) if name == "dark" then count = count + 1 end end `; // --- Closure callbacks with captured upvalues (simulates command.define/event.listen) --- const luaClosureCallbacks = ` local handlers = {} for i = 1, 1000 do local prefix = "handler_" .. i local config = { enabled = i % 2 == 0, priority = i % 5 } handlers[i] = function(input) if config.enabled then return prefix .. ": " .. input .. " (p=" .. config.priority .. ")" end return nil end end local results = {} local n = 0 for _, h in ipairs(handlers) do local r = h("test_event") if r then n = n + 1 results[n] = r end end `; // --- End-to-end realistic script: read+parse+transform+build --- const luaRealisticReadParseTransform = ` local pages = {} for i = 1, 50 do pages[i] = asyncGetPage(i) end local tasks = {} local n = 0 for _, content in ipairs(pages) do for date, text in string.gmatch(content, "(%d%d%d%d%-%d%d%-%d%d) ([^\\n]+)") do n = n + 1 tasks[n] = { date = date, text = string.trim(text), done = false } end end local filtered = {} local fn = 0 for _, t in ipairs(tasks) do if string.startsWith(t.date, "2024") then fn = fn + 1 filtered[fn] = t end end table.sort(filtered, function(a, b) return a.date > b.date end) local out = {} for i, t in ipairs(filtered) do out[i] = string.format("