Listening to Events
Motivation
The image pipeline and the view controller in Fresco have built-in instrumentation interfaces. One can employ this to track both performance and to react to events.
Fresco comes with two main instrumentation interfaces:
- The
RequestListener
is globally registered in theImagePipelineConfig
and logs all requests that are handled by the producer-consumer chain - The
ControllerListener
is added to an individualDraweeView
and is convenient for reacting on events such as “this image is fully loaded”
ControllerListener
While the RequestListener
is a global listener, the ControllerListener
is local to a certain DraweeView
. It is a good way to react to changes to the displayed view such as “image failed to load” or “image is fully loaded”. Again, it’s best to extend BaseControllerListener
for this.
A simple listener might look like the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyControllerListener extends new BaseControllerListener<ImageInfo>() {
@Override
public void onFinalImageSet(String id, ImageInfo imageInfo, Animatable animatable) {
Log.i("DraweeUpdate", "Image is fully loaded!");
}
@Override
public void onIntermediateImageSet(String id, ImageInfo imageInfo, Animatable animatable) {
Log.i("DraweeUpdate", "Image is partly loaded! (maybe it's a progressive JPEG?)");
if (imageInfo != null) {
int quality = imageInfo.getQualityInfo().getQuality();
Log.i("DraweeUpdate", "Image quality (number scans) is: " + quality);
}
}
@Override
public void onFailure(String id, Throwable throwable) {
Log.i("DraweeUpdate", "Image failed to load: " + throwable.getMessage());
}
}
You add it to your DraweeController
in the following way:
1
2
3
4
5
DraweeController controller = Fresco.newDraweeControllerBuilder()
.setImageRequest(request)
.setControllerListener(new MyControllerListener())
.build();
mSimpleDraweeView.setController(controller);
RequestListener
The RequestListener
comes with a large interface of callback methods. Most importantly, you will notice that they all provide the unique requestId
which allows to track a request across multiple stages.
Due to the large number of callbacks, it is advisable to extend from BaseRequestListener
instead and only implement the methods you are interested in. You register your listener in the Application class as follows:
1
2
3
4
5
6
7
8
final Set<RequestListener> listeners = new HashSet<>();
listeners.add(new MyRequestLoggingListener());
ImagePipelineConfig imagePipelineConfig = ImagePipelineConfig.newBuilder(this)
.setRequestListeners(listeners)
.build();
Fresco.initialize(this, imagePipelineConfig);
We will walk through the generated logging of one image request from the showcase app and discuss the individual meanings. You can observe these yourself in adb logcat
when running the showcase app:
1
RequestLoggingListener: time 2095589: onRequestSubmit: {requestId: 5, callerContext: null, isPrefetch: false}
onRequestSubmit(...)
is called when an ImageRequest
enters the image pipeline. Here you can make use of the caller context object to identify which feature of the app is sending the request.
1
2
RequestLoggingListener: time 2095590: onProducerStart: {requestId: 5, producer: BitmapMemoryCacheGetProducer}
RequestLoggingListener: time 2095591: onProducerFinishWithSuccess: {requestId: 5, producer: BitmapMemoryCacheGetProducer, elapsedTime: 1 ms, extraMap: {cached_value_found=false}}
The onProducerStart(...)
and onProducerFinishWithSuccess(...)
(or onProducerFinishWithFailure(...)
) are called for all producers along the pipeline. The one above is a check of the Bitmap cache.
1
2
3
4
5
6
7
8
9
10
RequestLoggingListener: time 2095592: onProducerStart: {requestId: 5, producer: BackgroundThreadHandoffProducer}
RequestLoggingListener: time 2095593: onProducerFinishWithSuccess: {requestId: 5, producer: BackgroundThreadHandoffProducer, elapsedTime: 1 ms, extraMap: null}
RequestLoggingListener: time 2095594: onProducerStart: {requestId: 5, producer: BitmapMemoryCacheProducer}
RequestLoggingListener: time 2095594: onProducerFinishWithSuccess: {requestId: 5, producer: BitmapMemoryCacheProducer, elapsedTime: 0 ms, extraMap: {cached_value_found=false}}
RequestLoggingListener: time 2095595: onProducerStart: {requestId: 5, producer: EncodedMemoryCacheProducer}
RequestLoggingListener: time 2095596: onProducerFinishWithSuccess: {requestId: 5, producer: EncodedMemoryCacheProducer, elapsedTime: 1 ms, extraMap: {cached_value_found=false}}
RequestLoggingListener: time 2095596: onProducerStart: {requestId: 5, producer: DiskCacheProducer}
RequestLoggingListener: time 2095598: onProducerFinishWithSuccess: {requestId: 5, producer: DiskCacheProducer, elapsedTime: 2 ms, extraMap: {cached_value_found=false}}
RequestLoggingListener: time 2095598: onProducerStart: {requestId: 5, producer: PartialDiskCacheProducer}
RequestLoggingListener: time 2095602: onProducerFinishWithSuccess: {requestId: 5, producer: PartialDiskCacheProducer, elapsedTime: 4 ms, extraMap: {cached_value_found=false}}
We see more of these when the request is handed over to the background (BackgroundThreadHandoffProducer
) and performs look-ups in the caches.
1
2
3
4
RequestLoggingListener: time 2095602: onProducerStart: {requestId: 5, producer: NetworkFetchProducer}
RequestLoggingListener: time 2095745: onProducerEvent: {requestId: 5, stage: NetworkFetchProducer, eventName: intermediate_result; elapsedTime: 143 ms}
RequestLoggingListener: time 2095764: onProducerFinishWithSuccess: {requestId: 5, producer: NetworkFetchProducer, elapsedTime: 162 ms, extraMap: {queue_time=140, total_time=161, image_size=40502, fetch_time=21}}
RequestLoggingListener: time 2095764: onUltimateProducerReached: {requestId: 5, producer: NetworkFetchProducer, elapsedTime: -1 ms, success: true}
For this particular request, the NetworkFetchProducer
is the “ultimate producer”. This means, it is the one that provides the definite input source for fulfilling the request. If the image is cached, the DiskCacheProducer
would be the “ultimate” producer.
1
2
3
RequestLoggingListener: time 2095766: onProducerStart: {requestId: 5, producer: DecodeProducer}
RequestLoggingListener: time 2095786: onProducerFinishWithSuccess: {requestId: 5, producer: DecodeProducer, elapsedTime: 20 ms, extraMap: {imageFormat=JPEG, ,hasGoodQuality=true, bitmapSize=788x525, isFinal=true, requestedImageSize=unknown, encodedImageSize=788x525, sampleSize=1, queueTime=0}
RequestLoggingListener: time 2095788: onRequestSuccess: {requestId: 5, elapsedTime: 198 ms}
On the way up, the DecodeProducer
also succeeds and finally the onRequestSuccess(...)
method is called.
You will notice that most of these methods are given optional information as a Map<String, String> extraMap
. The string constants to look-up the elements are usually public constants in the corresponding producer classes.