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:
Luke De Feo
2023-02-06 04:33:11 -08:00
committed by Facebook GitHub Bot
parent d3df6bc00e
commit d93c9d45a9
4 changed files with 309 additions and 16 deletions

View File

@@ -6,11 +6,26 @@
*
* @format
*/
import React from 'react';
import React, {useState} from 'react';
import {plugin} from '../index';
import {Button, Input, Tooltip} from 'antd';
import {PauseCircleOutlined, PlayCircleOutlined} from '@ant-design/icons';
import {
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 {FrameworkEventType} from '../types';
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
@@ -25,6 +40,20 @@ export const Controls: React.FC = () => {
const instance = usePlugin(plugin);
const searchTerm = useValue(instance.uiState.searchTerm);
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 (
<Layout.Horizontal pad="small" gap="small">
<Input
@@ -41,6 +70,196 @@ export const Controls: React.FC = () => {
{isPaused ? <PlayCircleOutlined /> : <PauseCircleOutlined />}
</Tooltip>
}></Button>
<MoreOptionsMenu
onSetEventMonitored={onSetEventMonitored}
frameworkEventTypes={[...frameworkEventMonitoring.entries()]}
/>
</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: [],
};
}

View File

@@ -183,7 +183,7 @@ function Visualization2DNode({
const isSelected = selectedNode === node.id;
const {isHovered, isLongHovered} = useHoverStates(node.id);
const ref = useRef<HTMLDivElement>(null);
let nestedChildren: NestedNode[];
//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 (
<Tooltip
visible={isLongHovered}
@@ -223,6 +227,7 @@ function Visualization2DNode({
<div
role="button"
tabIndex={0}
ref={ref}
style={{
position: 'absolute',
cursor: 'pointer',
@@ -230,8 +235,10 @@ function Visualization2DNode({
top: toPx(node.bounds.y),
width: toPx(node.bounds.width),
height: toPx(node.bounds.height),
opacity: isSelected ? 0.5 : 1,
backgroundColor: isSelected
opacity: isSelected || isHighlighted ? 0.3 : 1,
backgroundColor: isHighlighted
? 'red'
: isSelected
? theme.selectionBackgroundColor
: 'transparent',
}}

View File

@@ -8,15 +8,17 @@
*/
import {
PluginClient,
createState,
createDataSource,
produce,
Atom,
createDataSource,
createState,
PluginClient,
produce,
} from 'flipper-plugin';
import {
Events,
Id,
FrameworkEvent,
FrameworkEventType,
Metadata,
MetadataId,
PerfStatsEvent,
@@ -37,8 +39,10 @@ type UIState = {
searchTerm: Atom<string>;
isContextMenuOpen: Atom<boolean>;
hoveredNodes: Atom<Id[]>;
highlightedNodes: Atom<Set<Id>>;
focusedNode: Atom<Id | undefined>;
expandedNodes: Atom<Set<Id>>;
frameworkEventMonitoring: Atom<Map<FrameworkEventType, boolean>>;
};
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 frameworkEvents = createState<Map<Id, FrameworkEvent[]>>(new Map());
const highlightedNodes = createState(new Set<Id>());
const snapshot = createState<SnapshotInfo | null>(null);
const uiState: UIState = {
//used to disabled hover effects which cause rerenders and mess up the existing context menu
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),
//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>();
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) => {
if (event.snapshot) {
if (subtreeUpdate.snapshot) {
draft.snapshotInfo = {
nodeId: event.rootId,
base64Image: event.snapshot,
nodeId: subtreeUpdate.rootId,
base64Image: subtreeUpdate.snapshot,
};
}
event.nodes.forEach((node) => {
subtreeUpdate.nodes.forEach((node) => {
draft.nodes.set(node.id, {...node});
});
setParentPointers(rootId.get()!!, undefined, draft.nodes);
});
uiState.expandedNodes.update((draft) => {
for (const node of event.nodes) {
for (const node of subtreeUpdate.nodes) {
if (!seenNodes.has(node.id)) {
draft.add(node.id);
}
@@ -176,6 +228,7 @@ export function plugin(client: PluginClient<Events>) {
uiState,
uiActions: uiActions(uiState),
nodes,
frameworkEvents,
snapshot,
metadata,
perfEvents,
@@ -289,6 +342,8 @@ function collapseinActiveChildren(node: UINode, expandedNodes: Draft<Set<Id>>) {
}
}
const HighlightTime = 300;
export {Component} from './components/main';
setLogger({

View File

@@ -26,6 +26,18 @@ export type SubtreeUpdateEvent = {
rootId: Id;
nodes: UINode[];
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 = {