Open-source Fresco Android plugin
Summary: Let there be MIT! Reviewed By: oprisnik Differential Revision: D14150942 fbshipit-source-id: b907934e319d5ac7bd114a918fe79ca363724229
This commit is contained in:
committed by
Facebook Github Bot
parent
e69306b34d
commit
aac9c40183
@@ -0,0 +1,16 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
package com.facebook.flipper.plugins.fresco;
|
||||
|
||||
public interface FrescoFlipperDebugPrefHelper {
|
||||
|
||||
interface Listener {
|
||||
void onEnabledStatusChanged(boolean enabled);
|
||||
}
|
||||
|
||||
void setDebugOverlayEnabled(boolean enabled);
|
||||
|
||||
boolean isDebugOverlayEnabled();
|
||||
|
||||
void setDebugOverlayEnabledListener(Listener l);
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
package com.facebook.flipper.plugins.fresco;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.Base64;
|
||||
import com.facebook.cache.common.CacheKey;
|
||||
import com.facebook.common.internal.Predicate;
|
||||
import com.facebook.common.memory.manager.DebugMemoryManager;
|
||||
import com.facebook.common.references.CloseableReference;
|
||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||
import com.facebook.drawee.backends.pipeline.info.ImageLoadStatus;
|
||||
import com.facebook.drawee.backends.pipeline.info.ImageOriginUtils;
|
||||
import com.facebook.drawee.backends.pipeline.info.ImagePerfData;
|
||||
import com.facebook.drawee.backends.pipeline.info.ImagePerfDataListener;
|
||||
import com.facebook.flipper.core.FlipperArray;
|
||||
import com.facebook.flipper.core.FlipperConnection;
|
||||
import com.facebook.flipper.core.FlipperObject;
|
||||
import com.facebook.flipper.core.FlipperReceiver;
|
||||
import com.facebook.flipper.core.FlipperResponder;
|
||||
import com.facebook.flipper.perflogger.FlipperPerfLogger;
|
||||
import com.facebook.flipper.plugins.common.BufferingFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.fresco.objecthelper.FlipperObjectHelper;
|
||||
import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory;
|
||||
import com.facebook.imagepipeline.cache.CountingMemoryCacheInspector;
|
||||
import com.facebook.imagepipeline.cache.CountingMemoryCacheInspector.DumpInfoEntry;
|
||||
import com.facebook.imagepipeline.core.ImagePipelineFactory;
|
||||
import com.facebook.imagepipeline.debug.DebugImageTracker;
|
||||
import com.facebook.imagepipeline.debug.FlipperImageTracker;
|
||||
import com.facebook.imagepipeline.image.CloseableBitmap;
|
||||
import com.facebook.imagepipeline.image.CloseableImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Allows Sonar to display the contents of Fresco's caches. This is useful for developers to debug
|
||||
* what images are being held in cache as they navigate through their app.
|
||||
*/
|
||||
public class FrescoFlipperPlugin extends BufferingFlipperPlugin implements ImagePerfDataListener {
|
||||
|
||||
private static final String FRESCO_EVENT = "events";
|
||||
private static final String FRESCO_DEBUGOVERLAY_EVENT = "debug_overlay_event";
|
||||
|
||||
private static final int BITMAP_PREVIEW_WIDTH = 150;
|
||||
private static final int BITMAP_PREVIEW_HEIGHT = 150;
|
||||
private static final int BITMAP_SCALING_THRESHOLD_WIDTH = 200;
|
||||
private static final int BITMAP_SCALING_THRESHOLD_HEIGHT = 200;
|
||||
|
||||
/** Helper for clearing cache. */
|
||||
private static final Predicate<CacheKey> ALWAYS_TRUE_PREDICATE =
|
||||
new Predicate<CacheKey>() {
|
||||
@Override
|
||||
public boolean apply(CacheKey cacheKey) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private final FlipperImageTracker mFlipperImageTracker;
|
||||
private final PlatformBitmapFactory mPlatformBitmapFactory;
|
||||
@Nullable private final FlipperObjectHelper mSonarObjectHelper;
|
||||
private final DebugMemoryManager mMemoryManager;
|
||||
private final FlipperPerfLogger mPerfLogger;
|
||||
@Nullable private final FrescoFlipperDebugPrefHelper mDebugPrefHelper;
|
||||
|
||||
public FrescoFlipperPlugin(
|
||||
DebugImageTracker imageTracker,
|
||||
PlatformBitmapFactory bitmapFactory,
|
||||
@Nullable FlipperObjectHelper flipperObjectHelper,
|
||||
DebugMemoryManager memoryManager,
|
||||
FlipperPerfLogger perfLogger,
|
||||
@Nullable FrescoFlipperDebugPrefHelper debugPrefHelper) {
|
||||
mFlipperImageTracker =
|
||||
imageTracker instanceof FlipperImageTracker
|
||||
? (FlipperImageTracker) imageTracker
|
||||
: new FlipperImageTracker();
|
||||
mPlatformBitmapFactory = bitmapFactory;
|
||||
mSonarObjectHelper = flipperObjectHelper;
|
||||
mMemoryManager = memoryManager;
|
||||
mPerfLogger = perfLogger;
|
||||
mDebugPrefHelper = debugPrefHelper;
|
||||
}
|
||||
|
||||
public FlipperImageTracker getFlipperImageTracker() {
|
||||
return mFlipperImageTracker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "Fresco";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnect(FlipperConnection connection) {
|
||||
super.onConnect(connection);
|
||||
|
||||
connection.receive(
|
||||
"listImages",
|
||||
new FlipperReceiver() {
|
||||
@Override
|
||||
public void onReceive(FlipperObject params, FlipperResponder responder) throws Exception {
|
||||
if (!ensureFrescoInitialized(responder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mPerfLogger.startMarker("Sonar.Fresco.listImages");
|
||||
final ImagePipelineFactory imagePipelineFactory = Fresco.getImagePipelineFactory();
|
||||
final CountingMemoryCacheInspector.DumpInfo memoryCache =
|
||||
new CountingMemoryCacheInspector<>(
|
||||
imagePipelineFactory.getBitmapCountingMemoryCache())
|
||||
.dumpCacheContent();
|
||||
|
||||
responder.success(
|
||||
new FlipperObject.Builder()
|
||||
.put(
|
||||
"levels",
|
||||
new FlipperArray.Builder()
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("cacheType", "On screen bitmaps")
|
||||
.put("sizeBytes", memoryCache.size - memoryCache.lruSize)
|
||||
.put("imageIds", buildImageIdList(memoryCache.sharedEntries))
|
||||
.build())
|
||||
.put(
|
||||
new FlipperObject.Builder()
|
||||
.put("cacheType", "Bitmap memory cache")
|
||||
.put("clearKey", "memory")
|
||||
.put("sizeBytes", memoryCache.size)
|
||||
.put("maxSizeBytes", memoryCache.maxSize)
|
||||
.put("imageIds", buildImageIdList(memoryCache.lruEntries))
|
||||
.build())
|
||||
// TODO (t31947642): list images on disk
|
||||
.build())
|
||||
.build());
|
||||
mPerfLogger.endMarker("Sonar.Fresco.listImages");
|
||||
}
|
||||
});
|
||||
|
||||
connection.receive(
|
||||
"getImage",
|
||||
new FlipperReceiver() {
|
||||
@Override
|
||||
public void onReceive(FlipperObject params, final FlipperResponder responder)
|
||||
throws Exception {
|
||||
if (!ensureFrescoInitialized(responder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mPerfLogger.startMarker("Sonar.Fresco.getImage");
|
||||
String imageId = params.getString("imageId");
|
||||
CacheKey cacheKey = mFlipperImageTracker.getCacheKey(imageId);
|
||||
if (cacheKey == null) {
|
||||
respondError(responder, "ImageId " + imageId + " was evicted from cache");
|
||||
mPerfLogger.cancelMarker("Sonar.Fresco.getImage");
|
||||
return;
|
||||
}
|
||||
final ImagePipelineFactory imagePipelineFactory = Fresco.getImagePipelineFactory();
|
||||
CloseableReference<CloseableImage> ref =
|
||||
imagePipelineFactory.getBitmapCountingMemoryCache().get(cacheKey);
|
||||
if (ref == null) {
|
||||
respondError(responder, "no bitmap withId=" + imageId);
|
||||
mPerfLogger.cancelMarker("Sonar.Fresco.getImage");
|
||||
return;
|
||||
}
|
||||
final CloseableBitmap bitmap = (CloseableBitmap) ref.get();
|
||||
String encodedBitmap =
|
||||
bitmapToBase64Preview(bitmap.getUnderlyingBitmap(), mPlatformBitmapFactory);
|
||||
|
||||
responder.success(
|
||||
new FlipperObject.Builder()
|
||||
.put("imageId", imageId)
|
||||
.put("uri", mFlipperImageTracker.getUriString(cacheKey))
|
||||
.put("width", bitmap.getWidth())
|
||||
.put("height", bitmap.getHeight())
|
||||
.put("sizeBytes", bitmap.getSizeInBytes())
|
||||
.put("data", encodedBitmap)
|
||||
.build());
|
||||
|
||||
mPerfLogger.endMarker("Sonar.Fresco.getImage");
|
||||
}
|
||||
});
|
||||
|
||||
connection.receive(
|
||||
"clear",
|
||||
new FlipperReceiver() {
|
||||
@Override
|
||||
public void onReceive(FlipperObject params, FlipperResponder responder) {
|
||||
if (!ensureFrescoInitialized(responder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mPerfLogger.startMarker("Sonar.Fresco.clear");
|
||||
final String type = params.getString("type");
|
||||
switch (type) {
|
||||
case "memory":
|
||||
final ImagePipelineFactory imagePipelineFactory = Fresco.getImagePipelineFactory();
|
||||
imagePipelineFactory.getBitmapMemoryCache().removeAll(ALWAYS_TRUE_PREDICATE);
|
||||
break;
|
||||
case "disk":
|
||||
Fresco.getImagePipeline().clearDiskCaches();
|
||||
break;
|
||||
}
|
||||
mPerfLogger.endMarker("Sonar.Fresco.clear");
|
||||
}
|
||||
});
|
||||
|
||||
connection.receive(
|
||||
"trimMemory",
|
||||
new FlipperReceiver() {
|
||||
@Override
|
||||
public void onReceive(FlipperObject params, FlipperResponder responder) throws Exception {
|
||||
if (!ensureFrescoInitialized(responder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mMemoryManager != null) {
|
||||
mMemoryManager.trimMemory(
|
||||
DebugMemoryManager.ON_SYSTEM_LOW_MEMORY_WHILE_APP_IN_FOREGROUND);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
connection.receive(
|
||||
"enableDebugOverlay",
|
||||
new FlipperReceiver() {
|
||||
@Override
|
||||
public void onReceive(FlipperObject params, FlipperResponder responder) throws Exception {
|
||||
if (!ensureFrescoInitialized(responder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean enabled = params.getBoolean("enabled");
|
||||
if (mDebugPrefHelper != null) {
|
||||
mDebugPrefHelper.setDebugOverlayEnabled(enabled);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (mDebugPrefHelper != null) {
|
||||
mDebugPrefHelper.setDebugOverlayEnabledListener(
|
||||
new FrescoFlipperDebugPrefHelper.Listener() {
|
||||
@Override
|
||||
public void onEnabledStatusChanged(boolean enabled) {
|
||||
sendDebugOverlayEnabledEvent(enabled);
|
||||
}
|
||||
});
|
||||
sendDebugOverlayEnabledEvent(mDebugPrefHelper.isDebugOverlayEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean ensureFrescoInitialized(FlipperResponder responder) {
|
||||
mPerfLogger.startMarker("Sonar.Fresco.ensureFrescoInitialized");
|
||||
try {
|
||||
Fresco.getImagePipelineFactory();
|
||||
return true;
|
||||
} catch (NullPointerException e) {
|
||||
respondError(responder, "Fresco is not initialized yet");
|
||||
return false;
|
||||
} finally {
|
||||
mPerfLogger.endMarker("Sonar.Fresco.ensureFrescoInitialized");
|
||||
}
|
||||
}
|
||||
|
||||
private FlipperArray buildImageIdList(List<DumpInfoEntry<CacheKey, CloseableImage>> images) {
|
||||
|
||||
FlipperArray.Builder builder = new FlipperArray.Builder();
|
||||
for (DumpInfoEntry<CacheKey, CloseableImage> entry : images) {
|
||||
final FlipperImageTracker.ImageDebugData imageDebugData =
|
||||
mFlipperImageTracker.getImageDebugData(entry.key);
|
||||
|
||||
if (imageDebugData == null) {
|
||||
builder.put(mFlipperImageTracker.trackImage(entry.key).getUniqueId());
|
||||
} else {
|
||||
builder.put(imageDebugData.getUniqueId());
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private String bitmapToBase64Preview(Bitmap bitmap, PlatformBitmapFactory bitmapFactory) {
|
||||
if (bitmap.getWidth() < BITMAP_SCALING_THRESHOLD_WIDTH
|
||||
&& bitmap.getHeight() < BITMAP_SCALING_THRESHOLD_HEIGHT) {
|
||||
return bitmapToBase64WithoutScaling(bitmap);
|
||||
}
|
||||
mPerfLogger.startMarker("Sonar.Fresco.bitmap2base64-resize");
|
||||
|
||||
// TODO (t19034797): properly load images
|
||||
CloseableReference<Bitmap> scaledBitmapReference = null;
|
||||
try {
|
||||
float previewAspectRatio = BITMAP_PREVIEW_WIDTH / BITMAP_PREVIEW_HEIGHT;
|
||||
float imageAspectRatio = bitmap.getWidth() / bitmap.getHeight();
|
||||
|
||||
int scaledWidth;
|
||||
int scaledHeight;
|
||||
if (previewAspectRatio > imageAspectRatio) {
|
||||
scaledWidth = bitmap.getWidth() * BITMAP_PREVIEW_HEIGHT / bitmap.getHeight();
|
||||
scaledHeight = BITMAP_PREVIEW_HEIGHT;
|
||||
} else {
|
||||
scaledWidth = BITMAP_PREVIEW_WIDTH;
|
||||
scaledHeight = bitmap.getHeight() * BITMAP_PREVIEW_WIDTH / bitmap.getWidth();
|
||||
}
|
||||
scaledBitmapReference =
|
||||
bitmapFactory.createScaledBitmap(bitmap, scaledWidth, scaledHeight, false);
|
||||
return bitmapToBase64WithoutScaling(scaledBitmapReference.get());
|
||||
} finally {
|
||||
CloseableReference.closeSafely(scaledBitmapReference);
|
||||
mPerfLogger.endMarker("Sonar.Fresco.bitmap2base64-resize");
|
||||
}
|
||||
}
|
||||
|
||||
private String bitmapToBase64WithoutScaling(Bitmap bitmap) {
|
||||
mPerfLogger.startMarker("Sonar.Fresco.bitmap2base64-orig");
|
||||
ByteArrayOutputStream byteArrayOutputStream = null;
|
||||
try {
|
||||
byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
|
||||
|
||||
return "data:image/png;base64,"
|
||||
+ Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.DEFAULT);
|
||||
} finally {
|
||||
if (byteArrayOutputStream != null) {
|
||||
try {
|
||||
byteArrayOutputStream.close();
|
||||
} catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
mPerfLogger.endMarker("Sonar.Fresco.bitmap2base64-orig");
|
||||
}
|
||||
}
|
||||
|
||||
public void onImageLoadStatusUpdated(
|
||||
ImagePerfData imagePerfData, @ImageLoadStatus int imageLoadStatus) {
|
||||
if (imageLoadStatus != ImageLoadStatus.SUCCESS) {
|
||||
return;
|
||||
}
|
||||
|
||||
String requestId = imagePerfData.getRequestId();
|
||||
if (requestId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
FlipperImageTracker.ImageDebugData data = mFlipperImageTracker.getDebugDataForRequestId(requestId);
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
FlipperArray.Builder imageIdsBuilder = new FlipperArray.Builder();
|
||||
Set<CacheKey> cks = data.getCacheKeys();
|
||||
if (cks != null) {
|
||||
for (CacheKey ck : cks) {
|
||||
FlipperImageTracker.ImageDebugData d = mFlipperImageTracker.getImageDebugData(ck);
|
||||
if (d != null) {
|
||||
imageIdsBuilder.put(d.getUniqueId());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
imageIdsBuilder.put(data.getUniqueId());
|
||||
}
|
||||
|
||||
FlipperArray attribution;
|
||||
Object callerContext = imagePerfData.getCallerContext();
|
||||
if (callerContext == null) {
|
||||
attribution = new FlipperArray.Builder().put("unknown").build();
|
||||
} else if (mSonarObjectHelper == null) {
|
||||
attribution = new FlipperArray.Builder().put(callerContext.toString()).build();
|
||||
} else {
|
||||
attribution = mSonarObjectHelper.fromCallerContext(callerContext);
|
||||
}
|
||||
|
||||
FlipperObject.Builder response =
|
||||
new FlipperObject.Builder()
|
||||
.put("imageIds", imageIdsBuilder.build())
|
||||
.put("attribution", attribution)
|
||||
.put("startTime", imagePerfData.getControllerSubmitTimeMs())
|
||||
.put("endTime", imagePerfData.getControllerFinalImageSetTimeMs())
|
||||
.put("source", ImageOriginUtils.toString(imagePerfData.getImageOrigin()));
|
||||
|
||||
if (!imagePerfData.isPrefetch()) {
|
||||
response.put(
|
||||
"viewport",
|
||||
new FlipperObject.Builder()
|
||||
// TODO (t31947746): scan times
|
||||
.put("width", imagePerfData.getOnScreenWidthPx())
|
||||
.put("height", imagePerfData.getOnScreenHeightPx())
|
||||
.build());
|
||||
}
|
||||
|
||||
send(FRESCO_EVENT, response.build());
|
||||
}
|
||||
|
||||
public void onImageVisibilityUpdated(ImagePerfData imagePerfData, int visibilityState) {
|
||||
// ignored
|
||||
}
|
||||
|
||||
public void sendDebugOverlayEnabledEvent(final boolean enabled) {
|
||||
final FlipperObject.Builder builder = new FlipperObject.Builder().put("enabled", enabled);
|
||||
send(FRESCO_DEBUGOVERLAY_EVENT, builder.build());
|
||||
}
|
||||
|
||||
private static void respondError(FlipperResponder responder, String errorReason) {
|
||||
responder.error(new FlipperObject.Builder().put("reason", errorReason).build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2004-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.facebook.flipper.plugins.fresco;
|
||||
|
||||
import com.facebook.imagepipeline.debug.DebugImageTracker;
|
||||
import com.facebook.imagepipeline.listener.BaseRequestListener;
|
||||
import com.facebook.imagepipeline.request.ImageRequest;
|
||||
|
||||
/** Fresco image {@link RequestListener} that logs events for Sonar. */
|
||||
public class FrescoFlipperRequestListener extends BaseRequestListener {
|
||||
|
||||
private final DebugImageTracker mDebugImageTracker;
|
||||
|
||||
public FrescoFlipperRequestListener(DebugImageTracker debugImageTracker) {
|
||||
mDebugImageTracker = debugImageTracker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestStart(
|
||||
ImageRequest request, Object callerContext, String requestId, boolean isPrefetch) {
|
||||
mDebugImageTracker.trackImageRequest(request, requestId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user