/* * MusicPage.java * * Copyright (C) 2006-2011 Gabriel Burca (gburca dash virtmus at ebixio dot com) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package com.ebixio.virtmus; import com.ebixio.util.Log; import com.ebixio.util.NamedThreadFactory; import com.ebixio.virtmus.options.Options; import java.awt.Dimension; import java.awt.image.BufferedImage; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.PriorityBlockingQueue; /** * * @author Gabriel Burca <gburca dash virtmus at ebixio dot com> * * A MusicPage renderer. */ public class Renderer { private static transient PriorityBlockingQueue<JobRequest> renderQ = new PriorityBlockingQueue<JobRequest>(); private static transient Map<JobRequester, BufferedImage> renderResults = Collections.synchronizedMap( new HashMap<JobRequester, BufferedImage>() ); private static transient RenderThread renderThread = null; // Heap space must be increased proportional to the thread pool size private static transient ExecutorService execSvc; static { int numProcessors = Runtime.getRuntime().availableProcessors(); execSvc = Executors.newFixedThreadPool(numProcessors, new NamedThreadFactory("PageRenderers")); } // private static MusicPageRenderer instance; // public synchronized static MusicPageRenderer single() { // if (instance == null) { // instance = new MusicPageRenderer(); // } // return instance; // } public static BufferedImage getRenderedImage(JobRequester requester) { return renderResults.remove(requester); } public boolean requestRendering1(JobRequest request, MusicPage page) { // TODO: Consider switching to org.openide.util.RequestProcessor request.page = page; cancelRendering(request.requester); Renderer.renderQ.add(request); // TODO: Allow multiple threads if (renderThread == null || !renderThread.isAlive()) { renderThread = new RenderThread(); renderThread.setName("MusicPage render"); renderThread.start(); } return true; } public static void cancelRendering(JobRequester requester) { // Remove all previous jobs requested by this same requester for (JobRequest j: renderQ.toArray(new JobRequest[0])) { if (j.requester == requester) renderQ.remove(j); } } public static boolean requestRendering(JobRequest request, MusicPage page) { request.page = page; execSvc.execute(new RenderRunnable(request)); return true; } public interface JobRequester { public void renderingComplete(MusicPage mp, JobRequest jr); } public static class JobRequest implements Comparable { public static final int MAX_PRIORITY = 10; /** This is needed because all job requests get dumped into a static * render queue and when retrieved from the queue we need to know which * page should be asked to provide the image. */ public MusicPage page; public int pageNr; public JobRequester requester; public Integer priority; public Dimension dim; public Options.Rotation rotation = Options.Rotation.Clockwise_0; public boolean fillSize = false; /** * Creates a new job request to render an image. * @param requester Entity to be notified when the image is ready * @param pageNr The page number to render * @param priority The priority with which to render (0 .. MAX_PRIORITY). 0 = fastest. * @param dim The dimension to render at */ public JobRequest(JobRequester requester, int pageNr, int priority, Dimension dim) { this.requester = requester; this.pageNr = pageNr; this.priority = priority < 0 ? 0 : Math.min(priority, MAX_PRIORITY); this.dim = dim; } @Override public int compareTo(Object other) { return this.priority.compareTo( ((JobRequest)other).priority); } } /** Using a single thread to handle all the page rendering so that we don't * have to worry about making getImage thread safe or synchronized and so that * we can prioritize the renderings in one place. */ private class RenderThread extends Thread { @Override public void run() { while (!renderQ.isEmpty()) { JobRequest j = renderQ.poll(); this.setName("Rendering " + j.page.getName()); int maxPriority = this.getThreadGroup().getMaxPriority(); float relativePriority = 1.0F - (j.priority / JobRequest.MAX_PRIORITY); relativePriority = maxPriority * relativePriority; relativePriority = Math.max(Math.round(relativePriority), Thread.MIN_PRIORITY); relativePriority = Math.min(relativePriority, Thread.MAX_PRIORITY); this.setPriority((int)relativePriority); renderResults.put(j.requester, j.page.getImage(j.dim, j.rotation, j.fillSize)); j.requester.renderingComplete(j.page, j); } this.setName("MusicPage render"); } } private static class RenderRunnable extends Thread { JobRequest j; public RenderRunnable(JobRequest jr) { j = jr; } @Override public void run() { BufferedImage img = null; for (int i= 0; i < 3; i++) { try { img = j.page.getImage(j.dim, j.rotation, j.fillSize); break; } catch (Throwable e) { if (!(e instanceof OutOfMemoryError)) { Log.log(e); break; } Log.log("Caught OOM !!!!! " + j.pageNr + " in file " + j.page.imgSrc.sourceFile); try { sleep(250); // Wait a little for some memory to hopefully free up. // Gives other threads a chance to finish and release memory. } catch (InterruptedException ex) { } } } renderResults.put(j.requester, img); j.requester.renderingComplete(j.page, j); } } }