#include "src/heap/cppgc/compactor.h"
#include <map>
#include <numeric>
#include <unordered_map>
#include <unordered_set>
#include "include/cppgc/macros.h"
#include "src/heap/cppgc/compaction-worklists.h"
#include "src/heap/cppgc/globals.h"
#include "src/heap/cppgc/heap-base.h"
#include "src/heap/cppgc/heap-page.h"
#include "src/heap/cppgc/heap-space.h"
#include "src/heap/cppgc/memory.h"
#include "src/heap/cppgc/object-poisoner.h"
#include "src/heap/cppgc/raw-heap.h"
#include "src/heap/cppgc/stats-collector.h"
namespace cppgc {
namespace internal {
namespace {
static constexpr size_t kFreeListSizeThreshold = 512 * kKB;
class MovableReferences final {
using MovableReference = CompactionWorklists::MovableReference;
public:
explicit MovableReferences(HeapBase& heap)
: heap_(heap), heap_has_move_listeners_(heap.HasMoveListeners()) {}
void AddOrFilter(MovableReference*);
void Relocate(Address from, Address to, size_t size_including_header);
void RelocateInteriorReferences(Address from, Address to, size_t size);
void UpdateCallbacks();
private:
HeapBase& heap_;
std::unordered_map<MovableReference, MovableReference*> movable_references_;
std::map<MovableReference*, Address> interior_movable_references_;
const bool heap_has_move_listeners_;
#if DEBUG
std::unordered_set<const void*> moved_objects_;
std::unordered_map<MovableReference*, MovableReference>
interior_slot_to_object_;
#endif
};
void MovableReferences::AddOrFilter(MovableReference* slot) {
const BasePage* slot_page = BasePage::FromInnerAddress(&heap_, slot);
CHECK_NOT_NULL(slot_page);
const void* value = *slot;
if (!value) return;
const HeapObjectHeader& slot_header =
slot_page->ObjectHeaderFromInnerAddress(slot);
if (!slot_header.IsMarked()) return;
const BasePage* value_page = BasePage::FromInnerAddress(&heap_, value);
CHECK_NOT_NULL(value_page);
if (value_page->is_large() || !value_page->space().is_compactable()) return;
const HeapObjectHeader& value_header =
value_page->ObjectHeaderFromInnerAddress(value);
CHECK(value_header.IsMarked());
auto reference_it = movable_references_.find(value);
if (V8_UNLIKELY(reference_it != movable_references_.end())) {
CHECK_EQ(slot, reference_it->second);
return;
}
movable_references_.emplace(value, slot);
if (V8_LIKELY(!slot_page->space().is_compactable())) return;
CHECK_EQ(interior_movable_references_.end(),
interior_movable_references_.find(slot));
interior_movable_references_.emplace(slot, nullptr);
#if DEBUG
interior_slot_to_object_.emplace(slot, slot_header.ObjectStart());
#endif
}
void MovableReferences::Relocate(Address from, Address to,
size_t size_including_header) {
#if DEBUG
moved_objects_.insert(from);
#endif
if (V8_UNLIKELY(heap_has_move_listeners_)) {
heap_.CallMoveListeners(from - sizeof(HeapObjectHeader),
to - sizeof(HeapObjectHeader),
size_including_header);
}
if (!interior_movable_references_.empty()) {
const HeapObjectHeader& header = HeapObjectHeader::FromObject(to);
const size_t size = header.ObjectSize();
RelocateInteriorReferences(from, to, size);
}
auto it = movable_references_.find(from);
if (it == movable_references_.end()) {
return;
}
MovableReference* slot = it->second;
auto interior_it = interior_movable_references_.find(slot);
if (interior_it != interior_movable_references_.end()) {
MovableReference* slot_location =
reinterpret_cast<MovableReference*>(interior_it->second);
if (!slot_location) {
interior_it->second = to;
#if DEBUG
auto reverse_it = interior_slot_to_object_.find(slot);
DCHECK_NE(interior_slot_to_object_.end(), reverse_it);
DCHECK_EQ(moved_objects_.end(), moved_objects_.find(reverse_it->second));
#endif
} else {
slot = slot_location;
}
}
DCHECK_EQ(from, *slot);
*slot = to;
}
void MovableReferences::RelocateInteriorReferences(Address from, Address to,
size_t size) {
auto interior_it = interior_movable_references_.lower_bound(
reinterpret_cast<MovableReference*>(from));
if (interior_it == interior_movable_references_.end()) return;
DCHECK_GE(reinterpret_cast<Address>(interior_it->first), from);
size_t offset = reinterpret_cast<Address>(interior_it->first) - from;
while (offset < size) {
if (!interior_it->second) {
Address reference = to + offset;
interior_it->second = reference;
Address& reference_contents = *reinterpret_cast<Address*>(reference);
if (reference_contents > from && reference_contents < (from + size)) {
reference_contents = reference_contents - from + to;
}
}
interior_it++;
if (interior_it == interior_movable_references_.end()) return;
offset = reinterpret_cast<Address>(interior_it->first) - from;
}
}
class CompactionState final {
CPPGC_STACK_ALLOCATED();
using Pages = std::vector<NormalPage*>;
public:
CompactionState(NormalPageSpace* space, MovableReferences& movable_references)
: space_(space), movable_references_(movable_references) {}
void AddPage(NormalPage* page) {
DCHECK_EQ(space_, &page->space());
if (!current_page_)
current_page_ = page;
else
available_pages_.push_back(page);
}
void RelocateObject(const NormalPage* page, const Address header,
size_t size) {
Address compact_frontier =
current_page_->PayloadStart() + used_bytes_in_current_page_;
if (compact_frontier + size > current_page_->PayloadEnd()) {
ReturnCurrentPageToSpace();
current_page_ = available_pages_.back();
available_pages_.pop_back();
used_bytes_in_current_page_ = 0;
compact_frontier = current_page_->PayloadStart();
}
if (V8_LIKELY(compact_frontier != header)) {
if (current_page_ == page)
memmove(compact_frontier, header, size);
else
memcpy(compact_frontier, header, size);
movable_references_.Relocate(header + sizeof(HeapObjectHeader),
compact_frontier + sizeof(HeapObjectHeader),
size);
}
current_page_->object_start_bitmap().SetBit(compact_frontier);
used_bytes_in_current_page_ += size;
DCHECK_LE(used_bytes_in_current_page_, current_page_->PayloadSize());
}
void FinishCompactingSpace() {
if (used_bytes_in_current_page_ == 0) {
available_pages_.push_back(current_page_);
} else {
ReturnCurrentPageToSpace();
}
for (NormalPage* page : available_pages_) {
SetMemoryInaccessible(page->PayloadStart(), page->PayloadSize());
NormalPage::Destroy(page);
}
}
void FinishCompactingPage(NormalPage* page) {
#if DEBUG || defined(V8_USE_MEMORY_SANITIZER) || \
defined(V8_USE_ADDRESS_SANITIZER)
if (current_page_ != page) {
ZapMemory(page->PayloadStart(), page->PayloadSize());
} else {
ZapMemory(page->PayloadStart() + used_bytes_in_current_page_,
page->PayloadSize() - used_bytes_in_current_page_);
}
#endif
page->object_start_bitmap().MarkAsFullyPopulated();
}
private:
void ReturnCurrentPageToSpace() {
DCHECK_EQ(space_, ¤t_page_->space());
space_->AddPage(current_page_);
if (used_bytes_in_current_page_ != current_page_->PayloadSize()) {
size_t freed_size =
current_page_->PayloadSize() - used_bytes_in_current_page_;
Address payload = current_page_->PayloadStart();
Address free_start = payload + used_bytes_in_current_page_;
SetMemoryInaccessible(free_start, freed_size);
space_->free_list().Add({free_start, freed_size});
current_page_->object_start_bitmap().SetBit(free_start);
}
}
NormalPageSpace* space_;
MovableReferences& movable_references_;
NormalPage* current_page_ = nullptr;
size_t used_bytes_in_current_page_ = 0;
Pages available_pages_;
};
void CompactPage(NormalPage* page, CompactionState& compaction_state,
StickyBits sticky_bits) {
compaction_state.AddPage(page);
page->object_start_bitmap().Clear();
for (Address header_address = page->PayloadStart();
header_address < page->PayloadEnd();) {
HeapObjectHeader* header =
reinterpret_cast<HeapObjectHeader*>(header_address);
size_t size = header->AllocatedSize();
DCHECK_GT(size, 0u);
DCHECK_LT(size, kPageSize);
if (header->IsFree()) {
ASAN_UNPOISON_MEMORY_REGION(header_address, size);
header_address += size;
continue;
}
if (!header->IsMarked()) {
header->Finalize();
#if DEBUG || defined(V8_USE_MEMORY_SANITIZER) || \
defined(V8_USE_ADDRESS_SANITIZER)
ZapMemory(header, size);
#endif
header_address += size;
continue;
}
#if defined(CPPGC_YOUNG_GENERATION)
if (sticky_bits == StickyBits::kDisabled) header->Unmark();
#else
header->Unmark();
#endif
ASAN_UNPOISON_MEMORY_REGION(header->ObjectStart(), header->ObjectSize());
compaction_state.RelocateObject(page, header_address, size);
header_address += size;
}
compaction_state.FinishCompactingPage(page);
}
void CompactSpace(NormalPageSpace* space, MovableReferences& movable_references,
StickyBits sticky_bits) {
using Pages = NormalPageSpace::Pages;
#ifdef V8_USE_ADDRESS_SANITIZER
UnmarkedObjectsPoisoner().Traverse(*space);
#endif
DCHECK(space->is_compactable());
space->free_list().Clear();
Pages pages = space->RemoveAllPages();
if (pages.empty()) return;
CompactionState compaction_state(space, movable_references);
for (BasePage* page : pages) {
page->ResetMarkedBytes();
CompactPage(NormalPage::From(page), compaction_state, sticky_bits);
}
compaction_state.FinishCompactingSpace();
}
size_t UpdateHeapResidency(const std::vector<NormalPageSpace*>& spaces) {
return std::accumulate(spaces.cbegin(), spaces.cend(), 0u,
[](size_t acc, const NormalPageSpace* space) {
DCHECK(space->is_compactable());
if (!space->size()) return acc;
return acc + space->free_list().Size();
});
}
}
Compactor::Compactor(RawHeap& heap) : heap_(heap) {
for (auto& space : heap_) {
if (!space->is_compactable()) continue;
DCHECK_EQ(&heap, space->raw_heap());
compactable_spaces_.push_back(static_cast<NormalPageSpace*>(space.get()));
}
}
bool Compactor::ShouldCompact(GCConfig::MarkingType marking_type,
StackState stack_state) const {
if (compactable_spaces_.empty() ||
(marking_type == GCConfig::MarkingType::kAtomic &&
stack_state == StackState::kMayContainHeapPointers)) {
DCHECK(!enable_for_next_gc_for_testing_);
return false;
}
if (enable_for_next_gc_for_testing_) {
return true;
}
size_t free_list_size = UpdateHeapResidency(compactable_spaces_);
return free_list_size > kFreeListSizeThreshold;
}
void Compactor::InitializeIfShouldCompact(GCConfig::MarkingType marking_type,
StackState stack_state) {
DCHECK(!is_enabled_);
if (!ShouldCompact(marking_type, stack_state)) return;
compaction_worklists_ = std::make_unique<CompactionWorklists>();
is_enabled_ = true;
is_cancelled_ = false;
}
void Compactor::CancelIfShouldNotCompact(GCConfig::MarkingType marking_type,
StackState stack_state) {
if (!is_enabled_ || ShouldCompact(marking_type, stack_state)) return;
is_cancelled_ = true;
is_enabled_ = false;
}
Compactor::CompactableSpaceHandling Compactor::CompactSpacesIfEnabled() {
if (is_cancelled_ && compaction_worklists_) {
compaction_worklists_->movable_slots_worklist()->Clear();
compaction_worklists_.reset();
}
if (!is_enabled_) return CompactableSpaceHandling::kSweep;
StatsCollector::EnabledScope stats_scope(heap_.heap()->stats_collector(),
StatsCollector::kAtomicCompact);
MovableReferences movable_references(*heap_.heap());
CompactionWorklists::MovableReferencesWorklist::Local local(
*compaction_worklists_->movable_slots_worklist());
CompactionWorklists::MovableReference* slot;
while (local.Pop(&slot)) {
movable_references.AddOrFilter(slot);
}
compaction_worklists_.reset();
const StickyBits sticky_bits = heap_.heap()->sticky_bits();
for (NormalPageSpace* space : compactable_spaces_) {
CompactSpace(space, movable_references, sticky_bits);
}
enable_for_next_gc_for_testing_ = false;
is_enabled_ = false;
return CompactableSpaceHandling::kIgnore;
}
void Compactor::EnableForNextGCForTesting() {
DCHECK_NULL(heap_.heap()->marker());
enable_for_next_gc_for_testing_ = true;
}
}
}