From c96535e15fde233d2333b9580fabe8b0b2589c0b Mon Sep 17 00:00:00 2001 From: Luke De Feo Date: Thu, 27 Apr 2023 07:28:41 -0700 Subject: [PATCH] Infrastructure for stream inteceptor transform metadata Summary: Hooked up metadata to the stream inteceptor, enhanced error handling to deal with: 1. Recording subsequent metadata messaages that came in while in error state such that all of them are processed 2. Recording any frames that came in while in error state such that after recovering from error we have the latest state 3. Splitting out recoverable and non recoverable errors more explicitly Reviewed By: lblasa Differential Revision: D45079137 fbshipit-source-id: 67a2ffef72d94d2b1492f201a2228659720e306b --- .../public/ui-debugger/components/main.tsx | 30 +++-- desktop/plugins/public/ui-debugger/index.tsx | 126 ++++++++++-------- desktop/plugins/public/ui-debugger/types.tsx | 27 +++- 3 files changed, 117 insertions(+), 66 deletions(-) diff --git a/desktop/plugins/public/ui-debugger/components/main.tsx b/desktop/plugins/public/ui-debugger/components/main.tsx index ec64995bd..35509c646 100644 --- a/desktop/plugins/public/ui-debugger/components/main.tsx +++ b/desktop/plugins/public/ui-debugger/components/main.tsx @@ -27,13 +27,12 @@ import {Controls} from './Controls'; import {Button, Spin} from 'antd'; import {QueryClientProvider} from 'react-query'; import {Tree2} from './Tree'; +import {StreamInterceptorErrorView} from './StreamInterceptorErrorView'; export function Component() { const instance = usePlugin(plugin); const rootId = useValue(instance.rootId); - const streamInterceptorError = useValue( - instance.uiState.streamInterceptorError, - ); + const streamState = useValue(instance.uiState.streamState); const visualiserWidth = useValue(instance.uiState.visualiserWidth); const nodes: Map = useValue(instance.nodes); const metadata: Map = useValue(instance.metadata); @@ -54,13 +53,28 @@ export function Component() { setBottomPanelComponent(undefined); }; - if (showPerfStats) return ; - - if (streamInterceptorError != null) { - return streamInterceptorError; + if (streamState.state === 'UnrecoverableError') { + return ( + + ); } - if (rootId == null || nodes.size == 0) { + if (streamState.state === 'StreamInterceptorRetryableError') { + return ( + + ); + } + + if (showPerfStats) return ; + + if (rootId == null || streamState.state === 'RetryingAfterError') { return ( diff --git a/desktop/plugins/public/ui-debugger/index.tsx b/desktop/plugins/public/ui-debugger/index.tsx index dd4b14e12..8c348f19c 100644 --- a/desktop/plugins/public/ui-debugger/index.tsx +++ b/desktop/plugins/public/ui-debugger/index.tsx @@ -24,6 +24,7 @@ import { PerformanceStatsEvent, Snapshot, StreamInterceptorError, + StreamState, SubtreeUpdateEvent, UINode, } from './types'; @@ -31,8 +32,6 @@ import {Draft} from 'immer'; import {QueryClient, setLogger} from 'react-query'; import {tracker} from './tracker'; import {getStreamInterceptor} from './fb-stubs/StreamInterceptor'; -import React from 'react'; -import {StreamInterceptorErrorView} from './components/StreamInterceptorErrorView'; type SnapshotInfo = {nodeId: Id; base64Image: Snapshot}; type LiveClientState = { @@ -40,9 +39,14 @@ type LiveClientState = { nodes: Map; }; +type PendingData = { + metadata: Record; + frame: SubtreeUpdateEvent | null; +}; + type UIState = { isPaused: Atom; - streamInterceptorError: Atom; + streamState: Atom; searchTerm: Atom; isContextMenuOpen: Atom; hoveredNodes: Atom; @@ -71,15 +75,68 @@ export function plugin(client: PluginClient) { }); }); - client.onMessage('metadataUpdate', (event) => { + async function processMetadata( + incomingMetadata: Record, + ) { + const mappedMeta = await Promise.all( + Object.values(incomingMetadata).map((metadata) => + streamInterceptor.transformMetadata(metadata), + ), + ); + + metadata.update((draft) => { + for (const metadata of mappedMeta) { + draft.set(metadata.id, metadata); + } + }); + } + + //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) { + if (error instanceof StreamInterceptorError) { + const retryCallback = async () => { + uiState.streamState.set({state: 'RetryingAfterError'}); + + await processMetadata(pendingData.metadata); + if (pendingData.frame != null) { + await processSubtreeUpdate(pendingData.frame); + } + uiState.streamState.set({state: 'Ok'}); + pendingData.frame = null; + pendingData.metadata = {}; + }; + + uiState.streamState.set({ + state: 'StreamInterceptorRetryableError', + retryCallback: retryCallback, + error: error, + }); + } else { + console.error( + `[ui-debugger] Unexpected Error processing ${source}`, + error, + ); + + uiState.streamState.set({state: 'UnrecoverableError'}); + } + } + + client.onMessage('metadataUpdate', async (event) => { if (!event.attributeMetadata) { return; } - metadata.update((draft) => { - for (const [_key, value] of Object.entries(event.attributeMetadata)) { - draft.set(value.id, value); + + try { + await processMetadata(event.attributeMetadata); + } catch (error) { + for (const metadata of Object.values(event.attributeMetadata)) { + pendingData.metadata[metadata.id] = metadata; } - }); + handleStreamError('Metadata', error); + } }); const perfEvents = createDataSource([], { @@ -124,7 +181,7 @@ export function plugin(client: PluginClient) { //used to disabled hover effects which cause rerenders and mess up the existing context menu isContextMenuOpen: createState(false), - streamInterceptorError: createState(undefined), + streamState: createState({state: 'Ok'}), visualiserWidth: createState(Math.min(window.innerWidth / 4.5, 500)), highlightedNodes, @@ -171,7 +228,7 @@ export function plugin(client: PluginClient) { }; const seenNodes = new Set(); - const subTreeUpdateCallBack = async (subtreeUpdate: SubtreeUpdateEvent) => { + const processSubtreeUpdate = async (subtreeUpdate: SubtreeUpdateEvent) => { try { const processedNodes = await streamInterceptor.transformNodes( new Map(subtreeUpdate.nodes.map((node) => [node.id, {...node}])), @@ -182,36 +239,9 @@ export function plugin(client: PluginClient) { }); applyFrameworkEvents(subtreeUpdate); - - uiState.streamInterceptorError.set(undefined); } catch (error) { - if (error instanceof StreamInterceptorError) { - const retryCallback = () => { - uiState.streamInterceptorError.set(undefined); - //wipe the internal state so loading indicator appears - applyFrameData(new Map(), null); - subTreeUpdateCallBack(subtreeUpdate); - }; - uiState.streamInterceptorError.set( - , - ); - } else { - console.error( - `[ui-debugger] Unexpected Error processing frame from ${client.appName}`, - error, - ); - - uiState.streamInterceptorError.set( - , - ); - } + pendingData.frame = subtreeUpdate; + handleStreamError('Frame', error); } }; @@ -258,7 +288,6 @@ export function plugin(client: PluginClient) { } draft.nodes = nodes; - setParentPointers(rootId.get()!!, undefined, draft.nodes); }); uiState.expandedNodes.update((draft) => { @@ -283,7 +312,7 @@ export function plugin(client: PluginClient) { checkFocusedNodeStillActive(uiState, nodesAtom.get()); } } - client.onMessage('subtreeUpdate', subTreeUpdateCallBack); + client.onMessage('subtreeUpdate', processSubtreeUpdate); const queryClient = new QueryClient({}); @@ -302,21 +331,6 @@ export function plugin(client: PluginClient) { }; } -function setParentPointers( - cur: Id, - parent: Id | undefined, - nodes: Map, -) { - const node = nodes.get(cur); - if (node == null) { - return; - } - node.parent = parent; - node.children.forEach((child) => { - setParentPointers(child, cur, nodes); - }); -} - type UIActions = { onHoverNode: (node: Id) => void; onFocusNode: (focused?: Id) => void; diff --git a/desktop/plugins/public/ui-debugger/types.tsx b/desktop/plugins/public/ui-debugger/types.tsx index f46c02aff..447e4fda4 100644 --- a/desktop/plugins/public/ui-debugger/types.tsx +++ b/desktop/plugins/public/ui-debugger/types.tsx @@ -7,6 +7,18 @@ * @format */ +export type StreamState = + | {state: 'Ok'} + | {state: 'RetryingAfterError'} + | { + state: 'StreamInterceptorRetryableError'; + error: StreamInterceptorError; + retryCallback: () => Promise; + } + | { + state: 'UnrecoverableError'; + }; + export type Events = { init: InitEvent; subtreeUpdate: SubtreeUpdateEvent; @@ -32,7 +44,11 @@ export type FrameworkEventMetadata = { documentation: string; }; -type JSON = string | number | boolean | null | JSON[] | {[key: string]: JSON}; +type JsonObject = { + [key: string]: JSON; +}; + +type JSON = string | number | boolean | null | JSON[] | JsonObject; type Stacktrace = {type: 'stacktrace'; stacktrace: string[]}; type Reason = {type: 'reason'; reason: string}; @@ -163,7 +179,14 @@ export type Id = number; export type MetadataId = number; export type TreeState = {expandedNodes: Id[]}; -export type Tag = 'Native' | 'Declarative' | 'Android' | 'Litho' | 'CK' | 'iOS'; +export type Tag = + | 'Native' + | 'Declarative' + | 'Android' + | 'Litho' + | 'CK' + | 'iOS' + | 'BloksBoundTree'; export type Inspectable = | InspectableObject