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

Commit aaaa1ed6 authored by Mark Fasheh's avatar Mark Fasheh Committed by Android (Google) Code Review
Browse files

Merge changes I6b9ec5ab,I07ae62e3 into main

* changes:
  Add MessageHeap a min-heap for Message objects
  MessageQueue: extract compareMessages to Message.java
parents 266149e7 c840603f
Loading
Loading
Loading
Loading
+2 −18
Original line number Diff line number Diff line
@@ -289,28 +289,12 @@ public final class MessageQueue {
    static final class EnqueueOrder implements Comparator<Message> {
        @Override
        public int compare(Message m1, Message m2) {
            return compareMessages(m1, m2);
            return Message.compareMessages(m1, m2);
        }
    }

    private static final EnqueueOrder sEnqueueOrder = new EnqueueOrder();

    static int compareMessages(@NonNull Message m1, @NonNull Message m2) {
        // Primary queue order is by when.
        // Messages with an earlier when should come first in the queue.
        final long whenDiff = m1.when - m2.when;
        if (whenDiff > 0) return 1;
        if (whenDiff < 0) return -1;

        // Secondary queue order is by insert sequence.
        // If two messages were inserted with the same `when`, the one inserted
        // first should come first in the queue.
        final long insertSeqDiff = m1.insertSeq - m2.insertSeq;
        if (insertSeqDiff > 0) return 1;
        if (insertSeqDiff < 0) return -1;

        return 0;
    }

    private static boolean isBarrier(Message msg) {
        return msg != null && msg.target == null;
@@ -806,7 +790,7 @@ public final class MessageQueue {
                if (msg == null) {
                    earliest = asyncMsg;
                } else if (asyncMsg != null) {
                    if (compareMessages(msg, asyncMsg) > 0) {
                    if (Message.compareMessages(msg, asyncMsg) > 0) {
                        earliest = asyncMsg;
                    }
                }
+23 −1
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package android.os;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.util.TimeUtils;
@@ -143,7 +144,11 @@ public final class Message implements Parcelable {
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    public long when;

    /*package*/ long insertSeq;
    /** @hide */
    public long insertSeq;

    /** @hide */
    public int heapIndex;

    /*package*/ Bundle data;

@@ -547,6 +552,23 @@ public final class Message implements Parcelable {
    public Message() {
    }

    /*package*/ static int compareMessages(@NonNull Message m1, @NonNull Message m2) {
        // Primary queue order is by when.
        // Messages with an earlier when should come first in the queue.
        final long whenDiff = m1.when - m2.when;
        if (whenDiff > 0) return 1;
        if (whenDiff < 0) return -1;

        // Secondary queue order is by insert sequence.
        // If two messages were inserted with the same `when`, the one inserted
        // first should come first in the queue.
        final long insertSeqDiff = m1.insertSeq - m2.insertSeq;
        if (insertSeqDiff > 0) return 1;
        if (insertSeqDiff < 0) return -1;

        return 0;
    }

    @Override
    public String toString() {
        return toString(SystemClock.uptimeMillis());
+324 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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 athasEqualMessages
 *
 *      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 android.os;

import android.os.Message;

import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import dalvik.annotation.optimization.NeverCompile;

import java.util.Arrays;

/**
 * A min-heap of Message objects. Used by MessageQueue.
 * @hide
 */
public final class MessageHeap {
    static final int INITIAL_SIZE = 16;
    static final boolean DEBUG = false;
    private static final String TAG = "MessageHeap";

    Message[] mHeap = new Message[INITIAL_SIZE];
    int mNumElements = 0;

    private static int parentNodeIdx(int i) {
        return (i - 1) >>> 1;
    }

    private @Nullable Message getParentNode(int i) {
        return mHeap[parentNodeIdx(i)];
    }

    private static int rightNodeIdx(int i) {
        return 2 * i + 2;
    }

    private @Nullable Message getRightNode(int i) {
        return mHeap[rightNodeIdx(i)];
    }

    private static int leftNodeIdx(int i) {
        return 2 * i + 1;
    }

    private @Nullable Message getLeftNode(int i) {
        return mHeap[leftNodeIdx(i)];
    }

    public int capacity() {
        return mHeap.length;
    }

    public int size() {
        return mNumElements;
    }

    public boolean isEmpty() {
        return mNumElements == 0;
    }

    @Nullable Message getMessageAt(int index) {
        return mHeap[index];
    }

    /*
     * Returns:
     *    0 if x==y.
     *    A value less than 0 if x<y.
     *    A value greater than 0 if x>y.
     */
    private int compareMessagesByIdx(int x, int y) {
        return Message.compareMessages(mHeap[x], mHeap[y]);
    }

    private void swap(int x, int y) {
        Message tmp = mHeap[x];
        mHeap[x] = mHeap[y];
        mHeap[y] = tmp;
    }

    private void siftDown(int i) {
        int smallest = i;
        int right, left;

        while (true) {
            right = rightNodeIdx(i);
            left = leftNodeIdx(i);

            if (right < mNumElements && compareMessagesByIdx(right, smallest) < 0) {
                smallest = right;
            }

            if (left < mNumElements && compareMessagesByIdx(left, smallest) < 0) {
                smallest = left;
            }

            if (smallest != i) {
                swap(i, smallest);
                i = smallest;
                continue;
            }
            break;
        }
    }

    private boolean siftUp(int i) {
        boolean swapped = false;
        /*
         * We never pass null to compareMessages here, mHeap[i] is known to be occupied as is
         * its parent node
         */
        while (i != 0 && Message.compareMessages(mHeap[i], getParentNode(i)) < 0) {
            int p = parentNodeIdx(i);

            swap(i, p);
            swapped = true;
            i = p;
        }

        return swapped;
    }

    private void maybeGrow() {
        if (mNumElements == mHeap.length) {
            /*
             * Grow by 1.5x. We shrink by a factor of two below to avoid pinging between
             * grow/shrink.
             */
            int newSize = mHeap.length + (mHeap.length >>> 1);
            Message[] newHeap;
            if (DEBUG) {
                Log.d(TAG, "maybeGrow mNumElements " + mNumElements + " mHeap.length "
                        + mHeap.length + " newSize " + newSize);
            }

            newHeap = Arrays.copyOf(mHeap, newSize);
            mHeap = newHeap;
        }
    }

    public void add(@NonNull Message m) {
        maybeGrow();

        int i = mNumElements++;
        m.heapIndex = i;
        mHeap[i] = m;

        siftUp(i);
    }

    public void maybeShrink() {
        int nextShrinkSize = mHeap.length;
        final int minElem = Math.max(mNumElements, INITIAL_SIZE);
        int newSize = INITIAL_SIZE;
        while (nextShrinkSize > minElem) {
            newSize = nextShrinkSize;
            nextShrinkSize = nextShrinkSize >>> 1;
        }
        if (DEBUG) {
            Log.d(TAG, "maybeShrink chosen new size " + newSize + " mNumElements "
                    + mNumElements + " mHeap.length " + mHeap.length);
        }

        if (newSize >= INITIAL_SIZE
                && mNumElements <= newSize) {
            Message[] newHeap;

            newHeap = Arrays.copyOf(mHeap, newSize);
            mHeap = newHeap;

            if (DEBUG) {
                Log.d(TAG, "maybeShrink SHRUNK mNumElements " + mNumElements + " mHeap.length "
                        + mHeap.length + " newSize " + newSize);
            }
        }
    }

    public @Nullable Message poll() {
        if (mNumElements > 0) {
            Message ret = mHeap[0];
            mNumElements--;

            mHeap[0] = mHeap[mNumElements];
            mHeap[mNumElements] = null;

            siftDown(0);

            maybeShrink();
            return ret;
        }
        return null;
    }

    public @Nullable Message peek() {
        return mNumElements > 0 ? mHeap[0] : null;
    }

    public void remove(int i) throws IllegalArgumentException {
        if (i >= mNumElements || mNumElements == 0 || i < 0) {
            throw new IllegalArgumentException("Index " + i + " out of bounds: "
                    + mNumElements);
        } else if (i == (mNumElements - 1)) {
            mHeap[i] = null;
            mNumElements--;
        } else {
            mNumElements--;
            mHeap[i] = mHeap[mNumElements];
            mHeap[mNumElements] = null;
            if (!siftUp(i)) {
                siftDown(i);
            }
        }
        /* Don't shink here, let the caller do this once it has removed all matching items. */
    }

    public void removeMessage(@NonNull Message m) throws IllegalArgumentException {
        remove(m.heapIndex);
    }

    public void removeAll() {
        mHeap = new Message[INITIAL_SIZE];
        mNumElements = 0;
    }

    @Override
    public String toString() {
        StringBuilder b = new StringBuilder();
        b.append("MessageHeap size: ");
        b.append(mNumElements);
        b.append(" mHeap.length ");
        b.append(mHeap.length);
        for (int i = 0; i < mNumElements; i++) {
            b.append(" [");
            b.append(i);
            b.append("]\t");
            b.append(mHeap[i].when);
            b.append(" seq: ");
            b.append(mHeap[i].insertSeq);
            b.append(" async: ");
            b.append(mHeap[i].isAsynchronous());
        }
        return b.toString();
    }

    @NeverCompile
    private boolean verify(int root) {
        int right = rightNodeIdx(root);
        int left = leftNodeIdx(root);

        if (left >= mNumElements && right >= mNumElements) {
            return true;
        }

        if (left < mNumElements && compareMessagesByIdx(left, root) < 0) {
            Log.e(TAG, "Verify failure: root idx/when: " + root + "/" + mHeap[root].when
                    + " left node idx/when: " + left + "/" + mHeap[left].when);
            return false;
        }

        if (right < mNumElements && compareMessagesByIdx(right, root) < 0) {
            Log.e(TAG, "Verify failure: root idx/when: " + root + "/" + mHeap[root].when
                    + " right node idx/when: " + right + "/" + mHeap[right].when);
            return false;
        }

        if (!verify(right) || !verify(left)) {
            return false;
        }

        int localHeapCount = 0;
        for (int i = 0; i < mHeap.length; i++) {
            if (mHeap[i] != null) {
                localHeapCount++;
            }
        }

        if (mNumElements != localHeapCount) {
            Log.e(TAG, "Verify failure mNumLElements is " + mNumElements +
                    " but I counted " + localHeapCount + " heap elements");
            return false;
        }
        return true;
    }

    @NeverCompile
    private boolean checkDanglingReferences(String where) {
        /* First, let's make sure we didn't leave any dangling references */
        for (int i = mNumElements; i < mHeap.length; i++) {
            if (mHeap[i] != null) {
                Log.e(TAG, "[" + where
                        + "] Verify failure: dangling reference found at index "
                        + i + ": " + mHeap[i] + " Async " + mHeap[i].isAsynchronous()
                        + " mNumElements " + mNumElements + " mHeap.length " + mHeap.length);
                return false;
            }
        }
        return true;
    }

    @NeverCompile
    public boolean verify() {
        if (!checkDanglingReferences("MessageHeap")) {
            return false;
        }
        return verify(0);
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -131,6 +131,7 @@ per-file TestLooperManager.java = file:/MESSAGE_QUEUE_OWNERS
per-file Handler.java = file:/MESSAGE_QUEUE_OWNERS
per-file HandlerThread.java = file:/MESSAGE_QUEUE_OWNERS
per-file HandlerExecutor.java = file:/MESSAGE_QUEUE_OWNERS
per-file MessageHeap.java = file:/MESSAGE_QUEUE_OWNERS

# Stats
per-file IStatsBootstrapAtomService.aidl = file:/services/core/java/com/android/server/stats/OWNERS
+198 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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 athasEqualMessages
 *
 *      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 android.os;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import android.os.Message;
import android.os.MessageHeap;

import android.util.Log;

import androidx.test.runner.AndroidJUnit4;

import java.util.concurrent.atomic.AtomicLong;
import java.util.Random;

import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4.class)
public final class MessageHeapTest {
    private static final String TAG = "MessageHeapTest";

    private final AtomicLong mNextInsertSeq = new AtomicLong(1);
    private final AtomicLong mNextFrontInsertSeq = new AtomicLong(-1);

    private final Random mRand = new Random(8675309);

    private void insertMessage(MessageHeap heap, long when) {
        long seq = when != 0 ? mNextInsertSeq.incrementAndGet()
                : mNextFrontInsertSeq.decrementAndGet();

        Message m = new Message();
        m.when = when;
        m.insertSeq = seq;
        heap.add(m);
    }

    private static boolean popAndVerifyUntilEmpty(MessageHeap heap) {
        Message last = null;
        Message m;

        while (!heap.isEmpty()) {
            m = heap.poll();
            if (last != null && m.when < last.when) {
                Log.e(TAG, "popAndVerifyUntilEmpty: heap property broken last: " + last.when
                        + " popped: " + m.when);
                return false;
            }
            last = m;
        }
        return true;
    }

    private static boolean verify(MessageHeap heap) {
        if (!heap.verify()) {
            return false;
        }

        if (!popAndVerifyUntilEmpty(heap)) {
            return false;
        }
        return true;
    }

    private long getPositiveRandLong() {
        try {
            return Math.absExact(mRand.nextLong());
        } catch (ArithmeticException e) {
            return 0;
        }
    }

    private MessageHeap fillWithRandomValues(int numValues) {
        MessageHeap heap = new MessageHeap();
        for (int i = 0; i < numValues; i++) {
            insertMessage(heap, getPositiveRandLong());
        }
        return heap;
    }

    @Test
    public void fillSequentially() {
        MessageHeap heap = new MessageHeap();
        for (int i = 0; i < 4_000; i++) {
            insertMessage(heap, i);
        }
        assertTrue(verify(heap));
    }

    @Test
    public void reverseOrderTest() {
        MessageHeap heap = new MessageHeap();
        for (int i = 99; i >= 90; i--) {
            insertMessage(heap, i);
        }
        assertTrue(verify(heap));
    }

    @Test
    public void zerosTest() {
        MessageHeap heap = new MessageHeap();
        for (int i = 0; i < 10; i++) {
            insertMessage(heap, 0);
        }
        assertTrue(verify(heap));
    }

    @Test
    public void randomValuesTest() {
        final int numValues = 4_000;
        MessageHeap heap = new MessageHeap();
        for (int i = 0; i < numValues; i++) {
            insertMessage(heap, getPositiveRandLong());
        }
        assertTrue(verify(heap));
    }

    @Test
    public void fillWithRandomValuesAndZeros() {
        final int numValues = 4_000;
        MessageHeap heap = fillWithRandomValues(numValues);
        for (int i = 0; i < numValues; i++) {
            long when = getPositiveRandLong();
            if ((when % 4) == 0) {
                when = 0;
            }
            insertMessage(heap, when);
        }
        assertTrue(verify(heap));
    }

    @Test
    public void randomRemoveTest() {
        MessageHeap heap = fillWithRandomValues(4_000);
        for (int i = 0; i < heap.size(); i++) {
            if (mRand.nextBoolean()) {
                heap.remove(i);
            }
        }
        heap.maybeShrink();
        assertTrue(verify(heap));
    }

    @Test
    public void growTest() {
        MessageHeap heap = fillWithRandomValues(4_000);
        assertTrue(verify(heap));
    }

    @Test
    public void shrinkSlightly() {
        MessageHeap heap = fillWithRandomValues(4_000);
        for (int i = 0; i < 1_000; i++) {
            heap.remove(i);
        }
        heap.maybeShrink();
        assertTrue(verify(heap));
    }

    @Test
    public void shrinkCompletely() {
        MessageHeap heap = fillWithRandomValues(4_000);
        heap.removeAll();
        assertTrue(verify(heap));
    }

    @Test
    public void removeBoundsTest() {
        MessageHeap heap = new MessageHeap();
        for (int i = 0; i < 10; i++) {
            insertMessage(heap, 0);
        }
        try {
            heap.remove(heap.size());
            heap.remove(-1);
            fail("Expected IllegalArgumentException for out of bounds test");
        } catch (IllegalArgumentException e) {

        }
        assertTrue(verify(heap));
    }
}
Loading