// Copyright 2020 the V8 project 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 "src/heap/cppgc-internal/page-memory.h"

#include <algorithm>

#include "src/base/page-allocator.h"
#include "src/heap/cppgc-internal/platform.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace cppgc {
namespace internal {

TEST(MemoryRegionTest, Construct) {
  constexpr size_t kSize = 17;
  uint8_t dummy[kSize];
  const MemoryRegion region(dummy, kSize);
  EXPECT_EQ(dummy, region.base());
  EXPECT_EQ(kSize, region.size());
  EXPECT_EQ(dummy + kSize, region.end());
}

namespace {

Address AtOffset(uint8_t* base, intptr_t offset) {
  return reinterpret_cast<Address>(reinterpret_cast<intptr_t>(base) + offset);
}

}  // namespace

TEST(MemoryRegionTest, ContainsAddress) {
  constexpr size_t kSize = 7;
  uint8_t dummy[kSize];
  const MemoryRegion region(dummy, kSize);
  EXPECT_FALSE(region.Contains(AtOffset(dummy, -1)));
  EXPECT_TRUE(region.Contains(dummy));
  EXPECT_TRUE(region.Contains(dummy + kSize - 1));
  EXPECT_FALSE(region.Contains(AtOffset(dummy, kSize)));
}

TEST(MemoryRegionTest, ContainsMemoryRegion) {
  constexpr size_t kSize = 7;
  uint8_t dummy[kSize];
  const MemoryRegion region(dummy, kSize);
  const MemoryRegion contained_region1(dummy, kSize - 1);
  EXPECT_TRUE(region.Contains(contained_region1));
  const MemoryRegion contained_region2(dummy + 1, kSize - 1);
  EXPECT_TRUE(region.Contains(contained_region2));
  const MemoryRegion not_contained_region1(AtOffset(dummy, -1), kSize);
  EXPECT_FALSE(region.Contains(not_contained_region1));
  const MemoryRegion not_contained_region2(AtOffset(dummy, kSize), 1);
  EXPECT_FALSE(region.Contains(not_contained_region2));
}

namespace {

V8_NOINLINE uint8_t access(volatile const uint8_t& u) { return u; }

}  // namespace

TEST(PageBackendDeathTest, ReservationIsFreed) {
  // Full sequence as part of the death test macro as otherwise, the macro
  // may expand to statements that re-purpose the previously freed memory
  // and thus not crash.
  EXPECT_DEATH_IF_SUPPORTED(
      v8::base::PageAllocator allocator; Address base; {
        PageBackend backend(allocator, allocator);
        base = backend.TryAllocateLargePageMemory(1024);
      } access(*base);
      , "");
}

TEST(PageBackendTreeTest, AddNormalLookupRemove) {
  v8::base::PageAllocator allocator;
  PageBackend backend(allocator, allocator);
  auto* writable_base = backend.TryAllocateNormalPageMemory();
  auto* reserved_base = writable_base;
  auto& tree = backend.get_page_memory_region_tree_for_testing();
  ASSERT_EQ(reserved_base, tree.Lookup(reserved_base)->region().base());
  ASSERT_EQ(reserved_base,
            tree.Lookup(reserved_base + kPageSize - 1)->region().base());
  ASSERT_EQ(nullptr, tree.Lookup(reserved_base - 1));
  ASSERT_EQ(nullptr, tree.Lookup(reserved_base + kPageSize));
  backend.FreeNormalPageMemory(writable_base);
  ASSERT_EQ(nullptr, tree.Lookup(reserved_base));
  ASSERT_EQ(nullptr, tree.Lookup(reserved_base + kPageSize - 1));
}

TEST(PageBackendTreeTest, AddLargeLookupRemove) {
  v8::base::PageAllocator allocator;
  constexpr size_t kLargeSize = 5012;
  const size_t allocated_page_size =
      RoundUp(kLargeSize, allocator.AllocatePageSize());
  PageBackend backend(allocator, allocator);
  auto* writable_base = backend.TryAllocateLargePageMemory(kLargeSize);
  auto* reserved_base = writable_base;
  auto& tree = backend.get_page_memory_region_tree_for_testing();
  ASSERT_EQ(reserved_base, tree.Lookup(reserved_base)->region().base());
  ASSERT_EQ(
      reserved_base,
      tree.Lookup(reserved_base + allocated_page_size - 1)->region().base());
  ASSERT_EQ(nullptr, tree.Lookup(reserved_base - 1));
  ASSERT_EQ(nullptr, tree.Lookup(reserved_base + allocated_page_size));
  backend.FreeLargePageMemory(writable_base);
  ASSERT_EQ(nullptr, tree.Lookup(reserved_base));
  ASSERT_EQ(nullptr, tree.Lookup(reserved_base + allocated_page_size - 1));
}

TEST(PageBackendTreeTest, AddLookupRemoveMultiple) {
  v8::base::PageAllocator allocator;
  constexpr size_t kLargeSize = 3127;
  const size_t allocated_page_size =
      RoundUp(kLargeSize, allocator.AllocatePageSize());

  PageBackend backend(allocator, allocator);
  auto& tree = backend.get_page_memory_region_tree_for_testing();

  auto* writable_normal_base = backend.TryAllocateNormalPageMemory();
  auto* reserved_normal_base = writable_normal_base;
  auto* writable_large_base = backend.TryAllocateLargePageMemory(kLargeSize);
  auto* reserved_large_base = writable_large_base;

  ASSERT_EQ(reserved_normal_base,
            tree.Lookup(reserved_normal_base)->region().base());
  ASSERT_EQ(reserved_normal_base,
            tree.Lookup(reserved_normal_base + kPageSize - 1)->region().base());
  ASSERT_EQ(reserved_large_base,
            tree.Lookup(reserved_large_base)->region().base());
  ASSERT_EQ(reserved_large_base,
            tree.Lookup(reserved_large_base + allocated_page_size - 1)
                ->region()
                .base());

  backend.FreeNormalPageMemory(writable_normal_base);

  ASSERT_EQ(reserved_large_base,
            tree.Lookup(reserved_large_base)->region().base());
  ASSERT_EQ(reserved_large_base,
            tree.Lookup(reserved_large_base + allocated_page_size - 1)
                ->region()
                .base());

  backend.FreeLargePageMemory(writable_large_base);

  ASSERT_EQ(nullptr, tree.Lookup(reserved_large_base));
  ASSERT_EQ(nullptr,
            tree.Lookup(reserved_large_base + allocated_page_size - 1));
}

TEST(PageBackendPoolTest, ConstructorEmpty) {
  v8::base::PageAllocator allocator;
  PageBackend backend(allocator, allocator);
  auto& pool = backend.page_pool();
  EXPECT_EQ(nullptr, pool.Take());
}

TEST(PageBackendPoolTest, AddTake) {
  v8::base::PageAllocator allocator;
  PageBackend backend(allocator, allocator);
  auto& pool = backend.page_pool();
  auto& raw_pool = pool.get_raw_pool_for_testing();

  EXPECT_TRUE(raw_pool.empty());
  auto* writable_base1 = backend.TryAllocateNormalPageMemory();
  EXPECT_TRUE(raw_pool.empty());

  backend.FreeNormalPageMemory(writable_base1);
  EXPECT_FALSE(raw_pool.empty());
  EXPECT_TRUE(raw_pool[0]);
  EXPECT_EQ(raw_pool[0]->region().base(), writable_base1);

  auto* writable_base2 = backend.TryAllocateNormalPageMemory();
  EXPECT_TRUE(raw_pool.empty());
  EXPECT_EQ(writable_base1, writable_base2);
}

TEST(PageBackendPoolTest, AddTakeWithReleasePooledPagesInBetween) {
  v8::base::PageAllocator allocator;
  PageBackend backend(allocator, allocator);
  auto& pool = backend.page_pool();
  auto& raw_pool = pool.get_raw_pool_for_testing();

  EXPECT_TRUE(raw_pool.empty());
  auto* writable_base1 = backend.TryAllocateNormalPageMemory();
  EXPECT_TRUE(raw_pool.empty());
  EXPECT_EQ(0u, pool.PooledMemory());

  backend.FreeNormalPageMemory(writable_base1);
  EXPECT_FALSE(raw_pool.empty());
  EXPECT_TRUE(raw_pool[0]);
  EXPECT_EQ(raw_pool[0]->region().base(), writable_base1);
  size_t size = raw_pool[0]->region().size();
  EXPECT_EQ(size, pool.PooledMemory());

  backend.ReleasePooledPages();
  EXPECT_EQ(0u, pool.PooledMemory());
  EXPECT_TRUE(raw_pool.empty());

  auto* writable_base2 = backend.TryAllocateNormalPageMemory();
  EXPECT_TRUE(raw_pool.empty());
  EXPECT_EQ(0u, pool.PooledMemory());
  // After ReleasePooledPages() the memory is returned to the OS.
  EXPECT_NE(nullptr, writable_base2);
  // Should not die: memory is writable.
  memset(writable_base2, 12, size);
  backend.FreeNormalPageMemory(writable_base2);
}

TEST(PageBackendPoolTest, PoolMemoryAccounting) {
  v8::base::PageAllocator allocator;
  PageBackend backend(allocator, allocator);
  auto& pool = backend.page_pool();

  auto* writable_base1 = backend.TryAllocateNormalPageMemory();
  auto* writable_base2 = backend.TryAllocateNormalPageMemory();
  backend.FreeNormalPageMemory(writable_base1);
  backend.FreeNormalPageMemory(writable_base2);
  size_t normal_page_size = pool.get_raw_pool_for_testing()[0]->region().size();

  EXPECT_EQ(2 * normal_page_size, pool.PooledMemory());
  backend.ReleasePooledPages();
  EXPECT_EQ(0u, pool.PooledMemory());

  auto* writable_base3 = backend.TryAllocateNormalPageMemory();
  backend.FreeNormalPageMemory(writable_base3);
  EXPECT_EQ(normal_page_size, pool.PooledMemory());
  backend.ReleasePooledPages();
  EXPECT_EQ(0u, pool.PooledMemory());
}

TEST(PageBackendTest, AllocateNormalUsesPool) {
  v8::base::PageAllocator allocator;
  PageBackend backend(allocator, allocator);
  Address writeable_base1 = backend.TryAllocateNormalPageMemory();
  EXPECT_NE(nullptr, writeable_base1);
  backend.FreeNormalPageMemory(writeable_base1);
  Address writeable_base2 = backend.TryAllocateNormalPageMemory();
  EXPECT_NE(nullptr, writeable_base2);
  EXPECT_EQ(writeable_base1, writeable_base2);
}

TEST(PageBackendTest, AllocateLarge) {
  v8::base::PageAllocator allocator;
  PageBackend backend(allocator, allocator);
  Address writeable_base1 = backend.TryAllocateLargePageMemory(13731);
  EXPECT_NE(nullptr, writeable_base1);
  Address writeable_base2 = backend.TryAllocateLargePageMemory(9478);
  EXPECT_NE(nullptr, writeable_base2);
  EXPECT_NE(writeable_base1, writeable_base2);
  backend.FreeLargePageMemory(writeable_base1);
  backend.FreeLargePageMemory(writeable_base2);
}

TEST(PageBackendTest, LookupNormal) {
  v8::base::PageAllocator allocator;
  PageBackend backend(allocator, allocator);
  Address writeable_base = backend.TryAllocateNormalPageMemory();
  EXPECT_EQ(nullptr, backend.Lookup(writeable_base - 1));
  EXPECT_EQ(writeable_base, backend.Lookup(writeable_base));
  EXPECT_EQ(writeable_base, backend.Lookup(writeable_base + kPageSize - 1));
  EXPECT_EQ(nullptr, backend.Lookup(writeable_base + kPageSize));
}

TEST(PageBackendTest, LookupLarge) {
  v8::base::PageAllocator allocator;
  PageBackend backend(allocator, allocator);
  constexpr size_t kSize = 7934;
  Address writeable_base = backend.TryAllocateLargePageMemory(kSize);
  EXPECT_EQ(nullptr, backend.Lookup(writeable_base - 1));
  EXPECT_EQ(writeable_base, backend.Lookup(writeable_base));
  EXPECT_EQ(writeable_base, backend.Lookup(writeable_base + kSize - 1));
}

TEST(PageBackendDeathTest, DestructingBackendDestroysPageMemory) {
  v8::base::PageAllocator allocator;
  Address base;
  {
    PageBackend backend(allocator, allocator);
    base = backend.TryAllocateNormalPageMemory();
  }
  EXPECT_DEATH_IF_SUPPORTED(access(base[0]), "");
}

}  // namespace internal
}  // namespace cppgc
