using System; using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; using Age.Engine.Model; /// /// Buffered, diagnostic-only CSV writer for real Godot frame and retained-compositor cost. The writer is /// deliberately independent of Godot types so its schema and clipping arithmetic can be unit tested. /// public sealed class PerformanceFrameLog : IDisposable { private const int FlushIntervalFrames = 120; private static readonly double MillisecondsPerTick = 1000.0 / Stopwatch.Frequency; private readonly StreamWriter _writer; private Frame _current = new(); private bool _frameOpen; private bool _disposed; private int _framesSinceFlush; public long FrameCount { get; private set; } public long RecompositeCount { get; private set; } public string Path { get; } public PerformanceFrameLog(string path) { Path = path; var directory = System.IO.Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(directory)) Directory.CreateDirectory(directory); _writer = new StreamWriter(path, append: false, Encoding.UTF8, 64 * 1024); // Presentation coordinates are appended separately so a VM thread released by PulseFrame can be // distinguished from the coordinate observed at _Process entry. _writer.WriteLine( "frame,now_ms,delta_ms,main_ms,pulse_ms,movie_ms,should_recomposite_ms,recomposite_ms," + "clear_ms,snapshot_ms,resolve_ms,source_prep_ms,raster_ms,set_data_ms,texture_update_ms,ui_ms," + "allocated_bytes,recompose_allocated_bytes,snapshot_allocated_bytes," + "composite_allocated_bytes,source_prep_allocated_bytes,set_data_allocated_bytes," + "ui_allocated_bytes,gen0,gen1,gen2,recomposited,render_backend,screen_transition," + "gpu_draw_items,gpu_texture_uploads,gpu_texture_upload_ms," + "present_host_request,present_screen_transition,present_retained_mutation," + "present_continuous_channel,present_discrete_cell,object_visits," + "time_varying_objects,draw_layers,fill_layers,transition_layers,skipped_layers," + "integer_layers,fractional_translation_layers,axis_aligned_scale_layers," + "general_affine_layers,affine_layers,singular_layers,dynamic_layers,opaque_layers,alpha_layers," + "additive_layers,source_pixels,candidate_pixels,full_screen_layers,script,offset,opcode," + "present_script,present_offset,present_opcode"); } public static long Timestamp() => Stopwatch.GetTimestamp(); public static long AllocatedBytes() => GC.GetAllocatedBytesForCurrentThread(); public void BeginFrame(int frame, long nowMs, double deltaSeconds, string script, int offset, int opcode) { if (_disposed) return; if (_frameOpen) EndFrame(); _current = new Frame { Number = frame, NowMs = nowMs, DeltaMs = deltaSeconds * 1000.0, Script = script, Offset = offset, Opcode = opcode, Started = Timestamp(), AllocatedStart = GC.GetAllocatedBytesForCurrentThread(), Gen0Start = GC.CollectionCount(0), Gen1Start = GC.CollectionCount(1), Gen2Start = GC.CollectionCount(2), }; _frameOpen = true; } public void RecordPulse(long ticks) => _current.PulseTicks += ticks; public void RecordMovies(long ticks) => _current.MovieTicks += ticks; public void RecordShouldRecomposite(long ticks) => _current.ShouldTicks += ticks; public void RecordPresentationReasons(int reasons) { _current.PresentHostRequest |= (reasons & 1) != 0; _current.PresentScreenTransition |= (reasons & 2) != 0; _current.PresentRetainedMutation |= (reasons & 4) != 0; _current.PresentContinuousChannel |= (reasons & 8) != 0; _current.PresentDiscreteCell |= (reasons & 16) != 0; } public void RecordUi(long ticks) => _current.UiTicks += ticks; public void RecordClear(long ticks) => _current.ClearTicks += ticks; public void RecordSnapshot(long ticks) => _current.SnapshotTicks += ticks; public void RecordResolve(long ticks) => _current.ResolveTicks += ticks; public void RecordSourcePrep(long ticks) => _current.SourcePrepTicks += ticks; public void RecordRecomposeAllocation(long bytes) => _current.RecomposeAllocatedBytes += Math.Max(0, bytes); public void RecordSnapshotAllocation(long bytes) => _current.SnapshotAllocatedBytes += Math.Max(0, bytes); public void RecordCompositeAllocation(long bytes) => _current.CompositeAllocatedBytes += Math.Max(0, bytes); public void RecordSourcePrepAllocation(long bytes) => _current.SourcePrepAllocatedBytes += Math.Max(0, bytes); public void RecordSetDataAllocation(long bytes) => _current.SetDataAllocatedBytes += Math.Max(0, bytes); public void RecordUiAllocation(long bytes) => _current.UiAllocatedBytes += Math.Max(0, bytes); public void RecordSetData(long ticks) => _current.SetDataTicks += ticks; public void RecordTextureUpdate(long ticks) => _current.TextureUpdateTicks += ticks; public void RecordGpu(int drawItems, int textureUploads, long textureUploadTicks) { _current.GpuBackend = true; _current.GpuDrawItems += drawItems; _current.GpuTextureUploads += textureUploads; _current.GpuTextureUploadTicks += textureUploadTicks; } public void BeginRecomposite(bool screenTransition) { _current.Recomposited = true; _current.ScreenTransition |= screenTransition; _current.RecompositeStarted = Timestamp(); } public void RecordPresentationCoordinate(string script, int offset, int opcode) { _current.PresentScript = script; _current.PresentOffset = offset; _current.PresentOpcode = opcode; } public void EndRecomposite() { if (_current.RecompositeStarted == 0) return; _current.RecompositeTicks += Timestamp() - _current.RecompositeStarted; _current.RecompositeStarted = 0; } public void RecordObject(bool timeVarying) { _current.ObjectVisits++; if (timeVarying) _current.TimeVaryingObjects++; } public void RecordFillLayer() => _current.FillLayers++; public void RecordTransitionLayer() => _current.TransitionLayers++; public void RecordSkippedLayer() => _current.SkippedLayers++; public void RecordRaster(int sourceWidth, int sourceHeight, Affine2D localToDest, int destinationWidth, int destinationHeight, bool dynamic, BlendKind blend, long ticks) { _current.RasterTicks += ticks; RecordLayer(sourceWidth, sourceHeight, localToDest, destinationWidth, destinationHeight, dynamic, blend); } public void RecordGpuLayer(int sourceWidth, int sourceHeight, Affine2D localToDest, int destinationWidth, int destinationHeight, bool dynamic, BlendKind blend) => RecordLayer(sourceWidth, sourceHeight, localToDest, destinationWidth, destinationHeight, dynamic, blend); private void RecordLayer(int sourceWidth, int sourceHeight, Affine2D localToDest, int destinationWidth, int destinationHeight, bool dynamic, BlendKind blend) { _current.DrawLayers++; _current.SourcePixels += Math.Max(0L, (long)sourceWidth * sourceHeight); long candidates = EstimateCandidatePixels(localToDest, sourceWidth, sourceHeight, destinationWidth, destinationHeight); _current.CandidatePixels += candidates; if (candidates >= (long)destinationWidth * destinationHeight) _current.FullScreenLayers++; if (IsIntegerTranslation(localToDest)) _current.IntegerLayers++; else if (!localToDest.TryInverse(out _)) _current.SingularLayers++; else { _current.AffineLayers++; if (IsTranslation(localToDest)) _current.FractionalTranslationLayers++; else if (IsAxisAligned(localToDest)) _current.AxisAlignedScaleLayers++; else _current.GeneralAffineLayers++; } if (dynamic) _current.DynamicLayers++; switch (blend) { case BlendKind.Opaque: _current.OpaqueLayers++; break; case BlendKind.Additive: _current.AdditiveLayers++; break; default: _current.AlphaLayers++; break; } } public void EndFrame() { if (!_frameOpen || _disposed) return; long ended = Timestamp(); _current.MainTicks = ended - _current.Started; _current.AllocatedBytes = Math.Max(0, GC.GetAllocatedBytesForCurrentThread() - _current.AllocatedStart); _current.Gen0 = GC.CollectionCount(0) - _current.Gen0Start; _current.Gen1 = GC.CollectionCount(1) - _current.Gen1Start; _current.Gen2 = GC.CollectionCount(2) - _current.Gen2Start; Write(_current); FrameCount++; if (_current.Recomposited) RecompositeCount++; _frameOpen = false; if (++_framesSinceFlush >= FlushIntervalFrames) { _writer.Flush(); _framesSinceFlush = 0; } } public static bool IsIntegerTranslation(Affine2D m) => IsTranslation(m) && m.TX == Math.Truncate(m.TX) && m.TY == Math.Truncate(m.TY) && m.TX >= int.MinValue && m.TX <= int.MaxValue && m.TY >= int.MinValue && m.TY <= int.MaxValue; public static bool IsTranslation(Affine2D m) => m.XX == 1 && m.XY == 0 && m.YX == 0 && m.YY == 1; public static bool IsAxisAligned(Affine2D m) => m.XY == 0 && m.YX == 0; public static long EstimateCandidatePixels(Affine2D m, int width, int height, int destinationWidth, int destinationHeight) { if (width <= 0 || height <= 0 || destinationWidth <= 0 || destinationHeight <= 0) return 0; var a = m.Apply(0, 0); var b = m.Apply(width, 0); var c = m.Apply(0, height); var d = m.Apply(width, height); double left = Math.Min(Math.Min(a.X, b.X), Math.Min(c.X, d.X)); double top = Math.Min(Math.Min(a.Y, b.Y), Math.Min(c.Y, d.Y)); double right = Math.Max(Math.Max(a.X, b.X), Math.Max(c.X, d.X)); double bottom = Math.Max(Math.Max(a.Y, b.Y), Math.Max(c.Y, d.Y)); long x0 = Math.Max(0, ClampFloor(left)); long y0 = Math.Max(0, ClampFloor(top)); long x1 = Math.Min(destinationWidth, ClampCeiling(right)); long y1 = Math.Min(destinationHeight, ClampCeiling(bottom)); return x1 <= x0 || y1 <= y0 ? 0 : checked((x1 - x0) * (y1 - y0)); } private static long ClampFloor(double value) => !double.IsFinite(value) ? 0 : value <= long.MinValue ? long.MinValue : value >= long.MaxValue ? long.MaxValue : (long)Math.Floor(value); private static long ClampCeiling(double value) => !double.IsFinite(value) ? 0 : value <= long.MinValue ? long.MinValue : value >= long.MaxValue ? long.MaxValue : (long)Math.Ceiling(value); private void Write(Frame f) { var b = new StringBuilder(512); Append(b, f.Number); Append(b, f.NowMs); Append(b, f.DeltaMs); AppendTicks(b, f.MainTicks); AppendTicks(b, f.PulseTicks); AppendTicks(b, f.MovieTicks); AppendTicks(b, f.ShouldTicks); AppendTicks(b, f.RecompositeTicks); AppendTicks(b, f.ClearTicks); AppendTicks(b, f.SnapshotTicks); AppendTicks(b, f.ResolveTicks); AppendTicks(b, f.SourcePrepTicks); AppendTicks(b, f.RasterTicks); AppendTicks(b, f.SetDataTicks); AppendTicks(b, f.TextureUpdateTicks); AppendTicks(b, f.UiTicks); Append(b, f.AllocatedBytes); Append(b, f.RecomposeAllocatedBytes); Append(b, f.SnapshotAllocatedBytes); Append(b, f.CompositeAllocatedBytes); Append(b, f.SourcePrepAllocatedBytes); Append(b, f.SetDataAllocatedBytes); Append(b, f.UiAllocatedBytes); Append(b, f.Gen0); Append(b, f.Gen1); Append(b, f.Gen2); Append(b, f.Recomposited ? 1 : 0); Append(b, f.GpuBackend ? 1 : 0); Append(b, f.ScreenTransition ? 1 : 0); Append(b, f.GpuDrawItems); Append(b, f.GpuTextureUploads); AppendTicks(b, f.GpuTextureUploadTicks); Append(b, f.PresentHostRequest ? 1 : 0); Append(b, f.PresentScreenTransition ? 1 : 0); Append(b, f.PresentRetainedMutation ? 1 : 0); Append(b, f.PresentContinuousChannel ? 1 : 0); Append(b, f.PresentDiscreteCell ? 1 : 0); Append(b, f.ObjectVisits); Append(b, f.TimeVaryingObjects); Append(b, f.DrawLayers); Append(b, f.FillLayers); Append(b, f.TransitionLayers); Append(b, f.SkippedLayers); Append(b, f.IntegerLayers); Append(b, f.FractionalTranslationLayers); Append(b, f.AxisAlignedScaleLayers); Append(b, f.GeneralAffineLayers); Append(b, f.AffineLayers); Append(b, f.SingularLayers); Append(b, f.DynamicLayers); Append(b, f.OpaqueLayers); Append(b, f.AlphaLayers); Append(b, f.AdditiveLayers); Append(b, f.SourcePixels); Append(b, f.CandidatePixels); Append(b, f.FullScreenLayers); AppendEscaped(b, f.Script); Append(b, f.Offset); Append(b, f.Opcode); AppendEscaped(b, f.PresentScript); Append(b, f.PresentOffset); Append(b, f.PresentOpcode, last: true); _writer.WriteLine(b.ToString()); } private static void AppendTicks(StringBuilder b, long ticks) => Append(b, ticks * MillisecondsPerTick); private static void Append(StringBuilder b, long value, bool last = false) { b.Append(value.ToString(CultureInfo.InvariantCulture)); if (!last) b.Append(','); } private static void Append(StringBuilder b, double value) { b.Append(value.ToString("0.0000", CultureInfo.InvariantCulture)); b.Append(','); } private static void AppendEscaped(StringBuilder b, string value) { b.Append('"').Append(value.Replace("\"", "\"\"")).Append("\","); } public void Dispose() { if (_disposed) return; if (_frameOpen) EndFrame(); _disposed = true; _writer.Dispose(); } private sealed class Frame { public int Number, Offset, Opcode; public long NowMs, Started, MainTicks, PulseTicks, MovieTicks, ShouldTicks, RecompositeTicks; public long RecompositeStarted, ClearTicks, SnapshotTicks, ResolveTicks, SourcePrepTicks, RasterTicks; public long SetDataTicks, TextureUpdateTicks, UiTicks, AllocatedStart, AllocatedBytes; public long RecomposeAllocatedBytes, SnapshotAllocatedBytes, CompositeAllocatedBytes; public long SourcePrepAllocatedBytes, SetDataAllocatedBytes, UiAllocatedBytes; public int Gen0Start, Gen1Start, Gen2Start, Gen0, Gen1, Gen2; public double DeltaMs; public string Script = ""; public string PresentScript = ""; public int PresentOffset = -1, PresentOpcode = -1; public bool Recomposited, GpuBackend, ScreenTransition; public bool PresentHostRequest, PresentScreenTransition, PresentRetainedMutation; public bool PresentContinuousChannel, PresentDiscreteCell; public long ObjectVisits, TimeVaryingObjects, DrawLayers, FillLayers, TransitionLayers, SkippedLayers; public long IntegerLayers, FractionalTranslationLayers, AxisAlignedScaleLayers; public long GeneralAffineLayers, AffineLayers, SingularLayers, DynamicLayers; public long OpaqueLayers, AlphaLayers, AdditiveLayers, SourcePixels, CandidatePixels, FullScreenLayers; public long GpuDrawItems, GpuTextureUploads, GpuTextureUploadTicks; } }