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:
committed by
Facebook GitHub Bot
parent
27428522ce
commit
01dc22b1ab
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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 ?? (
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user