Isolate WMTrace buffer and limit it by size not number of elements

Currently the buffer is a BlockingQueue with fixed capacity for 200
elements. This implementation changes it to use up to 512KB of
memory, instead of a fixed number of elements.

It also encapsulates the buffer in a separate class to support a ring
buffer for continuous monitoring (future implementation)

Test: Flash a device. Enable window manager tracing. Use the device.
Stop the trace and pull the logged trace. Open the trace in Winscope and
check if the data is correctly stored. Unit tests:
atest WmTests:WindowTraceBufferTest

Change-Id: Ia515d350d82520d378ae7339a6486ca6a43e62c3
This commit is contained in:
Nataniel Borges
2019-01-03 10:32:03 -08:00
parent 1bafe67c78
commit 5a38bad677
4 changed files with 420 additions and 66 deletions

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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) {

View File

@@ -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);
}
}