/** * Copyright (c) Facebook, Inc. and its 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 {PersistedStateReducer, FlipperDevicePlugin} from '../plugin'; import {State, MiddlewareAPI} from '../reducers/index'; import {setPluginState} from '../reducers/pluginStates'; import {flipperRecorderAddEvent} from './pluginStateRecorder'; import { clearMessageQueue, queueMessage, Message, } from '../reducers/pluginMessageQueue'; import {Idler, BaseIdler} from './Idler'; import {pluginIsStarred, getSelectedPluginKey} from '../reducers/connections'; const MAX_BACKGROUND_TASK_TIME = 25; type StatEntry = { cpuTimeTotal: number; // Total time spend in persisted Reducer cpuTimeDelta: number; // Time spend since previous tracking tick messageCountTotal: number; // amount of message received for this plugin messageCountDelta: number; // amout of messages received since previous tracking tick maxTime: number; // maximum time spend in a single reducer call }; const pluginBackgroundStats = new Map(); export function resetPluginBackgroundStatsDelta() { pluginBackgroundStats.forEach(stat => { stat.cpuTimeDelta = 0; stat.messageCountDelta = 0; }); } export function getPluginBackgroundStats(): { cpuTime: number; // amount of ms cpu used since the last stats (typically every minute) byPlugin: {[plugin: string]: StatEntry}; } { let cpuTime: number = 0; const byPlugin = Array.from(pluginBackgroundStats.entries()).reduce( (aggregated, [pluginName, data]) => { cpuTime += data.cpuTimeDelta; aggregated[pluginName] = data; return aggregated; }, {} as {[plugin: string]: StatEntry}, ); return { cpuTime, byPlugin, }; } if (window) { // @ts-ignore window.flipperPrintPluginBackgroundStats = () => { console.table( Array.from(pluginBackgroundStats.entries()).map( ([ plugin, { cpuTimeDelta, cpuTimeTotal, messageCountDelta, messageCountTotal, maxTime, }, ]) => ({ plugin, cpuTimeTotal, messageCountTotal, cpuTimeDelta, messageCountDelta, maxTime, }), ), ); }; } function addBackgroundStat(plugin: string, cpuTime: number) { if (!pluginBackgroundStats.has(plugin)) { pluginBackgroundStats.set(plugin, { cpuTimeDelta: 0, cpuTimeTotal: 0, messageCountDelta: 0, messageCountTotal: 0, maxTime: 0, }); } const stat = pluginBackgroundStats.get(plugin)!; stat.cpuTimeDelta += cpuTime; stat.cpuTimeTotal += cpuTime; stat.messageCountDelta += 1; stat.messageCountTotal += 1; stat.maxTime = Math.max(stat.maxTime, cpuTime); if (cpuTime > MAX_BACKGROUND_TASK_TIME) { console.warn( `Plugin ${plugin} took too much time while doing background: ${cpuTime}ms. Handling background messages should take less than ${MAX_BACKGROUND_TASK_TIME}ms.`, ); } } function processMessage( state: State, pluginKey: string, plugin: { id: string; persistedStateReducer: PersistedStateReducer | null; }, message: {method: string; params?: any}, ): State { const statName = `${plugin.id}.${message.method}`; const reducerStartTime = Date.now(); flipperRecorderAddEvent(pluginKey, message.method, message.params); try { const newPluginState = plugin.persistedStateReducer!( state, message.method, message.params, ); addBackgroundStat(statName, Date.now() - reducerStartTime); return newPluginState; } catch (e) { console.error(`Failed to process event for plugin ${plugin.id}`, e); return state; } } export function processMessageImmediately( store: MiddlewareAPI, pluginKey: string, plugin: { defaultPersistedState: any; id: string; persistedStateReducer: PersistedStateReducer | null; }, message: {method: string; params?: any}, ) { const persistedState = getCurrentPluginState(store, plugin, pluginKey); const newPluginState = processMessage( persistedState, pluginKey, plugin, message, ); if (persistedState !== newPluginState) { store.dispatch( setPluginState({ pluginKey, state: newPluginState, }), ); } } export function processMessageLater( store: MiddlewareAPI, pluginKey: string, plugin: { defaultPersistedState: any; id: string; persistedStateReducer: PersistedStateReducer | null; maxQueueSize?: number; }, message: {method: string; params?: any}, ) { const isSelected = pluginKey === getSelectedPluginKey(store.getState().connections); switch (true) { case plugin.id === 'Navigation': // Navigation events are always processed, to make sure the navbar stays up to date case isSelected && getPendingMessages(store, pluginKey).length === 0: processMessageImmediately(store, pluginKey, plugin, message); break; case isSelected: case plugin instanceof FlipperDevicePlugin: case pluginIsStarred(store.getState().connections, plugin.id): store.dispatch( queueMessage( pluginKey, message.method, message.params, plugin.maxQueueSize, ), ); break; // In all other cases, messages will be dropped... } } export async function processMessageQueue( plugin: { defaultPersistedState: any; id: string; persistedStateReducer: PersistedStateReducer | null; }, pluginKey: string, store: MiddlewareAPI, progressCallback?: (progress: {current: number; total: number}) => void, idler: BaseIdler = new Idler(), ): Promise { if (!plugin.persistedStateReducer) { return true; } const total = getPendingMessages(store, pluginKey).length; let progress = 0; do { const messages = getPendingMessages(store, pluginKey); if (!messages.length) { break; } // there are messages to process! lets do so until we have to idle const persistedState = getCurrentPluginState(store, plugin, pluginKey); let offset = 0; let newPluginState = persistedState; do { newPluginState = processMessage( newPluginState, pluginKey, plugin, messages[offset], ); offset++; progress++; 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 // resistent to kicking off this process twice; grabbing, processing messages, saving state is done synchronosly // until the idler has to break store.dispatch(clearMessageQueue(pluginKey, offset)); if (newPluginState !== persistedState) { store.dispatch( setPluginState({ pluginKey, state: newPluginState, }), ); } if (idler.isCancelled()) { return false; } await idler.idle(); // new messages might have arrived, so keep looping } while (getPendingMessages(store, pluginKey).length); return true; } function getPendingMessages( store: MiddlewareAPI, pluginKey: string, ): Message[] { return store.getState().pluginMessageQueue[pluginKey] || []; } function getCurrentPluginState( store: MiddlewareAPI, plugin: {defaultPersistedState: any}, pluginKey: string, ) { // possible optimization: don't spread default state here by put proper default state when initializing clients return { ...plugin.defaultPersistedState, ...store.getState().pluginStates[pluginKey], }; }