Home | History | Annotate | Download | only in media
      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