1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 /** 6 * Each row in the filtered items list is backed by a SourceEntry. This 7 * instance contains all of the data pertaining to that row, and notifies 8 * its parent view (the EventsView) whenever its data changes. 9 * 10 * @constructor 11 */ 12 function SourceEntry(parentView, maxPreviousSourceId) { 13 this.maxPreviousSourceId_ = maxPreviousSourceId; 14 this.entries_ = []; 15 this.parentView_ = parentView; 16 this.isSelected_ = false; 17 this.isMatchedByFilter_ = false; 18 // If the first entry is a BEGIN_PHASE, set to true. 19 // Set to false when an END_PHASE matching the first entry is encountered. 20 this.isActive_ = false; 21 } 22 23 SourceEntry.prototype.isSelected = function() { 24 return this.isSelected_; 25 }; 26 27 SourceEntry.prototype.setSelectedStyles = function(isSelected) { 28 changeClassName(this.row_, 'selected', isSelected); 29 this.getSelectionCheckbox().checked = isSelected; 30 }; 31 32 SourceEntry.prototype.setMouseoverStyle = function(isMouseOver) { 33 changeClassName(this.row_, 'mouseover', isMouseOver); 34 }; 35 36 SourceEntry.prototype.setIsMatchedByFilter = function(isMatchedByFilter) { 37 if (this.isMatchedByFilter() == isMatchedByFilter) 38 return; // No change. 39 40 this.isMatchedByFilter_ = isMatchedByFilter; 41 42 this.setFilterStyles(isMatchedByFilter); 43 44 if (isMatchedByFilter) { 45 this.parentView_.incrementPostfilterCount(1); 46 } else { 47 this.parentView_.incrementPostfilterCount(-1); 48 // If we are filtering an entry away, make sure it is no longer 49 // part of the selection. 50 this.setSelected(false); 51 } 52 }; 53 54 SourceEntry.prototype.isMatchedByFilter = function() { 55 return this.isMatchedByFilter_; 56 }; 57 58 SourceEntry.prototype.setFilterStyles = function(isMatchedByFilter) { 59 // Hide rows which have been filtered away. 60 if (isMatchedByFilter) { 61 this.row_.style.display = ''; 62 } else { 63 this.row_.style.display = 'none'; 64 } 65 }; 66 67 SourceEntry.prototype.update = function(logEntry) { 68 if (logEntry.phase == LogEventPhase.PHASE_BEGIN && 69 this.entries_.length == 0) 70 this.isActive_ = true; 71 72 // Only the last event should have the same type first event, 73 if (this.isActive_ && 74 logEntry.phase == LogEventPhase.PHASE_END && 75 logEntry.type == this.entries_[0].type) 76 this.isActive_ = false; 77 78 var prevStartEntry = this.getStartEntry_(); 79 this.entries_.push(logEntry); 80 var curStartEntry = this.getStartEntry_(); 81 82 // If we just got the first entry for this source. 83 if (prevStartEntry != curStartEntry) { 84 if (!prevStartEntry) 85 this.createRow_(); 86 else 87 this.updateDescription_(); 88 } 89 90 // Update filters. 91 var matchesFilter = this.matchesFilter(this.parentView_.currentFilter_); 92 this.setIsMatchedByFilter(matchesFilter); 93 }; 94 95 SourceEntry.prototype.onCheckboxToggled_ = function() { 96 this.setSelected(this.getSelectionCheckbox().checked); 97 }; 98 99 SourceEntry.prototype.matchesFilter = function(filter) { 100 // Safety check. 101 if (this.row_ == null) 102 return false; 103 104 if (filter.isActive && !this.isActive_) 105 return false; 106 if (filter.isInactive && this.isActive_) 107 return false; 108 109 // Check source type, if needed. 110 if (filter.type) { 111 var sourceType = this.getSourceTypeString().toLowerCase(); 112 if (filter.type.indexOf(sourceType) == -1) 113 return false; 114 } 115 116 // Check source ID, if needed. 117 if (filter.id) { 118 if (filter.id.indexOf(this.getSourceId() + '') == -1) 119 return false; 120 } 121 122 if (filter.text == '') 123 return true; 124 125 var filterText = filter.text; 126 var entryText = PrintSourceEntriesAsText(this.entries_).toLowerCase(); 127 128 return entryText.indexOf(filterText) != -1; 129 }; 130 131 SourceEntry.prototype.setSelected = function(isSelected) { 132 if (isSelected == this.isSelected()) 133 return; 134 135 this.isSelected_ = isSelected; 136 137 this.setSelectedStyles(isSelected); 138 this.parentView_.modifySelectionArray(this, isSelected); 139 this.parentView_.onSelectionChanged(); 140 }; 141 142 SourceEntry.prototype.onClicked_ = function() { 143 this.parentView_.clearSelection(); 144 this.setSelected(true); 145 }; 146 147 SourceEntry.prototype.onMouseover_ = function() { 148 this.setMouseoverStyle(true); 149 }; 150 151 SourceEntry.prototype.onMouseout_ = function() { 152 this.setMouseoverStyle(false); 153 }; 154 155 SourceEntry.prototype.updateDescription_ = function() { 156 this.descriptionCell_.innerHTML = ''; 157 addTextNode(this.descriptionCell_, this.getDescription()); 158 }; 159 160 SourceEntry.prototype.createRow_ = function() { 161 // Create a row. 162 var tr = addNode(this.parentView_.tableBody_, 'tr'); 163 tr._id = this.getSourceId(); 164 tr.style.display = 'none'; 165 this.row_ = tr; 166 167 var selectionCol = addNode(tr, 'td'); 168 var checkbox = addNode(selectionCol, 'input'); 169 checkbox.type = 'checkbox'; 170 171 var idCell = addNode(tr, 'td'); 172 idCell.style.textAlign = 'right'; 173 174 var typeCell = addNode(tr, 'td'); 175 var descriptionCell = addNode(tr, 'td'); 176 this.descriptionCell_ = descriptionCell; 177 178 // Connect listeners. 179 checkbox.onchange = this.onCheckboxToggled_.bind(this); 180 181 var onclick = this.onClicked_.bind(this); 182 idCell.onclick = onclick; 183 typeCell.onclick = onclick; 184 descriptionCell.onclick = onclick; 185 186 tr.onmouseover = this.onMouseover_.bind(this); 187 tr.onmouseout = this.onMouseout_.bind(this); 188 189 // Set the cell values to match this source's data. 190 if (this.getSourceId() >= 0) 191 addTextNode(idCell, this.getSourceId()); 192 else 193 addTextNode(idCell, '-'); 194 var sourceTypeString = this.getSourceTypeString(); 195 addTextNode(typeCell, sourceTypeString); 196 this.updateDescription_(); 197 198 // Add a CSS classname specific to this source type (so CSS can specify 199 // different stylings for different types). 200 changeClassName(this.row_, 'source_' + sourceTypeString, true); 201 }; 202 203 /** 204 * Returns a description for this source log stream, which will be displayed 205 * in the list view. Most often this is a URL that identifies the request, 206 * or a hostname for a connect job, etc... 207 */ 208 SourceEntry.prototype.getDescription = function() { 209 var e = this.getStartEntry_(); 210 if (!e) 211 return ''; 212 213 if (e.source.type == LogSourceType.NONE) { 214 // NONE is what we use for global events that aren't actually grouped 215 // by a "source ID", so we will just stringize the event's type. 216 return getKeyWithValue(LogEventType, e.type); 217 } 218 219 if (e.params == undefined) 220 return ''; 221 222 var description = ''; 223 switch (e.source.type) { 224 case LogSourceType.URL_REQUEST: 225 case LogSourceType.SOCKET_STREAM: 226 case LogSourceType.HTTP_STREAM_JOB: 227 description = e.params.url; 228 break; 229 case LogSourceType.CONNECT_JOB: 230 description = e.params.group_name; 231 break; 232 case LogSourceType.HOST_RESOLVER_IMPL_REQUEST: 233 case LogSourceType.HOST_RESOLVER_IMPL_JOB: 234 description = e.params.host; 235 break; 236 case LogSourceType.DISK_CACHE_ENTRY: 237 case LogSourceType.MEMORY_CACHE_ENTRY: 238 description = e.params.key; 239 break; 240 case LogSourceType.SPDY_SESSION: 241 if (e.params.host) 242 description = e.params.host + ' (' + e.params.proxy + ')'; 243 break; 244 case LogSourceType.SOCKET: 245 if (e.params.source_dependency != undefined) { 246 var connectJobSourceEntry = 247 this.parentView_.getSourceEntry(e.params.source_dependency.id); 248 if (connectJobSourceEntry) 249 description = connectJobSourceEntry.getDescription(); 250 } 251 break; 252 } 253 254 if (description == undefined) 255 return ''; 256 return description; 257 }; 258 259 /** 260 * Returns the starting entry for this source. Conceptually this is the 261 * first entry that was logged to this source. However, we skip over the 262 * TYPE_REQUEST_ALIVE entries which wrap TYPE_URL_REQUEST_START_JOB / 263 * TYPE_SOCKET_STREAM_CONNECT. 264 */ 265 SourceEntry.prototype.getStartEntry_ = function() { 266 if (this.entries_.length < 1) 267 return undefined; 268 if (this.entries_.length >= 2) { 269 if (this.entries_[0].type == LogEventType.REQUEST_ALIVE || 270 this.entries_[0].type == LogEventType.SOCKET_POOL_CONNECT_JOB) 271 return this.entries_[1]; 272 } 273 return this.entries_[0]; 274 }; 275 276 SourceEntry.prototype.getLogEntries = function() { 277 return this.entries_; 278 }; 279 280 SourceEntry.prototype.getSourceTypeString = function() { 281 return getKeyWithValue(LogSourceType, this.entries_[0].source.type); 282 }; 283 284 SourceEntry.prototype.getSelectionCheckbox = function() { 285 return this.row_.childNodes[0].firstChild; 286 }; 287 288 SourceEntry.prototype.getSourceId = function() { 289 return this.entries_[0].source.id; 290 }; 291 292 /** 293 * Returns the largest source ID seen before this object was received. 294 * Used only for sorting SourceEntries without a source by source ID. 295 */ 296 SourceEntry.prototype.getMaxPreviousEntrySourceId = function() { 297 return this.maxPreviousSourceId_; 298 }; 299 300 SourceEntry.prototype.isActive = function() { 301 return this.isActive_; 302 }; 303 304 /** 305 * Returns time of last event if inactive. Returns current time otherwise. 306 */ 307 SourceEntry.prototype.getEndTime = function() { 308 if (this.isActive_) { 309 return (new Date()).getTime(); 310 } 311 else { 312 var endTicks = this.entries_[this.entries_.length - 1].time; 313 return g_browser.convertTimeTicksToDate(endTicks).getTime(); 314 } 315 }; 316 317 /** 318 * Returns the time between the first and last events with a matching 319 * source ID. If source is still active, uses the current time for the 320 * last event. 321 */ 322 SourceEntry.prototype.getDuration = function() { 323 var startTicks = this.entries_[0].time; 324 var startTime = g_browser.convertTimeTicksToDate(startTicks).getTime(); 325 var endTime = this.getEndTime(); 326 return endTime - startTime; 327 }; 328 329 /** 330 * Returns source ID of the entry whose row is currently above this one's. 331 * Returns null if no such node exists. 332 */ 333 SourceEntry.prototype.getPreviousNodeSourceId = function() { 334 if (!this.hasRow()) 335 return null; 336 var prevNode = this.row_.previousSibling; 337 if (prevNode == null) 338 return null; 339 return prevNode._id; 340 }; 341 342 /** 343 * Returns source ID of the entry whose row is currently below this one's. 344 * Returns null if no such node exists. 345 */ 346 SourceEntry.prototype.getNextNodeSourceId = function() { 347 if (!this.hasRow()) 348 return null; 349 var nextNode = this.row_.nextSibling; 350 if (nextNode == null) 351 return null; 352 return nextNode._id; 353 }; 354 355 SourceEntry.prototype.hasRow = function() { 356 return this.row_ != null; 357 }; 358 359 /** 360 * Moves current object's row before |entry|'s row. 361 */ 362 SourceEntry.prototype.moveBefore = function(entry) { 363 if (this.hasRow() && entry.hasRow()) { 364 this.row_.parentNode.insertBefore(this.row_, entry.row_); 365 } 366 }; 367 368 /** 369 * Moves current object's row after |entry|'s row. 370 */ 371 SourceEntry.prototype.moveAfter = function(entry) { 372 if (this.hasRow() && entry.hasRow()) { 373 this.row_.parentNode.insertBefore(this.row_, entry.row_.nextSibling); 374 } 375 }; 376 377 SourceEntry.prototype.remove = function() { 378 this.setSelected(false); 379 this.setIsMatchedByFilter(false); 380 this.row_.parentNode.removeChild(this.row_); 381 }; 382 383