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