Move Fresco plugin to oss directory

Summary: Moved the fresco plugin folder to open source directory

Reviewed By: passy

Differential Revision: D14126407

fbshipit-source-id: 15b2d1698e18b951742ec37ca94642e6511094b0
This commit is contained in:
Pritesh Nandgaonkar
2019-02-21 07:12:14 -08:00
committed by Facebook Github Bot
parent aac9c40183
commit 6ee8d72a1e
7 changed files with 913 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import type {ImageId, ImageData} from './api.js';
export type ImagesMap = {[imageId: ImageId]: ImageData};
const maxInflightRequests = 10;
export default class ImagePool {
cache: ImagesMap = {};
requested: {[imageId: ImageId]: boolean} = {};
queued: Array<ImageId> = [];
inFlightRequests: number = 0;
fetchImage: (imageId: ImageId) => void;
updateNotificationScheduled: boolean = false;
onPoolUpdated: (images: ImagesMap) => void;
constructor(
fetchImage: (imageId: ImageId) => void,
onPoolUpdated: (images: ImagesMap) => void,
) {
this.fetchImage = fetchImage;
this.onPoolUpdated = onPoolUpdated;
}
getImages(): ImagesMap {
return {...this.cache};
}
fetchImages(ids: Array<string>) {
for (const id of ids) {
if (!this.cache[id] && !this.requested[id]) {
this.requested[id] = true;
if (this.inFlightRequests < maxInflightRequests) {
this.inFlightRequests++;
this.fetchImage(id);
} else {
this.queued.unshift(id);
}
}
}
}
clear() {
this.cache = {};
this.requested = {};
}
_fetchCompleted(image: ImageData): void {
this.cache[image.imageId] = image;
delete this.requested[image.imageId];
if (this.queued.length > 0) {
this.fetchImage(this.queued.pop());
} else {
this.inFlightRequests--;
}
if (!this.updateNotificationScheduled) {
this.updateNotificationScheduled = true;
window.setTimeout(this._notify, 1000);
}
}
_notify = () => {
this.updateNotificationScheduled = false;
this.onPoolUpdated(this.getImages());
};
}

View File

@@ -0,0 +1,404 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import type {ImageId, ImageData, ImagesList} from './api.js';
import type {ImageEventWithId} from './index.js';
import {
Toolbar,
Button,
Spacer,
colors,
FlexBox,
FlexRow,
FlexColumn,
LoadingIndicator,
styled,
} from 'flipper';
import type {ImagesMap} from './ImagePool.js';
import {clipboard} from 'electron';
import {PureComponent} from 'react';
function formatMB(bytes: number) {
return Math.floor(bytes / (1024 * 1024)) + 'MB';
}
function formatKB(bytes: number) {
return Math.floor(bytes / 1024) + 'KB';
}
type ImagesCacheOverviewProps = {
images: ImagesList,
onClear: (type: string) => void,
onTrimMemory: () => void,
onRefresh: () => void,
onEnableDebugOverlay: (enabled: boolean) => void,
isDebugOverlayEnabled: boolean,
onEnableAutoRefresh: (enabled: boolean) => void,
isAutoRefreshEnabled: boolean,
onImageSelected: (selectedImage: ImageId) => void,
imagesMap: ImagesMap,
events: Array<ImageEventWithId>,
};
type ImagesCacheOverviewState = {|
selectedImage: ?ImageId,
size: number,
|};
export default class ImagesCacheOverview extends PureComponent<
ImagesCacheOverviewProps,
ImagesCacheOverviewState,
> {
static Container = styled(FlexColumn)({
backgroundColor: colors.white,
});
static Content = styled(FlexColumn)({
flex: 1,
overflow: 'auto',
});
static Empty = styled(FlexBox)({
alignItems: 'center',
height: '100%',
justifyContent: 'center',
width: '100%',
});
state = {
selectedImage: undefined,
size: 150,
};
onImageSelected = (selectedImage: ImageId) => {
this.setState({selectedImage});
this.props.onImageSelected(selectedImage);
};
onKeyDown = (e: SyntheticKeyboardEvent<*>) => {
const selectedImage = this.state.selectedImage;
const imagesMap = this.props.imagesMap;
if (selectedImage) {
if ((e.ctrlKey || e.metaKey) && e.key === 'c') {
clipboard.writeText(String(imagesMap[selectedImage]));
e.preventDefault();
}
}
};
onEnableDebugOverlayToggled = () => {
this.props.onEnableDebugOverlay(!this.props.isDebugOverlayEnabled);
};
onEnableAutoRefreshToggled = () => {
this.props.onEnableAutoRefresh(!this.props.isAutoRefreshEnabled);
};
onChangeSize = (e: SyntheticInputEvent<HTMLInputElement>) =>
this.setState({size: parseInt(e.target.value, 10)});
render() {
const hasImages =
this.props.images.reduce(
(c, cacheInfo) => c + cacheInfo.imageIds.length,
0,
) > 0;
if (!hasImages) {
return (
<ImagesCacheOverview.Empty>
<LoadingIndicator />
</ImagesCacheOverview.Empty>
);
}
return (
<ImagesCacheOverview.Container
grow={true}
onKeyDown={this.onKeyDown}
tabIndex="0">
<Toolbar position="top">
<Button icon="cross-outline" onClick={this.props.onTrimMemory}>
Trim Memory
</Button>
<Button onClick={this.onEnableDebugOverlayToggled}>
DebugOverlay {this.props.isDebugOverlayEnabled ? 'ON' : 'OFF'}
</Button>
<Button onClick={this.props.onRefresh}>Refresh</Button>
<Button onClick={this.onEnableAutoRefreshToggled}>
Auto Refresh {this.props.isAutoRefreshEnabled ? 'ON' : 'OFF'}
</Button>
<Spacer />
<input
type="range"
onChange={this.onChangeSize}
min={50}
max={150}
value={this.state.size}
/>
</Toolbar>
<ImagesCacheOverview.Content>
{this.props.images.map(data => {
const maxSize = data.maxSizeBytes;
const subtitle = maxSize
? formatMB(data.sizeBytes) + ' / ' + formatMB(maxSize)
: formatMB(data.sizeBytes);
const onClear = data.clearKey
? () => this.props.onClear(data.clearKey)
: null;
return (
<ImageGrid
title={data.cacheType}
subtitle={subtitle}
images={data.imageIds}
onImageSelected={this.onImageSelected}
selectedImage={this.state.selectedImage}
imagesMap={this.props.imagesMap}
size={this.state.size}
events={this.props.events}
onClear={onClear}
/>
);
})}
</ImagesCacheOverview.Content>
</ImagesCacheOverview.Container>
);
}
}
class ImageGrid extends PureComponent<{
title: string,
subtitle: string,
images: Array<ImageId>,
selectedImage: ?ImageId,
onImageSelected: (image: ImageId) => void,
onClear: ?() => void,
imagesMap: ImagesMap,
size: number,
events: Array<ImageEventWithId>,
}> {
static Content = styled('div')({
paddingLeft: 15,
});
render() {
const {images, onImageSelected, selectedImage} = this.props;
if (images.length === 0) {
return null;
}
return [
<ImageGridHeader
key="header"
title={this.props.title}
subtitle={this.props.subtitle}
onClear={this.props.onClear}
/>,
<ImageGrid.Content key="content">
{images.map(imageId => (
<ImageItem
imageId={imageId}
image={this.props.imagesMap[imageId]}
key={imageId}
selected={selectedImage != null && selectedImage === imageId}
onSelected={onImageSelected}
size={this.props.size}
numberOfRequests={
this.props.events.filter(e => e.imageIds.includes(imageId)).length
}
/>
))}
</ImageGrid.Content>,
];
}
}
class ImageGridHeader extends PureComponent<{
title: string,
subtitle: string,
onClear: ?() => void,
}> {
static Container = styled(FlexRow)({
color: colors.dark70,
paddingTop: 10,
paddingBottom: 10,
marginLeft: 15,
marginRight: 15,
marginBottom: 15,
borderBottom: `1px solid ${colors.light10}`,
flexShrink: 0,
alignItems: 'center',
position: 'sticky',
top: 0,
left: 0,
right: 0,
backgroundColor: colors.white,
zIndex: 3,
});
static Heading = styled('span')({
fontSize: 22,
fontWeight: 600,
});
static Subtitle = styled('span')({
fontSize: 22,
fontWeight: 300,
marginLeft: 15,
});
static ClearButton = styled(Button)({
alignSelf: 'center',
height: 30,
marginLeft: 'auto',
width: 100,
});
render() {
return (
<ImageGridHeader.Container>
<ImageGridHeader.Heading>{this.props.title}</ImageGridHeader.Heading>
<ImageGridHeader.Subtitle>
{this.props.subtitle}
</ImageGridHeader.Subtitle>
<Spacer />
{this.props.onClear ? (
<ImageGridHeader.ClearButton onClick={this.props.onClear}>
Clear Cache
</ImageGridHeader.ClearButton>
) : null}
</ImageGridHeader.Container>
);
}
}
class ImageItem extends PureComponent<{
imageId: ImageId,
image: ?ImageData,
selected: boolean,
onSelected: (image: ImageId) => void,
size: number,
numberOfRequests: number,
}> {
static Container = styled(FlexBox)(({size}) => ({
float: 'left',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
height: size,
width: size,
borderRadius: 4,
marginRight: 15,
marginBottom: 15,
backgroundColor: colors.light02,
}));
static Image = styled('img')({
borderRadius: 4,
maxHeight: '100%',
maxWidth: '100%',
objectFit: 'contain',
});
static Loading = styled('span')({
padding: '0 0',
});
static SelectedHighlight = styled('div')(props => ({
borderColor: colors.highlight,
borderStyle: 'solid',
borderWidth: props.selected ? 3 : 0,
borderRadius: 4,
boxShadow: props.selected ? `inset 0 0 0 1px ${colors.white}` : 'none',
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
top: 0,
}));
static HoverOverlay = styled(FlexColumn)(props => ({
alignItems: 'center',
backgroundColor: colors.whiteAlpha80,
bottom: props.selected ? 4 : 0,
fontSize: props.size > 100 ? 16 : 11,
justifyContent: 'center',
left: props.selected ? 4 : 0,
opacity: 0,
position: 'absolute',
right: props.selected ? 4 : 0,
top: props.selected ? 4 : 0,
overflow: 'hidden',
transition: '.1s opacity',
'&:hover': {
opacity: 1,
},
}));
static MemoryLabel = styled('span')({
fontWeight: 600,
marginBottom: 6,
});
static SizeLabel = styled('span')({
fontWeight: 300,
});
static Events = styled('div')({
position: 'absolute',
top: -5,
right: -5,
color: colors.white,
backgroundColor: colors.highlight,
fontWeight: 600,
borderRadius: 10,
fontSize: '0.85em',
zIndex: 2,
lineHeight: '20px',
width: 20,
textAlign: 'center',
});
static defaultProps = {
size: 150,
};
onClick = () => {
this.props.onSelected(this.props.imageId);
};
render() {
const {image, selected, size, numberOfRequests} = this.props;
return (
<ImageItem.Container onClick={this.onClick} size={size}>
{numberOfRequests > 0 &&
image != null && (
<ImageItem.Events>{numberOfRequests}</ImageItem.Events>
)}
{image != null ? (
<ImageItem.Image src={image.data} />
) : (
<LoadingIndicator size={25} />
)}
<ImageItem.SelectedHighlight selected={selected} />
{image != null && (
<ImageItem.HoverOverlay selected={selected} size={size}>
<ImageItem.MemoryLabel>
{formatKB(image.sizeBytes)}
</ImageItem.MemoryLabel>
<ImageItem.SizeLabel>
{image.width}&times;{image.height}
</ImageItem.SizeLabel>
</ImageItem.HoverOverlay>
)}
</ImageItem.Container>
);
}
}

View File

@@ -0,0 +1,160 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import type {ImageData} from './api.js';
import type {ImageEventWithId} from './index.js';
import {
Component,
DataDescription,
Text,
Panel,
ManagedDataInspector,
colors,
styled,
} from 'flipper';
type ImagesSidebarProps = {
image: ?ImageData,
events: Array<ImageEventWithId>,
};
type ImagesSidebarState = {};
const DataDescriptionKey = styled('span')({
color: colors.grapeDark1,
});
export default class ImagesSidebar extends Component<
ImagesSidebarProps,
ImagesSidebarState,
> {
render() {
return (
<div>
{this.renderUri()}
{this.props.events.map(e => <EventDetails key={e.eventId} event={e} />)}
</div>
);
}
renderUri() {
if (!this.props.image) {
return null;
}
if (!this.props.image.uri) {
return null;
}
return (
<p>
<DataDescriptionKey>URI</DataDescriptionKey>
<span key="sep">: </span>
<DataDescription
type="string"
value={this.props.image.uri}
setValue={function(path: Array<string>, val: any) {}}
/>
</p>
);
}
}
class EventDetails extends Component<{
event: ImageEventWithId,
}> {
static Container = styled(Panel)({
flexShrink: 0,
marginTop: '15px',
});
render() {
const {event} = this.props;
return (
<EventDetails.Container
heading={<RequestHeader event={event} />}
floating={false}
padded={false}
grow={false}
collapsed={false}>
<p>
<DataDescriptionKey>Attribution</DataDescriptionKey>
<span key="sep">: </span>
<ManagedDataInspector data={event.attribution} />
</p>
<p>
<DataDescriptionKey>Time start</DataDescriptionKey>
<span key="sep">: </span>
<DataDescription
type="number"
value={event.startTime}
setValue={function(path: Array<string>, val: any) {}}
/>
</p>
<p>
<DataDescriptionKey>Time end</DataDescriptionKey>
<span key="sep">: </span>
<DataDescription
type="number"
value={event.endTime}
setValue={function(path: Array<string>, val: any) {}}
/>
</p>
<p>
<DataDescriptionKey>Source</DataDescriptionKey>
<span key="sep">: </span>
<DataDescription
type="string"
value={event.source}
setValue={function(path: Array<string>, val: any) {}}
/>
</p>
{this.renderViewportData()}
</EventDetails.Container>
);
}
renderViewportData() {
const viewport = this.props.event.viewport;
if (!viewport) {
return null;
}
return (
<p>
<DataDescriptionKey>Viewport</DataDescriptionKey>
<span key="sep">: </span>
<DataDescription
type="string"
value={viewport.width + 'x' + viewport.height}
setValue={function(path: Array<string>, val: any) {}}
/>
</p>
);
// TODO (t31947746): grey box time, n-th scan time
}
}
class RequestHeader extends Component<{
event: ImageEventWithId,
}> {
dateString = timestamp => {
const date = new Date(timestamp);
return `${date.toTimeString().split(' ')[0]}.${(
'000' + date.getMilliseconds()
).substr(-3)}`;
};
render() {
const {event} = this.props;
const durationMs = event.endTime - event.startTime;
return (
<Text>
{event.viewport ? 'Request' : 'Prefetch'} at{' '}
{this.dateString(event.startTime)} ({durationMs}ms)
</Text>
);
}
}

67
src/plugins/fresco/api.js Normal file
View File

@@ -0,0 +1,67 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
export type ImageId = string;
// Listing images
export type CacheInfo = {|
cacheType: string,
clearKey?: null, // set this if this cache level supports clear(<key>)
sizeBytes: number,
maxSizeBytes?: number,
imageIds: Array<ImageId>,
|};
export type ImagesList = Array<CacheInfo>;
// The iOS Flipper api does not support a top-level array, so we wrap it in an object
export type ImagesListResponse = {|
levels: ImagesList,
|};
// listImages() -> ImagesListResponse
// Getting details on a specific image
export type ImageBytes = string;
export type ImageData = {|
imageId: ImageId,
uri?: string,
width: number,
height: number,
sizeBytes: number,
data: ImageBytes,
|};
// getImage({imageId: string}) -> ImageData
// Subscribing to image events (requests and prefetches)
export type Timestamp = number;
export type ViewportData = {|
width: number,
height: number,
scanDisplayTime: {[scan_number: number]: Timestamp},
|};
export type ImageEvent = {
imageIds: Array<ImageId>,
attribution: Array<string>,
startTime: Timestamp,
endTime: Timestamp,
source: string,
viewport?: ViewportData, // not set for prefetches
};
// Misc
export type FrescoDebugOverlayEvent = {|
enabled: boolean,
|};

192
src/plugins/fresco/index.js Normal file
View File

@@ -0,0 +1,192 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import type {
ImageId,
ImageData,
ImagesList,
ImagesListResponse,
ImageEvent,
FrescoDebugOverlayEvent,
} from './api.js';
import type {ImagesMap} from './ImagePool.js';
import React from 'react';
import ImagesCacheOverview from './ImagesCacheOverview.js';
import {
FlipperPlugin,
FlexRow,
Text,
DetailSidebar,
colors,
styled,
} from 'flipper';
import ImagesSidebar from './ImagesSidebar.js';
import ImagePool from './ImagePool.js';
export type ImageEventWithId = ImageEvent & {eventId: number};
type PluginState = {
images: ImagesList,
isDebugOverlayEnabled: boolean,
isAutoRefreshEnabled: boolean,
events: Array<ImageEventWithId>,
imagesMap: ImagesMap,
selectedImage: ?ImageId,
};
const EmptySidebar = styled(FlexRow)({
alignItems: 'center',
justifyContent: 'center',
color: colors.light30,
padding: 15,
fontSize: 16,
});
const DEBUG = false;
export default class extends FlipperPlugin<PluginState> {
state: PluginState;
imagePool: ImagePool;
nextEventId: number = 1;
state = {
images: [],
events: [],
selectedImage: null,
isDebugOverlayEnabled: false,
isAutoRefreshEnabled: false,
imagesMap: {},
};
init() {
if (DEBUG) {
// eslint-disable-next-line no-console
console.log('init()');
}
this.updateCaches('init');
this.client.subscribe('events', (event: ImageEvent) => {
this.setState({
events: [{eventId: this.nextEventId, ...event}, ...this.state.events],
});
this.nextEventId++;
});
this.client.subscribe(
'debug_overlay_event',
(event: FrescoDebugOverlayEvent) => {
this.setState({isDebugOverlayEnabled: event.enabled});
},
);
this.imagePool = new ImagePool(this.getImage, (images: ImagesMap) =>
this.setState({imagesMap: images}),
);
}
teardown() {
this.imagePool.clear();
}
updateCaches = (reason: string) => {
if (DEBUG) {
// eslint-disable-next-line no-console
console.log('Requesting images list (reason=' + reason + ')');
}
this.client.call('listImages').then((response: ImagesListResponse) => {
response.levels.forEach(data =>
this.imagePool.fetchImages(data.imageIds),
);
this.setState({images: response.levels});
});
};
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) => {
if (DEBUG) {
// eslint-disable-next-line no-console
console.log('<- getImage requested for ' + imageId);
}
this.client.call('getImage', {imageId}).then((image: ImageData) => {
if (DEBUG) {
// eslint-disable-next-line no-console
console.log('-> getImage ' + imageId + ' returned');
}
this.imagePool._fetchCompleted(image);
});
};
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.state.imagesMap[selectedImage];
const events = this.state.events.filter(e =>
e.imageIds.includes(selectedImage),
);
return <ImagesSidebar image={maybeImage} events={events} />;
};
render() {
return (
<React.Fragment>
<ImagesCacheOverview
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.state.imagesMap}
events={this.state.events}
/>
<DetailSidebar>{this.renderSidebar()}</DetailSidebar>
</React.Fragment>
);
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "Fresco",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"title": "Images",
"icon": "profile",
"bugs": {
"email": "oncall+fresco@xmail.facebook.com"
}
}

View File

@@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1