Index: chrome/browser/resources/profiler.js |
=================================================================== |
--- chrome/browser/resources/profiler.js (revision 113254) |
+++ chrome/browser/resources/profiler.js (working copy) |
@@ -1,2062 +0,0 @@ |
-// Copyright (c) 2011 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. |
- |
-var g_browserBridge; |
-var g_mainView; |
- |
-// TODO(eroman): The handling of "max" across snapshots is not correct. |
-// For starters the browser needs to be aware to generate new maximums. |
-// Secondly, we need to take into account the "max" of intermediary snapshots, |
-// not just the terminal ones. |
- |
-/** |
- * Main entry point called once the page has loaded. |
- */ |
-function onLoad() { |
- g_browserBridge = new BrowserBridge(); |
- g_mainView = new MainView(); |
-} |
- |
-document.addEventListener('DOMContentLoaded', onLoad); |
- |
-/** |
- * This class provides a "bridge" for communicating between the javascript and |
- * the browser. Used as a singleton. |
- */ |
-var BrowserBridge = (function() { |
- 'use strict'; |
- |
- /** |
- * @constructor |
- */ |
- function BrowserBridge() { |
- } |
- |
- BrowserBridge.prototype = { |
- //-------------------------------------------------------------------------- |
- // Messages sent to the browser |
- //-------------------------------------------------------------------------- |
- |
- sendGetData: function() { |
- chrome.send('getData'); |
- }, |
- |
- sendResetData: function() { |
- chrome.send('resetData'); |
- }, |
- |
- //-------------------------------------------------------------------------- |
- // Messages received from the browser. |
- //-------------------------------------------------------------------------- |
- |
- receivedData: function(data) { |
- // TODO(eroman): The browser should give an indication of which snapshot |
- // this data belongs to. For now we always assume it is for the latest. |
- g_mainView.addDataToSnapshot(data); |
- }, |
- }; |
- |
- return BrowserBridge; |
-})(); |
- |
-/** |
- * This class handles the presentation of our profiler view. Used as a |
- * singleton. |
- */ |
-var MainView = (function() { |
- 'use strict'; |
- |
- // -------------------------------------------------------------------------- |
- // Important IDs in the HTML document |
- // -------------------------------------------------------------------------- |
- |
- // The search box to filter results. |
- var FILTER_SEARCH_ID = 'filter-search'; |
- |
- // The container node to put all the "Group by" dropdowns into. |
- var GROUP_BY_CONTAINER_ID = 'group-by-container'; |
- |
- // The container node to put all the "Sort by" dropdowns into. |
- var SORT_BY_CONTAINER_ID = 'sort-by-container'; |
- |
- // The DIV to put all the tables into. |
- var RESULTS_DIV_ID = 'results-div'; |
- |
- // The container node to put all the column (visibility) checkboxes into. |
- var COLUMN_TOGGLES_CONTAINER_ID = 'column-toggles-container'; |
- |
- // The container node to put all the column (merge) checkboxes into. |
- var COLUMN_MERGE_TOGGLES_CONTAINER_ID = 'column-merge-toggles-container'; |
- |
- // The anchor which toggles visibility of column checkboxes. |
- var EDIT_COLUMNS_LINK_ID = 'edit-columns-link'; |
- |
- // The container node to show/hide when toggling the column checkboxes. |
- var EDIT_COLUMNS_ROW = 'edit-columns-row'; |
- |
- // The checkbox which controls whether things like "Worker Threads" and |
- // "PAC threads" will be merged together. |
- var MERGE_SIMILAR_THREADS_CHECKBOX_ID = 'merge-similar-threads-checkbox'; |
- |
- var RESET_DATA_LINK_ID = 'reset-data-link'; |
- |
- var TOGGLE_SNAPSHOTS_LINK_ID = 'snapshots-link'; |
- var SNAPSHOTS_ROW = 'snapshots-row'; |
- var SNAPSHOT_SELECTION_SUMMARY_ID = 'snapshot-selection-summary'; |
- var TAKE_SNAPSHOT_BUTTON_ID = 'snapshot-button'; |
- |
- // -------------------------------------------------------------------------- |
- // Row keys |
- // -------------------------------------------------------------------------- |
- |
- // Each row of our data is an array of values rather than a dictionary. This |
- // avoids some overhead from repeating the key string multiple times, and |
- // speeds up the property accesses a bit. The following keys are well-known |
- // indexes into the array for various properties. |
- // |
- // Note that the declaration order will also define the default display order. |
- |
- var BEGIN_KEY = 1; // Start at 1 rather than 0 to simplify sorting code. |
- var END_KEY = BEGIN_KEY; |
- |
- var KEY_COUNT = END_KEY++; |
- var KEY_RUN_TIME = END_KEY++; |
- var KEY_AVG_RUN_TIME = END_KEY++; |
- var KEY_MAX_RUN_TIME = END_KEY++; |
- var KEY_QUEUE_TIME = END_KEY++; |
- var KEY_AVG_QUEUE_TIME = END_KEY++; |
- var KEY_MAX_QUEUE_TIME = END_KEY++; |
- var KEY_BIRTH_THREAD = END_KEY++; |
- var KEY_DEATH_THREAD = END_KEY++; |
- var KEY_PROCESS_TYPE = END_KEY++; |
- var KEY_PROCESS_ID = END_KEY++; |
- var KEY_FUNCTION_NAME = END_KEY++; |
- var KEY_SOURCE_LOCATION = END_KEY++; |
- var KEY_FILE_NAME = END_KEY++; |
- var KEY_LINE_NUMBER = END_KEY++; |
- |
- var NUM_KEYS = END_KEY - BEGIN_KEY; |
- |
- // -------------------------------------------------------------------------- |
- // Aggregators |
- // -------------------------------------------------------------------------- |
- |
- // To generalize computing/displaying the aggregate "counts" for each column, |
- // we specify an optional "Aggregator" class to use with each property. |
- |
- // The following are actually "Aggregator factories". They create an |
- // aggregator instance by calling 'create()'. The instance is then fed |
- // each row one at a time via the 'consume()' method. After all rows have |
- // been consumed, the 'getValueAsText()' method will return the aggregated |
- // value. |
- |
- /** |
- * This aggregator counts the number of unique values that were fed to it. |
- */ |
- var UniquifyAggregator = (function() { |
- function Aggregator(key) { |
- this.key_ = key; |
- this.valuesSet_ = {}; |
- } |
- |
- Aggregator.prototype = { |
- consume: function(e) { |
- this.valuesSet_[e[this.key_]] = true; |
- }, |
- |
- getValueAsText: function() { |
- return getDictionaryKeys(this.valuesSet_).length + ' unique' |
- }, |
- }; |
- |
- return { |
- create: function(key) { return new Aggregator(key); } |
- }; |
- })(); |
- |
- /** |
- * This aggregator sums a numeric field. |
- */ |
- var SumAggregator = (function() { |
- function Aggregator(key) { |
- this.key_ = key; |
- this.sum_ = 0; |
- } |
- |
- Aggregator.prototype = { |
- consume: function(e) { |
- this.sum_ += e[this.key_]; |
- }, |
- |
- getValue: function() { |
- return this.sum_; |
- }, |
- |
- getValueAsText: function() { |
- return formatNumberAsText(this.getValue()); |
- }, |
- }; |
- |
- return { |
- create: function(key) { return new Aggregator(key); } |
- }; |
- })(); |
- |
- /** |
- * This aggregator computes an average by summing two |
- * numeric fields, and then dividing the totals. |
- */ |
- var AvgAggregator = (function() { |
- function Aggregator(numeratorKey, divisorKey) { |
- this.numeratorKey_ = numeratorKey; |
- this.divisorKey_ = divisorKey; |
- |
- this.numeratorSum_ = 0; |
- this.divisorSum_ = 0; |
- } |
- |
- Aggregator.prototype = { |
- consume: function(e) { |
- this.numeratorSum_ += e[this.numeratorKey_]; |
- this.divisorSum_ += e[this.divisorKey_]; |
- }, |
- |
- getValue: function() { |
- return this.numeratorSum_ / this.divisorSum_; |
- }, |
- |
- getValueAsText: function() { |
- return formatNumberAsText(this.getValue()); |
- }, |
- }; |
- |
- return { |
- create: function(numeratorKey, divisorKey) { |
- return { |
- create: function(key) { |
- return new Aggregator(numeratorKey, divisorKey); |
- }, |
- } |
- } |
- }; |
- })(); |
- |
- /** |
- * This aggregator finds the maximum for a numeric field. |
- */ |
- var MaxAggregator = (function() { |
- function Aggregator(key) { |
- this.key_ = key; |
- this.max_ = -Infinity; |
- } |
- |
- Aggregator.prototype = { |
- consume: function(e) { |
- this.max_ = Math.max(this.max_, e[this.key_]); |
- }, |
- |
- getValue: function() { |
- return this.max_; |
- }, |
- |
- getValueAsText: function() { |
- return formatNumberAsText(this.getValue()); |
- }, |
- }; |
- |
- return { |
- create: function(key) { return new Aggregator(key); } |
- }; |
- })(); |
- |
- // -------------------------------------------------------------------------- |
- // Key properties |
- // -------------------------------------------------------------------------- |
- |
- // Custom comparator for thread names (sorts main thread and IO thread |
- // higher than would happen lexicographically.) |
- var threadNameComparator = |
- createLexicographicComparatorWithExceptions([ |
- 'CrBrowserMain', |
- 'Chrome_IOThread', |
- 'Chrome_FileThread', |
- 'Chrome_HistoryThread', |
- 'Chrome_DBThread', |
- 'Still_Alive', |
- ]); |
- |
- function diffFuncForCount(a, b) { |
- return b - a; |
- } |
- |
- function diffFuncForMax(a, b) { |
- return b; |
- } |
- |
- /** |
- * Enumerates information about various keys. Such as whether their data is |
- * expected to be numeric or is a string, a descriptive name (title) for the |
- * property, and what function should be used to aggregate the property when |
- * displayed in a column. |
- * |
- * -------------------------------------- |
- * The following properties are required: |
- * -------------------------------------- |
- * |
- * [name]: This is displayed as the column's label. |
- * [aggregator]: Aggregator factory that is used to compute an aggregate |
- * value for this column. |
- * |
- * -------------------------------------- |
- * The following properties are optional: |
- * -------------------------------------- |
- * |
- * [inputJsonKey]: The corresponding key for this property in the original |
- * JSON dictionary received from the browser. If this is |
- * present, values for this key will be automatically |
- * populated during import. |
- * [comparator]: A comparator function for sorting this column. |
- * [textPrinter]: A function that transforms values into the user-displayed |
- * text shown in the UI. If unspecified, will default to the |
- * "toString()" function. |
- * [cellAlignment]: The horizonal alignment to use for columns of this |
- * property (for instance 'right'). If unspecified will |
- * default to left alignment. |
- * [sortDescending]: When first clicking on this column, we will default to |
- * sorting by |comparator| in ascending order. If this |
- * property is true, we will reverse that to descending. |
- * [diff]: Function to call to compute a "difference" value between |
- * parameters (a, b). This is used when calculating the difference |
- * between two snapshots. Diffing numeric quantities generally |
- * involves subtracting, but some fields like max may need to do |
- * something different. |
- */ |
- var KEY_PROPERTIES = []; |
- |
- KEY_PROPERTIES[KEY_PROCESS_ID] = { |
- name: 'PID', |
- cellAlignment: 'right', |
- aggregator: UniquifyAggregator, |
- }; |
- |
- KEY_PROPERTIES[KEY_PROCESS_TYPE] = { |
- name: 'Process type', |
- aggregator: UniquifyAggregator, |
- }; |
- |
- KEY_PROPERTIES[KEY_BIRTH_THREAD] = { |
- name: 'Birth thread', |
- inputJsonKey: 'birth_thread', |
- aggregator: UniquifyAggregator, |
- comparator: threadNameComparator, |
- }; |
- |
- KEY_PROPERTIES[KEY_DEATH_THREAD] = { |
- name: 'Exec thread', |
- inputJsonKey: 'death_thread', |
- aggregator: UniquifyAggregator, |
- comparator: threadNameComparator, |
- }; |
- |
- KEY_PROPERTIES[KEY_FUNCTION_NAME] = { |
- name: 'Function name', |
- inputJsonKey: 'location.function_name', |
- aggregator: UniquifyAggregator, |
- }; |
- |
- KEY_PROPERTIES[KEY_FILE_NAME] = { |
- name: 'File name', |
- inputJsonKey: 'location.file_name', |
- aggregator: UniquifyAggregator, |
- }; |
- |
- KEY_PROPERTIES[KEY_LINE_NUMBER] = { |
- name: 'Line number', |
- cellAlignment: 'right', |
- inputJsonKey: 'location.line_number', |
- aggregator: UniquifyAggregator, |
- }; |
- |
- KEY_PROPERTIES[KEY_COUNT] = { |
- name: 'Count', |
- cellAlignment: 'right', |
- sortDescending: true, |
- textPrinter: formatNumberAsText, |
- inputJsonKey: 'death_data.count', |
- aggregator: SumAggregator, |
- diff: diffFuncForCount, |
- }; |
- |
- KEY_PROPERTIES[KEY_QUEUE_TIME] = { |
- name: 'Total queue time', |
- cellAlignment: 'right', |
- sortDescending: true, |
- textPrinter: formatNumberAsText, |
- inputJsonKey: 'death_data.queue_ms', |
- aggregator: SumAggregator, |
- diff: diffFuncForCount, |
- }; |
- |
- KEY_PROPERTIES[KEY_MAX_QUEUE_TIME] = { |
- name: 'Max queue time', |
- cellAlignment: 'right', |
- sortDescending: true, |
- textPrinter: formatNumberAsText, |
- inputJsonKey: 'death_data.queue_ms_max', |
- aggregator: MaxAggregator, |
- diff: diffFuncForMax, |
- }; |
- |
- KEY_PROPERTIES[KEY_RUN_TIME] = { |
- name: 'Total run time', |
- cellAlignment: 'right', |
- sortDescending: true, |
- textPrinter: formatNumberAsText, |
- inputJsonKey: 'death_data.run_ms', |
- aggregator: SumAggregator, |
- diff: diffFuncForCount, |
- }; |
- |
- KEY_PROPERTIES[KEY_AVG_RUN_TIME] = { |
- name: 'Avg run time', |
- cellAlignment: 'right', |
- sortDescending: true, |
- textPrinter: formatNumberAsText, |
- aggregator: AvgAggregator.create(KEY_RUN_TIME, KEY_COUNT), |
- }; |
- |
- KEY_PROPERTIES[KEY_MAX_RUN_TIME] = { |
- name: 'Max run time', |
- cellAlignment: 'right', |
- sortDescending: true, |
- textPrinter: formatNumberAsText, |
- inputJsonKey: 'death_data.run_ms_max', |
- aggregator: MaxAggregator, |
- diff: diffFuncForMax, |
- }; |
- |
- KEY_PROPERTIES[KEY_AVG_QUEUE_TIME] = { |
- name: 'Avg queue time', |
- cellAlignment: 'right', |
- sortDescending: true, |
- textPrinter: formatNumberAsText, |
- aggregator: AvgAggregator.create(KEY_QUEUE_TIME, KEY_COUNT), |
- }; |
- |
- KEY_PROPERTIES[KEY_SOURCE_LOCATION] = { |
- name: 'Source location', |
- type: 'string', |
- aggregator: UniquifyAggregator, |
- }; |
- |
- /** |
- * Returns the string name for |key|. |
- */ |
- function getNameForKey(key) { |
- var props = KEY_PROPERTIES[key]; |
- if (props == undefined) |
- throw 'Did not define properties for key: ' + key; |
- return props.name; |
- } |
- |
- /** |
- * Ordered list of all keys. This is the order we generally want |
- * to display the properties in. Default to declaration order. |
- */ |
- var ALL_KEYS = []; |
- for (var k = BEGIN_KEY; k < END_KEY; ++k) |
- ALL_KEYS.push(k); |
- |
- // -------------------------------------------------------------------------- |
- // Default settings |
- // -------------------------------------------------------------------------- |
- |
- /** |
- * List of keys for those properties which we want to initially omit |
- * from the table. (They can be re-enabled by clicking [Edit columns]). |
- */ |
- var INITIALLY_HIDDEN_KEYS = [ |
- KEY_FILE_NAME, |
- KEY_LINE_NUMBER, |
- KEY_QUEUE_TIME, |
- ]; |
- |
- /** |
- * The ordered list of grouping choices to expose in the "Group by" |
- * dropdowns. We don't include the numeric properties, since they |
- * leads to awkward bucketing. |
- */ |
- var GROUPING_DROPDOWN_CHOICES = [ |
- KEY_PROCESS_TYPE, |
- KEY_PROCESS_ID, |
- KEY_BIRTH_THREAD, |
- KEY_DEATH_THREAD, |
- KEY_FUNCTION_NAME, |
- KEY_SOURCE_LOCATION, |
- KEY_FILE_NAME, |
- KEY_LINE_NUMBER, |
- ]; |
- |
- /** |
- * The ordered list of sorting choices to expose in the "Sort by" |
- * dropdowns. |
- */ |
- var SORT_DROPDOWN_CHOICES = ALL_KEYS; |
- |
- /** |
- * The ordered list of all columns that can be displayed in the tables (not |
- * including whatever has been hidden via [Edit Columns]). |
- */ |
- var ALL_TABLE_COLUMNS = ALL_KEYS; |
- |
- /** |
- * The initial keys to sort by when loading the page (can be changed later). |
- */ |
- var INITIAL_SORT_KEYS = [-KEY_COUNT]; |
- |
- /** |
- * The default sort keys to use when nothing has been specified. |
- */ |
- var DEFAULT_SORT_KEYS = [-KEY_COUNT]; |
- |
- /** |
- * The initial keys to group by when loading the page (can be changed later). |
- */ |
- var INITIAL_GROUP_KEYS = []; |
- |
- /** |
- * The columns to give the option to merge on. |
- */ |
- var MERGEABLE_KEYS = [ |
- KEY_PROCESS_ID, |
- KEY_PROCESS_TYPE, |
- KEY_BIRTH_THREAD, |
- KEY_DEATH_THREAD, |
- ]; |
- |
- /** |
- * The columns to merge by default. |
- */ |
- var INITIALLY_MERGED_KEYS = []; |
- |
- /** |
- * The full set of columns which define the "identity" for a row. A row is |
- * considered equivalent to another row if it matches on all of these |
- * fields. This list is used when merging the data, to determine which rows |
- * should be merged together. The remaining columns not listed in |
- * IDENTITY_KEYS will be aggregated. |
- */ |
- var IDENTITY_KEYS = [ |
- KEY_BIRTH_THREAD, |
- KEY_DEATH_THREAD, |
- KEY_PROCESS_TYPE, |
- KEY_PROCESS_ID, |
- KEY_FUNCTION_NAME, |
- KEY_SOURCE_LOCATION, |
- KEY_FILE_NAME, |
- KEY_LINE_NUMBER, |
- ]; |
- |
- /** |
- * The time (in milliseconds) to wait after receiving new data before |
- * re-drawing it to the screen. The reason we wait a bit is to avoid |
- * repainting repeatedly during the loading phase (which can slow things |
- * down). Note that this only slows down the addition of new data. It does |
- * not impact the latency of user-initiated operations like sorting or |
- * merging. |
- */ |
- var PROCESS_DATA_DELAY_MS = 500; |
- |
- /** |
- * The initial number of rows to display (the rest are hidden) when no |
- * grouping is selected. We use a higher limit than when grouping is used |
- * since there is a lot of vertical real estate. |
- */ |
- var INITIAL_UNGROUPED_ROW_LIMIT = 30; |
- |
- /** |
- * The initial number of rows to display (rest are hidden) for each group. |
- */ |
- var INITIAL_GROUP_ROW_LIMIT = 10; |
- |
- /** |
- * The number of extra rows to show/hide when clicking the "Show more" or |
- * "Show less" buttons. |
- */ |
- var LIMIT_INCREMENT = 10; |
- |
- // -------------------------------------------------------------------------- |
- // General utility functions |
- // -------------------------------------------------------------------------- |
- |
- /** |
- * Returns a list of all the keys in |dict|. |
- */ |
- function getDictionaryKeys(dict) { |
- var keys = []; |
- for (var key in dict) { |
- keys.push(key); |
- } |
- return keys; |
- } |
- |
- /** |
- * Formats the number |x| as a decimal integer. Strips off any decimal parts, |
- * and comma separates the number every 3 characters. |
- */ |
- function formatNumberAsText(x) { |
- var orig = x.toFixed(0); |
- |
- var parts = []; |
- for (var end = orig.length; end > 0; ) { |
- var chunk = Math.min(end, 3); |
- parts.push(orig.substr(end-chunk, chunk)); |
- end -= chunk; |
- } |
- return parts.reverse().join(','); |
- } |
- |
- /** |
- * Simple comparator function which works for both strings and numbers. |
- */ |
- function simpleCompare(a, b) { |
- if (a == b) |
- return 0; |
- if (a < b) |
- return -1; |
- return 1; |
- } |
- |
- /** |
- * Returns a comparator function that compares values lexicographically, |
- * but special-cases the values in |orderedList| to have a higher |
- * rank. |
- */ |
- function createLexicographicComparatorWithExceptions(orderedList) { |
- var valueToRankMap = {}; |
- for (var i = 0; i < orderedList.length; ++i) |
- valueToRankMap[orderedList[i]] = i; |
- |
- function getCustomRank(x) { |
- var rank = valueToRankMap[x]; |
- if (rank == undefined) |
- rank = Infinity; // Unmatched. |
- return rank; |
- } |
- |
- return function(a, b) { |
- var aRank = getCustomRank(a); |
- var bRank = getCustomRank(b); |
- |
- // Not matched by any of our exceptions. |
- if (aRank == bRank) |
- return simpleCompare(a, b); |
- |
- if (aRank < bRank) |
- return -1; |
- return 1; |
- }; |
- } |
- |
- /** |
- * Returns dict[key]. Note that if |key| contains periods (.), they will be |
- * interpreted as meaning a sub-property. |
- */ |
- function getPropertyByPath(dict, key) { |
- var cur = dict; |
- var parts = key.split('.'); |
- for (var i = 0; i < parts.length; ++i) { |
- if (cur == undefined) |
- return undefined; |
- cur = cur[parts[i]]; |
- } |
- return cur; |
- } |
- |
- /** |
- * Creates and appends a DOM node of type |tagName| to |parent|. Optionally, |
- * sets the new node's text to |opt_text|. Returns the newly created node. |
- */ |
- function addNode(parent, tagName, opt_text) { |
- var n = parent.ownerDocument.createElement(tagName); |
- parent.appendChild(n); |
- if (opt_text != undefined) { |
- addText(n, opt_text); |
- } |
- return n; |
- } |
- |
- /** |
- * Adds |text| to |parent|. |
- */ |
- function addText(parent, text) { |
- var textNode = parent.ownerDocument.createTextNode(text); |
- parent.appendChild(textNode); |
- return textNode; |
- } |
- |
- /** |
- * Deletes all the strings in |array| which appear in |valuesToDelete|. |
- */ |
- function deleteValuesFromArray(array, valuesToDelete) { |
- var valueSet = arrayToSet(valuesToDelete); |
- for (var i = 0; i < array.length; ) { |
- if (valueSet[array[i]]) { |
- array.splice(i, 1); |
- } else { |
- i++; |
- } |
- } |
- } |
- |
- /** |
- * Deletes all the repeated ocurrences of strings in |array|. |
- */ |
- function deleteDuplicateStringsFromArray(array) { |
- // Build up set of each entry in array. |
- var seenSoFar = {}; |
- |
- for (var i = 0; i < array.length; ) { |
- var value = array[i]; |
- if (seenSoFar[value]) { |
- array.splice(i, 1); |
- } else { |
- seenSoFar[value] = true; |
- i++; |
- } |
- } |
- } |
- |
- /** |
- * Builds a map out of the array |list|. |
- */ |
- function arrayToSet(list) { |
- var set = {}; |
- for (var i = 0; i < list.length; ++i) |
- set[list[i]] = true; |
- return set; |
- } |
- |
- function trimWhitespace(text) { |
- var m = /^\s*(.*)\s*$/.exec(text); |
- return m[1]; |
- } |
- |
- /** |
- * Selects the option in |select| which has a value of |value|. |
- */ |
- function setSelectedOptionByValue(select, value) { |
- for (var i = 0; i < select.options.length; ++i) { |
- if (select.options[i].value == value) { |
- select.options[i].selected = true; |
- return true; |
- } |
- } |
- return false; |
- } |
- |
- /** |
- * Adds a checkbox to |parent|. The checkbox will have a label on its right |
- * with text |label|. Returns the checkbox input node. |
- */ |
- function addLabeledCheckbox(parent, label) { |
- var labelNode = addNode(parent, 'label'); |
- var checkbox = addNode(labelNode, 'input'); |
- checkbox.type = 'checkbox'; |
- addText(labelNode, label); |
- return checkbox; |
- } |
- |
- /** |
- * Return the last component in a path which is separated by either forward |
- * slashes or backslashes. |
- */ |
- function getFilenameFromPath(path) { |
- var lastSlash = Math.max(path.lastIndexOf('/'), |
- path.lastIndexOf('\\')); |
- if (lastSlash == -1) |
- return path; |
- |
- return path.substr(lastSlash + 1); |
- } |
- |
- /** |
- * Returns the current time in milliseconds since unix epoch. |
- */ |
- function getTimeMillis() { |
- return (new Date()).getTime(); |
- } |
- |
- /** |
- * Toggle a node between hidden/invisible. |
- */ |
- function toggleNodeDisplay(n) { |
- if (n.style.display == '') { |
- n.style.display = 'none'; |
- } else { |
- n.style.display = ''; |
- } |
- } |
- |
- // -------------------------------------------------------------------------- |
- // Functions that augment, bucket, and compute aggregates for the input data. |
- // -------------------------------------------------------------------------- |
- |
- /** |
- * Adds new derived properties to row. Mutates the provided dictionary |e|. |
- */ |
- function augmentDataRow(e) { |
- computeDataRowAverages(e); |
- e[KEY_SOURCE_LOCATION] = e[KEY_FILE_NAME] + ' [' + e[KEY_LINE_NUMBER] + ']'; |
- } |
- |
- function computeDataRowAverages(e) { |
- e[KEY_AVG_QUEUE_TIME] = e[KEY_QUEUE_TIME] / e[KEY_COUNT]; |
- e[KEY_AVG_RUN_TIME] = e[KEY_RUN_TIME] / e[KEY_COUNT]; |
- } |
- |
- /** |
- * Creates and initializes an aggregator object for each key in |columns|. |
- * Returns an array whose keys are values from |columns|, and whose |
- * values are Aggregator instances. |
- */ |
- function initializeAggregates(columns) { |
- var aggregates = []; |
- |
- for (var i = 0; i < columns.length; ++i) { |
- var key = columns[i]; |
- var aggregatorFactory = KEY_PROPERTIES[key].aggregator; |
- aggregates[key] = aggregatorFactory.create(key); |
- } |
- |
- return aggregates; |
- } |
- |
- function consumeAggregates(aggregates, row) { |
- for (var key in aggregates) |
- aggregates[key].consume(row); |
- } |
- |
- function bucketIdenticalRows(rows, identityKeys, propertyGetterFunc) { |
- var identicalRows = {}; |
- for (var i = 0; i < rows.length; ++i) { |
- var r = rows[i]; |
- |
- var rowIdentity = []; |
- for (var j = 0; j < identityKeys.length; ++j) |
- rowIdentity.push(propertyGetterFunc(r, identityKeys[j])); |
- rowIdentity = rowIdentity.join('\n'); |
- |
- var l = identicalRows[rowIdentity]; |
- if (!l) { |
- l = []; |
- identicalRows[rowIdentity] = l; |
- } |
- l.push(r); |
- } |
- return identicalRows; |
- } |
- |
- /** |
- * Merges the rows in |origRows|, by collapsing the columns listed in |
- * |mergeKeys|. Returns an array with the merged rows (in no particular |
- * order). |
- * |
- * If |mergeSimilarThreads| is true, then threads with a similar name will be |
- * considered equivalent. For instance, "WorkerThread-1" and "WorkerThread-2" |
- * will be remapped to "WorkerThread-*". |
- */ |
- function mergeRows(origRows, mergeKeys, mergeSimilarThreads) { |
- // Define a translation function for each property. Normally we copy over |
- // properties as-is, but if we have been asked to "merge similar threads" we |
- // we will remap the thread names that end in a numeric suffix. |
- var propertyGetterFunc; |
- |
- if (mergeSimilarThreads) { |
- propertyGetterFunc = function(row, key) { |
- var value = row[key]; |
- // If the property is a thread name, try to remap it. |
- if (key == KEY_BIRTH_THREAD || key == KEY_DEATH_THREAD) { |
- var m = /^(.*[^\d])(\d+)$/.exec(value); |
- if (m) |
- value = m[1] + '*'; |
- } |
- return value; |
- } |
- } else { |
- propertyGetterFunc = function(row, key) { return row[key]; }; |
- } |
- |
- // Determine which sets of properties a row needs to match on to be |
- // considered identical to another row. |
- var identityKeys = IDENTITY_KEYS.slice(0); |
- deleteValuesFromArray(identityKeys, mergeKeys); |
- |
- // Set |aggregateKeys| to everything else, since we will be aggregating |
- // their value as part of the merge. |
- var aggregateKeys = ALL_KEYS.slice(0); |
- deleteValuesFromArray(aggregateKeys, IDENTITY_KEYS); |
- |
- // Group all the identical rows together, bucketed into |identicalRows|. |
- var identicalRows = |
- bucketIdenticalRows(origRows, identityKeys, propertyGetterFunc); |
- |
- var mergedRows = []; |
- |
- // Merge the rows and save the results to |mergedRows|. |
- for (var k in identicalRows) { |
- // We need to smash the list |l| down to a single row... |
- var l = identicalRows[k]; |
- |
- var newRow = []; |
- mergedRows.push(newRow); |
- |
- // Copy over all the identity columns to the new row (since they |
- // were the same for each row matched). |
- for (var i = 0; i < identityKeys.length; ++i) |
- newRow[identityKeys[i]] = propertyGetterFunc(l[0], identityKeys[i]); |
- |
- // Compute aggregates for the other columns. |
- var aggregates = initializeAggregates(aggregateKeys); |
- |
- // Feed the rows to the aggregators. |
- for (var i = 0; i < l.length; ++i) |
- consumeAggregates(aggregates, l[i]); |
- |
- // Suck out the data generated by the aggregators. |
- for (var aggregateKey in aggregates) |
- newRow[aggregateKey] = aggregates[aggregateKey].getValue(); |
- } |
- |
- return mergedRows; |
- } |
- |
- /** |
- * Takes two flat lists data1 and data2, and returns a new flat list which |
- * represents the difference between them. The exact meaning of "difference" |
- * is column specific, but for most numeric fields (like the count, or total |
- * time), it is found by subtracting. |
- * |
- * TODO(eroman): Some of this code is duplicated from mergeRows(). |
- */ |
- function subtractSnapshots(data1, data2) { |
- // These columns are computed from the other columns. We won't bother |
- // diffing/aggregating these, but rather will derive them again from the |
- // final row. |
- var COMPUTED_AGGREGATE_KEYS = [KEY_AVG_QUEUE_TIME, KEY_AVG_RUN_TIME]; |
- |
- // These are the keys which determine row equality. Since we are not doing |
- // any merging yet at this point, it is simply the list of all identity |
- // columns. |
- var identityKeys = IDENTITY_KEYS; |
- |
- // The columns to compute via aggregation is everything else. |
- var aggregateKeys = ALL_KEYS.slice(0); |
- deleteValuesFromArray(aggregateKeys, IDENTITY_KEYS); |
- deleteValuesFromArray(aggregateKeys, COMPUTED_AGGREGATE_KEYS); |
- |
- // Group all the identical rows for each list together. |
- var propertyGetterFunc = function(row, key) { return row[key]; }; |
- var identicalRows1 = |
- bucketIdenticalRows(data1, identityKeys, propertyGetterFunc); |
- var identicalRows2 = |
- bucketIdenticalRows(data2, identityKeys, propertyGetterFunc); |
- |
- var diffedRows = []; |
- |
- for (var k in identicalRows2) { |
- var rows2 = identicalRows2[k]; |
- var rows1 = identicalRows1[k]; |
- if (rows1 == undefined) |
- rows1 = []; |
- |
- var newRow = []; |
- |
- // Copy over all the identity columns to the new row (since they |
- // were the same for each row matched). |
- for (var i = 0; i < identityKeys.length; ++i) |
- newRow[identityKeys[i]] = propertyGetterFunc(rows2[0], identityKeys[i]); |
- |
- // The raw data for each snapshot *may* have contained duplicate rows, so |
- // smash them down into a single row using our aggregation functions. |
- var aggregates1 = initializeAggregates(aggregateKeys); |
- var aggregates2 = initializeAggregates(aggregateKeys); |
- for (var i = 0; i < rows1.length; ++i) |
- consumeAggregates(aggregates1, rows1[i]); |
- for (var i = 0; i < rows2.length; ++i) |
- consumeAggregates(aggregates2, rows2[i]); |
- |
- // Finally, diff the two merged rows. |
- for (var aggregateKey in aggregates2) { |
- var a = aggregates1[aggregateKey].getValue(); |
- var b = aggregates2[aggregateKey].getValue(); |
- |
- var diffFunc = KEY_PROPERTIES[aggregateKey].diff; |
- newRow[aggregateKey] = diffFunc(a, b); |
- } |
- |
- if (newRow[KEY_COUNT] == 0) { |
- // If a row's count has gone to zero, it means there were no new |
- // occurrences of it in the second snapshot, so remove it. |
- continue; |
- } |
- |
- // Since we excluded the averages during diffing phase, re-compute them |
- // using the diffed totals. |
- computeDataRowAverages(newRow); |
- diffedRows.push(newRow); |
- } |
- |
- return diffedRows; |
- } |
- |
- // -------------------------------------------------------------------------- |
- // HTML drawing code |
- // -------------------------------------------------------------------------- |
- |
- function getTextValueForProperty(key, value) { |
- if (value == undefined) { |
- // A value may be undefined as a result of having merging rows. We |
- // won't actually draw it, but this might be called by the filter. |
- return ''; |
- } |
- |
- var textPrinter = KEY_PROPERTIES[key].textPrinter; |
- if (textPrinter) |
- return textPrinter(value); |
- return value.toString(); |
- } |
- |
- /** |
- * Renders the property value |value| into cell |td|. The name of this |
- * property is |key|. |
- */ |
- function drawValueToCell(td, key, value) { |
- // Get a text representation of the value. |
- var text = getTextValueForProperty(key, value); |
- |
- // Apply the desired cell alignment. |
- var cellAlignment = KEY_PROPERTIES[key].cellAlignment; |
- if (cellAlignment) |
- td.align = cellAlignment; |
- |
- if (key == KEY_SOURCE_LOCATION) { |
- // Linkify the source column so it jumps to the source code. This doesn't |
- // take into account the particular code this build was compiled from, or |
- // local edits to source. It should however work correctly for top of tree |
- // builds. |
- var m = /^(.*) \[(\d+)\]$/.exec(text); |
- if (m) { |
- var filepath = m[1]; |
- var filename = getFilenameFromPath(filepath); |
- var linenumber = m[2]; |
- |
- var link = addNode(td, 'a', filename + ' [' + linenumber + ']'); |
- // http://chromesrc.appspot.com is a server I wrote specifically for |
- // this task. It redirects to the appropriate source file; the file |
- // paths given by the compiler can be pretty crazy and different |
- // between platforms. |
- link.href = 'http://chromesrc.appspot.com/?path=' + |
- encodeURIComponent(filepath) + '&line=' + linenumber; |
- link.target = '_blank'; |
- return; |
- } |
- } |
- |
- // String values can get pretty long. If the string contains no spaces, then |
- // CSS fails to wrap it, and it overflows the cell causing the table to get |
- // really big. We solve this using a hack: insert a <wbr> element after |
- // every single character. This will allow the rendering engine to wrap the |
- // value, and hence avoid it overflowing! |
- var kMinLengthBeforeWrap = 20; |
- |
- addText(td, text.substr(0, kMinLengthBeforeWrap)); |
- for (var i = kMinLengthBeforeWrap; i < text.length; ++i) { |
- addNode(td, 'wbr'); |
- addText(td, text.substr(i, 1)); |
- } |
- } |
- |
- // -------------------------------------------------------------------------- |
- // Helper code for handling the sort and grouping dropdowns. |
- // -------------------------------------------------------------------------- |
- |
- function addOptionsForGroupingSelect(select) { |
- // Add "no group" choice. |
- addNode(select, 'option', '---').value = ''; |
- |
- for (var i = 0; i < GROUPING_DROPDOWN_CHOICES.length; ++i) { |
- var key = GROUPING_DROPDOWN_CHOICES[i]; |
- var option = addNode(select, 'option', getNameForKey(key)); |
- option.value = key; |
- } |
- } |
- |
- function addOptionsForSortingSelect(select) { |
- // Add "no sort" choice. |
- addNode(select, 'option', '---').value = ''; |
- |
- // Add a divider. |
- addNode(select, 'optgroup').label = ''; |
- |
- for (var i = 0; i < SORT_DROPDOWN_CHOICES.length; ++i) { |
- var key = SORT_DROPDOWN_CHOICES[i]; |
- addNode(select, 'option', getNameForKey(key)).value = key; |
- } |
- |
- // Add a divider. |
- addNode(select, 'optgroup').label = ''; |
- |
- // Add the same options, but for descending. |
- for (var i = 0; i < SORT_DROPDOWN_CHOICES.length; ++i) { |
- var key = SORT_DROPDOWN_CHOICES[i]; |
- var n = addNode(select, 'option', getNameForKey(key) + ' (DESC)'); |
- n.value = reverseSortKey(key); |
- } |
- } |
- |
- /** |
- * Helper function used to update the sorting and grouping lists after a |
- * dropdown changes. |
- */ |
- function updateKeyListFromDropdown(list, i, select) { |
- // Update the list. |
- if (i < list.length) { |
- list[i] = select.value; |
- } else { |
- list.push(select.value); |
- } |
- |
- // Normalize the list, so setting 'none' as primary zeros out everything |
- // else. |
- for (var i = 0; i < list.length; ++i) { |
- if (list[i] == '') { |
- list.splice(i, list.length - i); |
- break; |
- } |
- } |
- } |
- |
- /** |
- * Comparator for property |key|, having values |value1| and |value2|. |
- * If the key has defined a custom comparator use it. Otherwise use a |
- * default "less than" comparison. |
- */ |
- function compareValuesForKey(key, value1, value2) { |
- var comparator = KEY_PROPERTIES[key].comparator; |
- if (comparator) |
- return comparator(value1, value2); |
- return simpleCompare(value1, value2); |
- } |
- |
- function reverseSortKey(key) { |
- return -key; |
- } |
- |
- function sortKeyIsReversed(key) { |
- return key < 0; |
- } |
- |
- function sortKeysMatch(key1, key2) { |
- return Math.abs(key1) == Math.abs(key2); |
- } |
- |
- function getKeysForCheckedBoxes(checkboxes) { |
- var keys = []; |
- for (var k in checkboxes) { |
- if (checkboxes[k].checked) |
- keys.push(k); |
- } |
- return keys; |
- } |
- |
- // -------------------------------------------------------------------------- |
- |
- /** |
- * @constructor |
- */ |
- function MainView() { |
- // Make sure we have a definition for each key. |
- for (var k = BEGIN_KEY; k < END_KEY; ++k) { |
- if (!KEY_PROPERTIES[k]) |
- throw 'KEY_PROPERTIES[] not defined for key: ' + k; |
- } |
- |
- this.init_(); |
- } |
- |
- MainView.prototype = { |
- addDataToSnapshot: function(data) { |
- // TODO(eroman): We need to know which snapshot this data belongs to! |
- // For now we assume it is the most recent snapshot. |
- var snapshotIndex = this.snapshots_.length - 1; |
- |
- var snapshot = this.snapshots_[snapshotIndex]; |
- |
- var pid = data.process_id; |
- var ptype = data.process_type; |
- |
- // Augment each data row with the process information. |
- var rows = data.list; |
- for (var i = 0; i < rows.length; ++i) { |
- // Transform the data from a dictionary to an array. This internal |
- // representation is more compact and faster to access. |
- var origRow = rows[i]; |
- var newRow = []; |
- |
- newRow[KEY_PROCESS_ID] = pid; |
- newRow[KEY_PROCESS_TYPE] = ptype; |
- |
- // Copy over the known properties which have a 1:1 mapping with JSON. |
- for (var k = BEGIN_KEY; k < END_KEY; ++k) { |
- var inputJsonKey = KEY_PROPERTIES[k].inputJsonKey; |
- if (inputJsonKey != undefined) { |
- newRow[k] = getPropertyByPath(origRow, inputJsonKey); |
- } |
- } |
- |
- if (newRow[KEY_COUNT] == 0) { |
- // When resetting the data, it is possible for the backend to give us |
- // counts of "0". There is no point adding these rows (in fact they |
- // will cause us to do divide by zeros when calculating averages and |
- // stuff), so we skip past them. |
- continue; |
- } |
- |
- // Add our computed properties. |
- augmentDataRow(newRow); |
- |
- snapshot.flatData.push(newRow); |
- } |
- |
- if (!arrayToSet(this.getSelectedSnapshotIndexes_())[snapshotIndex]) { |
- // Optimization: If this snapshot is not a data dependency for the |
- // current display, then don't bother updating anything. |
- return; |
- } |
- |
- // We may end up calling addDataToSnapshot_() repeatedly (once for each |
- // process). To avoid this from slowing us down we do bulk updates on a |
- // timer. |
- this.updateFlatDataSoon_(); |
- }, |
- |
- updateFlatDataSoon_: function() { |
- if (this.updateFlatDataPending_) { |
- // If a delayed task has already been posted to re-merge the data, |
- // then we don't need to do anything extra. |
- return; |
- } |
- |
- // Otherwise schedule updateFlatData_() to be called later. We want it to |
- // be called no more than once every PROCESS_DATA_DELAY_MS milliseconds. |
- |
- if (this.lastUpdateFlatDataTime_ == undefined) |
- this.lastUpdateFlatDataTime_ = 0; |
- |
- var timeSinceLastMerge = getTimeMillis() - this.lastUpdateFlatDataTime_; |
- var timeToWait = Math.max(0, PROCESS_DATA_DELAY_MS - timeSinceLastMerge); |
- |
- var functionToRun = function() { |
- // Do the actual update. |
- this.updateFlatData_(); |
- // Keep track of when we last ran. |
- this.lastUpdateFlatDataTime_ = getTimeMillis(); |
- this.updateFlatDataPending_ = false; |
- }.bind(this); |
- |
- this.updateFlatDataPending_ = true; |
- window.setTimeout(functionToRun, timeToWait); |
- }, |
- |
- /** |
- * Returns a list of the currently selected snapshots. This list is |
- * guaranteed to be of length 1 or 2. |
- */ |
- getSelectedSnapshotIndexes_: function() { |
- var indexes = this.getSelectedSnapshotBoxes_(); |
- for (var i = 0; i < indexes.length; ++i) |
- indexes[i] = indexes[i].__index; |
- return indexes; |
- }, |
- |
- /** |
- * Same as getSelectedSnapshotIndexes_(), only it returns the actual |
- * checkbox input DOM nodes rather than the snapshot ID. |
- */ |
- getSelectedSnapshotBoxes_: function() { |
- // Figure out which snaphots to use for our data. |
- var boxes = []; |
- for (var i = 0; i < this.snapshots_.length; ++i) { |
- var box = this.getSnapshotCheckbox_(i); |
- if (box.checked) |
- boxes.push(box); |
- } |
- return boxes; |
- }, |
- |
- /** |
- * This function should be called any time a snapshot dependency for what is |
- * being displayed on the screen has changed. It will re-calculate the |
- * difference between the two snapshots and update flatData_. |
- */ |
- updateFlatData_: function() { |
- var summaryDiv = $(SNAPSHOT_SELECTION_SUMMARY_ID); |
- |
- var selectedSnapshots = this.getSelectedSnapshotIndexes_(); |
- if (selectedSnapshots.length == 1) { |
- // If only one snapshot is chosen then we will display that snapshot's |
- // data in its entirety. |
- this.flatData_ = this.snapshots_[selectedSnapshots[0]].flatData; |
- |
- // Don't bother displaying any text when just 1 snapshot is selected, |
- // since it is obvious what this should do. |
- summaryDiv.innerText = ''; |
- } else if (selectedSnapshots.length == 2) { |
- // Otherwise if two snapshots were chosen, show the difference between |
- // them. |
- var snapshot1 = this.snapshots_[selectedSnapshots[0]]; |
- var snapshot2 = this.snapshots_[selectedSnapshots[1]]; |
- |
- this.flatData_ = |
- subtractSnapshots(snapshot1.flatData, snapshot2.flatData); |
- |
- var timeDeltaInSeconds = |
- ((snapshot2.time - snapshot1.time) / 1000).toFixed(0); |
- |
- // Explain that what is being shown is the difference between two |
- // snapshots. |
- summaryDiv.innerText = |
- 'Showing the difference between snapshots #' + |
- selectedSnapshots[0] + ' and #' + |
- selectedSnapshots[1] + ' (' + timeDeltaInSeconds + |
- ' seconds worth of data)'; |
- } else { |
- // This shouldn't be possible... |
- throw 'Unexpected number of selected snapshots'; |
- } |
- |
- // Recompute mergedData_ (since it is derived from flatData_) |
- this.updateMergedData_(); |
- }, |
- |
- updateMergedData_: function() { |
- // Recompute mergedData_. |
- this.mergedData_ = mergeRows(this.flatData_, |
- this.getMergeColumns_(), |
- this.shouldMergeSimilarThreads_()); |
- |
- // Recompute filteredData_ (since it is derived from mergedData_) |
- this.updateFilteredData_(); |
- }, |
- |
- updateFilteredData_: function() { |
- // Recompute filteredData_. |
- this.filteredData_ = []; |
- var filterFunc = this.getFilterFunction_(); |
- for (var i = 0; i < this.mergedData_.length; ++i) { |
- var r = this.mergedData_[i]; |
- if (!filterFunc(r)) { |
- // Not matched by our filter, discard. |
- continue; |
- } |
- this.filteredData_.push(r); |
- } |
- |
- // Recompute groupedData_ (since it is derived from filteredData_) |
- this.updateGroupedData_(); |
- }, |
- |
- updateGroupedData_: function() { |
- // Recompute groupedData_. |
- var groupKeyToData = {}; |
- var entryToGroupKeyFunc = this.getGroupingFunction_(); |
- for (var i = 0; i < this.filteredData_.length; ++i) { |
- var r = this.filteredData_[i]; |
- |
- var groupKey = entryToGroupKeyFunc(r); |
- |
- var groupData = groupKeyToData[groupKey]; |
- if (!groupData) { |
- groupData = { |
- key: JSON.parse(groupKey), |
- aggregates: initializeAggregates(ALL_KEYS), |
- rows: [], |
- }; |
- groupKeyToData[groupKey] = groupData; |
- } |
- |
- // Add the row to our list. |
- groupData.rows.push(r); |
- |
- // Update aggregates for each column. |
- consumeAggregates(groupData.aggregates, r); |
- } |
- this.groupedData_ = groupKeyToData; |
- |
- // Figure out a display order for the groups themselves. |
- this.sortedGroupKeys_ = getDictionaryKeys(groupKeyToData); |
- this.sortedGroupKeys_.sort(this.getGroupSortingFunction_()); |
- |
- // Sort the group data. |
- this.sortGroupedData_(); |
- }, |
- |
- sortGroupedData_: function() { |
- var sortingFunc = this.getSortingFunction_(); |
- for (var k in this.groupedData_) |
- this.groupedData_[k].rows.sort(sortingFunc); |
- |
- // Every cached data dependency is now up to date, all that is left is |
- // to actually draw the result. |
- this.redrawData_(); |
- }, |
- |
- getVisibleColumnKeys_: function() { |
- // Figure out what columns to include, based on the selected checkboxes. |
- var columns = this.getSelectionColumns_(); |
- columns = columns.slice(0); |
- |
- // Eliminate columns which we are merging on. |
- deleteValuesFromArray(columns, this.getMergeColumns_()); |
- |
- // Eliminate columns which we are grouped on. |
- if (this.sortedGroupKeys_.length > 0) { |
- // The grouping will be the the same for each so just pick the first. |
- var randomGroupKey = this.groupedData_[this.sortedGroupKeys_[0]].key; |
- |
- // The grouped properties are going to be the same for each row in our, |
- // table, so avoid drawing them in our table! |
- var keysToExclude = [] |
- |
- for (var i = 0; i < randomGroupKey.length; ++i) |
- keysToExclude.push(randomGroupKey[i].key); |
- deleteValuesFromArray(columns, keysToExclude); |
- } |
- |
- // If we are currently showing a "diff", hide the max columns, since we |
- // are not populating it correctly. See the TODO at the top of this file. |
- if (this.getSelectedSnapshotIndexes_().length > 1) |
- deleteValuesFromArray(columns, [KEY_MAX_RUN_TIME, KEY_MAX_QUEUE_TIME]); |
- |
- return columns; |
- }, |
- |
- redrawData_: function() { |
- // Clear the results div, sine we may be overwriting older data. |
- var parent = $(RESULTS_DIV_ID); |
- parent.innerHTML = ''; |
- |
- var columns = this.getVisibleColumnKeys_(); |
- |
- // Draw each group. |
- for (var i = 0; i < this.sortedGroupKeys_.length; ++i) { |
- var k = this.sortedGroupKeys_[i]; |
- this.drawGroup_(parent, k, columns); |
- } |
- }, |
- |
- /** |
- * Renders the information for a particular group. |
- */ |
- drawGroup_: function(parent, groupKey, columns) { |
- var groupData = this.groupedData_[groupKey]; |
- |
- var div = addNode(parent, 'div'); |
- div.className = 'group-container'; |
- |
- this.drawGroupTitle_(div, groupData.key); |
- |
- var table = addNode(div, 'table'); |
- |
- this.drawDataTable_(table, groupData, columns, groupKey); |
- }, |
- |
- /** |
- * Draws a title into |parent| that describes |groupKey|. |
- */ |
- drawGroupTitle_: function(parent, groupKey) { |
- if (groupKey.length == 0) { |
- // Empty group key means there was no grouping. |
- return; |
- } |
- |
- var parent = addNode(parent, 'div'); |
- parent.className = 'group-title-container'; |
- |
- // Each component of the group key represents the "key=value" constraint |
- // for this group. Show these as an AND separated list. |
- for (var i = 0; i < groupKey.length; ++i) { |
- if (i > 0) |
- addNode(parent, 'i', ' and '); |
- var e = groupKey[i]; |
- addNode(parent, 'b', getNameForKey(e.key) + ' = '); |
- addNode(parent, 'span', e.value); |
- } |
- }, |
- |
- /** |
- * Renders a table which summarizes all |column| fields for |data|. |
- */ |
- drawDataTable_: function(table, data, columns, groupKey) { |
- table.className = 'results-table'; |
- var thead = addNode(table, 'thead'); |
- var tbody = addNode(table, 'tbody'); |
- |
- var displaySettings = this.getGroupDisplaySettings_(groupKey); |
- var limit = displaySettings.limit; |
- |
- this.drawAggregateRow_(thead, data.aggregates, columns); |
- this.drawTableHeader_(thead, columns); |
- this.drawTableBody_(tbody, data.rows, columns, limit); |
- this.drawTruncationRow_(tbody, data.rows.length, limit, columns.length, |
- groupKey); |
- }, |
- |
- drawTableHeader_: function(thead, columns) { |
- var tr = addNode(thead, 'tr'); |
- for (var i = 0; i < columns.length; ++i) { |
- var key = columns[i]; |
- var th = addNode(tr, 'th', getNameForKey(key)); |
- th.onclick = this.onClickColumn_.bind(this, key); |
- |
- // Draw an indicator if we are currently sorted on this column. |
- // TODO(eroman): Should use an icon instead of asterisk! |
- for (var j = 0; j < this.currentSortKeys_.length; ++j) { |
- if (sortKeysMatch(this.currentSortKeys_[j], key)) { |
- var sortIndicator = addNode(th, 'span', '*'); |
- sortIndicator.style.color = 'red'; |
- if (sortKeyIsReversed(this.currentSortKeys_[j])) { |
- // Use double-asterisk for descending columns. |
- addText(sortIndicator, '*'); |
- } |
- break; |
- } |
- } |
- } |
- }, |
- |
- drawTableBody_: function(tbody, rows, columns, limit) { |
- for (var i = 0; i < rows.length && i < limit; ++i) { |
- var e = rows[i]; |
- |
- var tr = addNode(tbody, 'tr'); |
- |
- for (var c = 0; c < columns.length; ++c) { |
- var key = columns[c]; |
- var value = e[key]; |
- |
- var td = addNode(tr, 'td'); |
- drawValueToCell(td, key, value); |
- } |
- } |
- }, |
- |
- /** |
- * Renders a row that describes all the aggregate values for |columns|. |
- */ |
- drawAggregateRow_: function(tbody, aggregates, columns) { |
- var tr = addNode(tbody, 'tr'); |
- tr.className = 'aggregator-row'; |
- |
- for (var i = 0; i < columns.length; ++i) { |
- var key = columns[i]; |
- var td = addNode(tr, 'td'); |
- |
- // Most of our outputs are numeric, so we want to align them to the |
- // right. However for the unique counts we will center. |
- if (KEY_PROPERTIES[key].aggregator == UniquifyAggregator) { |
- td.align = 'center'; |
- } else { |
- td.align = 'right'; |
- } |
- |
- var aggregator = aggregates[key]; |
- if (aggregator) |
- td.innerText = aggregator.getValueAsText(); |
- } |
- }, |
- |
- /** |
- * Renders a row which describes how many rows the table has, how many are |
- * currently hidden, and a set of buttons to show more. |
- */ |
- drawTruncationRow_: function(tbody, numRows, limit, numColumns, groupKey) { |
- var numHiddenRows = Math.max(numRows - limit, 0); |
- var numVisibleRows = numRows - numHiddenRows; |
- |
- var tr = addNode(tbody, 'tr'); |
- tr.className = 'truncation-row'; |
- var td = addNode(tr, 'td'); |
- td.colSpan = numColumns; |
- |
- addText(td, numRows + ' rows'); |
- if (numHiddenRows > 0) { |
- var s = addNode(td, 'span', ' (' + numHiddenRows + ' hidden) '); |
- s.style.color = 'red'; |
- } |
- |
- if (numVisibleRows > LIMIT_INCREMENT) { |
- addNode(td, 'button', 'Show less').onclick = |
- this.changeGroupDisplayLimit_.bind( |
- this, groupKey, -LIMIT_INCREMENT); |
- } |
- if (numVisibleRows > 0) { |
- addNode(td, 'button', 'Show none').onclick = |
- this.changeGroupDisplayLimit_.bind(this, groupKey, -Infinity); |
- } |
- |
- if (numHiddenRows > 0) { |
- addNode(td, 'button', 'Show more').onclick = |
- this.changeGroupDisplayLimit_.bind(this, groupKey, LIMIT_INCREMENT); |
- addNode(td, 'button', 'Show all').onclick = |
- this.changeGroupDisplayLimit_.bind(this, groupKey, Infinity); |
- } |
- }, |
- |
- /** |
- * Adjusts the row limit for group |groupKey| by |delta|. |
- */ |
- changeGroupDisplayLimit_: function(groupKey, delta) { |
- // Get the current settings for this group. |
- var settings = this.getGroupDisplaySettings_(groupKey, true); |
- |
- // Compute the adjusted limit. |
- var newLimit = settings.limit; |
- var totalNumRows = this.groupedData_[groupKey].rows.length; |
- newLimit = Math.min(totalNumRows, newLimit); |
- newLimit += delta; |
- newLimit = Math.max(0, newLimit); |
- |
- // Update the settings with the new limit. |
- settings.limit = newLimit; |
- |
- // TODO(eroman): It isn't necessary to redraw *all* the data. Really we |
- // just need to insert the missing rows (everything else stays the same)! |
- this.redrawData_(); |
- }, |
- |
- /** |
- * Returns the rendering settings for group |groupKey|. This includes things |
- * like how many rows to display in the table. |
- */ |
- getGroupDisplaySettings_: function(groupKey, opt_create) { |
- var settings = this.groupDisplaySettings_[groupKey]; |
- if (!settings) { |
- // If we don't have any settings for this group yet, create some |
- // default ones. |
- if (groupKey == '[]') { |
- // (groupKey of '[]' is what we use for ungrouped data). |
- settings = {limit: INITIAL_UNGROUPED_ROW_LIMIT}; |
- } else { |
- settings = {limit: INITIAL_GROUP_ROW_LIMIT}; |
- } |
- if (opt_create) |
- this.groupDisplaySettings_[groupKey] = settings; |
- } |
- return settings; |
- }, |
- |
- init_: function() { |
- this.snapshots_ = []; |
- |
- // Start fetching the data from the browser; this will be our snapshot #0. |
- this.takeSnapshot_(); |
- |
- // Data goes through the following pipeline: |
- // (1) Raw data received from browser, and transformed into our own |
- // internal row format (where properties are indexed by KEY_* |
- // constants.) |
- // (2) We "augment" each row by adding some extra computed columns |
- // (like averages). |
- // (3) The rows are merged using current merge settings. |
- // (4) The rows that don't match current search expression are |
- // tossed out. |
- // (5) The rows are organized into "groups" based on current settings, |
- // and aggregate values are computed for each resulting group. |
- // (6) The rows within each group are sorted using current settings. |
- // (7) The grouped rows are drawn to the screen. |
- this.flatData_ = []; |
- this.mergedData_ = []; |
- this.filteredData_ = []; |
- this.groupedData_ = {}; |
- this.sortedGroupKeys_ = []; |
- |
- this.groupDisplaySettings_ = {}; |
- |
- this.fillSelectionCheckboxes_($(COLUMN_TOGGLES_CONTAINER_ID)); |
- this.fillMergeCheckboxes_($(COLUMN_MERGE_TOGGLES_CONTAINER_ID)); |
- |
- $(FILTER_SEARCH_ID).onsearch = this.onChangedFilter_.bind(this); |
- |
- this.currentSortKeys_ = INITIAL_SORT_KEYS.slice(0); |
- this.currentGroupingKeys_ = INITIAL_GROUP_KEYS.slice(0); |
- |
- this.fillGroupingDropdowns_(); |
- this.fillSortingDropdowns_(); |
- |
- $(EDIT_COLUMNS_LINK_ID).onclick = |
- toggleNodeDisplay.bind(null, $(EDIT_COLUMNS_ROW)); |
- |
- $(TOGGLE_SNAPSHOTS_LINK_ID).onclick = |
- toggleNodeDisplay.bind(null, $(SNAPSHOTS_ROW)); |
- |
- $(MERGE_SIMILAR_THREADS_CHECKBOX_ID).onchange = |
- this.onMergeSimilarThreadsCheckboxChanged_.bind(this); |
- |
- $(RESET_DATA_LINK_ID).onclick = |
- g_browserBridge.sendResetData.bind(g_browserBridge); |
- |
- $(TAKE_SNAPSHOT_BUTTON_ID).onclick = this.takeSnapshot_.bind(this); |
- }, |
- |
- takeSnapshot_: function() { |
- // Start a new empty snapshot. Make note of the current time, so we know |
- // when the snaphot was taken. |
- this.snapshots_.push({flatData: [], time: getTimeMillis()}); |
- |
- // Update the UI to reflect the new snapshot. |
- this.addSnapshotToList_(this.snapshots_.length - 1); |
- |
- // Ask the browser for the profiling data. We will receive the data |
- // later through a callback to addDataToSnapshot_(). |
- g_browserBridge.sendGetData(); |
- }, |
- |
- getSnapshotCheckbox_: function(i) { |
- return $(this.getSnapshotCheckboxId_(i)); |
- }, |
- |
- getSnapshotCheckboxId_: function(i) { |
- return 'snapshotCheckbox-' + i; |
- }, |
- |
- addSnapshotToList_: function(i) { |
- var tbody = $('snapshots-tbody'); |
- |
- var tr = addNode(tbody, 'tr'); |
- |
- var id = this.getSnapshotCheckboxId_(i); |
- |
- var checkboxCell = addNode(tr, 'td'); |
- var checkbox = addNode(checkboxCell, 'input'); |
- checkbox.type = 'checkbox'; |
- checkbox.id = id; |
- checkbox.__index = i; |
- checkbox.onclick = this.onSnapshotCheckboxChanged_.bind(this); |
- |
- addNode(tr, 'td', '#' + i); |
- |
- var labelCell = addNode(tr, 'td'); |
- var l = addNode(labelCell, 'label'); |
- |
- var dateString = new Date(this.snapshots_[i].time).toLocaleString(); |
- addText(l, dateString); |
- l.htmlFor = id; |
- |
- // If we are on snapshot 0, make it the default. |
- if (i == 0) { |
- checkbox.checked = true; |
- checkbox.__time = getTimeMillis(); |
- this.updateSnapshotCheckboxStyling_(); |
- } |
- }, |
- |
- updateSnapshotCheckboxStyling_: function() { |
- for (var i = 0; i < this.snapshots_.length; ++i) { |
- var checkbox = this.getSnapshotCheckbox_(i); |
- checkbox.parentNode.parentNode.className = |
- checkbox.checked ? 'selected_snapshot' : ''; |
- } |
- }, |
- |
- onSnapshotCheckboxChanged_: function(event) { |
- // Keep track of when we clicked this box (for when we need to uncheck |
- // older boxes). |
- event.target.__time = getTimeMillis(); |
- |
- // Find all the checked boxes. Either 1 or 2 can be checked. If a third |
- // was just checked, then uncheck one of the earlier ones so we only have |
- // 2. |
- var checked = this.getSelectedSnapshotBoxes_(); |
- checked.sort(function(a, b) { return b.__time - a.__time; }); |
- if (checked.length > 2) { |
- for (var i = 2; i < checked.length; ++i) |
- checked[i].checked = false; |
- checked.length = 2; |
- } |
- |
- // We should always have at least 1 selection. Prevent the user from |
- // unselecting the final box. |
- if (checked.length == 0) |
- event.target.checked = true; |
- |
- this.updateSnapshotCheckboxStyling_(); |
- |
- // Recompute flatData_ (since it is derived from selected snapshots). |
- this.updateFlatData_(); |
- }, |
- |
- fillSelectionCheckboxes_: function(parent) { |
- this.selectionCheckboxes_ = {}; |
- |
- var onChangeFunc = this.onSelectCheckboxChanged_.bind(this); |
- |
- for (var i = 0; i < ALL_TABLE_COLUMNS.length; ++i) { |
- var key = ALL_TABLE_COLUMNS[i]; |
- var checkbox = addLabeledCheckbox(parent, getNameForKey(key)); |
- checkbox.checked = true; |
- checkbox.onchange = onChangeFunc; |
- addText(parent, ' '); |
- this.selectionCheckboxes_[key] = checkbox; |
- } |
- |
- for (var i = 0; i < INITIALLY_HIDDEN_KEYS.length; ++i) { |
- this.selectionCheckboxes_[INITIALLY_HIDDEN_KEYS[i]].checked = false; |
- } |
- }, |
- |
- getSelectionColumns_: function() { |
- return getKeysForCheckedBoxes(this.selectionCheckboxes_); |
- }, |
- |
- getMergeColumns_: function() { |
- return getKeysForCheckedBoxes(this.mergeCheckboxes_); |
- }, |
- |
- shouldMergeSimilarThreads_: function() { |
- return $(MERGE_SIMILAR_THREADS_CHECKBOX_ID).checked; |
- }, |
- |
- fillMergeCheckboxes_: function(parent) { |
- this.mergeCheckboxes_ = {}; |
- |
- var onChangeFunc = this.onMergeCheckboxChanged_.bind(this); |
- |
- for (var i = 0; i < MERGEABLE_KEYS.length; ++i) { |
- var key = MERGEABLE_KEYS[i]; |
- var checkbox = addLabeledCheckbox(parent, getNameForKey(key)); |
- checkbox.onchange = onChangeFunc; |
- addText(parent, ' '); |
- this.mergeCheckboxes_[key] = checkbox; |
- } |
- |
- for (var i = 0; i < INITIALLY_MERGED_KEYS.length; ++i) { |
- this.mergeCheckboxes_[INITIALLY_MERGED_KEYS[i]].checked = true; |
- } |
- }, |
- |
- fillGroupingDropdowns_: function() { |
- var parent = $(GROUP_BY_CONTAINER_ID); |
- parent.innerHTML = ''; |
- |
- for (var i = 0; i <= this.currentGroupingKeys_.length; ++i) { |
- // Add a dropdown. |
- var select = addNode(parent, 'select'); |
- select.onchange = this.onChangedGrouping_.bind(this, select, i); |
- |
- addOptionsForGroupingSelect(select); |
- |
- if (i < this.currentGroupingKeys_.length) { |
- var key = this.currentGroupingKeys_[i]; |
- setSelectedOptionByValue(select, key); |
- } |
- } |
- }, |
- |
- fillSortingDropdowns_: function() { |
- var parent = $(SORT_BY_CONTAINER_ID); |
- parent.innerHTML = ''; |
- |
- for (var i = 0; i <= this.currentSortKeys_.length; ++i) { |
- // Add a dropdown. |
- var select = addNode(parent, 'select'); |
- select.onchange = this.onChangedSorting_.bind(this, select, i); |
- |
- addOptionsForSortingSelect(select); |
- |
- if (i < this.currentSortKeys_.length) { |
- var key = this.currentSortKeys_[i]; |
- setSelectedOptionByValue(select, key); |
- } |
- } |
- }, |
- |
- onChangedGrouping_: function(select, i) { |
- updateKeyListFromDropdown(this.currentGroupingKeys_, i, select); |
- this.fillGroupingDropdowns_(); |
- this.updateGroupedData_(); |
- }, |
- |
- onChangedSorting_: function(select, i) { |
- updateKeyListFromDropdown(this.currentSortKeys_, i, select); |
- this.fillSortingDropdowns_(); |
- this.sortGroupedData_(); |
- }, |
- |
- onSelectCheckboxChanged_: function() { |
- this.redrawData_(); |
- }, |
- |
- onMergeCheckboxChanged_: function() { |
- this.updateMergedData_(); |
- }, |
- |
- onMergeSimilarThreadsCheckboxChanged_: function() { |
- this.updateMergedData_(); |
- }, |
- |
- onChangedFilter_: function() { |
- this.updateFilteredData_(); |
- }, |
- |
- /** |
- * When left-clicking a column, change the primary sort order to that |
- * column. If we were already sorted on that column then reverse the order. |
- * |
- * When alt-clicking, add a secondary sort column. Similarly, if |
- * alt-clicking a column which was already being sorted on, reverse its |
- * order. |
- */ |
- onClickColumn_: function(key, event) { |
- // If this property wants to start off in descending order rather then |
- // ascending, flip it. |
- if (KEY_PROPERTIES[key].sortDescending) |
- key = reverseSortKey(key); |
- |
- // Scan through our sort order and see if we are already sorted on this |
- // key. If so, reverse that sort ordering. |
- var found_i = -1; |
- for (var i = 0; i < this.currentSortKeys_.length; ++i) { |
- var curKey = this.currentSortKeys_[i]; |
- if (sortKeysMatch(curKey, key)) { |
- this.currentSortKeys_[i] = reverseSortKey(curKey); |
- found_i = i; |
- break; |
- } |
- } |
- |
- if (event.altKey) { |
- if (found_i == -1) { |
- // If we weren't already sorted on the column that was alt-clicked, |
- // then add it to our sort. |
- this.currentSortKeys_.push(key); |
- } |
- } else { |
- if (found_i != 0 || |
- !sortKeysMatch(this.currentSortKeys_[found_i], key)) { |
- // If the column we left-clicked wasn't already our primary column, |
- // make it so. |
- this.currentSortKeys_ = [key]; |
- } else { |
- // If the column we left-clicked was already our primary column (and |
- // we just reversed it), remove any secondary sorts. |
- this.currentSortKeys_.length = 1; |
- } |
- } |
- |
- this.fillSortingDropdowns_(); |
- this.sortGroupedData_(); |
- }, |
- |
- getSortingFunction_: function() { |
- var sortKeys = this.currentSortKeys_.slice(0); |
- |
- // Eliminate the empty string keys (which means they were unspecified). |
- deleteValuesFromArray(sortKeys, ['']); |
- |
- // If no sort is specified, use our default sort. |
- if (sortKeys.length == 0) |
- sortKeys = [DEFAULT_SORT_KEYS]; |
- |
- return function(a, b) { |
- for (var i = 0; i < sortKeys.length; ++i) { |
- var key = Math.abs(sortKeys[i]); |
- var factor = sortKeys[i] < 0 ? -1 : 1; |
- |
- var propA = a[key]; |
- var propB = b[key]; |
- |
- var comparison = compareValuesForKey(key, propA, propB); |
- comparison *= factor; // Possibly reverse the ordering. |
- |
- if (comparison != 0) |
- return comparison; |
- } |
- |
- // Tie breaker. |
- return simpleCompare(JSON.stringify(a), JSON.stringify(b)); |
- }; |
- }, |
- |
- getGroupSortingFunction_: function() { |
- return function(a, b) { |
- var groupKey1 = JSON.parse(a); |
- var groupKey2 = JSON.parse(b); |
- |
- for (var i = 0; i < groupKey1.length; ++i) { |
- var comparison = compareValuesForKey( |
- groupKey1[i].key, |
- groupKey1[i].value, |
- groupKey2[i].value); |
- |
- if (comparison != 0) |
- return comparison; |
- } |
- |
- // Tie breaker. |
- return simpleCompare(a, b); |
- }; |
- }, |
- |
- getFilterFunction_: function() { |
- var searchStr = $(FILTER_SEARCH_ID).value; |
- |
- // Normalize the search expression. |
- searchStr = trimWhitespace(searchStr); |
- searchStr = searchStr.toLowerCase(); |
- |
- return function(x) { |
- // Match everything when there was no filter. |
- if (searchStr == '') |
- return true; |
- |
- // Treat the search text as a LOWERCASE substring search. |
- for (var k = BEGIN_KEY; k < END_KEY; ++k) { |
- var propertyText = getTextValueForProperty(k, x[k]); |
- if (propertyText.toLowerCase().indexOf(searchStr) != -1) |
- return true; |
- } |
- |
- return false; |
- }; |
- }, |
- |
- getGroupingFunction_: function() { |
- var groupings = this.currentGroupingKeys_.slice(0); |
- |
- // Eliminate the empty string groupings (which means they were |
- // unspecified). |
- deleteValuesFromArray(groupings, ['']); |
- |
- // Eliminate duplicate primary/secondary group by directives, since they |
- // are redundant. |
- deleteDuplicateStringsFromArray(groupings); |
- |
- return function(e) { |
- var groupKey = []; |
- |
- for (var i = 0; i < groupings.length; ++i) { |
- var entry = {key: groupings[i], |
- value: e[groupings[i]]}; |
- groupKey.push(entry); |
- } |
- |
- return JSON.stringify(groupKey); |
- }; |
- }, |
- }; |
- |
- return MainView; |
-})(); |