Merge "Support a ring buffer or a queue to store window manager traces"

This commit is contained in:
TreeHugger Robot
2019-01-29 18:10:38 +00:00
committed by Android (Google) Code Review
7 changed files with 326 additions and 104 deletions

View File

@@ -6056,6 +6056,7 @@ public class WindowManagerService extends IWindowManager.Stub
pw.println(" d[isplays]: active display contents");
pw.println(" t[okens]: token list");
pw.println(" w[indows]: window list");
pw.println(" trace: write Winscope trace to file");
pw.println(" cmd may also be a NAME to dump windows. NAME may");
pw.println(" be a partial substring in a window name, a");
pw.println(" Window hex object identifier, or");
@@ -6129,6 +6130,11 @@ public class WindowManagerService extends IWindowManager.Stub
mRoot.forAllWindows(w -> {pw.println(w);}, true /* traverseTopToBottom */);
}
return;
} else if ("trace".equals(cmd)) {
synchronized (mGlobalLock) {
mWindowTracing.writeTraceToFile();
}
return;
} else {
// Dumping a single name?
if (!dumpWindows(pw, cmd, args, opti, dumpAll)) {

View File

@@ -72,8 +72,7 @@ public class WindowManagerShellCommand extends ShellCommand {
// XXX this should probably be changed to use openFileForSystem() to create
// the output trace file, so the shell gets the correct semantics for where
// trace files can be written.
return mInternal.mWindowTracing.onShellCommand(this,
getNextArgRequired());
return mInternal.mWindowTracing.onShellCommand(this);
case "set-user-rotation":
return runSetDisplayUserRotation(pw);
case "set-fix-to-user-rotation":

View File

@@ -23,12 +23,15 @@ import static com.android.server.wm.WindowManagerTraceFileProto.MAGIC_NUMBER_L;
import android.os.Trace;
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.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Queue;
/**
* Buffer used for window tracing.
@@ -36,16 +39,15 @@ import java.util.concurrent.LinkedBlockingQueue;
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;
final Object mBufferLock = new Object();
final Queue<byte[]> mBuffer = new ArrayDeque<>();
final File mTraceFile;
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();
}
@@ -57,65 +59,45 @@ abstract class WindowTraceBuffer {
/**
* 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 {
void add(ProtoOutputStream proto) {
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);
synchronized (mBufferLock) {
boolean canAdd = canAdd(protoLength);
if (canAdd) {
mBuffer.offer(protoBytes);
mBufferSize += protoLength;
}
return canAdd;
mBufferLock.notify();
}
}
void writeNextBufferElementToFile() throws IOException {
byte[] proto;
/**
* Stops the buffer execution and flush all buffer content to the disk.
*
* @throws IOException if the buffer cannot write its contents to the {@link #mTraceFile}
*/
void dump() throws IOException, InterruptedException {
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);
}
Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "writeTraceToFile");
writeTraceToFile();
} 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;
}
@VisibleForTesting
boolean contains(byte[] other) {
return mBuffer.stream()
.anyMatch(p -> Arrays.equals(p, other));
}
private void initTraceFile() throws IOException {
@@ -132,25 +114,31 @@ abstract class WindowTraceBuffer {
* 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
* @param protoLength byte array representation of the Proto object to add
* @return {@code true} if the element can be added to the buffer or not
*/
abstract boolean canAdd(byte[] protoBytes) throws InterruptedException;
abstract boolean canAdd(int protoLength);
/**
* 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;
abstract void writeTraceToFile() throws IOException, InterruptedException;
/**
* Builder for a {@code WindowTraceBuffer} which creates a {@link WindowTraceQueueBuffer}
* Builder for a {@code WindowTraceBuffer} which creates a {@link WindowTraceRingBuffer} for
* continuous mode or a {@link WindowTraceQueueBuffer} otherwise
*/
static class Builder {
private boolean mContinuous;
private File mTraceFile;
private int mBufferCapacity;
Builder setContinuousMode(boolean continuous) {
mContinuous = continuous;
return this;
}
Builder setTraceFile(File traceFile) {
mTraceFile = traceFile;
@@ -175,7 +163,11 @@ abstract class WindowTraceBuffer {
throw new IllegalArgumentException("A valid trace file must be specified.");
}
return new WindowTraceQueueBuffer(mBufferCapacity, mTraceFile);
if (mContinuous) {
return new WindowTraceRingBuffer(mBufferCapacity, mTraceFile);
} else {
return new WindowTraceQueueBuffer(mBufferCapacity, mTraceFile);
}
}
}
}

View File

@@ -18,10 +18,14 @@ package com.android.server.wm;
import static android.os.Build.IS_USER;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* A buffer structure backed by a {@link java.util.concurrent.BlockingQueue} to store the first
@@ -29,14 +33,17 @@ import java.io.IOException;
* Once the buffer is full it will no longer accepts new elements.
*/
class WindowTraceQueueBuffer extends WindowTraceBuffer {
private Thread mWriterThread;
private static final String TAG = "WindowTracing";
private Thread mConsumerThread;
private boolean mCancel;
@VisibleForTesting
WindowTraceQueueBuffer(int size, File traceFile, boolean startWriterThread) throws IOException {
WindowTraceQueueBuffer(int size, File traceFile, boolean startConsumerThread)
throws IOException {
super(size, traceFile);
if (startWriterThread) {
initializeWriterThread();
if (startConsumerThread) {
initializeConsumerThread();
}
}
@@ -44,45 +51,56 @@ class WindowTraceQueueBuffer extends WindowTraceBuffer {
this(size, traceFile, !IS_USER);
}
private void initializeWriterThread() {
private void initializeConsumerThread() {
mCancel = false;
mWriterThread = new Thread(() -> {
mConsumerThread = new Thread(() -> {
try {
loop();
} catch (InterruptedException e) {
Log.i(TAG, "Interrupting trace consumer thread");
} catch (IOException e) {
throw new IllegalStateException("Failed to execute trace write loop thread", e);
Log.e(TAG, "Failed to execute trace consumer thread", e);
}
}, "window_tracing");
mWriterThread.start();
mConsumerThread.start();
}
private void loop() throws IOException {
private void loop() throws IOException, InterruptedException {
while (!mCancel) {
writeNextBufferElementToFile();
}
}
byte[] proto;
synchronized (mBufferLock) {
mBufferLock.wait();
private void restartWriterThread() throws InterruptedException {
if (mWriterThread != null) {
mCancel = true;
mWriterThread.interrupt();
mWriterThread.join();
initializeWriterThread();
proto = mBuffer.poll();
if (proto != null) {
mBufferSize -= proto.length;
}
}
if (proto != null) {
try (OutputStream os = new FileOutputStream(mTraceFile, true)) {
os.write(proto);
}
}
}
}
@Override
boolean canAdd(byte[] protoBytes) {
boolean canAdd(int protoLength) {
long availableSpace = getAvailableSpace();
return availableSpace >= protoBytes.length;
return availableSpace >= protoLength;
}
@Override
void writeToDisk() throws InterruptedException {
while (!mBuffer.isEmpty()) {
mBufferSizeLock.wait();
mBufferSizeLock.notify();
void writeTraceToFile() throws InterruptedException {
synchronized (mBufferLock) {
mCancel = true;
mBufferLock.notify();
}
if (mConsumerThread != null) {
mConsumerThread.join();
mConsumerThread = null;
}
restartWriterThread();
}
}

View File

@@ -0,0 +1,68 @@
/*
* 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 java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* A ring buffer to store the {@code #size size} bytes of window trace data.
* The buffer operates on a trace entry level, that is, if the new trace data is larger than the
* available buffer space, the buffer will discard as many full trace entries as necessary to fit
* the new trace.
*/
class WindowTraceRingBuffer extends WindowTraceBuffer {
WindowTraceRingBuffer(int size, File traceFile) throws IOException {
super(size, traceFile);
}
@Override
boolean canAdd(int protoLength) {
long availableSpace = getAvailableSpace();
while (availableSpace < protoLength) {
discardOldest();
availableSpace = getAvailableSpace();
}
return true;
}
@Override
void writeTraceToFile() throws IOException {
synchronized (mBufferLock) {
try (OutputStream os = new FileOutputStream(mTraceFile, true)) {
while (!mBuffer.isEmpty()) {
byte[] proto;
proto = mBuffer.poll();
mBufferSize -= proto.length;
os.write(proto);
}
}
}
}
private void discardOldest() {
byte[] item = mBuffer.poll();
if (item == null) {
throw new IllegalStateException("No element to discard from buffer");
}
mBufferSize -= item.length;
}
}

View File

@@ -53,6 +53,7 @@ class WindowTracing {
private WindowTraceBuffer mTraceBuffer;
private boolean mContinuousMode;
private boolean mEnabled;
private volatile boolean mEnabledLockFree;
@@ -70,13 +71,11 @@ class WindowTracing {
synchronized (mLock) {
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.");
}
writeTraceToFileLocked();
}
mTraceBuffer = mBufferBuilder.build();
mTraceBuffer = mBufferBuilder
.setContinuousMode(mContinuousMode)
.build();
mEnabled = mEnabledLockFree = true;
}
}
@@ -104,29 +103,29 @@ class WindowTracing {
logAndPrintln(pw, "ERROR: tracing was re-enabled while waiting for flush.");
throw new IllegalStateException("tracing enabled while waiting for flush.");
}
try {
mTraceBuffer.writeToDisk();
} catch (IOException e) {
Log.e(TAG, "Unable to write buffer to file", e);
} catch (InterruptedException e) {
Log.e(TAG, "Unable to interrupt window tracing file write thread", e);
}
writeTraceToFileLocked();
mTraceBuffer = null;
}
logAndPrintln(pw, "Trace written to " + mBufferBuilder.getFile() + ".");
}
}
private void setContinuousMode(boolean continuous, PrintWriter pw) {
logAndPrintln(pw, "Setting window tracing continuous mode to " + continuous);
if (mEnabled) {
logAndPrintln(pw, "Trace is currently active, change will take effect once the "
+ "trace is restarted.");
}
mContinuousMode = continuous;
}
private void appendTraceEntry(ProtoOutputStream proto) {
if (!mEnabledLockFree) {
return;
}
try {
mTraceBuffer.add(proto);
} catch (InterruptedException e) {
Log.e(TAG, "Unable to add element to trace", e);
Thread.currentThread().interrupt();
}
mTraceBuffer.add(proto);
}
boolean isEnabled() {
@@ -138,9 +137,10 @@ class WindowTracing {
return new WindowTracing(file);
}
int onShellCommand(ShellCommand shell, String cmd) {
int onShellCommand(ShellCommand shell) {
PrintWriter pw = shell.getOutPrintWriter();
try {
String cmd = shell.getNextArgRequired();
switch (cmd) {
case "start":
startTrace(pw);
@@ -148,6 +148,9 @@ class WindowTracing {
case "stop":
stopTrace(pw);
return 0;
case "continuous":
setContinuousMode(Boolean.valueOf(shell.getNextArgRequired()), pw);
return 0;
default:
pw.println("Unknown command: " + cmd);
return -1;
@@ -162,6 +165,7 @@ class WindowTracing {
if (!isEnabled()) {
return;
}
ProtoOutputStream os = new ProtoOutputStream();
long tokenOuter = os.start(ENTRY);
os.write(ELAPSED_REALTIME_NANOS, SystemClock.elapsedRealtimeNanos());
@@ -179,4 +183,33 @@ class WindowTracing {
appendTraceEntry(os);
Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
}
/**
* Writes the trace buffer to disk. This method has no internal synchronization and should be
* externally synchronized
*/
private void writeTraceToFileLocked() {
if (mTraceBuffer == null) {
return;
}
try {
mTraceBuffer.dump();
} catch (IOException e) {
Log.e(TAG, "Unable to write buffer to file", e);
} catch (InterruptedException e) {
Log.e(TAG, "Unable to interrupt window tracing file write thread", e);
}
}
/**
* Writes the trace buffer to disk and clones it into a new file for the bugreport.
* This method is synchronized with {@code #startTrace(PrintWriter)} and
* {@link #stopTrace(PrintWriter)}.
*/
void writeTraceToFile() {
synchronized (mLock) {
writeTraceToFileLocked();
}
}
}

View File

@@ -19,8 +19,6 @@ 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;
@@ -31,13 +29,14 @@ import android.util.proto.ProtoOutputStream;
import androidx.test.filters.SmallTest;
import com.android.internal.util.Preconditions;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
/**
@@ -49,8 +48,6 @@ import java.util.Arrays;
@SmallTest
@Presubmit
public class WindowTraceBufferTest {
private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
private File mFile;
@Before
@@ -81,18 +78,22 @@ public class WindowTraceBufferTest {
buffer.add(toWrite1);
assertTrue("First element should be in the list",
buffer.mBuffer.stream().anyMatch(p -> Arrays.equals(p, toWrite1Bytes)));
buffer.contains(toWrite1Bytes));
buffer.add(toWrite2);
assertTrue("First element should be in the list",
buffer.mBuffer.stream().anyMatch(p -> Arrays.equals(p, toWrite1Bytes)));
buffer.contains(toWrite1Bytes));
assertTrue("Second element should be in the list",
buffer.mBuffer.stream().anyMatch(p -> Arrays.equals(p, toWrite2Bytes)));
buffer.contains(toWrite2Bytes));
buffer.add(toWrite3);
assertTrue("First element should be in the list",
buffer.contains(toWrite1Bytes));
assertTrue("Second element should be in the list",
buffer.contains(toWrite2Bytes));
assertTrue("Third element should not be in the list",
buffer.mBuffer.stream().noneMatch(p -> Arrays.equals(p, toWrite3Bytes)));
!buffer.contains(toWrite3Bytes));
assertEquals("Buffer should have 2 elements", buffer.mBuffer.size(), 2);
assertEquals(String.format("Buffer is full, used space should be %d", bufferCapacity),
@@ -101,6 +102,111 @@ public class WindowTraceBufferTest {
buffer.getAvailableSpace(), 0);
}
@Test
public void testTraceRingBuffer_addItem() throws Exception {
ProtoOutputStream toWrite = getDummy(1);
final int objectSize = toWrite.getBytes().length;
final WindowTraceBuffer buffer = buildRingBuffer(objectSize);
Preconditions.checkArgument(buffer.mBuffer.isEmpty());
buffer.add(toWrite);
assertEquals("Item was not added to the buffer", buffer.mBuffer.size(), 1);
assertEquals("Total buffer getSize differs from inserted object",
buffer.mBufferSize, objectSize);
assertEquals("Available buffer space does not match used one",
buffer.getAvailableSpace(), 0);
}
@Test
public void testTraceRingBuffer_addItemMustOverwriteOne() 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 + 1;
final WindowTraceBuffer buffer = buildRingBuffer(bufferCapacity);
buffer.add(toWrite1);
assertTrue("First element should be in the list",
buffer.contains(toWrite1Bytes));
buffer.add(toWrite2);
assertTrue("First element should be in the list",
buffer.contains(toWrite1Bytes));
assertTrue("Second element should be in the list",
buffer.contains(toWrite2Bytes));
buffer.add(toWrite3);
assertTrue("First element should not be in the list",
!buffer.contains(toWrite1Bytes));
assertTrue("Second element should be in the list",
buffer.contains(toWrite2Bytes));
assertTrue("Third element should be in the list",
buffer.contains(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 - 1);
assertEquals(" Buffer is full, available space should be 0",
buffer.getAvailableSpace(), 1);
}
@Test
public void testTraceRingBuffer_addItemMustOverwriteMultiple() throws Exception {
ProtoOutputStream toWriteSmall1 = getDummy(1);
ProtoOutputStream toWriteSmall2 = getDummy(2);
byte[] toWriteSmall1Bytes = toWriteSmall1.getBytes();
byte[] toWriteSmall2Bytes = toWriteSmall2.getBytes();
final int objectSize = toWriteSmall1.getBytes().length;
final int bufferCapacity = objectSize * 2;
final WindowTraceBuffer buffer = buildRingBuffer(bufferCapacity);
ProtoOutputStream toWriteBig = new ProtoOutputStream();
toWriteBig.write(MAGIC_NUMBER, 1);
toWriteBig.write(MAGIC_NUMBER, 2);
byte[] toWriteBigBytes = toWriteBig.getBytes();
toWriteBig.flush();
buffer.add(toWriteSmall1);
assertTrue("First element should be in the list",
buffer.contains(toWriteSmall1Bytes));
buffer.add(toWriteSmall2);
assertTrue("First element should be in the list",
buffer.contains(toWriteSmall1Bytes));
assertTrue("Second element should be in the list",
buffer.contains(toWriteSmall2Bytes));
buffer.add(toWriteBig);
assertTrue("Third element should overwrite all others",
!buffer.contains(toWriteSmall1Bytes));
assertTrue("Third element should overwrite all others",
!buffer.contains(toWriteSmall2Bytes));
assertTrue("Third element should overwrite all others",
buffer.contains(toWriteBigBytes));
assertEquals(" Buffer should have only 1 big element", buffer.mBuffer.size(), 1);
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 WindowTraceBuffer buildRingBuffer(int capacity) throws IOException {
return new WindowTraceBuffer.Builder()
.setContinuousMode(true)
.setBufferCapacity(capacity)
.setTraceFile(mFile)
.build();
}
private ProtoOutputStream getDummy(int value) {
ProtoOutputStream toWrite = new ProtoOutputStream();
toWrite.write(MAGIC_NUMBER, value);