• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Protocol Buffers - Google's data interchange format
2 // Copyright 2008 Google Inc.  All rights reserved.
3 //
4 // Use of this source code is governed by a BSD-style
5 // license that can be found in the LICENSE file or at
6 // https://developers.google.com/open-source/licenses/bsd
7 
8 package com.google.protobuf;
9 
10 import static java.lang.Math.max;
11 import static java.lang.Math.min;
12 
13 import java.io.IOException;
14 import java.io.OutputStream;
15 import java.lang.ref.SoftReference;
16 import java.lang.reflect.Field;
17 import java.nio.ByteBuffer;
18 import java.nio.channels.WritableByteChannel;
19 
20 /** Utility class to provide efficient writing of {@link ByteBuffer}s to {@link OutputStream}s. */
21 final class ByteBufferWriter {
ByteBufferWriter()22   private ByteBufferWriter() {}
23 
24   /**
25    * Minimum size for a cached buffer. This prevents us from allocating buffers that are too small
26    * to be easily reused.
27    */
28   // TODO: tune this property or allow configuration?
29   private static final int MIN_CACHED_BUFFER_SIZE = 1024;
30 
31   /**
32    * Maximum size for a cached buffer. If a larger buffer is required, it will be allocated but not
33    * cached.
34    */
35   // TODO: tune this property or allow configuration?
36   private static final int MAX_CACHED_BUFFER_SIZE = 16 * 1024;
37 
38   /** The fraction of the requested buffer size under which the buffer will be reallocated. */
39   // TODO: tune this property or allow configuration?
40   private static final float BUFFER_REALLOCATION_THRESHOLD = 0.5f;
41 
42   /**
43    * Keeping a soft reference to a thread-local buffer. This buffer is used for writing a {@link
44    * ByteBuffer} to an {@link OutputStream} when no zero-copy alternative was available. Using a
45    * "soft" reference since VMs may keep this reference around longer than "weak" (e.g. HotSpot will
46    * maintain soft references until memory pressure warrants collection).
47    */
48   private static final ThreadLocal<SoftReference<byte[]>> BUFFER =
49       new ThreadLocal<SoftReference<byte[]>>();
50 
51   /** This is a hack for GAE, where {@code FileOutputStream} is unavailable. */
52   private static final Class<?> FILE_OUTPUT_STREAM_CLASS = safeGetClass("java.io.FileOutputStream");
53 
54   private static final long CHANNEL_FIELD_OFFSET = getChannelFieldOffset(FILE_OUTPUT_STREAM_CLASS);
55 
56   /**
57    * For testing purposes only. Clears the cached buffer to force a new allocation on the next
58    * invocation.
59    */
clearCachedBuffer()60   static void clearCachedBuffer() {
61     BUFFER.set(null);
62   }
63 
64   /**
65    * Writes the remaining content of the buffer to the given stream. The buffer {@code position}
66    * will remain unchanged by this method.
67    */
write(ByteBuffer buffer, OutputStream output)68   static void write(ByteBuffer buffer, OutputStream output) throws IOException {
69     final int initialPos = buffer.position();
70     try {
71       if (buffer.hasArray()) {
72         // Optimized write for array-backed buffers.
73         // Note that we're taking the risk that a malicious OutputStream could modify the array.
74         output.write(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
75       } else if (!writeToChannel(buffer, output)) {
76         // Read all of the data from the buffer to an array.
77         // TODO: Consider performance improvements for other "known" stream types.
78         final byte[] array = getOrCreateBuffer(buffer.remaining());
79         while (buffer.hasRemaining()) {
80           int length = min(buffer.remaining(), array.length);
81           buffer.get(array, 0, length);
82           output.write(array, 0, length);
83         }
84       }
85     } finally {
86       // Restore the initial position.
87       Java8Compatibility.position(buffer, initialPos);
88     }
89   }
90 
getOrCreateBuffer(int requestedSize)91   private static byte[] getOrCreateBuffer(int requestedSize) {
92     requestedSize = max(requestedSize, MIN_CACHED_BUFFER_SIZE);
93 
94     byte[] buffer = getBuffer();
95     // Only allocate if we need to.
96     if (buffer == null || needToReallocate(requestedSize, buffer.length)) {
97       buffer = new byte[requestedSize];
98 
99       // Only cache the buffer if it's not too big.
100       if (requestedSize <= MAX_CACHED_BUFFER_SIZE) {
101         setBuffer(buffer);
102       }
103     }
104     return buffer;
105   }
106 
needToReallocate(int requestedSize, int bufferLength)107   private static boolean needToReallocate(int requestedSize, int bufferLength) {
108     // First check against just the requested length to avoid the multiply.
109     return bufferLength < requestedSize
110         && bufferLength < requestedSize * BUFFER_REALLOCATION_THRESHOLD;
111   }
112 
getBuffer()113   private static byte[] getBuffer() {
114     SoftReference<byte[]> sr = BUFFER.get();
115     return sr == null ? null : sr.get();
116   }
117 
setBuffer(byte[] value)118   private static void setBuffer(byte[] value) {
119     BUFFER.set(new SoftReference<byte[]>(value));
120   }
121 
writeToChannel(ByteBuffer buffer, OutputStream output)122   private static boolean writeToChannel(ByteBuffer buffer, OutputStream output) throws IOException {
123     if (CHANNEL_FIELD_OFFSET >= 0 && FILE_OUTPUT_STREAM_CLASS.isInstance(output)) {
124       // Use a channel to write out the ByteBuffer. This will automatically empty the buffer.
125       WritableByteChannel channel = null;
126       try {
127         channel = (WritableByteChannel) UnsafeUtil.getObject(output, CHANNEL_FIELD_OFFSET);
128       } catch (ClassCastException e) {
129         // Absorb.
130       }
131       if (channel != null) {
132         channel.write(buffer);
133         return true;
134       }
135     }
136     return false;
137   }
138 
safeGetClass(String className)139   private static Class<?> safeGetClass(String className) {
140     try {
141       return Class.forName(className);
142     } catch (ClassNotFoundException e) {
143       return null;
144     }
145   }
146 
getChannelFieldOffset(Class<?> clazz)147   private static long getChannelFieldOffset(Class<?> clazz) {
148     try {
149       if (clazz != null && UnsafeUtil.hasUnsafeArrayOperations()) {
150         Field field = clazz.getDeclaredField("channel");
151         return UnsafeUtil.objectFieldOffset(field);
152       }
153     } catch (Throwable e) {
154       // Absorb
155     }
156     return -1;
157   }
158 }
159