The Bigger the World, the Harder a Day Ends: Three Performance Hurdles
In The Reincarnated, every resident, trading house and guild is living its own life. At the end of each day, the world runs an end-of-day settlement: workshops tally their output, trading houses restock, the guild buys up goods, residents eat and see the doctor, payments land in wallets… Only when all of it is done can the next day begin.
While the world was small, this ran smoothly. But as we added more towns and people, we ran into three hurdles, one after another.
Hurdle one: opening the observer window froze everything
During development we use an “observatory” window to watch the state of the world live. The trouble was that every time the window redrew, it reached straight into the world’s data—and the world itself was running on the same thread. The result: the window waited on the world, the world waited on the window, and the whole editor stuttered.
This isn’t just a tooling problem; the in-game interface players see would hit the same wall.
The fix: move the world onto its own background thread and have it publish a “snapshot” once a second. The interface only reads the snapshot and never touches the world while it’s changing. Once a second is plenty for human eyes, and the world can finally focus on its own work.
Hurdle two: each day took longer to end
After the move, a new problem surfaced: the simulation couldn’t keep up with the target speed, and the end-of-day settlement got slower every day. Day one took 1.6 seconds; a few days later, the slowest day took 22.
We timed each part of the settlement separately and found that about 90% of the time went into “write the entire commission record back to the database every time a commission is completed.” Yet nobody needs that record until the player actually saves.
The fix: write only when saving. Afterwards, the settlement went back to 1–2 seconds a day, and stopped growing with the day count.
Hurdle three: doing things at once, without miscounting money
Next, we wanted the world to hold up as it grows. The obvious approach is parallelism: much of the settlement is work that doesn’t depend on anything else—trading house A restocks, trading house B restocks, residents eat on their own. There’s no reason it all has to wait in a single line.
But parallelism carries a serious risk. Imagine two people working on the same wallet at the same moment:
The wallet holds 100 copper coins. A receives 30, and at the same moment B receives 50. Both first read “100,” work out 130 and 150, and write their result back. The wallet ends up at 150. Those 30 coins simply vanish.
While investigating, we also found this risk already existed: residents’ everyday actions and the settlement could already touch the same wallet at once—it just hadn’t been caught yet.
The design today: separate lanes, one-at-a-time counters
We didn’t rewrite every system. We changed just two things:
- One scheduler sorts the day’s work into lanes. Everything belonging to the same trading house or the same resident always goes into the same lane, so work for different owners can run at the same time. At the end of each stage, the scheduler waits for every lane to finish before moving on.
- Locks on shared counters: wallets, markets, relationships and guild membership rolls—things everyone touches—each get a “one at a time” lock. The lock only covers the short “read, work out, write back” step, and nobody may run off to another counter while holding one, so two people can never end up waiting on each other forever.
A few more rules:
- Switch between two modes at any time: “sequential mode” still does things one by one, which makes problems easy to track down; “parallel mode” uses multiple lanes. Comparing the two while measuring shows exactly where the bottleneck is.
- Two markets never settle at the same time: when a market runs short it buys from another market, so if both settled at once they could end up waiting on each other.
- How we know nothing was miscounted: in parallel, who gets to a deal first depends on timing, so we don’t require both modes to produce identical results. Instead we check conservation: every coin and every item in the world must be accounted for, nothing may go negative, and nobody may join the same kind of organization twice.
Results
In a test world with four towns and 128 residents:
- The full end-of-day settlement dropped from about 3.5 seconds to 2.6 seconds on average.
- The part where every organization settles its own business ran about twice as fast.
- Both modes ran with zero errors.
Not every part can run in parallel. Some stages have a natural order—goods must be put on sale before anyone can buy them—and we deliberately kept those one at a time.
Measuring also revealed the next bottleneck: at every moment, the world updates the countdown on status effects for every character, including residents of distant towns. We’re already planning a new approach, and we’ll share more once it’s further along.