Commit fde4fdf9 authored by Sami Kyostila's avatar Sami Kyostila

Add second batch JS benchmark and code generator

This patch adds a "Second batch JS" benchmark and a code generator for
creating synthetic Javascript content for the different variants of the
benchmark.

The benchmark flow is:

1. Load an initial page quickly. We also start a continuous animation
   which gauges the responsiveness of the main thread.
2. Start loading Javascript asynchronously.
3. When the loading completes, wait 1 second and click on a button
   which causes about 5% of the Javascript to execute.

The aim is to keep the page responsive during loading and minimize the
latency of the click handler. There are three variants with a small (15K),
medium (150K) and large (1.2M) amount of Javascript.

The generated Javascript consists of half top-level closures, half top-level
functions, each of which calls a varying number of inner functions to
perform arithmetic.

Note that currently the benchmark only measures the responsiveness of the
click handler, but eventually it will be able to track this over the entire
interaction flow (crbug.com/460206).

BUG=428350
R=rmcilroy@chromium.org

Committed: https://chromium.googlesource.com/chromium/src/+/b0a90f4b6c81fc65dd0c57ebc00b0ba6fe784b22

Review URL: https://codereview.chromium.org/1000203002

Cr-Commit-Position: refs/heads/master@{#321363}
parent 1f5eca31
...@@ -369,6 +369,35 @@ class SynchronizedScrollOffsetPage(ToughSchedulingCasesPage): ...@@ -369,6 +369,35 @@ class SynchronizedScrollOffsetPage(ToughSchedulingCasesPage):
interaction.End() interaction.End()
class SecondBatchJsPage(ToughSchedulingCasesPage):
"""Why: For testing dynamically loading a large batch of Javascript and
running a part of it in response to user input.
"""
def __init__(self, page_set, variant='medium'):
super(SecondBatchJsPage, self).__init__(
url='file://tough_scheduling_cases/second_batch_js.html?%s' % variant,
page_set=page_set)
def RunPageInteractions(self, action_runner):
# Do a dummy tap to warm up the synthetic tap code path.
action_runner.TapElement(selector='div[id="spinner"]')
# Begin the action immediately because we want the page to update smoothly
# even while resources are being loaded.
action_runner.WaitForJavaScriptCondition('window.__ready !== undefined')
interaction = action_runner.BeginGestureInteraction('LoadAction')
action_runner.ExecuteJavaScript('kickOffLoading()')
action_runner.WaitForJavaScriptCondition('window.__ready')
# Click one second after the resources have finished loading.
action_runner.Wait(1)
action_runner.TapElement(selector='input[id="run"]')
# Wait for the test to complete.
action_runner.WaitForJavaScriptCondition('window.__finished')
interaction.End()
class ToughSchedulingCasesPageSet(page_set_module.PageSet): class ToughSchedulingCasesPageSet(page_set_module.PageSet):
"""Tough scheduler latency test cases.""" """Tough scheduler latency test cases."""
...@@ -441,7 +470,7 @@ class ToughSchedulingCasesPageSet(page_set_module.PageSet): ...@@ -441,7 +470,7 @@ class ToughSchedulingCasesPageSet(page_set_module.PageSet):
slow_handler=True, slow_handler=True,
bounce=False, bounce=False,
page_set=self)) page_set=self))
# Why: Slow handler blocks scroll start until touch ACK timeout # Why: Slow handler blocks scroll start until touch ACK timeout
self.AddUserStory(EmptyTouchHandlerPage( self.AddUserStory(EmptyTouchHandlerPage(
name='desktop_slow_handler', name='desktop_slow_handler',
desktop=True, desktop=True,
...@@ -464,7 +493,7 @@ class ToughSchedulingCasesPageSet(page_set_module.PageSet): ...@@ -464,7 +493,7 @@ class ToughSchedulingCasesPageSet(page_set_module.PageSet):
slow_handler=True, slow_handler=True,
bounce=True, bounce=True,
page_set=self)) page_set=self))
# Why: Scroll bounce with slow handler on desktop, blocks only once until # Why: Scroll bounce with slow handler on desktop, blocks only once until
# ACK timeout. # ACK timeout.
self.AddUserStory(EmptyTouchHandlerPage( self.AddUserStory(EmptyTouchHandlerPage(
name='bounce_desktop_slow_handler', name='bounce_desktop_slow_handler',
...@@ -472,5 +501,9 @@ class ToughSchedulingCasesPageSet(page_set_module.PageSet): ...@@ -472,5 +501,9 @@ class ToughSchedulingCasesPageSet(page_set_module.PageSet):
slow_handler=True, slow_handler=True,
bounce=True, bounce=True,
page_set=self)) page_set=self))
# Why: For measuring the latency of scroll-synchronized effects. # Why: For measuring the latency of scroll-synchronized effects.
self.AddUserStory(SynchronizedScrollOffsetPage(page_set=self)) self.AddUserStory(SynchronizedScrollOffsetPage(page_set=self))
# Why: Test loading a large amount of Javascript.
self.AddUserStory(SecondBatchJsPage(page_set=self, variant='light'))
self.AddUserStory(SecondBatchJsPage(page_set=self, variant='medium'))
self.AddUserStory(SecondBatchJsPage(page_set=self, variant='heavy'))
#!/bin/sh
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
readonly LIGHT_OPTS="\
--closure-count=10 \
--function-count=10 \
--inner-function-count=10 \
--function-call-count=1 \
--closure-call-count=1 \
--inner-function-line-count=1 \
--loop-count=5"
readonly MEDIUM_OPTS="\
--closure-count=50 \
--function-count=50 \
--inner-function-count=20 \
--function-call-count=5 \
--closure-call-count=5 \
--inner-function-line-count=2 \
--loop-count=5"
readonly HEAVY_OPTS="\
--closure-count=200 \
--function-count=200 \
--inner-function-count=40 \
--function-call-count=10 \
--closure-call-count=10 \
--inner-function-line-count=3 \
--loop-count=5"
function generate {
local generator="./second_batch_js_generator.py"
cat << EOF
// Generated with $generator $@
//
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
EOF
$generator $@ | yui-compressor --type js;
}
generate $LIGHT_OPTS > second_batch_js_light.min.js
generate $MEDIUM_OPTS > second_batch_js_medium.min.js
generate $HEAVY_OPTS > second_batch_js_heavy.min.js
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Second batch JS</title>
</head>
<style>
#spinner {
width: 600px;
height: 10px;
border-right: black;
position: relative;
background: repeating-linear-gradient(
-45deg,
orange,
orange 21px,
yellow 21px,
yellow 42px
);
}
#spinner-container {
width: 300px;
height: 10px;
overflow: hidden;
border: solid thin darkorange;
border-radius: 4px;
margin-top: 50px;
}
.spinner-loaded #spinner {
background: repeating-linear-gradient(
-45deg,
steelblue,
steelblue 21px,
aqua 21px,
aqua 42px
);
}
#spinner-container.spinner-loaded {
border: solid thin steelblue;
}
input {
font-size: 150%;
width: 302px;
margin-top: 30px;
margin-bottom: 30px;
}
</style>
<center>
<div id="spinner-container">
<div id="spinner"></div>
</div>
<input id="load" type="button" value="Start loading" onclick="kickOffLoading()"></input>
<input id="run" style='display: none' type="button" value="Click me!" onclick="onRunClick()"></input>
<p id="results"></p>
<p>Note: running this test interactively may activate compositor
prioritization during loading, which may skew the results.</p>
</center>
<script>
// Flag that indicates the test is ready to begin.
window.__ready = false;
// Flag that indicates the test has finished executing.
window.__finished = false;
var results = document.getElementById('results');
function kickOffLoading() {
var loadButton = document.getElementById('load');
loadButton.disabled = true;
var variant =
location.search.length > 0 ? location.search.substr(1) : 'medium';
var script = document.createElement('script');
script.setAttribute('type', 'text/javascript')
script.setAttribute('src', 'second_batch_js_' + variant + '.min.js')
script.addEventListener('load', onLoadComplete);
document.body.appendChild(script);
}
function onLoadComplete() {
var loadButton = document.getElementById('load');
var runButton = document.getElementById('run');
loadButton.style.display = 'none';
runButton.style.display = 'block';
spinnerContainer.classList.add('spinner-loaded');
window.__ready = true;
}
function onRunClick() {
results.innerText = 'Your lucky number is: ' + main(1);
window.requestAnimationFrame(finishTest);
}
function finishTest() {
window.__finished = true;
}
// Perform main thread animation during the benchmark to gauge main thread
// responsiveness.
var spinner = document.getElementById('spinner');
var spinnerContainer = document.getElementById('spinner-container');
function animateSpinner(timestamp) {
var width = parseInt(window.getComputedStyle(spinnerContainer).width);
var x = -(timestamp / 8) % width;
spinner.style.left = x + 'px';
window.requestAnimationFrame(animateSpinner);
}
window.requestAnimationFrame(animateSpinner);
</script>
</html>
#!/usr/bin/env python
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from __future__ import print_function
import argparse
import StringIO
import random
import sys
import zlib
# Generates synthetic Javascript for measuring the speed of parsing,
# compilation and initial execution.
# - top-level closure count
# - top-level function count
# - inner function count
# - size of code in inner functions
# - number of closures to call
# - number of top-level functions to call
# - loop count
def _ParseArguments():
parser = argparse.ArgumentParser(
description='Synthetic Javascript generator')
parser.add_argument('--closure-count', metavar='N', type=int, default=1,
help='Number of top-level closures to generate')
parser.add_argument('--function-count', metavar='N', type=int, default=1,
help='Number of top-level functions to generate')
parser.add_argument('--inner-function-count', metavar='N', type=int,
default=1, help='Number of inner functions to generate')
parser.add_argument('--inner-function-line-count', metavar='N', type=int,
default=1, help='Lines of code in each inner function')
parser.add_argument('--closure-call-count', metavar='N', type=int,
default=1, help='Number of top-level closures to call')
parser.add_argument('--function-call-count', metavar='N', type=int,
default=1, help='Number of top-level functions to call')
parser.add_argument('--loop-count', metavar='N', type=int,
default=1, help='Number of top-level loop iterations')
return parser.parse_args()
def _CreateRandomGeneratorForKey(key):
return random.Random(zlib.crc32(key))
def _GenerateLeafFunction(out, name, line_count, indent=0):
operations = [
'value += 1',
'value -= 2',
'value *= 3',
'value /= 4',
'value = Math.sin(value)',
'value = Math.pow(value, 2)',
]
indent = ' ' * indent
rand = _CreateRandomGeneratorForKey(name)
print(indent + 'function %s(value) {' % name, file=out)
for _ in xrange(line_count):
print(indent + ' %s;' % rand.choice(operations), file=out)
print(indent + ' return value;', file=out)
print(indent + '}\n', file=out)
def _ClosureInnerFunctionName(closure_index, inner_index):
return 'closure%dInnerFunction%d' % (closure_index, inner_index)
def _TopLevelClosureEntryPoint(closure_index):
return 'closure%d' % (closure_index)
def _GenerateTopLevelClosures(
out, count, inner_function_count, inner_function_line_count):
for closure_index in xrange(count):
print('(function() { // closure %d' % closure_index, file=out)
for inner_index in xrange(inner_function_count):
_GenerateLeafFunction(
out,
_ClosureInnerFunctionName(closure_index, inner_index),
inner_function_line_count,
indent=1)
print('window.%s = function(value) {' %
_TopLevelClosureEntryPoint(closure_index), file=out)
for inner_index in xrange(inner_function_count):
print(' value = %s(value);' %
_ClosureInnerFunctionName(closure_index, inner_index), file=out)
print(' return value;', file=out)
print('}', file=out)
print('})(); // closure %d\n' % closure_index, file=out)
def _FunctionInnerFunctionName(function_index, inner_index):
return 'function%dInnerFunction%d' % (function_index, inner_index)
def _TopLevelFunctionEntryPoint(function_index):
return 'function%d' % (function_index)
def _GenerateTopLevelFunctions(
out, count, inner_function_count, inner_function_line_count):
for function_index in xrange(count):
for inner_index in xrange(inner_function_count / 2):
_GenerateLeafFunction(
out,
_FunctionInnerFunctionName(function_index, inner_index),
inner_function_line_count)
print('function %s(value) {' %
_TopLevelFunctionEntryPoint(function_index), file=out)
for inner_index in xrange(inner_function_count / 2, inner_function_count):
_GenerateLeafFunction(
out,
_FunctionInnerFunctionName(function_index, inner_index),
inner_function_line_count,
indent=1)
for inner_index in xrange(inner_function_count):
print(' value = %s(value);' %
_FunctionInnerFunctionName(function_index, inner_index), file=out)
print(' return value;', file=out)
print('}\n', file=out)
def _GenerateMain(out, loop_count, closure_call_count, function_call_count):
print('function main(value) {', file=out)
for _ in xrange(loop_count):
for i in xrange(closure_call_count):
print(' value = %s(value);' % _TopLevelClosureEntryPoint(i), file=out)
for i in xrange(function_call_count):
print(' value = %s(value);' % _TopLevelFunctionEntryPoint(i), file=out)
print(' return value;', file=out)
print('}\n', file=out)
def Main():
args = _ParseArguments()
out = StringIO.StringIO()
print('// WARNING: Generated source code. Do not edit.', file=out)
print('//', file=out)
print('// This file was generated with the following options:', file=out)
print('// %s' % ' '.join(sys.argv), file=out)
print(file=out)
_GenerateTopLevelClosures(
out,
args.closure_count,
args.inner_function_count,
args.inner_function_line_count)
_GenerateTopLevelFunctions(
out,
args.function_count,
args.inner_function_count,
args.inner_function_line_count)
_GenerateMain(
out,
args.loop_count,
args.closure_call_count,
args.function_call_count)
print(out.getvalue())
if __name__ == '__main__':
sys.exit(Main())
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment