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
This commit is contained in:
committed by
Facebook GitHub Bot
parent
beb5c85e69
commit
10d990c32c
232
desktop/plugins/leak_canary/index.tsx
Normal file
232
desktop/plugins/leak_canary/index.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* 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 {
|
||||
Panel,
|
||||
FlexRow,
|
||||
ElementsInspector,
|
||||
FlexColumn,
|
||||
ManagedDataInspector,
|
||||
Sidebar,
|
||||
Toolbar,
|
||||
Checkbox,
|
||||
FlipperPlugin,
|
||||
Button,
|
||||
styled,
|
||||
} from 'flipper';
|
||||
import {Element} from 'flipper';
|
||||
import {processLeaks} from './processLeakString';
|
||||
|
||||
type State = {
|
||||
leaks: Leak[];
|
||||
selectedIdx: number | null;
|
||||
selectedEid: string | null;
|
||||
showFullClassPaths: boolean;
|
||||
leaksCount: number;
|
||||
};
|
||||
|
||||
type LeakReport = {
|
||||
leaks: string[];
|
||||
};
|
||||
|
||||
export type Fields = {[key: string]: string};
|
||||
export type Leak = {
|
||||
title: string;
|
||||
root: string;
|
||||
elements: {[key: string]: Element};
|
||||
elementsSimple: {[key: string]: Element};
|
||||
instanceFields: {[key: string]: Fields};
|
||||
staticFields: {[key: string]: Fields};
|
||||
retainedSize: string;
|
||||
};
|
||||
|
||||
const Window = styled(FlexRow)({
|
||||
height: '100%',
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
const ToolbarItem = styled(FlexRow)({
|
||||
alignItems: 'center',
|
||||
marginLeft: '8px',
|
||||
});
|
||||
|
||||
export default class LeakCanary<PersistedState> extends FlipperPlugin<
|
||||
State,
|
||||
{type: 'LeakCanary'},
|
||||
PersistedState
|
||||
> {
|
||||
state: State = {
|
||||
leaks: [],
|
||||
selectedIdx: null,
|
||||
selectedEid: null,
|
||||
showFullClassPaths: false,
|
||||
leaksCount: 0,
|
||||
};
|
||||
|
||||
init() {
|
||||
this.client.subscribe('reportLeak', (results: LeakReport) => {
|
||||
// We only process new leaks instead of replacing the whole list in order
|
||||
// to both avoid redundant processing and to preserve the expanded/
|
||||
// collapsed state of the tree view
|
||||
const newLeaks = processLeaks(results.leaks.slice(this.state.leaksCount));
|
||||
|
||||
const leaks = this.state.leaks;
|
||||
for (let i = 0; i < newLeaks.length; i++) {
|
||||
leaks.push(newLeaks[i]);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
leaks: leaks,
|
||||
leaksCount: results.leaks.length,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_clearLeaks = () => {
|
||||
this.setState({
|
||||
leaks: [],
|
||||
leaksCount: 0,
|
||||
selectedIdx: null,
|
||||
selectedEid: null,
|
||||
});
|
||||
this.client.call('clear');
|
||||
};
|
||||
|
||||
_selectElement = (leakIdx: number, eid: string) => {
|
||||
this.setState({
|
||||
selectedIdx: leakIdx,
|
||||
selectedEid: eid,
|
||||
});
|
||||
};
|
||||
|
||||
_toggleElement = (leakIdx: number, eid: string) => {
|
||||
const {leaks} = this.state;
|
||||
const leak = leaks[leakIdx];
|
||||
|
||||
const element = leak.elements[eid];
|
||||
const elementSimple = leak.elementsSimple[eid];
|
||||
if (!element || !elementSimple) {
|
||||
return;
|
||||
}
|
||||
element.expanded = !element.expanded;
|
||||
elementSimple.expanded = !elementSimple.expanded;
|
||||
|
||||
this.setState({
|
||||
leaks: leaks,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a specific string value, determines what DataInspector type to treat
|
||||
* it as. Ensures that numbers, bools, etc render correctly.
|
||||
*/
|
||||
_extractValue(
|
||||
value: any,
|
||||
_: number, // depth
|
||||
): {mutable: boolean; type: string; value: any} {
|
||||
if (!isNaN(value)) {
|
||||
return {mutable: false, type: 'number', value: value};
|
||||
} else if (value == 'true' || value == 'false') {
|
||||
return {mutable: false, type: 'boolean', value: value};
|
||||
} else if (value == 'null') {
|
||||
return {mutable: false, type: 'null', value: value};
|
||||
}
|
||||
return {mutable: false, type: 'enum', value: value};
|
||||
}
|
||||
|
||||
renderSidebar() {
|
||||
const {selectedIdx, selectedEid, leaks} = this.state;
|
||||
|
||||
if (selectedIdx == null || selectedEid == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const leak = leaks[selectedIdx];
|
||||
const staticFields = leak.staticFields[selectedEid];
|
||||
const instanceFields = leak.instanceFields[selectedEid];
|
||||
|
||||
return (
|
||||
<Sidebar position="right" width={600} minWidth={300} maxWidth={900}>
|
||||
<Panel heading={'Instance'} floating={false} grow={false}>
|
||||
<ManagedDataInspector
|
||||
data={instanceFields}
|
||||
expandRoot={true}
|
||||
extractValue={this._extractValue}
|
||||
/>
|
||||
</Panel>
|
||||
<Panel heading={'Static'} floating={false} grow={false}>
|
||||
<ManagedDataInspector
|
||||
data={staticFields}
|
||||
expandRoot={true}
|
||||
extractValue={this._extractValue}
|
||||
/>
|
||||
</Panel>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {selectedIdx, selectedEid, showFullClassPaths} = this.state;
|
||||
const sidebar = this.renderSidebar();
|
||||
|
||||
return (
|
||||
<Window>
|
||||
<FlexColumn grow={true}>
|
||||
<FlexColumn grow={true} scrollable={true}>
|
||||
{this.state.leaks.map((leak: Leak, idx: number) => {
|
||||
const elements = showFullClassPaths
|
||||
? leak.elements
|
||||
: leak.elementsSimple;
|
||||
const selected = selectedIdx == idx ? selectedEid : null;
|
||||
return (
|
||||
<Panel
|
||||
collapsable={false}
|
||||
padded={false}
|
||||
heading={leak.title}
|
||||
floating={false}
|
||||
accessory={leak.retainedSize}>
|
||||
<ElementsInspector
|
||||
onElementSelected={eid => {
|
||||
this._selectElement(idx, eid);
|
||||
}}
|
||||
onElementHovered={() => {}}
|
||||
onElementExpanded={(eid /*, deep*/) => {
|
||||
this._toggleElement(idx, eid);
|
||||
}}
|
||||
onValueChanged={() => {}}
|
||||
selected={selected}
|
||||
searchResults={null}
|
||||
root={leak.root}
|
||||
elements={elements}
|
||||
/>
|
||||
</Panel>
|
||||
);
|
||||
})}
|
||||
</FlexColumn>
|
||||
<Toolbar>
|
||||
<ToolbarItem>
|
||||
<Button onClick={this._clearLeaks}>Clear</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Checkbox
|
||||
checked={showFullClassPaths}
|
||||
onChange={(checked: boolean) => {
|
||||
this.setState({showFullClassPaths: checked});
|
||||
}}
|
||||
/>
|
||||
Show full class path
|
||||
</ToolbarItem>
|
||||
</Toolbar>
|
||||
</FlexColumn>
|
||||
{sidebar}
|
||||
</Window>
|
||||
);
|
||||
}
|
||||
}
|
||||
16
desktop/plugins/leak_canary/package.json
Normal file
16
desktop/plugins/leak_canary/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "LeakCanary",
|
||||
"version": "1.0.0",
|
||||
"main": "index.tsx",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"flipper-plugin"
|
||||
],
|
||||
"dependencies": {},
|
||||
"devDependencies": {},
|
||||
"icon": "bird",
|
||||
"title": "LeakCanary",
|
||||
"bugs": {
|
||||
"email": "jhli@fb.com"
|
||||
}
|
||||
}
|
||||
263
desktop/plugins/leak_canary/processLeakString.tsx
Normal file
263
desktop/plugins/leak_canary/processLeakString.tsx
Normal file
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* 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
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {Leak} from './index';
|
||||
import {Element} from 'flipper';
|
||||
|
||||
/**
|
||||
* Utility Function to add a child element
|
||||
* @param childElementId
|
||||
* @param elementId
|
||||
* @param elements
|
||||
*/
|
||||
function safeAddChildElementId(
|
||||
childElementId: string,
|
||||
elementId: string,
|
||||
elements: Map<string, Element>,
|
||||
) {
|
||||
const element = elements.get(elementId);
|
||||
if (element && element.children) {
|
||||
element.children.push(childElementId);
|
||||
}
|
||||
}
|
||||
|
||||
function toObjectMap(
|
||||
dict: Map<any, any>,
|
||||
deep: boolean = false,
|
||||
): {[key: string]: any} {
|
||||
const result: {[key: string]: any} = {};
|
||||
for (let [key, value] of dict.entries()) {
|
||||
if (deep && value instanceof Map) {
|
||||
value = toObjectMap(value, true);
|
||||
}
|
||||
result[String(key)] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Element (for ElementsInspector) representing a single Object in
|
||||
* the path to GC root view.
|
||||
*/
|
||||
function getElementSimple(str: string, id: string): Element {
|
||||
// Below regex can handle both older and newer versions of LeakCanary
|
||||
const match = str.match(
|
||||
/\* (GC ROOT )?(\u21B3 )?([a-z]* )?([^A-Z]*.)?([A-Z].*)/,
|
||||
);
|
||||
let name = 'N/A';
|
||||
if (match) {
|
||||
name = match[5];
|
||||
}
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
expanded: true,
|
||||
children: [],
|
||||
attributes: [],
|
||||
data: {},
|
||||
decoration: '',
|
||||
extraInfo: {},
|
||||
};
|
||||
}
|
||||
|
||||
// Line marking the start of Details section
|
||||
const BEGIN_DETAILS_SECTION_INDICATOR = '* Details:';
|
||||
// Line following the end of the Details section
|
||||
const END_DETAILS_SECTION_INDICATOR = '* Excluded Refs:';
|
||||
const STATIC_PREFIX = 'static ';
|
||||
// Text that begins the line of the Object at GC root
|
||||
const LEAK_BEGIN_INDICATOR = 'has leaked:';
|
||||
const RETAINED_SIZE_INDICATOR = '* Retaining: ';
|
||||
|
||||
/**
|
||||
* Parses the lines given (at the given index) to extract information about both
|
||||
* static and instance fields of each class in the path to GC root. Returns three
|
||||
* objects, each one mapping the element ID of a specific element to the
|
||||
* corresponding static fields, instance fields, or package name of the class
|
||||
*/
|
||||
function generateFieldsList(
|
||||
lines: string[],
|
||||
i: number,
|
||||
): {
|
||||
staticFields: Map<string, Map<string, string>>;
|
||||
instanceFields: Map<string, Map<string, string>>;
|
||||
packages: Map<string, string>;
|
||||
} {
|
||||
const staticFields = new Map<string, Map<string, string>>();
|
||||
const instanceFields = new Map<string, Map<string, string>>();
|
||||
|
||||
let staticValues = new Map<string, string>();
|
||||
let instanceValues = new Map<string, string>();
|
||||
|
||||
let elementId = -1;
|
||||
let elementIdStr = '';
|
||||
|
||||
const packages = new Map<string, any>();
|
||||
|
||||
// Process everything between Details and Excluded Refs
|
||||
while (
|
||||
i < lines.length &&
|
||||
!lines[i].startsWith(END_DETAILS_SECTION_INDICATOR)
|
||||
) {
|
||||
const line = lines[i];
|
||||
if (line.startsWith('*')) {
|
||||
if (elementId != -1) {
|
||||
staticFields.set(elementIdStr, staticValues);
|
||||
instanceFields.set(elementIdStr, instanceValues);
|
||||
staticValues = new Map<string, string>();
|
||||
instanceValues = new Map<string, string>();
|
||||
}
|
||||
elementId++;
|
||||
elementIdStr = String(elementId);
|
||||
|
||||
// Extract package for each class
|
||||
let pkg = 'unknown';
|
||||
const match = line.match(/\* (.*)(of|Class) (.*)/);
|
||||
if (match) {
|
||||
pkg = match[3];
|
||||
}
|
||||
packages.set(elementIdStr, pkg);
|
||||
} else {
|
||||
// Field/value pairs represented in input lines as
|
||||
// | fieldName = value
|
||||
const match = line.match(/\|\s+(.*) = (.*)/);
|
||||
if (match) {
|
||||
const fieldName = match[1];
|
||||
const fieldValue = match[2];
|
||||
|
||||
if (fieldName.startsWith(STATIC_PREFIX)) {
|
||||
const strippedFieldName = fieldName.substr(7);
|
||||
staticValues.set(strippedFieldName, fieldValue);
|
||||
} else {
|
||||
instanceValues.set(fieldName, fieldValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
staticFields.set(elementIdStr, staticValues);
|
||||
instanceFields.set(elementIdStr, instanceValues);
|
||||
|
||||
return {staticFields, instanceFields, packages};
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a LeakCanary string containing data from a single leak. If the
|
||||
* string represents a valid leak, the function appends parsed data to the given
|
||||
* output list. If not, the list is returned as-is. This parsed data contains
|
||||
* the path to GC root, static/instance fields for each Object in the path, the
|
||||
* leak's retained size, and a title for the leak.
|
||||
*/
|
||||
function processLeak(output: Leak[], leakInfo: string): Leak[] {
|
||||
const lines = leakInfo.split('\n');
|
||||
|
||||
// Elements shows a Object's classname and package, wheras elementsSimple shows
|
||||
// just its classname
|
||||
const elements = new Map<string, Element>();
|
||||
const elementsSimple = new Map<string, Element>();
|
||||
|
||||
let rootElementId = '';
|
||||
|
||||
let i = 0;
|
||||
while (i < lines.length && !lines[i].endsWith(LEAK_BEGIN_INDICATOR)) {
|
||||
i++;
|
||||
}
|
||||
i++;
|
||||
|
||||
if (i >= lines.length) {
|
||||
return output;
|
||||
}
|
||||
|
||||
let elementId = 0;
|
||||
let elementIdStr = String(elementId);
|
||||
// Last element is leaked object
|
||||
let leakedObjName = '';
|
||||
while (i < lines.length && lines[i].startsWith('*')) {
|
||||
const line = lines[i];
|
||||
|
||||
const prevElementIdStr = String(elementId - 1);
|
||||
if (elementId !== 0) {
|
||||
// Add element to previous element's children
|
||||
safeAddChildElementId(elementIdStr, prevElementIdStr, elements);
|
||||
safeAddChildElementId(elementIdStr, prevElementIdStr, elementsSimple);
|
||||
} else {
|
||||
rootElementId = elementIdStr;
|
||||
}
|
||||
const element = getElementSimple(line, elementIdStr);
|
||||
leakedObjName = element.name;
|
||||
elements.set(elementIdStr, element);
|
||||
elementsSimple.set(elementIdStr, element);
|
||||
|
||||
i++;
|
||||
elementId++;
|
||||
elementIdStr = String(elementId);
|
||||
}
|
||||
|
||||
while (
|
||||
i < lines.length &&
|
||||
!lines[i].startsWith(RETAINED_SIZE_INDICATOR) &&
|
||||
!lines[i].startsWith(BEGIN_DETAILS_SECTION_INDICATOR)
|
||||
) {
|
||||
i++;
|
||||
}
|
||||
|
||||
let retainedSize = 'unknown size';
|
||||
|
||||
if (lines[i].startsWith(RETAINED_SIZE_INDICATOR)) {
|
||||
const match = lines[i].match(/\* Retaining: (.*)./);
|
||||
if (match) {
|
||||
retainedSize = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
while (
|
||||
i < lines.length &&
|
||||
!lines[i].startsWith(BEGIN_DETAILS_SECTION_INDICATOR)
|
||||
) {
|
||||
i++;
|
||||
}
|
||||
i++;
|
||||
|
||||
// Parse information on each object's fields, package
|
||||
const {staticFields, instanceFields, packages} = generateFieldsList(lines, i);
|
||||
|
||||
// While elementsSimple remains as-is, elements has the package of each class
|
||||
// inserted, in order to enable 'Show full class path'
|
||||
for (const [elementId, pkg] of packages.entries()) {
|
||||
const element = elements.get(elementId);
|
||||
if (!element) {
|
||||
continue;
|
||||
}
|
||||
// Gets everything before the field name, which is replaced by the package
|
||||
const match = element.name.match(/([^\. ]*)(.*)/);
|
||||
if (match && match.length === 3) {
|
||||
element.name = pkg + match[2];
|
||||
}
|
||||
}
|
||||
|
||||
output.push({
|
||||
title: leakedObjName,
|
||||
root: rootElementId,
|
||||
elements: toObjectMap(elements),
|
||||
elementsSimple: toObjectMap(elementsSimple),
|
||||
staticFields: toObjectMap(staticFields, true),
|
||||
instanceFields: toObjectMap(instanceFields, true),
|
||||
retainedSize: retainedSize,
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a set of LeakCanary strings, ignoring non-leaks - see processLeak above.
|
||||
*/
|
||||
export function processLeaks(leakInfos: string[]): Leak[] {
|
||||
const newLeaks = leakInfos.reduce(processLeak, []);
|
||||
return newLeaks;
|
||||
}
|
||||
4
desktop/plugins/leak_canary/yarn.lock
Normal file
4
desktop/plugins/leak_canary/yarn.lock
Normal file
@@ -0,0 +1,4 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user