Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
112 changes: 14 additions & 98 deletions Explorer/Assets/DCL/PerformanceAndDiagnostics/AutoPilot/AutoPilot.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
using Cysharp.Threading.Tasks;
using DCL.Profiling;
using DCL.RealmNavigation;
using Global.AppArgs;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using UnityEngine;
using Profiler = UnityEngine.Profiling.Profiler;

Expand All @@ -17,37 +11,27 @@ public sealed class AutoPilot
{
private readonly IAppArgs appArgs;
private readonly ILoadingStatus loadingStatus;
private readonly IProfiler profiler;
private StreamWriter csv;

public AutoPilot(IAppArgs appArgs, ILoadingStatus loadingStatus, IProfiler profiler)
public AutoPilot(IAppArgs appArgs, ILoadingStatus loadingStatus)
{
this.appArgs = appArgs;
this.loadingStatus = loadingStatus;
this.profiler = profiler;
}

public async UniTask RunAsync()
{
var exitCode = 0;
var sessionStarted = false;

try
{
if (appArgs.TryGetValue(AppArgsFlags.AUTOPILOT_CSV,
out string csvFile))
{
if (csvFile == null)
throw new Exception($"{nameof(csvFile)} is null");
if (!appArgs.TryGetValue(AppArgsFlags.AUTOPILOT_CSV, out string csvFile))
return;

csv = new StreamWriter(csvFile, false, new UTF8Encoding(false));
csv.NewLine = "\r\n"; // https://www.rfc-editor.org/rfc/rfc4180
await csv.WriteLineAsync("\"Frame\",\"CPU Time\",\"GPU Time\"");
}
if (csvFile == null)
throw new Exception($"{nameof(csvFile)} is null");

if (appArgs.TryGetValue(AppArgsFlags.AUTOPILOT_SUMMARY, out string summaryFile)
&& csv == null)
throw new Exception(
$"--{AppArgsFlags.AUTOPILOT_SUMMARY} requires --{AppArgsFlags.AUTOPILOT_CSV}");
appArgs.TryGetValue(AppArgsFlags.AUTOPILOT_SUMMARY, out string summaryFile);

while (loadingStatus.CurrentStage.Value != LoadingStatus.LoadingStage.Completed)
await UniTask.Yield();
Expand All @@ -66,28 +50,21 @@ public async UniTask RunAsync()
#endif
}

PerfSampler.Begin(csvFile, summaryFile);
sessionStarted = true;
await StandAtSpawnAsync();

if (summaryFile != null)
{
await csv.DisposeAsync();
csv = null;
await WriteSummaryAsync(csvFile, summaryFile);
}
PerfSampler.End();
sessionStarted = false;
}
catch (Exception ex)
{
if (csv != null)
await csv.WriteLineAsync(
$"\"Error: {ex.Message.Replace("\"", "\"\"")}\"");

exitCode = ex.HResult;
throw;
}
finally
{
if (csv != null)
await csv.DisposeAsync();
if (sessionStarted)
PerfSampler.End();

Application.Quit(exitCode);
}
Expand All @@ -96,73 +73,12 @@ await csv.WriteLineAsync(
/// <summary>
/// The minimal performance test: stand at spawn for one minute.
/// </summary>
private async UniTask StandAtSpawnAsync()
private static async UniTask StandAtSpawnAsync()
{
float startTime = Time.realtimeSinceStartup;

while (Time.realtimeSinceStartup - startTime < 90f)
{
await WriteSampleAsync();
await UniTask.Yield();
}
}

private UniTask WriteSampleAsync() =>
csv != null
? csv.WriteLineAsync(string.Format(
CultureInfo.InvariantCulture, "{0},{1},{2}",
Time.frameCount,
profiler.LastFrameTimeValueNs * 0.000001f,
profiler.LastGpuFrameTimeValueNs * 0.000001f)).AsUniTask()
: UniTask.CompletedTask;

private static async UniTask WriteSummaryAsync(string csvFile,
string summaryFile)
{
var cpuTimes = new List<float>();
var gpuTimes = new List<float>();

using (var csv = new StreamReader(csvFile))
{
await csv.ReadLineAsync(); // Discard the header line

while (!csv.EndOfStream)
{
string line = await csv.ReadLineAsync();
string[] columns = line.Split(',');
cpuTimes.Add(float.Parse(columns[1], CultureInfo.InvariantCulture));
gpuTimes.Add(float.Parse(columns[2], CultureInfo.InvariantCulture));
}
}

await using (var summary = new StreamWriter(summaryFile))
{
await summary.WriteAsync("CPU average: ");
await summary.WriteLineAsync(cpuTimes.Average().ToString(CultureInfo.InvariantCulture));
await summary.WriteAsync("CPU 1% worst: ");
await summary.WriteLineAsync(PercentWorst(cpuTimes, 0.01f).ToString(CultureInfo.InvariantCulture));
await summary.WriteAsync("CPU 0.1% worst: ");
await summary.WriteLineAsync(PercentWorst(cpuTimes, 0.001f).ToString(CultureInfo.InvariantCulture));
await summary.WriteAsync("CPU worst: ");
await summary.WriteLineAsync(cpuTimes.Max().ToString(CultureInfo.InvariantCulture));
await summary.WriteAsync("GPU average: ");
await summary.WriteLineAsync(gpuTimes.Average().ToString(CultureInfo.InvariantCulture));
await summary.WriteAsync("GPU 1% worst: ");
await summary.WriteLineAsync(PercentWorst(gpuTimes, 0.01f).ToString(CultureInfo.InvariantCulture));
await summary.WriteAsync("GPU 0.1% worst: ");
await summary.WriteLineAsync(PercentWorst(gpuTimes, 0.001f).ToString(CultureInfo.InvariantCulture));
await summary.WriteAsync("GPU worst: ");
await summary.WriteLineAsync(gpuTimes.Max().ToString(CultureInfo.InvariantCulture));
}
}

/// <remarks>
/// As done by GamersNexus:
/// https://www.youtube.com/watch?v=WcTxrzFqdyw#t=34m17s
/// </remarks>
private static float PercentWorst(List<float> times, float fraction) =>
times.OrderByDescending(i => i)
.Take((int)(times.Count * fraction))
.Average();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
using Cysharp.Threading.Tasks;
using DCL.Profiling;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using UnityEngine;

namespace DCL.PerformanceAndDiagnostics.AutoPilot
{
/// <summary>
/// Per-frame CPU/GPU time sampler that mirrors the original AutoPilot CSV +
/// summary writer, but is reusable from non-AutoPilot driving code (e.g.
/// AltTester-driven scenarios). The sampler is a plain static class so that
/// it can be configured once from the plugin system and then driven by
/// multiple, sequential fixtures within a single long-lived Player session.
/// </summary>
public static class PerfSampler
{
private static IProfiler profiler;
private static StreamWriter csv;
private static string currentCsvPath;
private static string currentSummaryPath;
private static bool sampling;
private static int sampleLoopToken;

/// <summary>
/// Wires the profiler used to read per-frame CPU and GPU times.
/// Must be called exactly once during plugin bootstrap, BEFORE any
/// call to <see cref="Begin"/>.
/// </summary>
public static void Configure(IProfiler profiler)
{
PerfSampler.profiler = profiler;
}

/// <summary>
/// Opens a new sampling session: writes the CSV header, remembers the
/// summary path, and kicks off the per-frame sampling loop. If a session
/// is already open this is logged and the call becomes a no-op (the
/// existing session keeps running). Throws if <see cref="Configure"/>
/// was never called.
/// </summary>
public static void Begin(string csvPath, string summaryPath)
{
if (profiler == null)
throw new InvalidOperationException(
$"{nameof(PerfSampler)}.{nameof(Configure)} must be called before {nameof(Begin)}.");

if (sampling)
{
DCL.Diagnostics.ReportHub.LogWarning(
DCL.Diagnostics.ReportCategory.ALWAYS,
$"{nameof(PerfSampler)}.{nameof(Begin)} called while a sampling session is already active. Ignoring.");
Comment thread
popuz marked this conversation as resolved.
return;
}

if (string.IsNullOrEmpty(csvPath))
throw new ArgumentException("CSV path is required", nameof(csvPath));

csv = new StreamWriter(csvPath, false, new UTF8Encoding(false));
csv.NewLine = "\r\n"; // https://www.rfc-editor.org/rfc/rfc4180
csv.WriteLine("\"Frame\",\"CPU Time\",\"GPU Time\"");

currentCsvPath = csvPath;
currentSummaryPath = summaryPath;
sampling = true;

int token = ++sampleLoopToken;
SampleLoopAsync(token).Forget();
}

/// <summary>
/// Closes the current sampling session: stops the sample loop, flushes
/// and closes the CSV, then (if a summary path was given) writes the
/// 8-line summary file. No-op if there is no open session.
/// </summary>
public static void End()
{
if (!sampling)
return;

sampling = false;

try
{
csv?.Dispose();
}
finally
{
csv = null;
}

if (!string.IsNullOrEmpty(currentSummaryPath))
WriteSummary(currentCsvPath, currentSummaryPath);

currentCsvPath = null;
currentSummaryPath = null;
}

private static async UniTaskVoid SampleLoopAsync(int token)
{
try
{
while (sampling && token == sampleLoopToken)
{
WriteSample();
await UniTask.Yield();
}
}
catch (Exception e)
{
DCL.Diagnostics.ReportHub.LogException(e, DCL.Diagnostics.ReportCategory.ALWAYS);
sampling = false;
}
Comment thread
popuz marked this conversation as resolved.
}

private static void WriteSample()
{
if (csv == null)
return;

csv.WriteLine(string.Format(
CultureInfo.InvariantCulture, "{0},{1},{2}",
Time.frameCount,
profiler.LastFrameTimeValueNs * 0.000001f,
profiler.LastGpuFrameTimeValueNs * 0.000001f));
}

private static void WriteSummary(string csvFile, string summaryFile)
{
var cpuTimes = new List<float>();
var gpuTimes = new List<float>();

using (var reader = new StreamReader(csvFile))
{
reader.ReadLine(); // Discard the header line

while (!reader.EndOfStream)
{
string line = reader.ReadLine();
if (line == null) break;
string[] columns = line.Split(',');
cpuTimes.Add(float.Parse(columns[1], CultureInfo.InvariantCulture));
gpuTimes.Add(float.Parse(columns[2], CultureInfo.InvariantCulture));
}
}

using (var summary = new StreamWriter(summaryFile))
{
summary.Write("CPU average: ");
summary.WriteLine(cpuTimes.Average().ToString(CultureInfo.InvariantCulture));
summary.Write("CPU 1% worst: ");
summary.WriteLine(PercentWorst(cpuTimes, 0.01f).ToString(CultureInfo.InvariantCulture));
summary.Write("CPU 0.1% worst: ");
summary.WriteLine(PercentWorst(cpuTimes, 0.001f).ToString(CultureInfo.InvariantCulture));
summary.Write("CPU worst: ");
summary.WriteLine(cpuTimes.Max().ToString(CultureInfo.InvariantCulture));
summary.Write("GPU average: ");
summary.WriteLine(gpuTimes.Average().ToString(CultureInfo.InvariantCulture));
summary.Write("GPU 1% worst: ");
summary.WriteLine(PercentWorst(gpuTimes, 0.01f).ToString(CultureInfo.InvariantCulture));
summary.Write("GPU 0.1% worst: ");
summary.WriteLine(PercentWorst(gpuTimes, 0.001f).ToString(CultureInfo.InvariantCulture));
summary.Write("GPU worst: ");
summary.WriteLine(gpuTimes.Max().ToString(CultureInfo.InvariantCulture));
}
}

/// <remarks>
/// As done by GamersNexus:
/// https://www.youtube.com/watch?v=WcTxrzFqdyw#t=34m17s
/// </remarks>
private static float PercentWorst(List<float> times, float fraction)
{
// Short sampling windows can yield < 1/fraction samples (e.g. an
// InWorld fixture that runs ~15s at ~30 FPS produces ~450 samples,
// so (int)(450 * 0.001f) is 0 and Take(0).Average() throws). Floor
// the count to 1 so 0.1% worst on small windows just reports the
// single worst frame instead of crashing summary generation.
if (times.Count == 0) return 0f;
var k = Math.Max(1, (int)(times.Count * fraction));
return times.OrderByDescending(i => i).Take(k).Average();
Comment thread
popuz marked this conversation as resolved.
Comment thread
popuz marked this conversation as resolved.
Comment thread
popuz marked this conversation as resolved.
Comment thread
popuz marked this conversation as resolved.
Comment thread
popuz marked this conversation as resolved.
Comment thread
popuz marked this conversation as resolved.
Comment thread
popuz marked this conversation as resolved.
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@ public void InjectToWorld(ref ArchSystemsWorldBuilder<Arch.Core.World> builder,

DebugViewCurrentSceneSystem.InjectToWorld(ref builder, debugContainerBuilder, scenesCache, realmData);

PerfSampler.Configure(profiler);

if (appArgs.HasFlag(AppArgsFlags.AUTOPILOT))
{
var autoPilot = new AutoPilot(appArgs, loadingStatus, profiler);
var autoPilot = new AutoPilot(appArgs, loadingStatus);
autoPilot.RunAsync().Forget();
}
}
Expand Down
Loading