// Copyright (c) 2024, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

#include "vm/heap/incremental_compactor.h"

#include "platform/assert.h"
#include "vm/dart_api_state.h"
#include "vm/globals.h"
#include "vm/heap/become.h"
#include "vm/heap/freelist.h"
#include "vm/heap/heap.h"
#include "vm/heap/pages.h"
#include "vm/log.h"
#include "vm/thread_barrier.h"
#include "vm/timeline.h"
#include "vm/visitor.h"

namespace dart {

void GCIncrementalCompactor::Prologue(PageSpace* old_space) {
  ASSERT(Thread::Current()->OwnsGCSafepoint());
  TIMELINE_FUNCTION_GC_DURATION(Thread::Current(), "StartIncrementalCompact");
  if (!SelectEvacuationCandidates(old_space)) {
    return;
  }
  CheckFreeLists(old_space);
}

bool GCIncrementalCompactor::Epilogue(PageSpace* old_space) {
  ASSERT(Thread::Current()->OwnsGCSafepoint());
  TIMELINE_FUNCTION_GC_DURATION(Thread::Current(), "FinishIncrementalCompact");
  if (!HasEvacuationCandidates(old_space)) {
    return false;
  }
  old_space->MakeIterable();
  CheckFreeLists(old_space);
  CheckPreEvacuate(old_space);
  Evacuate(old_space);
  CheckPostEvacuate(old_space);
  CheckFreeLists(old_space);
  FreeEvacuatedPages(old_space);
  VerifyAfterIncrementalCompaction(old_space);
  return true;
}

void GCIncrementalCompactor::Abort(PageSpace* old_space) {
  ASSERT(Thread::Current()->OwnsGCSafepoint());
  TIMELINE_FUNCTION_GC_DURATION(Thread::Current(), "AbortIncrementalCompact");

  {
    MonitorLocker ml(old_space->tasks_lock());
    switch (old_space->phase()) {
      case PageSpace::kDone:
        return;  // No incremental compact in progress.
      case PageSpace::kSweepingRegular:
      case PageSpace::kSweepingLarge:
        // No incremental compact in progress, the page list is incomplete, and
        // accessing page->next is a data race.
        return;
      case PageSpace::kMarking:
      case PageSpace::kAwaitingFinalization:
        break;  // Incremental compact may be in progress.
      default:
        UNREACHABLE();
    }
  }

  old_space->PauseConcurrentMarking();

  for (Page* page = old_space->pages_; page != nullptr; page = page->next()) {
    if (!page->is_evacuation_candidate()) continue;

    page->set_evacuation_candidate(false);

    uword start = page->object_start();
    uword end = page->object_end();
    uword current = start;
    while (current < end) {
      ObjectPtr obj = UntaggedObject::FromAddr(current);
      obj->untag()->ClearIsEvacuationCandidateUnsynchronized();
      current += obj->untag()->HeapSize();
    }
  }

  old_space->ResumeConcurrentMarking();
}

struct LiveBytes {
  Page* page;
  intptr_t live_bytes;
};

struct PrologueState {
  MallocGrowableArray<LiveBytes> pages;
  RelaxedAtomic<intptr_t> page_cursor;
  intptr_t page_limit;
  RelaxedAtomic<intptr_t> freelist_cursor;
  intptr_t freelist_limit;
};

class PrologueTask : public SafepointTask {
 public:
  PrologueTask(ThreadBarrier* barrier,
               IsolateGroup* isolate_group,
               PageSpace* old_space,
               PrologueState* state)
      : SafepointTask(isolate_group,
                      barrier,
                      Thread::kIncrementalCompactorTask),
        old_space_(old_space),
        state_(state) {}

  void RunEnteredIsolateGroup() override {
    MarkEvacuationCandidates();
    PruneFreeLists();
  }

  void MarkEvacuationCandidates() {
    TIMELINE_FUNCTION_GC_DURATION(Thread::Current(),
                                  "MarkEvacuationCandidates");
    for (;;) {
      intptr_t page_index = state_->page_cursor.fetch_add(1);
      if (page_index >= state_->page_limit) break;
      Page* page = state_->pages[page_index].page;

      // Already set, otherwise a barrier would be needed before moving onto
      // freelists.
      ASSERT(page->is_evacuation_candidate());

      uword start = page->object_start();
      uword end = page->object_end();
      uword current = start;
      while (current < end) {
        ObjectPtr obj = UntaggedObject::FromAddr(current);
        intptr_t cid = obj->untag()->GetClassId();
        if (cid != kFreeListElement && cid != kForwardingCorpse) {
          obj->untag()->SetIsEvacuationCandidateUnsynchronized();
        }
        current += obj->untag()->HeapSize();
      }
    }
  }

  void PruneFreeLists() {
    TIMELINE_FUNCTION_GC_DURATION(Thread::Current(), "PruneFreeLists");
    for (;;) {
      intptr_t chunk = state_->freelist_cursor.fetch_add(1);
      if (chunk >= state_->freelist_limit) break;
      intptr_t list_index = chunk / (FreeList::kNumLists + 1);
      intptr_t size_class_index = chunk % (FreeList::kNumLists + 1);
      FreeList* freelist = &old_space_->freelists_[list_index];

      // Empty bump-region, no need to prune this.
      ASSERT(freelist->top_ == freelist->end_);

      FreeListElement* current = freelist->free_lists_[size_class_index];
      freelist->free_lists_[size_class_index] = nullptr;
      while (current != nullptr) {
        FreeListElement* next = current->next();
        if (!Page::Of(current)->is_evacuation_candidate()) {
          current->set_next(freelist->free_lists_[size_class_index]);
          freelist->free_lists_[size_class_index] = current;
        }
        current = next;
      }
    }
  }

 private:
  PageSpace* old_space_;
  PrologueState* state_;

  DISALLOW_COPY_AND_ASSIGN(PrologueTask);
};

bool GCIncrementalCompactor::SelectEvacuationCandidates(PageSpace* old_space) {
  // Only evacuate pages that are at least half empty.
  constexpr intptr_t kEvacuationThreshold = Page::kPageSize / 2;

  // Evacuate no more than this amount of objects. This puts a bound on the
  // stop-the-world evacuate step that is similar to the existing longest
  // stop-the-world step of the scavenger.
  const intptr_t kMaxEvacuatedBytes =
      (old_space->heap_->new_space()->ThresholdInWords() << kWordSizeLog2) / 4;

  PrologueState state;
  {
    TIMELINE_FUNCTION_GC_DURATION(Thread::Current(),
                                  "SelectEvacuationCandidates");
    for (Page* page = old_space->pages_; page != nullptr; page = page->next()) {
      if (page->is_never_evacuate()) continue;

      intptr_t live_bytes = page->live_bytes();
      if (live_bytes > kEvacuationThreshold) continue;

      state.pages.Add({page, live_bytes});
    }
    state.pages.Sort([](const LiveBytes* a, const LiveBytes* b) -> int {
      if (a->live_bytes < b->live_bytes) return -1;
      if (a->live_bytes > b->live_bytes) return 1;
      return 0;
    });

    intptr_t num_candidates = 0;
    intptr_t cumulative_live_bytes = 0;
    for (intptr_t i = 0; i < state.pages.length(); i++) {
      intptr_t live_bytes = state.pages[i].live_bytes;
      if (cumulative_live_bytes + live_bytes <= kMaxEvacuatedBytes) {
        num_candidates++;
        cumulative_live_bytes += live_bytes;
        state.pages[i].page->set_evacuation_candidate(true);
      }
    }

#if defined(SUPPORT_TIMELINE)
    tbes.SetNumArguments(2);
    tbes.FormatArgument(0, "cumulative_live_bytes", "%" Pd,
                        cumulative_live_bytes);
    tbes.FormatArgument(1, "num_candidates", "%" Pd, num_candidates);
#endif

    state.page_cursor = 0;
    state.page_limit = num_candidates;
    state.freelist_cursor =
        PageSpace::kDataFreelist * (FreeList::kNumLists + 1);
    state.freelist_limit =
        old_space->num_freelists_ * (FreeList::kNumLists + 1);

    if (num_candidates == 0) return false;
  }

  old_space->ReleaseBumpAllocation();

  IsolateGroup* isolate_group = IsolateGroup::Current();
  const intptr_t num_tasks =
      isolate_group->heap()->new_space()->NumScavengeWorkers();
  RELEASE_ASSERT(num_tasks > 0);
  ThreadBarrier* barrier = new ThreadBarrier(num_tasks, /*initial=*/1);
  IntrusiveDList<SafepointTask> tasks;
  for (intptr_t i = 0; i < num_tasks; i++) {
    tasks.Append(new PrologueTask(barrier, isolate_group, old_space, &state));
  }
  isolate_group->safepoint_handler()->RunTasks(&tasks);

  for (intptr_t i = PageSpace::kDataFreelist, n = old_space->num_freelists_;
       i < n; i++) {
    FreeList* freelist = &old_space->freelists_[i];
    ASSERT(freelist->top_ == freelist->end_);
    freelist->free_map_.Reset();
    for (intptr_t j = 0; j < FreeList::kNumLists; j++) {
      freelist->free_map_.Set(j, freelist->free_lists_[j] != nullptr);
    }
  }

  return true;
}

// Free lists should not contain any evacuation candidates.
void GCIncrementalCompactor::CheckFreeLists(PageSpace* old_space) {
#if defined(DEBUG)
  for (intptr_t i = 0, n = old_space->num_freelists_; i < n; i++) {
    FreeList* freelist = &old_space->freelists_[i];
    if (freelist->top_ < freelist->end_) {
      Page* page = Page::Of(freelist->top_);
      ASSERT(!page->is_evacuation_candidate());
    }
    for (intptr_t j = 0; j <= FreeList::kNumLists; j++) {
      FreeListElement* current = freelist->free_lists_[j];
      while (current != nullptr) {
        Page* page = Page::Of(reinterpret_cast<uword>(current));
        ASSERT(!page->is_evacuation_candidate());
        current = current->next();
      }
    }
  }
#endif
}

static void objcpy(void* dst, const void* src, size_t size) {
  uword* __restrict dst_cursor = reinterpret_cast<uword*>(dst);
  const uword* __restrict src_cursor = reinterpret_cast<const uword*>(src);
  do {
    uword a = *src_cursor++;
    uword b = *src_cursor++;
    *dst_cursor++ = a;
    *dst_cursor++ = b;
    size -= (2 * sizeof(uword));
  } while (size > 0);
}

bool GCIncrementalCompactor::HasEvacuationCandidates(PageSpace* old_space) {
  for (Page* page = old_space->pages_; page != nullptr; page = page->next()) {
    if (page->is_evacuation_candidate()) return true;
  }
  return false;
}

void GCIncrementalCompactor::CheckPreEvacuate(PageSpace* old_space) {
  if (!FLAG_verify_before_gc) return;

  TIMELINE_FUNCTION_GC_DURATION(Thread::Current(), "CheckPreEvacuate");

  // Check evacuation candidate pages have evacuation candidate objects or free
  // space. I.e., we didn't allocate into it after selecting it as an evacuation
  // candidate.
  for (Page* page = old_space->pages_; page != nullptr; page = page->next()) {
    if (page->is_evacuation_candidate()) {
      uword start = page->object_start();
      uword end = page->object_end();
      uword current = start;
      while (current < end) {
        ObjectPtr obj = UntaggedObject::FromAddr(current);
        intptr_t size = obj->untag()->HeapSize();
        ASSERT(obj->untag()->IsEvacuationCandidate() ||
               obj->untag()->GetClassId() == kFreeListElement ||
               obj->untag()->GetClassId() == kForwardingCorpse);
        current += size;
      }
    }
  }

  // Check non-evac pages don't have evac candidates.
  for (Page* page = old_space->pages_; page != nullptr; page = page->next()) {
    if (!page->is_evacuation_candidate()) {
      uword start = page->object_start();
      uword end = page->object_end();
      uword current = start;
      while (current < end) {
        ObjectPtr obj = UntaggedObject::FromAddr(current);
        intptr_t size = obj->untag()->HeapSize();
        ASSERT(!obj->untag()->IsEvacuationCandidate());
        current += size;
      }
    }
  }
}

class IncrementalForwardingVisitor : public ObjectPointerVisitor,
                                     public PredicateObjectPointerVisitor,
                                     public ObjectVisitor,
                                     public HandleVisitor {
 public:
  explicit IncrementalForwardingVisitor(Thread* thread)
      : ObjectPointerVisitor(thread->isolate_group()), HandleVisitor() {}

  void VisitObject(ObjectPtr obj) override {
    if (obj->untag()->IsMarked()) {
      obj->untag()->VisitPointers(this);
    }
  }

  void VisitPointers(ObjectPtr* first, ObjectPtr* last) override {
    PredicateVisitPointers(first, last);
  }
  bool PredicateVisitPointers(ObjectPtr* first, ObjectPtr* last) override {
    bool has_new_target = false;
    for (ObjectPtr* ptr = first; ptr <= last; ptr++) {
      ObjectPtr target = *ptr;
      if (target->IsImmediateObject()) continue;
      if (target->IsNewObject()) {
        has_new_target = true;
        continue;
      }

      if (target->IsForwardingCorpse()) {
        ASSERT(!target->untag()->IsMarked());
        ASSERT(!target->untag()->IsEvacuationCandidate());
        uword addr = UntaggedObject::ToAddr(target);
        ForwardingCorpse* forwarder = reinterpret_cast<ForwardingCorpse*>(addr);
        *ptr = forwarder->target();
      } else {
        ASSERT(target->untag()->IsMarked());
        ASSERT(!target->untag()->IsEvacuationCandidate());
      }
    }
    return has_new_target;
  }

#if defined(DART_COMPRESSED_POINTERS)
  void VisitCompressedPointers(uword heap_base,
                               CompressedObjectPtr* first,
                               CompressedObjectPtr* last) override {
    PredicateVisitCompressedPointers(heap_base, first, last);
  }
  bool PredicateVisitCompressedPointers(uword heap_base,
                                        CompressedObjectPtr* first,
                                        CompressedObjectPtr* last) override {
    bool has_new_target = false;
    for (CompressedObjectPtr* ptr = first; ptr <= last; ptr++) {
      ObjectPtr target = ptr->Decompress(heap_base);
      if (target->IsImmediateObject()) continue;
      if (target->IsNewObject()) {
        has_new_target = true;
        continue;
      }

      if (target->IsForwardingCorpse()) {
        ASSERT(!target->untag()->IsMarked());
        ASSERT(!target->untag()->IsEvacuationCandidate());
        uword addr = UntaggedObject::ToAddr(target);
        ForwardingCorpse* forwarder = reinterpret_cast<ForwardingCorpse*>(addr);
        *ptr = forwarder->target();
      } else {
        ASSERT(target->untag()->IsMarked());
        ASSERT(!target->untag()->IsEvacuationCandidate());
      }
    }
    return has_new_target;
  }
#endif

  void VisitHandle(uword addr) override {
    FinalizablePersistentHandle* handle =
        reinterpret_cast<FinalizablePersistentHandle*>(addr);
    ObjectPtr target = handle->ptr();
    if (target->IsHeapObject() && target->IsForwardingCorpse()) {
      uword addr = UntaggedObject::ToAddr(target);
      ForwardingCorpse* forwarder = reinterpret_cast<ForwardingCorpse*>(addr);
      *handle->ptr_addr() = forwarder->target();
    }
  }

  void VisitTypedDataViewPointers(TypedDataViewPtr view,
                                  CompressedObjectPtr* first,
                                  CompressedObjectPtr* last) override {
    ObjectPtr old_backing = view->untag()->typed_data();
    VisitCompressedPointers(view->heap_base(), first, last);
    ObjectPtr new_backing = view->untag()->typed_data();

    const bool backing_moved = old_backing != new_backing;
    if (backing_moved) {
      typed_data_views_.Add(view);
    }
  }

  bool CanVisitSuspendStatePointers(SuspendStatePtr suspend_state) override {
    if ((suspend_state->untag()->pc() != 0) && !can_visit_stack_frames_) {
      // Visiting pointers of SuspendState objects with copied stack frame
      // needs to query stack map, which can touch other Dart objects
      // (such as GrowableObjectArray of InstructionsTable).
      // Those objects may have an inconsistent state during compaction,
      // so processing of SuspendState objects is postponed to the later
      // stage of compaction.
      suspend_states_.Add(suspend_state);
      return false;
    }
    return true;
  }

  void UpdateViews() {
    const intptr_t length = typed_data_views_.length();
    for (intptr_t i = 0; i < length; ++i) {
      auto raw_view = typed_data_views_[i];
      const classid_t cid = raw_view->untag()->typed_data()->GetClassId();
      // If we have external typed data we can simply return, since the backing
      // store lives in C-heap and will not move. Otherwise we have to update
      // the inner pointer.
      if (IsTypedDataClassId(cid)) {
        raw_view->untag()->RecomputeDataFieldForInternalTypedData();
      } else {
        ASSERT(IsExternalTypedDataClassId(cid));
      }
    }
  }

  void UpdateSuspendStates() {
    can_visit_stack_frames_ = true;
    const intptr_t length = suspend_states_.length();
    for (intptr_t i = 0; i < length; ++i) {
      auto suspend_state = suspend_states_[i];
      suspend_state->untag()->VisitPointers(this);
    }
  }

 private:
  bool can_visit_stack_frames_ = false;
  MallocGrowableArray<TypedDataViewPtr> typed_data_views_;
  MallocGrowableArray<SuspendStatePtr> suspend_states_;

  DISALLOW_COPY_AND_ASSIGN(IncrementalForwardingVisitor);
};

class StoreBufferForwardingVisitor : public ObjectPointerVisitor {
 public:
  StoreBufferForwardingVisitor(IsolateGroup* isolate_group,
                               IncrementalForwardingVisitor* visitor)
      : ObjectPointerVisitor(isolate_group), visitor_(visitor) {}

  void VisitPointers(ObjectPtr* first, ObjectPtr* last) override {
    for (ObjectPtr* ptr = first; ptr <= last; ptr++) {
      ObjectPtr obj = *ptr;
      ASSERT(!obj->IsImmediateOrNewObject());

      if (obj->IsForwardingCorpse()) {
        ASSERT(!obj->untag()->IsMarked());
        ASSERT(!obj->untag()->IsEvacuationCandidate());
        uword addr = UntaggedObject::ToAddr(obj);
        ForwardingCorpse* forwarder = reinterpret_cast<ForwardingCorpse*>(addr);
        obj = forwarder->target();
        *ptr = obj;
      } else {
        ASSERT(obj->untag()->IsMarked());
        ASSERT(!obj->untag()->IsEvacuationCandidate());
      }

      visitor_->VisitObject(obj);
    }
  }

#if defined(DART_COMPRESSED_POINTERS)
  void VisitCompressedPointers(uword heap_base,
                               CompressedObjectPtr* first,
                               CompressedObjectPtr* last) override {
    UNREACHABLE();  // Store buffer blocks are not compressed.
  }
#endif

 private:
  IncrementalForwardingVisitor* visitor_;

  DISALLOW_COPY_AND_ASSIGN(StoreBufferForwardingVisitor);
};

class EpilogueState {
 public:
  EpilogueState(Page* evac_page,
                StoreBufferBlock* block,
                Page* new_page,
                Mutex* pages_lock)
      : evac_page_(evac_page),
        block_(block),
        new_page_(new_page),
        pages_lock_(pages_lock) {}

  bool NextEvacPage(Page** page) {
    // Needs to be the old_space pages lock since evacuation may also allocate
    // new pages and race with page->next_.
    MutexLocker ml(pages_lock_);
    while (evac_page_ != nullptr) {
      Page* current = evac_page_;
      evac_page_ = current->next();
      if (current->is_evacuation_candidate()) {
        *page = current;
        return true;
      }
    }
    return false;
  }

  bool NextBlock(StoreBufferBlock** block) {
    MutexLocker ml(pages_lock_);
    if (block_ != nullptr) {
      StoreBufferBlock* current = block_;
      block_ = current->next();
      current->set_next(nullptr);
      *block = current;
      return true;
    }
    return false;
  }

  bool NextNewPage(Page** page) {
    MutexLocker ml(pages_lock_);
    if (new_page_ != nullptr) {
      Page* current = new_page_;
      new_page_ = current->next();
      *page = current;
      return true;
    }
    return false;
  }

  bool TakeOOM() { return oom_slice_.exchange(false); }
  bool TakeWeakHandles() { return weak_handles_slice_.exchange(false); }
  bool TakeWeakTables() { return weak_tables_slice_.exchange(false); }
  bool TakeRoots() { return roots_slice_.exchange(false); }
  bool TakeResetProgressBars() {
    return reset_progress_bars_slice_.exchange(false);
  }

  void AddNewFreeSize(intptr_t size) { new_free_size_ += size; }
  intptr_t NewFreeSize() { return new_free_size_; }

 private:
  Page* evac_page_;
  StoreBufferBlock* block_;
  Page* new_page_;
  Mutex* pages_lock_;

  RelaxedAtomic<bool> oom_slice_ = {true};
  RelaxedAtomic<bool> weak_handles_slice_ = {true};
  RelaxedAtomic<bool> weak_tables_slice_ = {true};
  RelaxedAtomic<bool> id_ring_slice_ = {true};
  RelaxedAtomic<bool> roots_slice_ = {true};
  RelaxedAtomic<bool> reset_progress_bars_slice_ = {true};
  RelaxedAtomic<intptr_t> new_free_size_ = {0};
};

class EpilogueTask : public SafepointTask {
 public:
  EpilogueTask(ThreadBarrier* barrier,
               IsolateGroup* isolate_group,
               PageSpace* old_space,
               FreeList* freelist,
               EpilogueState* state)
      : SafepointTask(isolate_group,
                      barrier,
                      Thread::kIncrementalCompactorTask),
        old_space_(old_space),
        freelist_(freelist),
        state_(state) {}

  void RunEnteredIsolateGroup() override {
    Thread* thread = Thread::Current();

    Evacuate();

    barrier_->Sync();

    IncrementalForwardingVisitor visitor(thread);
    if (state_->TakeOOM()) {
      old_space_->VisitRoots(&visitor);  // OOM reservation.
    }
    ForwardStoreBuffer(&visitor);
    ForwardRememberedCards(&visitor);
    ForwardNewSpace(&visitor);
    if (state_->TakeWeakHandles()) {
      TIMELINE_FUNCTION_GC_DURATION(thread, "WeakPersistentHandles");
      isolate_group_->VisitWeakPersistentHandles(&visitor);
    }
    if (state_->TakeWeakTables()) {
      TIMELINE_FUNCTION_GC_DURATION(thread, "WeakTables");
      isolate_group_->heap()->ForwardWeakTables(&visitor);
    }

    barrier_->Sync();

    {
      // After forwarding the heap because visits each view's underyling buffer.
      TIMELINE_FUNCTION_GC_DURATION(thread, "Views");
      visitor.UpdateViews();
    }

    if (state_->TakeRoots()) {
      // After forwarding the heap because visiting the stack requires stackmaps
      // to already be forwarded.
      TIMELINE_FUNCTION_GC_DURATION(thread, "Roots");
      isolate_group_->VisitObjectPointers(
          &visitor, ValidationPolicy::kDontValidateFrames);
    }

    barrier_->Sync();

    {
      // After processing the object store because of the dependency on
      // canonicalized_stack_map_entries.
      TIMELINE_FUNCTION_GC_DURATION(thread, "SuspendStates");
      visitor.UpdateSuspendStates();
    }

    if (state_->TakeResetProgressBars()) {
      // After ForwardRememberedCards.
      old_space_->ResetProgressBars();
    }
  }

  void Evacuate() {
    TIMELINE_FUNCTION_GC_DURATION(Thread::Current(), "Evacuate");

    old_space_->AcquireLock(freelist_);

    bool any_failed = false;
    intptr_t bytes_evacuated = 0;
    Page* page;
    while (state_->NextEvacPage(&page)) {
      ASSERT(page->is_evacuation_candidate());

      bool page_failed = false;
      uword start = page->object_start();
      uword end = page->object_end();
      uword current = start;
      while (current < end) {
        ObjectPtr obj = UntaggedObject::FromAddr(current);
        intptr_t size = obj->untag()->HeapSize();

        if (obj->untag()->IsMarked()) {
          uword copied = old_space_->TryAllocatePromoLocked(freelist_, size);
          if (copied == 0) {
            obj->untag()->ClearIsEvacuationCandidateUnsynchronized();
            page_failed = true;
            any_failed = true;
          } else {
            ASSERT(!Page::Of(copied)->is_evacuation_candidate());
            bytes_evacuated += size;
            objcpy(reinterpret_cast<void*>(copied),
                   reinterpret_cast<const void*>(current), size);
            ObjectPtr copied_obj = UntaggedObject::FromAddr(copied);

            copied_obj->untag()->ClearIsEvacuationCandidateUnsynchronized();
            if (IsTypedDataClassId(copied_obj->GetClassIdOfHeapObject())) {
              static_cast<TypedDataPtr>(copied_obj)
                  ->untag()
                  ->RecomputeDataField();
            }

            ForwardingCorpse::AsForwarder(current, size)
                ->set_target(copied_obj);
          }
        }

        current += size;
      }

      if (page_failed) {
        page->set_evacuation_candidate(false);
      }
    }

    old_space_->ReleaseLock(freelist_);
    old_space_->usage_.used_in_words -= (bytes_evacuated >> kWordSizeLog2);
#if defined(SUPPORT_TIMELINE)
    tbes.SetNumArguments(1);
    tbes.FormatArgument(0, "bytes_evacuated", "%" Pd, bytes_evacuated);
#endif

    if (any_failed) {
      OS::PrintErr("evacuation failed\n");
    }
  }

  void ForwardStoreBuffer(IncrementalForwardingVisitor* visitor) {
    TIMELINE_FUNCTION_GC_DURATION(Thread::Current(), "ForwardStoreBuffer");

    StoreBufferForwardingVisitor store_visitor(isolate_group_, visitor);
    StoreBuffer* store_buffer = isolate_group_->store_buffer();
    StoreBufferBlock* block;
    while (state_->NextBlock(&block)) {
      // Generated code appends to store buffers; tell MemorySanitizer.
      MSAN_UNPOISON(block, sizeof(*block));

      block->VisitObjectPointers(&store_visitor);

      store_buffer->PushBlock(block, StoreBuffer::kIgnoreThreshold);
    }
  }

  void ForwardRememberedCards(IncrementalForwardingVisitor* visitor) {
    TIMELINE_FUNCTION_GC_DURATION(Thread::Current(), "ForwardRememberedCards");
    for (Page* page = old_space_->large_pages_; page != nullptr;
         page = page->next()) {
      page->VisitRememberedCards(visitor, /*only_marked*/ true);
    }
  }

  void ForwardNewSpace(IncrementalForwardingVisitor* visitor) {
    TIMELINE_FUNCTION_GC_DURATION(Thread::Current(), "ForwardNewSpace");
    Page* page;
    while (state_->NextNewPage(&page)) {
      intptr_t free = ForwardAndSweepNewPage(visitor, page);
      state_->AddNewFreeSize(free);
    }
  }

  DART_NOINLINE
  intptr_t ForwardAndSweepNewPage(IncrementalForwardingVisitor* visitor,
                                  Page* page) {
    ASSERT(!page->is_image());
    ASSERT(!page->is_old());
    ASSERT(!page->is_executable());

    uword start = page->object_start();
    uword end = page->object_end();
    uword current = start;
    intptr_t free = 0;
    while (current < end) {
      ObjectPtr raw_obj = UntaggedObject::FromAddr(current);
      ASSERT(Page::Of(raw_obj) == page);
      uword tags = raw_obj->untag()->tags();
      intptr_t obj_size = raw_obj->untag()->HeapSize(tags);
      if (UntaggedObject::IsMarked(tags)) {
        raw_obj->untag()->ClearMarkBitUnsynchronized();
        ASSERT(Heap::IsAllocatableInNewSpace(obj_size));
        raw_obj->untag()->VisitPointers(visitor);
      } else {
        uword free_end = current + obj_size;
        while (free_end < end) {
          ObjectPtr next_obj = UntaggedObject::FromAddr(free_end);
          tags = next_obj->untag()->tags();
          if (UntaggedObject::IsMarked(tags)) {
            // Reached the end of the free block.
            break;
          }
          // Expand the free block by the size of this object.
          free_end += next_obj->untag()->HeapSize(tags);
        }
        obj_size = free_end - current;
#if defined(DEBUG)
        memset(reinterpret_cast<void*>(current), Heap::kZapByte, obj_size);
#endif  // DEBUG
        FreeListElement::AsElementNew(current, obj_size);
        free += obj_size;
      }
      current += obj_size;
    }
    return free;
  }

 private:
  PageSpace* old_space_;
  FreeList* freelist_;
  EpilogueState* state_;
};

void GCIncrementalCompactor::Evacuate(PageSpace* old_space) {
  IsolateGroup* isolate_group = IsolateGroup::Current();
  isolate_group->ReleaseStoreBuffers();
  EpilogueState state(
      old_space->pages_, isolate_group->store_buffer()->PopAll(),
      old_space->heap_->new_space()->head(), &old_space->pages_lock_);

  // This must use NumScavengeWorkers because that determines the number of
  // freelists available for workers.
  const intptr_t num_tasks =
      isolate_group->heap()->new_space()->NumScavengeWorkers();
  RELEASE_ASSERT(num_tasks > 0);
  ThreadBarrier* barrier = new ThreadBarrier(num_tasks, /*initial=*/1);
  IntrusiveDList<SafepointTask> tasks;
  for (intptr_t i = 0; i < num_tasks; i++) {
    tasks.Append(new EpilogueTask(barrier, isolate_group, old_space,
                                  old_space->DataFreeList(i), &state));
  }
  isolate_group->safepoint_handler()->RunTasks(&tasks);

  old_space->heap_->new_space()->set_freed_in_words(state.NewFreeSize() >>
                                                    kWordSizeLog2);
}

void GCIncrementalCompactor::CheckPostEvacuate(PageSpace* old_space) {
  if (!FLAG_verify_after_gc) return;

  TIMELINE_FUNCTION_GC_DURATION(Thread::Current(), "CheckPostEvacuate");

  // Check there are no remaining evac candidates
  for (Page* page = old_space->pages_; page != nullptr; page = page->next()) {
    uword start = page->object_start();
    uword end = page->object_end();
    uword current = start;
    while (current < end) {
      ObjectPtr obj = UntaggedObject::FromAddr(current);
      intptr_t size = obj->untag()->HeapSize();
      ASSERT(!obj->untag()->IsEvacuationCandidate() ||
             !obj->untag()->IsMarked());
      current += size;
    }
  }
}

void GCIncrementalCompactor::FreeEvacuatedPages(PageSpace* old_space) {
  Page* prev_page = nullptr;
  Page* page = old_space->pages_;
  while (page != nullptr) {
    Page* next_page = page->next();
    if (page->is_evacuation_candidate()) {
      old_space->FreePage(page, prev_page);
    } else {
      prev_page = page;
    }
    page = next_page;
  }
}

class VerifyAfterIncrementalCompactionVisitor : public ObjectVisitor,
                                                public ObjectPointerVisitor {
 public:
  VerifyAfterIncrementalCompactionVisitor()
      : ObjectVisitor(), ObjectPointerVisitor(IsolateGroup::Current()) {}

  void VisitObject(ObjectPtr obj) override {
    // New-space has been swept, but old-space has not.
    if (obj->IsNewObject()) {
      if (obj->untag()->GetClassId() != kFreeListElement) {
        current_ = obj;
        obj->untag()->VisitPointers(this);
      }
    } else {
      if (obj->untag()->IsMarked()) {
        current_ = obj;
        obj->untag()->VisitPointers(this);
      }
    }
  }

  void VisitPointers(ObjectPtr* from, ObjectPtr* to) override {
    for (ObjectPtr* ptr = from; ptr <= to; ptr++) {
      ObjectPtr obj = *ptr;
      if (!obj->IsHeapObject()) continue;
      if (obj->IsForwardingCorpse() || obj->IsFreeListElement() ||
          (obj->IsOldObject() && !obj->untag()->IsMarked())) {
        OS::PrintErr("object=0x%" Px ", slot=0x%" Px ", value=0x%" Px "\n",
                     static_cast<uword>(current_), reinterpret_cast<uword>(ptr),
                     static_cast<uword>(obj));
        failed_ = true;
      }
    }
  }

#if defined(DART_COMPRESSED_POINTERS)
  void VisitCompressedPointers(uword heap_base,
                               CompressedObjectPtr* from,
                               CompressedObjectPtr* to) override {
    for (CompressedObjectPtr* ptr = from; ptr <= to; ptr++) {
      ObjectPtr obj = ptr->Decompress(heap_base);
      if (!obj->IsHeapObject()) continue;
      if (obj->IsForwardingCorpse() || obj->IsFreeListElement() ||
          (obj->IsOldObject() && !obj->untag()->IsMarked())) {
        OS::PrintErr("object=0x%" Px ", slot=0x%" Px ", value=0x%" Px "\n",
                     static_cast<uword>(current_), reinterpret_cast<uword>(ptr),
                     static_cast<uword>(obj));
        failed_ = true;
      }
    }
  }
#endif

  bool failed() const { return failed_; }

 private:
  ObjectPtr current_;
  bool failed_ = false;

  DISALLOW_COPY_AND_ASSIGN(VerifyAfterIncrementalCompactionVisitor);
};

void GCIncrementalCompactor::VerifyAfterIncrementalCompaction(
    PageSpace* old_space) {
  if (!FLAG_verify_after_gc) return;
  TIMELINE_FUNCTION_GC_DURATION(Thread::Current(),
                                "VerifyAfterIncrementalCompaction");
  VerifyAfterIncrementalCompactionVisitor visitor;
  old_space->heap_->VisitObjects(&visitor);
  if (visitor.failed()) {
    FATAL("verify after incremental compact");
  }
}

}  // namespace dart
