/*
 * Copyright (C) 2017 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.Nullable;
import android.os.Binder;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.util.ArrayMap;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.BinderInternal.CallSession;
import com.android.internal.os.BinderLatencyProto.ApiStats;
import com.android.internal.os.BinderLatencyProto.Dims;
import com.android.internal.os.BinderLatencyProto.RepeatedApiStats;
import com.android.internal.util.FrameworkStatsLog;

import java.util.Random;

/** Collects statistics about Binder call latency per calling API and method. */
public class BinderLatencyObserver {
    private static final String TAG = "BinderLatencyObserver";
    private static final int MAX_ATOM_SIZE_BYTES = 4064;
    // Be conservative and leave 1K space for the last histogram so we don't go over the size limit.
    private static final int LAST_HISTOGRAM_BUFFER_SIZE_BYTES = 1000;

    // Latency observer parameters.
    public static final int PERIODIC_SAMPLING_INTERVAL_DEFAULT = 10;
    public static final int SHARDING_MODULO_DEFAULT = 1;
    public static final int STATSD_PUSH_INTERVAL_MINUTES_DEFAULT = 360;

    // Histogram buckets parameters.
    public static final int BUCKET_COUNT_DEFAULT = 100;
    public static final int FIRST_BUCKET_SIZE_DEFAULT = 5;
    public static final float BUCKET_SCALE_FACTOR_DEFAULT = 1.125f;

    @GuardedBy("mLock")
    private final ArrayMap<LatencyDims, int[]> mLatencyHistograms = new ArrayMap<>();
    private final Object mLock = new Object();

    // Sampling period to control how often to track CPU usage. 1 means all calls, 100 means ~1 out
    // of 100 requests.
    private int mPeriodicSamplingInterval = PERIODIC_SAMPLING_INTERVAL_DEFAULT;
    // Controls how many APIs will be collected per device. 1 means all APIs, 10 means every 10th
    // API will be collected.
    private int mShardingModulo = SHARDING_MODULO_DEFAULT;
    // Controls which shards will be collected on this device.
    private int mShardingOffset;

    private int mBucketCount = BUCKET_COUNT_DEFAULT;
    private int mFirstBucketSize = FIRST_BUCKET_SIZE_DEFAULT;
    private float mBucketScaleFactor = BUCKET_SCALE_FACTOR_DEFAULT;

    private int mStatsdPushIntervalMinutes = STATSD_PUSH_INTERVAL_MINUTES_DEFAULT;

    private final Random mRandom;
    private final Handler mLatencyObserverHandler;
    private final int mProcessSource;

    private BinderLatencyBuckets mLatencyBuckets;

    private Runnable mLatencyObserverRunnable = new Runnable() {
        @Override
        public void run() {
            // Schedule the next push.
            noteLatencyDelayed();

            ArrayMap<LatencyDims, int[]> histogramMap;
            synchronized (mLock) {
                // Copy the histograms map so we don't use the lock for longer than needed.
                histogramMap = new ArrayMap<>(mLatencyHistograms);
                mLatencyHistograms.clear();
            }

            BinderTransactionNameResolver resolver = new BinderTransactionNameResolver();
            ProtoOutputStream proto = new ProtoOutputStream();
            int histogramsWritten = 0;

            for (LatencyDims dims : histogramMap.keySet()) {
                // Start a new atom if the next histogram risks going over the atom size limit.
                if (proto.getRawSize() + LAST_HISTOGRAM_BUFFER_SIZE_BYTES > getMaxAtomSizeBytes()) {
                    if (histogramsWritten > 0) {
                        writeAtomToStatsd(proto);
                    }
                    proto = new ProtoOutputStream();
                    histogramsWritten = 0;
                }

                String transactionName = resolver.getMethodName(
                        dims.getBinderClass(), dims.getTransactionCode());
                fillApiStatsProto(proto, dims, transactionName, histogramMap.get(dims));
                histogramsWritten++;
            }
            // Push the final atom.
            if (histogramsWritten > 0) {
                writeAtomToStatsd(proto);
            }
        }
    };

    private void fillApiStatsProto(
            ProtoOutputStream proto, LatencyDims dims, String transactionName, int[] histogram) {
        // Find the part of the histogram to write.
        int firstNonEmptyBucket = 0;
        for (int i = 0; i < mBucketCount; i++) {
            if (histogram[i] != 0) {
                firstNonEmptyBucket = i;
                break;
            }
        }
        int lastNonEmptyBucket = mBucketCount - 1;
        for (int i = mBucketCount - 1; i >= 0; i--) {
            if (histogram[i] != 0) {
                lastNonEmptyBucket = i;
                break;
            }
        }

        // Start a new ApiStats proto.
        long apiStatsToken = proto.start(RepeatedApiStats.API_STATS);

        // Write the dims.
        long dimsToken = proto.start(ApiStats.DIMS);
        proto.write(Dims.PROCESS_SOURCE, mProcessSource);
        proto.write(Dims.SERVICE_CLASS_NAME, dims.getBinderClass().getName());
        proto.write(Dims.SERVICE_METHOD_NAME, transactionName);
        proto.end(dimsToken);

        // Write the histogram.
        proto.write(ApiStats.FIRST_BUCKET_INDEX, firstNonEmptyBucket);
        for (int i = firstNonEmptyBucket; i <= lastNonEmptyBucket; i++) {
            proto.write(ApiStats.BUCKETS, histogram[i]);
        }

        proto.end(apiStatsToken);
    }

    protected int getMaxAtomSizeBytes() {
        return MAX_ATOM_SIZE_BYTES;
    }

    protected void writeAtomToStatsd(ProtoOutputStream atom) {
        FrameworkStatsLog.write(
                FrameworkStatsLog.BINDER_LATENCY_REPORTED,
                atom.getBytes(),
                mPeriodicSamplingInterval,
                mShardingModulo,
                mBucketCount,
                mFirstBucketSize,
                mBucketScaleFactor);
    }

    private void noteLatencyDelayed() {
        mLatencyObserverHandler.removeCallbacks(mLatencyObserverRunnable);
        mLatencyObserverHandler.postDelayed(mLatencyObserverRunnable,
                mStatsdPushIntervalMinutes * 60 * 1000);
    }

    /** Injector for {@link BinderLatencyObserver}. */
    public static class Injector {
        public Random getRandomGenerator() {
            return new Random();
        }

        public Handler getHandler() {
            return new Handler(Looper.getMainLooper());
        }
    }

    public BinderLatencyObserver(Injector injector, int processSource) {
        mRandom = injector.getRandomGenerator();
        mLatencyObserverHandler = injector.getHandler();
        mLatencyBuckets = new BinderLatencyBuckets(
            mBucketCount, mFirstBucketSize, mBucketScaleFactor);
        mProcessSource = processSource;
        mShardingOffset = mRandom.nextInt(mShardingModulo);
        noteLatencyDelayed();
    }

    /** Should be called when a Binder call completes, will store latency data. */
    public void callEnded(@Nullable CallSession s) {
        if (s == null || s.exceptionThrown || !shouldKeepSample()) {
            return;
        }

        LatencyDims dims = LatencyDims.create(s.binderClass, s.transactionCode);

        if (!shouldCollect(dims)) {
            return;
        }

        long elapsedTimeMicro = getElapsedRealtimeMicro();
        long callDuration = elapsedTimeMicro - s.timeStarted;

        // Find the bucket this sample should go to.
        int bucketIdx = mLatencyBuckets.sampleToBucket(
                callDuration > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) callDuration);

        synchronized (mLock) {
            int[] buckets = mLatencyHistograms.get(dims);
            if (buckets == null) {
                buckets = new int[mBucketCount];
                mLatencyHistograms.put(dims, buckets);
            }

            // Increment the correct bucket.
            if (buckets[bucketIdx] < Integer.MAX_VALUE) {
                buckets[bucketIdx] += 1;
            }
        }
    }

    protected long getElapsedRealtimeMicro() {
        return SystemClock.elapsedRealtimeNanos() / 1000;
    }

    protected boolean shouldCollect(LatencyDims dims) {
        return (dims.hashCode() + mShardingOffset) % mShardingModulo == 0;
    }

    protected boolean shouldKeepSample() {
        return mRandom.nextInt() % mPeriodicSamplingInterval == 0;
    }

    /** Updates the sampling interval. */
    public void setSamplingInterval(int samplingInterval) {
        if (samplingInterval <= 0) {
            Slog.w(TAG, "Ignored invalid sampling interval (value must be positive): "
                    + samplingInterval);
            return;
        }

        synchronized (mLock) {
            if (samplingInterval != mPeriodicSamplingInterval) {
                mPeriodicSamplingInterval = samplingInterval;
                reset();
            }
        }
    }

    /** Updates the sharding modulo. */
    public void setShardingModulo(int shardingModulo) {
        if (shardingModulo <= 0) {
            Slog.w(TAG, "Ignored invalid sharding modulo (value must be positive): "
                    + shardingModulo);
            return;
        }

        synchronized (mLock) {
            if (shardingModulo != mShardingModulo) {
                mShardingModulo = shardingModulo;
                mShardingOffset = mRandom.nextInt(shardingModulo);
                reset();
            }
        }
    }

    /** Updates the statsd push interval. */
    public void setPushInterval(int pushIntervalMinutes) {
        if (pushIntervalMinutes <= 0) {
            Slog.w(TAG, "Ignored invalid push interval (value must be positive): "
                    + pushIntervalMinutes);
            return;
        }

        synchronized (mLock) {
            if (pushIntervalMinutes != mStatsdPushIntervalMinutes) {
                mStatsdPushIntervalMinutes = pushIntervalMinutes;
                reset();
            }
        }
    }

    /** Updates the histogram buckets parameters. */
    public void setHistogramBucketsParams(
            int bucketCount, int firstBucketSize, float bucketScaleFactor) {
        synchronized (mLock) {
            if (bucketCount != mBucketCount || firstBucketSize != mFirstBucketSize
                    || bucketScaleFactor != mBucketScaleFactor) {
                mBucketCount = bucketCount;
                mFirstBucketSize = firstBucketSize;
                mBucketScaleFactor = bucketScaleFactor;
                mLatencyBuckets = new BinderLatencyBuckets(
                    mBucketCount, mFirstBucketSize, mBucketScaleFactor);
                reset();
            }
        }
    }

    /** Resets the sample collection. */
    public void reset() {
        synchronized (mLock) {
            mLatencyHistograms.clear();
        }
        noteLatencyDelayed();
    }

    /** Container for binder latency information. */
    public static class LatencyDims {
        // Binder interface descriptor.
        private Class<? extends Binder> mBinderClass;
        // Binder transaction code.
        private int mTransactionCode;
        // Cached hash code, 0 if not set yet.
        private int mHashCode = 0;

        /** Creates a new instance of LatencyDims. */
        public static LatencyDims create(Class<? extends Binder> binderClass, int transactionCode) {
            return new LatencyDims(binderClass, transactionCode);
        }

        private LatencyDims(Class<? extends Binder> binderClass, int transactionCode) {
            this.mBinderClass = binderClass;
            this.mTransactionCode = transactionCode;
        }

        public Class<? extends Binder> getBinderClass() {
            return mBinderClass;
        }

        public int getTransactionCode() {
            return mTransactionCode;
        }

        @Override
        public boolean equals(final Object other) {
            if (other == null || !(other instanceof LatencyDims)) {
                return false;
            }
            LatencyDims o = (LatencyDims) other;
            return mTransactionCode == o.getTransactionCode() && mBinderClass == o.getBinderClass();
        }

        @Override
        public int hashCode() {
            if (mHashCode != 0) {
                return mHashCode;
            }
            int hash = mTransactionCode;
            hash = 31 * hash + mBinderClass.getName().hashCode();
            mHashCode = hash;
            return hash;
        }
    }

    @VisibleForTesting
    public ArrayMap<LatencyDims, int[]> getLatencyHistograms() {
        return mLatencyHistograms;
    }

    @VisibleForTesting
    public Runnable getStatsdPushRunnable() {
        return mLatencyObserverRunnable;
    }

    @VisibleForTesting
    public int getProcessSource() {
        return mProcessSource;
    }
}
