Merge from Chromium at DEPS revision r190564

This commit was generated by merge_to_master.py.

Change-Id: Ie7b06f551ab581fd65e508d8ab5e40eda59c14b2
diff --git a/Tools/TestResultServer/static-dashboards/flakiness_dashboard.js b/Tools/TestResultServer/static-dashboards/flakiness_dashboard.js
index 7589e34..510ecd5 100644
--- a/Tools/TestResultServer/static-dashboards/flakiness_dashboard.js
+++ b/Tools/TestResultServer/static-dashboards/flakiness_dashboard.js
@@ -134,28 +134,29 @@
     'platform/chromium/virtual/gpu/canvas/philip': 'canvas/philip'
 };
 
+var resourceLoader;
+
 //////////////////////////////////////////////////////////////////////////////
 // Methods and objects from dashboard_base.js to override.
 //////////////////////////////////////////////////////////////////////////////
 function generatePage()
 {
-    if (g_crossDashboardState.useTestData)
+    if (g_history.crossDashboardState.useTestData)
         return;
 
-    updateDefaultBuilderState();
     document.body.innerHTML = '<div id="loading-ui">LOADING...</div>';
-    showErrors();
+    resourceLoader.showErrors();
 
     // tests expands to all tests that match the CSV list.
     // result expands to all tests that ever have the given result
-    if (g_currentState.tests || g_currentState.result)
+    if (g_history.dashboardSpecificState.tests || g_history.dashboardSpecificState.result)
         generatePageForIndividualTests(individualTests());
-    else if (g_currentState.expectationsUpdate)
+    else if (g_history.dashboardSpecificState.expectationsUpdate)
         generatePageForExpectationsUpdate();
     else
-        generatePageForBuilder(g_currentState.builder);
+        generatePageForBuilder(g_history.dashboardSpecificState.builder || currentBuilderGroup().defaultBuilder());
 
-    for (var builder in g_builders)
+    for (var builder in currentBuilders())
         processTestResultsForBuilderAsync(builder);
 
     postHeightChangedMessage();
@@ -165,15 +166,15 @@
 {
     switch(key) {
     case 'tests':
-        validateParameter(g_currentState, key, value,
+        history.validateParameter(g_history.dashboardSpecificState, key, value,
             function() {
-                return isValidName(value);
+                return string.isValidName(value);
             });
         return true;
 
     case 'result':
         value = value.toUpperCase();
-        validateParameter(g_currentState, key, value,
+        history.validateParameter(g_history.dashboardSpecificState, key, value,
             function() {
                 for (var result in LAYOUT_TEST_EXPECTATIONS_MAP_) {
                     if (value == LAYOUT_TEST_EXPECTATIONS_MAP_[result])
@@ -184,17 +185,18 @@
         return true;
 
     case 'builder':
-        validateParameter(g_currentState, key, value,
+        history.validateParameter(g_history.dashboardSpecificState, key, value,
             function() {
-                return value in g_builders;
+                return value in currentBuilders();
             });
+
         return true;
 
     case 'sortColumn':
-        validateParameter(g_currentState, key, value,
+        history.validateParameter(g_history.dashboardSpecificState, key, value,
             function() {
                 // Get all possible headers since the actual used set of headers
-                // depends on the values in g_currentState, which are currently being set.
+                // depends on the values in g_history.dashboardSpecificState, which are currently being set.
                 var headers = tableHeaders(true);
                 for (var i = 0; i < headers.length; i++) {
                     if (value == sortColumnFromTableHeader(headers[i]))
@@ -205,7 +207,7 @@
         return true;
 
     case 'sortOrder':
-        validateParameter(g_currentState, key, value,
+        history.validateParameter(g_history.dashboardSpecificState, key, value,
             function() {
                 return value == FORWARD || value == BACKWARD;
             });
@@ -214,7 +216,7 @@
     case 'resultsHeight':
     case 'updateIndex':
     case 'revision':
-        validateParameter(g_currentState, key, Number(value),
+        history.validateParameter(g_history.dashboardSpecificState, key, Number(value),
             function() {
                 return value.match(/^\d+$/);
             });
@@ -232,7 +234,7 @@
     case 'showUnexpectedPasses':
     case 'showWontFixSkip':
     case 'expectationsUpdate':
-        g_currentState[key] = value == 'true';
+        g_history.dashboardSpecificState[key] = value == 'true';
         return true;
 
     default:
@@ -248,18 +250,25 @@
     showLargeExpectations: false,
     legacyExpectationsSemantics: true,
     showChrome: true,
-    showCorrectExpectations: !isLayoutTestResults(),
-    showWrongExpectations: !isLayoutTestResults(),
-    showWontFixSkip: !isLayoutTestResults(),
-    showSlow: !isLayoutTestResults(),
-    showSkipped: !isLayoutTestResults(),
-    showUnexpectedPasses: !isLayoutTestResults(),
+    showCorrectExpectations: !g_history.isLayoutTestResults(),
+    showWrongExpectations: !g_history.isLayoutTestResults(),
+    showWontFixSkip: !g_history.isLayoutTestResults(),
+    showSlow: !g_history.isLayoutTestResults(),
+    showSkipped: !g_history.isLayoutTestResults(),
+    showUnexpectedPasses: !g_history.isLayoutTestResults(),
     expectationsUpdate: false,
     updateIndex: 0,
     resultsHeight: 300,
     revision: null,
     tests: '',
     result: '',
+    builder: null
+};
+
+DB_SPECIFIC_INVALIDATING_PARAMETERS = {
+    'tests' : 'builder',
+    'testType': 'builder',
+    'group': 'builder'
 };
 
 //////////////////////////////////////////////////////////////////////////////
@@ -325,56 +334,56 @@
 function matchingElement(stringToMatch, elementsMap)
 {
     for (var element in elementsMap) {
-        if (stringContains(stringToMatch, elementsMap[element]))
+        if (string.contains(stringToMatch, elementsMap[element]))
             return element;
     }
 }
 
 function determineWKPlatform(builderName, basePlatform)
 {
-    var isWK2Builder = stringContains(builderName, 'WK2') || stringContains(builderName, 'WEBKIT2');
+    var isWK2Builder = string.contains(builderName, 'WK2') || string.contains(builderName, 'WEBKIT2');
     return basePlatform + (isWK2Builder ? '_WK2' : '_WK1');
 }
 
 function nonChromiumPlatform(builderNameUpperCase)
 {
-    if (stringContains(builderNameUpperCase, 'WINDOWS 7'))
+    if (string.contains(builderNameUpperCase, 'WINDOWS 7'))
         return 'APPLE_WIN_WIN7';
-    if (stringContains(builderNameUpperCase, 'WINDOWS XP'))
+    if (string.contains(builderNameUpperCase, 'WINDOWS XP'))
         return 'APPLE_WIN_XP';
-    if (stringContains(builderNameUpperCase, 'QT LINUX'))
+    if (string.contains(builderNameUpperCase, 'QT LINUX'))
         return 'QT_LINUX';
 
-    if (stringContains(builderNameUpperCase, 'LION'))
+    if (string.contains(builderNameUpperCase, 'LION'))
         return determineWKPlatform(builderNameUpperCase, 'APPLE_MAC_LION');
-    if (stringContains(builderNameUpperCase, 'SNOWLEOPARD'))
+    if (string.contains(builderNameUpperCase, 'SNOWLEOPARD'))
         return determineWKPlatform(builderNameUpperCase, 'APPLE_MAC_SNOWLEOPARD');
-    if (stringContains(builderNameUpperCase, 'GTK LINUX'))
+    if (string.contains(builderNameUpperCase, 'GTK LINUX'))
         return determineWKPlatform(builderNameUpperCase, 'GTK_LINUX');
-    if (stringContains(builderNameUpperCase, 'EFL'))
+    if (string.contains(builderNameUpperCase, 'EFL'))
         return determineWKPlatform(builderNameUpperCase, 'EFL_LINUX');
 }
 
 function chromiumPlatform(builderNameUpperCase)
 {
-    if (stringContains(builderNameUpperCase, 'MAC')) {
-        if (stringContains(builderNameUpperCase, '10.7'))
+    if (string.contains(builderNameUpperCase, 'MAC')) {
+        if (string.contains(builderNameUpperCase, '10.7'))
             return 'CHROMIUM_LION';
         // The webkit.org 'Chromium Mac Release (Tests)' bot runs SnowLeopard.
         return 'CHROMIUM_SNOWLEOPARD';
     }
-    if (stringContains(builderNameUpperCase, 'WIN7'))
+    if (string.contains(builderNameUpperCase, 'WIN7'))
         return 'CHROMIUM_WIN7';
-    if (stringContains(builderNameUpperCase, 'VISTA'))
+    if (string.contains(builderNameUpperCase, 'VISTA'))
         return 'CHROMIUM_VISTA';
-    if (stringContains(builderNameUpperCase, 'WIN') || stringContains(builderNameUpperCase, 'XP'))
+    if (string.contains(builderNameUpperCase, 'WIN') || string.contains(builderNameUpperCase, 'XP'))
         return 'CHROMIUM_XP';
-    if (stringContains(builderNameUpperCase, 'LINUX'))
+    if (string.contains(builderNameUpperCase, 'LINUX'))
         return 'CHROMIUM_LUCID';
-    if (stringContains(builderNameUpperCase, 'ANDROID'))
+    if (string.contains(builderNameUpperCase, 'ANDROID'))
         return 'CHROMIUM_ANDROID';
     // The interactive bot is XP, but doesn't have an OS in it's name.
-    if (stringContains(builderNameUpperCase, 'INTERACTIVE'))
+    if (string.contains(builderNameUpperCase, 'INTERACTIVE'))
         return 'CHROMIUM_XP';
 }
 
@@ -385,7 +394,7 @@
         var builderNameUpperCase = builderName.toUpperCase();
         
         var platform = '';
-        if (isLayoutTestResults() && g_crossDashboardState.group == '@ToT - webkit.org' && !stringContains(builderNameUpperCase, 'CHROMIUM'))
+        if (g_history.isLayoutTestResults() && currentBuilderGroupName() == '@ToT - webkit.org' && !string.contains(builderNameUpperCase, 'CHROMIUM'))
             platform = nonChromiumPlatform(builderNameUpperCase);
         else
             platform = chromiumPlatform(builderNameUpperCase);
@@ -393,7 +402,7 @@
         if (!platform)
             console.error('Could not resolve platform for builder: ' + builderName);
 
-        var buildType = stringContains(builderNameUpperCase, 'DBG') || stringContains(builderNameUpperCase, 'DEBUG') ? 'DEBUG' : 'RELEASE';
+        var buildType = string.contains(builderNameUpperCase, 'DBG') || string.contains(builderNameUpperCase, 'DEBUG') ? 'DEBUG' : 'RELEASE';
         g_perBuilderPlatformAndBuildType[builderName] = {platform: platform, buildType: buildType};
     }
     return g_perBuilderPlatformAndBuildType[builderName];
@@ -479,7 +488,7 @@
 function getAllTestsTrie()
 {
     if (!g_allTestsTrie)
-        g_allTestsTrie = new TestTrie(g_builders, g_resultsByBuilder);
+        g_allTestsTrie = new TestTrie(currentBuilders(), g_resultsByBuilder);
 
     return g_allTestsTrie;
 }
@@ -489,10 +498,10 @@
 // tests in the directory.
 function individualTests()
 {
-    if (g_currentState.result)
-        return allTestsWithResult(g_currentState.result);
+    if (g_history.dashboardSpecificState.result)
+        return allTestsWithResult(g_history.dashboardSpecificState.result);
 
-    if (!g_currentState.tests)
+    if (!g_history.dashboardSpecificState.tests)
         return [];
 
     return individualTestsForSubstringList();
@@ -501,11 +510,11 @@
 function substringList()
 {
     // Convert windows slashes to unix slashes.
-    var tests = g_currentState.tests.replace(/\\/g, '/');
-    var separator = stringContains(tests, ' ') ? ' ' : ',';
+    var tests = g_history.dashboardSpecificState.tests.replace(/\\/g, '/');
+    var separator = string.contains(tests, ' ') ? ' ' : ',';
     var testList = tests.split(separator);
 
-    if (isLayoutTestResults())
+    if (g_history.isLayoutTestResults())
         return testList;
 
     var testListWithoutModifiers = [];
@@ -534,7 +543,7 @@
 
         var hasAnyMatches = false;
         getAllTestsTrie().forEach(function(triePath) {
-            if (caseInsensitiveContains(triePath, path)) {
+            if (string.caseInsensitiveContains(triePath, path)) {
                 testsMap[triePath] = 1;
                 hasAnyMatches = true;
             }
@@ -610,7 +619,7 @@
 {
     if (!g_allTestsByPlatformAndBuildType[platform][buildType]) {
         var tests = {};
-        for (var thisBuilder in g_builders) {
+        for (var thisBuilder in currentBuilders()) {
             var thisBuilderBuildInfo = platformAndBuildType(thisBuilder);
             if (thisBuilderBuildInfo.buildType == buildType && thisBuilderBuildInfo.platform == platform) {
                 addTestsForBuilder(thisBuilder, tests);
@@ -642,7 +651,7 @@
         return {bugs: '', modifiers: modifiers};
     for (var j = 0; j < bugs.length; j++)
         modifiers = modifiers.replace(bugs[j], '');
-    return {bugs: bugs.join(' '), modifiers: collapseWhitespace(trimString(modifiers))};
+    return {bugs: bugs.join(' '), modifiers: string.collapseWhitespace(string.trimString(modifiers))};
 }
 
 function populateExpectationsData(resultsObject)
@@ -656,7 +665,7 @@
     var filteredModifiers = filterBugs(expectations.modifiers);
     resultsObject.modifiers = filteredModifiers.modifiers;
     resultsObject.bugs = filteredModifiers.bugs;
-    resultsObject.isWontFixSkip = stringContains(expectations.modifiers, 'WONTFIX') || stringContains(expectations.modifiers, 'SKIP'); 
+    resultsObject.isWontFixSkip = string.contains(expectations.modifiers, 'WONTFIX') || string.contains(expectations.modifiers, 'SKIP'); 
 }
 
 function platformObjectForName(platformName)
@@ -685,8 +694,8 @@
     var expectations = [];
     var lines = data.split('\n');
     lines.forEach(function(line) {
-        line = trimString(line);
-        if (!line || startsWith(line, '#'))
+        line = string.trimString(line);
+        if (!line || string.startsWith(line, '#'))
             return;
 
         // This code mimics _tokenize_line_using_new_format() in
@@ -895,8 +904,8 @@
 
         // Test has expectations, but no result in the builders results.
         // This means it's either SKIP or passes on all builds.
-        if (!allTestsForPlatformAndBuildType[test] && !stringContains(expectations.modifiers, 'WONTFIX')) {
-            if (stringContains(expectations.modifiers, 'SKIP'))
+        if (!allTestsForPlatformAndBuildType[test] && !string.contains(expectations.modifiers, 'WONTFIX')) {
+            if (string.contains(expectations.modifiers, 'SKIP'))
                 skipped.push(test);
             else if (!expectations.expectations.match(/^\s*PASS\s*$/)) {
                 // Don't show tests expected to always pass. This is used in ways like
@@ -919,7 +928,7 @@
 
 function processTestRunsForAllBuilders()
 {
-    for (var builder in g_builders)
+    for (var builder in currentBuilders())
         processTestRunsForBuilder(builder);
 }
 
@@ -935,8 +944,7 @@
     }
 
     processExpectations();
-    var start = Date.now();
-
+   
     var buildInfo = platformAndBuildType(builderName);
     var platform = buildInfo.platform;
     var buildType = buildInfo.buildType;
@@ -989,7 +997,6 @@
     }
 
     g_perBuilderFailures[builderName] = failures;
-    logTime('processTestRunsForBuilder: ' + builderName, start);
 }
 
 function processMissingAndExtraExpectations(resultsForTest)
@@ -1048,14 +1055,14 @@
     var missingExpectations = [];
     var extraExpectations = [];
 
-    if (isLayoutTestResults()) {
+    if (g_history.isLayoutTestResults()) {
         var expectationsArray = resultsForTest.expectations ? resultsForTest.expectations.split(' ') : [];
         extraExpectations = expectationsArray.filter(
             function(element) {
                 // FIXME: Once all the FAIL lines are removed from
                 // TestExpectations, delete all the legacyExpectationsSemantics
                 // code.
-                if (g_currentState.legacyExpectationsSemantics) {
+                if (g_history.dashboardSpecificState.legacyExpectationsSemantics) {
                     if (element == 'FAIL') {
                         for (var i = 0; i < FAIL_RESULTS.length; i++) {
                             if (resultsMap[FAIL_RESULTS[i]])
@@ -1065,7 +1072,7 @@
                     }
                 }
 
-                return element && !resultsMap[element] && !stringContains(element, 'BUG');
+                return element && !resultsMap[element] && !string.contains(element, 'BUG');
             });
 
         for (var result in resultsMap) {
@@ -1076,7 +1083,7 @@
                 // FIXME: Once all the FAIL lines are removed from
                 // TestExpectations, delete all the legacyExpectationsSemantics
                 // code.
-                if (g_currentState.legacyExpectationsSemantics) {
+                if (g_history.dashboardSpecificState.legacyExpectationsSemantics) {
                     if (expectation == 'FAIL') {
                         for (var j = 0; j < FAIL_RESULTS.length; j++) {
                             if (result == FAIL_RESULTS[j]) {
@@ -1105,9 +1112,9 @@
         // hundred runs. It's not worth the manual maintenance effort.
         // Also, if a test times out, then it should not be marked as slow.
         var minTimeForNeedsSlow = isDebug(resultsForTest.builder) ? 2 : 1;
-        if (isSlowTest(resultsForTest) && !resultsMap['TIMEOUT'] && (!resultsForTest.modifiers || !stringContains(resultsForTest.modifiers, 'SLOW')))
+        if (isSlowTest(resultsForTest) && !resultsMap['TIMEOUT'] && (!resultsForTest.modifiers || !string.contains(resultsForTest.modifiers, 'SLOW')))
             missingExpectations.push('SLOW');
-        else if (isFastTest(resultsForTest) && resultsForTest.modifiers && stringContains(resultsForTest.modifiers, 'SLOW'))
+        else if (isFastTest(resultsForTest) && resultsForTest.modifiers && string.contains(resultsForTest.modifiers, 'SLOW'))
             extraExpectations.push('SLOW');
 
         // If there are no missing results or modifiers besides build
@@ -1148,7 +1155,7 @@
     return '<a href="' + url + '" target="_blank">' + text + '</a>';
 }
 
-// FIXME: replaced with chromiumRevisionLink/webKitRevisionLink
+// FIXME: replaced with ui.html.chromiumRevisionLink/ui.html.webKitRevisionLink
 function createBlameListHTML(revisions, index, urlBase, separator, repo)
 {
     var thisRevision = revisions[index];
@@ -1199,7 +1206,7 @@
 // Returns the path to the failure log for this non-webkit test.
 function pathToFailureLog(testName)
 {
-    return '/steps/' + g_crossDashboardState.testType + '/logs/' + testName.split('.')[1]
+    return '/steps/' + g_history.crossDashboardState.testType + '/logs/' + testName.split('.')[1]
 }
 
 function showPopupForBuild(e, builder, index, opt_testName)
@@ -1223,9 +1230,9 @@
             'WebKit') +
         '</li>';
 
-    if (master == WEBKIT_BUILDER_MASTER) {
+    if (master.name == WEBKIT_BUILDER_MASTER) {
         var revision = g_resultsByBuilder[builder].webkitRevision[index];
-        html += '<li><span class=link onclick="setQueryParameter(\'revision\',' +
+        html += '<li><span class=link onclick="g_history.setQueryParameter(\'revision\',' +
             revision + ')">Show results for WebKit r' + revision +
             '</span></li>';
     } else {
@@ -1235,17 +1242,17 @@
             '</li>';
 
         var chromeRevision = g_resultsByBuilder[builder].chromeRevision[index];
-        if (chromeRevision && isLayoutTestResults()) {
-            html += '<li><a href="' + TEST_RESULTS_BASE_PATH + g_builders[builder] +
+        if (chromeRevision && g_history.isLayoutTestResults()) {
+            html += '<li><a href="' + TEST_RESULTS_BASE_PATH + currentBuilders()[builder] +
                 '/' + chromeRevision + '/layout-test-results.zip">layout-test-results.zip</a></li>';
         }
     }
 
-    if (!isLayoutTestResults() && opt_testName && isFailure(builder, opt_testName, index))
+    if (!g_history.isLayoutTestResults() && opt_testName && isFailure(builder, opt_testName, index))
         html += '<li>' + linkHTMLToOpenWindow(buildBasePath + pathToFailureLog(opt_testName), 'Failure log') + '</li>';
 
     html += '</ul>';
-    showPopup(e.target, html);
+    ui.popup.show(e.target, html);
 }
 
 function htmlForTestResults(test)
@@ -1291,7 +1298,7 @@
         var extraClassNames = '';
         var webkitRevision = g_resultsByBuilder[builder].webkitRevision;
         var isWebkitMerge = webkitRevision[i + 1] && webkitRevision[i] != webkitRevision[i + 1];
-        if (isWebkitMerge && master != WEBKIT_BUILDER_MASTER)
+        if (isWebkitMerge && master.name != WEBKIT_BUILDER_MASTER)
             extraClassNames += ' merge';
 
         html += '<td title="' + (resultString || 'NO DATA') + '. Click for more info." class="results ' + currentResult +
@@ -1312,23 +1319,23 @@
     if (tests.length || skippedPaths.length) {
         var buildInfo = platformAndBuildType(builder);
         html += '<h2 style="display:inline-block">Expectations for ' + buildInfo.platform + '-' + buildInfo.buildType + '</h2> ';
-        if (!g_currentState.showUnexpectedPasses && tests.length)
+        if (!g_history.dashboardSpecificState.showUnexpectedPasses && tests.length)
             html += showUnexpectedPassesLink;
         html += ' ';
-        if (!g_currentState.showSkipped && skippedPaths.length)
+        if (!g_history.dashboardSpecificState.showSkipped && skippedPaths.length)
             html += showSkippedLink;
     }
 
     var open = '<div onclick="selectContents(this)">';
 
-    if (g_currentState.showUnexpectedPasses && tests.length) {
+    if (g_history.dashboardSpecificState.showUnexpectedPasses && tests.length) {
         html += '<div id="passing-tests">' + showUnexpectedPassesLink;
         for (var i = 0; i < tests.length; i++)
             html += open + tests[i].test + '</div>';
         html += '</div>';
     }
 
-    if (g_currentState.showSkipped && skippedPaths.length)
+    if (g_history.dashboardSpecificState.showSkipped && skippedPaths.length)
         html += '<div id="skipped-tests">' + showSkippedLink + open + skippedPaths.join('</div>' + open) + '</div></div>';
     return html + '<br>';
 }
@@ -1337,18 +1344,18 @@
 function shouldHideTest(testResult)
 {
     if (testResult.isWontFixSkip)
-        return !g_currentState.showWontFixSkip;
+        return !g_history.dashboardSpecificState.showWontFixSkip;
 
     if (testResult.isFlaky)
-        return !g_currentState.showFlaky;
+        return !g_history.dashboardSpecificState.showFlaky;
 
     if (isSlowTest(testResult))
-        return !g_currentState.showSlow;
+        return !g_history.dashboardSpecificState.showSlow;
 
     if (testResult.meetsExpectations)
-        return !g_currentState.showCorrectExpectations;
+        return !g_history.dashboardSpecificState.showCorrectExpectations;
 
-    return !g_currentState.showWrongExpectations;
+    return !g_history.dashboardSpecificState.showWrongExpectations;
 }
 
 // Sets the browser's selection to the element's contents.
@@ -1372,7 +1379,7 @@
 
 function isCrossBuilderView()
 {
-    return g_currentState.tests || g_currentState.result || g_currentState.expectationsUpdate;
+    return g_history.dashboardSpecificState.tests || g_history.dashboardSpecificState.result || g_history.dashboardSpecificState.expectationsUpdate;
 }
 
 function tableHeaders(opt_getAll)
@@ -1384,7 +1391,7 @@
     if (!isCrossBuilderView() || opt_getAll)
         headers.push('test');
 
-    if (isLayoutTestResults() || opt_getAll)
+    if (g_history.isLayoutTestResults() || opt_getAll)
         headers.push('bugs', 'modifiers', 'expectations');
 
     headers.push('slowest run', 'flakiness (numbers are runtimes in seconds)');
@@ -1405,23 +1412,23 @@
     var html = '';
     for (var i = 0; i < headers.length; i++) {
         var header = headers[i];
-        if (startsWith(header, 'test') || startsWith(header, 'builder')) {
+        if (string.startsWith(header, 'test') || string.startsWith(header, 'builder')) {
             // If isCrossBuilderView() is true, we're just viewing a single test
             // with results for many builders, so the first column is builder names
             // instead of test paths.
             var testCellClassName = 'test-link' + (isCrossBuilderView() ? ' builder-name' : '');
-            var testCellHTML = isCrossBuilderView() ? test.builder : '<span class="link" onclick="setQueryParameter(\'tests\',\'' + test.test +'\');">' + test.test + '</span>';
+            var testCellHTML = isCrossBuilderView() ? test.builder : '<span class="link" onclick="g_history.setQueryParameter(\'tests\',\'' + test.test +'\');">' + test.test + '</span>';
 
             html += '<tr><td class="' + testCellClassName + '">' + testCellHTML;
-        } else if (startsWith(header, 'bugs'))
+        } else if (string.startsWith(header, 'bugs'))
             html += '<td class=options-container>' + (test.bugs ? htmlForBugs(test.bugs) : createBugHTML(test));
-        else if (startsWith(header, 'modifiers'))
+        else if (string.startsWith(header, 'modifiers'))
             html += '<td class=options-container>' + test.modifiers;
-        else if (startsWith(header, 'expectations'))
+        else if (string.startsWith(header, 'expectations'))
             html += '<td class=options-container>' + test.expectations;
-        else if (startsWith(header, 'slowest'))
+        else if (string.startsWith(header, 'slowest'))
             html += '<td>' + (test.slowestTime ? test.slowestTime + 's' : '');
-        else if (startsWith(header, 'flakiness'))
+        else if (string.startsWith(header, 'flakiness'))
             html += htmlForTestResults(test);
     }
     return html;
@@ -1436,8 +1443,8 @@
 {
     // Use the first word of the header title as the sortkey
     var thisSortValue = sortColumnFromTableHeader(headerName);
-    var arrowHTML = thisSortValue == g_currentState.sortColumn ?
-        '<span class=' + g_currentState.sortOrder + '>' + (g_currentState.sortOrder == FORWARD ? '&uarr;' : '&darr;' ) + '</span>' : '';
+    var arrowHTML = thisSortValue == g_history.dashboardSpecificState.sortColumn ?
+        '<span class=' + g_history.dashboardSpecificState.sortOrder + '>' + (g_history.dashboardSpecificState.sortOrder == FORWARD ? '&uarr;' : '&darr;' ) + '</span>' : '';
     return '<th sortValue=' + thisSortValue +
         // Extend last th through all the rest of the columns.
         (opt_fillColSpan ? ' colspan=10000' : '') +
@@ -1462,13 +1469,11 @@
 
 function appendHTML(html)
 {
-    var startTime = Date.now();
     // InnerHTML to a div that's not in the document. This is
     // ~300ms faster in Safari 4 and Chrome 4 on mac.
     var div = document.createElement('div');
     div.innerHTML = html;
     document.body.appendChild(div);
-    logTime('Time to innerHTML', startTime);
     postHeightChangedMessage();
 }
 
@@ -1515,12 +1520,12 @@
 
     var sort = 'sortColumn';
     var orderKey = 'sortOrder';
-    if (sortValue == g_currentState[sort] && g_currentState[orderKey] == FORWARD)
+    if (sortValue == g_history.dashboardSpecificState[sort] && g_history.dashboardSpecificState[orderKey] == FORWARD)
         order = BACKWARD;
     else
         order = FORWARD;
 
-    setQueryParameter(sort, sortValue, orderKey, order);
+    g_history.setQueryParameter(sort, sortValue, orderKey, order);
 }
 
 function sortTests(tests, column, order)
@@ -1575,7 +1580,7 @@
 {
     var modifiers = modifierString.split(' ');;
     return modifiers.filter(function(modifier) {
-        if (modifier in BUILD_TYPES || startsWith(modifier, 'BUG'))
+        if (modifier in BUILD_TYPES || string.startsWith(modifier, 'BUG'))
             return false;
 
         var matchesPlatformOrUnion = false;
@@ -1604,8 +1609,8 @@
 function generatePageForExpectationsUpdate()
 {
     // Always show all runs when auto-updating expectations.
-    if (!g_crossDashboardState.showAllRuns)
-        setQueryParameter('showAllRuns', true);
+    if (!g_history.crossDashboardState.showAllRuns)
+        g_history.setQueryParameter('showAllRuns', true);
 
     processTestRunsForAllBuilders();
     var testsNeedingUpdate = {};
@@ -1626,7 +1631,7 @@
         }
     }
 
-    for (var builder in g_builders) {
+    for (var builder in currentBuilders()) {
         var tests = g_perBuilderWithExpectationsButNoFailures[builder]
         for (var i = 0; i < tests.length; i++) {
             // Anything extra in this case is what is listed in expectations
@@ -1651,13 +1656,13 @@
 // @param {Array.<string>} keys Keys into the testNeedingUpdate object.
 function showUpdateInfoForTest(testsNeedingUpdate, keys)
 {
-    var test = keys[g_currentState.updateIndex];
+    var test = keys[g_history.dashboardSpecificState.updateIndex];
     document.body.innerHTML = '';
 
     // FIXME: Make this DOM creation less verbose.
     var index = document.createElement('div');
     index.style.cssFloat = 'right';
-    index.textContent = (g_currentState.updateIndex + 1) + ' of ' + keys.length + ' tests';
+    index.textContent = (g_history.dashboardSpecificState.updateIndex + 1) + ' of ' + keys.length + ' tests';
     document.body.appendChild(index);
 
     var buttonRegion = document.createElement('div');
@@ -1672,7 +1677,7 @@
     previousBtn.value = 'previous';
     previousBtn.addEventListener('click',
         function() {
-          setUpdateIndex(g_currentState.updateIndex - 1, testsNeedingUpdate, keys);
+          setUpdateIndex(g_history.dashboardSpecificState.updateIndex - 1, testsNeedingUpdate, keys);
         },
         false);
     buttonRegion.appendChild(previousBtn);
@@ -1734,7 +1739,7 @@
 // @param {Array.<string>} keys Keys into the testNeedingUpdate object.
 function handleUpdate(testsNeedingUpdate, keys)
 {
-    var test = keys[g_currentState.updateIndex];
+    var test = keys[g_history.dashboardSpecificState.updateIndex];
     var updates = testsNeedingUpdate[test];
     for (var builder in updates) {
         // Add included tests, and delete excluded tests if
@@ -1758,7 +1763,7 @@
 // @param {Array.<string>} keys Keys into the testNeedingUpdate object.
 function nextUpdate(testsNeedingUpdate, keys)
 {
-    setUpdateIndex(g_currentState.updateIndex + 1, testsNeedingUpdate, keys);
+    setUpdateIndex(g_history.dashboardSpecificState.updateIndex + 1, testsNeedingUpdate, keys);
 }
 
 
@@ -1773,7 +1778,7 @@
         newIndex = keys.length - 1;
     else if (newIndex == keys.length)
         newIndex = 0;
-    setQueryParameter("updateIndex", newIndex);
+    g_history.setQueryParameter("updateIndex", newIndex);
     showUpdateInfoForTest(testsNeedingUpdate, keys);
 }
 
@@ -1793,7 +1798,7 @@
     }
 
     var skippedBuilders = []
-    for (builder in currentBuilderGroup().builders) {
+    for (builder in currentBuilders()) {
         if (shownBuilders.indexOf(builder) == -1)
             skippedBuilders.push(builder);
     }
@@ -1818,12 +1823,12 @@
     html += '<div class=expectations test=' + test + '><div>' +
         linkHTMLToToggleState('showExpectations', 'results')
 
-    if (isLayoutTestResults() || isGPUTestResults()) {
-        if (isLayoutTestResults())
+    if (g_history.isLayoutTestResults() || g_history.isGPUTestResults()) {
+        if (g_history.isLayoutTestResults())
             html += ' | ' + linkHTMLToToggleState('showLargeExpectations', 'large thumbnails');
-        if (testResults && builderMaster(testResults[0].builder) == WEBKIT_BUILDER_MASTER) {
-            var revision = g_currentState.revision || '';
-            html += '<form onsubmit="setQueryParameter(\'revision\', revision.value);' +
+        if (testResults && currentBuilderGroup().master().name == WEBKIT_BUILDER_MASTER) {
+            var revision = g_history.dashboardSpecificState.revision || '';
+            html += '<form onsubmit="g_history.setQueryParameter(\'revision\', revision.value);' +
                 'return false;">Show results for WebKit revision: ' +
                 '<input name=revision placeholder="e.g. 65540" value="' + revision +
                 '" id=revision-input></form>';
@@ -1831,8 +1836,8 @@
             html += ' | <b>Only shows actual results/diffs from the most recent *failure* on each bot.</b>';
     } else {
       html += ' | <span>Results height:<input ' +
-          'onchange="setQueryParameter(\'resultsHeight\',this.value)" value="' +
-          g_currentState.resultsHeight + '" style="width:2.5em">px</span>';
+          'onchange="g_history.setQueryParameter(\'resultsHeight\',this.value)" value="' +
+          g_history.dashboardSpecificState.resultsHeight + '" style="width:2.5em">px</span>';
     }
     html += '</div></div>';
     return html;
@@ -1922,21 +1927,21 @@
         childContainer.appendChild(expectationsTitle(platformPart + suitePart, path, opt_builder));
         childContainer.className = 'expectations-item';
         item.className = 'expectation ' + fileExtension;
-        if (g_currentState.showLargeExpectations)
+        if (g_history.dashboardSpecificState.showLargeExpectations)
             item.className += ' large';
         childContainer.appendChild(item);
         handleFinishedLoadingExpectations(container);
     };
 
     var url = base + platformPart + path;
-    if (isImage || !startsWith(base, 'http://svn.webkit.org')) {
+    if (isImage || !string.startsWith(base, 'http://svn.webkit.org')) {
         var dummyNode = document.createElement(isImage ? 'img' : 'script');
         dummyNode.src = url;
         dummyNode.onload = function() {
             var item;
             if (isImage) {
                 item = dummyNode;
-                if (startsWith(base, 'http://svn.webkit.org'))
+                if (string.startsWith(base, 'http://svn.webkit.org'))
                     maybeAddPngChecksum(item, url);
             } else {
                 item = document.createElement('iframe');
@@ -2030,11 +2035,11 @@
         platforms['WIN'] = {};
         platforms['LINUX'] = {};
         allPlatforms.forEach(function(platform) {
-            if (startsWith(platform, 'MAC'))
+            if (string.startsWith(platform, 'MAC'))
                 platforms['MAC'][platform] = 1;
-            else if (startsWith(platform, 'WIN'))
+            else if (string.startsWith(platform, 'WIN'))
                 platforms['WIN'][platform] = 1;
-            else if (startsWith(platform, 'LINUX'))
+            else if (string.startsWith(platform, 'LINUX'))
                 platforms['LINUX'][platform] = 1;
         });
 
@@ -2048,7 +2053,7 @@
                 var nodesToRemove = [];
                 for (var j = 0, usedPlatformsLength = usedPlatforms.length; j < usedPlatformsLength; j++) {
                     var usedPlatform = usedPlatforms[j];
-                    if (startsWith(usedPlatform.textContent, platform))
+                    if (string.startsWith(usedPlatform.textContent, platform))
                         nodesToRemove.push(usedPlatform);
                 }
 
@@ -2077,11 +2082,11 @@
     var innerHTML;
     if (builder) {
         var resultsType;
-        if (endsWith(path, '-crash-log.txt'))
+        if (string.endsWith(path, '-crash-log.txt'))
             resultsType = 'STACKTRACE';
-        else if (endsWith(path, '-actual.txt') || endsWith(path, '-actual.png'))
+        else if (string.endsWith(path, '-actual.txt') || string.endsWith(path, '-actual.png'))
             resultsType = 'ACTUAL RESULTS';
-        else if (endsWith(path, '-wdiff.html'))
+        else if (string.endsWith(path, '-wdiff.html'))
             resultsType = 'WDIFF';
         else
             resultsType = 'DIFF';
@@ -2103,12 +2108,12 @@
 function loadExpectations(expectationsContainer)
 {
     var test = expectationsContainer.getAttribute('test');
-    if (isLayoutTestResults())
+    if (g_history.isLayoutTestResults())
         loadExpectationsLayoutTests(test, expectationsContainer);
     else {
         var results = g_testToResultsMap[test];
         for (var i = 0; i < results.length; i++)
-            if (isGPUTestResults())
+            if (g_history.isGPUTestResults())
                 loadGPUResultsForBuilder(results[i].builder, test, expectationsContainer);
             else
                 loadNonWebKitResultsForBuilder(results[i].builder, test, expectationsContainer);
@@ -2168,7 +2173,7 @@
         var item = document.createElement('iframe');
         item.src = dummyNode.src;
         item.className = itemClassName;
-        item.style.height = g_currentState.resultsHeight + 'px';
+        item.style.height = g_history.dashboardSpecificState.resultsHeight + 'px';
 
         if (opt_title) {
             var childContainer = document.createElement('div');
@@ -2255,9 +2260,9 @@
     var revisionContainer = document.createElement('div');
     revisionContainer.textContent = "Showing results for: "
     expectationsContainer.appendChild(revisionContainer);
-    for (var builder in g_builders) {
-        if (builderMaster(builder) == WEBKIT_BUILDER_MASTER) {
-            var latestRevision = g_currentState.revision || g_resultsByBuilder[builder].webkitRevision[0];
+    for (var builder in currentBuilders()) {
+        if (builderMaster(builder).name == WEBKIT_BUILDER_MASTER) {
+            var latestRevision = g_history.dashboardSpecificState.revision || g_resultsByBuilder[builder].webkitRevision[0];
             var buildInfo = buildInfoForRevision(builder, latestRevision);
             var revisionInfo = document.createElement('div');
             revisionInfo.style.cssText = 'background:lightgray;margin:0 3px;padding:0 2px;display:inline-block;';
@@ -2272,15 +2277,15 @@
     var testWithoutSuffix = test.substring(0, test.lastIndexOf('.'));
     var actualResultSuffixes = ['-actual.txt', '-actual.png', '-crash-log.txt', '-diff.txt', '-wdiff.html', '-diff.png'];
 
-    for (var builder in g_builders) {
+    for (var builder in currentBuilders()) {
         var actualResultsBase;
-        if (builderMaster(builder) == WEBKIT_BUILDER_MASTER) {
-            var latestRevision = g_currentState.revision || g_resultsByBuilder[builder].webkitRevision[0];
+        if (builderMaster(builder).name == WEBKIT_BUILDER_MASTER) {
+            var latestRevision = g_history.dashboardSpecificState.revision || g_resultsByBuilder[builder].webkitRevision[0];
             var buildInfo = buildInfoForRevision(builder, latestRevision);
             actualResultsBase = 'http://build.webkit.org/results/' + builder +
                 '/r' + buildInfo.revisionStart + ' (' + buildInfo.buildNumber + ')/';
         } else
-            actualResultsBase = TEST_RESULTS_BASE_PATH + g_builders[builder] + '/results/layout-test-results/';
+            actualResultsBase = TEST_RESULTS_BASE_PATH + currentBuilders()[builder] + '/results/layout-test-results/';
 
         for (var i = 0; i < actualResultSuffixes.length; i++) {
             addExpectationItem(expectationsContainers, expectationsContainer, null,
@@ -2324,7 +2329,7 @@
 
 function appendExpectations()
 {
-    var expectations = g_currentState.showExpectations ? document.getElementsByClassName('expectations') : [];
+    var expectations = g_history.dashboardSpecificState.showExpectations ? document.getElementsByClassName('expectations') : [];
     // Loading expectations is *very* slow. Use a large timeout to avoid
     // totally hanging the renderer.
     performChunkedAction(expectations, function(chunk) {
@@ -2346,13 +2351,13 @@
 function generatePageForIndividualTests(tests)
 {
     console.log('Number of tests: ' + tests.length);
-    if (g_currentState.showChrome)
+    if (g_history.dashboardSpecificState.showChrome)
         appendHTML(htmlForNavBar());
     performChunkedAction(tests, function(chunk) {
         appendHTML(htmlForIndividualTests(chunk));
     }, appendExpectations, 500);
-    if (g_currentState.showChrome)
-        $('tests-input').value = g_currentState.tests;
+    if (g_history.dashboardSpecificState.showChrome)
+        $('tests-input').value = g_history.dashboardSpecificState.tests;
 }
 
 function performChunkedAction(tests, handleChunk, onComplete, timeout, opt_index) {
@@ -2374,8 +2379,8 @@
     for (var i = 0; i < tests.length; i++) {
         var test = tests[i];
         var testNameHtml = '';
-        if (g_currentState.showChrome || tests.length > 1) {
-            if (isLayoutTestResults()) {
+        if (g_history.dashboardSpecificState.showChrome || tests.length > 1) {
+            if (g_history.isLayoutTestResults()) {
                 var suite = lookupVirtualTestSuite(test);
                 var base = suite ? baseTest(test, suite) : test;
                 var tracURL = TEST_URL_BASE_PATH_TRAC + base;
@@ -2392,13 +2397,13 @@
 function htmlForNavBar()
 {
     var extraHTML = '';
-    var html = htmlForTestTypeSwitcher(false, extraHTML, isCrossBuilderView());
+    var html = ui.html.testTypeSwitcher(false, extraHTML, isCrossBuilderView());
     html += '<div class=forms><form id=result-form ' +
-        'onsubmit="setQueryParameter(\'result\', result.value);' +
+        'onsubmit="g_history.setQueryParameter(\'result\', result.value);' +
         'return false;">Show all tests with result: ' +
         '<input name=result placeholder="e.g. CRASH" id=result-input>' +
         '</form><form id=tests-form ' +
-        'onsubmit="setQueryParameter(\'tests\', tests.value);' +
+        'onsubmit="g_history.setQueryParameter(\'tests\', tests.value);' +
         'return false;"><span>Show tests on all platforms: </span>' +
         '<input name=tests ' +
         'placeholder="Comma or space-separated list of tests or partial ' +
@@ -2410,14 +2415,14 @@
 
 function checkBoxToToggleState(key, text)
 {
-    var stateEnabled = g_currentState[key];
-    return '<label><input type=checkbox ' + (stateEnabled ? 'checked ' : '') + 'onclick="setQueryParameter(\'' + key + '\', ' + !stateEnabled + ')">' + text + '</label> ';
+    var stateEnabled = g_history.dashboardSpecificState[key];
+    return '<label><input type=checkbox ' + (stateEnabled ? 'checked ' : '') + 'onclick="g_history.setQueryParameter(\'' + key + '\', ' + !stateEnabled + ')">' + text + '</label> ';
 }
 
 function linkHTMLToToggleState(key, linkText)
 {
-    var stateEnabled = g_currentState[key];
-    return '<span class=link onclick="setQueryParameter(\'' + key + '\', ' + !stateEnabled + ')">' + (stateEnabled ? 'Hide' : 'Show') + ' ' + linkText + '</span>';
+    var stateEnabled = g_history.dashboardSpecificState[key];
+    return '<span class=link onclick="g_history.setQueryParameter(\'' + key + '\', ' + !stateEnabled + ')">' + (stateEnabled ? 'Hide' : 'Show') + ' ' + linkText + '</span>';
 }
 
 function headerForTestTableHtml()
@@ -2435,7 +2440,7 @@
     processTestRunsForBuilder(builderName);
 
     var results = g_perBuilderFailures[builderName];
-    sortTests(results, g_currentState.sortColumn, g_currentState.sortOrder);
+    sortTests(results, g_history.dashboardSpecificState.sortColumn, g_history.dashboardSpecificState.sortOrder);
 
     var testsHTML = '';
     if (results.length) {
@@ -2445,7 +2450,7 @@
         testsHTML = htmlForTestTable(tableRowsHTML);
     } else {
         testsHTML = '<div>No tests found. ';
-        if (isLayoutTestResults())
+        if (g_history.isLayoutTestResults())
             testsHTML += 'Try showing tests with correct expectations.</div>';
         else
             testsHTML += 'This means no tests have failed!</div>';
@@ -2453,7 +2458,7 @@
 
     var html = htmlForNavBar();
 
-    if (isLayoutTestResults())
+    if (g_history.isLayoutTestResults())
         html += htmlForTestsWithExpectationsButNoFailures(builderName) + headerForTestTableHtml();
 
     html += '<br>' + testsHTML;
@@ -2481,15 +2486,7 @@
 
 function isInvalidKeyForCrossBuilderView(key)
 {
-    return !(key in VALID_KEYS_FOR_CROSS_BUILDER_VIEW) && !(key in g_defaultCrossDashboardStateValues);
-}
-
-function updateDefaultBuilderState()
-{
-    if (isCrossBuilderView())
-        delete g_defaultDashboardSpecificStateValues.builder;
-    else
-        g_defaultDashboardSpecificStateValues.builder = g_defaultBuilderName;
+    return !(key in VALID_KEYS_FOR_CROSS_BUILDER_VIEW) && !(key in history.DEFAULT_CROSS_DASHBOARD_STATE_VALUES);
 }
 
 // Sets the page state to regenerate the page.
@@ -2499,18 +2496,17 @@
     for (key in params) {
         if (key == 'tests') {
             // Entering cross-builder view, only keep valid keys for that view.
-            for (var currentKey in g_currentState) {
+            for (var currentKey in g_history.dashboardSpecificState) {
               if (isInvalidKeyForCrossBuilderView(currentKey)) {
-                delete g_currentState[currentKey];
+                delete g_history.dashboardSpecificState[currentKey];
               }
             }
         } else if (isInvalidKeyForCrossBuilderView(key)) {
-            delete g_currentState.tests;
-            delete g_currentState.result;
+            delete g_history.dashboardSpecificState.tests;
+            delete g_history.dashboardSpecificState.result;
         }
     }
 
-    updateDefaultBuilderState();
     return true;
 }
 
@@ -2549,7 +2545,7 @@
         html += '<div class=' + expectation + '>' + expectationsMap()[expectation] + '</div>';
 
     html += '<div class=merge>WEBKIT MERGE</div>';
-    if (isLayoutTestResults()) {
+    if (g_history.isLayoutTestResults()) {
       html += '</div><br style="clear:both">' +
           '</div><h3>Test expectatons fallback order.</h3>';
 
@@ -2592,7 +2588,7 @@
 }
 
 if (window != parent)
-    window.addEventListener('blur', hidePopup);
+    window.addEventListener('blur', ui.popup.hide);
 
 document.addEventListener('keydown', function(e) {
     if (e.keyIdentifier == 'U+003F' || e.keyIdentifier == 'U+00BF') {
@@ -2602,6 +2598,11 @@
     } else if (e.keyIdentifier == 'U+001B') {
         // escape key
         hideLegend();
-        hidePopup();
+        ui.popup.hide();
     }
 }, false);
+
+window.addEventListener('load', function() {
+    resourceLoader = new loader.Loader(intializeHistory);
+    resourceLoader.load();
+}, false);