1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 // Round a number to the 1's place. 6 function formatNumber(str) { 7 str += ''; 8 if (str == '0') { 9 return 'N/A '; 10 } 11 var x = str.split('.'); 12 var x1 = x[0]; 13 var x2 = x.length > 1 ? '.' + x[1] : ''; 14 var regex = /(\d+)(\d{3})/; 15 while (regex.test(x1)) { 16 x1 = x1.replace(regex, '$1' + ',' + '$2'); 17 } 18 return x1; 19 } 20 21 // Configuration and results are stored globally. 22 window.iterations = 10; 23 window.interval = 200; 24 window.clearConnections = true; 25 window.clearCache = true; 26 window.enableSpdy = false; 27 window.results = {}; 28 window.results.data = new Array(); 29 window.testUrl = "http://www.google.com/"; 30 window.windowId = 0; 31 32 // Constant StatCounter Names 33 var kTCPReadBytes = "tcp.read_bytes"; 34 var kTCPWriteBytes = "tcp.write_bytes"; 35 var kRequestCount = "HttpNetworkTransaction.Count"; 36 var kConnectCount = "tcp.connect"; 37 var kSpdySessionCount = "spdy.sessions"; 38 39 // The list of currently running benchmarks 40 var benchmarks = new Array(); 41 var benchmarkIndex = 0; 42 var benchmarkWindow = 0; 43 44 function addBenchmark(benchmark) { 45 benchmarks.push(benchmark); 46 benchmarkIndex = 0; // Reset the counter when adding benchmarks. 47 } 48 49 // Array Remove - By John Resig (MIT Licensed) 50 Array.prototype.remove = function(from, to) { 51 var rest = this.slice((to || from) + 1 || this.length); 52 this.length = from < 0 ? this.length + from : from; 53 return this.push.apply(this, rest); 54 }; 55 56 function removeBenchmark(benchmark) { 57 var index; 58 for (var index = 0; index < benchmarks.length; ++index) { 59 if (benchmarks[index] == benchmark) { 60 break; 61 } 62 } 63 benchmarks.remove(index); 64 65 // Preserve index ordering when removing from the list. 66 if (index <= benchmarkIndex) { 67 benchmarkIndex--; // Note: it is okay to drop to -1 here. 68 } 69 } 70 71 function benchmarkStillRunning() { 72 for (var index = 0; index < benchmarks.length; ++index) { 73 if (benchmarks[index].isRunning()) { 74 return true; 75 } 76 } 77 return false; 78 } 79 80 function findBenchmark(url) { 81 for (var index = 0; index < benchmarks.length; ++index) { 82 // One common redirection case: if the url ends without a slash and refers 83 // to a directory, it actually would be redirected to the correct one 84 // (with a slash). In this case, the url returned by the JS and the one 85 // stored locally do not match. 86 if ((benchmarks[index].url() == url) || 87 (benchmarks[index].url() + '/' == url)) { 88 return benchmarks[index]; 89 } 90 } 91 return undefined; 92 } 93 94 function nextBenchmark() { 95 benchmarkIndex = (benchmarkIndex + 1) % benchmarks.length; 96 return benchmarks[benchmarkIndex]; 97 } 98 99 function show_options(tabs) { 100 var tab = tabs[0]; 101 if (window.testUrl == "") { 102 window.testUrl = tab.url; 103 } 104 var tabs = chrome.extension.getViews({"type": "tab"}); 105 if (tabs && tabs.length) { 106 // To avoid "Uncaught TypeError: Object Window has no method 107 // 'setUrl' ". Sometimes tabs are not the desired extension tabs. 108 if (tabs[0].$suburl != undefined) { 109 tabs[0].setUrl(testUrl); 110 } 111 var optionsUrl = chrome.extension.getURL("options.html"); 112 chrome.tabs.getAllInWindow(null, function(all) { 113 for (var i = 0; i < all.length; i++) { 114 if (all[i].url == optionsUrl) { 115 chrome.tabs.update(all[i].id, {selected: true}); 116 return; 117 } 118 } 119 }); 120 } else { 121 chrome.tabs.create({"url":"options.html"}); 122 } 123 } 124 125 chrome.browserAction.onClicked.addListener(show_options); 126 127 function Benchmark() { 128 var runCount_ = 0; 129 var count_; 130 var totalTime_; 131 var me_ = this; 132 var current_; 133 var initialRequestCount_; 134 var initialReadBytes_; 135 var initialWriteBytes_; 136 137 // Start a test run 138 this.start = function(url) { 139 // Check if a run is already in progress. 140 if (me_.isRunning()) { 141 return; 142 } 143 144 console.log("Benchmark testing url: " + url); 145 146 // Add this benchmark to the list of benchmarks running. 147 addBenchmark(this); 148 149 runCount_ = window.iterations; 150 count_ = 0; 151 totalTime_ = 0; 152 153 current_ = {}; 154 current_.url = url; 155 current_.timestamp = new Date(); 156 current_.viaSpdy = false; 157 current_.startLoadResults = new Array(); // times to start 158 current_.commitLoadResults = new Array(); // times to commit 159 current_.docLoadResults = new Array(); // times to docload 160 current_.paintResults = new Array(); // times to paint 161 current_.totalResults = new Array(); // times to complete load 162 current_.KbytesRead = new Array(); 163 current_.KbytesWritten = new Array(); 164 current_.readbpsResults = new Array(); 165 current_.writebpsResults = new Array(); 166 current_.totalTime = 0; 167 current_.iterations = 0; 168 current_.requests = new Array(); 169 current_.connects = new Array(); 170 current_.spdySessions = new Array(); 171 current_.domNum = 0; 172 current_.maxDepth = 0; 173 current_.minDepth = 0; 174 current_.avgDepth = 0; 175 }; 176 177 // Is the benchmark currently in progress. 178 this.isRunning = function() { 179 return runCount_ > 0; 180 }; 181 182 // The url which this benchmark is running. 183 this.url = function() { return current_.url; } 184 185 // Called when the test run completes. 186 this.finish = function() { 187 removeBenchmark(this); 188 189 // If we're the last benchmark, close the window. 190 if (benchmarks.length == 0) { 191 chrome.tabs.remove(benchmarkWindow.id); 192 benchmarkWindow = 0; 193 chrome.tabs.query({active: true, currentWindow: true}, show_options); 194 } 195 }; 196 197 // Update the UI after a test run. 198 this.displayResults = function() { 199 var score = 0; 200 if (count_ > 0) { 201 score = totalTime_ / count_; 202 var text = score.toFixed(1) + "ms avg"; 203 chrome.browserAction.setTitle({"title": text}); 204 } 205 if (runCount_) { 206 chrome.browserAction.setBadgeText({"text": "" + runCount_}); 207 chrome.browserAction.setBadgeBackgroundColor({"color": [255, 0, 0, 255]}); 208 } else { 209 chrome.browserAction.setBadgeText({"text": "" + score.toFixed()}); 210 chrome.browserAction.setBadgeBackgroundColor({"color": [0, 255, 0, 255]}); 211 } 212 213 // Reload the page after each run to show immediate results. 214 var tabs = chrome.extension.getViews({"type": "tab"}); 215 if (tabs && tabs.length) { 216 tabs[0].location.reload(true); 217 } 218 }; 219 220 // Called before starting a page load. 221 this.pageStart = function() { 222 initialReadBytes_ = chrome.benchmarking.counter(kTCPReadBytes); 223 initialWriteBytes_ = chrome.benchmarking.counter(kTCPWriteBytes); 224 initialRequestCount_ = chrome.benchmarking.counter(kRequestCount); 225 initialConnectCount_ = chrome.benchmarking.counter(kConnectCount); 226 initialSpdySessionCount_ = chrome.benchmarking.counter(kSpdySessionCount); 227 }; 228 229 this.openNextPage = function() { 230 var benchmark = nextBenchmark(); 231 benchmark.pageStart(); 232 chrome.tabs.create({"url": benchmark.url(),"selected": true}, 233 function(tab) { 234 benchmarkWindow = tab; 235 // script.js only executes on tested pages 236 // not the ones opened by the user. 237 chrome.tabs.executeScript(tab.id, {file: "script.js"}); 238 }); 239 }; 240 241 this.prepareToOpenPage = function() { 242 // After the previous page is closed, this function will apply 243 // any settings needed to prepare for opening a new page. 244 // Note: the previous page must be closed, otherwie, the cache 245 // clearing and connection clearing may not be thorough. 246 247 if (window.clearCache) { 248 chrome.benchmarking.clearCache(); 249 } 250 251 if (window.clearConnections) { 252 chrome.benchmarking.closeConnections(); 253 } 254 255 if (window.enableSpdy) { 256 chrome.benchmarking.enableSpdy(true); 257 } else { 258 chrome.benchmarking.enableSpdy(false); 259 } 260 261 // Go back to the browser so that tasks can run. 262 setTimeout(me_.openNextPage, window.interval); 263 }; 264 265 this.closePage = function() { 266 chrome.tabs.remove(benchmarkWindow.id, function() { 267 me_.prepareToOpenPage(); 268 }); 269 }; 270 271 // Run a single page in the benchmark 272 this.runPage = function() { 273 if (benchmarkWindow) { 274 // To avoid the error "Error during tabs.remove: No tab with id xx" 275 // while debugging, due to user manually closing the benchmark tab. 276 chrome.tabs.getAllInWindow(null, function(all) { 277 for (var i = 0; i < all.length; i++) { 278 if (all[i].id == benchmarkWindow.id) { 279 me_.closePage(); 280 return; 281 }; 282 }; 283 me_.prepareToOpenPage(); 284 }); 285 } else { 286 me_.prepareToOpenPage(); 287 } 288 }; 289 290 // Called when a page finishes loading. 291 this.pageFinished = function(load_times, domNum, depths) { 292 293 // Make sure the content can be fetched via spdy if it is enabled. 294 if (window.enableSpdy && !load_times.wasFetchedViaSpdy) { 295 alert("Can not fetch current url via spdy.\n" + 296 "Ending current test."); 297 me_.finish(); 298 // Move on to next benchmarked pages. 299 if (benchmarks.length > 0) { 300 if (window.clearConnections) { 301 chrome.benchmarking.closeConnections(); 302 } 303 setTimeout(me_.runPage, 100); 304 } 305 return; 306 } 307 308 // If last fetch was via spdy, current fetch should use spdy too. Same 309 // for vise versa. 310 if (current_.iterations > 0 && 311 current_.viaSpdy != load_times.wasFetchedViaSpdy) { 312 alert("Error: viaSpdy for current fetch is different from last fetch!\n" + 313 "Ending current test."); 314 // Current data set is invalid: remove from the result array. 315 var currIndex; 316 currIndex = window.results.data.indexOf(current_, 0); 317 window.results.data.splice(currIndex, 1); 318 me_.displayResults(); 319 me_.finish(); 320 if (benchmarks.length > 0) { 321 if (window.clearConnections) { 322 chrome.benchmarking.closeConnections(); 323 } 324 setTimeout(me_.runPage, 100); 325 } 326 return; 327 } 328 329 var requested = load_times.requestTime; 330 var started = load_times.startLoadTime; 331 var startLoadTime = 332 Math.round((load_times.startLoadTime - requested) * 1000.0); 333 var commitLoadTime = 334 Math.round((load_times.commitLoadTime - started) * 1000.0); 335 var docLoadTime = 336 Math.round((load_times.finishDocumentLoadTime - started) * 1000.0); 337 var paintTime = 338 Math.round((load_times.firstPaintTime - started) * 1000.0); 339 var totalTime = 340 Math.round((load_times.finishLoadTime - started) * 1000.0); 341 var firstPaintAfterLoadTime = 342 Math.round((load_times.firstPaintAfterLoadTime - started) * 1000.0); 343 344 if (paintTime < 0) { 345 // If the user navigates away from the test while it is running, 346 // paint may not occur. Also, some lightweight pages, such as the 347 // google home page, never trigger a paint measurement via the chrome 348 // page load timer. 349 // In this case, the time-to-first paint is effectively the same as the 350 // time to onLoad(). 351 paintTime = totalTime; 352 } 353 354 // For our toolbar counters 355 totalTime_ += totalTime; 356 count_++; 357 358 // Get the index of current benchmarked page in the result array. 359 var currIndex; 360 currIndex = window.results.data.indexOf(current_, 0); 361 362 // Record the result 363 current_.viaSpdy = load_times.wasFetchedViaSpdy; 364 current_.iterations++; 365 current_.startLoadResults.push(startLoadTime); 366 current_.commitLoadResults.push(commitLoadTime); 367 current_.docLoadResults.push(docLoadTime); 368 current_.paintResults.push(paintTime); 369 current_.totalResults.push(totalTime); 370 var bytesRead = chrome.benchmarking.counter(kTCPReadBytes) - 371 initialReadBytes_; 372 var bytesWrite = chrome.benchmarking.counter(kTCPWriteBytes) - 373 initialWriteBytes_; 374 current_.KbytesRead.push(bytesRead / 1024); 375 current_.KbytesWritten.push(bytesWrite / 1024); 376 current_.readbpsResults.push(bytesRead * 8 / totalTime); 377 current_.writebpsResults.push(bytesWrite * 8 / totalTime); 378 current_.requests.push(chrome.benchmarking.counter(kRequestCount) - 379 initialRequestCount_); 380 current_.connects.push(chrome.benchmarking.counter(kConnectCount) - 381 initialConnectCount_); 382 current_.spdySessions.push(chrome.benchmarking.counter(kSpdySessionCount) - 383 initialSpdySessionCount_); 384 current_.totalTime += totalTime; 385 current_.domNum = domNum; 386 current_.maxDepth = depths[0]; 387 current_.minDepth = depths[1]; 388 current_.avgDepth = depths[2]; 389 390 // Insert or update the result data after each run. 391 if (currIndex == -1) { 392 window.results.data.push(current_); 393 } else { 394 window.results.data[currIndex] = current_; 395 } 396 397 if (--runCount_ == 0) { 398 me_.finish(); 399 } 400 401 // If there are more tests, schedule them 402 if (runCount_ > 0 || benchmarks.length > 0) { 403 if (window.clearConnections) { 404 chrome.benchmarking.closeConnections(); 405 } 406 setTimeout(me_.runPage, 100); 407 } 408 409 // Update the UI 410 me_.displayResults(); 411 }; 412 } 413 414 chrome.runtime.onConnect.addListener(function(port) { 415 port.onMessage.addListener(function(data) { 416 if (data.message == "load") { 417 var benchmark = findBenchmark(data.url); 418 if (benchmark == undefined && benchmarkStillRunning()) { 419 alert("Error: Loaded url(" + data.url + ") is not the same as what " + 420 "you set in url box. This could happen if the request is " + 421 "redirected. Please use the redirected url for testing."); 422 // Stop the test here. 423 benchmarks = []; 424 } 425 if (benchmark != undefined && benchmark.isRunning()) { 426 benchmark.pageFinished(data.values, data.domNum, data.domDepths); 427 } 428 } 429 }); 430 }); 431 432 function run() { 433 if (window.clearCache) { 434 // Show a warning if we will try to clear the cache between runs 435 // but will also be reusing the same WebKit instance (i.e. Chrome 436 // is in single-process mode) because the WebKit cache might not get 437 // completely cleared between runs. 438 if (chrome.benchmarking.isSingleProcess()) { 439 alert("Warning: the WebKit cache may not be cleared correctly " + 440 "between runs because Chrome is running in single-process mode."); 441 } 442 } 443 benchmarks = []; 444 var urls = testUrl.split(","); 445 for (var i = 0; i < urls.length; i++) { 446 447 // Remove extra space at the beginning or end of a url. 448 urls[i] = removeSpace(urls[i]); 449 450 // Alert about and ignore blank page which does not get loaded. 451 if (urls[i] == "about:blank") { 452 alert("blank page loaded!"); 453 } else if (!checkScheme(urls[i])) { 454 // Alert about url that is not in scheme http:// or https://. 455 alert(urls[i] + " does not start with http:// or https://."); 456 } else { 457 var benchmark = new Benchmark(); 458 benchmark.start(urls[i]); // XXXMB - move to constructor 459 } 460 } 461 benchmarks[0].runPage(); 462 } 463 464 // Remove extra whitespace in the beginning or end of a url string. 465 function removeSpace(url) { 466 var tempUrl = url; 467 while (tempUrl.charAt(tempUrl.length-1) == " ") { 468 tempUrl = tempUrl.substring(0, tempUrl.length-1); 469 }; 470 while (tempUrl.charAt(0) == " ") { 471 tempUrl = tempUrl.substring(1, tempUrl.length); 472 }; 473 return tempUrl; 474 } 475 476 // Check whether a Url starts with http:// or https://. 477 function checkScheme(url) { 478 var httpStr = "http://"; 479 var httpsStr = "https://"; 480 var urlSubStr1 = url.substring(0, httpStr.length); 481 var urlSubStr2 = url.substring(0, httpsStr.length); 482 483 if ( (urlSubStr1 == httpStr) || (urlSubStr2 == httpsStr) ) { 484 return true; 485 } 486 return false; 487 } 488 489 // Run at startup 490 chrome.windows.getCurrent(function(currentWindow) { 491 window.windowId = currentWindow.id; 492 }); 493