1 #!/usr/bin/env python 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 3 # Use of this source code is governed by a BSD-style license that can be 4 # found in the LICENSE file. 5 6 """Jerkiness performance test for video playback. 7 8 Uses jerky tool, (http://go/jerky), to record a jerkiness metric for videos 9 sensitive to jerkiness. 10 11 Jerkiness is defined as a percentage of the average on screen frame time by the 12 formula below. Where smoothed_frame_time[i] represents a frame's on screen time 13 plus amortized measurement gap error (time taken to capture each frame). 14 15 sqrt(average((avg_frame_time - smoothed_frame_time[i])^2, i=m..n)) 16 ------------------------------------------------------------------ 17 avg_frame_time 18 19 Currently, only the Linux binaries are checked in for this test since we only 20 have a Linux performance bot. The current binary is a custom build with some 21 fixes from veganjerky (http://go/veganjerky) for timing, waiting, and stdout 22 flushing. 23 24 TODO(dalecurtis): Move Jerky tool sources into the Chromium tree. 25 26 TODO(dalecurtis): Jerky tool uses a questionable method for determining torn 27 frames, determine if it is actually worth recording. 28 """ 29 30 import glob 31 import logging 32 import os 33 import re 34 import subprocess 35 import tempfile 36 37 import pyauto_media 38 import pyauto 39 import pyauto_utils 40 41 # HTML test path; relative to src/chrome/test/data. 42 _TEST_HTML_PATH = os.path.join('media', 'html', 'media_jerky.html') 43 44 # Path under data path for test files. 45 _TEST_MEDIA_PATH = os.path.join('pyauto_private', 'media', 'birds') 46 47 # Path to Jerky tool executable. 48 _JERKY_PATH = os.path.join('pyauto_private', 'media', 'tools', 'jerky') 49 50 # Regular expression for extracting jerkiness percentage. Sample line: 51 # using 1:9 title 'test.log (35.36% jerky, 0 teared frames)' lw 2 with lines 52 _JERKY_LOG_REGEX = re.compile( 53 r'\((\d{0,3}\.?\d{0,2})% jerky, (\d+) teared frames\)') 54 55 # Regular expression for extracting computed fps. Sample line: 56 # INFO: 33797 us per frame => 29.6 fps. 57 _JERKY_LOG_FPS_REGEX = re.compile(r' => (\d+\.\d+) fps') 58 59 # Minimum and maximum number of iterations for each test. Due to timing issues 60 # the jerky tool may not always calculate the fps correctly. When invalid 61 # results are detected, the test is rerun up to the maxium # of times set below. 62 _JERKY_ITERATIONS_MIN = 3 63 _JERKY_ITERATIONS_MAX = 10 64 65 # The media files used for testing. Each entry represents a tuple of (filename, 66 # width, height, fps). The width and height are used to create a calibration 67 # pattern for jerky tool. The fps is used to ensure Jerky tool computed a valid 68 # result. 69 _TEST_VIDEOS = [('birds540.webm', 960, 540, 29.9)] 70 71 72 def GetTempFilename(): 73 """Returns an absolute path to an empty temp file.""" 74 f, path = tempfile.mkstemp(prefix='jerky_tmp') 75 os.close(f) 76 return path 77 78 79 class MediaJerkyPerfTest(pyauto.PyUITest): 80 """PyAuto test container. See file doc string for more information.""" 81 82 def StartJerkyCapture(self): 83 """Starts jerky tool in capture mode and waits until its ready to capture. 84 85 Returns: 86 A tuple of the jerky process and an absolute path to the capture log. 87 """ 88 jerky_log = GetTempFilename() 89 logging.debug('Logging data to %s', jerky_log) 90 process = subprocess.Popen( 91 [os.path.join(self.DataDir(), _JERKY_PATH), 92 'capture', '--log', jerky_log], 93 stdout=subprocess.PIPE) 94 95 # Set the jerky tool process to soft-realtime w/ round-robin scheduling. 96 subprocess.check_call(['sudo', 'chrt', '-r', '-p', str(process.pid)]) 97 98 # Wait for server to start up. 99 line = True 100 while line: 101 line = process.stdout.readline() 102 if 'Waiting for calibration pattern to disappear' in line: 103 return process, jerky_log 104 self.fail('Failed to launch Jerky tool.') 105 106 def AnalyzeJerkyCapture(self, jerky_log): 107 """Run jerky analyze on the specified log and return various metrics. 108 109 Once analyze has completed, the jerky_log and associated outputs will be 110 removed. 111 112 Args: 113 jerky_log: Absolute path to the capture log. 114 115 Returns: 116 Tuple of fps, jerkiness, and torn frames. 117 """ 118 results_log_base = GetTempFilename() 119 process = subprocess.Popen( 120 [os.path.join(self.DataDir(), _JERKY_PATH), 121 'analyze', '--ref', jerky_log, '--out', results_log_base], 122 stdout=subprocess.PIPE) 123 124 # Wait for process to end w/o risking deadlock. 125 stdout = process.communicate()[0] 126 self.assertEquals(process.returncode, 0) 127 128 # Scrape out the calculated FPS. 129 fps_match = None 130 for line in stdout.splitlines(): 131 fps_match = _JERKY_LOG_FPS_REGEX.search(line) 132 if fps_match: 133 break 134 135 # Open *.error.gnuplot and scrape out jerkiness. 136 jerky_match = None 137 with open('%s.error.gnuplot' % results_log_base) as results: 138 for line in results: 139 jerky_match = _JERKY_LOG_REGEX.search(line) 140 if jerky_match: 141 break 142 143 # Cleanup all the temp and results files jerky spits out. 144 for log in glob.glob('%s*' % results_log_base) + [jerky_log]: 145 os.unlink(log) 146 147 if fps_match and jerky_match: 148 return (float(fps_match.group(1)), float(jerky_match.group(1)), 149 int(jerky_match.group(2))) 150 return None, None, None 151 152 def testMediaJerkyPerformance(self): 153 """Launches Jerky tool and records jerkiness for HTML5 videos. 154 155 For each video, the test starts up jerky tool then plays until the Jerky 156 tool collects enough information. Next the capture log is analyzed using 157 Jerky's analyze command. If the computed fps matches the expected fps the 158 jerkiness metric is recorded. 159 160 The test will run up to _JERKY_ITERATIONS_MAX times in an attempt to get at 161 least _JERKY_ITERATIONS_MIN valid values. The results are recorded under 162 the 'jerkiness' variable for graphing on the bots. 163 """ 164 self.NavigateToURL(self.GetFileURLForDataPath(_TEST_HTML_PATH)) 165 166 # Xvfb on the bots is restricted to 1024x768 at present. Ensure we're using 167 # all of the real estate we can. Jerky tool needs a clear picture of every 168 # frame, so we can't clip the video in any way. 169 self.SetWindowDimensions(0, 0, 1024, 768) 170 171 for name, width, height, expected_fps in _TEST_VIDEOS: 172 jerkiness = [] 173 torn_frames = [] 174 file_url = self.GetFileURLForDataPath( 175 os.path.join(_TEST_MEDIA_PATH, name)) 176 177 # Initialize the calibration area for Jerky tool. 178 self.assertTrue(self.ExecuteJavascript( 179 'initializeTest(%d, %d);' % (width, height))) 180 181 runs_left = _JERKY_ITERATIONS_MIN 182 runs_total = 0 183 while runs_left > 0 and runs_total < _JERKY_ITERATIONS_MAX: 184 runs_total += 1 185 logging.info('Running Jerky perf test #%d for %s.', runs_total, name) 186 187 # Startup Jerky tool in capture mode. 188 jerky_process, jerky_log = self.StartJerkyCapture() 189 190 # Start playback of the test video. 191 self.assertTrue(self.ExecuteJavascript("startTest('%s');" % file_url)) 192 193 # Wait for jerky tool to finish if it hasn't already. 194 self.assertTrue(jerky_process.wait() == 0) 195 196 # Stop playback of the test video so the next run can cleanly find the 197 # calibration zone. 198 self.assertTrue(self.ExecuteJavascript('stopTest();')) 199 200 # Analyze the results. 201 jerky_fps, jerky_percent, jerky_torn_frames = self.AnalyzeJerkyCapture( 202 jerky_log) 203 if (jerky_fps is None or jerky_percent is None or 204 jerky_torn_frames is None): 205 logging.error('No metrics recorded for this run.') 206 continue 207 208 # Ensure the results for this run are valid. 209 if jerky_fps != expected_fps: 210 logging.error( 211 'Invalid fps detected (actual: %f, expected: %f, jerkiness: %f). ' 212 'Discarding results for this run.', jerky_fps, expected_fps, 213 jerky_percent) 214 continue 215 216 jerkiness.append(jerky_percent) 217 torn_frames.append(jerky_torn_frames) 218 runs_left -= 1 219 220 pyauto_utils.PrintPerfResult('jerkiness', name, jerkiness, '%') 221 222 223 if __name__ == '__main__': 224 pyauto_media.Main() 225