Back to Repositories

Testing Bitmap Pre-filling Memory Management in Glide

A comprehensive test suite for Glide’s BitmapPreFiller component that validates bitmap memory allocation, caching strategies, and configuration management. The tests ensure proper bitmap pre-filling behavior for memory optimization in Android applications.

Test Coverage Overview

The test suite provides extensive coverage of the BitmapPreFiller functionality, focusing on bitmap allocation ordering and memory management.

  • Validates allocation order generation for different bitmap sizes and configurations
  • Tests memory pool and cache size calculations
  • Verifies weight-based allocation distribution
  • Ensures proper handling of bitmap configurations

Implementation Analysis

The implementation uses JUnit with Robolectric for Android-specific testing, employing mock objects to simulate bitmap pool and memory cache behavior.

Key patterns include:
  • Mock-based dependency injection for BitmapPool and MemoryCache
  • Precise bitmap size calculations and memory allocation verification
  • Round-robin testing for allocation distribution

Technical Details

Testing infrastructure includes:

  • JUnit 4 test framework
  • Robolectric for Android runtime simulation
  • Mockito for mock object creation and verification
  • Google Truth for advanced assertions
  • Custom bitmap creation utilities

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through comprehensive setup and verification approaches.

  • Thorough setup and teardown management
  • Extensive edge case coverage
  • Clear test method naming and organization
  • Effective use of mock objects and verification

bumptech/glide

library/test/src/test/java/com/bumptech/glide/load/engine/prefill/BitmapPreFillerTest.java

            
package com.bumptech.glide.load.engine.prefill;

import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.graphics.Bitmap;
import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.engine.cache.MemoryCache;
import com.bumptech.glide.tests.Util.CreateBitmap;
import com.bumptech.glide.util.Util;
import com.google.common.collect.Range;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

@RunWith(RobolectricTestRunner.class)
@Config(sdk = ROBOLECTRIC_SDK)
public class BitmapPreFillerTest {
  private static final int DEFAULT_BITMAP_WIDTH = 100;
  private static final int DEFAULT_BITMAP_HEIGHT = 50;

  private static final int BITMAPS_IN_POOL = 10;
  private static final int BITMAPS_IN_CACHE = 10;

  private final Bitmap.Config defaultBitmapConfig = PreFillType.DEFAULT_CONFIG;
  private final Bitmap defaultBitmap =
      Bitmap.createBitmap(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT, defaultBitmapConfig);
  private final long defaultBitmapSize = Util.getBitmapByteSize(defaultBitmap);
  private final long poolSize = BITMAPS_IN_CACHE * defaultBitmapSize;
  private final long cacheSize = BITMAPS_IN_POOL * defaultBitmapSize;

  @Mock private BitmapPool pool;
  @Mock private MemoryCache cache;
  private BitmapPreFiller bitmapPreFiller;

  @Before
  public void setUp() {
    MockitoAnnotations.initMocks(this);
    when(pool.getMaxSize()).thenReturn(poolSize);
    when(pool.getDirty(anyInt(), anyInt(), any(Bitmap.Config.class)))
        .thenAnswer(new CreateBitmap());
    when(cache.getMaxSize()).thenReturn(cacheSize);

    bitmapPreFiller = new BitmapPreFiller(cache, pool, DecodeFormat.DEFAULT);
  }

  @Test
  public void testAllocationOrderContainsEnoughSizesToFillPoolAndMemoryCache() {
    PreFillQueue allocationOrder =
        bitmapPreFiller.generateAllocationOrder(
            new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT)
                .setConfig(defaultBitmapConfig)
                .build());

    assertEquals(BITMAPS_IN_POOL + BITMAPS_IN_CACHE, allocationOrder.getSize());
  }

  @Test
  public void testAllocationOrderThatDoesNotFitExactlyIntoGivenSizeRoundsDown() {
    PreFillType[] sizes =
        new PreFillType[] {
          new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT)
              .setConfig(defaultBitmapConfig)
              .build(),
          new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT)
              .setConfig(defaultBitmapConfig)
              .build(),
          new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT / 2)
              .setConfig(defaultBitmapConfig)
              .build(),
        };
    PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder(sizes);

    int byteSize = 0;
    while (!allocationOrder.isEmpty()) {
      PreFillType current = allocationOrder.remove();
      byteSize +=
          Util.getBitmapByteSize(current.getWidth(), current.getHeight(), current.getConfig());
    }

    int expectedSize = 0;
    long maxSize = poolSize + cacheSize;
    for (PreFillType current : sizes) {
      int currentSize =
          Util.getBitmapByteSize(current.getWidth(), current.getHeight(), current.getConfig());
      // See https://errorprone.info/bugpattern/NarrowingCompoundAssignment.
      expectedSize = (int) (expectedSize + (currentSize * (maxSize / (3 * currentSize))));
    }

    assertEquals(expectedSize, byteSize);
  }

  @Test
  public void testAllocationOrderDoesNotOverFillWithMultipleSizes() {
    PreFillQueue allocationOrder =
        bitmapPreFiller.generateAllocationOrder(
            new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT)
                .setConfig(defaultBitmapConfig)
                .build(),
            new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT)
                .setConfig(defaultBitmapConfig)
                .build(),
            new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT / 2)
                .setConfig(defaultBitmapConfig)
                .build());

    long byteSize = 0;
    while (!allocationOrder.isEmpty()) {
      PreFillType current = allocationOrder.remove();
      byteSize +=
          Util.getBitmapByteSize(current.getWidth(), current.getHeight(), current.getConfig());
    }

    assertThat(byteSize).isIn(Range.atMost(poolSize + cacheSize));
  }

  @Test
  public void testAllocationOrderDoesNotOverFillWithMultipleSizesAndWeights() {
    PreFillQueue allocationOrder =
        bitmapPreFiller.generateAllocationOrder(
            new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT)
                .setConfig(defaultBitmapConfig)
                .setWeight(4)
                .build(),
            new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT)
                .setConfig(defaultBitmapConfig)
                .build(),
            new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT / 3)
                .setConfig(defaultBitmapConfig)
                .setWeight(3)
                .build());

    long byteSize = 0;
    while (!allocationOrder.isEmpty()) {
      PreFillType current = allocationOrder.remove();
      byteSize +=
          Util.getBitmapByteSize(current.getWidth(), current.getHeight(), current.getConfig());
    }

    assertThat(byteSize).isIn(Range.atMost(poolSize + cacheSize));
  }

  @Test
  public void testAllocationOrderContainsSingleSizeIfSingleSizeIsProvided() {
    PreFillQueue allocationOrder =
        bitmapPreFiller.generateAllocationOrder(
            new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT)
                .setConfig(defaultBitmapConfig)
                .build());

    while (!allocationOrder.isEmpty()) {
      PreFillType size = allocationOrder.remove();
      assertEquals(DEFAULT_BITMAP_WIDTH, size.getWidth());
      assertEquals(DEFAULT_BITMAP_HEIGHT, size.getHeight());
      assertEquals(defaultBitmapConfig, size.getConfig());
    }
  }

  @Test
  public void testAllocationOrderSplitsEvenlyBetweenEqualSizesWithEqualWeights() {
    PreFillType smallWidth =
        new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT)
            .setConfig(defaultBitmapConfig)
            .build();
    PreFillType smallHeight =
        new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT / 2)
            .setConfig(defaultBitmapConfig)
            .build();
    PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder(smallWidth, smallHeight);

    int numSmallWidth = 0;
    int numSmallHeight = 0;
    while (!allocationOrder.isEmpty()) {
      PreFillType current = allocationOrder.remove();
      if (smallWidth.equals(current)) {
        numSmallWidth++;
      } else if (smallHeight.equals(current)) {
        numSmallHeight++;
      } else {
        fail("Unexpected size, size: " + current);
      }
    }

    assertEquals(numSmallWidth, numSmallHeight);
  }

  @Test
  public void testAllocationOrderSplitsByteSizeEvenlyBetweenUnEqualSizesWithEqualWeights() {
    PreFillType smallWidth =
        new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT)
            .setConfig(defaultBitmapConfig)
            .build();
    PreFillType normal =
        new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT)
            .setConfig(defaultBitmapConfig)
            .build();
    PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder(smallWidth, normal);

    int numSmallWidth = 0;
    int numNormal = 0;
    while (!allocationOrder.isEmpty()) {
      PreFillType current = allocationOrder.remove();
      if (smallWidth.equals(current)) {
        numSmallWidth++;
      } else if (normal.equals(current)) {
        numNormal++;
      } else {
        fail("Unexpected size, size: " + current);
      }
    }

    assertEquals(2 * numNormal, numSmallWidth);
  }

  @Test
  public void testAllocationOrderSplitsByteSizeUnevenlyBetweenEqualSizesWithUnequalWeights() {
    PreFillType doubleWeight =
        new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT)
            .setConfig(defaultBitmapConfig)
            .setWeight(2)
            .build();
    PreFillType normal =
        new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT / 2)
            .setConfig(defaultBitmapConfig)
            .build();
    PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder(doubleWeight, normal);

    int numDoubleWeight = 0;
    int numNormal = 0;
    while (!allocationOrder.isEmpty()) {
      PreFillType current = allocationOrder.remove();
      if (doubleWeight.equals(current)) {
        numDoubleWeight++;
      } else if (normal.equals(current)) {
        numNormal++;
      } else {
        fail("Unexpected size, size: " + current);
      }
    }

    assertEquals(2 * numNormal, numDoubleWeight);
  }

  @Test
  public void testAllocationOrderRoundRobinsDifferentSizes() {
    when(pool.getMaxSize()).thenReturn(defaultBitmapSize);
    when(cache.getMaxSize()).thenReturn(defaultBitmapSize);
    PreFillType smallWidth =
        new PreFillType.Builder(DEFAULT_BITMAP_WIDTH / 2, DEFAULT_BITMAP_HEIGHT)
            .setConfig(defaultBitmapConfig)
            .build();
    PreFillType smallHeight =
        new PreFillType.Builder(DEFAULT_BITMAP_WIDTH, DEFAULT_BITMAP_HEIGHT / 2)
            .setConfig(defaultBitmapConfig)
            .build();

    PreFillQueue allocationOrder = bitmapPreFiller.generateAllocationOrder(smallWidth, smallHeight);

    List<PreFillType> attributes = new ArrayList<>();
    while (!allocationOrder.isEmpty()) {
      attributes.add(allocationOrder.remove());
    }

    // Either width, height, width, height or height, width, height, width.
    try {
      assertThat(attributes)
          .containsExactly(smallWidth, smallHeight, smallWidth, smallHeight)
          .inOrder();
    } catch (AssertionError e) {
      assertThat(attributes)
          .containsExactly(smallHeight, smallWidth, smallHeight, smallWidth)
          .inOrder();
    }
  }

  @Test
  @SuppressWarnings("deprecation")
  public void testSetsConfigOnBuildersToDefaultIfNotSet() {
    PreFillType.Builder builder = mock(PreFillType.Builder.class);
    when(builder.build())
        .thenReturn(new PreFillType.Builder(100).setConfig(Bitmap.Config.RGB_565).build());

    bitmapPreFiller.preFill(builder);

    InOrder order = inOrder(builder);
    order
        .verify(builder)
        .setConfig(
            DecodeFormat.DEFAULT == DecodeFormat.PREFER_ARGB_8888
                ? Bitmap.Config.ARGB_8888
                : Bitmap.Config.RGB_565);
    order.verify(builder).build();
  }

  @Test
  public void testDoesNotSetConfigOnBuildersIfConfigIsAlreadySet() {
    PreFillType.Builder builder = mock(PreFillType.Builder.class);

    when(builder.getConfig()).thenReturn(Bitmap.Config.ARGB_4444);
    when(builder.build())
        .thenReturn(new PreFillType.Builder(100).setConfig(Bitmap.Config.ARGB_4444).build());
    bitmapPreFiller.preFill(builder);

    verify(builder, never()).setConfig(any(Bitmap.Config.class));
  }
}