Summary: ## Changelog Pull Request resolved: https://github.com/facebook/flipper/pull/995 Reviewed By: passy Differential Revision: D21112956 Pulled By: priteshrnandgaonkar fbshipit-source-id: 14d79dd399970c49835aab7eeb7dbd69ee323ab9
283 lines
7.4 KiB
TypeScript
283 lines
7.4 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, sleep} from 'flipper';
|
|
|
|
import {FlexColumn, Button, Toolbar, Panel} from 'flipper';
|
|
|
|
import {
|
|
Legend,
|
|
LineChart,
|
|
Line,
|
|
YAxis,
|
|
XAxis,
|
|
CartesianGrid,
|
|
ResponsiveContainer,
|
|
Tooltip,
|
|
} from 'recharts';
|
|
|
|
import adb from 'adbkit';
|
|
import {exec} from 'promisify-child-process';
|
|
|
|
const PALETTE = [
|
|
'#FFD700',
|
|
'#FF6347',
|
|
'#8A2BE2',
|
|
'#A52A2A',
|
|
'#40E0D0',
|
|
'#006400',
|
|
'#ADFF2F',
|
|
'#FF00FF',
|
|
];
|
|
|
|
// For now, let's limit the number of points shown
|
|
// The graph will automatically drop the oldest point if this number is reached
|
|
const MAX_POINTS = 100;
|
|
|
|
// This is to have some consistency in the Y axis scale
|
|
// Recharts should automatically adjust the axis if any data point gets above this value
|
|
const Y_AXIS_EXPECTED_MAX_MEM = 200;
|
|
|
|
const EXCLUDE_PROCESS_NAME_SUBSTRINGS = [
|
|
'(Nuwa)',
|
|
'Launcher',
|
|
'Built-in',
|
|
'Jio',
|
|
'(Preallocated',
|
|
];
|
|
|
|
type DataPoint = {
|
|
[key: string]: number;
|
|
};
|
|
|
|
type Colors = {
|
|
[key: string]: string;
|
|
};
|
|
|
|
type State = {
|
|
points: Array<DataPoint>;
|
|
colors: Colors;
|
|
monitoring: boolean;
|
|
};
|
|
|
|
export default class KaiOSGraphs extends FlipperDevicePlugin<State, any, any> {
|
|
state = {
|
|
points: [],
|
|
colors: {},
|
|
monitoring: false,
|
|
};
|
|
|
|
static supportsDevice(device: Device) {
|
|
return device instanceof KaiOSDevice;
|
|
}
|
|
|
|
async init() {
|
|
try {
|
|
await exec('adb root');
|
|
} catch (e) {
|
|
console.error('Error obtaining root on the device', e);
|
|
}
|
|
}
|
|
|
|
teardown() {
|
|
this.onStopMonitor();
|
|
}
|
|
|
|
onStartMonitor = () => {
|
|
this.setState(
|
|
{
|
|
monitoring: true,
|
|
},
|
|
() => {
|
|
// no await because monitoring runs in the background
|
|
this.monitorInBackground();
|
|
},
|
|
);
|
|
};
|
|
|
|
onStopMonitor = () => {
|
|
this.setState({
|
|
monitoring: false,
|
|
});
|
|
};
|
|
|
|
monitorInBackground = async () => {
|
|
while (this.state.monitoring) {
|
|
await this.updateFreeMem();
|
|
await sleep(1000);
|
|
}
|
|
};
|
|
|
|
executeShell = (command: string) => {
|
|
return (this.device as KaiOSDevice).adb
|
|
.shell(this.device.serial, command)
|
|
.then(adb.util.readAll)
|
|
.then((output) => {
|
|
return output.toString().trim();
|
|
});
|
|
};
|
|
|
|
getMemory = () => {
|
|
return this.executeShell('b2g-info').then((output) => {
|
|
const lines = output.split('\n').map((line) => line.trim());
|
|
let freeMem = null;
|
|
for (const line of lines) {
|
|
// TODO: regex validation
|
|
if (line.startsWith('Free + cache')) {
|
|
const fields = line.split(' ');
|
|
const mem = fields[fields.length - 2];
|
|
freeMem = parseFloat(mem);
|
|
}
|
|
}
|
|
|
|
const appInfoData: {[key: string]: number} = {};
|
|
let appInfoSectionFieldsCount;
|
|
const appInfoSectionFieldToIndex: {[key: string]: number} = {};
|
|
for (const line of lines) {
|
|
if (line.startsWith('System memory info:')) {
|
|
// We're outside of the app info section
|
|
// Reset the counter, since it is used for detecting if we need to parse
|
|
// app memory usage data
|
|
appInfoSectionFieldsCount = undefined;
|
|
break;
|
|
}
|
|
if (!line) {
|
|
continue;
|
|
}
|
|
if (appInfoSectionFieldsCount) {
|
|
let fields = line.trim().split(/\s+/);
|
|
// Assume that only name field can contain spaces
|
|
const name = fields
|
|
.slice(0, -appInfoSectionFieldsCount + 1)
|
|
.join(' ');
|
|
const restOfTheFields = fields.slice(-appInfoSectionFieldsCount + 1);
|
|
fields = [name, ...restOfTheFields];
|
|
if (
|
|
EXCLUDE_PROCESS_NAME_SUBSTRINGS.some((excludeSubstr) =>
|
|
name.includes(excludeSubstr),
|
|
)
|
|
) {
|
|
continue;
|
|
}
|
|
if (fields[1].match(/\D+/)) {
|
|
// TODO: probably implement this through something other than b2g
|
|
throw new Error('Support for names with spaces is not implemented');
|
|
}
|
|
|
|
if (name !== 'b2g') {
|
|
const ussString = fields[appInfoSectionFieldToIndex['USS']];
|
|
const uss = ussString ? parseFloat(ussString) : -1;
|
|
appInfoData[name + ' USS'] = uss;
|
|
} else {
|
|
const rssString = fields[appInfoSectionFieldToIndex['RSS']];
|
|
const rss = rssString ? parseFloat(rssString) : -1;
|
|
appInfoData[name + ' RSS'] = rss;
|
|
}
|
|
}
|
|
if (line.startsWith('NAME')) {
|
|
// We're in the app info section now
|
|
const fields = line.trim().split(/\s+/);
|
|
appInfoSectionFieldsCount = fields.length;
|
|
for (let i = 0; i < fields.length; i++) {
|
|
appInfoSectionFieldToIndex[fields[i]] = i;
|
|
}
|
|
}
|
|
}
|
|
return {'Total free': freeMem != null ? freeMem : -1, ...appInfoData};
|
|
});
|
|
};
|
|
|
|
getColors = (point: DataPoint) => {
|
|
const oldColors = this.state.colors;
|
|
let newColors: Colors | null = null;
|
|
let newColorsCount = 0;
|
|
const existingNames = Object.keys(oldColors);
|
|
for (const name of Object.keys(point)) {
|
|
if (!(name in oldColors)) {
|
|
if (!newColors) {
|
|
newColors = {...oldColors};
|
|
}
|
|
newColors[name] = PALETTE[existingNames.length + newColorsCount];
|
|
newColorsCount++;
|
|
}
|
|
}
|
|
|
|
return newColors;
|
|
};
|
|
|
|
updateFreeMem = () => {
|
|
// This can be improved by using immutable.js
|
|
// If more points are necessary
|
|
return this.getMemory().then((point) => {
|
|
const points = [...this.state.points.slice(-MAX_POINTS + 1), point];
|
|
const colors = this.getColors(point);
|
|
let newState = {};
|
|
if (colors) {
|
|
newState = {colors, points};
|
|
} else {
|
|
newState = {points};
|
|
}
|
|
|
|
this.setState(newState);
|
|
});
|
|
};
|
|
|
|
render() {
|
|
const pointsToDraw = this.state.points.map((point, idx) => ({
|
|
...(point as Object),
|
|
idx,
|
|
}));
|
|
const colors: Colors = this.state.colors;
|
|
|
|
const names = Object.keys(colors);
|
|
return (
|
|
<Panel
|
|
padded={false}
|
|
heading="Free memory"
|
|
floating={false}
|
|
collapsable={false}
|
|
grow={true}>
|
|
<Toolbar position="top">
|
|
{this.state.monitoring ? (
|
|
<Button onClick={this.onStopMonitor} icon="pause">
|
|
Pause
|
|
</Button>
|
|
) : (
|
|
<Button onClick={this.onStartMonitor} icon="play">
|
|
Start
|
|
</Button>
|
|
)}
|
|
</Toolbar>
|
|
<FlexColumn grow={true}>
|
|
<ResponsiveContainer height={500}>
|
|
<LineChart data={pointsToDraw}>
|
|
<XAxis type="number" domain={[0, MAX_POINTS]} dataKey="idx" />
|
|
<YAxis type="number" domain={[0, Y_AXIS_EXPECTED_MAX_MEM]} />
|
|
{names.map((name) => (
|
|
<Line
|
|
key={`line-${name}`}
|
|
type="linear"
|
|
dataKey={name}
|
|
stroke={colors[name]}
|
|
isAnimationActive={false}
|
|
/>
|
|
))}
|
|
<Tooltip />
|
|
<Legend verticalAlign="bottom" height={36} />
|
|
<CartesianGrid stroke="#eee" strokeDasharray="5 5" />
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</FlexColumn>
|
|
</Panel>
|
|
);
|
|
}
|
|
}
|