Files
flipper/src/devices/BaseDevice.tsx
Michel Weststrate d2a2e2ab75 Introduce async message queuing
Summary:
This diff introduces the logic for queueing incoming messages rather then directly processing them you are behind the `flipper_event_queue` GK.

The reason the queue processing is a bit complicated is to make the queue can be processed non-blocking, can be cancelled, and is safe to concurrency issues.
The idea here is that the queue is processed when we switch to a plugin, report it's progress, and abort the process when switching to another plugin without loosing any work.

This diff does not include
[x] updates to the UI (**SO DON"T LAND IN ISOLATION**)
[x] metrics to see the effect

The effect of the changes can be seen when profiling the application, before this change there are very regular CPU spikes (see the small yellow bar on the top):

https://pxl.cl/TQtl

These go away when the events are no longer processed
https://pxl.cl/TQtp

Reviewed By: nikoant

Differential Revision: D19095564

fbshipit-source-id: 0b8c3421acc4a4f240bf2aab5c1743132f69aa6e
2020-01-02 07:14:12 -08:00

180 lines
3.9 KiB
TypeScript

/**
* 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 stream from 'stream';
import {FlipperDevicePlugin} from 'flipper';
import {sortPluginsByName} from '../plugin';
export type LogLevel =
| 'unknown'
| 'verbose'
| 'debug'
| 'info'
| 'warn'
| 'error'
| 'fatal';
export type DeviceLogEntry = {
date: Date;
pid: number;
tid: number;
app?: string;
type: LogLevel;
tag: string;
message: string;
};
export type DeviceShell = {
stdout: stream.Readable;
stderr: stream.Readable;
stdin: stream.Writable;
};
export type DeviceLogListener = (entry: DeviceLogEntry) => void;
export type DeviceType =
| 'emulator'
| 'physical'
| 'archivedEmulator'
| 'archivedPhysical';
export type DeviceExport = {
os: OS;
title: string;
deviceType: DeviceType;
serial: string;
logs: Array<DeviceLogEntry>;
};
export type OS = 'iOS' | 'Android' | 'Windows' | 'MacOS' | 'JSWebApp';
export default class BaseDevice {
constructor(serial: string, deviceType: DeviceType, title: string, os: OS) {
this.serial = serial;
this.title = title;
this.deviceType = deviceType;
this.os = os;
}
// operating system of this device
os: OS;
// human readable name for this device
title: string;
// type of this device
deviceType: DeviceType;
// serial number for this device
serial: string;
// possible src of icon to display next to the device title
icon: string | null | undefined;
logListeners: Map<Symbol, DeviceLogListener> = new Map();
logEntries: Array<DeviceLogEntry> = [];
isArchived: boolean = false;
// if imported, stores the original source location
source = '';
// sorted list of supported device plugins
devicePlugins!: string[];
supportsOS(os: OS) {
return os.toLowerCase() === this.os.toLowerCase();
}
toJSON(): DeviceExport {
return {
os: this.os,
title: this.title,
deviceType: this.deviceType,
serial: this.serial,
logs: this.getLogs(),
};
}
teardown() {}
supportedColumns(): Array<string> {
return ['date', 'pid', 'tid', 'tag', 'message', 'type', 'time'];
}
addLogListener(callback: DeviceLogListener): Symbol {
const id = Symbol();
this.logListeners.set(id, callback);
return id;
}
_notifyLogListeners(entry: DeviceLogEntry) {
if (this.logListeners.size > 0) {
this.logListeners.forEach(listener => {
// prevent breaking other listeners, if one listener doesn't work.
try {
listener(entry);
} catch (e) {
console.error(`Log listener exception:`, e);
}
});
}
}
addLogEntry(entry: DeviceLogEntry) {
this.logEntries.push(entry);
this._notifyLogListeners(entry);
}
getLogs() {
return this.logEntries;
}
clearLogs(): Promise<void> {
// Only for device types that allow clearing.
this.logEntries = [];
return Promise.resolve();
}
removeLogListener(id: Symbol) {
this.logListeners.delete(id);
}
navigateToLocation(_location: string) {
throw new Error('unimplemented');
}
archive(): any | null | undefined {
return null;
}
screenshot(): Promise<Buffer> {
return Promise.reject(
new Error('No screenshot support for current device'),
);
}
async screenCaptureAvailable(): Promise<boolean> {
return false;
}
async startScreenCapture(_destination: string) {
throw new Error('startScreenCapture not implemented on BaseDevice ');
}
async stopScreenCapture(): Promise<string | null> {
return null;
}
loadDevicePlugins(devicePlugins?: Map<string, typeof FlipperDevicePlugin>) {
this.devicePlugins = Array.from(devicePlugins ? devicePlugins.values() : [])
.filter(plugin => plugin.supportsDevice(this))
.sort(sortPluginsByName)
.map(plugin => plugin.id);
}
}