Index: chrome/browser/resources/profiler.js |
=================================================================== |
--- chrome/browser/resources/profiler.js (revision 111053) |
+++ chrome/browser/resources/profiler.js (working copy) |
@@ -5,15 +5,17 @@ |
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(); |
- |
- // Ask the browser to send us the current data. |
- g_browserBridge.sendGetData(); |
} |
document.addEventListener('DOMContentLoaded', onLoad); |
@@ -49,7 +51,9 @@ |
//-------------------------------------------------------------------------- |
receivedData: function(data) { |
- g_mainView.addData(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); |
}, |
}; |
@@ -97,6 +101,11 @@ |
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 |
// -------------------------------------------------------------------------- |
@@ -277,6 +286,14 @@ |
'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 |
@@ -309,6 +326,11 @@ |
* [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 = []; |
@@ -363,6 +385,7 @@ |
textPrinter: formatNumberAsText, |
inputJsonKey: 'death_data.count', |
aggregator: SumAggregator, |
+ diff: diffFuncForCount, |
}; |
KEY_PROPERTIES[KEY_QUEUE_TIME] = { |
@@ -372,6 +395,7 @@ |
textPrinter: formatNumberAsText, |
inputJsonKey: 'death_data.queue_ms', |
aggregator: SumAggregator, |
+ diff: diffFuncForCount, |
}; |
KEY_PROPERTIES[KEY_MAX_QUEUE_TIME] = { |
@@ -381,6 +405,7 @@ |
textPrinter: formatNumberAsText, |
inputJsonKey: 'death_data.queue_ms_max', |
aggregator: MaxAggregator, |
+ diff: diffFuncForMax, |
}; |
KEY_PROPERTIES[KEY_RUN_TIME] = { |
@@ -390,6 +415,7 @@ |
textPrinter: formatNumberAsText, |
inputJsonKey: 'death_data.run_ms', |
aggregator: SumAggregator, |
+ diff: diffFuncForCount, |
}; |
KEY_PROPERTIES[KEY_AVG_RUN_TIME] = { |
@@ -407,6 +433,7 @@ |
textPrinter: formatNumberAsText, |
inputJsonKey: 'death_data.run_ms_max', |
aggregator: MaxAggregator, |
+ diff: diffFuncForMax, |
}; |
KEY_PROPERTIES[KEY_AVG_QUEUE_TIME] = { |
@@ -761,6 +788,17 @@ |
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. |
// -------------------------------------------------------------------------- |
@@ -769,9 +807,13 @@ |
* 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]; |
- e[KEY_SOURCE_LOCATION] = e[KEY_FILE_NAME] + ' [' + e[KEY_LINE_NUMBER] + ']'; |
} |
/** |
@@ -796,6 +838,26 @@ |
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 |
@@ -837,23 +899,9 @@ |
deleteValuesFromArray(aggregateKeys, IDENTITY_KEYS); |
// Group all the identical rows together, bucketed into |identicalRows|. |
- var identicalRows = {}; |
- for (var i = 0; i < origRows.length; ++i) { |
- var e = origRows[i]; |
+ var identicalRows = |
+ bucketIdenticalRows(origRows, identityKeys, propertyGetterFunc); |
- var rowIdentity = []; |
- for (var j = 0; j < identityKeys.length; ++j) |
- rowIdentity.push(propertyGetterFunc(e, identityKeys[j])); |
- rowIdentity = rowIdentity.join('\n'); |
- |
- var l = identicalRows[rowIdentity]; |
- if (!l) { |
- l = []; |
- identicalRows[rowIdentity] = l; |
- } |
- l.push(e); |
- } |
- |
var mergedRows = []; |
// Merge the rows and save the results to |mergedRows|. |
@@ -884,6 +932,85 @@ |
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 |
// -------------------------------------------------------------------------- |
@@ -1060,7 +1187,13 @@ |
} |
MainView.prototype = { |
- addData: function(data) { |
+ 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; |
@@ -1094,42 +1227,120 @@ |
// Add our computed properties. |
augmentDataRow(newRow); |
- this.flatData_.push(newRow); |
+ snapshot.flatData.push(newRow); |
} |
- // We may end up calling addData() repeatedly (once for each process). |
- // To avoid this from slowing us down we do bulk updates on a timer. |
- this.updateMergedDataSoon_(); |
+ 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_(); |
}, |
- updateMergedDataSoon_: function() { |
- if (this.updateMergedDataPending_) { |
+ 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 updateMergeData_() to be called later. We want it to |
+ // 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.lastUpdateMergedDataTime_ == undefined) |
- this.lastUpdateMergedDataTime_ = 0; |
+ if (this.lastUpdateFlatDataTime_ == undefined) |
+ this.lastUpdateFlatDataTime_ = 0; |
- var timeSinceLastMerge = getTimeMillis() - this.lastUpdateMergedDataTime_; |
+ var timeSinceLastMerge = getTimeMillis() - this.lastUpdateFlatDataTime_; |
var timeToWait = Math.max(0, PROCESS_DATA_DELAY_MS - timeSinceLastMerge); |
var functionToRun = function() { |
// Do the actual update. |
- this.updateMergedData_(); |
+ this.updateFlatData_(); |
// Keep track of when we last ran. |
- this.lastUpdateMergedDataTime_ = getTimeMillis(); |
- this.updateMergedDataPending_ = false; |
+ this.lastUpdateFlatDataTime_ = getTimeMillis(); |
+ this.updateFlatDataPending_ = false; |
}.bind(this); |
- this.updateMergedDataPending_ = true; |
+ 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_, |
@@ -1443,6 +1654,11 @@ |
}, |
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_* |
@@ -1475,24 +1691,108 @@ |
this.fillGroupingDropdowns_(); |
this.fillSortingDropdowns_(); |
- $(EDIT_COLUMNS_LINK_ID).onclick = this.toggleEditColumns_.bind(this); |
+ $(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); |
}, |
- toggleEditColumns_: function() { |
- var n = $(EDIT_COLUMNS_ROW); |
- if (n.style.display == '') { |
- n.style.display = 'none'; |
- } else { |
- n.style.display = ''; |
+ 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_ = {}; |