PImage Memory Leak Example
in
Core Library Questions
•
11 months ago
This problem may have first been discussed in the post:
http://forum.processing.org/topic/another-memory-leak-in-pimage.
I have a small, concrete example running in Processing 2.0 that demonstrates the problem. I have tested the code in Processing 1.5.1, where there is no problem. I have confirmed that Java garbage collection works correctly in 1.5.1 using JConsole to monitor the Java heap. JConsole confirms that no garbage collection is taking place in 2.0. I have also used Eclipse Memory Analyzer to inspect a heap dump from both versions of my code, running under 1.5.1 and running under 2.0b6, current as of today.
I am testing garbage collection by rotating and/or resizing an image. When I load an image that is 800 x 1181 pixels and zoom it to fit the screen ('F' key), I can rotate it ('T' key) about 120 or so times and then I will get a crash:
Exception in thread "Animation Thread" java.lang.OutOfMemoryError: Java heap space
at processing.core.PImage.init(PImage.java:235)
at processing.core.PImage.<init>(PImage.java:221)
at processing.core.PApplet.createImage(PApplet.java:1824)
at ImageLeakTest2x.rotateImage(ImageLeakTest2x.java:209)
at ImageLeakTest2x.rotatePixels(ImageLeakTest2x.java:192)
at ImageLeakTest2x.decode(ImageLeakTest2x.java:158)
at ImageLeakTest2x.keyPressed(ImageLeakTest2x.java:135)
at processing.core.PApplet.handleKeyEvent(PApplet.java:2756)
at processing.core.PApplet.dequeueKeyEvents(PApplet.java:2699)
at processing.core.PApplet.handleDraw(PApplet.java:2140)
at processing.core.PGraphicsJava2D.requestDraw(PGraphicsJava2D.java:190)
at processing.core.PApplet.run(PApplet.java:2006)
at java.lang.Thread.run(Thread.java:680)
This is very obvious in JConsole, where the heap grows and never shrinks. It does shrink when the same code is run in Processing 1.5.1, and does not crash.
Here's the code:
Maybe I am misunderstanding garbage collection and it's really version 1.5.1 that has things wrong, but it's not obvious to me. I would say that newImage (in rotateImage) goes out of scope, should be garbage collected, and that pointing img at the return value from rotateImage should not make any difference. Instead, each call to rotateImage leaves an image on the heap and it just stays there, glowering.
I await good answers from my smart friends.
cheers,
-- Paul
----- |(*,+,#,=)(#,=,*,+)(=,#,+,*)(+,*,=,#)| ---
I have a small, concrete example running in Processing 2.0 that demonstrates the problem. I have tested the code in Processing 1.5.1, where there is no problem. I have confirmed that Java garbage collection works correctly in 1.5.1 using JConsole to monitor the Java heap. JConsole confirms that no garbage collection is taking place in 2.0. I have also used Eclipse Memory Analyzer to inspect a heap dump from both versions of my code, running under 1.5.1 and running under 2.0b6, current as of today.
I am testing garbage collection by rotating and/or resizing an image. When I load an image that is 800 x 1181 pixels and zoom it to fit the screen ('F' key), I can rotate it ('T' key) about 120 or so times and then I will get a crash:
Exception in thread "Animation Thread" java.lang.OutOfMemoryError: Java heap space
at processing.core.PImage.init(PImage.java:235)
at processing.core.PImage.<init>(PImage.java:221)
at processing.core.PApplet.createImage(PApplet.java:1824)
at ImageLeakTest2x.rotateImage(ImageLeakTest2x.java:209)
at ImageLeakTest2x.rotatePixels(ImageLeakTest2x.java:192)
at ImageLeakTest2x.decode(ImageLeakTest2x.java:158)
at ImageLeakTest2x.keyPressed(ImageLeakTest2x.java:135)
at processing.core.PApplet.handleKeyEvent(PApplet.java:2756)
at processing.core.PApplet.dequeueKeyEvents(PApplet.java:2699)
at processing.core.PApplet.handleDraw(PApplet.java:2140)
at processing.core.PGraphicsJava2D.requestDraw(PGraphicsJava2D.java:190)
at processing.core.PApplet.run(PApplet.java:2006)
at java.lang.Thread.run(Thread.java:680)
This is very obvious in JConsole, where the heap grows and never shrinks. It does shrink when the same code is run in Processing 1.5.1, and does not crash.
Here's the code:
- // testing a memory leak in PImage
// by Paul Hertz, 2012
// http://paulhertz.net/
// press 'o' to open a file
// press 's' to save a timestamped .png file
// press 'r' to revert to the current file
// press 't' to turn the image 90 clockwise
// press 'v' to turn verbose output on and off
// press 'h' to show help message
import java.awt.Container;
import java.awt.Frame;
import java.io.*;
/** the currently selected file */
File selectedFile;
/** the primary image to display and glitch */
PImage img;
/** a version of the image scaled to fit the screen dimensions, for display only */
PImage fitImg;
/** true if image should fit screen, otherwise false */
boolean isFitToScreen = false;
/** maximum width for the display window */
int maxWindowWidth;
/** maximum height for the display window */
int maxWindowHeight;
/** width of the image when scaled to fit the display window */
int scaledWidth;
/** height of the image when scaled to fit the display window */
int scaledHeight;
/** current width of the frame (display window) */
int frameWidth;
/** current height of the frame (display window) */
int frameHeight;
/** reference to the frame (display window) */
Frame myFrame;
/** */
boolean verbose = false;
// file count for filenames in sequence
int fileCount = 0;
// timestamp for filenames
String timestamp;
public void setup() {
// println("Screen: "+ screenWidth +", "+ screenHeight);
println("Display: "+ displayWidth +", "+ displayHeight);
size(640, 480);
smooth();
// max window width is the screen width
maxWindowWidth = displayWidth;
// leave window height some room for title bar, etc.
maxWindowHeight = displayHeight - 56;
// image to display
img = createImage(width, height, ARGB);
// okay now to open an image file
openFile();
// Processing initializes the frame and hands it to you in the "frame" field.
// Eclipse does things differently. Use findFrame method to get the frame in Eclipse.
myFrame = findFrame();
myFrame.setResizable(true);
// the first time around, window won't be resized, a reload should resize it
revert();
// initialize timestamp, used in filename
// timestamp = year() + nf(month(),2) + nf(day(),2) + "-" + nf(hour(),2) + nf(minute(),2) + nf(second(),2);
timestamp = nf(day(),2) + nf(hour(),2) + nf(minute(),2) + nf(second(),2);
printHelp();
}
/**
* @return Frame where Processing draws, useful method in Eclipse
*/
public Frame findFrame() {
Container f = this.getParent();
while (!(f instanceof Frame) && f!=null)
f = f.getParent();
return (Frame) f;
}
/**
* Prints help message to the console
*/
public void printHelp() {
println("press 'o' to open a file");
println("press 's' to save a timestamped .png file");
println("press 'r' to revert to the current file");
println("press 't' to turn the image 90 clockwise");
println("press 'h' to show help message");
}
public void draw() {
if (isFitToScreen) {
image(fitImg, 0, 0);
}
else {
background(255);
image(img, 0, 0);
}
}
/* (non-Javadoc)
* handles key presses intended as commands
* @see processing.core.PApplet#keyPressed()
*/
public void keyPressed() {
if (key != CODED) {
decode(key);
}
}
/**
* associates characters input from keyboard with commands
* @param ch a char value representing a command
*/
public void decode(char ch) {
if (ch == 'o' || ch == 'O') {
openFile();
}
else if (ch == 'v' || ch == 'V') {
verbose = !verbose;
println("verbose is "+ verbose);
}
else if (ch == 's' || ch == 'S') {
saveFile();
}
else if (ch == 'r' || ch == 'R') {
revert();
}
else if (ch == 't' || ch == 'T') {
rotatePixels();
}
else if (ch == 'h' || ch == 'H') {
printHelp();
}
else if (ch == 'f' || ch == 'F') {
fitPixels(!isFitToScreen);
}
}
/********************************************/
/* */
/* >>> COMMANDS <<< */
/* */
/********************************************/
/**
* reverts display and display buffer to last opened file
*/
public void revert() {
if (null != selectedFile) {
loadFile();
if (isFitToScreen) fitPixels(true);
}
}
int rCount = 0;
/**
* rotates image and backup image 90 degrees clockwise
*/
public void rotatePixels() {
if (null == img) return;
img = rotateImage(img);
fitPixels(isFitToScreen);
// rotate undo buffer image, don't rotate snapshot
println(rCount++);
}
/**
* rotates image pixels 90 degrees clockwise
* @param image the image to rotate
* @return the rotated image
*/
public PImage rotateImage(PImage image) {
// rotate image 90 degrees
image.loadPixels();
int h = image.height;
int w = image.width;
int i = 0;
PImage newImage = createImage(h, w, ARGB);
newImage.loadPixels();
for (int ry = 0; ry < w; ry++) {
for (int rx = 0; rx < h; rx++) {
newImage.pixels[i++] = image.pixels[(h - 1 - rx) * image.width + ry];
}
}
newImage.updatePixels();
return newImage;
}
/**
* Fits large images to the screen, zooms small images to full screen. Displays as much of a large image
* as fits the screen if every pixel is displayed.
*
* @param fitToScreen true if image should be fit to screen, false if every pixel should displayed
*/
public void fitPixels(boolean fitToScreen) {
if (fitToScreen) {
fitImg = createImage(img.width, img.height, ARGB);
scaledWidth = fitImg.width;
scaledHeight = fitImg.height;
fitImg.copy(img, 0, 0, img.width, img.height, 0, 0, img.width, img.height);
// calculate proportions of window and image,
// be sure to convert ints to floats to get the math right
// ratio of the window height to the window width
float windowRatio = maxWindowHeight/(float)maxWindowWidth;
// ratio of the image height to the image width
float imageRatio = fitImg.height/(float)fitImg.width;
if (verbose) {
println("maxWindowWidth "+ maxWindowWidth +", maxWindowHeight "+ maxWindowHeight +", screen ratio "+ windowRatio);
println("image width "+ fitImg.width +", image height "+ fitImg.height +", image ratio "+ imageRatio);
}
if (imageRatio > windowRatio) {
// image is proportionally taller than the display window,
// so scale image height to fit the window height
scaledHeight = maxWindowHeight;
// and scale image width by window height divided by image height
scaledWidth = Math.round(fitImg.width * (maxWindowHeight / (float)fitImg.height));
}
else {
// image is proportionally equal to or wider than the display window,
// so scale image width to fit the windwo width
scaledWidth = maxWindowWidth;
// and scale image height by window width divided by image width
scaledHeight = Math.round(fitImg.height * (maxWindowWidth / (float)fitImg.width));
}
fitImg.resize(scaledWidth, scaledHeight);
if (null != myFrame) myFrame.setSize(scaledWidth, scaledHeight + 48);
}
else {
scaledWidth = img.width;
scaledHeight = img.height;
if (null != myFrame) {
frameWidth = scaledWidth <= maxWindowWidth ? scaledWidth : maxWindowWidth;
frameHeight = scaledHeight <= maxWindowHeight ? scaledHeight : maxWindowHeight;
myFrame.setSize(frameWidth, frameHeight + 38);
}
}
if (verbose) println("scaledWidth = "+ scaledWidth +", scaledHeight = "+ scaledHeight +", frameWidth = "+ frameWidth +", frameHeight = "+ frameHeight);
isFitToScreen = fitToScreen;
}
/********************************************/
/* */
/* >>> FILE I/O <<< */
/* */
/********************************************/
/**
* opens a user-specified file (JPEG, GIF or PNG only)
*/
public void openFile() {
selectInput("Choose an image file.", "fileSelected");
}
void fileSelected(File selection) {
if (selection == null) {
println("----- Window was closed or the user hit cancel.");
}
else {
selectedFile = selection;
println("User selected " + selection.getAbsolutePath());
loadFile();
}
}
/**
* saves current image to a uniquely-named file
*/
public void saveFile() {
String shortName = selectedFile.getName();
String[] parts = shortName.split("\\.");
String fName = parts[0] +"_"+ timestamp +"_"+ fileCount +".png";
fileCount++;
println("saving "+ fName);
img.save(fName);
}
/**
* loads a file into variable img.
*/
public void loadFile() {
println("selected file "+ selectedFile.getAbsolutePath());
img = loadImage(selectedFile.getAbsolutePath());
fitPixels(isFitToScreen);
println("image width "+ img.width +", image height "+ img.height);
}
Maybe I am misunderstanding garbage collection and it's really version 1.5.1 that has things wrong, but it's not obvious to me. I would say that newImage (in rotateImage) goes out of scope, should be garbage collected, and that pointing img at the return value from rotateImage should not make any difference. Instead, each call to rotateImage leaves an image on the heap and it just stays there, glowering.
I await good answers from my smart friends.
cheers,
-- Paul
----- |(*,+,#,=)(#,=,*,+)(=,#,+,*)(+,*,=,#)| ---
1