Files
flipper/desktop/plugins/public/ui-debugger/index.tsx
Luke De Feo 21b4423231 Refactor stream inteceptor
Summary:
There was a frustrating issue previously where the UI would jump everytime a new frame came in with bloks data. This would occur constantly as the client was generating new frames every 10 seconds. The issue was because the previous appraoch to speed up the augmentation was to display the raw client raw and then asynchronously process a new frame with the added derived components.

The issue was the raw client frame has less nodes so we would breifly display this, and because the reduciton trace was cache on the next tick we would show this causing a jump.

Now we have a new solution:

1. We use an event emitter to push frames / metadata into the interceptor
2. The interceptor should always produce 1 frame, this can be the same or augmented, for open source its just the same
3. We use react query to check if we already have the reduction trace cached, if so then we are able to quickly process the whole thing, if not its split into 2 parts.

React query is also nice since we can add a cache time for the reduciton traces which are very large and not useful one your screen is gone. This should help keep memory usage under control a bit.

one other benefit to this approach is i can remove the error handling stuff from the main plugin code, since the blok augmentation is not on the critical path and it can retry internally and push out a new frame whenever.

Reviewed By: antonk52

Differential Revision: D49272152

fbshipit-source-id: e5539d36231a32754e8612014195449b9faafdb1
2023-09-15 04:05:43 -07:00

313 lines
9.4 KiB
TypeScript

/**
* 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 {createDataSource, createState, PluginClient} from 'flipper-plugin';
import {
Events,
FrameScanEvent,
FrameworkEventType,
Id,
Metadata,
MetadataId,
PerformanceStatsEvent,
SnapshotInfo,
ClientNode,
FrameworkEventMetadata,
} from './ClientTypes';
import {
UIState,
NodeSelection,
StreamState,
ReadOnlyUIState,
LiveClientState,
WireFrameMode,
AugmentedFrameworkEvent,
StreamInterceptorEventEmitter,
} from './DesktopTypes';
import EventEmitter from 'eventemitter3';
import {addInterceptors} from './fb-stubs/StreamInterceptor';
import {prefetchSourceFileLocation} from './components/fb-stubs/IDEContextMenu';
import {checkFocusedNodeStillActive} from './plugin/ClientDataUtils';
import {uiActions} from './plugin/uiActions';
import {first} from 'lodash';
import {getNode} from './utils/map';
export function plugin(client: PluginClient<Events>) {
const rootId = createState<Id | undefined>(undefined);
const metadata = createState<Map<MetadataId, Metadata>>(new Map());
const streamInterceptor = new EventEmitter() as StreamInterceptorEventEmitter;
addInterceptors(client.device.os, streamInterceptor);
const snapshot = createState<SnapshotInfo | null>(null);
const nodesAtom = createState<Map<Id, ClientNode>>(new Map());
const frameworkEvents = createDataSource<AugmentedFrameworkEvent>([], {
indices: [['nodeId']],
limit: 10000,
});
const frameworkEventsCustomColumns = createState<Set<string>>(new Set());
const frameworkEventMetadata = createState<
Map<FrameworkEventType, FrameworkEventMetadata>
>(new Map());
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(),
};
let lastProcessedFrameTime = 0;
const _uiActions = uiActions(
uiState,
nodesAtom,
snapshot,
mutableLiveClientData,
);
const perfEvents = createDataSource<PerformanceStatsEvent, 'txId'>([], {
key: 'txId',
limit: 10 * 1024,
});
client.onMessage('init', (event) => {
console.log('[ui-debugger] init');
rootId.set(event.rootId);
uiState.frameworkEventMonitoring.update((draft) => {
event.frameworkEventMetadata?.forEach((frameworkEventMeta) => {
draft.set(frameworkEventMeta.type, false);
});
});
frameworkEventMetadata.update((draft) => {
event.frameworkEventMetadata?.forEach((frameworkEventMeta) => {
draft.set(frameworkEventMeta.type, frameworkEventMeta);
});
});
});
client.onConnect(() => {
uiState.isConnected.set(true);
console.log('[ui-debugger] connected');
});
client.onDisconnect(() => {
uiState.isConnected.set(false);
console.log('[ui-debugger] disconnected');
});
client.onMessage('metadataUpdate', async (event) => {
if (!event.attributeMetadata) {
return;
}
const metadata = Object.values(event.attributeMetadata);
streamInterceptor.emit('metadataReceived', metadata);
});
streamInterceptor.on('metadataUpdated', (updatedMetadata) => {
metadata.update((draft) => {
for (const meta of updatedMetadata) {
draft.set(meta.id, meta);
}
});
});
/**
* The message handling below is a temporary measure for a couple of weeks until
* clients migrate to the newer message/format.
*/
client.onMessage('perfStats', (event) => {
const stat = {
txId: event.txId,
observerType: event.observerType,
nodesCount: event.nodesCount,
start: event.start,
traversalMS: event.traversalComplete - event.start,
snapshotMS: event.snapshotComplete - event.traversalComplete,
queuingMS: event.queuingComplete - event.snapshotComplete,
deferredComputationMS:
event.deferredComputationComplete - event.queuingComplete,
serializationMS:
event.serializationComplete - event.deferredComputationComplete,
socketMS: event.socketComplete - event.serializationComplete,
};
client.logger.track('performance', 'subtreeUpdate', stat, 'ui-debugger');
perfEvents.append(stat);
});
client.onMessage('performanceStats', (event) => {
client.logger.track('performance', 'subtreeUpdate', event, 'ui-debugger');
perfEvents.append(event);
});
const processFrame = async (frameScan: FrameScanEvent) => {
const nodes = new Map(frameScan.nodes.map((node) => [node.id, {...node}]));
streamInterceptor.emit('frameReceived', {
frameTime: frameScan.frameTime,
snapshot: frameScan.snapshot,
nodes: nodes,
});
applyFrameworkEvents(frameScan, nodes);
};
streamInterceptor.on('frameUpdated', (frame) => {
if (frame.frameTime > lastProcessedFrameTime) {
applyFrameData(frame.nodes, frame.snapshot);
lastProcessedFrameTime = frame.frameTime;
const selectedNode = uiState.selectedNode.get();
if (selectedNode != null)
_uiActions.ensureAncestorsExpanded(selectedNode.id);
}
});
function applyFrameworkEvents(
frameScan: FrameScanEvent,
nodes: Map<Id, ClientNode>,
) {
const customColumns = frameworkEventsCustomColumns.get();
for (const frameworkEvent of frameScan.frameworkEvents ?? []) {
for (const key in frameworkEvent.payload) {
customColumns.add(key);
}
const treeRoot = getNode(frameworkEvent.treeId, nodes);
const treeRootFirstChild = getNode(first(treeRoot?.children), nodes);
frameworkEvents.append({
...frameworkEvent,
nodeName: nodes.get(frameworkEvent.nodeId)?.name,
rootComponentName: treeRootFirstChild?.name,
});
}
frameworkEventsCustomColumns.set(customColumns);
if (uiState.isPaused.get() === true) {
return;
}
const monitoredEvents = uiState.frameworkEventMonitoring.get();
const filterMainThread = uiState.filterMainThreadMonitoring.get();
const nodesToHighlight =
frameScan.frameworkEvents
?.filter(
(frameworkEvent) => monitoredEvents.get(frameworkEvent.type) === true,
)
.filter(
(frameworkEvent) =>
filterMainThread === false || frameworkEvent.thread === 'main',
)
.map((event) => event.nodeId) ?? [];
uiState.highlightedNodes.update((draft) => {
for (const node of nodesToHighlight) {
draft.add(node);
}
});
setTimeout(() => {
uiState.highlightedNodes.update((draft) => {
for (const nodeId of nodesToHighlight) {
draft.delete(nodeId);
}
});
}, HighlightTime);
}
function applyFrameData(
nodes: Map<Id, ClientNode>,
snapshotInfo: SnapshotInfo | undefined,
) {
if (snapshotInfo) {
mutableLiveClientData.snapshotInfo = snapshotInfo;
}
mutableLiveClientData.nodes = nodes;
if (!uiState.isPaused.get()) {
nodesAtom.set(mutableLiveClientData.nodes);
snapshot.set(mutableLiveClientData.snapshotInfo);
checkFocusedNodeStillActive(uiState, nodesAtom.get());
}
setTimeout(() => {
//let react render, this can happen async
for (const node of nodes.values()) {
prefetchSourceFileLocation(node);
}
}, 0);
}
client.onMessage('subtreeUpdate', (subtreeUpdate) => {
processFrame({
frameTime: subtreeUpdate.txId,
nodes: subtreeUpdate.nodes,
snapshot: {data: subtreeUpdate.snapshot, nodeId: subtreeUpdate.rootId},
frameworkEvents: subtreeUpdate.frameworkEvents,
});
});
client.onMessage('frameScan', processFrame);
return {
rootId,
uiState: uiState as ReadOnlyUIState,
uiActions: _uiActions,
nodes: nodesAtom,
frameworkEvents,
frameworkEventMetadata,
frameworkEventsCustomColumns,
snapshot,
metadata,
perfEvents,
os: client.device.os,
};
}
const HighlightTime = 300;
export {Component} from './components/main';
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()),
wireFrameMode: createState<WireFrameMode>('All'),
};
}