Android Performance: Be careful with byte[]

There are many cases where we use byte[] in our code. In fact, it is the “rawest” type possible in Java unless you go native. Thus, byte arrays are often used to store raw data such as bitmaps, audio and various binary objects.

The previous two articles on MTR were dedicated to audio decoders, including WAV and MP3. In both cases, raw PCM data that was the result of your decoding was a byte array (which you would later write to AudioTrack).

I already mentioned in one of those articles that you should consider streaming any audio that is longer than the reasonable maximum. However, even if your data will definitely fit into the heap, in most cases you can still do better than just using a byte array. Why? Read on (relevant for non-audio byte[]s as well!)

Some low level GC gore

While we Java developers are used to not take JVM heap topology and garbage collection into account too much (and that is generally the correct mindset), on small devices we should be aware of their existence more than on desktops or all the more on the server side. Yes, JVM will do as much as possible for you to fly free, yet it will not be able to handle extreme cases as smoothly without some assistance from your side.

As you remember from the article on decoding WAVs, a 10 second audio will take about 1.7Mb of heap. This is an acceptable size for Android where most devices will allocate either 16Mb or 24Mb maximum per process. However, storing this data as a large byte[] (which is the straightforward way and the first way that comes to one’s mind) is not a good idea and here is why.

Internally, byte arrays are represented as, well, byte arrays. For obvious reasons they require a continuous heap chunk. That is, if your heap is fragmented, JVM will have to run GC in order to defrag the heap – even if technically you had enough space (total!) to fit the amount of data that you needed.

As the result, you will have a lot of objects being transferred back and forth by GC to make room for that big fat array. Things get worse when you have a handful (more than one) large byte arrays that should definitely fit the heap if you look at the numbers (total size and free heap space), but will give GC a real hard time trying to lay them out in the heap space. The more large objects you have, the longer and scarier GC pauses will become.

Splitting byte[] data into chunks

The obvious solution would be to split the large byte[] into chunks. And in this case, the obvious thing to do is also a good one. Here’s a sketch of the ChunkedByteBlob class that implements a nice split byte container:

public final class ChunkedByteBlob {
	private final int totalSize;
	private final int chunkSize;
	private final int totalChunks;
	private final byte[][] chunks;
	
	public ChunkedByteBlob(int totalSize, int chunkSize) {
		if (totalSize <= 0) {
			throw new IllegalArgumentException("totalSize is <= 0: " + totalSize);
		} else if (totalSize < chunkSize) {
			throw new IllegalArgumentException("totalSize " + totalSize + " is < chunkSize " + chunkSize);
		}
		
		this.totalSize = totalSize;
		this.chunkSize = chunkSize;
		this.totalChunks = (int) Math.ceil((double) totalSize / (double) chunkSize);
		this.chunks = new byte[totalChunks][];
		
		int sizeLeft = totalSize;
		int i = 0;
		while (sizeLeft > 0) {
			int nextChunkSize = Math.min(sizeLeft, chunkSize);
			sizeLeft -= nextChunkSize;
			
			chunks[i] = new byte[nextChunkSize];
			i++;
		}
	}
	
	public int totalSize() {
		return totalSize;
	}
	
	public int totalChunks() {
		return totalChunks;
	}
	
	public byte[] chunk(int i) {
		return chunks[i];
	}	
	
	public void fillFrom(InputStream stream) throws IOException {
		for (int i = 0; i < totalChunks(); ++i) {
			int sizeToRead = chunk(i).length;
			int sizeRead = 0;
			boolean done = false;
			
			while (! done) {
				int readSize = stream.read(chunk(i), sizeRead, sizeToRead - sizeRead);
				if (readSize < 0) {
					throw new EOFException("unexpected EOF");					
				}
				sizeRead += readSize;
				
				if (sizeRead >= sizeToRead) {
					done = true;
				}
			}
		}		
	}

	public void writeTo(OutputStream stream) throws IOException {
		for (byte[] chunk : chunks) {
			stream.write(chunk, 0, chunk.length);
		}
	}
}

It should be obvious how this code works, but please do not just copy-paste it – study it, understand it fully and fix the bugs first. :)

Introducing this class in my latest app that deals with a lot of audio data removed a big nasty GC pause that happened every time I loaded another audio sample.

Why no benchmarks?
I intentionally do not provide any benchmarks or numbers here. Optimization stuff like this is highly specific to every use case, and you have to measure the outcome yourself. I believe there are cases where using basic byte[]s would be much better than using the ChunkedByteBlob! The point of this article is to make you aware of the potential problem and know one of the potential solutions to it.

AudioTrack won’t let you do that

I already wrote an article on shortcomings of Android audio API. Not being a big fan of blog rants, I can’t but come back to the fact that Android audio APIs are underdocumented, underdesigned and sometimes just broken.

In our case, the problem is that even if you split your large audio sample nicely into chunks, you can only write() once if your AudioTrack is static. Thus, you won’t be able to write chunks one by one and you will have to keep your entire sample as a single large byte[].

However, this does not affect MODE_STREAM or other use cases.

Conclusion

Some GC awareness is always good. On small devices such as Android phones it’s even better, and when dealing with extra-large objects it’s even better. Do not follow this recommendation blindly, but be aware of the problem and this simple solution that might be helpful for you too.

Tags: , , , , , ,

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>