diff options
author | mmenke@chromium.org <mmenke@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-08-26 15:33:18 +0000 |
---|---|---|
committer | mmenke@chromium.org <mmenke@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-08-26 15:33:18 +0000 |
commit | 1ba022e96a6d96b22b8a5d19b423c39785641597 (patch) | |
tree | 14229893625bd7352cab767926a9bffb560e7a35 /chrome/browser/resources/net_internals | |
parent | 62e00e6db79104bf79d19cff1391bf271dbd0347 (diff) | |
download | chromium_src-1ba022e96a6d96b22b8a5d19b423c39785641597.zip chromium_src-1ba022e96a6d96b22b8a5d19b423c39785641597.tar.gz chromium_src-1ba022e96a6d96b22b8a5d19b423c39785641597.tar.bz2 |
Sorting added to net-internals. Also, search looks through all of a SourceEntry's text.
Add "sort:X" to the search field in the Requests view, and log entries will be sorted depending on "X". Possible values are "id", "desc", "source", "duration", and "active". "Active" sorts active entries first, longest active at the top, and inactive entries below, most recently active at the top. "Source" sorts sourceless entries using the largest source ID seen up to that point. The rest should be self-explanatory.
"sort:-X" will reverse the order of the sort.
New entries are sorted as they are added.
"is:active" shows only the active events, and "is:-active" only shows the inactive ones.
Bug=50764
Test=manual
Review URL: http://codereview.chromium.org/3165024
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@57525 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/resources/net_internals')
-rw-r--r-- | chrome/browser/resources/net_internals/index.html | 6 | ||||
-rw-r--r-- | chrome/browser/resources/net_internals/main.js | 12 | ||||
-rw-r--r-- | chrome/browser/resources/net_internals/requestsview.js | 327 | ||||
-rw-r--r-- | chrome/browser/resources/net_internals/sourceentry.js | 133 |
4 files changed, 457 insertions, 21 deletions
diff --git a/chrome/browser/resources/net_internals/index.html b/chrome/browser/resources/net_internals/index.html index be73f52..3a37aa0 100644 --- a/chrome/browser/resources/net_internals/index.html +++ b/chrome/browser/resources/net_internals/index.html @@ -195,9 +195,9 @@ function toggleMoreExplanation() { <thead> <tr> <td><input type=checkbox id=selectAll /></td> - <td>ID</td> - <td>Source</td> - <td width=99%>Description</td> + <td id=sortById>ID</td> + <td id=sortBySource>Source</td> + <td id=sortByDescription width=99%>Description</td> </tr> </thead> <!-- Requests table body: This is where request rows go into --> diff --git a/chrome/browser/resources/net_internals/main.js b/chrome/browser/resources/net_internals/main.js index 38b708a..d9c20ee 100644 --- a/chrome/browser/resources/net_internals/main.js +++ b/chrome/browser/resources/net_internals/main.js @@ -31,6 +31,9 @@ function onLoaded() { 'filterCount', 'deleteSelected', 'selectAll', + 'sortById', + 'sortBySource', + 'sortByDescription', // IDs for the details view. "detailsTabHandles", @@ -199,6 +202,9 @@ BrowserBridge.prototype.sendGetHttpCacheInfo = function() { //------------------------------------------------------------------------------ BrowserBridge.prototype.receivedLogEntry = function(logEntry) { + // Silently drop entries received before ready to receive them. + if (!this.areLogTypesReady_()) + return; if (!logEntry.wasPassivelyCaptured) this.activelyCapturedEvents_.push(logEntry); for (var i = 0; i < this.logObservers_.length; ++i) @@ -290,6 +296,12 @@ BrowserBridge.prototype.receivedHttpCacheInfo = function(info) { this.httpCacheInfo_.update(info); }; +BrowserBridge.prototype.areLogTypesReady_ = function() { + return (LogEventType != null && + LogEventPhase != null && + LogSourceType != null); +} + //------------------------------------------------------------------------------ /** diff --git a/chrome/browser/resources/net_internals/requestsview.js b/chrome/browser/resources/net_internals/requestsview.js index 27209613..677f60c 100644 --- a/chrome/browser/resources/net_internals/requestsview.js +++ b/chrome/browser/resources/net_internals/requestsview.js @@ -26,7 +26,8 @@ * @constructor */ function RequestsView(tableBodyId, filterInputId, filterCountId, - deleteSelectedId, selectAllId, + deleteSelectedId, selectAllId, sortByIdId, + sortBySourceTypeId, sortByDescriptionId, tabHandlesContainerId, logTabId, timelineTabId, detailsLogBoxId, detailsTimelineBoxId, topbarId, middleboxId, bottombarId, sizerId) { @@ -35,6 +36,9 @@ function RequestsView(tableBodyId, filterInputId, filterCountId, // Next unique id to be assigned to a log entry without a source. this.nextId_ = -1; + // Used for sorting entries with automatically assigned IDs. + this.maxReceivedSourceId_ = 0; + // Initialize the sub-views. var leftPane = new TopMidBottomView(new DivView(topbarId), new DivView(middleboxId), @@ -68,10 +72,21 @@ function RequestsView(tableBodyId, filterInputId, filterCountId, document.getElementById(selectAllId).addEventListener( 'click', this.selectAll_.bind(this), true); - this.currentFilter_ = ''; + document.getElementById(sortByIdId).addEventListener( + 'click', this.sortById_.bind(this), true); + + document.getElementById(sortBySourceTypeId).addEventListener( + 'click', this.sortBySourceType_.bind(this), true); + + document.getElementById(sortByDescriptionId).addEventListener( + 'click', this.sortByDescription_.bind(this), true); + this.numPrefilter_ = 0; this.numPostfilter_ = 0; + // Sets sort order and filter. + this.setFilter_(''); + this.invalidateFilterCounter_(); this.invalidateDetailsView_(); } @@ -91,12 +106,239 @@ RequestsView.prototype.show = function(isVisible) { this.splitterView_.show(isVisible); }; +RequestsView.prototype.getFilterText_ = function() { + return this.filterInput_.value; +}; + +RequestsView.prototype.setFilterText_ = function(filterText) { + this.filterInput_.value = filterText; + this.onFilterTextChanged_(); +}; + RequestsView.prototype.onFilterTextChanged_ = function() { - this.setFilter_(this.filterInput_.value); + this.setFilter_(this.getFilterText_()); +}; + +/** + * Sorts active entries first. If both entries are inactive, puts the one + * that was active most recently first. If both are active, uses source ID, + * which puts longer lived events at the top, and behaves better than using + * duration or time of first event. + */ +RequestsView.compareActive_ = function(source1, source2) { + if (source1.isActive() && !source2.isActive()) + return -1; + if (!source1.isActive() && source2.isActive()) + return 1; + if (!source1.isActive()) { + var deltaEndTime = source1.getEndTime() - source2.getEndTime(); + if (deltaEndTime != 0) { + // The one that ended most recently (Highest end time) should be sorted + // first. + return -deltaEndTime; + } + // If both ended at the same time, then odds are they were related events, + // started one after another, so sort in the opposite order of their + // source IDs to get a more intuitive ordering. + return -RequestsView.compareSourceId_(source1, source2); + } + return RequestsView.compareSourceId_(source1, source2); +}; + +RequestsView.compareDescription_ = function(source1, source2) { + var source1Text = source1.getDescription().toLowerCase(); + var source2Text = source2.getDescription().toLowerCase(); + var compareResult = source1Text.localeCompare(source2Text); + if (compareResult != 0) + return compareResult; + return RequestsView.compareSourceId_(source1, source2); +}; + +RequestsView.compareDuration_ = function(source1, source2) { + var durationDifference = source2.getDuration() - source1.getDuration(); + if (durationDifference) + return durationDifference; + return RequestsView.compareSourceId_(source1, source2); +}; + +/** + * For the purposes of sorting by source IDs, entries without a source + * appear right after the SourceEntry with the highest source ID received + * before the sourceless entry. Any ambiguities are resolved by ordering + * the entries without a source by the order in which they were received. + */ +RequestsView.compareSourceId_ = function(source1, source2) { + var sourceId1 = source1.getSourceId(); + if (sourceId1 < 0) + sourceId1 = source1.getMaxPreviousEntrySourceId(); + var sourceId2 = source2.getSourceId(); + if (sourceId2 < 0) + sourceId2 = source2.getMaxPreviousEntrySourceId(); + + if (sourceId1 != sourceId2) + return sourceId1 - sourceId2; + + // One or both have a negative ID. In either case, the source with the + // highest ID should be sorted first. + return source2.getSourceId() - source1.getSourceId(); +}; + +RequestsView.compareSourceType_ = function(source1, source2) { + var source1Text = source1.getSourceTypeString(); + var source2Text = source2.getSourceTypeString(); + var compareResult = source1Text.localeCompare(source2Text); + if (compareResult != 0) + return compareResult; + return RequestsView.compareSourceId_(source1, source2); +}; + +RequestsView.prototype.comparisonFuncWithReversing_ = function(a, b) { + var result = this.comparisonFunction_(a, b); + if (this.doSortBackwards_) + result *= -1; + return result; +}; + +RequestsView.comparisonFunctionTable_ = { + // sort: and sort:- are allowed + '': RequestsView.compareSourceId_, + 'active': RequestsView.compareActive_, + 'desc': RequestsView.compareDescription_, + 'description': RequestsView.compareDescription_, + 'duration': RequestsView.compareDuration_, + 'id': RequestsView.compareSourceId_, + 'source': RequestsView.compareSourceType_, + 'type': RequestsView.compareSourceType_ +}; + +RequestsView.prototype.Sort_ = function() { + var sourceEntries = []; + for (var id in this.sourceIdToEntryMap_) { + // Can only sort items with an actual row in the table. + if (this.sourceIdToEntryMap_[id].hasRow()) + sourceEntries.push(this.sourceIdToEntryMap_[id]); + } + sourceEntries.sort(this.comparisonFuncWithReversing_.bind(this)); + + for (var i = sourceEntries.length - 2; i >= 0; --i) { + if (sourceEntries[i].getNextNodeSourceId() != + sourceEntries[i + 1].getSourceId()) + sourceEntries[i].moveBefore(sourceEntries[i + 1]); + } +}; + +/** + * Looks for the first occurence of |directive|:parameter in |sourceText|. + * Parameter can be an empty string. + * + * On success, returns an object with two fields: + * |remainingText| - |sourceText| with |directive|:parameter removed, + and excess whitespace deleted. + * |parameter| - the parameter itself. + * + * On failure, returns null. + */ +RequestsView.prototype.parseDirective_ = function(sourceText, directive) { + // Check if at start of string. Doesn't need preceding whitespace. + var regExp = new RegExp('^\\s*' + directive + ':(\\S*)\\s*', 'i'); + var matchInfo = regExp.exec(sourceText); + if (matchInfo == null) { + // Check if not at start of string. Must be preceded by whitespace. + regExp = new RegExp('\\s+' + directive + ':(\\S*)\\s*', 'i'); + matchInfo = regExp.exec(sourceText); + if (matchInfo == null) + return null; + } + + return {'remainingText': sourceText.replace(regExp, ' ').trim(), + 'parameter': matchInfo[1]}; +}; + +/** + * Just like parseDirective_, except can optionally be a '-' before or + * the parameter, to negate it. Before is more natural, after + * allows more convenient toggling. + * + * Returned value has the additional field |isNegated|, and a leading + * '-' will be removed from |parameter|, if present. + */ +RequestsView.prototype.parseNegatableDirective_ = function(sourceText, + directive) { + var matchInfo = this.parseDirective_(sourceText, directive); + if (matchInfo == null) + return null; + + // Remove any leading or trailing '-' from the directive. + var negationInfo = /^(-?)(\S*?)$/.exec(matchInfo.parameter); + matchInfo.parameter = negationInfo[2]; + matchInfo.isNegated = (negationInfo[1] == '-'); + return matchInfo; +}; + +/** + * Parse any "sort:" directives, and update |comparisonFunction_| and + * |doSortBackwards_|as needed. Note only the last valid sort directive + * is used. + * + * Returns |filterText| with all sort directives removed, including + * invalid ones. + */ +RequestsView.prototype.parseSortDirectives_ = function(filterText) { + this.comparisonFunction_ = RequestsView.compareSourceId_; + this.doSortBackwards_ = false; + + while (true) { + var sortInfo = this.parseNegatableDirective_(filterText, 'sort'); + if (sortInfo == null) + break; + var comparisonName = sortInfo.parameter.toLowerCase(); + if (RequestsView.comparisonFunctionTable_[comparisonName] != null) { + this.comparisonFunction_ = + RequestsView.comparisonFunctionTable_[comparisonName]; + this.doSortBackwards_ = sortInfo.isNegated; + } + filterText = sortInfo.remainingText; + } + + return filterText; +}; + +/** + * Parse any "is:" directives, and update |filter| accordingly. + * + * Returns |filterText| with all "is:" directives removed, including + * invalid ones. + */ +RequestsView.prototype.parseRestrictDirectives_ = function(filterText, filter) { + while (true) { + var filterInfo = this.parseNegatableDirective_(filterText, 'is'); + if (filterInfo == null) + break; + if (filterInfo.parameter == 'active') { + if (!filterInfo.isNegated) + filter.isActive = true; + else + filter.isInactive = true; + } + filterText = filterInfo.remainingText; + } + return filterText; }; RequestsView.prototype.setFilter_ = function(filterText) { - this.currentFilter_ = filterText; + var lastComparisonFunction = this.comparisonFunction_; + var lastDoSortBackwards = this.doSortBackwards_; + + filterText = this.parseSortDirectives_(filterText); + + if (lastComparisonFunction != this.comparisonFunction_ || + lastDoSortBackwards != this.doSortBackwards_) { + this.Sort_(); + } + + this.currentFilter_ = {}; + filterText = this.parseRestrictDirectives_(filterText, this.currentFilter_); + this.currentFilter_.text = filterText.trim(); // Iterate through all of the rows and see if they match the filter. for (var id in this.sourceIdToEntryMap_) { @@ -105,6 +347,43 @@ RequestsView.prototype.setFilter_ = function(filterText) { } }; +/** + * Repositions |sourceEntry|'s row in the table using an insertion sort. + * Significantly faster than sorting the entire table again, when only + * one entry has changed. + */ +RequestsView.prototype.InsertionSort_ = function(sourceEntry) { + // SourceEntry that should be after |sourceEntry|, if it needs + // to be moved earlier in the list. + var sourceEntryAfter = sourceEntry; + while (true) { + var prevSourceId = sourceEntryAfter.getPreviousNodeSourceId(); + if (prevSourceId == null) + break; + var prevSourceEntry = this.sourceIdToEntryMap_[prevSourceId]; + if (this.comparisonFuncWithReversing_(sourceEntry, prevSourceEntry) >= 0) + break; + sourceEntryAfter = prevSourceEntry; + } + if (sourceEntryAfter != sourceEntry) { + sourceEntry.moveBefore(sourceEntryAfter); + return; + } + + var sourceEntryBefore = sourceEntry; + while (true) { + var nextSourceId = sourceEntryBefore.getNextNodeSourceId(); + if (nextSourceId == null) + break; + var nextSourceEntry = this.sourceIdToEntryMap_[nextSourceId]; + if (this.comparisonFuncWithReversing_(sourceEntry, nextSourceEntry) <= 0) + break; + sourceEntryBefore = nextSourceEntry; + } + if (sourceEntryBefore != sourceEntry) + sourceEntry.moveAfter(sourceEntryBefore); +}; + RequestsView.prototype.onLogEntryAdded = function(logEntry) { var id = logEntry.source.id; @@ -118,15 +397,23 @@ RequestsView.prototype.onLogEntryAdded = function(logEntry) { var sourceEntry = this.sourceIdToEntryMap_[id]; if (!sourceEntry) { - sourceEntry = new SourceEntry(this, id); + sourceEntry = new SourceEntry(this, id, this.maxReceivedSourceId_); this.sourceIdToEntryMap_[id] = sourceEntry; this.incrementPrefilterCount(1); + if (id > this.maxReceivedSourceId_) + this.maxReceivedSourceId_ = id; } sourceEntry.update(logEntry); if (sourceEntry.isSelected()) this.invalidateDetailsView_(); + + // TODO(mmenke): Fix sorting when sorting by duration. + // Duration continuously increases for all entries that are + // still active. This can result in incorrect sorting, until + // Sort_ is called. + this.InsertionSort_(sourceEntry); }; RequestsView.prototype.incrementPrefilterCount = function(offset) { @@ -177,6 +464,34 @@ RequestsView.prototype.selectAll_ = function(event) { event.preventDefault(); }; +// If already using the specified sort method, flips direction. Otherwise, +// removes pre-existing sort parameter before adding the new one. +RequestsView.prototype.toggleSortMethod_ = function(sortMethod) { + // Remove old sort directives, if any. + var filterText = this.parseSortDirectives_(this.getFilterText_()); + + // If already using specified sortMethod, sort backwards. + if (!this.doSortBackwards_ && + RequestsView.comparisonFunctionTable_[sortMethod] == + this.comparisonFunction_) + sortMethod = '-' + sortMethod; + + filterText = 'sort:' + sortMethod + ' ' + filterText; + this.setFilterText_(filterText.trim()); +}; + +RequestsView.prototype.sortById_ = function(event) { + this.toggleSortMethod_('id'); +}; + +RequestsView.prototype.sortBySourceType_ = function(event) { + this.toggleSortMethod_('source'); +}; + +RequestsView.prototype.sortByDescription_ = function(event) { + this.toggleSortMethod_('desc'); +}; + RequestsView.prototype.modifySelectionArray = function( sourceEntry, addToSelection) { // Find the index for |sourceEntry| in the current selection list. @@ -196,7 +511,7 @@ RequestsView.prototype.modifySelectionArray = function( if (index == -1 && addToSelection) { this.currentSelectedSources_.push(sourceEntry); } -} +}; RequestsView.prototype.invalidateDetailsView_ = function() { this.detailsView_.setData(this.currentSelectedSources_); diff --git a/chrome/browser/resources/net_internals/sourceentry.js b/chrome/browser/resources/net_internals/sourceentry.js index 2a74668..b48ffa0 100644 --- a/chrome/browser/resources/net_internals/sourceentry.js +++ b/chrome/browser/resources/net_internals/sourceentry.js @@ -9,12 +9,16 @@ * * @constructor */ -function SourceEntry(parentView, id) { +function SourceEntry(parentView, id, maxPreviousSourceId) { this.id_ = id; + this.maxPreviousSourceId_ = maxPreviousSourceId; this.entries_ = []; this.parentView_ = parentView; this.isSelected_ = false; this.isMatchedByFilter_ = false; + // If the first entry is a BEGIN_PHASE, set to true. + // Set to false when an END_PHASE matching the first entry is encountered. + this.isActive_ = false; } SourceEntry.prototype.isSelected = function() { @@ -62,6 +66,16 @@ SourceEntry.prototype.setFilterStyles = function(isMatchedByFilter) { }; SourceEntry.prototype.update = function(logEntry) { + if (logEntry.phase == LogEventPhase.PHASE_BEGIN && + this.entries_.length == 0) + this.isActive_ = true; + + // Only the last event should have the same type first event, + if (this.isActive_ && + logEntry.phase == LogEventPhase.PHASE_END && + logEntry.type == this.entries_[0].type) + this.isActive_ = false; + var prevStartEntry = this.getStartEntry_(); this.entries_.push(logEntry); var curStartEntry = this.getStartEntry_(); @@ -72,27 +86,36 @@ SourceEntry.prototype.update = function(logEntry) { this.createRow_(); else this.updateDescription_(); - - // Only apply the filter during the first update. - // TODO(eroman): once filters use other data, apply it on each update. - var matchesFilter = this.matchesFilter(this.parentView_.currentFilter_); - this.setIsMatchedByFilter(matchesFilter); } + + // Update filters. + var matchesFilter = this.matchesFilter(this.parentView_.currentFilter_); + this.setIsMatchedByFilter(matchesFilter); }; SourceEntry.prototype.onCheckboxToggled_ = function() { this.setSelected(this.getSelectionCheckbox().checked); }; -SourceEntry.prototype.matchesFilter = function(filterText) { +SourceEntry.prototype.matchesFilter = function(filter) { // TODO(eroman): Support more advanced filter syntax. - if (filterText == '') + + // Safety check. + if (this.row_ == null) + return false; + + if (filter.isActive && !this.isActive_) + return false; + if (filter.isInactive && this.isActive_) + return false; + + if (filter.text == '') return true; - var filterText = filterText.toLowerCase(); + var filterText = filter.text.toLowerCase(); + var entryText = PrintSourceEntriesAsText(this.entries_).toLowerCase(); - return this.getDescription().toLowerCase().indexOf(filterText) != -1 || - this.getSourceTypeString().toLowerCase().indexOf(filterText) != -1; + return entryText.indexOf(filterText) != -1; }; SourceEntry.prototype.setSelected = function(isSelected) { @@ -122,11 +145,12 @@ SourceEntry.prototype.onMouseout_ = function() { SourceEntry.prototype.updateDescription_ = function() { this.descriptionCell_.innerHTML = ''; addTextNode(this.descriptionCell_, this.getDescription()); -} +}; SourceEntry.prototype.createRow_ = function() { // Create a row. var tr = addNode(this.parentView_.tableBody_, 'tr'); + tr._id = this.getSourceId(); tr.style.display = 'none'; this.row_ = tr; @@ -232,6 +256,91 @@ SourceEntry.prototype.getSourceId = function() { return this.id_; }; +/** + * Returns the largest source ID seen before this object was received. + * Used only for sorting SourceEntries without a source by source ID. + */ +SourceEntry.prototype.getMaxPreviousEntrySourceId = function() { + return this.maxPreviousSourceId_; +}; + +SourceEntry.prototype.isActive = function() { + return this.isActive_; +}; + +/** + * Returns time of last event if inactive. Returns current time otherwise. + */ +SourceEntry.prototype.getEndTime = function() { + if (this.isActive_) { + return (new Date()).getTime(); + } + else { + var endTicks = this.entries_[this.entries_.length - 1].time; + return g_browser.convertTimeTicksToDate(endTicks).getTime(); + } +}; + +/** + * Returns the time between the first and last events with a matching + * source ID. If source is still active, uses the current time for the + * last event. + */ +SourceEntry.prototype.getDuration = function() { + var startTicks = this.entries_[0].time; + var startTime = g_browser.convertTimeTicksToDate(startTicks).getTime(); + var endTime = this.getEndTime(); + return endTime - startTime; +}; + +/** + * Returns source ID of the entry whose row is currently above this one's. + * Returns null if no such node exists. + */ +SourceEntry.prototype.getPreviousNodeSourceId = function() { + if (!this.hasRow()) + return null; + var prevNode = this.row_.previousSibling; + if (prevNode == null) + return null; + return prevNode._id; +}; + +/** + * Returns source ID of the entry whose row is currently below this one's. + * Returns null if no such node exists. + */ +SourceEntry.prototype.getNextNodeSourceId = function() { + if (!this.hasRow()) + return null; + var nextNode = this.row_.nextSibling; + if (nextNode == null) + return null; + return nextNode._id; +}; + +SourceEntry.prototype.hasRow = function() { + return this.row_ != null; +}; + +/** + * Moves current object's row before |entry|'s row. + */ +SourceEntry.prototype.moveBefore = function(entry) { + if (this.hasRow() && entry.hasRow()) { + this.row_.parentNode.insertBefore(this.row_, entry.row_); + } +}; + +/** + * Moves current object's row after |entry|'s row. + */ +SourceEntry.prototype.moveAfter = function(entry) { + if (this.hasRow() && entry.hasRow()) { + this.row_.parentNode.insertBefore(this.row_, entry.row_.nextSibling); + } +}; + SourceEntry.prototype.remove = function() { this.setSelected(false); this.setIsMatchedByFilter(false); |