All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.day.cq.dam.commons.util.MemoryUtil Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 1997-2008 Day Management AG
 * Barfuesserplatz 6, 4001 Basel, Switzerland
 * All Rights Reserved.
 *
 * This software is the confidential and proprietary information of
 * Day Management AG, ("Confidential Information"). You shall not
 * disclose such Confidential Information and shall use it only in
 * accordance with the terms of the license agreement you entered into
 * with Day.
 */
package com.day.cq.dam.commons.util;

import java.io.IOException;
import java.util.Map;

import javax.imageio.IIOException;

import com.day.cq.dam.api.Asset;
import static com.day.cq.dam.api.DamConstants.EXIF_PIXELXDIMENSION;
import static com.day.cq.dam.api.DamConstants.EXIF_PIXELYDIMENSION;
import static com.day.cq.dam.api.DamConstants.TIFF_IMAGELENGTH;
import static com.day.cq.dam.api.DamConstants.TIFF_IMAGEWIDTH;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class offers some useful memory management functions for asset/image processing
 */
public class MemoryUtil {

    private static final String TIFF_BITS_PER_SAMPLE = "tiff:BitsPerSample";

	private static final String DAM_BITSPERPIXEL = "dam:Bitsperpixel";

	private static final String PHOTOSHOP_COLOR_MODE = "photoshop:ColorMode";

	private static final String TIFF_SAMPLES_PER_PIXEL = "tiff:SamplesPerPixel";

	/**
     * Logger instance for this class.
     */
    private static final Logger log = LoggerFactory
            .getLogger(MemoryUtil.class);

    /**
     * We assume about 100 megs as minimal cq requirement...
     */
    public static long MIN_CQ_MEMORY_REQUIREMENT = 1024 * 1024 * 100;

    private static final int DEFAULT_MAX_TRIALS = 100;

    /**
     * Callback throwing IOException and returning some (image) object
     * for {@link #tryUntilEnoughMemory(com.day.cq.dam.api.Asset, int, com.day.cq.dam.commons.util.MemoryUtil.Callback)}
     */
    public interface Callback {
        T execute() throws IOException;
    }

    /**
     * Tries to execute the callback until no memory exception occurs anymore. Will first check
     * if the asset can be loaded with the given JVM memory settings at all, then go into a
     * loop catching "not enough memory" exceptions and retry until loading worked or a maximum
     * number of retries is reached (100). If it fails, null will be returned.
     *
     * @param asset asset to load Java 2D image from
     * @param callback method that actually loads an image involving Java 2D memory,
     *                 return value will be passed through and returned by this method
     * @param  return type (image object etc.)
     * @return the value returned by the callback if successful, or null
     *         if memory limits prevented loading within a certain number of trials
     */
    public static  T tryUntilEnoughMemory(Asset asset, Callback callback) {
        return tryUntilEnoughMemory(asset, DEFAULT_MAX_TRIALS, callback);
    }

    /**
     * Tries to execute the callback until no memory exception occurs anymore. Will first check
     * if the asset can be loaded with the given JVM memory settings at all, then go into a
     * loop catching "not enough memory" exceptions and retry until loading worked or the given
     * maximum number of retries is reached. If it fails, null will be returned.
     *
     * @param asset asset to load Java 2D image from
     * @param maxTrials maximum number of trials, must be > 0
     * @param callback method that actually loads an image involving Java 2D memory,
     *                 return value will be passed through and returned by this method
     * @param  return type (image object etc.)
     * @return the value returned by the callback if successful, or null
     *         if memory limits prevented loading within a certain number of trials
     */
    public static  T tryUntilEnoughMemory(Asset asset, final int maxTrials, Callback callback) {
        // check if there is enough max memory for this asset at all
        if (!MemoryUtil.hasEnoughSystemMemory(asset)) {
            log.warn(
                "Failed loading image, insufficient memory. Increase heap size up to {} for asset: {}.",
                FileUtils.byteCountToDisplaySize(MemoryUtil.suggestMaxHeapSize(asset)), asset.getPath()
            );
            return null;
        }

        // loop until max trials are done or execution was successful (return)
        int trials = maxTrials <= 0 ? 1 : maxTrials;
        while (trials > 0) {
            try {

                // callback
                return callback.execute();

            } catch (IOException e) {
                // check for not enough memory exception
                if (e instanceof IIOException && e.getMessage().contains("Not enough memory")) {
                    trials--;

                    log.info(
                        "Insufficient memory, reloading image. Free memory: {}, asset: {}",
                        FileUtils.byteCountToDisplaySize(Runtime.getRuntime().freeMemory()), asset.getPath()
                    );

                    // sleep at least 1250ms and maximal 3750ms
                    try {
                        Thread.sleep((long) (2500 * (Math.random() + 0.5)));
                    } catch (InterruptedException ie) {
                        // to avoid repeated trials without pause in between, stop immediately
                        return null;
                    }
                } else {
                    log.warn("Error while loading image for asset {}: ", asset.getPath(), e);
                    return null;
                }
            }
        }

        log.warn(
            "Failed loading image, insufficient memory even after {} trials for asset: {}",
            maxTrials, asset.getPath()
        );
        return null;
    }

    /**
     * Checks if the available max. memory is enough to process the image
     *
     * @param asset asset to check
     * @return true either if the check succeeds or the check cannot
     *         be performed (no image data available e.g. the asset is not an image)
     *         otherwise false
     */
    public static boolean hasEnoughSystemMemory(Asset asset) {
        if (canCalculate(asset)) {
            long expectedImageMem = getExpectedImageMemory(asset);

            // check if minimal memory requirements would work to process the image
            return Runtime.getRuntime().maxMemory() >
                    (MIN_CQ_MEMORY_REQUIREMENT + expectedImageMem);
        } else {
            log.debug("Cannot calculate memory requirements for " + asset.getPath());
            return true;
        }
    }

    /**
     * Calculates the expected image memory consumption. Bytes required by each pixel are calculated from
     * metadata fields like photoshop:ColorMode or tiff:SamplesPerPixel.
     * If this information can be derived from metadata then it is defaulted to 4.
     *
     * @param asset asset to check
     * @return the calculated amount or -1 in case the expected
     * memory cannot be calculated
     */
    public static long getExpectedImageMemory(Asset asset) {
        if (canCalculate(asset)) {

            // TODO: this test is a bit shaky since the info is sometimes not reliable ...
            Long length = getAsLong(asset, EXIF_PIXELXDIMENSION);
            Long width =  getAsLong(asset,EXIF_PIXELYDIMENSION);
            if (length == null && width == null) {
                length = getAsLong(asset, TIFF_IMAGELENGTH);
                width = getAsLong(asset, TIFF_IMAGEWIDTH);
            }

            // rgb
            if (length == null && width == null) {
                return 4 * width * length;
            }
        }
        return -1;
    }
    
	/**
	 * Find pixel size from the asset metadata if available. If it can not
	 * calculate from metadata then returns 4.
	 *
	 * @return pixel size in bytes
	 */
	private static int getPixelSize(Asset asset, Map metadata) {
		int pixelSize = 4;
		int oneByte = 8;

		// don't need to use bytes per channel calculation here, as it already
		// gives bits per pixel
		if (metadata.containsKey(DAM_BITSPERPIXEL)) {
			pixelSize = (int) getAsLong(asset, metadata, DAM_BITSPERPIXEL);
			pixelSize = pixelSize / oneByte;
			return pixelSize;
		}

		int bitsPerChannel = (int) getBitsPerSample(asset, metadata);
		int bytesPerChannel = bitsPerChannel / oneByte;

		// find bytes per pixel this information is available in either of the
		// following properties
		// 1. photoshop:ColorMode
		// 2. tiff:SamplesPerPixel
		// 3. dam:Bitsperpixel
		if (metadata.containsKey(PHOTOSHOP_COLOR_MODE)) {
			int colorMode = (int) getAsLong(asset, metadata,
					PHOTOSHOP_COLOR_MODE);
			switch (colorMode) {
			case 0: // bitmap 1-bit per pixel
			case 1: // greyscale 8-bit per pixel
			case 2: // indexed color more 8bits per pixel
				pixelSize = 1;
				break;
			case 3: // RGB
				pixelSize = 3;
				break;
			case 4: // CMYK
			default:
				pixelSize = 4; // use 4 bytes per pixel as default
				break;
			}
			return pixelSize * bytesPerChannel;
		}

		if (metadata.containsKey(TIFF_SAMPLES_PER_PIXEL)) {
			pixelSize = (int) getAsLong(asset, metadata, TIFF_SAMPLES_PER_PIXEL);
			return pixelSize * bytesPerChannel;
		}

		return pixelSize;
	}
    
	/**
	 * gets number of bits required per channel or per sample in a pixel. Pixel
	 * have different channels like R G B or C M Y K
	 */
	private static long getBitsPerSample(Asset asset,
			Map metadata) {
		Object value = metadata.get(TIFF_BITS_PER_SAMPLE);
		if (value instanceof Long) {
			return (Long) value;
		} else if (value instanceof Object[]) {
			Object[] values = (Object[]) value;
			if (values.length > 0) {
				// take the first sample size (assuming it to be uniform for
				// simplicity)
				return getAsLong(asset, values[0]);
			}
		}

		return 8L;
	}

	private static long getAsLong(Asset asset, Map metadata,
			String name) {
	    String value = asset.getMetadataValue(name);
        Long retValue = null;
        try {
            retValue = Long.parseLong(value);
        } catch (NumberFormatException ignore) {

        }
        return retValue;
	}

	private static long getAsLong(Asset asset, Object value) {
		if (value instanceof Long) {
			return (Long) value;
		} else if (value instanceof Double) {
			return ((Double) value).longValue();
		} else if (value instanceof String) {
			// cast to string
			String strVal = (String) value;
			try {
				return Long.valueOf(strVal);
			} catch (NumberFormatException nme) {
				log.debug("Cannot convert {} to number for asset {}", value,
						asset.getPath());
			}
		}
		return 0L;
	}

    /**
     * Checks if enough memory is available for the asset/image processing
     * Note: it works for images only!
     *
     * @param asset asset to check
     * @return true either if the check succeeds or the check cannot
     *         be performed (no image data available e.g. the asset is not an image)
     *         otherwise false
     */
    public static boolean hasEnoughMemory(Asset asset) {
        if (canCalculate(asset)) {
            long expectedImageMem = getExpectedImageMemory(asset);

            // calculate free memory
            Runtime rt = Runtime.getRuntime();
            //rt.totalMemory() - rt.freeMemory() calculates used memory
            long maxFreeMem = rt.maxMemory() - (rt.totalMemory() - rt.freeMemory());
            return (maxFreeMem >= expectedImageMem);
        } else {
            log.debug("Cannot calculate memory requirements for " + asset.getPath());
            return true;
        }
    }

    /**
     * Suggests the minimal max. heap size
     *
     * @param asset asset to check
     * @return minimal max. heap size
     */
    public static long suggestMaxHeapSize(Asset asset) {
        return getExpectedImageMemory(asset) + MIN_CQ_MEMORY_REQUIREMENT;
    }

    private static boolean canCalculate(Asset asset) {
        return (StringUtils.isNotEmpty(asset.getMetadataValue(TIFF_IMAGELENGTH))
                && StringUtils.isNotEmpty(asset.getMetadataValue(TIFF_IMAGEWIDTH)))
            || (StringUtils.isNotEmpty(asset.getMetadataValue(EXIF_PIXELXDIMENSION))
                    && StringUtils.isNotEmpty(asset.getMetadataValue(EXIF_PIXELYDIMENSION)));
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy