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

Commit 5a38bad6 authored by Nataniel Borges's avatar Nataniel Borges
Browse files

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
parent 1bafe67c
Loading
Loading
Loading
Loading
+181 −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 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);
        }
    }
}
+88 −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 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();
    }
}
+36 −66
Original line number Diff line number Diff line
@@ -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,18 +41,25 @@ 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 {
@@ -69,15 +68,15 @@ class WindowTracing {
            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;
        }
    }
@@ -96,62 +95,37 @@ class WindowTracing {
            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) {
+115 −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 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);
    }
}