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:
Pascal Hartig
2019-02-21 03:23:57 -08:00
committed by Facebook Github Bot
parent e69306b34d
commit aac9c40183
3 changed files with 455 additions and 0 deletions

View File

@@ -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);
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}