// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/metrics/psi_memory_parser.h"

#include <stddef.h>

#include <cinttypes>
#include <map>
#include <memory>
#include <string>

#include "base/metrics/histogram.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "components/metrics/metrics_log_store.h"

namespace metrics {

namespace {

// Periods supported by standard Linux PSI metricvs.
constexpr uint32_t kMinCollectionInterval = 10;
constexpr uint32_t kMidCollectionInterval = 60;
constexpr uint32_t kMaxCollectionInterval = 300;

constexpr uint32_t kDefaultCollectionInterval = kMinCollectionInterval;

// Name of the histogram that represents the success and various failure modes
// for parsing PSI memory data.
const char kParsePSIMemoryHistogramName[] = "ChromeOS.CWP.ParsePSIMemory";

constexpr base::StringPiece kContentPrefixSome = "some";
constexpr base::StringPiece kContentPrefixFull = "full";
constexpr base::StringPiece kContentTerminator = " total=";
constexpr base::StringPiece kMetricTerminator = " ";

const char kMetricPrefixFormat[] = "avg%d=";

}  // namespace

PSIMemoryParser::PSIMemoryParser(uint32_t period)
    : period_(kDefaultCollectionInterval) {
  if (period == kMinCollectionInterval || period == kMidCollectionInterval ||
      period == kMaxCollectionInterval) {
    period_ = period;
  } else {
    LOG(WARNING) << "Ignoring invalid interval [" << period << "]";
  }

  metric_prefix_ = base::StringPrintf(kMetricPrefixFormat, period_);
}

PSIMemoryParser::~PSIMemoryParser() = default;

uint32_t PSIMemoryParser::GetPeriod() const {
  return period_;
}

int PSIMemoryParser::GetMetricValue(const base::StringPiece& content,
                                    size_t start,
                                    size_t end) {
  size_t value_start;
  size_t value_end;
  if (!internal::FindMiddleString(content, start, metric_prefix_,
                                  kMetricTerminator, &value_start,
                                  &value_end)) {
    return -1;
  }
  if (value_end > end) {
    return -1;  // Out of bounds of the search area.
  }

  double n;
  const base::StringPiece metric_value_text =
      content.substr(value_start, value_end - value_start);
  if (!base::StringToDouble(metric_value_text, &n)) {
    return -1;  // Unable to convert string to number
  }

  // Want to multiply by 100, but to avoid integer truncation,
  // do best-effort rounding.
  const int preround = static_cast<int>(n * 1000);
  return (preround + 5) / 10;
}

void PSIMemoryParser::LogParseStatus(ParsePSIMemStatus stat) {
  constexpr int statCeiling =
      static_cast<int>(ParsePSIMemStatus::kMaxValue) + 1;
  base::UmaHistogramExactLinear(kParsePSIMemoryHistogramName,
                                static_cast<int>(stat), statCeiling);
}

ParsePSIMemStatus PSIMemoryParser::ParseMetrics(
    const base::StringPiece& content,
    int* metric_some,
    int* metric_full) {
  size_t str_some_start;
  size_t str_some_end;
  size_t str_full_start;
  size_t str_full_end;

  // Example of content:
  //  some avg10=0.00 avg60=0.00 avg300=0.00 total=417963
  //  full avg10=0.00 avg60=0.00 avg300=0.00 total=205933
  // we will pick one of the columns depending on the colleciton period set

  DCHECK_NE(metric_some, nullptr);
  DCHECK_NE(metric_full, nullptr);

  if (!internal::FindMiddleString(content, 0, kContentPrefixSome,
                                  kContentTerminator, &str_some_start,
                                  &str_some_end)) {
    return ParsePSIMemStatus::kUnexpectedDataFormat;
  }

  if (!internal::FindMiddleString(content,
                                  str_some_end + kContentTerminator.length(),
                                  kContentPrefixFull, kContentTerminator,
                                  &str_full_start, &str_full_end)) {
    return ParsePSIMemStatus::kUnexpectedDataFormat;
  }

  int compute_some = GetMetricValue(content, str_some_start, str_some_end);
  if (compute_some < 0) {
    return ParsePSIMemStatus::kInvalidMetricFormat;
  }

  int compute_full = GetMetricValue(content, str_full_start, str_full_end);
  if (compute_full < 0) {
    return ParsePSIMemStatus::kInvalidMetricFormat;
  }

  *metric_some = compute_some;
  *metric_full = compute_full;

  return ParsePSIMemStatus::kSuccess;
}

ParsePSIMemStatus PSIMemoryParser::ParseMetrics(const uint8_t* content,
                                                uint32_t len,
                                                int* metric_some,
                                                int* metric_full) {
  // The cast below is admittedly sneaky, but inherently safe because
  // we are translating a const pointer into another const pointer,
  // and the data sizes of the pointed object are the same.
  const char* string_content = reinterpret_cast<const char*>(content);

  return ParseMetrics(base::StringPiece(string_content, len), metric_some,
                      metric_full);
}

namespace internal {

bool FindMiddleString(const base::StringPiece& content,
                      size_t search_start,
                      const base::StringPiece& prefix,
                      const base::StringPiece& suffix,
                      size_t* start,
                      size_t* end) {
  DCHECK_NE(start, nullptr);
  DCHECK_NE(end, nullptr);

  size_t compute_start = content.find(prefix, search_start);
  if (compute_start == std::string::npos) {
    return false;
  }
  compute_start += prefix.length();

  size_t compute_end = content.find(suffix, compute_start);
  if (compute_end == std::string::npos) {
    return false;
  }

  *start = compute_start;
  *end = compute_end;

  return true;
}

}  // namespace internal

}  // namespace metrics
