diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/UIDebuggerFlipperPlugin.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/UIDebuggerFlipperPlugin.kt index 7eb103e32..ecdd2417e 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/UIDebuggerFlipperPlugin.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/UIDebuggerFlipperPlugin.kt @@ -8,22 +8,23 @@ package com.facebook.flipper.plugins.uidebugger import android.app.Application -import android.util.Log import com.facebook.flipper.core.FlipperConnection import com.facebook.flipper.core.FlipperPlugin -import com.facebook.flipper.plugins.uidebugger.core.ApplicationInspector import com.facebook.flipper.plugins.uidebugger.core.ApplicationRef +import com.facebook.flipper.plugins.uidebugger.core.ConnectionRef import com.facebook.flipper.plugins.uidebugger.core.Context +import com.facebook.flipper.plugins.uidebugger.core.NativeScanScheduler import com.facebook.flipper.plugins.uidebugger.model.InitEvent -import com.facebook.flipper.plugins.uidebugger.model.NativeScanEvent +import com.facebook.flipper.plugins.uidebugger.scheduler.Scheduler import kotlinx.serialization.json.Json val LogTag = "FlipperUIDebugger" class UIDebuggerFlipperPlugin(val application: Application) : FlipperPlugin { - private val context: Context = Context(ApplicationRef(application)) - private var connection: FlipperConnection? = null + private val context: Context = Context(ApplicationRef(application), ConnectionRef(null)) + + private val nativeScanScheduler = Scheduler(NativeScanScheduler(context)) override fun getId(): String { return "ui-debugger" @@ -31,33 +32,26 @@ class UIDebuggerFlipperPlugin(val application: Application) : FlipperPlugin { @Throws(Exception::class) override fun onConnect(connection: FlipperConnection) { - this.connection = connection - // temp solution, get from descriptor - val inspector = ApplicationInspector(context) + this.context.connectionRef.connection = connection val rootDescriptor = - inspector.descriptorRegister.descriptorForClassUnsafe(context.applicationRef.javaClass) + context.descriptorRegister.descriptorForClassUnsafe(context.applicationRef.javaClass) + connection.send( InitEvent.name, Json.encodeToString( InitEvent.serializer(), InitEvent(rootDescriptor.getId(context.applicationRef)))) - try { - val nodes = inspector.traversal.traverse() - connection.send( - NativeScanEvent.name, - Json.encodeToString(NativeScanEvent.serializer(), NativeScanEvent(nodes))) - } catch (e: java.lang.Exception) { - Log.e(LogTag, e.message.toString(), e) - } + nativeScanScheduler.start() } @Throws(Exception::class) override fun onDisconnect() { - this.connection = null + this.context.connectionRef.connection = null + this.nativeScanScheduler.stop() } override fun runInBackground(): Boolean { - return true + return false } } diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/Context.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/Context.kt index eac7f2d0b..fb795e4df 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/Context.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/Context.kt @@ -7,4 +7,13 @@ package com.facebook.flipper.plugins.uidebugger.core -class Context(val applicationRef: ApplicationRef) {} +import com.facebook.flipper.core.FlipperConnection +import com.facebook.flipper.plugins.uidebugger.descriptors.DescriptorRegister + +data class Context( + val applicationRef: ApplicationRef, + val connectionRef: ConnectionRef, + val descriptorRegister: DescriptorRegister = DescriptorRegister.withDefaults() +) + +data class ConnectionRef(var connection: FlipperConnection?) diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/NativeScanScheduler.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/NativeScanScheduler.kt new file mode 100644 index 000000000..ca64f169a --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/core/NativeScanScheduler.kt @@ -0,0 +1,72 @@ +/* + * 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. + */ + +package com.facebook.flipper.plugins.uidebugger.core + +import android.os.Looper +import android.util.Log +import com.facebook.flipper.plugins.uidebugger.model.NativeScanEvent +import com.facebook.flipper.plugins.uidebugger.model.Node +import com.facebook.flipper.plugins.uidebugger.model.PerfStatsEvent +import com.facebook.flipper.plugins.uidebugger.scheduler.Scheduler +import kotlinx.serialization.json.Json + +data class ScanResult( + val txId: Long, + val scanStart: Long, + val scanEnd: Long, + val nodes: List +) + +class NativeScanScheduler(val context: Context) : Scheduler.Task { + val traversal = LayoutTraversal(context.descriptorRegister, context.applicationRef) + var txId = 0L + override fun execute(): ScanResult { + + val start = System.currentTimeMillis() + val nodes = traversal.traverse() + val scanEnd = System.currentTimeMillis() + + Log.d( + "LAYOUT_SCHEDULER", + Thread.currentThread().name + + Looper.myLooper() + + ", produced: " + + { + nodes.count() + } + + " nodes") + + return ScanResult(txId++, start, scanEnd, nodes) + } + + override fun process(result: ScanResult) { + + val serialized = + Json.encodeToString( + NativeScanEvent.serializer(), NativeScanEvent(result.txId, result.nodes)) + val serializationEnd = System.currentTimeMillis() + context.connectionRef.connection?.send( + NativeScanEvent.name, + serialized, + ) + + val socketEnd = System.currentTimeMillis() + + context.connectionRef.connection?.send( + PerfStatsEvent.name, + Json.encodeToString( + PerfStatsEvent.serializer(), + PerfStatsEvent( + result.txId, + result.scanStart, + result.scanEnd, + serializationEnd, + socketEnd, + result.nodes.size))) + } +} diff --git a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/model/Events.kt b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/model/Events.kt index 950d5d690..873a7ca8f 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/uidebugger/model/Events.kt +++ b/android/src/main/java/com/facebook/flipper/plugins/uidebugger/model/Events.kt @@ -15,8 +15,23 @@ data class InitEvent(val rootId: String) { } @kotlinx.serialization.Serializable -data class NativeScanEvent(val nodes: List) { +data class NativeScanEvent(val txId: Long, val nodes: List) { companion object { const val name = "nativeScan" } } + +/** Separate optional performance statistics event */ +@kotlinx.serialization.Serializable +data class PerfStatsEvent( + val txId: Long, + val start: Long, + val scanComplete: Long, + val serializationComplete: Long, + val socketComplete: Long, + val nodesCount: Int +) { + companion object { + const val name = "perfStats" + } +} diff --git a/desktop/plugins/public/ui-debugger/components/main.tsx b/desktop/plugins/public/ui-debugger/components/main.tsx index 64e16a255..7e6658398 100644 --- a/desktop/plugins/public/ui-debugger/components/main.tsx +++ b/desktop/plugins/public/ui-debugger/components/main.tsx @@ -7,35 +7,20 @@ * @format */ -import React from 'react'; -import {Id, plugin, UINode} from '../index'; -import {usePlugin, useValue} from 'flipper-plugin'; +import React, {useState} from 'react'; +import {PerfStatsEvent, plugin} from '../index'; +import { + DataTable, + DataTableColumn, + Layout, + usePlugin, + useValue, +} from 'flipper-plugin'; import {Tree} from 'antd'; import type {DataNode} from 'antd/es/tree'; import {DownOutlined} from '@ant-design/icons'; - -// function treeToAntTree(uiNode: UINode): DataNode { -// return { -// key: uiNode.id, -// title: uiNode.name, -// children: uiNode.children ? uiNode.children.map(treeToAntTree) : [], -// }; -// } - -// function treeToMap(uiNode: UINode): Map { -// const result = new Map(); -// -// function treeToMapRec(node: UINode): void { -// result.set(node.id, node); -// for (const child of node.children) { -// treeToMapRec(child); -// } -// } -// -// treeToMapRec(uiNode); -// -// return result; -// } +import {useHotkeys} from 'react-hotkeys-hook'; +import {Id, UINode} from '../types'; function nodesToAntTree(root: Id, nodes: Map): DataNode { function uiNodeToAntNode(id: Id): DataNode { @@ -50,26 +35,84 @@ function nodesToAntTree(root: Id, nodes: Map): DataNode { return uiNodeToAntNode(root); } +function formatDiff(start: number, end: number): string { + const ms = end - start; + return `${ms.toFixed(0)}ms`; +} + +export const columns: DataTableColumn[] = [ + { + key: 'txId', + title: 'TXID', + }, + { + key: 'nodesCount', + title: 'Total nodes', + }, + { + key: 'start', + title: 'Start', + onRender: (row: PerfStatsEvent) => { + console.log(row.start); + return new Date(row.start).toISOString(); + }, + }, + { + key: 'scanComplete', + title: 'Scan time', + onRender: (row: PerfStatsEvent) => { + return formatDiff(row.start, row.scanComplete); + }, + }, + { + key: 'serializationComplete', + title: 'Serialization time', + onRender: (row: PerfStatsEvent) => { + return formatDiff(row.scanComplete, row.serializationComplete); + }, + }, + { + key: 'socketComplete', + title: 'Socket send time', + onRender: (row: PerfStatsEvent) => { + return formatDiff(row.serializationComplete, row.socketComplete); + }, + }, +]; + export function Component() { const instance = usePlugin(plugin); const rootId = useValue(instance.rootId); const nodes = useValue(instance.nodes); + const [showPerfStats, setShowPerfStats] = useState(false); + + useHotkeys('ctrl+i', () => setShowPerfStats((show) => !show)); + + if (showPerfStats) + return ( + + dataSource={instance.perfEvents} + columns={columns} + /> + ); + if (rootId) { const antTree = nodesToAntTree(rootId, nodes); - console.log(antTree); - console.log(rootId); return ( - { - console.log(nodes.get(selected[0] as string)); - }} - defaultExpandAll - switcherIcon={} - treeData={[antTree]} - /> + + { + console.log(nodes.get(selected[0] as string)); + }} + defaultExpandAll + expandedKeys={[...nodes.keys()]} + switcherIcon={} + treeData={[antTree]} + /> + ); } diff --git a/desktop/plugins/public/ui-debugger/index.tsx b/desktop/plugins/public/ui-debugger/index.tsx index 378dd5dd4..9f030384d 100644 --- a/desktop/plugins/public/ui-debugger/index.tsx +++ b/desktop/plugins/public/ui-debugger/index.tsx @@ -7,62 +7,43 @@ * @format */ -import {PluginClient, createState} from 'flipper-plugin'; +import {PluginClient, createState, createDataSource} from 'flipper-plugin'; +import {Id, UINode} from './types'; -export type Inspectable = - | InspectableObject - | InspectableText - | InspectableNumber - | InspectableColor; - -export type InspectableText = { - type: 'text'; - value: string; - mutable: boolean; -}; - -export type InspectableNumber = { - type: 'number'; - value: number; - mutable: boolean; -}; - -export type InspectableColor = { - type: 'number'; - value: number; - mutable: boolean; -}; - -export type InspectableObject = { - type: 'object'; - fields: Record; -}; - -export type Id = string; -export type UINode = { - id: Id; - name: string; - attributes: Record; - children: Id[]; +export type PerfStatsEvent = { + txId: number; + start: number; + scanComplete: number; + serializationComplete: number; + socketComplete: number; + nodesCount: number; }; type Events = { init: {rootId: string}; - nativeScan: {nodes: UINode[]}; + nativeScan: {txId: number; nodes: UINode[]}; + perfStats: PerfStatsEvent; }; export function plugin(client: PluginClient) { const rootId = createState(undefined); - - const nodesAtom = createState>(new Map()); client.onMessage('init', (root) => rootId.set(root.rootId)); + const perfEvents = createDataSource([], { + key: 'txId', + limit: 10 * 1024, + }); + client.onMessage('perfStats', (event) => { + perfEvents.append(event); + }); + + const nodesAtom = createState>(new Map()); client.onMessage('nativeScan', ({nodes}) => { nodesAtom.set(new Map(nodes.map((node) => [node.id, node]))); console.log(nodesAtom.get()); }); - return {rootId, nodes: nodesAtom}; + return {rootId, nodes: nodesAtom, perfEvents}; } export {Component} from './components/main'; diff --git a/desktop/plugins/public/ui-debugger/package.json b/desktop/plugins/public/ui-debugger/package.json index 029302f9a..9ef7a9052 100644 --- a/desktop/plugins/public/ui-debugger/package.json +++ b/desktop/plugins/public/ui-debugger/package.json @@ -12,6 +12,9 @@ "keywords": [ "flipper-plugin" ], + "dependencies": { + "react-hotkeys-hook" : "^3.4.7" + }, "bugs": { "url": "https://github.com/facebook/flipper/issues" }, diff --git a/desktop/plugins/public/ui-debugger/types.tsx b/desktop/plugins/public/ui-debugger/types.tsx new file mode 100644 index 000000000..2997b12f0 --- /dev/null +++ b/desktop/plugins/public/ui-debugger/types.tsx @@ -0,0 +1,45 @@ +/** + * 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 + */ + +export type Inspectable = + | InspectableObject + | InspectableText + | InspectableNumber + | InspectableColor; + +export type InspectableText = { + type: 'text'; + value: string; + mutable: boolean; +}; + +export type InspectableNumber = { + type: 'number'; + value: number; + mutable: boolean; +}; + +export type InspectableColor = { + type: 'number'; + value: number; + mutable: boolean; +}; + +export type InspectableObject = { + type: 'object'; + fields: Record; +}; + +export type Id = string; +export type UINode = { + id: Id; + name: string; + attributes: Record; + children: Id[]; +}; diff --git a/desktop/plugins/public/yarn.lock b/desktop/plugins/public/yarn.lock index d8cddd3b9..9a08df115 100644 --- a/desktop/plugins/public/yarn.lock +++ b/desktop/plugins/public/yarn.lock @@ -1070,6 +1070,11 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: dependencies: react-is "^16.7.0" +hotkeys-js@3.9.4: + version "3.9.4" + resolved "https://registry.yarnpkg.com/hotkeys-js/-/hotkeys-js-3.9.4.tgz#ce1aa4c3a132b6a63a9dd5644fc92b8a9b9cbfb9" + integrity sha512-2zuLt85Ta+gIyvs4N88pCYskNrxf1TFv3LR9t5mdAZIX8BcgQQ48F2opUptvHa6m8zsy5v/a0i9mWzTrlNWU0Q== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1610,6 +1615,13 @@ react-devtools-inline@^4.24.3: source-map-js "^0.6.2" sourcemap-codec "^1.4.8" +react-hotkeys-hook@^3.4.7: + version "3.4.7" + resolved "https://registry.yarnpkg.com/react-hotkeys-hook/-/react-hotkeys-hook-3.4.7.tgz#e16a0a85f59feed9f48d12cfaf166d7df4c96b7a" + integrity sha512-+bbPmhPAl6ns9VkXkNNyxlmCAIyDAcWbB76O4I0ntr3uWCRuIQf/aRLartUahe9chVMPj+OEzzfk3CQSjclUEQ== + dependencies: + hotkeys-js "3.9.4" + react-is@16.10.2: version "16.10.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.10.2.tgz#984120fd4d16800e9a738208ab1fba422d23b5ab"