batch for more efficient message processing
Summary: `unstablebatched_updates` should be used whenever a non-react originating event might affect multiple components, to make sure that React batches them optimally. Applied it to the most import events that handle incoming device events Reviewed By: nikoant Differential Revision: D25052937 fbshipit-source-id: b2c783fb9c43be371553db39969280f9d7c3e260
This commit is contained in:
committed by
Facebook GitHub Bot
parent
375a612dff
commit
2e5b52d247
@@ -391,111 +391,113 @@ export default class Client extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let rawData;
|
batch(() => {
|
||||||
try {
|
let rawData;
|
||||||
rawData = JSON.parse(msg);
|
try {
|
||||||
} catch (err) {
|
rawData = JSON.parse(msg);
|
||||||
console.error(`Invalid JSON: ${msg}`, 'clientMessage');
|
} catch (err) {
|
||||||
return;
|
console.error(`Invalid JSON: ${msg}`, 'clientMessage');
|
||||||
}
|
|
||||||
|
|
||||||
const data: {
|
|
||||||
id?: number;
|
|
||||||
method?: string;
|
|
||||||
params?: Params;
|
|
||||||
success?: Object;
|
|
||||||
error?: ErrorType;
|
|
||||||
} = rawData;
|
|
||||||
|
|
||||||
const {id, method} = data;
|
|
||||||
|
|
||||||
if (
|
|
||||||
data.params?.api != 'flipper-messages' &&
|
|
||||||
flipperMessagesClientPlugin.isConnected()
|
|
||||||
) {
|
|
||||||
flipperMessagesClientPlugin.newMessage({
|
|
||||||
device: this.deviceSync?.displayTitle(),
|
|
||||||
app: this.query.app,
|
|
||||||
flipperInternalMethod: method,
|
|
||||||
plugin: data.params?.api,
|
|
||||||
pluginMethod: data.params?.method,
|
|
||||||
payload: data.params?.params,
|
|
||||||
direction: 'toFlipper:message',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id == null) {
|
|
||||||
const {error} = data;
|
|
||||||
if (error != null) {
|
|
||||||
console.error(
|
|
||||||
`Error received from device ${
|
|
||||||
method ? `when calling ${method}` : ''
|
|
||||||
}: ${error.message} + \nDevice Stack Trace: ${error.stacktrace}`,
|
|
||||||
'deviceError',
|
|
||||||
);
|
|
||||||
handleError(this.store, this.deviceSync, error);
|
|
||||||
} else if (method === 'refreshPlugins') {
|
|
||||||
this.refreshPlugins();
|
|
||||||
} else if (method === 'execute') {
|
|
||||||
invariant(data.params, 'expected params');
|
|
||||||
const params: Params = data.params;
|
|
||||||
const bytes = msg.length * 2; // string lengths are measured in UTF-16 units (not characters), so 2 bytes per char
|
|
||||||
emitBytesReceived(params.api, bytes);
|
|
||||||
|
|
||||||
const persistingPlugin: PluginDefinition | undefined =
|
|
||||||
this.store.getState().plugins.clientPlugins.get(params.api) ||
|
|
||||||
this.store.getState().plugins.devicePlugins.get(params.api);
|
|
||||||
|
|
||||||
let handled = false; // This is just for analysis
|
|
||||||
if (
|
|
||||||
persistingPlugin &&
|
|
||||||
((persistingPlugin as any).persistedStateReducer ||
|
|
||||||
// only send messages to enabled sandy plugins
|
|
||||||
this.sandyPluginStates.has(params.api))
|
|
||||||
) {
|
|
||||||
handled = true;
|
|
||||||
const pluginKey = getPluginKey(
|
|
||||||
this.id,
|
|
||||||
{serial: this.query.device_id},
|
|
||||||
params.api,
|
|
||||||
);
|
|
||||||
if (!this.messageBuffer[pluginKey]) {
|
|
||||||
this.messageBuffer[pluginKey] = {
|
|
||||||
plugin: (this.sandyPluginStates.get(params.api) ??
|
|
||||||
persistingPlugin) as any,
|
|
||||||
messages: [params],
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
this.messageBuffer[pluginKey].messages.push(params);
|
|
||||||
}
|
|
||||||
this.flushMessageBufferDebounced();
|
|
||||||
}
|
|
||||||
const apiCallbacks = this.broadcastCallbacks.get(params.api);
|
|
||||||
if (apiCallbacks) {
|
|
||||||
const methodCallbacks = apiCallbacks.get(params.method);
|
|
||||||
if (methodCallbacks) {
|
|
||||||
for (const callback of methodCallbacks) {
|
|
||||||
handled = true;
|
|
||||||
callback(params.params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!handled) {
|
|
||||||
console.warn(`Unhandled message ${params.api}.${params.method}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return; // method === 'execute'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.sdkVersion < 1) {
|
|
||||||
const callbacks = this.requestCallbacks.get(id);
|
|
||||||
if (!callbacks) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.requestCallbacks.delete(id);
|
|
||||||
this.finishTimingRequestResponse(callbacks.metadata);
|
const data: {
|
||||||
this.onResponse(data, callbacks.resolve, callbacks.reject);
|
id?: number;
|
||||||
}
|
method?: string;
|
||||||
|
params?: Params;
|
||||||
|
success?: Object;
|
||||||
|
error?: ErrorType;
|
||||||
|
} = rawData;
|
||||||
|
|
||||||
|
const {id, method} = data;
|
||||||
|
|
||||||
|
if (
|
||||||
|
data.params?.api != 'flipper-messages' &&
|
||||||
|
flipperMessagesClientPlugin.isConnected()
|
||||||
|
) {
|
||||||
|
flipperMessagesClientPlugin.newMessage({
|
||||||
|
device: this.deviceSync?.displayTitle(),
|
||||||
|
app: this.query.app,
|
||||||
|
flipperInternalMethod: method,
|
||||||
|
plugin: data.params?.api,
|
||||||
|
pluginMethod: data.params?.method,
|
||||||
|
payload: data.params?.params,
|
||||||
|
direction: 'toFlipper:message',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id == null) {
|
||||||
|
const {error} = data;
|
||||||
|
if (error != null) {
|
||||||
|
console.error(
|
||||||
|
`Error received from device ${
|
||||||
|
method ? `when calling ${method}` : ''
|
||||||
|
}: ${error.message} + \nDevice Stack Trace: ${error.stacktrace}`,
|
||||||
|
'deviceError',
|
||||||
|
);
|
||||||
|
handleError(this.store, this.deviceSync, error);
|
||||||
|
} else if (method === 'refreshPlugins') {
|
||||||
|
this.refreshPlugins();
|
||||||
|
} else if (method === 'execute') {
|
||||||
|
invariant(data.params, 'expected params');
|
||||||
|
const params: Params = data.params;
|
||||||
|
const bytes = msg.length * 2; // string lengths are measured in UTF-16 units (not characters), so 2 bytes per char
|
||||||
|
emitBytesReceived(params.api, bytes);
|
||||||
|
|
||||||
|
const persistingPlugin: PluginDefinition | undefined =
|
||||||
|
this.store.getState().plugins.clientPlugins.get(params.api) ||
|
||||||
|
this.store.getState().plugins.devicePlugins.get(params.api);
|
||||||
|
|
||||||
|
let handled = false; // This is just for analysis
|
||||||
|
if (
|
||||||
|
persistingPlugin &&
|
||||||
|
((persistingPlugin as any).persistedStateReducer ||
|
||||||
|
// only send messages to enabled sandy plugins
|
||||||
|
this.sandyPluginStates.has(params.api))
|
||||||
|
) {
|
||||||
|
handled = true;
|
||||||
|
const pluginKey = getPluginKey(
|
||||||
|
this.id,
|
||||||
|
{serial: this.query.device_id},
|
||||||
|
params.api,
|
||||||
|
);
|
||||||
|
if (!this.messageBuffer[pluginKey]) {
|
||||||
|
this.messageBuffer[pluginKey] = {
|
||||||
|
plugin: (this.sandyPluginStates.get(params.api) ??
|
||||||
|
persistingPlugin) as any,
|
||||||
|
messages: [params],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.messageBuffer[pluginKey].messages.push(params);
|
||||||
|
}
|
||||||
|
this.flushMessageBufferDebounced();
|
||||||
|
}
|
||||||
|
const apiCallbacks = this.broadcastCallbacks.get(params.api);
|
||||||
|
if (apiCallbacks) {
|
||||||
|
const methodCallbacks = apiCallbacks.get(params.method);
|
||||||
|
if (methodCallbacks) {
|
||||||
|
for (const callback of methodCallbacks) {
|
||||||
|
handled = true;
|
||||||
|
callback(params.params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!handled) {
|
||||||
|
console.warn(`Unhandled message ${params.api}.${params.method}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return; // method === 'execute'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.sdkVersion < 1) {
|
||||||
|
const callbacks = this.requestCallbacks.get(id);
|
||||||
|
if (!callbacks) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.requestCallbacks.delete(id);
|
||||||
|
this.finishTimingRequestResponse(callbacks.metadata);
|
||||||
|
this.onResponse(data, callbacks.resolve, callbacks.reject);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onResponse(
|
onResponse(
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {Idler, BaseIdler} from './Idler';
|
|||||||
import {pluginIsStarred, getSelectedPluginKey} from '../reducers/connections';
|
import {pluginIsStarred, getSelectedPluginKey} from '../reducers/connections';
|
||||||
import {deconstructPluginKey} from './clientUtils';
|
import {deconstructPluginKey} from './clientUtils';
|
||||||
import {defaultEnabledBackgroundPlugins} from './pluginUtils';
|
import {defaultEnabledBackgroundPlugins} from './pluginUtils';
|
||||||
import {_SandyPluginInstance} from 'flipper-plugin';
|
import {batch, _SandyPluginInstance} from 'flipper-plugin';
|
||||||
import {addBackgroundStat} from './pluginStats';
|
import {addBackgroundStat} from './pluginStats';
|
||||||
|
|
||||||
function processMessageClassic(
|
function processMessageClassic(
|
||||||
@@ -187,39 +187,44 @@ export async function processMessageQueue(
|
|||||||
: getCurrentPluginState(store, plugin, pluginKey);
|
: getCurrentPluginState(store, plugin, pluginKey);
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let newPluginState = persistedState;
|
let newPluginState = persistedState;
|
||||||
do {
|
batch(() => {
|
||||||
if (_SandyPluginInstance.is(plugin)) {
|
do {
|
||||||
// Optimization: we could send a batch of messages here
|
if (_SandyPluginInstance.is(plugin)) {
|
||||||
processMessagesSandy(pluginKey, plugin, [messages[offset]]);
|
// Optimization: we could send a batch of messages here
|
||||||
} else {
|
processMessagesSandy(pluginKey, plugin, [messages[offset]]);
|
||||||
newPluginState = processMessageClassic(
|
} else {
|
||||||
newPluginState,
|
newPluginState = processMessageClassic(
|
||||||
pluginKey,
|
newPluginState,
|
||||||
plugin,
|
pluginKey,
|
||||||
messages[offset],
|
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 (
|
||||||
|
!_SandyPluginInstance.is(plugin) &&
|
||||||
|
newPluginState !== persistedState
|
||||||
|
) {
|
||||||
|
store.dispatch(
|
||||||
|
setPluginState({
|
||||||
|
pluginKey,
|
||||||
|
state: newPluginState,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
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 (!_SandyPluginInstance.is(plugin) && newPluginState !== persistedState) {
|
|
||||||
store.dispatch(
|
|
||||||
setPluginState({
|
|
||||||
pluginKey,
|
|
||||||
state: newPluginState,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (idler.isCancelled()) {
|
if (idler.isCancelled()) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ test('Correct top level API exposed', () => {
|
|||||||
"Layout",
|
"Layout",
|
||||||
"NUX",
|
"NUX",
|
||||||
"TestUtils",
|
"TestUtils",
|
||||||
|
"batch",
|
||||||
"createState",
|
"createState",
|
||||||
"renderReactRoot",
|
"renderReactRoot",
|
||||||
"theme",
|
"theme",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export {
|
|||||||
usePlugin,
|
usePlugin,
|
||||||
} from './plugin/PluginContext';
|
} from './plugin/PluginContext';
|
||||||
export {createState, useValue, Atom} from './state/atom';
|
export {createState, useValue, Atom} from './state/atom';
|
||||||
|
export {batch} from './state/batch';
|
||||||
export {FlipperLib} from './plugin/FlipperLib';
|
export {FlipperLib} from './plugin/FlipperLib';
|
||||||
export {
|
export {
|
||||||
MenuEntry,
|
MenuEntry,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {SandyPluginDefinition} from './SandyPluginDefinition';
|
|||||||
import {BasePluginInstance, BasePluginClient} from './PluginBase';
|
import {BasePluginInstance, BasePluginClient} from './PluginBase';
|
||||||
import {FlipperLib} from './FlipperLib';
|
import {FlipperLib} from './FlipperLib';
|
||||||
import {RealFlipperDevice} from './DevicePlugin';
|
import {RealFlipperDevice} from './DevicePlugin';
|
||||||
|
import {batched} from '../state/batch';
|
||||||
|
|
||||||
type EventsContract = Record<string, any>;
|
type EventsContract = Record<string, any>;
|
||||||
type MethodsContract = Record<string, (params: any) => Promise<any>>;
|
type MethodsContract = Record<string, (params: any) => Promise<any>>;
|
||||||
@@ -146,10 +147,10 @@ export class SandyPluginInstance extends BasePluginInstance {
|
|||||||
return realClient.query.app;
|
return realClient.query.app;
|
||||||
},
|
},
|
||||||
onConnect: (cb) => {
|
onConnect: (cb) => {
|
||||||
this.events.on('connect', cb);
|
this.events.on('connect', batched(cb));
|
||||||
},
|
},
|
||||||
onDisconnect: (cb) => {
|
onDisconnect: (cb) => {
|
||||||
this.events.on('disconnect', cb);
|
this.events.on('disconnect', batched(cb));
|
||||||
},
|
},
|
||||||
send: async (method, params) => {
|
send: async (method, params) => {
|
||||||
this.assertConnected();
|
this.assertConnected();
|
||||||
@@ -160,11 +161,11 @@ export class SandyPluginInstance extends BasePluginInstance {
|
|||||||
params as any,
|
params as any,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onMessage: (event, callback) => {
|
onMessage: (event, cb) => {
|
||||||
this.events.on('event-' + event, callback);
|
this.events.on('event-' + event, batched(cb));
|
||||||
},
|
},
|
||||||
onUnhandledMessage: (callback) => {
|
onUnhandledMessage: (cb) => {
|
||||||
this.events.on('unhandled-event', callback);
|
this.events.on('unhandled-event', batched(cb));
|
||||||
},
|
},
|
||||||
supportsMethod: async (method) => {
|
supportsMethod: async (method) => {
|
||||||
this.assertConnected();
|
this.assertConnected();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {Atom} from '../state/atom';
|
|||||||
import {MenuEntry, NormalizedMenuEntry, normalizeMenuEntry} from './MenuEntry';
|
import {MenuEntry, NormalizedMenuEntry, normalizeMenuEntry} from './MenuEntry';
|
||||||
import {FlipperLib} from './FlipperLib';
|
import {FlipperLib} from './FlipperLib';
|
||||||
import {Device, RealFlipperDevice} from './DevicePlugin';
|
import {Device, RealFlipperDevice} from './DevicePlugin';
|
||||||
|
import {batched} from '../state/batch';
|
||||||
|
|
||||||
export interface BasePluginClient {
|
export interface BasePluginClient {
|
||||||
readonly device: Device;
|
readonly device: Device;
|
||||||
@@ -116,7 +117,7 @@ export abstract class BasePluginInstance {
|
|||||||
// To be called from constructory
|
// To be called from constructory
|
||||||
setCurrentPluginInstance(this);
|
setCurrentPluginInstance(this);
|
||||||
try {
|
try {
|
||||||
this.instanceApi = factory();
|
this.instanceApi = batched(factory)();
|
||||||
} finally {
|
} finally {
|
||||||
this.initialStates = undefined;
|
this.initialStates = undefined;
|
||||||
setCurrentPluginInstance(undefined);
|
setCurrentPluginInstance(undefined);
|
||||||
@@ -127,16 +128,16 @@ export abstract class BasePluginInstance {
|
|||||||
return {
|
return {
|
||||||
device: this.device,
|
device: this.device,
|
||||||
onActivate: (cb) => {
|
onActivate: (cb) => {
|
||||||
this.events.on('activate', cb);
|
this.events.on('activate', batched(cb));
|
||||||
},
|
},
|
||||||
onDeactivate: (cb) => {
|
onDeactivate: (cb) => {
|
||||||
this.events.on('deactivate', cb);
|
this.events.on('deactivate', batched(cb));
|
||||||
},
|
},
|
||||||
onDeepLink: (callback) => {
|
onDeepLink: (cb) => {
|
||||||
this.events.on('deeplink', callback);
|
this.events.on('deeplink', batched(cb));
|
||||||
},
|
},
|
||||||
onDestroy: (cb) => {
|
onDestroy: (cb) => {
|
||||||
this.events.on('destroy', cb);
|
this.events.on('destroy', batched(cb));
|
||||||
},
|
},
|
||||||
addMenuEntry: (...entries) => {
|
addMenuEntry: (...entries) => {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
|
|||||||
24
desktop/flipper-plugin/src/state/batch.tsx
Normal file
24
desktop/flipper-plugin/src/state/batch.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* 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 {unstable_batchedUpdates} from 'react-dom';
|
||||||
|
|
||||||
|
export const batch = unstable_batchedUpdates;
|
||||||
|
|
||||||
|
export function batched<T extends Function>(fn: T): T;
|
||||||
|
export function batched(fn: any) {
|
||||||
|
return function (this: any) {
|
||||||
|
let res: any;
|
||||||
|
batch(() => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
res = fn.apply(this, arguments);
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user