Documentation menu

Documentation / Under the hood

Performance

Achaea combat is decided on round trips, and Mudlet runs every package on one thread. Time Emunah spends is time the client is not drawing, reading or sending, so every hot path is measured, not assumed.

The paths that matter

PathRuns onWhose cost
Trigger matchingEvery line of game textMudlet's own, in C++
Trigger callbacksEvery line a pattern matchedEmunah's
engine.tick()Every promptEmunah's
Panel repaintsEvery prompt, and every item, affliction and defence changeEmunah's, and Qt's

A trigger whose pattern matches nearly every line is a per-line Lua handler in all but name. Emunah once had five of them.

What it costs now

Measured with test/bench.lua under a realistic lock: eight afflictions across every balance, herbs in hand, and health and mana below all four healing thresholds. Three runs on 15 October 2026 measured engine.tick() at 77.5 µs, allocating 2,000 bytes. That is up from 67.5 µs on 4 October; the rise came before that day's changes, and has not yet been profiled.

The first pass

BeforeAfter
engine.tick()124.1 µs79.0 µs
afflist.priority(aff, vector)0.58 µs0.28 µs
afflist.curesVia(aff, vector)0.22 µs0.09 µs
have.cure(option)1.60 µs0.87 µs
Ticker scroll step (12.5 Hz)47.6 µs, 4171 B1.7 µs, 424 B
Lines that run the name highlighter88%12%

Time to send, 3 October 2026

The second pass measured the time from a packet arriving to the cure leaving, and moved every repaint after the send.

PacketBeforeAfter
An affliction lands158 µs, 10 draws before the send85 µs, none
Herb balance returns, then Char.Vitals96 µs, 5 draws52 µs, none
engine.tick()79.0 µs, 2056 B67.5 µs, 2000 B
engine.has()0.17 µs0.10 µs

What was found

The name highlighter ran on nearly every line

Its trigger matched any capitalised word, on the reasoning that this was the cheapest possible filter. It was cheap, but it did not filter: nearly every line of Achaea's text has a capitalised word, so 88% of lines ran a full roster scan. The trigger is now built from the roster itself and rebuilt when the roster changes, and it fires on the 12% of lines that actually name somebody.

The ticker allocated about 140 strings a second

It rebuilt its whole reel at 12.5 Hz, invisible to every per-prompt benchmark. The reel is now compiled once per message change, and each step is one substring and one join.

Panels drew before the cure went out

The vitals strip painted before the engine ran, and the affliction panel painted inside the affliction update. Every panel now paints once, after the packet has been handled, coalesced so two afflictions in one packet paint the panel once.

Questions asked more often than their answers changed

Priorities were re-read 56 times a prompt, names were lowercased again on every read, and per-balance loops re-asked questions that did not depend on the balance. Each is now asked once per prompt, against data that is invalidated when it changes, never on a timer.

Measuring

lua5.1 test/bench.lua [iterations]     # time and bytes per call
lua5.1 test/profile.lua [iterations]   # sampling profiler: the hottest lines
lua5.1 test/latency.lua                # from a packet arriving to the command leaving
lua5.1 test/run.lua                    # includes the draw-count budgets

Four traps each produced confident, wrong numbers before they were noticed:

Rules for changes

The full record, with every measurement, is docs/performance.md in the repository.