diff options
-rw-r--r-- | chrome/test/perf/page_cycler_test.cc | 109 | ||||
-rw-r--r-- | tools/page_cycler/common/head.js | 10 | ||||
-rw-r--r-- | tools/page_cycler/common/report.html | 16 | ||||
-rw-r--r-- | tools/page_cycler/webpagereplay/README | 15 | ||||
-rw-r--r-- | tools/page_cycler/webpagereplay/extension/background.html | 2 | ||||
-rw-r--r-- | tools/page_cycler/webpagereplay/extension/background.js | 328 | ||||
-rw-r--r-- | tools/page_cycler/webpagereplay/extension/content.js | 14 | ||||
-rw-r--r-- | tools/page_cycler/webpagereplay/extension/manifest.json | 26 | ||||
-rw-r--r-- | tools/page_cycler/webpagereplay/extension/page_cycler.js | 54 | ||||
-rw-r--r-- | tools/page_cycler/webpagereplay/extension/start.js | 89 | ||||
-rw-r--r-- | tools/page_cycler/webpagereplay/start.html | 26 | ||||
-rw-r--r-- | tools/page_cycler/webpagereplay/start.js | 123 | ||||
-rw-r--r-- | tools/page_cycler/webpagereplay/tests/2012Q2.js | 77 | ||||
-rwxr-xr-x | tools/python/google/webpagereplay_utils.py | 269 |
14 files changed, 1141 insertions, 17 deletions
diff --git a/chrome/test/perf/page_cycler_test.cc b/chrome/test/perf/page_cycler_test.cc index 272a4f9..b662660 100644 --- a/chrome/test/perf/page_cycler_test.cc +++ b/chrome/test/perf/page_cycler_test.cc @@ -29,12 +29,14 @@ #ifndef NDEBUG static const int kTestIterations = 2; static const int kDatabaseTestIterations = 2; +static const int kWebPageReplayIterations = 2; #else static const int kTestIterations = 10; // For some unknown reason, the DB perf tests are much much slower on the // Vista perf bot, so we have to cut down the number of iterations to 5 // to make sure each test finishes in less than 10 minutes. static const int kDatabaseTestIterations = 5; +static const int kWebPageReplayIterations = 5; #endif static const int kIDBTestIterations = 5; @@ -42,6 +44,11 @@ static const int kIDBTestIterations = 5; // this URL's server should point to data/page_cycler/. static const char kBaseUrl[] = "http://localhost:8000/"; +// The following port numbers must match those in +// src/tools/python/google/webpagereplay_utils.py. +static const char kWebPageReplayHttpPort[] = "8080"; +static const char kWebPageReplayHttpsPort[] = "8413"; + namespace { void PopulateBufferCache(const FilePath& test_dir) { @@ -158,21 +165,17 @@ class PageCyclerTest : public UIPerfTest { return num_test_iterations_; } - // For HTTP tests, the name must be safe for use in a URL without escaping. - void RunPageCycler(const char* name, std::wstring* pages, - std::string* timings, bool use_http) { + virtual void GetTestUrl(const char* name, bool use_http, GURL *test_url) { FilePath test_path = GetDataPath(name); ASSERT_TRUE(file_util::DirectoryExists(test_path)) << "Missing test directory " << test_path.value(); - PopulateBufferCache(test_path); - GURL test_url; if (use_http) { - test_url = GURL(std::string(kBaseUrl) + name + "/start.html"); + *test_url = GURL(std::string(kBaseUrl) + name + "/start.html"); } else { test_path = test_path.Append(FILE_PATH_LITERAL("start.html")); - test_url = net::FilePathToFileURL(test_path); + *test_url = net::FilePathToFileURL(test_path); } // run N iterations @@ -182,7 +185,14 @@ class PageCyclerTest : public UIPerfTest { replacements.SetQuery( query_string.c_str(), url_parse::Component(0, query_string.length())); - test_url = test_url.ReplaceComponents(replacements); + *test_url = test_url->ReplaceComponents(replacements); + } + + // For HTTP tests, the name must be safe for use in a URL without escaping. + void RunPageCycler(const char* name, std::wstring* pages, + std::string* timings, bool use_http) { + GURL test_url; + GetTestUrl(name, use_http, &test_url); scoped_refptr<TabProxy> tab(GetActiveTab()); ASSERT_TRUE(tab.get()); @@ -443,6 +453,77 @@ class PageCyclerIndexedDatabaseReferenceTest : public PageCyclerReferenceTest { } }; +// Web Page Replay is a proxy server to record and serve pages +// with realistic network delays and bandwidth throttling. +// runtest.py launches replay.py to support these tests. +class PageCyclerWebPageReplayTest : public PageCyclerTest { + public: + PageCyclerWebPageReplayTest() { + // These Chrome command-line arguments need to be kept in sync + // with src/tools/python/google/webpagereplay_utils.py. + FilePath extension_path = GetPageCyclerWprPath("extension"); + launch_arguments_.AppendSwitchPath( + switches::kLoadExtension, extension_path); + // TODO(slamm): Instead of kHostResolverRules, add a new switch, + // kTestingFixedDnsPort, and configure Web Page Replay to run + // a DNS proxy on that port to test Chrome's DNS code. + launch_arguments_.AppendSwitchASCII( + switches::kHostResolverRules, "MAP * 127.0.0.1"); + launch_arguments_.AppendSwitchASCII( + switches::kTestingFixedHttpPort, kWebPageReplayHttpPort); + launch_arguments_.AppendSwitchASCII( + switches::kTestingFixedHttpsPort, kWebPageReplayHttpsPort); + launch_arguments_.AppendSwitch(switches::kEnableExperimentalExtensionApis); + launch_arguments_.AppendSwitch(switches::kEnableStatsTable); + launch_arguments_.AppendSwitch(switches::kEnableBenchmarking); + launch_arguments_.AppendSwitch(switches::kIgnoreCertificateErrors); + launch_arguments_.AppendSwitch(switches::kNoProxyServer); + } + + FilePath GetPageCyclerWprPath(const char* name) { + FilePath wpr_path; + PathService::Get(base::DIR_SOURCE_ROOT, &wpr_path); + wpr_path = wpr_path.AppendASCII("tools"); + wpr_path = wpr_path.AppendASCII("page_cycler"); + wpr_path = wpr_path.AppendASCII("webpagereplay"); + wpr_path = wpr_path.AppendASCII(name); + return wpr_path; + } + + virtual int GetTestIterations() OVERRIDE { + return kWebPageReplayIterations; + } + + virtual void GetTestUrl(const char* name, bool use_http, + GURL *test_url) OVERRIDE { + FilePath start_path = GetPageCyclerWprPath("start.html"); + + // Add query parameters for iterations and test name. + const std::string query_string = + "iterations=" + base::IntToString(GetTestIterations()) + + "&test=" + name + + "&auto=1"; + GURL::Replacements replacements; + replacements.SetQuery( + query_string.c_str(), + url_parse::Component(0, query_string.length())); + + *test_url = net::FilePathToFileURL(start_path); + *test_url = test_url->ReplaceComponents(replacements); + } + + void RunTest(const char* graph, const char* name) { + FilePath test_path = GetPageCyclerWprPath("tests"); + test_path = test_path.AppendASCII(name); + test_path = test_path.ReplaceExtension(FILE_PATH_LITERAL(".js")); + ASSERT_TRUE(file_util::PathExists(test_path)) + << "Missing test file " << test_path.value(); + + const bool use_http = false; // always use a file + PageCyclerTest::RunTestWithSuffix(graph, name, use_http, ""); + } +}; + // This macro simplifies setting up regular and reference build tests. #define PAGE_CYCLER_TESTS(test, name, use_http) \ TEST_F(PageCyclerTest, name) { \ @@ -497,6 +578,11 @@ TEST_F(PageCyclerExtensionWebRequestTest, name) { \ RunTest("times", "extension_webrequest", "_extwr", test, false); \ } +#define PAGE_CYCLER_WEBPAGEREPLAY_TESTS(test, name) \ +TEST_F(PageCyclerWebPageReplayTest, name) { \ + RunTest("times", test); \ +} + // file-URL tests PAGE_CYCLER_FILE_TESTS("moz", MozFile); PAGE_CYCLER_EXTENSIONS_FILE_TESTS("moz", MozFile); @@ -521,6 +607,13 @@ PAGE_CYCLER_HTTP_TESTS("intl2", Intl2Http); PAGE_CYCLER_HTTP_TESTS("dom", DomHttp); PAGE_CYCLER_HTTP_TESTS("bloat", BloatHttp); +// Web Page Replay (simulated network) tests. +// Windows is unsupported because of issues with loopback adapter and +// dummynet is unavailable on Vista and above. +#if !defined(OS_WIN) +PAGE_CYCLER_WEBPAGEREPLAY_TESTS("2012Q2", 2012Q2); +#endif + // HTML5 database tests // These tests are _really_ slow on XP/Vista. #if !defined(OS_WIN) diff --git a/tools/page_cycler/common/head.js b/tools/page_cycler/common/head.js index f12fb24..dddaa7ee 100644 --- a/tools/page_cycler/common/head.js +++ b/tools/page_cycler/common/head.js @@ -26,13 +26,11 @@ function __pages() { // fetch lazily return this.data; } function __get_timings() { - if (sessionStorage == null) + if (sessionStorage != null && + sessionStorage.getItem("__pc_timings") != null) { + return sessionStorage["__pc_timings"]; + } else { return __get_cookie("__pc_timings"); - else { - if (sessionStorage.getItem("__pc_timings") == null) - return ""; - else - return sessionStorage["__pc_timings"]; } } function __set_timings(timings) { diff --git a/tools/page_cycler/common/report.html b/tools/page_cycler/common/report.html index 221d8ab..9833fbe 100644 --- a/tools/page_cycler/common/report.html +++ b/tools/page_cycler/common/report.html @@ -79,6 +79,7 @@ document.write("</table>"); r.vari = r.vari / (ary.length - 1); r.stdd = Math.sqrt(r.vari); + r.errp = r.stdd / Math.sqrt((ary.length - 1) / 2) / r.mean * 100; return r; } @@ -88,7 +89,14 @@ document.write("</table>"); if (linkify) { var anchor = doc.createElement("A"); - anchor.href = text + "/index.html?skip=true"; + if (text.indexOf('http://localhost:') == 0 || + text.indexOf('file://') == 0) { + // URLs for page cycler HTTP and file tests. + anchor.href = text + "/index.html?skip=true"; + } else { + // For Web Page Replay, URLs are same as recorded pages. + anchor.href = text; + } anchor.appendChild(doc.createTextNode(text)); td.appendChild(anchor); } @@ -113,7 +121,7 @@ document.write("</table>"); function showReport() { var tbody = document.getElementById("tbody"); - var colsums = [0,0,0,0]; + var colsums = [0,0,0,0,0]; var timeVals = getTimeVals(); for (var i = 0; i < timeVals.length; ++i) { var tr = document.createElement("TR"); @@ -125,6 +133,7 @@ document.write("</table>"); appendTableCol(tr, r.max.toFixed(2)); appendTableCol(tr, r.mean.toFixed(2)); appendTableCol(tr, r.stdd.toFixed(2)); + appendTableCol(tr, r.errp.toFixed(2)); //appendTableCol(tr, r.chi2.toFixed(2)); for (var j = 0; j < timeVals[i].length; ++j) { @@ -138,6 +147,7 @@ document.write("</table>"); colsums[1] = colsums[1] + r.max; colsums[2] = colsums[2] + r.mean; colsums[3] = colsums[3] + r.stdd; + colsums[4] = colsums[4] + r.errp; tbody.appendChild(tr); } @@ -163,6 +173,7 @@ document.write("</table>"); <th>Max</th> <th>Mean</th> <th>Std.d</th> + <th>Err %</th> <th colspan="10">Runs</th> </tr> </thead> @@ -170,4 +181,3 @@ document.write("</table>"); </table> </body> </html> - diff --git a/tools/page_cycler/webpagereplay/README b/tools/page_cycler/webpagereplay/README new file mode 100644 index 0000000..6fce3e9 --- /dev/null +++ b/tools/page_cycler/webpagereplay/README @@ -0,0 +1,15 @@ +Page Cycler Tests with Web Page Replay +--------------------------------------------------------------------------- +Web Page Replay is a proxy that can record and "play" web pages with +simulated network characteristics -- without having to edit the pages +by hand. With WPR, tests can use "real" web content, and catch +performance issues that may result from introducing network delays and +bandwidth throttling. + +The Chromium extension here is used to load URLs, record the times, +and set the appropriate cookies for src/tools/page_cycler/common/report.html. + +For more information: +https://sites.google.com/a/chromium.org/dev/developers/testing/page-cyclers-wpr +https://sites.google.com/a/chromium.org/dev/developers/testing/page-cyclers +http://code.google.com/p/web-page-replay diff --git a/tools/page_cycler/webpagereplay/extension/background.html b/tools/page_cycler/webpagereplay/extension/background.html new file mode 100644 index 0000000..61a2902 --- /dev/null +++ b/tools/page_cycler/webpagereplay/extension/background.html @@ -0,0 +1,2 @@ +<script src="background.js"></script> +<script src="page_cycler.js"></script> diff --git a/tools/page_cycler/webpagereplay/extension/background.js b/tools/page_cycler/webpagereplay/extension/background.js new file mode 100644 index 0000000..207979e --- /dev/null +++ b/tools/page_cycler/webpagereplay/extension/background.js @@ -0,0 +1,328 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// start.js sends a "start" message to set this. +window.benchmarkConfiguration = {}; + +// The callback (e.g. report writer) is set via AddBenchmarckCallback. +window.benchmarkCallback; + +// Url to load before loading target page. +var kWaitUrl = "http://wprwprwpr/web-page-replay-generate-200"; + +// Constant StatCounter Names +var kTcpReadBytes = "tcp.read_bytes"; +var kTcpWriteBytes = "tcp.write_bytes"; +var kRequestCount = "HttpNetworkTransaction.Count"; +var kConnectCount = "tcp.connect"; + +function CHECK(expr, comment) { + if (!expr) { + console.log(comment); + alert(comment); + } +} + +function Result() { + var me_ = this; + this.url = ""; + this.firstPaintTime = 0; + this.readBytesKB = 0; + this.writeBytesKB = 0; + this.numRequests = 0; + this.numConnects = 0; + this.timing = {}; // window.performance.timing + this.getTotalTime = function() { + var totalTime = 0 + if (me_.timing.navigationStart && me_.timing.loadEventEnd) { + totalTime = me_.timing.loadEventEnd - me_.timing.navigationStart; + } + CHECK(totalTime >= 0); + return totalTime; + } +} + +// Collect all the results for a session (i.e. different pages). +function ResultsCollection() { + var results_ = []; + var pages_ = []; + var pageResults_ = {}; + + this.addResult = function(result) { + results_.push(result); + var url = result.url; + if (!(url in pageResults_)) { + pages_.push(url); + pageResults_[url] = []; + } + pageResults_[url].push(result); + } + + this.getPages = function() { + return pages_; + } + + this.getResults = function() { + return results_; + } + + this.getTotalTimes = function() { + return results_.map(function (t) { return t.getTotalTime(); }); + } +} + +// Load a url in the default tab and record the time. +function PageLoader(url, resultReadyCallback) { + var me_ = this; + var url_ = url; + var resultReadyCallback_ = resultReadyCallback; + + // If it record mode, wait a little longer for lazy loaded resources. + var postLoadGraceMs_ = window.isRecordMode ? 5000 : 0; + var loadInterval_ = window.loadInterval; + var checkInterval_ = window.checkInterval; + var timeout_ = window.timeout; + var maxLoadChecks_ = window.maxLoadChecks; + + var preloadFunc_; + var timeoutId_; + var isFinished_; + var result_; + + var initialReadBytes_; + var initialWriteBytes_; + var initialRequestCount_; + var initialConnectCount_; + + this.result = function() { return result_; }; + + this.run = function() { + timeoutId_ = null; + isFinished_ = false; + result_ = null; + initialReadBytes_ = chrome.benchmarking.counter(kTcpReadBytes); + initialWriteBytes_ = chrome.benchmarking.counter(kTcpWriteBytes); + initialRequestCount_ = chrome.benchmarking.counter(kRequestCount); + initialConnectCount_ = chrome.benchmarking.counter(kConnectCount); + + if (me_.preloadFunc_) { + me_.preloadFunc_(me_.load_); + } else { + me_.load_(); + } + }; + + this.setClearAll = function() { + me_.preloadFunc_ = me_.clearAll_; + }; + + this.setClearConnections = function() { + me_.preloadFunc_ = me_.clearConnections_; + }; + + this.clearAll_ = function(callback) { + chrome.tabs.getSelected(null, function(tab) { + chrome.tabs.update(tab.id, {"url": kWaitUrl}, function() { + chrome.benchmarking.clearHostResolverCache(); + chrome.benchmarking.clearPredictorCache(); + chrome.benchmarking.closeConnections(); + var dataToRemove = { + "appcache": true, + "cache": true, + "cookies": true, + "downloads": true, + "fileSystems": true, + "formData": true, + "history": true, + "indexedDB": true, + "localStorage": true, + "passwords": true, + "pluginData": true, + "webSQL": true + }; + // Add any items new to the API. + for (var prop in chrome.browsingData) { + var dataName = prop.replace("remove", ""); + if (dataName && dataName != prop) { + dataName = dataName.charAt(0).toLowerCase() + + dataName.substr(1); + if (!dataToRemove.hasOwnProperty(dataName)) { + console.log("New browsingData API item: " + dataName); + dataToRemove[dataName] = true; + } + } + } + chrome.browsingData.remove({}, dataToRemove, callback); + }); + }); + }; + + this.clearConnections_ = function(callback) { + chrome.benchmarking.closeConnections(); + callback(); + }; + + this.load_ = function() { + console.log("LOAD started: " + url_); + setTimeout(function() { + chrome.extension.onRequest.addListener(me_.finishLoad_); + timeoutId_ = setTimeout(function() { + me_.finishLoad_({"loadTimes": null, "timing": null}); + }, timeout_); + chrome.tabs.getSelected(null, function(tab) { + chrome.tabs.update(tab.id, {"url": url_}); + }); + }, loadInterval_); + }; + + this.finishLoad_ = function(msg) { + if (!isFinished_) { + isFinished_ = true; + clearTimeout(timeoutId_); + chrome.extension.onRequest.removeListener(me_.finishLoad_); + me_.saveResult_(msg.loadTimes, msg.timing); + } + }; + + this.saveResult_ = function(loadTimes, timing) { + result_ = new Result() + result_.url = url_; + if (!loadTimes || !timing) { + console.log("LOAD INCOMPLETE: " + url_); + } else { + console.log("LOAD complete: " + url_); + result_.timing = timing; + var baseTime = timing.navigationStart; + CHECK(baseTime); + result_.firstPaintTime = Math.max(0, + Math.round((1000.0 * loadTimes.firstPaintTime) - baseTime)); + } + result_.readBytesKB = (chrome.benchmarking.counter(kTcpReadBytes) - + initialReadBytes_) / 1024; + result_.writeBytesKB = (chrome.benchmarking.counter(kTcpWriteBytes) - + initialWriteBytes_) / 1024; + result_.numRequests = (chrome.benchmarking.counter(kRequestCount) - + initialRequestCount_); + result_.numConnects = (chrome.benchmarking.counter(kConnectCount) - + initialConnectCount_); + setTimeout(function() { resultReadyCallback_(me_); }, postLoadGraceMs_); + }; +} + +// Load page sets and prepare performance results. +function SessionLoader(resultsReadyCallback) { + var me_ = this; + var resultsReadyCallback_ = resultsReadyCallback; + var pageSets_ = benchmarkConfiguration.pageSets; + var iterations_ = window.iterations; + var retries_ = window.retries; + + var pageLoaders_ = []; + var resultsCollection_ = new ResultsCollection(); + var loaderIndex_ = 0; + var retryIndex_ = 0; + var iterationIndex_ = 0; + + this.run = function() { + me_.createLoaders_(); + me_.loadPage_(); + } + + this.getResultsCollection = function() { + return resultsCollection_; + } + + this.createLoaders_ = function() { + // Each url becomes one benchmark. + for (var i = 0; i < pageSets_.length; i++) { + for (var j = 0; j < pageSets_[i].length; j++) { + // Remove extra space at the beginning or end of a url. + var url = pageSets_[i][j].trim(); + // Alert about and ignore blank page which does not get loaded. + if (url == "about:blank") { + alert("blank page loaded!"); + } else if (!url.match(/https?:\/\//)) { + // Alert about url that is not in scheme http:// or https://. + alert("Skipping url without http:// or https://: " + url); + } else { + var loader = new PageLoader(url, me_.handleResult_) + if (j == 0) { + // Clear all browser data for the first page in a sub list. + loader.setClearAll(); + } else { + // Otherwise, only clear the connections. + loader.setClearConnections(); + } + pageLoaders_.push(loader); + } + } + } + } + + this.loadPage_ = function() { + console.log("LOAD url " + (loaderIndex_ + 1) + " of " + + pageLoaders_.length + + ", iteration " + (iterationIndex_ + 1) + " of " + + iterations_); + pageLoaders_[loaderIndex_].run(); + } + + this.handleResult_ = function(loader) { + var result = loader.result(); + resultsCollection_.addResult(result); + var totalTime = result.getTotalTime(); + if (!totalTime && retryIndex_ < retries_) { + retryIndex_++; + console.log("LOAD retry, " + retryIndex_); + } else { + retryIndex_ = 0; + console.log("RESULTS url " + (loaderIndex_ + 1) + " of " + + pageLoaders_.length + + ", iteration " + (iterationIndex_ + 1) + " of " + + iterations_ + ": " + totalTime); + loaderIndex_++; + if (loaderIndex_ >= pageLoaders_.length) { + iterationIndex_++; + if (iterationIndex_ < iterations_) { + loaderIndex_ = 0; + } else { + resultsReadyCallback_(me_); + return; + } + } + } + me_.loadPage_(); + } +} + +function AddBenchmarkCallback(callback) { + window.benchmarkCallback = callback; +} + +function Run() { + window.checkInterval = 500; + window.loadInterval = 1000; + window.timeout = 20000; // max ms before killing page. + window.retries = 0; + window.isRecordMode = benchmarkConfiguration.isRecordMode; + if (window.isRecordMode) { + window.iterations = 1; + window.timeout = 40000; + window.retries = 2; + } else { + window.iterations = benchmarkConfiguration["iterations"] || 3; + } + var sessionLoader = new SessionLoader(benchmarkCallback); + console.log("pageSets: " + JSON.stringify(benchmarkConfiguration.pageSets)); + sessionLoader.run(); +} + +chrome.extension.onConnect.addListener(function(port) { + port.onMessage.addListener(function(data) { + if (data.message == "start") { + window.benchmarkConfiguration = data.benchmark; + Run() + } + }); +});
\ No newline at end of file diff --git a/tools/page_cycler/webpagereplay/extension/content.js b/tools/page_cycler/webpagereplay/extension/content.js new file mode 100644 index 0000000..69713c3 --- /dev/null +++ b/tools/page_cycler/webpagereplay/extension/content.js @@ -0,0 +1,14 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Hook onload and then add a timeout to send timing information +// after the onload has completed. +window.addEventListener('load', function() { + window.setTimeout(function() { + chrome.extension.sendRequest({ + "timing": window.performance.timing, + "loadTimes": chrome.loadTimes(), + }); + }, 0); +}, false); diff --git a/tools/page_cycler/webpagereplay/extension/manifest.json b/tools/page_cycler/webpagereplay/extension/manifest.json new file mode 100644 index 0000000..a775768 --- /dev/null +++ b/tools/page_cycler/webpagereplay/extension/manifest.json @@ -0,0 +1,26 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +{ + "name": "Chromium Page Cycler with Web Page Replay.", + "version": "1.0.0.0", + "description": "Chromium Page Cycler with Web Page Replay.", + "background_page": "background.html", + "content_scripts": [ + { "matches": ["file:///*/start.html*"], + "js": ["start.js"], + "run_at": "document_idle" + }, + { "matches": ["http://*/*", "https://*/*"], + "js": ["content.js"], + "run_at": "document_start" + } + ], + "permissions": [ + "browsingData", + "cookies", + "experimental", + "tabs", + "<all_urls>" + ] +} diff --git a/tools/page_cycler/webpagereplay/extension/page_cycler.js b/tools/page_cycler/webpagereplay/extension/page_cycler.js new file mode 100644 index 0000000..76869b2 --- /dev/null +++ b/tools/page_cycler/webpagereplay/extension/page_cycler.js @@ -0,0 +1,54 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Returns the sum of all values in the array. +Array.sum = function(array) { + var sum = 0; + for (var i = array.length - 1; i >= 0; i--) { + sum += array[i]; + } + return sum; +}; + + +function WriteReport(sessionLoader) { + var iterations = window.iterations; + var reportUrl = window.benchmarkConfiguration.reportUrl; + var resultsCollection = sessionLoader.getResultsCollection(); + var times = resultsCollection.getTotalTimes(); + var pages = resultsCollection.getPages(); + + reportUrl += "?n=" + iterations; + reportUrl += "&i=" + iterations * pages.length; // "cycles" + reportUrl += "&td=" + Array.sum(times); // total time + reportUrl += "&tf=" + 0; // fudge time + console.log('reportUrl: ' + reportUrl); + chrome.cookies.set({ + "url": reportUrl, + "name": "__pc_done", + "value": "1", + "path": "/", + }); + chrome.cookies.set({ + "url": reportUrl, + "name": "__pc_pages", + "value": pages.map(function(x) { + return x.replace(/=/g, "%3D"); + }).join(","), + "path": "/", + }); + chrome.cookies.set({ + "url": reportUrl, + "name": "__pc_timings", + "value": times.join(","), + "path": "/", + }); + + chrome.tabs.getSelected(null, function(tab) { + console.log("Navigate to the report."); + chrome.tabs.update(tab.id, {"url": reportUrl}, null); + }); +} + +AddBenchmarkCallback(WriteReport); diff --git a/tools/page_cycler/webpagereplay/extension/start.js b/tools/page_cycler/webpagereplay/extension/start.js new file mode 100644 index 0000000..3250edb --- /dev/null +++ b/tools/page_cycler/webpagereplay/extension/start.js @@ -0,0 +1,89 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +document.cookie = "__navigated_to_report=0; path=/"; +document.cookie = "__pc_done=0; path=/"; +document.cookie = "__pc_timings=; path=/"; + +function dirname(path) { + var match = path.match(/(.*)\//); + if (match) { + return match[1]; + } else { + return "."; + } +} + +function IsWprRecordMode() { + var kStatusUrl = "http://wprwprwpr/web-page-replay-command-status"; + var isRecordMode; + var xhr = new XMLHttpRequest(); + var useAsync = false; + xhr.open("GET", kStatusUrl, useAsync); + xhr.timeout = 500; + xhr.onreadystatechange = function() { + if (xhr.readyState == 4 && xhr.status == 200) { + var status = JSON.parse(xhr.responseText); + isRecordMode = status["is_record_mode"]; + console.log("WPR record mode?: " + isRecordMode); + } + }; + try { + xhr.send(); + } catch(e) { + throw "Web Page Replay is not responding. Start WPR to continue." + } + return isRecordMode; +} + + +function TryStart() { + console.log("try start"); + var status_element = document.getElementById("status"); + + var config_json; + var config; + try { + config_json = document.getElementById("json").textContent; + config = JSON.parse(config_json); + } catch(err) { + console.log("Bad json data: " + config_json); + status_element.textContent = "Exception: " + err + "\njson data: " + + config_json; + return; + } + var isRecordMode = false; + try { + isRecordMode = IsWprRecordMode(); + } catch (err) { + status_element.textContent = err; + setTimeout(TryStart, 5000); + return; + } + + if (!config["shouldStart"]) { + status_element.textContent = + "Press 'Start' to continue (or load this page with 'auto=1')."; + return; + } + + try { + var reportDir = dirname(dirname(window.location.pathname)) + "/common"; + config["reportUrl"] = "file://" + reportDir + "/report.html"; + config["isRecordMode"] = isRecordMode; + var port = chrome.extension.connect(); + port.postMessage({message: "start", benchmark: config}); + console.log("sending start message: page count, " + + config["pageSets"].length); + } catch(err) { + console.log("TryStart retrying after exception: " + err); + status_element.textContent = "Exception: " + err; + setTimeout(TryStart, 1000); + return; + } + status_element.textContent = "STARTING"; +} + +// We wait before starting the test just to let chrome warm up better. +setTimeout(TryStart, 250); diff --git a/tools/page_cycler/webpagereplay/start.html b/tools/page_cycler/webpagereplay/start.html new file mode 100644 index 0000000..d263a35 --- /dev/null +++ b/tools/page_cycler/webpagereplay/start.html @@ -0,0 +1,26 @@ +<html> +<!-- Copyright (c) 2012 The Chromium Authors. All rights reserved. --> +<!-- Use of this source code is governed by a BSD-style license that can be --> +<!-- found in the LICENSE file. --> +<head> +<style> + .labeledtextarea * { + vertical-align: middle; + } +</style> +</head> +<body> +<h3>Page Cycler: Web Page Replay Test</h3> + +<p class=labeledtextarea> +<label for="status">Status:</label> +<textarea id=status style="width:40em;height:3em;"></textarea> +</p> + +<hr> +<p> +<div id=startform></div> +<textarea id=json style="visibility: hidden;"></textarea> +<script src="start.js"></script> +</body> +</html> diff --git a/tools/page_cycler/webpagereplay/start.js b/tools/page_cycler/webpagereplay/start.js new file mode 100644 index 0000000..6ff5e6d --- /dev/null +++ b/tools/page_cycler/webpagereplay/start.js @@ -0,0 +1,123 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// webpagereplay/start.js - Start Web Page Replay (WPR) test. +// +// This script is included by webpagereplay/start.html. +// The query parameter "test=TEST_NAME" is required to load the +// test configuration from webpagereplay/tests/TEST_NAME.js +// That JavaScript file defines a global, "pageSets", as a list of lists: +// [ [url_1, url_2], [url_3], ...], +// - Before each sublist: +// Run chrome.browingData.remove and close the connections. +// - Before each url in a sublist: +// Close the connections. +// +// These WPR tests use a Chrome extension to load the test URLs. +// The extension loads the test configuration via a DOM elemment +// (id=json). This script sets the content of that DOM element. +// +// The test runs immediately after being loaded. +// + + +var options = location.search.substring(1).split('&'); +function getopt(name) { + var r = new RegExp('^' + name + '='); + for (i = 0; i < options.length; ++i) { + if (options[i].match(r)) { + return options[i].substring(name.length + 1); + } + } + return null; +} + +function LoadTestConfigurationScript(testUrl, callback) { + var testjs = document.createElement('script'); + testjs.type = 'text/javascript'; + testjs.async = true; + testjs.src = testUrl + var s = document.getElementsByTagName('script')[0]; + testjs.addEventListener('load', callback, false); + s.parentNode.insertBefore(testjs, s); +} + +function ReloadIfStuck() { + setTimeout(function() { + var status = document.getElementById('status'); + // The status text is set to 'STARTING' by the extension. + if (status.textContent != 'STARTING') { + console.log('Benchmark stuck? Reloading.'); + window.location.reload(true); + } + }, 30000); +} + +function RenderForm() { + var form = document.createElement('FORM'); + form.setAttribute('action', 'start.html'); + + var label = document.createTextNode('Iterations: '); + form.appendChild(label); + + var input = document.createElement('INPUT'); + var iterations = getopt('iterations'); + input.setAttribute('name', 'iterations'); + input.setAttribute('value', iterations ? iterations : '5'); + form.appendChild(input); + + form.appendChild(document.createElement('P')); + + var label = document.createTextNode('Test: '); + form.appendChild(label); + + var input = document.createElement('INPUT'); + input.setAttribute('name', 'test'); + var test = getopt('test'); + input.setAttribute('value', test ? test : ''); + form.appendChild(input); + + var input = document.createElement('INPUT'); + input.setAttribute('name', 'auto'); + var auto = getopt('auto'); + input.setAttribute('value', 1); + input.setAttribute('type', 'hidden'); + form.appendChild(input); + + form.appendChild(document.createElement('P')); + + input = document.createElement('INPUT'); + input.setAttribute('type', 'submit'); + input.setAttribute('value', 'Start'); + form.appendChild(input); + + document.getElementById('startform').appendChild(form); +} + + +var iterations = getopt('iterations'); +var test_name = getopt('test'); +var is_auto_start = getopt('auto'); + +RenderForm(); +if (test_name) { + var testUrl = 'tests/' + test_name + '.js'; + LoadTestConfigurationScript(testUrl, function() { + var testConfig = {}; + if (iterations) { + testConfig['iterations'] = iterations; + } + // The pageSets global is set by test configuration script. + testConfig['pageSets'] = pageSets; + + if (is_auto_start) { + testConfig['shouldStart'] = 1; + ReloadIfStuck(); + } + // Write testConfig to "json" DOM element for the Chrome extension. + document.getElementById('json').textContent = JSON.stringify(testConfig); + }); +} else { + console.log('Need "test=TEST_NAME" query parameter.'); +} diff --git a/tools/page_cycler/webpagereplay/tests/2012Q2.js b/tools/page_cycler/webpagereplay/tests/2012Q2.js new file mode 100644 index 0000000..11bba9f --- /dev/null +++ b/tools/page_cycler/webpagereplay/tests/2012Q2.js @@ -0,0 +1,77 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/* + "pageSets" is a list of lists. + - Before each sublist: + Run chrome.browingData.remove and close the connections. + - Before each url in a sublist: + Close the connections. +*/ +var pageSets = [ + ["http://superfastpageload/web-page-replay-generate-200"], + + // Load url pairs without clearing browser data (e.g. cache) in-between. + ["http://go.com/", + "http://espn.go.com/"], + ["http://www.amazon.com/", + "http://www.amazon.com/Kindle-Fire-Amazon-Tablet/dp/B0051VVOB2/"], + ["http://www.baidu.com/", + "http://www.baidu.com/s?wd=obama"], + ["http://www.bing.com/", + "http://www.bing.com/search?q=cars"], + ["http://www.ebay.com/", + "http://fashion.ebay.com/womens-clothing"], + ["http://www.google.com/", + "http://www.google.com/search?q=dogs"], + ["http://www.yandex.ru/", + "http://yandex.ru/yandsearch?text=obama&lr=84"], + ["http://www.youtube.com", + "http://www.youtube.com/watch?v=xvN9Ri1GmuY&feature=g-sptl&cid=inp-hs-edt"], + + ["http://ameblo.jp/"], + ["http://en.rakuten.co.jp/"], + ["http://en.wikipedia.org/wiki/Lady_gaga"], + ["http://news.google.co.in"], + ["http://plus.google.com/"], // iframe error (result of no cookies?) + ["http://www.163.com/"], + ["http://www.apple.com/"], + ["http://www.bbc.co.uk/"], + ["http://www.cnet.com/"], + ["http://www.msn.com/"], + ["http://www.nytimes.com/"], + ["http://www.taobao.com/"], + ["http://www.yahoo.co.jp/"], + + // HTTPS pages. + ["https://wordpress.com/"], + ["https://www.conduit.com/"], + ["https://www.facebook.com", + "https://www.facebook.com/barackobama"], +]; + +/* + // Not included (need further investigation). + ["http://twitter.com/BarackObama", + "http://twitter.com/search?q=pizza"], // large variance on second page + ["http://www.fc2.com/"], // slow + ["http://sina.com.cn/"], // slow + ["http://www.mail.ru/"], // long load time (30s+) + ["http://www.sohu.com/"], // load does not finish (even without WPR) + + // Not included (trimmed pageSets to keep test under 10 minutes). + ["http://sfbay.craigslist.org/", + "http://sfbay.craigslist.org/search/sss?query=flowers"], + ["http://www.flickr.com/", + "http://www.flickr.com/photos/tags/flowers"], + ["http://www.linkedin.com/", + "http://www.linkedin.com/in/jeffweiner08"], + ["http://www.yahoo.com/", + "http://search.yahoo.com/search?p=disney"], + ["http://googleblog.blogspot.com/"], + ["http://www.adobe.com/reader/"], + ["http://www.cnn.com/"], + ["http://www.imdb.com/"], + ["http://www.qq.com/"], +*/ diff --git a/tools/python/google/webpagereplay_utils.py b/tools/python/google/webpagereplay_utils.py new file mode 100755 index 0000000..e93ec55 --- /dev/null +++ b/tools/python/google/webpagereplay_utils.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python +# Copyright (c) 2012 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""A class to help start/stop a Web Page Replay Server. + +The page cycler tests use this module to run Web Page Replay +(see tools/build/scripts/slave/runtest.py). + +If run from the command-line, the module will launch Web Page Replay +and the specified test: + + ./webpagereplay_utils.py --help # list options + ./webpagereplay_utils.py 2012Q2 # run a WPR-enabled test +""" + +import logging +import optparse +import os +import shutil +import signal +import subprocess +import sys +import tempfile +import time +import urllib + + +USAGE = '%s [options] CHROME_EXE TEST_NAME' % os.path.basename(sys.argv[0]) +USER_DATA_DIR = '{TEMP}/webpagereplay_utils-chrome' + +# The port numbers must match those in chrome/test/perf/page_cycler_test.cc. +HTTP_PORT = 8080 +HTTPS_PORT = 8413 + + +class ReplayError(Exception): + """Catch-all exception for the module.""" + pass + +class ReplayNotFoundError(Exception): + pass + +class ReplayNotStartedError(Exception): + pass + + +class ReplayLauncher(object): + LOG_FILE = 'log.txt' + + def __init__(self, replay_dir, archive_path, log_dir, replay_options=None): + """Initialize ReplayLauncher. + + Args: + replay_dir: directory that has replay.py and related modules. + archive_path: either a directory that contains WPR archives or, + a path to a specific WPR archive. + log_dir: where to write log.txt. + replay_options: a list of options strings to forward to replay.py. + """ + self.replay_dir = replay_dir + self.archive_path = archive_path + self.log_dir = log_dir + self.replay_options = replay_options if replay_options else [] + + self.log_name = os.path.join(self.log_dir, self.LOG_FILE) + self.log_fh = None + self.proxy_process = None + + self.wpr_py = os.path.join(self.replay_dir, 'replay.py') + if not os.path.exists(self.wpr_py): + raise ReplayNotFoundError('Path does not exist: %s' % self.wpr_py) + self.wpr_options = [ + '--port', str(HTTP_PORT), + '--ssl_port', str(HTTPS_PORT), + '--use_closest_match', + # TODO(slamm): Add traffic shaping (requires root): + # '--net', 'fios', + ] + self.wpr_options.extend(self.replay_options) + + def _OpenLogFile(self): + if not os.path.exists(self.log_dir): + os.makedirs(self.log_dir) + return open(self.log_name, 'w') + + def StartServer(self): + cmd_line = [self.wpr_py] + cmd_line.extend(self.wpr_options) + # TODO(slamm): Support choosing archive on-the-fly. + cmd_line.append(self.archive_path) + self.log_fh = self._OpenLogFile() + logging.debug('Starting Web-Page-Replay: %s', cmd_line) + self.proxy_process = subprocess.Popen( + cmd_line, stdout=self.log_fh, stderr=subprocess.STDOUT) + if not self.IsStarted(): + raise ReplayNotStartedError( + 'Web Page Replay failed to start. See the log file: ' + self.log_name) + + def IsStarted(self): + """Checks to see if the server is up and running.""" + for _ in range(5): + if self.proxy_process.poll() is not None: + # The process has exited. + break + try: + up_url = '%s://localhost:%s/web-page-replay-generate-200' + http_up_url = up_url % ('http', HTTP_PORT) + https_up_url = up_url % ('https', HTTPS_PORT) + if (200 == urllib.urlopen(http_up_url, None, {}).getcode() and + 200 == urllib.urlopen(https_up_url, None, {}).getcode()): + return True + except IOError: + time.sleep(1) + return False + + def StopServer(self): + if self.proxy_process: + logging.debug('Stopping Web-Page-Replay') + # Use a SIGINT here so that it can do graceful cleanup. + # Otherwise, we will leave subprocesses hanging. + self.proxy_process.send_signal(signal.SIGINT) + self.proxy_process.wait() + if self.log_fh: + self.log_fh.close() + + +class ChromiumPaths(object): + """Collect all the path handling together.""" + PATHS = { + 'archives': 'src/data/page_cycler/webpagereplay', + '.wpr': 'src/data/page_cycler/webpagereplay/{TEST_NAME}.wpr', + '.wpr_alt': 'src/tools/page_cycler/webpagereplay/tests/{TEST_NAME}.wpr', + 'start.html': 'src/tools/page_cycler/webpagereplay/start.html', + 'extension': 'src/tools/page_cycler/webpagereplay/extension', + 'logs': 'src/webpagereplay_logs/{TEST_EXE_NAME}', + 'replay': 'tools/build/third_party/webpagereplay', + } + + def __init__(self, **replacements): + """Initialize ChromiumPaths. + + Args: + replacements: a dict of format replacements for PATHS such as + {'TEST_NAME': '2012Q2', 'TEST_EXE_NAME': 'performance_ui_tests'}. + """ + module_dir = os.path.dirname(__file__) + self.base_dir = os.path.abspath(os.path.join( + module_dir, '..', '..', '..', '..')) + self.replacements = replacements + + def __getitem__(self, key): + path_parts = [x.format(**self.replacements) + for x in self.PATHS[key].split('/')] + return os.path.join(self.base_dir, *path_parts) + + +def LaunchChromium(chrome_exe, chromium_paths, test_name, + is_dns_forwarded, use_auto): + """Launch chromium to run WPR-backed page cycler tests. + + These options need to be kept in sync with + src/chrome/test/perf/page_cycler_test.cc. + """ + REPLAY_HOST='127.0.0.1' + user_data_dir = USER_DATA_DIR.format(**{'TEMP': tempfile.gettempdir()}) + chromium_args = [ + chrome_exe, + '--load-extension=%s' % chromium_paths['extension'], + '--testing-fixed-http-port=%s' % HTTP_PORT, + '--testing-fixed-https-port=%s' % HTTPS_PORT, + '--disable-background-networking', + '--enable-experimental-extension-apis', + '--enable-file-cookies', + '--enable-logging', + '--log-level=0', + '--enable-stats-table', + '--enable-benchmarking', + '--ignore-certificate-errors', + '--metrics-recording-only', + '--activate-on-launch', + '--no-first-run', + '--no-proxy-server', + '--user-data-dir=%s' % user_data_dir, + '--window-size=1280,1024', + ] + if not is_dns_forwarded: + chromium_args.append('--host-resolver-rules=MAP * %s' % REPLAY_HOST) + start_url = 'file://%s?test=%s' % (chromium_paths['start.html'], test_name) + if use_auto: + start_url += '&auto=1' + chromium_args.append(start_url) + if os.path.exists(user_data_dir): + shutil.rmtree(user_data_dir) + os.makedirs(user_data_dir) + try: + logging.debug('Starting Chrome: %s', chromium_args) + retval = subprocess.call(chromium_args) + finally: + shutil.rmtree(user_data_dir) + + +def main(): + log_level = logging.DEBUG + logging.basicConfig(level=log_level, + format='%(asctime)s %(filename)s:%(lineno)-3d' + ' %(levelname)s %(message)s', + datefmt='%y%m%d %H:%M:%S') + + option_parser = optparse.OptionParser(usage=USAGE) + option_parser.add_option( + '', '--auto', action='store_true', default=False, + help='Start test automatically.') + option_parser.add_option( + '', '--replay-dir', default=None, + help='Run replay from this directory instead of tools/build/third_party.') + replay_group = optparse.OptionGroup(option_parser, + 'Options for replay.py', 'These options are passed through to replay.py.') + replay_group.add_option( + '', '--record', action='store_true', default=False, + help='Record a new WPR archive.') + replay_group.add_option( # use default that does not require sudo + '', '--dns_forwarding', default=False, action='store_true', + help='Forward DNS requests to the local replay server.') + option_parser.add_option_group(replay_group) + options, args = option_parser.parse_args() + if len(args) != 2: + option_parser.error('Need CHROME_EXE and TEST_NAME.') + return 1 + chrome_exe, test_name = args + + if not os.path.exists(chrome_exe): + print >>sys.stderr, 'Chrome path does not exist:', chrome_exe + return 1 + + chromium_paths = ChromiumPaths( + TEST_NAME=test_name, + TEST_EXE_NAME='webpagereplay_utils') + if os.path.exists(chromium_paths['archives']): + archive_path = chromium_paths['.wpr'] + else: + archive_path = chromium_paths['.wpr_alt'] + if not os.path.exists(archive_path) and not options.record: + print >>sys.stderr, 'Archive does not exist:', archive_path + return 1 + + replay_options = [] + if options.record: + replay_options.append('--record') + if not options.dns_forwarding: + replay_options.append('--no-dns_forwarding') + + if options.replay_dir: + replay_dir = options.replay_dir + else: + replay_dir = chromium_paths['replay'] + wpr = ReplayLauncher(replay_dir, archive_path, + chromium_paths['logs'], replay_options) + try: + wpr.StartServer() + LaunchChromium(chrome_exe, chromium_paths, test_name, + options.dns_forwarding, options.auto) + finally: + wpr.StopServer() + return 0 + +if '__main__' == __name__: + sys.exit(main()) |