1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.util; 18 19 import android.annotation.NonNull; 20 import android.util.CharsetUtils; 21 22 import dalvik.system.VMRuntime; 23 24 import java.io.BufferedOutputStream; 25 import java.io.Closeable; 26 import java.io.DataOutput; 27 import java.io.DataOutputStream; 28 import java.io.Flushable; 29 import java.io.IOException; 30 import java.io.OutputStream; 31 import java.util.HashMap; 32 import java.util.Objects; 33 34 /** 35 * Optimized implementation of {@link DataOutput} which buffers data in memory 36 * before flushing to the underlying {@link OutputStream}. 37 * <p> 38 * Benchmarks have demonstrated this class is 2x more efficient than using a 39 * {@link DataOutputStream} with a {@link BufferedOutputStream}. 40 */ 41 public class FastDataOutput implements DataOutput, Flushable, Closeable { 42 private static final int MAX_UNSIGNED_SHORT = 65_535; 43 44 private final VMRuntime mRuntime; 45 private final OutputStream mOut; 46 47 private final byte[] mBuffer; 48 private final long mBufferPtr; 49 private final int mBufferCap; 50 51 private int mBufferPos; 52 53 /** 54 * Values that have been "interned" by {@link #writeInternedUTF(String)}. 55 */ 56 private HashMap<String, Short> mStringRefs = new HashMap<>(); 57 FastDataOutput(@onNull OutputStream out, int bufferSize)58 public FastDataOutput(@NonNull OutputStream out, int bufferSize) { 59 mRuntime = VMRuntime.getRuntime(); 60 mOut = Objects.requireNonNull(out); 61 if (bufferSize < 8) { 62 throw new IllegalArgumentException(); 63 } 64 65 mBuffer = (byte[]) mRuntime.newNonMovableArray(byte.class, bufferSize); 66 mBufferPtr = mRuntime.addressOf(mBuffer); 67 mBufferCap = mBuffer.length; 68 } 69 drain()70 private void drain() throws IOException { 71 if (mBufferPos > 0) { 72 mOut.write(mBuffer, 0, mBufferPos); 73 mBufferPos = 0; 74 } 75 } 76 77 @Override flush()78 public void flush() throws IOException { 79 drain(); 80 mOut.flush(); 81 } 82 83 @Override close()84 public void close() throws IOException { 85 mOut.close(); 86 } 87 88 @Override write(int b)89 public void write(int b) throws IOException { 90 writeByte(b); 91 } 92 93 @Override write(byte[] b)94 public void write(byte[] b) throws IOException { 95 write(b, 0, b.length); 96 } 97 98 @Override write(byte[] b, int off, int len)99 public void write(byte[] b, int off, int len) throws IOException { 100 if (mBufferCap < len) { 101 drain(); 102 mOut.write(b, off, len); 103 } else { 104 if (mBufferCap - mBufferPos < len) drain(); 105 System.arraycopy(b, off, mBuffer, mBufferPos, len); 106 mBufferPos += len; 107 } 108 } 109 110 @Override writeUTF(String s)111 public void writeUTF(String s) throws IOException { 112 // Attempt to write directly to buffer space if there's enough room, 113 // otherwise fall back to chunking into place 114 if (mBufferCap - mBufferPos < 2 + s.length()) drain(); 115 116 // Magnitude of this returned value indicates the number of bytes 117 // required to encode the string; sign indicates success/failure 118 int len = CharsetUtils.toModifiedUtf8Bytes(s, mBufferPtr, mBufferPos + 2, mBufferCap); 119 if (Math.abs(len) > MAX_UNSIGNED_SHORT) { 120 throw new IOException("Modified UTF-8 length too large: " + len); 121 } 122 123 if (len >= 0) { 124 // Positive value indicates the string was encoded into the buffer 125 // successfully, so we only need to prefix with length 126 writeShort(len); 127 mBufferPos += len; 128 } else { 129 // Negative value indicates buffer was too small and we need to 130 // allocate a temporary buffer for encoding 131 len = -len; 132 final byte[] tmp = (byte[]) mRuntime.newNonMovableArray(byte.class, len + 1); 133 CharsetUtils.toModifiedUtf8Bytes(s, mRuntime.addressOf(tmp), 0, tmp.length); 134 writeShort(len); 135 write(tmp, 0, len); 136 } 137 } 138 139 /** 140 * Write a {@link String} value with the additional signal that the given 141 * value is a candidate for being canonicalized, similar to 142 * {@link String#intern()}. 143 * <p> 144 * Canonicalization is implemented by writing each unique string value once 145 * the first time it appears, and then writing a lightweight {@code short} 146 * reference when that string is written again in the future. 147 * 148 * @see FastDataInput#readInternedUTF() 149 */ writeInternedUTF(@onNull String s)150 public void writeInternedUTF(@NonNull String s) throws IOException { 151 Short ref = mStringRefs.get(s); 152 if (ref != null) { 153 writeShort(ref); 154 } else { 155 writeShort(MAX_UNSIGNED_SHORT); 156 writeUTF(s); 157 158 // We can only safely intern when we have remaining values; if we're 159 // full we at least sent the string value above 160 ref = (short) mStringRefs.size(); 161 if (ref < MAX_UNSIGNED_SHORT) { 162 mStringRefs.put(s, ref); 163 } 164 } 165 } 166 167 @Override writeBoolean(boolean v)168 public void writeBoolean(boolean v) throws IOException { 169 writeByte(v ? 1 : 0); 170 } 171 172 @Override writeByte(int v)173 public void writeByte(int v) throws IOException { 174 if (mBufferCap - mBufferPos < 1) drain(); 175 mBuffer[mBufferPos++] = (byte) ((v >> 0) & 0xff); 176 } 177 178 @Override writeShort(int v)179 public void writeShort(int v) throws IOException { 180 if (mBufferCap - mBufferPos < 2) drain(); 181 mBuffer[mBufferPos++] = (byte) ((v >> 8) & 0xff); 182 mBuffer[mBufferPos++] = (byte) ((v >> 0) & 0xff); 183 } 184 185 @Override writeChar(int v)186 public void writeChar(int v) throws IOException { 187 writeShort((short) v); 188 } 189 190 @Override writeInt(int v)191 public void writeInt(int v) throws IOException { 192 if (mBufferCap - mBufferPos < 4) drain(); 193 mBuffer[mBufferPos++] = (byte) ((v >> 24) & 0xff); 194 mBuffer[mBufferPos++] = (byte) ((v >> 16) & 0xff); 195 mBuffer[mBufferPos++] = (byte) ((v >> 8) & 0xff); 196 mBuffer[mBufferPos++] = (byte) ((v >> 0) & 0xff); 197 } 198 199 @Override writeLong(long v)200 public void writeLong(long v) throws IOException { 201 if (mBufferCap - mBufferPos < 8) drain(); 202 int i = (int) (v >> 32); 203 mBuffer[mBufferPos++] = (byte) ((i >> 24) & 0xff); 204 mBuffer[mBufferPos++] = (byte) ((i >> 16) & 0xff); 205 mBuffer[mBufferPos++] = (byte) ((i >> 8) & 0xff); 206 mBuffer[mBufferPos++] = (byte) ((i >> 0) & 0xff); 207 i = (int) v; 208 mBuffer[mBufferPos++] = (byte) ((i >> 24) & 0xff); 209 mBuffer[mBufferPos++] = (byte) ((i >> 16) & 0xff); 210 mBuffer[mBufferPos++] = (byte) ((i >> 8) & 0xff); 211 mBuffer[mBufferPos++] = (byte) ((i >> 0) & 0xff); 212 } 213 214 @Override writeFloat(float v)215 public void writeFloat(float v) throws IOException { 216 writeInt(Float.floatToIntBits(v)); 217 } 218 219 @Override writeDouble(double v)220 public void writeDouble(double v) throws IOException { 221 writeLong(Double.doubleToLongBits(v)); 222 } 223 224 @Override writeBytes(String s)225 public void writeBytes(String s) throws IOException { 226 // Callers should use writeUTF() 227 throw new UnsupportedOperationException(); 228 } 229 230 @Override writeChars(String s)231 public void writeChars(String s) throws IOException { 232 // Callers should use writeUTF() 233 throw new UnsupportedOperationException(); 234 } 235 } 236