Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Changelog

## 0.1.3 (preview, not yet played)
## 0.1.3 (preview)

**The defaults now capture the most detail a session can hold without anyone touching a setting**, so every recording made from a plain
install is as informative as this mod can make it.
Expand All @@ -17,7 +17,8 @@ install is as informative as this mod can make it.
rather than the depth of any one row, and the budget system above already throttles itself if measuring gets expensive, so there was no
clear "more detail" case for moving them, only a "more rows" one.
- Expect a real, measurable increase in the mod's own cost from this alone — deep's component sampling plus double the sampling budget.
Nobody has played it yet; the five-minute check in `docs/TESTING.md` now starts by checking `overheadUs` is still reasonable.
Nobody had played it when it was released (it has been since: see `docs/TESTING.md`); step 10 of the five-minute check there measures
what it costs.

## 0.1.2 (preview, not yet played)

Expand All @@ -43,7 +44,7 @@ that recording. Each one that can be checked outside the game has a check that f
figures and the garbage-collection section**; `tools/perflog.py` says so.
- **Files are no longer held open.** 0.1.0 kept `frames.csv`, `profile.csv`, `spikes.csv` and `events.csv` open for writing, so zipping or copying the folder while the game ran silently left them
out (and a folder listing showed size 0). They are now opened, appended to and closed on each write, shared with readers, and retried if someone else holds them.
- **Every singleton of the game itself was labelled with no mod ("(unknown)")**; the mod map was installed without the game's own assemblies. They are now `game`. The analysis tool applies the same
- **Every singleton of the game itself was labeled with no mod ("(unknown)")**; the mod map was installed without the game's own assemblies. They are now `game`. The analysis tool applies the same
rule to older recordings.
- **`workingMB` was always 0** (Unity's Mono reports 0 for the process's memory). It now asks Windows, and the header says where the figure comes from (`# capability|workingSet|...`).
- **Loading steps now record how much the managed heap grew** during each one (`allocKB` of the load rows, "heap grew MB" in `summary.md`, and a finding in the report). The first recording's
Expand Down
13 changes: 7 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ packaging/ manifest.json, PerformanceLog.cfg. build.ps1 builds, tests and

```
.\build.ps1 -SkipTests build + package
dotnet run --project tests -c Release all C# checks (about 70)
dotnet run --project tests -c Release all C# checks (91 at 0.1.3; docs/TESTING.md keeps the current count)
python -m unittest discover -s tools -p "test_perflog.py"
dotnet run --project tests -c Release -- --print-columns
dotnet run --project tests -c Release -- --write-sample tests/fixtures/sample-with-mod
Expand Down Expand Up @@ -106,12 +106,13 @@ To read the game's own code (the way every patch target here was checked): `ilsp

## What is and is not verified

0.1.0 was run once in the real game (2026-09-20, 31 minutes, nine mods including BeaverBuddies, clean exit). Its recording is why 0.1.1 exists: see `CHANGELOG.md` and `docs/TESTING.md`
(what that run proved, what it broke, and what is still unproven). When you are given a recording, start with the `# capability` and `# capability-final` lines in the `frames.csv`
header and the "Read first" section of `summary.md`: they say which parts worked. `python tools/perflog.py report <folder>` prints a `KNOWN ISSUE` line for every known defect of the
version that made it (`KNOWN_ISSUES` in `tools/perflog.py`); add to that list whenever a version is found to record something wrong.
0.1.0 was first run in the real game on 2026-09-20 (31 minutes, nine mods including BeaverBuddies, clean exit). Its recording is why 0.1.1 exists; 0.1.1 and 0.1.3 have been
recorded in the game since. See `CHANGELOG.md` and `docs/TESTING.md` (what those runs proved, what the first one broke, and what is still unproven). When you are given a
recording, start with the `# capability` and `# capability-final` lines in the `frames.csv` header and the "Read first" section of `summary.md`: they say which parts worked.
`python tools/perflog.py report <folder>` prints a `KNOWN ISSUE` line for every known defect of the version that made it (`KNOWN_ISSUES` in `tools/perflog.py`); add to that list
whenever a version is found to record something wrong.

Two lessons from that run that shape how to change this code:
Lessons from the first run that shape how to change this code:
- **Count what a patch really does.** `# capability-final|patchCalls|...` showed 488601 wrapper swaps in 122150 frames: the hit counters are how a defect that only exists in the game shows up.
Keep a counter for anything done once per game or per frame.
- **The game keeps more than one singleton service alive** (the application's and the game's, both updated every frame), so anything remembered "per service" must hold several
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2026 Kyler Ramsey
Copyright (c) 2026 Timbermods

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
62 changes: 37 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,31 @@ A Timberborn 1.1 mod (built against **1.1.2.4**) that measures **where the game'
person, or an AI assistant such as Claude, can read to find out why a game is slow. It is a diagnostic tool for finding performance problems in
the game and in other mods. It does nothing else.

Version **0.1.2** is a **preview**. Its automated checks run against the game's real assemblies, and version 0.1.0 has been played once (a 31 minute session with
nine mods, ending in a normal exit): every patch applied and the recording was complete. That first recording also showed several defects, fixed in 0.1.1; 0.1.2
adds an in-game settings page and has not itself been played yet. See [CHANGELOG.md](CHANGELOG.md) for what changed in each version. If a part fails to start it
says so in the log and the summary, that part stays off, and the game carries on. Read [docs/TESTING.md](docs/TESTING.md) for what is and is not verified, and
how to check it in a game in five minutes.
Version **0.1.3** is a **preview**, published as a pre-release. Its automated checks run against the game's real assemblies.
Version 0.1.0 was the first to be played (a 31 minute session with nine mods, ending in a normal exit): every patch applied and the recording was complete.
That first recording also showed several defects, fixed in 0.1.1. 0.1.2 added an in-game settings page, and 0.1.3 made the most detailed profile the default.
0.1.1 and 0.1.3 have been played since. A 44 minute 0.1.3 recording with ten mods shows the new defaults working, and every 0.1.1 fix that a recording can show.
Not verified yet: whether a value changed on the settings page reaches a recording.

See [CHANGELOG.md](CHANGELOG.md) for what changed in each version, and [docs/TESTING.md](docs/TESTING.md) for what is and is not verified and how to check
it in a game in five minutes. If a part fails to start, it says so in the log and the summary, that part stays off, and the game carries on.

It only observes. It never records or replays an action, never uses the game's random numbers and never touches anything the simulation reads, so
it should not cause a desync in co-op. (That has not been played in co-op yet.)

## Install

1. Close Timberborn. Extract the release ZIP into `Documents\Timberborn\Mods`. It contains one `PerformanceLog` folder.
2. It requires the **Harmony** mod (2.4.1 or newer) and the **Mod Settings** mod (1.1.0.0 or newer) from the Steam Workshop.
3. Launch Timberborn, enable **Performance Log** in the mod manager, and restart.
4. Play. Leave normally (menu → exit) so the files are finished; if the game crashes the files are still readable and the summary is at most a
minute stale. (From 0.1.1 you can also copy or zip the folder while the game runs; 0.1.0 held four of its files open, so a zip made then left them out.) Look for `[PerformanceLog]` lines in `Player.log`
(`%USERPROFILE%\AppData\LocalLow\Mechanistry\Timberborn\Player.log`).
1. Open the [Releases page](https://github.com/timbermods/PerformanceLog/releases) and pick the newest pre-release (there is no stable release yet).
Under **Assets**, download `PerformanceLog-<version>.zip`, not "Source code".
2. Close Timberborn. Extract the ZIP into `Documents\Timberborn\Mods`. It contains one `PerformanceLog` folder.
3. Subscribe to the **Harmony** mod (2.4.1 or newer) and the **Mod Settings** mod (1.1.0.0 or newer) on the Steam Workshop. Performance Log needs both.
4. Launch Timberborn, enable **Performance Log** in the mod manager, and restart.
5. Play. Leave normally (menu → exit) so the files are finished. If the game crashes, the files are still readable and the summary is at most a
minute stale. You can also copy or zip the folder while the game runs (0.1.0 could not: it held four of its files open, so a zip left them out).
Look for `[PerformanceLog]` lines in `Player.log` (`%USERPROFILE%\AppData\LocalLow\Mechanistry\Timberborn\Player.log`).

Recordings go to `Documents\Timberborn\PerformanceLog\<date and time>\`, one folder per game session (a session is from a save finishing loading to leaving it).
Recordings go to `Documents\Timberborn\PerformanceLog\<date and time>\` (for example `2026-09-21_14-05-33`, local time), one folder per game session.
A session runs from a save finishing loading to leaving it. The `OutputFolder` setting can move them.

## What to do with a recording

Expand All @@ -40,8 +46,9 @@ python tools/perflog.py compare <folder A> <folder B>
python tools/perflog.py list
```

`report` says what stands out, with the evidence and what to check next; `compare` lines up two sessions and says what changed and what else differed
(different mods, game speed, colony size, computer). The tool is also in the release ZIP, in `PerformanceLog\tools`. For a fair comparison, record the
`report` says what stands out, with the evidence and what to check next. `compare` lines up two sessions and says what changed and what else differed
(different mods, game speed, colony size, computer). `list` shows every session in `Documents\Timberborn\PerformanceLog`, or in a folder you name.
The tool is also in the release ZIP, in `PerformanceLog\tools`. For a fair comparison, record the
same save at the same game speed for at least three minutes each, with the window in front, and change one thing.

## What it records
Expand Down Expand Up @@ -85,7 +92,7 @@ Nothing here changes what the game simulates, so co-op players may use different
| `SpikeContributors` | `8` | .cfg or Mod Settings | How many of the biggest contributors to each slow frame are written to `spikes.csv` (8 is the most it can hold). |
| `MaxSlowRowsPerMinute` | `300` | .cfg or Mod Settings | At most this many slow frames get a row a minute; the rest are only counted, so a game that is slow all the time cannot fill the disk. |
| `OutputFolder` | *(empty)* | .cfg only | Where session folders go. Empty = `Documents\Timberborn\PerformanceLog`. |
| `Watch` | *(none)* | .cfg only | Full names of methods to time: `Namespace.Type.Method`, separated by `;`, on as many lines as you like. Rows appear in `profile.csv` as kind `method`. |
| `Watch` | *(none)* | .cfg only | Full names of methods to time: `Namespace.Type.Method`, separated by `;`, on as many lines as you like (at most 40 methods; every overload of a name is watched). Rows appear in `profile.csv` as kind `method`. |

The first time you open the Mod Settings page it starts from whatever `PerformanceLog.cfg` already says; after that, whatever you set there is what
is used, and editing that number in the file no longer does anything (Mod Settings remembers it, not this mod).
Expand All @@ -96,16 +103,17 @@ To measure another mod's method, for example:
Watch = LateGamePerformance.HaulCache.OnTickStarted; LateGamePerformance.MetricsDump.OnTickStarted
```

The mod must be enabled so its type can be found. Every call of a watched method pays for a Harmony wrapper and a lookup even when it is not timed, so watching a method that runs thousands of times a tick costs
more than watching a rare one; the cost is in `overheadUs`. Methods with a `catch ... when` clause are refused (Harmony cannot patch them under Mono and the
attempt can crash the game) and the log says so.
The mod must be enabled so its type can be found. Every call of a watched method pays for a Harmony wrapper and a lookup even when it is not timed, so watching a
method that runs thousands of times a tick costs more than watching a rare one. The cost is in `overheadUs`. Methods with a `catch ... when` clause are refused
(Harmony cannot patch them under Mono and the attempt can crash the game). A `# watch|` line in the `frames.csv` header, and the "What each measurement source
could do" section of `summary.md`, say for each name whether it is being watched or why not.

## Working with other mods

The mod puts a timing wrapper in front of each of the game's singletons, but only on the first tick and first frame of a game, after every other mod's `Load` patches have run, so a mod that looks at
those singletons (BeaverBuddies reorders the once-per-tick ones by their type) still sees the game's own and tick order is what it would be without this mod. It never replaces a game method and
never skips the original. When another mod defers the game's save to the end of a tick (BeaverBuddies does), the `queued save` event reads about 0 ms and the real one is the `save (writing the world)`
event.
The mod puts a timing wrapper in front of each of the game's singletons. It does this only on the first tick and first frame of a game, after every other mod's
`Load` patches have run. So a mod that looks at those singletons (BeaverBuddies reorders the once-per-tick ones by their type) still sees the game's own, and the
tick order is what it would be without this mod. It never replaces a game method and never skips the original. When another mod defers the game's save to the
end of a tick (BeaverBuddies does), the `queued save` event reads about 0 ms and the real one is the `save (writing the world)` event.

## What it costs, and what it cannot see

Expand All @@ -120,27 +128,31 @@ Everything is timed on the game thread only.

## How it relates to the BeaverBuddies frame rate log

The measuring core is the frame rate log built for the BeaverBuddies Stability Fork in `1.0.10-perflog-preview` and `preview2`
The measuring core is the frame rate log built for the BeaverBuddies Stability Fork in `v1.0.10-perflog-preview` and `v1.0.10-perflog-preview2`
([`perflog-preview` branch](https://github.com/timbermods/BeaverBuddies-Stability-Fork/tree/perflog-preview)), rebuilt as a standalone mod that works in single
player and with any mods. What is new: it hooks the game itself (no other mod's code has to be edited), attributes time to mods, records loading and saves,
generates its own readme and summary, and comes with the analysis tool. What is left out because it is about the co-op layer: the network and event-hash
timings, waits for the other player, and the garbage collection experiment (this mod does not change how the game runs).

## Build from source

Install the .NET 8 SDK and Python 3, and have Timberborn (and the Harmony Workshop mod) installed:
Install the .NET 8 SDK and Python 3, and have Timberborn and the **Harmony** and **Mod Settings** Workshop mods installed (the build references their DLLs):

```powershell
.\build.ps1 -GameDir 'C:\Program Files (x86)\Steam\steamapps\common\Timberborn'
```

builds the mod, runs the checks, and creates `dist\PerformanceLog-0.1.2.zip`. `.\build.ps1 -Install` also copies it into your `Mods` folder. The checks alone:
builds the mod, runs the checks, and creates `dist\PerformanceLog-<version>.zip` (the version in `packaging/manifest.json`). `.\build.ps1 -Install` also copies it into
your `Mods` folder. The checks alone:

```
dotnet run --project tests -c Release
python -m unittest discover -s tools -p "test_perflog.py"
```

If Timberborn is not in the default Steam folder, pass `-GameDir` to `build.ps1` as above. To run the checks alone, give them the game folder too:
`dotnet run --project tests -c Release -p:GameDir='<game folder>' -- --managed '<game folder>\Timberborn_Data\Managed'`.

No game, Unity or Harmony DLLs are redistributed; they are only build references. See [CLAUDE.md](CLAUDE.md) for how the code is laid out and how to change it.

## License
Expand Down
Loading
Loading