Ability to highlight nodes that match monitored event
Summary: Listen to framework events and store in a map based on node id Added UI to allow for monitoring framework event types. The event type is a string separated by : Each segment of this string represents a level in the dialog hierachy. For example Litho:Layout:StateUpdateSync would have levels, Litho Layout StateUpdateSync When event type monitored and event comes in for a node flash the visualiser node briefly Reviewed By: lblasa Differential Revision: D42074988 fbshipit-source-id: 52458ad87ab84bf7b1749e87be516ed73106a6c0
This commit is contained in:
committed by
Facebook GitHub Bot
parent
d3df6bc00e
commit
d93c9d45a9
@@ -6,11 +6,26 @@
|
|||||||
*
|
*
|
||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React, {useState} from 'react';
|
||||||
import {plugin} from '../index';
|
import {plugin} from '../index';
|
||||||
import {Button, Input, Tooltip} from 'antd';
|
import {
|
||||||
import {PauseCircleOutlined, PlayCircleOutlined} from '@ant-design/icons';
|
Button,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Tooltip,
|
||||||
|
Dropdown,
|
||||||
|
Menu,
|
||||||
|
Typography,
|
||||||
|
TreeSelect,
|
||||||
|
Space,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
MoreOutlined,
|
||||||
|
PauseCircleOutlined,
|
||||||
|
PlayCircleOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import {usePlugin, useValue, Layout} from 'flipper-plugin';
|
import {usePlugin, useValue, Layout} from 'flipper-plugin';
|
||||||
|
import {FrameworkEventType} from '../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
@@ -25,6 +40,20 @@ export const Controls: React.FC = () => {
|
|||||||
const instance = usePlugin(plugin);
|
const instance = usePlugin(plugin);
|
||||||
const searchTerm = useValue(instance.uiState.searchTerm);
|
const searchTerm = useValue(instance.uiState.searchTerm);
|
||||||
const isPaused = useValue(instance.uiState.isPaused);
|
const isPaused = useValue(instance.uiState.isPaused);
|
||||||
|
|
||||||
|
const frameworkEventMonitoring: Map<FrameworkEventType, boolean> = useValue(
|
||||||
|
instance.uiState.frameworkEventMonitoring,
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSetEventMonitored: (
|
||||||
|
eventType: FrameworkEventType,
|
||||||
|
monitored: boolean,
|
||||||
|
) => void = (eventType: FrameworkEventType, monitored: boolean) => {
|
||||||
|
instance.uiState.frameworkEventMonitoring.update((draft) =>
|
||||||
|
draft.set(eventType, monitored),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Horizontal pad="small" gap="small">
|
<Layout.Horizontal pad="small" gap="small">
|
||||||
<Input
|
<Input
|
||||||
@@ -41,6 +70,196 @@ export const Controls: React.FC = () => {
|
|||||||
{isPaused ? <PlayCircleOutlined /> : <PauseCircleOutlined />}
|
{isPaused ? <PlayCircleOutlined /> : <PauseCircleOutlined />}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}></Button>
|
}></Button>
|
||||||
|
<MoreOptionsMenu
|
||||||
|
onSetEventMonitored={onSetEventMonitored}
|
||||||
|
frameworkEventTypes={[...frameworkEventMonitoring.entries()]}
|
||||||
|
/>
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function MoreOptionsMenu({
|
||||||
|
onSetEventMonitored,
|
||||||
|
frameworkEventTypes,
|
||||||
|
}: {
|
||||||
|
onSetEventMonitored: (
|
||||||
|
eventType: FrameworkEventType,
|
||||||
|
monitored: boolean,
|
||||||
|
) => void;
|
||||||
|
frameworkEventTypes: [FrameworkEventType, boolean][];
|
||||||
|
}) {
|
||||||
|
const [showFrameworkEventsModal, setShowFrameworkEventsModal] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const moreOptionsMenu = (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item
|
||||||
|
onClick={() => {
|
||||||
|
setShowFrameworkEventsModal(true);
|
||||||
|
}}>
|
||||||
|
Framework event monitoring
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dropdown
|
||||||
|
key="more"
|
||||||
|
mouseLeaveDelay={0.7}
|
||||||
|
overlay={moreOptionsMenu}
|
||||||
|
placement="bottomRight">
|
||||||
|
<Button type="text" icon={<MoreOutlined style={{fontSize: 20}} />} />
|
||||||
|
</Dropdown>
|
||||||
|
|
||||||
|
{/*invisible until shown*/}
|
||||||
|
<FrameworkEventsMonitoringModal
|
||||||
|
frameworkEventTypes={frameworkEventTypes}
|
||||||
|
onSetEventMonitored={onSetEventMonitored}
|
||||||
|
visible={showFrameworkEventsModal}
|
||||||
|
onCancel={() => setShowFrameworkEventsModal(false)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FrameworkEventsMonitoringModal({
|
||||||
|
visible,
|
||||||
|
onCancel,
|
||||||
|
onSetEventMonitored,
|
||||||
|
frameworkEventTypes,
|
||||||
|
}: {
|
||||||
|
visible: boolean;
|
||||||
|
onCancel: () => void;
|
||||||
|
onSetEventMonitored: (
|
||||||
|
eventType: FrameworkEventType,
|
||||||
|
monitored: boolean,
|
||||||
|
) => void;
|
||||||
|
frameworkEventTypes: [FrameworkEventType, boolean][];
|
||||||
|
}) {
|
||||||
|
const selectedFrameworkEvents = frameworkEventTypes
|
||||||
|
.filter(([, selected]) => selected)
|
||||||
|
.map(([eventType]) => eventType);
|
||||||
|
|
||||||
|
const treeData = buildTreeSelectData(
|
||||||
|
frameworkEventTypes.map(([type]) => type),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="Framework event monitoring"
|
||||||
|
visible={visible}
|
||||||
|
footer={null}
|
||||||
|
onCancel={onCancel}>
|
||||||
|
<Space direction="vertical" size="large">
|
||||||
|
<Typography.Text>
|
||||||
|
Monitoring an event will cause the relevant node in the visualizer and
|
||||||
|
tree to highlight briefly. Additionally a running total of the number
|
||||||
|
of each event will be show in the tree inline
|
||||||
|
</Typography.Text>
|
||||||
|
|
||||||
|
<TreeSelect
|
||||||
|
treeCheckable
|
||||||
|
showSearch={false}
|
||||||
|
showCheckedStrategy={TreeSelect.SHOW_PARENT}
|
||||||
|
placeholder="Select nodes to monitor"
|
||||||
|
virtual={false} //for scrollbar
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
treeData={treeData}
|
||||||
|
treeDefaultExpandAll
|
||||||
|
value={selectedFrameworkEvents}
|
||||||
|
onSelect={(_: any, node: any) => {
|
||||||
|
for (const leaf of getAllLeaves(node)) {
|
||||||
|
onSetEventMonitored(leaf, true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDeselect={(_: any, node: any) => {
|
||||||
|
for (const leaf of getAllLeaves(node)) {
|
||||||
|
onSetEventMonitored(leaf, false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type TreeSelectNode = {
|
||||||
|
title: string;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
children: TreeSelectNode[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In tree select you can select a parent which implicitly selects all children, we find them all here as the real state
|
||||||
|
* is in terms of the leaf nodes
|
||||||
|
*/
|
||||||
|
function getAllLeaves(treeSelectNode: TreeSelectNode) {
|
||||||
|
const result: string[] = [];
|
||||||
|
function getAllLeavesRec(node: TreeSelectNode) {
|
||||||
|
console.log(node);
|
||||||
|
if (node.children.length > 0) {
|
||||||
|
for (const child of node.children) {
|
||||||
|
getAllLeavesRec(child);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push(node.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getAllLeavesRec(treeSelectNode);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* transformed flat event type data structure into tree
|
||||||
|
*/
|
||||||
|
function buildTreeSelectData(eventTypes: string[]): TreeSelectNode[] {
|
||||||
|
const root: TreeSelectNode = buildTreeSelectNode('root', 'root');
|
||||||
|
|
||||||
|
eventTypes.forEach((eventType) => {
|
||||||
|
const eventSubtypes = eventType.split(':');
|
||||||
|
let currentNode = root;
|
||||||
|
|
||||||
|
// Find the parent node for the current id
|
||||||
|
for (let i = 0; i < eventSubtypes.length - 1; i++) {
|
||||||
|
let foundChild = false;
|
||||||
|
for (const child of currentNode.children) {
|
||||||
|
if (child.title === eventSubtypes[i]) {
|
||||||
|
currentNode = child;
|
||||||
|
foundChild = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundChild) {
|
||||||
|
const newNode: TreeSelectNode = buildTreeSelectNode(
|
||||||
|
eventSubtypes[i],
|
||||||
|
eventSubtypes.slice(0, i + 1).join(':'),
|
||||||
|
);
|
||||||
|
|
||||||
|
currentNode.children.push(newNode);
|
||||||
|
currentNode = newNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add the current id as a child of the parent node
|
||||||
|
currentNode.children.push(
|
||||||
|
buildTreeSelectNode(
|
||||||
|
eventSubtypes[eventSubtypes.length - 1],
|
||||||
|
eventSubtypes.slice(0, eventSubtypes.length).join(':'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return root.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTreeSelectNode(title: string, fullValue: string): TreeSelectNode {
|
||||||
|
return {
|
||||||
|
title: title,
|
||||||
|
key: fullValue,
|
||||||
|
value: fullValue,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ function Visualization2DNode({
|
|||||||
|
|
||||||
const isSelected = selectedNode === node.id;
|
const isSelected = selectedNode === node.id;
|
||||||
const {isHovered, isLongHovered} = useHoverStates(node.id);
|
const {isHovered, isLongHovered} = useHoverStates(node.id);
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
let nestedChildren: NestedNode[];
|
let nestedChildren: NestedNode[];
|
||||||
|
|
||||||
//if there is an active child don't draw the other children
|
//if there is an active child don't draw the other children
|
||||||
@@ -210,6 +210,10 @@ function Visualization2DNode({
|
|||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const isHighlighted = useValue(instance.uiState.highlightedNodes).has(
|
||||||
|
node.id,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
visible={isLongHovered}
|
visible={isLongHovered}
|
||||||
@@ -223,6 +227,7 @@ function Visualization2DNode({
|
|||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
ref={ref}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
@@ -230,8 +235,10 @@ function Visualization2DNode({
|
|||||||
top: toPx(node.bounds.y),
|
top: toPx(node.bounds.y),
|
||||||
width: toPx(node.bounds.width),
|
width: toPx(node.bounds.width),
|
||||||
height: toPx(node.bounds.height),
|
height: toPx(node.bounds.height),
|
||||||
opacity: isSelected ? 0.5 : 1,
|
opacity: isSelected || isHighlighted ? 0.3 : 1,
|
||||||
backgroundColor: isSelected
|
backgroundColor: isHighlighted
|
||||||
|
? 'red'
|
||||||
|
: isSelected
|
||||||
? theme.selectionBackgroundColor
|
? theme.selectionBackgroundColor
|
||||||
: 'transparent',
|
: 'transparent',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -8,15 +8,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PluginClient,
|
|
||||||
createState,
|
|
||||||
createDataSource,
|
|
||||||
produce,
|
|
||||||
Atom,
|
Atom,
|
||||||
|
createDataSource,
|
||||||
|
createState,
|
||||||
|
PluginClient,
|
||||||
|
produce,
|
||||||
} from 'flipper-plugin';
|
} from 'flipper-plugin';
|
||||||
import {
|
import {
|
||||||
Events,
|
Events,
|
||||||
Id,
|
Id,
|
||||||
|
FrameworkEvent,
|
||||||
|
FrameworkEventType,
|
||||||
Metadata,
|
Metadata,
|
||||||
MetadataId,
|
MetadataId,
|
||||||
PerfStatsEvent,
|
PerfStatsEvent,
|
||||||
@@ -37,8 +39,10 @@ type UIState = {
|
|||||||
searchTerm: Atom<string>;
|
searchTerm: Atom<string>;
|
||||||
isContextMenuOpen: Atom<boolean>;
|
isContextMenuOpen: Atom<boolean>;
|
||||||
hoveredNodes: Atom<Id[]>;
|
hoveredNodes: Atom<Id[]>;
|
||||||
|
highlightedNodes: Atom<Set<Id>>;
|
||||||
focusedNode: Atom<Id | undefined>;
|
focusedNode: Atom<Id | undefined>;
|
||||||
expandedNodes: Atom<Set<Id>>;
|
expandedNodes: Atom<Set<Id>>;
|
||||||
|
frameworkEventMonitoring: Atom<Map<FrameworkEventType, boolean>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function plugin(client: PluginClient<Events>) {
|
export function plugin(client: PluginClient<Events>) {
|
||||||
@@ -72,12 +76,23 @@ export function plugin(client: PluginClient<Events>) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const nodes = createState<Map<Id, UINode>>(new Map());
|
const nodes = createState<Map<Id, UINode>>(new Map());
|
||||||
|
const frameworkEvents = createState<Map<Id, FrameworkEvent[]>>(new Map());
|
||||||
|
|
||||||
|
const highlightedNodes = createState(new Set<Id>());
|
||||||
const snapshot = createState<SnapshotInfo | null>(null);
|
const snapshot = createState<SnapshotInfo | null>(null);
|
||||||
|
|
||||||
const uiState: UIState = {
|
const uiState: UIState = {
|
||||||
//used to disabled hover effects which cause rerenders and mess up the existing context menu
|
//used to disabled hover effects which cause rerenders and mess up the existing context menu
|
||||||
isContextMenuOpen: createState<boolean>(false),
|
isContextMenuOpen: createState<boolean>(false),
|
||||||
|
|
||||||
|
highlightedNodes,
|
||||||
|
|
||||||
|
//used to indicate whether we will higher the visualizer / tree when a matching event comes in
|
||||||
|
//also whether or not will show running total in the tree
|
||||||
|
frameworkEventMonitoring: createState(
|
||||||
|
new Map<FrameworkEventType, boolean>(),
|
||||||
|
),
|
||||||
|
|
||||||
isPaused: createState(false),
|
isPaused: createState(false),
|
||||||
|
|
||||||
//The reason for the array as that user could be hovering multiple overlapping nodes at once in the visualiser.
|
//The reason for the array as that user could be hovering multiple overlapping nodes at once in the visualiser.
|
||||||
@@ -131,23 +146,60 @@ export function plugin(client: PluginClient<Events>) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const seenNodes = new Set<Id>();
|
const seenNodes = new Set<Id>();
|
||||||
client.onMessage('subtreeUpdate', (event) => {
|
client.onMessage('subtreeUpdate', (subtreeUpdate) => {
|
||||||
|
uiState.frameworkEventMonitoring.update((draft) => {
|
||||||
|
(subtreeUpdate.frameworkEvents ?? []).forEach((frameworkEvent) => {
|
||||||
|
if (!draft.has(frameworkEvent.type))
|
||||||
|
draft.set(frameworkEvent.type, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
frameworkEvents.update((draft) => {
|
||||||
|
if (subtreeUpdate.frameworkEvents) {
|
||||||
|
subtreeUpdate.frameworkEvents.forEach((frameworkEvent) => {
|
||||||
|
if (
|
||||||
|
uiState.frameworkEventMonitoring.get().get(frameworkEvent.type) ===
|
||||||
|
true &&
|
||||||
|
uiState.isPaused.get() === false
|
||||||
|
) {
|
||||||
|
highlightedNodes.update((draft) => {
|
||||||
|
draft.add(frameworkEvent.nodeId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const frameworkEventsForNode = draft.get(frameworkEvent.nodeId);
|
||||||
|
if (frameworkEventsForNode) {
|
||||||
|
frameworkEventsForNode.push(frameworkEvent);
|
||||||
|
} else {
|
||||||
|
draft.set(frameworkEvent.nodeId, [frameworkEvent]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
highlightedNodes.update((laterDraft) => {
|
||||||
|
for (const event of subtreeUpdate.frameworkEvents!!.values()) {
|
||||||
|
laterDraft.delete(event.nodeId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, HighlightTime);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
liveClientData = produce(liveClientData, (draft) => {
|
liveClientData = produce(liveClientData, (draft) => {
|
||||||
if (event.snapshot) {
|
if (subtreeUpdate.snapshot) {
|
||||||
draft.snapshotInfo = {
|
draft.snapshotInfo = {
|
||||||
nodeId: event.rootId,
|
nodeId: subtreeUpdate.rootId,
|
||||||
base64Image: event.snapshot,
|
base64Image: subtreeUpdate.snapshot,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
event.nodes.forEach((node) => {
|
subtreeUpdate.nodes.forEach((node) => {
|
||||||
draft.nodes.set(node.id, {...node});
|
draft.nodes.set(node.id, {...node});
|
||||||
});
|
});
|
||||||
setParentPointers(rootId.get()!!, undefined, draft.nodes);
|
setParentPointers(rootId.get()!!, undefined, draft.nodes);
|
||||||
});
|
});
|
||||||
|
|
||||||
uiState.expandedNodes.update((draft) => {
|
uiState.expandedNodes.update((draft) => {
|
||||||
for (const node of event.nodes) {
|
for (const node of subtreeUpdate.nodes) {
|
||||||
if (!seenNodes.has(node.id)) {
|
if (!seenNodes.has(node.id)) {
|
||||||
draft.add(node.id);
|
draft.add(node.id);
|
||||||
}
|
}
|
||||||
@@ -176,6 +228,7 @@ export function plugin(client: PluginClient<Events>) {
|
|||||||
uiState,
|
uiState,
|
||||||
uiActions: uiActions(uiState),
|
uiActions: uiActions(uiState),
|
||||||
nodes,
|
nodes,
|
||||||
|
frameworkEvents,
|
||||||
snapshot,
|
snapshot,
|
||||||
metadata,
|
metadata,
|
||||||
perfEvents,
|
perfEvents,
|
||||||
@@ -289,6 +342,8 @@ function collapseinActiveChildren(node: UINode, expandedNodes: Draft<Set<Id>>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HighlightTime = 300;
|
||||||
|
|
||||||
export {Component} from './components/main';
|
export {Component} from './components/main';
|
||||||
|
|
||||||
setLogger({
|
setLogger({
|
||||||
|
|||||||
@@ -26,6 +26,18 @@ export type SubtreeUpdateEvent = {
|
|||||||
rootId: Id;
|
rootId: Id;
|
||||||
nodes: UINode[];
|
nodes: UINode[];
|
||||||
snapshot: Snapshot;
|
snapshot: Snapshot;
|
||||||
|
frameworkEvents?: FrameworkEvent[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Thread = 'Main' | 'Background';
|
||||||
|
|
||||||
|
export type FrameworkEventType = string;
|
||||||
|
|
||||||
|
export type FrameworkEvent = {
|
||||||
|
nodeId: Id;
|
||||||
|
type: FrameworkEventType;
|
||||||
|
thread: Thread;
|
||||||
|
timestamp: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InitEvent = {
|
export type InitEvent = {
|
||||||
|
|||||||
Reference in New Issue
Block a user