| | 1 | | using System; |
| | 2 | | using System.Collections.Generic; |
| | 3 | | using System.Linq; |
| | 4 | | using UnityEngine; |
| | 5 | | using DCL.FPSDisplay; |
| | 6 | | using DCL.SettingsCommon; |
| | 7 | | using Newtonsoft.Json; |
| | 8 | | using Unity.Profiling; |
| | 9 | |
|
| | 10 | | namespace DCL |
| | 11 | | { |
| | 12 | | /// <summary> |
| | 13 | | /// Performance Meter Tool |
| | 14 | | /// |
| | 15 | | /// It samples frames performance data for the target duration and prints a complete report when finished. |
| | 16 | | /// |
| | 17 | | /// There are 2 ways to trigger this tool usage: |
| | 18 | | /// A: While the client is running in the browser, open the browser console and run "clientDebug.RunPerformanceMeter |
| | 19 | | /// B: In Unity Editor select the "Main" gameobject and right-click on its DebugBridge Monobehaviour, from there a d |
| | 20 | | /// </summary> |
| | 21 | | public class PerformanceMeterController |
| | 22 | | { |
| | 23 | | private class SampleData : IComparable |
| | 24 | | { |
| | 25 | | public int frameNumber; |
| | 26 | | public float millisecondsConsumed; |
| | 27 | | public bool isHiccup = false; |
| | 28 | | public float currentTime; |
| | 29 | | public float frameTimeMs; |
| | 30 | |
|
| | 31 | | public override string ToString() |
| | 32 | | { |
| 0 | 33 | | return "frame number: " + frameNumber |
| | 34 | | + "\n frame consumed milliseconds: " + millisecondsConsumed |
| | 35 | | + "\n is hiccup: " + isHiccup |
| | 36 | | + "\n frame time: " + frameTimeMs; |
| | 37 | | } |
| | 38 | |
|
| | 39 | | public int CompareTo(object obj) |
| | 40 | | { |
| | 41 | | // 0 -> this and otherSample are equal |
| | 42 | | // 1 -> this is greater |
| | 43 | | // -1 -> otherSample is greater |
| | 44 | |
|
| 0 | 45 | | SampleData otherSample = obj as SampleData; |
| | 46 | |
|
| 0 | 47 | | if (otherSample == null) |
| 0 | 48 | | return 1; |
| | 49 | |
|
| 0 | 50 | | if (Math.Abs(this.frameTimeMs - otherSample.frameTimeMs) < float.Epsilon) |
| 0 | 51 | | return 0; |
| | 52 | |
|
| 0 | 53 | | return this.frameTimeMs > otherSample.frameTimeMs ? 1 : -1; |
| | 54 | | } |
| | 55 | | } |
| | 56 | |
|
| | 57 | | private PerformanceMetricsDataVariable metricsData; |
| | 58 | | private float currentDurationInSeconds = 0f; |
| | 59 | | private float targetDurationInSeconds = 0f; |
| 0 | 60 | | private List<SampleData> samples = new List<SampleData>(); |
| | 61 | |
|
| | 62 | | // auxiliar data |
| | 63 | | private SampleData lastSavedSample; |
| | 64 | |
|
| | 65 | | // reported data |
| | 66 | | private double highestFrameTime; |
| | 67 | | private double lowestFrameTime; |
| | 68 | | private double averageFrameTime; |
| | 69 | | private double marginOfError; |
| | 70 | |
|
| | 71 | | private float percentile1FrameTime; |
| | 72 | | private float percentile50FrameTime; |
| | 73 | | private float percentile99FrameTime; |
| | 74 | | private int totalHiccupFrames; |
| | 75 | | private float totalHiccupsTimeInSeconds; |
| | 76 | | private int totalFrames; |
| | 77 | | private float totalFramesTimeInSeconds; |
| | 78 | | private long lowestAllocation; |
| | 79 | | private long highestAllocation; |
| | 80 | | private long averageAllocation; |
| | 81 | | private long totalAllocation; |
| | 82 | | private ProfilerRecorder gcAllocatedInFrameRecorder; |
| | 83 | |
|
| | 84 | | private bool justStarted = false; |
| | 85 | |
|
| 0 | 86 | | public PerformanceMeterController() { metricsData = Resources.Load<PerformanceMetricsDataVariable>("ScriptableOb |
| | 87 | |
|
| | 88 | | private void ResetDataValues() |
| | 89 | | { |
| 0 | 90 | | samples.Clear(); |
| 0 | 91 | | currentDurationInSeconds = 0f; |
| 0 | 92 | | targetDurationInSeconds = 0f; |
| | 93 | |
|
| 0 | 94 | | lastSavedSample = null; |
| | 95 | |
|
| 0 | 96 | | highestFrameTime = 0; |
| 0 | 97 | | lowestFrameTime = 0; |
| 0 | 98 | | averageFrameTime = 0; |
| 0 | 99 | | percentile50FrameTime = 0; |
| 0 | 100 | | percentile99FrameTime = 0; |
| 0 | 101 | | totalHiccupFrames = 0; |
| 0 | 102 | | totalHiccupsTimeInSeconds = 0; |
| 0 | 103 | | totalFrames = 0; |
| 0 | 104 | | totalFramesTimeInSeconds = 0; |
| 0 | 105 | | lowestAllocation = long.MaxValue; |
| 0 | 106 | | highestAllocation = 0; |
| 0 | 107 | | averageAllocation = 0; |
| 0 | 108 | | totalAllocation = 0; |
| 0 | 109 | | } |
| | 110 | |
|
| | 111 | | /// <summary> |
| | 112 | | /// Starts the Performance Meter Tool sampling. |
| | 113 | | /// </summary> |
| | 114 | | /// <param name="durationInSeconds">The target duration for the running of the tool, after which a report will b |
| | 115 | | public void StartSampling(float durationInSeconds) |
| | 116 | | { |
| 0 | 117 | | Log("Start running... target duration: " + durationInSeconds + " seconds"); |
| | 118 | |
|
| 0 | 119 | | ResetDataValues(); |
| | 120 | |
|
| 0 | 121 | | targetDurationInSeconds = durationInSeconds; |
| 0 | 122 | | justStarted = true; |
| 0 | 123 | | gcAllocatedInFrameRecorder = ProfilerRecorder.StartNew(ProfilerCategory.Memory, "GC Allocated In Frame"); |
| 0 | 124 | | metricsData.OnChange += OnMetricsChange; |
| 0 | 125 | | } |
| | 126 | |
|
| | 127 | | /// <summary> |
| | 128 | | /// Stops the Performance Meter Tool sampling, processes the data gathered and prints a full report in the conso |
| | 129 | | /// </summary> |
| | 130 | | public void StopSampling() |
| | 131 | | { |
| 0 | 132 | | Log("Stopped running."); |
| | 133 | |
|
| 0 | 134 | | metricsData.OnChange -= OnMetricsChange; |
| | 135 | |
|
| 0 | 136 | | if (samples.Count == 0) |
| | 137 | | { |
| 0 | 138 | | Log("No samples were gathered, the duration time in seconds set is probably too small"); |
| | 139 | |
|
| 0 | 140 | | return; |
| | 141 | | } |
| | 142 | |
|
| 0 | 143 | | ProcessSamples(); |
| | 144 | |
|
| 0 | 145 | | ReportData(); |
| 0 | 146 | | } |
| | 147 | |
|
| | 148 | | /// <summary> |
| | 149 | | /// Callback triggered on every update made to the PerformanceMetricsDataVariable ScriptableObject, done every f |
| | 150 | | /// </summary> |
| | 151 | | /// /// <param name="newData">NEW version of the PerformanceMetricsDataVariable ScriptableObject</param> |
| | 152 | | /// /// <param name="oldData">OLD version of the PerformanceMetricsDataVariable ScriptableObject</param> |
| | 153 | | private void OnMetricsChange(PerformanceMetricsData newData, PerformanceMetricsData oldData) |
| | 154 | | { |
| | 155 | | // we avoid the first frame as when we are in editor, the context menu pauses everything and the next frame |
| 0 | 156 | | if (justStarted) |
| | 157 | | { |
| 0 | 158 | | justStarted = false; |
| 0 | 159 | | return; |
| | 160 | | } |
| | 161 | |
|
| 0 | 162 | | float secondsConsumed = 0; |
| | 163 | |
|
| 0 | 164 | | if (lastSavedSample != null) |
| | 165 | | { |
| 0 | 166 | | if (lastSavedSample.frameNumber == Time.frameCount) |
| | 167 | | { |
| 0 | 168 | | Log("PerformanceMetricsDataVariable changed more than once in the same frame!"); |
| | 169 | |
|
| 0 | 170 | | return; |
| | 171 | | } |
| | 172 | |
|
| 0 | 173 | | secondsConsumed = Time.timeSinceLevelLoad - lastSavedSample.currentTime; |
| | 174 | | } |
| | 175 | |
|
| 0 | 176 | | float frameTimeMs = Time.deltaTime * 1000f; |
| | 177 | |
|
| 0 | 178 | | SampleData newSample = new SampleData |
| | 179 | | { |
| | 180 | | frameTimeMs = frameTimeMs, |
| | 181 | | frameNumber = Time.frameCount, |
| | 182 | | millisecondsConsumed = secondsConsumed * 1000, |
| | 183 | | currentTime = Time.timeSinceLevelLoad, |
| | 184 | | isHiccup = secondsConsumed > FPSEvaluation.HICCUP_THRESHOLD_IN_SECONDS |
| | 185 | | }; |
| | 186 | |
|
| 0 | 187 | | samples.Add(newSample); |
| 0 | 188 | | lastSavedSample = newSample; |
| | 189 | |
|
| 0 | 190 | | if (newSample.isHiccup) |
| | 191 | | { |
| 0 | 192 | | totalHiccupFrames++; |
| 0 | 193 | | totalHiccupsTimeInSeconds += secondsConsumed; |
| | 194 | | } |
| | 195 | |
|
| 0 | 196 | | UpdateAllocations(); |
| | 197 | |
|
| 0 | 198 | | totalFrames++; |
| | 199 | |
|
| 0 | 200 | | currentDurationInSeconds += Time.deltaTime; |
| | 201 | |
|
| 0 | 202 | | if (currentDurationInSeconds > targetDurationInSeconds) |
| | 203 | | { |
| 0 | 204 | | totalFramesTimeInSeconds = currentDurationInSeconds; |
| 0 | 205 | | StopSampling(); |
| | 206 | | } |
| 0 | 207 | | } |
| | 208 | |
|
| | 209 | | private void UpdateAllocations() |
| | 210 | | { |
| 0 | 211 | | long lastAllocation = gcAllocatedInFrameRecorder.LastValue; |
| | 212 | |
|
| 0 | 213 | | if (highestAllocation < lastAllocation) |
| | 214 | | { |
| 0 | 215 | | highestAllocation = lastAllocation; |
| | 216 | | } |
| | 217 | |
|
| 0 | 218 | | if (lowestAllocation > lastAllocation) |
| | 219 | | { |
| 0 | 220 | | lowestAllocation = lastAllocation; |
| | 221 | | } |
| | 222 | |
|
| 0 | 223 | | totalAllocation += lastAllocation; |
| 0 | 224 | | } |
| | 225 | |
|
| | 226 | | /// <summary> |
| | 227 | | /// Process the data gathered from every frame sample to calculate the final highestFPS, lowestFPS, averageFPS, |
| | 228 | | /// </summary> |
| | 229 | | private void ProcessSamples() |
| | 230 | | { |
| | 231 | | // Sort the samples based on FPS count of each one, to be able to calculate the percentiles later |
| 0 | 232 | | var sortedSamples = new List<SampleData>(samples); |
| 0 | 233 | | sortedSamples.Sort(); |
| | 234 | |
|
| 0 | 235 | | int samplesCount = sortedSamples.Count; |
| | 236 | |
|
| 0 | 237 | | var benchmark = new BenchmarkResult(sortedSamples.Select(sample => (double)sample.frameTimeMs).ToArray()); |
| | 238 | |
|
| 0 | 239 | | highestFrameTime = benchmark.max; |
| 0 | 240 | | lowestFrameTime = benchmark.min; |
| 0 | 241 | | averageFrameTime = benchmark.mean; |
| 0 | 242 | | marginOfError = benchmark.rme; |
| | 243 | |
|
| 0 | 244 | | percentile1FrameTime = sortedSamples[Mathf.Min(Mathf.CeilToInt(samplesCount * 0.01f), sortedSamples.Count-1) |
| 0 | 245 | | percentile50FrameTime = sortedSamples[Mathf.Min(Mathf.CeilToInt(samplesCount * 0.5f), sortedSamples.Count-1) |
| 0 | 246 | | percentile99FrameTime = sortedSamples[Mathf.Min(Mathf.CeilToInt(samplesCount * 0.99f), sortedSamples.Count-1 |
| | 247 | |
|
| 0 | 248 | | averageAllocation = totalAllocation / sortedSamples.Count; |
| 0 | 249 | | } |
| | 250 | |
|
| | 251 | | /// <summary> |
| | 252 | | /// Formats and prints the final data report following the 3 steps: system info, processed values and frame samp |
| | 253 | | /// </summary> |
| | 254 | | private void ReportData() |
| | 255 | | { |
| | 256 | | // TODO: We could build a text file (or html) template with replaceable tags like #OPERATING_SYSTEM, #GRAPHI |
| | 257 | |
|
| | 258 | | // Step 1 - report relevant system info: hardware, cappedFPS, OS, sampling duration, etc. |
| 0 | 259 | | Log("Data report step 1 - System and Graphics info:" |
| | 260 | | + "\n * Sampling duration in seconds -> " + targetDurationInSeconds |
| | 261 | | + "\n * System Info -> Operating System -> " + SystemInfo.operatingSystem |
| | 262 | | + "\n * System Info -> Device Name -> " + SystemInfo.deviceName |
| | 263 | | + "\n * System Info -> Graphics Device Name -> " + SystemInfo.graphicsDeviceName |
| | 264 | | + "\n * System Info -> System RAM Size -> " + SystemInfo.systemMemorySize |
| | 265 | | + "\n * General Settings -> Scenes Load Radius -> " + Settings.i.generalSettings.Data.scenesLoadRadius |
| | 266 | | + "\n * Quality Settings -> FPSCap -> " + Settings.i.qualitySettings.Data.fpsCap |
| | 267 | | + "\n * Quality Settings -> Bloom -> " + Settings.i.qualitySettings.Data.bloom |
| | 268 | | + "\n * Quality Settings -> Shadow -> " + Settings.i.qualitySettings.Data.shadows |
| | 269 | | + "\n * Quality Settings -> Antialising -> " + Settings.i.qualitySettings.Data.antiAliasing |
| | 270 | | + "\n * Quality Settings -> Base Resolution -> " + Settings.i.qualitySettings.Data.baseResolution |
| | 271 | | + "\n * Quality Settings -> Display Name -> " + Settings.i.qualitySettings.Data.displayName |
| | 272 | | + "\n * Quality Settings -> Render Scale -> " + Settings.i.qualitySettings.Data.renderScale |
| | 273 | | + "\n * Quality Settings -> Shadow Distance -> " + Settings.i.qualitySettings.Data.shadowDistance |
| | 274 | | + "\n * Quality Settings -> Shadow Resolution -> " + Settings.i.qualitySettings.Data.shadowResolution |
| | 275 | | + "\n * Quality Settings -> Soft Shadows -> " + Settings.i.qualitySettings.Data.softShadows |
| | 276 | | + "\n * Quality Settings -> SSAO Quality -> " + Settings.i.qualitySettings.Data.ssaoQuality |
| | 277 | | + "\n * Quality Settings -> Camera Draw Distance -> " + |
| | 278 | | Settings.i.qualitySettings.Data.cameraDrawDistance |
| | 279 | | + "\n * Quality Settings -> Detail Object Culling Enabled -> " + |
| | 280 | | Settings.i.qualitySettings.Data.enableDetailObjectCulling |
| | 281 | | + "\n * Quality Settings -> Detail Object Culling Limit -> " + |
| | 282 | | Settings.i.qualitySettings.Data.detailObjectCullingLimit |
| | 283 | | + "\n * Quality Settings -> Reflection Quality -> " + |
| | 284 | | Settings.i.qualitySettings.Data.reflectionResolution |
| | 285 | | ); |
| | 286 | |
|
| | 287 | | // Step 2 - report processed data |
| 0 | 288 | | string format = "F1"; |
| | 289 | |
|
| 0 | 290 | | Log($"Data report step 2 - Processed values:" + |
| | 291 | | $"\n * PERFORMANCE SCORE (0-100) -> {CalculatePerformanceScore()}" + |
| | 292 | | $"\n * lowest frame time -> {lowestFrameTime.ToString(format)}ms" + |
| | 293 | | $"\n * average frame time -> {averageFrameTime.ToString(format)}ms" + |
| | 294 | | $"\n * highest frame time -> {highestFrameTime.ToString(format)}ms" + |
| | 295 | | $"\n * 1 percentile frame time -> {percentile1FrameTime.ToString(format)}ms" + |
| | 296 | | $"\n * 50 percentile frame time -> {percentile50FrameTime.ToString(format)}ms" + |
| | 297 | | $"\n * 99 percentile frame time -> {percentile99FrameTime.ToString(format)}ms" + |
| | 298 | | $"\n * error percentage -> ±{marginOfError.ToString(format)}%" + |
| | 299 | | $"\n * total hiccups (>{FPSEvaluation.HICCUP_THRESHOLD_IN_SECONDS}ms frames) -> {totalHiccupFrames} ({Ca |
| | 300 | | $"\n * total hiccups time (seconds) -> {totalHiccupsTimeInSeconds}" + |
| | 301 | | $"\n * total frames -> {totalFrames}" + |
| | 302 | | $"\n * total frames time (seconds) -> {totalFramesTimeInSeconds}" + |
| | 303 | | $"\n * lowest allocations (kb) -> {lowestAllocation / 1000.0}" + |
| | 304 | | $"\n * highest allocations (kb) -> {highestAllocation / 1000.0}" + |
| | 305 | | $"\n * average allocations (kb) -> {averageAllocation / 1000.0}" + |
| | 306 | | $"\n * total allocations (kb) -> {totalAllocation / 1000.0}" |
| | 307 | | ); |
| | 308 | |
|
| | 309 | | // Step 3 - report all samples data |
| 0 | 310 | | string rawSamplesJSON = "Data report step 3 - Raw samples:" |
| | 311 | | + "\n " |
| | 312 | | + "{\"frame-samples\": " + JsonConvert.SerializeObject(samples) + "}"; |
| | 313 | |
|
| | 314 | | #if !UNITY_WEBGL && UNITY_EDITOR |
| 0 | 315 | | string targetFilePath = Application.persistentDataPath + "/PerformanceMeterRawFrames.txt"; |
| 0 | 316 | | Log("Data report step 3 - Trying to dump raw samples JSON at: " + targetFilePath); |
| 0 | 317 | | System.IO.File.WriteAllText(targetFilePath, rawSamplesJSON); |
| | 318 | | #endif |
| | 319 | |
|
| 0 | 320 | | Log(rawSamplesJSON); |
| 0 | 321 | | } |
| | 322 | |
|
| | 323 | | /// <summary> |
| | 324 | | /// Calculates a performance score from 0 to 100 based on the average frame time (compared to the closest frame |
| | 325 | | /// </summary> |
| | 326 | | private int CalculatePerformanceScore() |
| | 327 | | { |
| 0 | 328 | | double desiredFrameTime = Settings.i.qualitySettings.Data.fpsCap ? 1000/30.0 : 1000/60.0; |
| 0 | 329 | | double frameScore = Mathf.Min((float)(desiredFrameTime/ averageFrameTime), 1); // from 0 to 1 |
| 0 | 330 | | double hiccupsScore = 1 - (float) totalHiccupFrames / samples.Count; // from 0 to 1 |
| 0 | 331 | | double performanceScore = (frameScore + hiccupsScore) / 2 * 100; // scores sum / amount of scores * 100 to h |
| | 332 | |
|
| 0 | 333 | | return Mathf.RoundToInt((float)performanceScore * 100f) / 100; |
| | 334 | | } |
| | 335 | |
|
| | 336 | | private float CalculateHiccupsPercentage() |
| | 337 | | { |
| 0 | 338 | | float percentage = ((float) totalHiccupFrames / totalFrames) * 100; |
| 0 | 339 | | percentage = Mathf.Round(percentage * 100f) / 100f; // to have 2 decimals |
| | 340 | |
|
| 0 | 341 | | return percentage; |
| | 342 | | } |
| | 343 | |
|
| | 344 | | /// <summary> |
| | 345 | | /// Logs the tool messages in console regardless of the "Debug.unityLogger.logEnabled" value. |
| | 346 | | /// </summary> |
| | 347 | | private void Log(string message) |
| | 348 | | { |
| 0 | 349 | | bool originalLogEnabled = Debug.unityLogger.logEnabled; |
| 0 | 350 | | Debug.unityLogger.logEnabled = true; |
| | 351 | |
|
| 0 | 352 | | Debug.Log("PerformanceMeter - " + message); |
| | 353 | |
|
| 0 | 354 | | Debug.unityLogger.logEnabled = originalLogEnabled; |
| 0 | 355 | | } |
| | 356 | | } |
| | 357 | | } |