diff options
author | vitalyr@chromium.org <vitalyr@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-06-07 09:00:47 +0000 |
---|---|---|
committer | vitalyr@chromium.org <vitalyr@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-06-07 09:00:47 +0000 |
commit | 7a158cc250768c944236b31042aa06d55757f72f (patch) | |
tree | c5396866aac95255801bfa07c404ce78f5375048 /tools/playback_benchmark | |
parent | 1d21e4402d5f87766399afb56b20964af3f8a64e (diff) | |
download | chromium_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.js | 318 | ||||
-rw-r--r-- | tools/playback_benchmark/playback.js | 256 | ||||
-rw-r--r-- | tools/playback_benchmark/playback_driver.py | 196 | ||||
-rw-r--r-- | tools/playback_benchmark/proxy_handler.py | 123 | ||||
-rw-r--r-- | tools/playback_benchmark/run.py | 39 |
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) + |