/*
 * Copyright (C) 2020 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.internal.os;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.SystemClock;
import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.HeavyHitterSketch;

import java.util.ArrayList;
import java.util.List;

/**
 * A watcher which makes stats on the incoming binder transaction, if the amount of some type of
 * transactions exceeds the threshold, the listener will be notified.
 */
public final class BinderCallHeavyHitterWatcher {
    private static final String TAG = "BinderCallHeavyHitterWatcher";

    /**
     * Whether or not this watcher is enabled.
     */
    @GuardedBy("mLock")
    private boolean mEnabled;

    /**
     * The listener to be notified in case the amount of some type of transactions exceeds the
     * threshold.
     */
    @GuardedBy("mLock")
    private BinderCallHeavyHitterListener mListener;

    /**
     * The heavy hitter stats.
     */
    @GuardedBy("mLock")
    private HeavyHitterSketch<Integer> mHeavyHitterSketch;

    /**
     * The candidates that could be the heavy hitters, so we track their hashcode and the actual
     * containers in this map.
     */
    @GuardedBy("mLock")
    private final SparseArray<HeavyHitterContainer> mHeavyHitterCandiates = new SparseArray<>();

    /**
     * The cache to receive the list of candidates (consists of the hashcode of heavy hitters).
     */
    @GuardedBy("mLock")
    private final ArrayList<Integer> mCachedCandidateList = new ArrayList<>();

    /**
     * The cache to receive the frequencies of each items in {@link #mCachedCandidateList}.
     */
    @GuardedBy("mLock")
    private final ArrayList<Float> mCachedCandidateFrequencies = new ArrayList<>();

    /**
     * The cache set to host the candidates.
     */
    @GuardedBy("mLock")
    private ArraySet<Integer> mCachedCandidateSet = new ArraySet<>();

    /**
     * The cache set to host the containers of candidates.
     */
    @GuardedBy("mLock")
    private HeavyHitterContainer[] mCachedCandidateContainers;

    /**
     * The index to the {@link #mCachedCandidateContainers}, denote the first available slot
     */
    @GuardedBy("mLock")
    private int mCachedCandidateContainersIndex;

    /**
     * The input size, should be {@link #mTotalInputSize} - validation size.
     */
    @GuardedBy("mLock")
    private int mInputSize;

    /**
     * The total input size.
     */
    @GuardedBy("mLock")
    private int mTotalInputSize;

    /**
     * The number of inputs so far
     */
    @GuardedBy("mLock")
    private int mCurrentInputSize;

    /**
     * The threshold to be considered as heavy hitters
     */
    @GuardedBy("mLock")
    private float mThreshold;

    /**
     * The timestamp of the start of current tracing.
     */
    @GuardedBy("mLock")
    private long mBatchStartTimeStamp;

    /**
     * The lock object
     */
    private final Object mLock = new Object();

    /**
     * The tolerance within which is approximately equal
     */
    private static final float EPSILON = 0.00001f;

    /**
     * Callback interface when the amount of some type of transactions exceeds the threshold.
     */
    public interface BinderCallHeavyHitterListener {
        /**
         * @param heavyHitters     The list of binder call heavy hitters
         * @param totalBinderCalls The total binder calls
         * @param threshold        The threshold to be considered as heavy hitters
         * @param timeSpan         The toal time span of all these binder calls
         */
        void onHeavyHit(List<HeavyHitterContainer> heavyHitters,
                int totalBinderCalls, float threshold, long timeSpan);
    }

    /**
     * Container to hold the potential heavy hitters
     */
    public static final class HeavyHitterContainer {
        /**
         * The caller UID
         */
        public int mUid;

        /**
         * The class of the Binder object which is being hit heavily
         */
        public Class mClass;

        /**
         * The transaction code within the Binder object which is being hit heavily
         */
        public int mCode;

        /**
         * The frequency of this being hit (a number between 0...1)
         */
        public float mFrequency;

        /**
         * Default constructor
         */
        public HeavyHitterContainer() {
        }

        /**
         * Copy constructor
         */
        public HeavyHitterContainer(@NonNull final HeavyHitterContainer other) {
            this.mUid = other.mUid;
            this.mClass = other.mClass;
            this.mCode = other.mCode;
            this.mFrequency = other.mFrequency;
        }

        @Override
        public boolean equals(final Object other) {
            if (other == null || !(other instanceof HeavyHitterContainer)) {
                return false;
            }
            HeavyHitterContainer o = (HeavyHitterContainer) other;
            return this.mUid == o.mUid && this.mClass == o.mClass && this.mCode == o.mCode
                    && Math.abs(this.mFrequency - o.mFrequency) < EPSILON;
        }

        @Override
        public int hashCode() {
            return hashCode(mUid, mClass, mCode);
        }

        /**
         * Compute the hashcode with given parameters.
         */
        static int hashCode(int uid, @NonNull Class clazz, int code) {
            int hash = uid;
            hash = 31 * hash + clazz.hashCode();
            hash = 31 * hash + code;
            return hash;
        }
    }

    /**
     * The static lock object
     */
    private static final Object sLock = new Object();

    /**
     * The default instance
     */
    @GuardedBy("sLock")
    private static BinderCallHeavyHitterWatcher sInstance = null;

    /**
     * Return the instance of the watcher
     */
    public static BinderCallHeavyHitterWatcher getInstance() {
        synchronized (sLock) {
            if (sInstance == null) {
                sInstance = new BinderCallHeavyHitterWatcher();
            }
            return sInstance;
        }
    }

    /**
     * Configure the parameters.
     *
     * @param enable    Whether or not to enable the watcher
     * @param batchSize The number of binder transactions it needs to receive before the conclusion
     * @param threshold The threshold to determine if some type of transactions are too many, it
     *                  should be a value between (0.0f, 1.0f]
     * @param listener  The callback interface
     */
    public void setConfig(final boolean enable, final int batchSize, final float threshold,
            @Nullable final BinderCallHeavyHitterListener listener) {
        synchronized (mLock) {
            if (!enable) {
                if (mEnabled) {
                    resetInternalLocked(null, null, 0, 0, 0.0f, 0);
                    mEnabled = false;
                }
                return;
            }
            mEnabled = true;
            // Validate the threshold, which is expected to be within (0.0f, 1.0f]
            if (threshold < EPSILON || threshold > 1.0f) {
                return;
            }

            if (batchSize == mTotalInputSize && Math.abs(threshold - mThreshold) < EPSILON) {
                // Shortcut: just update the listener, no need to reset the watcher itself.
                mListener = listener;
                return;
            }

            final int capacity = (int) (1.0f / threshold);
            final HeavyHitterSketch<Integer> sketch = HeavyHitterSketch.<Integer>newDefault();
            final float validationRatio = sketch.getRequiredValidationInputRatio();
            int inputSize = batchSize;
            if (!Float.isNaN(validationRatio)) {
                inputSize = (int) (batchSize * (1 - validationRatio));
            }
            try {
                sketch.setConfig(batchSize, capacity);
            } catch (IllegalArgumentException e) {
                // invalid parameter, ignore the config.
                Log.w(TAG, "Invalid parameter to heavy hitter watcher: "
                        + batchSize + ", " + capacity);
                return;
            }
            // Reset the watcher to start over with the new configuration.
            resetInternalLocked(listener, sketch, inputSize, batchSize, threshold, capacity);
        }
    }

    @GuardedBy("mLock")
    private void resetInternalLocked(@Nullable final BinderCallHeavyHitterListener listener,
            @Nullable final HeavyHitterSketch<Integer> sketch, final int inputSize,
            final int batchSize, final float threshold, final int capacity) {
        mListener = listener;
        mHeavyHitterSketch = sketch;
        mHeavyHitterCandiates.clear();
        mCachedCandidateList.clear();
        mCachedCandidateFrequencies.clear();
        mCachedCandidateSet.clear();
        mInputSize = inputSize;
        mTotalInputSize = batchSize;
        mCurrentInputSize = 0;
        mThreshold = threshold;
        mBatchStartTimeStamp = SystemClock.elapsedRealtime();
        initCachedCandidateContainersLocked(capacity);
    }

    @GuardedBy("mLock")
    private void initCachedCandidateContainersLocked(final int capacity) {
        if (capacity > 0) {
            mCachedCandidateContainers = new HeavyHitterContainer[capacity];
            for (int i = 0; i < mCachedCandidateContainers.length; i++) {
                mCachedCandidateContainers[i] = new HeavyHitterContainer();
            }
        } else {
            mCachedCandidateContainers = null;
        }
        mCachedCandidateContainersIndex = 0;
    }

    @GuardedBy("mLock")
    private @NonNull HeavyHitterContainer acquireHeavyHitterContainerLocked() {
        return mCachedCandidateContainers[mCachedCandidateContainersIndex++];
    }

    @GuardedBy("mLock")
    private void releaseHeavyHitterContainerLocked(@NonNull HeavyHitterContainer container) {
        mCachedCandidateContainers[--mCachedCandidateContainersIndex] = container;
    }

    /**
     * Called on incoming binder transaction
     *
     * @param callerUid The UID of the binder transaction's caller
     * @param clazz     The class of the Binder object serving the transaction
     * @param code      The binder transaction code
     */
    public void onTransaction(final int callerUid, @NonNull final Class clazz,
            final int code) {
        synchronized (mLock) {
            if (!mEnabled) {
                return;
            }

            final HeavyHitterSketch<Integer> sketch = mHeavyHitterSketch;
            if (sketch == null) {
                return;
            }

            // To reduce memory fragmentation, we only feed the hashcode to the sketch,
            // and keep the mapping from the hashcode to the sketch locally.
            // However, the mapping will not be built until the validation pass, by then
            // we will know the potential heavy hitters, so the mapping can focus on
            // those ones, which will significantly reduce the memory overhead.
            final int hashCode = HeavyHitterContainer.hashCode(callerUid, clazz, code);

            sketch.add(hashCode);
            mCurrentInputSize++;
            if (mCurrentInputSize == mInputSize) {
                // Retrieve the candidates
                sketch.getCandidates(mCachedCandidateList);
                mCachedCandidateSet.addAll(mCachedCandidateList);
                mCachedCandidateList.clear();
            } else if (mCurrentInputSize > mInputSize && mCurrentInputSize < mTotalInputSize) {
                // validation pass
                if (mCachedCandidateSet.contains(hashCode)) {
                    // It's one of the candidates
                    final int index = mHeavyHitterCandiates.indexOfKey(hashCode);
                    if (index < 0) {
                        // We got another hit, now write down its information
                        final HeavyHitterContainer container =
                                acquireHeavyHitterContainerLocked();
                        container.mUid = callerUid;
                        container.mClass = clazz;
                        container.mCode = code;
                        mHeavyHitterCandiates.put(hashCode, container);
                    }
                }
            } else if (mCurrentInputSize == mTotalInputSize) {
                // Reached the expected number of input, check top ones
                if (mListener != null) {
                    final List<Integer> result = sketch.getTopHeavyHitters(0,
                            mCachedCandidateList, mCachedCandidateFrequencies);
                    if (result != null) {
                        final int size = result.size();
                        if (size > 0) {
                            final ArrayList<HeavyHitterContainer> hitters = new ArrayList<>();
                            for (int i = 0; i < size; i++) {
                                final HeavyHitterContainer container = mHeavyHitterCandiates.get(
                                        result.get(i));
                                if (container != null) {
                                    final HeavyHitterContainer cont =
                                            new HeavyHitterContainer(container);
                                    cont.mFrequency = mCachedCandidateFrequencies.get(i);
                                    hitters.add(cont);
                                }
                            }
                            mListener.onHeavyHit(hitters, mTotalInputSize, mThreshold,
                                    SystemClock.elapsedRealtime() - mBatchStartTimeStamp);
                        }
                    }
                }
                // reset
                mHeavyHitterSketch.reset();
                mHeavyHitterCandiates.clear();
                mCachedCandidateList.clear();
                mCachedCandidateFrequencies.clear();
                mCachedCandidateSet.clear();
                mCachedCandidateContainersIndex = 0;
                mCurrentInputSize = 0;
                mBatchStartTimeStamp = SystemClock.elapsedRealtime();
            }
        }
    }
}
