Skip to content

Collect and show perf sample totals in widget#22326

Merged
PunkPun merged 3 commits intoOpenRA:bleedfrom
atlimit8:perf-sample-totals
Feb 15, 2026
Merged

Collect and show perf sample totals in widget#22326
PunkPun merged 3 commits intoOpenRA:bleedfrom
atlimit8:perf-sample-totals

Conversation

@atlimit8
Copy link
Member

@atlimit8 atlimit8 commented Feb 1, 2026

Both the rolling and cumulative active (not paused) means are shown.

The cumulative active means (averages) can be used with repeated runs of (multiple) replays to collect performance data and compare. This does not help in the discovery of slow areas, but does allow anyone (who can run the game and collect numbers) to test performance differences (requiring the ability to checkout code and build for checking code changes in the repository).

{
public class PerfGraphWidget : Widget
{
static string InnerFormatFloatWithoutPartial(double value, int digits)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this whole method seems a bit expensive and complicated just to be able to get a float on screen?

Copy link
Member Author

@atlimit8 atlimit8 Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a special 6 character format that excludes the one's place zero if a number < 0. I wanted to show as many useful digits while keeping the columns aligned or mostly aligned and have some room for longer sample names. Sadly, this was my attempt to make it readable. However, this is a reason to have fun writing a performant version as I have. The new version always appends 6 characters to the StringBuilder.

return text.Length <= digits ? text + '.' : text;
}

public static string FormatFloat(double value, int digits = 5)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this will be called at 60+ fps, please consider using caches text values that only update each second. or on a higher level.

var maxExtraDotSpace = 0;
foreach (var item in PerfHistory.Items.Values)
maxExtraDotSpace = Math.Max(maxExtraDotSpace, Game.Renderer.Fonts["Tiny"].Measure(item.Name).X);
var dotWidth = Game.Renderer.Fonts["Tiny"].Measure(".").X;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

measure is also horrible slow.

take it outside the loop at least.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Widths are now cached in a Cache dictionary.

@atlimit8 atlimit8 force-pushed the perf-sample-totals branch 2 times, most recently from 5cf14a5 to 141f238 Compare February 7, 2026 00:30
@atlimit8
Copy link
Member Author

atlimit8 commented Feb 7, 2026

PerfGraphWidget now recycles a StringBuilder to minimize allocations and has a performant custom 6 character format float formatter. The formatting is interwoven to avoid duplicate code that would multiply the size of the format method and possibly place more pressure on the cache.

Replace the PerfGraphWidget constructor in OpenRA.Mods.Common/Widgets/PerfGraphWidget.cs with the following and wait for the shell map in order to test:

		public PerfGraphWidget()
		{
			textWidthCache = new(GetTextWidth);
			dotWidth = textWidthCache["."];
			nextDotAdvance = textWidthCache[".."] - dotWidth;
			TestFormatFloat(double.NaN, "NaN   ");
			TestFormatFloat(-0.000001, "<0    ");
			TestFormatFloat(0.000001, ".00000");
			TestFormatFloat(0.000004, ".00000");
			TestFormatFloat(0.000005, ".00001");
			TestFormatFloat(0.000009, ".00001");
			TestFormatFloat(0.00001, ".00001");
			TestFormatFloat(0.00004, ".00004");
			TestFormatFloat(0.00005, ".00005");
			TestFormatFloat(0.00009, ".00009");
			TestFormatFloat(0.0001, ".00010");
			TestFormatFloat(0.0004, ".00040");
			TestFormatFloat(0.0005, ".00050");
			TestFormatFloat(0.0009, ".00090");
			TestFormatFloat(0.001, ".00100");
			TestFormatFloat(0.005, ".00500");
			TestFormatFloat(0.01, ".01000");
			TestFormatFloat(0.05, ".05000");
			TestFormatFloat(0.1, ".10000");
			TestFormatFloat(0.5, ".50000");
			TestFormatFloat(1, "1.0000");
			TestFormatFloat(9, "9.0000");
			TestFormatFloat(10, "10.000");
			TestFormatFloat(99, "99.000");
			TestFormatFloat(100, "100.00");
			TestFormatFloat(123.4321, "123.43");
			TestFormatFloat(1000, "1000.0");
			TestFormatFloat(10000, "10000.");
			TestFormatFloat(100000, "100000");
			TestFormatFloat(123432, "123432");
			TestFormatFloat(1000000, "TooBig");
			TestFormatFloat(1234321, "TooBig");
			TestFormatFloat(10000000, "TooBig");
			TestFormatFloat(100000000, "TooBig");
		}

		void TestFormatFloat(double value, string expected)
		{
			var builder = new StringBuilder();
			SixCharacterFormatFloat(builder, value);
			Console.WriteLine($"{(builder.Length == 6 && builder.ToString() == expected ? "PASS" : "FAIL")} {expected} : {builder} <= {value}");
		}

@atlimit8 atlimit8 force-pushed the perf-sample-totals branch 2 times, most recently from cd12e0f to 6ea2315 Compare February 7, 2026 02:51
@atlimit8
Copy link
Member Author

atlimit8 commented Feb 7, 2026

I made the formatter shorter with loops to reduce size, added comments and gave it a less confusing name.

Both the rolling and cumulative active (not paused) means are shown.
Copy link
Member

@PunkPun PunkPun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems a bit crazy to optimise rendering for just like 8 words that are only going to be rendered once a frame. Though if we are optimising, then there's still so many overheads left.

Could you also push the unit tests?

}

public void Tick()
public void Tick(bool gameIsActive = true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this makes sense? The game can be paused but the render loop will still be running.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that when I made this and there is still the rolling average. I figured that adding this so that the second column would consistently be the cumulative active means (averages).

@atlimit8
Copy link
Member Author

atlimit8 commented Feb 7, 2026

I have added the unit tests, handled a rounding issue and added handling of infinities.

Copy link
Member

@PunkPun PunkPun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

@PunkPun PunkPun merged commit 096ad0c into OpenRA:bleed Feb 15, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants