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
500
desktop/plugins/fresco/index.tsx
Normal file
500
desktop/plugins/fresco/index.tsx
Normal file
@@ -0,0 +1,500 @@
|
||||
/**
|
||||
* 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 {
|
||||
ImageId,
|
||||
ImageData,
|
||||
ImagesList,
|
||||
ImagesListResponse,
|
||||
ImageEvent,
|
||||
FrescoDebugOverlayEvent,
|
||||
AndroidCloseableReferenceLeakEvent,
|
||||
CacheInfo,
|
||||
} from './api';
|
||||
import {Fragment} from 'react';
|
||||
import {ImagesMap} from './ImagePool';
|
||||
import {MetricType, ReduxState} from 'flipper';
|
||||
import React from 'react';
|
||||
import ImagesCacheOverview from './ImagesCacheOverview';
|
||||
import {
|
||||
FlipperPlugin,
|
||||
FlexRow,
|
||||
Text,
|
||||
DetailSidebar,
|
||||
colors,
|
||||
styled,
|
||||
isProduction,
|
||||
Notification,
|
||||
BaseAction,
|
||||
} from 'flipper';
|
||||
import ImagesSidebar from './ImagesSidebar';
|
||||
import ImagePool from './ImagePool';
|
||||
|
||||
export type ImageEventWithId = ImageEvent & {eventId: number};
|
||||
|
||||
export type PersistedState = {
|
||||
surfaceList: Set<string>;
|
||||
images: ImagesList;
|
||||
events: Array<ImageEventWithId>;
|
||||
imagesMap: ImagesMap;
|
||||
closeableReferenceLeaks: Array<AndroidCloseableReferenceLeakEvent>;
|
||||
isLeakTrackingEnabled: boolean;
|
||||
nextEventId: number;
|
||||
};
|
||||
|
||||
type PluginState = {
|
||||
selectedSurfaces: Set<string>;
|
||||
selectedImage: ImageId | null;
|
||||
isDebugOverlayEnabled: boolean;
|
||||
isAutoRefreshEnabled: boolean;
|
||||
images: ImagesList;
|
||||
coldStartFilter: boolean;
|
||||
};
|
||||
|
||||
const EmptySidebar = styled(FlexRow)({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: colors.light30,
|
||||
padding: 15,
|
||||
fontSize: 16,
|
||||
});
|
||||
|
||||
export const InlineFlexRow = styled(FlexRow)({
|
||||
display: 'inline-block',
|
||||
});
|
||||
|
||||
const surfaceDefaultText = 'SELECT ALL SURFACES';
|
||||
|
||||
const debugLog = (...args: any[]) => {
|
||||
if (!isProduction()) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(...args);
|
||||
}
|
||||
};
|
||||
|
||||
type ImagesMetaData = {
|
||||
levels: ImagesListResponse;
|
||||
events: Array<ImageEventWithId>;
|
||||
imageDataList: Array<ImageData>;
|
||||
};
|
||||
|
||||
export default class FlipperImagesPlugin extends FlipperPlugin<
|
||||
PluginState,
|
||||
BaseAction,
|
||||
PersistedState
|
||||
> {
|
||||
static defaultPersistedState: PersistedState = {
|
||||
images: [],
|
||||
events: [],
|
||||
imagesMap: {},
|
||||
surfaceList: new Set(),
|
||||
closeableReferenceLeaks: [],
|
||||
isLeakTrackingEnabled: false,
|
||||
nextEventId: 0,
|
||||
};
|
||||
|
||||
static exportPersistedState = (
|
||||
callClient: (method: string, params?: any) => Promise<any>,
|
||||
persistedState: PersistedState,
|
||||
store?: ReduxState,
|
||||
): Promise<PersistedState> => {
|
||||
const defaultPromise = Promise.resolve(persistedState);
|
||||
if (!persistedState) {
|
||||
persistedState = FlipperImagesPlugin.defaultPersistedState;
|
||||
}
|
||||
if (!store) {
|
||||
return defaultPromise;
|
||||
}
|
||||
return Promise.all([
|
||||
callClient('listImages'),
|
||||
callClient('getAllImageEventsInfo'),
|
||||
]).then(async ([responseImages, responseEvents]) => {
|
||||
const levels: ImagesList = responseImages.levels;
|
||||
const events: Array<ImageEventWithId> = responseEvents.events;
|
||||
let pluginData: PersistedState = {
|
||||
...persistedState,
|
||||
images: persistedState ? [...persistedState.images, ...levels] : levels,
|
||||
closeableReferenceLeaks:
|
||||
(persistedState && persistedState.closeableReferenceLeaks) || [],
|
||||
};
|
||||
|
||||
events.forEach((event: ImageEventWithId, index) => {
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
const {attribution} = event;
|
||||
if (
|
||||
attribution &&
|
||||
attribution instanceof Array &&
|
||||
attribution.length > 0
|
||||
) {
|
||||
const surface = attribution[0] ? attribution[0].trim() : undefined;
|
||||
if (surface && surface.length > 0) {
|
||||
pluginData.surfaceList.add(surface);
|
||||
}
|
||||
}
|
||||
pluginData = {
|
||||
...pluginData,
|
||||
events: [{eventId: index, ...event}, ...pluginData.events],
|
||||
};
|
||||
});
|
||||
const idSet: Set<string> = levels.reduce((acc, level: CacheInfo) => {
|
||||
level.imageIds.forEach(id => {
|
||||
acc.add(id);
|
||||
});
|
||||
return acc;
|
||||
}, new Set<string>());
|
||||
const imageDataList: Array<ImageData> = [];
|
||||
for (const id of idSet) {
|
||||
try {
|
||||
const imageData: ImageData = await callClient('getImage', {
|
||||
imageId: id,
|
||||
});
|
||||
imageDataList.push(imageData);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
imageDataList.forEach((data: ImageData) => {
|
||||
const imagesMap = {...pluginData.imagesMap};
|
||||
imagesMap[data.imageId] = data;
|
||||
pluginData.imagesMap = imagesMap;
|
||||
});
|
||||
return pluginData;
|
||||
});
|
||||
};
|
||||
|
||||
static persistedStateReducer = (
|
||||
persistedState: PersistedState,
|
||||
method: string,
|
||||
data: AndroidCloseableReferenceLeakEvent | ImageEvent,
|
||||
): PersistedState => {
|
||||
if (method == 'closeable_reference_leak_event') {
|
||||
const event: AndroidCloseableReferenceLeakEvent = data as AndroidCloseableReferenceLeakEvent;
|
||||
return {
|
||||
...persistedState,
|
||||
closeableReferenceLeaks: persistedState.closeableReferenceLeaks.concat(
|
||||
event,
|
||||
),
|
||||
};
|
||||
} else if (method == 'events') {
|
||||
const event: ImageEvent = data as ImageEvent;
|
||||
debugLog('Received events', event);
|
||||
const {surfaceList} = persistedState;
|
||||
const {attribution} = event;
|
||||
if (attribution instanceof Array && attribution.length > 0) {
|
||||
const surface = attribution[0] ? attribution[0].trim() : undefined;
|
||||
if (surface && surface.length > 0) {
|
||||
surfaceList.add(surface);
|
||||
}
|
||||
}
|
||||
return {
|
||||
...persistedState,
|
||||
events: [
|
||||
{eventId: persistedState.nextEventId, ...event},
|
||||
...persistedState.events,
|
||||
],
|
||||
nextEventId: persistedState.nextEventId + 1,
|
||||
};
|
||||
}
|
||||
|
||||
return persistedState;
|
||||
};
|
||||
|
||||
static metricsReducer = (
|
||||
persistedState: PersistedState,
|
||||
): Promise<MetricType> => {
|
||||
const {events, imagesMap, closeableReferenceLeaks} = persistedState;
|
||||
|
||||
const wastedBytes = (events || []).reduce((acc, event) => {
|
||||
const {viewport, imageIds} = event;
|
||||
if (!viewport) {
|
||||
return acc;
|
||||
}
|
||||
return imageIds.reduce((innerAcc, imageID) => {
|
||||
const imageData: ImageData = imagesMap[imageID];
|
||||
if (!imageData) {
|
||||
return innerAcc;
|
||||
}
|
||||
const imageWidth: number = imageData.width;
|
||||
const imageHeight: number = imageData.height;
|
||||
const viewPortWidth: number = viewport.width;
|
||||
const viewPortHeight: number = viewport.height;
|
||||
const viewPortArea = viewPortWidth * viewPortHeight;
|
||||
const imageArea = imageWidth * imageHeight;
|
||||
return innerAcc + Math.max(0, imageArea - viewPortArea);
|
||||
}, acc);
|
||||
}, 0);
|
||||
|
||||
return Promise.resolve({
|
||||
WASTED_BYTES: wastedBytes,
|
||||
CLOSEABLE_REFERENCE_LEAKS: (closeableReferenceLeaks || []).length,
|
||||
});
|
||||
};
|
||||
|
||||
static getActiveNotifications = ({
|
||||
closeableReferenceLeaks = [],
|
||||
isLeakTrackingEnabled = false,
|
||||
}: PersistedState): Array<Notification> =>
|
||||
closeableReferenceLeaks
|
||||
.filter(_ => isLeakTrackingEnabled)
|
||||
.map((event: AndroidCloseableReferenceLeakEvent) => ({
|
||||
id: event.identityHashCode,
|
||||
title: `Leaked CloseableReference: ${event.className}`,
|
||||
message: (
|
||||
<Fragment>
|
||||
<InlineFlexRow>
|
||||
CloseableReference leaked for{' '}
|
||||
<Text code={true}>{event.className}</Text>
|
||||
(identity hashcode: {event.identityHashCode}).
|
||||
</InlineFlexRow>
|
||||
<InlineFlexRow>
|
||||
<Text bold={true}>Stacktrace:</Text>
|
||||
</InlineFlexRow>
|
||||
<InlineFlexRow>
|
||||
<Text code={true}>{event.stacktrace || '<unavailable>'}</Text>
|
||||
</InlineFlexRow>
|
||||
</Fragment>
|
||||
),
|
||||
severity: 'error',
|
||||
category: 'closeablereference_leak',
|
||||
}));
|
||||
|
||||
state: PluginState = {
|
||||
selectedSurfaces: new Set([surfaceDefaultText]),
|
||||
selectedImage: null,
|
||||
isDebugOverlayEnabled: false,
|
||||
isAutoRefreshEnabled: false,
|
||||
images: [],
|
||||
coldStartFilter: false,
|
||||
};
|
||||
imagePool: ImagePool | undefined;
|
||||
nextEventId: number = 1;
|
||||
|
||||
filterImages = (
|
||||
images: ImagesList,
|
||||
events: Array<ImageEventWithId>,
|
||||
surfaces: Set<string>,
|
||||
coldStart: boolean,
|
||||
): ImagesList => {
|
||||
if (!surfaces || (surfaces.has(surfaceDefaultText) && !coldStart)) {
|
||||
return images;
|
||||
}
|
||||
|
||||
const imageList = images.map((image: CacheInfo) => {
|
||||
const imageIdList = image.imageIds.filter(imageID => {
|
||||
const filteredEvents = events.filter((event: ImageEventWithId) => {
|
||||
const output =
|
||||
event.attribution &&
|
||||
event.attribution.length > 0 &&
|
||||
event.imageIds &&
|
||||
event.imageIds.includes(imageID);
|
||||
|
||||
if (surfaces.has(surfaceDefaultText)) {
|
||||
return output && coldStart && event.coldStart;
|
||||
}
|
||||
|
||||
return (
|
||||
(!coldStart || (coldStart && event.coldStart)) &&
|
||||
output &&
|
||||
surfaces.has(event.attribution[0])
|
||||
);
|
||||
});
|
||||
return filteredEvents.length > 0;
|
||||
});
|
||||
return {...image, imageIds: imageIdList};
|
||||
});
|
||||
return imageList;
|
||||
};
|
||||
|
||||
init() {
|
||||
debugLog('init()');
|
||||
this.updateCaches('init');
|
||||
this.client.subscribe(
|
||||
'debug_overlay_event',
|
||||
(event: FrescoDebugOverlayEvent) => {
|
||||
this.setState({isDebugOverlayEnabled: event.enabled});
|
||||
},
|
||||
);
|
||||
this.imagePool = new ImagePool(this.getImage, (images: ImagesMap) =>
|
||||
this.props.setPersistedState({imagesMap: images}),
|
||||
);
|
||||
|
||||
const images = this.filterImages(
|
||||
this.props.persistedState.images,
|
||||
this.props.persistedState.events,
|
||||
this.state.selectedSurfaces,
|
||||
this.state.coldStartFilter,
|
||||
);
|
||||
|
||||
this.setState({images});
|
||||
}
|
||||
|
||||
teardown() {
|
||||
this.imagePool ? this.imagePool.clear() : undefined;
|
||||
}
|
||||
|
||||
updateImagesOnUI = (
|
||||
images: ImagesList,
|
||||
surfaces: Set<string>,
|
||||
coldStart: boolean,
|
||||
) => {
|
||||
const filteredImages = this.filterImages(
|
||||
images,
|
||||
this.props.persistedState.events,
|
||||
surfaces,
|
||||
coldStart,
|
||||
);
|
||||
|
||||
this.setState({
|
||||
selectedSurfaces: surfaces,
|
||||
images: filteredImages,
|
||||
coldStartFilter: coldStart,
|
||||
});
|
||||
};
|
||||
updateCaches = (reason: string) => {
|
||||
debugLog('Requesting images list (reason=' + reason + ')');
|
||||
this.client.call('listImages').then((response: ImagesListResponse) => {
|
||||
response.levels.forEach(data =>
|
||||
this.imagePool ? this.imagePool.fetchImages(data.imageIds) : undefined,
|
||||
);
|
||||
this.props.setPersistedState({images: response.levels});
|
||||
this.updateImagesOnUI(
|
||||
this.props.persistedState.images,
|
||||
this.state.selectedSurfaces,
|
||||
this.state.coldStartFilter,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
onClear = (type: string) => {
|
||||
this.client.call('clear', {type});
|
||||
setTimeout(() => this.updateCaches('onClear'), 1000);
|
||||
};
|
||||
|
||||
onTrimMemory = () => {
|
||||
this.client.call('trimMemory', {});
|
||||
setTimeout(() => this.updateCaches('onTrimMemory'), 1000);
|
||||
};
|
||||
|
||||
onEnableDebugOverlay = (enabled: boolean) => {
|
||||
this.client.call('enableDebugOverlay', {enabled});
|
||||
};
|
||||
|
||||
onEnableAutoRefresh = (enabled: boolean) => {
|
||||
this.setState({isAutoRefreshEnabled: enabled});
|
||||
if (enabled) {
|
||||
// Delay the call just enough to allow the state change to complete.
|
||||
setTimeout(() => this.onAutoRefresh());
|
||||
}
|
||||
};
|
||||
|
||||
onAutoRefresh = () => {
|
||||
this.updateCaches('auto-refresh');
|
||||
if (this.state.isAutoRefreshEnabled) {
|
||||
setTimeout(() => this.onAutoRefresh(), 1000);
|
||||
}
|
||||
};
|
||||
|
||||
getImage = (imageId: string) => {
|
||||
debugLog('<- getImage requested for ' + imageId);
|
||||
this.client.call('getImage', {imageId}).then((image: ImageData) => {
|
||||
debugLog('-> getImage ' + imageId + ' returned');
|
||||
this.imagePool ? this.imagePool._fetchCompleted(image) : undefined;
|
||||
});
|
||||
};
|
||||
|
||||
onImageSelected = (selectedImage: ImageId) => this.setState({selectedImage});
|
||||
|
||||
renderSidebar = () => {
|
||||
const {selectedImage} = this.state;
|
||||
|
||||
if (selectedImage == null) {
|
||||
return (
|
||||
<EmptySidebar grow={true}>
|
||||
<Text align="center">
|
||||
Select an image to see the events associated with it.
|
||||
</Text>
|
||||
</EmptySidebar>
|
||||
);
|
||||
}
|
||||
|
||||
const maybeImage = this.props.persistedState.imagesMap[selectedImage];
|
||||
const events = this.props.persistedState.events.filter(e =>
|
||||
e.imageIds.includes(selectedImage),
|
||||
);
|
||||
return <ImagesSidebar image={maybeImage} events={events} />;
|
||||
};
|
||||
|
||||
onSurfaceChange = (surfaces: Set<string>) => {
|
||||
this.updateImagesOnUI(
|
||||
this.props.persistedState.images,
|
||||
surfaces,
|
||||
this.state.coldStartFilter,
|
||||
);
|
||||
};
|
||||
|
||||
onColdStartChange = (checked: boolean) => {
|
||||
this.updateImagesOnUI(
|
||||
this.props.persistedState.images,
|
||||
this.state.selectedSurfaces,
|
||||
checked,
|
||||
);
|
||||
};
|
||||
|
||||
onTrackLeaks = (checked: boolean) => {
|
||||
this.props.logger.track('usage', 'fresco:onTrackLeaks', {enabled: checked});
|
||||
this.props.setPersistedState({
|
||||
isLeakTrackingEnabled: checked,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const options = [...this.props.persistedState.surfaceList].reduce(
|
||||
(acc, item) => {
|
||||
return [...acc, item];
|
||||
},
|
||||
[surfaceDefaultText],
|
||||
);
|
||||
let {selectedSurfaces} = this.state;
|
||||
|
||||
if (selectedSurfaces.has(surfaceDefaultText)) {
|
||||
selectedSurfaces = new Set(options);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ImagesCacheOverview
|
||||
allSurfacesOption={surfaceDefaultText}
|
||||
surfaceOptions={new Set(options)}
|
||||
selectedSurfaces={selectedSurfaces}
|
||||
onChangeSurface={this.onSurfaceChange}
|
||||
coldStartFilter={this.state.coldStartFilter}
|
||||
onColdStartChange={this.onColdStartChange}
|
||||
images={this.state.images}
|
||||
onClear={this.onClear}
|
||||
onTrimMemory={this.onTrimMemory}
|
||||
onRefresh={() => this.updateCaches('refresh')}
|
||||
onEnableDebugOverlay={this.onEnableDebugOverlay}
|
||||
onEnableAutoRefresh={this.onEnableAutoRefresh}
|
||||
isDebugOverlayEnabled={this.state.isDebugOverlayEnabled}
|
||||
isAutoRefreshEnabled={this.state.isAutoRefreshEnabled}
|
||||
onImageSelected={this.onImageSelected}
|
||||
imagesMap={this.props.persistedState.imagesMap}
|
||||
events={this.props.persistedState.events}
|
||||
isLeakTrackingEnabled={
|
||||
this.props.persistedState.isLeakTrackingEnabled
|
||||
}
|
||||
onTrackLeaks={this.onTrackLeaks}
|
||||
/>
|
||||
<DetailSidebar>{this.renderSidebar()}</DetailSidebar>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user