blob: cf5b2966d8891b2f211fe233cc2dafa2d0dd59de [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.util;
import android.annotation.NonNull;
import android.util.CharsetUtils;
import dalvik.system.VMRuntime;
import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.Flushable;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Objects;
/**
* Optimized implementation of {@link DataOutput} which buffers data in memory
* before flushing to the underlying {@link OutputStream}.
* <p>
* Benchmarks have demonstrated this class is 2x more efficient than using a
* {@link DataOutputStream} with a {@link BufferedOutputStream}.
*/
public class FastDataOutput implements DataOutput, Flushable, Closeable {
private static final int MAX_UNSIGNED_SHORT = 65_535;
private final VMRuntime mRuntime;
private final OutputStream mOut;
private final byte[] mBuffer;
private final long mBufferPtr;
private final int mBufferCap;
private int mBufferPos;
/**
* Values that have been "interned" by {@link #writeInternedUTF(String)}.
*/
private HashMap<String, Short> mStringRefs = new HashMap<>();
public FastDataOutput(@NonNull OutputStream out, int bufferSize) {
mRuntime = VMRuntime.getRuntime();
mOut = Objects.requireNonNull(out);
if (bufferSize < 8) {
throw new IllegalArgumentException();
}
mBuffer = (byte[]) mRuntime.newNonMovableArray(byte.class, bufferSize);
mBufferPtr = mRuntime.addressOf(mBuffer);
mBufferCap = mBuffer.length;
}
private void drain() throws IOException {
if (mBufferPos > 0) {
mOut.write(mBuffer, 0, mBufferPos);
mBufferPos = 0;
}
}
@Override
public void flush() throws IOException {
drain();
mOut.flush();
}
@Override
public void close() throws IOException {
mOut.close();
}
@Override
public void write(int b) throws IOException {
writeByte(b);
}
@Override
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
if (mBufferCap < len) {
drain();
mOut.write(b, off, len);
} else {
if (mBufferCap - mBufferPos < len) drain();
System.arraycopy(b, off, mBuffer, mBufferPos, len);
mBufferPos += len;
}
}
@Override
public void writeUTF(String s) throws IOException {
// Attempt to write directly to buffer space if there's enough room,
// otherwise fall back to chunking into place
if (mBufferCap - mBufferPos < 2 + s.length()) drain();
// Magnitude of this returned value indicates the number of bytes
// required to encode the string; sign indicates success/failure
int len = CharsetUtils.toModifiedUtf8Bytes(s, mBufferPtr, mBufferPos + 2, mBufferCap);
if (Math.abs(len) > MAX_UNSIGNED_SHORT) {
throw new IOException("Modified UTF-8 length too large: " + len);
}
if (len >= 0) {
// Positive value indicates the string was encoded into the buffer
// successfully, so we only need to prefix with length
writeShort(len);
mBufferPos += len;
} else {
// Negative value indicates buffer was too small and we need to
// allocate a temporary buffer for encoding
len = -len;
final byte[] tmp = (byte[]) mRuntime.newNonMovableArray(byte.class, len + 1);
CharsetUtils.toModifiedUtf8Bytes(s, mRuntime.addressOf(tmp), 0, tmp.length);
writeShort(len);
write(tmp, 0, len);
}
}
/**
* Write a {@link String} value with the additional signal that the given
* value is a candidate for being canonicalized, similar to
* {@link String#intern()}.
* <p>
* Canonicalization is implemented by writing each unique string value once
* the first time it appears, and then writing a lightweight {@code short}
* reference when that string is written again in the future.
*
* @see FastDataInput#readInternedUTF()
*/
public void writeInternedUTF(@NonNull String s) throws IOException {
Short ref = mStringRefs.get(s);
if (ref != null) {
writeShort(ref);
} else {
writeShort(MAX_UNSIGNED_SHORT);
writeUTF(s);
// We can only safely intern when we have remaining values; if we're
// full we at least sent the string value above
ref = (short) mStringRefs.size();
if (ref < MAX_UNSIGNED_SHORT) {
mStringRefs.put(s, ref);
}
}
}
@Override
public void writeBoolean(boolean v) throws IOException {
writeByte(v ? 1 : 0);
}
@Override
public void writeByte(int v) throws IOException {
if (mBufferCap - mBufferPos < 1) drain();
mBuffer[mBufferPos++] = (byte) ((v >> 0) & 0xff);
}
@Override
public void writeShort(int v) throws IOException {
if (mBufferCap - mBufferPos < 2) drain();
mBuffer[mBufferPos++] = (byte) ((v >> 8) & 0xff);
mBuffer[mBufferPos++] = (byte) ((v >> 0) & 0xff);
}
@Override
public void writeChar(int v) throws IOException {
writeShort((short) v);
}
@Override
public void writeInt(int v) throws IOException {
if (mBufferCap - mBufferPos < 4) drain();
mBuffer[mBufferPos++] = (byte) ((v >> 24) & 0xff);
mBuffer[mBufferPos++] = (byte) ((v >> 16) & 0xff);
mBuffer[mBufferPos++] = (byte) ((v >> 8) & 0xff);
mBuffer[mBufferPos++] = (byte) ((v >> 0) & 0xff);
}
@Override
public void writeLong(long v) throws IOException {
if (mBufferCap - mBufferPos < 8) drain();
int i = (int) (v >> 32);
mBuffer[mBufferPos++] = (byte) ((i >> 24) & 0xff);
mBuffer[mBufferPos++] = (byte) ((i >> 16) & 0xff);
mBuffer[mBufferPos++] = (byte) ((i >> 8) & 0xff);
mBuffer[mBufferPos++] = (byte) ((i >> 0) & 0xff);
i = (int) v;
mBuffer[mBufferPos++] = (byte) ((i >> 24) & 0xff);
mBuffer[mBufferPos++] = (byte) ((i >> 16) & 0xff);
mBuffer[mBufferPos++] = (byte) ((i >> 8) & 0xff);
mBuffer[mBufferPos++] = (byte) ((i >> 0) & 0xff);
}
@Override
public void writeFloat(float v) throws IOException {
writeInt(Float.floatToIntBits(v));
}
@Override
public void writeDouble(double v) throws IOException {
writeLong(Double.doubleToLongBits(v));
}
@Override
public void writeBytes(String s) throws IOException {
// Callers should use writeUTF()
throw new UnsupportedOperationException();
}
@Override
public void writeChars(String s) throws IOException {
// Callers should use writeUTF()
throw new UnsupportedOperationException();
}
}