Process and render messages when a plugin is opened

Summary: This introduces the necessary UI changes, to kick off and render event progressing process where needed

Reviewed By: jknoxville

Differential Revision: D19175450

fbshipit-source-id: 61e3e8f59eeebf97eedbe715fa7db320286543e2
This commit is contained in:
Michel Weststrate
2020-01-02 07:12:06 -08:00
committed by Facebook Github Bot
parent d2a2e2ab75
commit 8c8f360572
6 changed files with 212 additions and 70 deletions

View File

@@ -121,7 +121,7 @@ export default class Client extends EventEmitter {
activePlugins: Set<string>; activePlugins: Set<string>;
device: Promise<BaseDevice>; device: Promise<BaseDevice>;
_deviceResolve: (device: BaseDevice) => void = _ => {}; _deviceResolve: (device: BaseDevice) => void = _ => {};
_deviceSet: boolean = false; _deviceSet: false | BaseDevice = false;
logger: Logger; logger: Logger;
lastSeenDeviceList: Array<BaseDevice>; lastSeenDeviceList: Array<BaseDevice>;
broadcastCallbacks: Map<string, Map<string, Set<Function>>>; broadcastCallbacks: Map<string, Map<string, Set<Function>>>;
@@ -166,6 +166,9 @@ export default class Client extends EventEmitter {
: new Promise((resolve, _reject) => { : new Promise((resolve, _reject) => {
this._deviceResolve = resolve; this._deviceResolve = resolve;
}); });
if (device) {
this._deviceSet = device;
}
const client = this; const client = this;
// node.js doesn't support requestIdleCallback // node.js doesn't support requestIdleCallback
@@ -232,7 +235,7 @@ export default class Client extends EventEmitter {
}), }),
'client-setMatchingDevice', 'client-setMatchingDevice',
).then(device => { ).then(device => {
this._deviceSet = true; this._deviceSet = device;
this._deviceResolve(device); this._deviceResolve(device);
}); });
} }
@@ -332,35 +335,38 @@ export default class Client extends EventEmitter {
const params: Params = data.params as Params; const params: Params = data.params as Params;
invariant(params, 'expected params'); invariant(params, 'expected params');
const persistingPlugin: const device = this.getDeviceSync();
| typeof FlipperPlugin if (device) {
| typeof FlipperDevicePlugin const persistingPlugin:
| undefined = | typeof FlipperPlugin
this.store.getState().plugins.clientPlugins.get(params.api) || | typeof FlipperDevicePlugin
this.store.getState().plugins.devicePlugins.get(params.api); | undefined =
this.store.getState().plugins.clientPlugins.get(params.api) ||
this.store.getState().plugins.devicePlugins.get(params.api);
if (persistingPlugin && persistingPlugin.persistedStateReducer) { if (persistingPlugin && persistingPlugin.persistedStateReducer) {
const pluginKey = getPluginKey( const pluginKey = getPluginKey(this.id, device, params.api);
this.id, flipperRecorderAddEvent(pluginKey, params.method, params.params);
this.getDeviceSync(), if (GK.get('flipper_event_queue')) {
params.api, processMessageLater(
); this.store,
flipperRecorderAddEvent(pluginKey, params.method, params.params); pluginKey,
if (GK.get('flipper_event_queue')) { persistingPlugin,
processMessageLater( params,
this.store, );
pluginKey, } else {
persistingPlugin, processMessageImmediately(
params, this.store,
); pluginKey,
} else { persistingPlugin,
processMessageImmediately( params,
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); const apiCallbacks = this.broadcastCallbacks.get(params.api);
if (!apiCallbacks) { if (!apiCallbacks) {
@@ -501,15 +507,8 @@ export default class Client extends EventEmitter {
}); });
} }
getDeviceSync(): BaseDevice { getDeviceSync(): BaseDevice | undefined {
let device: BaseDevice | undefined; return this._deviceSet || undefined;
this.device.then(d => {
device = d;
});
if (!device) {
throw new Error('Device not ready yet');
}
return device!;
} }
startTimingRequestResponse(data: RequestMetadata) { startTimingRequestResponse(data: RequestMetadata) {

View File

@@ -23,14 +23,21 @@ import {
colors, colors,
styled, styled,
ArchivedDevice, ArchivedDevice,
Glyph,
Label,
VBox,
View,
} from 'flipper'; } from 'flipper';
import {StaticView, setStaticView} from './reducers/connections'; import {StaticView, setStaticView} from './reducers/connections';
import React, {PureComponent} from 'react'; import React, {PureComponent} from 'react';
import {connect} from 'react-redux'; import {connect, ReactReduxContext} from 'react-redux';
import {setPluginState} from './reducers/pluginStates'; import {setPluginState} from './reducers/pluginStates';
import {selectPlugin} from './reducers/connections'; import {selectPlugin} from './reducers/connections';
import {State as Store} from './reducers/index'; import {State as Store} from './reducers/index';
import {activateMenuItems} from './MenuBar'; import {activateMenuItems} from './MenuBar';
import {Message} from './reducers/pluginMessageQueue';
import {Idler} from './utils/Idler';
import {processMessageQueue} from './utils/messageQueue';
const Container = styled(FlexColumn)({ const Container = styled(FlexColumn)({
width: 0, width: 0,
@@ -45,6 +52,36 @@ const SidebarContainer = styled(FlexRow)({
overflow: 'scroll', 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 (
<ProgressBarContainer>
<ProgressBarBar progress={progress} />
</ProgressBarContainer>
);
}
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 = { type OwnProps = {
logger: Logger; logger: Logger;
}; };
@@ -57,6 +94,7 @@ type StateFromProps = {
deepLinkPayload: string | null; deepLinkPayload: string | null;
selectedApp: string | null; selectedApp: string | null;
isArchivedDevice: boolean; isArchivedDevice: boolean;
pendingMessages: Message[] | undefined;
}; };
type DispatchFromProps = { type DispatchFromProps = {
@@ -71,7 +109,13 @@ type DispatchFromProps = {
type Props = StateFromProps & DispatchFromProps & OwnProps; type Props = StateFromProps & DispatchFromProps & OwnProps;
class PluginContainer extends PureComponent<Props> { type State = {
progress: {current: number; total: number};
};
class PluginContainer extends PureComponent<Props, State> {
static contextType = ReactReduxContext;
plugin: plugin:
| FlipperPlugin<any, any, any> | FlipperPlugin<any, any, any>
| FlipperDevicePlugin<any, any, any> | FlipperDevicePlugin<any, any, any>
@@ -97,14 +141,102 @@ class PluginContainer extends PureComponent<Props> {
} }
}; };
idler?: Idler;
pluginBeingProcessed: string = '';
state = {progress: {current: 0, total: 0}};
componentWillUnmount() { componentWillUnmount() {
if (this.plugin) { if (this.plugin) {
this.plugin._teardown(); this.plugin._teardown();
this.plugin = null; 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() { 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 (
<View grow>
<Waiting>
<VBox>
<Glyph
name="dashboard"
variant="outline"
size={24}
color={colors.light30}
/>
</VBox>
<VBox>
<Label>
Processing {this.state.progress.total} events for{' '}
{this.props.activePlugin?.id ?? 'plugin'}
</Label>
</VBox>
<VBox>
<ProgressBar
progress={this.state.progress.current / this.state.progress.total}
/>
</VBox>
</Waiting>
</View>
);
}
renderPlugin() {
const { const {
pluginState, pluginState,
setPluginState, setPluginState,
@@ -186,6 +318,7 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
}, },
pluginStates, pluginStates,
plugins: {devicePlugins, clientPlugins}, plugins: {devicePlugins, clientPlugins},
pluginMessageQueue,
}) => { }) => {
let pluginKey = null; let pluginKey = null;
let target = null; let target = null;
@@ -212,6 +345,10 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
? false ? false
: selectedDevice instanceof ArchivedDevice; : selectedDevice instanceof ArchivedDevice;
const pendingMessages = pluginKey
? pluginMessageQueue[pluginKey]
: undefined;
const s: StateFromProps = { const s: StateFromProps = {
pluginState: pluginStates[pluginKey as string], pluginState: pluginStates[pluginKey as string],
activePlugin: activePlugin, activePlugin: activePlugin,
@@ -220,6 +357,7 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
pluginKey, pluginKey,
isArchivedDevice, isArchivedDevice,
selectedApp: selectedApp || null, selectedApp: selectedApp || null,
pendingMessages,
}; };
return s; return s;
}, },

View File

@@ -72,8 +72,7 @@ export async function createMockFlipperWithPlugin(
); );
// yikes // yikes
client._deviceSet = true; client._deviceSet = device;
client.getDeviceSync = () => device;
client.device = { client.device = {
then() { then() {
return device; return device;

View File

@@ -123,7 +123,7 @@ test('queue - events are NOT processed immediately if plugin is NOT selected', a
// process the message // process the message
const pluginKey = getPluginKey(client.id, device, TestPlugin.id); const pluginKey = getPluginKey(client.id, device, TestPlugin.id);
await processMessageQueue(client, TestPlugin, pluginKey, store); await processMessageQueue(TestPlugin, pluginKey, store);
expect(store.getState().pluginStates).toEqual({ expect(store.getState().pluginStates).toEqual({
[pluginKey]: { [pluginKey]: {
count: 3, count: 3,
@@ -163,7 +163,6 @@ test('queue - events processing will be paused', async () => {
const idler = new TestIdler(); const idler = new TestIdler();
const p = processMessageQueue( const p = processMessageQueue(
client,
TestPlugin, TestPlugin,
pluginKey, pluginKey,
store, store,
@@ -224,7 +223,6 @@ test('queue - messages that arrive during processing will be queued', async () =
const idler = new TestIdler(); const idler = new TestIdler();
const p = processMessageQueue( const p = processMessageQueue(
client,
TestPlugin, TestPlugin,
pluginKey, pluginKey,
store, store,
@@ -288,7 +286,6 @@ test('queue - processing can be cancelled', async () => {
const idler = new TestIdler(); const idler = new TestIdler();
const p = processMessageQueue( const p = processMessageQueue(
client,
TestPlugin, TestPlugin,
pluginKey, pluginKey,
store, store,

View File

@@ -63,6 +63,7 @@ const ICONS = {
cross: [16], cross: [16],
checkmark: [16], checkmark: [16],
dashboard: [12], dashboard: [12],
'dashboard-outline': [24],
desktop: [12], desktop: [12],
directions: [12], directions: [12],
download: [16], download: [16],

View File

@@ -17,19 +17,27 @@ import {
Message, Message,
} from '../reducers/pluginMessageQueue'; } from '../reducers/pluginMessageQueue';
import {Idler, BaseIdler} from './Idler'; import {Idler, BaseIdler} from './Idler';
import Client from '../Client';
import {getPluginKey} from './pluginUtils'; import {getPluginKey} from './pluginUtils';
const MAX_BACKGROUND_TASK_TIME = 25; const MAX_BACKGROUND_TASK_TIME = 25;
const pluginBackgroundStats = new Map< type StatEntry = {
string, cpuTime: number; // Total time spend in persisted Reducer
{ messages: number; // amount of message received for this plugin
cpuTime: number; // Total time spend in persisted Reducer maxTime: number; // maximum time spend in a single reducer call
messages: number; // amount of message received for this plugin };
maxTime: number; // maximum time spend in a single reducer call
} const pluginBackgroundStats = new Map<string, StatEntry>();
>();
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) { if (window) {
// @ts-ignore // @ts-ignore
@@ -135,7 +143,7 @@ export function processMessageLater(
// if the plugin is active, and has no queued messaged, process the message immediately // if the plugin is active, and has no queued messaged, process the message immediately
if ( if (
selectedPlugin === pluginKey && selectedPlugin === pluginKey &&
getMessages(store, pluginKey).length === 0 getPendingMessages(store, pluginKey).length === 0
) { ) {
processMessageImmediately(store, pluginKey, plugin, message); processMessageImmediately(store, pluginKey, plugin, message);
} else { } else {
@@ -146,25 +154,26 @@ export function processMessageLater(
} }
export async function processMessageQueue( export async function processMessageQueue(
client: Client,
plugin: { plugin: {
defaultPersistedState: any; defaultPersistedState: any;
name: string; name: string;
persistedStateReducer: PersistedStateReducer; persistedStateReducer: PersistedStateReducer | null;
}, },
pluginKey: string, pluginKey: string,
store: Store, store: Store,
progressCallback?: (progress: string) => void, progressCallback?: (progress: {current: number; total: number}) => void,
idler: BaseIdler = new Idler(), idler: BaseIdler = new Idler(),
) { ) {
const total = getMessages(store, pluginKey).length; if (!plugin.persistedStateReducer) {
return;
}
const total = getPendingMessages(store, pluginKey).length;
let progress = 0; let progress = 0;
do { do {
const messages = getMessages(store, pluginKey); const messages = getPendingMessages(store, pluginKey);
if (!messages.length) { if (!messages.length) {
break; break;
} }
// there are messages to process! lets do so until we have to idle // there are messages to process! lets do so until we have to idle
const persistedState = const persistedState =
store.getState().pluginStates[pluginKey] ?? store.getState().pluginStates[pluginKey] ??
@@ -181,12 +190,10 @@ export async function processMessageQueue(
offset++; offset++;
progress++; progress++;
progressCallback?.( progressCallback?.({
`Processing events ${progress} / ${Math.max( total: Math.max(total, progress),
total, current: progress,
progress, });
)} (${Math.min(100, 100 * (progress / total))}%)`,
);
} while (offset < messages.length && !idler.shouldIdle()); } while (offset < messages.length && !idler.shouldIdle());
// save progress // save progress
// by writing progress away first and then idling, we make sure this logic is // 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()) { if (idler.isCancelled()) {
return; return;
} }
await idler.idle(); await idler.idle();
// new messages might have arrived, so keep looping // 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] || []; return store.getState().pluginMessageQueue[pluginKey] || [];
} }