/*
 * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

#include "precompiled.hpp"
#include "gc/z/zList.inline.hpp"
#include "gc/z/zNUMA.hpp"
#include "gc/z/zPage.inline.hpp"
#include "gc/z/zPageCache.hpp"
#include "gc/z/zStat.hpp"
#include "logging/log.hpp"

static const ZStatCounter ZCounterPageCacheHitL1("Memory", "Page Cache Hit L1", ZStatUnitOpsPerSecond);
static const ZStatCounter ZCounterPageCacheHitL2("Memory", "Page Cache Hit L2", ZStatUnitOpsPerSecond);
static const ZStatCounter ZCounterPageCacheMiss("Memory", "Page Cache Miss", ZStatUnitOpsPerSecond);
static const ZStatCounter ZCounterPageCacheFlush("Memory", "Page Cache Flush", ZStatUnitBytesPerSecond);

ZPageCache::ZPageCache() :
    _available(0),
    _small(),
    _medium(),
    _large() {}

ZPage* ZPageCache::alloc_small_page() {
  const uint32_t numa_id = ZNUMA::id();
  const uint32_t numa_count = ZNUMA::count();

  // Try NUMA local page cache
  ZPage* const l1_page = _small.get(numa_id).remove_first();
  if (l1_page != NULL) {
    ZStatInc(ZCounterPageCacheHitL1);
    return l1_page;
  }

  // Try NUMA remote page cache(s)
  uint32_t remote_numa_id = numa_id + 1;
  const uint32_t remote_numa_count = numa_count - 1;
  for (uint32_t i = 0; i < remote_numa_count; i++) {
    if (remote_numa_id == numa_count) {
      remote_numa_id = 0;
    }

    ZPage* const l2_page = _small.get(remote_numa_id).remove_first();
    if (l2_page != NULL) {
      ZStatInc(ZCounterPageCacheHitL2);
      return l2_page;
    }

    remote_numa_id++;
  }

  ZStatInc(ZCounterPageCacheMiss);
  return NULL;
}

ZPage* ZPageCache::alloc_medium_page() {
  ZPage* const l1_page = _medium.remove_first();
  if (l1_page != NULL) {
    ZStatInc(ZCounterPageCacheHitL1);
    return l1_page;
  }

  ZStatInc(ZCounterPageCacheMiss);
  return NULL;
}

ZPage* ZPageCache::alloc_large_page(size_t size) {
  // Find a page with the right size
  ZListIterator<ZPage> iter(&_large);
  for (ZPage* l1_page; iter.next(&l1_page);) {
    if (l1_page->size() == size) {
      // Page found
      _large.remove(l1_page);
      ZStatInc(ZCounterPageCacheHitL1);
      return l1_page;
    }
  }

  ZStatInc(ZCounterPageCacheMiss);
  return NULL;
}

ZPage* ZPageCache::alloc_page(uint8_t type, size_t size) {
  ZPage* page;

  if (type == ZPageTypeSmall) {
    page = alloc_small_page();
  } else if (type == ZPageTypeMedium) {
    page = alloc_medium_page();
  } else {
    page = alloc_large_page(size);
  }

  if (page != NULL) {
    _available -= page->size();
  }

  return page;
}

void ZPageCache::free_page(ZPage* page) {
  assert(!page->is_active(), "Invalid page state");
  assert(!page->is_pinned(), "Invalid page state");
  assert(!page->is_detached(), "Invalid page state");

  const uint8_t type = page->type();
  if (type == ZPageTypeSmall) {
    _small.get(page->numa_id()).insert_first(page);
  } else if (type == ZPageTypeMedium) {
    _medium.insert_first(page);
  } else {
    _large.insert_first(page);
  }

  _available += page->size();
}

void ZPageCache::flush_list(ZList<ZPage>* from, size_t requested, ZList<ZPage>* to, size_t* flushed) {
  while (*flushed < requested) {
    // Flush least recently used
    ZPage* const page = from->remove_last();
    if (page == NULL) {
      break;
    }

    *flushed += page->size();
    to->insert_last(page);
  }
}

void ZPageCache::flush_per_numa_lists(ZPerNUMA<ZList<ZPage> >* from, size_t requested, ZList<ZPage>* to, size_t* flushed) {
  const uint32_t numa_count = ZNUMA::count();
  uint32_t numa_empty = 0;
  uint32_t numa_next = 0;

  // Flush lists round-robin
  while (*flushed < requested) {
    ZPage* const page = from->get(numa_next).remove_last();

    if (++numa_next == numa_count) {
      numa_next = 0;
    }

    if (page == NULL) {
      // List is empty
      if (++numa_empty == numa_count) {
        // All lists are empty
        break;
      }

      // Try next list
      continue;
    }

    // Flush page
    numa_empty = 0;
    *flushed += page->size();
    to->insert_last(page);
  }
}

void ZPageCache::flush(ZList<ZPage>* to, size_t requested) {
  size_t flushed = 0;

  // Prefer flushing large, then medium and last small pages
  flush_list(&_large, requested, to, &flushed);
  flush_list(&_medium, requested, to, &flushed);
  flush_per_numa_lists(&_small, requested, to, &flushed);

  ZStatInc(ZCounterPageCacheFlush, flushed);

  log_info(gc, heap)("Page Cache Flushed: "
                     SIZE_FORMAT "M requested, "
                     SIZE_FORMAT "M(" SIZE_FORMAT "M->" SIZE_FORMAT "M) flushed",
                     requested / M, flushed / M , _available / M, (_available - flushed) / M);

  _available -= flushed;
}
