Javascript  |  697行  |  21.58 KB

// 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));