diff --git a/desktop/plugins/public/ui-debugger/components/StreamInterceptorErrorView.tsx b/desktop/plugins/public/ui-debugger/components/StreamInterceptorErrorView.tsx new file mode 100644 index 000000000..d8ea47e88 --- /dev/null +++ b/desktop/plugins/public/ui-debugger/components/StreamInterceptorErrorView.tsx @@ -0,0 +1,36 @@ +/** + * 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 {Button, Result} from 'antd'; +import * as React from 'react'; + +export function StreamInterceptorErrorView({ + retryCallback, + title, + message, +}: { + title: string; + message: string; + retryCallback?: () => void; +}): React.ReactElement { + return ( + + Retry + + ) + } + /> + ); +} diff --git a/desktop/plugins/public/ui-debugger/components/main.tsx b/desktop/plugins/public/ui-debugger/components/main.tsx index 445c7072b..ec64995bd 100644 --- a/desktop/plugins/public/ui-debugger/components/main.tsx +++ b/desktop/plugins/public/ui-debugger/components/main.tsx @@ -31,6 +31,9 @@ import {Tree2} from './Tree'; export function Component() { const instance = usePlugin(plugin); const rootId = useValue(instance.rootId); + const streamInterceptorError = useValue( + instance.uiState.streamInterceptorError, + ); const visualiserWidth = useValue(instance.uiState.visualiserWidth); const nodes: Map = useValue(instance.nodes); const metadata: Map = useValue(instance.metadata); @@ -53,7 +56,17 @@ export function Component() { if (showPerfStats) return ; - if (rootId) { + if (streamInterceptorError != null) { + return streamInterceptorError; + } + + if (rootId == null || nodes.size == 0) { + return ( + + + + ); + } else { return ( @@ -98,12 +111,6 @@ export function Component() { ); } - - return ( - - - - ); } export function Centered(props: {children: React.ReactNode}) { diff --git a/desktop/plugins/public/ui-debugger/fb-stubs/StreamInterceptor.tsx b/desktop/plugins/public/ui-debugger/fb-stubs/StreamInterceptor.tsx new file mode 100644 index 000000000..f3a2d2f17 --- /dev/null +++ b/desktop/plugins/public/ui-debugger/fb-stubs/StreamInterceptor.tsx @@ -0,0 +1,28 @@ +/** + * 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 {Id, Metadata, StreamInterceptor, UINode} from '../types'; + +export function getStreamInterceptor(): StreamInterceptor { + return new NoOpStreamInterceptor(); +} + +class NoOpStreamInterceptor implements StreamInterceptor { + init() { + return null; + } + + async transformNodes(nodes: Map): Promise> { + return nodes; + } + + async transformMetadata(metadata: Metadata): Promise { + return metadata; + } +} diff --git a/desktop/plugins/public/ui-debugger/index.tsx b/desktop/plugins/public/ui-debugger/index.tsx index 72f51a18f..dd4b14e12 100644 --- a/desktop/plugins/public/ui-debugger/index.tsx +++ b/desktop/plugins/public/ui-debugger/index.tsx @@ -16,18 +16,23 @@ import { } from 'flipper-plugin'; import { Events, - Id, FrameworkEvent, FrameworkEventType, + Id, Metadata, MetadataId, PerformanceStatsEvent, Snapshot, + StreamInterceptorError, + SubtreeUpdateEvent, UINode, } from './types'; 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 = { @@ -37,6 +42,7 @@ type LiveClientState = { type UIState = { isPaused: Atom; + streamInterceptorError: Atom; searchTerm: Atom; isContextMenuOpen: Atom; hoveredNodes: Atom; @@ -50,7 +56,9 @@ type UIState = { export function plugin(client: PluginClient) { const rootId = createState(undefined); + const metadata = createState>(new Map()); + const streamInterceptor = getStreamInterceptor(); const device = client.device.os; @@ -106,7 +114,7 @@ export function plugin(client: PluginClient) { perfEvents.append(event); }); - const nodes = createState>(new Map()); + const nodesAtom = createState>(new Map()); const frameworkEvents = createState>(new Map()); const highlightedNodes = createState(new Set()); @@ -116,6 +124,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), visualiserWidth: createState(Math.min(window.innerWidth / 4.5, 500)), highlightedNodes, @@ -148,9 +157,9 @@ export function plugin(client: PluginClient) { collapseinActiveChildren(node, draft); }); }); - nodes.set(liveClientData.nodes); + nodesAtom.set(liveClientData.nodes); snapshot.set(liveClientData.snapshotInfo); - checkFocusedNodeStillActive(uiState, nodes.get()); + checkFocusedNodeStillActive(uiState, nodesAtom.get()); } }; @@ -162,7 +171,51 @@ export function plugin(client: PluginClient) { }; const seenNodes = new Set(); - client.onMessage('subtreeUpdate', (subtreeUpdate) => { + const subTreeUpdateCallBack = async (subtreeUpdate: SubtreeUpdateEvent) => { + try { + const processedNodes = await streamInterceptor.transformNodes( + new Map(subtreeUpdate.nodes.map((node) => [node.id, {...node}])), + ); + applyFrameData(processedNodes, { + nodeId: subtreeUpdate.rootId, + base64Image: subtreeUpdate.snapshot, + }); + + 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( + , + ); + } + } + }; + + function applyFrameworkEvents(subtreeUpdate: SubtreeUpdateEvent) { frameworkEvents.update((draft) => { if (subtreeUpdate.frameworkEvents) { subtreeUpdate.frameworkEvents.forEach((frameworkEvent) => { @@ -192,23 +245,24 @@ export function plugin(client: PluginClient) { }, HighlightTime); } }); + } + //todo deal with racecondition, where bloks screen is fetching, takes time then you go back get more recent frame then bloks screen comes and overrites it + function applyFrameData( + nodes: Map, + snapshotInfo: SnapshotInfo | null, + ) { liveClientData = produce(liveClientData, (draft) => { - if (subtreeUpdate.snapshot) { - draft.snapshotInfo = { - nodeId: subtreeUpdate.rootId, - base64Image: subtreeUpdate.snapshot, - }; + if (snapshotInfo) { + draft.snapshotInfo = snapshotInfo; } - subtreeUpdate.nodes.forEach((node) => { - draft.nodes.set(node.id, {...node}); - }); + draft.nodes = nodes; setParentPointers(rootId.get()!!, undefined, draft.nodes); }); uiState.expandedNodes.update((draft) => { - for (const node of subtreeUpdate.nodes) { + for (const node of nodes.values()) { if (!seenNodes.has(node.id)) { draft.add(node.id); } @@ -223,20 +277,21 @@ export function plugin(client: PluginClient) { }); if (!uiState.isPaused.get()) { - nodes.set(liveClientData.nodes); + nodesAtom.set(liveClientData.nodes); snapshot.set(liveClientData.snapshotInfo); - checkFocusedNodeStillActive(uiState, nodes.get()); + checkFocusedNodeStillActive(uiState, nodesAtom.get()); } - }); + } + client.onMessage('subtreeUpdate', subTreeUpdateCallBack); const queryClient = new QueryClient({}); return { rootId, uiState, - uiActions: uiActions(uiState, nodes), - nodes, + uiActions: uiActions(uiState, nodesAtom), + nodes: nodesAtom, frameworkEvents, snapshot, metadata, @@ -391,6 +446,7 @@ function collapseinActiveChildren(node: UINode, expandedNodes: Draft>) { const HighlightTime = 300; export {Component} from './components/main'; +export * from './types'; setLogger({ log: (...args) => { diff --git a/desktop/plugins/public/ui-debugger/types.tsx b/desktop/plugins/public/ui-debugger/types.tsx index 019ae9e3e..f46c02aff 100644 --- a/desktop/plugins/public/ui-debugger/types.tsx +++ b/desktop/plugins/public/ui-debugger/types.tsx @@ -15,6 +15,8 @@ export type Events = { metadataUpdate: UpdateMetadataEvent; }; +export type StreamFlowState = {paused: boolean}; + export type SubtreeUpdateEvent = { txId: number; rootId: Id; @@ -242,3 +244,18 @@ export type InspectableUnknown = { type: 'unknown'; value: string; }; + +export interface StreamInterceptor { + transformNodes(nodes: Map): Promise>; + + transformMetadata(metadata: Metadata): Promise; +} + +export class StreamInterceptorError extends Error { + title: string; + + constructor(title: string, message: string) { + super(message); + this.title = title; + } +}