// Copyright 2013 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/discardable_memory_provider.h"

#include "base/bind.h"
#include "base/containers/hash_tables.h"
#include "base/containers/mru_cache.h"
#include "base/debug/trace_event.h"
#include "base/synchronization/lock.h"
#include "base/sys_info.h"

namespace base {
namespace internal {

namespace {

// This is admittedly pretty magical. It's approximately enough memory for four
// 2560x1600 images.
static const size_t kDefaultDiscardableMemoryLimit = 64 * 1024 * 1024;
static const size_t kDefaultBytesToReclaimUnderModeratePressure =
    (3 * kDefaultDiscardableMemoryLimit) / 4;

}  // namespace

DiscardableMemoryProvider::DiscardableMemoryProvider()
    : allocations_(AllocationMap::NO_AUTO_EVICT),
      bytes_allocated_(0),
      discardable_memory_limit_(kDefaultDiscardableMemoryLimit),
      bytes_to_reclaim_under_moderate_pressure_(
          kDefaultBytesToReclaimUnderModeratePressure) {
}

DiscardableMemoryProvider::~DiscardableMemoryProvider() {
  DCHECK(allocations_.empty());
  DCHECK_EQ(0u, bytes_allocated_);
}

void DiscardableMemoryProvider::RegisterMemoryPressureListener() {
  AutoLock lock(lock_);
  DCHECK(base::MessageLoop::current());
  DCHECK(!memory_pressure_listener_);
  memory_pressure_listener_.reset(
      new MemoryPressureListener(
          base::Bind(&DiscardableMemoryProvider::OnMemoryPressure,
                     Unretained(this))));
}

void DiscardableMemoryProvider::UnregisterMemoryPressureListener() {
  AutoLock lock(lock_);
  DCHECK(memory_pressure_listener_);
  memory_pressure_listener_.reset();
}

void DiscardableMemoryProvider::SetDiscardableMemoryLimit(size_t bytes) {
  AutoLock lock(lock_);
  discardable_memory_limit_ = bytes;
  EnforcePolicyWithLockAcquired();
}

void DiscardableMemoryProvider::SetBytesToReclaimUnderModeratePressure(
    size_t bytes) {
  AutoLock lock(lock_);
  bytes_to_reclaim_under_moderate_pressure_ = bytes;
}

void DiscardableMemoryProvider::Register(
    const DiscardableMemory* discardable, size_t bytes) {
  AutoLock lock(lock_);
  // A registered memory listener is currently required. This DCHECK can be
  // moved or removed if we decide that it's useful to relax this condition.
  // TODO(reveman): Enable this DCHECK when skia and blink are able to
  // register memory pressure listeners. crbug.com/333907
  // DCHECK(memory_pressure_listener_);
  DCHECK(allocations_.Peek(discardable) == allocations_.end());
  allocations_.Put(discardable, Allocation(bytes));
}

void DiscardableMemoryProvider::Unregister(
    const DiscardableMemory* discardable) {
  AutoLock lock(lock_);
  AllocationMap::iterator it = allocations_.Peek(discardable);
  if (it == allocations_.end())
    return;

  if (it->second.memory) {
    size_t bytes = it->second.bytes;
    DCHECK_LE(bytes, bytes_allocated_);
    bytes_allocated_ -= bytes;
    free(it->second.memory);
  }
  allocations_.Erase(it);
}

scoped_ptr<uint8, FreeDeleter> DiscardableMemoryProvider::Acquire(
    const DiscardableMemory* discardable,
    bool* purged) {
  AutoLock lock(lock_);
  // NB: |allocations_| is an MRU cache, and use of |Get| here updates that
  // cache.
  AllocationMap::iterator it = allocations_.Get(discardable);
  CHECK(it != allocations_.end());

  if (it->second.memory) {
    scoped_ptr<uint8, FreeDeleter> memory(it->second.memory);
    it->second.memory = NULL;
    *purged = false;
    return memory.Pass();
  }

  size_t bytes = it->second.bytes;
  if (!bytes)
    return scoped_ptr<uint8, FreeDeleter>();

  if (discardable_memory_limit_) {
    size_t limit = 0;
    if (bytes < discardable_memory_limit_)
      limit = discardable_memory_limit_ - bytes;

    PurgeLRUWithLockAcquiredUntilUsageIsWithin(limit);
  }

  // Check for overflow.
  if (std::numeric_limits<size_t>::max() - bytes < bytes_allocated_)
    return scoped_ptr<uint8, FreeDeleter>();

  scoped_ptr<uint8, FreeDeleter> memory(static_cast<uint8*>(malloc(bytes)));
  if (!memory)
    return scoped_ptr<uint8, FreeDeleter>();

  bytes_allocated_ += bytes;
  *purged = true;
  return memory.Pass();
}

void DiscardableMemoryProvider::Release(
    const DiscardableMemory* discardable,
    scoped_ptr<uint8, FreeDeleter> memory) {
  AutoLock lock(lock_);
  // NB: |allocations_| is an MRU cache, and use of |Get| here updates that
  // cache.
  AllocationMap::iterator it = allocations_.Get(discardable);
  CHECK(it != allocations_.end());

  DCHECK(!it->second.memory);
  it->second.memory = memory.release();

  EnforcePolicyWithLockAcquired();
}

void DiscardableMemoryProvider::PurgeAll() {
  AutoLock lock(lock_);
  PurgeLRUWithLockAcquiredUntilUsageIsWithin(0);
}

bool DiscardableMemoryProvider::IsRegisteredForTest(
    const DiscardableMemory* discardable) const {
  AutoLock lock(lock_);
  AllocationMap::const_iterator it = allocations_.Peek(discardable);
  return it != allocations_.end();
}

bool DiscardableMemoryProvider::CanBePurgedForTest(
    const DiscardableMemory* discardable) const {
  AutoLock lock(lock_);
  AllocationMap::const_iterator it = allocations_.Peek(discardable);
  return it != allocations_.end() && it->second.memory;
}

size_t DiscardableMemoryProvider::GetBytesAllocatedForTest() const {
  AutoLock lock(lock_);
  return bytes_allocated_;
}

void DiscardableMemoryProvider::OnMemoryPressure(
    MemoryPressureListener::MemoryPressureLevel pressure_level) {
  switch (pressure_level) {
    case MemoryPressureListener::MEMORY_PRESSURE_MODERATE:
      Purge();
      return;
    case MemoryPressureListener::MEMORY_PRESSURE_CRITICAL:
      PurgeAll();
      return;
  }

  NOTREACHED();
}

void DiscardableMemoryProvider::Purge() {
  AutoLock lock(lock_);

  if (bytes_to_reclaim_under_moderate_pressure_ == 0)
    return;

  size_t limit = 0;
  if (bytes_to_reclaim_under_moderate_pressure_ < bytes_allocated_)
    limit = bytes_allocated_ - bytes_to_reclaim_under_moderate_pressure_;

  PurgeLRUWithLockAcquiredUntilUsageIsWithin(limit);
}

void DiscardableMemoryProvider::PurgeLRUWithLockAcquiredUntilUsageIsWithin(
    size_t limit) {
  TRACE_EVENT1(
      "base",
      "DiscardableMemoryProvider::PurgeLRUWithLockAcquiredUntilUsageIsWithin",
      "limit", limit);

  lock_.AssertAcquired();

  for (AllocationMap::reverse_iterator it = allocations_.rbegin();
       it != allocations_.rend();
       ++it) {
    if (bytes_allocated_ <= limit)
      break;
    if (!it->second.memory)
      continue;

    size_t bytes = it->second.bytes;
    DCHECK_LE(bytes, bytes_allocated_);
    bytes_allocated_ -= bytes;
    free(it->second.memory);
    it->second.memory = NULL;
  }
}

void DiscardableMemoryProvider::EnforcePolicyWithLockAcquired() {
  PurgeLRUWithLockAcquiredUntilUsageIsWithin(discardable_memory_limit_);
}

}  // namespace internal
}  // namespace base
