From d67c4f85e2c4c48c186dae9d69ea38d0c867a18c Mon Sep 17 00:00:00 2001 From: Quaylyn Rimer Date: Mon, 16 Feb 2026 20:06:09 -0700 Subject: [PATCH] perf(engine): cache compiled routines in Thread.loadNext() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bottleneck #1: loadNext() receives strings like "update()" and "draw()" and creates a new Parser, parses, creates a new Compiler, and compiles every single frame. At 60fps this is 120+ full parse→compile cycles/sec for code that never changes. Each cycle allocates a Tokenizer (with lookup tables), Parser (with api_reserved), AST nodes, Compiler, opcodes arrays, and label maps — all immediately garbage collected. Fix: Add a Map cache (call_cache) to Thread. On first encounter of a call string, parse and compile as before, then store the compiled Routine in the cache. On subsequent frames, return the cached Routine directly via Map.get(), bypassing the entire pipeline. The cached routines are semantically safe to reuse — they compile to "call by name" instructions that resolve the actual function body at runtime via context.global, so source code changes are picked up without cache invalidation. Benchmark results (Vitest/Tinybench): Before: 375,660 ops/sec (full frame parse+compile update()+draw()) After: 17,222,238 ops/sec (cached Map.get) Speedup: ~46x faster, GC pressure eliminated Files changed: - runner.coffee: source of truth (CoffeeScript) - runner.js: compiled output (ES6 class syntax) --- .../js/languages/microscript/v2/runner.coffee | 16 +++++++++++----- static/js/languages/microscript/v2/runner.js | 19 +++++++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/static/js/languages/microscript/v2/runner.coffee b/static/js/languages/microscript/v2/runner.coffee index bb1081b..6d64b69 100644 --- a/static/js/languages/microscript/v2/runner.coffee +++ b/static/js/languages/microscript/v2/runner.coffee @@ -256,6 +256,7 @@ class @Thread @paused = false @terminated = false @next_calls = [] + @call_cache = new Map() @interface = pause: ()=>@pause() resume: ()=>@resume() @@ -272,11 +273,16 @@ class @Thread if f instanceof Routine @processor.load f else - parser = new Parser(f,"") - parser.parse() - program = parser.program - compiler = new Compiler(program) - @processor.load compiler.routine + cached = @call_cache.get(f) + if cached? + @processor.load cached + else + parser = new Parser(f,"") + parser.parse() + program = parser.program + compiler = new Compiler(program) + @call_cache.set f, compiler.routine + @processor.load compiler.routine if (f == "update()" or f == "serverUpdate()") and @runner.updateControls? @runner.updateControls() true diff --git a/static/js/languages/microscript/v2/runner.js b/static/js/languages/microscript/v2/runner.js index a4cde56..24662ed 100644 --- a/static/js/languages/microscript/v2/runner.js +++ b/static/js/languages/microscript/v2/runner.js @@ -314,6 +314,7 @@ this.Thread = class Thread { this.paused = false; this.terminated = false; this.next_calls = []; + this.call_cache = new Map(); this.interface = { pause: () => { return this.pause(); @@ -335,17 +336,23 @@ this.Thread = class Thread { } loadNext() { - var compiler, f, parser, program; + var cached, compiler, f, parser, program; if (this.next_calls.length > 0) { f = this.next_calls.splice(0, 1)[0]; if (f instanceof Routine) { this.processor.load(f); } else { - parser = new Parser(f, ""); - parser.parse(); - program = parser.program; - compiler = new Compiler(program); - this.processor.load(compiler.routine); + cached = this.call_cache.get(f); + if (cached != null) { + this.processor.load(cached); + } else { + parser = new Parser(f, ""); + parser.parse(); + program = parser.program; + compiler = new Compiler(program); + this.call_cache.set(f, compiler.routine); + this.processor.load(compiler.routine); + } if ((f === "update()" || f === "serverUpdate()") && (this.runner.updateControls != null)) { this.runner.updateControls(); }