diff --git a/WORKSPACE b/WORKSPACE index b3dbc06ec29..ece00866de1 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -229,7 +229,7 @@ new_git_repository( name = "neon_animation", build_file = "bower.BUILD", remote = "https://github.com/polymerelements/neon-animation.git", - tag = "v1.2.2", + tag = "v1.2.3", ) new_git_repository( diff --git a/tensorflow/tensorboard/TAG b/tensorflow/tensorboard/TAG index 3c032078a4a..d6b24041cf0 100644 --- a/tensorflow/tensorboard/TAG +++ b/tensorflow/tensorboard/TAG @@ -1 +1 @@ -18 +19 diff --git a/tensorflow/tensorboard/bower.json b/tensorflow/tensorboard/bower.json index ce1d0251dfe..5c58bd35fd0 100644 --- a/tensorflow/tensorboard/bower.json +++ b/tensorflow/tensorboard/bower.json @@ -44,8 +44,8 @@ "iron-behaviors": "PolymerElements/iron-behaviors#1.0.13", "iron-checked-element-behavior": "PolymerElements/iron-checked-element-behavior#1.0.4", "iron-collapse": "PolymerElements/iron-collapse#1.0.8", - "iron-dropdown": "PolymerElements/iron-dropdown#1.3.0", - "iron-fit-behavior": "PolymerElements/iron-fit-behavior#1.0.6", + "iron-dropdown": "PolymerElements/iron-dropdown#1.4.0", + "iron-fit-behavior": "PolymerElements/iron-fit-behavior#1.2.0", "iron-flex-layout": "PolymerElements/iron-flex-layout#1.3.0", "iron-form-element-behavior": "PolymerElements/iron-form-element-behavior#1.0.6", "iron-icon": "PolymerElements/iron-icon#1.0.8", @@ -118,8 +118,8 @@ "iron-behaviors": "1.0.13", "iron-checked-element-behavior": "1.0.4", "iron-collapse": "1.0.8", - "iron-dropdown": "1.3.0", - "iron-fit-behavior": "1.0.6", + "iron-dropdown": "1.4.0", + "iron-fit-behavior": "1.2.0", "iron-flex-layout": "1.3.0", "iron-form-element-behavior": "1.0.6", "iron-icon": "1.0.8", diff --git a/tensorflow/tensorboard/dist/tf-tensorboard.html b/tensorflow/tensorboard/dist/tf-tensorboard.html index 3180190a0f0..7bea1a11631 100644 --- a/tensorflow/tensorboard/dist/tf-tensorboard.html +++ b/tensorflow/tensorboard/dist/tf-tensorboard.html @@ -196,7 +196,7 @@ var TF; <template> <div id="outer-container" class="scrollbar"> <template is="dom-repeat" items="[[names]]"> - <div class="run-row" color-class$="[[_applyColorClass(item, classScale)]]"> + <div class="run-row"> <div class="checkbox-container vertical-align-container"> <paper-checkbox class="checkbox vertical-align-center" name="[[item]]" checked$="[[_isChecked(item,outSelected.*)]]" on-change="_checkboxChange"></paper-checkbox> </div> @@ -273,7 +273,10 @@ var TF; return []; }, }, - classScale: Function, // map from run name to css class + colorScale: Object, // map from run name to css class + }, + listeners: { + 'dom-change': 'onDomChange', }, observers: [ "_initializeOutSelected(names.*)", @@ -281,6 +284,18 @@ var TF; _initializeOutSelected: function(change) { this.outSelected = change.base.slice(); }, + onDomChange: function(e) { + var checkboxes = Array.prototype.slice.call(this.querySelectorAll("paper-checkbox")); + var scale = this.colorScale; + checkboxes.forEach(function(p) { + var color = scale.scale(p.name); + p.customStyle['--paper-checkbox-checked-color'] = color; + p.customStyle['--paper-checkbox-checked-ink-color'] = color; + p.customStyle['--paper-checkbox-unchecked-color'] = color; + p.customStyle['--paper-checkbox-unchecked-ink-color'] = color; + }); + this.updateStyles(); + }, _checkboxChange: function(e) { var name = e.srcElement.name; var idx = this.outSelected.indexOf(name); @@ -318,7 +333,7 @@ var TF; Runs </h3> </div> - <tf-multi-checkbox names="[[runs]]" out-selected="{{outSelected}}" class-scale="[[classScale]]"></tf-multi-checkbox> + <tf-multi-checkbox id="multiCheckbox" names="[[runs]]" out-selected="{{outSelected}}" color-scale="[[colorScale]]"></tf-multi-checkbox> <paper-button class="x-button" id="toggle-all" on-tap="_toggleAll"> Toggle All Runs </paper-button> @@ -368,7 +383,7 @@ var TF; outSelected: {type: Array, notify: true}, // runs: an array of strings, representing the run names that may be chosen runs: Array, - classScale: Object, // map from run name to color class (css) + colorScale: Object, // TF.ColorScale }, _toggleAll: function() { if (this.outSelected.length > 0) { @@ -452,58 +467,210 @@ var TF; </script> </dom-module> -<dom-module id="tf-color-scale" assetpath="../tf-event-dashboard/"> +<dom-module id="tf-color-scale" assetpath="../tf-color-scale/"> + <script>var TF; +(function (TF) { + TF.palettes = { + googleStandard: [ + '#db4437', + '#ff7043', + '#f4b400', + '#0f9d58', + '#00796b', + '#00acc1', + '#4285f4', + '#5c6bc0', + '#ab47bc' // purple 400 + ], + googleCool: [ + '#9e9d24', + '#0f9d58', + '#00796b', + '#00acc1', + '#4285f4', + '#5c6bc0', + '#607d8b' // blue gray 500 + ], + googleWarm: [ + '#795548', + '#ab47bc', + '#f06292', + '#c2185b', + '#db4437', + '#ff7043', + '#f4b400' // google yellow 700 + ], + googleColorBlind: [ + '#c53929', + '#ff7043', + '#f7cb4d', + '#0b8043', + '#80deea', + '#4285f4', + '#5e35b1' // deep purple 600 + ], + // This rainbow palette attempts to keep a constant brightness across hues. + constantValue: [ + '#f44336', '#ffa216', '#c2d22d', '#51b455', '#1ca091', '#505ec4', + '#a633ba' + ] + }; +})(TF || (TF = {})); +</script> + <script>/* Copyright 2015 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ +// Each color scale is initialized with a configurable number of base hues. +// There are also several palettes available. +// TF.palettes.googleStandard, TF.palettes.googleColorBlind, +// TF.palettes.googleCool, TF.palettes.googleWarm, TF.palettes.constantValue +// Each string is hashed to an integer, +// then mapped to one of the base hues above. +// If there is a collision, the color that is later in an alphabetical sort +// gets nudged a little darker or lighter to disambiguate. +// I would call it mostly stable, in that the same array of strings will +// always return the same colors, but the same individual string may +// shift a little depending on its peers. +// +// runs = ["train", "test", "test1", "test2"] +// ccs = new TF.ColorScale(12, "googleStandard"); +// ccs.domain(runs); +// ccs.getColor("train"); +// ccs.getColor("test1"); +var TF; +(function (TF) { + var ColorScale = (function () { + /** + * The palette you provide defines your spectrum. The colorscale will + * always use the full spectrum you provide. When you define "numColors" + * it resamples at regular intervals along the full extent of the spectrum. + * Thus you get the maximum distance between hues for the "numColors" + * given. This allows the programmer to tweak the algorithm depending on + * how big your expected domain is. If you generally think you're going to + * have a small number of elements in the domain, then a small numColors + * will be serviceable. With large domains, a small numColors would produce + * too many hash collisions, so you'd want to bump it up to the threshold + * of human perception (probably around 14 or 18). + * + * @param {number} [numColors=12] - The number of base colors you want + * in the palette. The more colors, the smaller the number + * the more hash collisions you will have, but the more + * differentiable the base colors will be. + * + * @param {string[]} [palette=TF.palettes.googleColorBlind] - The color + * palette you want as an Array of hex strings. Note, the + * length of the array in this palette is independent of the + * param numColors above. The scale will interpolate to + * create the proper "numColors" given in the first param. + * + */ + function ColorScale(numColors, palette) { + if (numColors === void 0) { numColors = 12; } + if (palette === void 0) { palette = TF.palettes.googleColorBlind; } + this.numColors = numColors; + this.domain([]); + if (palette.length < 2) { + throw new Error('Not enough colors in palette. Must be more than one.'); + } + var k = (this.numColors - 1) / (palette.length - 1); + this.internalColorScale = + d3.scale.linear() + .domain(d3.range(palette.length).map(function (i) { return i * k; })) + .range(palette); + } + ColorScale.prototype.hash = function (s) { + function h(hash, str) { + hash = (hash << 5) - hash + str.charCodeAt(0); + return hash & hash; + } + return Math.abs(Array.prototype.reduce.call(s, h, 0)) % this.numColors; + }; + /** + * Set the domain of strings so we can calculate collisions preemptively. + * Can be reset at any point. + * + * @param {string[]} strings - An array of strings to use as the domain + * for your scale. + */ + ColorScale.prototype.domain = function (strings) { + var _this = this; + this.buckets = d3.range(this.numColors).map(function () { return []; }); + var sortedUniqueKeys = d3.set(strings).values().sort(function (a, b) { + return a.localeCompare(b); + }); + sortedUniqueKeys.forEach(function (s) { return _this.addToDomain(s); }); + return this; + }; + ColorScale.prototype.getBucketForString = function (s) { + var bucketIdx = this.hash(s); + return this.buckets[bucketIdx]; + }; + ColorScale.prototype.addToDomain = function (s) { + var bucketIdx = this.hash(s); + var bucket = this.buckets[bucketIdx]; + if (bucket.indexOf(s) === -1) { + bucket.push(s); + } + }; + ColorScale.prototype.nudge = function (color, amount) { + // If amount is zero, just give back same color + if (amount === 0) { + return color; + } + else if (amount === 1) { + return d3.hcl(color).brighter(0.6); + } + else { + return d3.hcl(color).darker((amount - 1) / 2); + } + }; + /** + * Use the color scale to transform an element in the domain into a color. + * If there was a hash conflict, the color will be "nudged" darker or + * lighter so that it is unique. + * @param {string} The input string to map to a color. + * @return {string} The color corresponding to that input string. + * @throws Will error if input string is not in the scale's domain. + */ + ColorScale.prototype.scale = function (s) { + var bucket = this.getBucketForString(s); + var idx = bucket.indexOf(s); + if (idx === -1) { + throw new Error('String was not in the domain.'); + } + var color = this.internalColorScale(this.hash(s)); + return this.nudge(color, idx).toString(); + }; + return ColorScale; + }()); + TF.ColorScale = ColorScale; +})(TF || (TF = {})); +</script> <script> (function() { - // TODO(danmane) - get Plottable team to make an API point for this - Plottable.Scales.Color._LOOP_LIGHTEN_FACTOR = 0; - var classColorPairs = [ - ["light-blue", "#03A9F4"], - ["red" , "#f44366"], - ["green" , "#4CAF50"], - ["purple" , "#9c27b0"], - ["teal" , "#009688"], - ["pink" , "#e91e63"], - ["orange" , "#ff9800"], - ["brown" , "#795548"], - ["indigo" , "#3f51b5"], - ]; - var classes = _.pluck(classColorPairs, 0); - var colors = _.pluck(classColorPairs, 1); Polymer({ is: "tf-color-scale", properties: { runs: Array, - outClassScale: { - type: Object, - notify: true, - readOnly: true, - value: function() { - return new d3.scale.ordinal().range(classes); - }, - // TODO(danmane): the class scale will not update if the domain changes. - // this behavior is inconsistent with the ColorScale. - // in practice we don't change runs after initial load so it's not currently an issue - }, outColorScale: { type: Object, + computed: "makeColorScale(runs.*)", notify: true, - readOnly: true, - value: function() { - var scale = new Plottable.Scales.Color().range(colors); - scale.onUpdate(this._notifyColorScaleDomainChange.bind(this)); - return scale; - }, }, }, - observers: ["_changeRuns(runs.*)"], - _changeRuns: function(runs) { - this.outClassScale.domain(this.runs); - this.outColorScale.domain(this.runs); - }, - _notifyColorScaleDomainChange: function() { - this.notifyPath("outColorScale.domain_path", this.outColorScale.domain()); - this.outColorScale.domain_path = null; + makeColorScale: function(runs) { + return new TF.ColorScale().domain(this.runs); }, }); })(); @@ -1073,12 +1240,12 @@ var TF; var Y_AXIS_FORMATTER_PRECISION = 3; var TOOLTIP_Y_PIXEL_OFFSET = 15; var TOOLTIP_X_PIXEL_OFFSET = 0; - var TOOLTIP_CIRCLE_SIZE = 3; + var TOOLTIP_CIRCLE_SIZE = 4; var TOOLTIP_CLOSEST_CIRCLE_SIZE = 6; var BaseChart = (function () { function BaseChart(tag, dataFn, xType, colorScale, tooltip) { this.dataFn = dataFn; - this.datasets = {}; + this.run2datasets = {}; this.tag = tag; this.colorScale = colorScale; this.tooltip = tooltip; @@ -1107,11 +1274,11 @@ var TF; }); }; BaseChart.prototype.getDataset = function (run) { - if (this.datasets[run] === undefined) { - this.datasets[run] = + if (this.run2datasets[run] === undefined) { + this.run2datasets[run] = new Plottable.Dataset([], { run: run, tag: this.tag }); } - return this.datasets[run]; + return this.run2datasets[run]; }; BaseChart.prototype.buildChart = function (xType) { if (this.outer) { @@ -1156,20 +1323,58 @@ var TF; TF.BaseChart = BaseChart; var LineChart = (function (_super) { __extends(LineChart, _super); - function LineChart() { - _super.apply(this, arguments); + function LineChart(tag, dataFn, xType, colorScale, tooltip) { + this.datasets = []; + // lastPointDataset is a dataset that contains just the last point of + // every dataset we're currently drawing. + this.lastPointsDataset = new Plottable.Dataset(); + // need to do a single bind, so we can deregister the callback from + // old Plottable.Datasets. (Deregistration is done by identity checks.) + this.updateLastPointDataset = this._updateLastPointDataset.bind(this); + _super.call(this, tag, dataFn, xType, colorScale, tooltip); } LineChart.prototype.buildPlot = function (xAccessor, xScale, yScale) { + var _this = this; this.yAccessor = function (d) { return d.scalar; }; - var plot = new Plottable.Plots.Line(); - plot.x(xAccessor, xScale); - plot.y(this.yAccessor, yScale); - plot.attr('stroke', function (d, i, dataset) { - return dataset.metadata().run; - }, this.colorScale); - this.plot = plot; - var group = this.setupTooltips(plot); - return group; + var linePlot = new Plottable.Plots.Line(); + linePlot.x(xAccessor, xScale); + linePlot.y(this.yAccessor, yScale); + linePlot.attr('stroke', function (d, i, dataset) { + return _this.colorScale.scale(dataset.metadata().run); + }); + this.linePlot = linePlot; + var group = this.setupTooltips(linePlot); + // The scatterPlot will display the last point for each dataset. + // This way, if there is only one datum for the series, it is still + // visible. We hide it when tooltips are active to keep things clean. + var scatterPlot = new Plottable.Plots.Scatter(); + scatterPlot.x(xAccessor, xScale); + scatterPlot.y(this.yAccessor, yScale); + scatterPlot.attr('fill', function (d) { return _this.colorScale.scale(d.run); }); + scatterPlot.attr('opacity', 1); + scatterPlot.size(TOOLTIP_CIRCLE_SIZE * 2); + scatterPlot.datasets([this.lastPointsDataset]); + this.scatterPlot = scatterPlot; + return new Plottable.Components.Group([scatterPlot, group]); + }; + /** Iterates over every dataset, takes the last point, and puts all these + * points in the lastPointsDataset. + */ + LineChart.prototype._updateLastPointDataset = function () { + var relativeAccessor = relativeX().accessor; + var data = this.datasets + .map(function (d) { + var datum = null; + if (d.data().length > 0) { + var idx = d.data().length - 1; + datum = d.data()[idx]; + datum.run = d.metadata().run; + datum.relative = relativeAccessor(datum, idx, d); + } + return datum; + }) + .filter(function (x) { return x != null; }); + this.lastPointsDataset.data(data); }; LineChart.prototype.setupTooltips = function (plot) { var _this = this; @@ -1181,6 +1386,7 @@ var TF; var group = new Plottable.Components.Group([plot, pointsComponent]); var hideTooltips = function () { _this.tooltip.style('opacity', 0); + _this.scatterPlot.attr('opacity', 1); pointsComponent.content().selectAll('.point').remove(); }; var enabled = true; @@ -1233,6 +1439,7 @@ var TF; LineChart.prototype.drawTooltips = function (closestPoint) { var _this = this; // Formatters for value, step, and wall_time + this.scatterPlot.attr('opacity', 0); var valueFormatter = multiscaleFormatter(Y_TOOLTIP_FORMATTER_PRECISION); var stepFormatter = stepX().tooltipFormatter; var wall_timeFormatter = wallX().tooltipFormatter; @@ -1282,9 +1489,11 @@ var TF; LineChart.prototype.changeRuns = function (runs) { var _this = this; _super.prototype.changeRuns.call(this, runs); - var datasets = runs.map(function (r) { return _this.getDataset(r); }); - datasets.reverse(); // draw first run on top - this.plot.datasets(datasets); + runs.reverse(); // draw first run on top + this.datasets.forEach(function (d) { return d.offUpdate(_this.updateLastPointDataset); }); + this.datasets = runs.map(function (r) { return _this.getDataset(r); }); + this.datasets.forEach(function (d) { return d.onUpdate(_this.updateLastPointDataset); }); + this.linePlot.datasets(this.datasets); }; return LineChart; }(BaseChart)); @@ -1316,11 +1525,11 @@ var TF; p.y(y, yScale); p.y0(y0); p.attr('fill', function (d, i, dataset) { - return dataset.metadata().run; - }, _this.colorScale); + return _this.colorScale.scale(dataset.metadata().run); + }); p.attr('stroke', function (d, i, dataset) { - return dataset.metadata().run; - }, _this.colorScale); + return _this.colorScale.scale(dataset.metadata().run); + }); p.attr('stroke-weight', function (d, i, m) { return '0.5px'; }); p.attr('stroke-opacity', function () { return opacities[i]; }); p.attr('fill-opacity', function () { return opacities[i]; }); @@ -1329,7 +1538,7 @@ var TF; var medianPlot = new Plottable.Plots.Line(); medianPlot.x(xAccessor, xScale); medianPlot.y(medianAccessor, yScale); - medianPlot.attr('stroke', function (d, i, m) { return m.run; }, this.colorScale); + medianPlot.attr('stroke', function (d, i, m) { return _this.colorScale.scale(m.run); }); this.plots = plots; return new Plottable.Components.Group(plots); }; @@ -1403,11 +1612,15 @@ var TF; scale: scale, axis: new Plottable.Axes.Numeric(scale, 'bottom'), accessor: function (d, index, dataset) { + // We may be rendering the final-point datum for scatterplot. + // If so, we will have already provided the 'relative' property + if (d.relative != null) { + return d.relative; + } var data = dataset.data(); - // I can't imagine how this function would be called when the data - // is empty - // (after all, it iterates over the data), but lets guard just to be - // safe. + // I can't imagine how this function would be called when the data is + // empty (after all, it iterates over the data), but lets guard just + // to be safe. var first = data.length > 0 ? +data[0].wall_time : 0; return (+d.wall_time - first) / (60 * 60 * 1000); // ms to hours }, @@ -2633,7 +2846,7 @@ var TF; <dom-module id="tf-event-dashboard" assetpath="../tf-event-dashboard/"> <template> <div id="plumbing"> - <tf-color-scale id="colorScale" runs="[[runs]]" out-color-scale="{{colorScale}}" out-class-scale="{{classScale}}"></tf-color-scale> + <tf-color-scale id="colorScale" runs="[[runs]]" out-color-scale="{{colorScale}}"></tf-color-scale> </div> <tf-dashboard-layout> @@ -2646,7 +2859,7 @@ var TF; <tf-x-type-selector id="xTypeSelector" out-x-type="{{xType}}"></tf-x-type-selector> </div> <div class="sidebar-section"> - <tf-run-selector id="runSelector" runs="[[runs]]" class-scale="[[classScale]]" out-selected="{{selectedRuns}}"></tf-run-selector> + <tf-run-selector id="runSelector" runs="[[runs]]" color-scale="[[colorScale]]" out-selected="{{selectedRuns}}"></tf-run-selector> </div> </div> <div class="center"> @@ -2698,6 +2911,10 @@ var TF; computed: "_getVisibleTags(selectedRuns.*, run2tag.*)" }, _show_download_links: Boolean, + colorScale: { + type: Object, + notify: true, + }, }, attached: function() { this.async(function() { @@ -2742,7 +2959,7 @@ var TF; <dom-module id="tf-histogram-dashboard" assetpath="../tf-histogram-dashboard/"> <template> <div id="plumbing"> - <tf-color-scale id="colorScale" runs="[[runs]]" out-color-scale="{{colorScale}}" out-class-scale="{{classScale}}"></tf-color-scale> + <tf-color-scale id="colorScale" runs="[[runs]]" out-color-scale="{{colorScale}}"></tf-color-scale> </div> <tf-dashboard-layout> @@ -2754,7 +2971,7 @@ var TF; <tf-x-type-selector id="xTypeSelector" out-x-type="{{xType}}"></tf-x-type-selector> </div> <div class="sidebar-section"> - <tf-run-selector id="runSelector" runs="[[runs]]" class-scale="[[classScale]]" out-selected="{{selectedRuns}}"></tf-run-selector> + <tf-run-selector id="runSelector" runs="[[runs]]" color-scale="[[colorScale]]" out-selected="{{selectedRuns}}"></tf-run-selector> </div> </div> @@ -6028,6 +6245,21 @@ var tf; })(graph = tf.graph || (tf.graph = {})); })(tf || (tf = {})); // Close module tf.graph.parser. </script> +<script>/* Copyright 2015 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the 'License'); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an 'AS IS' BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ +</script> <script>var __extends = (this && this.__extends) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } @@ -9241,6 +9473,38 @@ var tf; return querySelector.replace(/([:.\[\],/\\\(\)])/g, '\\$1'); } util.escapeQuerySelector = escapeQuerySelector; + // For unit conversion. + util.MEMORY_UNITS = [ + // Atomic unit. + { symbol: 'B' }, + // numUnits specifies how many previous units this unit contains. + { symbol: 'KB', numUnits: 1024 }, { symbol: 'MB', numUnits: 1024 }, + { symbol: 'GB', numUnits: 1024 }, { symbol: 'TB', numUnits: 1024 }, + { symbol: 'PB', numUnits: 1024 } + ]; + util.TIME_UNITS = [ + // Atomic unit. Finest granularity in TensorFlow stat collection. + { symbol: 'µs' }, + // numUnits specifies how many previous units this unit contains. + { symbol: 'ms', numUnits: 1000 }, { symbol: 's', numUnits: 1000 }, + { symbol: 'min', numUnits: 60 }, { symbol: 'hr', numUnits: 60 }, + { symbol: 'days', numUnits: 24 } + ]; + /** + * Returns the human readable version of the unit. + * (e.g. 1.35 GB, 23 MB, 34 ms, 6.53 min etc). + */ + function convertUnitsToHumanReadable(value, units, unitIndex) { + unitIndex = unitIndex == null ? 0 : unitIndex; + if (unitIndex + 1 < units.length && + value >= units[unitIndex + 1].numUnits) { + return tf.graph.util.convertUnitsToHumanReadable(value / units[unitIndex + 1].numUnits, units, unitIndex + 1); + } + // toPrecision() has the tendency to return a number in scientific + // notation and (number - 0) brings it back to normal notation. + return (value.toPrecision(3) - 0) + ' ' + units[unitIndex].symbol; + } + util.convertUnitsToHumanReadable = convertUnitsToHumanReadable; })(util = graph.util || (graph.util = {})); })(graph = tf.graph || (tf.graph = {})); })(tf || (tf = {})); @@ -10981,9 +11245,10 @@ Polymer({ <dom-module id="tf-node-info" assetpath="../tf-graph-info/"> <style> .sub-list-group { - padding: 8px 12px 0px; font-weight: 500; font-size: 12pt; + padding-bottom: 8px; + width: 100%; } .sub-list { @@ -11007,6 +11272,24 @@ Polymer({ font-weight: 400; } + .sub-list-table { + display: table; + width: 100%; + } + + .sub-list-table-row { + display: table-row; + } + + .sub-list-table-cell { + color: #565656; + display: table-cell; + font-size: 11pt; + font-weight: 400; + max-width: 200px; + padding: 0 4px; + } + paper-item { padding: 0; background: #e9e9e9; @@ -11018,7 +11301,7 @@ Polymer({ } .expandedInfo { - padding: 0 0 8px; + padding: 8px 12px; } .controlDeps { @@ -11188,6 +11471,31 @@ Polymer({ </div> </template> </div> + <template is="dom-if" if="{{_hasDisplayableNodeStats}}"> + <div class="sub-list-group node-stats"> + Node Stats + <div class="sub-list-table"> + <template is="dom-if" if="{{_nodeStats.totalBytes}}"> + <div class="sub-list-table-row"> + <div class="sub-list-table-cell">Memory</div> + <div class="sub-list-table-cell">[[_nodeStatsFormattedBytes]]</div> + </div> + </template> + <template is="dom-if" if="{{_nodeStats.totalMicros}}"> + <div class="sub-list-table-row"> + <div class="sub-list-table-cell">Compute Time</div> + <div class="sub-list-table-cell">[[_nodeStatsFormattedComputeTime]]</div> + </div> + </template> + <template is="dom-if" if="{{_nodeStats.outputSize}}"> + <div class="sub-list-table-row"> + <div class="sub-list-table-cell">Tensor Output Size</div> + <div class="sub-list-table-cell">[[_nodeStatsFormattedOutputSize]]</div> + </div> + </template> + </div> + </div> + </template> <div class="toggle-include-group"> <paper-button raised="" class="toggle-include" on-click="_toggleInclude"> <span>[[_auxButtonText]]</span> @@ -11225,6 +11533,27 @@ Polymer({ computed: '_getNode(nodeName, graphHierarchy)', observer: '_resetState' }, + _nodeStats: { + type: Object, + computed: '_getNodeStats(nodeName, graphHierarchy)', + observer: '_resetState' + }, + _hasDisplayableNodeStats: { + type: Object, + computed: '_getHasDisplayableNodeStats(_nodeStats)', + }, + _nodeStatsFormattedBytes: { + type: String, + computed: '_getNodeStatsFormattedBytes(_nodeStats)', + }, + _nodeStatsFormattedComputeTime: { + type: String, + computed: '_getNodeStatsFormattedComputeTime(_nodeStats)', + }, + _nodeStatsFormattedOutputSize: { + type: String, + computed: '_getNodeStatsFormattedOutputSize(_nodeStats)', + }, // The enum value of the include property of the selected node. nodeInclude: { type: Number, @@ -11282,6 +11611,50 @@ Polymer({ _getNode: function(nodeName, graphHierarchy) { return graphHierarchy.node(nodeName); }, + _getNodeStats: function(nodeName, graphHierarchy) { + var node = this._getNode(nodeName, graphHierarchy); + if (node) { + return node.stats; + } + return null; + }, + _getHasDisplayableNodeStats: function(stats) { + if (stats && + (stats.totalBytes > 0 || + stats.totalBytes > 0 || + stats.outputSize)) { + return true; + } + return false; + }, + _getNodeStatsFormattedBytes(stats) { + if (!stats || !stats.totalBytes) { + return; + } + + return tf.graph.util.convertUnitsToHumanReadable( + stats.totalBytes, tf.graph.util.MEMORY_UNITS); + }, + _getNodeStatsFormattedComputeTime(stats) { + if (!stats || !stats.totalMicros) { + return; + } + + return tf.graph.util.convertUnitsToHumanReadable( + stats.totalMicros, tf.graph.util.TIME_UNITS); + }, + _getNodeStatsFormattedOutputSize(stats) { + if (!stats || !stats.outputSize || !stats.outputSize.length) { + return; + } + + // TODO(nsthorat): Display more than just the first tensor shape. + if (stats.outputSize[0].length === 0) { + return "scalar"; + } + + return "[" + stats.outputSize[0].join(", ") + "]"; + }, _getPrintableHTMLNodeName: function(nodeName) { // Insert an optional line break before each slash so that // long node names wrap cleanly at path boundaries. @@ -12097,11 +12470,15 @@ Polymer({ var minValue = params.minValue; var maxValue = params.maxValue; if (colorBy === 'memory') { - minValue = convertToHumanReadable(minValue, MEMORY_UNITS); - maxValue = convertToHumanReadable(maxValue, MEMORY_UNITS); + minValue = tf.graph.util.convertUnitsToHumanReadable( + minValue, tf.graph.util.MEMORY_UNITS); + maxValue = tf.graph.util.convertUnitsToHumanReadable( + maxValue, tf.graph.util.MEMORY_UNITS); } else if (colorBy === 'compute_time') { - minValue = convertToHumanReadable(minValue, TIME_UNITS); - maxValue = convertToHumanReadable(maxValue, TIME_UNITS); + minValue = tf.graph.util.convertUnitsToHumanReadable( + minValue, tf.graph.util.TIME_UNITS); + maxValue = tf.graph.util.convertUnitsToHumanReadable( + maxValue, tf.graph.util.TIME_UNITS); } return { minValue: minValue, @@ -12155,43 +12532,6 @@ Polymer({ this.$.graphdownload.setAttribute('download', graphPath + '.png'); } }); - -// Private methods. -var MEMORY_UNITS = [ - // Atomic unit. - {symbol: 'B'}, - // numUnits specifies how many previous units this unit contains. - {symbol: 'KB', numUnits: 1024}, - {symbol: 'MB', numUnits: 1024}, - {symbol: 'GB', numUnits: 1024}, - {symbol: 'TB', numUnits: 1024}, - {symbol: 'PB', numUnits: 1024} -]; -var TIME_UNITS = [ - // Atomic unit. Finest granularity in TensorFlow stat collection. - {symbol: 'µs'}, - // numUnits specifies how many previous units this unit contains. - {symbol: 'ms', numUnits: 1000}, - {symbol: 's', numUnits: 1000}, - {symbol: 'min', numUnits: 60}, - {symbol: 'hr', numUnits: 60}, - {symbol: 'days', numUnits: 24} -]; - -/** - * Returns the human readable version of the unit. - * (e.g. 1.35 GB, 23 MB, 34 ms, 6.53 min etc). - */ -function convertToHumanReadable(value, units, unitIndex) { - unitIndex = unitIndex == null ? 0 : unitIndex; - if (unitIndex + 1 < units.length && value >= units[unitIndex + 1].numUnits) { - return convertToHumanReadable(value / units[unitIndex + 1].numUnits, - units, unitIndex + 1); - } - // toPrecision() has the tendency to return a number in scientific - // notation and (number - 0) brings it back to normal notation. - return (value.toPrecision(3) - 0) + ' ' + units[unitIndex].symbol; -} })(); // Closing private scope. </script> </dom-module>