| | 1 | | using System; |
| | 2 | | using System.Collections.Generic; |
| | 3 | | using UnityEngine; |
| | 4 | | using DCL.FPSDisplay; |
| | 5 | | using DCL.SettingsCommon; |
| | 6 | | using Newtonsoft.Json; |
| | 7 | |
|
| | 8 | | namespace DCL |
| | 9 | | { |
| | 10 | | /// <summary> |
| | 11 | | /// Performance Meter Tool |
| | 12 | | /// |
| | 13 | | /// It samples frames performance data for the target duration and prints a complete report when finished. |
| | 14 | | /// |
| | 15 | | /// There are 2 ways to trigger this tool usage: |
| | 16 | | /// A: While the client is running in the browser, open the browser console and run "clientDebug.RunPerformanceMeter |
| | 17 | | /// B: In Unity Editor select the "Main" gameobject and right-click on its DebugBridge Monobehaviour, from there a d |
| | 18 | | /// </summary> |
| | 19 | | public class PerformanceMeterController |
| | 20 | | { |
| | 21 | | private class SampleData : IComparable |
| | 22 | | { |
| | 23 | | public int frameNumber; |
| | 24 | | public float millisecondsConsumed; |
| | 25 | | public bool isHiccup = false; |
| | 26 | | public float currentTime; |
| | 27 | | public float fpsAtThisFrameInTime; |
| | 28 | |
|
| | 29 | | public override string ToString() |
| | 30 | | { |
| 0 | 31 | | return "frame number: " + frameNumber |
| | 32 | | + "\n frame consumed milliseconds: " + millisecondsConsumed |
| | 33 | | + "\n is hiccup: " + isHiccup |
| | 34 | | + "\n fps at this frame: " + fpsAtThisFrameInTime; |
| | 35 | | } |
| | 36 | |
|
| | 37 | | public int CompareTo(object obj) |
| | 38 | | { |
| | 39 | | // 0 -> this and otherSample are equal |
| | 40 | | // 1 -> this is greater |
| | 41 | | // -1 -> otherSample is greater |
| | 42 | |
|
| 0 | 43 | | SampleData otherSample = obj as SampleData; |
| | 44 | |
|
| 0 | 45 | | if (otherSample == null) |
| 0 | 46 | | return 1; |
| | 47 | |
|
| 0 | 48 | | if (this.fpsAtThisFrameInTime == otherSample.fpsAtThisFrameInTime) |
| 0 | 49 | | return 0; |
| | 50 | |
|
| 0 | 51 | | return this.fpsAtThisFrameInTime > otherSample.fpsAtThisFrameInTime ? 1 : -1; |
| | 52 | | } |
| | 53 | | } |
| | 54 | |
|
| | 55 | | private PerformanceMetricsDataVariable metricsData; |
| | 56 | | private float currentDurationInSeconds = 0f; |
| | 57 | | private float targetDurationInSeconds = 0f; |
| 0 | 58 | | private List<SampleData> samples = new List<SampleData>(); |
| | 59 | |
|
| | 60 | | // auxiliar data |
| | 61 | | private SampleData lastSavedSample; |
| | 62 | | private float fpsSum = 0; |
| | 63 | |
|
| | 64 | | // reported data |
| | 65 | | private float highestFPS; |
| | 66 | | private float lowestFPS; |
| | 67 | | private float averageFPS; |
| | 68 | | private float percentile50FPS; |
| | 69 | | private float percentile95FPS; |
| | 70 | | private int totalHiccupFrames; |
| | 71 | | private float totalHiccupsTimeInSeconds; |
| | 72 | | private int totalFrames; |
| | 73 | | private float totalFramesTimeInSeconds; |
| | 74 | |
|
| 0 | 75 | | public PerformanceMeterController() { metricsData = Resources.Load<PerformanceMetricsDataVariable>("ScriptableOb |
| | 76 | |
|
| | 77 | | private void ResetDataValues() |
| | 78 | | { |
| 0 | 79 | | samples.Clear(); |
| 0 | 80 | | currentDurationInSeconds = 0f; |
| 0 | 81 | | targetDurationInSeconds = 0f; |
| | 82 | |
|
| 0 | 83 | | lastSavedSample = null; |
| 0 | 84 | | fpsSum = 0; |
| | 85 | |
|
| 0 | 86 | | highestFPS = 0; |
| 0 | 87 | | lowestFPS = 0; |
| 0 | 88 | | averageFPS = 0; |
| 0 | 89 | | percentile50FPS = 0; |
| 0 | 90 | | percentile95FPS = 0; |
| 0 | 91 | | totalHiccupFrames = 0; |
| 0 | 92 | | totalHiccupsTimeInSeconds = 0; |
| 0 | 93 | | totalFrames = 0; |
| 0 | 94 | | totalFramesTimeInSeconds = 0; |
| 0 | 95 | | } |
| | 96 | |
|
| | 97 | | /// <summary> |
| | 98 | | /// Starts the Performance Meter Tool sampling. |
| | 99 | | /// </summary> |
| | 100 | | /// <param name="durationInSeconds">The target duration for the running of the tool, after which a report will b |
| | 101 | | public void StartSampling(float durationInSeconds) |
| | 102 | | { |
| 0 | 103 | | Log("Start running... target duration: " + durationInSeconds + " seconds"); |
| | 104 | |
|
| 0 | 105 | | ResetDataValues(); |
| | 106 | |
|
| 0 | 107 | | targetDurationInSeconds = durationInSeconds; |
| | 108 | |
|
| 0 | 109 | | metricsData.OnChange += OnMetricsChange; |
| 0 | 110 | | } |
| | 111 | |
|
| | 112 | | /// <summary> |
| | 113 | | /// Stops the Performance Meter Tool sampling, processes the data gathered and prints a full report in the conso |
| | 114 | | /// </summary> |
| | 115 | | public void StopSampling() |
| | 116 | | { |
| 0 | 117 | | Log("Stopped running."); |
| | 118 | |
|
| 0 | 119 | | metricsData.OnChange -= OnMetricsChange; |
| | 120 | |
|
| 0 | 121 | | if (samples.Count == 0) |
| | 122 | | { |
| 0 | 123 | | Log("No samples were gathered, the duration time in seconds set is probably too small"); |
| 0 | 124 | | return; |
| | 125 | | } |
| | 126 | |
|
| 0 | 127 | | ProcessSamples(); |
| | 128 | |
|
| 0 | 129 | | ReportData(); |
| 0 | 130 | | } |
| | 131 | |
|
| | 132 | | /// <summary> |
| | 133 | | /// Callback triggered on every update made to the PerformanceMetricsDataVariable ScriptableObject, done every f |
| | 134 | | /// </summary> |
| | 135 | | /// /// <param name="newData">NEW version of the PerformanceMetricsDataVariable ScriptableObject</param> |
| | 136 | | /// /// <param name="oldData">OLD version of the PerformanceMetricsDataVariable ScriptableObject</param> |
| | 137 | | private void OnMetricsChange(PerformanceMetricsData newData, PerformanceMetricsData oldData) |
| | 138 | | { |
| 0 | 139 | | float secondsConsumed = 0; |
| | 140 | |
|
| 0 | 141 | | if (lastSavedSample != null) |
| | 142 | | { |
| 0 | 143 | | if (lastSavedSample.frameNumber == Time.frameCount) |
| | 144 | | { |
| 0 | 145 | | Log("PerformanceMetricsDataVariable changed more than once in the same frame!"); |
| 0 | 146 | | return; |
| | 147 | | } |
| | 148 | |
|
| 0 | 149 | | secondsConsumed = Time.timeSinceLevelLoad - lastSavedSample.currentTime; |
| | 150 | | } |
| | 151 | |
|
| 0 | 152 | | SampleData newSample = new SampleData() |
| | 153 | | { |
| | 154 | | frameNumber = Time.frameCount, |
| | 155 | | fpsAtThisFrameInTime = newData.fpsCount, |
| | 156 | | millisecondsConsumed = secondsConsumed * 1000, |
| | 157 | | currentTime = Time.timeSinceLevelLoad |
| | 158 | | }; |
| 0 | 159 | | newSample.isHiccup = secondsConsumed > FPSEvaluation.HICCUP_THRESHOLD_IN_SECONDS; |
| 0 | 160 | | samples.Add(newSample); |
| 0 | 161 | | lastSavedSample = newSample; |
| | 162 | |
|
| 0 | 163 | | if (newSample.isHiccup) |
| | 164 | | { |
| 0 | 165 | | totalHiccupFrames++; |
| 0 | 166 | | totalHiccupsTimeInSeconds += secondsConsumed; |
| | 167 | | } |
| | 168 | |
|
| 0 | 169 | | fpsSum += newData.fpsCount; |
| | 170 | |
|
| 0 | 171 | | totalFrames++; |
| | 172 | |
|
| 0 | 173 | | currentDurationInSeconds += Time.deltaTime; |
| 0 | 174 | | if (currentDurationInSeconds > targetDurationInSeconds) |
| | 175 | | { |
| 0 | 176 | | totalFramesTimeInSeconds = currentDurationInSeconds; |
| 0 | 177 | | StopSampling(); |
| | 178 | | } |
| 0 | 179 | | } |
| | 180 | |
|
| | 181 | | /// <summary> |
| | 182 | | /// Process the data gathered from every frame sample to calculate the final highestFPS, lowestFPS, averageFPS, |
| | 183 | | /// </summary> |
| | 184 | | private void ProcessSamples() |
| | 185 | | { |
| | 186 | | // Sort the samples based on FPS count of each one, to be able to calculate the percentiles later |
| 0 | 187 | | var sortedSamples = new List<SampleData>(samples); |
| 0 | 188 | | sortedSamples.Sort(); |
| | 189 | |
|
| 0 | 190 | | int samplesCount = sortedSamples.Count; |
| | 191 | |
|
| 0 | 192 | | highestFPS = sortedSamples[samplesCount - 1].fpsAtThisFrameInTime; |
| 0 | 193 | | lowestFPS = sortedSamples[0].fpsAtThisFrameInTime; |
| | 194 | |
|
| 0 | 195 | | averageFPS = fpsSum / sortedSamples.Count; |
| | 196 | |
|
| 0 | 197 | | percentile50FPS = sortedSamples[Mathf.CeilToInt(samplesCount * 0.5f)].fpsAtThisFrameInTime; |
| 0 | 198 | | percentile95FPS = sortedSamples[Mathf.CeilToInt(samplesCount * 0.95f)].fpsAtThisFrameInTime; |
| 0 | 199 | | } |
| | 200 | |
|
| | 201 | | /// <summary> |
| | 202 | | /// Formats and prints the final data report following the 3 steps: system info, processed values and frame samp |
| | 203 | | /// </summary> |
| | 204 | | private void ReportData() |
| | 205 | | { |
| | 206 | | // TODO: We could build a text file (or html) template with replaceable tags like #OPERATING_SYSTEM, #GRAPHI |
| | 207 | |
|
| | 208 | | // Step 1 - report relevant system info: hardware, cappedFPS, OS, sampling duration, etc. |
| 0 | 209 | | Log("Data report step 1 - System and Graphics info:" |
| | 210 | | + "\n * Sampling duration in seconds -> " + targetDurationInSeconds |
| | 211 | | + "\n * System Info -> Operating System -> " + SystemInfo.operatingSystem |
| | 212 | | + "\n * System Info -> Device Name -> " + SystemInfo.deviceName |
| | 213 | | + "\n * System Info -> Graphics Device Name -> " + SystemInfo.graphicsDeviceName |
| | 214 | | + "\n * System Info -> System RAM Size -> " + SystemInfo.systemMemorySize |
| | 215 | | + "\n * General Settings -> Scenes Load Radius -> " + Settings.i.generalSettings.Data.scenesLoadRadius |
| | 216 | | + "\n * Quality Settings -> FPSCap -> " + Settings.i.qualitySettings.Data.fpsCap |
| | 217 | | + "\n * Quality Settings -> Bloom -> " + Settings.i.qualitySettings.Data.bloom |
| | 218 | | + "\n * Quality Settings -> Shadow -> " + Settings.i.qualitySettings.Data.shadows |
| | 219 | | + "\n * Quality Settings -> Antialising -> " + Settings.i.qualitySettings.Data.antiAliasing |
| | 220 | | + "\n * Quality Settings -> Base Resolution -> " + Settings.i.qualitySettings.Data.baseResolution |
| | 221 | | + "\n * Quality Settings -> Display Name -> " + Settings.i.qualitySettings.Data.displayName |
| | 222 | | + "\n * Quality Settings -> Render Scale -> " + Settings.i.qualitySettings.Data.renderScale |
| | 223 | | + "\n * Quality Settings -> Shadow Distance -> " + Settings.i.qualitySettings.Data.shadowDistance |
| | 224 | | + "\n * Quality Settings -> Shadow Resolution -> " + Settings.i.qualitySettings.Data.shadowResolution |
| | 225 | | + "\n * Quality Settings -> Soft Shadows -> " + Settings.i.qualitySettings.Data.softShadows |
| | 226 | | + "\n * Quality Settings -> SSAO Quality -> " + Settings.i.qualitySettings.Data.ssaoQuality |
| | 227 | | + "\n * Quality Settings -> Camera Draw Distance -> " + Settings.i.qualitySettings.Data.cameraDrawDistan |
| | 228 | | + "\n * Quality Settings -> Detail Object Culling Enabled -> " + Settings.i.qualitySettings.Data.enableD |
| | 229 | | + "\n * Quality Settings -> Detail Object Culling Limit -> " + Settings.i.qualitySettings.Data.detailObj |
| | 230 | | + "\n * Quality Settings -> Reflection Quality -> " + Settings.i.qualitySettings.Data.reflectionResoluti |
| | 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 && UNITY_EDITOR |
| 0 | 253 | | string targetFilePath = Application.persistentDataPath + "/PerformanceMeterRawFrames.txt"; |
| 0 | 254 | | Log("Data report step 3 - Trying to dump raw samples JSON at: " + targetFilePath); |
| 0 | 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.qualitySettings.Data.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 | | } |