Files
flipper/desktop/plugins/kaios-allocations/index.tsx
Anton Nikolaev 10d990c32c Move plugins to "sonar/desktop/plugins"
Summary:
Plugins moved from "sonar/desktop/src/plugins" to "sonar/desktop/plugins".

Fixed all the paths after moving.

New "desktop" folder structure:
- `src` - Flipper desktop app JS code executing in Electron Renderer (Chrome) process.
- `static` - Flipper desktop app JS code executing in Electron Main (Node.js) process.
- `plugins` - Flipper desktop JS plugins.
- `pkg` - Flipper packaging lib and CLI tool.
- `doctor` - Flipper diagnostics lib and CLI tool.
- `scripts` - Build scripts for Flipper desktop app.
- `headless` - Headless version of Flipper desktop app.
- `headless-tests` - Integration tests running agains Flipper headless version.

Reviewed By: mweststrate

Differential Revision: D20344186

fbshipit-source-id: d020da970b2ea1e001f9061a8782bfeb54e31ba0
2020-03-14 14:35:18 -07:00

443 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>
);
}
}