performancemeasurements.py (15766B)
1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\packages\statscapture\performancemeasurements.py 2 import json 3 import os 4 import shutil 5 import tempfile 6 import time 7 import blue 8 import evegraphics.settings as gfxsettings 9 import trinity 10 import uthread2 11 from functools import reduce 12 PERFMEASUREMENTS_URL = 'http://trilambda-ws:8088/' 13 PERFMEASUREMENTS_UPLOAD_URL = PERFMEASUREMENTS_URL + 'measurement/' 14 15 def _CaptureRenderSteps(): 16 steps = [] 17 for rjIndex, rj in enumerate(trinity.renderJobs.recurring): 18 for stepIndex, step in enumerate(rj.steps): 19 step.statName = 'Trinity/RenderJobs/Recurring/%s/%s' % (rj.name or 'RJ%s' % (stepIndex + 1), step.name or 'Step%s' % (stepIndex + 1)) 20 step.debugCaptureCpuTime = True 21 step.debugCaptureGpuTime = True 22 steps.append(step) 23 24 return steps 25 26 27 def _GetRenderTargetBitmap(): 28 rt = trinity.device.GetRenderContext().GetDefaultBackBuffer() 29 if not rt.isReadable: 30 readable = trinity.Tr2RenderTarget(rt.width, rt.height, 1, rt.format) 31 rt.Resolve(readable) 32 return trinity.Tr2HostBitmap(readable) 33 else: 34 return trinity.Tr2HostBitmap(rt) 35 36 37 def _GetMemoryUsage(): 38 try: 39 meg = 1.0 / 1024.0 / 1024.0 40 return blue.pyos.cpuUsage[-1].virtualMemory * meg 41 except: 42 return 0 43 44 45 class _FrameTimeInfo(object): 46 47 def __init__(self, frameTimes): 48 times = sorted(frameTimes) 49 self.median = frameTimes[len(times) / 2] 50 self.min = times[0] 51 self.max = times[-1] 52 summed = reduce(lambda x, y: x + y, times) 53 self.avg = summed / len(times) 54 55 56 class _MemoryInfo(object): 57 58 def __init__(self, start, end): 59 self.start = start 60 self.end = end 61 62 63 class _DrawPrimitivesInfo(object): 64 65 def __init__(self, drawPrimitives): 66 if drawPrimitives: 67 self.min = int(min(drawPrimitives.values())) 68 self.max = int(max(drawPrimitives.values())) 69 self.avg = sum(drawPrimitives.values()) / len(drawPrimitives) 70 else: 71 self.min = 0 72 self.max = 0 73 self.avg = 0 74 75 76 class _MeasurementInfo(object): 77 78 def __init__(self, frameTimes, startSysMem, endSysMem, startGpuMem, endGpuMem, drawPrimitives): 79 self.frameTime = _FrameTimeInfo(frameTimes) 80 self.systemMemory = _MemoryInfo(startSysMem, endSysMem) 81 self.gpuMemory = _MemoryInfo(startGpuMem, endGpuMem) 82 self.drawPrimitives = _DrawPrimitivesInfo(drawPrimitives) 83 84 85 class Measurement(object): 86 STATE_NOT_STARTED = 0 87 STATE_RUNNING = 1 88 STATE_FINISHED = 2 89 90 def __init__(self, captureRenderSteps = True, perFrameCapture = True, statFilter = ()): 91 self._steps = [] 92 self._captureRenderSteps = captureRenderSteps 93 self._perFrameCapture = perFrameCapture 94 self._startTime = 0 95 self._endTime = time.localtime() 96 self._statistics = [] 97 self._state = self.STATE_NOT_STARTED 98 self._frameTimes = {} 99 self._drawPrimitives = {} 100 self._screenshots = [] 101 self._startSysMem = 0 102 self._endSysMem = 0 103 self._startGpuMem = 0 104 self._endGpuMem = 0 105 self._statFilter = statFilter 106 107 def _Ticker(self): 108 self._startTime = blue.os.GetWallclockTime() 109 t0 = self._startTime 110 dp = blue.statistics.Find('Trinity/AL/sceneDrawcallCount') 111 while self._state == self.STATE_RUNNING: 112 uthread2.Yield() 113 if self._state != self.STATE_RUNNING: 114 break 115 t1 = blue.os.GetWallclockTime() 116 ms = float(blue.os.TimeDiffInUs(t0, t1)) / 1000.0 117 t0 = t1 118 timeFromStartInMs = float(blue.os.TimeDiffInUs(self._startTime, t1)) / 1000.0 119 self._frameTimes[timeFromStartInMs] = ms 120 self._drawPrimitives[timeFromStartInMs] = int(dp.value) if dp else 0 121 122 def GetState(self): 123 return self._state 124 125 def Begin(self): 126 if self._state == self.STATE_RUNNING: 127 raise RuntimeError('already started') 128 if self._captureRenderSteps: 129 self._steps = _CaptureRenderSteps() 130 uthread2.Yield() 131 uthread2.Yield() 132 self._startTime = blue.os.GetWallclockTime() 133 self._frameTimes.clear() 134 self._drawPrimitives.clear() 135 del self._screenshots[:] 136 blue.statistics.BeginCapture() 137 self._startSysMem = _GetMemoryUsage() 138 self._startGpuMem = getattr(blue.statistics.Find('Trinity/AL/gpuMemoryEst/total'), 'value', 0) / 1024.0 / 1024.0 139 self._state = self.STATE_RUNNING 140 uthread2.StartTasklet(self._Ticker) 141 142 def End(self): 143 if self._state != self.STATE_RUNNING: 144 raise RuntimeError('measurement is not running') 145 try: 146 self._endTime = time.localtime() 147 self._endSysMem = _GetMemoryUsage() 148 self._endGpuMem = getattr(blue.statistics.Find('Trinity/AL/gpuMemoryEst/total'), 'value', 0) / 1024.0 / 1024.0 149 statistics = blue.statistics.EndCapture() 150 types = {0: 0, 151 1: 0, 152 2: 1, 153 3: 2} 154 self._statistics = [ {'name': name, 155 'type': types.get(getattr(blue.statistics.Find(name), 'type', 0), 0), 156 'perFrame': values} for name, values in statistics.items() if not self._statFilter or name in self._statFilter ] 157 for step in self._steps: 158 step.debugCaptureCpuTime = False 159 step.debugCaptureGpuTime = False 160 step.statName = '' 161 162 del self._steps[:] 163 finally: 164 self._state = self.STATE_FINISHED 165 166 def GetEndTime(self): 167 if self._state != self.STATE_FINISHED: 168 raise RuntimeError('measurement data is not available') 169 return self._endTime 170 171 def GetInfo(self): 172 if self._state != self.STATE_FINISHED: 173 raise RuntimeError('measurement data is not available') 174 return _MeasurementInfo(self._frameTimes.values(), self._startSysMem, self._endSysMem, self._startGpuMem, self._endGpuMem, self._drawPrimitives) 175 176 def GetFrameTimes(self): 177 if self._state != self.STATE_FINISHED: 178 raise RuntimeError('measurement data is not available') 179 return self._frameTimes 180 181 def TakeScreenshot(self): 182 if self._state != self.STATE_RUNNING: 183 raise RuntimeError('measurement is not running') 184 screenshot = _GetRenderTargetBitmap() 185 screenshotTime = float(blue.os.TimeDiffInUs(self._startTime, blue.os.GetWallclockTime())) / 1000.0 186 self.AddScreenshot(screenshotTime, screenshot) 187 188 def AddScreenshot(self, screenshotTime, screenshot): 189 self._screenshots.append((screenshotTime, screenshot)) 190 191 def ExportCsv(self, path): 192 if self._state != self.STATE_FINISHED: 193 raise RuntimeError('measurement data is not available') 194 if not os.path.exists(os.path.dirname(path)): 195 os.makedirs(os.path.dirname(path)) 196 dataTable = [','.join(map(str, sorted(self._frameTimes.keys()))), 197 ','.join([ str(self._frameTimes[k]) for k in sorted(self._frameTimes.keys()) ]), 198 ','.join((str(self._drawPrimitives.get(k, '')) for k in sorted(self._frameTimes.keys()))), 199 '%s' % self._endSysMem] 200 data = '\n'.join(dataTable) 201 with open(path, 'w') as f: 202 f.write(data) 203 204 def GetData(self, name, appData, benchmarkData = None): 205 import socket 206 aaLabels = {gfxsettings.AA_QUALITY_DISABLED: 'Disabled', 207 gfxsettings.AA_QUALITY_TAA_LOW: 'Low', 208 gfxsettings.AA_QUALITY_TAA_MEDIUM: 'Medium', 209 gfxsettings.AA_QUALITY_TAA_HIGH: 'High'} 210 if blue.sysinfo.isWine: 211 osLabel = blue.sysinfo.wineHostOs 212 elif blue.sysinfo.os.platform == blue.OsPlatform.OSX: 213 osLabel = 'macOS %s.%s.%s' % (blue.sysinfo.os.majorVersion, blue.sysinfo.os.minorVersion, blue.sysinfo.os.buildNumber) 214 else: 215 osLabel = 'Windows %s.%s' % (blue.sysinfo.os.majorVersion, blue.sysinfo.os.minorVersion) 216 217 def yesNo(value): 218 if value: 219 return 'Yes' 220 return 'No' 221 222 arch = blue.sysinfo.cpu.architecture 223 if blue.sysinfo.isRosetta: 224 arch = 'x64 (Rosetta)' 225 data = {'machine': {'name': socket.gethostname(), 226 'cpu': blue.sysinfo.cpu.brand, 227 'memory': blue.sysinfo.GetMemory().totalPhysical, 228 'gpu': trinity.adapters.GetAdapterInfo(trinity.device.adapter).description, 229 'os': osLabel}, 230 'app': {'trinityPlatform': trinity.platform, 231 'architecture': arch, 232 'settings': {'FullScreen': trinity.mainWindow.GetWindowState().windowMode == trinity.Tr2WindowMode.FULL_SCREEN, 233 'Size': '%s x %s' % (trinity.device.width, trinity.device.height), 234 'Present Interval': trinity.PRESENT_INTERVAL.GetNameFromValue(trinity.device.presentationInterval), 235 'Anti-Aliasing': aaLabels.get(gfxsettings.Get(gfxsettings.GFX_ANTI_ALIASING), '?'), 236 'Post Processing': ['Low', 'Medium', 'High'][gfxsettings.Get(gfxsettings.GFX_POST_PROCESSING_QUALITY)], 237 'Shader Quality': ['', 238 'Low', 239 'Medium', 240 'High'][gfxsettings.Get(gfxsettings.GFX_SHADER_QUALITY)], 241 'Texture Quality': ['High', 'Medium', 'Low'][gfxsettings.Get(gfxsettings.GFX_TEXTURE_QUALITY)], 242 'LOD Quality': ['', 243 'Low', 244 'Medium', 245 'High'][gfxsettings.Get(gfxsettings.GFX_LOD_QUALITY)], 246 'Shadow Quality': ['Disabled', 247 'Low', 248 'High', 249 'Raytraced'][gfxsettings.Get(gfxsettings.GFX_SHADOW_QUALITY)], 250 'Reflection Quality': ['Disabled', 251 'Low', 252 'Medium', 253 'High', 254 'Highest'][gfxsettings.Get(gfxsettings.GFX_REFLECTION_QUALITY)], 255 'AO Quality': ['Disabled', 256 'Low', 257 'Medium', 258 'High'][gfxsettings.Get(gfxsettings.GFX_AO_QUALITY)], 259 'Volumetric Quality': ['Low', 260 'Medium', 261 'High', 262 'Ultra'][gfxsettings.Get(gfxsettings.GFX_VOLUMETRIC_QUALITY)], 263 'Upscaling Technique': trinity.UPSCALING_TECHNIQUE.GetNameFromValue(trinity.device.upscalingTechnique).lower(), 264 'Upscaling Setting': trinity.UPSCALING_SETTING.GetNameFromValue(trinity.device.upscalingSetting).lower(), 265 'Framegeneration': yesNo(gfxsettings.Get(gfxsettings.GFX_FRAMEGENERATION_ENABLED)), 266 'Turrets Enabled': yesNo(gfxsettings.Get(gfxsettings.UI_TURRETS_ENABLED)), 267 'Effects': yesNo(gfxsettings.Get(gfxsettings.UI_EFFECTS_ENABLED)), 268 'Missile Effects': yesNo(gfxsettings.Get(gfxsettings.UI_MISSILES_ENABLED)), 269 'Ship Explosions': yesNo(gfxsettings.Get(gfxsettings.UI_EXPLOSION_EFFECTS_ENABLED)), 270 'Drone Models': yesNo(gfxsettings.Get(gfxsettings.UI_DRONE_MODELS_ENABLED)), 271 'Trails': yesNo(gfxsettings.Get(gfxsettings.UI_TRAILS_ENABLED)), 272 'GPU Particles': yesNo(gfxsettings.Get(gfxsettings.UI_GPU_PARTICLES_ENABLED)), 273 'Asteroid Environments': yesNo(gfxsettings.Get(gfxsettings.UI_ASTEROID_ATMOSPHERICS)), 274 'Inspace Skinning Effect': yesNo(gfxsettings.Get(gfxsettings.UI_MODELSKINSINSPACE_ENABLED)), 275 'DOF Enabled': yesNo(gfxsettings.Get(gfxsettings.GFX_DOF_POSTPROCESS_ENABLED)), 276 'GDPR Enabled': yesNo(trinity.settings.GetValue('gdrEnabled'))}}, 277 'benchmark': {'name': name, 278 'timestamp': time.mktime(self._endTime) * 1000}, 279 'frameTimes': sorted(self._frameTimes.keys()), 280 'stats': self._statistics} 281 data['app'].update(appData) 282 if benchmarkData: 283 data['benchmark'].update(benchmarkData) 284 try: 285 driverInfo = trinity.adapters.GetAdapterInfo(trinity.device.adapter).GetDriverInfo() 286 data['machine']['gpu driver'] = '%s %s' % (driverInfo.driverVersionString, driverInfo.driverDate) 287 except trinity.ALError: 288 pass 289 290 return data 291 292 def Export(self, name, path, appData, benchmarkData = None): 293 if self._state != self.STATE_FINISHED: 294 raise RuntimeError('measurement data is not available') 295 if not os.path.exists(os.path.dirname(path)): 296 os.makedirs(os.path.dirname(path)) 297 exportName = os.path.splitext(os.path.basename(path))[0] 298 exportFolderPath = os.path.dirname(path) 299 data = self.GetData(name, appData, benchmarkData) 300 if self._screenshots: 301 data['screenshots'] = [ {'time': s[0] / 1000, 302 'href': '%s-%s.png' % (exportName, i)} for i, s in enumerate(self._screenshots) ] 303 outPaths = [] 304 with open(path, 'w') as f: 305 json.dump(data, f, indent=2) 306 outPaths.append(path) 307 for i, each in enumerate(self._screenshots): 308 screenPath = os.path.abspath(os.path.join(exportFolderPath, '%s-%s.png' % (exportName, i))) 309 each[1].Save(screenPath) 310 outPaths.append(screenPath) 311 312 return outPaths 313 314 def Upload(self, name, appData, benchmarkData = None, tags = '', saveDir = ''): 315 import requests 316 if saveDir: 317 try: 318 os.makedirs(saveDir) 319 except OSError: 320 pass 321 322 tempDir = saveDir if saveDir else tempfile.mkdtemp() 323 try: 324 paths = self.Export(name, os.path.join(tempDir, 'measurement.json'), appData, benchmarkData) 325 files = [ (os.path.basename(x), open(x, 'rb')) for x in paths ] 326 try: 327 ret = requests.request('POST', PERFMEASUREMENTS_UPLOAD_URL, data={'tags': tags}, files={str(i):v for i, v in enumerate(files)}).json() 328 result = bool(ret['success']) 329 finally: 330 for each in files: 331 each[1].close() 332 333 if not result: 334 raise RuntimeError('failed to upload') 335 finally: 336 if not saveDir: 337 shutil.rmtree(tempDir) 338 339 def GetAggregatedStats(self, statsFilter): 340 aggregatedStats = {} 341 for statistic in self._statistics: 342 if statistic['name'] in statsFilter and len(statistic['perFrame']): 343 mean = sum(statistic['perFrame']) / len(statistic['perFrame']) 344 aggregatedStats[statistic['name']] = mean 345 346 return aggregatedStats