UID Refactor 5/n Refactor index.tsx

Summary:
This file was huge and was hard to understand what was going on.  changes:

1. UIActions moved out to separate file
2. create UIstate moved out
3. All declared state (atoms or plain js objects) moved to the top of the function like a class
4. utilities moved out

Reviewed By: lblasa

Differential Revision: D47547844

fbshipit-source-id: e7fa705a14a23bff2415016a488147bed7ad9e91
This commit is contained in:
Luke De Feo
2023-07-21 07:17:31 -07:00
committed by Facebook GitHub Bot
parent 957a336349
commit efb23be4cf
4 changed files with 305 additions and 271 deletions

View File

@@ -16,8 +16,14 @@ import {
Tag, Tag,
ClientNode, ClientNode,
Metadata, Metadata,
SnapshotInfo,
} from './ClientTypes'; } from './ClientTypes';
export type LiveClientState = {
snapshotInfo: SnapshotInfo | null;
nodes: Map<Id, ClientNode>;
};
export type UIState = { export type UIState = {
viewMode: Atom<ViewMode>; viewMode: Atom<ViewMode>;
isConnected: Atom<boolean>; isConnected: Atom<boolean>;

View File

@@ -7,12 +7,7 @@
* @format * @format
*/ */
import { import {createDataSource, createState, PluginClient} from 'flipper-plugin';
Atom,
createDataSource,
createState,
PluginClient,
} from 'flipper-plugin';
import { import {
Events, Events,
FrameScanEvent, FrameScanEvent,
@@ -28,23 +23,18 @@ import {
import { import {
UIState, UIState,
NodeSelection, NodeSelection,
SelectionSource,
StreamInterceptorError, StreamInterceptorError,
StreamState, StreamState,
UIActions,
ViewMode,
ReadOnlyUIState, ReadOnlyUIState,
LiveClientState,
} from './DesktopTypes'; } from './DesktopTypes';
import {Draft} from 'immer';
import {tracker} from './utils/tracker';
import {getStreamInterceptor} from './fb-stubs/StreamInterceptor'; import {getStreamInterceptor} from './fb-stubs/StreamInterceptor';
import {prefetchSourceFileLocation} from './components/fb-stubs/IDEContextMenu'; import {prefetchSourceFileLocation} from './components/fb-stubs/IDEContextMenu';
import {debounce} from 'lodash'; import {
checkFocusedNodeStillActive,
type LiveClientState = { collapseinActiveChildren,
snapshotInfo: SnapshotInfo | null; } from './plugin/ClientDataUtils';
nodes: Map<Id, ClientNode>; import {uiActions} from './plugin/uiActions';
};
type PendingData = { type PendingData = {
metadata: Record<MetadataId, Metadata>; metadata: Record<MetadataId, Metadata>;
@@ -53,12 +43,38 @@ type PendingData = {
export function plugin(client: PluginClient<Events>) { export function plugin(client: PluginClient<Events>) {
const rootId = createState<Id | undefined>(undefined); const rootId = createState<Id | undefined>(undefined);
const metadata = createState<Map<MetadataId, Metadata>>(new Map()); const metadata = createState<Map<MetadataId, Metadata>>(new Map());
const streamInterceptor = getStreamInterceptor(client.device.os); const streamInterceptor = getStreamInterceptor(client.device.os);
const snapshot = createState<SnapshotInfo | null>(null);
const nodesAtom = createState<Map<Id, ClientNode>>(new Map());
const frameworkEvents = createDataSource<FrameworkEvent>([], {
indices: [['nodeId']],
limit: 10000,
});
const uiState: UIState = createUIState();
//this is the client data is what drives all of desktop UI
//it is always up-to-date with the client regardless of whether we are paused or not
const mutableLiveClientData: LiveClientState = {
snapshotInfo: null,
nodes: new Map(),
};
const perfEvents = createDataSource<PerformanceStatsEvent, 'txId'>([], {
key: 'txId',
limit: 10 * 1024,
});
//this keeps track of all node ids we have seen so we dont keep reexpanding nodes when they come in again.
//Could probably be removed if we refactor the nodes to be expanded by default and only collapsed is toggled on
const seenNodes = new Set<Id>();
//this holds pending any pending data that needs to be applied in the event of a stream interceptor error
//while in the error state more metadata or a more recent frame may come in so both cases need to apply the same darta
const pendingData: PendingData = {frame: null, metadata: {}};
let lastFrameTime = 0; let lastFrameTime = 0;
const os = client.device.os;
client.onMessage('init', (event) => { client.onMessage('init', (event) => {
console.log('[ui-debugger] init'); console.log('[ui-debugger] init');
@@ -105,10 +121,6 @@ export function plugin(client: PluginClient<Events>) {
} }
} }
//this holds pending any pending data that needs to be applied in the event of a stream interceptor error
//while in the error state more metadata or a more recent frame may come in so both cases need to apply the same darta
const pendingData: PendingData = {frame: null, metadata: {}};
function handleStreamError(source: 'Frame' | 'Metadata', error: any) { function handleStreamError(source: 'Frame' | 'Metadata', error: any) {
if (error instanceof StreamInterceptorError) { if (error instanceof StreamInterceptorError) {
const retryCallback = async () => { const retryCallback = async () => {
@@ -162,11 +174,6 @@ export function plugin(client: PluginClient<Events>) {
await processMetadata(event.attributeMetadata); await processMetadata(event.attributeMetadata);
}); });
const perfEvents = createDataSource<PerformanceStatsEvent, 'txId'>([], {
key: 'txId',
limit: 10 * 1024,
});
/** /**
* The message handling below is a temporary measure for a couple of weeks until * The message handling below is a temporary measure for a couple of weeks until
* clients migrate to the newer message/format. * clients migrate to the newer message/format.
@@ -194,55 +201,6 @@ export function plugin(client: PluginClient<Events>) {
perfEvents.append(event); perfEvents.append(event);
}); });
const nodesAtom = createState<Map<Id, ClientNode>>(new Map());
const frameworkEvents = createDataSource<FrameworkEvent>([], {
indices: [['nodeId']],
limit: 10000,
});
const highlightedNodes = createState(new Set<Id>());
const snapshot = createState<SnapshotInfo | null>(null);
const uiState: UIState = {
isConnected: createState(false),
viewMode: createState({mode: 'default'}),
//used to disabled hover effects which cause rerenders and mess up the existing context menu
isContextMenuOpen: createState<boolean>(false),
streamState: createState<StreamState>({state: 'Ok'}),
visualiserWidth: createState(Math.min(window.innerWidth / 4.5, 500)),
highlightedNodes,
selectedNode: createState<NodeSelection | undefined>(undefined),
//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>(),
),
filterMainThreadMonitoring: 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 nodes are sorted by area since you most likely want to select the smallest node under your cursor
hoveredNodes: createState<Id[]>([]),
searchTerm: createState<string>(''),
focusedNode: createState<Id | undefined>(undefined),
expandedNodes: createState<Set<Id>>(new Set()),
};
//this is the client data is what drives all of desktop UI
//it is always up-to-date with the client regardless of whether we are paused or not
const mutableLiveClientData: LiveClientState = {
snapshotInfo: null,
nodes: new Map(),
};
const seenNodes = new Set<Id>();
const processFrame = async (frameScan: FrameScanEvent) => { const processFrame = async (frameScan: FrameScanEvent) => {
try { try {
const [processedNodes, additionalMetadata] = const [processedNodes, additionalMetadata] =
@@ -295,14 +253,14 @@ export function plugin(client: PluginClient<Events>) {
) )
.map((event) => event.nodeId) ?? []; .map((event) => event.nodeId) ?? [];
highlightedNodes.update((draft) => { uiState.highlightedNodes.update((draft) => {
for (const node of nodesToHighlight) { for (const node of nodesToHighlight) {
draft.add(node); draft.add(node);
} }
}); });
setTimeout(() => { setTimeout(() => {
highlightedNodes.update((draft) => { uiState.highlightedNodes.update((draft) => {
for (const nodeId of nodesToHighlight) { for (const nodeId of nodesToHighlight) {
draft.delete(nodeId); draft.delete(nodeId);
} }
@@ -367,202 +325,45 @@ export function plugin(client: PluginClient<Events>) {
snapshot, snapshot,
metadata, metadata,
perfEvents, perfEvents,
os, os: client.device.os,
}; };
} }
function uiActions(
uiState: UIState,
nodes: Atom<Map<Id, ClientNode>>,
snapshot: Atom<SnapshotInfo | null>,
liveClientData: LiveClientState,
): UIActions {
const onExpandNode = (node: Id) => {
uiState.expandedNodes.update((draft) => {
draft.add(node);
});
};
const onSelectNode = (node: Id | undefined, source: SelectionSource) => {
if (node == null || uiState.selectedNode.get()?.id === node) {
uiState.selectedNode.set(undefined);
} else {
uiState.selectedNode.set({id: node, source});
}
if (node) {
const selectedNode = nodes.get().get(node);
const tags = selectedNode?.tags;
if (tags) {
tracker.track('node-selected', {
name: selectedNode.name,
tags,
source: source,
});
}
let current = selectedNode?.parent;
// expand entire ancestory in case it has been manually collapsed
uiState.expandedNodes.update((expandedNodesDraft) => {
while (current != null) {
expandedNodesDraft.add(current);
current = nodes.get().get(current)?.parent;
}
});
}
};
const onCollapseNode = (node: Id) => {
uiState.expandedNodes.update((draft) => {
draft.delete(node);
});
};
const onHoverNode = (...node: Id[]) => {
if (node != null) {
uiState.hoveredNodes.set(node);
} else {
uiState.hoveredNodes.set([]);
}
};
const onContextMenuOpen = (open: boolean) => {
tracker.track('context-menu-opened', {});
uiState.isContextMenuOpen.set(open);
};
const onFocusNode = (node?: Id) => {
if (node != null) {
const focusedNode = nodes.get().get(node);
const tags = focusedNode?.tags;
if (tags) {
tracker.track('node-focused', {name: focusedNode.name, tags});
}
uiState.selectedNode.set(undefined);
}
uiState.focusedNode.set(node);
};
const setVisualiserWidth = (width: number) => {
uiState.visualiserWidth.set(width);
};
const onSetFilterMainThreadMonitoring = (toggled: boolean) => {
uiState.filterMainThreadMonitoring.set(toggled);
};
const onSetViewMode = (viewMode: ViewMode) => {
uiState.viewMode.set(viewMode);
};
const onSetFrameworkEventMonitored = (
eventType: FrameworkEventType,
monitored: boolean,
) => {
tracker.track('framework-event-monitored', {eventType, monitored});
uiState.frameworkEventMonitoring.update((draft) =>
draft.set(eventType, monitored),
);
};
const onPlayPauseToggled = () => {
const isPaused = !uiState.isPaused.get();
tracker.track('play-pause-toggled', {paused: isPaused});
uiState.isPaused.set(isPaused);
if (!isPaused) {
//When going back to play mode then set the atoms to the live state to rerender the latest
//Also need to fixed expanded state for any change in active child state
uiState.expandedNodes.update((draft) => {
liveClientData.nodes.forEach((node) => {
collapseinActiveChildren(node, draft);
});
});
nodes.set(liveClientData.nodes);
snapshot.set(liveClientData.snapshotInfo);
checkFocusedNodeStillActive(uiState, nodes.get());
}
};
const searchTermUpdatedDebounced = debounce((searchTerm: string) => {
tracker.track('search-term-updated', {searchTerm});
}, 250);
const onSearchTermUpdated = (searchTerm: string) => {
uiState.searchTerm.set(searchTerm);
searchTermUpdatedDebounced(searchTerm);
};
return {
onExpandNode,
onCollapseNode,
onHoverNode,
onSelectNode,
onContextMenuOpen,
onFocusNode,
setVisualiserWidth,
onSetFilterMainThreadMonitoring,
onSetViewMode,
onSetFrameworkEventMonitored,
onPlayPauseToggled,
onSearchTermUpdated,
};
}
function checkFocusedNodeStillActive(
uiState: UIState,
nodes: Map<Id, ClientNode>,
) {
const focusedNodeId = uiState.focusedNode.get();
const focusedNode = focusedNodeId && nodes.get(focusedNodeId);
if (!focusedNode || !isFocusedNodeAncestryAllActive(focusedNode, nodes)) {
uiState.focusedNode.set(undefined);
}
}
function isFocusedNodeAncestryAllActive(
focused: ClientNode,
nodes: Map<Id, ClientNode>,
): boolean {
let node = focused;
while (node != null) {
if (node.parent == null) {
return true;
}
const parent = nodes.get(node.parent);
if (parent == null) {
//should also never happen
return false;
}
if (parent.activeChild != null && parent.activeChild !== node.id) {
return false;
}
node = parent;
}
//wont happen
return false;
}
function collapseinActiveChildren(
node: ClientNode,
expandedNodes: Draft<Set<Id>>,
) {
if (node.activeChild) {
expandedNodes.add(node.activeChild);
for (const child of node.children) {
if (child !== node.activeChild) {
expandedNodes.delete(child);
}
}
}
}
const HighlightTime = 300; const HighlightTime = 300;
export {Component} from './components/main'; export {Component} from './components/main';
export * from './ClientTypes'; export * from './ClientTypes';
function createUIState(): UIState {
return {
isConnected: createState(false),
viewMode: createState({mode: 'default'}),
//used to disabled hover effects which cause rerenders and mess up the existing context menu
isContextMenuOpen: createState<boolean>(false),
streamState: createState<StreamState>({state: 'Ok'}),
visualiserWidth: createState(Math.min(window.innerWidth / 4.5, 500)),
highlightedNodes: createState(new Set<Id>()),
selectedNode: createState<NodeSelection | undefined>(undefined),
//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>(),
),
filterMainThreadMonitoring: 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 nodes are sorted by area since you most likely want to select the smallest node under your cursor
hoveredNodes: createState<Id[]>([]),
searchTerm: createState<string>(''),
focusedNode: createState<Id | undefined>(undefined),
expandedNodes: createState<Set<Id>>(new Set()),
};
}

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {Draft} from 'flipper-plugin';
import {ClientNode, Id} from '../ClientTypes';
import {UIState} from '../DesktopTypes';
export function collapseinActiveChildren(
node: ClientNode,
expandedNodes: Draft<Set<Id>>,
) {
if (node.activeChild) {
expandedNodes.add(node.activeChild);
for (const child of node.children) {
if (child !== node.activeChild) {
expandedNodes.delete(child);
}
}
}
}
export function checkFocusedNodeStillActive(
uiState: UIState,
nodes: Map<Id, ClientNode>,
) {
const focusedNodeId = uiState.focusedNode.get();
const focusedNode = focusedNodeId && nodes.get(focusedNodeId);
if (!focusedNode || !isFocusedNodeAncestryAllActive(focusedNode, nodes)) {
uiState.focusedNode.set(undefined);
}
}
function isFocusedNodeAncestryAllActive(
focused: ClientNode,
nodes: Map<Id, ClientNode>,
): boolean {
let node = focused;
while (node != null) {
if (node.parent == null) {
return true;
}
const parent = nodes.get(node.parent);
if (parent == null) {
//should also never happen
return false;
}
if (parent.activeChild != null && parent.activeChild !== node.id) {
return false;
}
node = parent;
}
//wont happen
return false;
}

View File

@@ -0,0 +1,162 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {Atom} from 'flipper-plugin';
import {debounce} from 'lodash';
import {ClientNode, FrameworkEventType, Id, SnapshotInfo} from '../ClientTypes';
import {
LiveClientState,
SelectionSource,
UIActions,
UIState,
ViewMode,
} from '../DesktopTypes';
import {tracker} from '../utils/tracker';
import {
checkFocusedNodeStillActive,
collapseinActiveChildren,
} from './ClientDataUtils';
export function uiActions(
uiState: UIState,
nodes: Atom<Map<Id, ClientNode>>,
snapshot: Atom<SnapshotInfo | null>,
liveClientData: LiveClientState,
): UIActions {
const onExpandNode = (node: Id) => {
uiState.expandedNodes.update((draft) => {
draft.add(node);
});
};
const onSelectNode = (node: Id | undefined, source: SelectionSource) => {
if (node == null || uiState.selectedNode.get()?.id === node) {
uiState.selectedNode.set(undefined);
} else {
uiState.selectedNode.set({id: node, source});
}
if (node) {
const selectedNode = nodes.get().get(node);
const tags = selectedNode?.tags;
if (tags) {
tracker.track('node-selected', {
name: selectedNode.name,
tags,
source: source,
});
}
let current = selectedNode?.parent;
// expand entire ancestory in case it has been manually collapsed
uiState.expandedNodes.update((expandedNodesDraft) => {
while (current != null) {
expandedNodesDraft.add(current);
current = nodes.get().get(current)?.parent;
}
});
}
};
const onCollapseNode = (node: Id) => {
uiState.expandedNodes.update((draft) => {
draft.delete(node);
});
};
const onHoverNode = (...node: Id[]) => {
if (node != null) {
uiState.hoveredNodes.set(node);
} else {
uiState.hoveredNodes.set([]);
}
};
const onContextMenuOpen = (open: boolean) => {
tracker.track('context-menu-opened', {});
uiState.isContextMenuOpen.set(open);
};
const onFocusNode = (node?: Id) => {
if (node != null) {
const focusedNode = nodes.get().get(node);
const tags = focusedNode?.tags;
if (tags) {
tracker.track('node-focused', {name: focusedNode.name, tags});
}
uiState.selectedNode.set(undefined);
}
uiState.focusedNode.set(node);
};
const setVisualiserWidth = (width: number) => {
uiState.visualiserWidth.set(width);
};
const onSetFilterMainThreadMonitoring = (toggled: boolean) => {
uiState.filterMainThreadMonitoring.set(toggled);
};
const onSetViewMode = (viewMode: ViewMode) => {
uiState.viewMode.set(viewMode);
};
const onSetFrameworkEventMonitored = (
eventType: FrameworkEventType,
monitored: boolean,
) => {
tracker.track('framework-event-monitored', {eventType, monitored});
uiState.frameworkEventMonitoring.update((draft) =>
draft.set(eventType, monitored),
);
};
const onPlayPauseToggled = () => {
const isPaused = !uiState.isPaused.get();
tracker.track('play-pause-toggled', {paused: isPaused});
uiState.isPaused.set(isPaused);
if (!isPaused) {
//When going back to play mode then set the atoms to the live state to rerender the latest
//Also need to fixed expanded state for any change in active child state
uiState.expandedNodes.update((draft) => {
liveClientData.nodes.forEach((node) => {
collapseinActiveChildren(node, draft);
});
});
nodes.set(liveClientData.nodes);
snapshot.set(liveClientData.snapshotInfo);
checkFocusedNodeStillActive(uiState, nodes.get());
}
};
const searchTermUpdatedDebounced = debounce((searchTerm: string) => {
tracker.track('search-term-updated', {searchTerm});
}, 250);
const onSearchTermUpdated = (searchTerm: string) => {
uiState.searchTerm.set(searchTerm);
searchTermUpdatedDebounced(searchTerm);
};
return {
onExpandNode,
onCollapseNode,
onHoverNode,
onSelectNode,
onContextMenuOpen,
onFocusNode,
setVisualiserWidth,
onSetFilterMainThreadMonitoring,
onSetViewMode,
onSetFrameworkEventMonitored,
onPlayPauseToggled,
onSearchTermUpdated,
};
}