diff options
16 files changed, 445 insertions, 192 deletions
diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/common.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/common.ts index 8af6f5d74e..202426784d 100644 --- a/tensorflow/tensorboard/components/tf-graph-common/lib/common.ts +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/common.ts @@ -100,7 +100,7 @@ export function time<T>(msg: string, task: () => T) { export interface ProgressTracker { updateProgress(incrementValue: number): void; setMessage(msg: string): void; - reportError(msg: string): void; + reportError(msg: string, err: Error): void; } /** @@ -124,7 +124,10 @@ export function getTracker(polymerComponent: any) { msg: polymerComponent.progress.msg }); }, - reportError: function(msg) { + reportError: function(msg: string, err) { + // Log the stack trace in the console. + console.error(err.stack); + // And send a user-friendly message to the UI. polymerComponent.set("progress", { value: polymerComponent.progress.value, msg: msg, @@ -146,7 +149,7 @@ export function getSubtaskTracker(parentTracker: ProgressTracker, setMessage: function(progressMsg) { // The parent should show a concatenation of its message along with // its subtask tracker message. - parentTracker.setMessage(subtaskMsg + " : " + progressMsg); + parentTracker.setMessage(subtaskMsg + ": " + progressMsg); }, updateProgress: function(incrementValue) { // Update the parent progress relative to the child progress. @@ -155,10 +158,10 @@ export function getSubtaskTracker(parentTracker: ProgressTracker, parentTracker .updateProgress(incrementValue * impactOnTotalProgress / 100); }, - reportError: function(errorMsg) { + reportError: function(msg: string, err: Error) { // The parent should show a concatenation of its message along with // its subtask error message. - parentTracker.reportError(subtaskMsg + " : " + errorMsg); + parentTracker.reportError(subtaskMsg + ": " + msg, err); } }; } @@ -181,7 +184,9 @@ export function runAsyncTask<T>(msg: string, incProgressValue: number, // Return the result to be used by other tasks. resolve(result); } catch (e) { - reject(result); + // Errors that happen inside asynchronous tasks are + // reported to the tracker using a user-friendly message. + tracker.reportError("Failed " + msg, e); } }, ASYNC_TASK_DELAY); }); diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/graph.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/graph.ts index af58f46032..de9c6360b6 100644 --- a/tensorflow/tensorboard/components/tf-graph-common/lib/graph.ts +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/graph.ts @@ -34,6 +34,9 @@ export enum InclusionType {INCLUDE, EXCLUDE, UNSPECIFIED}; /** Indicates if a series is to be grouped in the graph when rendered. */ export enum SeriesGroupingType {GROUP, UNGROUP}; +/** Attribute key reserved for the shapes of the output tensors. */ +const OUTPUT_SHAPES_KEY = "_output_shapes"; + /** * A BaseEdge is the label object (in the graphlib sense) for an edge in the * original, full graph produced after parsing. Subsequent graphs, like those @@ -111,16 +114,31 @@ export interface Node { include: InclusionType; } +export type TensorShape = number[]; + export interface OpNode extends Node { op: string; device: string; - attr: {key: string, value: Object}[]; + attr: {key: string, value: any}[]; inputs: NormalizedInput[]; inEmbeddings: OpNode[]; outEmbeddings: OpNode[]; // The name of the SeriesNode that can contain this node in its series. // If there is no such node, then this is null. owningSeries: string; + /** + * Array of tensor shapes. Null if the number of output tensors is unknown, + * otherwise the length will equal the number of output tensors. + * + * Each tensor shape is an array of numbers, or null. Details: + * - null means unknown rank, and therefore entire shape is unknown. + * - [4, 2, 1] means rank-3 tensor of size 4x2x1. + * - [] means a scalar (rank-0 tensor). + * - [1] means rank-1 tensor of size 1 (not the same as scalar). + * - [5, -1, 3] means rank-3 tensor of shape is 5x?x3. The size + * of the middle dimension is unknown (encoded as -1). + */ + outputShapes: TensorShape[]; } export interface BridgeNode extends Node { @@ -304,7 +322,7 @@ class OpNodeImpl implements OpNode { op: string; device: string; stats: NodeStats; - attr: {key: string, value: Object}[]; + attr: {key: string, value: any}[]; inputs: NormalizedInput[]; type: NodeType; isGroupNode: boolean; @@ -314,22 +332,24 @@ class OpNodeImpl implements OpNode { parentNode: Node; include: InclusionType; owningSeries: string; + outputShapes: TensorShape[]; /** * Constructs a new Op node. * * @param rawNode The raw node. - * @param normalizedInputs An array of normalized - * inputs that denote the incoming edges to the current node. Each input - * contains the normalized name of the source node, whether it has a - * number part and whether it is a control dependency. */ - constructor(rawNode: tf.TFNode, normalizedInputs: NormalizedInput[]) { + constructor(rawNode: tf.TFNode) { this.op = rawNode.op; this.name = rawNode.name; this.device = rawNode.device; this.attr = rawNode.attr; - this.inputs = normalizedInputs; + // An array of normalized inputs that denote the incoming edges to + // the current node. Each input contains the normalized name of the + // source node, whether it has a number part and whether it is a + // control dependency. + this.inputs = normalizeInputs(rawNode.input); + this.outputShapes = extractOutputShapes(rawNode.attr); // additional properties this.type = NodeType.OP; this.isGroupNode = false; @@ -548,7 +568,12 @@ export interface Metaedge extends graphlib.EdgeObject { */ numRefEdges: number; - addBaseEdge(edge: BaseEdge): void; + /** + * Total size (number of units) of all the tensors flowing through this edge. + */ + totalSize: number; + + addBaseEdge(edge: BaseEdge, h: hierarchy.Hierarchy): void; } export function createMetaedge(v: string, w: string): Metaedge { @@ -566,6 +591,7 @@ class MetaedgeImpl implements Metaedge { numRegularEdges: number; numControlEdges: number; numRefEdges: number; + totalSize: number; constructor(v: string, w: string) { this.v = v; @@ -575,9 +601,10 @@ class MetaedgeImpl implements Metaedge { this.numRegularEdges = 0; this.numControlEdges = 0; this.numRefEdges = 0; + this.totalSize = 0; } - addBaseEdge(edge: BaseEdge): void { + addBaseEdge(edge: BaseEdge, h: hierarchy.Hierarchy): void { this.baseEdgeList.push(edge); if (edge.isControlDependency) { this.numControlEdges += 1; @@ -587,6 +614,38 @@ class MetaedgeImpl implements Metaedge { if (edge.isReferenceEdge) { this.numRefEdges += 1; } + // Compute the size of the tensor flowing through this + // base edge. + this.totalSize += MetaedgeImpl.computeSizeOfEdge(edge, h); + } + + private static computeSizeOfEdge(edge: BaseEdge, h: hierarchy.Hierarchy) + : number { + let opNode = <OpNode> h.node(edge.v); + if (opNode.outputShapes == null) { + // No shape information. Asssume a single number. This gives + // a lower bound for the total size. + return 1; + } + // Sum the sizes of all output tensors. + return _(opNode.outputShapes).map(shape => { + // If the shape is unknown, treat it as 1 when computing + // total size. This gives a lower bound for the total size. + if (shape == null) { + return 1; + } + // Multiply all shapes to get the total size of the tensor. + // E.g. The total size of [4, 2, 1] is 4 * 2 * 1. + return _(shape).reduce((accumulated, currSize) => { + // If this particular dimension is unknown, treat + // it as 1 when computing total size. This gives a lower bound + // for the total size. + if (currSize === -1) { + currSize = 1; + } + return accumulated * currSize; + }, 1); + }).sum(); } } @@ -647,6 +706,49 @@ class SeriesNodeImpl implements SeriesNode { } /** + * Extracts the shapes of the output tensors from the attr property in the + * node proto. + */ +function extractOutputShapes(attr: {key: string, value: any}[]): TensorShape[] { + let result = null; + // We don't know anything about the output tensors. + if (!attr) { + return null; + } + for (let i = 0; i < attr.length; i++) { + let {key, value} = attr[i]; + if (key === OUTPUT_SHAPES_KEY) { + // Map all output tensors into array of numbers denoting their shape. + let result = value.list.shape.map(shape => { + if (shape.unknown_rank) { + // This output tensor is of unknown rank. We don't know if it is a + // scalar, or a tensor, or of what shape it is. + return null; + } + if (shape.dim == null || + (shape.dim.length === 1 && shape.dim[0].size == null)) { + // This output tensor is a scalar. + return []; + } + // This output tensor has a known rank. Map each dimension size + // into a number. + return shape.dim.map(dim => { + // Size can be -1 if this particular dimension is unknown. + return dim.size; + }); + }); + // Since we already processed it, remove the entry from the attribute + // list (saves memory). + attr.splice(i, 1); + return result; + } + } + // We didn't find OUTPUT_SHAPES_KEY in attributes, so we don't know anything + // about the output tensors. + return result; +} + +/** * Normalizes the inputs and extracts associated metadata: * 1) Inputs can contain a colon followed by a number at the end * (e.g. inputName:1) and we remove this from the input name, and take note @@ -729,8 +831,7 @@ export function build(rawNodes: tf.TFNode[], params: BuildParams, let opNodes = new Array<OpNode>(rawNodes.length); let index = 0; _.each(rawNodes, rawNode => { - let normalizedInputs = normalizeInputs(rawNode.input); - let opNode = new OpNodeImpl(rawNode, normalizedInputs); + let opNode = new OpNodeImpl(rawNode); if (isInEmbeddedPred(opNode)) { embeddingNodeNames.push(opNode.name); inEmbedding[opNode.name] = opNode; @@ -819,9 +920,6 @@ export function build(rawNodes: tf.TFNode[], params: BuildParams, return graph; }, tracker); - }) - .catch(function(reason) { - throw new Error("Failure creating graph"); }); }; diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/hierarchy.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/hierarchy.ts index 0af9905924..2895efc8bb 100644 --- a/tensorflow/tensorboard/components/tf-graph-common/lib/hierarchy.ts +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/hierarchy.ts @@ -150,7 +150,7 @@ class HierarchyImpl implements Hierarchy { // Copy the BaseEdge from the parent's Metaedge into this // bridgegraph Metaedge. - bridgeMetaedge.addBaseEdge(baseEdge); + bridgeMetaedge.addBaseEdge(baseEdge, this); }); }) .value(); // force lodash chain execution. @@ -250,16 +250,14 @@ class HierarchyImpl implements Hierarchy { getOneWayEdges(node: GroupNode|OpNode, inEdges: boolean) { let edges = { control: [], regular: [] }; // A node with no parent cannot have any edges. - if (!node.parentNode) { - return edges; - } - if (node.parentNode.isGroupNode) { - let parentNode = <GroupNode>node.parentNode; - let metagraph = parentNode.metagraph; - let bridgegraph = this.getBridgegraph(parentNode.name); - findEdgeTargetsInGraph(metagraph, node, inEdges, edges); - findEdgeTargetsInGraph(bridgegraph, node, inEdges, edges); + if (!node.parentNode || !node.parentNode.isGroupNode) { + return edges; } + let parentNode = <GroupNode> node.parentNode; + let metagraph = parentNode.metagraph; + let bridgegraph = this.getBridgegraph(parentNode.name); + findEdgeTargetsInGraph(metagraph, node, inEdges, edges); + findEdgeTargetsInGraph(bridgegraph, node, inEdges, edges); return edges; } @@ -365,20 +363,23 @@ class HierarchyImpl implements Hierarchy { function findEdgeTargetsInGraph( graph: graphlib.Graph<GroupNode|OpNode, Metaedge>, node: Node, inbound: boolean, targets: Edges): void { - _.each(<Metaedge[]> graph.edges(), e => { - let [selfName, otherName] = inbound ? [e.w, e.v] : [e.v, e.w]; - if (selfName === node.name) { - if (node.isGroupNode) { - let targetList = graph.edge(e).numRegularEdges - ? targets.regular : targets.control; - targetList.push(otherName); - } else { - _.each(graph.edge(e).baseEdgeList, baseEdge => { - let targetList = baseEdge.isControlDependency - ? targets.control : targets.regular; - targetList.push(inbound ? baseEdge.v : baseEdge.w); - }); - } + let edges = inbound ? graph.inEdges(node.name) : graph.outEdges(node.name); + _.each(edges, e => { + let otherName = inbound ? e.v : e.w; + let metaedge = graph.edge(e); + + if (node.isGroupNode && metaedge.baseEdgeList.length > 1) { + let targetList = metaedge.numRegularEdges + ? targets.regular : targets.control; + targetList.push(otherName); + } else { + // Enumerate all the base edges if the node is an OpNode, or the + // metaedge has only 1 edge in it. + _.each(metaedge.baseEdgeList, (baseEdge: BaseEdge) => { + let targetList = baseEdge.isControlDependency + ? targets.control : targets.regular; + targetList.push(inbound ? baseEdge.v : baseEdge.w); + }); } }); } @@ -428,8 +429,6 @@ export function build(graph: tf.graph.SlimGraph, params: HierarchyParams, }) .then(() => { return h; - }).catch(function(reason) { - throw new Error("Failure creating graph hierarchy"); }); }; @@ -552,10 +551,8 @@ function addEdges(h: Hierarchy, graph: SlimGraph, !baseEdge.isControlDependency) { sharedAncestorNode.hasNonControlEdges = true; } - metaedge.addBaseEdge(baseEdge); - + metaedge.addBaseEdge(baseEdge, h); }); - }; /** diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/parser.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/parser.ts index 0b8a308fe3..18ee1dcf8c 100644 --- a/tensorflow/tensorboard/components/tf-graph-common/lib/parser.ts +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/parser.ts @@ -97,9 +97,6 @@ export function readAndParseData(dataset: {path: string, statsPath: string}, nodes: nodes, statsJson: statsJson }; - }) - .catch(function(reason) { - throw new Error("Failure parsing graph definition"); }); } @@ -139,7 +136,9 @@ export function parsePbtxt(input: string): TFNode[] { "node.attr.value.list.type": true, "node.attr.value.shape.dim": true, "node.attr.value.tensor.string_val": true, - "node.attr.value.tensor.tensor_shape.dim": true + "node.attr.value.tensor.tensor_shape.dim": true, + "node.attr.value.list.shape": true, + "node.attr.value.list.shape.dim": true }; /** diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/render.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/render.ts index df4119ef5b..f0ff1fbfc6 100644 --- a/tensorflow/tensorboard/components/tf-graph-common/lib/render.ts +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/render.ts @@ -79,6 +79,18 @@ export let SeriesNodeColors = { DEFAULT_STROKE: "#b2b2b2" }; +/** The minimum stroke width of an edge. */ +const MIN_EDGE_WIDTH = 0.75; + +/** The maximum stroke width of an edge. */ +const MAX_EDGE_WIDTH = 12; + +/** The exponent used in the power scale for edge thickness. */ +const EDGE_WIDTH_SCALE_EXPONENT = 0.3; + +/** The domain (min and max value) for the edge width. */ +const DOMAIN_EDGE_WIDTH_SCALE = [1, 5E6]; + /** * Parameters that affect how the graph is rendered on the screen. */ @@ -158,6 +170,7 @@ export class RenderGraphInfo { private deviceColorMap: d3.scale.Ordinal<string, string>; private memoryUsageScale: d3.scale.Linear<string, string>; private computeTimeScale: d3.scale.Linear<string, string>; + edgeWidthScale: d3.scale.Pow<number, number>; // Since the rendering information for each node is constructed lazily, // upon node's expansion by the user, we keep a map between the node's name // and whether the rendering information was already constructed for that @@ -173,6 +186,12 @@ export class RenderGraphInfo { .range(_.map(d3.range(hierarchy.devices.length), MetanodeColors.DEVICE_PALETTE)); + this.edgeWidthScale = d3.scale.pow() + .exponent(EDGE_WIDTH_SCALE_EXPONENT) + .domain(DOMAIN_EDGE_WIDTH_SCALE) + .range([MIN_EDGE_WIDTH, MAX_EDGE_WIDTH]) + .clamp(true); + let topLevelGraph = hierarchy.root.metagraph; // Find the maximum and minimum memory usage. let memoryExtent = d3.extent(topLevelGraph.nodes(), @@ -218,6 +237,13 @@ export class RenderGraphInfo { } /** + * Get the underlying node in the hierarchical graph by its name. + */ + getNodeByName(nodeName: string): Node { + return this.hierarchy.node(nodeName); + } + + /** * Get a previously created RenderNodeInfo for the specified node name, * or create one if it hasn't been created yet. */ diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/scene/annotation.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/annotation.ts index 425eea0408..4301acc8d9 100644 --- a/tensorflow/tensorboard/components/tf-graph-common/lib/scene/annotation.ts +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/annotation.ts @@ -39,11 +39,11 @@ module tf.graph.scene.annotation { * @param container selection of the container. * @param annotationData node.{in|out}Annotations * @param d node to build group for. - * @param sceneBehavior polymer scene element. + * @param sceneElement <tf-graph-scene> polymer element. * @return selection of appended objects */ export function buildGroup(container, annotationData: render.AnnotationList, - d: render.RenderNodeInfo, sceneBehavior) { + d: render.RenderNodeInfo, sceneElement) { // Select all children and join with data. let annotationGroups = container.selectAll(function() { // using d3's selector function @@ -60,7 +60,7 @@ export function buildGroup(container, annotationData: render.AnnotationList, let aGroup = d3.select(this); // Add annotation to the index in the scene - sceneBehavior.addAnnotationGroup(a, d, aGroup); + sceneElement.addAnnotationGroup(a, d, aGroup); // Append annotation edge let edgeType = Class.Annotation.EDGE; let metaedge = a.renderMetaedgeInfo && a.renderMetaedgeInfo.metaedge; @@ -71,11 +71,11 @@ export function buildGroup(container, annotationData: render.AnnotationList, if (metaedge && metaedge.numRefEdges) { edgeType += " " + Class.Edge.REF_LINE; } - edge.appendEdge(aGroup, a, sceneBehavior, edgeType); + edge.appendEdge(aGroup, a, sceneElement, edgeType); - if (a.annotationType !== tf.graph.render.AnnotationType.ELLIPSIS) { + if (a.annotationType !== render.AnnotationType.ELLIPSIS) { addAnnotationLabelFromNode(aGroup, a); - buildShape(aGroup, a, sceneBehavior); + buildShape(aGroup, a); } else { addAnnotationLabel(aGroup, a.node.name, a, Class.Annotation.ELLIPSIS); } @@ -89,9 +89,9 @@ export function buildGroup(container, annotationData: render.AnnotationList, }) .each(function(a) { let aGroup = d3.select(this); - update(aGroup, d, a, sceneBehavior); - if (a.annotationType !== tf.graph.render.AnnotationType.ELLIPSIS) { - addInteraction(aGroup, d, a, sceneBehavior); + update(aGroup, d, a, sceneElement); + if (a.annotationType !== render.AnnotationType.ELLIPSIS) { + addInteraction(aGroup, d, a, sceneElement); } }); @@ -100,7 +100,7 @@ export function buildGroup(container, annotationData: render.AnnotationList, let aGroup = d3.select(this); // Remove annotation from the index in the scene - sceneBehavior.removeAnnotationGroup(a, d, aGroup); + sceneElement.removeAnnotationGroup(a, d, aGroup); }) .remove(); return annotationGroups; @@ -110,13 +110,13 @@ export function buildGroup(container, annotationData: render.AnnotationList, * Maps an annotation enum to a class name used in css rules. */ function annotationToClassName(annotationType: render.AnnotationType) { - return (tf.graph.render.AnnotationType[annotationType] || "") + return (render.AnnotationType[annotationType] || "") .toLowerCase() || null; } -function buildShape(aGroup, a: render.Annotation, sceneBehavior) { - if (a.annotationType === tf.graph.render.AnnotationType.SUMMARY) { - let summary = scene.selectOrCreateChild(aGroup, "use"); +function buildShape(aGroup, a: render.Annotation) { + if (a.annotationType === render.AnnotationType.SUMMARY) { + let summary = selectOrCreateChild(aGroup, "use"); summary.attr({ "class": "summary", "xlink:href": "#summary-icon", @@ -125,7 +125,7 @@ function buildShape(aGroup, a: render.Annotation, sceneBehavior) { } else { let shape = node.buildShape(aGroup, a, Class.Annotation.NODE); // add title tag to get native tooltips - scene.selectOrCreateChild(shape, "title").text(a.node.name); + selectOrCreateChild(shape, "title").text(a.node.name); } } @@ -152,16 +152,16 @@ function addAnnotationLabel(aGroup, label, a, additionalClassNames, } function addInteraction(selection, d: render.RenderNodeInfo, - annotation: tf.graph.render.Annotation, sceneBehavior) { + annotation: render.Annotation, sceneElement) { selection .on("mouseover", a => { - sceneBehavior.fire("annotation-highlight", { + sceneElement.fire("annotation-highlight", { name: a.node.name, hostName: d.node.name }); }) .on("mouseout", a => { - sceneBehavior.fire("annotation-unhighlight", { + sceneElement.fire("annotation-unhighlight", { name: a.node.name, hostName: d.node.name }); @@ -170,15 +170,15 @@ function addInteraction(selection, d: render.RenderNodeInfo, // Stop this event"s propagation so that it isn't also considered a // graph-select. (<Event>d3.event).stopPropagation(); - sceneBehavior.fire("annotation-select", { + sceneElement.fire("annotation-select", { name: a.node.name, hostName: d.node.name }); }); - if (annotation.annotationType !== tf.graph.render.AnnotationType.SUMMARY && - annotation.annotationType !== tf.graph.render.AnnotationType.CONSTANT) { - selection.on("contextmenu", tf.graph.scene.contextmenu.getMenu( - tf.graph.scene.node.getContextMenu(annotation.node, sceneBehavior))); + if (annotation.annotationType !== render.AnnotationType.SUMMARY && + annotation.annotationType !== render.AnnotationType.CONSTANT) { + selection.on("contextmenu", contextmenu.getMenu( + node.getContextMenu(annotation.node, sceneElement))); } }; @@ -188,21 +188,21 @@ function addInteraction(selection, d: render.RenderNodeInfo, * @param aGroup selection of a "g.annotation" element. * @param d Host node data. * @param a annotation node data. - * @param scene Polymer scene element. + * @param scene <tf-graph-scene> polymer element. */ function update(aGroup, d: render.RenderNodeInfo, a: render.Annotation, - sceneBehavior) { + sceneElement) { let cx = layout.computeCXPositionOfNodeShape(d); // Annotations that point to embedded nodes (constants,summary) // don't have a render information attached so we don't stylize these. // Also we don't stylize ellipsis annotations (the string "... and X more"). if (a.renderNodeInfo && - a.annotationType !== tf.graph.render.AnnotationType.ELLIPSIS) { - node.stylize(aGroup, a.renderNodeInfo, sceneBehavior, + a.annotationType !== render.AnnotationType.ELLIPSIS) { + node.stylize(aGroup, a.renderNodeInfo, sceneElement, Class.Annotation.NODE); } - if (a.annotationType === tf.graph.render.AnnotationType.SUMMARY) { + if (a.annotationType === render.AnnotationType.SUMMARY) { // Update the width of the annotation to give space for the image. a.width += 10; } @@ -224,11 +224,11 @@ function update(aGroup, d: render.RenderNodeInfo, a: render.Annotation, }); // Node position (only one of the shape selection will be non-empty.) - scene.positionEllipse(aGroup.select("." + Class.Annotation.NODE + " ellipse"), + positionEllipse(aGroup.select("." + Class.Annotation.NODE + " ellipse"), cx + a.dx, d.y + a.dy, a.width, a.height); - scene.positionRect(aGroup.select("." + Class.Annotation.NODE + " rect"), + positionRect(aGroup.select("." + Class.Annotation.NODE + " rect"), cx + a.dx, d.y + a.dy, a.width, a.height); - scene.positionRect(aGroup.select("." + Class.Annotation.NODE + " use"), + positionRect(aGroup.select("." + Class.Annotation.NODE + " use"), cx + a.dx, d.y + a.dy, a.width, a.height); // Edge position diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/scene/edge.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/edge.ts index 5ae6244e00..d0f1e8fad6 100644 --- a/tensorflow/tensorboard/components/tf-graph-common/lib/scene/edge.ts +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/edge.ts @@ -19,10 +19,13 @@ limitations under the License. module tf.graph.scene.edge { +/** Delimiter between dimensions when showing sizes of tensors. */ +const TENSOR_SHAPE_DELIM = "×"; + export type EdgeData = {v: string, w: string, label: render.RenderMetaedgeInfo}; export function getEdgeKey(edgeObj: EdgeData) { - return edgeObj.v + tf.graph.EDGE_KEY_DELIM + edgeObj.w; + return edgeObj.v + EDGE_KEY_DELIM + edgeObj.w; } /** @@ -41,14 +44,14 @@ export function getEdgeKey(edgeObj: EdgeData) { * * @param sceneGroup container * @param graph - * @param sceneBehavior Parent scene module. + * @param sceneElement <tf-graph-scene> polymer element. * @return selection of the created nodeGroups */ export function buildGroup(sceneGroup, - graph: graphlib.Graph<tf.graph.render.RenderNodeInfo, - tf.graph.render.RenderMetaedgeInfo>, sceneBehavior) { + graph: graphlib.Graph<render.RenderNodeInfo, render.RenderMetaedgeInfo>, + sceneElement) { let edges: EdgeData[] = []; - let edgeData = _.reduce(graph.edges(), (edges, edgeObj) => { + edges = _.reduce(graph.edges(), (edges, edgeObj) => { let edgeLabel = graph.edge(edgeObj); edges.push({ v: edgeObj.v, @@ -68,8 +71,7 @@ export function buildGroup(sceneGroup, // See https://github.com/mbostock/d3/releases/tag/v2.0.0 // (It's not listed in the d3 wiki.) return this.childNodes; - }) - .data(edgeData, getEdgeKey); + }).data(edges, getEdgeKey); // Make edges a group to support rendering multiple lines for metaedge edgeGroups.enter() @@ -80,7 +82,7 @@ export function buildGroup(sceneGroup, let edgeGroup = d3.select(this); d.label.edgeGroup = edgeGroup; // index node group for quick highlighting - sceneBehavior._edgeGroupIndex[getEdgeKey(d)] = edgeGroup; + sceneElement._edgeGroupIndex[getEdgeKey(d)] = edgeGroup; // If any edges are reference edges, add the reference edge class. let extraEdgeClass = d.label.metaedge && d.label.metaedge.numRefEdges @@ -88,22 +90,57 @@ export function buildGroup(sceneGroup, : undefined; // Add line during enter because we're assuming that type of line // normally does not change. - appendEdge(edgeGroup, d, scene, extraEdgeClass); + appendEdge(edgeGroup, d, sceneElement, extraEdgeClass); }); edgeGroups.each(position); edgeGroups.each(function(d) { - stylize(d3.select(this), d, sceneBehavior); + stylize(d3.select(this), d, sceneElement); }); edgeGroups.exit() .each(d => { - delete sceneBehavior._edgeGroupIndex[getEdgeKey(d)]; + delete sceneElement._edgeGroupIndex[getEdgeKey(d)]; }) .remove(); return edgeGroups; }; +export function getShapeLabelFromNode(node: OpNode, + renderInfo: render.RenderGraphInfo) { + if (node.outputShapes == null || node.outputShapes.length === 0) { + return null; + } + // TODO(smilkov): Figure out exactly which output tensor this + // edge is from. + let shape = node.outputShapes[0]; + if (shape == null) { + return null; + } + if (shape.length === 0) { + return "scalar"; + } + return shape.map(size => { + return size === -1 ? "?" : size; + }).join(TENSOR_SHAPE_DELIM); +} + +/** + * Creates the label for the given metaedge. If the metaedge consists + * of only 1 tensor, and it's shape is known, the label will contain that + * shape. Otherwise, the label will say the number of tensors in the metaedge. + */ +export function getLabelForEdge(metaedge: Metaedge, + renderInfo: render.RenderGraphInfo): string { + let isMultiEdge = metaedge.baseEdgeList.length > 1; + if (isMultiEdge) { + return metaedge.baseEdgeList.length + " tensors"; + } else { + let node = <OpNode> renderInfo.getNodeByName(metaedge.baseEdgeList[0].v); + return getShapeLabelFromNode(node, renderInfo); + } +} + /** * For a given d3 selection and data object, create a path to represent the * edge described in d.label. @@ -112,15 +149,49 @@ export function buildGroup(sceneGroup, * will sometimes be undefined, for example for some Annotation edges for which * there is no underlying Metaedge in the hierarchical graph. */ -export function appendEdge(edgeGroup, d: EdgeData, sceneBehavior, edgeClass?) { +export function appendEdge(edgeGroup, d: EdgeData, + sceneElement: {renderHierarchy: render.RenderGraphInfo}, + edgeClass: string) { + let size = 1; + if (d.label != null && d.label.metaedge != null) { + // There is an underlying Metaedge. + size = d.label.metaedge.totalSize; + } edgeClass = edgeClass || Class.Edge.LINE; // set default type if (d.label && d.label.structural) { edgeClass += " " + Class.Edge.STRUCTURAL; } + // Give the path a unique id, which will be used to link + // the textPath (edge label) to this path. + let pathId = "path_" + getEdgeKey(d); + let strokeWidth = sceneElement.renderHierarchy.edgeWidthScale(size); edgeGroup.append("path") - .attr("class", edgeClass); + .attr({ + "id": pathId, + "class": edgeClass, + }).style({ + "stroke-width": strokeWidth + "px" + }); + + if (d.label == null || d.label.metaedge == null) { + // There is no associated metaedge, thus no text. + // This happens for annotation edges. + return; + } + let labelForEdge = getLabelForEdge(d.label.metaedge, + sceneElement.renderHierarchy); + if (labelForEdge == null) { + // We have no information to show on this edge. + return; + } + edgeGroup.append("text").append("textPath").attr({ + "xlink:href": "#" + pathId, + "startOffset": "50%", + "text-anchor": "middle", + "dominant-baseline": "central" + }).text(labelForEdge); }; export let interpolate = d3.svg.line<{x: number, y: number}>() @@ -134,8 +205,9 @@ export let interpolate = d3.svg.line<{x: number, y: number}>() function getEdgePathInterpolator(d: EdgeData, i: number, a: string) { let renderMetaedgeInfo = <render.RenderMetaedgeInfo> d.label; let adjoiningMetaedge = renderMetaedgeInfo.adjoiningMetaedge; + let points = renderMetaedgeInfo.points; if (!adjoiningMetaedge) { - return d3.interpolate(a, interpolate(renderMetaedgeInfo.points)); + return d3.interpolate(a, interpolate(points)); } let renderPath = this; @@ -158,7 +230,6 @@ function getEdgePathInterpolator(d: EdgeData, i: number, a: string) { // Update the relevant point in the renderMetaedgeInfo's points list, then // re-interpolate the path. - let points = renderMetaedgeInfo.points; let index = inbound ? 0 : points.length - 1; points[index].x = adjoiningPoint.x; points[index].y = adjoiningPoint.y; @@ -169,10 +240,8 @@ function getEdgePathInterpolator(d: EdgeData, i: number, a: string) { function position(d) { d3.select(this).select("path." + Class.Edge.LINE) - .each(function(d) { - let path = d3.select(this); - path.transition().attrTween("d", getEdgePathInterpolator); - }); + .transition() + .attrTween("d", getEdgePathInterpolator); }; /** diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/scene/minimap.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/minimap.ts index 32566c99ef..0be15da0e0 100644 --- a/tensorflow/tensorboard/components/tf-graph-common/lib/scene/minimap.ts +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/minimap.ts @@ -262,7 +262,8 @@ export class Minimap { downloadContext.drawImage(image, 0, 0, this.downloadCanvas.width, this.downloadCanvas.height); }; - image.src = "data:image/svg+xml;base64," + btoa(svgXml); + let blob = new Blob([svgXml], {type: "image/svg+xml;charset=utf-8"}); + image.src = URL.createObjectURL(blob); } /** diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/scene/node.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/node.ts index 58ec821877..6d467c348a 100644 --- a/tensorflow/tensorboard/components/tf-graph-common/lib/scene/node.ts +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/node.ts @@ -61,11 +61,11 @@ module tf.graph.scene.node { * * @param sceneGroup selection of the container * @param nodeData array of render node information to map - * @param sceneBehavior parent scene module + * @param sceneElement <tf-graph-scene> polymer element * @return selection of the created nodeGroups */ export function buildGroup(sceneGroup, - nodeData: render.RenderNodeInfo[], sceneBehavior) { + nodeData: render.RenderNodeInfo[], sceneElement) { let container = scene.selectOrCreateChild(sceneGroup, "g", Class.Node.CONTAINER); // Select all children and join with data. @@ -88,7 +88,7 @@ export function buildGroup(sceneGroup, .each(function(d) { let nodeGroup = d3.select(this); // index node group for quick stylizing - sceneBehavior.addNodeGroup(d.node.name, nodeGroup); + sceneElement.addNodeGroup(d.node.name, nodeGroup); }); // UPDATE @@ -98,58 +98,58 @@ export function buildGroup(sceneGroup, }) .each(function(d) { let nodeGroup = d3.select(this); - // add g.in-annotations (always add -- to keep layer order consistent.) + // Add g.in-annotations (always add -- to keep layer order consistent.) let inAnnotationBox = scene.selectOrCreateChild(nodeGroup, "g", Class.Annotation.INBOX); annotation.buildGroup(inAnnotationBox, d.inAnnotations, d, - sceneBehavior); + sceneElement); - // add g.out-annotations (always add -- to keep layer order consistent.) + // Add g.out-annotations (always add -- to keep layer order consistent.) let outAnnotationBox = scene.selectOrCreateChild(nodeGroup, "g", Class.Annotation.OUTBOX); annotation.buildGroup(outAnnotationBox, d.outAnnotations, d, - sceneBehavior); + sceneElement); - // label - let label = labelBuild(nodeGroup, d, sceneBehavior); - // Do not add interaction to metanode labels as they live inside the - // metanode shape which already has the same interactions. - addInteraction(label, d, sceneBehavior, d.node.type === NodeType.META); - - // build .shape below label - let shape = buildShape(nodeGroup, d, Class.Node.SHAPE, label.node()); + // Build .shape first (background of the node). + let shape = buildShape(nodeGroup, d, Class.Node.SHAPE); if (d.node.isGroupNode) { - addButton(shape, d, sceneBehavior); + addButton(shape, d, sceneElement); } - addInteraction(shape, d, sceneBehavior); + addInteraction(shape, d, sceneElement); - // build subscene on the top + // Build subscene on the top. subsceneBuild(nodeGroup, <render.RenderGroupNodeInfo> d, - sceneBehavior); + sceneElement); + + // Build label last. Should be on top of everything else. + let label = labelBuild(nodeGroup, d, sceneElement); + // Do not add interaction to metanode labels as they live inside the + // metanode shape which already has the same interactions. + addInteraction(label, d, sceneElement, d.node.type === NodeType.META); - stylize(nodeGroup, d, sceneBehavior); - position(nodeGroup, d, sceneBehavior); + stylize(nodeGroup, d, sceneElement); + position(nodeGroup, d); }); // EXIT nodeGroups.exit() .each(function(d) { // remove all indices on remove - sceneBehavior.removeNodeGroup(d.node.name); + sceneElement.removeNodeGroup(d.node.name); let nodeGroup = d3.select(this); if (d.inAnnotations.list.length > 0) { nodeGroup.select("." + Class.Annotation.INBOX) .selectAll("." + Class.Annotation.GROUP) .each(a => { - sceneBehavior.removeAnnotationGroup(a, d); + sceneElement.removeAnnotationGroup(a, d); }); } if (d.outAnnotations.list.length > 0) { nodeGroup.select("." + Class.Annotation.OUTBOX) .selectAll("." + Class.Annotation.GROUP) .each(a => { - sceneBehavior.removeAnnotationGroup(a, d); + sceneElement.removeAnnotationGroup(a, d); }); } }) @@ -163,17 +163,17 @@ export function buildGroup(sceneGroup, * * @param nodeGroup selection of the container * @param renderNodeInfo the render information for the node. - * @param sceneBehavior parent scene module + * @param sceneElement <tf-graph-scene> polymer element. * @return Selection of the subscene group, or null if node group does not have * a subscene. Op nodes, bridge nodes and unexpanded group nodes will * not have a subscene. */ function subsceneBuild(nodeGroup, - renderNodeInfo: render.RenderGroupNodeInfo, sceneBehavior) { + renderNodeInfo: render.RenderGroupNodeInfo, sceneElement) { if (renderNodeInfo.node.isGroupNode) { if (renderNodeInfo.expanded) { // Recursively build the subscene. - return scene.buildGroup(nodeGroup, renderNodeInfo, sceneBehavior, + return scene.buildGroup(nodeGroup, renderNodeInfo, sceneElement, Class.Subscene.GROUP); } // Clean out existing subscene if the node is not expanded. @@ -198,9 +198,9 @@ function subscenePosition(nodeGroup, d: render.RenderNodeInfo) { * * @param selection The group node selection. * @param d Info about the node being rendered. - * @param sceneBehavior parent scene module. + * @param sceneElement <tf-graph-scene> polymer element. */ -function addButton(selection, d: render.RenderNodeInfo, sceneBehavior) { +function addButton(selection, d: render.RenderNodeInfo, sceneElement) { let group = scene.selectOrCreateChild( selection, "g", Class.Node.BUTTON_CONTAINER); scene.selectOrCreateChild(group, "circle", Class.Node.BUTTON_CIRCLE); @@ -212,7 +212,7 @@ function addButton(selection, d: render.RenderNodeInfo, sceneBehavior) { // Stop this event's propagation so that it isn't also considered a // node-select. (<Event>d3.event).stopPropagation(); - sceneBehavior.fire("node-toggle-expand", { name: d.node.name }); + sceneElement.fire("node-toggle-expand", { name: d.node.name }); }); scene.positionButton(group, d); }; @@ -226,39 +226,39 @@ function addButton(selection, d: render.RenderNodeInfo, sceneBehavior) { * given interaction would cause conflicts with the expand/collapse button. */ function addInteraction(selection, d: render.RenderNodeInfo, - sceneBehavior, disableInteraction?: boolean) { + sceneElement, disableInteraction?: boolean) { if (disableInteraction) { selection.attr("pointer-events", "none"); return; } - let contextMenuFunction = tf.graph.scene.contextmenu.getMenu( - getContextMenu(d.node, sceneBehavior)); + let contextMenuFunction = contextmenu.getMenu( + getContextMenu(d.node, sceneElement)); selection.on("dblclick", d => { - sceneBehavior.fire("node-toggle-expand", { name: d.node.name }); + sceneElement.fire("node-toggle-expand", { name: d.node.name }); }) .on("mouseover", d => { // don't send mouseover over expanded group, // otherwise it is causing too much glitches - if (sceneBehavior.isNodeExpanded(d)) { return; } + if (sceneElement.isNodeExpanded(d)) { return; } - sceneBehavior.fire("node-highlight", { name: d.node.name }); + sceneElement.fire("node-highlight", { name: d.node.name }); }) .on("mouseout", d => { // don't send mouseover over expanded group, // otherwise it is causing too much glitches - if (sceneBehavior.isNodeExpanded(d)) { return; } + if (sceneElement.isNodeExpanded(d)) { return; } - sceneBehavior.fire("node-unhighlight", { name: d.node.name }); + sceneElement.fire("node-unhighlight", { name: d.node.name }); }) .on("click", d => { // Stop this event's propagation so that it isn't also considered // a graph-select. (<Event>d3.event).stopPropagation(); - sceneBehavior.fire("node-select", { name: d.node.name }); + sceneElement.fire("node-select", { name: d.node.name }); }) .on("contextmenu", (d, i) => { - sceneBehavior.fire("node-select", { name: d.node.name }); + sceneElement.fire("node-select", { name: d.node.name }); contextMenuFunction.call(d, i); }); }; @@ -266,13 +266,13 @@ function addInteraction(selection, d: render.RenderNodeInfo, /** * Returns the d3 context menu specification for the provided node. */ -export function getContextMenu(node: Node, sceneBehavior) { +export function getContextMenu(node: Node, sceneElement) { let menu = [{ title: d => { - return tf.graph.getIncludeNodeButtonString(node.include); + return getIncludeNodeButtonString(node.include); }, action: (elm, d, i) => { - sceneBehavior.fire("node-toggle-extract", { name: node.name }); + sceneElement.fire("node-toggle-extract", { name: node.name }); } }]; if (canBeInSeries(node)) { @@ -281,7 +281,7 @@ export function getContextMenu(node: Node, sceneBehavior) { return getGroupSettingLabel(node); }, action: (elm, d, i) => { - sceneBehavior.fire("node-toggle-seriesgroup", + sceneElement.fire("node-toggle-seriesgroup", { name: getSeriesName(node) }); } }); @@ -343,10 +343,10 @@ export function getGroupSettingLabel(node: Node) { * Append svg text for label and assign data. * @param nodeGroup * @param renderNodeInfo The render node information for the label. - * @param sceneBehavior parent scene module. + * @param sceneElement <tf-graph-scene> polymer element. */ function labelBuild(nodeGroup, renderNodeInfo: render.RenderNodeInfo, - sceneBehavior) { + sceneElement) { let namePath = renderNodeInfo.node.name.split("/"); let text = namePath[namePath.length - 1]; @@ -355,13 +355,18 @@ function labelBuild(nodeGroup, renderNodeInfo: render.RenderNodeInfo, !renderNodeInfo.expanded; let label = scene.selectOrCreateChild(nodeGroup, "text", Class.Node.LABEL); + + // Make sure the label is visually on top among its siblings. + let labelNode = <HTMLElement> label.node(); + labelNode.parentNode.appendChild(labelNode); + label.attr("dy", ".35em") .attr("text-anchor", "middle"); if (useFontScale) { - if (text.length > sceneBehavior.maxMetanodeLabelLength) { - text = text.substr(0, sceneBehavior.maxMetanodeLabelLength - 2) + "..."; + if (text.length > sceneElement.maxMetanodeLabelLength) { + text = text.substr(0, sceneElement.maxMetanodeLabelLength - 2) + "..."; } - let scale = getLabelFontScale(sceneBehavior); + let scale = getLabelFontScale(sceneElement); label.attr("font-size", scale(text.length) + "px"); } label.text(text); @@ -373,13 +378,13 @@ function labelBuild(nodeGroup, renderNodeInfo: render.RenderNodeInfo, * initialized once by getLabelFontScale. */ let fontScale = null; -function getLabelFontScale(sceneBehavior) { +function getLabelFontScale(sceneElement) { if (!fontScale) { fontScale = d3.scale.linear() - .domain([sceneBehavior.maxMetanodeLabelLengthLargeFont, - sceneBehavior.maxMetanodeLabelLength]) - .range([sceneBehavior.maxMetanodeLabelLengthFontSize, - sceneBehavior.minMetanodeLabelLengthFontSize]).clamp(true); + .domain([sceneElement.maxMetanodeLabelLengthLargeFont, + sceneElement.maxMetanodeLabelLength]) + .range([sceneElement.maxMetanodeLabelLengthFontSize, + sceneElement.minMetanodeLabelLengthFontSize]).clamp(true); } return fontScale; } @@ -401,13 +406,11 @@ function labelPosition(nodeGroup, cx: number, cy: number, * @param nodeGroup * @param d Render node information. * @param nodeClass class for the element. - * @param before Reference DOM node for insertion. * @return Selection of the shape. */ -export function buildShape(nodeGroup, d, nodeClass: string, before?) { +export function buildShape(nodeGroup, d, nodeClass: string) { // Create a group to house the underlying visual elements. - let shapeGroup = scene.selectOrCreateChild(nodeGroup, "g", nodeClass, - before); + let shapeGroup = scene.selectOrCreateChild(nodeGroup, "g", nodeClass); // TODO(jimbo): DOM structure should be templated in HTML somewhere, not JS. switch (d.node.type) { case NodeType.OP: @@ -458,7 +461,7 @@ export function nodeClass(d: render.RenderNodeInfo) { }; /** Modify node and its subscene and its label's positional attributes */ -function position(nodeGroup, d: render.RenderNodeInfo, sceneBehavior) { +function position(nodeGroup, d: render.RenderNodeInfo) { let shapeGroup = scene.selectChild(nodeGroup, "g", Class.Node.SHAPE); let cx = layout.computeCXPositionOfNodeShape(d); switch (d.node.type) { @@ -520,15 +523,15 @@ export enum ColorBy { STRUCTURE, DEVICE, COMPUTE_TIME, MEMORY }; */ export function getFillForNode(templateIndex, colorBy, renderInfo: render.RenderNodeInfo, isExpanded: boolean): string { - let colorParams = tf.graph.render.MetanodeColors; + let colorParams = render.MetanodeColors; switch (colorBy) { case ColorBy.STRUCTURE: - if (renderInfo.node.type === tf.graph.NodeType.META) { + if (renderInfo.node.type === NodeType.META) { let tid = (<Metanode>renderInfo.node).templateId; return tid === null ? colorParams.UNKNOWN : colorParams.STRUCTURE_PALETTE(templateIndex(tid), isExpanded); - } else if (renderInfo.node.type === tf.graph.NodeType.SERIES) { + } else if (renderInfo.node.type === NodeType.SERIES) { // If expanded, we're showing the background rect, which we want to // appear gray. Otherwise we're showing a stack of ellipses which we // want to show white. @@ -587,10 +590,10 @@ export function getFillForNode(templateIndex, colorBy, * that can't be done in css). */ export function stylize(nodeGroup, renderInfo: render.RenderNodeInfo, - sceneBehavior, nodeClass?) { + sceneElement, nodeClass?) { nodeClass = nodeClass || Class.Node.SHAPE; - let isHighlighted = sceneBehavior.isNodeHighlighted(renderInfo.node.name); - let isSelected = sceneBehavior.isNodeSelected(renderInfo.node.name); + let isHighlighted = sceneElement.isNodeHighlighted(renderInfo.node.name); + let isSelected = sceneElement.isNodeSelected(renderInfo.node.name); let isExtract = renderInfo.isInExtract || renderInfo.isOutExtract; let isExpanded = renderInfo.expanded; nodeGroup.classed("highlighted", isHighlighted); @@ -601,8 +604,8 @@ export function stylize(nodeGroup, renderInfo: render.RenderNodeInfo, // Main node always exists here and it will be reached before subscene, // so d3 selection is fine here. let node = nodeGroup.select("." + nodeClass + " ." + Class.Node.COLOR_TARGET); - let fillColor = getFillForNode(sceneBehavior.templateIndex, - ColorBy[sceneBehavior.colorBy.toUpperCase()], + let fillColor = getFillForNode(sceneElement.templateIndex, + ColorBy[sceneElement.colorBy.toUpperCase()], renderInfo, isExpanded); node.style("fill", fillColor); @@ -617,7 +620,7 @@ export function stylize(nodeGroup, renderInfo: render.RenderNodeInfo, export function getStrokeForFill(fill: string) { // If node is colored by a gradient, then use a dark gray outline. return fill.substring(0, 3) === "url" ? - tf.graph.render.MetanodeColors.GRADIENT_OUTLINE : + render.MetanodeColors.GRADIENT_OUTLINE : d3.rgb(fill).darker().toString(); } diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/scene/scene.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/scene.ts index 24c16e31ee..685ad646f7 100644 --- a/tensorflow/tensorboard/components/tf-graph-common/lib/scene/scene.ts +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/scene.ts @@ -243,12 +243,12 @@ export function selectChild(container, tagName: string, className?: string) { * * @param container D3 selection of the parent. * @param renderNode render node of a metanode or series node. - * @param sceneBehavior Parent scene module. + * @param sceneElement <tf-graph-scene> polymer element. * @param sceneClass class attribute of the scene (default="scene"). */ export function buildGroup(container, renderNode: render.RenderGroupNodeInfo, - sceneBehavior, + sceneElement, sceneClass: string) { sceneClass = sceneClass || Class.Scene.GROUP; let isNewSceneGroup = selectChild(container, "g", sceneClass).empty(); @@ -272,17 +272,17 @@ export function buildGroup(container, } // Create the layer of edges for this scene (paths). - edge.buildGroup(coreGroup, renderNode.coreGraph, sceneBehavior); + edge.buildGroup(coreGroup, renderNode.coreGraph, sceneElement); // Create the layer of nodes for this scene (ellipses, rects etc). - node.buildGroup(coreGroup, coreNodes, sceneBehavior); + node.buildGroup(coreGroup, coreNodes, sceneElement); // In-extract if (renderNode.isolatedInExtract.length > 0) { let inExtractGroup = selectOrCreateChild(sceneGroup, "g", Class.Scene.INEXTRACT); node.buildGroup(inExtractGroup, renderNode.isolatedInExtract, - sceneBehavior); + sceneElement); } else { selectChild(sceneGroup, "g", Class.Scene.INEXTRACT).remove(); } @@ -292,7 +292,7 @@ export function buildGroup(container, let outExtractGroup = selectOrCreateChild(sceneGroup, "g", Class.Scene.OUTEXTRACT); node.buildGroup(outExtractGroup, renderNode.isolatedOutExtract, - sceneBehavior); + sceneElement); } else { selectChild(sceneGroup, "g", Class.Scene.OUTEXTRACT).remove(); } @@ -345,9 +345,9 @@ function position(sceneGroup, renderNode: render.RenderGroupNodeInfo) { }; /** Adds a click listener to a group that fires a graph-select event */ -export function addGraphClickListener(graphGroup, sceneBehavior) { +export function addGraphClickListener(graphGroup, sceneElement) { d3.select(graphGroup).on("click", () => { - sceneBehavior.fire("graph-select"); + sceneElement.fire("graph-select"); }); }; diff --git a/tensorflow/tensorboard/components/tf-graph-info/tf-node-info.html b/tensorflow/tensorboard/components/tf-graph-info/tf-node-info.html index 3aac0ca436..cb9abc6cee 100644 --- a/tensorflow/tensorboard/components/tf-graph-info/tf-node-info.html +++ b/tensorflow/tensorboard/components/tf-graph-info/tf-node-info.html @@ -176,6 +176,7 @@ class="non-control-list-item" card-node="[[_node]]" item-node="[[_getNode(item, graphHierarchy)]]" + edge-label="[[_getPredEdgeLabel(item)]]" item-render-info="[[_getRenderInfo(item, renderHierarchy)]]" name="[[item]]" item-type="predecessors" @@ -226,6 +227,7 @@ class="non-control-list-item" card-node="[[_node]]" item-node="[[_getNode(item, graphHierarchy)]]" + edge-label="[[_getSuccEdgeLabel(item)]]" item-render-info="[[_getRenderInfo(item, renderHierarchy)]]" name="[[item]]" item-type="successor" @@ -364,6 +366,31 @@ // long node names wrap cleanly at path boundaries. return (nodeName || '').replace(/\//g, '<wbr>/'); }, + _getPredEdgeLabel: function(sourceName) { + return this._getEdgeLabel(sourceName, this.nodeName); + }, + _getSuccEdgeLabel: function(destName) { + return this._getEdgeLabel(this.nodeName, destName); + }, + _getEdgeLabel: function(sourceName, destName) { + if (!this._node) { + // The user clicked outside, thus no node is selected and + // the info card should be hidden. + return; + } + var parent = this._node.parentNode; + var sourceNode = this.graphHierarchy.node(sourceName); + if (!sourceNode.isGroupNode) { + // Show the tensor shape directly. + return tf.graph.scene.edge.getShapeLabelFromNode(sourceNode); + } + sourceName = this.renderHierarchy.getNearestVisibleAncestor(sourceName); + destName = this.renderHierarchy.getNearestVisibleAncestor(destName); + var metaedge = parent.metagraph.edge(sourceName, destName) || + parent.bridgegraph.edge(sourceName, destName); + return tf.graph.scene.edge.getLabelForEdge(metaedge, + this.renderHierarchy); + }, _getRenderInfo: function(nodeName, renderHierarchy) { return this.renderHierarchy.getOrCreateRenderNodeByName(nodeName); }, diff --git a/tensorflow/tensorboard/components/tf-graph-info/tf-node-list-item.html b/tensorflow/tensorboard/components/tf-graph-info/tf-node-list-item.html index 33035d3d68..8f0f610149 100644 --- a/tensorflow/tensorboard/components/tf-graph-info/tf-node-list-item.html +++ b/tensorflow/tensorboard/components/tf-graph-info/tf-node-list-item.html @@ -9,6 +9,7 @@ font-size: 11pt; font-weight: 400; position: relative; + display: inline-block; } #list-item:hover { @@ -20,7 +21,6 @@ } #list-item span { - display: block; margin-left: 40px; } @@ -28,6 +28,13 @@ color: #999; } + #list-item span.edge-label { + float: right; + font-size: 10px; + margin-left: 3px; + margin-right: 5px; + } + .node-icon { position: absolute; top: 1px; @@ -44,6 +51,7 @@ node="[[itemNode]]" render-info="[[itemRenderInfo]]" template-index="[[templateIndex]]"></tf-graph-icon> <span title$="[[name]]">[[name]]</span> + <span class="edge-label">[[edgeLabel]]</span> </div> </template> @@ -63,6 +71,8 @@ * @type {tf.graph.Node} */ itemNode: Object, + /** The edge label associated with this item. */ + edgeLabel: String, /** * The render node information for the item node. Used by the graph * icon in determining fill color. @@ -75,7 +85,7 @@ }, colorBy: String, colorByParams: Object, - templateIndex: Function, + templateIndex: Function }, _itemTypeChanged: function() { @@ -94,7 +104,6 @@ type: this.itemType }); } - }); })(); </script> diff --git a/tensorflow/tensorboard/components/tf-graph-loader/tf-graph-loader.html b/tensorflow/tensorboard/components/tf-graph-loader/tf-graph-loader.html index 8f1d72cfa9..56d4f90ec4 100644 --- a/tensorflow/tensorboard/components/tf-graph-loader/tf-graph-loader.html +++ b/tensorflow/tensorboard/components/tf-graph-loader/tf-graph-loader.html @@ -129,8 +129,10 @@ Polymer({ this._setHasStats(statsJson != null); this._setOutGraphHierarchy(graphHierarchy); }.bind(this)) - .catch(function(reason) { - tracker.reportError("Graph visualization failed: " + reason); + .catch(function(e) { + // Generic error catch, for errors that happened outside + // asynchronous tasks. + tracker.reportError("Graph visualization failed: " + e, e); }); }, _selectedDatasetChanged: function(datasetIndex, datasets) { diff --git a/tensorflow/tensorboard/components/tf-graph/demo/tf-graph-demo.html b/tensorflow/tensorboard/components/tf-graph/demo/tf-graph-demo.html index 80118d95fd..2e5ef3d1de 100644 --- a/tensorflow/tensorboard/components/tf-graph/demo/tf-graph-demo.html +++ b/tensorflow/tensorboard/components/tf-graph/demo/tf-graph-demo.html @@ -93,6 +93,10 @@ var datasets = [ statsPath: "mnist_train_stats.json" }, { + name: "Mnist Train (with shapes)", + path: "mnist_train_shapes.pbtxt", + }, + { name: "Inception Train (huge)", path: "inception_train.pbtxt", }, @@ -117,7 +121,7 @@ var datasets = [ path: "ptb_word_lstm_test_eval.pbtxt", }, { - name: "Cifar10 Train", + name: "Cifar10 Train (with shapes)", path: "cifar10_train.pbtxt", }, { @@ -166,7 +170,7 @@ Polymer({ }, selectedDataset: { type: Number, - value: 1 + value: 9 // Cifar train with shapes. }, _progress: Object }, diff --git a/tensorflow/tensorboard/components/tf-graph/tf-graph-style.html b/tensorflow/tensorboard/components/tf-graph/tf-graph-style.html index 3e6f7f2112..e369940976 100644 --- a/tensorflow/tensorboard/components/tf-graph/tf-graph-style.html +++ b/tensorflow/tensorboard/components/tf-graph/tf-graph-style.html @@ -278,7 +278,6 @@ ::content .edge > path.edgeline { fill: none; - marker-end: url(#arrowhead); stroke: #bbb; stroke-linecap: round; stroke-width: 0.75; @@ -288,6 +287,12 @@ marker-start: url(#ref-arrowhead); } +/* Labels showing tensor shapes on edges */ +::content .edge > text { + font-size: 3.5px; + fill: #666; +} + ::content #arrowhead { fill: #bbb; } diff --git a/tensorflow/tensorboard/components/tf-graph/tf-graph.html b/tensorflow/tensorboard/components/tf-graph/tf-graph.html index bbe86cee82..7d38694a9b 100644 --- a/tensorflow/tensorboard/components/tf-graph/tf-graph.html +++ b/tensorflow/tensorboard/components/tf-graph/tf-graph.html @@ -214,6 +214,10 @@ Polymer({ this.set('highlightedNode', null); }, _nodeToggleExpand: function(event) { + // Immediately select the node that is about to be expanded. + this._nodeSelected(event); + + // Compute the sub-hierarchy scene. var nodeName = event.detail.name; var renderNode = this.renderHierarchy.getRenderNodeByName(nodeName); // Op nodes are not expandable. @@ -222,9 +226,13 @@ Polymer({ } this.renderHierarchy.buildSubhierarchy(nodeName); renderNode.expanded = !renderNode.expanded; - this.querySelector('#scene').setNodeExpanded(renderNode); - // Also select the expanded node. - this._nodeSelected(event); + + // Expand the node with some delay so that the user can immediately see + // the visual effect of selecting that node, before the expansion is + // done. + this.async(function() { + this.querySelector('#scene').setNodeExpanded(renderNode); + }, 75); }, _nodeToggleExtract: function(event) { // Toggle the include setting of the specified node appropriately. |