001 /**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements. See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership. The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License. You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018 package org.apache.hadoop.hdfs.util;
019
020 import java.util.HashMap;
021 import java.util.LinkedList;
022 import java.util.Map;
023 import java.util.Queue;
024
025 import org.apache.commons.logging.Log;
026 import org.apache.commons.logging.LogFactory;
027 import org.apache.hadoop.HadoopIllegalArgumentException;
028 import org.apache.hadoop.classification.InterfaceAudience;
029 import org.apache.hadoop.util.Time;
030
031 import com.google.common.base.Preconditions;
032
033 /**
034 * Manage byte array creation and release.
035 */
036 @InterfaceAudience.Private
037 public abstract class ByteArrayManager {
038 static final Log LOG = LogFactory.getLog(ByteArrayManager.class);
039 private static final ThreadLocal<StringBuilder> debugMessage = new ThreadLocal<StringBuilder>() {
040 protected StringBuilder initialValue() {
041 return new StringBuilder();
042 }
043 };
044
045 private static void logDebugMessage() {
046 final StringBuilder b = debugMessage.get();
047 LOG.debug(b);
048 b.setLength(0);
049 }
050
051 static final int MIN_ARRAY_LENGTH = 32;
052 static final byte[] EMPTY_BYTE_ARRAY = {};
053
054 /**
055 * @return the least power of two greater than or equal to n, i.e. return
056 * the least integer x with x >= n and x a power of two.
057 *
058 * @throws HadoopIllegalArgumentException
059 * if n <= 0.
060 */
061 public static int leastPowerOfTwo(final int n) {
062 if (n <= 0) {
063 throw new HadoopIllegalArgumentException("n = " + n + " <= 0");
064 }
065
066 final int highestOne = Integer.highestOneBit(n);
067 if (highestOne == n) {
068 return n; // n is a power of two.
069 }
070 final int roundUp = highestOne << 1;
071 if (roundUp < 0) {
072 final long overflow = ((long) highestOne) << 1;
073 throw new ArithmeticException(
074 "Overflow: for n = " + n + ", the least power of two (the least"
075 + " integer x with x >= n and x a power of two) = "
076 + overflow + " > Integer.MAX_VALUE = " + Integer.MAX_VALUE);
077 }
078 return roundUp;
079 }
080
081 /**
082 * A counter with a time stamp so that it is reset automatically
083 * if there is no increment for the time period.
084 */
085 static class Counter {
086 private final long countResetTimePeriodMs;
087 private long count = 0L;
088 private long timestamp = Time.monotonicNow();
089
090 Counter(long countResetTimePeriodMs) {
091 this.countResetTimePeriodMs = countResetTimePeriodMs;
092 }
093
094 synchronized long getCount() {
095 return count;
096 }
097
098 /**
099 * Increment the counter, and reset it if there is no increment
100 * for acertain time period.
101 *
102 * @return the new count.
103 */
104 synchronized long increment() {
105 final long now = Time.monotonicNow();
106 if (now - timestamp > countResetTimePeriodMs) {
107 count = 0; // reset the counter
108 }
109 timestamp = now;
110 return ++count;
111 }
112 }
113
114 /** A map from integers to counters. */
115 static class CounterMap {
116 /** @see ByteArrayManager.Conf#countResetTimePeriodMs */
117 private final long countResetTimePeriodMs;
118 private final Map<Integer, Counter> map = new HashMap<Integer, Counter>();
119
120 private CounterMap(long countResetTimePeriodMs) {
121 this.countResetTimePeriodMs = countResetTimePeriodMs;
122 }
123
124 /**
125 * @return the counter for the given key;
126 * and create a new counter if it does not exist.
127 */
128 synchronized Counter get(final Integer key, final boolean createIfNotExist) {
129 Counter count = map.get(key);
130 if (count == null && createIfNotExist) {
131 count = new Counter(countResetTimePeriodMs);
132 map.put(key, count);
133 }
134 return count;
135 }
136
137 synchronized void clear() {
138 map.clear();
139 }
140 }
141
142 /** Manage byte arrays with the same fixed length. */
143 static class FixedLengthManager {
144 private final int byteArrayLength;
145 private final int maxAllocated;
146 private final Queue<byte[]> freeQueue = new LinkedList<byte[]>();
147
148 private int numAllocated = 0;
149
150 FixedLengthManager(int arrayLength, int maxAllocated) {
151 this.byteArrayLength = arrayLength;
152 this.maxAllocated = maxAllocated;
153 }
154
155 /**
156 * Allocate a byte array.
157 *
158 * If the number of allocated arrays >= maximum, the current thread is
159 * blocked until the number of allocated arrays drops to below the maximum.
160 *
161 * The byte array allocated by this method must be returned for recycling
162 * via the {@link FixedLengthManager#recycle(byte[])} method.
163 */
164 synchronized byte[] allocate() throws InterruptedException {
165 if (LOG.isDebugEnabled()) {
166 debugMessage.get().append(", ").append(this);
167 }
168 for(; numAllocated >= maxAllocated;) {
169 if (LOG.isDebugEnabled()) {
170 debugMessage.get().append(": wait ...");
171 logDebugMessage();
172 }
173
174 wait();
175
176 if (LOG.isDebugEnabled()) {
177 debugMessage.get().append("wake up: ").append(this);
178 }
179 }
180 numAllocated++;
181
182 final byte[] array = freeQueue.poll();
183 if (LOG.isDebugEnabled()) {
184 debugMessage.get().append(", recycled? ").append(array != null);
185 }
186 return array != null? array : new byte[byteArrayLength];
187 }
188
189 /**
190 * Recycle the given byte array, which must have the same length as the
191 * array length managed by this object.
192 *
193 * The byte array may or may not be allocated
194 * by the {@link FixedLengthManager#allocate()} method.
195 */
196 synchronized int recycle(byte[] array) {
197 Preconditions.checkNotNull(array);
198 Preconditions.checkArgument(array.length == byteArrayLength);
199 if (LOG.isDebugEnabled()) {
200 debugMessage.get().append(", ").append(this);
201 }
202
203 if (numAllocated == maxAllocated) {
204 if (LOG.isDebugEnabled()) {
205 debugMessage.get().append(", notifyAll");
206 }
207 notifyAll();
208 }
209 numAllocated--;
210 if (numAllocated < 0) {
211 // it is possible to drop below 0 since
212 // some byte arrays may not be created by the allocate() method.
213 numAllocated = 0;
214 }
215
216 if (freeQueue.size() < maxAllocated - numAllocated) {
217 if (LOG.isDebugEnabled()) {
218 debugMessage.get().append(", freeQueue.offer");
219 }
220 freeQueue.offer(array);
221 }
222 return freeQueue.size();
223 }
224
225 @Override
226 public synchronized String toString() {
227 return "[" + byteArrayLength + ": " + numAllocated + "/"
228 + maxAllocated + ", free=" + freeQueue.size() + "]";
229 }
230 }
231
232 /** A map from array lengths to byte array managers. */
233 static class ManagerMap {
234 private final int countLimit;
235 private final Map<Integer, FixedLengthManager> map = new HashMap<Integer, FixedLengthManager>();
236
237 ManagerMap(int countLimit) {
238 this.countLimit = countLimit;
239 }
240
241 /** @return the manager for the given array length. */
242 synchronized FixedLengthManager get(final Integer arrayLength,
243 final boolean createIfNotExist) {
244 FixedLengthManager manager = map.get(arrayLength);
245 if (manager == null && createIfNotExist) {
246 manager = new FixedLengthManager(arrayLength, countLimit);
247 map.put(arrayLength, manager);
248 }
249 return manager;
250 }
251
252 synchronized void clear() {
253 map.clear();
254 }
255 }
256
257 public static class Conf {
258 /**
259 * The count threshold for each array length so that a manager is created
260 * only after the allocation count exceeds the threshold.
261 */
262 private final int countThreshold;
263 /**
264 * The maximum number of arrays allowed for each array length.
265 */
266 private final int countLimit;
267 /**
268 * The time period in milliseconds that the allocation count for each array
269 * length is reset to zero if there is no increment.
270 */
271 private final long countResetTimePeriodMs;
272
273 public Conf(int countThreshold, int countLimit, long countResetTimePeriodMs) {
274 this.countThreshold = countThreshold;
275 this.countLimit = countLimit;
276 this.countResetTimePeriodMs = countResetTimePeriodMs;
277 }
278 }
279
280 /**
281 * Create a byte array for the given length, where the length of
282 * the returned array is larger than or equal to the given length.
283 *
284 * The current thread may be blocked if some resource is unavailable.
285 *
286 * The byte array created by this method must be released
287 * via the {@link ByteArrayManager#release(byte[])} method.
288 *
289 * @return a byte array with length larger than or equal to the given length.
290 */
291 public abstract byte[] newByteArray(int size) throws InterruptedException;
292
293 /**
294 * Release the given byte array.
295 *
296 * The byte array may or may not be created
297 * by the {@link ByteArrayManager#newByteArray(int)} method.
298 *
299 * @return the number of free array.
300 */
301 public abstract int release(byte[] array);
302
303 public static ByteArrayManager newInstance(Conf conf) {
304 return conf == null? new NewByteArrayWithoutLimit(): new Impl(conf);
305 }
306
307 /**
308 * A dummy implementation which simply calls new byte[].
309 */
310 static class NewByteArrayWithoutLimit extends ByteArrayManager {
311 @Override
312 public byte[] newByteArray(int size) throws InterruptedException {
313 return new byte[size];
314 }
315
316 @Override
317 public int release(byte[] array) {
318 return 0;
319 }
320 }
321
322 /**
323 * Manage byte array allocation and provide a mechanism for recycling the byte
324 * array objects.
325 */
326 static class Impl extends ByteArrayManager {
327 private final Conf conf;
328
329 private final CounterMap counters;
330 private final ManagerMap managers;
331
332 Impl(Conf conf) {
333 this.conf = conf;
334 this.counters = new CounterMap(conf.countResetTimePeriodMs);
335 this.managers = new ManagerMap(conf.countLimit);
336 }
337
338 /**
339 * Allocate a byte array, where the length of the allocated array
340 * is the least power of two of the given length
341 * unless the given length is less than {@link #MIN_ARRAY_LENGTH}.
342 * In such case, the returned array length is equal to {@link #MIN_ARRAY_LENGTH}.
343 *
344 * If the number of allocated arrays exceeds the capacity,
345 * the current thread is blocked until
346 * the number of allocated arrays drops to below the capacity.
347 *
348 * The byte array allocated by this method must be returned for recycling
349 * via the {@link ByteArrayManager#recycle(byte[])} method.
350 *
351 * @return a byte array with length larger than or equal to the given length.
352 */
353 @Override
354 public byte[] newByteArray(final int arrayLength) throws InterruptedException {
355 if (LOG.isDebugEnabled()) {
356 debugMessage.get().append("allocate(").append(arrayLength).append(")");
357 }
358
359 final byte[] array;
360 if (arrayLength == 0) {
361 array = EMPTY_BYTE_ARRAY;
362 } else {
363 final int powerOfTwo = arrayLength <= MIN_ARRAY_LENGTH?
364 MIN_ARRAY_LENGTH: leastPowerOfTwo(arrayLength);
365 final long count = counters.get(powerOfTwo, true).increment();
366 final boolean aboveThreshold = count > conf.countThreshold;
367 // create a new manager only if the count is above threshold.
368 final FixedLengthManager manager = managers.get(powerOfTwo, aboveThreshold);
369
370 if (LOG.isDebugEnabled()) {
371 debugMessage.get().append(": count=").append(count)
372 .append(aboveThreshold? ", aboveThreshold": ", belowThreshold");
373 }
374 array = manager != null? manager.allocate(): new byte[powerOfTwo];
375 }
376
377 if (LOG.isDebugEnabled()) {
378 logDebugMessage();
379 }
380 return array;
381 }
382
383 /**
384 * Recycle the given byte array.
385 *
386 * The byte array may or may not be allocated
387 * by the {@link ByteArrayManager#allocate(int)} method.
388 */
389 @Override
390 public int release(final byte[] array) {
391 Preconditions.checkNotNull(array);
392 if (LOG.isDebugEnabled()) {
393 debugMessage.get().append("recycle: array.length=").append(array.length);
394 }
395
396 final int freeQueueSize;
397 if (array.length == 0) {
398 freeQueueSize = -1;
399 } else {
400 final FixedLengthManager manager = managers.get(array.length, false);
401 freeQueueSize = manager == null? -1: manager.recycle(array);
402 }
403
404 if (LOG.isDebugEnabled()) {
405 debugMessage.get().append(", freeQueueSize=").append(freeQueueSize);
406 logDebugMessage();
407 }
408 return freeQueueSize;
409 }
410
411 CounterMap getCounters() {
412 return counters;
413 }
414
415 ManagerMap getManagers() {
416 return managers;
417 }
418 }
419 }