Files
flipper/desktop/plugins/public/leak_canary/index.tsx
Lorenzo Blasa c871b221fd Add catch handlers to client calls
Summary:
Client calls return a promise. Not dealing with the error causes promise unhandled rejection errors.

This change adds a catch block and logs the error instead.

Reviewed By: antonk52

Differential Revision: D37955029

fbshipit-source-id: 50cac5a5339961d16868ab2102d3c77790e6943c
2022-07-19 04:05:12 -07:00

280 lines
7.1 KiB
TypeScript

/**
* Copyright (c) Meta Platforms, Inc. and 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,
DataDescriptionType,
} 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[];
};
type LeakCanary2Report = {
leaks: Leak2[];
};
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;
details?: string;
};
export type Leak2 = {
title: string;
root: string;
elements: {[key: string]: Element};
retainedSize: string;
details: 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) => {
this._addNewLeaks(processLeaks(results.leaks));
});
this.client.subscribe('reportLeak2', (results: LeakCanary2Report) => {
this._addNewLeaks(results.leaks.map(this._adaptLeak2));
});
}
_addNewLeaks = (incomingLeaks: Leak[]) => {
// 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 = incomingLeaks.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: leaks.length,
});
};
_adaptLeak2 = (leak: Leak2): Leak => {
return {
title: leak.title,
root: leak.root,
elements: leak.elements,
elementsSimple: leak.elements,
staticFields: {},
instanceFields: {},
retainedSize: leak.retainedSize,
details: leak.details,
};
};
_clearLeaks = () => {
this.setState({
leaks: [],
leaksCount: 0,
selectedIdx: null,
selectedEid: null,
});
this.client.call('clear').catch((e) => {
console.warn('[LeakCanary] clear failed with error', e);
});
};
_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: DataDescriptionType; 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}>
{instanceFields && (
<Panel heading={'Instance'} floating={false} grow={false}>
<ManagedDataInspector
data={instanceFields}
expandRoot
extractValue={this._extractValue}
/>
</Panel>
)}
{staticFields && (
<Panel heading={'Static'} floating={false} grow={false}>
<ManagedDataInspector
data={staticFields}
expandRoot
extractValue={this._extractValue}
/>
</Panel>
)}
{leak.details && (
<Panel heading={'Details'} floating={false} grow={false}>
<pre>{leak.details}</pre>
</Panel>
)}
</Sidebar>
);
}
render() {
const {selectedIdx, selectedEid, showFullClassPaths} = this.state;
const sidebar = this.renderSidebar();
return (
<Window>
<FlexColumn grow>
<FlexColumn grow scrollable>
{this.state.leaks.map((leak: Leak, idx: number) => {
const elements = showFullClassPaths
? leak.elements
: leak.elementsSimple;
const selected = selectedIdx == idx ? selectedEid : null;
return (
<Panel
key={idx}
collapsable
padded={false}
heading={leak.title}
floating
accessory={leak.retainedSize}>
<ElementsInspector
onElementSelected={(eid) => {
this._selectElement(idx, eid);
}}
onElementHovered={() => {}}
onElementExpanded={(eid /*, deep*/) => {
this._toggleElement(idx, eid);
}}
selected={selected}
searchResults={null}
root={leak.root}
elements={elements}
scrollable={false}
/>
</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>
);
}
}