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
This commit is contained in:
Luke De Feo
2023-04-27 07:28:41 -07:00
committed by Facebook GitHub Bot
parent fd673d0535
commit c96535e15f
3 changed files with 117 additions and 66 deletions

View File

@@ -27,13 +27,12 @@ import {Controls} from './Controls';
import {Button, Spin} from 'antd'; import {Button, Spin} from 'antd';
import {QueryClientProvider} from 'react-query'; import {QueryClientProvider} from 'react-query';
import {Tree2} from './Tree'; import {Tree2} from './Tree';
import {StreamInterceptorErrorView} from './StreamInterceptorErrorView';
export function Component() { export function Component() {
const instance = usePlugin(plugin); const instance = usePlugin(plugin);
const rootId = useValue(instance.rootId); const rootId = useValue(instance.rootId);
const streamInterceptorError = useValue( const streamState = useValue(instance.uiState.streamState);
instance.uiState.streamInterceptorError,
);
const visualiserWidth = useValue(instance.uiState.visualiserWidth); const visualiserWidth = useValue(instance.uiState.visualiserWidth);
const nodes: Map<Id, UINode> = useValue(instance.nodes); const nodes: Map<Id, UINode> = useValue(instance.nodes);
const metadata: Map<MetadataId, Metadata> = useValue(instance.metadata); const metadata: Map<MetadataId, Metadata> = useValue(instance.metadata);
@@ -54,13 +53,28 @@ export function Component() {
setBottomPanelComponent(undefined); setBottomPanelComponent(undefined);
}; };
if (showPerfStats) return <PerfStats events={instance.perfEvents} />; if (streamState.state === 'UnrecoverableError') {
return (
if (streamInterceptorError != null) { <StreamInterceptorErrorView
return streamInterceptorError; title="Oops"
message="Something has gone horribly wrong, we are aware of this and are looking into it"
/>
);
} }
if (rootId == null || nodes.size == 0) { if (streamState.state === 'StreamInterceptorRetryableError') {
return (
<StreamInterceptorErrorView
message={streamState.error.message}
title={streamState.error.title}
retryCallback={streamState.retryCallback}
/>
);
}
if (showPerfStats) return <PerfStats events={instance.perfEvents} />;
if (rootId == null || streamState.state === 'RetryingAfterError') {
return ( return (
<Centered> <Centered>
<Spin data-testid="loading-indicator" /> <Spin data-testid="loading-indicator" />

View File

@@ -24,6 +24,7 @@ import {
PerformanceStatsEvent, PerformanceStatsEvent,
Snapshot, Snapshot,
StreamInterceptorError, StreamInterceptorError,
StreamState,
SubtreeUpdateEvent, SubtreeUpdateEvent,
UINode, UINode,
} from './types'; } from './types';
@@ -31,8 +32,6 @@ import {Draft} from 'immer';
import {QueryClient, setLogger} from 'react-query'; import {QueryClient, setLogger} from 'react-query';
import {tracker} from './tracker'; import {tracker} from './tracker';
import {getStreamInterceptor} from './fb-stubs/StreamInterceptor'; import {getStreamInterceptor} from './fb-stubs/StreamInterceptor';
import React from 'react';
import {StreamInterceptorErrorView} from './components/StreamInterceptorErrorView';
type SnapshotInfo = {nodeId: Id; base64Image: Snapshot}; type SnapshotInfo = {nodeId: Id; base64Image: Snapshot};
type LiveClientState = { type LiveClientState = {
@@ -40,9 +39,14 @@ type LiveClientState = {
nodes: Map<Id, UINode>; nodes: Map<Id, UINode>;
}; };
type PendingData = {
metadata: Record<MetadataId, Metadata>;
frame: SubtreeUpdateEvent | null;
};
type UIState = { type UIState = {
isPaused: Atom<boolean>; isPaused: Atom<boolean>;
streamInterceptorError: Atom<React.ReactNode | undefined>; streamState: Atom<StreamState>;
searchTerm: Atom<string>; searchTerm: Atom<string>;
isContextMenuOpen: Atom<boolean>; isContextMenuOpen: Atom<boolean>;
hoveredNodes: Atom<Id[]>; hoveredNodes: Atom<Id[]>;
@@ -71,15 +75,68 @@ export function plugin(client: PluginClient<Events>) {
}); });
}); });
client.onMessage('metadataUpdate', (event) => { async function processMetadata(
incomingMetadata: Record<MetadataId, Metadata>,
) {
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) { if (!event.attributeMetadata) {
return; return;
} }
metadata.update((draft) => {
for (const [_key, value] of Object.entries(event.attributeMetadata)) { try {
draft.set(value.id, value); 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<PerformanceStatsEvent, 'txId'>([], { const perfEvents = createDataSource<PerformanceStatsEvent, 'txId'>([], {
@@ -124,7 +181,7 @@ export function plugin(client: PluginClient<Events>) {
//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),
streamInterceptorError: createState<React.ReactNode | undefined>(undefined), streamState: createState<StreamState>({state: 'Ok'}),
visualiserWidth: createState(Math.min(window.innerWidth / 4.5, 500)), visualiserWidth: createState(Math.min(window.innerWidth / 4.5, 500)),
highlightedNodes, highlightedNodes,
@@ -171,7 +228,7 @@ export function plugin(client: PluginClient<Events>) {
}; };
const seenNodes = new Set<Id>(); const seenNodes = new Set<Id>();
const subTreeUpdateCallBack = async (subtreeUpdate: SubtreeUpdateEvent) => { const processSubtreeUpdate = async (subtreeUpdate: SubtreeUpdateEvent) => {
try { try {
const processedNodes = await streamInterceptor.transformNodes( const processedNodes = await streamInterceptor.transformNodes(
new Map(subtreeUpdate.nodes.map((node) => [node.id, {...node}])), new Map(subtreeUpdate.nodes.map((node) => [node.id, {...node}])),
@@ -182,36 +239,9 @@ export function plugin(client: PluginClient<Events>) {
}); });
applyFrameworkEvents(subtreeUpdate); applyFrameworkEvents(subtreeUpdate);
uiState.streamInterceptorError.set(undefined);
} catch (error) { } catch (error) {
if (error instanceof StreamInterceptorError) { pendingData.frame = subtreeUpdate;
const retryCallback = () => { handleStreamError('Frame', error);
uiState.streamInterceptorError.set(undefined);
//wipe the internal state so loading indicator appears
applyFrameData(new Map(), null);
subTreeUpdateCallBack(subtreeUpdate);
};
uiState.streamInterceptorError.set(
<StreamInterceptorErrorView
message={error.message}
title={error.title}
retryCallback={retryCallback}
/>,
);
} else {
console.error(
`[ui-debugger] Unexpected Error processing frame from ${client.appName}`,
error,
);
uiState.streamInterceptorError.set(
<StreamInterceptorErrorView
message="Something has gone horribly wrong, we are aware of this and are looking into it"
title="Oops"
/>,
);
}
} }
}; };
@@ -258,7 +288,6 @@ export function plugin(client: PluginClient<Events>) {
} }
draft.nodes = nodes; draft.nodes = nodes;
setParentPointers(rootId.get()!!, undefined, draft.nodes);
}); });
uiState.expandedNodes.update((draft) => { uiState.expandedNodes.update((draft) => {
@@ -283,7 +312,7 @@ export function plugin(client: PluginClient<Events>) {
checkFocusedNodeStillActive(uiState, nodesAtom.get()); checkFocusedNodeStillActive(uiState, nodesAtom.get());
} }
} }
client.onMessage('subtreeUpdate', subTreeUpdateCallBack); client.onMessage('subtreeUpdate', processSubtreeUpdate);
const queryClient = new QueryClient({}); const queryClient = new QueryClient({});
@@ -302,21 +331,6 @@ export function plugin(client: PluginClient<Events>) {
}; };
} }
function setParentPointers(
cur: Id,
parent: Id | undefined,
nodes: Map<Id, UINode>,
) {
const node = nodes.get(cur);
if (node == null) {
return;
}
node.parent = parent;
node.children.forEach((child) => {
setParentPointers(child, cur, nodes);
});
}
type UIActions = { type UIActions = {
onHoverNode: (node: Id) => void; onHoverNode: (node: Id) => void;
onFocusNode: (focused?: Id) => void; onFocusNode: (focused?: Id) => void;

View File

@@ -7,6 +7,18 @@
* @format * @format
*/ */
export type StreamState =
| {state: 'Ok'}
| {state: 'RetryingAfterError'}
| {
state: 'StreamInterceptorRetryableError';
error: StreamInterceptorError;
retryCallback: () => Promise<void>;
}
| {
state: 'UnrecoverableError';
};
export type Events = { export type Events = {
init: InitEvent; init: InitEvent;
subtreeUpdate: SubtreeUpdateEvent; subtreeUpdate: SubtreeUpdateEvent;
@@ -32,7 +44,11 @@ export type FrameworkEventMetadata = {
documentation: string; 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 Stacktrace = {type: 'stacktrace'; stacktrace: string[]};
type Reason = {type: 'reason'; reason: string}; type Reason = {type: 'reason'; reason: string};
@@ -163,7 +179,14 @@ export type Id = number;
export type MetadataId = number; export type MetadataId = number;
export type TreeState = {expandedNodes: Id[]}; 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 = export type Inspectable =
| InspectableObject | InspectableObject