/** * 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 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 ( {instanceFields && ( )} {staticFields && ( )} {leak.details && (
{leak.details}
)}
); } render() { const {selectedIdx, selectedEid, showFullClassPaths} = this.state; const sidebar = this.renderSidebar(); return ( {this.state.leaks.map((leak: Leak, idx: number) => { const elements = showFullClassPaths ? leak.elements : leak.elementsSimple; const selected = selectedIdx == idx ? selectedEid : null; return ( { this._selectElement(idx, eid); }} onElementHovered={() => {}} onElementExpanded={(eid /*, deep*/) => { this._toggleElement(idx, eid); }} selected={selected} searchResults={null} root={leak.root} elements={elements} scrollable={false} /> ); })} { this.setState({showFullClassPaths: checked}); }} /> Show full class path {sidebar} ); } }