import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
0
testing/talos/talos/__init__.py
Normal file
20
testing/talos/talos/base_profile/localstore.rdf
Normal 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>
|
||||
BIN
testing/talos/talos/base_profile/permissions.sqlite
Normal file
11
testing/talos/talos/base_profile/prefs.js
Normal 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);
|
||||
93
testing/talos/talos/cmanager.py
Normal 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
|
||||
181
testing/talos/talos/cmanager_linux.py
Normal 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")
|
||||
86
testing/talos/talos/cmanager_mac.py
Normal 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))
|
||||
235
testing/talos/talos/cmanager_win32.py
Normal 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
|
||||
153
testing/talos/talos/cmdline.py
Normal 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)
|
||||
472
testing/talos/talos/config.py
Normal 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])
|
||||
87
testing/talos/talos/directoryLinks.json
Normal file
174
testing/talos/talos/ffsetup.py
Normal 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()
|
||||
193
testing/talos/talos/filter.py
Normal 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])
|
||||
74
testing/talos/talos/generate-tart-xpi.html
Normal 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>
|
||||
71
testing/talos/talos/generate-tresize-xpi.html
Normal 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>
|
||||
22
testing/talos/talos/getInfo.html
Normal 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>
|
||||
158
testing/talos/talos/mainthreadio.py
Normal 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))
|
||||
151
testing/talos/talos/mtio-whitelist.json
Normal 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": {}
|
||||
}
|
||||
257
testing/talos/talos/output.py
Normal 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])
|
||||
65
testing/talos/talos/pageloader/README
Normal 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
|
||||
4
testing/talos/talos/pageloader/chrome.manifest
Normal 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
|
||||
92
testing/talos/talos/pageloader/chrome/MozillaFileLogger.js
Normal 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
|
||||
|
||||
172
testing/talos/talos/pageloader/chrome/Profiler.js
Normal 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) + '"');
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
81
testing/talos/talos/pageloader/chrome/memory.js
Normal 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");
|
||||
}
|
||||
}
|
||||
910
testing/talos/talos/pageloader/chrome/pageloader.js
Normal 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");
|
||||
}
|
||||
|
||||
58
testing/talos/talos/pageloader/chrome/pageloader.xul
Normal 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>
|
||||
110
testing/talos/talos/pageloader/chrome/quit.js
Normal 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;
|
||||
}
|
||||
|
||||
159
testing/talos/talos/pageloader/chrome/report.js
Normal 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;
|
||||
}
|
||||
26
testing/talos/talos/pageloader/chrome/talos-content.js
Normal 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();
|
||||
272
testing/talos/talos/pageloader/chrome/tscroll.js
Normal 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) {}
|
||||
206
testing/talos/talos/pageloader/components/tp-cmdline.js
Normal 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;
|
||||
}
|
||||
21
testing/talos/talos/pageloader/install.rdf
Normal 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>
|
||||
BIN
testing/talos/talos/pageloader/pageloader-signed.xpi
Normal file
3
testing/talos/talos/profiler/README.txt
Normal 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/.
|
||||
0
testing/talos/talos/profiler/__init__.py
Normal file
BIN
testing/talos/talos/profiler/dump_syms_mac
Normal file
10
testing/talos/talos/profiler/sps.py
Normal 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)
|
||||
313
testing/talos/talos/profiler/symFileManager.py
Normal 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))
|
||||
29
testing/talos/talos/profiler/symLogging.py
Normal 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
|
||||
393
testing/talos/talos/profiler/symbolication.py
Normal 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"])
|
||||
314
testing/talos/talos/profiler/symbolicationRequest.py
Normal 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
|
||||
522
testing/talos/talos/results.py
Normal 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)
|
||||
248
testing/talos/talos/run_tests.py
Normal 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()
|
||||
130
testing/talos/talos/scripts/MozillaFileLogger.js
Normal 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
|
||||
|
||||
135
testing/talos/talos/scripts/Profiler.js
Normal 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) + '"');
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
14
testing/talos/talos/scripts/jszip.min.js
vendored
Normal file
137
testing/talos/talos/scripts/report.py
Normal 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()
|
||||
173
testing/talos/talos/scripts/talos-debug.js
Normal 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;
|
||||
}
|
||||
79
testing/talos/talos/scripts/xpigen.js
Normal 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);
|
||||
}
|
||||
}
|
||||
209
testing/talos/talos/sps_profile.py
Normal 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)
|
||||
0
testing/talos/talos/startup_test/__init__.py
Normal 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]);
|
||||
|
|
@ -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/
|
||||
|
|
@ -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>
|
||||
21
testing/talos/talos/startup_test/sessionrestore/index.html
Normal 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>
|
||||
42
testing/talos/talos/startup_test/sessionrestore/main.js
Normal 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, {});
|
||||
|
|
@ -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}
|
||||
124
testing/talos/talos/startup_test/tpaint.html
Normal 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>
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
content tresize content/
|
||||
overlay chrome://browser/content/browser.xul chrome://tresize/content/tresize.overlay.xul
|
||||
|
|
@ -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) + '"');
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
@ -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);
|
||||
})()
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
21
testing/talos/talos/startup_test/tresize/addon/install.rdf
Normal 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>
|
||||
69
testing/talos/talos/startup_test/tspaint_test.html
Normal 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>
|
||||
5
testing/talos/talos/talos-powers/README
Normal 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.
|
||||
|
||||
5
testing/talos/talos/talos-powers/chrome.manifest
Normal 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
|
||||
123
testing/talos/talos/talos-powers/chrome/talos-powers-content.js
Normal 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
|
||||
|
|
@ -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]);
|
||||
256
testing/talos/talos/talos-powers/content/TalosContentProfiler.js
Normal 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);
|
||||
},
|
||||
};
|
||||
})();
|
||||
202
testing/talos/talos/talos-powers/content/TalosParentProfiler.js
Normal 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);
|
||||
}
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
|
@ -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
|
||||
|
||||
})();
|
||||
21
testing/talos/talos/talos-powers/install.rdf
Normal 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>
|
||||
BIN
testing/talos/talos/talos-powers/talos-powers-signed.xpi
Normal file
148
testing/talos/talos/talos_process.py
Normal 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
|
||||
44
testing/talos/talos/talosconfig.py
Normal 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
|
|
@ -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
|
||||
85
testing/talos/talos/tests/a11y/a11y.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
testing/talos/talos/tests/a11y/a11y.manifest
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
% dhtml.html
|
||||
% tablemutation.html
|
||||
52
testing/talos/talos/tests/a11y/dhtml.html
Normal 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>
|
||||
134
testing/talos/talos/tests/a11y/tablemutation.html
Normal 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>
|
||||
BIN
testing/talos/talos/tests/canvasmark/HelveticaNeueLTStd-Lt.otf
Normal file
BIN
testing/talos/talos/tests/canvasmark/HelveticaNeueLTStd-Md.otf
Normal file
3
testing/talos/talos/tests/canvasmark/canvasmark.manifest
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
% http://localhost/tests/canvasmark/index.html?auto=true
|
||||
|
||||
|
||||
BIN
testing/talos/talos/tests/canvasmark/images/asteroid1.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
testing/talos/talos/tests/canvasmark/images/asteroid2.png
Normal file
|
After Width: | Height: | Size: 996 KiB |
BIN
testing/talos/talos/tests/canvasmark/images/asteroid3.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
testing/talos/talos/tests/canvasmark/images/asteroid4.png
Normal file
|
After Width: | Height: | Size: 933 KiB |
BIN
testing/talos/talos/tests/canvasmark/images/bg3_1.jpg
Normal file
|
After Width: | Height: | Size: 221 KiB |
BIN
testing/talos/talos/tests/canvasmark/images/canvasmark2013.jpg
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
testing/talos/talos/tests/canvasmark/images/enemyship1.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
testing/talos/talos/tests/canvasmark/images/fruit.jpg
Normal file
|
After Width: | Height: | Size: 522 KiB |
BIN
testing/talos/talos/tests/canvasmark/images/player.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
testing/talos/talos/tests/canvasmark/images/texture5.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
208
testing/talos/talos/tests/canvasmark/index.html
Normal 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 <canvas> 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 "about:flags" and toggle: <code>Disable GPU VSync "Disables synchronisation with the display's vertical refresh rate when GPU rendering."</code> This will resolve the issue with the Chrome implementation of "requestAnimationFrame()" 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 "unfair" 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>
|
||||
27
testing/talos/talos/tests/canvasmark/license.txt
Normal 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.
|
||||