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