content: /*
   * 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.statsd.loadtest;
  
  import android.annotation.Nullable;
  import android.app.Activity;
  import android.app.AlarmManager;
  import android.app.PendingIntent;
  import android.app.StatsManager;
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.graphics.Color;
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.IStatsManager;
  import android.os.PowerManager;
  import android.os.PowerManager.WakeLock;
  import android.os.ServiceManager;
  import android.os.SystemClock;
  import android.text.Editable;
  import android.text.TextWatcher;
  import android.util.Log;
  import android.util.StatsLog;
  import android.view.View;
  import android.view.inputmethod.InputMethodManager;
  import android.view.MotionEvent;
  import android.view.View.OnFocusChangeListener;
  import android.widget.AdapterView;
  import android.widget.ArrayAdapter;
  import android.widget.Button;
  import android.widget.CheckBox;
  import android.widget.EditText;
  import android.widget.Spinner;
  import android.widget.TextView;
  import android.widget.Toast;
  
  import com.android.os.StatsLog.ConfigMetricsReport;
  import com.android.os.StatsLog.ConfigMetricsReportList;
  import com.android.os.StatsLog.StatsdStatsReport;
  import com.android.internal.os.StatsdConfigProto.TimeUnit;
  
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
  /**
   * Runs a load test for statsd.
   * How it works:
   * <ul>
   * <li> Sets up and pushes a custom config with metrics that exercise a large swath of code paths.
   * <li> Periodically logs certain atoms into logd.
   * <li> Impact on battery can be printed to logcat, or a bug report can be filed and analyzed
   * in battery Historian.
   * </ul>
   * The load depends on how demanding the config is, as well as how frequently atoms are pushsed
   * to logd. Those are all controlled by 4 adjustable parameters:
   * <ul>
   * <li> The 'replication' parameter artificially multiplies the number of metrics in the config.
   * <li> The bucket size controls the time-bucketing the aggregate metrics.
   * <li> The period parameter controls how frequently atoms are pushed to logd.
   * <li> The 'burst' parameter controls how many atoms are pushed at the same time (per period).
   * </ul>
   */
  public class LoadtestActivity extends Activity implements AdapterView.OnItemSelectedListener {
  
      private static final String TAG = "loadtest.LoadtestActivity";
      public static final String TYPE = "type";
      private static final String PUSH_ALARM = "push_alarm";
      public static final String PERF_ALARM = "perf_alarm";
      private static final String SET_REPLICATION = "set_replication";
      private static final String REPLICATION = "replication";
      private static final String START = "start";
      private static final String STOP = "stop";
      private static final Map<String, TimeUnit> TIME_UNIT_MAP = initializeTimeUnitMap();
      private static final List<String> TIME_UNIT_LABELS = initializeTimeUnitLabels();
  
      public final static class PusherAlarmReceiver extends BroadcastReceiver {
          @Override
          public void onReceive(Context context, Intent intent) {
              Intent activityIntent = new Intent(context, LoadtestActivity.class);
              activityIntent.putExtra(TYPE, PUSH_ALARM);
              context.startActivity(activityIntent);
          }
      }
  
      public final static class StopperAlarmReceiver extends BroadcastReceiver {
          @Override
          public void onReceive(Context context, Intent intent) {
              Intent activityIntent = new Intent(context, LoadtestActivity.class);
              activityIntent.putExtra(TYPE, STOP);
              context.startActivity(activityIntent);
          }
      }
  
      private static Map<String, TimeUnit> initializeTimeUnitMap() {
          Map<String, TimeUnit> labels = new HashMap();
          labels.put("1m", TimeUnit.ONE_MINUTE);
          labels.put("5m", TimeUnit.FIVE_MINUTES);
          labels.put("10m", TimeUnit.TEN_MINUTES);
          labels.put("30m", TimeUnit.THIRTY_MINUTES);
          labels.put("1h", TimeUnit.ONE_HOUR);
          labels.put("3h", TimeUnit.THREE_HOURS);
          labels.put("6h", TimeUnit.SIX_HOURS);
          labels.put("12h", TimeUnit.TWELVE_HOURS);
          labels.put("1d", TimeUnit.ONE_DAY);
          labels.put("1s", TimeUnit.CTS);
          return labels;
      }
  
      private static List<String> initializeTimeUnitLabels() {
          List<String> labels = new ArrayList();
          labels.add("1s");
          labels.add("1m");
          labels.add("5m");
          labels.add("10m");
          labels.add("30m");
          labels.add("1h");
          labels.add("3h");
          labels.add("6h");
          labels.add("12h");
          labels.add("1d");
          return labels;
      }
  
      private AlarmManager mAlarmMgr;
  
      /**
       * Used to periodically log atoms to logd.
       */
      private PendingIntent mPushPendingIntent;
  
      /**
       * Used to end the loadtest.
       */
      private PendingIntent mStopPendingIntent;
  
      private Button mStartStop;
      private EditText mReplicationText;
      private Spinner mBucketSpinner;
      private EditText mPeriodText;
      private EditText mBurstText;
      private EditText mDurationText;
      private TextView mReportText;
      private CheckBox mPlaceboCheckBox;
      private CheckBox mCountMetricCheckBox;
      private CheckBox mDurationMetricCheckBox;
      private CheckBox mEventMetricCheckBox;
      private CheckBox mValueMetricCheckBox;
      private CheckBox mGaugeMetricCheckBox;
  
      /**
       * When the load test started.
       */
      private long mStartedTimeMillis;
  
      /**
       * For measuring perf data.
       */
      private PerfData mPerfData;
  
      /**
       * For communicating with statsd.
       */
      private StatsManager mStatsManager;
  
      private PowerManager mPowerManager;
      private WakeLock mWakeLock;
  
      /**
       * If true, we only measure the effect of the loadtest infrastructure. No atom are pushed and
       * the configuration is empty.
       */
      private boolean mPlacebo;
  
      /**
       * Whether to include CountMetric in the config.
       */
      private boolean mIncludeCountMetric;
  
      /**
       * Whether to include DurationMetric in the config.
       */
      private boolean mIncludeDurationMetric;
  
      /**
       * Whether to include EventMetric in the config.
       */
      private boolean mIncludeEventMetric;
  
      /**
       * Whether to include ValueMetric in the config.
       */
      private boolean mIncludeValueMetric;
  
      /**
       * Whether to include GaugeMetric in the config.
       */
      private boolean mIncludeGaugeMetric;
  
      /**
       * The burst size.
       */
      private int mBurst;
  
      /**
       * The metrics replication.
       */
      private int mReplication;
  
      /**
       * The period, in seconds, at which batches of atoms are pushed.
       */
      private long mPeriodSecs;
  
      /**
       * The bucket size, in minutes, for aggregate metrics.
       */
      private TimeUnit mBucket;
  
      /**
       * The duration, in minutes, of the loadtest.
       */
      private long mDurationMins;
  
      /**
       * Whether the loadtest has started.
       */
      private boolean mStarted = false;
  
      /**
       * Orchestrates the logging of pushed events into logd.
       */
      private SequencePusher mPusher;
  
      /**
       * Generates statsd configs.
       */
      private ConfigFactory mFactory;
  
      /**
       * For intra-minute periods.
       */
      private final Handler mHandler = new Handler();
  
      /**
       * Number of metrics in the current config.
       */
      private int mNumMetrics;
  
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
  
          Log.d(TAG, "Starting loadtest Activity");
  
          setContentView(R.layout.activity_loadtest);
          mReportText = (TextView) findViewById(R.id.report_text);
          initBurst();
          initReplication();
          initBucket();
          initPeriod();
          initDuration();
          initPlacebo();
          initMetricWhitelist();
  
          // Hide the keyboard outside edit texts.
          findViewById(R.id.outside).setOnTouchListener(new View.OnTouchListener() {
              @Override
              public boolean onTouch(View v, MotionEvent event) {
                  InputMethodManager imm =
                          (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
                  if (getCurrentFocus() != null) {
                      imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
                  }
                  return true;
              }
          });
  
          mStartStop = findViewById(R.id.start_stop);
          mStartStop.setOnClickListener(new View.OnClickListener() {
              @Override
              public void onClick(View view) {
                  if (mStarted) {
                      stopLoadtest();
                  } else {
                      startLoadtest();
                  }
              }
          });
  
          mAlarmMgr = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
          mStatsManager = (StatsManager) getSystemService("stats");
          mPowerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
          mFactory = new ConfigFactory(this);
          stopLoadtest();
          mReportText.setText("");
      }
  
      @Override
      public void onNewIntent(Intent intent) {
          String type = intent.getStringExtra(TYPE);
          if (type == null) {
              return;
          }
          switch (type) {
              case PERF_ALARM:
                  onPerfAlarm();
                  break;
              case PUSH_ALARM:
                  onAlarm();
                  break;
              case SET_REPLICATION:
                  if (intent.hasExtra(REPLICATION)) {
                      setReplication(intent.getIntExtra(REPLICATION, 0));
                  }
                  break;
              case START:
                  startLoadtest();
                  break;
              case STOP:
                  stopLoadtest();
                  break;
              default:
                  throw new IllegalArgumentException("Unknown type: " + type);
          }
      }
  
      @Override
      public void onDestroy() {
          Log.d(TAG, "Destroying");
          mPerfData.onDestroy();
          stopLoadtest();
          clearConfigs();
          super.onDestroy();
      }
  
      @Nullable
      public StatsdStatsReport getMetadata() {
          if (!statsdRunning()) {
              return null;
          }
          if (mStatsManager != null) {
              byte[] data;
              try {
                  data = mStatsManager.getStatsMetadata();
              } catch (StatsManager.StatsUnavailableException e) {
                  Log.e(TAG, "Failed to get data from statsd", e);
                  return null;
              }
              if (data != null) {
                  StatsdStatsReport report = null;
                  boolean good = false;
                  try {
                      return StatsdStatsReport.parseFrom(data);
                  } catch (com.google.protobuf.InvalidProtocolBufferException e) {
                      Log.d(TAG, "Bad StatsdStatsReport");
                  }
              }
          }
          return null;
      }
  
      @Nullable
      public List<ConfigMetricsReport> getData() {
          if (!statsdRunning()) {
              return null;
          }
          if (mStatsManager != null) {
              byte[] data;
              try {
                  data = mStatsManager.getReports(ConfigFactory.CONFIG_ID);
              } catch (StatsManager.StatsUnavailableException e) {
                  Log.e(TAG, "Failed to get data from statsd", e);
                  return null;
              }
              if (data != null) {
                  ConfigMetricsReportList reports = null;
                  try {
                      reports = ConfigMetricsReportList.parseFrom(data);
                      Log.d(TAG, "Num reports: " + reports.getReportsCount());
                      StringBuilder sb = new StringBuilder();
                      DisplayProtoUtils.displayLogReport(sb, reports);
                      Log.d(TAG, sb.toString());
                  } catch (com.google.protobuf.InvalidProtocolBufferException e) {
                      Log.d(TAG, "Invalid data");
                  }
                  if (reports != null) {
                      return reports.getReportsList();
                  }
              }
          }
          return null;
      }
  
      @Override
      public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
          String item = parent.getItemAtPosition(position).toString();
  
          mBucket = TIME_UNIT_MAP.get(item);
      }
  
      @Override
      public void onNothingSelected(AdapterView<?> parent) {
          // Another interface callback
      }
  
      private void onPerfAlarm() {
          if (mPerfData != null) {
              mPerfData.onAlarm(this);
          }
          // Piggy-back on that alarm to show the elapsed time.
          long elapsedTimeMins = (long) Math.floor(
                  (SystemClock.elapsedRealtime() - mStartedTimeMillis) / 60 / 1000);
          mReportText.setText("Loadtest in progress.\n"
                  + "num metrics =" + mNumMetrics
                  + "\nElapsed time = " + elapsedTimeMins + " min(s)");
      }
  
      private void onAlarm() {
          Log.d(TAG, "ON ALARM");
  
          // Set the next task.
          scheduleNext();
  
          // Do the work.
          mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "StatsdLoadTest");
          mWakeLock.acquire();
          if (mPusher != null) {
              mPusher.next();
          }
          mWakeLock.release();
          mWakeLock = null;
      }
  
      /**
       * Schedules the next cycle of pushing atoms into logd.
       */
      private void scheduleNext() {
          Intent intent = new Intent(this, PusherAlarmReceiver.class);
          intent.putExtra(TYPE, PUSH_ALARM);
          mPushPendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
          long nextTime = SystemClock.elapsedRealtime() + mPeriodSecs * 1000;
          mAlarmMgr.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTime, mPushPendingIntent);
      }
  
      private synchronized void startLoadtest() {
          if (mStarted) {
              return;
          }
  
          // Clean up the state.
          stopLoadtest();
  
          // Prepare to push a sequence of atoms to logd.
          mPusher = new SequencePusher(mBurst, mPlacebo);
  
          // Create a config and push it to statsd.
          if (!setConfig(mFactory.getConfig(mReplication, mBucket, mPlacebo,
                  mIncludeCountMetric, mIncludeDurationMetric, mIncludeEventMetric,
                  mIncludeValueMetric, mIncludeGaugeMetric))) {
              return;
          }
  
          // Remember to stop in the future.
          Intent intent = new Intent(this, StopperAlarmReceiver.class);
          intent.putExtra(TYPE, STOP);
          mStopPendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
          long nextTime = SystemClock.elapsedRealtime() + mDurationMins * 60 * 1000;
          mAlarmMgr.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTime, mStopPendingIntent);
  
          // Log atoms.
          scheduleNext();
  
          // Start tracking performance.
          mPerfData = new PerfData(this, mPlacebo, mReplication, mBucket, mPeriodSecs, mBurst,
                  mIncludeCountMetric, mIncludeDurationMetric, mIncludeEventMetric, mIncludeValueMetric,
                  mIncludeGaugeMetric);
          mPerfData.startRecording(this);
  
          mReportText.setText("Loadtest in progress.\nnum metrics =" + mNumMetrics);
          mStartedTimeMillis = SystemClock.elapsedRealtime();
  
          updateStarted(true);
      }
  
      private synchronized void stopLoadtest() {
          if (mPushPendingIntent != null) {
              Log.d(TAG, "Canceling pre-existing push alarm");
              mAlarmMgr.cancel(mPushPendingIntent);
              mPushPendingIntent = null;
          }
          if (mStopPendingIntent != null) {
              Log.d(TAG, "Canceling pre-existing stop alarm");
              mAlarmMgr.cancel(mStopPendingIntent);
              mStopPendingIntent = null;
          }
          if (mWakeLock != null) {
              mWakeLock.release();
              mWakeLock = null;
          }
          if (mPerfData != null) {
              mPerfData.stopRecording(this);
              mPerfData.onDestroy();
              mPerfData = null;
          }
  
          // Obtain the latest data and display it.
          getData();
  
          long elapsedTimeMins = (long) Math.floor(
                  (SystemClock.elapsedRealtime() - mStartedTimeMillis) / 60 / 1000);
          mReportText.setText("Loadtest ended. Elapsed time = " + elapsedTimeMins + " min(s)");
          clearConfigs();
          updateStarted(false);
      }
  
      private synchronized void updateStarted(boolean started) {
          mStarted = started;
          mStartStop.setBackgroundColor(started ?
                  Color.parseColor("#FFFF0000") : Color.parseColor("#FF00FF00"));
          mStartStop.setText(started ? getString(R.string.stop) : getString(R.string.start));
          updateControlsEnabled();
      }
  
      private void updateControlsEnabled() {
          mBurstText.setEnabled(!mPlacebo && !mStarted);
          mReplicationText.setEnabled(!mPlacebo && !mStarted);
          mPeriodText.setEnabled(!mStarted);
          mBucketSpinner.setEnabled(!mPlacebo && !mStarted);
          mDurationText.setEnabled(!mStarted);
          mPlaceboCheckBox.setEnabled(!mStarted);
  
          boolean enabled = !mStarted && !mPlaceboCheckBox.isChecked();
          mCountMetricCheckBox.setEnabled(enabled);
          mDurationMetricCheckBox.setEnabled(enabled);
          mEventMetricCheckBox.setEnabled(enabled);
          mValueMetricCheckBox.setEnabled(enabled);
          mGaugeMetricCheckBox.setEnabled(enabled);
      }
  
      private boolean statsdRunning() {
          if (IStatsManager.Stub.asInterface(ServiceManager.getService("stats")) == null) {
              Log.d(TAG, "Statsd not running");
              Toast.makeText(LoadtestActivity.this, "Statsd NOT running!", Toast.LENGTH_LONG).show();
              return false;
          }
          return true;
      }
  
      private int sanitizeInt(int val, int min, int max) {
          if (val > max) {
              val = max;
          } else if (val < min) {
              val = min;
          }
          return val;
      }
  
      private void clearConfigs() {
          // TODO: Clear all configs instead of specific ones.
          if (mStatsManager != null) {
              if (mStarted) {
                  try {
                      mStatsManager.removeConfig(ConfigFactory.CONFIG_ID);
                      Log.d(TAG, "Removed loadtest statsd configs.");
                  } catch (StatsManager.StatsUnavailableException e) {
                      Log.e(TAG, "Failed to remove loadtest configs.", e);
                  }
              }
          }
      }
  
      private boolean setConfig(ConfigFactory.ConfigMetadata configData) {
          if (mStatsManager != null) {
              try {
                  mStatsManager.addConfig(ConfigFactory.CONFIG_ID, configData.bytes);
                  mNumMetrics = configData.numMetrics;
                  Log.d(TAG, "Config pushed to statsd");
                  return true;
              } catch (StatsManager.StatsUnavailableException | IllegalArgumentException e) {
                  Log.e(TAG, "Failed to push config to statsd", e);
              }
          }
          return false;
      }
  
      private synchronized void setReplication(int replication) {
          if (mStarted) {
              return;
          }
          mReplicationText.setText("" + replication);
      }
  
      private synchronized void setPeriodSecs(long periodSecs) {
          mPeriodSecs = periodSecs;
      }
  
      private synchronized void setBurst(int burst) {
          mBurst = burst;
      }
  
      private synchronized void setDurationMins(long durationMins) {
          mDurationMins = durationMins;
      }
  
  
      private void handleFocus(EditText editText) {
        /*
          editText.setOnFocusChangeListener(new OnFocusChangeListener() {
              @Override
              public void onFocusChange(View v, boolean hasFocus) {
                  if (!hasFocus && editText.getText().toString().isEmpty()) {
                      editText.setText("-1", TextView.BufferType.EDITABLE);
                  }
              }
          });
        */
      }
  
      private void initBurst() {
          mBurst = getResources().getInteger(R.integer.burst_default);
          mBurstText = (EditText) findViewById(R.id.burst);
          mBurstText.addTextChangedListener(new NumericalWatcher(mBurstText, 0, 1000) {
              @Override
              public void onNewValue(int newValue) {
                  setBurst(newValue);
              }
          });
          handleFocus(mBurstText);
      }
  
      private void initReplication() {
          mReplication = getResources().getInteger(R.integer.replication_default);
          mReplicationText = (EditText) findViewById(R.id.replication);
          mReplicationText.addTextChangedListener(new NumericalWatcher(mReplicationText, 1, 4096) {
              @Override
              public void onNewValue(int newValue) {
                  mReplication = newValue;
              }
          });
          handleFocus(mReplicationText);
      }
  
      private void initBucket() {
          String defaultValue = getResources().getString(R.string.bucket_default);
          mBucket = TimeUnit.valueOf(defaultValue);
          mBucketSpinner = (Spinner) findViewById(R.id.bucket_spinner);
  
          ArrayAdapter<String> dataAdapter = new ArrayAdapter<String>(
                  this, R.layout.spinner_item, TIME_UNIT_LABELS);
  
          mBucketSpinner.setAdapter(dataAdapter);
          mBucketSpinner.setOnItemSelectedListener(this);
  
          for (String label : TIME_UNIT_MAP.keySet()) {
              if (defaultValue.equals(TIME_UNIT_MAP.get(label).toString())) {
                  mBucketSpinner.setSelection(dataAdapter.getPosition(label));
              }
          }
      }
  
      private void initPeriod() {
          mPeriodSecs = getResources().getInteger(R.integer.period_default);
          mPeriodText = (EditText) findViewById(R.id.period);
          mPeriodText.addTextChangedListener(new NumericalWatcher(mPeriodText, 1, 60) {
              @Override
              public void onNewValue(int newValue) {
                  setPeriodSecs(newValue);
              }
          });
          handleFocus(mPeriodText);
      }
  
      private void initDuration() {
          mDurationMins = getResources().getInteger(R.integer.duration_default);
          mDurationText = (EditText) findViewById(R.id.duration);
          mDurationText.addTextChangedListener(new NumericalWatcher(mDurationText, 1, 24 * 60) {
              @Override
              public void onNewValue(int newValue) {
                  setDurationMins(newValue);
              }
          });
          handleFocus(mDurationText);
      }
  
      private void initPlacebo() {
          mPlaceboCheckBox = findViewById(R.id.placebo);
          mPlacebo = false;
          mPlaceboCheckBox.setOnClickListener(new View.OnClickListener() {
              @Override
              public void onClick(View view) {
                  mPlacebo = mPlaceboCheckBox.isChecked();
                  updateControlsEnabled();
              }
          });
      }
  
      private void initMetricWhitelist() {
          mCountMetricCheckBox = findViewById(R.id.include_count);
          mCountMetricCheckBox.setOnClickListener(new View.OnClickListener() {
              @Override
              public void onClick(View view) {
                  mIncludeCountMetric = mCountMetricCheckBox.isChecked();
              }
          });
          mDurationMetricCheckBox = findViewById(R.id.include_duration);
          mDurationMetricCheckBox.setOnClickListener(new View.OnClickListener() {
              @Override
              public void onClick(View view) {
                  mIncludeDurationMetric = mDurationMetricCheckBox.isChecked();
              }
          });
          mEventMetricCheckBox = findViewById(R.id.include_event);
          mEventMetricCheckBox.setOnClickListener(new View.OnClickListener() {
              @Override
              public void onClick(View view) {
                  mIncludeEventMetric = mEventMetricCheckBox.isChecked();
              }
          });
          mValueMetricCheckBox = findViewById(R.id.include_value);
          mValueMetricCheckBox.setOnClickListener(new View.OnClickListener() {
              @Override
              public void onClick(View view) {
                  mIncludeValueMetric = mValueMetricCheckBox.isChecked();
              }
          });
          mGaugeMetricCheckBox = findViewById(R.id.include_gauge);
          mGaugeMetricCheckBox.setOnClickListener(new View.OnClickListener() {
              @Override
              public void onClick(View view) {
                  mIncludeGaugeMetric = mGaugeMetricCheckBox.isChecked();
              }
          });
  
          mIncludeCountMetric = mCountMetricCheckBox.isChecked();
          mIncludeDurationMetric = mDurationMetricCheckBox.isChecked();
          mIncludeEventMetric = mEventMetricCheckBox.isChecked();
          mIncludeValueMetric = mValueMetricCheckBox.isChecked();
          mIncludeGaugeMetric = mGaugeMetricCheckBox.isChecked();
      }
  }
  