blob: f21f52329ffd544c50dcc751b64c345e7832807b [file] [log] [blame]
# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""This is a client side WebGL aquarium test.
Description of some of the test result output:
- interframe time: The time elapsed between two frames. It is the elapsed
time between two consecutive calls to the render() function.
- render time: The time it takes in Javascript to construct a frame and
submit all the GL commands. It is the time it takes for a render()
function call to complete.
"""
import functools
import logging
import math
import os
import sampler
import system_sampler
import threading
import time
from autotest_lib.client.bin import fps_meter
from autotest_lib.client.bin import utils
from autotest_lib.client.common_lib import error
from autotest_lib.client.common_lib.cros import chrome
from autotest_lib.client.common_lib.cros import memory_eater
from autotest_lib.client.cros.graphics import graphics_utils
from autotest_lib.client.cros import service_stopper
from autotest_lib.client.cros.power import power_rapl, power_status, power_utils
# Minimum battery charge percentage to run the test
BATTERY_INITIAL_CHARGED_MIN = 10
# Measurement duration in seconds.
MEASUREMENT_DURATION = 30
POWER_DESCRIPTION = 'avg_energy_rate_1000_fishes'
# Time to exclude from calculation after playing a webgl demo [seconds].
STABILIZATION_DURATION = 10
class graphics_WebGLAquarium(graphics_utils.GraphicsTest):
"""WebGL aquarium graphics test."""
version = 1
_backlight = None
_power_status = None
_service_stopper = None
_test_power = False
active_tab = None
flip_stats = {}
kernel_sampler = None
perf_keyval = {}
sampler_lock = None
test_duration_secs = 30
test_setting_num_fishes = 50
test_settings = {
50: ('setSetting2', 2),
1000: ('setSetting6', 6),
}
def setup(self):
"""Testcase setup."""
tarball_path = os.path.join(self.bindir,
'webgl_aquarium_static.tar.bz2')
utils.extract_tarball_to_dir(tarball_path, self.srcdir)
def initialize(self):
"""Testcase initialization."""
super(graphics_WebGLAquarium, self).initialize()
self.sampler_lock = threading.Lock()
# TODO: Create samplers for other platforms (e.g. x86).
if utils.get_board().lower() in ['daisy', 'daisy_spring']:
# Enable ExynosSampler on Exynos platforms. The sampler looks for
# exynos-drm page flip states: 'wait_kds', 'rendered', 'prepared',
# and 'flipped' in kernel debugfs.
# Sample 3-second durtaion for every 5 seconds.
self.kernel_sampler = sampler.ExynosSampler(period=5, duration=3)
self.kernel_sampler.sampler_callback = self.exynos_sampler_callback
self.kernel_sampler.output_flip_stats = (
self.exynos_output_flip_stats)
def cleanup(self):
"""Testcase cleanup."""
if self._backlight:
self._backlight.restore()
if self._service_stopper:
self._service_stopper.restore_services()
super(graphics_WebGLAquarium, self).cleanup()
def setup_webpage(self, browser, test_url, num_fishes):
"""Open fish tank in a new tab.
@param browser: The Browser object to run the test with.
@param test_url: The URL to the aquarium test site.
@param num_fishes: The number of fishes to run the test with.
"""
# Create tab and load page. Set the number of fishes when page is fully
# loaded.
tab = browser.tabs.New()
tab.Navigate(test_url)
tab.Activate()
self.active_tab = tab
tab.WaitForDocumentReadyStateToBeComplete()
# Set the number of fishes when document finishes loading. Also reset
# our own FPS counter and start recording FPS and rendering time.
utils.wait_for_value(
lambda: tab.EvaluateJavaScript(
'if (document.readyState === "complete") {'
' setSetting(document.getElementById("%s"), %d);'
' g_crosFpsCounter.reset();'
' true;'
'} else {'
' false;'
'}' % self.test_settings[num_fishes]
),
expected_value=True,
timeout_sec=30)
return tab
def tear_down_webpage(self):
"""Close the tab containing testing webpage."""
# Do not close the tab when the sampler_callback is
# doing its work.
with self.sampler_lock:
self.active_tab.Close()
self.active_tab = None
def run_fish_test(self, browser, test_url, num_fishes, perf_log=True):
"""Run the test with the given number of fishes.
@param browser: The Browser object to run the test with.
@param test_url: The URL to the aquarium test site.
@param num_fishes: The number of fishes to run the test with.
@param perf_log: Report perf data only if it's set to True.
"""
tab = self.setup_webpage(browser, test_url, num_fishes)
if self.kernel_sampler:
self.kernel_sampler.start_sampling_thread()
time.sleep(self.test_duration_secs)
if self.kernel_sampler:
self.kernel_sampler.stop_sampling_thread()
self.kernel_sampler.output_flip_stats('flip_stats_%d' % num_fishes)
self.flip_stats = {}
# Get average FPS and rendering time, then close the tab.
avg_fps = tab.EvaluateJavaScript('g_crosFpsCounter.getAvgFps();')
if math.isnan(float(avg_fps)):
raise error.TestFail('Failed: Could not get FPS count.')
avg_interframe_time = tab.EvaluateJavaScript(
'g_crosFpsCounter.getAvgInterFrameTime();')
avg_render_time = tab.EvaluateJavaScript(
'g_crosFpsCounter.getAvgRenderTime();')
std_interframe_time = tab.EvaluateJavaScript(
'g_crosFpsCounter.getStdInterFrameTime();')
std_render_time = tab.EvaluateJavaScript(
'g_crosFpsCounter.getStdRenderTime();')
self.perf_keyval['avg_fps_%04d_fishes' % num_fishes] = avg_fps
self.perf_keyval['avg_interframe_time_%04d_fishes' % num_fishes] = (
avg_interframe_time)
self.perf_keyval['avg_render_time_%04d_fishes' % num_fishes] = (
avg_render_time)
self.perf_keyval['std_interframe_time_%04d_fishes' % num_fishes] = (
std_interframe_time)
self.perf_keyval['std_render_time_%04d_fishes' % num_fishes] = (
std_render_time)
logging.info('%d fish(es): Average FPS = %f, '
'average render time = %f', num_fishes, avg_fps,
avg_render_time)
if perf_log:
# Report frames per second to chromeperf/ dashboard.
self.output_perf_value(
description='avg_fps_%04d_fishes' % num_fishes,
value=avg_fps,
units='fps',
higher_is_better=True)
# Intel only: Record the power consumption for the next few seconds.
rapl_rate = power_rapl.get_rapl_measurement(
'rapl_%04d_fishes' % num_fishes)
# Remove entries that we don't care about.
rapl_rate = {key: rapl_rate[key]
for key in rapl_rate.keys() if key.endswith('pwr')}
# Report to chromeperf/ dashboard.
for key, values in rapl_rate.iteritems():
self.output_perf_value(
description=key,
value=values,
units='W',
higher_is_better=False,
graph='rapl_power_consumption'
)
def run_power_test(self, browser, test_url, ac_ok):
"""Runs the webgl power consumption test and reports the perf results.
@param browser: The Browser object to run the test with.
@param test_url: The URL to the aquarium test site.
@param ac_ok: Boolean on whether its ok to have AC power supplied.
"""
self._backlight = power_utils.Backlight()
self._backlight.set_default()
self._service_stopper = service_stopper.ServiceStopper(
service_stopper.ServiceStopper.POWER_DRAW_SERVICES)
self._service_stopper.stop_services()
if not ac_ok:
self._power_status = power_status.get_status()
# Verify that we are running on battery and the battery is
# sufficiently charged.
self._power_status.assert_battery_state(BATTERY_INITIAL_CHARGED_MIN)
measurements = [
power_status.SystemPower(self._power_status.battery_path)
]
def get_power():
power_logger = power_status.PowerLogger(measurements)
power_logger.start()
time.sleep(STABILIZATION_DURATION)
start_time = time.time()
time.sleep(MEASUREMENT_DURATION)
power_logger.checkpoint('result', start_time)
keyval = power_logger.calc()
logging.info('Power output %s', keyval)
return keyval['result_' + measurements[0].domain + '_pwr']
self.run_fish_test(browser, test_url, 1000, perf_log=False)
if not ac_ok:
energy_rate = get_power()
# This is a power specific test so we are not capturing
# avg_fps and avg_render_time in this test.
self.perf_keyval[POWER_DESCRIPTION] = energy_rate
self.output_perf_value(
description=POWER_DESCRIPTION,
value=energy_rate,
units='W',
higher_is_better=False)
def exynos_sampler_callback(self, sampler_obj):
"""Sampler callback function for ExynosSampler.
@param sampler_obj: The ExynosSampler object that invokes this callback
function.
"""
if sampler_obj.stopped:
return
with self.sampler_lock:
now = time.time()
results = {}
info_str = ['\nfb_id wait_kds flipped']
for value in sampler_obj.frame_buffers.itervalues():
results[value.fb] = {}
for state, stats in value.states.iteritems():
results[value.fb][state] = (stats.avg, stats.stdev)
info_str.append('%s: %s %s' % (value.fb,
results[value.fb]['wait_kds'][0],
results[value.fb]['flipped'][0]))
results['avg_fps'] = self.active_tab.EvaluateJavaScript(
'g_crosFpsCounter.getAvgFps();')
results['avg_render_time'] = self.active_tab.EvaluateJavaScript(
'g_crosFpsCounter.getAvgRenderTime();')
self.active_tab.ExecuteJavaScript('g_crosFpsCounter.reset();')
info_str.append('avg_fps: %s, avg_render_time: %s' %
(results['avg_fps'], results['avg_render_time']))
self.flip_stats[now] = results
logging.info('\n'.join(info_str))
def exynos_output_flip_stats(self, file_name):
"""Pageflip statistics output function for ExynosSampler.
@param file_name: The output file name.
"""
# output format:
# time fb_id avg_rendered avg_prepared avg_wait_kds avg_flipped
# std_rendered std_prepared std_wait_kds std_flipped
with open(file_name, 'w') as f:
for t in sorted(self.flip_stats.keys()):
if ('avg_fps' in self.flip_stats[t] and
'avg_render_time' in self.flip_stats[t]):
f.write('%s %s %s\n' %
(t, self.flip_stats[t]['avg_fps'],
self.flip_stats[t]['avg_render_time']))
for fb, stats in self.flip_stats[t].iteritems():
if not isinstance(fb, int):
continue
f.write('%s %s ' % (t, fb))
f.write('%s %s %s %s ' % (stats['rendered'][0],
stats['prepared'][0],
stats['wait_kds'][0],
stats['flipped'][0]))
f.write('%s %s %s %s\n' % (stats['rendered'][1],
stats['prepared'][1],
stats['wait_kds'][1],
stats['flipped'][1]))
def write_samples(self, samples, filename):
"""Writes all samples to result dir with the file name "samples'.
@param samples: A list of all collected samples.
@param filename: The file name to save under result directory.
"""
out_file = os.path.join(self.resultsdir, filename)
with open(out_file, 'w') as f:
for sample in samples:
print >> f, sample
def run_fish_test_with_memory_pressure(
self, browser, test_url, num_fishes, memory_pressure):
"""Measure fps under memory pressure.
It measure FPS of WebGL aquarium while adding memory pressure. It runs
in 2 phases:
1. Allocate non-swappable memory until |memory_to_reserve_mb| is
remained. The memory is not accessed after allocated.
2. Run "active" memory consumer in the background. After allocated,
Its content is accessed sequentially by page and looped around
infinitely.
The second phase is opeared in two possible modes:
1. "single" mode, which means only one "active" memory consumer. After
running a single memory consumer with a given memory size, it waits
for a while to see if system can afford current memory pressure
(definition here is FPS > 5). If it does, kill current consumer and
launch another consumer with a larger memory size. The process keeps
going until system couldn't afford the load.
2. "multiple"mode. It simply launch memory consumers with a given size
one by one until system couldn't afford the load (e.g., FPS < 5).
In "single" mode, CPU load is lighter so we expect swap in/swap out
rate to be correlated to FPS better. In "multiple" mode, since there
are multiple busy loop processes, CPU pressure is another significant
cause of frame drop. Frame drop can happen easily due to busy CPU
instead of memory pressure.
@param browser: The Browser object to run the test with.
@param test_url: The URL to the aquarium test site.
@param num_fishes: The number of fishes to run the test with.
@param memory_pressure: Memory pressure parameters.
"""
consumer_mode = memory_pressure.get('consumer_mode', 'single')
memory_to_reserve_mb = memory_pressure.get('memory_to_reserve_mb', 500)
# Empirical number to quickly produce memory pressure.
if consumer_mode == 'single':
default_consumer_size_mb = memory_to_reserve_mb + 100
else:
default_consumer_size_mb = memory_to_reserve_mb / 2
consumer_size_mb = memory_pressure.get(
'consumer_size_mb', default_consumer_size_mb)
# Setup fish tank.
self.setup_webpage(browser, test_url, num_fishes)
# Drop all file caches.
utils.drop_caches()
def fps_near_zero(fps_sampler):
"""Returns whether recent fps goes down to near 0.
@param fps_sampler: A system_sampler.Sampler object.
"""
last_fps = fps_sampler.get_last_avg_fps(6)
if last_fps:
logging.info('last fps %f', last_fps)
if last_fps <= 5:
return True
return False
max_allocated_mb = 0
# Consume free memory and release them by the end.
with memory_eater.consume_free_memory(memory_to_reserve_mb):
fps_sampler = system_sampler.SystemSampler(
memory_eater.MemoryEater.get_active_consumer_pids)
end_condition = functools.partial(fps_near_zero, fps_sampler)
with fps_meter.FPSMeter(fps_sampler.sample):
# Collects some samples before running memory pressure.
time.sleep(5)
try:
if consumer_mode == 'single':
# A single run couldn't generate samples representative
# enough.
# First runs squeeze more inactive anonymous memory into
# zram so in later runs we have a more stable memory
# stat.
max_allocated_mb = max(
memory_eater.run_single_memory_pressure(
consumer_size_mb, 100, end_condition, 10, 3,
900),
memory_eater.run_single_memory_pressure(
consumer_size_mb, 20, end_condition, 10, 3,
900),
memory_eater.run_single_memory_pressure(
consumer_size_mb, 10, end_condition, 10, 3,
900))
elif consumer_mode == 'multiple':
max_allocated_mb = (
memory_eater.run_multi_memory_pressure(
consumer_size_mb, end_condition, 10, 900))
else:
raise error.TestFail(
'Failed: Unsupported consumer mode.')
except memory_eater.TimeoutException as e:
raise error.TestFail(e)
samples = fps_sampler.get_samples()
self.write_samples(samples, 'memory_pressure_fps_samples.txt')
self.perf_keyval['num_samples'] = len(samples)
self.perf_keyval['max_allocated_mb'] = max_allocated_mb
logging.info(self.perf_keyval)
self.output_perf_value(
description='max_allocated_mb_%d_fishes_reserved_%d_mb' % (
num_fishes, memory_to_reserve_mb),
value=max_allocated_mb,
units='MB',
higher_is_better=True)
@graphics_utils.GraphicsTest.failure_report_decorator('graphics_WebGLAquarium')
def run_once(self,
test_duration_secs=30,
test_setting_num_fishes=(50, 1000),
power_test=False,
ac_ok=False,
memory_pressure=None):
"""Find a browser with telemetry, and run the test.
@param test_duration_secs: The duration in seconds to run each scenario
for.
@param test_setting_num_fishes: A list of the numbers of fishes to
enable in the test.
@param power_test: Boolean on whether to run power_test
@param ac_ok: Boolean on whether its ok to have AC power supplied.
@param memory_pressure: A dictionay which specifies memory pressure
parameters:
'consumer_mode': 'single' or 'multiple' to have one or moultiple
concurrent memory consumers.
'consumer_size_mb': Amount of memory to allocate. In 'single'
mode, a single memory consumer would allocate memory by the
specific size. It then gradually allocates more memory until
FPS down to near 0. In 'multiple' mode, memory consumers of
this size would be spawn one by one until FPS down to near 0.
'memory_to_reserve_mb': Amount of memory to reserve before
running memory consumer. In practical we allocate mlocked
memory (i.e., not swappable) to consume free memory until this
amount of free memory remained.
"""
self.test_duration_secs = test_duration_secs
self.test_setting_num_fishes = test_setting_num_fishes
with chrome.Chrome(logged_in=False, init_network_controller=True) as cr:
cr.browser.platform.SetHTTPServerDirectories(self.srcdir)
test_url = cr.browser.platform.http_server.UrlOf(
os.path.join(self.srcdir, 'aquarium.html'))
if not utils.wait_for_idle_cpu(60.0, 0.1):
if not utils.wait_for_idle_cpu(20.0, 0.2):
raise error.TestFail('Failed: Could not get idle CPU.')
if not utils.wait_for_cool_machine():
raise error.TestFail('Failed: Could not get cold machine.')
if memory_pressure:
self.run_fish_test_with_memory_pressure(
cr.browser, test_url, num_fishes=1000,
memory_pressure=memory_pressure)
self.tear_down_webpage()
elif power_test:
self._test_power = True
self.run_power_test(cr.browser, test_url, ac_ok)
self.tear_down_webpage()
else:
for n in self.test_setting_num_fishes:
self.run_fish_test(cr.browser, test_url, n)
self.tear_down_webpage()
self.write_perf_keyval(self.perf_keyval)