Add active leak tracking in Fresco

Summary:
Adds a "Track Leaks" option that will show notifications (even retroactively)
for `CloseableReferences` that were tracked.

Reviewed By: danielbuechele

Differential Revision: D15622596

fbshipit-source-id: ef610379aa96f9a5e541f741af608db30bee74e1
This commit is contained in:
Pascal Hartig
2019-06-05 11:31:40 -07:00
committed by Facebook Github Bot
parent bef1ff26bc
commit 1973432f78
3 changed files with 70 additions and 0 deletions

View File

@@ -81,6 +81,8 @@ type ImagesCacheOverviewProps = {
onImageSelected: (selectedImage: ImageId) => void, onImageSelected: (selectedImage: ImageId) => void,
imagesMap: ImagesMap, imagesMap: ImagesMap,
events: Array<ImageEventWithId>, events: Array<ImageEventWithId>,
onTrackLeaks: (enabled: boolean) => void,
isLeakTrackingEnabled: boolean,
}; };
type ImagesCacheOverviewState = {| type ImagesCacheOverviewState = {|
@@ -184,6 +186,11 @@ export default class ImagesCacheOverview extends PureComponent<
onClick={this.props.onColdStartChange} onClick={this.props.onColdStartChange}
label="Show Cold Start Images" label="Show Cold Start Images"
/> />
<Toggle
toggled={this.props.isLeakTrackingEnabled}
onClick={this.props.onTrackLeaks}
label="Track Leaks"
/>
<Spacer /> <Spacer />
<input <input
type="range" type="range"

View File

@@ -9,6 +9,7 @@ import FrescoPlugin from '../index.js';
import type {PersistedState, ImageEventWithId} from '../index.js'; import type {PersistedState, ImageEventWithId} from '../index.js';
import type {AndroidCloseableReferenceLeakEvent} from '../api.js'; import type {AndroidCloseableReferenceLeakEvent} from '../api.js';
import type {MetricType} from 'flipper'; import type {MetricType} from 'flipper';
import type {Notification} from '../../../plugin';
function mockPersistedState( function mockPersistedState(
imageSizes: Array<{ imageSizes: Array<{
@@ -52,6 +53,7 @@ function mockPersistedState(
events, events,
imagesMap, imagesMap,
closeableReferenceLeaks: [], closeableReferenceLeaks: [],
isLeakTrackingEnabled: false,
}; };
} }
@@ -276,3 +278,36 @@ test('closeable reference metrics on input', () => {
const metrics = metricsReducer(persistedState); const metrics = metricsReducer(persistedState);
expect(metrics).resolves.toMatchObject({CLOSEABLE_REFERENCE_LEAKS: 2}); expect(metrics).resolves.toMatchObject({CLOSEABLE_REFERENCE_LEAKS: 2});
}); });
test('notifications for leaks', () => {
const notificationReducer: (
persistedState: PersistedState,
) => Array<Notification> = (FrescoPlugin.getActiveNotifications: any);
const closeableReferenceLeaks: Array<AndroidCloseableReferenceLeakEvent> = [
{
identityHashCode: 'deadbeef',
className: 'com.facebook.imagepipeline.memory.NativeMemoryChunk',
},
{
identityHashCode: 'f4c3b00c',
className: 'com.facebook.flipper.SomeMemoryAbstraction',
},
];
const persistedStateWithoutTracking = {
...mockPersistedState(),
closeableReferenceLeaks,
isLeakTrackingEnabled: false,
};
const emptyNotifs = notificationReducer(persistedStateWithoutTracking);
expect(emptyNotifs).toHaveLength(0);
const persistedStateWithTracking = {
...mockPersistedState(),
closeableReferenceLeaks,
isLeakTrackingEnabled: true,
};
const notifs = notificationReducer(persistedStateWithTracking);
expect(notifs).toHaveLength(2);
expect(notifs[0].message).toContain('deadbeef');
expect(notifs[1].title).toContain('SomeMemoryAbstraction');
});

View File

@@ -30,6 +30,7 @@ import {
} from 'flipper'; } from 'flipper';
import ImagesSidebar from './ImagesSidebar.js'; import ImagesSidebar from './ImagesSidebar.js';
import ImagePool from './ImagePool.js'; import ImagePool from './ImagePool.js';
import type {Notification} from '../../plugin';
export type ImageEventWithId = ImageEvent & {eventId: number}; export type ImageEventWithId = ImageEvent & {eventId: number};
@@ -39,6 +40,7 @@ export type PersistedState = {
events: Array<ImageEventWithId>, events: Array<ImageEventWithId>,
imagesMap: ImagesMap, imagesMap: ImagesMap,
closeableReferenceLeaks: Array<AndroidCloseableReferenceLeakEvent>, closeableReferenceLeaks: Array<AndroidCloseableReferenceLeakEvent>,
isLeakTrackingEnabled: boolean,
}; };
type PluginState = { type PluginState = {
@@ -80,6 +82,7 @@ export default class extends FlipperPlugin<PluginState, *, PersistedState> {
imagesMap: {}, imagesMap: {},
surfaceList: new Set(), surfaceList: new Set(),
closeableReferenceLeaks: [], closeableReferenceLeaks: [],
isLeakTrackingEnabled: false,
}; };
static exportPersistedState = ( static exportPersistedState = (
@@ -184,6 +187,21 @@ export default class extends FlipperPlugin<PluginState, *, PersistedState> {
}); });
}; };
static getActiveNotifications = ({
closeableReferenceLeaks,
isLeakTrackingEnabled,
}: PersistedState): Array<Notification> =>
closeableReferenceLeaks
.filter(_ => isLeakTrackingEnabled)
.map((event: AndroidCloseableReferenceLeakEvent, index) => ({
id: event.identityHashCode,
title: `Leaked CloseableReference: ${event.className}`,
message: `CloseableReference leaked for ${event.className}
(identity hashcode: ${event.identityHashCode})`,
severity: 'error',
category: 'closeablereference_leak',
}));
state: PluginState; state: PluginState;
imagePool: ImagePool; imagePool: ImagePool;
nextEventId: number = 1; nextEventId: number = 1;
@@ -381,6 +399,12 @@ export default class extends FlipperPlugin<PluginState, *, PersistedState> {
); );
}; };
onTrackLeaks = (checked: boolean) => {
this.props.setPersistedState({
isLeakTrackingEnabled: checked,
});
};
render() { render() {
const options = [...this.props.persistedState.surfaceList].reduce( const options = [...this.props.persistedState.surfaceList].reduce(
(acc, item) => { (acc, item) => {
@@ -407,6 +431,10 @@ export default class extends FlipperPlugin<PluginState, *, PersistedState> {
onImageSelected={this.onImageSelected} onImageSelected={this.onImageSelected}
imagesMap={this.props.persistedState.imagesMap} imagesMap={this.props.persistedState.imagesMap}
events={this.props.persistedState.events} events={this.props.persistedState.events}
isLeakTrackingEnabled={
this.props.persistedState.isLeakTrackingEnabled
}
onTrackLeaks={this.onTrackLeaks}
/> />
<DetailSidebar>{this.renderSidebar()}</DetailSidebar> <DetailSidebar>{this.renderSidebar()}</DetailSidebar>
</React.Fragment> </React.Fragment>