Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 98d92aa5 authored by Nataniel Borges's avatar Nataniel Borges
Browse files

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

Currently the WM uses a BlockingQueue with limited capacity to store
the traces and a separate thread to continuously write the buffer
elements to disk. This implementation allows the WM trace to use its
current logging behavior or to use a ring buffer instead.

The ring buffer:
- Stores the most recent traces and discard the oldest ones when it
reaches maximum capacity.
- Write to disk only when the trace logging is stopped or during a
bugreport, instead of having a thread to continuously write to disk.

The ring buffer can be selected (or disabled)  using the command:
- adb shell cmd window tracing continuous (true/false)
Any value other than "true" (case insensitive) is considered false

After selecting ring buffer, start and stop WM trace
monitoring with 'adb shell cmd window tracing (start/stop)'

Current capacity is fixed to 512KB. Future implementations will make
this value configurable.

Test: Flash a device. Enable continuous mode and window manager
tracing. Use the device. Stop the trace and pull the logged trace.
Open the trace in Winscope and check if the last X KB of data are
there. Unit tests: atest WmTests:WindowTraceBufferTest

Change-Id: Id4108b0231ab7af504240ea35c8a030fecf8b8a9
parent ffa0d6c9
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -6045,6 +6045,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");
@@ -6118,6 +6119,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)) {
+1 −2
Original line number Diff line number Diff line
@@ -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":
+40 −48
Original line number Diff line number Diff line
@@ -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;
        try {
            proto = take();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return;
    }

    /**
     * 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 {
            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.");
            }

            if (mContinuous) {
                return new WindowTraceRingBuffer(mBufferCapacity, mTraceFile);
            } else {
                return new WindowTraceQueueBuffer(mBufferCapacity, mTraceFile);
            }
        }
    }
}
+43 −25
Original line number Diff line number Diff line
@@ -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();

                proto = mBuffer.poll();
                if (proto != null) {
                    mBufferSize -= proto.length;
                }
            }

    private void restartWriterThread() throws InterruptedException {
        if (mWriterThread != null) {
            mCancel = true;
            mWriterThread.interrupt();
            mWriterThread.join();
            initializeWriterThread();
            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();
    }
}
+68 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
Loading