diff --git a/src/Client.tsx b/src/Client.tsx index 5484e1b57..372e5a8de 100644 --- a/src/Client.tsx +++ b/src/Client.tsx @@ -121,7 +121,7 @@ export default class Client extends EventEmitter { activePlugins: Set; device: Promise; _deviceResolve: (device: BaseDevice) => void = _ => {}; - _deviceSet: boolean = false; + _deviceSet: false | BaseDevice = false; logger: Logger; lastSeenDeviceList: Array; broadcastCallbacks: Map>>; @@ -166,6 +166,9 @@ export default class Client extends EventEmitter { : new Promise((resolve, _reject) => { this._deviceResolve = resolve; }); + if (device) { + this._deviceSet = device; + } const client = this; // node.js doesn't support requestIdleCallback @@ -232,7 +235,7 @@ export default class Client extends EventEmitter { }), 'client-setMatchingDevice', ).then(device => { - this._deviceSet = true; + this._deviceSet = device; this._deviceResolve(device); }); } @@ -332,35 +335,38 @@ export default class Client extends EventEmitter { const params: Params = data.params as Params; invariant(params, 'expected params'); - const persistingPlugin: - | typeof FlipperPlugin - | typeof FlipperDevicePlugin - | undefined = - this.store.getState().plugins.clientPlugins.get(params.api) || - this.store.getState().plugins.devicePlugins.get(params.api); + const device = this.getDeviceSync(); + if (device) { + const persistingPlugin: + | typeof FlipperPlugin + | typeof FlipperDevicePlugin + | undefined = + this.store.getState().plugins.clientPlugins.get(params.api) || + this.store.getState().plugins.devicePlugins.get(params.api); - if (persistingPlugin && persistingPlugin.persistedStateReducer) { - const pluginKey = getPluginKey( - this.id, - this.getDeviceSync(), - params.api, - ); - flipperRecorderAddEvent(pluginKey, params.method, params.params); - if (GK.get('flipper_event_queue')) { - processMessageLater( - this.store, - pluginKey, - persistingPlugin, - params, - ); - } else { - processMessageImmediately( - this.store, - pluginKey, - persistingPlugin, - params, - ); + if (persistingPlugin && persistingPlugin.persistedStateReducer) { + const pluginKey = getPluginKey(this.id, device, params.api); + flipperRecorderAddEvent(pluginKey, params.method, params.params); + if (GK.get('flipper_event_queue')) { + processMessageLater( + this.store, + pluginKey, + persistingPlugin, + params, + ); + } else { + processMessageImmediately( + this.store, + pluginKey, + persistingPlugin, + params, + ); + } } + } else { + console.warn( + `Received a message for plugin ${params.api}.${params.method}, which will be ignored because the device has not connected yet`, + ); } const apiCallbacks = this.broadcastCallbacks.get(params.api); if (!apiCallbacks) { @@ -501,15 +507,8 @@ export default class Client extends EventEmitter { }); } - getDeviceSync(): BaseDevice { - let device: BaseDevice | undefined; - this.device.then(d => { - device = d; - }); - if (!device) { - throw new Error('Device not ready yet'); - } - return device!; + getDeviceSync(): BaseDevice | undefined { + return this._deviceSet || undefined; } startTimingRequestResponse(data: RequestMetadata) { diff --git a/src/PluginContainer.tsx b/src/PluginContainer.tsx index a175c873d..2bd7e5920 100644 --- a/src/PluginContainer.tsx +++ b/src/PluginContainer.tsx @@ -23,14 +23,21 @@ import { colors, styled, ArchivedDevice, + Glyph, + Label, + VBox, + View, } from 'flipper'; import {StaticView, setStaticView} from './reducers/connections'; import React, {PureComponent} from 'react'; -import {connect} from 'react-redux'; +import {connect, ReactReduxContext} from 'react-redux'; import {setPluginState} from './reducers/pluginStates'; import {selectPlugin} from './reducers/connections'; import {State as Store} from './reducers/index'; import {activateMenuItems} from './MenuBar'; +import {Message} from './reducers/pluginMessageQueue'; +import {Idler} from './utils/Idler'; +import {processMessageQueue} from './utils/messageQueue'; const Container = styled(FlexColumn)({ width: 0, @@ -45,6 +52,36 @@ const SidebarContainer = styled(FlexRow)({ overflow: 'scroll', }); +const Waiting = styled(FlexColumn)({ + width: '100%', + height: '100%', + flexGrow: 1, + background: colors.light02, + alignItems: 'center', + justifyContent: 'center', + textAlign: 'center', +}); + +function ProgressBar({progress}: {progress: number}) { + return ( + + + + ); +} + +const ProgressBarContainer = styled.div({ + border: `1px solid ${colors.cyan}`, + borderRadius: 4, + width: 300, +}); + +const ProgressBarBar = styled.div<{progress: number}>(({progress}) => ({ + background: colors.cyan, + width: `${Math.min(100, Math.round(progress * 100))}%`, + height: 8, +})); + type OwnProps = { logger: Logger; }; @@ -57,6 +94,7 @@ type StateFromProps = { deepLinkPayload: string | null; selectedApp: string | null; isArchivedDevice: boolean; + pendingMessages: Message[] | undefined; }; type DispatchFromProps = { @@ -71,7 +109,13 @@ type DispatchFromProps = { type Props = StateFromProps & DispatchFromProps & OwnProps; -class PluginContainer extends PureComponent { +type State = { + progress: {current: number; total: number}; +}; + +class PluginContainer extends PureComponent { + static contextType = ReactReduxContext; + plugin: | FlipperPlugin | FlipperDevicePlugin @@ -97,14 +141,102 @@ class PluginContainer extends PureComponent { } }; + idler?: Idler; + pluginBeingProcessed: string = ''; + + state = {progress: {current: 0, total: 0}}; + componentWillUnmount() { if (this.plugin) { this.plugin._teardown(); this.plugin = null; } + this.cancelCurrentQueue(); + } + + componentDidMount() { + this.processMessageQueue(); + } + + componentDidUpdate() { + this.processMessageQueue(); + } + + processMessageQueue() { + const {pluginKey, pendingMessages, activePlugin} = this.props; + if (pluginKey !== this.pluginBeingProcessed) { + this.pluginBeingProcessed = pluginKey ?? ''; + this.cancelCurrentQueue(); + this.setState({progress: {current: 0, total: 0}}); + if ( + activePlugin && + activePlugin.persistedStateReducer && + pluginKey && + pendingMessages?.length + ) { + // this.setState({progress: {current: 0, total: 0}}); + this.idler = new Idler(); + processMessageQueue( + activePlugin, + pluginKey, + this.context.store, + progress => { + this.setState({progress}); + }, + this.idler, + ); + } + } + } + + cancelCurrentQueue() { + if (this.idler && !this.idler.isCancelled()) { + this.idler.cancel(); + } } render() { + const {activePlugin, pluginKey, target, pendingMessages} = this.props; + if (!activePlugin || !target || !pluginKey) { + console.warn(`No selected plugin. Rendering empty!`); + return null; + } + if (!pendingMessages || pendingMessages.length === 0) { + return this.renderPlugin(); + } else { + return this.renderPluginLoader(); + } + } + + renderPluginLoader() { + return ( + + + + + + + + + + + + + + ); + } + + renderPlugin() { const { pluginState, setPluginState, @@ -186,6 +318,7 @@ export default connect( }, pluginStates, plugins: {devicePlugins, clientPlugins}, + pluginMessageQueue, }) => { let pluginKey = null; let target = null; @@ -212,6 +345,10 @@ export default connect( ? false : selectedDevice instanceof ArchivedDevice; + const pendingMessages = pluginKey + ? pluginMessageQueue[pluginKey] + : undefined; + const s: StateFromProps = { pluginState: pluginStates[pluginKey as string], activePlugin: activePlugin, @@ -220,6 +357,7 @@ export default connect( pluginKey, isArchivedDevice, selectedApp: selectedApp || null, + pendingMessages, }; return s; }, diff --git a/src/test-utils/createMockFlipperWithPlugin.tsx b/src/test-utils/createMockFlipperWithPlugin.tsx index 218cfddd2..bdf971b70 100644 --- a/src/test-utils/createMockFlipperWithPlugin.tsx +++ b/src/test-utils/createMockFlipperWithPlugin.tsx @@ -72,8 +72,7 @@ export async function createMockFlipperWithPlugin( ); // yikes - client._deviceSet = true; - client.getDeviceSync = () => device; + client._deviceSet = device; client.device = { then() { return device; diff --git a/src/utils/__tests__/messageQueue.node.tsx b/src/utils/__tests__/messageQueue.node.tsx index f94251db5..061f73201 100644 --- a/src/utils/__tests__/messageQueue.node.tsx +++ b/src/utils/__tests__/messageQueue.node.tsx @@ -123,7 +123,7 @@ test('queue - events are NOT processed immediately if plugin is NOT selected', a // process the message const pluginKey = getPluginKey(client.id, device, TestPlugin.id); - await processMessageQueue(client, TestPlugin, pluginKey, store); + await processMessageQueue(TestPlugin, pluginKey, store); expect(store.getState().pluginStates).toEqual({ [pluginKey]: { count: 3, @@ -163,7 +163,6 @@ test('queue - events processing will be paused', async () => { const idler = new TestIdler(); const p = processMessageQueue( - client, TestPlugin, pluginKey, store, @@ -224,7 +223,6 @@ test('queue - messages that arrive during processing will be queued', async () = const idler = new TestIdler(); const p = processMessageQueue( - client, TestPlugin, pluginKey, store, @@ -288,7 +286,6 @@ test('queue - processing can be cancelled', async () => { const idler = new TestIdler(); const p = processMessageQueue( - client, TestPlugin, pluginKey, store, diff --git a/src/utils/icons.js b/src/utils/icons.js index 1d424a7be..ffd1ff51a 100644 --- a/src/utils/icons.js +++ b/src/utils/icons.js @@ -63,6 +63,7 @@ const ICONS = { cross: [16], checkmark: [16], dashboard: [12], + 'dashboard-outline': [24], desktop: [12], directions: [12], download: [16], diff --git a/src/utils/messageQueue.tsx b/src/utils/messageQueue.tsx index 23ea09638..5c7c5f142 100644 --- a/src/utils/messageQueue.tsx +++ b/src/utils/messageQueue.tsx @@ -17,19 +17,27 @@ import { Message, } from '../reducers/pluginMessageQueue'; import {Idler, BaseIdler} from './Idler'; -import Client from '../Client'; import {getPluginKey} from './pluginUtils'; const MAX_BACKGROUND_TASK_TIME = 25; -const pluginBackgroundStats = new Map< - string, - { - cpuTime: number; // Total time spend in persisted Reducer - messages: number; // amount of message received for this plugin - maxTime: number; // maximum time spend in a single reducer call - } ->(); +type StatEntry = { + cpuTime: number; // Total time spend in persisted Reducer + messages: number; // amount of message received for this plugin + maxTime: number; // maximum time spend in a single reducer call +}; + +const pluginBackgroundStats = new Map(); + +export function getPluginBackgroundStats(): {[plugin: string]: StatEntry} { + return Array.from(Object.entries(pluginBackgroundStats)).reduce( + (aggregated, [pluginName, data]) => { + aggregated[pluginName] = data; + return aggregated; + }, + {} as {[plugin: string]: StatEntry}, + ); +} if (window) { // @ts-ignore @@ -135,7 +143,7 @@ export function processMessageLater( // if the plugin is active, and has no queued messaged, process the message immediately if ( selectedPlugin === pluginKey && - getMessages(store, pluginKey).length === 0 + getPendingMessages(store, pluginKey).length === 0 ) { processMessageImmediately(store, pluginKey, plugin, message); } else { @@ -146,25 +154,26 @@ export function processMessageLater( } export async function processMessageQueue( - client: Client, plugin: { defaultPersistedState: any; name: string; - persistedStateReducer: PersistedStateReducer; + persistedStateReducer: PersistedStateReducer | null; }, pluginKey: string, store: Store, - progressCallback?: (progress: string) => void, + progressCallback?: (progress: {current: number; total: number}) => void, idler: BaseIdler = new Idler(), ) { - const total = getMessages(store, pluginKey).length; + if (!plugin.persistedStateReducer) { + return; + } + const total = getPendingMessages(store, pluginKey).length; let progress = 0; do { - const messages = getMessages(store, pluginKey); + const messages = getPendingMessages(store, pluginKey); if (!messages.length) { break; } - // there are messages to process! lets do so until we have to idle const persistedState = store.getState().pluginStates[pluginKey] ?? @@ -181,12 +190,10 @@ export async function processMessageQueue( offset++; progress++; - progressCallback?.( - `Processing events ${progress} / ${Math.max( - total, - progress, - )} (${Math.min(100, 100 * (progress / total))}%)`, - ); + progressCallback?.({ + total: Math.max(total, progress), + current: progress, + }); } while (offset < messages.length && !idler.shouldIdle()); // save progress // by writing progress away first and then idling, we make sure this logic is @@ -205,11 +212,12 @@ export async function processMessageQueue( if (idler.isCancelled()) { return; } + await idler.idle(); // new messages might have arrived, so keep looping - } while (getMessages(store, pluginKey).length); + } while (getPendingMessages(store, pluginKey).length); } -function getMessages(store: Store, pluginKey: string): Message[] { +function getPendingMessages(store: Store, pluginKey: string): Message[] { return store.getState().pluginMessageQueue[pluginKey] || []; }