From 8f5fcf9444a38f52cdf39dc944fa111dec1f0e0f Mon Sep 17 00:00:00 2001 From: Lorenzo Blasa Date: Tue, 4 Apr 2023 05:54:42 -0700 Subject: [PATCH] FrameworkEventsInspector Summary: As events get bigger, this change includes the following: - Dedicated event inspector - Stacktrace viewer for events with stacktrace attribution - Stacktrace viewer is displayed within a new BottomPanel. BottomPanel can display any React component and can be reused in the future in different use cases. Reviewed By: LukeDefeo Differential Revision: D44628768 fbshipit-source-id: 71a9ef87e71c9a17f58c2544a1aa356eed14ed27 --- .../public/ui-debugger/components/main.tsx | 115 ++++++++++++++---- .../components/sidebar/Inspector.tsx | 39 ++---- .../inspector/FrameworkEventsInspector.tsx | 76 ++++++++++++ .../sidebar/inspector/StackTraceInspector.tsx | 47 +++++++ 4 files changed, 223 insertions(+), 54 deletions(-) create mode 100644 desktop/plugins/public/ui-debugger/components/sidebar/inspector/FrameworkEventsInspector.tsx create mode 100644 desktop/plugins/public/ui-debugger/components/sidebar/inspector/StackTraceInspector.tsx diff --git a/desktop/plugins/public/ui-debugger/components/main.tsx b/desktop/plugins/public/ui-debugger/components/main.tsx index 1f84bfe2b..7bc0d39a1 100644 --- a/desktop/plugins/public/ui-debugger/components/main.tsx +++ b/desktop/plugins/public/ui-debugger/components/main.tsx @@ -7,7 +7,7 @@ * @format */ -import React, {useState} from 'react'; +import React, {ReactNode, useEffect, useRef, useState} from 'react'; import {plugin} from '../index'; import { DetailSidebar, @@ -23,7 +23,7 @@ import {Visualization2D} from './Visualization2D'; import {useKeyboardModifiers} from '../hooks/useKeyboardModifiers'; import {Inspector} from './sidebar/Inspector'; import {Controls} from './Controls'; -import {Spin} from 'antd'; +import {Button, Spin} from 'antd'; import {QueryClientProvider} from 'react-query'; import {Tree2} from './Tree'; @@ -40,38 +40,59 @@ export function Component() { const {ctrlPressed} = useKeyboardModifiers(); + const [bottomPanelComponent, setBottomPanelComponent] = useState< + ReactNode | undefined + >(); + const openBottomPanelWithContent = (component: ReactNode) => { + setBottomPanelComponent(component); + }; + const dismissBottomPanel = () => { + setBottomPanelComponent(undefined); + }; + if (showPerfStats) return ; if (rootId) { return ( - - - + + <> + + + - { - instance.uiActions.setVisualiserWidth(width); - }} - gutter> - - - - - - - - + maxWidth={800} + onResize={(width) => { + instance.uiActions.setVisualiserWidth(width); + }} + gutter> + + + + + + + + + + + {bottomPanelComponent} + + ); @@ -93,3 +114,45 @@ export function Centered(props: {children: React.ReactNode}) { ); } + +type BottomPanelProps = { + dismiss: () => void; + children: React.ReactNode; +}; +export function BottomPanel({dismiss, children}: BottomPanelProps) { + const bottomPanelRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + bottomPanelRef.current && + !bottomPanelRef.current.contains(event.target) + ) { + dismiss(); + } + }; + // Add event listener when the component is mounted. + document.addEventListener('mousedown', handleClickOutside); + + // Remove event listener when component is unmounted. + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [bottomPanelRef, dismiss]); + + if (!children) { + return <>; + } + return ( +
+ + {children} +
+ +
+
+
+ ); +} diff --git a/desktop/plugins/public/ui-debugger/components/sidebar/Inspector.tsx b/desktop/plugins/public/ui-debugger/components/sidebar/Inspector.tsx index f8d064b7f..57cb0b86b 100644 --- a/desktop/plugins/public/ui-debugger/components/sidebar/Inspector.tsx +++ b/desktop/plugins/public/ui-debugger/components/sidebar/Inspector.tsx @@ -7,33 +7,25 @@ * @format */ -import React from 'react'; +import React, {ReactNode} from 'react'; // eslint-disable-next-line rulesdir/no-restricted-imports-clone import {Glyph} from 'flipper'; -import { - Layout, - Tab, - Tabs, - theme, - usePlugin, - useValue, - TimelineDataDescription, -} from 'flipper-plugin'; - +import {Layout, Tab, Tabs, theme, usePlugin, useValue} from 'flipper-plugin'; import {Id, Metadata, MetadataId, UINode} from '../../types'; - import {IdentityInspector} from './inspector/IdentityInspector'; import {AttributesInspector} from './inspector/AttributesInspector'; import {Tooltip} from 'antd'; import {NoData} from './inspector/NoData'; import {plugin} from '../../index'; +import {FrameworkEventsInspector} from './inspector/FrameworkEventsInspector'; type Props = { nodes: Map; metadata: Map; + showExtra: (element: ReactNode) => void; }; -export const Inspector: React.FC = ({nodes, metadata}) => { +export const Inspector: React.FC = ({nodes, metadata, showExtra}) => { const instance = usePlugin(plugin); const selectedNodeId = useValue(instance.uiState.selectedNode); const frameworkEvents = useValue(instance.frameworkEvents); @@ -43,7 +35,7 @@ export const Inspector: React.FC = ({nodes, metadata}) => { return ; } - const events = selectedNodeId + const selectedFrameworkEvents = selectedNodeId ? frameworkEvents?.get(selectedNodeId) : undefined; @@ -96,7 +88,7 @@ export const Inspector: React.FC = ({nodes, metadata}) => { metadata={metadata} /> - {events && ( + {selectedFrameworkEvents && ( = ({nodes, metadata}) => { }> - { - return { - moment: e.timestamp, - display: e.type.slice(e.type.lastIndexOf(':') + 1), - color: theme.primaryColor, - key: e.timestamp.toString(), - properties: e.payload as any, - }; - }), - current: '', - }} + )} diff --git a/desktop/plugins/public/ui-debugger/components/sidebar/inspector/FrameworkEventsInspector.tsx b/desktop/plugins/public/ui-debugger/components/sidebar/inspector/FrameworkEventsInspector.tsx new file mode 100644 index 000000000..5dce08fe2 --- /dev/null +++ b/desktop/plugins/public/ui-debugger/components/sidebar/inspector/FrameworkEventsInspector.tsx @@ -0,0 +1,76 @@ +/** + * 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} from 'antd'; +import {theme, TimelineDataDescription} from 'flipper-plugin'; +import {FrameworkEvent, UINode} from '../../../types'; +import React, {ReactNode, useState} from 'react'; +import {StackTraceInspector} from './StackTraceInspector'; + +type Props = { + node: UINode; + events: FrameworkEvent[]; + showExtra?: (element: ReactNode) => void; +}; +export const FrameworkEventsInspector: React.FC = ({ + node, + events, + showExtra, +}) => { + const [selectedEvent, setSelectedEvent] = useState( + events[events.length - 1], + ); + + const showStacktrace = () => { + const attribution = selectedEvent.attribution; + if (attribution?.type === 'stacktrace') { + const stacktraceInspector = ( + + ); + showExtra?.(stacktraceInspector); + } + }; + + const hasStacktrace = (event: FrameworkEvent) => { + return event.attribution?.type === 'stacktrace'; + }; + + return ( + <> + { + const idx = parseInt(current, 10); + setSelectedEvent(events[idx]); + }} + timeline={{ + time: events.map((e, idx) => { + return { + moment: e.timestamp, + display: e.type.slice(e.type.lastIndexOf(':') + 1), + color: theme.primaryColor, + key: idx.toString(), + properties: e.payload as any, + }; + }), + current: (events.length - 1).toString(), + }} + /> + {hasStacktrace(selectedEvent) && ( + + )} + + ); +}; diff --git a/desktop/plugins/public/ui-debugger/components/sidebar/inspector/StackTraceInspector.tsx b/desktop/plugins/public/ui-debugger/components/sidebar/inspector/StackTraceInspector.tsx new file mode 100644 index 000000000..ccc07a473 --- /dev/null +++ b/desktop/plugins/public/ui-debugger/components/sidebar/inspector/StackTraceInspector.tsx @@ -0,0 +1,47 @@ +/** + * 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 React from 'react'; +// eslint-disable-next-line rulesdir/no-restricted-imports-clone +import {StackTrace} from 'flipper'; +import {Tag} from '../../../types'; + +const FacebookLibraries = ['Facebook']; +const CKFilter = ['UIDCKAnalyticsListener']; + +const REGEX = + /\d+\s+(?[\s\w\.]+\w)\s+(?
0x\w+?)\s+(?.+) \+ (?\d+)/; + +function isSystemLibrary(libraryName: string | null | undefined): boolean { + return libraryName ? !FacebookLibraries.includes(libraryName) : false; +} + +type Props = { + stacktrace: string[]; + tags: Tag[]; +}; +export const StackTraceInspector: React.FC = ({stacktrace, tags}) => { + const filters = tags.includes('CK') ? CKFilter : []; + return ( + + {stacktrace + ?.filter((line) => filters.every((filter) => !line.includes(filter))) + .map((line) => { + const trace = REGEX.exec(line)?.groups; + return { + bold: !isSystemLibrary(trace?.library), + library: trace?.library, + address: trace?.address, + caller: trace?.caller, + lineNumber: trace?.lineNumber, + }; + }) ?? [{caller: 'No stacktrace available'}]} + + ); +};