import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

View file

@ -0,0 +1,20 @@
<?xml version="1.0"?>
<RDF:RDF xmlns:NC="http://home.netscape.com/NC-rdf#"
xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<RDF:Description RDF:about="chrome://browser/content/browser.xul#main-window"
width="1024"
height="768"
sizemode="normal"
screenX="0"
screenY="0" />
<RDF:Description RDF:about="chrome://browser/content/browser.xul">
<NC:persist RDF:resource="chrome://browser/content/browser.xul#main-window"/>
<NC:persist RDF:resource="chrome://browser/content/browser.xul#toolbar-menubar"/>
<NC:persist RDF:resource="chrome://browser/content/browser.xul#sidebar-box"/>
<NC:persist RDF:resource="chrome://browser/content/browser.xul#sidebar-title"/>
</RDF:Description>
<RDF:Description RDF:about="chrome://browser/content/browser.xul#sidebar-box"
sidebarcommand=""
width=""
src="" />
</RDF:RDF>

Binary file not shown.

View file

@ -0,0 +1,11 @@
// This file is needed to work around a Firefox bug where capability.principal
// prefs in user.js don't get recognized until the second browser launch
// which is too late for our purposes of using quit.js. Loading the principals
// from prefs.js avoids this issue.
user_pref("capability.principal.codebase.p0.granted", "UniversalPreferencesWrite UniversalXPConnect UniversalPreferencesRead");
user_pref("capability.principal.codebase.p0.id", "file://");
user_pref("capability.principal.codebase.p0.subjectName", "");
user_pref("capability.principal.codebase.p1.granted", "UniversalPreferencesWrite UniversalXPConnect UniversalPreferencesRead");
user_pref("capability.principal.codebase.p1.id", "http://localhost");
user_pref("capability.principal.codebase.p1.subjectName", "");
user_pref("signed.applets.codebase_principal_support", true);

View file

@ -0,0 +1,93 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import mozinfo
import threading
class CounterManager(object):
counterDict = {}
def __init__(self):
self.allCounters = {}
self.registeredCounters = {}
def _loadCounters(self):
"""Loads all of the counters defined in the counterDict"""
for counter in self.counterDict.keys():
self.allCounters[counter] = self.counterDict[counter]
def registerCounters(self, counters):
"""Registers a list of counters that will be monitoring.
Only counters whose names are found in allCounters will be added
"""
for counter in counters:
if counter in self.allCounters:
self.registeredCounters[counter] = \
[self.allCounters[counter], []]
def getCounterValue(self, counterName):
"""Returns the last value of the counter 'counterName'"""
if mozinfo.os == 'linux':
from talos.cmanager_linux import LinuxCounterManager \
as DefaultCounterManager
elif mozinfo.os == 'win':
from talos.cmanager_win32 import WinCounterManager \
as DefaultCounterManager
else: # mac
from talos.cmanager_mac import MacCounterManager \
as DefaultCounterManager
class CounterManagement(object):
def __init__(self, process_name, counters, resolution):
"""
Public interface to manage counters.
On creation, create a thread to collect counters. Call :meth:`start`
to start collecting counters with that thread.
Be sure to call :meth:`stop` to stop the thread.
"""
assert counters
self._raw_counters = counters
self._process_name = process_name
self._counter_results = \
dict([(counter, []) for counter in self._raw_counters])
self._resolution = resolution
self._stop = threading.Event()
self._thread = threading.Thread(target=self._collect)
self._process = None
def _collect(self):
manager = DefaultCounterManager(self._process_name, self._process,
self._raw_counters)
while not self._stop.wait(self._resolution):
# Get the output from all the possible counters
for count_type in self._raw_counters:
val = manager.getCounterValue(count_type)
if val:
self._counter_results[count_type].append(val)
def start(self, process):
"""
start the counter management thread.
:param process: a psutil.Process instance representing the browser
process.
"""
self._process = process
self._thread.start()
def stop(self):
self._stop.set()
self._thread.join()
def results(self):
assert not self._thread.is_alive()
return self._counter_results

View file

@ -0,0 +1,181 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import re
import subprocess
from cmanager import CounterManager
def xrestop(binary='xrestop'):
"""
python front-end to running xrestop:
http://www.freedesktop.org/wiki/Software/xrestop
For each monitored process, `xrestop -m 1 -b` produces output like:
0 - Thunderbird ( PID: 2035 ):
res_base : 0x1600000
res_mask : 0x1fffff
windows : 69
GCs : 35
fonts : 1
pixmaps : 175
pictures : 272
glyphsets : 73
colormaps : 0
passive grabs : 0
cursors : 9
unknowns : 42
pixmap bytes : 4715737
other bytes : ~13024
total bytes : ~4728761
"""
process_regex = re.compile(r'([0-9]+) - (.*) \( PID: *(.*) *\):')
args = ['-m', '1', '-b']
command = [binary] + args
process = subprocess.Popen(command,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = process.communicate()
if process.returncode:
raise Exception(
"Unexpected error executing '%s':\n%s"
% (subprocess.list2cmdline(command), stdout)
)
# process output
retval = {}
index = name = pid = None
for line in stdout.strip().splitlines():
line = line.rstrip()
match = process_regex.match(line)
if match:
index, name, pid = match.groups()
try:
pid = int(pid)
except ValueError:
# ignore processes without PIDs
index = name = pid = None
continue
index = int(index)
retval[pid] = dict(index=index, name=name)
else:
if not pid:
continue
counter, value = line.split(':', 1)
counter = counter.strip()
value = value.strip()
retval[pid][counter] = value
return retval
def GetPrivateBytes(pids):
"""Calculate the amount of private, writeable memory allocated to a
process.
This code was adapted from 'pmap.c', part of the procps project.
"""
privateBytes = 0
for pid in pids:
mapfile = '/proc/%s/maps' % pid
with open(mapfile) as maps:
private = 0
for line in maps:
# split up
(range, line) = line.split(" ", 1)
(start, end) = range.split("-")
flags = line.split(" ", 1)[0]
size = int(end, 16) - int(start, 16)
if flags.find("p") >= 0:
if flags.find("w") >= 0:
private += size
privateBytes += private
return privateBytes
def GetResidentSize(pids):
"""Retrieve the current resident memory for a given process"""
# for some reason /proc/PID/stat doesn't give accurate information
# so we use status instead
RSS = 0
for pid in pids:
file = '/proc/%s/status' % pid
with open(file) as status:
for line in status:
if line.find("VmRSS") >= 0:
RSS += int(line.split()[1]) * 1024
return RSS
def GetXRes(pids):
"""Returns the total bytes used by X or raises an error if total bytes
is not available"""
XRes = 0
xres_output = xrestop()
for pid in pids:
if pid in xres_output:
data = xres_output[pid]['total bytes']
data = data.lstrip('~') # total bytes is like '~4728761'
try:
data = float(data)
XRes += data
except ValueError:
print("Invalid data, not a float")
raise
else:
raise Exception("Could not find PID=%s in xrestop output" % pid)
return XRes
class LinuxCounterManager(CounterManager):
"""This class manages the monitoring of a process with any number of
counters.
A counter can be any function that takes an argument of one pid and
returns a piece of data about that process.
Some examples are: CalcCPUTime, GetResidentSize, and GetPrivateBytes
"""
counterDict = {"Private Bytes": GetPrivateBytes,
"RSS": GetResidentSize,
"XRes": GetXRes}
def __init__(self, process_name, process, counters):
"""Args:
counters: A list of counters to monitor. Any counters whose name
does not match a key in 'counterDict' will be ignored.
"""
CounterManager.__init__(self)
self.process = process
self._loadCounters()
self.registerCounters(counters)
def getCounterValue(self, counterName):
"""Returns the last value of the counter 'counterName'"""
try:
return self.registeredCounters[counterName][0](self.pidList())
except:
return None
def pidList(self):
"""Updates the list of PIDs we're interested in"""
try:
return [self.process.pid] + [child.pid
for child in self.process.children()]
except:
print("WARNING: problem updating child PID's")

View file

@ -0,0 +1,86 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""CounterManager for Mac OSX"""
import subprocess
from cmanager import CounterManager
import sys
def GetProcessData(pid):
"""Runs a ps on the process identified by pid and returns the output line
as a list (pid, vsz, rss)
"""
command = ['ps -o pid,vsize,rss -p'+str(pid)]
try:
handle = subprocess.Popen(command, stdout=subprocess.PIPE,
universal_newlines=True, shell=True)
handle.wait()
data = handle.stdout.readlines()
except:
print("Unexpected error executing '%s': %s", (command, sys.exc_info()))
raise
# First line is header output should look like:
# PID VSZ RSS
# 3210 75964 920
line = data[1]
line = line.split()
if line[0] == str(pid):
return line
def GetPrivateBytes(pid):
"""Calculate the amount of private, writeable memory allocated to a
process.
"""
psData = GetProcessData(pid)
return int(psData[1]) * 1024 # convert to bytes
def GetResidentSize(pid):
"""Retrieve the current resident memory for a given process"""
psData = GetProcessData(pid)
return int(psData[2]) * 1024 # convert to bytes
class MacCounterManager(CounterManager):
"""This class manages the monitoring of a process with any number of
counters.
A counter can be any function that takes an argument of one pid and
returns a piece of data about that process.
Some examples are: CalcCPUTime, GetResidentSize, and GetPrivateBytes
"""
counterDict = {"Private Bytes": GetPrivateBytes,
"RSS": GetResidentSize}
def __init__(self, process_name, process, counters):
"""Args:
counters: A list of counters to monitor. Any counters whose name
does not match a key in 'counterDict' will be ignored.
"""
CounterManager.__init__(self)
# the last process is the useful one
self.pid = process.pid
self._loadCounters()
self.registerCounters(counters)
def getCounterValue(self, counterName):
"""Returns the last value of the counter 'counterName'"""
if counterName not in self.registeredCounters:
print("Warning: attempting to collect counter %s and it is not"
" registered" % counterName)
return
try:
return self.registeredCounters[counterName][0](self.pid)
except Exception as e:
print("Error in collecting counter: %s, pid: %s, exception: %s"
% (counterName, self.pid, e))

View file

@ -0,0 +1,235 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from cmanager import CounterManager
from ctypes import windll
from ctypes.wintypes import DWORD, HANDLE, LPSTR, LPCSTR, LPCWSTR, Structure, \
pointer, LONG
from ctypes import byref, create_string_buffer, memmove, Union, c_double, \
c_longlong
import struct
from utils import TalosError
pdh = windll.pdh
_LONGLONG = c_longlong
class _PDH_COUNTER_PATH_ELEMENTS_A(Structure):
_fields_ = [("szMachineName", LPSTR),
("szObjectName", LPSTR),
("szInstanceName", LPSTR),
("szParentInstance", LPSTR),
("dwInstanceIndex", DWORD),
("szCounterName", LPSTR)]
_PDH_MORE_DATA = -2147481646 # the need more space error
def _getExpandedCounterPaths(processName, counterName):
'''
Get list of expanded counter paths given a counter name. Returns a
list of strings or None, if no counter paths can be created
'''
pcchPathListLength = DWORD(0)
szWildCardPath = LPSTR('\\process(%s)\\%s' % (processName, counterName))
if pdh.PdhExpandCounterPathA(
szWildCardPath,
LPSTR(None),
pointer(pcchPathListLength)
) != _PDH_MORE_DATA:
return []
pathListLength = pcchPathListLength.value
szExpandedPathList = LPCSTR('\0' * pathListLength)
if pdh.PdhExpandCounterPathA(szWildCardPath, szExpandedPathList,
pointer(pcchPathListLength)) != 0:
return []
buffer = create_string_buffer(pcchPathListLength.value)
memmove(buffer, szExpandedPathList, pcchPathListLength.value)
paths = []
i = 0
path = ''
for j in range(0, pcchPathListLength.value):
c = struct.unpack_from('c', buffer, offset=j)[0]
if c == '\0':
if j == i:
# double null: we're done
break
paths.append(path)
path = ''
i = j + 1
else:
path += c
return paths
class _PDH_Counter_Union(Union):
_fields_ = [('longValue', LONG),
('doubleValue', c_double),
('largeValue', _LONGLONG),
('AnsiStringValue', LPCSTR),
('WideStringValue', LPCWSTR)]
class _PDH_FMT_COUNTERVALUE(Structure):
_fields_ = [('CStatus', DWORD),
('union', _PDH_Counter_Union)]
_PDH_FMT_LONG = 0x00000100
class WinCounterManager(CounterManager):
def __init__(self, process_name, process, counters,
childProcess="plugin-container"):
CounterManager.__init__(self)
self.childProcess = childProcess
self.registeredCounters = {}
self.registerCounters(counters)
# PDH might need to be "refreshed" if it has been queried while the
# browser is closed
pdh.PdhEnumObjectsA(None, None, 0, 1, 0, True)
for counter in self.registeredCounters:
try:
# Add the counter path for the default process.
self._addCounter(process_name, 'process', counter)
except TalosError:
# Assume that this is a memory counter for the system,
# not a process counter
# If we got an error that has nothing to do with that,
# the exception will almost certainly be re-raised
self._addCounter(process_name, 'Memory', counter)
self._updateCounterPathsForChildProcesses(counter)
def _addCounter(self, processName, counterType, counterName):
pCounterPathElements = _PDH_COUNTER_PATH_ELEMENTS_A(
LPSTR(None),
LPSTR(counterType),
LPSTR(processName),
LPSTR(None),
DWORD(-1),
LPSTR(counterName)
)
pcchbufferSize = DWORD(0)
# First run we just try to get the buffer size so we can allocate a
# string big enough to fill it
if pdh.PdhMakeCounterPathA(pointer(pCounterPathElements),
LPCSTR(0), pointer(pcchbufferSize),
DWORD(0)) != _PDH_MORE_DATA:
raise TalosError(
"Could not create counter path for counter %s for %s"
% (counterName, processName)
)
szFullPathBuffer = LPCSTR('\0'*pcchbufferSize.value)
# Then we run to get the actual value
if pdh.PdhMakeCounterPathA(pointer(pCounterPathElements),
szFullPathBuffer, pointer(pcchbufferSize),
DWORD(0)) != 0:
raise TalosError(
"Could not create counter path for counter %s for %s"
% (counterName, processName)
)
path = szFullPathBuffer.value
hq = HANDLE()
if pdh.PdhOpenQuery(None, None, byref(hq)) != 0:
raise TalosError("Could not open win32 counter query")
hc = HANDLE()
if pdh.PdhAddCounterA(hq, path, 0, byref(hc)) != 0:
raise TalosError("Could not add win32 counter %s" % path)
self.registeredCounters[counterName] = [hq, [(hc, path)]]
def registerCounters(self, counters):
# self.registeredCounters[counter][0] is a counter query handle
# self.registeredCounters[counter][1] is a list of tuples, the first
# member of which is a counter handle, the second a counter path
for counter in counters:
# Main_RSS is collected inside of pageloader
if counter.strip() == 'Main_RSS':
continue
# mainthread_io is collected from the browser via environment
# variables
if counter.strip() == 'mainthread_io':
continue
self.registeredCounters[counter] = []
def _updateCounterPathsForChildProcesses(self, counter):
# Create a counter path for each instance of the child process that
# is running. If any of these paths are not in our counter list,
# add them to our counter query and append them to the counter list,
# so that we'll begin tracking their statistics. We don't need to
# worry about removing invalid paths from the list, as
# getCounterValue() will generate a value of 0 for those.
hq = self.registeredCounters[counter][0]
oldCounterListLength = len(self.registeredCounters[counter][1])
pdh.PdhEnumObjectsA(None, None, 0, 1, 0, True)
expandedPaths = _getExpandedCounterPaths(self.childProcess, counter)
if not expandedPaths:
return
for expandedPath in expandedPaths:
alreadyInCounterList = False
for singleCounter in self.registeredCounters[counter][1]:
if expandedPath == singleCounter[1]:
alreadyInCounterList = True
if not alreadyInCounterList:
try:
newhc = HANDLE()
if pdh.PdhAddCounterA(hq, expandedPath, 0,
byref(newhc)) != 0:
raise TalosError(
"Could not add expanded win32 counter %s"
% expandedPath
)
self.registeredCounters[counter][1].append((newhc,
expandedPath))
except:
continue
if oldCounterListLength != len(self.registeredCounters[counter][1]):
pdh.PdhCollectQueryData(hq)
def getCounterValue(self, counter):
# Update counter paths, to catch any new child processes that might
# have been launched since last call. Then iterate through all
# counter paths for this counter, and return a combined value.
if counter not in self.registeredCounters:
return None
if self.registeredCounters[counter] == []:
return None
self._updateCounterPathsForChildProcesses(counter)
hq = self.registeredCounters[counter][0]
# we'll just ignore the return value here, in case no counters
# are valid anymore
pdh.PdhCollectQueryData(hq)
aggregateValue = 0
for singleCounter in self.registeredCounters[counter][1]:
hc = singleCounter[0]
dwType = DWORD(0)
value = _PDH_FMT_COUNTERVALUE()
# if we can't get a value, just assume a value of 0
if pdh.PdhGetFormattedCounterValue(hc, _PDH_FMT_LONG,
byref(dwType),
byref(value)) == 0:
aggregateValue += value.union.longValue
return aggregateValue

View file

@ -0,0 +1,153 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
import argparse
import os
from mozlog.commandline import add_logging_group
class _StopAction(argparse.Action):
def __init__(self, option_strings, dest=argparse.SUPPRESS,
default=argparse.SUPPRESS, help=None):
super(_StopAction, self).__init__(
option_strings=option_strings,
dest=dest,
default=default,
nargs=0,
help=help)
class _ListTests(_StopAction):
def __call__(self, parser, namespace, values, option_string=None):
from talos import test
print('Available tests:')
print('================\n')
test_class_names = [
(test_class.name(), test_class.description())
for test_class in test.test_dict().itervalues()
]
test_class_names.sort()
for name, description in test_class_names:
print(name)
print('-'*len(name))
print(description)
print # Appends a single blank line to the end
parser.exit()
class _ListSuite(_StopAction):
def __call__(self, parser, namespace, values, option_string=None):
from talos.config import suites_conf
print('Available suites:')
conf = suites_conf()
max_suite_name = max([len(s) for s in conf])
pattern = " %%-%ds (%%s)" % max_suite_name
for name in conf:
print(pattern % (name, ':'.join(conf[name]['tests'])))
print
parser.exit()
def create_parser(mach_interface=False):
parser = argparse.ArgumentParser()
add_arg = parser.add_argument
if not mach_interface:
add_arg('-e', '--executablePath', required=True, dest="browser_path",
help="path to executable we are testing")
add_arg('-t', '--title', default='qm-pxp01',
help="Title of the test run")
add_arg('--branchName', dest="branch_name", default='',
help="Name of the branch we are testing on")
add_arg('--browserWait', dest='browser_wait', default=5, type=int,
help="Amount of time allowed for the browser to cleanly close")
add_arg('-a', '--activeTests',
help="List of tests to run, separated by ':' (ex. damp:cart)")
add_arg('--suite',
help="Suite to use (instead of --activeTests)")
add_arg('--disable-e10s', dest='e10s',
action='store_false', default=True,
help="disable e10s")
add_arg('--noChrome', action='store_true',
help="do not run tests as chrome")
add_arg('--rss', action='store_true',
help="Collect RSS counters from pageloader instead of the"
" operating system")
add_arg('--mainthread', action='store_true',
help="Collect mainthread IO data from the browser by setting"
" an environment variable")
add_arg("--mozAfterPaint", action='store_true', dest="tpmozafterpaint",
help="wait for MozAfterPaint event before recording the time")
add_arg('--spsProfile', action="store_true", dest="sps_profile",
help="Profile the run and output the results in $MOZ_UPLOAD_DIR")
add_arg('--spsProfileInterval', dest='sps_profile_interval', type=int,
help="How frequently to take samples (ms)")
add_arg('--spsProfileEntries', dest="sps_profile_entries", type=int,
help="How many samples to take with the profiler")
add_arg('--extension', dest='extensions', action='append',
default=['${talos}/talos-powers/talos-powers-signed.xpi',
'${talos}/pageloader/pageloader-signed.xpi'],
help="Extension to install while running")
add_arg('--fast', action='store_true',
help="Run tp tests as tp_fast")
add_arg('--symbolsPath', dest='symbols_path',
help="Path to the symbols for the build we are testing")
add_arg('--xperf_path',
help="Path to windows performance tool xperf.exe")
add_arg('--test_timeout', type=int, default=1200,
help="Time to wait for the browser to output to the log file")
add_arg('--errorFile', dest='error_filename',
default=os.path.abspath('browser_failures.txt'),
help="Filename to store the errors found during the test."
" Currently used for xperf only.")
add_arg('--noShutdown', dest='shutdown', action='store_true',
help="Record time browser takes to shutdown after testing")
add_arg('--setpref', action='append', default=[], dest="extraPrefs",
metavar="PREF=VALUE",
help="defines an extra user preference")
add_arg('--webServer', dest='webserver',
help="DEPRECATED")
if not mach_interface:
add_arg('--develop', action='store_true', default=False,
help="useful for running tests on a developer machine."
" Doesn't upload to the graph servers.")
add_arg("--cycles", type=int,
help="number of browser cycles to run")
add_arg("--tpmanifest",
help="manifest file to test")
add_arg('--tpcycles', type=int,
help="number of pageloader cycles to run")
add_arg('--tptimeout', type=int,
help='number of milliseconds to wait for a load event after'
' calling loadURI before timing out')
add_arg('--tppagecycles', type=int,
help='number of pageloader cycles to run for each page in'
' the manifest')
add_arg('--tpdelay', type=int,
help="length of the pageloader delay")
add_arg('--sourcestamp',
help='Specify the hg revision or sourcestamp for the changeset'
' we are testing. This will use the value found in'
' application.ini if it is not specified.')
add_arg('--repository',
help='Specify the url for the repository we are testing. '
'This will use the value found in application.ini if'
' it is not specified.')
add_arg('--framework',
help='Will post to the specified framework for Perfherder. '
'Default "talos". Used primarily for experiments on '
'new platforms')
add_arg('--print-tests', action=_ListTests,
help="print available tests")
add_arg('--print-suites', action=_ListSuite,
help="list available suites")
add_logging_group(parser)
return parser
def parse_args(argv=None):
parser = create_parser()
return parser.parse_args(argv)

View file

@ -0,0 +1,472 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
import sys
import os
import copy
from mozlog.commandline import setup_logging
from talos import utils, test
from talos.cmdline import parse_args
class ConfigurationError(Exception):
pass
DEFAULTS = dict(
# args to pass to browser
extra_args='',
buildid='testbuildid',
init_url='getInfo.html',
env={'NO_EM_RESTART': '1'},
# base data for all tests
basetest=dict(
cycles=1,
profile_path='${talos}/base_profile',
responsiveness=False,
e10s=False,
sps_profile=False,
sps_profile_interval=1,
sps_profile_entries=100000,
resolution=1,
rss=False,
mainthread=False,
shutdown=False,
timeout=3600,
tpchrome=True,
tpcycles=10,
tpmozafterpaint=False,
tpdisable_e10s=False,
tpnoisy=True,
tppagecycles=1,
tploadnocache=False,
tpscrolltest=False,
tprender=False,
win_counters=[],
w7_counters=[],
linux_counters=[],
mac_counters=[],
xperf_counters=[],
setup=None,
cleanup=None,
),
# default preferences to run with
# these are updated with --extraPrefs from the commandline
# for extension scopes, see
# see https://developer.mozilla.org/en/Installing_extensions
preferences={
'app.update.enabled': False,
'browser.addon-watch.interval': -1, # Deactivate add-on watching
'browser.aboutHomeSnippets.updateUrl':
'https://127.0.0.1/about-dummy/',
'browser.bookmarks.max_backups': 0,
'browser.cache.disk.smart_size.enabled': False,
'browser.cache.disk.smart_size.first_run': False,
'browser.chrome.dynamictoolbar': False,
'browser.dom.window.dump.enabled': True,
'browser.EULA.override': True,
'browser.link.open_newwindow': 2,
'browser.reader.detectedFirstArticle': True,
'browser.shell.checkDefaultBrowser': False,
'browser.warnOnQuit': False,
'browser.tabs.remote.autostart': False,
'dom.allow_scripts_to_close_windows': True,
'dom.disable_open_during_load': False,
'dom.disable_window_flip': True,
'dom.disable_window_move_resize': True,
'dom.max_chrome_script_run_time': 0,
'dom.max_script_run_time': 0,
'extensions.autoDisableScopes': 10,
'extensions.checkCompatibility': False,
'extensions.enabledScopes': 5,
'extensions.update.notifyUser': False,
'hangmonitor.timeout': 0,
'network.proxy.http': 'localhost',
'network.proxy.http_port': 80,
'network.proxy.type': 1,
'security.enable_java': False,
'security.fileuri.strict_origin_policy': False,
'dom.send_after_paint_to_content': True,
'security.turn_off_all_security_so_that_viruses_can_'
'take_over_this_computer': True,
'browser.newtabpage.directory.source':
'${webserver}/directoryLinks.json',
'browser.newtabpage.directory.ping': '',
'browser.newtabpage.introShown': True,
'browser.safebrowsing.provider.google.gethashURL':
'http://127.0.0.1/safebrowsing-dummy/gethash',
'browser.safebrowsing.provider.google.updateURL':
'http://127.0.0.1/safebrowsing-dummy/update',
'browser.safebrowsing.provider.mozilla.gethashURL':
'http://127.0.0.1/safebrowsing-dummy/gethash',
'browser.safebrowsing.provider.mozilla.updateURL':
'http://127.0.0.1/safebrowsing-dummy/update',
'privacy.trackingprotection.introURL':
'http://127.0.0.1/trackingprotection/tour',
'browser.safebrowsing.phishing.enabled': False,
'browser.safebrowsing.malware.enabled': False,
'browser.safebrowsing.forbiddenURIs.enabled': False,
'browser.safebrowsing.blockedURIs.enabled': False,
'privacy.trackingprotection.enabled': False,
'privacy.trackingprotection.pbmode.enabled': False,
'browser.search.isUS': True,
'browser.search.countryCode': 'US',
'browser.selfsupport.url':
'https://127.0.0.1/selfsupport-dummy/',
'extensions.update.url':
'http://127.0.0.1/extensions-dummy/updateURL',
'extensions.update.background.url':
'http://127.0.0.1/extensions-dummy/updateBackgroundURL',
'extensions.blocklist.enabled': False,
'extensions.blocklist.url':
'http://127.0.0.1/extensions-dummy/blocklistURL',
'extensions.hotfix.url':
'http://127.0.0.1/extensions-dummy/hotfixURL',
'extensions.update.enabled': False,
'extensions.webservice.discoverURL':
'http://127.0.0.1/extensions-dummy/discoveryURL',
'extensions.getAddons.maxResults': 0,
'extensions.getAddons.get.url':
'http://127.0.0.1/extensions-dummy/repositoryGetURL',
'extensions.getAddons.getWithPerformance.url':
'http://127.0.0.1/extensions-dummy'
'/repositoryGetWithPerformanceURL',
'extensions.getAddons.search.browseURL':
'http://127.0.0.1/extensions-dummy/repositoryBrowseURL',
'extensions.getAddons.search.url':
'http://127.0.0.1/extensions-dummy/repositorySearchURL',
'media.gmp-manager.url':
'http://127.0.0.1/gmpmanager-dummy/update.xml',
'media.gmp-manager.updateEnabled': False,
'extensions.systemAddon.update.url':
'http://127.0.0.1/dummy-system-addons.xml',
'media.navigator.enabled': True,
'media.peerconnection.enabled': True,
'media.navigator.permission.disabled': True,
'media.capturestream_hints.enabled': True,
'browser.contentHandlers.types.0.uri': 'http://127.0.0.1/rss?url=%s',
'browser.contentHandlers.types.1.uri': 'http://127.0.0.1/rss?url=%s',
'browser.contentHandlers.types.2.uri': 'http://127.0.0.1/rss?url=%s',
'browser.contentHandlers.types.3.uri': 'http://127.0.0.1/rss?url=%s',
'browser.contentHandlers.types.4.uri': 'http://127.0.0.1/rss?url=%s',
'browser.contentHandlers.types.5.uri': 'http://127.0.0.1/rss?url=%s',
'identity.fxaccounts.auth.uri': 'https://127.0.0.1/fxa-dummy/',
'datareporting.healthreport.about.reportUrl':
'http://127.0.0.1/abouthealthreport/',
'datareporting.healthreport.documentServerURI':
'http://127.0.0.1/healthreport/',
'datareporting.policy.dataSubmissionPolicyBypassNotification': True,
'general.useragent.updates.enabled': False,
'browser.webapps.checkForUpdates': 0,
'browser.search.geoSpecificDefaults': False,
'browser.snippets.enabled': False,
'browser.snippets.syncPromo.enabled': False,
'toolkit.telemetry.server': 'https://127.0.0.1/telemetry-dummy/',
'experiments.manifest.uri':
'https://127.0.0.1/experiments-dummy/manifest',
'network.http.speculative-parallel-limit': 0,
'app.update.badge': False,
'lightweightThemes.selectedThemeID': "",
'devtools.webide.widget.enabled': False,
'devtools.webide.widget.inNavbarByDefault': False,
'devtools.chrome.enabled': False,
'devtools.debugger.remote-enabled': False,
'devtools.theme': "light",
'devtools.timeline.enabled': False,
'identity.fxaccounts.migrateToDevEdition': False,
'media.libavcodec.allow-obsolete': True
}
)
# keys to generated self.config that are global overrides to tests
GLOBAL_OVERRIDES = (
'cycles',
'sps_profile',
'sps_profile_interval',
'sps_profile_entries',
'rss',
'mainthread',
'shutdown',
'tpcycles',
'tpdelay',
'tppagecycles',
'tpmanifest',
'tptimeout',
'tpmozafterpaint',
)
CONF_VALIDATORS = []
def validator(func):
"""
A decorator that register configuration validators to execute against the
configuration.
They will be executed in the order of declaration.
"""
CONF_VALIDATORS.append(func)
return func
def convert_url(config, url):
webserver = config['webserver']
if not webserver:
return url
if '://' in url:
# assume a fully qualified url
return url
if '.html' in url:
url = 'http://%s/%s' % (webserver, url)
return url
@validator
def fix_xperf(config):
# BBB: remove doubly-quoted xperf values from command line
# (needed for buildbot)
# https://bugzilla.mozilla.org/show_bug.cgi?id=704654#c43
if config['xperf_path']:
xperf_path = config['xperf_path']
quotes = ('"', "'")
for quote in quotes:
if xperf_path.startswith(quote) and xperf_path.endswith(quote):
config['xperf_path'] = xperf_path[1:-1]
break
if not os.path.exists(config['xperf_path']):
raise ConfigurationError(
"xperf.exe cannot be found at the path specified")
@validator
def set_webserver(config):
# pick a free port
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('', 0))
port = sock.getsockname()[1]
sock.close()
config['webserver'] = 'localhost:%d' % port
@validator
def update_prefs(config):
# if e10s is enabled, set prefs accordingly
if config['e10s']:
config['preferences']['browser.tabs.remote.autostart'] = True
config['preferences']['extensions.e10sBlocksEnabling'] = False
else:
config['preferences']['browser.tabs.remote.autostart'] = False
config['preferences']['browser.tabs.remote.autostart.1'] = False
config['preferences']['browser.tabs.remote.autostart.2'] = False
# update prefs from command line
prefs = config.pop('extraPrefs')
if prefs:
for arg in prefs:
k, v = arg.split('=', 1)
config['preferences'][k] = utils.parse_pref(v)
@validator
def fix_init_url(config):
if 'init_url' in config:
config['init_url'] = convert_url(config, config['init_url'])
def get_counters(config):
counters = set()
if config['rss']:
counters.add('Main_RSS')
return counters
def get_active_tests(config):
activeTests = config.pop('activeTests').strip().split(':')
# ensure tests are available
availableTests = test.test_dict()
if not set(activeTests).issubset(availableTests):
missing = [i for i in activeTests
if i not in availableTests]
raise ConfigurationError("No definition found for test(s): %s"
% missing)
# disabled DAMP on winXP: frequent hangs, <3% of devtools users on winXP
if utils.PLATFORM_TYPE == 'win_':
activeTests = [i for i in activeTests if i != 'damp']
return activeTests
def get_global_overrides(config):
global_overrides = {}
for key in GLOBAL_OVERRIDES:
# get global overrides for all tests
value = config[key]
if value is not None:
global_overrides[key] = value
if key != 'sps_profile':
config.pop(key)
# add noChrome to global overrides (HACK)
noChrome = config.pop('noChrome')
if noChrome:
global_overrides['tpchrome'] = False
# HACK: currently xperf tests post results to graph server and
# we want to ensure we don't publish shutdown numbers
# This is also hacked because "--noShutdown -> shutdown:True"
if config['xperf_path']:
global_overrides['shutdown'] = False
return global_overrides
def build_manifest(config, manifestName):
# read manifest lines
with open(manifestName, 'r') as fHandle:
manifestLines = fHandle.readlines()
# write modified manifest lines
with open(manifestName + '.develop', 'w') as newHandle:
for line in manifestLines:
newline = line.replace('localhost', config['webserver'])
newline = newline.replace('page_load_test', 'tests')
newHandle.write(newline)
newManifestName = manifestName + '.develop'
# return new manifest
return newManifestName
def get_test(config, global_overrides, counters, test_instance):
mozAfterPaint = getattr(test_instance, 'tpmozafterpaint', None)
test_instance.update(**global_overrides)
# update original value of mozAfterPaint, this could be 'false',
# so check for None
if mozAfterPaint is not None:
test_instance.tpmozafterpaint = mozAfterPaint
# fix up url
url = getattr(test_instance, 'url', None)
if url:
test_instance.url = utils.interpolate(convert_url(config, url))
# fix up tpmanifest
tpmanifest = getattr(test_instance, 'tpmanifest', None)
if tpmanifest:
test_instance.tpmanifest = \
build_manifest(config, utils.interpolate(tpmanifest))
# add any counters
if counters:
keys = ('linux_counters', 'mac_counters',
'win_counters', 'w7_counters', 'xperf_counters')
for key in keys:
if key not in test_instance.keys:
# only populate attributes that will be output
continue
if not isinstance(getattr(test_instance, key, None), list):
setattr(test_instance, key, [])
_counters = getattr(test_instance, key)
_counters.extend([counter for counter in counters
if counter not in _counters])
return dict(test_instance.items())
@validator
def tests(config):
counters = get_counters(config)
global_overrides = get_global_overrides(config)
activeTests = get_active_tests(config)
test_dict = test.test_dict()
tests = []
for test_name in activeTests:
test_class = test_dict[test_name]
tests.append(get_test(config, global_overrides, counters,
test_class()))
config['tests'] = tests
def get_browser_config(config):
required = ('preferences', 'extensions', 'browser_path', 'browser_wait',
'extra_args', 'buildid', 'env', 'init_url', 'webserver')
optional = {'bcontroller_config': '${talos}/bcontroller.json',
'branch_name': '',
'child_process': 'plugin-container',
'develop': False,
'e10s': False,
'process': '',
'framework': 'talos',
'repository': None,
'sourcestamp': None,
'symbols_path': None,
'test_timeout': 1200,
'xperf_path': None,
'error_filename': None,
}
browser_config = dict(title=config['title'])
browser_config.update(dict([(i, config[i]) for i in required]))
browser_config.update(dict([(i, config.get(i, j))
for i, j in optional.items()]))
return browser_config
def suites_conf():
import json
with open(os.path.join(os.path.dirname(utils.here),
'talos.json')) as f:
return json.load(f)['suites']
def get_config(argv=None):
argv = argv or sys.argv[1:]
cli_opts = parse_args(argv=argv)
if cli_opts.suite:
# read the suite config, update the args
try:
suite_conf = suites_conf()[cli_opts.suite]
except KeyError:
raise ConfigurationError('No such suite: %r' % cli_opts.suite)
argv += ['-a', ':'.join(suite_conf['tests'])]
argv += suite_conf.get('talos_options', [])
# args needs to be reparsed now
elif not cli_opts.activeTests:
raise ConfigurationError('--activeTests or --suite required!')
cli_opts = parse_args(argv=argv)
setup_logging("talos", cli_opts, {"tbpl": sys.stdout})
config = copy.deepcopy(DEFAULTS)
config.update(cli_opts.__dict__)
for validate in CONF_VALIDATORS:
validate(config)
# remove None Values
for k, v in config.items():
if v is None:
del config[k]
return config
def get_configs(argv=None):
config = get_config(argv=argv)
browser_config = get_browser_config(config)
return config, browser_config
if __name__ == '__main__':
cfgs = get_configs()
print(cfgs[0])
print
print(cfgs[1])

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,174 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""
Set up a browser environment before running a test.
"""
import os
import re
import tempfile
import mozfile
from mozprocess import ProcessHandler
from mozprofile.profile import Profile
from mozlog import get_proxy_logger
from talos import utils
from talos.utils import TalosError
from talos.sps_profile import SpsProfile
LOG = get_proxy_logger()
class FFSetup(object):
"""
Initialize the browser environment before running a test.
This prepares:
- the environment vars for running the test in the browser,
available via the instance member *env*.
- the profile used to run the test, available via the
instance member *profile_dir*.
- sps profiling, available via the instance member *sps_profile*
of type :class:`SpsProfile` or None if not used.
Note that the browser will be run once with the profile, to ensure
this is basically working and negate any performance noise with the
real test run (installing the profile the first time takes time).
This class should be used as a context manager::
with FFSetup(browser_config, test_config) as setup:
# setup.env is initialized, and setup.profile_dir created
pass
# here the profile is removed
"""
PROFILE_REGEX = re.compile('__metrics(.*)__metrics',
re.DOTALL | re.MULTILINE)
def __init__(self, browser_config, test_config):
self.browser_config, self.test_config = browser_config, test_config
self._tmp_dir = tempfile.mkdtemp()
self.env = None
# The profile dir must be named 'profile' because of xperf analysis
# (in etlparser.py). TODO fix that ?
self.profile_dir = os.path.join(self._tmp_dir, 'profile')
self.sps_profile = None
def _init_env(self):
self.env = dict(os.environ)
for k, v in self.browser_config['env'].iteritems():
self.env[k] = str(v)
self.env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
# for winxp e10s logging:
# https://bugzilla.mozilla.org/show_bug.cgi?id=1037445
self.env['MOZ_WIN_INHERIT_STD_HANDLES_PRE_VISTA'] = '1'
if self.browser_config['symbols_path']:
self.env['MOZ_CRASHREPORTER'] = '1'
else:
self.env['MOZ_CRASHREPORTER_DISABLE'] = '1'
self.env['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = '1'
self.env["LD_LIBRARY_PATH"] = \
os.path.dirname(self.browser_config['browser_path'])
def _init_profile(self):
preferences = dict(self.browser_config['preferences'])
if self.test_config.get('preferences'):
test_prefs = dict(
[(i, utils.parse_pref(j))
for i, j in self.test_config['preferences'].items()]
)
preferences.update(test_prefs)
# interpolate webserver value in prefs
webserver = self.browser_config['webserver']
if '://' not in webserver:
webserver = 'http://' + webserver
for name, value in preferences.items():
if type(value) is str:
value = utils.interpolate(value, webserver=webserver)
preferences[name] = value
extensions = self.browser_config['extensions'][:]
if self.test_config.get('extensions'):
extensions.append(self.test_config['extensions'])
if self.browser_config['develop'] or \
self.browser_config['branch_name'] == 'Try':
extensions = [os.path.dirname(i) for i in extensions]
profile = Profile.clone(
os.path.normpath(self.test_config['profile_path']),
self.profile_dir,
restore=False)
profile.set_preferences(preferences)
profile.addon_manager.install_addons(extensions)
def _run_profile(self):
command_args = utils.GenerateBrowserCommandLine(
self.browser_config["browser_path"],
self.browser_config["extra_args"],
self.profile_dir,
self.browser_config["init_url"]
)
def browser_log(line):
LOG.process_output(browser.pid, line)
browser = ProcessHandler(command_args, env=self.env,
processOutputLine=browser_log)
browser.run()
LOG.process_start(browser.pid, ' '.join(command_args))
try:
exit_code = browser.wait()
except KeyboardInterrupt:
browser.kill()
raise
LOG.process_exit(browser.pid, exit_code)
results_raw = '\n'.join(browser.output)
if not self.PROFILE_REGEX.search(results_raw):
LOG.info("Could not find %s in browser output"
% self.PROFILE_REGEX.pattern)
LOG.info("Raw results:%s" % results_raw)
raise TalosError("browser failed to close after being initialized")
def _init_sps_profile(self):
upload_dir = os.getenv('MOZ_UPLOAD_DIR')
if self.test_config.get('sps_profile') and not upload_dir:
LOG.critical("Profiling ignored because MOZ_UPLOAD_DIR was not"
" set")
if upload_dir and self.test_config.get('sps_profile'):
self.sps_profile = SpsProfile(upload_dir,
self.browser_config,
self.test_config)
self.sps_profile.update_env(self.env)
def clean(self):
mozfile.remove(self._tmp_dir)
if self.sps_profile:
self.sps_profile.clean()
def __enter__(self):
LOG.info('Initialising browser for %s test...'
% self.test_config['name'])
self._init_env()
self._init_profile()
try:
self._run_profile()
except:
self.clean()
raise
self._init_sps_profile()
LOG.info('Browser initialized.')
return self
def __exit__(self, type, value, tb):
self.clean()

View file

@ -0,0 +1,193 @@
import math
"""
data filters:
takes a series of run data and applies statistical transforms to it
Each filter is a simple function, but it also have attached a special
`prepare` method that create a tuple with one instance of a
:class:`Filter`; this allow to write stuff like::
from talos import filter
filters = filter.ignore_first.prepare(1) + filter.median.prepare()
for filter in filters:
data = filter(data)
# data is filtered
"""
class Filter(object):
def __init__(self, func, *args, **kwargs):
"""
Takes a filter function, and save args and kwargs that
should be used when the filter is used.
"""
self.func = func
self.args = args
self.kwargs = kwargs
def apply(self, data):
"""
Apply the filter on the data, and return the new data
"""
return self.func(data, *self.args, **self.kwargs)
def define_filter(func):
"""
decorator to attach the prepare method.
"""
def prepare(*args, **kwargs):
return (Filter(func, *args, **kwargs),)
func.prepare = prepare
return func
# filters that return a scalar
@define_filter
def mean(series):
"""
mean of data; needs at least one data point
"""
return sum(series)/float(len(series))
@define_filter
def median(series):
"""
median of data; needs at least one data point
"""
series = sorted(series)
if len(series) % 2:
# odd
return series[len(series)/2]
else:
# even
middle = len(series)/2 # the higher of the middle 2, actually
return 0.5*(series[middle-1] + series[middle])
@define_filter
def variance(series):
"""
variance: http://en.wikipedia.org/wiki/Variance
"""
_mean = mean(series)
variance = sum([(i-_mean)**2 for i in series])/float(len(series))
return variance
@define_filter
def stddev(series):
"""
standard deviation: http://en.wikipedia.org/wiki/Standard_deviation
"""
return variance(series)**0.5
@define_filter
def dromaeo(series):
"""
dromaeo: https://wiki.mozilla.org/Dromaeo, pull the internal calculation
out
* This is for 'runs/s' based tests, not 'ms' tests.
* chunksize: defined in dromaeo: tests/dromaeo/webrunner.js#l8
"""
means = []
chunksize = 5
series = list(dromaeo_chunks(series, chunksize))
for i in series:
means.append(mean(i))
return geometric_mean(means)
@define_filter
def dromaeo_chunks(series, size):
for i in range(0, len(series), size):
yield series[i:i+size]
@define_filter
def geometric_mean(series):
"""
geometric_mean: http://en.wikipedia.org/wiki/Geometric_mean
"""
total = 0
for i in series:
total += math.log(i+1)
return math.exp(total / len(series)) - 1
# filters that return a list
@define_filter
def ignore_first(series, number=1):
"""
ignore first datapoint
"""
if len(series) <= number:
# don't modify short series
return series
return series[number:]
@define_filter
def ignore(series, function):
"""
ignore the first value of a list given by function
"""
if len(series) <= 1:
# don't modify short series
return series
series = series[:] # do not mutate the original series
value = function(series)
series.remove(value)
return series
@define_filter
def ignore_max(series):
"""
ignore maximum data point
"""
return ignore(series, max)
@define_filter
def ignore_min(series):
"""
ignore minimum data point
"""
return ignore(series, min)
@define_filter
def v8_subtest(series, name):
"""
v8 benchmark score - modified for no sub benchmarks.
* removed Crypto and kept Encrypt/Decrypt standalone
* removed EarlyBoyer and kept Earley/Boyer standalone
this is not 100% in parity but within .3%
"""
reference = {'Encrypt': 266181.,
'Decrypt': 266181.,
'DeltaBlue': 66118.,
'Earley': 666463.,
'Boyer': 666463.,
'NavierStokes': 1484000.,
'RayTrace': 739989.,
'RegExp': 910985.,
'Richards': 35302.,
'Splay': 81491.
}
return reference[name] / geometric_mean(series)
@define_filter
def responsiveness_Metric(val_list):
return sum([float(x)*float(x) / 1000000.0 for x in val_list])

View file

@ -0,0 +1,74 @@
<html>
<!--
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<head>
<meta charset="UTF-8"/>
<title>TART/CART addon xpi generator</title>
<script src="scripts/jszip.min.js"></script>
<script src="scripts/xpigen.js"></script>
<script>
var base = "tests/tart/addon/";
var files = [
"chrome.manifest",
"install.rdf",
"content/framescript.js",
"content/tart.overlay.xul",
"content/tart.html",
"content/tart.ico",
"content/tart.js",
"content/blank.icon.html",
"content/Profiler.js",
"content/tab-min-width-1px.css"
];
function triggerBuildXpi() {
function $(id) { return document.getElementById(id); }
$("status-text").innerHTML = "Preparing ...";
$("result").style.display = "none";
$("status").style.display = "inline";
createXpiDataUri(base, files, {
onprogress: function(p) { $("status-text").innerHTML = "Preparing ... " + p.toFixed(0) + "%"; },
onerror : function(e) { $("status-text").innerHTML = "Oops, couldn't generate XPI. Reload to retry.<br/>" + e; },
onsuccess : function(u) { $("xpi-link").href = u;
$("status").style.display = "none";
$("result").style.display = "inline"; }
});
}
</script>
</head>
<body onload="triggerBuildXpi()" style="font-family: sans-serif; color:#555">
<h3>TART addon</h3>
<h4>
Install / Download:
<span id="status">
<span id="status-text" style="color: #c40;">Not ready</span>
</span>
<span id="result" style="display: none;">
<a id="xpi-link" href="#" style="background-color: #dfd;">tart.xpi</a><br/>
<span style="color: grey";>(gibberish file name is expected, just rename/save as tart.xpi)</span>
</span>
</h4>
<p>
Once installed, to run TART or CART tests, visit
<span style="color: green; font-family: monospace;">chrome://tart/content/tart.html</span>
and follow the instructions.
</p>
<p>
You can also build the addon yourself on a local talos repo: zip the content
of <span style="font-family: monospace;">talos/tests/tart/addon/</span> and rename the zip to xpi.
</p>
</body>
</html>

View file

@ -0,0 +1,71 @@
<html>
<!--
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<head>
<meta charset="UTF-8"/>
<title>Tresize addon xpi generator</title>
<script src="scripts/jszip.min.js"></script>
<script src="scripts/xpigen.js"></script>
<script>
var base = "startup_test/tresize/addon/";
var files = [
"chrome.manifest",
"install.rdf",
"content/framescript.js",
"content/Profiler.js",
"content/tresize.overlay.xul",
"content/tresize.js",
"content/tresize-test.html"
];
function triggerBuildXpi() {
function $(id) { return document.getElementById(id); }
$("status-text").innerHTML = "Preparing ...";
$("result").style.display = "none";
$("status").style.display = "inline";
createXpiDataUri(base, files, {
onprogress: function(p) { $("status-text").innerHTML = "Preparing ... " + p.toFixed(0) + "%"; },
onerror : function(e) { $("status-text").innerHTML = "Oops, couldn't generate XPI. Reload to retry.<br/>" + e; },
onsuccess : function(u) { $("xpi-link").href = u;
$("status").style.display = "none";
$("result").style.display = "inline"; }
});
}
</script>
</head>
<body onload="triggerBuildXpi()" style="font-family: sans-serif; color:#555">
<h3>TResize addon</h3>
<h4>
Install / Download:
<span id="status">
<span id="status-text" style="color: #c40;">Not ready</span>
</span>
<span id="result" style="display: none;">
<a id="xpi-link" href="#" style="background-color: #dfd;">tresize.xpi</a><br/>
<span style="color: grey";>(gibberish file name is expected, just rename/save as tresize.xpi)</span>
</span>
</h4>
<p>
Once installed, to run Tresize tests, visit
<span style="color: green; font-family: monospace;">chrome://tresize/content/tresize-test.html</span>
and follow the instructions.
</p>
<p>
You can also build the addon yourself on a local talos repo: zip the content
of <span style="font-family: monospace;">talos/statup_test/tresize/addon/</span> and rename the zip to xpi.
</p>
</body>
</html>

View file

@ -0,0 +1,22 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this file,
- You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<meta charset="utf-8">
<head>
<script language="Javascript" type="text/javascript" src="scripts/MozillaFileLogger.js"></script>
<script language="Javascript" type="text/javascript" src="tests/quit.js"></script>
<title>shutdown script</title>
</head>
<body onload="
dumpLog('__metrics\tScreen width/height:' + screen.width + '/' + screen.height + '\n\tcolorDepth:' + screen.colorDepth + '\n');
dumpLog('\tBrowser inner width/height: ' + window.innerWidth + '/' + window.innerHeight + '\n');
//JMAHER: this is temporarily commented out because this hangs the fennec browser
// dumpLog('\tBrowser outer width/height: ' + window.outerWidth + '/' + window.outerHeight);
dumpLog('__metrics\n');
goQuitApplication(true);
">
</body>
</html>

View file

@ -0,0 +1,158 @@
# -*- Mode: python; tab-width: 8; indent-tabs-mode: nil -*-
# vim: set ts=8 sts=4 et sw=4 tw=80:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import os
import utils
import whitelist
SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
STAGE_STARTUP = 0
STAGE_STRINGS = ('startup', 'normal', 'shutdown')
LENGTH_IO_ENTRY = 5
LENGTH_NEXT_STAGE_ENTRY = 2
TOKEN_NEXT_STAGE = 'NEXT-STAGE'
INDEX_OPERATION = 1
INDEX_DURATION = 2
INDEX_EVENT_SOURCE = 3
INDEX_FILENAME = 4
KEY_COUNT = 'Count'
KEY_DURATION = 'Duration'
KEY_NO_FILENAME = '(not available)'
KEY_RUN_COUNT = 'RunCount'
LEAKED_SYMLINK_PREFIX = "::\\{"
PATH_SUBSTITUTIONS = {'profile': '{profile}', 'firefox': '{xre}',
'desktop': '{desktop}',
'fonts': '{fonts}', 'appdata': ' {appdata}'}
NAME_SUBSTITUTIONS = {'installtime': '{time}', 'prefetch': '{prefetch}',
'thumbnails': '{thumbnails}',
'windows media player': '{media_player}'}
TUPLE_FILENAME_INDEX = 2
WHITELIST_FILENAME = os.path.join(SCRIPT_DIR, 'mtio-whitelist.json')
def parse(logfilename, data):
try:
with open(logfilename, 'r') as logfile:
if not logfile:
return False
stage = STAGE_STARTUP
for line in logfile:
prev_filename = str()
entries = line.strip().split(',')
if len(entries) == LENGTH_IO_ENTRY:
if stage == STAGE_STARTUP:
continue
if entries[INDEX_FILENAME] == KEY_NO_FILENAME:
continue
# Format 1: I/O entry
# Temporary hack: logs are leaking Windows NT symlinks.
# We need to ignore those.
if entries[INDEX_FILENAME]\
.startswith(LEAKED_SYMLINK_PREFIX):
continue
# We'll key each entry on (stage, event source,
# filename, operation)
key_tuple = (STAGE_STRINGS[stage],
entries[INDEX_EVENT_SOURCE],
entries[INDEX_FILENAME],
entries[INDEX_OPERATION])
if key_tuple not in data:
data[key_tuple] = {
KEY_COUNT: 1,
KEY_RUN_COUNT: 1,
KEY_DURATION: float(entries[INDEX_DURATION])
}
else:
if prev_filename != entries[INDEX_FILENAME]:
data[key_tuple][KEY_RUN_COUNT] += 1
data[key_tuple][KEY_COUNT] += 1
data[key_tuple][KEY_DURATION] += \
float(entries[INDEX_DURATION])
prev_filename = entries[INDEX_FILENAME]
elif len(entries) == LENGTH_NEXT_STAGE_ENTRY and \
entries[1] == TOKEN_NEXT_STAGE:
# Format 2: next stage
stage = stage + 1
return True
except IOError as e:
print("%s: %s" % (e.filename, e.strerror))
return False
def write_output(outfilename, data):
# Write the data out so that we can track it
try:
with open(outfilename, 'w') as outfile:
outfile.write("[\n")
for idx, (key, value) in utils.indexed_items(data.iteritems()):
output = " [\"%s\", \"%s\", \"%s\", \"%s\", %d, %d, %f]" % (
key[0], key[1], key[2], key[3], value[KEY_COUNT],
value[KEY_RUN_COUNT], value[KEY_DURATION])
outfile.write(output)
if idx >= 0:
outfile.write(",\n")
else:
outfile.write("\n")
outfile.write("]\n")
return True
except IOError as e:
print("%s: %s" % (e.filename, e.strerror))
return False
def main(argv):
if len(argv) < 4:
print("Usage: %s <main_thread_io_log_file> <output_file> <xre_path>"
% argv[0])
return 1
if not os.path.exists(argv[3]):
print("XRE Path \"%s\" does not exist" % argv[3])
return 1
data = {}
if not parse(argv[1], data):
print("Log parsing failed")
return 1
wl = whitelist.Whitelist(test_name='mainthreadio',
paths={"{xre}": argv[3]},
path_substitutions=PATH_SUBSTITUTIONS,
name_substitutions=NAME_SUBSTITUTIONS)
if not wl.load(WHITELIST_FILENAME):
print("Failed to load whitelist")
return 1
wl.filter(data, TUPLE_FILENAME_INDEX)
if not write_output(argv[2], data):
return 1
# Disabled until we enable TBPL oranges
# search for unknown filenames
errors = wl.check(data, TUPLE_FILENAME_INDEX)
if errors:
strs = wl.get_error_strings(errors)
wl.print_errors(strs)
# search for duration > 1.0
errors = wl.checkDuration(data, TUPLE_FILENAME_INDEX, 'Duration')
if errors:
strs = wl.get_error_strings(errors)
wl.print_errors(strs)
return 0
if __name__ == "__main__":
import sys
sys.exit(main(sys.argv))

View file

@ -0,0 +1,151 @@
{"{FONTS}\\ARIAL.TTF": {},
"{FONTS}\\ARIALBD.TTF": {},
"{FONTS}\\ARIALBI.TTF": {},
"{FONTS}\\ARIALI.TTF": {},
"{FONTS}\\ARIBLK.TTF": {},
"{FONTS}\\CONSOLA.TTF": {},
"{FONTS}\\CORBEL.TTF": {},
"{FONTS}\\CORBELB.TTF": {},
"{FONTS}\\COUR.TTF": {},
"{FONTS}\\COURBD.TTF": {},
"{FONTS}\\GAUTAMI.TTF": {},
"{FONTS}\\GEORGIA.TTF": {},
"{FONTS}\\GEORGIAB.TTF": {},
"{FONTS}\\GEORGIAI.TTF": {},
"{FONTS}\\GEORGIAZ.TTF": {},
"{FONTS}\\GULIM.TTC": {},
"{FONTS}\\IMPACT.TTF": {},
"{FONTS}\\ISKPOTA.TTF": {},
"{FONTS}\\KALINGA.TTF": {},
"{FONTS}\\KARTIKA.TTF": {},
"{FONTS}\\L_10646.TTF": {},
"{FONTS}\\LATHA.TTF": {},
"{FONTS}\\LUCON.TTF": {},
"{FONTS}\\MANGAL.TTF": {},
"{FONTS}\\MANGALB.TTF": {},
"{FONTS}\\MEIRYO.TTC": {},
"{FONTS}\\MEIRYOB.TTC": {},
"{FONTS}\\MICROSS.TTF": {},
"{FONTS}\\MINGLIU.TTC": {},
"{FONTS}\\MINGLIUB.TTC": {},
"{FONTS}\\MSGOTHIC.TTC": {},
"{FONTS}\\MSMINCHO.TTC": {},
"{FONTS}\\MSUIGHUR.TTF": {},
"{FONTS}\\MSYH.TTF": {},
"{FONTS}\\MSYHBD.TTF": {},
"{FONTS}\\PALA.TTF": {},
"{FONTS}\\PALAB.TTF": {},
"{FONTS}\\RAAVI.TTF": {},
"{FONTS}\\SEGOEUI.TTF": {},
"{FONTS}\\SEGOEUIB.TTF": {},
"{FONTS}\\SEGOEUII.TTF": {},
"{FONTS}\\SEGOEUIL.TTF": {},
"{FONTS}\\SEGOEUIZ.TTF": {},
"{FONTS}\\SEGUISYM.TTF": {},
"{FONTS}\\SHRUTI.TTF": {},
"{FONTS}\\SIMHEI.TTF": {},
"{FONTS}\\SIMKAI.TTF": {},
"{FONTS}\\SIMSUN.TTC": {},
"{FONTS}\\SIMSUNB.TTF": {},
"{FONTS}\\SYLFAEN.TTF": {},
"{FONTS}\\TAHOMA.TTF": {},
"{FONTS}\\TAHOMABD.TTF": {},
"{FONTS}\\TIMESBD.TTF": {},
"{FONTS}\\TIMESBI.TTF": {},
"{FONTS}\\TIMESI.TTF": {},
"{FONTS}\\TREBUC.TTF": {},
"{FONTS}\\TREBUCBD.TTF": {},
"{FONTS}\\TREBUCBI.TTF": {},
"{FONTS}\\TREBUCIT.TTF": {},
"{FONTS}\\TUNGA.TTF": {},
"{FONTS}\\VERDANA.TTF": {},
"{FONTS}\\VERDANAB.TTF": {},
"{FONTS}\\VERDANAI.TTF": {},
"{FONTS}\\VERDANAZ.TTF": {},
"{FONTS}\\VRINDA.TTF": {},
"{profile}": {},
"{profile}\\": {},
"{profile}\\_CACHE_CLEAN_": {},
"{profile}\\addons.sqlite": {},
"{profile}\\blocklist.xml": {},
"{profile}\\Cache\\_CACHE_001_": {},
"{profile}\\Cache\\_CACHE_002_": {},
"{profile}\\Cache\\_CACHE_003_": {},
"{profile}\\Cache\\_CACHE_MAP_": {},
"{profile}\\cert8.db": {},
"{profile}\\cookies.sqlite": {},
"{profile}\\cookies.sqlite-journal": {},
"{profile}\\cookies.sqlite-shm": {},
"{profile}\\cookies.sqlite-wal": {},
"{profile}\\extensions\\pageloader@mozilla.org": {},
"{profile}\\extensions\\pageloader@mozilla.org\\plugins": {},
"{profile}\\extensions\\pageloader@mozilla.org\\searchplugins": {},
"{profile}\\extensions\\pageloader@mozilla.org\\chrome\\profiler.js": {},
"{profile}\\extensions\\pageloader@mozilla.org\\chrome\\tscroll.js": {},
"{profile}\\extensions\\pageloader@mozilla.org\\chrome\\talos-content.js": {},
"{profile}\\extensions\\talos-powers@mozilla.org": {},
"{profile}\\extensions\\talos-powers@mozilla.org\\chrome.manifest": {},
"{profile}\\extensions\\talos-powers@mozilla.org\\plugins": {},
"{profile}\\extensions\\talos-powers@mozilla.org\\searchplugins": {},
"{profile}\\extensions\\talos-powers@mozilla.org\\chrome\\talos-powers-content.js": {},
"{profile}\\extensions\\talos-powers@mozilla.org\\components\\TalosPowersService.js": {},
"{profile}\\key3.db": {},
"{profile}\\localstore-1.rdf": {},
"{profile}\\localstore.rdf": {},
"{profile}\\pluginreg.dat": {},
"{profile}\\pluginreg.dat.tmp": {},
"{profile}\\plugins": {},
"{profile}\\places.sqlite": {},
"{profile}\\places.sqlite-journal": {},
"{profile}\\places.sqlite-wal": {},
"{profile}\\prefs-1.js": {},
"{profile}\\prefs.js": {},
"{profile}\\searchplugins": {},
"{profile}\\secmod.db": {},
"{profile}\\startupCache\\startupCache.4.little": {},
"{profile}\\cache2\\index": {},
"{profile}\\cache2\\index.log": {},
"{profile}\\cache2\\index.tmp": {},
"{profile}\\Telemetry.ShutdownTime.txt.tmp": {},
"{profile}\\{thumbnails}": {},
"{xre}\\browser\\plugins": {},
"{xre}\\browser\\searchplugins": {},
"{xre}\\browser\\searchplugins\\yahoo.xml": {},
"{xre}\\browser\\searchplugins\\twitter.xml": {},
"{xre}\\browser\\searchplugins\\ebay.xml": {},
"{xre}\\browser\\searchplugins\\amazondotcom.xml": {},
"{xre}\\browser\\searchplugins\\bing.xml": {},
"{xre}\\browser\\searchplugins\\ddg.xml": {},
"{xre}\\browser\\searchplugins\\google.xml": {},
"{xre}\\browser\\searchplugins\\wikipedia.xml": {},
"{xre}\\distribution\\searchplugins": {},
"c:\\users": {},
"C:\\windows\\system32\\propsys.dll": {},
"C:\\Program Files\\NVIDIA Corporation\\3D Vision": {},
"C:\\Program Files\\NVIDIA Corporation\\3D Vision\\npnv3dv.dll": {},
"C:\\Program Files\\NVIDIA Corporation\\3D Vision\\npnv3dvstreaming.dll": {},
"C:\\Program Files\\{media_player}": {},
"c:\\users\\cltbld": {},
"c:\\users\\cltbld\\appdata": {},
"c:\\users\\cltbld\\appdata\\local": {},
"{AppData}": {},
"{AppData}\\local": {},
"{AppData}\\local\\temp": {},
"{AppData}\\locallow\\mozilla": {},
"{AppData}\\Roaming": {},
"{AppData}\\roaming\\temp": {},
"{AppData}\\roaming\\mozilla": {},
"{AppData}\\roaming\\mozilla\\plugins": {},
"{AppData}\\Roaming\\Microsoft": {},
"{AppData}\\Roaming\\Microsoft\\desktop.ini": {},
"{AppData}\\Roaming\\Microsoft\\Windows": {},
"{Desktop}\\desktop.ini": {},
"C:\\ProgramData": {},
"C:\\ProgramData\\Microsoft\\desktop.ini": {},
"C:\\ProgramData\\Microsoft": {},
"C:\\ProgramData\\Microsoft\\Windows": {},
"C:\\Users\\desktop.ini": {},
"C:\\Users": {},
"C:\\Users\\Public": {},
"C:\\Users\\Public\\desktop.ini": {}
}

View file

@ -0,0 +1,257 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""output formats for Talos"""
import filter
import json
import utils
from mozlog import get_proxy_logger
# NOTE: we have a circular dependency with output.py when we import results
import results as TalosResults
LOG = get_proxy_logger()
def filesizeformat(bytes):
"""
Format the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 102
bytes, etc).
"""
bytes = float(bytes)
formats = ('B', 'KB', 'MB')
for f in formats:
if bytes < 1024:
return "%.1f%s" % (bytes, f)
bytes /= 1024
return "%.1fGB" % bytes # has to be GB
class Output(object):
"""abstract base class for Talos output"""
@classmethod
def check(cls, urls):
"""check to ensure that the urls are valid"""
def __init__(self, results):
"""
- results : TalosResults instance
"""
self.results = results
def __call__(self):
suites = []
test_results = {
'framework': {
'name': self.results.results[0].framework,
},
'suites': suites,
}
for test in self.results.results:
# serialize test results
tsresult = None
if not test.using_xperf:
subtests = []
suite = {
'name': test.name(),
'subtests': subtests,
}
if self.results.extra_options:
suite['extraOptions'] = self.results.extra_options
suites.append(suite)
vals = []
replicates = {}
# TODO: counters!!!! we don't have any, but they suffer the
# same
for result in test.results:
# XXX this will not work for manifests which list
# the same page name twice. It also ignores cycles
for page, val in result.raw_values():
if page == 'NULL':
page = test.name()
if tsresult is None:
tsresult = r = TalosResults.Results()
r.results = [{'index': 0, 'page': test.name(),
'runs': val}]
else:
r = tsresult.results[0]
if r['page'] == test.name():
r['runs'].extend(val)
replicates.setdefault(page, []).extend(val)
tresults = [tsresult] if tsresult else test.results
for result in tresults:
filtered_results = \
result.values(suite['name'],
test.test_config['filters'])
vals.extend([[i['value'], j] for i, j in filtered_results])
for val, page in filtered_results:
if page == 'NULL':
# no real subtests
page = test.name()
subtest = {
'name': page,
'value': val['filtered'],
'replicates': replicates[page],
}
subtests.append(subtest)
if test.test_config.get('lower_is_better') is not None:
subtest['lowerIsBetter'] = \
test.test_config['lower_is_better']
if test.test_config.get('alert_threshold') is not None:
subtest['alertThreshold'] = \
test.test_config['alert_threshold']
if test.test_config.get('unit'):
subtest['unit'] = test.test_config['unit']
# if there is more than one subtest, calculate a summary result
if len(subtests) > 1:
suite['value'] = self.construct_results(
vals, testname=test.name())
if test.test_config.get('lower_is_better') is not None:
suite['lowerIsBetter'] = \
test.test_config['lower_is_better']
if test.test_config.get('alert_threshold') is not None:
suite['alertThreshold'] = \
test.test_config['alert_threshold']
# counters results_aux data
counter_subtests = []
for cd in test.all_counter_results:
for name, vals in cd.items():
# We want to add the xperf data as talos_counters
# exclude counters whose values are tuples (bad for
# graphserver)
if len(vals) > 0 and isinstance(vals[0], list):
continue
# mainthread IO is a list of filenames and accesses, we do
# not report this as a counter
if 'mainthreadio' in name:
continue
# responsiveness has it's own metric, not the mean
# TODO: consider doing this for all counters
if 'responsiveness' is name:
subtest = {
'name': name,
'value': filter.responsiveness_Metric(vals)
}
counter_subtests.append(subtest)
continue
subtest = {
'name': name,
'value': 0.0,
}
counter_subtests.append(subtest)
if test.using_xperf:
if len(vals) > 0:
subtest['value'] = vals[0]
else:
# calculate mean value
if len(vals) > 0:
varray = [float(v) for v in vals]
subtest['value'] = filter.mean(varray)
if counter_subtests:
suites.append({'name': test.name(),
'subtests': counter_subtests})
return test_results
def output(self, results, results_url, tbpl_output):
"""output to the a file if results_url starts with file://
- results : json instance
- results_url : file:// URL
"""
# parse the results url
results_url_split = utils.urlsplit(results_url)
results_scheme, results_server, results_path, _, _ = results_url_split
if results_scheme in ('http', 'https'):
self.post(results, results_server, results_path, results_scheme,
tbpl_output)
elif results_scheme == 'file':
with open(results_path, 'w') as f:
for result in results:
f.write("%s\n" % result)
else:
raise NotImplementedError(
"%s: %s - only http://, https://, and file:// supported"
% (self.__class__.__name__, results_url)
)
# This is the output that treeherder expects to find when parsing the
# log file
if 'spsProfile' not in self.results.extra_options:
LOG.info("PERFHERDER_DATA: %s" % json.dumps(results))
if results_scheme in ('file'):
json.dump(results, open(results_path, 'w'), indent=2,
sort_keys=True)
def post(self, results, server, path, scheme, tbpl_output):
raise NotImplementedError("Abstract base class")
@classmethod
def shortName(cls, name):
"""short name for counters"""
names = {"Working Set": "memset",
"% Processor Time": "%cpu",
"Private Bytes": "pbytes",
"RSS": "rss",
"XRes": "xres",
"Modified Page List Bytes": "modlistbytes",
"Main_RSS": "main_rss"}
return names.get(name, name)
@classmethod
def isMemoryMetric(cls, resultName):
"""returns if the result is a memory metric"""
memory_metric = ['memset', 'rss', 'pbytes', 'xres', 'modlistbytes',
'main_rss', 'content_rss'] # measured in bytes
return bool([i for i in memory_metric if i in resultName])
@classmethod
def v8_Metric(cls, val_list):
results = [i for i, j in val_list]
score = 100 * filter.geometric_mean(results)
return score
@classmethod
def JS_Metric(cls, val_list):
"""v8 benchmark score"""
results = [i for i, j in val_list]
LOG.info("javascript benchmark")
return sum(results)
@classmethod
def CanvasMark_Metric(cls, val_list):
"""CanvasMark benchmark score (NOTE: this is identical to JS_Metric)"""
results = [i for i, j in val_list]
LOG.info("CanvasMark benchmark")
return sum(results)
def construct_results(self, vals, testname):
if 'responsiveness' in testname:
return filter.responsiveness_Metric([val for (val, page) in vals])
elif testname.startswith('v8_7'):
return self.v8_Metric(vals)
elif testname.startswith('kraken'):
return self.JS_Metric(vals)
elif testname.startswith('tcanvasmark'):
return self.CanvasMark_Metric(vals)
elif len(vals) > 1:
return filter.geometric_mean([i for i, j in vals])
else:
return filter.mean([i for i, j in vals])

View file

@ -0,0 +1,65 @@
Pageload Test Component
=======================
Usage:
./firefox -tp file:///path/to/manifest.txt [-tpargs...]
See ./firefox -help for other arguments.
Manifest file format
====================
Comments in the manifest file start with a #. Each line may be:
* a URL (absolute or relative to the manifest)
This URL is added to the list of tests.
* one or more flags, followed by whitespace, followed by a URL
The only flag supported currently is '%', which indicates that
a test will do its own timing. (See Self-timing Tests below.)
* "include" followed by whitespace, followed by a URL
Parse the given manifest file.
Self-timing Tests
=================
Most timing tests are interested in timing how long it takes the page
to load; that is, from the start of page loading until the 'load'
event is dispatched. By default, this is what the pageloader will
time. However, if a test URL has the % flag, the test is expected to
report its own timing. For this purpose, the pageloader will provide
a function named "tpRecordTime" in the test's global object that it
should call once it has performed whatever timing it wants to do.
The given value will be used as the timing result for this test.
Output format
=============
The result is a dump to stdout via dump() --
browser.dom.window.dump.enabled must be set to true in the profile.
Sample output:
__start_tp_report
_x_x_mozilla_page_load,778.5,NaN,NaN
_x_x_mozilla_page_load_details,avgmedian|778.5|average|766.75|minimum|NaN|maximum|NaN|stddev|NaN|0;file:///c:/proj/mozilla-cvs/perf/tp2/base/www.cnn.com/index.html;778.5;766.75;722;1027;1027;788;777;722;780|...
__end_tp_report
Note that the minimum, maximum, stddev are not calculated; they're
always reported as NaN. (They were the minimum and maximum values of
any sampled value, and the standard deviation across all sampled
values -- not very useful.)
TODO
====
* Command line option to choose whether to run with or without browser chrome. Currently runs without.
* Tinderbox-dropping style output
* better yet would be to teach tinderbox about JSON

View file

@ -0,0 +1,4 @@
content pageloader chrome/
component {8AF052F5-8EFE-4359-8266-E16498A82E8B} components/tp-cmdline.js
contract @mozilla.org/commandlinehandler/general-startup;1?type=tp {8AF052F5-8EFE-4359-8266-E16498A82E8B}
category command-line-handler m-tp @mozilla.org/commandlinehandler/general-startup;1?type=tp

View file

@ -0,0 +1,92 @@
/**
* MozillaFileLogger, a log listener that can write to a local file.
*/
//double logging to account for normal mode and ipc mode (mobile_profile only)
//Ideally we would remove the dump() and just do ipc logging
function dumpLog(msg) {
dump(msg);
MozillaFileLogger.log(msg);
}
if (Cc === undefined) {
var Cc = Components.classes;
var Ci = Components.interfaces;
}
const FOSTREAM_CID = "@mozilla.org/network/file-output-stream;1";
const LF_CID = "@mozilla.org/file/local;1";
// File status flags. It is a bitwise OR of the following bit flags.
// Only one of the first three flags below may be used.
const PR_READ_ONLY = 0x01; // Open for reading only.
const PR_WRITE_ONLY = 0x02; // Open for writing only.
const PR_READ_WRITE = 0x04; // Open for reading and writing.
// If the file does not exist, the file is created.
// If the file exists, this flag has no effect.
const PR_CREATE_FILE = 0x08;
// The file pointer is set to the end of the file prior to each write.
const PR_APPEND = 0x10;
// If the file exists, its length is truncated to 0.
const PR_TRUNCATE = 0x20;
// If set, each write will wait for both the file data
// and file status to be physically updated.
const PR_SYNC = 0x40;
// If the file does not exist, the file is created. If the file already
// exists, no action and NULL is returned.
const PR_EXCL = 0x80;
/** Init the file logger with the absolute path to the file.
It will create and append if the file already exists **/
var MozillaFileLogger = {};
MozillaFileLogger.init = function(path) {
MozillaFileLogger._file = Cc[LF_CID].createInstance(Ci.nsILocalFile);
MozillaFileLogger._file.initWithPath(path);
MozillaFileLogger._foStream = Cc[FOSTREAM_CID].createInstance(Ci.nsIFileOutputStream);
MozillaFileLogger._foStream.init(this._file, PR_WRITE_ONLY | PR_CREATE_FILE | PR_APPEND,
0o664, 0);
}
MozillaFileLogger.getLogCallback = function() {
return function (msg) {
var data = msg.num + " " + msg.level + " " + msg.info.join(' ') + "\n";
if (MozillaFileLogger._foStream)
MozillaFileLogger._foStream.write(data, data.length);
if (data.indexOf("SimpleTest FINISH") >= 0) {
MozillaFileLogger.close();
}
}
}
// This is only used from chrome space by the reftest harness
MozillaFileLogger.log = function(msg) {
try {
if (MozillaFileLogger._foStream)
MozillaFileLogger._foStream.write(msg, msg.length);
} catch(ex) {}
}
MozillaFileLogger.close = function() {
if(MozillaFileLogger._foStream)
MozillaFileLogger._foStream.close();
MozillaFileLogger._foStream = null;
MozillaFileLogger._file = null;
}
try {
var prefs = Cc['@mozilla.org/preferences-service;1']
.getService(Ci.nsIPrefBranch2);
var filename = prefs.getCharPref('talos.logfile');
MozillaFileLogger.init(filename);
} catch (ex) {} //pref does not exist, return empty string

View file

@ -0,0 +1,172 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// - NOTE: This file is duplicated verbatim at:
// - talos/scripts/Profiler.js
// - talos/pageloader/chrome/Profiler.js
// - talos/tests/devtools/addon/content/Profiler.js
// - talos/tests/tart/addon/content/Profiler.js
// - talos/startup_test/tresize/addon/content/Profiler.js
//
// - Please keep these copies in sync.
// - Please make sure your changes apply cleanly to all use cases.
// Finer grained profiler control
//
// Use this object to pause and resume the profiler so that it only profiles the
// relevant parts of our tests.
var Profiler;
(function(){
var _profiler;
// If this script is loaded in a framescript context, there won't be a
// document object, so just use a fallback value in that case.
var test_name = this.document ? this.document.location.pathname : "unknown";
// Whether Profiler has been initialized. Until that happens, most calls
// will be ignored.
var enabled = false;
// The subtest name that beginTest() was called with.
var currentTest = "";
// Profiling settings.
var profiler_interval, profiler_entries, profiler_threadsArray, profiler_dir;
try {
// Outside of talos, this throws a security exception which no-op this file.
// (It's not required nor allowed for addons since Firefox 17)
// It's used inside talos from non-privileged pages (like during tscroll),
// and it works because talos disables all/most security measures.
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
} catch (e) {}
try {
_profiler = Components.classes["@mozilla.org/tools/profiler;1"].getService(Components.interfaces.nsIProfiler);
} catch (ex) { (typeof(dumpLog) == "undefined" ? dump : dumpLog)(ex + "\n"); }
// Parses an url query string into a JS object.
function searchToObject(locationSearch) {
var pairs = locationSearch.substring(1).split("&");
var result = {};
for (var i in pairs) {
if (pairs[i] !== "") {
var pair = pairs[i].split("=");
result[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || "");
}
}
return result;
}
Profiler = {
/**
* Initialize the profiler using profiler settings supplied in a JS object.
* The following properties on the object are respected:
* - sps_profile_interval
* - sps_profile_entries
* - sps_profile_threads
* - sps_profile_dir
*/
initFromObject: function Profiler__initFromObject (obj) {
if (obj &&
("sps_profile_dir" in obj) && typeof obj.sps_profile_dir == "string" &&
("sps_profile_interval" in obj) && Number.isFinite(obj.sps_profile_interval * 1) &&
("sps_profile_entries" in obj) && Number.isFinite(obj.sps_profile_entries * 1) &&
("sps_profile_threads" in obj) && typeof obj.sps_profile_threads == "string") {
profiler_interval = obj.sps_profile_interval;
profiler_entries = obj.sps_profile_entries;
profiler_threadsArray = obj.sps_profile_threads.split(",");
profiler_dir = obj.sps_profile_dir;
enabled = true;
}
},
initFromURLQueryParams: function Profiler__initFromURLQueryParams (locationSearch) {
this.initFromObject(searchToObject(locationSearch));
},
beginTest: function Profiler__beginTest (testName) {
currentTest = testName;
if (_profiler && enabled) {
_profiler.StartProfiler(profiler_entries, profiler_interval,
["js", "leaf", "stackwalk", "threads"], 4,
profiler_threadsArray, profiler_threadsArray.length);
if (_profiler.PauseSampling) {
_profiler.PauseSampling();
}
}
},
finishTest: function Profiler__finishTest () {
if (_profiler && enabled) {
_profiler.dumpProfileToFile(profiler_dir + "/" + currentTest + ".sps");
_profiler.StopProfiler();
}
},
finishTestAsync: function Profiler__finishTest () {
if (!(_profiler && enabled)) {
return;
}
return new Promise((resolve, reject) => {
Services.profiler.getProfileDataAsync().then((profile) => {
let profileFile = profiler_dir + "/" + currentTest + ".sps";
Components.utils.import("resource://gre/modules/NetUtil.jsm");
Components.utils.import("resource://gre/modules/FileUtils.jsm");
var file = Components.classes["@mozilla.org/file/local;1"].
createInstance(Components.interfaces.nsILocalFile);
file.initWithPath(profileFile);
var ostream = FileUtils.openSafeFileOutputStream(file);
var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
var istream = converter.convertToInputStream(JSON.stringify(profile));
// The last argument (the callback) is optional.
NetUtil.asyncCopy(istream, ostream, function(status) {
if (!Components.isSuccessCode(status)) {
reject();
return;
}
resolve();
});
}, (error) => {
Cu.reportError("Failed to gather profile: " + error);
reject();
});
});
},
finishStartupProfiling: function Profiler__finishStartupProfiling () {
if (_profiler && enabled) {
_profiler.dumpProfileToFile(profiler_dir + "/startup.sps");
_profiler.StopProfiler();
}
},
resume: function Profiler__resume (name, explicit) {
if (_profiler) {
if (_profiler.ResumeSampling) {
_profiler.ResumeSampling();
}
_profiler.AddMarker(explicit ? name : 'Start of test "' + (name || test_name) + '"');
}
},
pause: function Profiler__pause (name, explicit) {
if (_profiler) {
if (_profiler.PauseSampling) {
_profiler.PauseSampling();
}
_profiler.AddMarker(explicit ? name : 'End of test "' + (name || test_name) + '"');
}
},
mark: function Profiler__mark (marker, explicit) {
if (_profiler) {
_profiler.AddMarker(explicit ? marker : 'Profiler: "' + (marker || test_name) + '"');
}
}
};
})();

View file

@ -0,0 +1,81 @@
var gChildProcess = true;
var gMemCallback = null;
/*
* Initialize memory collector. Determine if we have a child process.
*/
function initializeMemoryCollector(callback, args) {
gMemCallback = function() { return callback(args); };
var os = Components.classes["@mozilla.org/observer-service;1"].
getService(Components.interfaces.nsIObserverService);
os.addObserver(function () {
var os = Components.classes["@mozilla.org/observer-service;1"].
getService(Components.interfaces.nsIObserverService);
memTimer.cancel();
memTimer = null;
os.removeObserver(arguments.callee, "child-memory-reporter-update", false);
os.addObserver(collectAndReport, "child-memory-reporter-update", false);
gMemCallback();
}, "child-memory-reporter-update", false);
/*
* Assume we have a child process, but if timer fires before we call the observer
* we will assume there is no child process.
*/
var event = {
notify: function(timer) {
memTimer = null;
gChildProcess = false;
gMemCallback();
}
}
var memTimer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
memTimer.initWithCallback(event, 10000, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
os.notifyObservers(null, "child-memory-reporter-request", "generation=1 anonymize=0 minimize=0 DMDident=0");
}
/*
* Collect memory from all processes and callback when done collecting.
*/
function collectMemory(callback, args) {
gMemCallback = function() { return callback(args); };
if (gChildProcess) {
var os = Components.classes["@mozilla.org/observer-service;1"].
getService(Components.interfaces.nsIObserverService);
os.notifyObservers(null, "child-memory-reporter-request", null);
} else {
collectAndReport(null, null, null);
}
}
function collectAndReport(aSubject, aTopic, aData) {
dumpLine(collectRSS());
gMemCallback();
}
function collectRSS() {
var mgr = Components.classes["@mozilla.org/memory-reporter-manager;1"].
getService(Components.interfaces.nsIMemoryReporterManager);
return "RSS: Main: " + mgr.resident + "\n";
}
/*
* Cleanup and stop memory collector.
*/
function stopMemCollector() {
if (gChildProcess) {
var os = Cc["@mozilla.org/observer-service;1"].
getService(Ci.nsIObserverService);
os.removeObserver(collectAndReport, "child-memory-reporter-update");
}
}

View file

@ -0,0 +1,910 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
try {
if (Cc === undefined) {
var Cc = Components.classes;
var Ci = Components.interfaces;
}
} catch (ex) {}
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/Task.jsm");
Components.utils.import("resource:///modules/E10SUtils.jsm");
var NUM_CYCLES = 5;
var numPageCycles = 1;
var numRetries = 0;
var maxRetries = 3;
var pageFilterRegexp = null;
var useBrowser = true;
var winWidth = 1024;
var winHeight = 768;
var doRenderTest = false;
var pages;
var pageIndex;
var start_time;
var cycle;
var pageCycle;
var report;
var noisy = false;
var timeout = -1;
var delay = 250;
var running = false;
var forceCC = true;
var reportRSS = true;
var useMozAfterPaint = false;
var gPaintWindow = window;
var gPaintListener = false;
var loadNoCache = false;
var scrollTest = false;
var gDisableE10S = false;
var gUseE10S = false;
var profilingInfo = false;
//when TEST_DOES_OWN_TIMING, we need to store the time from the page as MozAfterPaint can be slower than pageload
var gTime = -1;
var gStartTime = -1;
var gReference = -1;
var content;
// These are binary flags. Use 1/2/4/8/...
var TEST_DOES_OWN_TIMING = 1;
var EXECUTE_SCROLL_TEST = 2;
var browserWindow = null;
var recordedName = null;
var pageUrls;
// the io service
var gIOS = null;
/**
* SingleTimeout class. Allow to register one and only one callback using
* setTimeout at a time.
*/
var SingleTimeout = function() {
this.timeoutEvent = undefined;
};
/**
* Register a callback with the given timeout.
*
* If timeout is < 0, this is a no-op.
*
* If a callback was previously registered and has not been called yet, it is
* first cleared with clear().
*/
SingleTimeout.prototype.register = function(callback, timeout) {
if (timeout >= 0) {
if (this.timeoutEvent !== undefined) {
this.clear();
}
var that = this;
this.timeoutEvent = setTimeout(function() {
that.timeoutEvent = undefined;
callback();
}, timeout);
}
};
/**
* Clear a registered callback.
*/
SingleTimeout.prototype.clear = function() {
if (this.timeoutEvent !== undefined) {
clearTimeout(this.timeoutEvent);
this.timeoutEvent = undefined;
}
};
var failTimeout = new SingleTimeout();
function plInit() {
if (running) {
return;
}
running = true;
cycle = 0;
pageCycle = 1;
try {
var args;
/*
* Desktop firefox:
* non-chrome talos runs - tp-cmdline will create and load pageloader
* into the main window of the app which displays and tests content.
* chrome talos runs - tp-cmdline does the same however pageloader
* creates a new chromed browser window below for content.
*/
// cmdline arguments are on window
args = window.arguments[0].wrappedJSObject;
var manifestURI = args.manifest;
var startIndex = 0;
var endIndex = -1;
if (args.startIndex) startIndex = parseInt(args.startIndex);
if (args.endIndex) endIndex = parseInt(args.endIndex);
if (args.numCycles) NUM_CYCLES = parseInt(args.numCycles);
if (args.numPageCycles) numPageCycles = parseInt(args.numPageCycles);
if (args.width) winWidth = parseInt(args.width);
if (args.height) winHeight = parseInt(args.height);
if (args.filter) pageFilterRegexp = new RegExp(args.filter);
if (args.noisy) noisy = true;
if (args.timeout) timeout = parseInt(args.timeout);
if (args.delay) delay = parseInt(args.delay);
if (args.mozafterpaint) useMozAfterPaint = true;
if (args.rss) reportRSS = true;
if (args.loadnocache) loadNoCache = true;
if (args.scrolltest) scrollTest = true;
if (args.disableE10S) gDisableE10S = true;
if (args.profilinginfo) profilingInfo = JSON.parse(args.profilinginfo)
if (profilingInfo) {
Profiler.initFromObject(profilingInfo);
}
forceCC = !args.noForceCC;
doRenderTest = args.doRender;
if (forceCC &&
!window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindowUtils)
.garbageCollect) {
forceCC = false;
}
gIOS = Cc["@mozilla.org/network/io-service;1"]
.getService(Ci.nsIIOService);
if (args.offline)
gIOS.offline = true;
var fileURI = gIOS.newURI(manifestURI, null, null);
pages = plLoadURLsFromURI(fileURI);
if (!pages) {
dumpLine('tp: could not load URLs, quitting');
plStop(true);
}
if (pages.length == 0) {
dumpLine('tp: no pages to test, quitting');
plStop(true);
}
if (startIndex < 0)
startIndex = 0;
if (endIndex == -1 || endIndex >= pages.length)
endIndex = pages.length-1;
if (startIndex > endIndex) {
dumpLine("tp: error: startIndex >= endIndex");
plStop(true);
}
pages = pages.slice(startIndex,endIndex+1);
pageUrls = pages.map(function(p) { return p.url.spec.toString(); });
report = new Report();
if (doRenderTest)
renderReport = new Report();
pageIndex = 0;
if (profilingInfo) {
Profiler.beginTest(getCurrentPageShortName());
}
if (args.useBrowserChrome) {
// Create a new chromed browser window for content
var wwatch = Cc["@mozilla.org/embedcomp/window-watcher;1"]
.getService(Ci.nsIWindowWatcher);
var blank = Cc["@mozilla.org/supports-string;1"]
.createInstance(Ci.nsISupportsString);
blank.data = "about:blank";
browserWindow = wwatch.openWindow
(null, "chrome://browser/content/", "_blank",
"chrome,all,dialog=no,width=" + winWidth + ",height=" + winHeight, blank);
gPaintWindow = browserWindow;
// get our window out of the way
window.resizeTo(10,10);
var browserLoadFunc = function (ev) {
browserWindow.removeEventListener('load', browserLoadFunc, true);
function firstPageCanLoadAsRemote() {
return E10SUtils.canLoadURIInProcess(pageUrls[0], Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT);
}
// do this half a second after load, because we need to be
// able to resize the window and not have it get clobbered
// by the persisted values
setTimeout(function () {
// For e10s windows, since bug 1261842, the initial browser is remote unless
// it attempts to browse to a URI that should be non-remote (landed at bug 1047603).
//
// However, when it loads such URI and reinitializes as non-remote, we lose the
// load listener and the injected tpRecordTime.
//
// The preferred pageloader behaviour in e10s is to run the pages as as remote,
// so if the page can load as remote, we will load it as remote.
//
// It also probably means that per test (or, in fact, per pageloader browser
// instance which adds the load listener and injects tpRecordTime), all the
// pages should be able to load in the same mode as the initial page - due
// to this reinitialization on the switch.
if (browserWindow.gMultiProcessBrowser) {
if (!firstPageCanLoadAsRemote())
browserWindow.XULBrowserWindow.forceInitialBrowserNonRemote(null);
// Implicit else: initial browser in e10s is remote by default.
}
browserWindow.resizeTo(winWidth, winHeight);
browserWindow.moveTo(0, 0);
browserWindow.focus();
content = browserWindow.getBrowser();
gUseE10S = !gDisableE10S || (plPageFlags() & EXECUTE_SCROLL_TEST) ||
(content.selectedBrowser &&
content.selectedBrowser.getAttribute("remote") == "true")
// Load the frame script for e10s / IPC message support
if (gUseE10S) {
let contentScript = "data:,function _contentLoadHandler(e) { " +
" if (e.originalTarget.defaultView == content) { " +
" content.wrappedJSObject.tpRecordTime = function(t, s, n) { sendAsyncMessage('PageLoader:RecordTime', { time: t, startTime: s, testName: n }); }; ";
if (useMozAfterPaint) {
contentScript += "" +
"function _contentPaintHandler() { " +
" var utils = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowUtils); " +
" if (utils.isMozAfterPaintPending) { " +
" addEventListener('MozAfterPaint', function(e) { " +
" removeEventListener('MozAfterPaint', arguments.callee, true); " +
" sendAsyncMessage('PageLoader:LoadEvent', {}); " +
" }, true); " +
" } else { " +
" sendAsyncMessage('PageLoader:LoadEvent', {}); " +
" } " +
"}; " +
"content.setTimeout(_contentPaintHandler, 0); ";
} else {
contentScript += " sendAsyncMessage('PageLoader:LoadEvent', {}); ";
}
contentScript += "" +
" }" +
"} " +
"addEventListener('load', _contentLoadHandler, true); ";
content.selectedBrowser.messageManager.loadFrameScript(contentScript, false, true);
content.selectedBrowser.messageManager.loadFrameScript("chrome://pageloader/content/talos-content.js", false);
content.selectedBrowser.messageManager.loadFrameScript("chrome://pageloader/content/tscroll.js", false, true);
content.selectedBrowser.messageManager.loadFrameScript("chrome://pageloader/content/Profiler.js", false, true);
}
if (reportRSS) {
initializeMemoryCollector(plLoadPage, 100);
} else {
setTimeout(plLoadPage, 100);
}
}, 500);
};
browserWindow.addEventListener('load', browserLoadFunc, true);
} else {
// Loading content into the initial window we create
gPaintWindow = window;
window.resizeTo(winWidth, winHeight);
content = document.getElementById('contentPageloader');
if (reportRSS) {
initializeMemoryCollector(plLoadPage, delay);
} else {
setTimeout(plLoadPage, delay);
}
}
} catch(e) {
dumpLine("pageloader exception: " + e);
plStop(true);
}
}
function plPageFlags() {
return pages[pageIndex].flags;
}
var ContentListener = {
receiveMessage: function(message) {
switch (message.name) {
case 'PageLoader:LoadEvent': return plLoadHandlerMessage(message);
case 'PageLoader:RecordTime': return plRecordTimeMessage(message);
}
},
};
// load the current page, start timing
var removeLastAddedListener = null;
var removeLastAddedMsgListener = null;
function plLoadPage() {
var pageName = pages[pageIndex].url.spec;
if (removeLastAddedListener) {
removeLastAddedListener();
removeLastAddedListener = null;
}
if (removeLastAddedMsgListener) {
removeLastAddedMsgListener();
removeLastAddedMsgListener = null;
}
if ((plPageFlags() & TEST_DOES_OWN_TIMING) && !gUseE10S) {
// if the page does its own timing, use a capturing handler
// to make sure that we can set up the function for content to call
content.addEventListener('load', plLoadHandlerCapturing, true);
removeLastAddedListener = function() {
content.removeEventListener('load', plLoadHandlerCapturing, true);
if (useMozAfterPaint) {
content.removeEventListener("MozAfterPaint", plPaintedCapturing, true);
gPaintListener = false;
}
};
} else if (!gUseE10S) {
// if the page doesn't do its own timing, use a bubbling handler
// to make sure that we're called after the page's own onload() handling
// XXX we use a capturing event here too -- load events don't bubble up
// to the <browser> element. See bug 390263.
content.addEventListener('load', plLoadHandler, true);
removeLastAddedListener = function() {
content.removeEventListener('load', plLoadHandler, true);
if (useMozAfterPaint) {
gPaintWindow.removeEventListener("MozAfterPaint", plPainted, true);
gPaintListener = false;
}
};
}
// If the test browser is remote (e10s / IPC) we need to use messages to watch for page load
if (gUseE10S) {
let mm = content.selectedBrowser.messageManager;
mm.addMessageListener('PageLoader:LoadEvent', ContentListener);
mm.addMessageListener('PageLoader:RecordTime', ContentListener);
removeLastAddedMsgListener = function() {
mm.removeMessageListener('PageLoader:LoadEvent', ContentListener);
mm.removeMessageListener('PageLoader:RecordTime', ContentListener);
};
}
failTimeout.register(loadFail, timeout);
// record which page we are about to open
Profiler.mark("Opening " + pages[pageIndex].url.path);
if (reportRSS) {
collectMemory(startAndLoadURI, pageName);
} else {
startAndLoadURI(pageName);
}
}
function startAndLoadURI(pageName) {
if (!(plPageFlags() & TEST_DOES_OWN_TIMING)) {
// Resume the profiler because we're really measuring page load time.
// If the test is doing its own timing, it'll also need to do its own
// profiler pausing / resuming.
Profiler.resume("Starting to load URI " + pageName, true);
}
start_time = Date.now();
if (loadNoCache) {
content.loadURIWithFlags(pageName, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
} else {
content.loadURI(pageName);
}
}
function getTestName() { // returns tp5n
var pageName = pages[pageIndex].url.spec;
let parts = pageName.split('/');
if (parts.length > 4) {
return parts[4];
}
return "pageloader";
}
function getCurrentPageShortName() {
var pageName = pages[pageIndex].url.spec;
let parts = pageName.split('/');
if (parts.length > 5) {
return parts[5];
}
return "page_" + pageIndex;
}
function loadFail() {
var pageName = pages[pageIndex].url.spec;
numRetries++;
if (numRetries >= maxRetries) {
dumpLine('__FAILTimeout in ' + getTestName() + '__FAIL');
dumpLine('__FAILTimeout (' + numRetries + '/' + maxRetries + ') exceeded on ' + pageName + '__FAIL');
Profiler.finishTest();
plStop(true);
} else {
dumpLine('__WARNTimeout (' + numRetries + '/' + maxRetries + ') exceeded on ' + pageName + '__WARN');
// TODO: make this a cleaner cleanup
pageCycle--;
content.removeEventListener('load', plLoadHandler, true);
content.removeEventListener('load', plLoadHandlerCapturing, true);
content.removeEventListener("MozAfterPaint", plPaintedCapturing, true);
content.removeEventListener("MozAfterPaint", plPainted, true);
gPaintWindow.removeEventListener("MozAfterPaint", plPaintedCapturing, true);
gPaintWindow.removeEventListener("MozAfterPaint", plPainted, true);
removeLastAddedListener = null;
removeLastAddedMsgListener = null;
gPaintListener = false;
//TODO: consider adding a tab and removing the old tab?!?
setTimeout(plLoadPage, delay);
}
}
var plNextPage = Task.async(function*() {
var doNextPage = false;
if (pageCycle < numPageCycles) {
pageCycle++;
doNextPage = true;
} else {
if (profilingInfo) {
yield Profiler.finishTestAsync();
}
if (pageIndex < pages.length-1) {
pageIndex++;
if (profilingInfo) {
Profiler.beginTest(getCurrentPageShortName());
}
recordedName = null;
pageCycle = 1;
doNextPage = true;
}
}
if (doNextPage == true) {
if (forceCC) {
var tccstart = new Date();
window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindowUtils)
.garbageCollect();
var tccend = new Date();
report.recordCCTime(tccend - tccstart);
// Now asynchronously trigger GC / CC in the content process if we're
// in an e10s window.
if (browserWindow.gMultiProcessBrowser) {
yield forceContentGC();
}
}
setTimeout(plLoadPage, delay);
} else {
plStop(false);
}
});
function forceContentGC() {
return new Promise((resolve) => {
let mm = browserWindow.getBrowser().selectedBrowser.messageManager;
mm.addMessageListener("Talos:ForceGC:OK", function onTalosContentForceGC(msg) {
mm.removeMessageListener("Talos:ForceGC:OK", onTalosContentForceGC);
resolve();
});
mm.sendAsyncMessage("Talos:ForceGC");
});
}
function plRecordTime(time) {
var pageName = pages[pageIndex].url.spec;
var i = pageIndex
if (i < pages.length-1) {
i++;
} else {
i = 0;
}
var nextName = pages[i].url.spec;
if (!recordedName) {
recordedName = pageUrls[pageIndex];
}
if (typeof(time) == "string") {
var times = time.split(',');
var names = recordedName.split(',');
for (var t = 0; t < times.length; t++) {
if (names.length == 1) {
report.recordTime(names, times[t]);
} else {
report.recordTime(names[t], times[t]);
}
}
} else {
report.recordTime(recordedName, time);
}
if (noisy) {
dumpLine("Cycle " + (cycle+1) + "(" + pageCycle + ")" + ": loaded " + pageName + " (next: " + nextName + ")");
}
}
function plLoadHandlerCapturing(evt) {
// make sure we pick up the right load event
if (evt.type != 'load' ||
evt.originalTarget.defaultView.frameElement)
return;
//set the tpRecordTime function (called from test pages we load) to store a global time.
content.contentWindow.wrappedJSObject.tpRecordTime = function (time, startTime, testName) {
gTime = time;
gStartTime = startTime;
recordedName = testName;
setTimeout(plWaitForPaintingCapturing, 0);
}
content.contentWindow.wrappedJSObject.plGarbageCollect = function () {
window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindowUtils)
.garbageCollect();
}
content.removeEventListener('load', plLoadHandlerCapturing, true);
removeLastAddedListener = null;
setTimeout(plWaitForPaintingCapturing, 0);
}
// Shim function this is really defined in tscroll.js
function sendScroll() {
const SCROLL_TEST_STEP_PX = 10;
const SCROLL_TEST_NUM_STEPS = 100;
// The page doesn't really use tpRecordTime. Instead, we trigger the scroll test,
// and the scroll test will call tpRecordTime which will take us to the next page
let details = {target: 'content', stepSize: SCROLL_TEST_STEP_PX, opt_numSteps: SCROLL_TEST_NUM_STEPS};
let mm = content.selectedBrowser.messageManager;
mm.sendAsyncMessage("PageLoader:ScrollTest", { details: details });
}
function plWaitForPaintingCapturing() {
if (gPaintListener)
return;
var utils = gPaintWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindowUtils);
if (utils.isMozAfterPaintPending && useMozAfterPaint) {
if (gPaintListener == false)
gPaintWindow.addEventListener("MozAfterPaint", plPaintedCapturing, true);
gPaintListener = true;
return;
}
_loadHandlerCapturing();
}
function plPaintedCapturing() {
gPaintWindow.removeEventListener("MozAfterPaint", plPaintedCapturing, true);
gPaintListener = false;
_loadHandlerCapturing();
}
function _loadHandlerCapturing() {
failTimeout.clear();
if (!(plPageFlags() & TEST_DOES_OWN_TIMING)) {
dumpLine("tp: Capturing onload handler used with page that doesn't do its own timing?");
plStop(true);
}
if (useMozAfterPaint) {
if (gStartTime != null && gStartTime >= 0) {
gTime = (new Date()) - gStartTime;
gStartTime = -1;
}
}
if (gTime !== -1) {
plRecordTime(gTime);
Profiler.pause("capturing load handler fired", true);
gTime = -1;
recordedName = null;
setTimeout(plNextPage, delay);
}
}
// the onload handler
function plLoadHandler(evt) {
// make sure we pick up the right load event
if (evt.type != 'load' ||
evt.originalTarget.defaultView.frameElement)
return;
content.removeEventListener('load', plLoadHandler, true);
setTimeout(waitForPainted, 0);
}
// This is called after we have received a load event, now we wait for painted
function waitForPainted() {
var utils = gPaintWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindowUtils);
if (!utils.isMozAfterPaintPending || !useMozAfterPaint) {
_loadHandler();
return;
}
if (gPaintListener == false)
gPaintWindow.addEventListener("MozAfterPaint", plPainted, true);
gPaintListener = true;
}
function plPainted() {
gPaintWindow.removeEventListener("MozAfterPaint", plPainted, true);
gPaintListener = false;
_loadHandler();
}
function _loadHandler() {
failTimeout.clear();
var end_time = Date.now();
var time = (end_time - start_time);
Profiler.pause("Bubbling load handler fired.", true);
// does this page want to do its own timing?
// if so, we shouldn't be here
if (plPageFlags() & TEST_DOES_OWN_TIMING) {
dumpLine("tp: Bubbling onload handler used with page that does its own timing?");
plStop(true);
}
plRecordTime(time);
if (doRenderTest)
runRenderTest();
plNextPage();
}
// the core handler for remote (e10s) browser
function plLoadHandlerMessage() {
failTimeout.clear();
if ((plPageFlags() & EXECUTE_SCROLL_TEST)) {
// Let the page settle down after its load event, then execute the scroll test.
setTimeout(sendScroll, 500);
} else if ((plPageFlags() & TEST_DOES_OWN_TIMING)) {
var time;
if (typeof(gStartTime) != "number")
gStartTime = Date.parse(gStartTime);
if (gTime !== -1) {
if (useMozAfterPaint && gStartTime >= 0) {
time = Date.now() - gStartTime;
gStartTime = -1;
} else if (!useMozAfterPaint) {
time = gTime;
}
gTime = -1;
}
if (time !== undefined) {
plRecordTime(time);
if (doRenderTest)
runRenderTest();
plNextPage();
}
} else {
_loadHandler();
}
}
// the record time handler used for remote (e10s) browser
function plRecordTimeMessage(message) {
gTime = message.json.time;
gStartTime = message.json.startTime;
recordedName = message.json.testName;
if (useMozAfterPaint) {
gStartTime = message.json.startTime;
}
_loadHandlerCapturing();
}
function runRenderTest() {
const redrawsPerSample = 500;
if (!Ci.nsIDOMWindowUtils)
return;
var win;
if (browserWindow)
win = content.contentWindow;
else
win = window;
var wu = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
var start = Date.now();
for (var j = 0; j < redrawsPerSample; j++)
wu.redraw();
var end = Date.now();
renderReport.recordTime(pageIndex, end - start);
}
function plStop(force) {
if (reportRSS) {
collectMemory(plStopAll, force);
} else {
plStopAll(force);
}
}
function plStopAll(force) {
try {
if (force == false) {
pageIndex = 0;
pageCycle = 1;
if (cycle < NUM_CYCLES-1) {
cycle++;
recordedName = null;
setTimeout(plLoadPage, delay);
return;
}
/* output report */
dumpLine(report.getReport());
dumpLine(report.getReportSummary());
}
} catch (e) {
dumpLine(e);
}
if (reportRSS) {
stopMemCollector();
}
if (content) {
content.removeEventListener('load', plLoadHandlerCapturing, true);
content.removeEventListener('load', plLoadHandler, true);
if (useMozAfterPaint)
content.removeEventListener("MozAfterPaint", plPaintedCapturing, true);
content.removeEventListener("MozAfterPaint", plPainted, true);
if (gUseE10S) {
let mm = content.selectedBrowser.messageManager;
mm.removeMessageListener('PageLoader:LoadEvent', ContentListener);
mm.removeMessageListener('PageLoader:RecordTime', ContentListener);
mm.loadFrameScript("data:,removeEventListener('load', _contentLoadHandler, true);", false, true);
}
}
if (MozillaFileLogger && MozillaFileLogger._foStream)
MozillaFileLogger.close();
goQuitApplication();
}
/* Returns array */
function plLoadURLsFromURI(manifestUri) {
var fstream = Cc["@mozilla.org/network/file-input-stream;1"]
.createInstance(Ci.nsIFileInputStream);
var uriFile = manifestUri.QueryInterface(Ci.nsIFileURL);
if (uriFile.file.isFile() === false) {
dumpLine("tp: invalid file: %s" % uriFile.file);
return null;
}
try {
fstream.init(uriFile.file, -1, 0, 0);
} catch(ex) {
dumpLine("tp: the file %s doesn't exist" % uriFile.file);
return null;
}
var lstream = fstream.QueryInterface(Ci.nsILineInputStream);
var d = [];
var lineNo = 0;
var line = {value:null};
var more;
do {
lineNo++;
more = lstream.readLine(line);
var s = line.value;
// strip comments (only leading ones)
s = s.replace(/^#.*/, '');
// strip leading and trailing whitespace
s = s.replace(/^\s*/, '').replace(/\s*$/, '');
if (!s)
continue;
var flags = 0;
var urlspec = s;
// split on whitespace, and figure out if we have any flags
var items = s.split(/\s+/);
if (items[0] == "include") {
if (items.length != 2) {
dumpLine("tp: Error on line " + lineNo + " in " + manifestUri.spec + ": include must be followed by the manifest to include!");
return null;
}
var subManifest = gIOS.newURI(items[1], null, manifestUri);
if (subManifest == null) {
dumpLine("tp: invalid URI on line " + manifestUri.spec + ":" + lineNo + " : '" + line.value + "'");
return null;
}
var subItems = plLoadURLsFromURI(subManifest);
if (subItems == null)
return null;
d = d.concat(subItems);
} else {
// For scrollTest flag, we accept "normal" pages but treat them as TEST_DOES_OWN_TIMING
// together with EXECUTE_SCROLL_TEST which makes us run the scroll test on load.
// We do this by artificially "injecting" the TEST_DOES_OWN_TIMING flag ("%") to the item
// and then let the default flow for this flag run without further modifications
// (other than calling the scroll test once the page is loaded).
// Note that if we have the scrollTest flag but the item already has "%", then we do
// nothing (the scroll test will not execute, and the page will report with its
// own tpRecordTime and not the one from the scroll test).
if (scrollTest && items.length == 1) { // scroll enabled and no "%"
items.unshift("%");
flags |= EXECUTE_SCROLL_TEST;
}
if (items.length == 2) {
if (items[0].indexOf("%") != -1)
flags |= TEST_DOES_OWN_TIMING;
urlspec = items[1];
} else if (items.length != 1) {
dumpLine("tp: Error on line " + lineNo + " in " + manifestUri.spec + ": whitespace must be %-escaped!");
return null;
}
var url = gIOS.newURI(urlspec, null, manifestUri);
if (pageFilterRegexp && !pageFilterRegexp.test(url.spec))
continue;
d.push({ url: url,
flags: flags });
}
} while (more);
return d;
}
function dumpLine(str) {
if (MozillaFileLogger && MozillaFileLogger._foStream)
MozillaFileLogger.log(str + "\n");
dump(str);
dump("\n");
}

View file

@ -0,0 +1,58 @@
<?xml version="1.0"?>
<!-- ***** BEGIN LICENSE BLOCK *****
- Version: MPL 1.1/GPL 2.0/LGPL 2.1
-
- The contents of this file are subject to the Mozilla Public License Version
- 1.1 (the "License"); you may not use this file except in compliance with
- the License. You may obtain a copy of the License at
- http://www.mozilla.org/MPL/
-
- Software distributed under the License is distributed on an "AS IS" basis,
- WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- for the specific language governing rights and limitations under the
- License.
-
- The Original Code is tp.
-
- The Initial Developer of the Original Code is the Mozilla Corporation.
- Portions created by the Initial Developer are Copyright (C) 2007
- the Initial Developer. All Rights Reserved.
-
- Contributor(s):
- Rob Helmer <rhelmer@mozilla.com>
- Vladimir Vukicevic <vladimir@mozilla.com>
-
- Alternatively, the contents of this file may be used under the terms of
- either the GNU General Public License Version 2 or later (the "GPL"), or
- the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- in which case the provisions of the GPL or the LGPL are applicable instead
- of those above. If you wish to allow use of your version of this file only
- under the terms of either the GPL or the LGPL, and not to allow others to
- use your version of this file under the terms of the MPL, indicate your
- decision by deleting the provisions above and replace them with the notice
- and other provisions required by the LGPL or the GPL. If you do not delete
- the provisions above, a recipient may use your version of this file under
- the terms of any one of the MPL, the GPL or the LGPL.
-
- ***** END LICENSE BLOCK ***** -->
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<window
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
title="pageloader"
screenX="0" screenY="0"
onload="plInit()">
<script type="application/x-javascript"
src="chrome://global/content/globalOverlay.js"/>
<script type="application/x-javascript" src="Profiler.js"></script>
<script type="application/x-javascript" src="MozillaFileLogger.js"></script>
<script type="application/x-javascript" src="report.js"></script>
<script type="application/x-javascript" src="pageloader.js"></script>
<script type="application/x-javascript" src="quit.js"></script>
<script type="application/x-javascript" src="memory.js"></script>
<browser id="contentPageloader" src="about:blank"
type="content" flex="1"/>
</window>

View file

@ -0,0 +1,110 @@
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; -*- */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is The Original Code is Mozilla Automated Testing Code
*
* The Initial Developer of the Original Code is
* Mozilla Corporation.
* Portions created by the Initial Developer are Copyright (C) 2005
* the Initial Developer. All Rights Reserved.
*
* Contributor(s): Bob Clary <bob@bclary.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/*
From mozilla/toolkit/content
These files did not have a license
*/
function canQuitApplication()
{
var os = Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService);
if (!os)
{
return true;
}
try
{
var cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"]
.createInstance(Components.interfaces.nsISupportsPRBool);
os.notifyObservers(cancelQuit, "quit-application-requested", null);
// Something aborted the quit process.
if (cancelQuit.data)
{
return false;
}
}
catch (ex)
{
}
return true;
}
function goQuitApplication()
{
if (!canQuitApplication())
{
return false;
}
const kAppStartup = '@mozilla.org/toolkit/app-startup;1';
const kAppShell = '@mozilla.org/appshell/appShellService;1';
var appService;
var forceQuit;
if (kAppStartup in Components.classes)
{
appService = Components.classes[kAppStartup].
getService(Components.interfaces.nsIAppStartup);
forceQuit = Components.interfaces.nsIAppStartup.eForceQuit;
}
else if (kAppShell in Components.classes)
{
appService = Components.classes[kAppShell].
getService(Components.interfaces.nsIAppShellService);
forceQuit = Components.interfaces.nsIAppShellService.eForceQuit;
}
else
{
throw 'goQuitApplication: no AppStartup/appShell';
}
try
{
appService.quit(forceQuit);
}
catch(ex)
{
throw('goQuitApplication: ' + ex);
}
return true;
}

View file

@ -0,0 +1,159 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// given an array of strings, finds the longest common prefix
function findCommonPrefixLength(strs) {
if (strs.length < 2)
return 0;
var len = 0;
do {
var newlen = len + 1;
var newprefix = null;
var failed = false;
for (var i = 0; i < strs.length; i++) {
if (newlen > strs[i].length) {
failed = true;
break;
}
var s = strs[i].substr(0, newlen);
if (newprefix == null) {
newprefix = s;
} else if (newprefix != s) {
failed = true;
break;
}
}
if (failed)
break;
len++;
} while (true);
return len;
}
// Constructor
function Report() {
this.timeVals = {};
this.totalCCTime = 0;
this.showTotalCCTime = false;
}
Report.prototype.pageNames = function() {
var retval = new Array();
for (var page in this.timeVals) {
retval.push(page);
}
return retval;
}
Report.prototype.getReport = function() {
var report;
var pages = this.pageNames();
var prefixLen = findCommonPrefixLength(pages);
report = "__start_tp_report\n";
report += "_x_x_mozilla_page_load\n";
report += "_x_x_mozilla_page_load_details\n";
report += "|i|pagename|runs|\n";
for (var i=0; i < pages.length; i++) {
report += '|'+
i + ';'+
pages[i].substr(prefixLen) + ';'+
this.timeVals[pages[i]].join(";") +
"\n";
}
report += "__end_tp_report\n";
if (this.showTotalCCTime) {
report += "__start_cc_report\n";
report += "_x_x_mozilla_cycle_collect," + this.totalCCTime + "\n";
report += "__end_cc_report\n";
}
var now = (new Date()).getTime();
report += "__startTimestamp" + now + "__endTimestamp\n"; //timestamp for determning shutdown time, used by talos
return report;
}
Report.prototype.getReportSummary = function() {
function average(arr) {
var sum = 0;
for (var i in arr)
sum += arr[i];
return sum / (arr.length || 1);
}
function median(arr) {
if (!arr.length)
return 0; // As good indication for "not available" as any other value.
var sorted = arr.slice(0).sort();
var mid = Math.floor(arr.length / 2);
if (sorted.length % 2)
return sorted[mid];
return average(sorted.slice(mid, mid + 2));
}
// We use sample stddev and not population stddev because
// well.. it's a sample and we can't collect all/infinite number of values.
function stddev(arr) {
if (arr.length <= 1) {
return 0;
}
var avg = average(arr);
var squareDiffArr = arr.map( function(v) { return Math.pow(v - avg, 2); } );
var sum = squareDiffArr.reduce( function(a, b) { return a + b; } );
var rv = Math.sqrt(sum / (arr.length - 1));
return rv;
}
var report = "";
var pages = this.pageNames();
var prefixLen = findCommonPrefixLength(pages);
report += "------- Summary: start -------\n";
report += "Number of tests: " + pages.length + "\n";
for (var i=0; i < pages.length; i++) {
var results = this.timeVals[pages[i]].map(function(v) {
return Number(v);
});
report += '\n[#'+ i + '] ' + pages[i].substr(prefixLen)
+ ' Cycles:' + results.length
+ ' Average:' + average(results).toFixed(2)
+ ' Median:' + median(results).toFixed(2)
+ ' stddev:' + stddev(results).toFixed(2)
+ ' (' + (100 * stddev(results) / median(results)).toFixed(1) + '%)'
+ (results.length < 5 ? '' : (' stddev-sans-first:' + stddev(results.slice(1)).toFixed(2)))
+ '\nValues: ' + results.map(function(v) {
return v.toFixed(1);
}).join(" ")
+ "\n";
}
report += "-------- Summary: end --------\n";
return report;
}
Report.prototype.recordTime = function(pageName, ms) {
if (this.timeVals[pageName] == undefined) {
this.timeVals[pageName] = new Array();
}
this.timeVals[pageName].push(ms);
}
Report.prototype.recordCCTime = function(ms) {
this.totalCCTime += ms;
this.showTotalCCTime = true;
}

View file

@ -0,0 +1,26 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
const TalosContent = {
init() {
addMessageListener("Talos:ForceGC", this);
},
receiveMessage(msg) {
if (msg.name == "Talos:ForceGC") {
this.forceGC();
}
},
forceGC() {
content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.garbageCollect();
sendAsyncMessage("Talos:ForceGC:OK");
},
};
TalosContent.init();

View file

@ -0,0 +1,272 @@
// Note: This file is used at both tscrollx and tp5o_scroll. With the former as
// unprivileged code.
// - Please make sure that any changes apply cleanly to all use cases.
function testScroll(target, stepSize, opt_reportFunc, opt_numSteps)
{
var win;
if (target == "content") {
target = content.wrappedJSObject;
win = content;
} else {
win = window;
}
var result = {
names: [],
values: [],
};
// We report multiple results, so we base the name on the path.
// Everything after '/tp5n/' if exists (for tp5o_scroll), or the file name at
// the path if non-empty (e.g. with tscrollx), or the last dir otherwise (e.g.
// 'mydir' for 'http://my.domain/dir1/mydir/').
var href = win.location.href;
var testBaseName = href.split("/tp5n/")[1]
|| href.split("/").pop()
|| href.split("/").splice(-2, 1)[0]
|| "REALLY_WEIRD_URI";
// Verbatim copy from talos-powers/content/TalosPowersContent.js
// If the origin changes, this copy should be updated.
TalosPowersParent = {
replyId: 1,
// dispatch an event to the framescript and register the result/callback event
exec: function(commandName, arg, callback, opt_custom_window) {
let win = opt_custom_window || window;
let replyEvent = "TalosPowers:ParentExec:ReplyEvent:" + this.replyId++;
if (callback) {
win.addEventListener(replyEvent, function rvhandler(e) {
win.removeEventListener(replyEvent, rvhandler);
callback(e.detail);
});
}
win.dispatchEvent(
new win.CustomEvent("TalosPowers:ParentExec:QueryEvent", {
bubbles: true,
detail: {
command: {
name: commandName,
data: arg,
},
listeningTo: replyEvent,
}
})
);
},
};
// End of code from talos-powers
var report;
/**
* Sets up the value of 'report' as a function for reporting the test result[s].
* Chooses between the "usual" tpRecordTime which the pageloader addon injects
* to pages, or a custom function in case we're a framescript which pageloader
* added to the tested page, or a debug tpRecordTime from talos-debug.js if
* running in a plain browser.
*
* @returns Promise
*/
function P_setupReportFn() {
return new Promise(function(resolve) {
report = opt_reportFunc || win.tpRecordTime;
if (report == 'PageLoader:RecordTime') {
report = function(duration, start, name) {
var msg = { time: duration, startTime: start, testName: name };
sendAsyncMessage('PageLoader:RecordTime', msg);
}
resolve();
return;
}
// Not part of the test and does nothing if we're within talos.
// Provides an alternative tpRecordTime (with some stats display) if running in a browser.
if (!report && document.head) {
var imported = document.createElement('script');
imported.addEventListener("load", function() {
report = tpRecordTime;
resolve();
});
imported.src = '../../scripts/talos-debug.js?dummy=' + Date.now(); // For some browsers to re-read
document.head.appendChild(imported);
return;
}
resolve();
});
}
function FP_wait(ms) {
return function() {
return new Promise(function(resolve) {
setTimeout(resolve, ms);
});
};
}
function rAF(fn) {
return content.requestAnimationFrame(fn);
}
function P_rAF() {
return new Promise(function(resolve) {
rAF(resolve);
});
}
function myNow() {
return (win.performance && win.performance.now) ?
win.performance.now() :
Date.now();
};
var isWindow = target.self === target;
var getPos = isWindow ? function() { return target.pageYOffset; }
: function() { return target.scrollTop; };
var gotoTop = isWindow ? function() { target.scroll(0, 0); ensureScroll(); }
: function() { target.scrollTop = 0; ensureScroll(); };
var doScrollTick = isWindow ? function() { target.scrollBy(0, stepSize); ensureScroll(); }
: function() { target.scrollTop += stepSize; ensureScroll(); };
var setSmooth = isWindow ? function() { target.document.scrollingElement.style.scrollBehavior = "smooth"; }
: function() { target.style.scrollBehavior = "smooth"; };
var gotoBottom = isWindow ? function() { target.scrollTo(0, target.scrollMaxY); }
: function() { target.scrollTop = target.scrollHeight; };
function ensureScroll() { // Ensure scroll by reading computed values. screenY is for X11.
if (!this.dummyEnsureScroll) {
this.dummyEnsureScroll = 1;
}
this.dummyEnsureScroll += win.screenY + getPos();
}
// For reference, rAF should fire on vsync, but Gecko currently doesn't use vsync.
// Instead, it uses 1000/layout.frame_rate
// (with 60 as default value when layout.frame_rate == -1).
function P_syncScrollTest() {
return new Promise(function(resolve) {
// We should be at the top of the page now.
var start = myNow();
var lastScrollPos = getPos();
var lastScrollTime = start;
var durations = [];
function tick() {
var now = myNow();
var duration = now - lastScrollTime;
lastScrollTime = now;
durations.push(duration);
doScrollTick();
/* stop scrolling if we can't scroll more, or if we've reached requested number of steps */
if ((getPos() == lastScrollPos) || (opt_numSteps && (durations.length >= (opt_numSteps + 2)))) {
if (typeof(Profiler) !== "undefined") {
Profiler.pause();
}
// Note: The first (1-5) intervals WILL be longer than the rest.
// First interval might include initial rendering and be extra slow.
// Also requestAnimationFrame needs to sync (optimally in 1 frame) after long frames.
// Suggested: Ignore the first 5 intervals.
durations.pop(); // Last step was 0.
durations.pop(); // and the prev one was shorter and with end-of-page logic, ignore both.
if (win.talosDebug)
win.talosDebug.displayData = true; // In a browser: also display all data points.
// For analysis (otherwise, it's too many data points for talos):
var sum = 0;
for (var i = 0; i < durations.length; i++)
sum += Number(durations[i]);
// Report average interval or (failsafe) 0 if no intervls were recorded
result.values.push(durations.length ? sum / durations.length : 0);
result.names.push(testBaseName);
resolve();
return;
}
lastScrollPos = getPos();
rAF(tick);
}
if (typeof(Profiler) !== "undefined") {
Profiler.resume();
}
rAF(tick);
});
}
function P_testAPZScroll() {
var APZ_MEASURE_MS = 1000;
function startFrameTimeRecording(cb) {
TalosPowersParent.exec("startFrameTimeRecording", null, cb, win);
}
function stopFrameTimeRecording(handle, cb) {
TalosPowersParent.exec("stopFrameTimeRecording", handle, cb, win);
}
return new Promise(function(resolve, reject) {
setSmooth();
var startts = Date.now();
var handle = -1;
startFrameTimeRecording(function(rv) {
handle = rv;
});
// Get the measurements after APZ_MEASURE_MS of scrolling
setTimeout(function() {
var endts = Date.now();
stopFrameTimeRecording(handle, function(intervals) {
function average(arr) {
var sum = 0;
for(var i = 0; i < arr.length; i++)
sum += arr[i];
return arr.length ? sum / arr.length : 0;
}
// remove two frames on each side of the recording to get a cleaner result
result.values.push(average(intervals.slice(2, intervals.length - 2)));
result.names.push("CSSOM." + testBaseName);
resolve();
});
}, APZ_MEASURE_MS);
gotoBottom(); // trigger the APZ scroll
});
}
P_setupReportFn()
.then(FP_wait(260))
.then(gotoTop)
.then(P_rAF)
.then(P_syncScrollTest)
.then(gotoTop)
.then(FP_wait(260))
.then(P_testAPZScroll)
.then(function() {
report(result.values.join(","), 0, result.names.join(","));
});
}
// This code below here is unique to tscroll.js inside of pageloader
try {
function handleMessageFromChrome(message) {
var payload = message.data.details;
testScroll(payload.target, payload.stepSize, 'PageLoader:RecordTime', payload.opt_numSteps);
}
addMessageListener("PageLoader:ScrollTest", handleMessageFromChrome);
} catch (e) {}

View file

@ -0,0 +1,206 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is DOM Inspector.
*
* The Initial Developer of the Original Code is
* Christopher A. Aillon <christopher@aillon.com>.
* Portions created by the Initial Developer are Copyright (C) 2003
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Christopher A. Aillon <christopher@aillon.com>
* L. David Baron, Mozilla Corporation <dbaron@dbaron.org> (modified for reftest)
* Vladimir Vukicevic, Mozilla Corporation <dbaron@dbaron.org> (modified for tp)
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
// This only implements nsICommandLineHandler, since it needs
// to handle multiple arguments.
const TP_CMDLINE_CONTRACTID = "@mozilla.org/commandlinehandler/general-startup;1?type=tp";
const TP_CMDLINE_CLSID = Components.ID('{8AF052F5-8EFE-4359-8266-E16498A82E8B}');
const CATMAN_CONTRACTID = "@mozilla.org/categorymanager;1";
const nsISupports = Components.interfaces.nsISupports;
const nsICategoryManager = Components.interfaces.nsICategoryManager;
const nsICommandLine = Components.interfaces.nsICommandLine;
const nsICommandLineHandler = Components.interfaces.nsICommandLineHandler;
const nsIComponentRegistrar = Components.interfaces.nsIComponentRegistrar;
const nsISupportsString = Components.interfaces.nsISupportsString;
const nsIWindowWatcher = Components.interfaces.nsIWindowWatcher;
Components.utils.import("resource://gre/modules/Services.jsm");
function PageLoaderCmdLineHandler() {}
PageLoaderCmdLineHandler.prototype =
{
/* nsISupports */
QueryInterface : function handler_QI(iid) {
if (iid.equals(nsISupports))
return this;
if (nsICommandLineHandler && iid.equals(nsICommandLineHandler))
return this;
throw Components.results.NS_ERROR_NO_INTERFACE;
},
/* nsICommandLineHandler */
handle : function handler_handle(cmdLine) {
var args = {};
try {
var uristr = cmdLine.handleFlagWithParam("tp", false);
if (uristr == null)
return;
try {
args.manifest = cmdLine.resolveURI(uristr).spec;
} catch (e) {
return;
}
args.numCycles = cmdLine.handleFlagWithParam("tpcycles", false);
args.numPageCycles = cmdLine.handleFlagWithParam("tppagecycles", false);
args.startIndex = cmdLine.handleFlagWithParam("tpstart", false);
args.endIndex = cmdLine.handleFlagWithParam("tpend", false);
args.filter = cmdLine.handleFlagWithParam("tpfilter", false);
args.useBrowserChrome = cmdLine.handleFlag("tpchrome", false);
args.doRender = cmdLine.handleFlag("tprender", false);
args.width = cmdLine.handleFlagWithParam("tpwidth", false);
args.height = cmdLine.handleFlagWithParam("tpheight", false);
args.profilinginfo = cmdLine.handleFlagWithParam("tpprofilinginfo", false);
args.offline = cmdLine.handleFlag("tpoffline", false);
args.noisy = cmdLine.handleFlag("tpnoisy", false);
args.timeout = cmdLine.handleFlagWithParam("tptimeout", false);
args.delay = cmdLine.handleFlagWithParam("tpdelay", false);
args.noForceCC = cmdLine.handleFlag("tpnoforcecc", false);
args.mozafterpaint = cmdLine.handleFlag("tpmozafterpaint", false);
args.loadnocache = cmdLine.handleFlag("tploadnocache", false);
args.scrolltest = cmdLine.handleFlag("tpscrolltest", false);
args.disableE10s = cmdLine.handleFlag("tpdisable_e10s", false);
args.rss = cmdLine.handleFlag("rss", false);
}
catch (e) {
return;
}
let chromeURL = "chrome://pageloader/content/pageloader.xul";
args.wrappedJSObject = args;
Services.ww.openWindow(null, chromeURL, "_blank",
"chrome,dialog=no,all", args);
// Don't pass command line to the default app processor
cmdLine.preventDefault = true;
},
helpInfo :
" -tp <file> Run pageload perf tests on given manifest\n" +
" -tpfilter str Only include pages from manifest that contain str (regexp)\n" +
" -tpcycles n Loop through pages n times\n" +
" -tppagecycles n Loop through each page n times before going onto the next page\n" +
" -tpstart n Start at index n in the manifest\n" +
" -tpend n End with index n in the manifest\n" +
" -tpchrome Test with normal browser chrome\n" +
" -tprender Run render-only benchmark for each page\n" +
" -tpwidth width Width of window\n" +
" -tpheight height Height of window\n" +
" -tbprofilinginfo A JSON object describing profiler settings\n" +
" -tpoffline Force offline mode\n" +
" -tpnoisy Dump the name of the last loaded page to console\n" +
" -tptimeout Max amount of time given for a page to load, quit if exceeded\n" +
" -tpdelay Amount of time to wait between each pageload\n" +
" -tpnoforcecc Don't force cycle collection between each pageload\n" +
" -tpmozafterpaint Measure Time after recieving MozAfterPaint event instead of load event\n" +
" -tpscrolltest Unknown\n" +
" -tpdisable_e10s disable pageloader e10s code path\n" +
" -rss Dump RSS after each page is loaded\n"
};
var PageLoaderCmdLineFactory =
{
createInstance : function(outer, iid)
{
if (outer != null) {
throw Components.results.NS_ERROR_NO_AGGREGATION;
}
return new PageLoaderCmdLineHandler().QueryInterface(iid);
}
};
function NSGetFactory(cid) {
if (!cid.equals(TP_CMDLINE_CLSID))
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
return PageLoaderCmdLineFactory;
}
var PageLoaderCmdLineModule =
{
registerSelf : function(compMgr, fileSpec, location, type)
{
compMgr = compMgr.QueryInterface(nsIComponentRegistrar);
compMgr.registerFactoryLocation(TP_CMDLINE_CLSID,
"PageLoader CommandLine Service",
TP_CMDLINE_CONTRACTID,
fileSpec,
location,
type);
var catman = Components.classes[CATMAN_CONTRACTID].getService(nsICategoryManager);
catman.addCategoryEntry("command-line-handler",
"m-tp",
TP_CMDLINE_CONTRACTID, true, true);
},
unregisterSelf : function(compMgr, fileSpec, location)
{
compMgr = compMgr.QueryInterface(nsIComponentRegistrar);
compMgr.unregisterFactoryLocation(TP_CMDLINE_CLSID, fileSpec);
catman = Components.classes[CATMAN_CONTRACTID].getService(nsICategoryManager);
catman.deleteCategoryEntry("command-line-handler",
"m-tp", true);
},
getClassObject : function(compMgr, cid, iid)
{
return NSGetFactory(cid);
},
canUnload : function(compMgr)
{
return true;
}
};
function NSGetModule(compMgr, fileSpec) {
return PageLoaderCmdLineModule;
}

View file

@ -0,0 +1,21 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>pageloader@mozilla.org</em:id>
<em:version>1.0.13</em:version>
<em:targetApplication>
<Description>
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
<em:minVersion>44.0</em:minVersion>
<em:maxVersion>*</em:maxVersion>
</Description>
</em:targetApplication>
<!-- Front End MetaData -->
<em:name>PageLoader extension</em:name>
<em:description>Cycles through pages and measures load times</em:description>
<em:creator>Vladimir Vukicevic</em:creator>
<em:multiprocessCompatible>true</em:multiprocessCompatible>
</Description>
</RDF>

Binary file not shown.

View file

@ -0,0 +1,3 @@
The files in this directory were imported from https://github.com/vdjeric/Snappy-Symbolication-Server/ and https://github.com/mstange/analyze-tryserver-profiles/ with the permission of their respective authors. Some of the files were then cleaned up to remove code that we don't use.
The dump_syms_mac binary was copied from the objdir of a Firefox build on Mac. It's a byproduct of the regular Firefox build process and gets generated in objdir/dist/host/bin/.

View file

Binary file not shown.

View file

@ -0,0 +1,10 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import json
def save_profile(profile, filename):
with open(filename, "w") as f:
json.dump(profile, f)

View file

@ -0,0 +1,313 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from symLogging import LogTrace, LogError, LogMessage
import itertools
import os
import re
import threading
import time
from bisect import bisect
# Libraries to keep prefetched
PREFETCHED_LIBS = ["xul.pdb", "firefox.pdb"]
class SymbolInfo:
def __init__(self, addressMap):
self.sortedAddresses = sorted(addressMap.keys())
self.sortedSymbols = [addressMap[address]
for address in self.sortedAddresses]
self.entryCount = len(self.sortedAddresses)
# TODO: Add checks for address < funcEnd ?
def Lookup(self, address):
nearest = bisect(self.sortedAddresses, address) - 1
if nearest < 0:
return None
return self.sortedSymbols[nearest]
def GetEntryCount(self):
return self.entryCount
# Singleton for .sym / .nmsym file cache management
class SymFileManager:
# Symbol cache data structures
sCache = {}
sCacheCount = 0
sCacheLock = threading.Lock()
sMruSymbols = []
sOptions = {}
sCallbackTimer = None
def __init__(self, options):
self.sOptions = options
def GetLibSymbolMap(self, libName, breakpadId, symbolSources):
# Empty lib name means client couldn't associate frame with any lib
if libName == "":
return None
# Check cache first
libSymbolMap = None
self.sCacheLock.acquire()
try:
if libName in self.sCache and breakpadId in self.sCache[libName]:
libSymbolMap = self.sCache[libName][breakpadId]
self.UpdateMruList(libName, breakpadId)
finally:
self.sCacheLock.release()
if libSymbolMap is None:
LogTrace(
"Need to fetch PDB file for " + libName + " " + breakpadId)
# Guess the name of the .sym or .nmsym file on disk
if libName[-4:] == ".pdb":
symFileNameWithoutExtension = re.sub(r"\.[^\.]+$", "", libName)
else:
symFileNameWithoutExtension = libName
# Look in the symbol dirs for this .sym or .nmsym file
for extension, source in itertools.product([".sym", ".nmsym"],
symbolSources):
symFileName = symFileNameWithoutExtension + extension
pathSuffix = os.sep + libName + os.sep + \
breakpadId + os.sep + symFileName
path = self.sOptions["symbolPaths"][source] + pathSuffix
libSymbolMap = self.FetchSymbolsFromFile(path)
if libSymbolMap:
break
if not libSymbolMap:
LogTrace("No matching sym files, tried " + str(symbolSources))
return None
LogTrace("Storing libSymbolMap under [" + libName + "][" +
breakpadId + "]")
self.sCacheLock.acquire()
try:
self.MaybeEvict(libSymbolMap.GetEntryCount())
if libName not in self.sCache:
self.sCache[libName] = {}
self.sCache[libName][breakpadId] = libSymbolMap
self.sCacheCount += libSymbolMap.GetEntryCount()
self.UpdateMruList(libName, breakpadId)
LogTrace(str(self.sCacheCount) +
" symbols in cache after fetching symbol file")
finally:
self.sCacheLock.release()
return libSymbolMap
def FetchSymbolsFromFile(self, path):
try:
symFile = open(path, "r")
except Exception as e:
LogTrace("Error opening file " + path + ": " + str(e))
return None
LogMessage("Parsing SYM file at " + path)
try:
symbolMap = {}
lineNum = 0
publicCount = 0
funcCount = 0
if path.endswith(".sym"):
for line in symFile:
lineNum += 1
if line[0:7] == "PUBLIC ":
line = line.rstrip()
fields = line.split(" ")
if len(fields) < 4:
LogTrace("Line " + str(lineNum) + " is messed")
continue
address = int(fields[1], 16)
symbolMap[address] = " ".join(fields[3:])
publicCount += 1
elif line[0:5] == "FUNC ":
line = line.rstrip()
fields = line.split(" ")
if len(fields) < 5:
LogTrace("Line " + str(lineNum) + " is messed")
continue
address = int(fields[1], 16)
symbolMap[address] = " ".join(fields[4:])
funcCount += 1
elif path.endswith(".nmsym"):
addressLength = 0
for line in symFile:
lineNum += 1
if line.startswith(" "):
continue
if addressLength == 0:
addressLength = line.find(" ")
address = int(line[0:addressLength], 16)
# Some lines have the form
# "address space letter space symbol",
# some have the form "address space symbol".
# The letter has a meaning, but we ignore it.
if line[addressLength + 2] == " ":
symbol = line[addressLength + 3:].rstrip()
else:
symbol = line[addressLength + 1:].rstrip()
symbolMap[address] = symbol
publicCount += 1
except Exception as e:
LogError("Error parsing SYM file " + path)
return None
logString = "Found " + \
str(len(symbolMap.keys())) + " unique entries from "
logString += str(publicCount) + " PUBLIC lines, " + \
str(funcCount) + " FUNC lines"
LogTrace(logString)
return SymbolInfo(symbolMap)
def PrefetchRecentSymbolFiles(self):
global PREFETCHED_LIBS
LogMessage("Prefetching recent symbol files")
# Schedule next timer callback
interval = self.sOptions['prefetchInterval'] * 60 * 60
self.sCallbackTimer = threading.Timer(
interval, self.PrefetchRecentSymbolFiles)
self.sCallbackTimer.start()
thresholdTime = time.time() - \
self.sOptions['prefetchThreshold'] * 60 * 60
symDirsToInspect = {}
for pdbName in PREFETCHED_LIBS:
symDirsToInspect[pdbName] = []
topLibPath = self.sOptions['symbolPaths'][
'FIREFOX'] + os.sep + pdbName
try:
symbolDirs = os.listdir(topLibPath)
for symbolDir in symbolDirs:
candidatePath = topLibPath + os.sep + symbolDir
mtime = os.path.getmtime(candidatePath)
if mtime > thresholdTime:
symDirsToInspect[pdbName].append(
(mtime, candidatePath))
except Exception as e:
LogError("Error while pre-fetching: " + str(e))
LogMessage("Found " + str(len(symDirsToInspect[pdbName])) +
" new " + pdbName + " recent dirs")
# Only prefetch the most recent N entries
symDirsToInspect[pdbName].sort(reverse=True)
symDirsToInspect[pdbName] = symDirsToInspect[pdbName][
:self.sOptions['prefetchMaxSymbolsPerLib']]
# Don't fetch symbols already in cache.
# Ideally, mutex would be held from check to insert in self.sCache,
# but we don't want to hold the lock during I/O. This won't cause
# inconsistencies.
self.sCacheLock.acquire()
try:
for pdbName in symDirsToInspect:
for (mtime, symbolDirPath) in symDirsToInspect[pdbName]:
pdbId = os.path.basename(symbolDirPath)
if pdbName in self.sCache and \
pdbId in self.sCache[pdbName]:
symDirsToInspect[pdbName].remove(
(mtime, symbolDirPath))
finally:
self.sCacheLock.release()
# Read all new symbol files in at once
fetchedSymbols = {}
fetchedCount = 0
for pdbName in symDirsToInspect:
# The corresponding symbol file name ends with .sym
symFileName = re.sub(r"\.[^\.]+$", ".sym", pdbName)
for (mtime, symbolDirPath) in symDirsToInspect[pdbName]:
pdbId = os.path.basename(symbolDirPath)
symbolFilePath = symbolDirPath + os.sep + symFileName
symbolInfo = self.FetchSymbolsFromFile(symbolFilePath)
if symbolInfo:
# Stop if the prefetched items are bigger than the cache
if fetchedCount + symbolInfo.GetEntryCount() > \
self.sOptions["maxCacheEntries"]:
break
fetchedSymbols[(pdbName, pdbId)] = symbolInfo
fetchedCount += symbolInfo.GetEntryCount()
else:
LogError("Couldn't fetch .sym file symbols for " +
symbolFilePath)
continue
# Insert new symbols into global symbol cache
self.sCacheLock.acquire()
try:
# Make room for the new symbols
self.MaybeEvict(fetchedCount)
for (pdbName, pdbId) in fetchedSymbols:
if pdbName not in self.sCache:
self.sCache[pdbName] = {}
if pdbId in self.sCache[pdbName]:
continue
newSymbolFile = fetchedSymbols[(pdbName, pdbId)]
self.sCache[pdbName][pdbId] = newSymbolFile
self.sCacheCount += newSymbolFile.GetEntryCount()
# Move new symbols to front of MRU list to give them a chance
self.UpdateMruList(pdbName, pdbId)
finally:
self.sCacheLock.release()
LogMessage("Finished prefetching recent symbol files")
def UpdateMruList(self, pdbName, pdbId):
libId = (pdbName, pdbId)
if libId in self.sMruSymbols:
self.sMruSymbols.remove(libId)
self.sMruSymbols.insert(0, libId)
def MaybeEvict(self, freeEntriesNeeded):
maxCacheSize = self.sOptions["maxCacheEntries"]
LogTrace("Cache occupancy before MaybeEvict: " +
str(self.sCacheCount) + "/" + str(maxCacheSize))
if self.sCacheCount == 0 or \
self.sCacheCount + freeEntriesNeeded <= maxCacheSize:
# No need to lock mutex here, this doesn't need to be 100%
return
# If adding the new entries would exceed the max cache size,
# evict so that cache is at 70% capacity after new entries added
numOldEntriesAfterEvict = max(
0, (0.70 * maxCacheSize) - freeEntriesNeeded)
numToEvict = self.sCacheCount - numOldEntriesAfterEvict
# Evict symbols until evict quota is met, starting with least recently
# used
for (pdbName, pdbId) in reversed(self.sMruSymbols):
if numToEvict <= 0:
break
evicteeCount = self.sCache[pdbName][pdbId].GetEntryCount()
del self.sCache[pdbName][pdbId]
self.sCacheCount -= evicteeCount
self.sMruSymbols.pop()
numToEvict -= evicteeCount
LogTrace("Cache occupancy after MaybeEvict: " +
str(self.sCacheCount) + "/" + str(maxCacheSize))

View file

@ -0,0 +1,29 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import sys
import threading
import time
gEnableTracing = False
def LogTrace(string):
global gEnableTracing
if gEnableTracing:
threadName = threading.currentThread().getName().ljust(12)
print >> sys.stdout, time.asctime() + " " + threadName + " TRACE " + \
string
def LogError(string):
threadName = threading.currentThread().getName().ljust(12)
print >> sys.stderr, time.asctime() + " " + threadName + " ERROR " + \
string
def LogMessage(string):
threadName = threading.currentThread().getName().ljust(12)
print >> sys.stdout, time.asctime() + " " + threadName + " " + \
string

View file

@ -0,0 +1,393 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import cStringIO
import hashlib
import json
import os
import platform
import re
import subprocess
import urllib2
import zipfile
from distutils import spawn
from symFileManager import SymFileManager
from symbolicationRequest import SymbolicationRequest
from symLogging import LogMessage
"""
Symbolication is broken when using type 'str' in python 2.7, so we use 'basestring'.
But for python 3.0 compatibility, 'basestring' isn't defined, but the 'str' type works.
So we force 'basestring' to 'str'.
"""
try:
basestring
except NameError:
basestring = str
class SymbolError(Exception):
pass
class OSXSymbolDumper:
def __init__(self):
self.dump_syms_bin = os.path.join(
os.path.dirname(__file__), 'dump_syms_mac')
if not os.path.exists(self.dump_syms_bin):
raise SymbolError("No dump_syms_mac binary in this directory")
def store_symbols(self, lib_path, expected_breakpad_id,
output_filename_without_extension):
"""
Returns the filename at which the .sym file was created, or None if no
symbols were dumped.
"""
output_filename = output_filename_without_extension + ".sym"
def get_archs(filename):
"""
Find the list of architectures present in a Mach-O file.
"""
return subprocess.Popen(["lipo", "-info", filename],
stdout=subprocess.PIPE)\
.communicate()[0].split(':')[2].strip().split()
def process_file(arch):
proc = subprocess.Popen([self.dump_syms_bin, "-a", arch, lib_path],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()
if proc.returncode != 0:
return None
module = stdout.splitlines()[0]
bits = module.split(' ', 4)
if len(bits) != 5:
return None
_, platform, cpu_arch, actual_breakpad_id, debug_file = bits
if actual_breakpad_id != expected_breakpad_id:
return None
with open(output_filename, "w") as f:
f.write(stdout)
return output_filename
for arch in get_archs(lib_path):
result = process_file(arch)
if result is not None:
return result
return None
class LinuxSymbolDumper:
def __init__(self):
self.nm = spawn.find_executable("nm")
if not self.nm:
raise SymbolError(
"Could not find nm, necessary for symbol dumping")
def store_symbols(self, lib_path, breakpad_id,
output_filename_without_extension):
"""
Returns the filename at which the .sym file was created, or None if no
symbols were dumped.
"""
output_filename = output_filename_without_extension + ".nmsym"
proc = subprocess.Popen([self.nm, "--demangle", lib_path],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()
if proc.returncode != 0:
return
with open(output_filename, "w") as f:
f.write(stdout)
# Append nm -D output to the file. On Linux, most system libraries
# have no "normal" symbols, but they have "dynamic" symbols, which
# nm -D shows.
proc = subprocess.Popen([self.nm, "--demangle", "-D", lib_path],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()
if proc.returncode == 0:
f.write(stdout)
return output_filename
class ProfileSymbolicator:
def __init__(self, options):
self.options = options
self.sym_file_manager = SymFileManager(self.options)
self.symbol_dumper = self.get_symbol_dumper()
self.main_start_time = None
def get_symbol_dumper(self):
try:
if platform.system() == "Darwin":
return OSXSymbolDumper()
elif platform.system() == "Linux":
return LinuxSymbolDumper()
except SymbolError:
return None
def integrate_symbol_zip_from_url(self, symbol_zip_url):
if self.have_integrated(symbol_zip_url):
return
LogMessage("Retrieving symbol zip from {symbol_zip_url}...".format(
symbol_zip_url=symbol_zip_url))
try:
io = urllib2.urlopen(symbol_zip_url, None, 30)
with zipfile.ZipFile(cStringIO.StringIO(io.read())) as zf:
self.integrate_symbol_zip(zf)
self._create_file_if_not_exists(self._marker_file(symbol_zip_url))
except IOError:
LogMessage("Symbol zip request failed.")
def integrate_symbol_zip_from_file(self, filename):
if self.have_integrated(filename):
return
with open(filename, 'rb') as f:
with zipfile.ZipFile(f) as zf:
self.integrate_symbol_zip(zf)
self._create_file_if_not_exists(self._marker_file(filename))
def _create_file_if_not_exists(self, filename):
try:
os.makedirs(os.path.dirname(filename))
except OSError:
pass
try:
open(filename, 'a').close()
except IOError:
pass
def integrate_symbol_zip(self, symbol_zip_file):
symbol_zip_file.extractall(self.options["symbolPaths"]["FIREFOX"])
def _marker_file(self, symbol_zip_url):
marker_dir = os.path.join(
self.options["symbolPaths"]["FIREFOX"], ".markers")
return os.path.join(marker_dir,
hashlib.sha1(symbol_zip_url).hexdigest())
def have_integrated(self, symbol_zip_url):
return os.path.isfile(self._marker_file(symbol_zip_url))
def get_unknown_modules_in_profile(self, profile_json):
if "libs" not in profile_json:
return []
shared_libraries = json.loads(profile_json["libs"])
memoryMap = []
for lib in shared_libraries:
memoryMap.append(self._module_from_lib(lib))
rawRequest = {"stacks": [[]], "memoryMap": memoryMap,
"version": 4, "symbolSources": ["FIREFOX", "WINDOWS"]}
request = SymbolicationRequest(self.sym_file_manager, rawRequest)
if not request.isValidRequest:
return []
request.Symbolicate(0) # This sets request.knownModules
unknown_modules = []
for i, lib in enumerate(shared_libraries):
if not request.knownModules[i]:
unknown_modules.append(lib)
return unknown_modules
def dump_and_integrate_missing_symbols(self, profile_json,
symbol_zip_path):
if not self.symbol_dumper:
return
unknown_modules = self.get_unknown_modules_in_profile(profile_json)
if not unknown_modules:
return
# We integrate the dumped symbols by dumping them directly into our
# symbol directory.
output_dir = self.options["symbolPaths"]["FIREFOX"]
# Additionally, we add all dumped symbol files to the missingsymbols
# zip file.
with zipfile.ZipFile(symbol_zip_path, 'a', zipfile.ZIP_DEFLATED) as zf:
for lib in unknown_modules:
self.dump_and_integrate_symbols_for_lib(lib, output_dir, zf)
def dump_and_integrate_symbols_for_lib(self, lib, output_dir, zip):
[name, breakpadId] = self._module_from_lib(lib)
expected_name_without_extension = os.path.join(name, breakpadId, name)
for extension in [".sym", ".nmsym"]:
expected_name = expected_name_without_extension + extension
if expected_name in zip.namelist():
# No need to dump the symbols again if we already have it in
# the missingsymbols zip file from a previous run.
zip.extract(expected_name, output_dir)
return
lib_path = lib['name']
if not os.path.exists(lib_path):
return
output_filename_without_extension = os.path.join(
output_dir, expected_name_without_extension)
store_path = os.path.dirname(output_filename_without_extension)
if not os.path.exists(store_path):
os.makedirs(store_path)
# Dump the symbols.
sym_file = self.symbol_dumper.store_symbols(
lib_path, lib["breakpadId"], output_filename_without_extension)
if sym_file:
rootlen = len(os.path.join(output_dir, '_')) - 1
output_filename = sym_file[rootlen:]
if output_filename not in zip.namelist():
zip.write(sym_file, output_filename)
def symbolicate_profile(self, profile_json):
if "libs" not in profile_json:
return
if profile_json["meta"].get("version", 2) == 3:
self.symbolicate_profile_v3(profile_json)
else:
self.symbolicate_profile_v2(profile_json)
profile_start_time = profile_json["meta"].get("startTime", 0)
delta_time = 0
# The profile in the main process will have the startTime that
# we'll offset our markers by, and also happens to be the one
# we'll see first when recursively symbolicating, so we'll stash
# it in main_start_time.
if self.main_start_time is None:
self.main_start_time = profile_start_time
else:
# We're a subprocess, so our markers will need to be offset
# by the difference between the parent process start time
# and this process's start time.
delta_time = profile_start_time - self.main_start_time
for i, thread in enumerate(profile_json["threads"]):
if isinstance(thread, basestring):
thread_json = json.loads(thread)
self.symbolicate_profile(thread_json)
profile_json["threads"][i] = json.dumps(thread_json)
else:
for marker in thread["markers"]["data"]:
if marker[1]:
marker[1] += delta_time
def symbolicate_profile_v2(self, profile_json):
shared_libraries = json.loads(profile_json["libs"])
shared_libraries.sort(key=lambda lib: lib["start"])
addresses = self._find_addresses_v2(profile_json)
symbols_to_resolve = self._assign_symbols_to_libraries(
addresses, shared_libraries)
symbolication_table = self._resolve_symbols(symbols_to_resolve)
self._substitute_symbols_v2(profile_json, symbolication_table)
def symbolicate_profile_v3(self, profile_json):
shared_libraries = json.loads(profile_json["libs"])
shared_libraries.sort(key=lambda lib: lib["start"])
addresses = self._find_addresses_v3(profile_json)
symbols_to_resolve = self._assign_symbols_to_libraries(
addresses, shared_libraries)
# print symbols_to_resolve
symbolication_table = self._resolve_symbols(symbols_to_resolve)
self._substitute_symbols_v3(profile_json, symbolication_table)
def _find_addresses_v3(self, profile_json):
addresses = set()
for thread in profile_json["threads"]:
if isinstance(thread, basestring):
continue
for s in thread["stringTable"]:
if s[0:2] == "0x":
addresses.add(s)
return addresses
def _substitute_symbols_v3(self, profile_json, symbolication_table):
for thread in profile_json["threads"]:
if isinstance(thread, basestring):
continue
for i, s in enumerate(thread["stringTable"]):
thread["stringTable"][i] = symbolication_table.get(s, s)
def _find_addresses_v2(self, profile_json):
addresses = set()
for thread in profile_json["threads"]:
for sample in thread["samples"]:
for frame in sample["frames"]:
if frame["location"][0:2] == "0x":
addresses.add(frame["location"])
if "lr" in frame and frame["lr"][0:2] == "0x":
addresses.add(frame["lr"])
return addresses
def _get_containing_library(self, address, libs):
left = 0
right = len(libs) - 1
while left <= right:
mid = (left + right) / 2
if address >= libs[mid]["end"]:
left = mid + 1
elif address < libs[mid]["start"]:
right = mid - 1
else:
return libs[mid]
return None
def _assign_symbols_to_libraries(self, addresses, shared_libraries):
libs_with_symbols = {}
for address in addresses:
lib = self._get_containing_library(
int(address, 0), shared_libraries)
if not lib:
continue
if lib["start"] not in libs_with_symbols:
libs_with_symbols[lib["start"]] = {
"library": lib, "symbols": set()}
libs_with_symbols[lib["start"]]["symbols"].add(address)
return libs_with_symbols.values()
def _module_from_lib(self, lib):
if "breakpadId" in lib:
return [lib["name"].split("/")[-1], lib["breakpadId"]]
pdbSig = re.sub("[{}\-]", "", lib["pdbSignature"])
return [lib["pdbName"], pdbSig + lib["pdbAge"]]
def _resolve_symbols(self, symbols_to_resolve):
memoryMap = []
processedStack = []
all_symbols = []
for moduleIndex, library_with_symbols in enumerate(symbols_to_resolve):
lib = library_with_symbols["library"]
symbols = library_with_symbols["symbols"]
memoryMap.append(self._module_from_lib(lib))
all_symbols += symbols
for symbol in symbols:
processedStack.append(
[moduleIndex, int(symbol, 0) - lib["start"]])
rawRequest = {"stacks": [processedStack], "memoryMap": memoryMap,
"version": 4, "symbolSources": ["FIREFOX", "WINDOWS"]}
request = SymbolicationRequest(self.sym_file_manager, rawRequest)
if not request.isValidRequest:
return {}
symbolicated_stack = request.Symbolicate(0)
return dict(zip(all_symbols, symbolicated_stack))
def _substitute_symbols_v2(self, profile_json, symbolication_table):
for thread in profile_json["threads"]:
for sample in thread["samples"]:
for frame in sample["frames"]:
frame["location"] = symbolication_table.get(
frame["location"], frame["location"])

View file

@ -0,0 +1,314 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from symLogging import LogTrace, LogError
import re
import json
import urllib2
# Precompiled regex for validating lib names
# Empty lib name means client couldn't associate frame with any lib
gLibNameRE = re.compile("[0-9a-zA-Z_+\-\.]*$")
# Maximum number of times a request can be forwarded to a different server
# for symbolication. Also prevents loops.
MAX_FORWARDED_REQUESTS = 3
"""
Symbolication is broken when using type 'str' in python 2.7, so we use 'basestring'.
But for python 3.0 compatibility, 'basestring' isn't defined, but the 'str' type works.
So we force 'basestring' to 'str'.
"""
try:
basestring
except NameError:
basestring = str
class ModuleV3:
def __init__(self, libName, breakpadId):
self.libName = libName
self.breakpadId = breakpadId
def getModuleV3(libName, breakpadId):
if not isinstance(libName, basestring) or not gLibNameRE.match(libName):
LogTrace("Bad library name: " + str(libName))
return None
if not isinstance(breakpadId, basestring):
LogTrace("Bad breakpad id: " + str(breakpadId))
return None
return ModuleV3(libName, breakpadId)
class SymbolicationRequest:
def __init__(self, symFileManager, rawRequests):
self.Reset()
self.symFileManager = symFileManager
self.stacks = []
self.combinedMemoryMap = []
self.knownModules = []
self.symbolSources = []
self.ParseRequests(rawRequests)
def Reset(self):
self.symFileManager = None
self.isValidRequest = False
self.combinedMemoryMap = []
self.knownModules = []
self.stacks = []
self.forwardCount = 0
def ParseRequests(self, rawRequests):
self.isValidRequest = False
try:
if not isinstance(rawRequests, dict):
LogTrace("Request is not a dictionary")
return
if "version" not in rawRequests:
LogTrace("Request is missing 'version' field")
return
version = rawRequests["version"]
if version != 4:
LogTrace("Invalid version: %s" % version)
return
if "forwarded" in rawRequests:
if not isinstance(rawRequests["forwarded"], (int, int)):
LogTrace("Invalid 'forwards' field: %s"
% rawRequests["forwarded"])
return
self.forwardCount = rawRequests["forwarded"]
# Client specifies which sets of symbols should be used
if "symbolSources" in rawRequests:
try:
sourceList = [x.upper()
for x in rawRequests["symbolSources"]]
for source in sourceList:
if source in self.symFileManager\
.sOptions["symbolPaths"]:
self.symbolSources.append(source)
else:
LogTrace("Unrecognized symbol source: " + source)
continue
except:
self.symbolSources = []
pass
if not self.symbolSources:
self.symbolSources.append(
self.symFileManager.sOptions["defaultApp"])
self.symbolSources.append(
self.symFileManager.sOptions["defaultOs"])
if "memoryMap" not in rawRequests:
LogTrace("Request is missing 'memoryMap' field")
return
memoryMap = rawRequests["memoryMap"]
if not isinstance(memoryMap, list):
LogTrace("'memoryMap' field in request is not a list")
if "stacks" not in rawRequests:
LogTrace("Request is missing 'stacks' field")
return
stacks = rawRequests["stacks"]
if not isinstance(stacks, list):
LogTrace("'stacks' field in request is not a list")
return
# Check memory map is well-formatted
cleanMemoryMap = []
for module in memoryMap:
if not isinstance(module, list):
LogTrace(
"Entry in memory map is not a list: " + str(module))
return
if len(module) != 2:
LogTrace("Entry in memory map is not a 2 item list: " +
str(module))
return
module = getModuleV3(*module)
if module is None:
return
cleanMemoryMap.append(module)
self.combinedMemoryMap = cleanMemoryMap
self.knownModules = [False] * len(self.combinedMemoryMap)
# Check stack is well-formatted
for stack in stacks:
if not isinstance(stack, list):
LogTrace("stack is not a list")
return
for entry in stack:
if not isinstance(entry, list):
LogTrace("stack entry is not a list")
return
if len(entry) != 2:
LogTrace("stack entry doesn't have exactly 2 elements")
return
self.stacks.append(stack)
except Exception as e:
LogTrace("Exception while parsing request: " + str(e))
return
self.isValidRequest = True
def ForwardRequest(self, indexes, stack, modules, symbolicatedStack):
LogTrace("Forwarding " + str(len(stack)) + " PCs for symbolication")
try:
url = self.symFileManager.sOptions["remoteSymbolServer"]
rawModules = []
moduleToIndex = {}
newIndexToOldIndex = {}
for moduleIndex, m in modules:
l = [m.libName, m.breakpadId]
newModuleIndex = len(rawModules)
rawModules.append(l)
moduleToIndex[m] = newModuleIndex
newIndexToOldIndex[newModuleIndex] = moduleIndex
rawStack = []
for entry in stack:
moduleIndex = entry[0]
offset = entry[1]
module = self.combinedMemoryMap[moduleIndex]
newIndex = moduleToIndex[module]
rawStack.append([newIndex, offset])
requestVersion = 4
while True:
requestObj = {
"symbolSources": self.symbolSources,
"stacks": [rawStack],
"memoryMap": rawModules,
"forwarded": self.forwardCount + 1,
"version": requestVersion
}
requestJson = json.dumps(requestObj)
headers = {"Content-Type": "application/json"}
requestHandle = urllib2.Request(url, requestJson, headers)
try:
response = urllib2.urlopen(requestHandle)
except Exception as e:
if requestVersion == 4:
# Try again with version 3
requestVersion = 3
continue
raise e
succeededVersion = requestVersion
break
except Exception as e:
LogError("Exception while forwarding request: " + str(e))
return
try:
responseJson = json.loads(response.read())
except Exception as e:
LogError("Exception while reading server response to forwarded"
" request: " + str(e))
return
try:
if succeededVersion == 4:
responseKnownModules = responseJson['knownModules']
for newIndex, known in enumerate(responseKnownModules):
if known and newIndex in newIndexToOldIndex:
self.knownModules[newIndexToOldIndex[newIndex]] = True
responseSymbols = responseJson['symbolicatedStacks'][0]
else:
responseSymbols = responseJson[0]
if len(responseSymbols) != len(stack):
LogError(str(len(responseSymbols)) + " symbols in response, " +
str(len(stack)) + " PCs in request!")
return
for index in range(0, len(stack)):
symbol = responseSymbols[index]
originalIndex = indexes[index]
symbolicatedStack[originalIndex] = symbol
except Exception as e:
LogError("Exception while parsing server response to forwarded"
" request: " + str(e))
return
def Symbolicate(self, stackNum):
# Check if we should forward requests when required sym files don't
# exist
shouldForwardRequests = False
if self.symFileManager.sOptions["remoteSymbolServer"] and \
self.forwardCount < MAX_FORWARDED_REQUESTS:
shouldForwardRequests = True
# Symbolicate each PC
pcIndex = -1
symbolicatedStack = []
missingSymFiles = []
unresolvedIndexes = []
unresolvedStack = []
unresolvedModules = []
stack = self.stacks[stackNum]
for moduleIndex, module in enumerate(self.combinedMemoryMap):
if not self.symFileManager.GetLibSymbolMap(module.libName,
module.breakpadId,
self.symbolSources):
missingSymFiles.append((module.libName, module.breakpadId))
if shouldForwardRequests:
unresolvedModules.append((moduleIndex, module))
else:
self.knownModules[moduleIndex] = True
for entry in stack:
pcIndex += 1
moduleIndex = entry[0]
offset = entry[1]
if moduleIndex == -1:
symbolicatedStack.append(hex(offset))
continue
module = self.combinedMemoryMap[moduleIndex]
if (module.libName, module.breakpadId) in missingSymFiles:
if shouldForwardRequests:
unresolvedIndexes.append(pcIndex)
unresolvedStack.append(entry)
symbolicatedStack.append(
hex(offset) + " (in " + module.libName + ")")
continue
functionName = None
libSymbolMap = self.symFileManager.GetLibSymbolMap(
module.libName,
module.breakpadId,
self.symbolSources
)
functionName = libSymbolMap.Lookup(offset)
if functionName is None:
functionName = hex(offset)
symbolicatedStack.append(
functionName + " (in " + module.libName + ")")
# Ask another server for help symbolicating unresolved addresses
if len(unresolvedStack) > 0 or len(unresolvedModules) > 0:
self.ForwardRequest(unresolvedIndexes, unresolvedStack,
unresolvedModules, symbolicatedStack)
return symbolicatedStack

View file

@ -0,0 +1,522 @@
#!/usr/bin/env python
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""
objects and methods for parsing and serializing Talos results
see https://wiki.mozilla.org/Buildbot/Talos/DataFormat
"""
import json
import os
import re
import csv
from talos import output, utils, filter
class TalosResults(object):
"""Container class for Talos results"""
def __init__(self):
self.results = []
self.extra_options = []
def add(self, test_results):
self.results.append(test_results)
def add_extra_option(self, extra_option):
self.extra_options.append(extra_option)
def output(self, output_formats):
"""
output all results to appropriate URLs
- output_formats: a dict mapping formats to a list of URLs
"""
tbpl_output = {}
try:
for key, urls in output_formats.items():
_output = output.Output(self)
results = _output()
for url in urls:
_output.output(results, url, tbpl_output)
except utils.TalosError as e:
# print to results.out
try:
_output = output.GraphserverOutput(self)
results = _output()
_output.output(
'file://%s' % os.path.join(os.getcwd(), 'results.out'),
results
)
except:
pass
print('\nFAIL: %s' % str(e).replace('\n', '\nRETURN:'))
raise e
if tbpl_output:
print("TinderboxPrint: TalosResult: %s" % json.dumps(tbpl_output))
class TestResults(object):
"""container object for all test results across cycles"""
def __init__(self, test_config, global_counters=None, framework=None):
self.results = []
self.test_config = test_config
self.format = None
self.global_counters = global_counters or {}
self.all_counter_results = []
self.framework = framework
self.using_xperf = False
def name(self):
return self.test_config['name']
def mainthread(self):
return self.test_config['mainthread']
def add(self, results, counter_results=None):
"""
accumulate one cycle of results
- results : TalosResults instance or path to browser log
- counter_results : counters accumulated for this cycle
"""
# convert to a results class via parsing the browser log
browserLog = BrowserLogResults(
results,
counter_results=counter_results,
global_counters=self.global_counters
)
results = browserLog.results()
self.using_xperf = browserLog.using_xperf
# ensure the results format matches previous results
if self.results:
if not results.format == self.results[0].format:
raise utils.TalosError("Conflicting formats for results")
else:
self.format = results.format
self.results.append(results)
if counter_results:
self.all_counter_results.append(counter_results)
class Results(object):
def filter(self, testname, filters):
"""
filter the results set;
applies each of the filters in order to the results data
filters should be callables that take a list
the last filter should return a scalar (float or int)
returns a list of [[data, page], ...]
"""
retval = []
for result in self.results:
page = result['page']
data = result['runs']
remaining_filters = []
# ignore* functions return a filtered set of data
for f in filters:
if f.func.__name__.startswith('ignore'):
data = f.apply(data)
else:
remaining_filters.append(f)
# apply the summarization filters
for f in remaining_filters:
if f.func.__name__ == "v8_subtest":
# for v8_subtest we need to page for reference data
data = filter.v8_subtest(data, page)
else:
data = f.apply(data)
summary = {
'filtered': data, # backward compatibility with perfherder
'value': data
}
retval.append([summary, page])
return retval
def raw_values(self):
return [(result['page'], result['runs']) for result in self.results]
def values(self, testname, filters):
"""return filtered (value, page) for each value"""
return [[val, page] for val, page in self.filter(testname, filters)
if val['filtered'] > -1]
class TsResults(Results):
"""
results for Ts tests
"""
format = 'tsformat'
def __init__(self, string, counter_results=None):
self.counter_results = counter_results
string = string.strip()
lines = string.splitlines()
# gather the data
self.results = []
index = 0
# Handle the case where we support a pagename in the results
# (new format)
for line in lines:
result = {}
r = line.strip().split(',')
r = [i for i in r if i]
if len(r) <= 1:
continue
result['index'] = index
result['page'] = r[0]
# note: if we have len(r) >1, then we have pagename,raw_results
result['runs'] = [float(i) for i in r[1:]]
self.results.append(result)
index += 1
# The original case where we just have numbers and no pagename
if not self.results:
result = {}
result['index'] = index
result['page'] = 'NULL'
result['runs'] = [float(val) for val in string.split('|')]
self.results.append(result)
class PageloaderResults(Results):
"""
results from a browser_dump snippet
https://wiki.mozilla.org/Buildbot/Talos/DataFormat#browser_output.txt
"""
format = 'tpformat'
def __init__(self, string, counter_results=None):
"""
- string : string of relevent part of browser dump
- counter_results : counter results dictionary
"""
self.counter_results = counter_results
string = string.strip()
lines = string.splitlines()
# currently we ignore the metadata on top of the output (e.g.):
# _x_x_mozilla_page_load
# _x_x_mozilla_page_load_details
# |i|pagename|runs|
lines = [line for line in lines if ';' in line]
# gather the data
self.results = []
for line in lines:
result = {}
r = line.strip('|').split(';')
r = [i for i in r if i]
if len(r) <= 2:
continue
result['index'] = int(r[0])
result['page'] = r[1]
result['runs'] = [float(i) for i in r[2:]]
# fix up page
result['page'] = self.format_pagename(result['page'])
self.results.append(result)
def format_pagename(self, page):
"""
fix up the page for reporting
"""
page = page.rstrip('/')
if '/' in page:
page = page.split('/')[0]
return page
class BrowserLogResults(object):
"""parse the results from the browser log output"""
# tokens for the report types
report_tokens = [
('tsformat', ('__start_report', '__end_report')),
('tpformat', ('__start_tp_report', '__end_tp_report'))
]
# tokens for timestamps, in order (attribute, (start_delimeter,
# end_delimter))
time_tokens = [
('startTime', ('__startTimestamp', '__endTimestamp')),
('beforeLaunchTime', ('__startBeforeLaunchTimestamp',
'__endBeforeLaunchTimestamp')),
('endTime', ('__startAfterTerminationTimestamp',
'__endAfterTerminationTimestamp'))
]
# regular expression for failure case if we can't parse the tokens
RESULTS_REGEX_FAIL = re.compile('__FAIL(.*?)__FAIL',
re.DOTALL | re.MULTILINE)
# regular expression for RSS results
RSS_REGEX = re.compile('RSS:\s+([a-zA-Z0-9]+):\s+([0-9]+)$')
# regular expression for responsiveness results
RESULTS_RESPONSIVENESS_REGEX = re.compile(
'MOZ_EVENT_TRACE\ssample\s\d*?\s(\d*\.?\d*)$',
re.DOTALL | re.MULTILINE
)
# classes for results types
classes = {'tsformat': TsResults,
'tpformat': PageloaderResults}
# If we are using xperf, we do not upload the regular results, only
# xperf counters
using_xperf = False
def __init__(self, results_raw, counter_results=None,
global_counters=None):
"""
- shutdown : whether to record shutdown results or not
"""
self.counter_results = counter_results
self.global_counters = global_counters
self.results_raw = results_raw
# parse the results
try:
match = self.RESULTS_REGEX_FAIL.search(self.results_raw)
if match:
self.error(match.group(1))
raise utils.TalosError(match.group(1))
self.parse()
except utils.TalosError:
# TODO: consider investigating this further or adding additional
# information
raise # reraise failing exception
# accumulate counter results
self.counters(self.counter_results, self.global_counters)
def error(self, message):
"""raise a TalosError for bad parsing of the browser log"""
raise utils.TalosError(message)
def parse(self):
position = -1
# parse the report
for format, tokens in self.report_tokens:
report, position = self.get_single_token(*tokens)
if report is None:
continue
self.browser_results = report
self.format = format
previous_tokens = tokens
break
else:
self.error("Could not find report in browser output: %s"
% self.report_tokens)
# parse the timestamps
for attr, tokens in self.time_tokens:
# parse the token contents
value, _last_token = self.get_single_token(*tokens)
# check for errors
if not value:
self.error("Could not find %s in browser output: (tokens: %s)"
% (attr, tokens))
try:
value = int(value)
except ValueError:
self.error("Could not cast %s to an integer: %s"
% (attr, value))
if _last_token < position:
self.error("%s [character position: %s] found before %s"
" [character position: %s]"
% (tokens, _last_token, previous_tokens, position))
# process
setattr(self, attr, value)
position = _last_token
previous_tokens = tokens
def get_single_token(self, start_token, end_token):
"""browser logs should only have a single instance of token pairs"""
try:
parts, last_token = utils.tokenize(self.results_raw,
start_token, end_token)
except AssertionError as e:
self.error(str(e))
if not parts:
return None, -1 # no match
if len(parts) != 1:
self.error("Multiple matches for %s,%s" % (start_token, end_token))
return parts[0], last_token
def results(self):
"""return results instance appropriate to the format detected"""
if self.format not in self.classes:
raise utils.TalosError(
"Unable to find a results class for format: %s"
% repr(self.format)
)
return self.classes[self.format](self.browser_results)
# methods for counters
def counters(self, counter_results=None, global_counters=None):
"""accumulate all counters"""
if counter_results is not None:
self.rss(counter_results)
if global_counters is not None:
if 'shutdown' in global_counters:
self.shutdown(global_counters)
if 'responsiveness' in global_counters:
global_counters['responsiveness'].extend(self.responsiveness())
self.xperf(global_counters)
def xperf(self, counter_results):
"""record xperf counters in counter_results dictionary"""
counters = ['main_startup_fileio',
'main_startup_netio',
'main_normal_fileio',
'main_normal_netio',
'nonmain_startup_fileio',
'nonmain_normal_fileio',
'nonmain_normal_netio']
mainthread_counter_keys = ['readcount', 'readbytes', 'writecount',
'writebytes']
mainthread_counters = ['_'.join(['mainthread', counter_key])
for counter_key in mainthread_counter_keys]
self.mainthread_io(counter_results)
if not set(counters).union(set(mainthread_counters))\
.intersection(counter_results.keys()):
# no xperf counters to accumulate
return
filename = 'etl_output_thread_stats.csv'
if not os.path.exists(filename):
print("Warning: we are looking for xperf results file %s, and"
" didn't find it" % filename)
return
contents = open(filename).read()
lines = contents.splitlines()
reader = csv.reader(lines)
header = None
for row in reader:
# Read CSV
row = [i.strip() for i in row]
if not header:
# We are assuming the first row is the header and all other
# data is counters
header = row
continue
values = dict(zip(header, row))
# Format for talos
thread = values['thread']
counter = values['counter'].rsplit('_io_bytes', 1)[0]
counter_name = '%s_%s_%sio' % (thread, values['stage'], counter)
value = float(values['value'])
# Accrue counter
if counter_name in counter_results:
counter_results.setdefault(counter_name, []).append(value)
self.using_xperf = True
if (set(mainthread_counters).intersection(counter_results.keys())):
filename = 'etl_output.csv'
if not os.path.exists(filename):
print("Warning: we are looking for xperf results file"
" %s, and didn't find it" % filename)
return
contents = open(filename).read()
lines = contents.splitlines()
reader = csv.reader(lines)
header = None
for row in reader:
row = [i.strip() for i in row]
if not header:
# We are assuming the first row is the header and all
# other data is counters
header = row
continue
values = dict(zip(header, row))
for i, mainthread_counter in enumerate(mainthread_counters):
if int(values[mainthread_counter_keys[i]]) > 0:
counter_results.setdefault(mainthread_counter, [])\
.append([int(values[mainthread_counter_keys[i]]),
values['filename']])
def rss(self, counter_results):
"""record rss counters in counter_results dictionary"""
counters = ['Main', 'Content']
if not set(['%s_RSS' % i for i in counters])\
.intersection(counter_results.keys()):
# no RSS counters to accumulate
return
for line in self.results_raw.split('\n'):
rssmatch = self.RSS_REGEX.search(line)
if rssmatch:
(type, value) = (rssmatch.group(1), rssmatch.group(2))
# type will be 'Main' or 'Content'
counter_name = '%s_RSS' % type
if counter_name in counter_results:
counter_results[counter_name].append(value)
def mainthread_io(self, counter_results):
"""record mainthread IO counters in counter_results dictionary"""
# we want to measure mtio on xperf runs.
# this will be shoved into the xperf results as we ignore those
SCRIPT_DIR = \
os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
filename = os.path.join(SCRIPT_DIR, 'mainthread_io.json')
try:
contents = open(filename).read()
counter_results.setdefault('mainthreadio', []).append(contents)
self.using_xperf = True
except:
# silent failure is fine here as we will only see this on tp5n runs
pass
def shutdown(self, counter_results):
"""record shutdown time in counter_results dictionary"""
counter_results.setdefault('shutdown', [])\
.append(int(self.endTime - self.startTime))
def responsiveness(self):
return self.RESULTS_RESPONSIVENESS_REGEX.findall(self.results_raw)

View file

@ -0,0 +1,248 @@
#!/usr/bin/env python
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import mozversion
import os
import sys
import time
import traceback
import urllib
import utils
import mozhttpd
from mozlog import get_proxy_logger
from talos.results import TalosResults
from talos.ttest import TTest
from talos.utils import TalosError, TalosRegression
from talos.config import get_configs, ConfigurationError
# directory of this file
here = os.path.dirname(os.path.realpath(__file__))
LOG = get_proxy_logger()
def useBaseTestDefaults(base, tests):
for test in tests:
for item in base:
if item not in test:
test[item] = base[item]
if test[item] is None:
test[item] = ''
return tests
def buildCommandLine(test):
"""build firefox command line options for tp tests"""
# sanity check pageloader values
# mandatory options: tpmanifest, tpcycles
if test['tpcycles'] not in range(1, 1000):
raise TalosError('pageloader cycles must be int 1 to 1,000')
if test.get('tpdelay') and test['tpdelay'] not in range(1, 10000):
raise TalosError('pageloader delay must be int 1 to 10,000')
if 'tpmanifest' not in test:
raise TalosError("tpmanifest not found in test: %s" % test)
# if profiling is on, override tppagecycles to prevent test hanging
if test['sps_profile']:
LOG.info("sps profiling is enabled so talos is reducing the number "
"of cycles, please disregard reported numbers")
for cycle_var in ['tppagecycles', 'tpcycles', 'cycles']:
if test[cycle_var] > 2:
test[cycle_var] = 2
# build pageloader command from options
url = ['-tp', test['tpmanifest']]
CLI_bool_options = ['tpchrome', 'tpmozafterpaint', 'tpdisable_e10s',
'tpnoisy', 'rss', 'tprender', 'tploadnocache',
'tpscrolltest']
CLI_options = ['tpcycles', 'tppagecycles', 'tpdelay', 'tptimeout']
for key in CLI_bool_options:
if test.get(key):
url.append('-%s' % key)
for key in CLI_options:
value = test.get(key)
if value:
url.extend(['-%s' % key, str(value)])
# XXX we should actually return the list but since we abuse
# the url as a command line flag to pass to firefox all over the place
# will just make a string for now
return ' '.join(url)
def setup_webserver(webserver):
"""use mozhttpd to setup a webserver"""
LOG.info("starting webserver on %r" % webserver)
host, port = webserver.split(':')
return mozhttpd.MozHttpd(host=host, port=int(port), docroot=here)
def run_tests(config, browser_config):
"""Runs the talos tests on the given configuration and generates a report.
"""
# get the test data
tests = config['tests']
tests = useBaseTestDefaults(config.get('basetest', {}), tests)
paths = ['profile_path', 'tpmanifest', 'extensions', 'setup', 'cleanup']
for test in tests:
# Check for profile_path, tpmanifest and interpolate based on Talos
# root https://bugzilla.mozilla.org/show_bug.cgi?id=727711
# Build command line from config
for path in paths:
if test.get(path):
test[path] = utils.interpolate(test[path])
if test.get('tpmanifest'):
test['tpmanifest'] = \
os.path.normpath('file:/%s' % (urllib.quote(test['tpmanifest'],
'/\\t:\\')))
if not test.get('url'):
# build 'url' for tptest
test['url'] = buildCommandLine(test)
test['url'] = utils.interpolate(test['url'])
test['setup'] = utils.interpolate(test['setup'])
test['cleanup'] = utils.interpolate(test['cleanup'])
# pass --no-remote to firefox launch, if --develop is specified
# we do that to allow locally the user to have another running firefox
# instance
if browser_config['develop']:
browser_config['extra_args'] = '--no-remote'
# with addon signing for production talos, we want to develop without it
if browser_config['develop'] or browser_config['branch_name'] == 'Try':
browser_config['preferences']['xpinstall.signatures.required'] = False
# set defaults
testdate = config.get('testdate', '')
# get the process name from the path to the browser
if not browser_config['process']:
browser_config['process'] = \
os.path.basename(browser_config['browser_path'])
# fix paths to substitute
# `os.path.dirname(os.path.abspath(__file__))` for ${talos}
# https://bugzilla.mozilla.org/show_bug.cgi?id=705809
browser_config['extensions'] = [utils.interpolate(i)
for i in browser_config['extensions']]
browser_config['bcontroller_config'] = \
utils.interpolate(browser_config['bcontroller_config'])
# normalize browser path to work across platforms
browser_config['browser_path'] = \
os.path.normpath(browser_config['browser_path'])
binary = browser_config["browser_path"]
version_info = mozversion.get_version(binary=binary)
browser_config['browser_name'] = version_info['application_name']
browser_config['browser_version'] = version_info['application_version']
browser_config['buildid'] = version_info['application_buildid']
try:
browser_config['repository'] = version_info['application_repository']
browser_config['sourcestamp'] = version_info['application_changeset']
except KeyError:
if not browser_config['develop']:
print("unable to find changeset or repository: %s" % version_info)
sys.exit()
else:
browser_config['repository'] = 'develop'
browser_config['sourcestamp'] = 'develop'
# get test date in seconds since epoch
if testdate:
date = int(time.mktime(time.strptime(testdate,
'%a, %d %b %Y %H:%M:%S GMT')))
else:
date = int(time.time())
LOG.debug("using testdate: %d" % date)
LOG.debug("actual date: %d" % int(time.time()))
# results container
talos_results = TalosResults()
# results links
if not browser_config['develop'] and not config['sps_profile']:
results_urls = dict(
# another hack; datazilla stands for Perfherder
# and do not require url, but a non empty dict is required...
output_urls=['local.json'],
)
else:
# local mode, output to files
results_urls = dict(output_urls=[os.path.abspath('local.json')])
httpd = setup_webserver(browser_config['webserver'])
httpd.start()
# if e10s add as extra results option
if config['e10s']:
talos_results.add_extra_option('e10s')
if config['sps_profile']:
talos_results.add_extra_option('spsProfile')
testname = None
# run the tests
timer = utils.Timer()
LOG.suite_start(tests=[test['name'] for test in tests])
try:
for test in tests:
testname = test['name']
LOG.test_start(testname)
mytest = TTest()
talos_results.add(mytest.runTest(browser_config, test))
LOG.test_end(testname, status='OK')
except TalosRegression as exc:
LOG.error("Detected a regression for %s" % testname)
# by returning 1, we report an orange to buildbot
# http://docs.buildbot.net/latest/developer/results.html
LOG.test_end(testname, status='FAIL', message=str(exc),
stack=traceback.format_exc())
return 1
except Exception as exc:
# NOTE: if we get into this condition, talos has an internal
# problem and cannot continue
# this will prevent future tests from running
LOG.test_end(testname, status='ERROR', message=str(exc),
stack=traceback.format_exc())
# indicate a failure to buildbot, turn the job red
return 2
finally:
LOG.suite_end()
httpd.stop()
LOG.info("Completed test suite (%s)" % timer.elapsed())
# output results
if results_urls:
talos_results.output(results_urls)
if browser_config['develop'] or config['sps_profile']:
print("Thanks for running Talos locally. Results are in %s"
% (results_urls['output_urls']))
# we will stop running tests on a failed test, or we will return 0 for
# green
return 0
def main(args=sys.argv[1:]):
try:
config, browser_config = get_configs()
except ConfigurationError as exc:
sys.exit("ERROR: %s" % exc)
sys.exit(run_tests(config, browser_config))
if __name__ == '__main__':
main()

View file

@ -0,0 +1,130 @@
/**
* MozFileLogger, a log listener that can write to a local file.
*/
var ipcMode = false; // running in e10s build and need to use IPC?
function contentDispatchEvent(type, data, sync) {
if (typeof(data) === "undefined") {
data = {};
}
var element = document.createEvent("datacontainerevent");
element.initEvent("contentEvent", true, false);
element.setData("sync", sync);
element.setData("type", type);
element.setData("data", JSON.stringify(data));
document.dispatchEvent(element);
}
function contentSyncEvent(type, data) {
contentDispatchEvent(type, data, 1);
}
function contentAsyncEvent(type, data) {
contentDispatchEvent(type, data, 0);
}
//double logging to account for normal mode and ipc mode (mobile_profile only)
//Ideally we would remove the dump() and just do ipc logging
function dumpLog(msg) {
dump(msg);
MozFileLogger.log(msg);
}
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
if (Cc === undefined) {
var Cc = Components.classes;
var Ci = Components.interfaces;
}
const FOSTREAM_CID = "@mozilla.org/network/file-output-stream;1";
const LF_CID = "@mozilla.org/file/local;1";
// File status flags. It is a bitwise OR of the following bit flags.
// Only one of the first three flags below may be used.
const PR_READ_ONLY = 0x01; // Open for reading only.
const PR_WRITE_ONLY = 0x02; // Open for writing only.
const PR_READ_WRITE = 0x04; // Open for reading and writing.
// If the file does not exist, the file is created.
// If the file exists, this flag has no effect.
const PR_CREATE_FILE = 0x08;
// The file pointer is set to the end of the file prior to each write.
const PR_APPEND = 0x10;
// If the file exists, its length is truncated to 0.
const PR_TRUNCATE = 0x20;
// If set, each write will wait for both the file data
// and file status to be physically updated.
const PR_SYNC = 0x40;
// If the file does not exist, the file is created. If the file already
// exists, no action and NULL is returned.
const PR_EXCL = 0x80;
/** Init the file logger with the absolute path to the file.
It will create and append if the file already exists **/
var MozFileLogger = {};
MozFileLogger.init = function(path) {
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
MozFileLogger._file = Cc[LF_CID].createInstance(Ci.nsILocalFile);
MozFileLogger._file.initWithPath(path);
MozFileLogger._foStream = Cc[FOSTREAM_CID].createInstance(Ci.nsIFileOutputStream);
MozFileLogger._foStream.init(this._file, PR_WRITE_ONLY | PR_CREATE_FILE | PR_APPEND,
0o664, 0);
}
MozFileLogger.getLogCallback = function() {
return function (msg) {
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
var data = msg.num + " " + msg.level + " " + msg.info.join(' ') + "\n";
if (MozFileLogger._foStream)
MozFileLogger._foStream.write(data, data.length);
if (data.indexOf("SimpleTest FINISH") >= 0) {
MozFileLogger.close();
}
}
}
// This is only used from chrome space by the reftest harness
MozFileLogger.log = function(msg) {
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
try {
if (MozFileLogger._foStream)
MozFileLogger._foStream.write(msg, msg.length);
} catch(ex) {}
}
MozFileLogger.close = function() {
if (ipcMode) {
contentAsyncEvent("LoggerClose");
return;
}
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
if(MozFileLogger._foStream)
MozFileLogger._foStream.close();
MozFileLogger._foStream = null;
MozFileLogger._file = null;
}
try {
var prefs = Components.classes['@mozilla.org/preferences-service;1']
.getService(Components.interfaces.nsIPrefBranch2);
var filename = prefs.getCharPref('talos.logfile');
MozFileLogger.init(filename);
} catch (ex) {} //pref does not exist, return empty string

View file

@ -0,0 +1,135 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// - NOTE: This file is duplicated verbatim at:
// - talos/scripts/Profiler.js
// - talos/pageloader/chrome/Profiler.js
// - talos/tests/devtools/addon/content/Profiler.js
// - talos/tests/tart/addon/content/Profiler.js
// - talos/startup_test/tresize/addon/content/Profiler.js
//
// - Please keep these copies in sync.
// - Please make sure your changes apply cleanly to all use cases.
// Finer grained profiler control
//
// Use this object to pause and resume the profiler so that it only profiles the
// relevant parts of our tests.
var Profiler;
(function(){
var _profiler;
// If this script is loaded in a framescript context, there won't be a
// document object, so just use a fallback value in that case.
var test_name = this.document ? this.document.location.pathname : "unknown";
// Whether Profiler has been initialized. Until that happens, most calls
// will be ignored.
var enabled = false;
// The subtest name that beginTest() was called with.
var currentTest = "";
// Profiling settings.
var profiler_interval, profiler_entries, profiler_threadsArray, profiler_dir;
try {
// Outside of talos, this throws a security exception which no-op this file.
// (It's not required nor allowed for addons since Firefox 17)
// It's used inside talos from non-privileged pages (like during tscroll),
// and it works because talos disables all/most security measures.
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
} catch (e) {}
try {
_profiler = Components.classes["@mozilla.org/tools/profiler;1"].getService(Components.interfaces.nsIProfiler);
} catch (ex) { (typeof(dumpLog) == "undefined" ? dump : dumpLog)(ex + "\n"); }
// Parses an url query string into a JS object.
function searchToObject(locationSearch) {
var pairs = locationSearch.substring(1).split("&");
var result = {};
for (var i in pairs) {
if (pairs[i] !== "") {
var pair = pairs[i].split("=");
result[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || "");
}
}
return result;
}
Profiler = {
/**
* Initialize the profiler using profiler settings supplied in a JS object.
* The following properties on the object are respected:
* - sps_profile_interval
* - sps_profile_entries
* - sps_profile_threads
* - sps_profile_dir
*/
initFromObject: function Profiler__initFromObject (obj) {
if (obj &&
("sps_profile_dir" in obj) && typeof obj.sps_profile_dir == "string" &&
("sps_profile_interval" in obj) && Number.isFinite(obj.sps_profile_interval * 1) &&
("sps_profile_entries" in obj) && Number.isFinite(obj.sps_profile_entries * 1) &&
("sps_profile_threads" in obj) && typeof obj.sps_profile_threads == "string") {
profiler_interval = obj.sps_profile_interval;
profiler_entries = obj.sps_profile_entries;
profiler_threadsArray = obj.sps_profile_threads.split(",");
profiler_dir = obj.sps_profile_dir;
enabled = true;
}
},
initFromURLQueryParams: function Profiler__initFromURLQueryParams (locationSearch) {
this.initFromObject(searchToObject(locationSearch));
},
beginTest: function Profiler__beginTest (testName) {
currentTest = testName;
if (_profiler && enabled) {
_profiler.StartProfiler(profiler_entries, profiler_interval,
["js", "leaf", "stackwalk", "threads"], 4,
profiler_threadsArray, profiler_threadsArray.length);
if (_profiler.PauseSampling) {
_profiler.PauseSampling();
}
}
},
finishTest: function Profiler__finishTest () {
if (_profiler && enabled) {
_profiler.dumpProfileToFile(profiler_dir + "/" + currentTest + ".sps");
_profiler.StopProfiler();
}
},
finishStartupProfiling: function Profiler__finishStartupProfiling () {
if (_profiler && enabled) {
_profiler.dumpProfileToFile(profiler_dir + "/startup.sps");
_profiler.StopProfiler();
}
},
resume: function Profiler__resume (name, explicit) {
if (_profiler) {
if (_profiler.ResumeSampling) {
_profiler.ResumeSampling();
}
_profiler.AddMarker(explicit ? name : 'Start of test "' + (name || test_name) + '"');
}
},
pause: function Profiler__pause (name, explicit) {
if (_profiler) {
if (_profiler.PauseSampling) {
_profiler.PauseSampling();
}
_profiler.AddMarker(explicit ? name : 'End of test "' + (name || test_name) + '"');
}
},
mark: function Profiler__mark (marker, explicit) {
if (_profiler) {
_profiler.AddMarker(explicit ? marker : 'Profiler: "' + (marker || test_name) + '"');
}
}
};
})();

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,137 @@
from datetime import datetime
import sys
import os
import csv
import numpy
import collections
import argparse
import compare
from calendar import day_name
sys.path.insert(1, os.path.join(sys.path[0], '..'))
def get_branch(platform):
if platform.startswith('OSX'):
return compare.branch_map['Inbound']['pgo']['id']
return compare.branch_map['Inbound']['nonpgo']['id']
def get_all_test_tuples():
ret = []
for test in compare.test_map:
for platform in compare.platform_map:
ret.extend(get_tuple(test, platform))
return ret
def get_tuple(test, platform):
return [(compare.test_map[test]['id'],
get_branch(platform),
compare.platform_map[platform],
test, platform)]
def generate_report(tuple_list, filepath, mode='variance'):
avg = []
for test in tuple_list:
testid, branchid, platformid = test[:3]
data_dict = compare.getGraphData(testid, branchid, platformid)
week_avgs = []
if data_dict:
data = data_dict['test_runs']
data.sort(key=lambda x: x[3])
data = data[int(0.1*len(data)):int(0.9*len(data) + 1)]
time_dict = collections.OrderedDict()
days = {}
for point in data:
time = datetime.fromtimestamp(point[2]).strftime('%Y-%m-%d')
time_dict[time] = time_dict.get(time, []) + [point[3]]
for time in time_dict:
runs = len(time_dict[time])
weekday = datetime.strptime(time, '%Y-%m-%d').strftime('%A')
variance = numpy.var(time_dict[time])
if mode == 'variance':
days[weekday] = days.get(weekday, []) + [variance]
elif mode == 'count':
days[weekday] = days.get(weekday, []) + [runs]
line = ["-".join(test[3:])]
for day in day_name:
if mode == 'variance':
# removing top and bottom 10% to reduce outlier influence
tenth = len(days[day])/10
average = numpy.average(
sorted(days[day])[tenth:tenth*9 + 1]
)
elif mode == 'count':
average = numpy.average(days[day])
line.append("%.3f" % average)
week_avgs.append(average)
outliers = is_normal(week_avgs)
for j in range(7):
if j in outliers:
line[j + 1] = "**" + str(line[j + 1]) + "**"
avg.append(line)
with open(filepath, 'wb') as report:
avgs_header = csv.writer(report, quoting=csv.QUOTE_ALL)
avgs_header.writerow(['test-platform'] + list(day_name))
for line in avg:
out = csv.writer(report, quoting=csv.QUOTE_ALL)
out.writerow(line)
def is_normal(y):
# This is a crude initial attempt at detecting normal distributions
# TODO: Improve this
limit = 1.5
clean_week = []
outliers = []
# find a baseline for the week
if (min(y[0:4]) * limit) <= max(y[0:4]):
for i in range(1, 5):
if y[i] > (y[i-1]*limit) or y[i] > (y[i+1]*limit):
outliers.append(i)
continue
clean_week.append(y[i])
else:
clean_week = y
# look at weekends now
avg = sum(clean_week) / len(clean_week)
for i in range(5, 7):
# look for something outside of the 20% window
if (y[i]*1.2) < avg or y[i] > (avg*1.2):
outliers.append(i)
return outliers
def main():
parser = argparse.ArgumentParser(description="Generate weekdays reports")
parser.add_argument("--test", help="show only the test named TEST")
parser.add_argument("--platform",
help="show only the platform named PLATFORM")
parser.add_argument("--mode", help="select mode", default='variance')
args = parser.parse_args()
tuple_list = get_all_test_tuples()
f = 'report'
if args.platform:
tuple_list = filter(lambda x: x[4] == args.platform, tuple_list)
f += '-%s' % args.platform
if args.test:
tuple_list = filter(lambda x: x[3] == args.test, tuple_list)
f += '-%s' % args.test
f += '-%s' % args.mode
generate_report(tuple_list, filepath=f + '.csv', mode=args.mode)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,173 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/******************************************************************
* window.talosDebug provides some statistical functions
* (sum, average, median, stddev) and a tpRecordTime method which
* reports some statistics about the data set, including detected
* stability point of the data (detecting first few noisy elements).
* If tpRecordTime doesn't exist globally (i.e. running outside of
* talos, e.g. in a browser), then it points it to window.talosDebug.tpRecordTime
* Can be controlled by few properties (disable display, hardcoded
* stability point, etc)
*
* talos-debug.js: Bug 849558
*****************************************************************/
window.talosDebug = {
// Optional config properties
disabled : false,
ignore: -1, // Number of items to ignore at the begining of the set. -1 for auto-detect.
displayData: false, // If true, will also display all the data points.
fixed: 2, // default floating point digits for display.
// End of config
sum: function (values) {
return values.reduce(function(a, b){return a + b;});
},
average: function (values) {
var d = window.talosDebug;
return values.length ? d.sum(values) / values.length : 999999;
},
median: function (values) {
var clone = values.slice(0);
var sorted = clone.sort(function(a, b){ return (a > b) ? 1 : ((a < b) ? -1 : 0); });
var len = values.length;
if (!len)
return 999999;
if (len % 2) // We have a middle number
return sorted[(len - 1) / 2];
return (sorted[len / 2] + sorted[len / 2 - 1]) / 2; // Average value of the two middle items.
},
stddev: function (values, avg) {
if (values.length <= 1) {
return 0;
}
return Math.sqrt(
values.map(function (v) { return Math.pow(v - avg, 2); })
.reduce(function (a, b) { return a + b; }) / (values.length - 1));
},
// Estimate the number of warmup iterations of this data set (in a completely unscrientific way).
// returns -1 if could not find a stability point (supposedly meaning that the values
// are still in the process of convergence).
// Algorithm (not based on a scientific method which I know of):
// Target: Find a locally noisy value after the values show stability
// (stddev of a moving window stops decreasing), while trying to make sure
// that it wasn't a glitch and the next 2 floating window stddev are, indeed, not
// still decreasing.
// Do the above for few different window widths (3-7), get the maximum result of those.
// Now we should have an index which is at least the 2nd value within the stable part.
// We get stddev for that index..end (baseStd), and then go backwards as long as stddev is
// decreasing or within ~1% of baseStd, and return the earliest index for which it is.
detectWarmup: function (values) {
var MIN_WIDTH = 3;
var MAX_WIDTH = 7;
var d = window.talosDebug;
function windowStd(from, winSize) {
var win = values.slice(from, from + winSize);
return d.stddev(win, d.median(values));
}
var stableFrom = -1;
var overallAverage = d.median(values);
var overallStd = d.stddev(values, overallAverage);
for (var winWidth = MIN_WIDTH; winWidth < (MAX_WIDTH + 1); winWidth++) {
var prevStd = windowStd(0, winWidth);
for (var i = 1; i < values.length - winWidth - 3; i++) {
var w0 = windowStd(i + 0, winWidth);
var w1 = windowStd(i + 1, winWidth);
var w2 = windowStd(i + 2, winWidth);
var currWindow = values.slice(i, i + winWidth);
if (w0 >= prevStd && !(w1 < w0 && w2 < w1)) {
if (i > stableFrom)
stableFrom = i;
break;
}
prevStd = w0;
}
}
function withinPercentage(base, value, percentage) {
return Math.abs(base - value) < base * percentage / 100;
}
// Now go backwards as long as stddev decreases or doesn't increase in more than 1%.
var baseStd = d.stddev(values.slice(stableFrom), overallAverage);
var len = values.length;
while (true) {
var current = d.stddev(values.slice(stableFrom - 1), overallAverage);
// 100/len : the more items we have, the more sensitively we compare:
// for 100 items, we're allowing 2% over baseStd.
if (stableFrom > 0 && (current < baseStd || withinPercentage(baseStd, current, 200 / (len ? len : 100)))) {
stableFrom--;
} else {
break;
}
}
return stableFrom;
},
statsDisplay: function (collection) {
var d = window.talosDebug;
var std = d.stddev(collection, d.average(collection));
var avg = d.average(collection);
var med = d.median(collection);
return "Count: " + collection.length +
"\nAverage: " + avg.toFixed(d.fixed) +
"\nMedian: " + med.toFixed(d.fixed) +
"\nStdDev: " + std.toFixed(d.fixed) + " (" + (100 * std / (avg ? avg : 1)).toFixed(d.fixed) + "% of average)";
},
tpRecordTime: function (dataCSV) {
var d = window.talosDebug;
if (d.disabled)
return;
var collection = ("" + dataCSV).split(",").map(function(item){ return parseFloat(item); });
var res = d.statsDisplay(collection);
var warmup = (d.ignore >= 0) ? d.ignore : d.detectWarmup(collection);
if (warmup >= 0) {
res += "\n\nWarmup " + ((d.ignore >= 0) ? "requested: " : "auto-detected: ") + warmup;
warmedUp = collection.slice(warmup);
if (warmup) {
res += "\nAfter ignoring first " + (warmup > 1 ? (warmup + " items") : "item") + ":\n";
res += d.statsDisplay(warmedUp);
}
} else {
res += "\n\nWarmup auto-detection: Failed.";
}
if (d.displayData) {
var disp = collection.map(function(item){
return item.toFixed(d.fixed);
});
if (warmup >= 0)
disp.splice(warmup, 0, "[warmed-up:]");
res += "\n\nRecorded:\n" + disp.join(", ");
res += "\n\nStddev from item NN to last:\n";
disp = collection.map(function(value, index) {
return d.stddev(collection.slice(index), d.average(collection.slice(index))).toFixed(d.fixed);
});
if (warmup >= 0)
disp.splice(warmup, 0, "[warmed-up:]");
res += disp.join(", ");
} else {
res += "\n\n[set window.talosDebug.displayData=true for data]";
}
alert(res);
}
}
// Enable testing outside of talos by providing an alternative report function.
if (typeof (tpRecordTime) === 'undefined') {
tpRecordTime = window.talosDebug.tpRecordTime;
}

View file

@ -0,0 +1,79 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// base: relative or absolute path (http[s] or file, untested with ftp)
// files: array of file names relative to base to include at the zip
// callbacks: object with optional functions:
// onsuccess(result), onerror(exception), onprogress(percentage)
function createXpiDataUri(base, files, callbacks) {
// Synchronous XHR for http[s]/file (untested ftp), throws on any error
// Note that on firefox, file:// XHR can't access files outside base dir
function readBinFile(url) {
// The DOM will qualify the URI for us if it's relative (not IE6)
function isFileUri(uri) {
var a = document.createElement("a");
a.href = uri;
return a.href.toLowerCase().indexOf("file://") == 0;
}
var r = new XMLHttpRequest();
r.open("GET", url, false);
r.requestType = "arraybuffer";
r.overrideMimeType("text/plain; charset=x-user-defined");
try { r.send(); }
catch (e) { throw "FileNotRetrieved: " + url + " - " + e; }
// For 'file://' Firefox sets status=0 on success or throws otherwise
// In Firefox 34-ish onwards, success status is 200.
if (!(r.readyState == 4 && (r.status == 0 || r.status == 200)))
throw ("FileNotRetrieved: " + url + " - " + r.status + " " + r.statusText);
return r.response;
}
// Create base64 string for a binary array (btoa fails on arbitrary binary data)
function base64EncArr(aBytes) {
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding
"use strict;"
function uint6ToB64(nUint6) {
return nUint6 < 26 ? nUint6 + 65 : nUint6 < 52 ? nUint6 + 71 : nUint6 < 62 ?
nUint6 - 4 : nUint6 === 62 ? 43 : nUint6 === 63 ? 47 : 65;
}
var nMod3 = 2, sB64Enc = "";
for (var nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) {
nMod3 = nIdx % 3;
if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) { sB64Enc += "\r\n"; }
nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24);
if (nMod3 === 2 || aBytes.length - nIdx === 1) {
sB64Enc += String.fromCharCode(uint6ToB64(nUint24 >>> 18 & 63), uint6ToB64(nUint24 >>> 12 & 63),
uint6ToB64(nUint24 >>> 6 & 63), uint6ToB64(nUint24 & 63));
nUint24 = 0;
}
}
return sB64Enc.substr(0, sB64Enc.length - 2 + nMod3) + (nMod3 === 2 ? '' : nMod3 === 1 ? '=' : '==');
}
// Create the zip/xpi
try {
function dummy() {}
var onsuccess = callbacks.onsuccess || dummy;
var onerror = callbacks.onerror || dummy;
var onprogress = callbacks.onprogress || dummy;
var zip = new JSZip();
for (var i = 0; i < files.length; i++) {
zip.file(files[i], readBinFile(base + files[i]),
{binary: true, compression: "deflate"});
onprogress(100 * (i + 1) / (files.length + 1));
}
zip = zip.generate({type: "uint8array"});
onprogress(100);
setTimeout(onsuccess, 0, "data:application/x-xpinstall;base64," + base64EncArr(zip));
} catch (e) {
setTimeout(onerror, 0, e);
}
}

View file

@ -0,0 +1,209 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""
module to handle sps profilling.
"""
import os
import tempfile
import zipfile
import json
import mozfile
from mozlog import get_proxy_logger
from talos.profiler import symbolication, sps
LOG = get_proxy_logger()
class SpsProfile(object):
"""
Handle sps profilling.
This allow to collect sps profiling data and to zip results in one file.
"""
def __init__(self, upload_dir, browser_config, test_config):
self.upload_dir = upload_dir
self.browser_config, self.test_config = browser_config, test_config
# Create a temporary directory into which the tests can put
# their profiles. These files will be assembled into one big
# zip file later on, which is put into the MOZ_UPLOAD_DIR.
sps_profile_dir = tempfile.mkdtemp()
sps_profile_interval = test_config.get('sps_profile_interval', 1)
sps_profile_entries = test_config.get('sps_profile_entries', 1000000)
sps_profile_threads = 'GeckoMain,Compositor'
# Make sure no archive already exists in the location where
# we plan to output our profiler archive
self.profile_arcname = os.path.join(
self.upload_dir,
"profile_{0}.sps.zip".format(test_config['name'])
)
LOG.info("Clearing archive {0}".format(self.profile_arcname))
mozfile.remove(self.profile_arcname)
self.symbol_paths = {
'FIREFOX': tempfile.mkdtemp(),
'THUNDERBIRD': tempfile.mkdtemp(),
'WINDOWS': tempfile.mkdtemp()
}
LOG.info("Activating Gecko Profiling. Temp. profile dir:"
" {0}, interval: {1}, entries: {2}"
.format(sps_profile_dir,
sps_profile_interval,
sps_profile_entries))
self.profiling_info = {
"sps_profile_interval": sps_profile_interval,
"sps_profile_entries": sps_profile_entries,
"sps_profile_dir": sps_profile_dir,
"sps_profile_threads": sps_profile_threads
}
def option(self, name):
return self.profiling_info["sps_profile_" + name]
def update_env(self, env):
"""
update the given env to update some env vars if required.
"""
if not self.test_config.get('sps_profile_startup'):
return
# Set environment variables which will cause profiling to
# start as early as possible. These are consumed by Gecko
# itself, not by Talos JS code.
env.update({
'MOZ_PROFILER_STARTUP': '1',
'MOZ_PROFILER_INTERVAL': str(self.option('interval')),
'MOZ_PROFILER_ENTRIES': str(self.option('entries')),
"MOZ_PROFILER_THREADS": str(self.option('threads'))
})
def _save_sps_profile(self, cycle, symbolicator, missing_symbols_zip,
profile_path):
try:
with open(profile_path, 'r') as profile_file:
profile = json.load(profile_file)
symbolicator.dump_and_integrate_missing_symbols(
profile,
missing_symbols_zip)
symbolicator.symbolicate_profile(profile)
sps.save_profile(profile, profile_path)
except MemoryError:
LOG.critical(
"Ran out of memory while trying"
" to symbolicate profile {0} (cycle {1})"
.format(profile_path, cycle),
exc_info=True
)
except Exception:
LOG.critical("Encountered an exception during profile"
" symbolication {0} (cycle {1})"
.format(profile_path, cycle),
exc_info=True)
def symbolicate(self, cycle):
"""
Symbolicate sps profiling data for one cycle.
:param cycle: the number of the cycle of the test currently run.
"""
symbolicator = symbolication.ProfileSymbolicator({
# Trace-level logging (verbose)
"enableTracing": 0,
# Fallback server if symbol is not found locally
"remoteSymbolServer":
"http://symbolapi.mozilla.org:80/talos/",
# Maximum number of symbol files to keep in memory
"maxCacheEntries": 2000000,
# Frequency of checking for recent symbols to
# cache (in hours)
"prefetchInterval": 12,
# Oldest file age to prefetch (in hours)
"prefetchThreshold": 48,
# Maximum number of library versions to pre-fetch
# per library
"prefetchMaxSymbolsPerLib": 3,
# Default symbol lookup directories
"defaultApp": "FIREFOX",
"defaultOs": "WINDOWS",
# Paths to .SYM files, expressed internally as a
# mapping of app or platform names to directories
# Note: App & OS names from requests are converted
# to all-uppercase internally
"symbolPaths": self.symbol_paths
})
if self.browser_config['symbols_path']:
if mozfile.is_url(self.browser_config['symbols_path']):
symbolicator.integrate_symbol_zip_from_url(
self.browser_config['symbols_path']
)
else:
symbolicator.integrate_symbol_zip_from_file(
self.browser_config['symbols_path']
)
missing_symbols_zip = os.path.join(self.upload_dir,
"missingsymbols.zip")
try:
mode = zipfile.ZIP_DEFLATED
except NameError:
mode = zipfile.ZIP_STORED
sps_profile_dir = self.option('dir')
with zipfile.ZipFile(self.profile_arcname, 'a', mode) as arc:
# Collect all individual profiles that the test
# has put into sps_profile_dir.
for profile_filename in os.listdir(sps_profile_dir):
testname = profile_filename
if testname.endswith(".sps"):
testname = testname[0:-4]
profile_path = os.path.join(sps_profile_dir, profile_filename)
self._save_sps_profile(cycle, symbolicator,
missing_symbols_zip,
profile_path)
# Our zip will contain one directory per subtest,
# and each subtest directory will contain one or
# more cycle_i.sps files. For example, with
# test_config['name'] == 'tscrollx',
# profile_filename == 'iframe.svg.sps', i == 0,
# we'll get path_in_zip ==
# 'profile_tscrollx/iframe.svg/cycle_0.sps'.
cycle_name = "cycle_{0}.sps".format(cycle)
path_in_zip = \
os.path.join(
"profile_{0}".format(self.test_config['name']),
testname,
cycle_name
)
LOG.info(
"Adding profile {0} to archive {1}"
.format(path_in_zip, self.profile_arcname)
)
try:
arc.write(profile_path, path_in_zip)
except Exception:
LOG.exception(
"Failed to copy profile {0} as {1} to"
" archive {2}".format(profile_path,
path_in_zip,
self.profile_arcname)
)
def clean(self):
"""
Clean up temp folders created with the instance creation.
"""
mozfile.remove(self.option('dir'))
for symbol_path in self.symbol_paths.values():
mozfile.remove(symbol_path)

View file

@ -0,0 +1,115 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
"resource://gre/modules/Timer.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "StartupPerformance",
"resource:///modules/sessionstore/StartupPerformance.jsm");
// Observer Service topics.
const STARTUP_TOPIC = "profile-after-change";
// Process Message Manager topics.
const MSG_REQUEST = "session-restore-test?duration";
const MSG_PROVIDE = "session-restore-test:duration";
function nsSessionRestoreTalosTest() { }
nsSessionRestoreTalosTest.prototype = {
classID: Components.ID("{716346e5-0c45-4aa2-b601-da36f3c74bd8}"),
_xpcom_factory: XPCOMUtils.generateSingletonFactory(nsSessionRestoreTalosTest),
//////////////////////////////////////////////////////////////////////////////
//// nsISupports
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
//////////////////////////////////////////////////////////////////////////////
//// nsIObserver
observe: function DS_observe(aSubject, aTopic, aData) {
switch (aTopic) {
case STARTUP_TOPIC:
this.init();
break;
case StartupPerformance.RESTORED_TOPIC:
this.onReady(true);
break;
default:
throw new Error(`Unknown topic ${aTopic}`);
}
},
/**
* Perform initialization on profile-after-change.
*/
init: function() {
if (StartupPerformance.isRestored) {
this.onReady(true);
} else {
let sessionStartup = Cc["@mozilla.org/browser/sessionstartup;1"]
.getService(Ci.nsISessionStartup);
sessionStartup.onceInitialized.then(() => {
if (sessionStartup.sessionType == Ci.nsISessionStartup.NO_SESSION
|| sessionStartup.sessionType == Ci.nsISessionStartup.DEFER_SESSION) {
this.onReady(false);
} else {
Services.obs.addObserver(this, StartupPerformance.RESTORED_TOPIC, false);
}
});
}
},
/**
* Session Restore is complete, hurray.
*/
onReady: function(hasRestoredTabs) {
if (hasRestoredTabs) {
Services.obs.removeObserver(this, StartupPerformance.RESTORED_TOPIC);
}
try {
setTimeout(function() {
// `StartupPerformance.latestRestoredTimeStamp` actually becomes available only on the next tick.
let startup_info = Services.startup.getStartupInfo();
let duration =
hasRestoredTabs
? StartupPerformance.latestRestoredTimeStamp - startup_info.sessionRestoreInit
: startup_info.sessionRestored - startup_info.sessionRestoreInit;
// Broadcast startup duration information immediately, in case the talos
// page is already loaded.
Services.ppmm.broadcastAsyncMessage(MSG_PROVIDE, {duration});
// Now, in case the talos page isn't loaded yet, prepare to respond if it
// requestions the duration information.
Services.ppmm.addMessageListener(MSG_REQUEST, function listener() {
Services.ppmm.removeMessageListener(MSG_REQUEST, listener);
Services.ppmm.broadcastAsyncMessage(MSG_PROVIDE, {duration});
});
}, 0);
} catch (ex) {
dump(`SessionRestoreTalosTest: error ${ex}\n`);
dump(ex.stack);
dump("\n");
}
},
};
////////////////////////////////////////////////////////////////////////////////
//// Module
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsSessionRestoreTalosTest]);

View file

@ -0,0 +1,8 @@
# Register a component to be informed of startup. This component can then register
# itself to watch sessionstore-windows-restored. Once it has observed
# sessionstore-windows-restored, it will open the webpage with the harness.
component {716346e5-0c45-4aa2-b601-da36f3c74bd8} SessionRestoreTalosTest.js
contract @mozilla.org/talos/session-restore-test;1 {716346e5-0c45-4aa2-b601-da36f3c74bd8}
category profile-after-change nsSessionRestoreTalosTest @mozilla.org/talos/session-restore-test;1
content session-restore-test content/

View file

@ -0,0 +1,20 @@
<?xml version="1.0"?><RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#"><Description about="urn:mozilla:install-manifest">
<!-- Required Items -->
<em:id>session-restore-test-2@mozilla.org</em:id>
<em:name>Session Restore Startup Performance Test</em:name>
<em:version>2.0.9</em:version>
<em:targetApplication>
<Description>
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
<em:minVersion>1.5</em:minVersion>
<em:maxVersion>*</em:maxVersion>
</Description>
</em:targetApplication>
<!-- Optional Items -->
<em:creator>David Rajchenbach-Teller</em:creator>
<em:description>Bug 936630, bug 1098357. This add-on broadcasts the duration of session restore.</em:description>
<em:homepageURL>https://bugzilla.mozilla.org/show_bug.cgi?id=1098357</em:homepageURL>
</Description></RDF>

View file

@ -0,0 +1,21 @@
<html>
<head>
<meta charset="UTF-8"/>
<title>Session Restore Regression Test</title>
<script type="text/javascript" src="../../scripts/MozillaFileLogger.js"></script>
<script type="text/javascript" src="../../tests/quit.js"></script>
<script type="text/javascript" src="../../scripts/Profiler.js"></script>
<script type="text/javascript" src="./main.js">
</script>
<div>
<strong>Time between sessionRestoreInit and sessionRestored</strong>
<span id="sessionRestoreInit-to-sessionRestored">
(in progress)
</span>
</div>
</body>
</html>

View file

@ -0,0 +1,42 @@
"use strict";
var Services = Components.utils.import("resource://gre/modules/Services.jsm", {}).Services;
// Process Message Manager topics.
const MSG_REQUEST = "session-restore-test?duration";
const MSG_PROVIDE = "session-restore-test:duration";
Services.cpmm.addMessageListener(MSG_PROVIDE,
/**
* Display the result, send it to the harness and quit.
*/
function finish(msg) {
console.log(`main.js: received data on ${MSG_PROVIDE}`, msg);
Services.cpmm.removeMessageListener(MSG_PROVIDE, finish);
var duration = msg.data.duration;
Profiler.pause("This test measures the time between sessionRestoreInit and sessionRestored, ignore everything around that");
Profiler.initFromURLQueryParams(location.search);
Profiler.finishStartupProfiling();
setTimeout(function () {
// Show result on screen. Nice but not really necessary.
document.getElementById("sessionRestoreInit-to-sessionRestored").textContent = duration + "ms";
// Report data to Talos, if possible
dumpLog("__start_report" +
duration +
"__end_report\n\n");
// Next one is required by the test harness but not used
dumpLog("__startTimestamp" +
Date.now() +
"__endTimestamp\n\n");
goQuitApplication();
}, 0);
});
// In case the add-on has broadcasted the message before we were loaded,
// request a second broadcast.
Services.cpmm.sendAsyncMessage(MSG_REQUEST, {});

View file

@ -0,0 +1 @@
{"profile-after-change":true,"final-ui-startup":true,"sessionstore-windows-restored":true,"quit-application-granted":true,"quit-application":true,"sessionstore-final-state-write-complete":true,"profile-change-net-teardown":true,"profile-change-teardown":true,"profile-before-change":true}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,124 @@
<html>
<meta charset='utf-8'>
<script language="JavaScript" type="text/javascript" src="../scripts/MozillaFileLogger.js"></script>
<script language="JavaScript" type="text/javascript" src="../scripts/Profiler.js"></script>
<script language="JavaScript" type="text/javascript" src="chrome://talos-powers-content/content/TalosPowersContent.js"></script>
<script language="JavaScript" type="text/javascript" src="../tests/quit.js"></script>
<script>
var OPENER_DELAY = 1000; // ms delay between tests
var REPEAT_COUNT = 20;
var kid, kidStartTime, kidEndTime, windowIndex, openTimes;
var auto = document.location.search.split('=')[1]; // ?auto=1
/*
* tpaint -- measures the amount of time between opening a new window
* its contents being painted.
*
* XXX When run manually, outside of automation, need to:
* 1) allow popups
* 2) set dom.send_after_paint_to_content = true
* 3) set browser.link.open_newwindow = 2 (else it opens in a tab)
*/
function startTest() {
// Initialize
windowIndex = -1;
openTimes = [];
Profiler.initFromURLQueryParams(location.search);
Profiler.beginTest("tpaint");
scheduleNextWindow();
}
function scheduleNextWindow() {
windowIndex++;
if (windowIndex >= REPEAT_COUNT) {
window.setTimeout(reportTimes, 0);
return;
}
TalosPowersContent.forceCCAndGC();
window.setTimeout(openWindow, OPENER_DELAY);
}
function calcMedian( numbers ) {
// Avoid changing the original array.
var sortedNumbers = Array.from(numbers);
sortedNumbers.sort( function (a,b){ return a-b; } );
var n = Math.floor( sortedNumbers.length / 2 );
if (sortedNumbers.length % 2) {
return sortedNumbers[n];
}
return ( sortedNumbers[n-1] + sortedNumbers[n] ) / 2;
}
function reportTimes() {
Profiler.finishTest();
var min = 99999, max = 0, avg = 0;
var count = openTimes.length;
for (var i = 0; i < count; i++) {
var time = openTimes[i];
avg += time;
if (time < min)
min = time;
if (time > max)
max = time;
}
avg = (avg / count).toFixed(2);
min = min.toFixed(2);
max = max.toFixed(2);
med = calcMedian(openTimes);
if (auto) {
dumpLog("__start_report" + openTimes.join('|') + "__end_report");
var now = (new Date()).getTime();
dumpLog("__startTimestamp" + now + "__endTimestamp\n");
dumpLog("openingTimes="+openTimes.slice(1)+"\n");
dumpLog("avgOpenTime:" + avg + "\n" );
dumpLog("minOpenTime:" + min + "\n" );
dumpLog("maxOpenTime:" + max + "\n" );
dumpLog("medOpenTime:" + med + "\n" );
dumpLog("__xulWinOpenTime:" + med + "\n");
goQuitApplication();
window.close();
} else {
alert("Average over " + count + " runs: " + avg + "\n" +
"(min=" + min + ", max=" + max + ")\n" +
"runs=" + openTimes.join(','));
}
}
function childIsOpen() {
kidEndTime = window.performance.now();
Profiler.pause('tpaint ' + windowIndex);
openTimes[windowIndex] = kidEndTime - kidStartTime;
scheduleNextWindow();
}
var kidHTML = "<html><meta charset='utf-8'><script>" +
"var e = 'MozAfterPaint';" +
"function done() {" +
" window.removeEventListener(e, done, true);" +
" window.opener.childIsOpen();" +
" window.close();" +
"}" +
"window.addEventListener(e, done, true);" +
"</" + "script>TPAINT</html>";
var kidURI = "data:text/html," + encodeURI(kidHTML);
function openWindow() {
Profiler.resume('tpaint ' + windowIndex);
kidStartTime = window.performance.now();
kid = window.open(kidURI);
}
if (auto)
startTest();
</script>
<h2>tpaint runner</h2>
<button onclick="startTest()">Start</button>
</html>

View file

@ -0,0 +1,2 @@
content tresize content/
overlay chrome://browser/content/browser.xul chrome://tresize/content/tresize.overlay.xul

View file

@ -0,0 +1,135 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// - NOTE: This file is duplicated verbatim at:
// - talos/scripts/Profiler.js
// - talos/pageloader/chrome/Profiler.js
// - talos/tests/devtools/addon/content/Profiler.js
// - talos/tests/tart/addon/content/Profiler.js
// - talos/startup_test/tresize/addon/content/Profiler.js
//
// - Please keep these copies in sync.
// - Please make sure your changes apply cleanly to all use cases.
// Finer grained profiler control
//
// Use this object to pause and resume the profiler so that it only profiles the
// relevant parts of our tests.
var Profiler;
(function(){
var _profiler;
// If this script is loaded in a framescript context, there won't be a
// document object, so just use a fallback value in that case.
var test_name = this.document ? this.document.location.pathname : "unknown";
// Whether Profiler has been initialized. Until that happens, most calls
// will be ignored.
var enabled = false;
// The subtest name that beginTest() was called with.
var currentTest = "";
// Profiling settings.
var profiler_interval, profiler_entries, profiler_threadsArray, profiler_dir;
try {
// Outside of talos, this throws a security exception which no-op this file.
// (It's not required nor allowed for addons since Firefox 17)
// It's used inside talos from non-privileged pages (like during tscroll),
// and it works because talos disables all/most security measures.
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
} catch (e) {}
try {
_profiler = Components.classes["@mozilla.org/tools/profiler;1"].getService(Components.interfaces.nsIProfiler);
} catch (ex) { (typeof(dumpLog) == "undefined" ? dump : dumpLog)(ex + "\n"); }
// Parses an url query string into a JS object.
function searchToObject(locationSearch) {
var pairs = locationSearch.substring(1).split("&");
var result = {};
for (var i in pairs) {
if (pairs[i] !== "") {
var pair = pairs[i].split("=");
result[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || "");
}
}
return result;
}
Profiler = {
/**
* Initialize the profiler using profiler settings supplied in a JS object.
* The following properties on the object are respected:
* - sps_profile_interval
* - sps_profile_entries
* - sps_profile_threads
* - sps_profile_dir
*/
initFromObject: function Profiler__initFromObject (obj) {
if (obj &&
("sps_profile_dir" in obj) && typeof obj.sps_profile_dir == "string" &&
("sps_profile_interval" in obj) && Number.isFinite(obj.sps_profile_interval * 1) &&
("sps_profile_entries" in obj) && Number.isFinite(obj.sps_profile_entries * 1) &&
("sps_profile_threads" in obj) && typeof obj.sps_profile_threads == "string") {
profiler_interval = obj.sps_profile_interval;
profiler_entries = obj.sps_profile_entries;
profiler_threadsArray = obj.sps_profile_threads.split(",");
profiler_dir = obj.sps_profile_dir;
enabled = true;
}
},
initFromURLQueryParams: function Profiler__initFromURLQueryParams (locationSearch) {
this.initFromObject(searchToObject(locationSearch));
},
beginTest: function Profiler__beginTest (testName) {
currentTest = testName;
if (_profiler && enabled) {
_profiler.StartProfiler(profiler_entries, profiler_interval,
["js", "leaf", "stackwalk", "threads"], 4,
profiler_threadsArray, profiler_threadsArray.length);
if (_profiler.PauseSampling) {
_profiler.PauseSampling();
}
}
},
finishTest: function Profiler__finishTest () {
if (_profiler && enabled) {
_profiler.dumpProfileToFile(profiler_dir + "/" + currentTest + ".sps");
_profiler.StopProfiler();
}
},
finishStartupProfiling: function Profiler__finishStartupProfiling () {
if (_profiler && enabled) {
_profiler.dumpProfileToFile(profiler_dir + "/startup.sps");
_profiler.StopProfiler();
}
},
resume: function Profiler__resume (name, explicit) {
if (_profiler) {
if (_profiler.ResumeSampling) {
_profiler.ResumeSampling();
}
_profiler.AddMarker(explicit ? name : 'Start of test "' + (name || test_name) + '"');
}
},
pause: function Profiler__pause (name, explicit) {
if (_profiler) {
if (_profiler.PauseSampling) {
_profiler.PauseSampling();
}
_profiler.AddMarker(explicit ? name : 'End of test "' + (name || test_name) + '"');
}
},
mark: function Profiler__mark (marker, explicit) {
if (_profiler) {
_profiler.AddMarker(explicit ? marker : 'Profiler: "' + (marker || test_name) + '"');
}
}
};
})();

View file

@ -0,0 +1,19 @@
(function() {
const TRESIZE_PREFIX = "tresize@mozilla.org:";
addEventListener(TRESIZE_PREFIX + "chrome-run-event", function (e) {
var uniqueMessageId = TRESIZE_PREFIX + content.document.documentURI + Date.now() + Math.random();
addMessageListener(TRESIZE_PREFIX + "chrome-run-reply", function done(reply) {
if (reply.data.id == uniqueMessageId) {
removeMessageListener(TRESIZE_PREFIX + "chrome-run-reply", done);
content.wrappedJSObject.logResults(reply.data.result);
}
});
sendAsyncMessage(TRESIZE_PREFIX + "chrome-run-message", {
id: uniqueMessageId,
locationSearch: e.detail.locationSearch
});
}, false);
})()

View file

@ -0,0 +1,40 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this file,
- You can obtain one at http://mozilla.org/MPL/2.0/. -->
<html>
<head>
<script language="Javascript" type="text/javascript" src="../../../../scripts/Profiler.js"></script>
<script language="Javascript" type="text/javascript" src="../../../../scripts/MozillaFileLogger.js"></script>
<script language="JavaScript" type="text/javascript" src="../../../../tests/quit.js"></script>
<script language="javascript" type="text/javascript">
function runTest() {
dispatchEvent(
new CustomEvent("tresize@mozilla.org:chrome-run-event",
{ bubbles: true,
detail: { locationSearch: location.search } })
);
}
function logResults(data) {
if (typeof dumpLog != "undefined") {
data.forEach(function(line) {
dumpLog(line);
});
window.setTimeout(function() {
goQuitApplication();
window.close();
}, 0);
} else {
alert(data[0].split("__start_report")[1].split("__end_report")[0]);
}
}
</script>
</head>
<body id="body" onload="runTest();">
<div style="text-align:center; margin-left:auto; margin-right:auto; background-color:Silver; width:100px; height:100%;">RESIZE</div>
</body>
</html>

View file

@ -0,0 +1,81 @@
/*
* tresize-test - A purer form of paint measurement than tpaint. This
* test opens a single window positioned at 10,10 and sized to 425,425,
* then resizes the window outward |max| times measuring the amount of
* time it takes to repaint each resize. Dumps the resulting dataset
* and average to stdout or logfile.
*/
var dataSet = new Array();
var windowSize = 425;
var resizeIncrement = 2;
var count = 0;
var max = 300;
var dumpDataSet = false;
var doneCallback = null;
function painted() {
window.removeEventListener("MozAfterPaint", painted, true);
var paintTime = window.performance.now();
Profiler.pause("resize " + count);
dataSet[count].end = paintTime;
setTimeout(resizeCompleted, 20);
}
function resizeTest() {
try {
windowSize += resizeIncrement;
window.addEventListener("MozAfterPaint", painted, true);
Profiler.resume("resize " + count);
dataSet[count] = {'start': window.performance.now()};
window.resizeTo(windowSize,windowSize);
} catch(ex) { finish([ex + '\n']); }
}
function testCompleted() {
try {
Profiler.finishTest();
var total = 0;
var diffs = [];
for (var idx = 0; idx < count; idx++) {
var diff = dataSet[idx].end - dataSet[idx].start;
total += diff;
diffs.push(diff);
}
var average = (total/count);
var retVal = [];
if (dumpDataSet) {
retVal.push('__start_reporttresize-test.html,' + diffs + '__end_report\n');
} else {
retVal.push('__start_report' + average + '__end_report\n');
}
retVal.push('__startTimestamp' + Date.now() + '__endTimestamp\n');
finish(retVal);
} catch(ex) { finish([ex + '\n']); }
}
function resizeCompleted() {
count++;
if (count >= max) {
testCompleted();
} else {
resizeTest();
}
}
function runTest(callback, locationSearch) {
doneCallback = callback;
window.moveTo(10,10);
window.resizeTo(windowSize,windowSize);
Profiler.initFromURLQueryParams(locationSearch);
Profiler.beginTest("tresize");
resizeTest();
}
function finish(msgs) {
if (doneCallback) {
doneCallback(msgs);
}
}

View file

@ -0,0 +1,25 @@
<?xml version="1.0"?>
<overlay id="Scrapper-Overlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/x-javascript" src="Profiler.js" />
<script type="application/x-javascript" src="tresize.js" />
<script type="application/x-javascript">
(function(){
const TRESIZE_PREFIX = "tresize@mozilla.org:";
var groupMM = window.getGroupMessageManager("browsers");
groupMM.loadFrameScript("chrome://tresize/content/framescript.js", true);
// listener/executor on the chrome process for tresize.html
groupMM.addMessageListener(TRESIZE_PREFIX + "chrome-run-message", function listener(m) {
function sendResult(result) {
groupMM.broadcastAsyncMessage(TRESIZE_PREFIX + "chrome-run-reply", {
id: m.data.id,
result: result
});
}
runTest(sendResult, m.data.locationSearch);
});
})();
</script>
</overlay>

View file

@ -0,0 +1,21 @@
<?xml version="1.0"?><RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#"><Description about="urn:mozilla:install-manifest">
<!-- Required Items -->
<em:id>tresize@mozilla.org</em:id>
<em:name>TResize - Firefox Window Resize Test</em:name>
<em:version>1.0.1</em:version>
<em:targetApplication>
<Description>
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
<em:minVersion>1.5</em:minVersion>
<em:maxVersion>*</em:maxVersion>
</Description>
</em:targetApplication>
<!-- Optional Items -->
<em:creator>Joel Maher</em:creator>
<em:description>https://wiki.mozilla.org/Buildbot/Talos/Tests</em:description>
<em:homepageURL>https://wiki.mozilla.org/Buildbot/Talos/Tests</em:homepageURL>
<em:multiprocessCompatible>true</em:multiprocessCompatible>
</Description></RDF>

View file

@ -0,0 +1,69 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this file,
- You can obtain one at http://mozilla.org/MPL/2.0/. -->
<html>
<meta charset="utf-8">
<!-- Pick off begin time as a cgi argument and print it out -->
<!-- call this with an arg, e.g. file://foo/startup-test.html?begin=12345678 -->
<!-- In-line this to avoid compilation. -->
<script language="Javascript" type="text/javascript" src="chrome://talos-powers-content/content/TalosPowersContent.js"></script>
<script language="Javascript" type="text/javascript" src="../scripts/MozillaFileLogger.js"></script>
<script language="Javascript" type="text/javascript" src="../scripts/Profiler.js"></script>
<script language="JavaScript" type="text/javascript" src="../tests/quit.js"></script>
<script language="javascript" type="text/javascript">
function painted() {
Profiler.pause();
Profiler.initFromURLQueryParams(location.search);
Profiler.finishStartupProfiling();
window.removeEventListener("MozAfterPaint", painted, true);
TalosPowersContent.getStartupInfo().then(function(startupInfo) {
let startupTime = startupInfo['firstPaint'] - startupInfo['process'];
document.body.textContent = 'Startup time = ' + startupTime + ' ms';
if (window.dump) {
dumpLog('__start_report' + startupTime + '__end_report\n\n');
}
dumpConsoleAndQuit();
});
}
function failed() {
if (window.dump) {
dumpLog('BROWSER FAILED TO GENERATE MOZAFTERPAINT IN 5 SECONDS');
}
dumpConsoleAndQuit();
}
function dumpConsoleAndQuit() {
var messages = {}
try {
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
var consoleService = Components.classes['@mozilla.org/consoleservice;1'].getService(Components.interfaces.nsIConsoleService);
consoleService.getMessageArray(messages, {});
} catch(ex) {
dumpLog(ex + '\n');
}
for (var i = 0; i < messages.value.length; i++)
dumpLog(messages.value[i].message + '\n');
// Close window asynchronously, there might still be startup operations that still need to run
window.setTimeout(function() {
if (window.dump) {
dumpLog('__startTimestamp' + Date.now() + '__endTimestamp\n');
}
goQuitApplication();
window.close();
}, 0);
}
window.addEventListener("MozAfterPaint", painted, true);
setTimeout(failed, 5000);
</script>
<body></body>
</html>

View file

@ -0,0 +1,5 @@
# Talos Powers Add-on
This add-ons purpose is to give talos content some special powers
that are normally restricted to the browser.

View file

@ -0,0 +1,5 @@
content talos-powers chrome/
content talos-powers-content content/ contentaccessible=yes
component {f5d53443-d58d-4a2f-8df0-98525d4f91ad} components/TalosPowersService.js
contract @mozilla.org/talos/talos-powers-service;1 {f5d53443-d58d-4a2f-8df0-98525d4f91ad}
category profile-after-change TalosPowersService @mozilla.org/talos/talos-powers-service;1

View file

@ -0,0 +1,123 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// This file is loaded as a framescript
var { interfaces: Ci, utils: Cu } = Components;
/**
* Content that wants to quit the whole session should
* fire the TalosQuitApplication custom event. This will
* attempt to force-quit the browser.
*/
addEventListener("TalosQuitApplication", event => {
// If we're loaded in a low-priority background process, like
// the background page thumbnailer, then we shouldn't be allowed
// to quit the whole application. This is a workaround until
// bug 1164459 is fixed.
let priority = docShell.QueryInterface(Ci.nsIDocumentLoader)
.loadGroup
.QueryInterface(Ci.nsISupportsPriority)
.priority;
if (priority != Ci.nsISupportsPriority.PRIORITY_LOWEST) {
sendAsyncMessage("Talos:ForceQuit", event.detail);
}
});
addEventListener("TalosContentProfilerCommand", (e) => {
let name = e.detail.name;
let data = e.detail.data;
sendAsyncMessage("TalosContentProfiler:Command", { name, data });
});
addMessageListener("TalosContentProfiler:Response", (msg) => {
let name = msg.data.name;
let data = msg.data.data;
let event = Cu.cloneInto({
bubbles: true,
detail: {
name: name,
data: data,
},
}, content);
content.dispatchEvent(
new content.CustomEvent("TalosContentProfilerResponse", event));
});
addEventListener("TalosPowersContentForceCCAndGC", (e) => {
Cu.forceGC();
Cu.forceCC();
Cu.forceShrinkingGC();
sendSyncMessage("TalosPowersContent:ForceCCAndGC");
});
addEventListener("TalosPowersContentFocus", (e) => {
if (content.location.protocol != "file:" &&
content.location.hostname != "localhost" &&
content.location.hostname != "127.0.0.1") {
throw new Error("TalosPowersContentFocus may only be used with local content");
}
content.focus();
let contentEvent = Cu.cloneInto({
bubbles: true,
}, content);
content.dispatchEvent(new content.CustomEvent("TalosPowersContentFocused", contentEvent));
}, true, true);
addEventListener("TalosPowersContentGetStartupInfo", (e) => {
sendAsyncMessage("TalosPowersContent:GetStartupInfo");
addMessageListener("TalosPowersContent:GetStartupInfo:Result",
function onResult(msg) {
removeMessageListener("TalosPowersContent:GetStartupInfo:Result",
onResult);
let event = Cu.cloneInto({
bubbles: true,
detail: msg.data,
}, content);
content.dispatchEvent(
new content.CustomEvent("TalosPowersContentGetStartupInfoResult",
event));
});
});
/* *
* Mediator for the generic ParentExec mechanism.
* Listens for a query event from the content, forwards it as a query message
* to the parent, listens to a parent reply message, and forwards it as a reply
* event for the content to capture.
* The consumer API for this mechanism is at content/TalosPowersContent.js
* and the callees are at ParentExecServices at components/TalosPowersService.js
*/
addEventListener("TalosPowers:ParentExec:QueryEvent", function (e) {
if (content.location.protocol != "file:" &&
content.location.hostname != "localhost" &&
content.location.hostname != "127.0.0.1") {
throw new Error("TalosPowers:ParentExec may only be used with local content");
}
let uniqueMessageId = "TalosPowers:ParentExec:"
+ content.document.documentURI + Date.now() + Math.random();
// Listener for the reply from the parent process
addMessageListener("TalosPowers:ParentExec:ReplyMsg", function done(reply) {
if (reply.data.id != uniqueMessageId)
return;
removeMessageListener("TalosPowers:ParentExec:ReplyMsg", done);
// reply to content via an event
let contentEvent = Cu.cloneInto({
bubbles: true,
detail: reply.data.result
}, content);
content.dispatchEvent(new content.CustomEvent(e.detail.listeningTo, contentEvent));
});
// Send the query to the parent process
sendAsyncMessage("TalosPowers:ParentExec:QueryMsg", {
command: e.detail.command,
id: uniqueMessageId
});
}, false, true); // wantsUntrusted since we're exposing to unprivileged

View file

@ -0,0 +1,319 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const { interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
const FRAME_SCRIPT = "chrome://talos-powers/content/talos-powers-content.js";
function TalosPowersService() {
this.wrappedJSObject = this;
};
TalosPowersService.prototype = {
classDescription: "Talos Powers",
classID: Components.ID("{f5d53443-d58d-4a2f-8df0-98525d4f91ad}"),
contractID: "@mozilla.org/talos/talos-powers-service;1",
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
observe(subject, topic, data) {
switch(topic) {
case "profile-after-change":
// Note that this observation is registered in the chrome.manifest
// for this add-on.
this.init();
break;
case "xpcom-shutdown":
this.uninit();
break;
}
},
init() {
Services.mm.loadFrameScript(FRAME_SCRIPT, true);
Services.mm.addMessageListener("Talos:ForceQuit", this);
Services.mm.addMessageListener("TalosContentProfiler:Command", this);
Services.mm.addMessageListener("TalosPowersContent:ForceCCAndGC", this);
Services.mm.addMessageListener("TalosPowersContent:GetStartupInfo", this);
Services.mm.addMessageListener("TalosPowers:ParentExec:QueryMsg", this);
Services.obs.addObserver(this, "xpcom-shutdown", false);
},
uninit() {
Services.obs.removeObserver(this, "xpcom-shutdown", false);
},
receiveMessage(message) {
switch(message.name) {
case "Talos:ForceQuit": {
this.forceQuit(message.data);
break;
}
case "TalosContentProfiler:Command": {
this.receiveProfileCommand(message);
break;
}
case "TalosPowersContent:ForceCCAndGC": {
Cu.forceGC();
Cu.forceCC();
Cu.forceShrinkingGC();
break;
}
case "TalosPowersContent:GetStartupInfo": {
this.receiveGetStartupInfo(message);
break;
}
case "TalosPowers:ParentExec:QueryMsg": {
this.RecieveParentExecCommand(message);
break;
}
}
},
/**
* Enable the SPS profiler with some settings and then pause
* immediately.
*
* @param data (object)
* A JavaScript object with the following properties:
*
* entries (int):
* The sampling buffer size in bytes.
*
* interval (int):
* The sampling interval in milliseconds.
*
* threadsArray (array of strings):
* The thread names to sample.
*/
profilerBegin(data) {
Services.profiler
.StartProfiler(data.entries, data.interval,
["js", "leaf", "stackwalk", "threads"], 4,
data.threadsArray, data.threadsArray.length);
Services.profiler.PauseSampling();
},
/**
* Assuming the Profiler is running, dumps the Profile from all sampled
* processes and threads to the disk. The Profiler will be stopped once
* the profiles have been dumped. This method returns a Promise that
* will resolve once this has occurred.
*
* @param profileFile (string)
* The name of the file to write to.
*
* @returns Promise
*/
profilerFinish(profileFile) {
return new Promise((resolve, reject) => {
Services.profiler.getProfileDataAsync().then((profile) => {
let encoder = new TextEncoder();
let array = encoder.encode(JSON.stringify(profile));
OS.File.writeAtomic(profileFile, array, {
tmpPath: profileFile + ".tmp",
}).then(() => {
Services.profiler.StopProfiler();
resolve();
});
}, (error) => {
Cu.reportError("Failed to gather profile: " + error);
// FIXME: We should probably send a message down to the
// child which causes it to reject the waiting Promise.
reject();
});
});
},
/**
* Pauses the Profiler, optionally setting a parent process marker before
* doing so.
*
* @param marker (string, optional)
* A marker to set before pausing.
*/
profilerPause(marker=null) {
if (marker) {
Services.profiler.AddMarker(marker);
}
Services.profiler.PauseSampling();
},
/**
* Resumes a pausedProfiler, optionally setting a parent process marker
* after doing so.
*
* @param marker (string, optional)
* A marker to set after resuming.
*/
profilerResume(marker=null) {
Services.profiler.ResumeSampling();
if (marker) {
Services.profiler.AddMarker(marker);
}
},
/**
* Adds a marker to the Profile in the parent process.
*/
profilerMarker(marker) {
Services.profiler.AddMarker(marker);
},
receiveProfileCommand(message) {
const ACK_NAME = "TalosContentProfiler:Response";
let mm = message.target.messageManager;
let name = message.data.name;
let data = message.data.data;
switch(name) {
case "Profiler:Begin": {
this.profilerBegin(data);
// profilerBegin will cause the parent to send an async message to any
// child processes to start profiling. Because messages are serviced
// in order, we know that by the time that the child services the
// ACK message, that the profiler has started in its process.
mm.sendAsyncMessage(ACK_NAME, { name });
break;
}
case "Profiler:Finish": {
// The test is done. Dump the profile.
let profileFile = data.profileFile;
this.profilerFinish(data.profileFile).then(() => {
mm.sendAsyncMessage(ACK_NAME, { name });
});
break;
}
case "Profiler:Pause": {
this.profilerPause(data.marker);
mm.sendAsyncMessage(ACK_NAME, { name });
break;
}
case "Profiler:Resume": {
this.profilerResume(data.marker);
mm.sendAsyncMessage(ACK_NAME, { name });
break;
}
case "Profiler:Marker": {
this.profilerMarker(data.marker);
mm.sendAsyncMessage(ACK_NAME, { name });
}
}
},
forceQuit(messageData) {
if (messageData && messageData.waitForSafeBrowsing) {
let SafeBrowsing = Cu.import("resource://gre/modules/SafeBrowsing.jsm", {}).SafeBrowsing;
let whenDone = () => {
this.forceQuit();
};
SafeBrowsing.addMozEntriesFinishedPromise.then(whenDone, whenDone);
// Speed things up in case nobody else called this:
SafeBrowsing.init();
return;
}
let enumerator = Services.wm.getEnumerator(null);
while (enumerator.hasMoreElements()) {
let domWindow = enumerator.getNext();
domWindow.close();
}
try {
Services.startup.quit(Services.startup.eForceQuit);
} catch(e) {
dump('Force Quit failed: ' + e);
}
},
receiveGetStartupInfo(message) {
let mm = message.target.messageManager;
let startupInfo = Services.startup.getStartupInfo();
if (!startupInfo["firstPaint"]) {
// It's possible that we were called early enough that
// the firstPaint measurement hasn't been set yet. In
// that case, we set up an observer for the next time
// a window is painted and re-retrieve the startup info.
let obs = function(subject, topic) {
Services.obs.removeObserver(this, topic);
startupInfo = Services.startup.getStartupInfo();
mm.sendAsyncMessage("TalosPowersContent:GetStartupInfo:Result",
startupInfo);
};
Services.obs.addObserver(obs, "widget-first-paint", false);
} else {
mm.sendAsyncMessage("TalosPowersContent:GetStartupInfo:Result",
startupInfo);
}
},
// These services are exposed to local unprivileged content.
// Each service is a function which accepts an argument, a callback for sending
// the reply (possibly async), and the parent window as a utility.
// arg/reply semantice are service-specific.
// To add a service: add a method at ParentExecServices here, then at the content:
// <script src="chrome://talos-powers-content/content/TalosPowersContent.js"></script>
// and then e.g. TalosPowersParent.exec("sampleParentService", myArg, myCallback)
// Sample service:
/*
// arg: anything. return: sample reply
sampleParentService: function(arg, callback, win) {
win.setTimeout(function() {
callback("sample reply for: " + arg);
}, 500);
},
*/
ParentExecServices: {
// arg: ignored. return: handle (number) for use with stopFrameTimeRecording
startFrameTimeRecording: function(arg, callback, win) {
var rv = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.startFrameTimeRecording();
callback(rv);
},
// arg: handle from startFrameTimeRecording. return: array with composition intervals
stopFrameTimeRecording: function(arg, callback, win) {
var rv = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.stopFrameTimeRecording(arg);
callback(rv);
},
},
RecieveParentExecCommand(msg) {
function sendResult(result) {
let mm = msg.target.messageManager;
mm.sendAsyncMessage("TalosPowers:ParentExec:ReplyMsg", {
id: msg.data.id,
result: result
});
}
let command = msg.data.command;
if (!this.ParentExecServices.hasOwnProperty(command.name))
throw new Error("TalosPowers:ParentExec: Invalid service '" + command.name + "'");
this.ParentExecServices[command.name](command.data, sendResult, msg.target.ownerGlobal);
},
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TalosPowersService]);

View file

@ -0,0 +1,256 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This utility script is for instrumenting your Talos test for
* performance profiles while running within content. If your test
* is running in the parent process, you should use
* TalosParentProfiler.js instead to avoid the messaging overhead.
*/
var TalosContentProfiler;
(function() {
// Whether or not this TalosContentProfiler object has had initFromObject
// or initFromURLQueryParams called on it. Any functions that will send
// events to the parent to change the behaviour of the SPS Profiler
// should only be called after calling either initFromObject or
// initFromURLQueryParams.
var initted = false;
// The subtest name that beginTest() was called with.
var currentTest = "unknown";
// Profiler settings.
var interval, entries, threadsArray, profileDir;
try {
// Outside of talos, this throws a security exception which no-op this file.
// (It's not required nor allowed for addons since Firefox 17)
// It's used inside talos from non-privileged pages (like during tscroll),
// and it works because talos disables all/most security measures.
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
} catch (e) {}
Components.utils.import("resource://gre/modules/Services.jsm");
/**
* Emits a TalosContentProfiler prefixed event and then returns a Promise
* that resolves once a corresponding acknowledgement event is
* dispatched on our document.
*
* @param name
* The name of the event that will be TalosContentProfiler prefixed and
* eventually sent to the parent.
* @param data (optional)
* The data that will be sent to the parent.
* @returns Promise
* Resolves when a corresponding acknowledgement event is dispatched
* on this document.
*/
function sendEventAndWait(name, data={}) {
return new Promise((resolve) => {
var event = new CustomEvent("TalosContentProfilerCommand", {
bubbles: true,
detail: {
name: name,
data: data,
}
});
document.dispatchEvent(event);
addEventListener("TalosContentProfilerResponse", function onResponse(event) {
if (event.detail.name != name) {
return;
}
removeEventListener("TalosContentProfilerResponse", onResponse);
resolve(event.detail.data);
});
});
}
/**
* Parses an url query string into a JS object.
*
* @param locationSearch (string)
* The location string to parse.
* @returns Object
* The GET params from the location string as
* key-value pairs in the Object.
*/
function searchToObject(locationSearch) {
var pairs = locationSearch.substring(1).split("&");
var result = {};
for (var i in pairs) {
if (pairs[i] !== "") {
var pair = pairs[i].split("=");
result[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || "");
}
}
return result;
}
TalosContentProfiler = {
/**
* Initialize the profiler using profiler settings supplied in a JS object.
*
* @param obj (object)
* The following properties on the object are respected:
* sps_profile_interval (int)
* sps_profile_entries (int)
* sps_profile_threads (string, comma separated list of threads to filter with)
* sps_profile_dir (string)
*/
initFromObject(obj={}) {
if (!initted) {
if (("sps_profile_dir" in obj) && typeof obj.sps_profile_dir == "string" &&
("sps_profile_interval" in obj) && Number.isFinite(obj.sps_profile_interval * 1) &&
("sps_profile_entries" in obj) && Number.isFinite(obj.sps_profile_entries * 1) &&
("sps_profile_threads" in obj) && typeof obj.sps_profile_threads == "string") {
interval = obj.sps_profile_interval;
entries = obj.sps_profile_entries;
threadsArray = obj.sps_profile_threads.split(",");
profileDir = obj.sps_profile_dir;
initted = true;
} else {
console.error("Profiler could not init with object: " + JSON.stringify(obj));
}
}
},
/**
* Initialize the profiler using a string from a location string.
*
* @param locationSearch (string)
* The location string to initialize with.
*/
initFromURLQueryParams(locationSearch) {
this.initFromObject(searchToObject(locationSearch));
},
/**
* A Talos test is about to start. This will return a Promise that
* resolves once the Profiler has been initialized. Note that the
* SPS profiler will be paused immediately after starting and that
* resume() should be called in order to collect samples.
*
* @param testName (string)
* The name of the test to use in Profiler markers.
* @returns Promise
* Resolves once the SPS profiler has been initialized and paused.
*/
beginTest(testName) {
if (initted) {
currentTest = testName;
return sendEventAndWait("Profiler:Begin", {
interval,
entries,
threadsArray,
});
} else {
var msg = "You should not call beginTest without having first " +
"initted the Profiler"
console.error(msg);
return Promise.reject(msg);
}
},
/**
* A Talos test has finished. This will stop the SPS profiler from sampling,
* and return a Promise that resolves once the Profiler has finished dumping
* the multi-process profile to disk.
*
* @returns Promise
* Resolves once the profile has been dumped to disk. The test should
* not try to quit the browser until this has resolved.
*/
finishTest() {
if (initted) {
let profileFile = profileDir + "/" + currentTest + ".sps";
return sendEventAndWait("Profiler:Finish", { profileFile });
} else {
var msg = "You should not call finishTest without having first " +
"initted the Profiler";
console.error(msg);
return Promise.reject(msg);
}
},
/**
* A start-up test has finished. Callers don't need to run beginTest or
* finishTest, but should pause the sampler as soon as possible, and call
* this function to dump the profile.
*
* @returns Promise
* Resolves once the profile has been dumped to disk. The test should
* not try to quit the browser until this has resolved.
*/
finishStartupProfiling() {
if (initted) {
let profileFile = profileDir + "/startup.sps";
return sendEventAndWait("Profiler:Finish", { profileFile });
}
return Promise.resolve();
},
/**
* Resumes the SPS profiler sampler. Can also simultaneously set a marker.
*
* @returns Promise
* Resolves once the SPS profiler has resumed.
*/
resume(marker="") {
if (initted) {
return sendEventAndWait("Profiler:Resume", { marker });
}
return Promise.resolve();
},
/**
* Pauses the SPS profiler sampler. Can also simultaneously set a marker.
*
* @returns Promise
* Resolves once the SPS profiler has paused.
*/
pause(marker="") {
if (initted) {
return sendEventAndWait("Profiler:Pause", { marker });
}
return Promise.resolve();
},
/**
* Adds a marker to the profile.
*
* @returns Promise
* Resolves once the marker has been set.
*/
mark(marker) {
if (initted) {
// If marker is omitted, just use the test name
if (!marker) {
marker = currentTest;
}
return sendEventAndWait("Profiler:Marker", { marker });
}
return Promise.resolve();
},
/**
* Add a marker to the profile on the content process samples.
* This occurs synchronously.
*/
contentMarker(marker) {
Services.profiler.AddMarker(marker);
},
};
})();

View file

@ -0,0 +1,202 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This utility script is for instrumenting your Talos test for
* performance profiles while running within the parent process.
* Almost all of the functions that this script exposes to the
* SPS Profiler are synchronous, except for finishTest, since that
* involves requesting the profiles from any content processes and
* then writing to disk.
*
* If your test is running in the content process, you should use
* TalosContentProfiler.js instead.
*/
var TalosParentProfiler;
(function() {
// Whether or not this TalosContentProfiler object has had initFromObject
// or initFromURLQueryParams called on it. Any functions that change the
// state of the SPS Profiler should only be called after calling either
// initFromObject or initFromURLQueryParams.
let initted = false;
// The subtest name that beginTest() was called with.
let currentTest = "unknown";
// Profiler settings.
let interval, entries, threadsArray, profileDir;
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/Console.jsm");
// Use a bit of XPCOM hackery to get at the Talos Powers service
// implementation...
let TalosPowers =
Components.classes["@mozilla.org/talos/talos-powers-service;1"]
.getService(Components.interfaces.nsISupports)
.wrappedJSObject;
/**
* Parses an url query string into a JS object.
*
* @param locationSearch (string)
* The location string to parse.
* @returns Object
* The GET params from the location string as
* key-value pairs in the Object.
*/
function searchToObject(locationSearch) {
let pairs = locationSearch.substring(1).split("&");
let result = {};
for (let i in pairs) {
if (pairs[i] !== "") {
let pair = pairs[i].split("=");
result[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || "");
}
}
return result;
}
TalosParentProfiler = {
/**
* Initialize the profiler using profiler settings supplied in a JS object.
*
* @param obj (object)
* The following properties on the object are respected:
* sps_profile_interval (int)
* sps_profile_entries (int)
* sps_profile_threads (string, comma separated list of threads to filter with)
* sps_profile_dir (string)
*/
initFromObject(obj={}) {
if (!initted) {
if (("sps_profile_dir" in obj) && typeof obj.sps_profile_dir == "string" &&
("sps_profile_interval" in obj) && Number.isFinite(obj.sps_profile_interval * 1) &&
("sps_profile_entries" in obj) && Number.isFinite(obj.sps_profile_entries * 1) &&
("sps_profile_threads" in obj) && typeof obj.sps_profile_threads == "string") {
interval = obj.sps_profile_interval;
entries = obj.sps_profile_entries;
threadsArray = obj.sps_profile_threads.split(",");
profileDir = obj.sps_profile_dir;
initted = true;
} else {
console.error("Profiler could not init with object: " + JSON.stringify(obj));
}
}
},
/**
* Initialize the profiler using a string from a location string.
*
* @param locationSearch (string)
* The location string to initialize with.
*/
initFromURLQueryParams(locationSearch) {
this.initFromObject(searchToObject(locationSearch));
},
/**
* A Talos test is about to start. Note that the SPS profiler will be
* paused immediately after starting and that resume() should be called
* in order to collect samples.
*
* @param testName (string)
* The name of the test to use in Profiler markers.
*/
beginTest(testName) {
if (initted) {
currentTest = testName;
TalosPowers.profilerBegin({ entries, interval, threadsArray });
} else {
let msg = "You should not call beginTest without having first " +
"initted the Profiler"
console.error(msg);
}
},
/**
* A Talos test has finished. This will stop the SPS profiler from sampling,
* and return a Promise that resolves once the Profiler has finished dumping
* the multi-process profile to disk.
*
* @returns Promise
* Resolves once the profile has been dumped to disk. The test should
* not try to quit the browser until this has resolved.
*/
finishTest() {
if (initted) {
let profileFile = profileDir + "/" + currentTest + ".sps";
return TalosPowers.profilerFinish(profileFile);
} else {
let msg = "You should not call finishTest without having first " +
"initted the Profiler";
console.error(msg);
return Promise.reject(msg);
}
},
/**
* A start-up test has finished. Callers don't need to run beginTest or
* finishTest, but should pause the sampler as soon as possible, and call
* this function to dump the profile.
*
* @returns Promise
* Resolves once the profile has been dumped to disk. The test should
* not try to quit the browser until this has resolved.
*/
finishStartupProfiling() {
if (initted) {
let profileFile = profileDir + "/startup.sps";
return TalosPowers.profilerFinish(profileFile);
}
return Promise.resolve();
},
/**
* Resumes the SPS profiler sampler. Can also simultaneously set a marker.
*
* @returns Promise
* Resolves once the SPS profiler has resumed.
*/
resume(marker="") {
if (initted) {
TalosPowers.profilerResume(marker);
}
},
/**
* Pauses the SPS profiler sampler. Can also simultaneously set a marker.
*
* @returns Promise
* Resolves once the SPS profiler has paused.
*/
pause(marker="") {
if (initted) {
TalosPowers.profilerPause(marker);
}
},
/**
* Adds a marker to the profile.
*
* @returns Promise
* Resolves once the marker has been set.
*/
mark(marker) {
if (initted) {
// If marker is omitted, just use the test name
if (!marker) {
marker = currentTest;
}
TalosPowers.profilerMarker(marker);
}
},
};
})();

View file

@ -0,0 +1,91 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// This file should be executed by the [possibly unprivileged] consumer, e.g.:
// <script src="chrome://talos-powers-content/content/TalosPowersContent.js"></script>
// and then e.g. TalosPowersParent.exec("sampleParentService", myArg, myCallback)
// It marely sends a query event and possibly listens to a reply event, and does not
// depend on any special privileges.
var TalosPowersContent;
var TalosPowersParent;
(function() {
TalosPowersContent = {
/**
* Synchronously force CC and GC in this process, as well as in the
* parent process.
*/
forceCCAndGC() {
var event = new CustomEvent("TalosPowersContentForceCCAndGC", {
bubbles: true,
});
document.dispatchEvent(event);
},
focus(callback) {
if (callback) {
addEventListener("TalosPowersContentFocused", function focused() {
removeEventListener("TalosPowersContentFocused", focused);
callback();
});
};
document.dispatchEvent(new CustomEvent("TalosPowersContentFocus", {
bubbles: true,
}));
},
getStartupInfo() {
return new Promise((resolve) => {
var event = new CustomEvent("TalosPowersContentGetStartupInfo", {
bubbles: true,
});
document.dispatchEvent(event);
addEventListener("TalosPowersContentGetStartupInfoResult",
function onResult(e) {
removeEventListener("TalosPowersContentGetStartupInfoResult",
onResult);
resolve(e.detail);
});
});
},
};
/**
* Generic interface to service functions which run at the parent process.
*/
// If including this script proves too much touble, you may embed the following
// code verbatim instead, and keep the copy up to date with its source here:
TalosPowersParent = {
replyId: 1,
// dispatch an event to the framescript and register the result/callback event
exec: function(commandName, arg, callback, opt_custom_window) {
let win = opt_custom_window || window;
let replyEvent = "TalosPowers:ParentExec:ReplyEvent:" + this.replyId++;
if (callback) {
win.addEventListener(replyEvent, function rvhandler(e) {
win.removeEventListener(replyEvent, rvhandler);
callback(e.detail);
});
}
win.dispatchEvent(
new win.CustomEvent("TalosPowers:ParentExec:QueryEvent", {
bubbles: true,
detail: {
command: {
name: commandName,
data: arg,
},
listeningTo: replyEvent,
}
})
);
},
};
// End of possibly embedded code
})();

View file

@ -0,0 +1,21 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>talos-powers@mozilla.org</em:id>
<em:version>1.0.11</em:version>
<em:targetApplication>
<Description>
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
<em:minVersion>44.0</em:minVersion>
<em:maxVersion>*</em:maxVersion>
</Description>
</em:targetApplication>
<!-- Front End MetaData -->
<em:name>Talos Powers</em:name>
<em:description>Gives Talos content special powers.</em:description>
<em:creator>Mike Conley</em:creator>
<em:multiprocessCompatible>true</em:multiprocessCompatible>
</Description>
</RDF>

View file

@ -0,0 +1,148 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
import time
import psutil
import mozcrash
from mozprocess import ProcessHandler
from threading import Event
from mozlog import get_proxy_logger
from utils import TalosError
LOG = get_proxy_logger()
class ProcessContext(object):
"""
Store useful results of the browser execution.
"""
def __init__(self):
self.output = None
self.process = None
@property
def pid(self):
return self.process and self.process.pid
def kill_process(self):
"""
Kill the process, returning the exit code or None if the process
is already finished.
"""
if self.process and self.process.is_running():
LOG.debug("Terminating %s" % self.process)
self.process.terminate()
try:
return self.process.wait(3)
except psutil.TimeoutExpired:
self.process.kill()
# will raise TimeoutExpired if unable to kill
return self.process.wait(3)
class Reader(object):
def __init__(self, event):
self.output = []
self.got_end_timestamp = False
self.got_timeout = False
self.timeout_message = ''
self.event = event
self.proc = None
def __call__(self, line):
if line.find('__endTimestamp') != -1:
self.got_end_timestamp = True
self.event.set()
elif line == 'TART: TIMEOUT':
self.got_timeout = True
self.timeout_message = 'TART'
self.event.set()
if not (line.startswith('JavaScript error:') or
line.startswith('JavaScript warning:')):
LOG.process_output(self.proc.pid, line)
self.output.append(line)
def run_browser(command, minidump_dir, timeout=None, on_started=None,
**kwargs):
"""
Run the browser using the given `command`.
After the browser prints __endTimestamp, we give it 5
seconds to quit and kill it if it's still alive at that point.
Note that this method ensure that the process is killed at
the end. If this is not possible, an exception will be raised.
:param command: the commad (as a string list) to run the browser
:param minidump_dir: a path where to extract minidumps in case the
browser hang. This have to be the same value
used in `mozcrash.check_for_crashes`.
:param timeout: if specified, timeout to wait for the browser before
we raise a :class:`TalosError`
:param on_started: a callback that can be used to do things just after
the browser has been started. The callback must takes
an argument, which is the psutil.Process instance
:param kwargs: additional keyword arguments for the :class:`ProcessHandler`
instance
Returns a ProcessContext instance, with available output and pid used.
"""
context = ProcessContext()
first_time = int(time.time()) * 1000
wait_for_quit_timeout = 5
event = Event()
reader = Reader(event)
kwargs['storeOutput'] = False
kwargs['processOutputLine'] = reader
kwargs['onFinish'] = event.set
proc = ProcessHandler(command, **kwargs)
reader.proc = proc
proc.run()
LOG.process_start(proc.pid, ' '.join(command))
try:
context.process = psutil.Process(proc.pid)
if on_started:
on_started(context.process)
# wait until we saw __endTimestamp in the proc output,
# or the browser just terminated - or we have a timeout
if not event.wait(timeout):
# try to extract the minidump stack if the browser hangs
mozcrash.kill_and_get_minidump(proc.pid, minidump_dir)
raise TalosError("timeout")
if reader.got_end_timestamp:
for i in range(1, wait_for_quit_timeout):
if proc.wait(1) is not None:
break
if proc.poll() is None:
LOG.info(
"Browser shutdown timed out after {0} seconds, terminating"
" process.".format(wait_for_quit_timeout)
)
elif reader.got_timeout:
raise TalosError('TIMEOUT: %s' % reader.timeout_message)
finally:
# this also handle KeyboardInterrupt
# ensure early the process is really terminated
return_code = context.kill_process()
if return_code is None:
return_code = proc.wait(1)
reader.output.append(
"__startBeforeLaunchTimestamp%d__endBeforeLaunchTimestamp"
% first_time)
reader.output.append(
"__startAfterTerminationTimestamp%d__endAfterTerminationTimestamp"
% (int(time.time()) * 1000))
if return_code is not None:
LOG.process_exit(proc.pid, return_code)
else:
LOG.debug("Unable to detect exit code of the process %s." % proc.pid)
context.output = reader.output
return context

View file

@ -0,0 +1,44 @@
import os
import json
def writeConfigFile(obj, vals):
data = dict((opt, obj[opt]) for opt in (vals or obj.keys()))
return json.dumps(data)
def generateTalosConfig(command_line, browser_config, test_config, pid=None):
bcontroller_vars = ['command', 'child_process', 'process', 'browser_wait',
'test_timeout', 'browser_path', 'error_filename']
if 'xperf_path' in browser_config:
bcontroller_vars.append('xperf_path')
bcontroller_vars.extend(['buildid', 'sourcestamp', 'repository',
'title'])
if 'name' in test_config:
bcontroller_vars.append('testname')
browser_config['testname'] = test_config['name']
browser_config['command'] = ' '.join(command_line)
if (('xperf_providers' in test_config) and
('xperf_user_providers' in test_config) and
('xperf_stackwalk' in test_config)): # noqa
print("extending with xperf!")
browser_config['xperf_providers'] = test_config['xperf_providers']
browser_config['xperf_user_providers'] = \
test_config['xperf_user_providers']
browser_config['xperf_stackwalk'] = test_config['xperf_stackwalk']
browser_config['processID'] = pid
browser_config['approot'] = \
os.path.dirname(browser_config['browser_path'])
bcontroller_vars.extend(['xperf_providers', 'xperf_user_providers',
'xperf_stackwalk', 'processID', 'approot'])
content = writeConfigFile(browser_config, bcontroller_vars)
with open(browser_config['bcontroller_config'], "w") as fhandle:
fhandle.write(content)
return content

712
testing/talos/talos/test.py Normal file
View file

@ -0,0 +1,712 @@
import os
from talos import filter, utils
"""
test definitions for Talos
"""
_TESTS = {} # internal dict of Talos test classes
def register_test():
"""Decorator to register Talos test classes"""
def wrapper(klass):
assert issubclass(klass, Test)
assert klass.name() not in _TESTS
_TESTS[klass.name()] = klass
return klass
return wrapper
def test_dict():
"""Return the dict of the registered test classes"""
return _TESTS
class Test(object):
"""abstract base class for a Talos test case"""
cycles = None # number of cycles
keys = []
desktop = True
filters = filter.ignore_first.prepare(1) + filter.median.prepare()
lower_is_better = True
alert_threshold = 2.0
@classmethod
def name(cls):
return cls.__name__
@classmethod
def description(cls):
if cls.__doc__ is None:
return "No documentation available yet."
else:
doc = cls.__doc__
description_lines = [i.strip() for i in doc.strip().splitlines()]
return "\n".join(description_lines)
def __init__(self, **kw):
self.update(**kw)
def update(self, **kw):
self.__dict__.update(kw)
def items(self):
"""
returns a list of 2-tuples
"""
retval = [('name', self.name())]
for key in self.keys:
value = getattr(self, key, None)
if value is not None:
retval.append((key, value))
return retval
def __str__(self):
"""string form appropriate for YAML output"""
items = self.items()
key, value = items.pop(0)
lines = ["- %s: %s" % (key, value)]
for key, value in items:
lines.append(' %s: %s' % (key, value))
return '\n'.join(lines)
# ts-style startup tests (ts, twinopen, ts_cold, etc)
# The overall test number is calculated by excluding the max opening time
# and taking an average of the remaining numbers.
class TsBase(Test):
"""abstract base class for ts-style tests"""
keys = [
'url',
'url_timestamp',
'timeout',
'cycles',
'shutdown', # If True, collect data on shutdown (using the value
# provided by __startTimestamp/__endTimestamp).
# Otherwise, ignore shutdown data
# (__startTimestamp/__endTimestamp is still
# required but ignored).
'profile_path', # The path containing the template profile. This
# directory is copied to the temporary profile during
# initialization of the test. If some of the files may
# be overwritten by Firefox and need to be reinstalled
# before each pass, use key |reinstall|
'sps_profile',
'sps_profile_interval',
'sps_profile_entries',
'sps_profile_startup',
'preferences',
'xperf_counters',
'xperf_providers',
'xperf_user_providers',
'xperf_stackwalk',
'tpmozafterpaint',
'extensions',
'filters',
'setup',
'cleanup',
'reinstall', # A list of files from the profile directory that
# should be copied to the temporary profile prior to
# running each cycle, to avoid one cycle overwriting
# the data used by the next another cycle (may be used
# e.g. for sessionstore.js to ensure that all cycles
# use the exact same sessionstore.js, rather than a
# more recent copy).
]
@register_test()
class ts_paint(TsBase):
"""
Launches tspaint_test.html with the current timestamp in the url,
waits for [MozAfterPaint and onLoad] to fire, then records the end
time and calculates the time to startup.
"""
cycles = 20
timeout = 150
sps_profile_startup = True
sps_profile_entries = 10000000
url = 'startup_test/tspaint_test.html'
shutdown = False
xperf_counters = []
win7_counters = []
filters = filter.ignore_first.prepare(1) + filter.median.prepare()
tpmozafterpaint = True
rss = False
mainthread = False
responsiveness = False
unit = 'ms'
@register_test()
class sessionrestore(TsBase):
"""
A start up test measuring the time it takes to load a sessionstore.js file.
1. Set up Firefox to restore from a given sessionstore.js file.
2. Launch Firefox.
3. Measure the delta between firstPaint and sessionRestored.
"""
extensions = \
'${talos}/startup_test/sessionrestore/addon/sessionrestore-signed.xpi'
cycles = 10
timeout = 1000000
sps_profile_startup = True
sps_profile_entries = 10000000
profile_path = '${talos}/startup_test/sessionrestore/profile'
url = 'startup_test/sessionrestore/index.html'
shutdown = False
reinstall = ['sessionstore.js', 'sessionCheckpoints.json']
# Restore the session
preferences = {'browser.startup.page': 3}
unit = 'ms'
@register_test()
class sessionrestore_no_auto_restore(sessionrestore):
"""
A start up test measuring the time it takes to load a sessionstore.js file.
1. Set up Firefox to *not* restore automatically from sessionstore.js file.
2. Launch Firefox.
3. Measure the delta between firstPaint and sessionRestored.
"""
# Restore about:home
preferences = {'browser.startup.page': 1}
@register_test()
class tpaint(TsBase):
"""
Tests the amount of time it takes the open a new window. This test does
not include startup time. Multiple test windows are opened in succession,
results reported are the average amount of time required to create and
display a window in the running instance of the browser.
(Measures ctrl-n performance.)
"""
url = 'file://${talos}/startup_test/tpaint.html?auto=1'
timeout = 300
sps_profile_interval = 1
sps_profile_entries = 2000000
tpmozafterpaint = True
filters = filter.ignore_first.prepare(5) + filter.median.prepare()
unit = 'ms'
@register_test()
class tresize(TsBase):
"""
This test does some resize thing.
"""
extensions = '${talos}/startup_test/tresize/addon/tresize-signed.xpi'
cycles = 20
url = 'startup_test/tresize/addon/content/tresize-test.html'
timeout = 150
sps_profile_interval = 2
sps_profile_entries = 1000000
tpmozafterpaint = True
filters = filter.ignore_first.prepare(5) + filter.median.prepare()
unit = 'ms'
# pageloader tests(tp5, etc)
# The overall test number is determined by first calculating the median
# page load time for each page in the set (excluding the max page load
# per individual page). The max median from that set is then excluded and
# the average is taken; that becomes the number reported to the tinderbox
# waterfall.
class PageloaderTest(Test):
"""abstract base class for a Talos Pageloader test"""
tpmanifest = None # test manifest
tpcycles = 1 # number of time to run each page
cycles = None
timeout = None
keys = ['tpmanifest', 'tpcycles', 'tppagecycles', 'tprender', 'tpchrome',
'tpmozafterpaint', 'tploadnocache', 'rss', 'mainthread',
'resolution', 'cycles', 'sps_profile', 'sps_profile_interval',
'sps_profile_entries', 'tptimeout', 'win_counters', 'w7_counters',
'linux_counters', 'mac_counters', 'tpscrolltest', 'xperf_counters',
'timeout', 'shutdown', 'responsiveness', 'profile_path',
'xperf_providers', 'xperf_user_providers', 'xperf_stackwalk',
'filters', 'preferences', 'extensions', 'setup', 'cleanup',
'lower_is_better', 'alert_threshold', 'unit']
@register_test()
class tabpaint(PageloaderTest):
"""
Tests the amount of time it takes to open new tabs, triggered from
both the parent process and the content process.
"""
extensions = '${talos}/tests/tabpaint/tabpaint-signed.xpi'
tpmanifest = '${talos}/tests/tabpaint/tabpaint.manifest'
tppagecycles = 20
sps_profile_entries = 1000000
tploadnocache = True
unit = 'ms'
preferences = {
# By default, Talos is configured to open links from
# content in new windows. We're overriding them so that
# they open in new tabs instead.
# See http://kb.mozillazine.org/Browser.link.open_newwindow
# and http://kb.mozillazine.org/Browser.link.open_newwindow.restriction
'browser.link.open_newwindow': 3,
'browser.link.open_newwindow.restriction': 2,
}
@register_test()
class tps(PageloaderTest):
"""
Tests the amount of time it takes to switch between tabs
"""
extensions = '${talos}/tests/tabswitch/tabswitch-signed.xpi'
tpmanifest = '${talos}/tests/tabswitch/tps.manifest'
tppagecycles = 5
sps_profile_entries = 1000000
tploadnocache = True
preferences = {
'addon.test.tabswitch.urlfile': os.path.join('${talos}',
'tests',
'tp5o.html'),
'addon.test.tabswitch.webserver': '${webserver}',
# limit the page set number for winxp as we have issues.
# see https://bugzilla.mozilla.org/show_bug.cgi?id=1195288
'addon.test.tabswitch.maxurls':
45 if utils.PLATFORM_TYPE == 'win_' else -1,
}
unit = 'ms'
@register_test()
class tart(PageloaderTest):
"""
Tab Animation Regression Test
Tests tab animation on these cases:
1. Simple: single new tab of about:blank open/close without affecting
(shrinking/expanding) other tabs.
2. icon: same as above with favicons and long title instead of about:blank.
3. Newtab: newtab open with thumbnails preview - without affecting other
tabs, with and without preload.
4. Fade: opens a tab, then measures fadeout/fadein (tab animation without
the overhead of opening/closing a tab).
- Case 1 is tested with DPI scaling of 1.
- Case 2 is tested with DPI scaling of 1.0 and 2.0.
- Case 3 is tested with the default scaling of the test system.
- Case 4 is tested with DPI scaling of 2.0 with the "icon" tab
(favicon and long title).
- Each animation produces 3 test results:
- error: difference between the designated duration and the actual
completion duration from the trigger.
- half: average interval over the 2nd half of the animation.
- all: average interval over all recorded intervals.
"""
tpmanifest = '${talos}/tests/tart/tart.manifest'
extensions = '${talos}/tests/tart/addon/tart-signed.xpi'
tpcycles = 1
tppagecycles = 25
tploadnocache = True
tpmozafterpaint = False
sps_profile_interval = 10
sps_profile_entries = 1000000
win_counters = w7_counters = linux_counters = mac_counters = None
"""
ASAP mode
The recording API is broken with OMTC before ~2013-11-27
After ~2013-11-27, disabling OMTC will also implicitly disable
OGL HW composition to disable OMTC with older firefox builds, also
set 'layers.offmainthreadcomposition.enabled': False
"""
preferences = {'layout.frame_rate': 0,
'docshell.event_starvation_delay_hint': 1,
'dom.send_after_paint_to_content': False}
filters = filter.ignore_first.prepare(1) + filter.median.prepare()
unit = 'ms'
@register_test()
class cart(PageloaderTest):
"""
Customize Animation Regression Test
Tests Australis customize animations (default DPI scaling). Uses the
TART addon but with a different URL.
Reports the same animation values as TART (.half/.all/.error).
All comments for TART also apply here (e.g. for ASAP+OMTC, etc)
Subtests are:
1-customize-enter - from triggering customize mode until it's ready for
the user
2-customize-exit - exiting customize
3-customize-enter-css - only the CSS animation part of entering customize
"""
tpmanifest = '${talos}/tests/tart/cart.manifest'
extensions = '${talos}/tests/tart/addon/tart-signed.xpi'
tpcycles = 1
tppagecycles = 25
tploadnocache = True
tpmozafterpaint = False
sps_profile_interval = 1
sps_profile_entries = 10000000
win_counters = w7_counters = linux_counters = mac_counters = None
"""
ASAP mode
"""
preferences = {'layout.frame_rate': 0,
'docshell.event_starvation_delay_hint': 1,
'dom.send_after_paint_to_content': False}
filters = filter.ignore_first.prepare(1) + filter.median.prepare()
unit = 'ms'
@register_test()
class damp(PageloaderTest):
"""
Devtools At Maximum Performance
Tests the speed of DevTools toolbox open, close, and page reload
for each tool, across a very simple and very complicated page.
"""
tpmanifest = '${talos}/tests/devtools/damp.manifest'
extensions = '${talos}/tests/devtools/addon/devtools-signed.xpi'
tpcycles = 1
tppagecycles = 25
tploadnocache = True
tpmozafterpaint = False
sps_profile_interval = 10
sps_profile_entries = 1000000
win_counters = w7_counters = linux_counters = mac_counters = None
filters = filter.ignore_first.prepare(1) + filter.median.prepare()
preferences = {'devtools.memory.enabled': True,
'addon.test.damp.webserver': '${webserver}'}
unit = 'ms'
@register_test()
class glterrain(PageloaderTest):
"""
Simple rotating WebGL scene with moving light source over a
textured terrain.
Measures average frame intervals.
The same sequence is measured 4 times for combinations of alpha and
antialias as canvas properties.
Each of these 4 runs is reported as a different test name.
"""
tpmanifest = '${talos}/tests/webgl/glterrain.manifest'
tpcycles = 1
tppagecycles = 25
tploadnocache = True
tpmozafterpaint = False
sps_profile_interval = 10
sps_profile_entries = 2000000
win_counters = w7_counters = linux_counters = mac_counters = None
""" ASAP mode """
preferences = {'layout.frame_rate': 0,
'docshell.event_starvation_delay_hint': 1,
'dom.send_after_paint_to_content': False}
filters = filter.ignore_first.prepare(1) + filter.median.prepare()
unit = 'frame interval'
@register_test()
class tp5n(PageloaderTest):
"""
Tests the time it takes Firefox to load the tp5 web page test set.
The tp5 is an updated web page test set to 100 pages from April 8th, 2011.
Effort was made for the pages to no longer be splash screens/login
pages/home pages but to be pages that better reflect the actual content
of the site in question.
"""
resolution = 20
shutdown = True
tpmanifest = '${talos}/tests/tp5n/tp5n.manifest'
tpcycles = 1
tppagecycles = 1
cycles = 1
tpmozafterpaint = True
tptimeout = 5000
rss = True
mainthread = True
w7_counters = []
win_counters = []
linux_counters = []
mac_counters = []
xperf_counters = ['main_startup_fileio', 'main_startup_netio',
'main_normal_fileio', 'main_normal_netio',
'nonmain_startup_fileio', 'nonmain_normal_fileio',
'nonmain_normal_netio', 'mainthread_readcount',
'mainthread_readbytes', 'mainthread_writecount',
'mainthread_writebytes']
xperf_providers = ['PROC_THREAD', 'LOADER', 'HARD_FAULTS', 'FILENAME',
'FILE_IO', 'FILE_IO_INIT']
xperf_user_providers = ['Mozilla Generic Provider',
'Microsoft-Windows-TCPIP']
xperf_stackwalk = ['FileCreate', 'FileRead', 'FileWrite', 'FileFlush',
'FileClose']
filters = filter.ignore_first.prepare(1) + filter.median.prepare()
timeout = 1800
setup = '${talos}/xtalos/start_xperf.py -c ${talos}/bcontroller.json'
cleanup = '${talos}/xtalos/parse_xperf.py -c ${talos}/bcontroller.json'
preferences = {'extensions.enabledScopes': '',
'talos.logfile': 'browser_output.txt'}
unit = 'ms'
@register_test()
class tp5o(PageloaderTest):
"""
Derived from the tp5n pageset, this is the 49 most reliable webpages.
"""
tpcycles = 1
tppagecycles = 25
cycles = 1
tpmozafterpaint = True
tptimeout = 5000
rss = True
mainthread = False
tpmanifest = '${talos}/tests/tp5n/tp5o.manifest'
win_counters = ['Main_RSS', 'Private Bytes', '% Processor Time']
w7_counters = ['Main_RSS', 'Private Bytes', '% Processor Time',
'Modified Page List Bytes']
linux_counters = ['Private Bytes', 'XRes', 'Main_RSS']
mac_counters = ['Main_RSS']
responsiveness = True
sps_profile_interval = 2
sps_profile_entries = 4000000
filters = filter.ignore_first.prepare(5) + filter.median.prepare()
timeout = 1800
unit = 'ms'
@register_test()
class tp5o_scroll(PageloaderTest):
"""
Tests scroll (like tscrollx does, including ASAP) but on the tp5o pageset.
"""
tpmanifest = '${talos}/tests/tp5n/tp5o.manifest'
tpcycles = 1
tppagecycles = 12
sps_profile_interval = 2
sps_profile_entries = 2000000
tpscrolltest = True
"""ASAP mode"""
tpmozafterpaint = False
preferences = {'layout.frame_rate': 0,
'docshell.event_starvation_delay_hint': 1,
'dom.send_after_paint_to_content': False,
'layout.css.scroll-behavior.spring-constant': "'10'",
'toolkit.framesRecording.bufferSize': 10000}
filters = filter.ignore_first.prepare(1) + filter.median.prepare()
unit = '1/FPS'
@register_test()
class v8_7(PageloaderTest):
"""
This is the V8 (version 7) javascript benchmark taken verbatim and
slightly modified to fit into our pageloader extension and talos harness.
The previous version of this test is V8 version 5 which was run on
selective branches and operating systems.
"""
tpmanifest = '${talos}/tests/v8_7/v8.manifest'
sps_profile_interval = 1
sps_profile_entries = 1000000
tpcycles = 1
resolution = 20
tpmozafterpaint = False
preferences = {'dom.send_after_paint_to_content': False}
filters = filter.v8_subtest.prepare()
unit = 'score'
lower_is_better = False
@register_test()
class kraken(PageloaderTest):
"""
This is the Kraken javascript benchmark taken verbatim and slightly
modified to fit into our pageloader extension and talos harness.
"""
tpmanifest = '${talos}/tests/kraken/kraken.manifest'
tpcycles = 1
tppagecycles = 1
sps_profile_interval = 0.1
sps_profile_entries = 1000000
tpmozafterpaint = False
preferences = {'dom.send_after_paint_to_content': False}
filters = filter.mean.prepare()
unit = 'score'
@register_test()
class basic_compositor_video(PageloaderTest):
"""
Video test
"""
tpmanifest = '${talos}/tests/video/video.manifest'
tpcycles = 1
tppagecycles = 12
timeout = 10000
sps_profile_interval = 1
sps_profile_entries = 2000000
preferences = {'full-screen-api.allow-trusted-requests-only': False,
'layers.acceleration.force-enabled': False,
'layers.acceleration.disabled': True,
'layout.frame_rate': 0,
'docshell.event_starvation_delay_hint': 1,
'full-screen-api.warning.timeout': 500,
'media.ruin-av-sync.enabled': True}
filters = filter.ignore_first.prepare(1) + filter.median.prepare()
unit = 'ms/frame'
lower_is_better = True
@register_test()
class tcanvasmark(PageloaderTest):
"""
CanvasMark benchmark v0.6
"""
tpmanifest = '${talos}/tests/canvasmark/canvasmark.manifest'
win_counters = w7_counters = linux_counters = mac_counters = None
tpcycles = 5
tppagecycles = 1
timeout = 900
sps_profile_interval = 10
sps_profile_entries = 2500000
tpmozafterpaint = False
preferences = {'dom.send_after_paint_to_content': False}
filters = filter.ignore_first.prepare(1) + filter.median.prepare()
unit = 'score'
lower_is_better = False
class dromaeo(PageloaderTest):
"""abstract base class for dramaeo tests"""
filters = filter.dromaeo.prepare()
lower_is_better = False
alert_threshold = 5.0
@register_test()
class dromaeo_css(dromaeo):
"""
Dromaeo suite of tests for JavaScript performance testing.
See the Dromaeo wiki (https://wiki.mozilla.org/Dromaeo)
for more information.
Each page in the manifest is part of the dromaemo css benchmark.
"""
sps_profile_interval = 2
sps_profile_entries = 10000000
tpmanifest = '${talos}/tests/dromaeo/css.manifest'
unit = 'score'
@register_test()
class dromaeo_dom(dromaeo):
"""
Dromaeo suite of tests for JavaScript performance testing.
See the Dromaeo wiki (https://wiki.mozilla.org/Dromaeo)
for more information.
Each page in the manifest is part of the dromaemo dom benchmark.
"""
sps_profile_interval = 2
sps_profile_entries = 10000000
tpmanifest = '${talos}/tests/dromaeo/dom.manifest'
tpdisable_e10s = True
unit = 'score'
@register_test()
class tsvgm(PageloaderTest):
"""
An svg-only number that measures SVG rendering performance.
"""
tpmanifest = '${talos}/tests/svgx/svgm.manifest'
tpcycles = 1
tppagecycles = 7
tpmozafterpaint = False
sps_profile_interval = 10
sps_profile_entries = 1000000
"""ASAP mode"""
preferences = {'layout.frame_rate': 0,
'docshell.event_starvation_delay_hint': 1,
'dom.send_after_paint_to_content': False}
filters = filter.ignore_first.prepare(2) + filter.median.prepare()
unit = 'ms'
@register_test()
class tsvgx(PageloaderTest):
"""
An svg-only number that measures SVG rendering performance.
"""
tpmanifest = '${talos}/tests/svgx/svgx.manifest'
tpcycles = 1
tppagecycles = 25
tpmozafterpaint = False
sps_profile_interval = 10
sps_profile_entries = 1000000
"""ASAP mode"""
preferences = {'layout.frame_rate': 0,
'docshell.event_starvation_delay_hint': 1,
'dom.send_after_paint_to_content': False}
filters = filter.ignore_first.prepare(5) + filter.median.prepare()
unit = 'ms'
@register_test()
class tsvgr_opacity(PageloaderTest):
"""
An svg-only number that measures SVG rendering performance.
"""
tpmanifest = '${talos}/tests/svg_opacity/svg_opacity.manifest'
tpcycles = 1
tppagecycles = 25
tpmozafterpaint = True
sps_profile_interval = 1
sps_profile_entries = 10000000
filters = filter.ignore_first.prepare(5) + filter.median.prepare()
unit = 'ms'
@register_test()
class tscrollx(PageloaderTest):
"""
This test does some scrolly thing.
"""
tpmanifest = '${talos}/tests/scroll/scroll.manifest'
tpcycles = 1
tppagecycles = 25
tpmozafterpaint = False
sps_profile_interval = 1
sps_profile_entries = 1000000
""" ASAP mode """
preferences = {'layout.frame_rate': 0,
'docshell.event_starvation_delay_hint': 1,
'dom.send_after_paint_to_content': False,
'layout.css.scroll-behavior.spring-constant': "'10'",
'toolkit.framesRecording.bufferSize': 10000}
filters = filter.ignore_first.prepare(5) + filter.median.prepare()
unit = 'ms'
@register_test()
class a11yr(PageloaderTest):
"""
This test ensures basic a11y tables and permutations do not cause
performance regressions.
"""
tpmanifest = '${talos}/tests/a11y/a11y.manifest'
tpcycles = 1
tppagecycles = 25
tpmozafterpaint = True
preferences = {'dom.send_after_paint_to_content': False}
unit = 'ms'
alert_threshold = 5.0

View file

@ -0,0 +1,85 @@
gAccService = 0;
// Make sure not to touch Components before potentially invoking enablePrivilege,
// because otherwise it won't be there.
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
nsIAccessible = Components.interfaces.nsIAccessible;
nsIDOMNode = Components.interfaces.nsIDOMNode;
function initAccessibility()
{
netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
if (!gAccService) {
var service = Components.classes["@mozilla.org/accessibilityService;1"];
if (service) { // fails if build lacks accessibility module
gAccService =
Components.classes["@mozilla.org/accessibilityService;1"]
.getService(Components.interfaces.nsIAccessibilityService);
}
}
return gAccService;
}
function getAccessible(aAccOrElmOrID, aInterfaces)
{
if (!aAccOrElmOrID) {
return null;
}
var elm = null;
if (aAccOrElmOrID instanceof nsIAccessible) {
elm = aAccOrElmOrID.DOMNode;
} else if (aAccOrElmOrID instanceof nsIDOMNode) {
elm = aAccOrElmOrID;
} else {
elm = document.getElementById(aAccOrElmOrID);
}
var acc = (aAccOrElmOrID instanceof nsIAccessible) ? aAccOrElmOrID : null;
if (!acc) {
try {
acc = gAccService.getAccessibleFor(elm);
} catch (e) {
}
}
if (!aInterfaces) {
return acc;
}
if (aInterfaces instanceof Array) {
for (var index = 0; index < aInterfaces.length; index++) {
try {
acc.QueryInterface(aInterfaces[index]);
} catch (e) {
}
}
return acc;
}
try {
acc.QueryInterface(aInterfaces);
} catch (e) {
}
return acc;
}
// Walk accessible tree of the given identifier to ensure tree creation
function ensureAccessibleTree(aAccOrElmOrID)
{
acc = getAccessible(aAccOrElmOrID);
var child = acc.firstChild;
while (child) {
ensureAccessibleTree(child);
try {
child = child.nextSibling;
} catch (e) {
child = null;
}
}
}

View file

@ -0,0 +1,2 @@
% dhtml.html
% tablemutation.html

View file

@ -0,0 +1,52 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<meta charset="utf-8">
<head>
<title>accessibility perf for dhtml</title>
<script src="a11y.js"></script>
<script>
// based on: http://hacks.mozilla.org/2010/05/better-performance-with-lazy-frame-construction/
var ppDate = null;
function doInsertion()
{
if (!initAccessibility()) {
dump("__FAILinit:AccessibilityUnvailable__FAIL");
return;
}
var container = document.getElementById("container");
var lastchild = document.getElementById("lastchild");
var start = new Date();
var i,div;
for (i = 0; i < 2000; i++) {
div = document.createElement("div");
div.innerHTML = "<input type='text' value='a bc def g hijkl mn op qrs'/>";
container.insertBefore(div, lastchild);
}
for (i = 0; i < 2000; i++) {
div = document.createElement("div");
div.innerHTML = "<ul><li>foo<ul><li>bar</li></ul></li><li>baz</li></ul>";
container.insertBefore(div, lastchild);
}
for (i = 0; i < 2000; i++) {
div = document.createElement("div");
div.innerHTML = "<div role='progressbar'>progressbar</div>" +
"<span id='span" + i + "'>cb</span>";
container.appendChild(div);
}
document.documentElement.offsetLeft; // flush layout
setTimeout(postProcessingRecord, 0, start);
}
function postProcessingRecord(s)
{
//alert(new Date() - s);
tpRecordTime(new Date() - s, s);
}
</script>
</head>
<body onload="doInsertion();">
<div id="container" role="application"><div id="lastchild"></div></div>
</body>
</html>

View file

@ -0,0 +1,134 @@
<head>
<meta charset="utf-8">
<title>Accessibility Table Mutation</title>
<script src="a11y.js"></script>
</head>
<body>
<div id="start">
<table id="datatable">
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
<tr><td>Row #0</td><td>348003</td><td>745393</td><td>658838</td><td>273744</td></tr><tr><td>Row #1</td><td>482498</td><td>724073</td><td>792474</td><td>549091</td></tr><tr><td>Row #2</td><td>639859</td><td>151692</td><td>251949</td><td>323840</td></tr><tr><td>Row #3</td><td>269274</td><td>211292</td><td>892989</td><td>473276</td></tr><tr><td>Row #4</td><td>148736</td><td>438719</td><td>789802</td><td>986023</td></tr><tr><td>Row #5</td><td>999863</td><td>782360</td><td>648663</td><td>959764</td></tr><tr><td>Row #6</td><td>302562</td><td>122962</td><td>623747</td><td>961374</td></tr><tr><td>Row #7</td><td>620408</td><td>740513</td><td>562968</td><td>868412</td></tr><tr><td>Row #8</td><td>485907</td><td>221807</td><td>142157</td><td>868405</td></tr><tr><td>Row #9</td><td>845880</td><td>834632</td><td>417497</td><td>485740</td></tr><tr><td>Row #10</td><td>886324</td><td>569447</td><td>709581</td><td>155598</td></tr><tr><td>Row #11</td><td>680740</td><td>602570</td><td>528874</td><td>729476</td></tr><tr><td>Row #12</td><td>941289</td><td>318677</td><td>715499</td><td>941153</td></tr><tr><td>Row #13</td><td>101037</td><td>364163</td><td>900918</td><td>303600</td></tr><tr><td>Row #14</td><td>387126</td><td>524665</td><td>264974</td><td>907534</td></tr><tr><td>Row #15</td><td>265179</td><td>727943</td><td>775947</td><td>651086</td></tr><tr><td>Row #16</td><td>849750</td><td>818104</td><td>519492</td><td>695631</td></tr><tr><td>Row #17</td><td>652736</td><td>836990</td><td>181371</td><td>539061</td></tr><tr><td>Row #18</td><td>406437</td><td>790952</td><td>594659</td><td>987177</td></tr><tr><td>Row #19</td><td>393523</td><td>123534</td><td>716653</td><td>334813</td></tr><tr><td>Row #20</td><td>342211</td><td>432153</td><td>275966</td><td>343248</td></tr><tr><td>Row #21</td><td>696316</td><td>176885</td><td>546849</td><td>983442</td></tr><tr><td>Row #22</td><td>601550</td><td>711824</td><td>890977</td><td>766730</td></tr><tr><td>Row #23</td><td>439767</td><td>666925</td><td>417816</td><td>289518</td></tr><tr><td>Row #24</td><td>485029</td><td>837309</td><td>885150</td><td>137766</td></tr>
</table>
</div>
</body>
<script>
window.onload = function () {
setTimeout("mutateTable()",100);
};
function mutateTable() {
var htmTable = document.getElementById('datatable');
var TableRows = [ ];
// Locate all TR elements of the table
var htmRow = htmTable.childNodes[0];
while(htmRow) {
if(htmRow.nodeType == 1) {
if(htmRow.tagName == 'TR') {
TableRows.push(htmRow);
}
else {
htmRow = htmRow.childNodes[0];
}
}
htmRow = htmRow.nextSibling;
}
var Range = document.createRange();
Range.setStartBefore(TableRows[0]);
Range.setEndAfter(TableRows[TableRows.length-1]);
// Put all rows into a Fragment
var Fragment = document.createDocumentFragment();
for(var Index in TableRows) {
Fragment.appendChild(TableRows[Index]);
}
// Start with a cached accessible tree
if (!initAccessibility()) {
dump("__FAILinit:AccessibilityUnvailable__FAIL");
return;
}
ensureAccessibleTree("start");
var start = new Date();
// Cause all kinds of crazy events
Range.deleteContents();
Range.insertNode(Fragment);
document.documentElement.offsetLeft; // flush layout
setTimeout (postProcessingRecord, 0, start);
}
function postProcessingRecord(s)
{
//alert(new Date() - s);
tpRecordTime(new Date() - s, s);
}
</script>

View file

@ -0,0 +1,3 @@
% http://localhost/tests/canvasmark/index.html?auto=true

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 996 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

@ -0,0 +1,208 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>CanvasMark 2013 - HTML5 Canvas 2D Rendering and JavaScript Benchmark</title>
<script src="../../scripts/Profiler.js"></script>
<script src="scripts/mathlib-min.js"></script>
<script src="scripts/k3d-min.js"></script>
<script src="scripts/canvasmark_v6.js"></script>
<script async defer src="scripts/jquery-1.4.2.min.js"></script>
<style type="text/css">
@font-face
{
font-family: HelveticaNeue;
src: url('HelveticaNeueLTStd-Lt.otf');
}
@font-face
{
font-family: HelveticaNeueBold;
src: url('HelveticaNeueLTStd-Md.otf');
}
@font-face
{
font-family: OstrichSans;
src: url('ostrich-black-webfont.woff');
}
body
{
background-color: #aaa;
font-family: HelveticaNeue, Helvetica;
color: #333;
}
.title
{
font-family: OstrichSans, Helvetica;
padding-left: 20px;
padding-top: 6px;
color: #000;
font-size: 1.75em;
}
P
{
font-size: 1em;
padding-left: 20px;
padding-top: 0;
padding-bottom: 0;
line-height: 1.5em;
}
P.left
{
padding-left: 2em;
}
a, a:visited, a:active, a:hover
{
color: #225588;
text-decoration: none;
}
a:hover
{
text-decoration: underline;
}
.box-shadow
{
box-shadow: 0px 3px 16px #222;
}
.wrapper
{
padding-top: 12px;
text-align: center;
}
.info
{
display: inline-block;
margin-left: 1.5em;
text-align: left;
vertical-align: top;
}
.infopanel
{
background-color: #F0F0FF;
display: inline-block;
line-height: 2em;
padding: 1.5em;
width: 28em;
}
canvas
{
border: 1px solid #333;
background-color: #000;
}
.results
{
}
.results-wrapper
{
display: none;
border: 1px solid #aaa;
background-color: #ddd;
margin-left: 1em;
}
.prose
{
font-family: HelveticaNeueBold, Helvetica;
font-size: 0.85em;
line-height: 1.4em;
}
.footer
{
margin-top: 16px;
text-align: center;
}
.small
{
background-color: #eee;
display: inline-block;
font-family: HelveticaNeueBold, Helvetica;
font-size: 0.85em;
padding: 6px 12px;
text-align: center;
}
.button-overlay
{
position: absolute;
right: 1em;
bottom: 1em;
}
.toggle-area
{
display: none;
}
a.toggler,
a.toggler:visited,
a.toggler:hover,
a.toggler:active
{
text-decoration: none !important;
}
</style>
<meta name="description" content="CanvasMark - HTML5 Canvas 2D Rendering and JavaScript Benchmark by Kevin Roast" />
</head>
<body>
<div class="button-overlay">
<!-- AddThis Button BEGIN -->
<div class="addthis_toolbox addthis_default_style addthis_32x32_style">
<a class="addthis_button_preferred_1"></a>
<a class="addthis_button_preferred_2"></a>
<a class="addthis_button_compact"></a>
<a class="addthis_counter addthis_bubble_style"></a>
</div>
<!-- AddThis Button END -->
</div>
<div class="wrapper">
<canvas class="box-shadow" id="canvas" width="640" height="640"></canvas>
<div class="info">
<div class="infopanel">
<div class="title">CanvasMark 2013 - HTML5 Canvas 2D Rendering and JavaScript Benchmark</div>
<div>
<p>Tests the HTML5 &lt;canvas&gt; rendering performance for commonly used operations in HTML5 games: bitmaps, canvas drawing, alpha blending, polygon fills, shadows and text functions.</p>
</div>
<div id="results-wrapper" class="results-wrapper">
<div class="results" id="results"></div>
<div class="results"><p><a id="tweetlink" href="#" target="new">Tweet this result.</a></p></div>
</div>
<div>
<p>Run benchmark using HTML5 compatible browser: <a href="http://www.google.co.uk/chrome" target="new">Chrome</a> | <a href="http://www.mozilla.com/firefox" target="new">FireFox</a> | <a href="http://www.apple.com/safari/download" target="new">Safari</a> | <a href="http://www.opera.com/download" target="new">Opera</a> | <a href="http://windows.microsoft.com/en-US/internet-explorer/products/ie/home" target="new">IE9/10</a>.</p>
</div>
<div>
<p>This benchmark suite uses a number of elements from my HTML5 games including <a href="../asteroids">Asteroids</a> and <a href="../arena5">Arena5</a>. See more <a href="../index.html">HTML5 Canvas experiments</a>.</p>
</div>
<div>
<p class="prose">Important notes for Windows + Chrome users! <a class="toggler" title="More..." href="#" onclick="$('#toggle-1').toggle();return false;">[+]</a>
<span id="toggle-1" class="toggle-area"><br>To get the best benchmark score for your machine, it is advisable to Disable VSync. Go to &quot;about:flags&quot; and toggle: <code>Disable GPU VSync &quot;Disables synchronisation with the display's vertical refresh rate when GPU rendering.&quot;</code> This will resolve the issue with the Chrome implementation of &quot;requestAnimationFrame()&quot; that tries to maintain a steady 60 frames-per-second (FPS) but on Windows with accelerated 2D canvas support, it will drop immediately down to 30 FPS when 60 FPS is not achievable with no gradual degredation. On Mac/Linux the drop in FPS is gradual and therefore does not affect the benchmark. So if you see the FPS counter drop directly from around 60 FPS to 30 FPS then you should do this. This will not produce an &quot;unfair&quot; score as scores are based on time not the number of frames generated.</span>
</p>
</div>
<div>
<p class="prose">How to interpret the results <a class="toggler" title="More..." href="#" onclick="$('#toggle-2').toggle();return false;">[+]</a>
<span id="toggle-2" class="toggle-area"><br>CanvasMark gives a score for the browser based on the combined performance in each of the various stress tests. You should ensure the browser window is not minimized and that no other CPU or GPU intensive processes are running during the test. The results can only be compared to other browsers running on the same machine - as each machine with different CPU or graphics will produce difference results.</span>
</p>
</div>
<div>
<p class="prose">How does it work <a class="toggler" title="More..." href="#" onclick="$('#toggle-3').toggle();return false;">[+]</a>
<span id="toggle-3" class="toggle-area"><br>Each test is run and progressively tuned to stress the browser until a steady 30 frames-per-second (FPS) is reached. More objects are added to the scene or the scene is made more complex to render until that point is reached. Then the test is considered completed and the next test is started. The score is based on the length of time the browser was able to maintain the test scene at greater than 30 FPS, multiplied by a weighting for the complexity of each test type.</span>
</p>
</div>
<div>
<p class="prose">Benchmark version 1.1 [25-03-2013]</p>
</div>
</div>
</div>
</div>
<div class="footer">
<div class="small">Follow CanvasMark <a href="http://twitter.com/canvasbench" target="new">on Twitter</a> or just <a href="http://twitter.com/home/?status=CanvasMark%20-%20HTML5%20Canvas%20performance%20and%20benchmarking%20tool:%20http://bit.ly/canvasmark%20%23javascript%20%23html5%20(@canvasbench)" target="new">Tweet this.</a> Last updated: 25th March 2013 by Kevin Roast</div>
</div>
</body>
</html>

View file

@ -0,0 +1,27 @@
Copyright (C) 2013 Kevin Roast kevtoast@yahoo.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
A "link back" to the original holding website "www.kev3d.co.uk" or a reference
to the original author "Kevin Roast" shall be provided on any copies or
substantial portions of the Software.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or other
dealings in this Software without prior written authorization.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

Some files were not shown because too many files have changed in this diff Show more