// Copyright 2022 The Centipede Authors.
//
// 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
//
//      https://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.

// TODO(ussuri): Upgrade to optionally measure the metrics of a given thread,
//  not the entire process (available via /proc/self/tasks/<tid>/<file>).

// Utility classes to capture and log system resource usage of the current
// process.

#ifndef THIRD_PARTY_CENTIPEDE_RUSAGE_STATS_H_
#define THIRD_PARTY_CENTIPEDE_RUSAGE_STATS_H_

#include <sys/resource.h>

#include <array>
#include <cstddef>
#include <cstdint>
#include <iosfwd>
#include <ostream>
#include <string>

#include "absl/time/time.h"

namespace centipede::perf {

// Memory size in bytes.
using MemSize = int64_t;
// How many CPU hyperthreaded cores the process has been using on average.
// 1 corresponds to 1 hypercore. The max is the number of hyperthreaded cores
// on the system.
using CpuHyperCores = double;
// What percentage of the allotted system scheduling time the process has
// actually utilized for CPU processing, as opposed to idling (e.g. waiting for
// I/O etc.). The theoretical max is 1.0, which corresponds to 100% utilization,
// however the value can go slightly higher due to rounding errors in the system
// scheduler's accounting logic.
using CpuUtilization = long double;

//------------------------------------------------------------------------------
//                               RUsageScope
//
// Specifies the scope of resource usage measurements: a process or a thread.
//------------------------------------------------------------------------------
class RUsageScope {
 public:
  enum ProcFile : size_t { kSched = 0, kStatm = 1, kStatus = 2, kNum = 3 };

  // Static ctors for supported use cases. If the same scope is used repeatedly,
  // callers should prefer caching it, as construction may involve syscalls.
  static RUsageScope ThisProcess();
  static RUsageScope Process(pid_t pid);
  static RUsageScope ThisThread();
  static RUsageScope ThisProcessThread(pid_t tid);
  static RUsageScope Thread(pid_t pid, pid_t tid);

  // Copyable and movable.
  RUsageScope(const RUsageScope&) = default;
  RUsageScope& operator=(const RUsageScope&) = default;
  RUsageScope(RUsageScope&&) = default;
  RUsageScope& operator=(RUsageScope&&) = default;

  // Returns a path to the /proc/<pid>/<file> or /proc/<pid>/task/<tid>/<file>.
  [[nodiscard]] const std::string& GetProcFilePath(ProcFile file) const;

  template <typename OStream>
  friend OStream& operator<<(OStream& os, const RUsageScope& s) {
      return os << s.description_;
  }

 private:
  explicit RUsageScope(pid_t pid);
  RUsageScope(pid_t pid, pid_t tid);

  std::string description_;
  std::array<std::string, ProcFile::kNum> proc_file_paths_;
};

//------------------------------------------------------------------------------
//                               ProcessTimer
//
// Measures the system, user, and wall times of the process. Can be a global
// variable because the implementation depends on nothing but syscalls.
// The parameterless RUsageTiming::Snapshot() uses the default global timer that
// starts with the process; clients also have an option to define and pass a
// custom timer to count from some other point in time.
//------------------------------------------------------------------------------

class ProcessTimer {
 public:
  ProcessTimer();
  void Get(double& user, double& sys, double& wall) const;

 private:
  absl::Time start_time_;
  struct rusage start_rusage_;
};

//------------------------------------------------------------------------------
//                                RUsageTiming
//
// An interface to measure, store, manipulate, and log the system timing stats
// of a process or a thread.
//------------------------------------------------------------------------------

struct RUsageTiming {
  //----------------------------------------------------------------------------
  //             Static factory ctors and friend operators

  static RUsageTiming Zero();
  static RUsageTiming Min();
  static RUsageTiming Max();

  // Returns the system timing stats for the specified rusage scope.
  // NOTE: Clients must cache the r-value returned by `RUsageScope` static ctors
  // to be able to call this (this is on purpose).
  static RUsageTiming Snapshot(const RUsageScope& scope);
  // Same as above, but using a custom timer. The caller is responsible for
  // setting up and passing the same timer object to all Snapshot() calls to get
  // consistent results.
  static RUsageTiming Snapshot(  //
      const RUsageScope& scope, const ProcessTimer& timer);

  // Comparisons. NOTE: `is_delta` is always ignored.
  friend bool operator==(const RUsageTiming& t1, const RUsageTiming& t2);
  friend bool operator!=(const RUsageTiming& t1, const RUsageTiming& t2);
  friend bool operator<(const RUsageTiming& t1, const RUsageTiming& t2);
  friend bool operator<=(const RUsageTiming& t1, const RUsageTiming& t2);
  friend bool operator>(const RUsageTiming& t1, const RUsageTiming& t2);
  friend bool operator>=(const RUsageTiming& t1, const RUsageTiming& t2);

  // Returns the low-water resource usage between the two args.
  static RUsageTiming LowWater(const RUsageTiming& t1, const RUsageTiming& t2);
  // Returns the high-water value between the two args.
  static RUsageTiming HighWater(const RUsageTiming& t1, const RUsageTiming& t2);

  // Returns the value with `is_delta` set to true. Useful for signed logging.
  friend RUsageTiming operator+(const RUsageTiming& t);
  // Returns the negated value with `is_delta` set to true.
  friend RUsageTiming operator-(const RUsageTiming& t);
  // Returns the signed delta between two stats, with `is_delta` set to true.
  friend RUsageTiming operator-(const RUsageTiming& t1, const RUsageTiming& t2);
  // Returns the sum of two stats, with `is_delta` set to true iff `t1` or `t2`
  // or both are deltas.
  friend RUsageTiming operator+(const RUsageTiming& t1, const RUsageTiming& t2);
  // Returns a RUsageTiming where every field is divided by `div`. `is_delta` is
  // carried over from `t`.
  friend RUsageTiming operator/(const RUsageTiming& t, int64_t div);

  // Streams `t.ShortStr()`.
  friend std::ostream& operator<<(std::ostream& os, const RUsageTiming& t);

  //----------------------------------------------------------------------------
  //                           Non-static methods

  // Returns the metrics as short string. If `is_delta` is true, positive values
  // will be prefixed with a '+'.
  std::string ShortStr() const;
  // Returns a formatted representation of the metrics. The format is fixed, so
  // if multiple objects get printed with newline separators, they will form a
  // table. If `is_delta` is true, positive values will be prefixed with a '+'.
  std::string FormattedStr() const;

  //----------------------------------------------------------------------------
  //                              Public data

  absl::Duration wall_time = absl::ZeroDuration();
  absl::Duration user_time = absl::ZeroDuration();
  absl::Duration sys_time = absl::ZeroDuration();
  CpuUtilization cpu_utilization = 0.;
  CpuHyperCores cpu_hyper_cores = 0.;
  // If true, positive values will be printed with a '+'.
  bool is_delta = false;
};

//------------------------------------------------------------------------------
//                               RUsageMemory
//
// An interface to measure, store, manipulate, and log the system memory usage
// of a process or a thread.
//------------------------------------------------------------------------------

struct RUsageMemory {
  //----------------------------------------------------------------------------
  //              Static factory ctors and friend operators

  static RUsageMemory Zero();
  static RUsageMemory Min();
  static RUsageMemory Max();

  // Returns the system memory stats for the specified rusage scope.
  // NOTE: Clients must cache the r-value returned by `RUsageScope` static ctors
  // to be able to call this (this is on purpose).
  static RUsageMemory Snapshot(const RUsageScope& scope);

  // Comparisons. NOTE: `is_delta` is always ignored.
  friend bool operator==(const RUsageMemory& m1, const RUsageMemory& m2);
  friend bool operator!=(const RUsageMemory& m1, const RUsageMemory& m2);
  friend bool operator<(const RUsageMemory& m1, const RUsageMemory& m2);
  friend bool operator<=(const RUsageMemory& m1, const RUsageMemory& m2);
  friend bool operator>(const RUsageMemory& m1, const RUsageMemory& m2);
  friend bool operator>=(const RUsageMemory& m1, const RUsageMemory& m2);

  // Returns the low-water value between the two args.
  static RUsageMemory LowWater(const RUsageMemory& m1, const RUsageMemory& m2);
  // Returns the high-water value usage between the two args.
  static RUsageMemory HighWater(const RUsageMemory& m1, const RUsageMemory& m2);

  // Returns the value with `is_delta` set to true. Useful for signed logging.
  friend RUsageMemory operator+(const RUsageMemory& m);
  // Returns the negated value with `is_delta` set to true.
  friend RUsageMemory operator-(const RUsageMemory& m);
  // Returns the signed delta of two stats, with `is_delta` set to true.
  friend RUsageMemory operator-(const RUsageMemory& m1, const RUsageMemory& m2);
  // Returns the sum of two stats, with `is_delta` set to true iff `m1` or `m2`
  // or both are deltas.
  friend RUsageMemory operator+(const RUsageMemory& m1, const RUsageMemory& m2);
  // Returns a value with every metric divided by `div`. `is_delta` is
  // carried over from `m`.
  friend RUsageMemory operator/(const RUsageMemory& m, int64_t div);

  // Streams `m.ShortStr()`.
  friend std::ostream& operator<<(std::ostream& os, const RUsageMemory& m);

  //----------------------------------------------------------------------------
  //                           Non-static methods

  // Returns the metrics as short string. If `is_delta` is true, positive values
  // will be prefixed with a '+'.
  std::string ShortStr() const;
  // Returns a formatted representation of the metrics. The format is fixed, so
  // if multiple objects get printed with newline separators, they will form a
  // table. If `is_delta` is true, positive values will be prefixed with a '+'.
  std::string FormattedStr() const;

  //----------------------------------------------------------------------------
  //                                   Data

  // Memory sizes are all in bytes. For the meaning of these, cf. `man proc` or
  // https://man7.org/linux/man-pages/man5/proc.5.html, sections
  // /proc/[pid]/{stat,statm,status}.
  MemSize mem_vsize = 0;
  MemSize mem_vpeak = 0;
  MemSize mem_rss = 0;
  MemSize mem_data = 0;
  MemSize mem_shared = 0;
  // If true, positive values will be printed with a '+'.
  bool is_delta = false;
};

//------------------------------------------------------------------------------
//                       Pretty-printing of the stats
//------------------------------------------------------------------------------

// Formats `duration` as the most compact human-readable string. Differences
// from absl::FormatDuration():
// - Durations up to 1s are rounded up to whole numbers of ns/us/ms.
// - Durations longer than 1s are rounded up to 2 decimals and are never
//   converted to hours/minutes/seconds.
// - Positive durations can be prefixed with a '+' (useful to indicate that the
//   value is a positive delta).
std::string FormatInOptimalUnits(absl::Duration duration, bool always_signed);
// Formats `bytes` as the most compact human-readable string in SI units.
// `always_signed` prints '+' before positive numbers (useful to indicate
// positive deltas).
std::string FormatInOptimalUnits(MemSize bytes, bool always_signed);
// Formats CPU utilization as a percentage.
// `always_signed` prints '+' before positive numbers (useful to indicate
// positive deltas).
std::string FormatInOptimalUnits(CpuUtilization util, bool always_signed);
// Formats CPU hypercores with decimal precision.
// `always_signed` prints '+' before positive numbers (useful to indicate
// positive deltas).
std::string FormatInOptimalUnits(CpuHyperCores cores, bool always_signed);

}  // namespace centipede::perf

#endif  // THIRD_PARTY_CENTIPEDE_RUSAGE_STATS_H_
