/*
 * Copyright (C) 2007 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 android.core;

import android.test.suitebuilder.annotation.LargeTest;
import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.Log;
import android.test.suitebuilder.annotation.Suppress;
import dalvik.system.VMRuntime;
import junit.framework.TestCase;

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.LinkedList;
import java.util.Random;


public class HeapTest extends TestCase {

    private static final String TAG = "HeapTest";

    /**
     * Returns a WeakReference to an object that has no
     * other references.  This is done in a separate method
     * to ensure that the Object's address isn't sitting in
     * a stale local register.
     */
    private WeakReference<Object> newRef() {
        return new WeakReference<Object>(new Object());
    }

    /**
     * Allocates the specified number of bytes. This is done in a separate method
     * to ensure that the Object's address isn't sitting in a stale local register.
     */
    private void allocateMemory(int size) {
        byte[] b = new byte[size];
    }

    @MediumTest
    public void testMinimumHeapSize() throws Exception {
        VMRuntime r = VMRuntime.getRuntime();
        final boolean RUN_FLAKY = false;

        long origSize = r.getMinimumHeapSize();
        if (RUN_FLAKY) {
            /* Check that the default value is zero.  This will break if anyone
             * in this process sets the minimum heap size to a positive value
             * before calling this test.
             */
            assertTrue(origSize == 0);
        }

        long size = 4 * 1024 * 1024;
        long oldSize = r.setMinimumHeapSize(size);
        assertTrue(oldSize == origSize);

        long newSize = r.getMinimumHeapSize();
        /* This will fail if the maximum heap size (-Xmx) is smaller than 4MB.
         */
        assertTrue(newSize == size);

        /* Make sure that getting the size doesn't change anything.
         */
        newSize = r.getMinimumHeapSize();
        assertTrue(newSize == size);

        /* This test is flaky; if the heap is already large and fragmented,
         * it can fail.  It can also fail if another thread causes a GC
         * at the wrong time.
         */
        if (RUN_FLAKY) {
            /* Increase the minimum size, allocate a big object, and make sure that
             * a GC didn't happen.
             */
            WeakReference ref = newRef();
            assertNotNull(ref.get());

            r.setMinimumHeapSize(8 * 1024 * 1024);
            allocateMemory(4 * 1024 * 1024);

            /* If a GC happened, this reference will be null.
             */
            assertNotNull(ref.get());
        }

        /* Restore the original setting.
         */
        r.setMinimumHeapSize(origSize);
        newSize = r.getMinimumHeapSize();
        assertTrue(newSize == origSize);

        /* Clean up any large stuff we've allocated,
         * and re-establish the normal utilization ratio.
         */
        Runtime.getRuntime().gc();
    }

    private static void makeRefs(Object objects[], SoftReference<Object> refs[]) {
        for (int i = 0; i < objects.length; i++) {
            objects[i] = (Object) new byte[8 * 1024];
            refs[i] = new SoftReference<Object>(objects[i]);
        }
    }

    private static <T> int checkRefs(SoftReference<T> refs[], int last) {
        int i;
        int numCleared = 0;
        for (i = 0; i < refs.length; i++) {
            Object o = refs[i].get();
            if (o == null) {
                numCleared++;
            }
        }
        if (numCleared != last) {
            Log.i(TAG, "****** " + numCleared + "/" + i + " cleared ******");
        }
        return numCleared;
    }

    private static void clearRefs(Object objects[], int skip) {
        for (int i = 0; i < objects.length; i += skip) {
            objects[i] = null;
        }
    }

    private static void clearRefs(Object objects[]) {
        clearRefs(objects, 1);
    }

    private static <T> void checkRefs(T objects[], SoftReference<T> refs[]) {
        boolean ok = true;

        for (int i = 0; i < objects.length; i++) {
            if (refs[i].get() != objects[i]) {
                ok = false;
            }
        }
        if (!ok) {
            throw new RuntimeException("Test failed: soft refs not cleared");
        }
    }

    @MediumTest
    public void testGcSoftRefs() throws Exception {
        final int NUM_REFS = 128;

        Object objects[] = new Object[NUM_REFS];
        SoftReference<Object> refs[] = new SoftReference[objects.length];

        /* Create a bunch of objects and a parallel array
         * of SoftReferences.
         */
        makeRefs(objects, refs);
        Runtime.getRuntime().gc();

        /* Let go of some of the hard references to the objects so that
         * the references can be cleared.
         */
        clearRefs(objects, 3);

        /* Collect all softly-reachable objects.
         */
        VMRuntime.getRuntime().gcSoftReferences();
        Runtime.getRuntime().runFinalization();

        /* Make sure that the objects were collected.
         */
        checkRefs(objects, refs);

        /* Remove more hard references and re-check.
         */
        clearRefs(objects, 2);
        VMRuntime.getRuntime().gcSoftReferences();
        Runtime.getRuntime().runFinalization();
        checkRefs(objects, refs);

        /* Remove the rest of the references and re-check.
         */
        /* Remove more hard references and re-check.
         */
        clearRefs(objects);
        VMRuntime.getRuntime().gcSoftReferences();
        Runtime.getRuntime().runFinalization();
        checkRefs(objects, refs);
    }

    public void xxtestSoftRefPartialClean() throws Exception {
        final int NUM_REFS = 128;

        Object objects[] = new Object[NUM_REFS];
        SoftReference<Object> refs[] = new SoftReference[objects.length];

        /* Create a bunch of objects and a parallel array
        * of SoftReferences.
        */
        makeRefs(objects, refs);
        Runtime.getRuntime().gc();

        /* Let go of the hard references to the objects so that
        * the references can be cleared.
        */
        clearRefs(objects);

        /* Start creating a bunch of temporary and permanent objects
        * to drive GC.
        */
        final int NUM_OBJECTS = 64 * 1024;
        Object junk[] = new Object[NUM_OBJECTS];
        Random random = new Random();

        int i = 0;
        int mod = 0;
        int totalSize = 0;
        int cleared = -1;
        while (i < junk.length && totalSize < 8 * 1024 * 1024) {
            int r = random.nextInt(64 * 1024) + 128;
            Object o = (Object) new byte[r];
            if (++mod % 16 == 0) {
                junk[i++] = o;
                totalSize += r * 4;
            }
            cleared = checkRefs(refs, cleared);
        }
    }

    private static void makeRefs(Object objects[], WeakReference<Object> refs[]) {
        for (int i = 0; i < objects.length; i++) {
            objects[i] = new Object();
            refs[i] = new WeakReference<Object>(objects[i]);
        }
    }

    private static <T> void checkRefs(T objects[], WeakReference<T> refs[]) {
        boolean ok = true;

        for (int i = 0; i < objects.length; i++) {
            if (refs[i].get() != objects[i]) {
                ok = false;
            }
        }
        if (!ok) {
            throw new RuntimeException("Test failed: " +
                    "weak refs not cleared");
        }
    }

    @MediumTest
    public void testWeakRefs() throws Exception {
        final int NUM_REFS = 16;

        Object objects[] = new Object[NUM_REFS];
        WeakReference<Object> refs[] = new WeakReference[objects.length];

        /* Create a bunch of objects and a parallel array
        * of WeakReferences.
        */
        makeRefs(objects, refs);
        Runtime.getRuntime().gc();
        checkRefs(objects, refs);

        /* Clear out every other strong reference.
        */
        for (int i = 0; i < objects.length; i += 2) {
            objects[i] = null;
        }
        Runtime.getRuntime().gc();
        checkRefs(objects, refs);

        /* Clear out the rest of them.
        */
        for (int i = 0; i < objects.length; i++) {
            objects[i] = null;
        }
        Runtime.getRuntime().gc();
        checkRefs(objects, refs);
    }

    private static void makeRefs(Object objects[], PhantomReference<Object> refs[],
            ReferenceQueue<Object> queue) {
        for (int i = 0; i < objects.length; i++) {
            objects[i] = new Object();
            refs[i] = new PhantomReference<Object>(objects[i], queue);
        }
    }

    static <T> void checkRefs(T objects[], PhantomReference<T> refs[],
            ReferenceQueue<T> queue) {
        boolean ok = true;

        /* Make sure that the reference that should be on
        * the queue are marked as enqueued.  Once we
        * pull them off the queue, they will no longer
        * be marked as enqueued.
        */
        for (int i = 0; i < objects.length; i++) {
            if (objects[i] == null && refs[i] != null) {
                if (!refs[i].isEnqueued()) {
                    ok = false;
                }
            }
        }
        if (!ok) {
            throw new RuntimeException("Test failed: " +
                    "phantom refs not marked as enqueued");
        }

        /* Make sure that all of the references on the queue
        * are supposed to be there.
        */
        PhantomReference<T> ref;
        while ((ref = (PhantomReference<T>) queue.poll()) != null) {
            /* Find the list index that corresponds to this reference.
            */
            int i;
            for (i = 0; i < objects.length; i++) {
                if (refs[i] == ref) {
                    break;
                }
            }
            if (i == objects.length) {
                throw new RuntimeException("Test failed: " +
                        "unexpected ref on queue");
            }
            if (objects[i] != null) {
                throw new RuntimeException("Test failed: " +
                        "reference enqueued for strongly-reachable " +
                        "object");
            }
            refs[i] = null;

            /* TODO: clear doesn't do much, since we're losing the
            * strong ref to the ref object anyway.  move the ref
            * into another list.
            */
            ref.clear();
        }

        /* We've visited all of the enqueued references.
        * Make sure that there aren't any other references
        * that should have been enqueued.
        *
        * NOTE: there is a race condition here;  this assumes
        * that the VM has serviced all outstanding reference
        * enqueue() calls.
        */
        for (int i = 0; i < objects.length; i++) {
            if (objects[i] == null && refs[i] != null) {
//                System.out.println("HeapTest/PhantomRefs: refs[" + i +
//                        "] should be enqueued");
                ok = false;
            }
        }
        if (!ok) {
            throw new RuntimeException("Test failed: " +
                    "phantom refs not enqueued");
        }
    }

    @MediumTest
    public void testPhantomRefs() throws Exception {
        final int NUM_REFS = 16;

        Object objects[] = new Object[NUM_REFS];
        PhantomReference<Object> refs[] = new PhantomReference[objects.length];
        ReferenceQueue<Object> queue = new ReferenceQueue<Object>();

        /* Create a bunch of objects and a parallel array
        * of PhantomReferences.
        */
        makeRefs(objects, refs, queue);
        Runtime.getRuntime().gc();
        checkRefs(objects, refs, queue);

        /* Clear out every other strong reference.
        */
        for (int i = 0; i < objects.length; i += 2) {
            objects[i] = null;
        }
        // System.out.println("HeapTest/PhantomRefs: cleared evens");
        Runtime.getRuntime().gc();
        Runtime.getRuntime().runFinalization();
        checkRefs(objects, refs, queue);

        /* Clear out the rest of them.
        */
        for (int i = 0; i < objects.length; i++) {
            objects[i] = null;
        }
        // System.out.println("HeapTest/PhantomRefs: cleared all");
        Runtime.getRuntime().gc();
        Runtime.getRuntime().runFinalization();
        checkRefs(objects, refs, queue);
    }

    private static int sNumFinalized = 0;
    private static final Object sLock = new Object();

    private static class FinalizableObject {
        protected void finalize() {
            // System.out.println("gc from finalize()");
            Runtime.getRuntime().gc();
            synchronized (sLock) {
                sNumFinalized++;
            }
        }
    }

    private static void makeRefs(FinalizableObject objects[],
            WeakReference<FinalizableObject> refs[]) {
        for (int i = 0; i < objects.length; i++) {
            objects[i] = new FinalizableObject();
            refs[i] = new WeakReference<FinalizableObject>(objects[i]);
        }
    }

    @LargeTest
    public void testWeakRefsAndFinalizers() throws Exception {
        final int NUM_REFS = 16;

        FinalizableObject objects[] = new FinalizableObject[NUM_REFS];
        WeakReference<FinalizableObject> refs[] = new WeakReference[objects.length];
        int numCleared;

        /* Create a bunch of objects and a parallel array
        * of WeakReferences.
        */
        makeRefs(objects, refs);
        Runtime.getRuntime().gc();
        checkRefs(objects, refs);

        /* Clear out every other strong reference.
        */
        sNumFinalized = 0;
        numCleared = 0;
        for (int i = 0; i < objects.length; i += 2) {
            objects[i] = null;
            numCleared++;
        }
        // System.out.println("HeapTest/WeakRefsAndFinalizers: cleared evens");
        Runtime.getRuntime().gc();
        Runtime.getRuntime().runFinalization();
        checkRefs(objects, refs);
        if (sNumFinalized != numCleared) {
            throw new RuntimeException("Test failed: " +
                    "expected " + numCleared + " finalizations, saw " +
                    sNumFinalized);
        }

        /* Clear out the rest of them.
        */
        sNumFinalized = 0;
        numCleared = 0;
        for (int i = 0; i < objects.length; i++) {
            if (objects[i] != null) {
                objects[i] = null;
                numCleared++;
            }
        }
        // System.out.println("HeapTest/WeakRefsAndFinalizers: cleared all");
        Runtime.getRuntime().gc();
        Runtime.getRuntime().runFinalization();
        checkRefs(objects, refs);
        if (sNumFinalized != numCleared) {
            throw new RuntimeException("Test failed: " +
                    "expected " + numCleared + " finalizations, saw " +
                    sNumFinalized);
        }
    }

    // TODO: flaky test
    //@MediumTest
    public void testOomeLarge() throws Exception {
        /* Just shy of the typical max heap size so that it will actually
         * try to allocate it instead of short-circuiting.
         */
        final int SIXTEEN_MB = (16 * 1024 * 1024 - 32);

        Boolean sawEx = false;
        byte a[];

        try {
            a = new byte[SIXTEEN_MB];
        } catch (OutOfMemoryError oom) {
            //Log.i(TAG, "HeapTest/OomeLarge caught " + oom);
            sawEx = true;
        }

        if (!sawEx) {
            throw new RuntimeException("Test failed: " +
                    "OutOfMemoryError not thrown");
        }
    }

    //See bug 1308253 for reasons.
    @Suppress
    public void disableTestOomeSmall() throws Exception {
        final int SIXTEEN_MB = (16 * 1024 * 1024);
        final int LINK_SIZE = 6 * 4; // estimated size of a LinkedList's node

        Boolean sawEx = false;

        LinkedList<Object> list = new LinkedList<Object>();

        /* Allocate progressively smaller objects to fill up the entire heap.
         */
        int objSize = 1 * 1024 * 1024;
        while (objSize >= LINK_SIZE) {
            try {
                for (int i = 0; i < SIXTEEN_MB / objSize; i++) {
                    list.add((Object)new byte[objSize]);
                }
            } catch (OutOfMemoryError oom) {
                sawEx = true;
            }

            if (!sawEx) {
                throw new RuntimeException("Test failed: " +
                        "OutOfMemoryError not thrown while filling heap");
            }
            sawEx = false;

            objSize = (objSize * 4) / 5;
        }
    }

    // TODO: flaky test
    //@SmallTest
    public void testExternalOomeLarge() {
        /* Just shy of the typical max heap size so that it will actually
         * try to allocate it instead of short-circuiting.
         */
        final int HUGE_SIZE = (16 * 1024 * 1024 - 32);

        assertFalse(VMRuntime.getRuntime().trackExternalAllocation(HUGE_SIZE));
    }

    /**
     * "Allocates" external memory in progressively smaller chunks until there's
     * only roughly 16 bytes left.
     *
     * @return the number of bytes allocated
     */
    private long allocateMaxExternal() {
        final VMRuntime runtime = VMRuntime.getRuntime();
        final int SIXTEEN_MB = (16 * 1024 * 1024);
        final int MIN_SIZE = 16;
        long totalAllocated = 0;
        boolean success;

        success = false;
        try {
            /* "Allocate" progressively smaller chunks to "fill up" the entire heap.
             */
            int objSize = 1 * 1024 * 1024;
            while (objSize >= MIN_SIZE) {
                boolean sawFailure = false;
                for (int i = 0; i < SIXTEEN_MB / objSize; i++) {
                    if (runtime.trackExternalAllocation(objSize)) {
                        totalAllocated += objSize;
                    } else {
                        sawFailure = true;
                        break;
                    }
                }

                if (!sawFailure) {
                    throw new RuntimeException("Test failed: " +
                            "no failure while filling heap");
                }

                objSize = (objSize * 4) / 5;
            }
            success = true;
        } finally {
            if (!success) {
                runtime.trackExternalFree(totalAllocated);
                totalAllocated = 0;
            }
        }
        return totalAllocated;
    }

    public void xxtest00ExternalOomeSmall() {
        VMRuntime.getRuntime().trackExternalFree(allocateMaxExternal());
    }

    /**
     * Allocates as much external memory as possible, then allocates from the heap
     * until an OOME is caught.
     *
     * It's nice to run this test while the real heap is small, hence the '00' in its
     * name to force it to run before testOomeSmall().
     */
    public void xxtest00CombinedOomeSmall() {
        long totalAllocated = 0;
        boolean sawEx = false;
        try {
            totalAllocated = allocateMaxExternal();
            LinkedList<Object> list = new LinkedList<Object>();
            try {
                while (true) {
                    list.add((Object)new byte[8192]);
                }
                /*NOTREACHED*/
            } catch (OutOfMemoryError oom) {
                sawEx = true;
            }
        } finally {
            VMRuntime.getRuntime().trackExternalFree(totalAllocated);
        }
        assertTrue(sawEx);
    }

    //TODO: test external alloc debugging/inspection
}
