class TfGraphDebuggerDataCard extends LegacyElementMixin()

in tensorboard/plugins/graph/tf_graph_debugger_data_card/tf-graph-debugger-data-card.ts [26:584]


class TfGraphDebuggerDataCard extends LegacyElementMixin(PolymerElement) {
  static readonly template = html`
    <style>
      :host {
        font-size: 12px;
        margin: 0;
        padding: 0;
        display: block;
      }

      h2 {
        padding: 0;
        text-align: center;
        margin: 0;
      }

      .health-pill-legend {
        padding: 15px;
      }

      .health-pill-legend h2 {
        text-align: left;
      }

      .health-pill-entry {
        margin: 10px 10px 10px 0;
      }

      .health-pill-entry .color-preview {
        width: 26px;
        height: 26px;
        border-radius: 3px;
        display: inline-block;
        margin: 0 10px 0 0;
      }

      .health-pill-entry .color-label,
      .health-pill-entry .tensor-count {
        color: #777;
        display: inline-block;
        height: 26px;
        font-size: 22px;
        line-height: 26px;
        vertical-align: top;
      }

      .health-pill-entry .tensor-count {
        float: right;
      }

      #health-pill-step-slider {
        width: 100%;
        margin: 0 0 0 -15px;
        /* 31 comes from adding a padding of 15px from both sides of the paper-slider, subtracting
   * 1px so that the slider width aligns with the image (the last slider marker takes up 1px),
   * and adding 2px to account for a border of 1px on both sides of the image. 30 - 1 + 2.
   * Apparently, the paper-slider lacks a mixin for those padding values. */
        width: calc(100% + 31px);
      }

      #health-pills-loading-spinner {
        width: 20px;
        height: 20px;
        vertical-align: top;
      }

      #health-pill-step-number-input {
        text-align: center;
        vertical-align: top;
      }

      #numeric-alerts-table-container {
        max-height: 400px;
        overflow-x: hidden;
        overflow-y: auto;
      }

      #numeric-alerts-table {
        text-align: left;
      }

      #numeric-alerts-table td {
        vertical-align: top;
      }

      #numeric-alerts-table .first-offense-td {
        display: inline-block;
      }

      .first-offense-td {
        width: 80px;
      }

      .tensor-device-td {
        max-width: 140px;
        word-wrap: break-word;
      }

      .tensor-section-within-table {
        color: #266236;
        cursor: pointer;
        opacity: 0.8;
        text-decoration: underline;
      }

      .tensor-section-within-table:hover {
        opacity: 1;
      }

      .device-section-within-table {
        color: #666;
      }

      .mini-health-pill {
        width: 130px;
      }

      .mini-health-pill > div {
        height: 100%;
        width: 60px;
        border-radius: 3px;
      }

      #event-counts-th {
        padding: 0 0 0 10px;
      }

      .negative-inf-mini-health-pill-section {
        background: rgb(255, 141, 0);
        width: 20px;
      }

      .positive-inf-mini-health-pill-section {
        background: rgb(0, 62, 212);
        width: 20px;
      }

      .nan-mini-health-pill-section {
        background: rgb(204, 47, 44);
        width: 20px;
      }

      .negative-inf-mini-health-pill-section,
      .positive-inf-mini-health-pill-section,
      .nan-mini-health-pill-section {
        color: #fff;
        display: inline-block;
        height: 100%;
        line-height: 20px;
        margin: 0 0 0 10px;
        text-align: center;
      }

      .no-numeric-alerts-notification {
        margin: 0;
      }
    </style>
    <paper-material elevation="1" class="card health-pill-legend">
      <div class="title">
        Enable all (not just sampled) steps. Requires slow disk read.
      </div>
      <paper-toggle-button
        id="enableAllStepsModeToggle"
        checked="{{allStepsModeEnabled}}"
      >
      </paper-toggle-button>
      <h2>
        Step of Health Pills:
        <template is="dom-if" if="[[allStepsModeEnabled]]">
          <input
            type="number"
            id="health-pill-step-number-input"
            min="0"
            max="[[_biggestStepEverSeen]]"
            value="{{specificHealthPillStep::input}}"
          />
        </template>
        <template is="dom-if" if="[[!allStepsModeEnabled]]">
          [[_currentStepDisplayValue]]
        </template>
        <paper-spinner-lite
          active
          hidden$="[[!areHealthPillsLoading]]"
          id="health-pills-loading-spinner"
        ></paper-spinner-lite>
      </h2>
      <template is="dom-if" if="[[allStepsModeEnabled]]">
        <paper-slider
          id="health-pill-step-slider"
          immediate-value="{{specificHealthPillStep}}"
          max="[[_biggestStepEverSeen]]"
          snaps
          step="1"
          value="{{specificHealthPillStep}}"
        ></paper-slider>
      </template>
      <template is="dom-if" if="[[!allStepsModeEnabled]]">
        <template is="dom-if" if="[[_maxStepIndex]]">
          <paper-slider
            id="health-pill-step-slider"
            immediate-value="{{healthPillStepIndex}}"
            max="[[_maxStepIndex]]"
            snaps
            step="1"
            value="{{healthPillStepIndex}}"
          ></paper-slider>
        </template>
      </template>
      <h2>
        Health Pill
        <template is="dom-if" if="[[healthPillValuesForSelectedNode]]">
          Counts for Selected Node
        </template>
        <template is="dom-if" if="[[!healthPillValuesForSelectedNode]]">
          Legend
        </template>
      </h2>
      <template is="dom-repeat" items="[[healthPillEntries]]">
        <div class="health-pill-entry">
          <div
            class="color-preview"
            style="background:[[item.background_color]]"
          ></div>
          <div class="color-label">[[item.label]]</div>
          <div class="tensor-count">
            [[_computeTensorCountString(healthPillValuesForSelectedNode,
            index)]]
          </div>
        </div>
      </template>
      <div hidden$="[[!_hasDebuggerNumericAlerts(debuggerNumericAlerts)]]">
        <h2 id="numeric-alerts-header">Numeric Alerts</h2>
        <p>Alerts are sorted from top to bottom by increasing timestamp.</p>
        <div id="numeric-alerts-table-container">
          <table id="numeric-alerts-table">
            <thead>
              <tr>
                <th>First Offense</th>
                <th>Tensor (Device)</th>
                <th id="event-counts-th">Event Counts</th>
              </tr>
            </thead>
            <tbody id="numeric-alerts-body"></tbody>
          </table>
        </div>
      </div>
      <template
        is="dom-if"
        if="[[!_hasDebuggerNumericAlerts(debuggerNumericAlerts)]]"
      >
        <p class="no-numeric-alerts-notification">
          No numeric alerts so far. That is likely good. Alerts indicate the
          presence of NaN or (+/-) Infinity values, which may be concerning.
        </p>
      </template>
    </paper-material>
  `;
  @property({type: Object})
  renderHierarchy: tf_graph_render.RenderGraphInfo;
  @property({
    type: Array,
    notify: true,
  })
  debuggerNumericAlerts: any;
  @property({type: Object})
  nodeNamesToHealthPills: any;
  @property({
    type: Number,
    notify: true,
  })
  healthPillStepIndex: any;
  // Only relevant if we are in all steps mode, in which case the user may want to view health
  // pills for a specific step.
  @property({
    type: Number,
    notify: true,
  })
  specificHealthPillStep: number = 0;
  // Two-ways
  @property({
    type: String,
    notify: true,
  })
  selectedNode: any;
  @property({
    type: String,
    notify: true,
  })
  highlightedNode: any;
  // The enum value of the include property of the selected node.
  @property({
    type: Number,
    notify: true,
  })
  selectedNodeInclude: any;
  // Whether health pills are currently being loaded, in which case we show a spinner (and the
  // current health pills shown might be out of date).
  @property({type: Boolean})
  areHealthPillsLoading: any;
  @property({
    type: Array,
  })
  healthPillEntries: unknown[] = tf_graph_scene.healthPillEntries;
  // When all-steps mode is enabled, the user can request health pills for any step. In this
  // mode, Tensorboard makes a request every time the user drags the slider to a different step.
  @property({
    type: Boolean,
    notify: true,
  })
  allStepsModeEnabled: any;
  ready() {
    super.ready();
    var mainContainer = document.getElementById('mainContainer');
    var scrollbarContainer = document.querySelector(
      'tf-dashboard-layout .scrollbar'
    ) as HTMLElement | null;
    if (mainContainer && scrollbarContainer) {
      // If this component is being used inside of TensorBoard's dashboard layout, it may easily
      // cause the dashboard layout element to overflow, giving the user 2 scroll bars. Prevent
      // that by hiding whatever content overflows - the user will have to expand the viewport to
      // use this debugging card.
      mainContainer.style.overflow = 'hidden';
      scrollbarContainer.style.overflow = 'hidden';
    }
  }
  _healthPillsAvailable(debuggerDataEnabled: any, nodeNamesToHealthPills: any) {
    // So long as there is a mapping (even if empty) from node name to health pills, show the
    // legend and slider. We do that because, even if no health pills exist at the current step,
    // the user may desire to change steps, and the slider must show for the user to do that.
    return debuggerDataEnabled && nodeNamesToHealthPills;
  }
  _computeTensorCountString(
    healthPillValuesForSelectedNode: any,
    valueIndex: any
  ) {
    if (!healthPillValuesForSelectedNode) {
      // No health pill data is available.
      return '';
    }
    return healthPillValuesForSelectedNode[valueIndex].toFixed(0);
  }
  @computed(
    'nodeNamesToHealthPills',
    'healthPillStepIndex',
    'selectedNode',
    'allStepsModeEnabled',
    'areHealthPillsLoading'
  )
  get healthPillValuesForSelectedNode(): unknown[] | null {
    var nodeNamesToHealthPills = this.nodeNamesToHealthPills;
    var healthPillStepIndex = this.healthPillStepIndex;
    var selectedNode = this.selectedNode;
    var allStepsModeEnabled = this.allStepsModeEnabled;
    var areHealthPillsLoading = this.areHealthPillsLoading;
    if (areHealthPillsLoading) {
      // Health pills are loading. Do not render data that is out of date.
      return null;
    }
    if (!selectedNode) {
      // No node is selected.
      return null;
    }
    const healthPills = nodeNamesToHealthPills[selectedNode];
    if (!healthPills) {
      // This node lacks a health pill.
      return null;
    }
    // If all steps mode is enabled, we use the first health pill in the list because the JSON
    // response from the server is a mapping between node name and a list of 1 health pill.
    const healthPill =
      healthPills[allStepsModeEnabled ? 0 : healthPillStepIndex];
    if (!healthPill) {
      // This node lacks a health pill at the current step.
      return null;
    }
    // The health pill count values start at 2. Each health pill contains 6 values.
    return healthPill.value.slice(2, 8);
  }
  @computed(
    'nodeNamesToHealthPills',
    'healthPillStepIndex',
    'allStepsModeEnabled',
    'specificHealthPillStep',
    'areHealthPillsLoading'
  )
  get _currentStepDisplayValue(): any {
    var nodeNamesToHealthPills = this.nodeNamesToHealthPills;
    var healthPillStepIndex = this.healthPillStepIndex;
    var allStepsModeEnabled = this.allStepsModeEnabled;
    var specificHealthPillStep = this.specificHealthPillStep;
    var areHealthPillsLoading = this.areHealthPillsLoading;
    if (allStepsModeEnabled) {
      // The user seeks health pills for specific step from the server.
      return specificHealthPillStep.toFixed(0);
    }
    if (areHealthPillsLoading) {
      // The current step is undefined.
      return 0;
    }
    for (let nodeName in nodeNamesToHealthPills) {
      // All nodes have the same number of steps stored, so only examine 1 node. We cannot
      // directly index into the nodeNamesToHealthPills object because we do not have a key.
      // If all steps mode is enabled, we only have 1 step to show.
      return nodeNamesToHealthPills[nodeName][healthPillStepIndex].step.toFixed(
        0
      );
    }
    // The current step could not be computed.
    return 0;
  }
  // The biggest step value ever seen. Used to determine what steps of health pills to let the
  // user fetch in all steps mode.
  @computed('nodeNamesToHealthPills')
  get _biggestStepEverSeen(): number {
    var nodeNamesToHealthPills = this.nodeNamesToHealthPills;
    for (let nodeName in nodeNamesToHealthPills) {
      // All nodes have the same number of steps stored, so only examine 1 node.
      // The index is 1 less than the count. Tensorboard backend logic guarantees that the length
      // of the array will be greater than 1.
      var healthPills = nodeNamesToHealthPills[nodeName];
      return Math.max(
        this._biggestStepEverSeen,
        healthPills[healthPills.length - 1].step
      );
    }
    // No steps seen so far. Default to 0.
    return this._biggestStepEverSeen || 0;
  }
  @computed('nodeNamesToHealthPills')
  get _maxStepIndex(): number {
    var nodeNamesToHealthPills = this.nodeNamesToHealthPills;
    for (let nodeName in nodeNamesToHealthPills) {
      // All nodes have the same number of steps stored, so only examine 1 node.
      // The index is 1 less than the count. Tensorboard backend logic guarantees that the length
      // of the array will be greater than 1.
      return nodeNamesToHealthPills[nodeName].length - 1;
    }
    // Return a falsy value. The slider should be hidden.
    return 0;
  }
  _hasDebuggerNumericAlerts(debuggerNumericAlerts: any) {
    return debuggerNumericAlerts && debuggerNumericAlerts.length;
  }
  @observe('debuggerNumericAlerts')
  _updateAlertsList() {
    var debuggerNumericAlerts = this.debuggerNumericAlerts;
    var alertBody = this.$$('#numeric-alerts-body');
    if (!alertBody) {
      return;
    }
    (alertBody as HTMLElement).innerText = '';
    for (var i = 0; i < debuggerNumericAlerts.length; i++) {
      var alert = debuggerNumericAlerts[i];
      var tableRow = document.createElement('tr');
      var timestampTd = document.createElement('td');
      timestampTd.innerText = tf_graph_util.computeHumanFriendlyTime(
        alert.first_timestamp
      );
      timestampTd.classList.add('first-offense-td');
      tableRow.appendChild(timestampTd);
      var tensorDeviceTd = document.createElement('td');
      tensorDeviceTd.classList.add('tensor-device-td');
      var tensorSection = document.createElement('div');
      tensorSection.classList.add('tensor-section-within-table');
      tensorSection.innerText = alert.tensor_name;
      this._addOpExpansionListener(tensorSection, alert.tensor_name);
      tensorDeviceTd.appendChild(tensorSection);
      var deviceSection = document.createElement('div');
      deviceSection.classList.add('device-section-within-table');
      deviceSection.innerText = '(' + alert.device_name + ')';
      tensorDeviceTd.appendChild(deviceSection);
      tableRow.appendChild(tensorDeviceTd);
      var miniHealthPill = document.createElement('div');
      miniHealthPill.classList.add('mini-health-pill');
      var miniHealthPillTd = document.createElement('td');
      miniHealthPillTd.classList.add('mini-health-pill-td');
      miniHealthPillTd.appendChild(miniHealthPill);
      tableRow.appendChild(miniHealthPillTd);
      if (alert.neg_inf_event_count) {
        var negativeInfCountSection = document.createElement('div');
        negativeInfCountSection.classList.add(
          'negative-inf-mini-health-pill-section'
        );
        negativeInfCountSection.innerText = alert.neg_inf_event_count;
        negativeInfCountSection.setAttribute(
          'title',
          alert.neg_inf_event_count + ' events with -\u221E'
        );
        miniHealthPill.appendChild(negativeInfCountSection);
      }
      if (alert.pos_inf_event_count) {
        var positiveInfCountSection = document.createElement('div');
        positiveInfCountSection.classList.add(
          'positive-inf-mini-health-pill-section'
        );
        positiveInfCountSection.innerText = alert.pos_inf_event_count;
        positiveInfCountSection.setAttribute(
          'title',
          alert.pos_inf_event_count + ' events with +\u221E'
        );
        miniHealthPill.appendChild(positiveInfCountSection);
      }
      if (alert.nan_event_count) {
        var nanCountSection = document.createElement('div');
        nanCountSection.classList.add('nan-mini-health-pill-section');
        nanCountSection.innerText = alert.nan_event_count;
        nanCountSection.setAttribute(
          'title',
          alert.nan_event_count + ' events with NaN'
        );
        miniHealthPill.appendChild(nanCountSection);
      }
      (PolymerDom.dom(alertBody) as any).appendChild(tableRow);
    }
  }
  // Adds a listener to an element, so that when that element is clicked, the tensor with
  // tensorName expands.
  _addOpExpansionListener(clickableElement: any, tensorName: any) {
    clickableElement.addEventListener('click', () => {
      // When the user clicks on a tensor name, expand all nodes until the user can see the
      // associated node.
      var nameOfNodeToSelect = tf_graph_render.expandUntilNodeIsShown(
        document.getElementById('scene'),
        this.renderHierarchy,
        tensorName
      );
      // Store the current scroll of the graph info card. Node selection alters that scroll, and
      // we restore the scroll later.
      var previousScrollFromBottom: any;
      var graphInfoCard = document.querySelector(
        'tf-graph-info#graph-info'
      ) as any;
      if (graphInfoCard) {
        previousScrollFromBottom =
          graphInfoCard.scrollHeight - graphInfoCard.scrollTop;
      }
      // Update the selected node within graph logic.
      var previousSelectedNode = this.selectedNode;
      this.set('selectedNode', nameOfNodeToSelect);
      // Scroll the graph info card back down if necessary so that user can see the alerts section
      // again. Selecting the node causes the info card to scroll to the top, which may mean the
      // user no longer sees the list of alerts.
      var scrollToOriginalLocation = () => {
        graphInfoCard.scrollTop =
          graphInfoCard.scrollHeight - previousScrollFromBottom;
      };
      if (graphInfoCard) {
        // This component is used within an info card. Restore the original scroll.
        if (previousSelectedNode) {
          // The card for the selected node has already opened. Immediately restore the scroll.
          scrollToOriginalLocation();
        } else {
          // Give some time for the DOM of the info card to be created before scrolling down.
          window.setTimeout(scrollToOriginalLocation, 20);
        }
      }
    });
  }
}