// 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/stats-collector.h"

#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace cppgc {
namespace internal {

namespace {

constexpr size_t kNoMarkedBytes = 0;

constexpr size_t kMinReportedSize = StatsCollector::kAllocationThresholdBytes;

class StatsCollectorTest : public ::testing::Test {
 public:
  static constexpr Platform* kNoPlatform = nullptr;

  StatsCollectorTest() : stats(kNoPlatform) {}

  void FakeAllocate(size_t bytes) {
    stats.NotifyAllocation(bytes);
    stats.NotifySafePointForConservativeCollection();
  }

  void FakeFree(size_t bytes) {
    stats.NotifyExplicitFree(bytes);
    stats.NotifySafePointForConservativeCollection();
  }

  StatsCollector stats;
};

}  // namespace

TEST_F(StatsCollectorTest, NoMarkedBytes) {
  stats.NotifyMarkingStarted(CollectionType::kMajor,
                             GCConfig::MarkingType::kAtomic,
                             GCConfig::IsForcedGC::kNotForced);
  stats.NotifyMarkingCompleted(kNoMarkedBytes);
  stats.NotifySweepingCompleted(GCConfig::SweepingType::kAtomic);
  auto event = stats.GetPreviousEventForTesting();
  EXPECT_EQ(0u, event.marked_bytes);
}

TEST_F(StatsCollectorTest, EventPrevGCMarkedObjectSize) {
  stats.NotifyMarkingStarted(CollectionType::kMajor,
                             GCConfig::MarkingType::kAtomic,
                             GCConfig::IsForcedGC::kNotForced);
  stats.NotifyMarkingCompleted(1024);
  stats.NotifySweepingCompleted(GCConfig::SweepingType::kAtomic);
  auto event = stats.GetPreviousEventForTesting();
  EXPECT_EQ(1024u, event.marked_bytes);
}

TEST_F(StatsCollectorTest, AllocationNoReportBelowAllocationThresholdBytes) {
  constexpr size_t kObjectSize = 17;
  EXPECT_LT(kObjectSize, StatsCollector::kAllocationThresholdBytes);
  FakeAllocate(kObjectSize);
  EXPECT_EQ(0u, stats.allocated_object_size());
}

TEST_F(StatsCollectorTest, AlllocationReportAboveAllocationThresholdBytes) {
  constexpr size_t kObjectSize = StatsCollector::kAllocationThresholdBytes;
  EXPECT_GE(kObjectSize, StatsCollector::kAllocationThresholdBytes);
  FakeAllocate(kObjectSize);
  EXPECT_EQ(kObjectSize, stats.allocated_object_size());
}

TEST_F(StatsCollectorTest, InitialAllocatedObjectSize) {
  stats.NotifyMarkingStarted(CollectionType::kMajor,
                             GCConfig::MarkingType::kAtomic,
                             GCConfig::IsForcedGC::kNotForced);
  EXPECT_EQ(0u, stats.allocated_object_size());
  stats.NotifyMarkingCompleted(kNoMarkedBytes);
  EXPECT_EQ(0u, stats.allocated_object_size());
  stats.NotifySweepingCompleted(GCConfig::SweepingType::kAtomic);
  EXPECT_EQ(0u, stats.allocated_object_size());
}

TEST_F(StatsCollectorTest, AllocatedObjectSize) {
  stats.NotifyMarkingStarted(CollectionType::kMajor,
                             GCConfig::MarkingType::kAtomic,
                             GCConfig::IsForcedGC::kNotForced);
  FakeAllocate(kMinReportedSize);
  EXPECT_EQ(kMinReportedSize, stats.allocated_object_size());
  stats.NotifyMarkingCompleted(kMinReportedSize);
  EXPECT_EQ(kMinReportedSize, stats.allocated_object_size());
  stats.NotifySweepingCompleted(GCConfig::SweepingType::kAtomic);
  EXPECT_EQ(kMinReportedSize, stats.allocated_object_size());
}

TEST_F(StatsCollectorTest, AllocatedObjectSizeNoMarkedBytes) {
  stats.NotifyMarkingStarted(CollectionType::kMajor,
                             GCConfig::MarkingType::kAtomic,
                             GCConfig::IsForcedGC::kNotForced);
  FakeAllocate(kMinReportedSize);
  EXPECT_EQ(kMinReportedSize, stats.allocated_object_size());
  stats.NotifyMarkingCompleted(kNoMarkedBytes);
  EXPECT_EQ(0u, stats.allocated_object_size());
  stats.NotifySweepingCompleted(GCConfig::SweepingType::kAtomic);
  EXPECT_EQ(0u, stats.allocated_object_size());
}

TEST_F(StatsCollectorTest, AllocatedObjectSizeAllocateAfterMarking) {
  stats.NotifyMarkingStarted(CollectionType::kMajor,
                             GCConfig::MarkingType::kAtomic,
                             GCConfig::IsForcedGC::kNotForced);
  FakeAllocate(kMinReportedSize);
  EXPECT_EQ(kMinReportedSize, stats.allocated_object_size());
  stats.NotifyMarkingCompleted(kMinReportedSize);
  FakeAllocate(kMinReportedSize);
  EXPECT_EQ(2 * kMinReportedSize, stats.allocated_object_size());
  stats.NotifySweepingCompleted(GCConfig::SweepingType::kAtomic);
  EXPECT_EQ(2 * kMinReportedSize, stats.allocated_object_size());
}

class MockAllocationObserver : public StatsCollector::AllocationObserver {
 public:
  MOCK_METHOD(void, AllocatedObjectSizeIncreased, (size_t), (override));
  MOCK_METHOD(void, AllocatedObjectSizeDecreased, (size_t), (override));
  MOCK_METHOD(void, ResetAllocatedObjectSize, (size_t), (override));
  MOCK_METHOD(void, AllocatedSizeIncreased, (size_t), (override));
  MOCK_METHOD(void, AllocatedSizeDecreased, (size_t), (override));
};

TEST_F(StatsCollectorTest, RegisterUnregisterObserver) {
  MockAllocationObserver observer;
  stats.RegisterObserver(&observer);
  stats.UnregisterObserver(&observer);
}

TEST_F(StatsCollectorTest, ObserveAllocatedObjectSizeIncreaseAndDecrease) {
  MockAllocationObserver observer;
  stats.RegisterObserver(&observer);
  EXPECT_CALL(observer, AllocatedObjectSizeIncreased(kMinReportedSize));
  FakeAllocate(kMinReportedSize);
  EXPECT_CALL(observer, AllocatedObjectSizeDecreased(kMinReportedSize));
  FakeFree(kMinReportedSize);
  stats.UnregisterObserver(&observer);
}

namespace {

void FakeGC(StatsCollector* stats, size_t marked_bytes) {
  stats->NotifyMarkingStarted(CollectionType::kMajor,
                              GCConfig::MarkingType::kAtomic,
                              GCConfig::IsForcedGC::kNotForced);
  stats->NotifyMarkingCompleted(marked_bytes);
  stats->NotifySweepingCompleted(GCConfig::SweepingType::kAtomic);
}

}  // namespace

TEST_F(StatsCollectorTest, ObserveResetAllocatedObjectSize) {
  MockAllocationObserver observer;
  stats.RegisterObserver(&observer);
  EXPECT_CALL(observer, AllocatedObjectSizeIncreased(kMinReportedSize));
  FakeAllocate(kMinReportedSize);
  EXPECT_CALL(observer, ResetAllocatedObjectSize(64));
  FakeGC(&stats, 64);
  stats.UnregisterObserver(&observer);
}

TEST_F(StatsCollectorTest, ObserveAllocatedMemoryIncreaseAndDecrease) {
  MockAllocationObserver observer;
  stats.RegisterObserver(&observer);
  static constexpr size_t kAllocatedMemorySize = 4096;
  EXPECT_CALL(observer, AllocatedSizeIncreased(kAllocatedMemorySize));
  stats.NotifyAllocatedMemory(kAllocatedMemorySize);
  static constexpr size_t kFreedMemorySize = 2048;
  EXPECT_CALL(observer, AllocatedSizeDecreased(kFreedMemorySize));
  stats.NotifyFreedMemory(kFreedMemorySize);
  stats.UnregisterObserver(&observer);
}

namespace {

class AllocationObserverTriggeringGC final
    : public StatsCollector::AllocationObserver {
 public:
  AllocationObserverTriggeringGC(StatsCollector* stats, double survival_ratio)
      : stats(stats), survival_ratio_(survival_ratio) {}

  void AllocatedObjectSizeIncreased(size_t bytes) final {
    increase_call_count++;
    increased_size_bytes += bytes;
    if (increase_call_count == 1) {
      FakeGC(stats, bytes * survival_ratio_);
    }
  }

  // // Mock out the rest to trigger warnings if used.
  MOCK_METHOD(void, AllocatedObjectSizeDecreased, (size_t), (override));
  MOCK_METHOD(void, ResetAllocatedObjectSize, (size_t), (override));

  size_t increase_call_count = 0;
  size_t increased_size_bytes = 0;
  StatsCollector* stats;
  double survival_ratio_;
};

}  // namespace

TEST_F(StatsCollectorTest, ObserverTriggersGC) {
  constexpr double kSurvivalRatio = 0.5;
  AllocationObserverTriggeringGC gc_observer(&stats, kSurvivalRatio);
  MockAllocationObserver mock_observer;
  // Internal detail: First registered observer is also notified first.
  stats.RegisterObserver(&gc_observer);
  stats.RegisterObserver(&mock_observer);

  // Both observers see the exact allocated object size byte count.
  EXPECT_CALL(mock_observer,
              ResetAllocatedObjectSize(kMinReportedSize * kSurvivalRatio));
  EXPECT_CALL(gc_observer,
              ResetAllocatedObjectSize(kMinReportedSize * kSurvivalRatio));

  // Since the GC clears counters, mock_observer should see an increase call
  // with a delta of zero bytes. This expectation makes use of the internal
  // detail that first registered observer triggers GC.
  EXPECT_CALL(mock_observer, AllocatedObjectSizeIncreased(0));

  // Trigger scenario.
  FakeAllocate(kMinReportedSize);

  EXPECT_EQ(1u, gc_observer.increase_call_count);
  EXPECT_EQ(kMinReportedSize, gc_observer.increased_size_bytes);

  stats.UnregisterObserver(&gc_observer);
  stats.UnregisterObserver(&mock_observer);
}

TEST_F(StatsCollectorTest, AllocatedMemorySize) {
  EXPECT_EQ(0u, stats.allocated_memory_size());
  stats.NotifyAllocatedMemory(1024);
  EXPECT_EQ(1024u, stats.allocated_memory_size());
  stats.NotifyFreedMemory(1024);
  EXPECT_EQ(0u, stats.allocated_memory_size());
}

TEST_F(StatsCollectorTest, DiscardedMemorySize) {
  EXPECT_EQ(0u, stats.discarded_memory_size());
  stats.IncrementDiscardedMemory(1024);
  EXPECT_EQ(1024u, stats.discarded_memory_size());
  stats.DecrementDiscardedMemory(1024);
  EXPECT_EQ(0u, stats.discarded_memory_size());
}

TEST_F(StatsCollectorTest, ResidentMemorySizeWithoutDiscarded) {
  EXPECT_EQ(0u, stats.resident_memory_size());
  stats.NotifyAllocatedMemory(1024);
  EXPECT_EQ(1024u, stats.resident_memory_size());
  stats.NotifyFreedMemory(1024);
  EXPECT_EQ(0u, stats.resident_memory_size());
}

TEST_F(StatsCollectorTest, ResidentMemorySizeWithDiscarded) {
  EXPECT_EQ(0u, stats.resident_memory_size());
  stats.NotifyAllocatedMemory(8192);
  EXPECT_EQ(8192u, stats.resident_memory_size());
  stats.IncrementDiscardedMemory(4096);
  EXPECT_EQ(4096u, stats.resident_memory_size());
  stats.DecrementDiscardedMemory(4096);
  EXPECT_EQ(8192u, stats.resident_memory_size());
  stats.NotifyFreedMemory(8192);
  EXPECT_EQ(0u, stats.resident_memory_size());
}

}  // namespace internal
}  // namespace cppgc
