Cost attribution
Circinus instruments Harmony patches and selected engine methods with a prefix and a postfix that read a stopwatch. Cost is attributed to the mod whose declaring assembly owns the patch method, which resolves reliably: on a 916-mod list, 1,723 of 1,723 patch costs were attributed with none unresolved.
Time is reported as share of measured frame time, never as raw milliseconds. Milliseconds are entangled with the hardware they were recorded on, so averaging them across machines produces a hardware census wearing a leaderboard's clothing.
Instrumentation is not free, so it is disclosed rather than hidden: our own patches are tagged and shown in the tables, and a run records whether profiling was active.
Rates and denominators
A count is not a measurement. "1.4 million calls" says nothing until you know what it was counted over, so every count on this site and in the tool is divided by something before it is shown. The thing it is divided by is always the frame.
That identity is the reason for the choice. Share of frame, average milliseconds, worst frame, calls per frame and microseconds per call all divide by the same window, so a row is arithmetically closed: the numbers multiply out, and one that does not is a bug you can see without leaving the table.
Why not per second, or per game tick
Both were tried. Neither survives contact with the question being asked.
| Per second | moves with game speed and frame rate. The same code at 3x speed reports three times the rate, which measures the speed setting |
| Per 900 game ticks | immune to game speed, but its denominator is zero whenever the game is paused, and it combines with nothing else in its own row — every neighbouring column divides by frames instead |
| Per frame | frames never stop advancing while there is anything to measure, and it shares its denominator with every column beside it |
The per-tick figure is still recorded and still shown, in the detail panel, because it answers a real and different question: how often does this happen in the simulation. That is not the same question as where did my frame go, and it is no longer allowed to headline a table built to answer the second one.
Share of frame says something is expensive. It cannot say why. Five thousand calls at 0.4µs and three calls at 700µs produce the same share and need opposite remedies — call it less often, versus make the call cheaper. Both figures are published for every patch, because publishing only one of them hands the reader a conclusion they cannot act on.
Rates gain decimal places as they shrink. A patch that runs once every 250 frames is a real reading, and rounding it to 0.0 would make it indistinguishable from one that never ran at all.
Seen, measured, ranked
Three different run counts appear beside each mod, and they are shown as three because collapsing them into one hides which question is being answered.
| Seen | runs the mod was loaded in. Presence. Increases whenever anyone submits a run containing it, whether or not they profiled anything |
| Measured | runs that profiled it. Nothing is instrumented until a target is armed in game, so this is normally far smaller. It is the denominator of every cost figure |
| Ranked | runs whose cost could be normalised to a share of frame, which needs a recorded measurement window. The 25-run floor applies to this one |
seen ≥ measured ≥ ranked always holds. A large gap between the first two is information about the corpus rather than about the mod: it means plenty of people run it and nobody has pointed the profiler at it.
The mods list previously rendered the narrowest of these three counts under the broadest label. A mod present in hundreds of runs could sit at a run count that never moved, because the number being displayed only counted the runs that happened to have armed it. If you noticed the column looking frozen, you were reading it correctly and it was wrong.
Tick rate and targets
Ticks per second is not one quantity. RimWorld's target rate is 60 x TickRateMultiplier, so it depends entirely on the speed the player chose, and on any mod that alters the multiplier:
| Normal | target 60 tps |
| Fast | target 180 tps |
| Superfast | target 360 tps |
| Ultrafast | target 900 tps |
| Modded | anything, 330, 660 and 3000 all occur in real runs |
A player achieving 200 tps at normal speed has hit the ceiling; one achieving 200 at ultrafast is at 22% of target and struggling. Comparing those two raw numbers is meaningless, so every comparison on this site uses percentage of target.
Ticks per second is the simulation; frames per second is rendering. They have different targets, different bottlenecks and different fixes. The simulation can stall while rendering stays perfectly smooth, and the reverse. They are never combined into one figure.
The four speeds are not four fixed rates
The multiplier is computed, not looked up, and the engine changes it without being asked. All of the following are vanilla readings:
| Superfast, nothing happening | 12x → target 720 |
| Superfast, no map loaded | 18x → target 1080 |
| Ultrafast, boosted | 150x → target 9000 |
| Any speed, during danger | forced to 1x → target 60 |
So a band is always labelled by the target that was observed, never by the speed button that was pressed. A run recorded during a raid reads as normal speed throughout, because that is what the engine was doing.
Comparing across speeds
A mean tick rate over a whole run is not a property of the game. Tick rate is capped by the speed the player selected, so the average reports how long each speed button was held as much as it reports the code. Two runs of identical performance disagree by a factor of three if one sat at normal and the other at fast.
A run that happened to spend longer at ultrafast looked like an improvement. A run interrupted by a raid — which pins the engine to normal speed — looked like a regression. Neither statement was about the mod list, and both were being reported as verdicts.
Every run is therefore broken into speed bands, one per observed target, and two runs are compared band by band. Within a band the ceiling is identical in both by construction, so what is left is the game.
- A band needs 5 samples in both runs to be compared. Below that it is shown and excluded from the verdict
- A speed only one run recorded appears as an explicit gap, naming which run is missing it. “Not measured” and “unchanged” must not look alike
- Runs sharing no comparable speed get a refusal, not a number
- Speed-mix overlap is reported when the two runs spent their time differently. Below 75% the whole-run means are dimmed, because on their own they mostly describe the speed buttons
The speed sweep
Left alone, a recording captures whatever speeds the player happened to use, and two runs rarely overlap enough to compare. The speed sweep makes coverage deliberate: it drives the game through normal, fast, superfast and ultrafast in turn, dwelling at each, so every run ends with comparable samples at all four.
It verifies rather than assumes. Setting the game speed is a request the engine can refuse or override, so the sweep records the target it observed in each phase and the run says so when a phase did not run at the rate it asked for. A sweep that was silently overridden and a sweep that worked must not produce the same-looking run.
Paused samples are dropped rather than counted as a zero-tick band. Frame-driven work keeps running while paused, so those samples are real — they are just not evidence about tick rate, and a zero band would drag every average down in proportion to time spent reading a menu.
The hardware index
The index is a measured score, not a specification lookup. Circinus runs a frozen single-threaded benchmark and reports:
The workload mixes dependent integer and floating point arithmetic with an unpredictable branch, so it cannot be vectorised or optimised away, and it is deliberately similar in character to pathfinding and tick maths. An index that ranked machines differently from how RimWorld ranks them would be worse than no index.
Best of five, not the average
Every source of interference is one-directional: thermal throttling, background load and laptop power profiles can only ever make a pass slower. The fastest of five passes is therefore the least contaminated estimate, and averaging just folds in whatever noise happened to be present.
Frozen by contract
Changing the workload changes every score, so the benchmark carries a version. Scores from different versions are refused rather than silently mixed. Version 1 reported an arbitrary 100000/ms; version 2 reports the iteration rate above, and no v1 score is comparable to a v2 one.
Because it is measured rather than looked up, two machines with the same processor and different cooling, memory or power settings will score differently. That is the point.
How cohorts are formed
A cohort is the set of runs that are actually comparable to yours. Three axes:
- Hardware index, within +/- 60 of yours
- Mod count band, because share of frame shrinks as more mods compete
- Game speed, because the target differs and a target is not optional context
Bucketing on processor and graphics model strings instead would produce thousands of cohorts of two runs each, and the minimum-sample floor would then make nothing publishable at all. A measured index collapses that into a usable number of groups and needs no lookup table to curate.
On the cohort page, the mod count describes the run being compared. Raising it says that run had more mods loaded and still reached the same tick rate, which compares more favourably. It does not model adding mods to your game, which would lower the result rather than raise the ranking.
Percentiles
Placement is reported as a percentile of the cohort, not as a distance from the median. "27% above the median" reads naturally as "above 27% of machines", which is a different and much stronger claim; a percentile says exactly one thing.
The distribution is modelled as normal around the cohort median. Once enough real runs exist this will be replaced by the empirical distribution, which needs no assumption about shape.
Correlations
The predictors panel reports Pearson correlation between each hardware attribute and the achieved percentage of target, across the cohort. RimWorld's simulation is overwhelmingly single-thread CPU bound, which is why the CPU index correlates strongly and the graphics card essentially not at all.
Players routinely blame the graphics card for a low tick rate and buy the wrong upgrade. Capturing all three attributes lets the site demonstrate the relationship rather than claim it, which costs nothing extra once the data is collected.
When we refuse
A tool that answers everything confidently is a tool that is sometimes confidently wrong. Circinus declines in these cases by design:
| Fewer than 25 runs | no ranking, no verdict, figures shown but not placed |
| Profiling mismatch | an observed run is not comparable to an unobserved one |
| Different iteration counts | a different iteration count is a different experiment |
| Different calibration versions | scores are not on the same scale |
| Incomplete runs | kept and labelled, never silently averaged in |
A missing profiler reads as no data, never as zero cost. Otherwise an absent measurement would look like a performance win.
Induced cost, which we cannot attribute
Cost is time spent inside a mod's own code. If mod A spawns four hundred extra things and mod B's tick becomes more expensive as a result, that time is recorded against B, and B looks guilty.
Separating the two requires a controlled comparison on one machine with and without mod A, which is the run diff feature in the tool itself. The site cannot do it and does not claim to. Every figure here means "time spent in this mod's code" and never "time this mod caused".
Measured versus projected
Hardware indices are labelled in the interface:
- Measured - a real calibration score submitted from that processor
- Projected - scaled from published single-thread ratios, anchored on a measured part
Projected entries are estimates and are marked as such wherever they appear. They will be replaced by observed medians as runs arrive, not by better guesses. An uncorrected projection is indistinguishable from a measurement to anybody reading the site, which is why the label is carried all the way through.
The tier names are provisional in the same way. They should be percentiles of the observed corpus rather than fixed thresholds, and will be once a corpus exists.
What gets sent
Submission is opt-in and off by default.
- Coarsened hardware: processor model with vendor noise stripped, memory rounded to a tier, graphics model, operating system family with the build number removed
- The package ids of your installed mods, and a hash of the list, so an error can be attributed to a mod and runs with similar lists can be compared. Names, versions and load order are not sent
- Per-mod and per-patch costs, and the sample series. Per-patch rows carry the patched method's type and name, the patch kind, and raw call counts and timings — every rate shown on the site is derived from those at query time, never stored pre-divided
- An install id that is salted and hashed server side, used to rate-limit and de-duplicate rather than to identify
File paths are scrubbed before anything leaves the machine, which is covered by tests. No account, no email, nothing personal. Export and delete by install id.
API
Everything the site shows is served by a versioned REST API using the same types the in-game mod writes, so the client and the server cannot drift apart in how a run document is read.