diff --git a/.github/workflows/perf-gate.yml b/.github/workflows/perf-gate.yml
index 12d677dc95..85fd662dce 100644
--- a/.github/workflows/perf-gate.yml
+++ b/.github/workflows/perf-gate.yml
@@ -1,4 +1,4 @@
-name: Scrolling Performance Gate
+name: Performance Gate
on:
push:
@@ -63,7 +63,7 @@ jobs:
retention-days: 7
perf-benchmarks:
- name: Scrolling Benchmarks (Linux, ShortRun)
+ name: Benchmarks (Linux, ShortRun)
runs-on: ubuntu-latest
# Only run on pushes to develop/main, not on every PR (slow and not blocking).
if: github.event_name == 'push'
@@ -89,7 +89,7 @@ jobs:
- name: Build Release
run: dotnet build --configuration Release --no-restore -property:NoWarn=0618%3B0612
- - name: Run scrolling benchmarks (ShortRun ≈ 30–60 s)
+ - name: Run benchmarks (ShortRun ≈ 30–60 s)
id: run_benchmarks
run: |
dotnet run \
@@ -97,7 +97,7 @@ jobs:
--configuration Release \
--no-build \
-- \
- --filter '*Scroll*' \
+ --filter '*Scroll*' '*Config*' '*Scheme*' '*Theme*' \
--job short \
--exporters json \
--artifacts ./BenchmarkResults
@@ -172,7 +172,7 @@ jobs:
)
# --- Write step summary ---
- summary = "## 📊 Scrolling Benchmark Comparison\n\n"
+ summary = "## 📊 Benchmark Comparison\n\n"
summary += "| Benchmark | Baseline | Current | Ratio |\n"
summary += "|-----------|----------|---------|-------|\n"
summary += "\n".join(rows) + "\n\n"
diff --git a/Tests/Benchmarks/Configuration/ConfigurationManagerLoadBenchmark.cs b/Tests/Benchmarks/Configuration/ConfigurationManagerLoadBenchmark.cs
new file mode 100644
index 0000000000..1af8f3b722
--- /dev/null
+++ b/Tests/Benchmarks/Configuration/ConfigurationManagerLoadBenchmark.cs
@@ -0,0 +1,45 @@
+using BenchmarkDotNet.Attributes;
+using Terminal.Gui.Configuration;
+
+namespace Terminal.Gui.Benchmarks.Configuration;
+
+///
+/// Measures the cold-start cost of loading the embedded library configuration:
+/// ConfigurationManager.Disable (true) → Enable (ConfigLocations.LibraryResources) → Apply ().
+/// This is the app-startup hot path.
+///
+///
+///
+/// Run:
+/// dotnet run --project Tests/Benchmarks -c Release -- --filter '*ConfigurationManagerLoad*'
+///
+///
+[MemoryDiagnoser]
+[BenchmarkCategory ("Configuration")]
+public class ConfigurationManagerLoadBenchmark
+{
+ /// Resets ConfigurationManager to a clean state before each iteration.
+ [IterationSetup]
+ public void IterationSetup ()
+ {
+ ConfigurationManager.Disable (true);
+ }
+
+ ///
+ /// Loads the embedded library configuration from scratch and applies it.
+ /// Captures the full deserialize + merge + apply path.
+ ///
+ [Benchmark]
+ public void LoadAndApply ()
+ {
+ ConfigurationManager.Enable (ConfigLocations.LibraryResources);
+ ConfigurationManager.Apply ();
+ }
+
+ /// Ensures ConfigurationManager is disabled after all iterations.
+ [GlobalCleanup]
+ public void Cleanup ()
+ {
+ ConfigurationManager.Disable (true);
+ }
+}
diff --git a/Tests/Benchmarks/Configuration/SchemeAttributeBenchmark.cs b/Tests/Benchmarks/Configuration/SchemeAttributeBenchmark.cs
new file mode 100644
index 0000000000..6e97a10e7f
--- /dev/null
+++ b/Tests/Benchmarks/Configuration/SchemeAttributeBenchmark.cs
@@ -0,0 +1,47 @@
+using BenchmarkDotNet.Attributes;
+using Terminal.Gui.Drawing;
+using TgAttribute = Terminal.Gui.Drawing.Attribute;
+
+namespace Terminal.Gui.Benchmarks.Configuration;
+
+///
+/// Measures for roles at different depths of the derivation chain:
+///
+/// - — explicitly set (O(1) lookup)
+/// - — derived from
+/// - — deepest derivation (Code → Editable → Normal)
+///
+/// No required; operates on a standalone
+/// instance.
+///
+///
+///
+/// Run:
+/// dotnet run --project Tests/Benchmarks -c Release -- --filter '*SchemeAttribute*'
+///
+///
+[MemoryDiagnoser]
+[BenchmarkCategory ("Configuration", "Scheme")]
+public class SchemeAttributeBenchmark
+{
+ private Scheme _scheme = null!;
+
+ /// Creates a scheme with only explicitly set.
+ [GlobalSetup]
+ public void Setup ()
+ {
+ _scheme = new Scheme { Normal = new TgAttribute (Color.White, Color.Black) };
+ }
+
+ /// Lookup for an explicitly-set role — the fastest path.
+ [Benchmark (Baseline = true)]
+ public TgAttribute GetNormal () => _scheme.GetAttributeForRole (VisualRole.Normal);
+
+ /// Lookup for a role derived from Focus (which itself is derived from Normal).
+ [Benchmark]
+ public TgAttribute GetHotFocus () => _scheme.GetAttributeForRole (VisualRole.HotFocus);
+
+ /// Lookup for the deepest derivation path: Code → Editable → Normal.
+ [Benchmark]
+ public TgAttribute GetCode () => _scheme.GetAttributeForRole (VisualRole.Code);
+}
diff --git a/Tests/Benchmarks/Configuration/SchemeSerializationBenchmark.cs b/Tests/Benchmarks/Configuration/SchemeSerializationBenchmark.cs
new file mode 100644
index 0000000000..4d1b419c46
--- /dev/null
+++ b/Tests/Benchmarks/Configuration/SchemeSerializationBenchmark.cs
@@ -0,0 +1,63 @@
+using System.Text.Json;
+using BenchmarkDotNet.Attributes;
+using Terminal.Gui.Configuration;
+using Terminal.Gui.Drawing;
+using TgAttribute = Terminal.Gui.Drawing.Attribute;
+
+namespace Terminal.Gui.Benchmarks.Configuration;
+
+///
+/// Measures serialize-then-deserialize of a representative Base via
+/// and . Catches regressions in the JSON
+/// code paths when future PRs add fields to .
+///
+///
+///
+/// Run:
+/// dotnet run --project Tests/Benchmarks -c Release -- --filter '*SchemeSerialization*'
+///
+///
+[MemoryDiagnoser]
+[BenchmarkCategory ("Configuration", "Scheme")]
+public class SchemeSerializationBenchmark
+{
+ private Scheme _scheme = null!;
+ private string _json = null!;
+ private JsonSerializerOptions _options = null!;
+
+ ///
+ /// Creates a representative Base scheme with only explicitly set
+ /// and prepares serialization options with the .
+ ///
+ [GlobalSetup]
+ public void Setup ()
+ {
+ _scheme = new Scheme { Normal = new TgAttribute (Color.White, Color.Black) };
+
+ _options = new JsonSerializerOptions
+ {
+ Converters = { new SchemeJsonConverter () },
+ PropertyNameCaseInsensitive = true
+ };
+
+ // Pre-serialize to have a stable JSON string for deserialization benchmarks.
+ _json = JsonSerializer.Serialize (_scheme, _options);
+ }
+
+ /// Serializes a to JSON.
+ [Benchmark]
+ public string Serialize () => JsonSerializer.Serialize (_scheme, _options);
+
+ /// Deserializes a from JSON.
+ [Benchmark]
+ public Scheme? Deserialize () => JsonSerializer.Deserialize (_json, _options);
+
+ /// Full round-trip: serialize then immediately deserialize.
+ [Benchmark (Baseline = true)]
+ public Scheme? RoundTrip ()
+ {
+ string json = JsonSerializer.Serialize (_scheme, _options);
+
+ return JsonSerializer.Deserialize (json, _options);
+ }
+}
diff --git a/Tests/Benchmarks/Configuration/ThemeSwitchBenchmark.cs b/Tests/Benchmarks/Configuration/ThemeSwitchBenchmark.cs
new file mode 100644
index 0000000000..0affe3bdcb
--- /dev/null
+++ b/Tests/Benchmarks/Configuration/ThemeSwitchBenchmark.cs
@@ -0,0 +1,74 @@
+using BenchmarkDotNet.Attributes;
+using Terminal.Gui.Configuration;
+
+namespace Terminal.Gui.Benchmarks.Configuration;
+
+///
+/// Measures the cost of switching the active theme via
+/// ThemeManager.Theme = "X"; ConfigurationManager.Apply ().
+/// Parametric over every built-in theme name shipped in the embedded config.json.
+///
+///
+///
+/// Run:
+/// dotnet run --project Tests/Benchmarks -c Release -- --filter '*ThemeSwitch*'
+///
+///
+[MemoryDiagnoser]
+[BenchmarkCategory ("Configuration", "Theme")]
+public class ThemeSwitchBenchmark
+{
+ /// The built-in theme to switch to during each benchmark invocation.
+ [ParamsSource (nameof (ThemeNames))]
+ public string ThemeName { get; set; } = ThemeManager.DEFAULT_THEME_NAME;
+
+ /// Returns the set of built-in theme names available after loading library resources.
+ public static IEnumerable ThemeNames
+ {
+ get
+ {
+ ConfigurationManager.Disable (true);
+ ConfigurationManager.Enable (ConfigLocations.LibraryResources);
+
+ IEnumerable names = ThemeManager.GetThemeNames ();
+
+ ConfigurationManager.Disable (true);
+
+ return names;
+ }
+ }
+
+ /// Loads the embedded configuration so all built-in themes are available.
+ [GlobalSetup]
+ public void Setup ()
+ {
+ ConfigurationManager.Disable (true);
+ ConfigurationManager.Enable (ConfigLocations.LibraryResources);
+ }
+
+ /// Resets the theme to Default before each iteration for a stable starting point.
+ [IterationSetup]
+ public void IterationSetup ()
+ {
+ ThemeManager.Theme = ThemeManager.DEFAULT_THEME_NAME;
+ ConfigurationManager.Apply ();
+ }
+
+ ///
+ /// Switches the active theme and applies the change.
+ /// This is the user-facing hot path when cycling themes via a .
+ ///
+ [Benchmark]
+ public void SwitchTheme ()
+ {
+ ThemeManager.Theme = ThemeName;
+ ConfigurationManager.Apply ();
+ }
+
+ /// Ensures ConfigurationManager is disabled after all iterations.
+ [GlobalCleanup]
+ public void Cleanup ()
+ {
+ ConfigurationManager.Disable (true);
+ }
+}
diff --git a/Tests/Benchmarks/README.md b/Tests/Benchmarks/README.md
index f08f70d27f..5ed8111231 100644
--- a/Tests/Benchmarks/README.md
+++ b/Tests/Benchmarks/README.md
@@ -126,6 +126,37 @@ Minimal `View` subclass with a large `ContentSize` and no rendering logic. Isola
dotnet run --project Tests/Benchmarks -c Release -- --filter '*Scroll*'
```
+## Configuration Benchmarks
+
+The `Configuration/` directory contains benchmarks for the configuration, theming, and scheme subsystems.
+
+### ConfigurationManagerLoadBenchmark
+
+Measures the cold-start cost of `ConfigurationManager.Disable(true)` → `Enable(ConfigLocations.LibraryResources)` → `Apply()`. This is the app-startup hot path covering embedded-config load, deserialization, and apply.
+
+### ThemeSwitchBenchmark
+
+Measures `ThemeManager.Theme = "X"; ConfigurationManager.Apply()` against the embedded configuration. Parametric over all built-in theme names (`Default`, `Dark`, `Light`, `TurboPascal 5`, `Anders`, `Green Phosphor`, `Amber Phosphor`).
+
+### SchemeAttributeBenchmark
+
+Measures `Scheme.GetAttributeForRole(VisualRole)` for roles at different depths of the derivation chain:
+- **GetNormal**: Explicitly-set role — the fastest path
+- **GetHotFocus**: Derived from `Focus` (which itself derives from `Normal`)
+- **GetCode**: Deepest derivation path (`Code` → `Editable` → `Normal`)
+
+No `ConfigurationManager` required; operates on a standalone `Scheme` instance.
+
+### SchemeSerializationBenchmark
+
+Measures serialize/deserialize of a representative `Base` `Scheme` via `JsonSerializer` + `SchemeJsonConverter`. Catches regressions in JSON code paths when future PRs add fields to `Scheme`.
+
+### Run all configuration benchmarks
+
+```bash
+dotnet run --project Tests/Benchmarks -c Release -- --filter '*Config*|*Scheme*|*Theme*'
+```
+
## Adding New Benchmarks
1. Create a new class in an appropriate subdirectory (e.g., `Layout/`, `Text/`, `ViewBase/`, `Scrolling/`)
@@ -157,7 +188,7 @@ This detects if a draw function accidentally iterates the entire document instea
The `.github/workflows/perf-gate.yml` workflow runs on every push to `main` / `develop` (not PRs) and:
-1. Runs the `*Scroll*` benchmarks with `--job short` (~30–60 s total)
+1. Runs the `*Scroll*`, `*Config*`, `*Scheme*`, and `*Theme*` benchmarks with `--job short` (~30–60 s total)
2. Compares results to `Tests/Benchmarks/baseline.json`
3. **Fails** if any benchmark exceeds **3×** the baseline
4. **Celebrates** 🎉 if any benchmark drops below **0.8×** the baseline
@@ -169,7 +200,7 @@ After a deliberate performance change, re-run the focused scrolling benchmarks,
```bash
# Run ShortRun and export JSON results
-dotnet run --project Tests/Benchmarks -c Release -- --filter '*Scroll*' -j short --exporters json
+dotnet run --project Tests/Benchmarks -c Release -- --filter '*Scroll*|*Config*|*Scheme*|*Theme*' -j short --exporters json
# Inspect the JSON output in BenchmarkDotNet.Artifacts/ and update baseline.json
```
diff --git a/Tests/Benchmarks/baseline.json b/Tests/Benchmarks/baseline.json
index 63a907f4e5..6374d8c2d2 100644
--- a/Tests/Benchmarks/baseline.json
+++ b/Tests/Benchmarks/baseline.json
@@ -1,6 +1,6 @@
{
- "_comment": "Baseline benchmark results for the scrolling performance gate.",
- "_howto": "Re-run 'dotnet run --project Tests/Benchmarks -c Release -- --filter \"*Scroll*\" -j short --exporters json' then update this file.",
+ "_comment": "Baseline benchmark results for the performance gate (Scrolling + Configuration).",
+ "_howto": "Re-run 'dotnet run --project Tests/Benchmarks -c Release -- --filter \"*Scroll*|*Config*|*Scheme*|*Theme*\" -j short --exporters json' then update this file.",
"_version": "1",
"benchmarks": [
{
@@ -142,6 +142,108 @@
"params": "Rows=1000",
"meanNs": 300000,
"comment": "Placeholder — run actual benchmarks to set real baseline"
+ },
+
+ {
+ "type": "ConfigurationManagerLoadBenchmark",
+ "method": "LoadAndApply",
+ "params": "",
+ "meanNs": 5000000,
+ "comment": "Placeholder — run actual benchmarks to set real baseline"
+ },
+
+ {
+ "type": "ThemeSwitchBenchmark",
+ "method": "SwitchTheme",
+ "params": "ThemeName=Default",
+ "meanNs": 1000000,
+ "comment": "Placeholder — run actual benchmarks to set real baseline"
+ },
+ {
+ "type": "ThemeSwitchBenchmark",
+ "method": "SwitchTheme",
+ "params": "ThemeName=Dark",
+ "meanNs": 1000000,
+ "comment": "Placeholder — run actual benchmarks to set real baseline"
+ },
+ {
+ "type": "ThemeSwitchBenchmark",
+ "method": "SwitchTheme",
+ "params": "ThemeName=Light",
+ "meanNs": 1000000,
+ "comment": "Placeholder — run actual benchmarks to set real baseline"
+ },
+ {
+ "type": "ThemeSwitchBenchmark",
+ "method": "SwitchTheme",
+ "params": "ThemeName=TurboPascal 5",
+ "meanNs": 1000000,
+ "comment": "Placeholder — run actual benchmarks to set real baseline"
+ },
+ {
+ "type": "ThemeSwitchBenchmark",
+ "method": "SwitchTheme",
+ "params": "ThemeName=Anders",
+ "meanNs": 1000000,
+ "comment": "Placeholder — run actual benchmarks to set real baseline"
+ },
+ {
+ "type": "ThemeSwitchBenchmark",
+ "method": "SwitchTheme",
+ "params": "ThemeName=Green Phosphor",
+ "meanNs": 1000000,
+ "comment": "Placeholder — run actual benchmarks to set real baseline"
+ },
+ {
+ "type": "ThemeSwitchBenchmark",
+ "method": "SwitchTheme",
+ "params": "ThemeName=Amber Phosphor",
+ "meanNs": 1000000,
+ "comment": "Placeholder — run actual benchmarks to set real baseline"
+ },
+
+ {
+ "type": "SchemeAttributeBenchmark",
+ "method": "GetNormal",
+ "params": "",
+ "meanNs": 50,
+ "comment": "Placeholder — run actual benchmarks to set real baseline"
+ },
+ {
+ "type": "SchemeAttributeBenchmark",
+ "method": "GetHotFocus",
+ "params": "",
+ "meanNs": 500,
+ "comment": "Placeholder — run actual benchmarks to set real baseline"
+ },
+ {
+ "type": "SchemeAttributeBenchmark",
+ "method": "GetCode",
+ "params": "",
+ "meanNs": 1000,
+ "comment": "Placeholder — run actual benchmarks to set real baseline"
+ },
+
+ {
+ "type": "SchemeSerializationBenchmark",
+ "method": "Serialize",
+ "params": "",
+ "meanNs": 5000,
+ "comment": "Placeholder — run actual benchmarks to set real baseline"
+ },
+ {
+ "type": "SchemeSerializationBenchmark",
+ "method": "Deserialize",
+ "params": "",
+ "meanNs": 5000,
+ "comment": "Placeholder — run actual benchmarks to set real baseline"
+ },
+ {
+ "type": "SchemeSerializationBenchmark",
+ "method": "RoundTrip",
+ "params": "",
+ "meanNs": 10000,
+ "comment": "Placeholder — run actual benchmarks to set real baseline"
}
]
}