Editor/Output/ArchBuildProfile.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Sunless.Architecture;

// Where one rebuild's time actually went. Ambient the way ArchBuildMemo is, because the phases worth naming are
// spread across the scene reconcile, the builder and the tool, and none of them could be handed a profile without
// threading one through every generator standing between them.
public sealed class ArchBuildProfile : IDisposable {
	public static ArchBuildProfile Current { get; private set; }

	readonly ArchBuildProfile held;
	readonly long opened = Stopwatch.GetTimestamp();
	readonly Dictionary<string, double> phases = new();
	readonly List<string> order = new();
	readonly Dictionary<string, double> roles = new();
	readonly Dictionary<string, int> counts = new();

	ArchBuildProfile() {
		held = Current;
		Current = this;
	}

	public static ArchBuildProfile Begin() => new();

	public void Dispose() {
		Total = Since( opened );
		Current = held;
	}

	public double Total { get; private set; }

	public IEnumerable<KeyValuePair<string, double>> Phases => order.Select( name => new KeyValuePair<string, double>( name, phases[name] ) );

	// The generators, heaviest first - which one to attack is the whole question the generate phase raises and
	// cannot answer.
	public IEnumerable<KeyValuePair<string, double>> Roles => roles.OrderByDescending( entry => entry.Value );

	public int Drawn( string role ) => counts.TryGetValue( role, out var held ) ? held : 0;

	// Nothing open means nothing measured, so every call site reads the same whether anybody is profiling or not -
	// `using` over a null disposable is a no-op.
	public static IDisposable Stage( string name ) {
		return Current is { } profile ? new Phase( profile, name ) : null;
	}

	public static long Now => Current is null ? 0L : Stopwatch.GetTimestamp();

	public static void Ran( string role, long from ) {
		if ( Current is not { } profile || from == 0L ) {
			return;
		}

		profile.roles[role] = profile.roles.TryGetValue( role, out var held ) ? held + Since( from ) : Since( from );
		profile.counts[role] = profile.Drawn( role ) + 1;
	}

	static double Since( long from ) => (Stopwatch.GetTimestamp() - from) * 1000d / Stopwatch.Frequency;

	void Took( string name, long from ) {
		if ( !phases.ContainsKey( name ) ) {
			order.Add( name );
			phases[name] = 0d;
		}

		phases[name] += Since( from );
	}

	readonly struct Phase : IDisposable {
		readonly ArchBuildProfile profile;
		readonly string name;
		readonly long from;

		public Phase( ArchBuildProfile profile, string name ) {
			this.profile = profile;
			this.name = name;
			from = Stopwatch.GetTimestamp();
		}

		public void Dispose() => profile.Took( name, from );
	}
}