diff options
4 files changed, 420 insertions, 66 deletions
diff --git a/services/core/java/com/android/server/wm/WindowTraceBuffer.java b/services/core/java/com/android/server/wm/WindowTraceBuffer.java new file mode 100644 index 000000000000..936ee85697b8 --- /dev/null +++ b/services/core/java/com/android/server/wm/WindowTraceBuffer.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2019 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.server.wm; + +import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER; +import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_H; +import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_L; + +import android.os.Trace; +import android.util.proto.ProtoOutputStream; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * Buffer used for window tracing. + */ +abstract class WindowTraceBuffer { + private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L; + + final Object mBufferSizeLock = new Object(); + final BlockingQueue<byte[]> mBuffer; + int mBufferSize; + private final int mBufferCapacity; + private final File mTraceFile; + + WindowTraceBuffer(int size, File traceFile) throws IOException { + mBufferCapacity = size; + mTraceFile = traceFile; + mBuffer = new LinkedBlockingQueue<>(); + + initTraceFile(); + } + + int getAvailableSpace() { + return mBufferCapacity - mBufferSize; + } + + /** + * Inserts the specified element into this buffer. + * + * This method is synchronized with {@code #take()} and {@code #clear()} + * for consistency. + * + * @param proto the element to add + * @return {@code true} if the inserted item was inserted into the buffer + * @throws IllegalStateException if the element cannot be added because it is larger + * than the buffer size. + */ + boolean add(ProtoOutputStream proto) throws InterruptedException { + byte[] protoBytes = proto.getBytes(); + int protoLength = protoBytes.length; + if (protoLength > mBufferCapacity) { + throw new IllegalStateException("Trace object too large for the buffer. Buffer size:" + + mBufferCapacity + " Object size: " + protoLength); + } + synchronized (mBufferSizeLock) { + boolean canAdd = canAdd(protoBytes); + if (canAdd) { + mBuffer.offer(protoBytes); + mBufferSize += protoLength; + } + return canAdd; + } + } + + void writeNextBufferElementToFile() throws IOException { + byte[] proto; + try { + proto = take(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + + try { + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "writeToFile"); + try (OutputStream os = new FileOutputStream(mTraceFile, true)) { + os.write(proto); + } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); + } + } + + /** + * Retrieves and removes the head of this queue, waiting if necessary + * until an element becomes available. + * + * This method is synchronized with {@code #add(ProtoOutputStream)} and {@code #clear()} + * for consistency. + * + * @return the head of this buffer, or {@code null} if this buffer is empty + */ + private byte[] take() throws InterruptedException { + byte[] item = mBuffer.take(); + synchronized (mBufferSizeLock) { + mBufferSize -= item.length; + return item; + } + } + + private void initTraceFile() throws IOException { + mTraceFile.delete(); + try (OutputStream os = new FileOutputStream(mTraceFile)) { + mTraceFile.setReadable(true, false); + ProtoOutputStream proto = new ProtoOutputStream(os); + proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE); + proto.flush(); + } + } + + /** + * Checks if the element can be added to the buffer. The element is already certain to be + * smaller than the overall buffer size. + * + * @param protoBytes byte array representation of the Proto object to add + * @return <tt>true<</tt> if the element can be added to the buffer or not + */ + abstract boolean canAdd(byte[] protoBytes) throws InterruptedException; + + /** + * Flush all buffer content to the disk. + * + * @throws IOException if the buffer cannot write its contents to the {@link #mTraceFile} + */ + abstract void writeToDisk() throws IOException, InterruptedException; + + /** + * Builder for a {@code WindowTraceBuffer} which creates a {@link WindowTraceQueueBuffer} + */ + static class Builder { + private File mTraceFile; + private int mBufferCapacity; + + + Builder setTraceFile(File traceFile) { + mTraceFile = traceFile; + return this; + } + + Builder setBufferCapacity(int size) { + mBufferCapacity = size; + return this; + } + + File getFile() { + return mTraceFile; + } + + WindowTraceBuffer build() throws IOException { + if (mBufferCapacity <= 0) { + throw new IllegalStateException("Buffer capacity must be greater than 0."); + } + + if (mTraceFile == null) { + throw new IllegalArgumentException("A valid trace file must be specified."); + } + + return new WindowTraceQueueBuffer(mBufferCapacity, mTraceFile); + } + } +} diff --git a/services/core/java/com/android/server/wm/WindowTraceQueueBuffer.java b/services/core/java/com/android/server/wm/WindowTraceQueueBuffer.java new file mode 100644 index 000000000000..b7fc7ac8cb5e --- /dev/null +++ b/services/core/java/com/android/server/wm/WindowTraceQueueBuffer.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2019 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.server.wm; + +import static android.os.Build.IS_USER; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.File; +import java.io.IOException; + +/** + * A buffer structure backed by a {@link java.util.concurrent.BlockingQueue} to store the first + * {@code #size size} bytes of window trace elements. + * Once the buffer is full it will no longer accepts new elements. + */ +class WindowTraceQueueBuffer extends WindowTraceBuffer { + private Thread mWriterThread; + private boolean mCancel; + + @VisibleForTesting + WindowTraceQueueBuffer(int size, File traceFile, boolean startWriterThread) throws IOException { + super(size, traceFile); + if (startWriterThread) { + initializeWriterThread(); + } + } + + WindowTraceQueueBuffer(int size, File traceFile) throws IOException { + this(size, traceFile, !IS_USER); + } + + private void initializeWriterThread() { + mCancel = false; + mWriterThread = new Thread(() -> { + try { + loop(); + } catch (IOException e) { + throw new IllegalStateException("Failed to execute trace write loop thread", e); + } + }, "window_tracing"); + mWriterThread.start(); + } + + private void loop() throws IOException { + while (!mCancel) { + writeNextBufferElementToFile(); + } + } + + private void restartWriterThread() throws InterruptedException { + if (mWriterThread != null) { + mCancel = true; + mWriterThread.interrupt(); + mWriterThread.join(); + initializeWriterThread(); + } + } + + @Override + boolean canAdd(byte[] protoBytes) { + long availableSpace = getAvailableSpace(); + return availableSpace >= protoBytes.length; + } + + @Override + void writeToDisk() throws InterruptedException { + while (!mBuffer.isEmpty()) { + mBufferSizeLock.wait(); + mBufferSizeLock.notify(); + } + restartWriterThread(); + } +} diff --git a/services/core/java/com/android/server/wm/WindowTracing.java b/services/core/java/com/android/server/wm/WindowTracing.java index 8fa56bb065c6..63539c4f9fd9 100644 --- a/services/core/java/com/android/server/wm/WindowTracing.java +++ b/services/core/java/com/android/server/wm/WindowTracing.java @@ -17,31 +17,23 @@ package com.android.server.wm; import static android.os.Build.IS_USER; + import static com.android.server.wm.WindowManagerTraceFileProto.ENTRY; -import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER; -import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_H; -import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_L; import static com.android.server.wm.WindowManagerTraceProto.ELAPSED_REALTIME_NANOS; import static com.android.server.wm.WindowManagerTraceProto.WHERE; import static com.android.server.wm.WindowManagerTraceProto.WINDOW_MANAGER_SERVICE; +import android.annotation.Nullable; import android.content.Context; import android.os.ShellCommand; import android.os.SystemClock; import android.os.Trace; -import android.annotation.Nullable; import android.util.Log; import android.util.proto.ProtoOutputStream; -import com.android.internal.annotations.VisibleForTesting; - import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.OutputStream; import java.io.PrintWriter; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; /** * A class that allows window manager to dump its state continuously to a trace file, such that a @@ -49,35 +41,42 @@ import java.util.concurrent.BlockingQueue; */ class WindowTracing { + /** + * Maximum buffer size, currently defined as 512 KB + * Size was experimentally defined to fit between 100 to 150 elements. + */ + private static final int WINDOW_TRACE_BUFFER_SIZE = 512 * 1024; private static final String TAG = "WindowTracing"; - private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L; private final Object mLock = new Object(); - private final File mTraceFile; - private final BlockingQueue<ProtoOutputStream> mWriteQueue = new ArrayBlockingQueue<>(200); + private final WindowTraceBuffer.Builder mBufferBuilder; + + private WindowTraceBuffer mTraceBuffer; private boolean mEnabled; private volatile boolean mEnabledLockFree; WindowTracing(File file) { - mTraceFile = file; + mBufferBuilder = new WindowTraceBuffer.Builder() + .setTraceFile(file) + .setBufferCapacity(WINDOW_TRACE_BUFFER_SIZE); } void startTrace(@Nullable PrintWriter pw) throws IOException { - if (IS_USER){ + if (IS_USER) { logAndPrintln(pw, "Error: Tracing is not supported on user builds."); return; } synchronized (mLock) { - logAndPrintln(pw, "Start tracing to " + mTraceFile + "."); - mWriteQueue.clear(); - mTraceFile.delete(); - try (OutputStream os = new FileOutputStream(mTraceFile)) { - mTraceFile.setReadable(true, false); - ProtoOutputStream proto = new ProtoOutputStream(os); - proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE); - proto.flush(); + logAndPrintln(pw, "Start tracing to " + mBufferBuilder.getFile() + "."); + if (mTraceBuffer != null) { + try { + mTraceBuffer.writeToDisk(); + } catch (InterruptedException e) { + logAndPrintln(pw, "Error: Unable to flush the previous buffer."); + } } + mTraceBuffer = mBufferBuilder.build(); mEnabled = mEnabledLockFree = true; } } @@ -91,67 +90,42 @@ class WindowTracing { } void stopTrace(@Nullable PrintWriter pw) { - if (IS_USER){ + if (IS_USER) { logAndPrintln(pw, "Error: Tracing is not supported on user builds."); return; } synchronized (mLock) { - logAndPrintln(pw, "Stop tracing to " + mTraceFile + ". Waiting for traces to flush."); + logAndPrintln(pw, "Stop tracing to " + mBufferBuilder.getFile() + + ". Waiting for traces to flush."); mEnabled = mEnabledLockFree = false; - while (!mWriteQueue.isEmpty()) { + + synchronized (mLock) { if (mEnabled) { logAndPrintln(pw, "ERROR: tracing was re-enabled while waiting for flush."); throw new IllegalStateException("tracing enabled while waiting for flush."); } try { - mLock.wait(); - mLock.notify(); + mTraceBuffer.writeToDisk(); + } catch (IOException e) { + Log.e(TAG, "Unable to write buffer to file", e); } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + Log.e(TAG, "Unable to interrupt window tracing file write thread", e); } } - logAndPrintln(pw, "Trace written to " + mTraceFile + "."); + logAndPrintln(pw, "Trace written to " + mBufferBuilder.getFile() + "."); } } - void appendTraceEntry(ProtoOutputStream proto) { + private void appendTraceEntry(ProtoOutputStream proto) { if (!mEnabledLockFree) { return; } - if (!mWriteQueue.offer(proto)) { - Log.e(TAG, "Dropping window trace entry, queue full"); - } - } - - void loop() { - for (;;) { - loopOnce(); - } - } - - @VisibleForTesting - void loopOnce() { - ProtoOutputStream proto; try { - proto = mWriteQueue.take(); + mTraceBuffer.add(proto); } catch (InterruptedException e) { + Log.e(TAG, "Unable to add element to trace", e); Thread.currentThread().interrupt(); - return; - } - - synchronized (mLock) { - try { - Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "writeToFile"); - try (OutputStream os = new FileOutputStream(mTraceFile, true /* append */)) { - os.write(proto.getBytes()); - } - } catch (IOException e) { - Log.e(TAG, "Failed to write file " + mTraceFile, e); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); - } - mLock.notify(); } } @@ -161,11 +135,7 @@ class WindowTracing { static WindowTracing createDefaultAndStartLooper(Context context) { File file = new File("/data/misc/wmtrace/wm_trace.pb"); - WindowTracing windowTracing = new WindowTracing(file); - if (!IS_USER){ - new Thread(windowTracing::loop, "window_tracing").start(); - } - return windowTracing; + return new WindowTracing(file); } int onShellCommand(ShellCommand shell, String cmd) { diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTraceBufferTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowTraceBufferTest.java new file mode 100644 index 000000000000..8d834974148c --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTraceBufferTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2019 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.server.wm; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER; +import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_H; +import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_L; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.platform.test.annotations.Presubmit; +import android.util.proto.ProtoOutputStream; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + + +/** + * Test class for {@link WindowTraceBuffer} and {@link WindowTraceQueueBuffer}. + * + * Build/Install/Run: + * atest WmTests:WindowTraceBufferTest + */ +@SmallTest +@Presubmit +public class WindowTraceBufferTest { + private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L; + + private File mFile; + + @Before + public void setUp() throws Exception { + final Context testContext = getInstrumentation().getContext(); + mFile = testContext.getFileStreamPath("tracing_test.dat"); + mFile.delete(); + } + + @After + public void tearDown() throws Exception { + mFile.delete(); + } + + @Test + public void testTraceQueueBuffer_addItem() throws Exception { + ProtoOutputStream toWrite1 = getDummy(1); + ProtoOutputStream toWrite2 = getDummy(2); + ProtoOutputStream toWrite3 = getDummy(3); + byte[] toWrite1Bytes = toWrite1.getBytes(); + byte[] toWrite2Bytes = toWrite2.getBytes(); + byte[] toWrite3Bytes = toWrite3.getBytes(); + + final int objectSize = toWrite1.getBytes().length; + final int bufferCapacity = objectSize * 2; + + final WindowTraceBuffer buffer = buildQueueBuffer(bufferCapacity); + + buffer.add(toWrite1); + assertTrue("First element should be in the list", + buffer.mBuffer.stream().anyMatch(p -> Arrays.equals(p, toWrite1Bytes))); + + buffer.add(toWrite2); + assertTrue("First element should be in the list", + buffer.mBuffer.stream().anyMatch(p -> Arrays.equals(p, toWrite1Bytes))); + assertTrue("Second element should be in the list", + buffer.mBuffer.stream().anyMatch(p -> Arrays.equals(p, toWrite2Bytes))); + + buffer.add(toWrite3); + + assertTrue("Third element should not be in the list", + buffer.mBuffer.stream().noneMatch(p -> Arrays.equals(p, toWrite3Bytes))); + + assertEquals("Buffer should have 2 elements", buffer.mBuffer.size(), 2); + assertEquals(String.format("Buffer is full, used space should be %d", bufferCapacity), + buffer.mBufferSize, bufferCapacity); + assertEquals("Buffer is full, available space should be 0", + buffer.getAvailableSpace(), 0); + } + + private ProtoOutputStream getDummy(int value) { + ProtoOutputStream toWrite = new ProtoOutputStream(); + toWrite.write(MAGIC_NUMBER, value); + toWrite.flush(); + + return toWrite; + } + + private WindowTraceBuffer buildQueueBuffer(int size) throws IOException { + return new WindowTraceQueueBuffer(size, mFile, false); + } +} |