Skip to content
Closed
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
39 changes: 39 additions & 0 deletions .github/workflows/build-benchmark-v11port.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Build BenchmarkAndOptimizerV11Port
on:
push:
branches:
- pr/benchmark-optimizer-v11-clean
paths:
- 'BenchmarkAndOptimizerV11Port/**'
- '.github/workflows/build-benchmark-v11port.yml'
pull_request:
paths:
- 'BenchmarkAndOptimizerV11Port/**'
- '.github/workflows/build-benchmark-v11port.yml'
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Build CSV profiler
run: dotnet build BenchmarkAndOptimizerV11Port/BenchmarkAndOptimizerV11Port.csproj -c Release
- name: Stage ready-to-install mod
shell: bash
run: |
set -euxo pipefail
ROOT="${RUNNER_TEMP}/timberborn-artifact/BenchmarkAndOptimizerV11Port"
mkdir -p "$ROOT/version-1.1/Scripts"
cp BenchmarkAndOptimizerV11Port/manifest.json "$ROOT/version-1.1/manifest.json"
cp BenchmarkAndOptimizerV11Port/bin/Release/netstandard2.1/BenchmarkAndOptimizerV11.dll "$ROOT/version-1.1/Scripts/"
test -s "$ROOT/version-1.1/Scripts/BenchmarkAndOptimizerV11.dll"
- uses: actions/upload-artifact@v4
with:
name: BenchmarkAndOptimizerV11Port_1.1.104
path: ${{ runner.temp }}/timberborn-artifact/
if-no-files-found: error
33 changes: 33 additions & 0 deletions BenchmarkAndOptimizerV11Port/BenchmarkAndOptimizerV11Port.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>BenchmarkAndOptimizerV11</AssemblyName>
<RootNamespace>BenchmarkAndOptimizerV11</RootNamespace>
<AssemblyVersion>1.1.104.0</AssemblyVersion>
<FileVersion>1.1.104.0</FileVersion>
</PropertyGroup>
<ItemGroup>
<Compile Remove="References/**/*.cs" />
<Compile Remove="Stubs/**/*.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Lib.Harmony" Version="2.4.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<ProjectReference Include="Stubs/Timberborn.Modding/Timberborn.Modding.Stub.csproj" Private="false" />
<ProjectReference Include="Stubs/Timberborn.ModManagerScene/Timberborn.ModManagerScene.Stub.csproj" Private="false" />
<ProjectReference Include="Stubs/Timberborn.SettingsSystem/Timberborn.SettingsSystem.Stub.csproj" Private="false" />
<ProjectReference Include="Stubs/Timberborn.Options/Timberborn.Options.Stub.csproj" Private="false" />
<ProjectReference Include="Stubs/Timberborn.OptionsGame/Timberborn.OptionsGame.Stub.csproj" Private="false" />
<ProjectReference Include="Stubs/Timberborn.SingletonSystem/Timberborn.SingletonSystem.Stub.csproj" Private="false" />
<ProjectReference Include="Stubs/Bindito.Core/Bindito.Core.Stub.csproj" Private="false" />
<ProjectReference Include="Stubs/UnityEngine.UIElementsModule/UnityEngine.UIElementsModule.Stub.csproj" Private="false" />
<ProjectReference Include="Stubs/Timberborn.CoreUI/Timberborn.CoreUI.Stub.csproj" Private="false" />
<ProjectReference Include="Stubs/ModSettings.Core/ModSettings.Core.Stub.csproj" Private="false" />
<ProjectReference Include="Stubs/ModSettings.CoreUI/ModSettings.CoreUI.Stub.csproj" Private="false" />
<ProjectReference Include="Stubs/ModSettings.CommonUI/ModSettings.CommonUI.Stub.csproj" Private="false" />
<ProjectReference Include="Stubs/TimberUi/TimberUi.Stub.csproj" Private="false" />
</ItemGroup>
</Project>
83 changes: 83 additions & 0 deletions BenchmarkAndOptimizerV11Port/DispatcherPatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System.Collections;
using System.Reflection;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using Newtonsoft.Json;
using HarmonyLib;
using Timberborn.ModManagerScene;

namespace BenchmarkAndOptimizerV11;

static class DispatcherPatcher
{
static bool isPatched;

readonly record struct Target(string TypeName, string MethodName, string PrefixName);

static readonly Target[] Targets =
{
new("Timberborn.TickSystem.TickableSingletonService", "TickSingletons", nameof(PrefixTickSingletons)),
new("Timberborn.TickSystem.TickableEntity", "TickTickableComponents", nameof(PrefixTickableComponents)),
new("Timberborn.SingletonSystem.SingletonLifecycleService", "UpdateSingletons", nameof(PrefixUpdateSingletons)),
new("Timberborn.SingletonSystem.SingletonLifecycleService", "LateUpdateSingletons", nameof(PrefixLateUpdateSingletons)),
};

public static void Patch(Harmony harmony)
{
if (isPatched)
{
return;
}

foreach (var target in Targets)
{
var type = AccessTools.TypeByName(target.TypeName);
var original = type is null ? null : AccessTools.Method(type, target.MethodName);
var prefix = AccessTools.Method(typeof(DispatcherPatcher), target.PrefixName);

if (original is null || prefix is null)
{
Runtime.Log($"warning: target not found: {target.TypeName}.{target.MethodName}; leaving vanilla behavior");
continue;
}

harmony.Patch(original, prefix: new HarmonyMethod(prefix) { priority = Priority.Low });
Runtime.Log($"benchmark patch installed: {target.TypeName}.{target.MethodName}");
}

isPatched = true;
}

public static void Unpatch(Harmony harmony)
{
if (!isPatched)
{
return;
}

foreach (var target in Targets)
{
var type = AccessTools.TypeByName(target.TypeName);
var original = type is null ? null : AccessTools.Method(type, target.MethodName);
if (original is not null)
{
harmony.Unpatch(original, HarmonyPatchType.Prefix, harmony.Id);
}
}

isPatched = false;
Runtime.Log("benchmark dispatcher patches removed");
}

public static bool PrefixTickSingletons(object __instance) =>
Runtime.TryDispatch(__instance, "_tickableSingletons", "_tickableSingleton", "Tick");

public static bool PrefixTickableComponents(object __instance) =>
Runtime.TryDispatch(__instance, "_tickableComponents", "_tickableComponent", "StartAndTick", "Enabled");

public static bool PrefixUpdateSingletons(object __instance) =>
Runtime.TryDispatch(__instance, "_updatableSingletons", null, "UpdateSingleton");

public static bool PrefixLateUpdateSingletons(object __instance) =>
Runtime.TryDispatch(__instance, "_lateUpdatableSingletons", null, "LateUpdateSingleton");
}
76 changes: 76 additions & 0 deletions BenchmarkAndOptimizerV11Port/FrameProfileSampler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using HarmonyLib;

namespace BenchmarkAndOptimizerV11;

/// <summary>
/// Optional, read-only frame/GC samples during the existing 120s CSV benchmark.
/// Records one row per ten Unity UpdateSingletons calls, and never skips game logic.
/// This does not turn on legacy scene-wide, mod-specific or invasive profilers.
/// </summary>
static class FrameProfileSampler
{
static MethodBase? targetMethod;
static bool isPatched;
static long startTick;
static long sampleTick;
static int frames;
static int lastSampleFrame;
static readonly List<string> Rows = [];

public static void Patch(Harmony harmony)
{
if (isPatched) { return; }
var lifecycle = AccessTools.TypeByName("Timberborn.SingletonSystem.SingletonLifecycleService");
targetMethod = lifecycle is null ? null : AccessTools.Method(lifecycle, "UpdateSingletons");
if (targetMethod is null) { return; }
try
{
harmony.Patch(targetMethod, postfix: new HarmonyMethod(AccessTools.Method(typeof(FrameProfileSampler), nameof(AfterUpdate))));
Rows.Clear();
Rows.Add("Frame,ElapsedSeconds,FrameMsAvg,FPS,GC0,GC1,GC2,ManagedHeapBytes");
startTick = sampleTick = Stopwatch.GetTimestamp();
frames = lastSampleFrame = 0;
isPatched = true;
}
catch { targetMethod = null; isPatched = false; }
}

static void AfterUpdate()
{
if (!isPatched || !Runtime.IsBenchmarking) { return; }
++frames;
if (frames - lastSampleFrame < 10) { return; }
var now = Stopwatch.GetTimestamp();
var n = frames - lastSampleFrame;
var seconds = (now - sampleTick) / (double)Stopwatch.Frequency;
if (seconds <= 0) { return; }
Rows.Add(FormattableString.Invariant(
$"{frames},{(now - startTick) / (double)Stopwatch.Frequency:F3},{1000 * seconds / n:F3},{n / seconds:F2},{GC.CollectionCount(0)},{GC.CollectionCount(1)},{GC.CollectionCount(2)},{GC.GetTotalMemory(false)}"));
sampleTick = now;
lastSampleFrame = frames;
}

public static void StopAndWrite(Harmony? harmony, string modPath)
{
if (!isPatched) { return; }
isPatched = false;
if (harmony is not null && targetMethod is not null)
{
try { harmony.Unpatch(targetMethod, HarmonyPatchType.Postfix, harmony.Id); }
catch { /* Never alter the caller's benchmark completion. */ }
}
targetMethod = null;
try
{
File.WriteAllLines(Path.Combine(modPath,
$"optimizer-v11-frames-{DateTime.Now:yyyyMMdd-HHmmss}.csv"), Rows);
}
catch { /* CSV profiling failure must not break gameplay. */ }
Rows.Clear();
}
}
6 changes: 6 additions & 0 deletions BenchmarkAndOptimizerV11Port/IsExternalInit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit
{
}
}
20 changes: 20 additions & 0 deletions BenchmarkAndOptimizerV11Port/ModStarter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Collections;
using System.Reflection;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using Newtonsoft.Json;
using HarmonyLib;
using Timberborn.ModManagerScene;

namespace BenchmarkAndOptimizerV11;

public sealed class ModStarter : IModStarter
{
public void StartMod(IModEnvironment modEnvironment)
{
var harmony = new Harmony("tharbad08.BenchmarkAndOptimizerV11Port");
Runtime.Initialize(modEnvironment.ModPath, harmony);

Runtime.Log($"loaded; config={Runtime.ConfigPath}; normal throttling uses direct method patches");
}
}
49 changes: 49 additions & 0 deletions BenchmarkAndOptimizerV11Port/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Benchmark & Optimizer — isolated Timberborn v1.1 port

This branch is based on `chatgpt/BenchmarkAndOptimizerV11Port`. It retains only the original benchmark/CSV profiler and opt-in interval controls. It is **not** the TB Optimizer gameplay mod.

- **Per-system CSV benchmark:** start the 120-second sampling session from Mod Settings. Results `optimizer-v11-benchmark-*.csv` contain type, mod origin, total time, invocation count, average/min/max duration. TimberPhysics has observational StepAll/Physics.Simulate timing when installed.
- **Frame/GC CSV profile:** the same session writes `optimizer-v11-frames-*.csv` with ten-frame-average FPS/frame-time plus GC generation counts and managed heap. FPS is actual rendered-frame cadence, **not** simulation speed; no game-speed metric is inferred.
- **Update intervals:** per-system controls default to 1 (vanilla). All simulation-critical/global schedulers are pinned to interval 1. The benchmark temporarily removes interval patches to time unthrottled gameplay and restores them after completion.
- **No gameplay optimizations** (BFR, EBR, preview, fauna, soil, SuperCursor, TimberPhysics catch-up/collider, GC tweaks), no startup guard or save serialization patches.
- **No always-on tracing:** profiler hooks are installed only during a requested benchmark. The full historical diagnostic and experiment sources remain on the original parent branch; heavy legacy profilers are not installed in the isolated branch.

The benchmark's existing dispatcher instrumentation calls original tick/update methods via validated reflected dispatch. In-game verification is still required even if GitHub Actions succeeds.

## Running alongside TB Optimizer

**Keep both mods enabled.** This port profiles the running colony, while [TB Optimizer](https://github.com/tharbad08/TB-Optimizer) applies stable, individually configurable fixes. Their mod IDs, DLL assembly names and Harmony owners are distinct.

- During benchmarking, this mod temporarily disables **only its own optional update-interval throttles**. TB Optimizer remains active and the CSV measures the actual optimized configuration, not a vanilla baseline.
- At the default interval **1**, Benchmark & Optimizer does not throttle game systems between benchmark sessions. Avoid increasing intervals when comparing the stable TB Optimizer fixes: skipped callbacks can change simulation behavior.
- The mods intentionally share observation/optimization targets without sharing ownership: `SingletonLifecycleService.UpdateSingletons` (benchmark dispatcher and frame/GC sampling versus TB Optimizer's optional GC postfix), and TimberPhysics `PhysicsSimulator.UpdateSingleton` (read-only split timings versus TB Optimizer's optional catch-up limiter).
- This has been checked at the source and Harmony patch-owner level. Full in-game concurrency and benchmark results still require validation.



## System discovery, filter, and future improvements

**Existing behavior**
- A startup scan discovers eligible tick/update systems from assemblies that are already loaded.
- The **120-second benchmark** can discover additional systems encountered during gameplay. Currently it is the way to exercise runtime discovery as well as collect timing data; the initial startup scan alone might not provide the full list.
- **Neither startup scanning nor benchmarking guarantees discovering every system:** a mod may load later, or its code may never run during the benchmark.
- The Update Intervals panel includes a filter for system name and mod origin. Its crash fix changes visibility of existing rows instead of destroying and recreating controls during each keystroke.
- **Refresh detected systems** refreshes the displayed in-memory list; it does not independently perform a new scan. `optimizer-v11.json` stores interval configuration, **not** a complete catalog of detected systems.

**Future work — documentation only, not implemented**
1. Add a standalone **Detect systems** button that performs safe discovery without needing to start the 120-second benchmark.
2. Consider triggering discovery **after the save finishes loading** and/or **when opening the mod settings window**, so systems unavailable at initial startup can be detected. Avoid expensive repeated full scans and never disturb simulation/tick scheduling.
3. Save the **entire discovered-system catalog** to a human-readable, manually editable JSON file, including full type name, mod/assembly origin, protected status, and current interval. Design safe validation/reload of manual edits; manual configuration must never override protections for critical simulation systems.
4. Make it clear in the UI that **even a separate discovery scan may not detect systems loaded later or never activated**. Offer a way to refresh the catalog as gameplay or mod loading reveals more systems.

These suggestions are future enhancements only; no discovery timing, JSON export, or UI behavior is changed by this note.


## Verified source ownership

- [TB-Optimizer](https://github.com/tharbad08/TimberbornMods/tree/TB-Optimizer/BenchmarkAndOptimizerV11Port/Optimizations): gameplay fixes are split into per-mod files there.
- [TBSave](https://github.com/tharbad08/TimberbornMods/tree/chatgpt/TBSave): save/loading investigations and the Bindito generated-assembly startup guard are preserved there. They are research snapshots, not independently verified isolated save-only binaries.
- [TBGC](https://github.com/tharbad08/TimberbornMods/tree/chatgpt/TBGC): GC investigation/prototypes are preserved there; this is not proof of an independent live GC optimization.
- [EBR-Optimizations](https://github.com/tharbad08/TimberbornMods/tree/chatgpt/EBR-Optimizations): legacy EBR-specific experiments and profiling remain preserved; active EBR performance fix is in TB-Optimizer.

See [TICK_SAFETY.md](TICK_SAFETY.md) for protected systems and discovered failure cases.
Loading