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