/*
 * Copyright (c) 2021, 2025, 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 "gc/g1/g1CardSet.inline.hpp"
#include "gc/g1/g1CardSetContainers.hpp"
#include "gc/g1/g1CardSetMemory.hpp"
#include "gc/g1/g1HeapRegionRemSet.hpp"
#include "gc/g1/g1MonotonicArenaFreePool.hpp"
#include "gc/shared/gcTraceTime.inline.hpp"
#include "gc/shared/workerThread.hpp"
#include "logging/log.hpp"
#include "memory/allocation.hpp"
#include "unittest.hpp"
#include "utilities/powerOfTwo.hpp"

class G1CardSetTest : public ::testing::Test {

  class G1CountCardsClosure : public G1CardSet::CardClosure {
  public:
    size_t _num_cards;

    G1CountCardsClosure() : _num_cards(0) { }
    void do_card(uint region_idx, uint card_idx) override {
      _num_cards++;
    }
  };

  static WorkerThreads* _workers;
  static uint _max_workers;

  static WorkerThreads* workers() {
    if (_workers == nullptr) {
      _max_workers = os::processor_count();
      _workers = new WorkerThreads("G1CardSetTest Workers", _max_workers);
      _workers->initialize_workers();
      _workers->set_active_workers(_max_workers);
    }
    return _workers;
  }

  // Check whether iteration agrees with the expected number of entries. If the
  // add has been single-threaded, we can also check whether the occupied()
  // (which is an estimate in that case) agrees.
  static void check_iteration(G1CardSet* card_set,
                              const size_t expected,
                              const bool add_was_single_threaded = true);

public:
  G1CardSetTest() { }
  ~G1CardSetTest() { }

  static uint next_random(uint& seed, uint i) {
    // Park-Miller random number generator
    seed = (seed * 279470273u) % 0xfffffffb;
    return (seed % i);
  }

  static void cardset_basic_test();
  static void cardset_mt_test();

  static void add_cards(G1CardSet* card_set, uint cards_per_region, uint* cards, uint num_cards, G1AddCardResult* results);
  static void contains_cards(G1CardSet* card_set, uint cards_per_region, uint* cards, uint num_cards);

  static void translate_cards(uint cards_per_region, uint region_idx, uint* cards, uint num_cards);

  static void iterate_cards(G1CardSet* card_set, G1CardSet::CardClosure* cl);
};

WorkerThreads* G1CardSetTest::_workers = nullptr;
uint G1CardSetTest::_max_workers = 0;

void G1CardSetTest::add_cards(G1CardSet* card_set, uint cards_per_region, uint* cards, uint num_cards, G1AddCardResult* results) {
  for (uint i = 0; i < num_cards; i++) {

    uint region_idx = cards[i] / cards_per_region;
    uint card_idx = cards[i] % cards_per_region;

    G1AddCardResult res = card_set->add_card(region_idx, card_idx);
    if (results != nullptr) {
      ASSERT_TRUE(res == results[i]);
    }
  }
}

class G1CheckCardClosure : public G1CardSet::CardClosure {
  G1CardSet* _card_set;

  uint _cards_per_region;
  uint* _cards_to_expect;
  uint _num_cards;

  bool _wrong_region_idx;

public:
  G1CheckCardClosure(G1CardSet* card_set, uint cards_per_region, uint* cards_to_expect, uint num_cards) :
    _card_set(card_set),
    _cards_per_region(cards_per_region),
    _cards_to_expect(cards_to_expect),
    _num_cards(num_cards),
    _wrong_region_idx(false) {
  }

  void do_card(uint region_idx, uint card_idx) override {
    uint card = _cards_per_region * region_idx + card_idx;
    for (uint i = 0; i < _num_cards; i++) {
      if (_cards_to_expect[i] == card) {
        _cards_to_expect[i] = (uint)-1;
      }
    }
  }

  bool all_found() const {
    bool all_good = true;
    for (uint i = 0; i < _num_cards; i++) {
      if (_cards_to_expect[i] != (uint)-1) {
        log_error(gc)("Could not find card %u in region %u",
                      _cards_to_expect[i] % _cards_per_region,
                      _cards_to_expect[i] / _cards_per_region);
        all_good = false;
      }
    }
    return all_good;
  }
};

void G1CardSetTest::contains_cards(G1CardSet* card_set, uint cards_per_region, uint* cards, uint num_cards) {
  for (uint i = 0; i < num_cards; i++) {
    uint region_idx = cards[i] / cards_per_region;
    uint card_idx = cards[i] % cards_per_region;

    ASSERT_TRUE(card_set->contains_card(region_idx, card_idx));
  }

  G1CheckCardClosure cl(card_set, cards_per_region, cards, num_cards);
  card_set->iterate_cards(cl);

  ASSERT_TRUE(cl.all_found());
}

// Offsets the card indexes in the cards array by the region_idx.
void G1CardSetTest::translate_cards(uint cards_per_region, uint region_idx, uint* cards, uint num_cards) {
  for (uint i = 0; i < num_cards; i++) {
    cards[i] = cards_per_region * region_idx + cards[i];
  }
}

class G1CountCardsOccupied : public G1CardSet::ContainerPtrClosure {
  size_t _num_occupied;

public:
  G1CountCardsOccupied() : _num_occupied(0) { }

  void do_containerptr(uint region_idx, size_t num_occupied, G1CardSet::ContainerPtr container) override {
    _num_occupied += num_occupied;
  }

  size_t num_occupied() const { return _num_occupied; }
};

void G1CardSetTest::check_iteration(G1CardSet* card_set, const size_t expected, const bool single_threaded) {

  class CheckIterator : public G1CardSet::CardClosure {
  public:
    G1CardSet* _card_set;
    size_t _num_found;

    CheckIterator(G1CardSet* card_set) : _card_set(card_set), _num_found(0) { }

    void do_card(uint region_idx, uint card_idx) override {
      ASSERT_TRUE(_card_set->contains_card(region_idx, card_idx));
      _num_found++;
    }
  } cl(card_set);

  card_set->iterate_cards(cl);

  ASSERT_TRUE(expected == cl._num_found);
  // We can assert this only if we are single-threaded.
  if (single_threaded) {
    ASSERT_EQ(card_set->occupied(), cl._num_found);
  }
}

void G1CardSetTest::cardset_basic_test() {

  const uint CardsPerRegion = 2048;
  const double FullCardSetThreshold = 0.8;
  const double BitmapCoarsenThreshold = 0.9;

  G1CardSetConfiguration config(28,
                                BitmapCoarsenThreshold,
                                8,
                                FullCardSetThreshold,
                                CardsPerRegion,
                                0);
  G1CardSetFreePool free_pool(config.num_mem_object_types());
  G1CardSetMemoryManager mm(&config, &free_pool);

  {
    G1CardSet card_set(&config, &mm);

    uint cards1[] = { 1, 2, 3 };
    G1AddCardResult results1[] = { Added, Added, Added };
    translate_cards(CardsPerRegion, 99, cards1, ARRAY_SIZE(cards1));
    add_cards(&card_set, CardsPerRegion, cards1, ARRAY_SIZE(cards1), results1);
    contains_cards(&card_set, CardsPerRegion, cards1, ARRAY_SIZE(cards1));
    ASSERT_TRUE(card_set.occupied() == ARRAY_SIZE(cards1));

    G1CountCardsClosure count_cards;
    card_set.iterate_cards(count_cards);
    ASSERT_TRUE(count_cards._num_cards == ARRAY_SIZE(cards1));

    check_iteration(&card_set, card_set.occupied());

    card_set.clear();
    ASSERT_TRUE(card_set.occupied() == 0);

    check_iteration(&card_set, 0);
  }

  {
    G1CardSet card_set(&config, &mm);

    uint cards1[] = { 0, 2047, 17, 17 };
    G1AddCardResult results1[] = { Added, Added, Added, Found };
    translate_cards(CardsPerRegion, 100, cards1, ARRAY_SIZE(cards1));
    add_cards(&card_set, CardsPerRegion, cards1, ARRAY_SIZE(cards1), results1);
    // -1 because of the duplicate at the end.
    contains_cards(&card_set, CardsPerRegion, cards1, ARRAY_SIZE(cards1) - 1);
    ASSERT_TRUE(card_set.occupied() == ARRAY_SIZE(cards1) - 1);

    G1CountCardsClosure count_cards;
    card_set.iterate_cards(count_cards);
    ASSERT_TRUE(count_cards._num_cards == ARRAY_SIZE(cards1) - 1);

    check_iteration(&card_set, card_set.occupied());

    card_set.clear();
    ASSERT_TRUE(card_set.occupied() == 0);
  }

  {
    G1CardSet card_set(&config, &mm);

    uint cards1[] = { 0, 2047, 17, 18 /* for region 100 */,
                      1,  128, 35, 17 /* for region 990 */
                    };
    translate_cards(CardsPerRegion, 100, &cards1[0], 4);
    translate_cards(CardsPerRegion, 990, &cards1[4], 4);

    add_cards(&card_set, CardsPerRegion, cards1, ARRAY_SIZE(cards1), nullptr);
    contains_cards(&card_set, CardsPerRegion, cards1, ARRAY_SIZE(cards1));
    ASSERT_TRUE(card_set.occupied() == ARRAY_SIZE(cards1));

    G1CountCardsClosure count_cards;
    card_set.iterate_cards(count_cards);
    ASSERT_TRUE(count_cards._num_cards == ARRAY_SIZE(cards1));

    check_iteration(&card_set, card_set.occupied());

    card_set.clear();
    ASSERT_TRUE(card_set.occupied() == 0);
  }

  {
    G1CardSet card_set(&config, &mm);

    uint cards1[100];
    for (uint i = 0; i < ARRAY_SIZE(cards1); i++) {
      cards1[i] = i + 3;
      translate_cards(CardsPerRegion, i, &cards1[i], 1);
    }
    add_cards(&card_set, CardsPerRegion, cards1, ARRAY_SIZE(cards1), nullptr);
    contains_cards(&card_set, CardsPerRegion, cards1, ARRAY_SIZE(cards1));

    ASSERT_TRUE(card_set.num_containers() == ARRAY_SIZE(cards1));
    ASSERT_TRUE(card_set.occupied() == ARRAY_SIZE(cards1));

    G1CountCardsClosure count_cards;
    card_set.iterate_cards(count_cards);
    ASSERT_TRUE(count_cards._num_cards == ARRAY_SIZE(cards1));

    check_iteration(&card_set, card_set.occupied());

    card_set.clear();
    ASSERT_TRUE(card_set.occupied() == 0);
  }

  {
    G1CardSet card_set(&config, &mm);

    // Generate non-prime numbers from 1 to 1000
    uint count = 0;
    for (uint i = 2; i < 33; i++) {
      if (!card_set.contains_card(100, i)) {
        for (uint j = i * i; j < 1000; j += i) {
          G1AddCardResult res = card_set.add_card(100, j);
          count += (res == Added);
        }
      }
    }

    G1CountCardsOccupied cl;
    card_set.iterate_containers(&cl);

    ASSERT_TRUE(count == card_set.occupied());
    ASSERT_TRUE(card_set.occupied() == cl.num_occupied());

    check_iteration(&card_set, card_set.occupied());

    card_set.clear();
    ASSERT_TRUE(card_set.occupied() == 0);
  }
  { // Test coarsening to full
    G1CardSet card_set(&config, &mm);

    uint count = 0;
    uint i = 10;
    uint bitmap_threshold = config.cards_in_howl_bitmap_threshold();
    for (; i <  bitmap_threshold + 10; i++) {
      G1AddCardResult res = card_set.add_card(99, i);
      ASSERT_TRUE(res == Added);
      count++;
      ASSERT_TRUE(count == card_set.occupied());
    }

    G1AddCardResult res = card_set.add_card(99, config.max_cards_in_howl_bitmap() - 1);
    // Adding above card should have coarsened Bitmap -> Full.
    ASSERT_TRUE(res == Added);
    ASSERT_TRUE(config.max_cards_in_howl_bitmap() == card_set.occupied());

    res = card_set.add_card(99, config.max_cards_in_howl_bitmap() - 2);
    ASSERT_TRUE(res == Found);

    uint threshold = config.cards_in_howl_threshold();
    uint adjusted_threshold = config.cards_in_howl_bitmap_threshold() * config.num_buckets_in_howl();
    i = config.max_cards_in_howl_bitmap();
    count = i;
    for (; i <  threshold; i++) {
      G1AddCardResult res = card_set.add_card(99, i);
      ASSERT_TRUE(res == Added);
      count++;
      ASSERT_TRUE(count == card_set.occupied());
    }

    res = card_set.add_card(99, CardsPerRegion - 1);
    // Adding above card should have coarsened Howl -> Full.
    ASSERT_TRUE(res == Added);
    ASSERT_TRUE(CardsPerRegion == card_set.occupied());

    check_iteration(&card_set, card_set.occupied());

    res = card_set.add_card(99, CardsPerRegion - 2);
    ASSERT_TRUE(res == Found);

    G1CountCardsClosure count_cards;
    card_set.iterate_cards(count_cards);
    ASSERT_TRUE(count_cards._num_cards == config.max_cards_in_region());

    card_set.clear();
    ASSERT_TRUE(card_set.occupied() == 0);
  }
}

class G1CardSetMtTestTask : public WorkerTask {
  G1CardSet* _card_set;

  size_t _added;
  size_t _found;

public:
  G1CardSetMtTestTask(G1CardSet* card_set) :
    WorkerTask(""),
    _card_set(card_set),
    _added(0),
    _found(0) { }

  void work(uint worker_id) {
    uint seed = worker_id;
    size_t added = 0;
    size_t found = 0;

    for (uint i = 0; i < 100000; i++) {
      uint region = G1CardSetTest::next_random(seed, 1000);
      uint card = G1CardSetTest::next_random(seed, 10000);

      G1AddCardResult res = _card_set->add_card(region, card);

      ASSERT_TRUE(res == Added || res == Found);
      if (res == Added) {
        added++;
      } else if (res == Found) {
        found++;
      }
    }
    AtomicAccess::add(&_added, added);
    AtomicAccess::add(&_found, found);
  }

  size_t added() const { return _added; }
  size_t found() const { return _found; }
};

void G1CardSetTest::cardset_mt_test() {
  const uint CardsPerRegion = 16384;
  const double FullCardSetThreshold = 1.0;
  const uint BitmapCoarsenThreshold = 1.0;

  G1CardSetConfiguration config(120,
                                BitmapCoarsenThreshold,
                                8,
                                FullCardSetThreshold,
                                CardsPerRegion,
                                0);
  G1CardSetFreePool free_pool(config.num_mem_object_types());
  G1CardSetMemoryManager mm(&config, &free_pool);

  G1CardSet card_set(&config, &mm);

  const uint num_workers = workers()->active_workers();

  G1CardSetMtTestTask cl(&card_set);

  {
    GCTraceTime(Error, gc) x("Cardset test");
    _workers->run_task(&cl, num_workers);
  }

  size_t num_found = 0;
  // Now check the contents of the card set.
  for (uint i = 0; i < num_workers; i++) {
    uint seed = i;

    for (uint j = 0; j < 100000; j++) {
      uint region = G1CardSetTest::next_random(seed, 1000);
      uint card = G1CardSetTest::next_random(seed, 10000);

      bool contains = card_set.contains_card(region, card);
      ASSERT_TRUE(contains);

      num_found += contains;
    }
  }

  ASSERT_TRUE(num_found == cl.added() + cl.found());

  G1CountCardsClosure count_cards;
  card_set.iterate_cards(count_cards);

  check_iteration(&card_set, count_cards._num_cards, false /* add_was_single_threaded */);

  // During coarsening we try to unblock concurrent threads as soon as possible,
  // so we do not add the cards from the smaller CardSetContainer to the larger
  // one immediately, allowing addition by concurrent threads after allocating
  // the space immediately. So the amount of "successfully added" results may be
  // (and in case of many threads typically is) higher than the number of unique
  // cards.
  ASSERT_TRUE(count_cards._num_cards <= cl.added());
}

TEST_VM(G1CardSetTest, basic_cardset_test) {
  G1CardSetTest::cardset_basic_test();
}

TEST_VM(G1CardSetTest, mt_cardset_test) {
  G1CardSetTest::cardset_mt_test();
}
