| // Copyright 2017 The Bazel Authors. All rights reserved. | |
| // | |
| // 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.google.devtools.build.lib.runtime; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import com.google.common.base.Preconditions; | |
| import com.google.common.base.Splitter; | |
| import com.google.common.collect.ImmutableList; | |
| import java.io.IOException; | |
| import java.io.OutputStream; | |
| /** | |
| * {@link OutputStream} suitably synchronized for producer-consumer use cases. The method {@link | |
| * #readAndReset()} allows to read the bytes accumulated so far and simultaneously truncate | |
| * precisely the bytes read. Moreover, upon such a reset the amount of memory retained is reset to a | |
| * small constant. This is a difference with resecpt to the behaviour of the standard classes {@link | |
| * ByteArrayOutputStream} which only resets the index but keeps the array. This difference matters, | |
| * as we need to support output peeks without retaining this amount of memory for the rest of the | |
| * build. | |
| * | |
| * <p>This class is expected to be used with the {@link BuildEventStreamer}. | |
| */ | |
| public class SynchronizedOutputStream extends OutputStream { | |
| // The maximal amount of bytes we intend to store in the buffer. However, | |
| // the requirement that a single write be written in one go is more important, | |
| // so the actual size we store in this buffer can be the maximum (not the sum) | |
| // of this value and the amount of bytes written in a single call to the | |
| // {@link write(byte[] buffer, int offset, int count)} method. | |
| private final int maxBufferedLength; | |
| private final int maxChunkSize; | |
| private byte[] buf; | |
| private long count; | |
| private boolean discardAll; | |
| // The event streamer that is supposed to flush stdout/stderr. | |
| private BuildEventStreamer streamer; | |
| public SynchronizedOutputStream(int maxBufferedLength, int maxChunkSize) { | |
| Preconditions.checkArgument(maxChunkSize > 0); | |
| buf = new byte[64]; | |
| count = 0; | |
| discardAll = false; | |
| this.maxBufferedLength = maxBufferedLength; | |
| this.maxChunkSize = Math.max(maxChunkSize, maxBufferedLength); | |
| } | |
| public void registerStreamer(BuildEventStreamer streamer) { | |
| this.streamer = streamer; | |
| } | |
| /** | |
| * Read the contents of the stream and simultaneously clear them. Also, reset the amount of memory | |
| * retained to a constant amount. | |
| */ | |
| public synchronized Iterable<String> readAndReset() { | |
| String content = new String(buf, 0, (int) count, UTF_8); | |
| buf = new byte[64]; | |
| count = 0; | |
| return content.isEmpty() | |
| ? ImmutableList.of() | |
| : Splitter.fixedLength(maxChunkSize).split(content); | |
| } | |
| @Override | |
| public void write(int oneByte) throws IOException { | |
| if (discardAll) { | |
| return; | |
| } | |
| // We change the dependency with respect to that of the super class: write(int) | |
| // now calls write(int[], int, int) which is implemented without any dependencies. | |
| write(new byte[] {(byte) oneByte}, 0, 1); | |
| } | |
| @Override | |
| public void write(byte[] buffer, int offset, int count) throws IOException { | |
| // As we base the less common write(int) on this method, we may not depend not call write(int) | |
| // directly or indirectly (e.g., by calling super.write(int[], int, int)). | |
| synchronized (this) { | |
| if (discardAll) { | |
| return; | |
| } | |
| } | |
| boolean shouldFlush = false; | |
| // As we have to do the flushing outside the synchronized block, we have to expect | |
| // other writes to come immediately after flushing, so we have to do the check inside | |
| // a while loop. | |
| boolean didWrite = false; | |
| while (!didWrite) { | |
| synchronized (this) { | |
| if (this.count + (long) count < maxBufferedLength || this.count == 0) { | |
| if (this.count + (long) count >= (long) buf.length) { | |
| // We need to increase the buffer; if within the permissible range range for array | |
| // sizes, we at least double it, otherwise we only increase as far as needed. | |
| long newsize; | |
| if (2 * (long) buf.length + count < (long) Integer.MAX_VALUE) { | |
| newsize = 2 * (long) buf.length + count; | |
| } else { | |
| newsize = this.count + count; | |
| } | |
| byte[] newbuf = new byte[(int) newsize]; | |
| System.arraycopy(buf, 0, newbuf, 0, (int) this.count); | |
| this.buf = newbuf; | |
| } | |
| System.arraycopy(buffer, offset, buf, (int) this.count, count); | |
| this.count += (long) count; | |
| didWrite = true; | |
| } else { | |
| shouldFlush = true; | |
| } | |
| if (this.count >= maxBufferedLength) { | |
| shouldFlush = true; | |
| } | |
| } | |
| if (shouldFlush && streamer != null) { | |
| streamer.flush(); | |
| shouldFlush = false; | |
| } | |
| } | |
| } | |
| } |