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

#include "base/memory/memory_pressure_monitor_chromeos.h"

#include <fcntl.h>
#include <sys/select.h>

#include "base/metrics/histogram_macros.h"
#include "base/posix/eintr_wrapper.h"
#include "base/process/process_metrics.h"
#include "base/single_thread_task_runner.h"
#include "base/sys_info.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"

namespace base {
namespace chromeos {

namespace {

// The time between memory pressure checks. While under critical pressure, this
// is also the timer to repeat cleanup attempts.
const int kMemoryPressureIntervalMs = 1000;

// The time which should pass between two moderate memory pressure calls.
const int kModerateMemoryPressureCooldownMs = 10000;

// Number of event polls before the next moderate pressure event can be sent.
const int kModerateMemoryPressureCooldown =
    kModerateMemoryPressureCooldownMs / kMemoryPressureIntervalMs;

// Threshold constants to emit pressure events.
const int kNormalMemoryPressureModerateThresholdPercent = 60;
const int kNormalMemoryPressureCriticalThresholdPercent = 95;
const int kAggressiveMemoryPressureModerateThresholdPercent = 35;
const int kAggressiveMemoryPressureCriticalThresholdPercent = 70;

// The possible state for memory pressure level. The values should be in line
// with values in MemoryPressureListener::MemoryPressureLevel and should be
// updated if more memory pressure levels are introduced.
enum MemoryPressureLevelUMA {
  MEMORY_PRESSURE_LEVEL_NONE = 0,
  MEMORY_PRESSURE_LEVEL_MODERATE,
  MEMORY_PRESSURE_LEVEL_CRITICAL,
  NUM_MEMORY_PRESSURE_LEVELS
};

// This is the file that will exist if low memory notification is available
// on the device.  Whenever it becomes readable, it signals a low memory
// condition.
const char kLowMemFile[] = "/dev/chromeos-low-mem";

// Converts a |MemoryPressureThreshold| value into a used memory percentage for
// the moderate pressure event.
int GetModerateMemoryThresholdInPercent(
    MemoryPressureMonitor::MemoryPressureThresholds thresholds) {
  return thresholds == MemoryPressureMonitor::
                           THRESHOLD_AGGRESSIVE_CACHE_DISCARD ||
         thresholds == MemoryPressureMonitor::THRESHOLD_AGGRESSIVE
             ? kAggressiveMemoryPressureModerateThresholdPercent
             : kNormalMemoryPressureModerateThresholdPercent;
}

// Converts a |MemoryPressureThreshold| value into a used memory percentage for
// the critical pressure event.
int GetCriticalMemoryThresholdInPercent(
    MemoryPressureMonitor::MemoryPressureThresholds thresholds) {
  return thresholds == MemoryPressureMonitor::
                           THRESHOLD_AGGRESSIVE_TAB_DISCARD ||
         thresholds == MemoryPressureMonitor::THRESHOLD_AGGRESSIVE
             ? kAggressiveMemoryPressureCriticalThresholdPercent
             : kNormalMemoryPressureCriticalThresholdPercent;
}

// Converts free percent of memory into a memory pressure value.
MemoryPressureListener::MemoryPressureLevel GetMemoryPressureLevelFromFillLevel(
    int actual_fill_level,
    int moderate_threshold,
    int critical_threshold) {
  if (actual_fill_level < moderate_threshold)
    return MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE;
  return actual_fill_level < critical_threshold
             ? MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE
             : MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL;
}

// This function will be called less than once a second. It will check if
// the kernel has detected a low memory situation.
bool IsLowMemoryCondition(int file_descriptor) {
  fd_set fds;
  struct timeval tv;

  FD_ZERO(&fds);
  FD_SET(file_descriptor, &fds);

  tv.tv_sec = 0;
  tv.tv_usec = 0;

  return HANDLE_EINTR(select(file_descriptor + 1, &fds, NULL, NULL, &tv)) > 0;
}

}  // namespace

MemoryPressureMonitor::MemoryPressureMonitor(
    MemoryPressureThresholds thresholds)
    : current_memory_pressure_level_(
          MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE),
      moderate_pressure_repeat_count_(0),
      seconds_since_reporting_(0),
      moderate_pressure_threshold_percent_(
          GetModerateMemoryThresholdInPercent(thresholds)),
      critical_pressure_threshold_percent_(
          GetCriticalMemoryThresholdInPercent(thresholds)),
      low_mem_file_(HANDLE_EINTR(::open(kLowMemFile, O_RDONLY))),
      dispatch_callback_(
          base::Bind(&MemoryPressureListener::NotifyMemoryPressure)),
      weak_ptr_factory_(this) {
  StartObserving();
  LOG_IF(ERROR,
         base::SysInfo::IsRunningOnChromeOS() && !low_mem_file_.is_valid())
      << "Cannot open kernel listener";
}

MemoryPressureMonitor::~MemoryPressureMonitor() {
  StopObserving();
}

void MemoryPressureMonitor::ScheduleEarlyCheck() {
  ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE, BindOnce(&MemoryPressureMonitor::CheckMemoryPressure,
                          weak_ptr_factory_.GetWeakPtr()));
}

MemoryPressureListener::MemoryPressureLevel
MemoryPressureMonitor::GetCurrentPressureLevel() {
  return current_memory_pressure_level_;
}

// static
MemoryPressureMonitor* MemoryPressureMonitor::Get() {
  return static_cast<MemoryPressureMonitor*>(
      base::MemoryPressureMonitor::Get());
}

void MemoryPressureMonitor::StartObserving() {
  timer_.Start(FROM_HERE,
               TimeDelta::FromMilliseconds(kMemoryPressureIntervalMs),
               Bind(&MemoryPressureMonitor::
                        CheckMemoryPressureAndRecordStatistics,
                    weak_ptr_factory_.GetWeakPtr()));
}

void MemoryPressureMonitor::StopObserving() {
  // If StartObserving failed, StopObserving will still get called.
  timer_.Stop();
}

void MemoryPressureMonitor::CheckMemoryPressureAndRecordStatistics() {
  CheckMemoryPressure();
  if (seconds_since_reporting_++ == 5) {
    seconds_since_reporting_ = 0;
    RecordMemoryPressure(current_memory_pressure_level_, 1);
  }
  // Record UMA histogram statistics for the current memory pressure level.
  // TODO(lgrey): Remove this once there's a usable history for the
  // "Memory.PressureLevel" statistic
  MemoryPressureLevelUMA memory_pressure_level_uma(MEMORY_PRESSURE_LEVEL_NONE);
  switch (current_memory_pressure_level_) {
    case MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE:
      memory_pressure_level_uma = MEMORY_PRESSURE_LEVEL_NONE;
      break;
    case MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE:
      memory_pressure_level_uma = MEMORY_PRESSURE_LEVEL_MODERATE;
      break;
    case MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL:
      memory_pressure_level_uma = MEMORY_PRESSURE_LEVEL_CRITICAL;
      break;
  }

  UMA_HISTOGRAM_ENUMERATION("ChromeOS.MemoryPressureLevel",
                            memory_pressure_level_uma,
                            NUM_MEMORY_PRESSURE_LEVELS);
}

void MemoryPressureMonitor::CheckMemoryPressure() {
  MemoryPressureListener::MemoryPressureLevel old_pressure =
      current_memory_pressure_level_;

  // If we have the kernel low memory observer, we use it's flag instead of our
  // own computation (for now). Note that in "simulation mode" it can be null.
  // TODO(skuhne): We need to add code which makes sure that the kernel and this
  // computation come to similar results and then remove this override again.
  // TODO(skuhne): Add some testing framework here to see how close the kernel
  // and the internal functions are.
  if (low_mem_file_.is_valid() && IsLowMemoryCondition(low_mem_file_.get())) {
    current_memory_pressure_level_ =
        MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL;
  } else {
    current_memory_pressure_level_ = GetMemoryPressureLevelFromFillLevel(
        GetUsedMemoryInPercent(),
        moderate_pressure_threshold_percent_,
        critical_pressure_threshold_percent_);

    // When listening to the kernel, we ignore the reported memory pressure
    // level from our own computation and reduce critical to moderate.
    if (low_mem_file_.is_valid() &&
        current_memory_pressure_level_ ==
        MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL) {
      current_memory_pressure_level_ =
          MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE;
    }
  }

  // In case there is no memory pressure we do not notify.
  if (current_memory_pressure_level_ ==
      MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE) {
    return;
  }
  if (old_pressure == current_memory_pressure_level_) {
    // If the memory pressure is still at the same level, we notify again for a
    // critical level. In case of a moderate level repeat however, we only send
    // a notification after a certain time has passed.
    if (current_memory_pressure_level_ ==
        MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE &&
          ++moderate_pressure_repeat_count_ <
              kModerateMemoryPressureCooldown) {
      return;
    }
  } else if (current_memory_pressure_level_ ==
               MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE &&
             old_pressure ==
               MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL) {
    // When we reducing the pressure level from critical to moderate, we
    // restart the timeout and do not send another notification.
    moderate_pressure_repeat_count_ = 0;
    return;
  }
  moderate_pressure_repeat_count_ = 0;
  dispatch_callback_.Run(current_memory_pressure_level_);
}

// Gets the used ChromeOS memory in percent.
int MemoryPressureMonitor::GetUsedMemoryInPercent() {
  base::SystemMemoryInfoKB info;
  if (!base::GetSystemMemoryInfo(&info)) {
    VLOG(1) << "Cannot determine the free memory of the system.";
    return 0;
  }
  // TODO(skuhne): Instead of adding the kernel memory pressure calculation
  // logic here, we should have a kernel mechanism similar to the low memory
  // notifier in ChromeOS which offers multiple pressure states.
  // To track this, we have crbug.com/381196.

  // The available memory consists of "real" and virtual (z)ram memory.
  // Since swappable memory uses a non pre-deterministic compression and
  // the compression creates its own "dynamic" in the system, it gets
  // de-emphasized by the |kSwapWeight| factor.
  const int kSwapWeight = 4;

  // The total memory we have is the "real memory" plus the virtual (z)ram.
  int total_memory = info.total + info.swap_total / kSwapWeight;

  // The kernel internally uses 50MB.
  const int kMinFileMemory = 50 * 1024;

  // Most file memory can be easily reclaimed.
  int file_memory = info.active_file + info.inactive_file;
  // unless it is dirty or it's a minimal portion which is required.
  file_memory -= info.dirty + kMinFileMemory;

  // Available memory is the sum of free, swap and easy reclaimable memory.
  int available_memory =
      info.free + info.swap_free / kSwapWeight + file_memory;

  DCHECK(available_memory < total_memory);
  int percentage = ((total_memory - available_memory) * 100) / total_memory;
  return percentage;
}

void MemoryPressureMonitor::SetDispatchCallback(
    const DispatchCallback& callback) {
  dispatch_callback_ = callback;
}

}  // namespace chromeos
}  // namespace base
