content: /*
   * Copyright (C) 2018 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.Process;
  
  import com.android.internal.util.ArrayUtils;
  
  import java.io.IOException;
  import java.nio.file.Files;
  import java.nio.file.Path;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.List;
  
  /**
   * Reads and parses {@code time_in_state} files in the {@code proc} filesystem.
   *
   * Every line in a {@code time_in_state} file contains two numbers, separated by a single space
   * character. The first number is the frequency of the CPU used in kilohertz. The second number is
   * the time spent in this frequency. In the {@code time_in_state} file, this is given in 10s of
   * milliseconds, but this class returns in milliseconds. This can be per user, process, or thread
   * depending on which {@code time_in_state} file is used.
   *
   * For example, a {@code time_in_state} file would look like this:
   * <pre>
   *   300000 3
   *   364800 0
   *   ...
   *   1824000 0
   *   1900800 1
   * </pre>
   *
   * This file would indicate that the CPU has spent 30 milliseconds at frequency 300,000KHz (300Mhz)
   * and 10 milliseconds at frequency 1,900,800KHz (1.9GHz).
   *
   * <p>This class will also read {@code time_in_state} files with headers, such as:
   * <pre>
   *   cpu0
   *   300000 3
   *   364800 0
   *   ...
   *   cpu4
   *   300000 1
   *   364800 4
   * </pre>
   */
  public class ProcTimeInStateReader {
      private static final String TAG = "ProcTimeInStateReader";
  
      /**
       * The format of a single line of the {@code time_in_state} file that exports the frequency
       * values
       */
      private static final List<Integer> TIME_IN_STATE_LINE_FREQUENCY_FORMAT = Arrays.asList(
              Process.PROC_OUT_LONG | Process.PROC_SPACE_TERM,
              Process.PROC_NEWLINE_TERM
      );
  
      /**
       * The format of a single line of the {@code time_in_state} file that exports the time values
       */
      private static final List<Integer> TIME_IN_STATE_LINE_TIME_FORMAT = Arrays.asList(
              Process.PROC_SPACE_TERM,
              Process.PROC_OUT_LONG | Process.PROC_NEWLINE_TERM
      );
  
      /**
       * The format of a header line of the {@code time_in_state} file
       */
      private static final List<Integer> TIME_IN_STATE_HEADER_LINE_FORMAT =
              Collections.singletonList(Process.PROC_NEWLINE_TERM);
  
      /**
       * The format of the {@code time_in_state} file to extract times, defined using {@link
       * Process}'s {@code PROC_OUT_LONG} and related variables
       */
      private int[] mTimeInStateTimeFormat;
  
      /**
       * The frequencies reported in each {@code time_in_state} file
       *
       * Defined on first successful read of {@code time_in_state} file.
       */
      private long[] mFrequenciesKhz;
  
      /**
       * @param initialTimeInStateFile the file to base the format of the frequency files on, and to
       * read frequencies from. Expected to be in the same format as all other {@code time_in_state}
       * files, and contain the same frequencies.
       * @throws IOException if reading the initial {@code time_in_state} file failed
       */
      public ProcTimeInStateReader(Path initialTimeInStateFile) throws IOException {
          initializeTimeInStateFormat(initialTimeInStateFile);
      }
  
      /**
       * Read the CPU usages from a file
       *
       * @param timeInStatePath path where the CPU usages are read from
       * @return list of CPU usage times from the file. These correspond to the CPU frequencies given
       * by {@link ProcTimeInStateReader#getFrequenciesKhz}
       */
      @Nullable
      public long[] getUsageTimesMillis(final Path timeInStatePath) {
          // Read in the time_in_state file
          final long[] readLongs = new long[mFrequenciesKhz.length];
          final boolean readSuccess = Process.readProcFile(
                  timeInStatePath.toString(),
                  mTimeInStateTimeFormat,
                  null, readLongs, null);
          if (!readSuccess) {
              return null;
          }
          // Usage time is given in 10ms, so convert to ms
          for (int i = 0; i < readLongs.length; i++) {
              readLongs[i] *= 10;
          }
          return readLongs;
      }
  
      /**
       * Get the frequencies found in each {@code time_in_state} file
       *
       * @return list of CPU frequencies. These correspond to the CPU times given by {@link
       * ProcTimeInStateReader#getUsageTimesMillis(Path)}()}.
       */
      @Nullable
      public long[] getFrequenciesKhz() {
          return mFrequenciesKhz;
      }
  
      /**
       * Set the {@link #mTimeInStateTimeFormat} and {@link #mFrequenciesKhz} variables based on the
       * an input file. If the file is empty, these variables aren't set
       *
       * This needs to be run once on the first invocation of {@link #getUsageTimesMillis(Path)}. This
       * is because we need to know how many frequencies are available in order to parse time
       * {@code time_in_state} file using {@link Process#readProcFile}, which only accepts
       * fixed-length formats. Also, as the frequencies do not change between {@code time_in_state}
       * files, we read and store them here.
       *
       * @param timeInStatePath the input file to base the format off of
       */
      private void initializeTimeInStateFormat(final Path timeInStatePath) throws IOException {
          // Read the bytes of the `time_in_state` file
          byte[] timeInStateBytes = Files.readAllBytes(timeInStatePath);
  
          // Iterate over the lines of the time_in_state file, for each one adding a line to the
          // formats. These formats are used to extract either the frequencies or the times from a
          // time_in_state file
          // Also check if each line is a header, and handle this in the created format arrays
          ArrayList<Integer> timeInStateFrequencyFormat = new ArrayList<>();
          ArrayList<Integer> timeInStateTimeFormat = new ArrayList<>();
          int numFrequencies = 0;
          for (int i = 0; i < timeInStateBytes.length; i++) {
              // If the first character of the line is not a digit, we treat it as a header
              if (!Character.isDigit(timeInStateBytes[i])) {
                  timeInStateFrequencyFormat.addAll(TIME_IN_STATE_HEADER_LINE_FORMAT);
                  timeInStateTimeFormat.addAll(TIME_IN_STATE_HEADER_LINE_FORMAT);
              } else {
                  timeInStateFrequencyFormat.addAll(TIME_IN_STATE_LINE_FREQUENCY_FORMAT);
                  timeInStateTimeFormat.addAll(TIME_IN_STATE_LINE_TIME_FORMAT);
                  numFrequencies++;
              }
              // Go to the next line
              while (i < timeInStateBytes.length && timeInStateBytes[i] != '\n') {
                  i++;
              }
          }
  
          if (numFrequencies == 0) {
              throw new IOException("Empty time_in_state file");
          }
  
          // Read the frequencies from the `time_in_state` file and store them, as they will be the
          // same for every `time_in_state` file
          final long[] readLongs = new long[numFrequencies];
          final boolean readSuccess = Process.parseProcLine(
                  timeInStateBytes, 0, timeInStateBytes.length,
                  ArrayUtils.convertToIntArray(timeInStateFrequencyFormat), null, readLongs, null);
          if (!readSuccess) {
              throw new IOException("Failed to parse time_in_state file");
          }
  
          mTimeInStateTimeFormat = ArrayUtils.convertToIntArray(timeInStateTimeFormat);
          mFrequenciesKhz = readLongs;
      }
  }
  