// Copyright (c) 2013 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.

'use strict';

base.requireStylesheet('tcmalloc.tcmalloc_snapshot_view');

base.require('tracing.analysis.object_snapshot_view');
base.require('tracing.analysis.util');

base.exportTo('tcmalloc', function() {

  var tsRound = tracing.analysis.tsRound;

  /*
   * Displays a heap memory snapshot in a human readable form.
   * @constructor
   */
  var TcmallocSnapshotView = ui.define(
      'heap-snapshot-view',
      tracing.analysis.ObjectSnapshotView);

  TcmallocSnapshotView.prototype = {
    __proto__: tracing.analysis.ObjectSnapshotView.prototype,

    decorate: function() {
      this.classList.add('tcmalloc-snapshot-view');
    },

    updateContents: function() {
      var snapshot = this.objectSnapshot_;
      if (!snapshot || !snapshot.heap_) {
        this.textContent = 'No heap found.';
        return;
      }
      // Clear old snapshot view.
      this.textContent = '';

      // Note: "total" may actually be less than the largest allocation bin.
      // This might happen if one stack is doing a lot of allocation, then
      // passing off to another stack for deallocation.  That stack will
      // have a high "current bytes" count and the other one might be
      // negative or zero. So "total" may be smaller than the largest trace.
      var subhead = document.createElement('div');
      subhead.textContent = 'Retaining ' +
          this.getByteString_(snapshot.total_.currentBytes) + ' in ' +
          snapshot.total_.currentAllocs +
          ' allocations. Showing > 0.1 MB.';
      subhead.className = 'subhead';
      this.appendChild(subhead);

      // Build a nested tree-view of allocations
      var myList = this.buildAllocList_(snapshot.heap_, false);
      this.appendChild(myList);
    },

    /**
     * Creates a nested list with clickable entries.
     * @param {Object} heapEntry The current trace heap entry.
     * @param {boolean} hide Whether this list is hidden by default.
     * @return {Element} A <ul> list element.
     */
    buildAllocList_: function(heapEntry, hide) {
      var myList = document.createElement('ul');
      myList.hidden = hide;
      var keys = Object.keys(heapEntry.children);
      keys.sort(function(a, b) {
        // Sort from large to small.
        return heapEntry.children[b].currentBytes -
            heapEntry.children[a].currentBytes;
      });
      for (var i = 0; i < keys.length; i++) {
        var traceName = keys[i];
        var trace = heapEntry.children[traceName];
        // Don't show small nodes - they make things harder to see.
        if (trace.currentBytes < 100 * 1024)
          continue;
        var childCount = Object.keys(trace.children).length;
        var isLeaf = childCount == 0;
        var myItem = this.buildItem_(
            traceName, isLeaf, trace.currentBytes, trace.currentAllocs);
        myList.appendChild(myItem);
        // Build a nested <ul> list of my children.
        if (childCount > 0)
          myItem.appendChild(this.buildAllocList_(trace, true));
      }
      return myList;
    },

    /*
     * Returns a <li> for an allocation traceName of size bytes.
     */
    buildItem_: function(traceName, isLeaf, bytes, allocs) {
      var myItem = document.createElement('li');
      myItem.className = 'trace-item';
      myItem.id = traceName;

      var byteDiv = document.createElement('div');
      byteDiv.textContent = this.getByteString_(bytes);
      byteDiv.className = 'trace-bytes';
      myItem.appendChild(byteDiv);

      if (traceName.length == 0) {
        // The empty trace name indicates that the allocations occurred at
        // this trace level, not in a sub-trace. This looks weird as the
        // empty string, so replace it with something non-empty and don't
        // give that line an expander.
        traceName = '(here)';
      } else if (traceName.indexOf('..') == 0) {
        // Tasks in RunTask have special handling. They show the path of the
        // filename. Convert '../../foo.cc' into 'RunTask from foo.cc'.
        var lastSlash = traceName.lastIndexOf('/');
        if (lastSlash != -1)
          traceName = 'Task from ' + traceName.substr(lastSlash + 1);
      }
      var traceDiv = document.createElement('div');
      traceDiv.textContent = traceName;
      traceDiv.className = 'trace-name';
      myItem.appendChild(traceDiv);

      // Don't allow leaf nodes to be expanded.
      if (isLeaf)
        return myItem;

      // Expand the element when it is clicked.
      var self = this;
      myItem.addEventListener('click', function(event) {
        // Allow click on the +/- image (li) or child divs.
        if (this == event.target || this == event.target.parentElement) {
          this.classList.toggle('expanded');
          var child = this.querySelector('ul');
          child.hidden = !child.hidden;
          // Highlight this stack trace in the timeline view.
          self.onItemClicked_(this);
        }
      });
      myItem.classList.add('collapsed');
      return myItem;
    },

    onItemClicked_: function(traceItem) {
      // Compute the full stack trace the user just clicked.
      var traces = [];
      while (traceItem.classList.contains('trace-item')) {
        var traceNameDiv = traceItem.firstElementChild.nextElementSibling;
        traces.unshift(traceNameDiv.textContent);
        var traceNameUl = traceItem.parentElement;
        traceItem = traceNameUl.parentElement;
      }
      // Tell the instance that this stack trace is selected.
      var instance = this.objectSnapshot_.objectInstance;
      instance.selectedTraces = traces;
      // Invalid the viewport to cause a redraw.
      var trackView = document.querySelector('.timeline-track-view');
      trackView.viewport_.dispatchChangeEvent();
    },

    /*
     * Returns a human readable string for a size in bytes.
     */
    getByteString_: function(bytes) {
      var mb = bytes / 1024 / 1024;
      return mb.toFixed(1) + ' MB';
    }
  };

  tracing.analysis.ObjectSnapshotView.register(
      'memory::Heap', TcmallocSnapshotView);

  return {
    TcmallocSnapshotView: TcmallocSnapshotView
  };

});