1// Copyright (c) 2012 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/** @constructor */ 6function TaskManager() { } 7 8cr.addSingletonGetter(TaskManager); 9 10TaskManager.prototype = { 11 /** 12 * Handle window close. 13 * @this 14 */ 15 onClose: function() { 16 if (!this.disabled_) { 17 this.disabled_ = true; 18 commands.disableTaskManager(); 19 } 20 }, 21 22 /** 23 * Handles selection changes. 24 * This is also called when data of tasks are refreshed, even if selection 25 * has not been changed. 26 * @this 27 */ 28 onSelectionChange: function() { 29 var sm = this.selectionModel_; 30 var dm = this.dataModel_; 31 var selectedIndexes = sm.selectedIndexes; 32 var isEndProcessEnabled = true; 33 if (selectedIndexes.length == 0) 34 isEndProcessEnabled = false; 35 for (var i = 0; i < selectedIndexes.length; i++) { 36 var index = selectedIndexes[i]; 37 var task = dm.item(index); 38 if (task['type'] == 'BROWSER') 39 isEndProcessEnabled = false; 40 } 41 if (this.isEndProcessEnabled_ != isEndProcessEnabled) { 42 if (isEndProcessEnabled) 43 $('kill-process').removeAttribute('disabled'); 44 else 45 $('kill-process').setAttribute('disabled', 'true'); 46 47 this.isEndProcessEnabled_ = isEndProcessEnabled; 48 } 49 }, 50 51 /** 52 * Closes taskmanager dialog. 53 * After this function is called, onClose() will be called. 54 * @this 55 */ 56 close: function() { 57 window.close(); 58 }, 59 60 /** 61 * Sends commands to kill selected processes. 62 * @this 63 */ 64 killSelectedProcesses: function() { 65 var selectedIndexes = this.selectionModel_.selectedIndexes; 66 var dm = this.dataModel_; 67 var uniqueIds = []; 68 for (var i = 0; i < selectedIndexes.length; i++) { 69 var index = selectedIndexes[i]; 70 var task = dm.item(index); 71 uniqueIds.push(task['uniqueId'][0]); 72 } 73 74 commands.killSelectedProcesses(uniqueIds); 75 }, 76 77 /** 78 * Initializes taskmanager. 79 * @this 80 */ 81 initialize: function(dialogDom, opt) { 82 if (!dialogDom) { 83 console.log('ERROR: dialogDom is not defined.'); 84 return; 85 } 86 87 measureTime.startInterval('Load.DOM'); 88 89 this.opt_ = opt; 90 91 this.initialized_ = true; 92 93 this.elementsCache_ = {}; 94 this.dialogDom_ = dialogDom; 95 this.document_ = dialogDom.ownerDocument; 96 97 this.localized_column_ = []; 98 for (var i = 0; i < DEFAULT_COLUMNS.length; i++) { 99 var columnLabelId = DEFAULT_COLUMNS[i][1]; 100 this.localized_column_[i] = loadTimeData.getString(columnLabelId); 101 } 102 103 this.initElements_(); 104 this.initColumnModel_(); 105 this.selectionModel_ = new cr.ui.ListSelectionModel(); 106 this.dataModel_ = new cr.ui.ArrayDataModel([]); 107 108 this.selectionModel_.addEventListener('change', 109 this.onSelectionChange.bind(this)); 110 111 // Initializes compare functions for column sort. 112 var dm = this.dataModel_; 113 // List of columns to sort by its numerical value as opposed to the 114 // formatted value, e.g., 20480 vs. 20KB. 115 var COLUMNS_SORTED_BY_VALUE = [ 116 'cpuUsage', 'physicalMemory', 'sharedMemory', 'privateMemory', 117 'networkUsage', 'webCoreImageCacheSize', 'webCoreScriptsCacheSize', 118 'webCoreCSSCacheSize', 'fps', 'videoMemory', 'sqliteMemoryUsed', 119 'goatsTeleported', 'v8MemoryAllocatedSize']; 120 121 for (var i = 0; i < DEFAULT_COLUMNS.length; i++) { 122 var columnId = DEFAULT_COLUMNS[i][0]; 123 var compareFunc = (function() { 124 var columnIdToSort = columnId; 125 if (COLUMNS_SORTED_BY_VALUE.indexOf(columnId) != -1) 126 columnIdToSort += 'Value'; 127 128 return function(a, b) { 129 var aValues = a[columnIdToSort]; 130 var bValues = b[columnIdToSort]; 131 var aValue = aValues && aValues[0] || 0; 132 var bvalue = bValues && bValues[0] || 0; 133 return dm.defaultValuesCompareFunction(aValue, bvalue); 134 }; 135 })(); 136 dm.setCompareFunction(columnId, compareFunc); 137 } 138 139 if (isColumnEnabled(DEFAULT_SORT_COLUMN)) 140 dm.sort(DEFAULT_SORT_COLUMN, DEFAULT_SORT_DIRECTION); 141 142 this.initTable_(); 143 144 commands.enableTaskManager(); 145 146 // Populate the static localized strings. 147 i18nTemplate.process(this.document_, loadTimeData); 148 149 measureTime.recordInterval('Load.DOM'); 150 measureTime.recordInterval('Load.Total'); 151 152 loadDelayedIncludes(this); 153 }, 154 155 /** 156 * Initializes the visibilities and handlers of the elements. 157 * This method is called by initialize(). 158 * @private 159 * @this 160 */ 161 initElements_: function() { 162 // <if expr="pp_ifdef('chromeos')"> 163 // The 'close-window' element exists only on ChromeOS. 164 // This <if ... /if> section is removed while flattening HTML if chrome is 165 // built as Desktop Chrome. 166 if (!this.opt_['isShowCloseButton']) 167 $('close-window').style.display = 'none'; 168 $('close-window').addEventListener('click', this.close.bind(this)); 169 // </if> 170 171 $('kill-process').addEventListener('click', 172 this.killSelectedProcesses.bind(this)); 173 $('about-memory-link').addEventListener('click', commands.openAboutMemory); 174 }, 175 176 /** 177 * Additional initialization of taskmanager. This function is called when 178 * the loading of delayed scripts finished. 179 * @this 180 */ 181 delayedInitialize: function() { 182 this.initColumnMenu_(); 183 this.initTableMenu_(); 184 185 var dm = this.dataModel_; 186 for (var i = 0; i < dm.length; i++) { 187 var processId = dm.item(i)['processId'][0]; 188 for (var j = 0; j < DEFAULT_COLUMNS.length; j++) { 189 var columnId = DEFAULT_COLUMNS[j][0]; 190 191 var row = dm.item(i)[columnId]; 192 if (!row) 193 continue; 194 195 for (var k = 0; k < row.length; k++) { 196 var labelId = 'detail-' + columnId + '-pid' + processId + '-' + k; 197 var label = $(labelId); 198 199 // Initialize a context-menu, if the label exists and its context- 200 // menu is not initialized yet. 201 if (label && !label.contextMenu) 202 cr.ui.contextMenuHandler.setContextMenu(label, 203 this.tableContextMenu_); 204 } 205 } 206 } 207 208 this.isFinishedInitDelayed_ = true; 209 var t = this.table_; 210 t.redraw(); 211 addEventListener('resize', t.redraw.bind(t)); 212 }, 213 214 initColumnModel_: function() { 215 var tableColumns = new Array(); 216 for (var i = 0; i < DEFAULT_COLUMNS.length; i++) { 217 var column = DEFAULT_COLUMNS[i]; 218 var columnId = column[0]; 219 if (!isColumnEnabled(columnId)) 220 continue; 221 222 tableColumns.push(new cr.ui.table.TableColumn(columnId, 223 this.localized_column_[i], 224 column[2])); 225 } 226 227 for (var i = 0; i < tableColumns.length; i++) { 228 tableColumns[i].renderFunction = this.renderColumn_.bind(this); 229 } 230 231 this.columnModel_ = new cr.ui.table.TableColumnModel(tableColumns); 232 }, 233 234 initColumnMenu_: function() { 235 this.column_menu_commands_ = []; 236 237 this.commandsElement_ = this.document_.createElement('commands'); 238 this.document_.body.appendChild(this.commandsElement_); 239 240 this.columnSelectContextMenu_ = this.document_.createElement('menu'); 241 for (var i = 0; i < DEFAULT_COLUMNS.length; i++) { 242 var column = DEFAULT_COLUMNS[i]; 243 244 // Creates command element to receive event. 245 var command = this.document_.createElement('command'); 246 command.id = COMMAND_CONTEXTMENU_COLUMN_PREFIX + '-' + column[0]; 247 cr.ui.Command.decorate(command); 248 this.column_menu_commands_[command.id] = command; 249 this.commandsElement_.appendChild(command); 250 251 // Creates menuitem element. 252 var item = this.document_.createElement('menuitem'); 253 item.command = command; 254 command.menuitem = item; 255 item.textContent = this.localized_column_[i]; 256 if (isColumnEnabled(column[0])) 257 item.setAttributeNode(this.document_.createAttribute('checked')); 258 this.columnSelectContextMenu_.appendChild(item); 259 } 260 261 this.document_.body.appendChild(this.columnSelectContextMenu_); 262 cr.ui.Menu.decorate(this.columnSelectContextMenu_); 263 264 cr.ui.contextMenuHandler.setContextMenu(this.table_.header, 265 this.columnSelectContextMenu_); 266 cr.ui.contextMenuHandler.setContextMenu(this.table_.list, 267 this.columnSelectContextMenu_); 268 269 this.document_.addEventListener('command', this.onCommand_.bind(this)); 270 this.document_.addEventListener('canExecute', 271 this.onCommandCanExecute_.bind(this)); 272 }, 273 274 initTableMenu_: function() { 275 this.table_menu_commands_ = []; 276 this.tableContextMenu_ = this.document_.createElement('menu'); 277 278 var addMenuItem = function(tm, commandId, string_id) { 279 // Creates command element to receive event. 280 var command = tm.document_.createElement('command'); 281 command.id = COMMAND_CONTEXTMENU_TABLE_PREFIX + '-' + commandId; 282 cr.ui.Command.decorate(command); 283 tm.table_menu_commands_[command.id] = command; 284 tm.commandsElement_.appendChild(command); 285 286 // Creates menuitem element. 287 var item = tm.document_.createElement('menuitem'); 288 item.command = command; 289 command.menuitem = item; 290 item.textContent = loadTimeData.getString(string_id); 291 tm.tableContextMenu_.appendChild(item); 292 }; 293 294 addMenuItem(this, 'inspect', 'inspect'); 295 addMenuItem(this, 'activate', 'activate'); 296 297 this.document_.body.appendChild(this.tableContextMenu_); 298 cr.ui.Menu.decorate(this.tableContextMenu_); 299 }, 300 301 initTable_: function() { 302 if (!this.dataModel_ || !this.selectionModel_ || !this.columnModel_) { 303 console.log('ERROR: some models are not defined.'); 304 return; 305 } 306 307 this.table_ = this.dialogDom_.querySelector('.detail-table'); 308 cr.ui.Table.decorate(this.table_); 309 310 this.table_.dataModel = this.dataModel_; 311 this.table_.selectionModel = this.selectionModel_; 312 this.table_.columnModel = this.columnModel_; 313 314 // Expands height of row when a process has some tasks. 315 this.table_.fixedHeight = false; 316 317 this.table_.list.addEventListener('contextmenu', 318 this.onTableContextMenuOpened_.bind(this), 319 true); 320 321 // Sets custom row render function. 322 this.table_.setRenderFunction(this.getRow_.bind(this)); 323 }, 324 325 /** 326 * Returns a list item element of the list. This method trys to reuse the 327 * cached element, or creates a new element. 328 * @return {cr.ui.ListItem} list item element which contains the given data. 329 * @private 330 * @this 331 */ 332 getRow_: function(data, table) { 333 // Trys to reuse the cached row; 334 var listItemElement = this.renderRowFromCache_(data, table); 335 if (listItemElement) 336 return listItemElement; 337 338 // Initializes the cache. 339 var pid = data['processId'][0]; 340 this.elementsCache_[pid] = { 341 listItem: null, 342 cell: [], 343 icon: [], 344 columns: {} 345 }; 346 347 // Create new row. 348 return this.renderRow_(data, table); 349 }, 350 351 /** 352 * Returns a list item element with re-using the previous cached element, or 353 * returns null if failed. 354 * @return {cr.ui.ListItem} cached un-used element to be reused. 355 * @private 356 * @this 357 */ 358 renderRowFromCache_: function(data, table) { 359 var pid = data['processId'][0]; 360 361 // Checks whether the cache exists or not. 362 var cache = this.elementsCache_[pid]; 363 if (!cache) 364 return null; 365 366 var listItemElement = cache.listItem; 367 var cm = table.columnModel; 368 // Checks whether the number of columns has been changed or not. 369 if (cache.cachedColumnSize != cm.size) 370 return null; 371 // Checks whether the number of childlen tasks has been changed or not. 372 if (cache.cachedChildSize != data['uniqueId'].length) 373 return null; 374 375 // Updates informations of the task if necessary. 376 for (var i = 0; i < cm.size; i++) { 377 var columnId = cm.getId(i); 378 var columnData = data[columnId]; 379 var oldColumnData = listItemElement.data[columnId]; 380 var columnElements = cache.columns[columnId]; 381 382 if (!columnData || !oldColumnData || !columnElements) 383 return null; 384 385 // Sets new width of the cell. 386 var cellElement = cache.cell[i]; 387 cellElement.style.width = cm.getWidth(i) + '%'; 388 389 for (var j = 0; j < columnData.length; j++) { 390 // Sets the new text, if the text has been changed. 391 if (oldColumnData[j] != columnData[j]) { 392 var textElement = columnElements[j]; 393 textElement.textContent = columnData[j]; 394 } 395 } 396 } 397 398 // Updates icon of the task if necessary. 399 var oldIcons = listItemElement.data['icon']; 400 var newIcons = data['icon']; 401 if (oldIcons && newIcons) { 402 for (var j = 0; j < columnData.length; j++) { 403 var oldIcon = oldIcons[j]; 404 var newIcon = newIcons[j]; 405 if (oldIcon != newIcon) { 406 var iconElement = cache.icon[j]; 407 iconElement.src = newIcon; 408 } 409 } 410 } 411 listItemElement.data = data; 412 413 // Removes 'selected' and 'lead' attributes. 414 listItemElement.removeAttribute('selected'); 415 listItemElement.removeAttribute('lead'); 416 417 return listItemElement; 418 }, 419 420 /** 421 * Create a new list item element. 422 * @return {cr.ui.ListItem} created new list item element. 423 * @private 424 * @this 425 */ 426 renderRow_: function(data, table) { 427 var pid = data['processId'][0]; 428 var cm = table.columnModel; 429 var listItem = new cr.ui.ListItem({label: ''}); 430 431 listItem.className = 'table-row'; 432 433 for (var i = 0; i < cm.size; i++) { 434 var cell = document.createElement('div'); 435 cell.style.width = cm.getWidth(i) + '%'; 436 cell.className = 'table-row-cell'; 437 cell.id = 'column-' + pid + '-' + cm.getId(i); 438 cell.appendChild( 439 cm.getRenderFunction(i).call(null, data, cm.getId(i), table)); 440 441 listItem.appendChild(cell); 442 443 // Stores the cell element to the dictionary. 444 this.elementsCache_[pid].cell[i] = cell; 445 } 446 447 // Specifies the height of the row. The height of each row is 448 // 'num_of_tasks * HEIGHT_OF_TASK' px. 449 listItem.style.height = (data['uniqueId'].length * HEIGHT_OF_TASK) + 'px'; 450 451 listItem.data = data; 452 453 // Stores the list item element, the number of columns and the number of 454 // childlen. 455 this.elementsCache_[pid].listItem = listItem; 456 this.elementsCache_[pid].cachedColumnSize = cm.size; 457 this.elementsCache_[pid].cachedChildSize = data['uniqueId'].length; 458 459 return listItem; 460 }, 461 462 /** 463 * Create a new element of the cell. 464 * @return {HTMLDIVElement} created cell 465 * @private 466 * @this 467 */ 468 renderColumn_: function(entry, columnId, table) { 469 var container = this.document_.createElement('div'); 470 container.className = 'detail-container-' + columnId; 471 var pid = entry['processId'][0]; 472 473 var cache = []; 474 var cacheIcon = []; 475 476 if (entry && entry[columnId]) { 477 container.id = 'detail-container-' + columnId + '-pid' + entry.processId; 478 479 for (var i = 0; i < entry[columnId].length; i++) { 480 var label = document.createElement('div'); 481 if (columnId == 'title') { 482 // Creates a page title element with icon. 483 var image = this.document_.createElement('img'); 484 image.className = 'detail-title-image'; 485 image.src = entry['icon'][i]; 486 image.id = 'detail-title-icon-pid' + pid + '-' + i; 487 label.appendChild(image); 488 var text = this.document_.createElement('div'); 489 text.className = 'detail-title-text'; 490 text.id = 'detail-title-text-pid' + pid + '-' + i; 491 text.textContent = entry['title'][i]; 492 label.appendChild(text); 493 494 // Chech if the delayed scripts (included in includes.js) have been 495 // loaded or not. If the delayed scripts ware not loaded yet, a 496 // context menu could not be initialized. In such case, it will be 497 // initialized at delayedInitialize() just after loading of delayed 498 // scripts instead of here. 499 if (this.isFinishedInitDelayed_) 500 cr.ui.contextMenuHandler.setContextMenu(label, 501 this.tableContextMenu_); 502 503 label.addEventListener('dblclick', (function(uniqueId) { 504 commands.activatePage(uniqueId); 505 }).bind(this, entry['uniqueId'][i])); 506 507 label.data = entry; 508 label.index_in_group = i; 509 510 cache[i] = text; 511 cacheIcon[i] = image; 512 } else { 513 label.textContent = entry[columnId][i]; 514 cache[i] = label; 515 } 516 label.id = 'detail-' + columnId + '-pid' + pid + '-' + i; 517 label.className = 'detail-' + columnId + ' pid' + pid; 518 container.appendChild(label); 519 } 520 521 this.elementsCache_[pid].columns[columnId] = cache; 522 if (columnId == 'title') 523 this.elementsCache_[pid].icon = cacheIcon; 524 } 525 return container; 526 }, 527 528 /** 529 * Updates the task list with the supplied task. 530 * @private 531 * @this 532 */ 533 processTaskChange: function(task) { 534 var dm = this.dataModel_; 535 var sm = this.selectionModel_; 536 if (!dm || !sm) return; 537 538 this.table_.list.startBatchUpdates(); 539 sm.beginChange(); 540 541 var type = task.type; 542 var start = task.start; 543 var length = task.length; 544 var tasks = task.tasks; 545 546 // We have to store the selected pids and restore them after 547 // splice(), because it might replace some items but the replaced 548 // items would lose the selection. 549 var oldSelectedIndexes = sm.selectedIndexes; 550 551 // Create map of selected PIDs. 552 var selectedPids = {}; 553 for (var i = 0; i < oldSelectedIndexes.length; i++) { 554 var item = dm.item(oldSelectedIndexes[i]); 555 if (item) selectedPids[item['processId'][0]] = true; 556 } 557 558 var args = tasks.slice(); 559 args.unshift(start, dm.length); 560 dm.splice.apply(dm, args); 561 562 // Create new array of selected indexes from map of old PIDs. 563 var newSelectedIndexes = []; 564 for (var i = 0; i < dm.length; i++) { 565 if (selectedPids[dm.item(i)['processId'][0]]) 566 newSelectedIndexes.push(i); 567 } 568 569 sm.selectedIndexes = newSelectedIndexes; 570 571 var pids = []; 572 for (var i = 0; i < dm.length; i++) { 573 pids.push(dm.item(i)['processId'][0]); 574 } 575 576 // Sweeps unused caches, which elements no longer exist on the list. 577 for (var pid in this.elementsCache_) { 578 if (pids.indexOf(pid) == -1) 579 delete this.elementsCache_[pid]; 580 } 581 582 sm.endChange(); 583 this.table_.list.endBatchUpdates(); 584 }, 585 586 /** 587 * Respond to a command being executed. 588 * @this 589 */ 590 onCommand_: function(event) { 591 var command = event.command; 592 var commandId = command.id.split('-', 2); 593 594 var mainCommand = commandId[0]; 595 var subCommand = commandId[1]; 596 597 if (mainCommand == COMMAND_CONTEXTMENU_COLUMN_PREFIX) { 598 this.onColumnContextMenu_(subCommand, command); 599 } else if (mainCommand == COMMAND_CONTEXTMENU_TABLE_PREFIX) { 600 var targetUniqueId = this.currentContextMenuTarget_; 601 602 if (!targetUniqueId) 603 return; 604 605 if (subCommand == 'inspect') 606 commands.inspect(targetUniqueId); 607 else if (subCommand == 'activate') 608 commands.activatePage(targetUniqueId); 609 610 this.currentContextMenuTarget_ = undefined; 611 } 612 }, 613 614 onCommandCanExecute_: function(event) { 615 event.canExecute = true; 616 }, 617 618 /** 619 * Store resourceIndex of target resource of context menu, because resource 620 * will be replaced when it is refreshed. 621 * @this 622 */ 623 onTableContextMenuOpened_: function(e) { 624 if (!this.isFinishedInitDelayed_) 625 return; 626 627 var mc = this.table_menu_commands_; 628 var inspectMenuitem = 629 mc[COMMAND_CONTEXTMENU_TABLE_PREFIX + '-inspect'].menuitem; 630 var activateMenuitem = 631 mc[COMMAND_CONTEXTMENU_TABLE_PREFIX + '-activate'].menuitem; 632 633 // Disabled by default. 634 inspectMenuitem.disabled = true; 635 activateMenuitem.disabled = true; 636 637 var target = e.target; 638 for (;; target = target.parentNode) { 639 if (!target) return; 640 var classes = target.classList; 641 if (classes && 642 Array.prototype.indexOf.call(classes, 'detail-title') != -1) break; 643 } 644 645 var indexInGroup = target.index_in_group; 646 647 // Sets the uniqueId for current target page under the mouse corsor. 648 this.currentContextMenuTarget_ = target.data['uniqueId'][indexInGroup]; 649 650 // Enables if the page can be inspected. 651 if (target.data['canInspect'][indexInGroup]) 652 inspectMenuitem.disabled = false; 653 654 // Enables if the page can be activated. 655 if (target.data['canActivate'][indexInGroup]) 656 activateMenuitem.disabled = false; 657 }, 658 659 onColumnContextMenu_: function(columnId, command) { 660 var menuitem = command.menuitem; 661 var checkedItemCount = 0; 662 var checked = isColumnEnabled(columnId); 663 664 // Leaves a item visible when user tries making invisible but it is the 665 // last one. 666 var enabledColumns = getEnabledColumns(); 667 for (var id in enabledColumns) { 668 if (enabledColumns[id]) 669 checkedItemCount++; 670 } 671 if (checkedItemCount == 1 && checked) 672 return; 673 674 // Toggles the visibility of the column. 675 var newChecked = !checked; 676 menuitem.checked = newChecked; 677 setColumnEnabled(columnId, newChecked); 678 679 this.initColumnModel_(); 680 this.table_.columnModel = this.columnModel_; 681 this.table_.redraw(); 682 }, 683}; 684 685// |taskmanager| has been declared in preload.js. 686taskmanager = TaskManager.getInstance(); 687 688function init() { 689 var params = parseQueryParams(window.location); 690 var opt = {}; 691 opt['isShowCloseButton'] = params.showclose; 692 taskmanager.initialize(document.body, opt); 693} 694 695document.addEventListener('DOMContentLoaded', init); 696document.addEventListener('Close', taskmanager.onClose.bind(taskmanager)); 697