| // Copyright (c) 2012 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. |
| |
| /** @constructor */ |
| function TaskManager() { } |
| |
| cr.addSingletonGetter(TaskManager); |
| |
| TaskManager.prototype = { |
| /** |
| * Handle window close. |
| * @this |
| */ |
| onClose: function() { |
| if (!this.disabled_) { |
| this.disabled_ = true; |
| commands.disableTaskManager(); |
| } |
| }, |
| |
| /** |
| * Handles selection changes. |
| * This is also called when data of tasks are refreshed, even if selection |
| * has not been changed. |
| * @this |
| */ |
| onSelectionChange: function() { |
| var sm = this.selectionModel_; |
| var dm = this.dataModel_; |
| var selectedIndexes = sm.selectedIndexes; |
| var isEndProcessEnabled = true; |
| if (selectedIndexes.length == 0) |
| isEndProcessEnabled = false; |
| for (var i = 0; i < selectedIndexes.length; i++) { |
| var index = selectedIndexes[i]; |
| var task = dm.item(index); |
| if (task['type'] == 'BROWSER') |
| isEndProcessEnabled = false; |
| } |
| if (this.isEndProcessEnabled_ != isEndProcessEnabled) { |
| if (isEndProcessEnabled) |
| $('kill-process').removeAttribute('disabled'); |
| else |
| $('kill-process').setAttribute('disabled', 'true'); |
| |
| this.isEndProcessEnabled_ = isEndProcessEnabled; |
| } |
| }, |
| |
| /** |
| * Closes taskmanager dialog. |
| * After this function is called, onClose() will be called. |
| * @this |
| */ |
| close: function() { |
| window.close(); |
| }, |
| |
| /** |
| * Sends commands to kill selected processes. |
| * @this |
| */ |
| killSelectedProcesses: function() { |
| var selectedIndexes = this.selectionModel_.selectedIndexes; |
| var dm = this.dataModel_; |
| var uniqueIds = []; |
| for (var i = 0; i < selectedIndexes.length; i++) { |
| var index = selectedIndexes[i]; |
| var task = dm.item(index); |
| uniqueIds.push(task['uniqueId'][0]); |
| } |
| |
| commands.killSelectedProcesses(uniqueIds); |
| }, |
| |
| /** |
| * Initializes taskmanager. |
| * @this |
| */ |
| initialize: function(dialogDom, opt) { |
| if (!dialogDom) { |
| console.log('ERROR: dialogDom is not defined.'); |
| return; |
| } |
| |
| measureTime.startInterval('Load.DOM'); |
| |
| this.opt_ = opt; |
| |
| this.initialized_ = true; |
| |
| this.elementsCache_ = {}; |
| this.dialogDom_ = dialogDom; |
| this.document_ = dialogDom.ownerDocument; |
| |
| this.localized_column_ = []; |
| for (var i = 0; i < DEFAULT_COLUMNS.length; i++) { |
| var columnLabelId = DEFAULT_COLUMNS[i][1]; |
| this.localized_column_[i] = loadTimeData.getString(columnLabelId); |
| } |
| |
| this.initElements_(); |
| this.initColumnModel_(); |
| this.selectionModel_ = new cr.ui.ListSelectionModel(); |
| this.dataModel_ = new cr.ui.ArrayDataModel([]); |
| |
| this.selectionModel_.addEventListener('change', |
| this.onSelectionChange.bind(this)); |
| |
| // Initializes compare functions for column sort. |
| var dm = this.dataModel_; |
| // List of columns to sort by its numerical value as opposed to the |
| // formatted value, e.g., 20480 vs. 20KB. |
| var COLUMNS_SORTED_BY_VALUE = [ |
| 'cpuUsage', 'physicalMemory', 'sharedMemory', 'privateMemory', |
| 'networkUsage', 'webCoreImageCacheSize', 'webCoreScriptsCacheSize', |
| 'webCoreCSSCacheSize', 'fps', 'videoMemory', 'sqliteMemoryUsed', |
| 'goatsTeleported', 'v8MemoryAllocatedSize']; |
| |
| for (var i = 0; i < DEFAULT_COLUMNS.length; i++) { |
| var columnId = DEFAULT_COLUMNS[i][0]; |
| var compareFunc = (function() { |
| var columnIdToSort = columnId; |
| if (COLUMNS_SORTED_BY_VALUE.indexOf(columnId) != -1) |
| columnIdToSort += 'Value'; |
| |
| return function(a, b) { |
| var aValues = a[columnIdToSort]; |
| var bValues = b[columnIdToSort]; |
| var aValue = aValues && aValues[0] || 0; |
| var bvalue = bValues && bValues[0] || 0; |
| return dm.defaultValuesCompareFunction(aValue, bvalue); |
| }; |
| })(); |
| dm.setCompareFunction(columnId, compareFunc); |
| } |
| |
| if (isColumnEnabled(DEFAULT_SORT_COLUMN)) |
| dm.sort(DEFAULT_SORT_COLUMN, DEFAULT_SORT_DIRECTION); |
| |
| this.initTable_(); |
| |
| commands.enableTaskManager(); |
| |
| // Populate the static localized strings. |
| i18nTemplate.process(this.document_, loadTimeData); |
| |
| measureTime.recordInterval('Load.DOM'); |
| measureTime.recordInterval('Load.Total'); |
| |
| loadDelayedIncludes(this); |
| }, |
| |
| /** |
| * Initializes the visibilities and handlers of the elements. |
| * This method is called by initialize(). |
| * @private |
| * @this |
| */ |
| initElements_: function() { |
| // <if expr="pp_ifdef('chromeos')"> |
| // The 'close-window' element exists only on ChromeOS. |
| // This <if ... /if> section is removed while flattening HTML if chrome is |
| // built as Desktop Chrome. |
| if (!this.opt_['isShowCloseButton']) |
| $('close-window').style.display = 'none'; |
| $('close-window').addEventListener('click', this.close.bind(this)); |
| // </if> |
| |
| $('kill-process').addEventListener('click', |
| this.killSelectedProcesses.bind(this)); |
| $('about-memory-link').addEventListener('click', commands.openAboutMemory); |
| }, |
| |
| /** |
| * Additional initialization of taskmanager. This function is called when |
| * the loading of delayed scripts finished. |
| * @this |
| */ |
| delayedInitialize: function() { |
| this.initColumnMenu_(); |
| this.initTableMenu_(); |
| |
| var dm = this.dataModel_; |
| for (var i = 0; i < dm.length; i++) { |
| var processId = dm.item(i)['processId'][0]; |
| for (var j = 0; j < DEFAULT_COLUMNS.length; j++) { |
| var columnId = DEFAULT_COLUMNS[j][0]; |
| |
| var row = dm.item(i)[columnId]; |
| if (!row) |
| continue; |
| |
| for (var k = 0; k < row.length; k++) { |
| var labelId = 'detail-' + columnId + '-pid' + processId + '-' + k; |
| var label = $(labelId); |
| |
| // Initialize a context-menu, if the label exists and its context- |
| // menu is not initialized yet. |
| if (label && !label.contextMenu) |
| cr.ui.contextMenuHandler.setContextMenu(label, |
| this.tableContextMenu_); |
| } |
| } |
| } |
| |
| this.isFinishedInitDelayed_ = true; |
| var t = this.table_; |
| t.redraw(); |
| addEventListener('resize', t.redraw.bind(t)); |
| }, |
| |
| initColumnModel_: function() { |
| var tableColumns = new Array(); |
| for (var i = 0; i < DEFAULT_COLUMNS.length; i++) { |
| var column = DEFAULT_COLUMNS[i]; |
| var columnId = column[0]; |
| if (!isColumnEnabled(columnId)) |
| continue; |
| |
| tableColumns.push(new cr.ui.table.TableColumn(columnId, |
| this.localized_column_[i], |
| column[2])); |
| } |
| |
| for (var i = 0; i < tableColumns.length; i++) { |
| tableColumns[i].renderFunction = this.renderColumn_.bind(this); |
| } |
| |
| this.columnModel_ = new cr.ui.table.TableColumnModel(tableColumns); |
| }, |
| |
| initColumnMenu_: function() { |
| this.column_menu_commands_ = []; |
| |
| this.commandsElement_ = this.document_.createElement('commands'); |
| this.document_.body.appendChild(this.commandsElement_); |
| |
| this.columnSelectContextMenu_ = this.document_.createElement('menu'); |
| for (var i = 0; i < DEFAULT_COLUMNS.length; i++) { |
| var column = DEFAULT_COLUMNS[i]; |
| |
| // Creates command element to receive event. |
| var command = this.document_.createElement('command'); |
| command.id = COMMAND_CONTEXTMENU_COLUMN_PREFIX + '-' + column[0]; |
| cr.ui.Command.decorate(command); |
| this.column_menu_commands_[command.id] = command; |
| this.commandsElement_.appendChild(command); |
| |
| // Creates menuitem element. |
| var item = this.document_.createElement('menuitem'); |
| item.command = command; |
| command.menuitem = item; |
| item.textContent = this.localized_column_[i]; |
| if (isColumnEnabled(column[0])) |
| item.setAttributeNode(this.document_.createAttribute('checked')); |
| this.columnSelectContextMenu_.appendChild(item); |
| } |
| |
| this.document_.body.appendChild(this.columnSelectContextMenu_); |
| cr.ui.Menu.decorate(this.columnSelectContextMenu_); |
| |
| cr.ui.contextMenuHandler.setContextMenu(this.table_.header, |
| this.columnSelectContextMenu_); |
| cr.ui.contextMenuHandler.setContextMenu(this.table_.list, |
| this.columnSelectContextMenu_); |
| |
| this.document_.addEventListener('command', this.onCommand_.bind(this)); |
| this.document_.addEventListener('canExecute', |
| this.onCommandCanExecute_.bind(this)); |
| }, |
| |
| initTableMenu_: function() { |
| this.table_menu_commands_ = []; |
| this.tableContextMenu_ = this.document_.createElement('menu'); |
| |
| var addMenuItem = function(tm, commandId, string_id) { |
| // Creates command element to receive event. |
| var command = tm.document_.createElement('command'); |
| command.id = COMMAND_CONTEXTMENU_TABLE_PREFIX + '-' + commandId; |
| cr.ui.Command.decorate(command); |
| tm.table_menu_commands_[command.id] = command; |
| tm.commandsElement_.appendChild(command); |
| |
| // Creates menuitem element. |
| var item = tm.document_.createElement('menuitem'); |
| item.command = command; |
| command.menuitem = item; |
| item.textContent = loadTimeData.getString(string_id); |
| tm.tableContextMenu_.appendChild(item); |
| }; |
| |
| addMenuItem(this, 'inspect', 'inspect'); |
| addMenuItem(this, 'activate', 'activate'); |
| |
| this.document_.body.appendChild(this.tableContextMenu_); |
| cr.ui.Menu.decorate(this.tableContextMenu_); |
| }, |
| |
| initTable_: function() { |
| if (!this.dataModel_ || !this.selectionModel_ || !this.columnModel_) { |
| console.log('ERROR: some models are not defined.'); |
| return; |
| } |
| |
| this.table_ = this.dialogDom_.querySelector('.detail-table'); |
| cr.ui.Table.decorate(this.table_); |
| |
| this.table_.dataModel = this.dataModel_; |
| this.table_.selectionModel = this.selectionModel_; |
| this.table_.columnModel = this.columnModel_; |
| |
| // Expands height of row when a process has some tasks. |
| this.table_.fixedHeight = false; |
| |
| this.table_.list.addEventListener('contextmenu', |
| this.onTableContextMenuOpened_.bind(this), |
| true); |
| |
| // Sets custom row render function. |
| this.table_.setRenderFunction(this.getRow_.bind(this)); |
| }, |
| |
| /** |
| * Returns a list item element of the list. This method trys to reuse the |
| * cached element, or creates a new element. |
| * @return {cr.ui.ListItem} list item element which contains the given data. |
| * @private |
| * @this |
| */ |
| getRow_: function(data, table) { |
| // Trys to reuse the cached row; |
| var listItemElement = this.renderRowFromCache_(data, table); |
| if (listItemElement) |
| return listItemElement; |
| |
| // Initializes the cache. |
| var pid = data['processId'][0]; |
| this.elementsCache_[pid] = { |
| listItem: null, |
| cell: [], |
| icon: [], |
| columns: {} |
| }; |
| |
| // Create new row. |
| return this.renderRow_(data, table); |
| }, |
| |
| /** |
| * Returns a list item element with re-using the previous cached element, or |
| * returns null if failed. |
| * @return {cr.ui.ListItem} cached un-used element to be reused. |
| * @private |
| * @this |
| */ |
| renderRowFromCache_: function(data, table) { |
| var pid = data['processId'][0]; |
| |
| // Checks whether the cache exists or not. |
| var cache = this.elementsCache_[pid]; |
| if (!cache) |
| return null; |
| |
| var listItemElement = cache.listItem; |
| var cm = table.columnModel; |
| // Checks whether the number of columns has been changed or not. |
| if (cache.cachedColumnSize != cm.size) |
| return null; |
| // Checks whether the number of childlen tasks has been changed or not. |
| if (cache.cachedChildSize != data['uniqueId'].length) |
| return null; |
| |
| // Updates informations of the task if necessary. |
| for (var i = 0; i < cm.size; i++) { |
| var columnId = cm.getId(i); |
| var columnData = data[columnId]; |
| var oldColumnData = listItemElement.data[columnId]; |
| var columnElements = cache.columns[columnId]; |
| |
| if (!columnData || !oldColumnData || !columnElements) |
| return null; |
| |
| // Sets new width of the cell. |
| var cellElement = cache.cell[i]; |
| cellElement.style.width = cm.getWidth(i) + '%'; |
| |
| for (var j = 0; j < columnData.length; j++) { |
| // Sets the new text, if the text has been changed. |
| if (oldColumnData[j] != columnData[j]) { |
| var textElement = columnElements[j]; |
| textElement.textContent = columnData[j]; |
| } |
| } |
| } |
| |
| // Updates icon of the task if necessary. |
| var oldIcons = listItemElement.data['icon']; |
| var newIcons = data['icon']; |
| if (oldIcons && newIcons) { |
| for (var j = 0; j < columnData.length; j++) { |
| var oldIcon = oldIcons[j]; |
| var newIcon = newIcons[j]; |
| if (oldIcon != newIcon) { |
| var iconElement = cache.icon[j]; |
| iconElement.src = newIcon; |
| } |
| } |
| } |
| listItemElement.data = data; |
| |
| // Removes 'selected' and 'lead' attributes. |
| listItemElement.removeAttribute('selected'); |
| listItemElement.removeAttribute('lead'); |
| |
| return listItemElement; |
| }, |
| |
| /** |
| * Create a new list item element. |
| * @return {cr.ui.ListItem} created new list item element. |
| * @private |
| * @this |
| */ |
| renderRow_: function(data, table) { |
| var pid = data['processId'][0]; |
| var cm = table.columnModel; |
| var listItem = new cr.ui.ListItem({label: ''}); |
| |
| listItem.className = 'table-row'; |
| |
| for (var i = 0; i < cm.size; i++) { |
| var cell = document.createElement('div'); |
| cell.style.width = cm.getWidth(i) + '%'; |
| cell.className = 'table-row-cell'; |
| cell.id = 'column-' + pid + '-' + cm.getId(i); |
| cell.appendChild( |
| cm.getRenderFunction(i).call(null, data, cm.getId(i), table)); |
| |
| listItem.appendChild(cell); |
| |
| // Stores the cell element to the dictionary. |
| this.elementsCache_[pid].cell[i] = cell; |
| } |
| |
| // Specifies the height of the row. The height of each row is |
| // 'num_of_tasks * HEIGHT_OF_TASK' px. |
| listItem.style.height = (data['uniqueId'].length * HEIGHT_OF_TASK) + 'px'; |
| |
| listItem.data = data; |
| |
| // Stores the list item element, the number of columns and the number of |
| // childlen. |
| this.elementsCache_[pid].listItem = listItem; |
| this.elementsCache_[pid].cachedColumnSize = cm.size; |
| this.elementsCache_[pid].cachedChildSize = data['uniqueId'].length; |
| |
| return listItem; |
| }, |
| |
| /** |
| * Create a new element of the cell. |
| * @return {HTMLDIVElement} created cell |
| * @private |
| * @this |
| */ |
| renderColumn_: function(entry, columnId, table) { |
| var container = this.document_.createElement('div'); |
| container.className = 'detail-container-' + columnId; |
| var pid = entry['processId'][0]; |
| |
| var cache = []; |
| var cacheIcon = []; |
| |
| if (entry && entry[columnId]) { |
| container.id = 'detail-container-' + columnId + '-pid' + entry.processId; |
| |
| for (var i = 0; i < entry[columnId].length; i++) { |
| var label = document.createElement('div'); |
| if (columnId == 'title') { |
| // Creates a page title element with icon. |
| var image = this.document_.createElement('img'); |
| image.className = 'detail-title-image'; |
| image.src = entry['icon'][i]; |
| image.id = 'detail-title-icon-pid' + pid + '-' + i; |
| label.appendChild(image); |
| var text = this.document_.createElement('div'); |
| text.className = 'detail-title-text'; |
| text.id = 'detail-title-text-pid' + pid + '-' + i; |
| text.textContent = entry['title'][i]; |
| label.appendChild(text); |
| |
| // Chech if the delayed scripts (included in includes.js) have been |
| // loaded or not. If the delayed scripts ware not loaded yet, a |
| // context menu could not be initialized. In such case, it will be |
| // initialized at delayedInitialize() just after loading of delayed |
| // scripts instead of here. |
| if (this.isFinishedInitDelayed_) |
| cr.ui.contextMenuHandler.setContextMenu(label, |
| this.tableContextMenu_); |
| |
| label.addEventListener('dblclick', (function(uniqueId) { |
| commands.activatePage(uniqueId); |
| }).bind(this, entry['uniqueId'][i])); |
| |
| label.data = entry; |
| label.index_in_group = i; |
| |
| cache[i] = text; |
| cacheIcon[i] = image; |
| } else { |
| label.textContent = entry[columnId][i]; |
| cache[i] = label; |
| } |
| label.id = 'detail-' + columnId + '-pid' + pid + '-' + i; |
| label.className = 'detail-' + columnId + ' pid' + pid; |
| container.appendChild(label); |
| } |
| |
| this.elementsCache_[pid].columns[columnId] = cache; |
| if (columnId == 'title') |
| this.elementsCache_[pid].icon = cacheIcon; |
| } |
| return container; |
| }, |
| |
| /** |
| * Updates the task list with the supplied task. |
| * @private |
| * @this |
| */ |
| processTaskChange: function(task) { |
| var dm = this.dataModel_; |
| var sm = this.selectionModel_; |
| if (!dm || !sm) return; |
| |
| this.table_.list.startBatchUpdates(); |
| sm.beginChange(); |
| |
| var type = task.type; |
| var start = task.start; |
| var length = task.length; |
| var tasks = task.tasks; |
| |
| // We have to store the selected pids and restore them after |
| // splice(), because it might replace some items but the replaced |
| // items would lose the selection. |
| var oldSelectedIndexes = sm.selectedIndexes; |
| |
| // Create map of selected PIDs. |
| var selectedPids = {}; |
| for (var i = 0; i < oldSelectedIndexes.length; i++) { |
| var item = dm.item(oldSelectedIndexes[i]); |
| if (item) selectedPids[item['processId'][0]] = true; |
| } |
| |
| var args = tasks.slice(); |
| args.unshift(start, dm.length); |
| dm.splice.apply(dm, args); |
| |
| // Create new array of selected indexes from map of old PIDs. |
| var newSelectedIndexes = []; |
| for (var i = 0; i < dm.length; i++) { |
| if (selectedPids[dm.item(i)['processId'][0]]) |
| newSelectedIndexes.push(i); |
| } |
| |
| sm.selectedIndexes = newSelectedIndexes; |
| |
| var pids = []; |
| for (var i = 0; i < dm.length; i++) { |
| pids.push(dm.item(i)['processId'][0]); |
| } |
| |
| // Sweeps unused caches, which elements no longer exist on the list. |
| for (var pid in this.elementsCache_) { |
| if (pids.indexOf(pid) == -1) |
| delete this.elementsCache_[pid]; |
| } |
| |
| sm.endChange(); |
| this.table_.list.endBatchUpdates(); |
| }, |
| |
| /** |
| * Respond to a command being executed. |
| * @this |
| */ |
| onCommand_: function(event) { |
| var command = event.command; |
| var commandId = command.id.split('-', 2); |
| |
| var mainCommand = commandId[0]; |
| var subCommand = commandId[1]; |
| |
| if (mainCommand == COMMAND_CONTEXTMENU_COLUMN_PREFIX) { |
| this.onColumnContextMenu_(subCommand, command); |
| } else if (mainCommand == COMMAND_CONTEXTMENU_TABLE_PREFIX) { |
| var targetUniqueId = this.currentContextMenuTarget_; |
| |
| if (!targetUniqueId) |
| return; |
| |
| if (subCommand == 'inspect') |
| commands.inspect(targetUniqueId); |
| else if (subCommand == 'activate') |
| commands.activatePage(targetUniqueId); |
| |
| this.currentContextMenuTarget_ = undefined; |
| } |
| }, |
| |
| onCommandCanExecute_: function(event) { |
| event.canExecute = true; |
| }, |
| |
| /** |
| * Store resourceIndex of target resource of context menu, because resource |
| * will be replaced when it is refreshed. |
| * @this |
| */ |
| onTableContextMenuOpened_: function(e) { |
| if (!this.isFinishedInitDelayed_) |
| return; |
| |
| var mc = this.table_menu_commands_; |
| var inspectMenuitem = |
| mc[COMMAND_CONTEXTMENU_TABLE_PREFIX + '-inspect'].menuitem; |
| var activateMenuitem = |
| mc[COMMAND_CONTEXTMENU_TABLE_PREFIX + '-activate'].menuitem; |
| |
| // Disabled by default. |
| inspectMenuitem.disabled = true; |
| activateMenuitem.disabled = true; |
| |
| var target = e.target; |
| for (;; target = target.parentNode) { |
| if (!target) return; |
| var classes = target.classList; |
| if (classes && |
| Array.prototype.indexOf.call(classes, 'detail-title') != -1) break; |
| } |
| |
| var indexInGroup = target.index_in_group; |
| |
| // Sets the uniqueId for current target page under the mouse corsor. |
| this.currentContextMenuTarget_ = target.data['uniqueId'][indexInGroup]; |
| |
| // Enables if the page can be inspected. |
| if (target.data['canInspect'][indexInGroup]) |
| inspectMenuitem.disabled = false; |
| |
| // Enables if the page can be activated. |
| if (target.data['canActivate'][indexInGroup]) |
| activateMenuitem.disabled = false; |
| }, |
| |
| onColumnContextMenu_: function(columnId, command) { |
| var menuitem = command.menuitem; |
| var checkedItemCount = 0; |
| var checked = isColumnEnabled(columnId); |
| |
| // Leaves a item visible when user tries making invisible but it is the |
| // last one. |
| var enabledColumns = getEnabledColumns(); |
| for (var id in enabledColumns) { |
| if (enabledColumns[id]) |
| checkedItemCount++; |
| } |
| if (checkedItemCount == 1 && checked) |
| return; |
| |
| // Toggles the visibility of the column. |
| var newChecked = !checked; |
| menuitem.checked = newChecked; |
| setColumnEnabled(columnId, newChecked); |
| |
| this.initColumnModel_(); |
| this.table_.columnModel = this.columnModel_; |
| this.table_.redraw(); |
| }, |
| }; |
| |
| // |taskmanager| has been declared in preload.js. |
| taskmanager = TaskManager.getInstance(); |
| |
| function init() { |
| var params = parseQueryParams(window.location); |
| var opt = {}; |
| opt['isShowCloseButton'] = params.showclose; |
| taskmanager.initialize(document.body, opt); |
| } |
| |
| document.addEventListener('DOMContentLoaded', init); |
| document.addEventListener('Close', taskmanager.onClose.bind(taskmanager)); |