Reviewed By: bhamodi Differential Revision: D33331422 fbshipit-source-id: 016e8dcc0c0c7f1fc353a348b54fda0d5e2ddc01
484 lines
14 KiB
TypeScript
484 lines
14 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 {
|
|
ImageId,
|
|
ImageData,
|
|
ImagesList,
|
|
ImagesListResponse,
|
|
ImageEvent,
|
|
FrescoDebugOverlayEvent,
|
|
AndroidCloseableReferenceLeakEvent,
|
|
CacheInfo,
|
|
ImagesMap,
|
|
} from './api';
|
|
|
|
import {
|
|
PluginClient,
|
|
createState,
|
|
usePlugin,
|
|
useValue,
|
|
DetailSidebar,
|
|
Layout,
|
|
Tabs,
|
|
Tab,
|
|
} from 'flipper-plugin';
|
|
import React from 'react';
|
|
import ImagesCacheOverview from './ImagesCacheOverview';
|
|
import ImagesMemoryOverview from './ImagesMemoryOverview';
|
|
import {isProduction} from 'flipper';
|
|
|
|
import {Typography} from 'antd';
|
|
|
|
import ImagesSidebar from './ImagesSidebar';
|
|
import ImagePool from './ImagePool';
|
|
|
|
export type ImageEventWithId = ImageEvent & {eventId: number};
|
|
export type AllImageEventsInfo = {
|
|
events: Array<ImageEventWithId>;
|
|
};
|
|
|
|
const surfaceDefaultText = 'SELECT ALL SURFACES';
|
|
|
|
const debugLog = (...args: any[]) => {
|
|
if (!isProduction()) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(...args);
|
|
}
|
|
};
|
|
|
|
type Methods = {
|
|
getAllImageEventsInfo(params: {}): Promise<AllImageEventsInfo>;
|
|
listImages(params: {showDiskImages: boolean}): Promise<ImagesListResponse>;
|
|
getImage(params: {imageId: string}): Promise<ImageData>;
|
|
clear(params: {type: string}): Promise<void>;
|
|
trimMemory(params: {}): Promise<void>;
|
|
enableDebugOverlay(params: {enabled: boolean}): Promise<void>;
|
|
};
|
|
|
|
type Events = {
|
|
closeable_reference_leak_event: AndroidCloseableReferenceLeakEvent;
|
|
events: ImageEvent;
|
|
debug_overlay_event: FrescoDebugOverlayEvent;
|
|
};
|
|
|
|
export function plugin(client: PluginClient<Events, Methods>) {
|
|
const selectedSurfaces = createState<Set<string>>(
|
|
new Set([surfaceDefaultText]),
|
|
);
|
|
const currentSelectedImage = createState<ImageId | null>(null);
|
|
const isDebugOverlayEnabled = createState<boolean>(false);
|
|
const isAutoRefreshEnabled = createState<boolean>(false);
|
|
const currentImages = createState<ImagesList>([]);
|
|
const coldStartFilter = createState<boolean>(false);
|
|
const imagePool = createState<ImagePool | null>(null);
|
|
|
|
const surfaceList = createState<Set<string>>(new Set(), {
|
|
persist: 'surfaceList',
|
|
});
|
|
const images = createState<ImagesList>([], {persist: 'images'});
|
|
const events = createState<Array<ImageEventWithId>>([], {persist: 'events'});
|
|
const imagesMap = createState<ImagesMap>({}, {persist: 'imagesMap'});
|
|
const isLeakTrackingEnabled = createState<boolean>(false, {
|
|
persist: 'isLeakTrackingEnabled',
|
|
});
|
|
const showDiskImages = createState<boolean>(false, {
|
|
persist: 'showDiskImages',
|
|
});
|
|
const nextEventId = createState<number>(0, {persist: 'nextEventId'});
|
|
|
|
client.onConnect(() => {
|
|
init();
|
|
});
|
|
|
|
client.onDestroy(() => {
|
|
imagePool?.get()?.clear();
|
|
});
|
|
|
|
client.onMessage('closeable_reference_leak_event', (event) => {
|
|
if (isLeakTrackingEnabled) {
|
|
client.showNotification({
|
|
id: event.identityHashCode,
|
|
title: `Leaked CloseableReference: ${event.className}`,
|
|
message: (
|
|
<Layout.Container>
|
|
<Typography.Text>CloseableReference leaked for </Typography.Text>
|
|
<Typography.Text code>{event.className}</Typography.Text>
|
|
<Typography.Text>
|
|
(identity hashcode: {event.identityHashCode}).
|
|
</Typography.Text>
|
|
<Typography.Text strong>Stacktrace:</Typography.Text>
|
|
<Typography.Text code>
|
|
{event.stacktrace || '<unavailable>'}
|
|
</Typography.Text>
|
|
</Layout.Container>
|
|
),
|
|
severity: 'error',
|
|
category: 'closeablereference_leak',
|
|
});
|
|
}
|
|
});
|
|
|
|
client.onExport(async () => {
|
|
const [responseImages, responseEvents] = await Promise.all([
|
|
client.send('listImages', {showDiskImages: showDiskImages.get()}),
|
|
client.send('getAllImageEventsInfo', {}),
|
|
]);
|
|
const levels: ImagesList = responseImages.levels;
|
|
const newEvents: Array<ImageEventWithId> = responseEvents.events;
|
|
|
|
images.set([...images.get(), ...levels]);
|
|
|
|
newEvents.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) {
|
|
surfaceList.set(new Set([...surfaceList.get(), surface]));
|
|
}
|
|
}
|
|
events.set([{...event, eventId: index}, ...events.get()]);
|
|
});
|
|
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 client.send('getImage', {
|
|
imageId: id,
|
|
});
|
|
imageDataList.push(imageData);
|
|
} catch (e) {
|
|
console.error('[fresco] getImage failed:', e);
|
|
}
|
|
}
|
|
|
|
const imagesMapCopy = {...imagesMap.get()};
|
|
imageDataList.forEach((data: ImageData) => {
|
|
imagesMapCopy[data.imageId] = data;
|
|
});
|
|
imagesMap.set(imagesMapCopy);
|
|
});
|
|
|
|
client.onMessage('debug_overlay_event', (event) => {
|
|
isDebugOverlayEnabled.set(event.enabled);
|
|
});
|
|
|
|
client.onMessage('events', (event) => {
|
|
debugLog('Received events', event);
|
|
const {attribution} = event;
|
|
if (attribution instanceof Array && attribution.length > 0) {
|
|
const surface = attribution[0] ? attribution[0].trim() : undefined;
|
|
if (surface && surface.length > 0) {
|
|
surfaceList.update((draft) => (draft = new Set([...draft, surface])));
|
|
}
|
|
}
|
|
events.update((draft) => {
|
|
draft.unshift({
|
|
eventId: nextEventId.get(),
|
|
...event,
|
|
});
|
|
});
|
|
|
|
nextEventId.set(nextEventId.get() + 1);
|
|
});
|
|
|
|
function onClear(type: string) {
|
|
client.send('clear', {type});
|
|
setTimeout(() => updateCaches('onClear'), 1000);
|
|
}
|
|
|
|
function onTrimMemory() {
|
|
client.send('trimMemory', {});
|
|
setTimeout(() => updateCaches('onTrimMemory'), 1000);
|
|
}
|
|
|
|
function onEnableDebugOverlay(enabled: boolean) {
|
|
client.send('enableDebugOverlay', {enabled});
|
|
}
|
|
|
|
function onEnableAutoRefresh(enabled: boolean) {
|
|
isAutoRefreshEnabled.set(enabled);
|
|
|
|
if (enabled) {
|
|
// Delay the call just enough to allow the state change to complete.
|
|
setTimeout(() => onAutoRefresh());
|
|
}
|
|
}
|
|
|
|
function onAutoRefresh() {
|
|
updateCaches('auto-refresh');
|
|
if (isAutoRefreshEnabled.get()) {
|
|
setTimeout(() => onAutoRefresh(), 1000);
|
|
}
|
|
}
|
|
|
|
function getImage(imageId: string) {
|
|
if (!client.isConnected) {
|
|
debugLog(`Cannot fetch image ${imageId}: disconnected`);
|
|
return;
|
|
}
|
|
debugLog('<- getImage requested for ' + imageId);
|
|
client
|
|
.send('getImage', {imageId})
|
|
.then((image: ImageData) => {
|
|
debugLog('-> getImage ' + imageId + ' returned');
|
|
imagePool.get()?._fetchCompleted(image);
|
|
})
|
|
.catch((e) => console.error('[fresco] getImage failed:', e));
|
|
}
|
|
|
|
function onImageSelected(selectedImage: ImageId) {
|
|
currentSelectedImage.set(selectedImage);
|
|
}
|
|
|
|
function onSurfaceChange(surfaces: Set<string>) {
|
|
updateImagesOnUI(images.get(), surfaces, coldStartFilter.get());
|
|
}
|
|
|
|
function onColdStartChange(checked: boolean) {
|
|
updateImagesOnUI(images.get(), selectedSurfaces.get(), checked);
|
|
}
|
|
|
|
function onTrackLeaks(checked: boolean) {
|
|
client.logger.track('usage', 'fresco:onTrackLeaks', {
|
|
enabled: checked,
|
|
});
|
|
|
|
isLeakTrackingEnabled.set(checked);
|
|
}
|
|
|
|
function onShowDiskImages(checked: boolean) {
|
|
client.logger.track('usage', 'fresco:onShowDiskImages', {
|
|
enabled: checked,
|
|
});
|
|
|
|
showDiskImages.set(checked);
|
|
updateCaches('refresh');
|
|
}
|
|
|
|
function init() {
|
|
debugLog('init()');
|
|
if (client.isConnected) {
|
|
updateCaches('init');
|
|
} else {
|
|
debugLog(`not connected)`);
|
|
}
|
|
imagePool.set(
|
|
new ImagePool(getImage, (images: ImagesMap) => imagesMap.set(images)),
|
|
);
|
|
|
|
const filteredImages = filterImages(
|
|
images.get(),
|
|
events.get(),
|
|
selectedSurfaces.get(),
|
|
coldStartFilter.get(),
|
|
);
|
|
|
|
images.set(filteredImages);
|
|
}
|
|
|
|
function 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;
|
|
}
|
|
|
|
function updateImagesOnUI(
|
|
newImages: ImagesList,
|
|
surfaces: Set<string>,
|
|
coldStart: boolean,
|
|
) {
|
|
const filteredImages = filterImages(
|
|
newImages,
|
|
events.get(),
|
|
surfaces,
|
|
coldStart,
|
|
);
|
|
|
|
selectedSurfaces.set(surfaces);
|
|
images.set(filteredImages);
|
|
coldStartFilter.set(coldStart);
|
|
}
|
|
|
|
function updateCaches(reason: string) {
|
|
debugLog('Requesting images list (reason=' + reason + ')');
|
|
client
|
|
.send('listImages', {
|
|
showDiskImages: showDiskImages.get(),
|
|
})
|
|
.then((response: ImagesListResponse) => {
|
|
response.levels.forEach((data) =>
|
|
imagePool?.get()?.fetchImages(data.imageIds),
|
|
);
|
|
images.set(response.levels);
|
|
updateImagesOnUI(
|
|
images.get(),
|
|
selectedSurfaces.get(),
|
|
coldStartFilter.get(),
|
|
);
|
|
})
|
|
.catch((e) => console.error('[fresco] listImages failed:', e));
|
|
}
|
|
|
|
return {
|
|
selectedSurfaces,
|
|
currentSelectedImage,
|
|
isDebugOverlayEnabled,
|
|
isAutoRefreshEnabled,
|
|
currentImages,
|
|
coldStartFilter,
|
|
surfaceList,
|
|
images,
|
|
events,
|
|
imagesMap,
|
|
isLeakTrackingEnabled,
|
|
showDiskImages,
|
|
nextEventId,
|
|
imagePool,
|
|
onSurfaceChange,
|
|
onColdStartChange,
|
|
onClear,
|
|
onTrimMemory,
|
|
updateCaches,
|
|
onEnableDebugOverlay,
|
|
onEnableAutoRefresh,
|
|
onImageSelected,
|
|
onTrackLeaks,
|
|
onShowDiskImages,
|
|
};
|
|
}
|
|
|
|
export function Component() {
|
|
const instance = usePlugin(plugin);
|
|
|
|
let selectedSurfaces = useValue(instance.selectedSurfaces);
|
|
const isDebugOverlayEnabled = useValue(instance.isDebugOverlayEnabled);
|
|
const isAutoRefreshEnabled = useValue(instance.isAutoRefreshEnabled);
|
|
const coldStartFilter = useValue(instance.coldStartFilter);
|
|
|
|
const surfaceList = useValue(instance.surfaceList);
|
|
const images = useValue(instance.images);
|
|
const events = useValue(instance.events);
|
|
const imagesMap = useValue(instance.imagesMap);
|
|
const isLeakTrackingEnabled = useValue(instance.isLeakTrackingEnabled);
|
|
const showDiskImages = useValue(instance.showDiskImages);
|
|
|
|
const options = [...surfaceList].reduce(
|
|
(acc, item) => {
|
|
return [...acc, item];
|
|
},
|
|
[surfaceDefaultText],
|
|
);
|
|
|
|
if (selectedSurfaces.has(surfaceDefaultText)) {
|
|
selectedSurfaces = new Set(options);
|
|
}
|
|
|
|
return (
|
|
<Tabs defaultActiveKey="images" grow>
|
|
<Tab tab="Images" key="images">
|
|
<ImagesCacheOverview
|
|
allSurfacesOption={surfaceDefaultText}
|
|
surfaceOptions={new Set(options)}
|
|
selectedSurfaces={selectedSurfaces}
|
|
onChangeSurface={instance.onSurfaceChange}
|
|
coldStartFilter={coldStartFilter}
|
|
onColdStartChange={instance.onColdStartChange}
|
|
images={images}
|
|
onClear={instance.onClear}
|
|
onTrimMemory={instance.onTrimMemory}
|
|
onRefresh={() => instance.updateCaches('refresh')}
|
|
onEnableDebugOverlay={instance.onEnableDebugOverlay}
|
|
onEnableAutoRefresh={instance.onEnableAutoRefresh}
|
|
isDebugOverlayEnabled={isDebugOverlayEnabled}
|
|
isAutoRefreshEnabled={isAutoRefreshEnabled}
|
|
onImageSelected={instance.onImageSelected}
|
|
imagesMap={imagesMap}
|
|
events={events}
|
|
isLeakTrackingEnabled={isLeakTrackingEnabled}
|
|
onTrackLeaks={instance.onTrackLeaks}
|
|
showDiskImages={showDiskImages}
|
|
onShowDiskImages={instance.onShowDiskImages}
|
|
/>
|
|
<DetailSidebar>
|
|
<Sidebar />
|
|
</DetailSidebar>
|
|
</Tab>
|
|
<Tab tab="Memory" key="memory">
|
|
<ImagesMemoryOverview images={images} imagesMap={imagesMap} />
|
|
</Tab>
|
|
</Tabs>
|
|
);
|
|
}
|
|
|
|
function Sidebar() {
|
|
const instance = usePlugin(plugin);
|
|
const events = useValue(instance.events);
|
|
const imagesMap = useValue(instance.imagesMap);
|
|
const currentSelectedImage = useValue(instance.currentSelectedImage);
|
|
|
|
if (currentSelectedImage == null) {
|
|
return (
|
|
<Layout.Container pad>
|
|
<Typography.Text>
|
|
Select an image to see the events associated with it.
|
|
</Typography.Text>
|
|
</Layout.Container>
|
|
);
|
|
}
|
|
|
|
const maybeImage = imagesMap[currentSelectedImage];
|
|
const filteredEvents = events.filter((e) =>
|
|
e.imageIds.includes(currentSelectedImage),
|
|
);
|
|
return <ImagesSidebar image={maybeImage} events={filteredEvents} />;
|
|
}
|