Attributes Metadata

Summary:
Before this change, attributes and attribute metadata were intermingled and sent as one unit via subtree update event.

This represented a few issues:
- Repetitiveness. For each declared and dynamic attribute, metadata was included on each value unit.
- Metadata can vary in size and thus can have a negative impact on payload size.
- The attribute name which is part of metadata is a string which always overhead on processing.
- Metadata instantiation is not cheap thus this also incurs in processing overhead i.e. even instantiating a single string can have an impact.

The proposal is to separate metadata of attributes from the actual node reported attributes. This solves the problems mentioned above.

Reviewed By: LukeDefeo

Differential Revision: D40674156

fbshipit-source-id: 0788551849fbce53065f819ba503e7e4afc03cc0
This commit is contained in:
Lorenzo Blasa
2022-11-10 11:52:28 -08:00
committed by Facebook GitHub Bot
parent 27428522ce
commit 01dc22b1ab
33 changed files with 663 additions and 267 deletions

View File

@@ -11,7 +11,7 @@ import React, {useState} from 'react';
import {plugin} from '../index';
import {DetailSidebar, Layout, usePlugin, useValue} from 'flipper-plugin';
import {useHotkeys} from 'react-hotkeys-hook';
import {Id, Snapshot, UINode} from '../types';
import {Id, Metadata, MetadataId, Snapshot, UINode} from '../types';
import {PerfStats} from './PerfStats';
import {Tree} from './Tree';
import {Visualization2D} from './Visualization2D';
@@ -22,6 +22,7 @@ export function Component() {
const instance = usePlugin(plugin);
const rootId = useValue(instance.rootId);
const nodes: Map<Id, UINode> = useValue(instance.nodes);
const metadata: Map<MetadataId, Metadata> = useValue(instance.metadata);
const snapshots: Map<Id, Snapshot> = useValue(instance.snapshots);
const [showPerfStats, setShowPerfStats] = useState(false);
@@ -32,13 +33,16 @@ export function Component() {
const {ctrlPressed} = useKeyboardModifiers();
function renderSidebar(node: UINode | undefined) {
function renderSidebar(
node: UINode | undefined,
metadata: Map<MetadataId, Metadata>,
) {
if (!node) {
return;
}
return (
<DetailSidebar width={350}>
<Inspector node={node} />
<Inspector metadata={metadata} node={node} />
</DetailSidebar>
);
}
@@ -68,7 +72,7 @@ export function Component() {
onSelectNode={setSelectedNode}
modifierPressed={ctrlPressed}
/>
{selectedNode && renderSidebar(nodes.get(selectedNode))}
{selectedNode && renderSidebar(nodes.get(selectedNode), metadata)}
</Layout.Horizontal>
);
}

View File

@@ -11,16 +11,17 @@ import React from 'react';
// eslint-disable-next-line rulesdir/no-restricted-imports-clone
import {Glyph} from 'flipper';
import {Layout, Tab, Tabs} from 'flipper-plugin';
import {UINode} from '../../types';
import {Metadata, MetadataId, UINode} from '../../types';
import {IdentityInspector} from './inspector/IdentityInspector';
import {AttributesInspector} from './inspector/AttributesInspector';
import {DocumentationInspector} from './inspector/DocumentationInspector';
type Props = {
node: UINode;
metadata: Map<MetadataId, Metadata>;
};
export const Inspector: React.FC<Props> = ({node}) => {
export const Inspector: React.FC<Props> = ({node, metadata}) => {
return (
<Layout.Container gap pad>
<Tabs grow centered>
@@ -38,7 +39,11 @@ export const Inspector: React.FC<Props> = ({node}) => {
<Glyph name="data-table" size={16} />
</Layout.Horizontal>
}>
<AttributesInspector mode="attributes" node={node} />
<AttributesInspector
mode="attribute"
node={node}
metadata={metadata}
/>
</Tab>
<Tab
tab={
@@ -46,7 +51,7 @@ export const Inspector: React.FC<Props> = ({node}) => {
<Glyph name="square-ruler" size={16} />
</Layout.Horizontal>
}>
<AttributesInspector mode="layout" node={node} />
<AttributesInspector mode="layout" node={node} metadata={metadata} />
</Tab>
<Tab
tab={

View File

@@ -8,7 +8,13 @@
*/
import React from 'react';
import {Inspectable, InspectableObject, UINode} from '../../../types';
import {
Inspectable,
InspectableObject,
Metadata,
MetadataId,
UINode,
} from '../../../types';
import {DataInspector, Panel, styled} from 'flipper-plugin';
import {Checkbox, Col, Row} from 'antd';
import {displayableName} from '../utilities/displayableName';
@@ -73,21 +79,25 @@ const NamedAttributeInspector: React.FC<NamedAttributeInspectorProps> = ({
};
const ObjectAttributeInspector: React.FC<{
metadata: Map<MetadataId, Metadata>;
name: string;
value: Record<string, Inspectable>;
fields: Record<MetadataId, Inspectable>;
level: number;
}> = ({name, value, level}) => {
}> = ({metadata, name, fields, level}) => {
return (
<div style={ContainerStyle}>
{name}
{Object.keys(value).map(function (key, _) {
{Object.keys(fields).map(function (key, _) {
const metadataId: number = Number(key);
const inspectableValue = fields[metadataId];
const attributeName = metadata.get(metadataId)?.name ?? '';
return (
<ObjectContainer
key={key}
key={metadataId}
style={{
paddingLeft: level,
}}>
{create(key, value[key], level + 2)}
{create(metadata, attributeName, inspectableValue, level + 2)}
</ObjectContainer>
);
})}
@@ -95,145 +105,143 @@ const ObjectAttributeInspector: React.FC<{
);
};
function create(key: string, inspectable: Inspectable, level: number = 2) {
function create(
metadata: Map<MetadataId, Metadata>,
name: string,
inspectable: Inspectable,
level: number = 2,
) {
switch (inspectable.type) {
case 'boolean':
return (
<NamedAttributeInspector name={displayableName(key)}>
<NamedAttributeInspector name={displayableName(name)}>
<Checkbox checked={inspectable.value} disabled />
</NamedAttributeInspector>
);
case 'enum':
return (
<NamedAttributeInspector name={displayableName(key)}>
<NamedAttributeInspector name={displayableName(name)}>
<EnumValue>{inspectable.value.value}</EnumValue>
</NamedAttributeInspector>
);
case 'text':
return (
<NamedAttributeInspector name={displayableName(key)}>
<NamedAttributeInspector name={displayableName(name)}>
<TextValue>{inspectable.value}</TextValue>
</NamedAttributeInspector>
);
case 'number':
return (
<NamedAttributeInspector name={displayableName(key)}>
<NamedAttributeInspector name={displayableName(name)}>
<NumberValue>{inspectable.value}</NumberValue>
</NamedAttributeInspector>
);
case 'color':
return (
<NamedAttributeInspector name={displayableName(key)}>
<NamedAttributeInspector name={displayableName(name)}>
<ColorInspector color={inspectable.value} />
</NamedAttributeInspector>
);
case 'size':
return (
<NamedAttributeInspector name={displayableName(key)}>
<NamedAttributeInspector name={displayableName(name)}>
<SizeInspector value={inspectable.value} />
</NamedAttributeInspector>
);
case 'bounds':
return (
<NamedAttributeInspector name={displayableName(key)}>
<NamedAttributeInspector name={displayableName(name)}>
<BoundsInspector value={inspectable.value} />
</NamedAttributeInspector>
);
case 'coordinate':
return (
<NamedAttributeInspector name={displayableName(key)}>
<NamedAttributeInspector name={displayableName(name)}>
<CoordinateInspector value={inspectable.value} />
</NamedAttributeInspector>
);
case 'coordinate3d':
return (
<NamedAttributeInspector name={displayableName(key)}>
<NamedAttributeInspector name={displayableName(name)}>
<Coordinate3DInspector value={inspectable.value} />
</NamedAttributeInspector>
);
case 'space':
return (
<NamedAttributeInspector name={displayableName(key)}>
<NamedAttributeInspector name={displayableName(name)}>
<SpaceBoxInspector value={inspectable.value} />
</NamedAttributeInspector>
);
case 'object':
return (
<ObjectAttributeInspector
name={displayableName(key)}
value={inspectable.fields}
metadata={metadata}
name={displayableName(name)}
fields={inspectable.fields}
level={level}
/>
);
default:
return (
<NamedAttributeInspector name={displayableName(key)}>
<NamedAttributeInspector name={displayableName(name)}>
<TextValue>{JSON.stringify(inspectable)}</TextValue>
</NamedAttributeInspector>
);
}
}
/**
* Filter out those inspectables that affect sizing, positioning, and
* overall layout of elements.
*/
const layoutFilter = new Set([
'size',
'padding',
'margin',
'bounds',
'position',
'globalPosition',
'localVisibleRect',
'rotation',
'scale',
'pivot',
'layoutParams',
'layoutDirection',
'translation',
'elevation',
]);
function createSection(
mode: InspectorMode,
metadata: Map<MetadataId, Metadata>,
name: string,
inspectable: InspectableObject,
) {
const fields = Object.keys(inspectable.fields).filter(
(key) =>
(mode === 'attributes' && !layoutFilter.has(key)) ||
(mode === 'layout' && layoutFilter.has(key)),
);
if (!fields || fields.length === 0) {
return;
const children: any[] = [];
Object.keys(inspectable.fields).forEach((key, _index) => {
const metadataId: number = Number(key);
const attributeMetadata = metadata.get(metadataId);
if (attributeMetadata && attributeMetadata.type === mode) {
const attributeValue = inspectable.fields[metadataId];
children.push(create(metadata, attributeMetadata.name, attributeValue));
}
});
if (children.length > 0) {
return (
<Panel key={mode.concat(name)} title={name}>
{...children}
</Panel>
);
}
return (
<Panel key={name} title={name}>
{fields.map(function (key, _) {
return create(key, inspectable.fields[key]);
})}
</Panel>
);
}
type InspectorMode = 'layout' | 'attributes';
type InspectorMode = 'layout' | 'attribute';
type Props = {
node: UINode;
metadata: Map<MetadataId, Metadata>;
mode: InspectorMode;
rawDisplayEnabled?: boolean;
};
export const AttributesInspector: React.FC<Props> = ({
node,
metadata,
mode,
rawDisplayEnabled = false,
}) => {
return (
<>
{Object.keys(node.attributes).map(function (key, _) {
const metadataId: number = Number(key);
/**
* The node top-level attributes refer to the displayable panels.
* The panel name is obtained by querying the metadata.
* The inspectable contains the actual attributes belonging to each panel.
*/
return createSection(
mode,
key,
node.attributes[key] as InspectableObject,
metadata,
metadata.get(metadataId)?.name ?? '',
node.attributes[metadataId] as InspectableObject,
);
})}
{rawDisplayEnabled ?? (

View File

@@ -8,12 +8,36 @@
*/
import {PluginClient, createState, createDataSource} from 'flipper-plugin';
import {Events, Id, PerfStatsEvent, Snapshot, TreeState, UINode} from './types';
import {
Events,
Id,
Metadata,
MetadataId,
PerfStatsEvent,
Snapshot,
TreeState,
UINode,
} from './types';
import './node_modules/react-complex-tree/lib/style.css';
export function plugin(client: PluginClient<Events>) {
const rootId = createState<Id | undefined>(undefined);
client.onMessage('init', (root) => rootId.set(root.rootId));
const metadata = createState<Map<MetadataId, Metadata>>(new Map());
client.onMessage('init', (event) => {
rootId.set(event.rootId);
});
client.onMessage('metadataUpdate', (event) => {
if (!event.attributeMetadata) {
return;
}
metadata.update((draft) => {
for (const [_key, value] of Object.entries(event.attributeMetadata)) {
draft.set(value.id, value);
}
});
});
const perfEvents = createDataSource<PerfStatsEvent, 'txId'>([], {
key: 'txId',
@@ -23,13 +47,13 @@ export function plugin(client: PluginClient<Events>) {
perfEvents.append(event);
});
const nodesAtom = createState<Map<Id, UINode>>(new Map());
const snapshotsAtom = createState<Map<Id, Snapshot>>(new Map());
const nodes = createState<Map<Id, UINode>>(new Map());
const snapshots = createState<Map<Id, Snapshot>>(new Map());
const treeState = createState<TreeState>({expandedNodes: []});
client.onMessage('coordinateUpdate', (event) => {
nodesAtom.update((draft) => {
nodes.update((draft) => {
const node = draft.get(event.nodeId);
if (!node) {
console.warn(`Coordinate update for non existing node `, event);
@@ -42,13 +66,13 @@ export function plugin(client: PluginClient<Events>) {
const seenNodes = new Set<Id>();
client.onMessage('subtreeUpdate', (event) => {
snapshotsAtom.update((draft) => {
snapshots.update((draft) => {
draft.set(event.rootId, event.snapshot);
});
nodesAtom.update((draft) => {
for (const node of event.nodes) {
nodes.update((draft) => {
event.nodes.forEach((node) => {
draft.set(node.id, node);
}
});
});
treeState.update((draft) => {
@@ -74,8 +98,9 @@ export function plugin(client: PluginClient<Events>) {
return {
rootId,
snapshots: snapshotsAtom,
nodes: nodesAtom,
nodes,
metadata,
snapshots,
perfEvents,
treeState,
};

View File

@@ -14,6 +14,7 @@ export type Events = {
subtreeUpdate: SubtreeUpdateEvent;
coordinateUpdate: CoordinateUpdateEvent;
perfStats: PerfStatsEvent;
metadataUpdate: UpdateMetadataEvent;
};
export type CoordinateUpdateEvent = {
@@ -29,7 +30,9 @@ export type SubtreeUpdateEvent = {
snapshot: Snapshot;
};
export type InitEvent = {rootId: Id};
export type InitEvent = {
rootId: Id;
};
export type PerfStatsEvent = {
txId: number;
@@ -43,16 +46,29 @@ export type PerfStatsEvent = {
nodesCount: number;
};
export type UpdateMetadataEvent = {
attributeMetadata: Record<MetadataId, Metadata>;
};
export type UINode = {
id: Id;
name: string;
attributes: Record<string, Inspectable>;
attributes: Record<MetadataId, Inspectable>;
children: Id[];
bounds: Bounds;
tags: Tag[];
activeChild?: Id;
};
export type Metadata = {
id: MetadataId;
type: string;
namespace: string;
name: string;
mutable: boolean;
tags?: string[];
};
export type Bounds = {
x: number;
y: number;
@@ -93,6 +109,7 @@ export type Color = {
export type Snapshot = string;
export type Id = number | TreeItemIndex;
export type MetadataId = number;
export type TreeState = {expandedNodes: Id[]};
export type Tag = 'Native' | 'Declarative' | 'Android' | 'Litho ';
@@ -113,64 +130,54 @@ export type Inspectable =
export type InspectableText = {
type: 'text';
value: string;
mutable: boolean;
};
export type InspectableNumber = {
type: 'number';
value: number;
mutable: boolean;
};
export type InspectableBoolean = {
type: 'boolean';
value: boolean;
mutable: boolean;
};
export type InspectableEnum = {
type: 'enum';
value: {value: string; values: string[]};
mutable: boolean;
};
export type InspectableColor = {
type: 'color';
value: Color;
mutable: boolean;
};
export type InspectableBounds = {
type: 'bounds';
value: Bounds;
mutable: boolean;
};
export type InspectableSize = {
type: 'size';
value: Size;
mutable: boolean;
};
export type InspectableCoordinate = {
type: 'coordinate';
value: Coordinate;
mutable: boolean;
};
export type InspectableCoordinate3D = {
type: 'coordinate3d';
value: Coordinate3D;
mutable: boolean;
};
export type InspectableSpaceBox = {
type: 'space';
value: SpaceBox;
mutable: boolean;
};
export type InspectableObject = {
type: 'object';
fields: Record<string, Inspectable>;
fields: Record<MetadataId, Inspectable>;
};