Skip to content

Commit c052d26

Browse files
Google AI Edgecopybara-github
Google AI Edge
authored andcommitted
Add support for a special "node ids" node attribute type.
Each entry will allow users to click to reval/select the corresponding node. PiperOrigin-RevId: 733463893
1 parent bbc150a commit c052d26

File tree

9 files changed

+151
-46
lines changed

9 files changed

+151
-46
lines changed

src/ui/src/components/visualizer/common/input_graph.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
IncomingEdge,
2424
KeyValueList,
2525
MetadataItem,
26+
NodeAttributeList,
2627
} from './types';
2728

2829
/** A collection of graphs. This is the input to the visualizer. */
@@ -136,7 +137,7 @@ export declare interface GraphNode {
136137
subgraphIds?: string[];
137138

138139
/** The attributes of the node. */
139-
attrs?: KeyValueList;
140+
attrs?: NodeAttributeList;
140141

141142
/** A list of incoming edges. */
142143
incomingEdges?: IncomingEdge[];

src/ui/src/components/visualizer/common/model_graph.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
GroupNodeAttributes,
2323
IncomingEdge,
2424
KeyValuePairs,
25+
NodeAttributePairs,
2526
OutgoingEdge,
2627
Point,
2728
} from './types';
@@ -198,7 +199,7 @@ export declare interface OpNode extends ModelNodeBase {
198199
outgoingEdges?: OutgoingEdge[];
199200

200201
/** The attributes of the node. */
201-
attrs?: KeyValuePairs;
202+
attrs?: NodeAttributePairs;
202203

203204
/**
204205
* Metadata for inputs, indexed by input ids. Each input can have multiple

src/ui/src/components/visualizer/common/types.ts

+33
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,39 @@ export declare interface KeyValue {
3131
/** A type for a list of key-value pairs. */
3232
export type KeyValueList = KeyValue[];
3333

34+
/** A list of node attributes. */
35+
export type NodeAttributeList = NodeAttribute[];
36+
37+
/** Node attributes as a record. */
38+
export type NodeAttributePairs = Record<string, NodeAttributeValue>;
39+
40+
/** A type for a single node attribute. */
41+
export declare interface NodeAttribute {
42+
key: string;
43+
value: NodeAttributeValue;
44+
}
45+
46+
/** A single node attribute value. */
47+
export type NodeAttributeValue = string | SpecialNodeAttributeValue;
48+
49+
/** non-string node attribute value. */
50+
export type SpecialNodeAttributeValue = NodeIdsNodeAttributeValue;
51+
52+
/** Node attribute value types. */
53+
export enum NodeAttributeValueType {
54+
NODE_IDS = 'node_ids',
55+
}
56+
57+
/**
58+
* A "node ids" node attribute value.
59+
*
60+
* Clicking on a node id will jump to the corresponding node in the graph.
61+
*/
62+
export declare interface NodeIdsNodeAttributeValue {
63+
type: NodeAttributeValueType.NODE_IDS;
64+
nodeIds: string[];
65+
}
66+
3467
/** An item in input/output metadata. */
3568
export interface MetadataItem {
3669
id: string;

src/ui/src/components/visualizer/common/utils.ts

+33-18
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
FieldLabel,
4040
KeyValueList,
4141
KeyValuePairs,
42+
NodeAttributeValueType,
4243
NodeDataProviderResultProcessedData,
4344
NodeDataProviderRunData,
4445
NodeDataProviderValueInfo,
@@ -401,24 +402,26 @@ export function getOpNodeAttrsKeyValuePairsForAttrsTable(
401402
for (const attrId of Object.keys(attrs)) {
402403
const key = attrId;
403404
const value = attrs[attrId];
404-
const matchTargets = [`${key}:${value}`, `${key}=${value}`];
405-
if (
406-
filterRegex.trim() === '' ||
407-
matchTargets.some((matchTarget) => regex.test(matchTarget))
408-
) {
409-
// Remove new line chars and spaces.
410-
let processedValue = value;
411-
if (key === TENSOR_VALUES_KEY) {
412-
// For __value attribute, remove all white space chars.
413-
processedValue = value.replace(/\s/gm, '');
414-
} else {
415-
// For other attributes, only remove newline chars.
416-
processedValue = value.replace(/(\r\n|\n|\r)/gm, ' ');
405+
if (typeof value === 'string') {
406+
const matchTargets = [`${key}:${value}`, `${key}=${value}`];
407+
if (
408+
filterRegex.trim() === '' ||
409+
matchTargets.some((matchTarget) => regex.test(matchTarget))
410+
) {
411+
// Remove new line chars and spaces.
412+
let processedValue = value;
413+
if (key === TENSOR_VALUES_KEY) {
414+
// For __value attribute, remove all white space chars.
415+
processedValue = value.replace(/\s/gm, '');
416+
} else {
417+
// For other attributes, only remove newline chars.
418+
processedValue = value.replace(/(\r\n|\n|\r)/gm, ' ');
419+
}
420+
keyValuePairs.push({
421+
key,
422+
value: processedValue,
423+
});
417424
}
418-
keyValuePairs.push({
419-
key,
420-
value: processedValue,
421-
});
422425
}
423426
}
424427
return keyValuePairs;
@@ -784,7 +787,19 @@ export function getAttributesFromNode(
784787
): KeyValuePairs {
785788
let attrs: KeyValuePairs = {};
786789
if (isOpNode(node)) {
787-
attrs = {...(node.attrs || {})};
790+
for (const [key, value] of Object.entries(node.attrs || {})) {
791+
if (typeof value === 'string') {
792+
attrs[key] = value;
793+
} else {
794+
switch (value.type) {
795+
case NodeAttributeValueType.NODE_IDS:
796+
attrs[key] = value.nodeIds.join(',');
797+
break;
798+
default:
799+
break;
800+
}
801+
}
802+
}
788803
// Add id to attribute.
789804
attrs['id'] = node.id;
790805
} else if (isGroupNode(node)) {

src/ui/src/components/visualizer/info_panel.ng.html

+24-5
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,30 @@
3434
<tr [class.search-match]="isSearchMatchedAttrId(item.label)">
3535
<td class="key"><hoverable-label [label]="item.label"></hoverable-label></td>
3636
<td class="value">
37-
<expandable-info-text
38-
[text]="item.value" [type]="item.label"
39-
[bgColor]="item.bgColor || 'transparent'"
40-
[textColor]="item.textColor || 'black'">
41-
</expandable-info-text>
37+
@if (item.specialValue) {
38+
@switch (item.specialValue.type) {
39+
@case (NodeAttributeValueType.NODE_IDS) {
40+
@for (nodeId of item.specialValue.nodeIds; track nodeId) {
41+
<div class="node-id-attribute"
42+
(click)="handleLocateNode(nodeId, $event)">
43+
{{nodeId}}
44+
<div class="locator-icon-container"
45+
[matTooltip]="locatorTooltip"
46+
matTooltipClass="multiline-tooltip-left"
47+
matTooltipPosition="right">
48+
<mat-icon class="locator-icon">my_location</mat-icon>
49+
</div>
50+
</div>
51+
}
52+
}
53+
}
54+
} @else {
55+
<expandable-info-text
56+
[text]="item.value" [type]="item.label"
57+
[bgColor]="item.bgColor || 'transparent'"
58+
[textColor]="item.textColor || 'black'">
59+
</expandable-info-text>
60+
}
4261
</td>
4362
</tr>
4463
}

src/ui/src/components/visualizer/info_panel.scss

+5
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@
125125
margin-right: 0;
126126
}
127127
}
128+
129+
.node-id-attribute {
130+
display: flex;
131+
cursor: pointer;
132+
}
128133
}
129134

130135
.section:not(:first-child) {

src/ui/src/components/visualizer/info_panel.ts

+19-5
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,16 @@ import {
4747
KeyValue,
4848
KeyValueList,
4949
KeyValuePairs,
50+
NodeAttributeValueType,
5051
NodeDataProviderRunInfo,
52+
NodeIdsNodeAttributeValue,
5153
OutgoingEdge,
5254
SearchMatchAttr,
5355
SearchMatchInputMetadata,
5456
SearchMatchOutputMetadata,
5557
SearchMatchType,
5658
SearchResults,
59+
SpecialNodeAttributeValue,
5760
} from './common/types';
5861
import {
5962
getNamespaceLabel,
@@ -101,6 +104,7 @@ interface InfoItem {
101104
bgColor?: string;
102105
textColor?: string;
103106
loading?: boolean;
107+
specialValue?: SpecialNodeAttributeValue;
104108
}
105109

106110
interface OutputItem {
@@ -155,6 +159,8 @@ export class InfoPanel {
155159
@ViewChildren('inputValueContent')
156160
inputValueContents = new QueryList<ElementRef<HTMLElement>>();
157161

162+
readonly NodeAttributeValueType = NodeAttributeValueType;
163+
158164
private curModelGraph?: ModelGraph;
159165
private curSelectedNode?: ModelNode;
160166
private readonly curShowOnOpNodeInfoIds = new Set<string>();
@@ -769,12 +775,17 @@ export class InfoPanel {
769775
if (key.startsWith('__')) {
770776
continue;
771777
}
778+
const value = attrs[key];
779+
const strValue = typeof value === 'string' ? value : '';
780+
const specialValue: SpecialNodeAttributeValue | undefined =
781+
typeof value === 'string' ? undefined : value;
772782
attrSection.items.push({
773783
section: attrSection,
774784
label: key,
775-
value: attrs[key],
785+
value: strValue,
776786
canShowOnNode: true,
777787
showOnNode: this.curShowOnOpNodeAttrIds.has(key),
788+
specialValue,
778789
});
779790
}
780791
if (attrSection.items.length > 0) {
@@ -1009,10 +1020,13 @@ export class InfoPanel {
10091020
// Add tensor values to metadata if existed.
10101021
const attrs = sourceOpNode.attrs || {};
10111022
if (attrs[TENSOR_VALUES_KEY]) {
1012-
metadataList.push({
1013-
key: this.inputMetadataValuesKey,
1014-
value: attrs[TENSOR_VALUES_KEY],
1015-
});
1023+
const value = attrs[TENSOR_VALUES_KEY];
1024+
if (typeof value === 'string') {
1025+
metadataList.push({
1026+
key: this.inputMetadataValuesKey,
1027+
value,
1028+
});
1029+
}
10161030
}
10171031
return metadataList;
10181032
}

src/ui/src/components/visualizer/io_tree.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,11 @@ export class IoTree implements OnChanges {
252252

253253
if (isOpNode(modelNode)) {
254254
const attrs = modelNode.attrs || {};
255-
return attrs[TENSOR_VALUES_KEY] || '<empty>';
255+
const value = attrs[TENSOR_VALUES_KEY];
256+
if (value && typeof value === 'string') {
257+
return value;
258+
}
259+
return '<empty>';
256260
}
257261

258262
return '';

src/ui/src/components/visualizer/worker/graph_processor.ts

+28-15
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import {
3131
import {
3232
KeyValuePairs,
3333
MetadataItem,
34+
NodeAttributePairs,
35+
NodeAttributeValue,
3436
NodeDataProviderRunData,
3537
ShowOnNodeItemData,
3638
} from '../common/types';
@@ -147,14 +149,18 @@ export class GraphProcessor {
147149
const attrValue = graphNode.attrs?.find(
148150
(attr) => attr.key === attrKey,
149151
)?.value;
150-
if (attrValue && attrValue.match(attrValueRegex)) {
152+
if (
153+
attrValue &&
154+
typeof attrValue === 'string' &&
155+
attrValue.match(attrValueRegex)
156+
) {
151157
opNode.hideInLayout = true;
152158
break;
153159
}
154160
}
155161
}
156162
if (graphNode.attrs) {
157-
const attrs: KeyValuePairs = {};
163+
const attrs: NodeAttributePairs = {};
158164
for (const attr of graphNode.attrs) {
159165
attrs[attr.key] = this.processAttrValue(attr.key, attr.value);
160166
}
@@ -762,21 +768,28 @@ export class GraphProcessor {
762768
}
763769
}
764770

765-
private processAttrValue(key: string, value: string): string {
766-
// Process const value that in `dense<...>` format. This is for backward
767-
// compatibility.
768-
if (value.startsWith('dense<')) {
769-
const matches = value.match(CONST_VALUE_REGEX);
770-
if (matches != null && matches.length > 1) {
771-
const strTensorValue = matches[1];
772-
return formatTensorValues(strTensorValue);
771+
private processAttrValue(
772+
key: string,
773+
value: NodeAttributeValue,
774+
): NodeAttributeValue {
775+
if (typeof value === 'string') {
776+
// Process const value that in `dense<...>` format. This is for backward
777+
// compatibility.
778+
if (value.startsWith('dense<')) {
779+
const matches = value.match(CONST_VALUE_REGEX);
780+
if (matches != null && matches.length > 1) {
781+
const strTensorValue = matches[1];
782+
return formatTensorValues(strTensorValue);
783+
}
773784
}
785+
// Process tensor values.
786+
else if (key === TENSOR_VALUES_KEY) {
787+
return formatTensorValues(value);
788+
}
789+
return value.replaceAll('"', '') || '<empty>';
790+
} else {
791+
return value;
774792
}
775-
// Process tensor values.
776-
else if (key === TENSOR_VALUES_KEY) {
777-
return formatTensorValues(value);
778-
}
779-
return value.replaceAll('"', '') || '<empty>';
780793
}
781794

782795
private processMetadataList(metadataItems: MetadataItem[]) {

0 commit comments

Comments
 (0)