Summary: Here I'm changing plugin repository structure to allow re-using of shared packages between both public and fb-internal plugins, and to ensure that public plugins has their own yarn.lock as this will be required to implement reproducible jobs checking plugin compatibility with released flipper versions. Please note that there are a lot of moved files in this diff, make sure to click "Expand all" to see all that actually changed (there are not much of them actually). New proposed structure for plugin packages: ``` - root - node_modules - modules included into Flipper: flipper, flipper-plugin, react, antd, emotion -- plugins --- node_modules - modules used by both public and fb-internal plugins (shared libs will be linked here, see D27034936) --- public ---- node_modules - modules used by public plugins ---- pluginA ----- node_modules - modules used by plugin A exclusively ---- pluginB ----- node_modules - modules used by plugin B exclusively --- fb ---- node_modules - modules used by fb-internal plugins ---- pluginC ----- node_modules - modules used by plugin C exclusively ---- pluginD ----- node_modules - modules used by plugin D exclusively ``` I've moved all public plugins under dir "plugins/public" and excluded them from root yarn workspaces. Instead, they will have their own yarn workspaces config and yarn.lock and they will use flipper modules as peer dependencies. Reviewed By: mweststrate Differential Revision: D27034108 fbshipit-source-id: c2310e3c5bfe7526033f51b46c0ae40199fd7586
444 lines
12 KiB
TypeScript
444 lines
12 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 React from 'react';
|
|
import {FlipperDevicePlugin, Device, KaiOSDevice} from 'flipper';
|
|
|
|
import {
|
|
Button,
|
|
Toolbar,
|
|
ManagedTable,
|
|
Panel,
|
|
Label,
|
|
Input,
|
|
Select,
|
|
} from 'flipper';
|
|
|
|
import {sleep} from 'flipper';
|
|
|
|
import util from 'util';
|
|
import {exec} from 'promisify-child-process';
|
|
|
|
import FirefoxClient from 'firefox-client';
|
|
import BaseClientMethods from 'firefox-client/lib/client-methods';
|
|
import extend from 'firefox-client/lib/extend';
|
|
|
|
// This uses legacy `extend` from `firefox-client`, since this seems to be what the implementation expects
|
|
// It's probably possible to rewrite this in a modern way and properly type it, but for now leaving this as it is
|
|
const ClientMethods: any = extend(BaseClientMethods, {
|
|
initialize: function (client: any, actor: any) {
|
|
this.client = client;
|
|
this.actor = actor;
|
|
|
|
this.cb = function (this: typeof ClientMethods, message: any) {
|
|
if (message.from === this.actor) {
|
|
this.emit(message.type, message);
|
|
}
|
|
}.bind(this);
|
|
|
|
this.client.on('message', this.cb);
|
|
},
|
|
|
|
disconnect: function () {
|
|
this.client.removeListener('message', this.cb);
|
|
},
|
|
});
|
|
|
|
function Memory(this: typeof ClientMethods, client: any, actor: any): any {
|
|
this.initialize(client, actor);
|
|
}
|
|
|
|
// Repetitive, it is probably better to refactor this
|
|
// to use API like `runCommand(commandName, params): Promise`
|
|
Memory.prototype = extend(ClientMethods, {
|
|
attach: function (cb: any) {
|
|
this.request('attach', function (err: any, resp: any) {
|
|
cb(err, resp);
|
|
});
|
|
},
|
|
|
|
getState: function (cb: any) {
|
|
this.request('getState', function (err: any, resp: any) {
|
|
cb(err, resp);
|
|
});
|
|
},
|
|
|
|
takeCensus: function (cb: any) {
|
|
this.request('takeCensus', function (err: any, resp: any) {
|
|
cb(err, resp);
|
|
});
|
|
},
|
|
|
|
getAllocations: function (cb: any) {
|
|
this.request('getAllocations', function (err: any, resp: any) {
|
|
cb(err, resp);
|
|
});
|
|
},
|
|
|
|
startRecordingAllocations: function (options: any, cb: any) {
|
|
this.request(
|
|
'startRecordingAllocations',
|
|
{options},
|
|
function (err: any, resp: any) {
|
|
cb(err, resp);
|
|
},
|
|
);
|
|
},
|
|
|
|
stopRecordingAllocations: function (cb: any) {
|
|
this.request('stopRecordingAllocations', function (err: any, resp: any) {
|
|
cb(err, resp);
|
|
});
|
|
},
|
|
|
|
measure: function (cb: any) {
|
|
this.request('measure', function (err: any, resp: any) {
|
|
cb(err, resp);
|
|
});
|
|
},
|
|
|
|
getAllocationsSettings: function (cb: any) {
|
|
this.request('getAllocationsSettings', function (err: any, resp: any) {
|
|
cb(err, resp);
|
|
});
|
|
},
|
|
});
|
|
|
|
const ffPromisify = (o: {[key: string]: any}, m: string) =>
|
|
util.promisify(o[m].bind(o));
|
|
|
|
const ColumnSizes = {
|
|
timestamp: 'flex',
|
|
freeMem: 'flex',
|
|
};
|
|
|
|
const Columns = {
|
|
timestamp: {
|
|
value: 'time',
|
|
resizable: true,
|
|
},
|
|
allocationSize: {
|
|
value: 'Allocation bytes',
|
|
resizable: true,
|
|
},
|
|
functionName: {
|
|
value: 'Function',
|
|
resizable: true,
|
|
},
|
|
};
|
|
|
|
type Allocation = {
|
|
timestamp: number;
|
|
allocationSize: number;
|
|
functionName: string;
|
|
};
|
|
|
|
type State = {
|
|
apps: {[key: string]: string};
|
|
runningAppName: null | string;
|
|
allocationData: Array<Allocation>;
|
|
totalAllocations: number;
|
|
totalAllocatedBytes: number;
|
|
monitoring: boolean;
|
|
allocationsBySize: {[key: string]: number};
|
|
minAllocationSizeInTable: number;
|
|
};
|
|
|
|
const LOCALSTORAGE_APP_NAME_KEY = '__KAIOS_ALLOCATIONS_PLUGIN_CACHED_APP_NAME';
|
|
const LOCALSTORAGE_MIN_ALLOCATION_SIZE_KEY =
|
|
'__KAIOS_ALLOCATIONS_PLUGIN_MIN_ALLOCATION_SIZE';
|
|
const DEFAULT_MIN_ALLOCATION_SIZE = 128;
|
|
|
|
function getMinAllocationSizeFromLocalStorage(): number {
|
|
const ls = localStorage.getItem(LOCALSTORAGE_MIN_ALLOCATION_SIZE_KEY);
|
|
if (!ls) {
|
|
return DEFAULT_MIN_ALLOCATION_SIZE;
|
|
}
|
|
const parsed = parseInt(ls, 10);
|
|
return !isNaN(parsed) ? parsed : DEFAULT_MIN_ALLOCATION_SIZE;
|
|
}
|
|
|
|
export default class AllocationsPlugin extends FlipperDevicePlugin<
|
|
State,
|
|
any,
|
|
any
|
|
> {
|
|
currentApp: any = null;
|
|
memory: any = null;
|
|
client: any = null;
|
|
webApps: any = null;
|
|
|
|
state: State = {
|
|
apps: {},
|
|
runningAppName: null,
|
|
monitoring: false,
|
|
allocationData: [],
|
|
totalAllocations: 0,
|
|
totalAllocatedBytes: 0,
|
|
allocationsBySize: {},
|
|
minAllocationSizeInTable: getMinAllocationSizeFromLocalStorage(),
|
|
};
|
|
|
|
static supportsDevice(device: Device) {
|
|
return device instanceof KaiOSDevice;
|
|
}
|
|
|
|
onStartMonitor = async () => {
|
|
if (this.state.monitoring) {
|
|
return;
|
|
}
|
|
// TODO: try to reconnect in case of failure
|
|
await ffPromisify(
|
|
this.memory,
|
|
'startRecordingAllocations',
|
|
)({
|
|
probability: 1.0,
|
|
maxLogLength: 20000,
|
|
drainAllocationsTimeout: 1500,
|
|
trackingAllocationSites: true,
|
|
});
|
|
this.setState({monitoring: true});
|
|
};
|
|
|
|
onStopMonitor = async () => {
|
|
if (!this.state.monitoring) {
|
|
return;
|
|
}
|
|
this.tearDownApp();
|
|
this.setState({monitoring: false});
|
|
};
|
|
|
|
// reloads the list of apps every two seconds
|
|
reloadAppListWhenNotMonitoring = async () => {
|
|
while (true) {
|
|
if (!this.state.monitoring) {
|
|
try {
|
|
await this.processListOfApps();
|
|
} catch (e) {
|
|
console.error('Exception, attempting to reconnect', e);
|
|
await this.connectToDebugApi();
|
|
// processing the list of the apps is going to be automatically retried now
|
|
}
|
|
}
|
|
|
|
await sleep(2000);
|
|
}
|
|
};
|
|
|
|
async connectToDebugApi(): Promise<void> {
|
|
this.client = new FirefoxClient({log: false});
|
|
await ffPromisify(this.client, 'connect')(6000, 'localhost');
|
|
this.webApps = await ffPromisify(this.client, 'getWebapps')();
|
|
}
|
|
|
|
async processListOfApps(): Promise<void> {
|
|
const runningAppUrls = await ffPromisify(this.webApps, 'listRunningApps')();
|
|
|
|
const lastUsedAppName = localStorage.getItem(LOCALSTORAGE_APP_NAME_KEY);
|
|
let runningAppName = null;
|
|
const appTitleToUrl: {[key: string]: string} = {};
|
|
for (const runningAppUrl of runningAppUrls) {
|
|
const app = await ffPromisify(this.webApps, 'getApp')(runningAppUrl);
|
|
appTitleToUrl[app.title] = runningAppUrl;
|
|
if (app.title === lastUsedAppName) {
|
|
runningAppName = app.title;
|
|
}
|
|
}
|
|
|
|
if (runningAppName && this.state.runningAppName !== runningAppName) {
|
|
this.setUpApp(appTitleToUrl[runningAppName]);
|
|
}
|
|
|
|
this.setState({
|
|
apps: appTitleToUrl,
|
|
runningAppName,
|
|
});
|
|
}
|
|
|
|
async init() {
|
|
await exec(
|
|
'adb forward tcp:6000 localfilesystem:/data/local/debugger-socket',
|
|
);
|
|
await this.connectToDebugApi();
|
|
await this.processListOfApps();
|
|
// no await because reloading runs in the background
|
|
this.reloadAppListWhenNotMonitoring();
|
|
}
|
|
|
|
async teardown() {
|
|
if (this.state.monitoring) {
|
|
await this.onStopMonitor();
|
|
}
|
|
}
|
|
|
|
async setUpApp(appUrl: string) {
|
|
this.currentApp = await ffPromisify(this.webApps, 'getApp')(appUrl);
|
|
if (!this.currentApp) {
|
|
// TODO: notify user?
|
|
throw new Error('Cannot connect to app');
|
|
}
|
|
|
|
const {
|
|
tab: {memoryActor},
|
|
} = this.currentApp;
|
|
this.memory = new (Memory as any)(this.currentApp.client, memoryActor);
|
|
await ffPromisify(this.memory, 'attach')();
|
|
this.currentApp.client.on('message', this.processAllocationsMsg);
|
|
}
|
|
|
|
async tearDownApp() {
|
|
if (!this.currentApp) {
|
|
return;
|
|
}
|
|
this.currentApp.client.off('message', this.processAllocationsMsg);
|
|
await ffPromisify(this.memory, 'stopRecordingAllocations')();
|
|
this.currentApp = null;
|
|
this.memory = null;
|
|
}
|
|
|
|
processAllocationsMsg = (msg: any) => {
|
|
if (msg.type !== 'allocations') {
|
|
return;
|
|
}
|
|
this.updateAllocations(msg.data);
|
|
};
|
|
|
|
updateAllocations = (data: any) => {
|
|
const {allocations, allocationsTimestamps, allocationSizes, frames} = data;
|
|
const newAllocationData = [...this.state.allocationData];
|
|
let newTotalAllocations = this.state.totalAllocations;
|
|
let newTotalAllocatedBytes = this.state.totalAllocatedBytes;
|
|
const newAllocationsBySize = {...this.state.allocationsBySize};
|
|
for (let i = 0; i < allocations.length; ++i) {
|
|
const frameId = allocations[i];
|
|
const timestamp = allocationsTimestamps[i];
|
|
const allocationSize = allocationSizes[i];
|
|
const functionName = frames[frameId]
|
|
? frames[frameId].functionDisplayName
|
|
: null;
|
|
if (allocationSize >= this.state.minAllocationSizeInTable) {
|
|
newAllocationData.push({timestamp, allocationSize, functionName});
|
|
}
|
|
newAllocationsBySize[allocationSize] =
|
|
(newAllocationsBySize[allocationSize] || 0) + 1;
|
|
newTotalAllocations++;
|
|
newTotalAllocatedBytes += allocationSize;
|
|
}
|
|
this.setState({
|
|
allocationData: newAllocationData,
|
|
totalAllocations: newTotalAllocations,
|
|
totalAllocatedBytes: newTotalAllocatedBytes,
|
|
allocationsBySize: newAllocationsBySize,
|
|
});
|
|
};
|
|
|
|
buildMemRows = () => {
|
|
return this.state.allocationData.map((info) => {
|
|
return {
|
|
columns: {
|
|
timestamp: {
|
|
value: info.timestamp,
|
|
filterValue: info.timestamp,
|
|
},
|
|
allocationSize: {
|
|
value: info.allocationSize,
|
|
filterValue: info.allocationSize,
|
|
},
|
|
functionName: {
|
|
value: info.functionName,
|
|
filterValue: info.functionName,
|
|
},
|
|
},
|
|
key: `${info.timestamp} ${info.allocationSize} ${info.functionName}`,
|
|
copyText: `${info.timestamp} ${info.allocationSize} ${info.functionName}`,
|
|
filterValue: `${info.timestamp} ${info.allocationSize} ${info.functionName}`,
|
|
};
|
|
});
|
|
};
|
|
|
|
onAppChange = (newAppTitle: string) => {
|
|
localStorage[LOCALSTORAGE_APP_NAME_KEY] = newAppTitle;
|
|
this.setState({runningAppName: newAppTitle});
|
|
this.tearDownApp();
|
|
this.setUpApp(this.state.apps[newAppTitle]);
|
|
};
|
|
|
|
onMinAllocationSizeChange = (event: any) => {
|
|
const newMinAllocationSize = event.target.value;
|
|
this.setState({
|
|
minAllocationSizeInTable: newMinAllocationSize,
|
|
});
|
|
};
|
|
|
|
render() {
|
|
const appTitlesForSelect: {[key: string]: string} = {};
|
|
|
|
for (const [appTitle] of Object.entries(this.state.apps)) {
|
|
appTitlesForSelect[appTitle] = appTitle;
|
|
}
|
|
|
|
return (
|
|
<React.Fragment>
|
|
<Panel
|
|
padded={false}
|
|
heading="Page allocations"
|
|
floating={false}
|
|
collapsable={false}
|
|
grow={true}>
|
|
<Toolbar position="top">
|
|
<Select
|
|
options={appTitlesForSelect}
|
|
onChangeWithKey={this.onAppChange}
|
|
selected={this.state.runningAppName}
|
|
disabled={this.state.monitoring}
|
|
/>
|
|
|
|
{this.state.monitoring ? (
|
|
<Button onClick={this.onStopMonitor} icon="pause">
|
|
Pause
|
|
</Button>
|
|
) : (
|
|
<Button onClick={this.onStartMonitor} icon="play">
|
|
Start
|
|
</Button>
|
|
)}
|
|
<Label>
|
|
Min allocation size in bytes{' '}
|
|
<Input
|
|
placeholder="min bytes"
|
|
value={this.state.minAllocationSizeInTable}
|
|
type="number"
|
|
onChange={this.onMinAllocationSizeChange}
|
|
disabled={this.state.monitoring}
|
|
/>
|
|
</Label>
|
|
</Toolbar>
|
|
|
|
<Label>
|
|
Total number of allocations: {this.state.totalAllocations}
|
|
</Label>
|
|
<br />
|
|
<Label>
|
|
Total MBs allocated:{' '}
|
|
{(this.state.totalAllocatedBytes / 1024 / 1024).toFixed(3)}
|
|
</Label>
|
|
<ManagedTable
|
|
multiline={true}
|
|
columnSizes={ColumnSizes}
|
|
columns={Columns}
|
|
floating={false}
|
|
zebra={true}
|
|
rows={this.buildMemRows()}
|
|
/>
|
|
</Panel>
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
}
|