desktop/plugins/public/leak_canary/processLeakString.tsx (183 lines of code) (raw):

/** * 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 {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; }