summaryrefslogtreecommitdiffstats
path: root/tools/playback_benchmark
diff options
context:
space:
mode:
authorvitalyr@chromium.org <vitalyr@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2010-06-07 09:00:47 +0000
committervitalyr@chromium.org <vitalyr@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2010-06-07 09:00:47 +0000
commit7a158cc250768c944236b31042aa06d55757f72f (patch)
treec5396866aac95255801bfa07c404ce78f5375048 /tools/playback_benchmark
parent1d21e4402d5f87766399afb56b20964af3f8a64e (diff)
downloadchromium_src-7a158cc250768c944236b31042aa06d55757f72f.zip
chromium_src-7a158cc250768c944236b31042aa06d55757f72f.tar.gz
chromium_src-7a158cc250768c944236b31042aa06d55757f72f.tar.bz2
Landing for Pavel Podivilov (podivilov@chromium.org).
Playback benchmark scripts. Original review: http://codereview.chromium.org/1515006/show BUG=none TEST=none TBR=podivilov Review URL: http://codereview.chromium.org/2626002 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@49041 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'tools/playback_benchmark')
-rw-r--r--tools/playback_benchmark/common.js318
-rw-r--r--tools/playback_benchmark/playback.js256
-rw-r--r--tools/playback_benchmark/playback_driver.py196
-rw-r--r--tools/playback_benchmark/proxy_handler.py123
-rw-r--r--tools/playback_benchmark/run.py39
5 files changed, 932 insertions, 0 deletions
diff --git a/tools/playback_benchmark/common.js b/tools/playback_benchmark/common.js
new file mode 100644
index 0000000..801d602
--- /dev/null
+++ b/tools/playback_benchmark/common.js
@@ -0,0 +1,318 @@
+// Copyright 2010 Google Inc. All Rights Reserved.
+
+/**
+ * @fileoverview Classes and functions used during recording and playback.
+ */
+
+var Benchmark = Benchmark || {};
+
+Benchmark.functionList = [
+ ['setTimeout', 'setTimeout'],
+ ['clearTimeout', 'clearTimeout'],
+ ['setInterval', 'setInterval'],
+ ['clearInterval', 'clearInterval'],
+ ['XMLHttpRequest', 'XMLHttpRequest'],
+ ['addEventListenerToWindow', 'addEventListener'],
+ ['addEventListenerToNode', 'addEventListener', ['Node', 'prototype']],
+ ['removeEventListenerFromNode', 'removeEventListener', ['Node', 'prototype']],
+ ['addEventListenerToXHR', 'addEventListener',
+ ['XMLHttpRequest', 'prototype']],
+ ['random', 'random', ['Math']],
+ ['Date', 'Date'],
+ ['documentWriteln', 'writeln', ['document']],
+ ['documentWrite', 'write', ['document']]
+];
+
+Benchmark.timeoutMapping = [];
+
+Benchmark.ignoredListeners = ['mousemove', 'mouseover', 'mouseout'];
+
+Benchmark.originals = {};
+
+Benchmark.overrides = {
+ setTimeout: function(callback, timeout) {
+ var event = {type: 'timeout', timeout: timeout};
+ var eventId = Benchmark.agent.createAsyncEvent(event);
+ var timerId = Benchmark.originals.setTimeout.call(this, function() {
+ Benchmark.agent.fireAsyncEvent(eventId, callback);
+ }, Benchmark.playback ? 0 : timeout);
+ Benchmark.timeoutMapping[timerId] = eventId;
+ return timerId;
+ },
+
+ clearTimeout: function(timerId) {
+ var eventId = Benchmark.timeoutMapping[timerId];
+ if (eventId == undefined) return;
+ Benchmark.agent.cancelAsyncEvent(eventId);
+ Benchmark.originals.clearTimeout.call(this, timerId);
+ },
+
+ setInterval: function(callback, timeout) {
+ console.warn('setInterval');
+ },
+
+ clearInterval: function(timerId) {
+ console.warn('clearInterval');
+ },
+
+ XMLHttpRequest: function() {
+ return new Benchmark.XMLHttpRequestWrapper();
+ },
+
+ addEventListener: function(type, listener, useCapture, target, targetType,
+ originalFunction) {
+ var event = {type: 'addEventListener', target: targetType, eventType: type};
+ var eventId = Benchmark.agent.createAsyncEvent(event);
+ listener.eventId = eventId;
+ listener.wrapper = function(e) {
+ Benchmark.agent.fireAsyncEvent(eventId, function() {
+ listener.call(target, e);
+ });
+ };
+ originalFunction.call(target, type, listener.wrapper, useCapture);
+ },
+
+ addEventListenerToWindow: function(type, listener, useCapture) {
+ if (Benchmark.ignoredListeners.indexOf(type) != -1) return;
+ Benchmark.overrides.addEventListener(
+ type, listener, useCapture, this, 'window',
+ Benchmark.originals.addEventListenerToWindow);
+ },
+
+ addEventListenerToNode: function(type, listener, useCapture) {
+ if (Benchmark.ignoredListeners.indexOf(type) != -1) return;
+ Benchmark.overrides.addEventListener(
+ type, listener, useCapture, this, 'node',
+ Benchmark.originals.addEventListenerToNode);
+ },
+
+ addEventListenerToXHR: function(type, listener, useCapture) {
+ Benchmark.overrides.addEventListener(
+ type, listener, useCapture, this, 'xhr',
+ Benchmark.originals.addEventListenerToXHR);
+ },
+
+ removeEventListener: function(type, listener, useCapture, target,
+ originalFunction) {
+ Benchmark.agent.cancelAsyncEvent(listener.eventId);
+ originalFunction.call(target, listener.wrapper, useCapture);
+ },
+
+ removeEventListenerFromWindow: function(type, listener, useCapture) {
+ removeEventListener(type, listener, useCapture, this,
+ Benchmark.originals.removeEventListenerFromWindow);
+ },
+
+ removeEventListenerFromNode: function(type, listener, useCapture) {
+ removeEventListener(type, listener, useCapture, this,
+ Benchmark.originals.removeEventListenerFromNode);
+ },
+
+ removeEventListenerFromXHR: function(type, listener, useCapture) {
+ removeEventListener(type, listener, useCapture, this,
+ Benchmark.originals.removeEventListenerFromXHR);
+ },
+
+ random: function() {
+ return Benchmark.agent.random();
+ },
+
+ Date: function() {
+ var a = arguments;
+ var D = Benchmark.originals.Date, d;
+ switch(a.length) {
+ case 0: d = new D(Benchmark.agent.dateNow()); break;
+ case 1: d = new D(a[0]); break;
+ case 2: d = new D(a[0], a[1]); break;
+ case 3: d = new D(a[0], a[1], a[2]); break;
+ default: Benchmark.die('window.Date', arguments);
+ }
+ d.getTimezoneOffset = function() { return -240; };
+ return d;
+ },
+
+ dateNow: function() {
+ return Benchmark.agent.dateNow();
+ },
+
+ documentWriteln: function() {
+ console.warn('writeln');
+ },
+
+ documentWrite: function() {
+ console.warn('write');
+ }
+};
+
+/**
+ * Replaces window functions specified by Benchmark.functionList with overrides
+ * and optionally saves original functions to Benchmark.originals.
+ * @param {Object} wnd Window object.
+ * @param {boolean} storeOriginals When true, original functions are saved to
+ * Benchmark.originals.
+ */
+Benchmark.installOverrides = function(wnd, storeOriginals) {
+ // Substitute window functions with overrides.
+ for (var i = 0; i < Benchmark.functionList.length; ++i) {
+ var info = Benchmark.functionList[i], object = wnd;
+ var propertyName = info[1], pathToProperty = info[2];
+ if (pathToProperty)
+ for (var j = 0; j < pathToProperty.length; ++j)
+ object = object[pathToProperty[j]];
+ if (storeOriginals)
+ Benchmark.originals[info[0]] = object[propertyName];
+ object[propertyName] = Benchmark.overrides[info[0]];
+ }
+ wnd.__defineSetter__('onload', function() {
+ console.warn('window.onload setter')}
+ );
+
+ // Substitute window functions of static frames when DOM content is loaded.
+ Benchmark.originals.addEventListenerToWindow.call(wnd, 'DOMContentLoaded',
+ function() {
+ var frames = document.getElementsByTagName('iframe');
+ for (var i = 0, frame; frame = frames[i]; ++i) {
+ Benchmark.installOverrides(frame.contentWindow);
+ }
+ }, true);
+
+ // Substitute window functions of dynamically added frames.
+ Benchmark.originals.addEventListenerToWindow.call(
+ wnd, 'DOMNodeInsertedIntoDocument', function(e) {
+ if (e.target.tagName && e.target.tagName.toLowerCase() != 'iframe')
+ return;
+ if (e.target.contentWindow)
+ Benchmark.installOverrides(e.target.contentWindow);
+ }, true);
+};
+
+// Install overrides on top window.
+Benchmark.installOverrides(window, true);
+
+/**
+ * window.XMLHttpRequest wrapper. Notifies Benchmark.agent when request is
+ * opened, aborted, and when it's ready state changes to DONE.
+ * @constructor
+ */
+Benchmark.XMLHttpRequestWrapper = function() {
+ this.request = new Benchmark.originals.XMLHttpRequest();
+ this.wrapperReadyState = 0;
+};
+
+// Create XMLHttpRequestWrapper functions and property accessors using original
+// ones.
+(function() {
+ var request = new Benchmark.originals.XMLHttpRequest();
+ for (var property in request) {
+ if (property === 'channel') continue; // Quick fix for FF.
+ if (typeof(request[property]) == 'function') {
+ (function(property) {
+ var f = Benchmark.originals.XMLHttpRequest.prototype[property];
+ Benchmark.XMLHttpRequestWrapper.prototype[property] = function() {
+ f.apply(this.request, arguments);
+ };
+ })(property);
+ } else {
+ (function(property) {
+ Benchmark.XMLHttpRequestWrapper.prototype.__defineGetter__(property,
+ function() { return this.request[property]; });
+ Benchmark.XMLHttpRequestWrapper.prototype.__defineSetter__(property,
+ function(value) {
+ this.request[property] = value;
+ });
+
+ })(property);
+ }
+ }
+})();
+
+// Define onreadystatechange getter.
+Benchmark.XMLHttpRequestWrapper.prototype.__defineGetter__('onreadystatechange',
+ function() { return this.clientOnReadyStateChange; });
+
+// Define onreadystatechange setter.
+Benchmark.XMLHttpRequestWrapper.prototype.__defineSetter__('onreadystatechange',
+ function(value) { this.clientOnReadyStateChange = value; });
+
+Benchmark.XMLHttpRequestWrapper.prototype.__defineGetter__('readyState',
+ function() { return this.wrapperReadyState; });
+
+Benchmark.XMLHttpRequestWrapper.prototype.__defineSetter__('readyState',
+ function() {});
+
+
+/**
+ * Wrapper for XMLHttpRequest.open.
+ */
+Benchmark.XMLHttpRequestWrapper.prototype.open = function() {
+ var url = Benchmark.extractURL(arguments[1]);
+ var event = {type: 'request', method: arguments[0], url: url};
+ this.eventId = Benchmark.agent.createAsyncEvent(event);
+
+ var request = this.request;
+ var requestWrapper = this;
+ Benchmark.originals.XMLHttpRequest.prototype.open.apply(request, arguments);
+ request.onreadystatechange = function() {
+ if (this.readyState != 4 || requestWrapper.cancelled) return;
+ var callback = requestWrapper.clientOnReadyStateChange || function() {};
+ Benchmark.agent.fireAsyncEvent(requestWrapper.eventId, function() {
+ requestWrapper.wrapperReadyState = 4;
+ callback.call(request);
+ });
+ }
+};
+
+/**
+ * Wrapper for XMLHttpRequest.abort.
+ */
+Benchmark.XMLHttpRequestWrapper.prototype.abort = function() {
+ this.cancelled = true;
+ Benchmark.originals.XMLHttpRequest.prototype.abort.apply(
+ this.request, arguments);
+ Benchmark.agent.cancelAsyncEvent(this.eventId);
+};
+
+/**
+ * Driver url for reporting results.
+ * @const {string}
+ */
+Benchmark.DRIVER_URL = '/benchmark/';
+
+/**
+ * Posts request as json to Benchmark.DRIVER_URL.
+ * @param {Object} request Request to post.
+ */
+Benchmark.post = function(request, async) {
+ if (async === undefined) async = true;
+ var xmlHttpRequest = new Benchmark.originals.XMLHttpRequest();
+ xmlHttpRequest.open("POST", Benchmark.DRIVER_URL, async);
+ xmlHttpRequest.setRequestHeader("Content-type", "application/json");
+ xmlHttpRequest.send(JSON.stringify(request));
+};
+
+/**
+ * Extracts url string.
+ * @param {(string|Object)} url Object or string representing url.
+ * @return {string} Extracted url.
+ */
+Benchmark.extractURL = function(url) {
+ if (typeof(url) == 'string') return url;
+ return url.nI || url.G || '';
+};
+
+
+/**
+ * Logs error message to console and throws an exception.
+ * @param {string} message Error message
+ */
+Benchmark.die = function(message) {
+ // Debugging stuff.
+ var position = top.Benchmark.playback ? top.Benchmark.agent.timelinePosition :
+ top.Benchmark.agent.timeline.length;
+ message = message + ' at position ' + position;
+ console.error(message);
+ Benchmark.post({error: message});
+ console.log(Benchmark.originals.setTimeout.call(window, function() {}, 9999));
+ try { (0)() } catch(ex) { console.error(ex.stack); }
+ throw message;
+}; \ No newline at end of file
diff --git a/tools/playback_benchmark/playback.js b/tools/playback_benchmark/playback.js
new file mode 100644
index 0000000..0c8c4bf
--- /dev/null
+++ b/tools/playback_benchmark/playback.js
@@ -0,0 +1,256 @@
+// Copyright 2010 Google Inc. All Rights Reserved.
+
+/**
+ * @fileoverview Playback agent.
+ */
+
+var Benchmark = Benchmark || {};
+
+/**
+ * Playback agent class.
+ * @param {Object} data Test data.
+ * @constructor
+ */
+Benchmark.Agent = function(data) {
+ this.timeline = data.timeline;
+ this.timelinePosition = 0;
+ this.steps = data.steps;
+ this.stepsPosition = 0;
+ this.randoms = data.randoms;
+ this.randomsPosition = 0;
+ this.ticks = data.ticks;
+ this.ticksPosition = 0;
+ this.delayedScriptElements = {};
+ this.callStackDepth = 0;
+ document.cookie = data.cookie;
+ if (window.innerWidth != data.width || window.innerHeight != data.height) {
+ Benchmark.die('Wrong window size: ' +
+ window.innerWidth + 'x' + window.innerHeight +
+ ' instead of ' + data.width + 'x' + data.height);
+ }
+ this.startTime = Benchmark.originals.Date.now();
+};
+
+/**
+ * Returns current timeline event.
+ * @return {Object} Event.
+ */
+Benchmark.Agent.prototype.getCurrentEvent = function() {
+ return this.timeline[this.timelinePosition];
+};
+
+/**
+ * Returns next recorded event in timeline. If event is the last event in
+ * timeline, posts test results to driver.
+ * @param {Object} event Event that actually happened, should correspond to
+ * the recorded one (used for debug only).
+ * @return {Object} Recorded event from timeline.
+ */
+Benchmark.Agent.prototype.getNextEvent = function(event) {
+ var recordedEvent = this.getCurrentEvent();
+ this.ensureEqual(event, recordedEvent);
+ if (event.type == 'random' || event.type == 'ticks') {
+ recordedEvent.count -= 1;
+ if (recordedEvent.count == 0) {
+ this.timelinePosition += 1;
+ }
+ } else {
+ this.timelinePosition += 1;
+ }
+ if (this.timelinePosition == this.steps[this.stepsPosition][1]) {
+ var score = Benchmark.originals.Date.now() - this.startTime;
+ Benchmark.reportScore(score);
+ }
+ return recordedEvent;
+};
+
+/**
+ * Checks if two events can be considered equal. Throws exception if events
+ * differ.
+ * @param {Object} event Event that actually happened.
+ * @param {Object} recordedEvent Event taken from timeline.
+ */
+Benchmark.Agent.prototype.ensureEqual = function(event, recordedEvent) {
+ var equal = false;
+ if (event.type == recordedEvent.type &&
+ event.type in Benchmark.eventPropertiesMap) {
+ equal = true;
+ var properties = Benchmark.eventPropertiesMap[event.type];
+ for (var i = 0; i < properties.length && equal; ++i)
+ if (event[properties[i]] != recordedEvent[properties[i]])
+ equal = false;
+ }
+ if (!equal) {
+ Benchmark.die('unexpected event: ' + JSON.stringify(event) +
+ ' instead of ' + JSON.stringify(recordedEvent));
+ }
+};
+
+/**
+ * Gets next event from timeline and returns it's identifier.
+ * @param {Object} event Object with event information.
+ * @return {number} Event identifier.
+ */
+Benchmark.Agent.prototype.createAsyncEvent = function(event) {
+ return this.getNextEvent(event).id;
+};
+
+/**
+ * Stores callback to be invoked according to timeline order.
+ * @param {number} eventId 'Parent' event identifier.
+ * @param {function} callback Callback.
+ */
+Benchmark.Agent.prototype.fireAsyncEvent = function(eventId, callback) {
+ var event = this.timeline[eventId];
+ if (!event.callbackReference) return;
+ this.timeline[event.callbackReference].callback = callback;
+ this.fireSome();
+};
+
+/**
+ * Ensures that things are happening according to recorded timeline.
+ * @param {number} eventId Identifier of cancelled event.
+ */
+Benchmark.Agent.prototype.cancelAsyncEvent = function(eventId) {
+ this.getNextEvent({type: 'cancel', reference: eventId});
+};
+
+/**
+ * Checks if script isn't going to be executed too early and delays script
+ * execution if necessary.
+ * @param {number} scriptId Unique script identifier.
+ * @param {HTMLElement} doc Document element.
+ * @param {boolean} inlined Indicates whether script is a text block in the page
+ * or resides in a separate file.
+ * @param {string} src Script url (if script is not inlined).
+ */
+Benchmark.Agent.prototype.readyToExecuteScript = function(scriptId, doc,
+ inlined, src) {
+ var event = this.getCurrentEvent();
+ if (event.type == 'willExecuteScript' && event.scriptId == scriptId) {
+ this.timelinePosition += 1;
+ return true;
+ }
+ var element;
+ var elements = doc.getElementsByTagName('script');
+ for (var i = 0, el; (el = elements[i]) && !element; ++i) {
+ if (inlined) {
+ if (el.src) continue;
+ var text = el.textContent;
+ if (scriptId == text.substring(2, text.indexOf("*/")))
+ element = elements[i];
+ } else {
+ if (!el.src) continue;
+ if (el.src.indexOf(src) != -1 || src.indexOf(el.src) != -1) {
+ element = el;
+ }
+ }
+ }
+ if (!element) {
+ Benchmark.die('script element not found', scriptId, src);
+ }
+ for (var el2 = element; el2; el2 = el2.parentElement) {
+ if (el2.onload) {
+ console.log('found', el2);
+ }
+ }
+ this.delayedScriptElements[scriptId] = element;
+ return false;
+};
+
+/**
+ * Ensures that things are happening according to recorded timeline.
+ * @param {Object} event Object with event information.
+ */
+Benchmark.Agent.prototype.didExecuteScript = function(scriptId ) {
+ this.getNextEvent({type: 'didExecuteScript', scriptId: scriptId});
+ this.fireSome();
+};
+
+/**
+ * Invokes async events' callbacks according to timeline order.
+ */
+Benchmark.Agent.prototype.fireSome = function() {
+ while (this.timelinePosition < this.timeline.length) {
+ var event = this.getCurrentEvent();
+ if (event.type == 'willFire') {
+ if(!event.callback) break;
+ this.timelinePosition += 1;
+ this.callStackDepth += 1;
+ event.callback();
+ this.callStackDepth -= 1;
+ this.getNextEvent({type: 'didFire', reference: event.reference});
+ } else if (event.type == 'willExecuteScript') {
+ if (event.scriptId in this.delayedScriptElements) {
+ var element = this.delayedScriptElements[event.scriptId];
+ var parent = element.parentElement;
+ var cloneElement = element.cloneNode();
+ delete this.delayedScriptElements[event.scriptId];
+ parent.replaceChild(cloneElement, element);
+ }
+ break;
+ } else if (this.callStackDepth > 0) {
+ break;
+ } else {
+ Benchmark.die('unexpected event in fireSome:' + JSON.stringify(event));
+ }
+ }
+};
+
+/**
+ * Returns recorded random.
+ * @return {number} Recorded random.
+ */
+Benchmark.Agent.prototype.random = function() {
+ this.getNextEvent({type: 'random'});
+ return this.randoms[this.randomsPosition++];
+};
+
+/**
+ * Returns recorded ticks.
+ * @return {number} Recorded ticks.
+ */
+Benchmark.Agent.prototype.dateNow = function(event) {
+ this.getNextEvent({type: 'ticks'});
+ return this.ticks[this.ticksPosition++];
+};
+
+/**
+ * Event type -> property list mapping used for matching events.
+ * @const
+ */
+Benchmark.eventPropertiesMap = {
+ 'timeout': ['timeout'],
+ 'request': ['url'],
+ 'addEventListener': ['eventType'],
+ 'script load': ['src'],
+ 'willExecuteScript': ['scriptId'],
+ 'didExecuteScript': ['scriptId'],
+ 'willFire': ['reference'],
+ 'didFire': ['reference'],
+ 'cancel': ['reference'],
+ 'random': [],
+ 'ticks': []
+};
+
+/**
+ * Agent used by native window functions wrappers.
+ */
+Benchmark.agent = new Benchmark.Agent(Benchmark.data);
+
+/**
+ * Playback flag.
+ * @const
+ */
+Benchmark.playback = true;
+
+Benchmark.reportScore = function(score) {
+ Benchmark.score = score;
+};
+
+Benchmark.originals.addEventListenerToWindow.call(
+ window, 'message', function(event) {
+ if (Benchmark.score) {
+ event.source.postMessage(Benchmark.score, event.origin);
+ }
+ }, false);
diff --git a/tools/playback_benchmark/playback_driver.py b/tools/playback_benchmark/playback_driver.py
new file mode 100644
index 0000000..cf8d2a0
--- /dev/null
+++ b/tools/playback_benchmark/playback_driver.py
@@ -0,0 +1,196 @@
+#!/usr/bin/env python
+#
+# Copyright 2010 Google Inc. All Rights Reserved.
+
+"""Playback driver."""
+
+import cgi
+import simplejson as json
+import os
+import string
+import sys
+import threading
+import urlparse
+
+START_PAGE = """<html>
+ <script type="text/javascript">
+var runCount = $run_count;
+var results = [];
+
+function run() {
+ var wnd = window.open('?resource=start_page_popup', '',
+ 'width=$width, height=$height');
+ var timerId = setInterval(function() {
+ wnd.postMessage('ping', '$target_origin');
+ }, 300);
+ var handleMessage = function(event) {
+ clearInterval(timerId);
+ wnd.close();
+ document.writeln('<div>' + event.data + '</div>');
+ results.push(event.data);
+ runCount -= 1;
+ window.removeEventListener('message', handleMessage);
+ if (runCount > 0) {
+ run();
+ } else {
+ var xmlHttpRequest = new XMLHttpRequest();
+ xmlHttpRequest.open("POST", '/benchmark/', true);
+ xmlHttpRequest.setRequestHeader("Content-type", "application/json");
+ xmlHttpRequest.send(JSON.stringify({results: results}));
+ }
+ }
+ window.addEventListener('message', handleMessage, false);
+}
+
+run();
+ </script>
+</html>
+"""
+
+START_PAGE_POPUP = """<html>
+ <script type="text/javascript">
+window.setTimeout(function() {
+ console.log(window.innerWidth, window.innerHeight);
+ if (window.innerWidth == $width && window.innerHeight == $height) {
+ window.location = '$start_url';
+ } else {
+ window.resizeBy($width - window.innerWidth, $height - window.innerHeight);
+ window.location = window.location;
+ }
+}, 200);
+ </script>
+</html>
+"""
+
+DATA_JS = 'Benchmark.data = $data;'
+
+
+def ReadFile(file_name, mode='r'):
+ f = open(file_name, mode)
+ data = f.read()
+ f.close()
+ return data
+
+
+def ReadJSON(file_name):
+ f = open(file_name, 'r')
+ data = json.load(f)
+ f.close()
+ return data
+
+
+class PlaybackRequestHandler(object):
+ """This class is used to process HTTP requests during test playback.
+
+ Attributes:
+ test_dir: directory containing test files.
+ test_callback: function to be called when the test is finished.
+ script_dir: directory where javascript files are located.
+ """
+
+ def __init__(self, test_dir, test_callback=None, script_dir=os.getcwd()):
+ self.test_dir = test_dir
+ self.test_callback = test_callback
+ self.script_dir = script_dir
+
+ def ProcessRequest(self, handler):
+ "Processes single HTTP request."
+
+ parse_result = urlparse.urlparse(handler.path)
+ if parse_result.path.endswith('/benchmark/'):
+ query = cgi.parse_qs(parse_result.query)
+ if 'run_test' in query:
+ run_count = 1
+ if 'run_count' in query:
+ run_count = query['run_count'][0]
+ self._StartTest(handler, self.test_dir, run_count)
+ elif 'resource' in query:
+ self._GetBenchmarkResource(query['resource'][0], handler)
+ else:
+ self._ProcessBenchmarkReport(handler.body, handler)
+ else:
+ self._GetApplicationResource(handler)
+
+ def _StartTest(self, handler, test_dir, run_count):
+ "Sends test start page to browser."
+
+ cache_data = ReadJSON(os.path.join(test_dir, 'cache.json'))
+
+ # Load cached responses.
+ self.cache = {}
+ responses_dir = os.path.join(test_dir, 'responses')
+ for request in cache_data['requests']:
+ response_file = os.path.join(responses_dir, request['response_file'])
+ response = ReadFile(response_file, 'rb')
+ key = (request['method'], request['path'])
+ self.cache[key] = {'response': response, 'headers': request['headers']}
+
+ # Load benchmark scripts.
+ self.benchmark_resources = {}
+ data = ReadFile(os.path.join(test_dir, 'data.json'))
+ data = string.Template(DATA_JS).substitute(data=data)
+ self.benchmark_resources['data.js'] = {'data': data,
+ 'type': 'application/javascript'}
+ for resource in ('common.js', 'playback.js'):
+ resource_file = os.path.join(self.script_dir, resource)
+ self.benchmark_resources[resource] = {'data': ReadFile(resource_file),
+ 'type': 'application/javascript'}
+
+ # Format start page.
+ parse_result = urlparse.urlparse(cache_data['start_url'])
+ target_origin = '%s://%s' % (parse_result.scheme, parse_result.netloc)
+ start_page = string.Template(START_PAGE).substitute(
+ run_count=run_count, target_origin=target_origin,
+ width=cache_data['width'], height=cache_data['height'])
+ self.benchmark_resources['start_page'] = {
+ 'data': start_page,
+ 'type': 'text/html; charset=UTF-8'
+ }
+
+ start_page_popup = string.Template(START_PAGE_POPUP).substitute(
+ start_url=cache_data['start_url'],
+ width=cache_data['width'], height=cache_data['height'])
+ self.benchmark_resources['start_page_popup'] = {
+ 'data': start_page_popup,
+ 'type': 'text/html; charset=UTF-8'
+ }
+
+ self._GetBenchmarkResource('start_page', handler)
+
+ def _GetBenchmarkResource(self, resource, handler):
+ "Sends requested resource to browser."
+
+ if resource in self.benchmark_resources:
+ resource = self.benchmark_resources[resource]
+ handler.send_response(200)
+ handler.send_header('content-length', len(resource['data']))
+ handler.send_header('content-type', resource['type'])
+ handler.end_headers()
+ handler.wfile.write(resource['data'])
+ else:
+ handler.send_response(404)
+ handler.end_headers()
+
+ def _ProcessBenchmarkReport(self, content, handler):
+ "Reads benchmark score from report content and invokes callback."
+
+ handler.send_response(204)
+ handler.end_headers()
+ content = json.loads(content)
+ if 'results' in content:
+ results = content['results']
+ sys.stdout.write('Results: %s\n' % results)
+ if self.test_callback: self.test_callback(results)
+ elif 'error' in content:
+ sys.stderr.write('Error: %s\n' % content['error'])
+
+ def _GetApplicationResource(self, handler):
+ "Searches for response in cache. If not found, responds with 204."
+ key = (handler.command, handler.path)
+ if key in self.cache:
+ sys.stdout.write('%s %s -> found\n' % key)
+ handler.wfile.write(self.cache[key]['response'])
+ else:
+ sys.stderr.write('%s %s -> not found\n' % key)
+ handler.send_response(204, "not in cache")
+ handler.end_headers()
diff --git a/tools/playback_benchmark/proxy_handler.py b/tools/playback_benchmark/proxy_handler.py
new file mode 100644
index 0000000..05da078
--- /dev/null
+++ b/tools/playback_benchmark/proxy_handler.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python
+#
+# Copyright 2010 Google Inc. All Rights Reserved.
+
+"""HTTP proxy request handler with SSL support.
+
+ RequestHandler: Utility class for parsing HTTP requests.
+ ProxyHandler: HTTP proxy handler.
+"""
+
+import BaseHTTPServer
+import cgi
+import OpenSSL
+import os
+import socket
+import SocketServer
+import sys
+import traceback
+import urlparse
+
+
+class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+ """Class for reading HTTP requests and writing HTTP responses"""
+
+ protocol_version = "HTTP/1.1"
+ request_version = protocol_version
+
+ class HTTPRequestException(Exception): pass
+
+ def __init__(self, rfile, wfile, server):
+ self.rfile = rfile
+ self.wfile = wfile
+ self.server = server
+
+ def ReadRequest(self):
+ "Reads and parses single HTTP request from self.rfile"
+
+ self.raw_requestline = self.rfile.readline()
+ if not self.raw_requestline:
+ self.close_connection = 1
+ raise HTTPRequestException('failed to read request line')
+ if not self.parse_request():
+ raise HTTPRequestException('failed to parse request')
+ self.headers = dict(self.headers)
+ self.body = None
+ if 'content-length' in self.headers:
+ self.body = self.rfile.read(int(self.headers['content-length']))
+
+ def log_message(self, format, *args):
+ pass
+
+
+class ProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+ "Request handler class for proxy server"
+
+ server_version = "PlaybackProxy/0.0.1"
+ protocol_version = "HTTP/1.1"
+
+ def do_CONNECT(self):
+ "Handles CONNECT HTTP request"
+
+ server = self.path.split(':')[0]
+ certificate_file = os.path.join(self.certificate_directory, server)
+ if not os.path.isfile(certificate_file):
+ sys.stderr.write('request to connect %s is ignored\n' % server)
+ self.send_response(501)
+ self.send_header('Proxy-agent', self.version_string())
+ self.end_headers()
+ return
+
+ # Send confirmation to browser.
+ self.send_response(200, 'Connection established')
+ self.send_header('Proxy-agent', self.version_string())
+ self.end_headers()
+
+ # Create SSL context.
+ context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
+ context.use_privatekey_file(certificate_file)
+ context.use_certificate_file(certificate_file)
+
+ # Create and initialize SSL connection atop of tcp socket.
+ ssl_connection = OpenSSL.SSL.Connection(context, self.connection)
+ ssl_connection.set_accept_state()
+ ssl_connection.do_handshake()
+ ssl_rfile = socket._fileobject(ssl_connection, "rb", self.rbufsize)
+ ssl_wfile = socket._fileobject(ssl_connection, "wb", self.wbufsize)
+
+ # Handle http requests coming from ssl_connection.
+ handler = RequestHandler(ssl_rfile, ssl_wfile, self.path)
+ try:
+ handler.close_connection = 1
+ while True:
+ handler.ReadRequest()
+ self.driver.ProcessRequest(handler)
+ if handler.close_connection: break
+ except (OpenSSL.SSL.SysCallError, OpenSSL.SSL.ZeroReturnError):
+ pass
+ finally:
+ self.close_connection = 1
+
+ def do_GET(self):
+ self.driver.ProcessRequest(self)
+
+ def do_POST(self):
+ if 'content-length' in self.headers:
+ self.body = self.rfile.read(int(self.headers['content-length']))
+ self.driver.ProcessRequest(self)
+
+ def log_message(self, format, *args):
+ sys.stdout.write((format % args) + '\n')
+
+
+class ThreadingHTTPServer (SocketServer.ThreadingMixIn,
+ BaseHTTPServer.HTTPServer):
+ pass
+
+
+def CreateServer(driver, port, certificate_directory=None):
+ if not certificate_directory:
+ certificate_directory = os.path.join(os.getcwd(), 'certificates')
+ ProxyHandler.driver = driver
+ ProxyHandler.certificate_directory = certificate_directory
+ return ThreadingHTTPServer(('', port), ProxyHandler)
diff --git a/tools/playback_benchmark/run.py b/tools/playback_benchmark/run.py
new file mode 100644
index 0000000..231ee84
--- /dev/null
+++ b/tools/playback_benchmark/run.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+#
+# Copyright 2010 Google Inc. All Rights Reserved.
+
+"""Script for test playback.
+
+Prerequisites:
+1. OpenSSL library - http://www.openssl.org/
+2. Python interface to the OpenSSL library - https://launchpad.net/pyopenssl
+
+Example usage:
+python run.py -t <test_dir>
+"""
+
+from optparse import OptionParser
+
+import playback_driver
+import proxy_handler
+
+
+def Run(options):
+ driver = playback_driver.PlaybackRequestHandler(options.test_dir)
+ httpd = proxy_handler.CreateServer(driver, options.port)
+ sa = httpd.socket.getsockname()
+ print "Serving HTTP on", sa[0], "port", sa[1], "..."
+ httpd.serve_forever()
+
+
+if __name__ == '__main__':
+ parser = OptionParser()
+ parser.add_option("-t", "--test-dir", dest="test_dir",
+ help="directory containing recorded test data")
+ parser.add_option("-p", "--port", dest="port", type="int", default=8000)
+ options = parser.parse_args()[0]
+ if not options.test_dir:
+ raise Exception('please specify test directory')
+
+ Run(options)
+