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.
6 function TaskManager() { }
8 cr.addSingletonGetter(TaskManager);
10 TaskManager.prototype = {
12 * Handle window close.
16 if (!this.disabled_) {
17 this.disabled_ = true;
18 commands.disableTaskManager();
23 * Handles selection changes.
24 * This is also called when data of tasks are refreshed, even if selection
25 * has not been changed.
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;
41 if (this.isEndProcessEnabled_ != isEndProcessEnabled) {
42 if (isEndProcessEnabled)
43 $('kill-process').removeAttribute('disabled');
45 $('kill-process').setAttribute('disabled', 'true');
47 this.isEndProcessEnabled_ = isEndProcessEnabled;
52 * Closes taskmanager dialog.
53 * After this function is called, onClose() will be called.
61 * Sends commands to kill selected processes.
64 killSelectedProcesses: function() {
65 var selectedIndexes = this.selectionModel_.selectedIndexes;
66 var dm = this.dataModel_;
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]);
74 commands.killSelectedProcesses(uniqueIds);
78 * Initializes taskmanager.
81 initialize: function(dialogDom, opt) {
83 console.log('ERROR: dialogDom is not defined.');
87 measureTime.startInterval('Load.DOM');
91 this.initialized_ = true;
93 this.elementsCache_ = {};
94 this.dialogDom_ = dialogDom;
95 this.document_ = dialogDom.ownerDocument;
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);
103 this.initElements_();
104 this.initColumnModel_();
105 this.selectionModel_ = new cr.ui.ListSelectionModel();
106 this.dataModel_ = new cr.ui.ArrayDataModel([]);
108 this.selectionModel_.addEventListener('change',
109 this.onSelectionChange.bind(this));
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'];
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';
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);
136 dm.setCompareFunction(columnId, compareFunc);
139 if (isColumnEnabled(DEFAULT_SORT_COLUMN))
140 dm.sort(DEFAULT_SORT_COLUMN, DEFAULT_SORT_DIRECTION);
144 commands.enableTaskManager();
146 // Populate the static localized strings.
147 i18nTemplate.process(this.document_, loadTimeData);
149 measureTime.recordInterval('Load.DOM');
150 measureTime.recordInterval('Load.Total');
152 loadDelayedIncludes(this);
156 * Initializes the visibilities and handlers of the elements.
157 * This method is called by initialize().
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));
171 $('kill-process').addEventListener('click',
172 this.killSelectedProcesses.bind(this));
173 $('about-memory-link').addEventListener('click', commands.openAboutMemory);
177 * Additional initialization of taskmanager. This function is called when
178 * the loading of delayed scripts finished.
181 delayedInitialize: function() {
182 this.initColumnMenu_();
183 this.initTableMenu_();
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];
191 var row = dm.item(i)[columnId];
195 for (var k = 0; k < row.length; k++) {
196 var labelId = 'detail-' + columnId + '-pid' + processId + '-' + k;
197 var label = $(labelId);
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_);
208 this.isFinishedInitDelayed_ = true;
211 addEventListener('resize', t.redraw.bind(t));
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))
222 tableColumns.push(new cr.ui.table.TableColumn(columnId,
223 this.localized_column_[i],
227 for (var i = 0; i < tableColumns.length; i++) {
228 tableColumns[i].renderFunction = this.renderColumn_.bind(this);
231 this.columnModel_ = new cr.ui.table.TableColumnModel(tableColumns);
234 initColumnMenu_: function() {
235 this.column_menu_commands_ = [];
237 this.commandsElement_ = this.document_.createElement('commands');
238 this.document_.body.appendChild(this.commandsElement_);
240 this.columnSelectContextMenu_ = this.document_.createElement('menu');
241 for (var i = 0; i < DEFAULT_COLUMNS.length; i++) {
242 var column = DEFAULT_COLUMNS[i];
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);
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);
261 this.document_.body.appendChild(this.columnSelectContextMenu_);
262 cr.ui.Menu.decorate(this.columnSelectContextMenu_);
264 cr.ui.contextMenuHandler.setContextMenu(this.table_.header,
265 this.columnSelectContextMenu_);
266 cr.ui.contextMenuHandler.setContextMenu(this.table_.list,
267 this.columnSelectContextMenu_);
269 this.document_.addEventListener('command', this.onCommand_.bind(this));
270 this.document_.addEventListener('canExecute',
271 this.onCommandCanExecute_.bind(this));
274 initTableMenu_: function() {
275 this.table_menu_commands_ = [];
276 this.tableContextMenu_ = this.document_.createElement('menu');
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);
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);
294 addMenuItem(this, 'inspect', 'inspect');
295 addMenuItem(this, 'activate', 'activate');
297 this.document_.body.appendChild(this.tableContextMenu_);
298 cr.ui.Menu.decorate(this.tableContextMenu_);
301 initTable_: function() {
302 if (!this.dataModel_ || !this.selectionModel_ || !this.columnModel_) {
303 console.log('ERROR: some models are not defined.');
307 this.table_ = this.dialogDom_.querySelector('.detail-table');
308 cr.ui.Table.decorate(this.table_);
310 this.table_.dataModel = this.dataModel_;
311 this.table_.selectionModel = this.selectionModel_;
312 this.table_.columnModel = this.columnModel_;
314 // Expands height of row when a process has some tasks.
315 this.table_.fixedHeight = false;
317 this.table_.list.addEventListener('contextmenu',
318 this.onTableContextMenuOpened_.bind(this),
321 // Sets custom row render function.
322 this.table_.setRenderFunction(this.getRow_.bind(this));
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.
332 getRow_: function(data, table) {
333 // Trys to reuse the cached row;
334 var listItemElement = this.renderRowFromCache_(data, table);
336 return listItemElement;
338 // Initializes the cache.
339 var pid = data['processId'][0];
340 this.elementsCache_[pid] = {
348 return this.renderRow_(data, table);
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.
358 renderRowFromCache_: function(data, table) {
359 var pid = data['processId'][0];
361 // Checks whether the cache exists or not.
362 var cache = this.elementsCache_[pid];
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)
371 // Checks whether the number of childlen tasks has been changed or not.
372 if (cache.cachedChildSize != data['uniqueId'].length)
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];
382 if (!columnData || !oldColumnData || !columnElements)
385 // Sets new width of the cell.
386 var cellElement = cache.cell[i];
387 cellElement.style.width = cm.getWidth(i) + '%';
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];
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;
411 listItemElement.data = data;
413 // Removes 'selected' and 'lead' attributes.
414 listItemElement.removeAttribute('selected');
415 listItemElement.removeAttribute('lead');
417 return listItemElement;
421 * Create a new list item element.
422 * @return {cr.ui.ListItem} created new list item element.
426 renderRow_: function(data, table) {
427 var pid = data['processId'][0];
428 var cm = table.columnModel;
429 var listItem = new cr.ui.ListItem({label: ''});
431 listItem.className = 'table-row';
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);
439 cm.getRenderFunction(i).call(null, data, cm.getId(i), table));
441 listItem.appendChild(cell);
443 // Stores the cell element to the dictionary.
444 this.elementsCache_[pid].cell[i] = cell;
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';
451 listItem.data = data;
453 // Stores the list item element, the number of columns and the number of
455 this.elementsCache_[pid].listItem = listItem;
456 this.elementsCache_[pid].cachedColumnSize = cm.size;
457 this.elementsCache_[pid].cachedChildSize = data['uniqueId'].length;
463 * Create a new element of the cell.
464 * @return {HTMLDIVElement} created cell
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];
476 if (entry && entry[columnId]) {
477 container.id = 'detail-container-' + columnId + '-pid' + entry.processId;
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);
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_);
503 label.addEventListener('dblclick', (function(uniqueId) {
504 commands.activatePage(uniqueId);
505 }).bind(this, entry['uniqueId'][i]));
508 label.index_in_group = i;
511 cacheIcon[i] = image;
513 label.textContent = entry[columnId][i];
516 label.id = 'detail-' + columnId + '-pid' + pid + '-' + i;
517 label.className = 'detail-' + columnId + ' pid' + pid;
518 container.appendChild(label);
521 this.elementsCache_[pid].columns[columnId] = cache;
522 if (columnId == 'title')
523 this.elementsCache_[pid].icon = cacheIcon;
529 * Updates the task list with the supplied task.
533 processTaskChange: function(task) {
534 var dm = this.dataModel_;
535 var sm = this.selectionModel_;
536 if (!dm || !sm) return;
538 this.table_.list.startBatchUpdates();
541 var type = task.type;
542 var start = task.start;
543 var length = task.length;
544 var tasks = task.tasks;
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;
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;
558 var args = tasks.slice();
559 args.unshift(start, dm.length);
560 dm.splice.apply(dm, args);
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);
569 sm.selectedIndexes = newSelectedIndexes;
572 for (var i = 0; i < dm.length; i++) {
573 pids.push(dm.item(i)['processId'][0]);
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];
583 this.table_.list.endBatchUpdates();
587 * Respond to a command being executed.
590 onCommand_: function(event) {
591 var command = event.command;
592 var commandId = command.id.split('-', 2);
594 var mainCommand = commandId[0];
595 var subCommand = commandId[1];
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_;
605 if (subCommand == 'inspect')
606 commands.inspect(targetUniqueId);
607 else if (subCommand == 'activate')
608 commands.activatePage(targetUniqueId);
610 this.currentContextMenuTarget_ = undefined;
614 onCommandCanExecute_: function(event) {
615 event.canExecute = true;
619 * Store resourceIndex of target resource of context menu, because resource
620 * will be replaced when it is refreshed.
623 onTableContextMenuOpened_: function(e) {
624 if (!this.isFinishedInitDelayed_)
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;
633 // Disabled by default.
634 inspectMenuitem.disabled = true;
635 activateMenuitem.disabled = true;
637 var target = e.target;
638 for (;; target = target.parentNode) {
640 var classes = target.classList;
642 Array.prototype.indexOf.call(classes, 'detail-title') != -1) break;
645 var indexInGroup = target.index_in_group;
647 // Sets the uniqueId for current target page under the mouse corsor.
648 this.currentContextMenuTarget_ = target.data['uniqueId'][indexInGroup];
650 // Enables if the page can be inspected.
651 if (target.data['canInspect'][indexInGroup])
652 inspectMenuitem.disabled = false;
654 // Enables if the page can be activated.
655 if (target.data['canActivate'][indexInGroup])
656 activateMenuitem.disabled = false;
659 onColumnContextMenu_: function(columnId, command) {
660 var menuitem = command.menuitem;
661 var checkedItemCount = 0;
662 var checked = isColumnEnabled(columnId);
664 // Leaves a item visible when user tries making invisible but it is the
666 var enabledColumns = getEnabledColumns();
667 for (var id in enabledColumns) {
668 if (enabledColumns[id])
671 if (checkedItemCount == 1 && checked)
674 // Toggles the visibility of the column.
675 var newChecked = !checked;
676 menuitem.checked = newChecked;
677 setColumnEnabled(columnId, newChecked);
679 this.initColumnModel_();
680 this.table_.columnModel = this.columnModel_;
681 this.table_.redraw();
685 // |taskmanager| has been declared in preload.js.
686 taskmanager = TaskManager.getInstance();
689 var params = parseQueryParams(window.location);
691 opt['isShowCloseButton'] = params.showclose;
692 taskmanager.initialize(document.body, opt);
695 document.addEventListener('DOMContentLoaded', init);
696 document.addEventListener('Close', taskmanager.onClose.bind(taskmanager));