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:
committed by
Facebook Github Bot
parent
aac9c40183
commit
6ee8d72a1e
75
src/plugins/fresco/ImagePool.js
Normal file
75
src/plugins/fresco/ImagePool.js
Normal 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());
|
||||||
|
};
|
||||||
|
}
|
||||||
404
src/plugins/fresco/ImagesCacheOverview.js
Normal file
404
src/plugins/fresco/ImagesCacheOverview.js
Normal 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}×{image.height}
|
||||||
|
</ImageItem.SizeLabel>
|
||||||
|
</ImageItem.HoverOverlay>
|
||||||
|
)}
|
||||||
|
</ImageItem.Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
160
src/plugins/fresco/ImagesSidebar.js
Normal file
160
src/plugins/fresco/ImagesSidebar.js
Normal 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
67
src/plugins/fresco/api.js
Normal 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
192
src/plugins/fresco/index.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/plugins/fresco/package.json
Normal file
11
src/plugins/fresco/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/plugins/fresco/yarn.lock
Normal file
4
src/plugins/fresco/yarn.lock
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user