I've been using the JAudiotagger library for a number of years now, to tag music files in bliss. It's stable and well supported, but while profiling the other day I noticed a potential performance improvement.
JAudiotagger is a Java API for tagging music files. When I first started work on bliss I chose this library for a number of reasons...
I was recently profiling bliss to see how I could cut down on IO and CPU usage to give
an all round faster experience. I hooked up the excellent YourKit
profiler. It showed me a lot of CPU time being spent in a JAudiotagger method, StandardArtwork.setImageFromData()
.
Here's the code:
/** * Should be called when you wish to prime the artwork for saving * * @return */ public boolean setImageFromData() { try { BufferedImage image = (BufferedImage)getImage(); setWidth(image.getWidth()); setHeight(image.getHeight()); } catch(IOException ioe) { return false; } return true; }
The method is called by Tag
implementations so that image metadata can be stored. Image metadata include
such data as the width and the height of the image. Thus, the image is loaded using the Java Image I/O API
and the width and height populated.
The problem is that the Java Image I/O API can be pretty slow, or at least, reading a full
BufferedImage
can be slow, depending on the image. Worse, consider this call is made on a per-tag,
in other words per-file, basis. Common use of this method means setting the same cover art, resulting in a call
to setImageFromData
, for every track in a given album. This means the same image is loaded again
and again.
I worked out a simple efficiency saving here. By passing in my own implementation of Artwork
, I
overrode the setImageFromData()
method to do nothing, and I set the width, height and other metadata before
I passed in the Artwork
instance. Here's the new code (note: "artwork
" is my own
struct for holding image metadata and it contains information found from previously reading the image using
the Java Image I/O API):
Artwork jatArtwork = new StandardArtwork() { @Override public boolean setImageFromData() { // do nothing, already primed and want to avoid overhead of loading the buffered image return true; } }; try { openInputStream = artwork.openInputStream(); if(openInputStream==null) LOG.warn("Attempting to read null " + InputStream.class.getSimpleName() + " from " + artwork.getSource() + ". Will write null artwork"); jatArtwork.setBinaryData(openInputStream == null ? null : IOUtils.toByteArray(openInputStream)); } finally { if(openInputStream!=null) openInputStream.close(); } jatArtwork.setDescription(artwork.getDescription()); jatArtwork.setMimeType(artwork.getMimeType()); jatArtwork.setPictureType(artwork.getPictureType()); jatArtwork.setImageUrl(artwork.getImageUrl()); jatArtwork.setLinked(artwork.isLinked()); if(!artwork.isLinked()) { jatArtwork.setWidth(artwork.getCoverArtLocation().getWidth()); jatArtwork.setHeight(artwork.getCoverArtLocation().getHeight()); }
At least in terms of setting artwork, this means we create a BufferedImage
O(1) times rather
than O(N), a huge difference, especially on those larger albums!
Instead of loading image metadata using BufferedImage
, look at the ImageReader
classes.
These are a lot faster for the simple tasks of reading width and height.
Here's my code to read image dimensions for InputStream
s using this API ("Option
" is
my own type for representing optional values, similar to
Scala's Option and just preferable to returning null):
public static Option<CoverArtDimension> dimensionsOf(InputStream is) throws IOException { if(is==null) return Option.none(); ImageInputStream in = ImageIO.createImageInputStream(is); try { return read(in).map(new Option.Function<ImageReader, CoverArtDimension>() { @Override public CoverArtDimension run(ImageReader reader) { try { return new CoverArtDimension(reader.getWidth(0), reader.getHeight(0)); } catch (IOException e) { throw Throwables.propagate(e); } finally { reader.dispose(); } } }); } finally { if (in != null) in.close(); } } /** * Return an {@link ImageReader} for the {@link ImageInputStream}, if one can be found. * Clients must dispose() of the {@link ImageReader} once they are finished with it. They * should also close the {@link ImageInputStream} they passed in. * @param in * @return * @throws IOException */ private static Option<ImageReader> read(ImageInputStream in) throws IOException { final Iterator readers = ImageIO.getImageReaders(in); while (readers.hasNext()) { ImageReader reader = (ImageReader) readers.next(); reader.setInput(in); return Option.some(reader); } return Option.none(); }
HTH!
Thanks to JD Hancock for the image above.