com.day.cq.dam.commons.util.MemoryUtil Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of aem-sdk-api Show documentation
Show all versions of aem-sdk-api Show documentation
The Adobe Experience Manager SDK
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)));
}
}