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>;
device: Promise<BaseDevice>;
_deviceResolve: (device: BaseDevice) => void = _ => {};
_deviceSet: boolean = false;
_deviceSet: false | BaseDevice = false;
logger: Logger;
lastSeenDeviceList: Array<BaseDevice>;
broadcastCallbacks: Map<string, Map<string, Set<Function>>>;
@@ -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) {

View File

@@ -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 (
<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 = {
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<Props> {
type State = {
progress: {current: number; total: number};
};
class PluginContainer extends PureComponent<Props, State> {
static contextType = ReactReduxContext;
plugin:
| FlipperPlugin<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() {
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 (
<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 {
pluginState,
setPluginState,
@@ -186,6 +318,7 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
},
pluginStates,
plugins: {devicePlugins, clientPlugins},
pluginMessageQueue,
}) => {
let pluginKey = null;
let target = null;
@@ -212,6 +345,10 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
? 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<StateFromProps, DispatchFromProps, OwnProps, Store>(
pluginKey,
isArchivedDevice,
selectedApp: selectedApp || null,
pendingMessages,
};
return s;
},

View File

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

View File

@@ -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,

View File

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

View File

@@ -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<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) {
// @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] || [];
}