/*
 * Copyright (C) 2012 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.gallery3d.util;

import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;

import java.util.ArrayList;
import java.util.Random;

// The Profile class is used to collect profiling information for a thread. It
// samples stack traces for a thread periodically. enable() and disable() is
// used to enable and disable profiling for the calling thread. The profiling
// information can then be dumped to a file using the dumpToFile() method.
//
// The disableAll() method can be used to disable profiling for all threads and
// can be called in onPause() to ensure all profiling is disabled when an
// activity is paused.
public class Profile {
    @SuppressWarnings("unused")
    private static final String TAG = "Profile";
    private static final int NS_PER_MS = 1000000;

    // This is a watchdog entry for one thread.
    // For every cycleTime period, we dump the stack of the thread.
    private static class WatchEntry {
        Thread thread;

        // Both are in milliseconds
        int cycleTime;
        int wakeTime;

        boolean isHolding;
        ArrayList<String[]> holdingStacks = new ArrayList<String[]>();
    }

    // This is a watchdog thread which dumps stacks of other threads periodically.
    private static Watchdog sWatchdog = new Watchdog();

    private static class Watchdog {
        private ArrayList<WatchEntry> mList = new ArrayList<WatchEntry>();
        private HandlerThread mHandlerThread;
        private Handler mHandler;
        private Runnable mProcessRunnable = new Runnable() {
            @Override
            public void run() {
                synchronized (Watchdog.this) {
                    processList();
                }
            }
        };
        private Random mRandom = new Random();
        private ProfileData mProfileData = new ProfileData();

        public Watchdog() {
            mHandlerThread = new HandlerThread("Watchdog Handler",
                    Process.THREAD_PRIORITY_FOREGROUND);
            mHandlerThread.start();
            mHandler = new Handler(mHandlerThread.getLooper());
        }

        public synchronized void addWatchEntry(Thread thread, int cycleTime) {
            WatchEntry e = new WatchEntry();
            e.thread = thread;
            e.cycleTime = cycleTime;
            int firstDelay = 1 + mRandom.nextInt(cycleTime);
            e.wakeTime = (int) (System.nanoTime() / NS_PER_MS) + firstDelay;
            mList.add(e);
            processList();
        }

        public synchronized void removeWatchEntry(Thread thread) {
            for (int i = 0; i < mList.size(); i++) {
                if (mList.get(i).thread == thread) {
                    mList.remove(i);
                    break;
                }
            }
            processList();
        }

        public synchronized void removeAllWatchEntries() {
            mList.clear();
            processList();
        }

        private void processList() {
            mHandler.removeCallbacks(mProcessRunnable);
            if (mList.size() == 0) return;

            int currentTime = (int) (System.nanoTime() / NS_PER_MS);
            int nextWakeTime = 0;

            for (WatchEntry entry : mList) {
                if (currentTime > entry.wakeTime) {
                    entry.wakeTime += entry.cycleTime;
                    Thread thread = entry.thread;
                    sampleStack(entry);
                }

                if (entry.wakeTime > nextWakeTime) {
                    nextWakeTime = entry.wakeTime;
                }
            }

            long delay = nextWakeTime - currentTime;
            mHandler.postDelayed(mProcessRunnable, delay);
        }

        private void sampleStack(WatchEntry entry) {
            Thread thread = entry.thread;
            StackTraceElement[] stack = thread.getStackTrace();
            String[] lines = new String[stack.length];
            for (int i = 0; i < stack.length; i++) {
                lines[i] = stack[i].toString();
            }
            if (entry.isHolding) {
                entry.holdingStacks.add(lines);
            } else {
                mProfileData.addSample(lines);
            }
        }

        private WatchEntry findEntry(Thread thread) {
            for (int i = 0; i < mList.size(); i++) {
                WatchEntry entry = mList.get(i);
                if (entry.thread == thread) return entry;
            }
            return null;
        }

        public synchronized void dumpToFile(String filename) {
            mProfileData.dumpToFile(filename);
        }

        public synchronized void reset() {
            mProfileData.reset();
        }

        public synchronized void hold(Thread t) {
            WatchEntry entry = findEntry(t);

            // This can happen if the profiling is disabled (probably from
            // another thread). Same check is applied in commit() and drop()
            // below.
            if (entry == null) return;

            entry.isHolding = true;
        }

        public synchronized void commit(Thread t) {
            WatchEntry entry = findEntry(t);
            if (entry == null) return;
            ArrayList<String[]> stacks = entry.holdingStacks;
            for (int i = 0; i < stacks.size(); i++) {
                mProfileData.addSample(stacks.get(i));
            }
            entry.isHolding = false;
            entry.holdingStacks.clear();
        }

        public synchronized void drop(Thread t) {
            WatchEntry entry = findEntry(t);
            if (entry == null) return;
            entry.isHolding = false;
            entry.holdingStacks.clear();
        }
    }

    // Enable profiling for the calling thread. Periodically (every cycleTimeInMs
    // milliseconds) sample the stack trace of the calling thread.
    public static void enable(int cycleTimeInMs) {
        Thread t = Thread.currentThread();
        sWatchdog.addWatchEntry(t, cycleTimeInMs);
    }

    // Disable profiling for the calling thread.
    public static void disable() {
        sWatchdog.removeWatchEntry(Thread.currentThread());
    }

    // Disable profiling for all threads.
    public static void disableAll() {
        sWatchdog.removeAllWatchEntries();
    }

    // Dump the profiling data to a file.
    public static void dumpToFile(String filename) {
        sWatchdog.dumpToFile(filename);
    }

    // Reset the collected profiling data.
    public static void reset() {
        sWatchdog.reset();
    }

    // Hold the future samples coming from current thread until commit() or
    // drop() is called, and those samples are recorded or ignored as a result.
    // This must called after enable() to be effective.
    public static void hold() {
        sWatchdog.hold(Thread.currentThread());
    }

    public static void commit() {
        sWatchdog.commit(Thread.currentThread());
    }

    public static void drop() {
        sWatchdog.drop(Thread.currentThread());
    }
}
