// Copyright 2009-2010 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/profiler/heap-profiler.h"

#include <fstream>
#include <optional>
#include <utility>

#include "include/v8-profiler.h"
#include "src/api/api-inl.h"
#include "src/base/platform/platform.h"
#include "src/debug/debug.h"
#include "src/heap/combined-heap.h"
#include "src/heap/heap-inl.h"
#include "src/heap/heap-layout-inl.h"
#include "src/heap/heap.h"
#include "src/objects/cpp-heap-object-wrapper-inl.h"
#include "src/objects/js-array-buffer-inl.h"
#include "src/profiler/allocation-tracker.h"
#include "src/profiler/heap-snapshot-generator-inl.h"
#include "src/profiler/sampling-heap-profiler.h"
#include "src/utils/output-stream.h"

namespace v8::internal {

HeapProfiler::HeapProfiler(Heap* heap)
    : ids_(new HeapObjectsMap(heap)),
      names_(new StringsStorage()),
      is_tracking_object_moves_(false),
      is_taking_snapshot_(false) {}

HeapProfiler::~HeapProfiler() = default;

void HeapProfiler::DeleteAllSnapshots() {
  snapshots_.clear();
  MaybeClearStringsStorage();
}

void HeapProfiler::MaybeClearStringsStorage() {
  if (snapshots_.empty() && !sampling_heap_profiler_ && !allocation_tracker_ &&
      !is_taking_snapshot_) {
    names_.reset(new StringsStorage());
  }
}

void HeapProfiler::RemoveSnapshot(HeapSnapshot* snapshot) {
  snapshots_.erase(
      std::find_if(snapshots_.begin(), snapshots_.end(),
                   [&](const std::unique_ptr<HeapSnapshot>& entry) {
                     return entry.get() == snapshot;
                   }));
}

std::vector<v8::Local<v8::Value>> HeapProfiler::GetDetachedJSWrapperObjects() {
  heap()->CollectAllAvailableGarbage(GarbageCollectionReason::kHeapProfiler);

  std::vector<v8::Local<v8::Value>> js_objects_found;
  HeapObjectIterator iterator(heap());
  for (Tagged<HeapObject> obj = iterator.Next(); !obj.is_null();
       obj = iterator.Next()) {
    if (TrustedHeapLayout::InCodeSpace(obj)) continue;
    if (!IsJSApiWrapperObject(obj)) continue;
    // Ensure object is wrappable, otherwise GetDetachedness() can crash
    CppHeapObjectWrapper wrapper = CppHeapObjectWrapper(Cast<JSObject>(obj));
    if (!wrapper.GetCppHeapWrappable(isolate(), kAnyCppHeapPointer)) continue;

    v8::Local<v8::Value> data(
        Utils::ToLocal(direct_handle(Cast<JSObject>(obj), isolate())));
    v8::EmbedderGraph::Node::Detachedness detachedness =
        GetDetachedness(data, 0);

    if (detachedness != v8::EmbedderGraph::Node::Detachedness::kDetached)
      continue;

    js_objects_found.push_back(data);
  }

  return js_objects_found;
}

void HeapProfiler::AddBuildEmbedderGraphCallback(
    v8::HeapProfiler::BuildEmbedderGraphCallback callback, void* data) {
  build_embedder_graph_callbacks_.push_back({callback, data});
}

void HeapProfiler::RemoveBuildEmbedderGraphCallback(
    v8::HeapProfiler::BuildEmbedderGraphCallback callback, void* data) {
  auto it = std::find(build_embedder_graph_callbacks_.begin(),
                      build_embedder_graph_callbacks_.end(),
                      std::make_pair(callback, data));
  if (it != build_embedder_graph_callbacks_.end())
    build_embedder_graph_callbacks_.erase(it);
}

void HeapProfiler::BuildEmbedderGraph(
    Isolate* isolate, v8::EmbedderGraph* graph,
    UnorderedCppHeapExternalObjectSet&& cpp_heap_external_objects) {
  if (internal_build_embedder_graph_callback_.first) {
    internal_build_embedder_graph_callback_.first(
        reinterpret_cast<v8::Isolate*>(isolate), graph,
        internal_build_embedder_graph_callback_.second,
        std::move(cpp_heap_external_objects));
  }
  for (const auto& cb : build_embedder_graph_callbacks_) {
    cb.first(reinterpret_cast<v8::Isolate*>(isolate), graph, cb.second);
  }
}

void HeapProfiler::SetInternalBuildEmbedderGraphCallback(
    InternalBuildEmbedderGraphCallback callback, void* data) {
  internal_build_embedder_graph_callback_ = {callback, data};
}

void HeapProfiler::SetGetDetachednessCallback(
    v8::HeapProfiler::GetDetachednessCallback callback, void* data) {
  get_detachedness_callback_ = {callback, data};
}

v8::EmbedderGraph::Node::Detachedness HeapProfiler::GetDetachedness(
    const v8::Local<v8::Value> v8_value, uint16_t class_id) {
  DCHECK(HasGetDetachednessCallback());
  return get_detachedness_callback_.first(
      reinterpret_cast<v8::Isolate*>(heap()->isolate()), v8_value, class_id,
      get_detachedness_callback_.second);
}

const char* HeapProfiler::CopyNameForHeapSnapshot(const char* name) {
  CHECK(is_taking_snapshot_);
  return names_->GetCopy(name);
}

HeapSnapshot* HeapProfiler::TakeSnapshot(
    const v8::HeapProfiler::HeapSnapshotOptions options) {
  is_taking_snapshot_ = true;
  HeapSnapshot* result =
      new HeapSnapshot(this, options.snapshot_mode, options.numerics_mode);

  // We need a stack marker here to allow deterministic passes over the stack.
  // The garbage collection and the filling of references in GenerateSnapshot
  // should scan the same part of the stack.
  heap()->stack().SetMarkerIfNeededAndCallback([this, &options, &result]() {
    std::optional<CppClassNamesAsHeapObjectNameScope> use_cpp_class_name;
    if (result->expose_internals() && heap()->cpp_heap()) {
      use_cpp_class_name.emplace(heap()->cpp_heap());
    }

    // Allow usages of v8::HeapProfiler::ObjectNameResolver for now.
    // TODO(https://crbug.com/333672197): remove.
    START_ALLOW_USE_DEPRECATED()
    HeapSnapshotGenerator generator(
        result, options.control, options.global_object_name_resolver,
        options.context_name_resolver, heap(), options.stack_state);
    END_ALLOW_USE_DEPRECATED()
    if (!generator.GenerateSnapshot()) {
      delete result;
      result = nullptr;
    } else {
      snapshots_.emplace_back(result);
    }
  });
  ids_->RemoveDeadEntries();
  if (native_move_listener_) {
    native_move_listener_->StartListening();
  }
  is_tracking_object_moves_ = true;
  heap()->isolate()->UpdateLogObjectRelocation();
  is_taking_snapshot_ = false;

  return result;
}

// Precondition: only call this if you have just completed a full GC cycle.
void HeapProfiler::WriteSnapshotToDiskAfterGC(HeapSnapshotMode snapshot_mode) {
  // We need to set a stack marker for the stack walk performed by the
  // snapshot generator to work.
  heap()->stack().SetMarkerIfNeededAndCallback([this, snapshot_mode]() {
    int64_t time = V8::GetCurrentPlatform()->CurrentClockTimeMilliseconds();
    int pid = base::OS::GetCurrentProcessId();
    std::string filename = "v8-heap-" + std::to_string(time) + "-" +
                           std::to_string(pid) + ".heapsnapshot";
    if (v8_flags.heap_snapshot_path.value()) {
      filename = v8_flags.heap_snapshot_path.value();
    }
    v8::HeapProfiler::HeapSnapshotOptions options;
    std::unique_ptr<HeapSnapshot> result(
        new HeapSnapshot(this, snapshot_mode, options.numerics_mode));
    // Allow usages of v8::HeapProfiler::ObjectNameResolver for now.
    // TODO(https://crbug.com/333672197): remove.
    START_ALLOW_USE_DEPRECATED()
    HeapSnapshotGenerator generator(
        result.get(), options.control, options.global_object_name_resolver,
        options.context_name_resolver, heap(), options.stack_state);
    END_ALLOW_USE_DEPRECATED()
    if (!generator.GenerateSnapshotAfterGC()) return;
    i::FileOutputStream stream(filename.c_str());
    if (stream.IsOpen()) {
      HeapSnapshotJSONSerializer serializer(result.get());
      serializer.Serialize(&stream);
      PrintF("Wrote heap snapshot to %s.\n", filename.c_str());
    } else {
      PrintF("Failed to open heap snapshot file '%s' for writing.\n",
             filename.c_str());
    }
  });
}

void HeapProfiler::TakeSnapshotToFile(
    const v8::HeapProfiler::HeapSnapshotOptions options, std::string filename) {
  HeapSnapshot* snapshot = TakeSnapshot(options);
  if (!snapshot) return;
  FileOutputStream stream(filename.c_str());
  if (stream.IsOpen()) {
    HeapSnapshotJSONSerializer serializer(snapshot);
    serializer.Serialize(&stream);
  } else {
    RemoveSnapshot(snapshot);
  }
}

std::string HeapProfiler::TakeSnapshotToString(
    const v8::HeapProfiler::HeapSnapshotOptions options) {
  HeapSnapshot* snapshot = TakeSnapshot(options);
  StringOutputStream stream;
  HeapSnapshotJSONSerializer serializer(snapshot);
  serializer.Serialize(&stream);
  return stream.str();
}

bool HeapProfiler::StartSamplingHeapProfiler(
    uint64_t sample_interval, int stack_depth,
    v8::HeapProfiler::SamplingFlags flags) {
  if (sampling_heap_profiler_) return false;
  sampling_heap_profiler_.reset(new SamplingHeapProfiler(
      heap(), names_.get(), sample_interval, stack_depth, flags));
  return true;
}

void HeapProfiler::StopSamplingHeapProfiler() {
  sampling_heap_profiler_.reset();
  MaybeClearStringsStorage();
}

v8::AllocationProfile* HeapProfiler::GetAllocationProfile() {
  if (sampling_heap_profiler_) {
    return sampling_heap_profiler_->GetAllocationProfile();
  } else {
    return nullptr;
  }
}

void HeapProfiler::StartHeapObjectsTracking(bool track_allocations) {
  ids_->UpdateHeapObjectsMap();
  if (native_move_listener_) {
    native_move_listener_->StartListening();
  }
  is_tracking_object_moves_ = true;
  heap()->isolate()->UpdateLogObjectRelocation();
  DCHECK(!allocation_tracker_);
  if (track_allocations) {
    allocation_tracker_.reset(new AllocationTracker(ids_.get(), names_.get()));
    heap()->AddHeapObjectAllocationTracker(this);
  }
}

SnapshotObjectId HeapProfiler::PushHeapObjectsStats(OutputStream* stream,
                                                    int64_t* timestamp_us) {
  return ids_->PushHeapObjectsStats(stream, timestamp_us);
}

void HeapProfiler::StopHeapObjectsTracking() {
  ids_->StopHeapObjectsTracking();
  if (allocation_tracker_) {
    allocation_tracker_.reset();
    MaybeClearStringsStorage();
    heap()->RemoveHeapObjectAllocationTracker(this);
  }
}

int HeapProfiler::GetSnapshotsCount() const {
  return static_cast<int>(snapshots_.size());
}

bool HeapProfiler::IsTakingSnapshot() const { return is_taking_snapshot_; }

HeapSnapshot* HeapProfiler::GetSnapshot(int index) {
  return snapshots_.at(index).get();
}

SnapshotObjectId HeapProfiler::GetSnapshotObjectId(DirectHandle<Object> obj) {
  if (!IsHeapObject(*obj)) return v8::HeapProfiler::kUnknownObjectId;
  return ids_->FindEntry(Cast<HeapObject>(*obj).address());
}

SnapshotObjectId HeapProfiler::GetSnapshotObjectId(NativeObject obj) {
  // Try to find id of regular native node first.
  SnapshotObjectId id = ids_->FindEntry(reinterpret_cast<Address>(obj));
  // In case no id has been found, check whether there exists an entry where the
  // native objects has been merged into a V8 entry.
  if (id == v8::HeapProfiler::kUnknownObjectId) {
    id = ids_->FindMergedNativeEntry(obj);
  }
  return id;
}

void HeapProfilerNativeMoveListener::ObjectMoveEvent(Address from, Address to,
                                                     int size) {
  profiler_->ObjectMoveEvent(from, to, size, /*is_native_object=*/true);
}

void HeapProfiler::ObjectMoveEvent(Address from, Address to, int size,
                                   bool is_native_object) {
  base::MutexGuard guard(&profiler_mutex_);
  bool known_object = ids_->MoveObject(from, to, size);
  if (!known_object && allocation_tracker_ && !is_native_object) {
    allocation_tracker_->address_to_trace()->MoveObject(from, to, size);
  }
}

void HeapProfiler::AllocationEvent(Address addr, int size) {
  DisallowGarbageCollection no_gc;
  if (allocation_tracker_) {
    allocation_tracker_->AllocationEvent(addr, size);
  }
}

void HeapProfiler::UpdateObjectSizeEvent(Address addr, int size) {
  ids_->UpdateObjectSize(addr, size);
}

DirectHandle<HeapObject> HeapProfiler::FindHeapObjectById(SnapshotObjectId id) {
  CombinedHeapObjectIterator iterator(heap(),
                                      HeapObjectIterator::kFilterUnreachable);
  // Make sure that the object with the given id is still reachable.
  for (Tagged<HeapObject> obj = iterator.Next(); !obj.is_null();
       obj = iterator.Next()) {
    if (ids_->FindEntry(obj.address()) == id)
      return DirectHandle<HeapObject>(obj, isolate());
  }
  return DirectHandle<HeapObject>();
}

void HeapProfiler::ClearHeapObjectMap() {
  ids_.reset(new HeapObjectsMap(heap()));
  if (!allocation_tracker_) {
    if (native_move_listener_) {
      native_move_listener_->StopListening();
    }
    is_tracking_object_moves_ = false;
    heap()->isolate()->UpdateLogObjectRelocation();
  }
}

Heap* HeapProfiler::heap() const { return ids_->heap(); }

Isolate* HeapProfiler::isolate() const { return heap()->isolate(); }

void HeapProfiler::QueryObjects(DirectHandle<Context> context,
                                v8::QueryObjectPredicate* predicate,
                                std::vector<v8::Global<v8::Object>>* objects) {
  // We need a stack marker here to allow deterministic passes over the stack.
  // The garbage collection and the two object heap iterators should scan the
  // same part of the stack.
  heap()->stack().SetMarkerIfNeededAndCallback([this, predicate, objects]() {
    {
      HandleScope handle_scope(isolate());
      std::vector<Handle<JSTypedArray>> on_heap_typed_arrays;
      CombinedHeapObjectIterator heap_iterator(
          heap(), HeapObjectIterator::kFilterUnreachable);
      for (Tagged<HeapObject> heap_obj = heap_iterator.Next();
           !heap_obj.is_null(); heap_obj = heap_iterator.Next()) {
        if (IsFeedbackVector(heap_obj)) {
          Cast<FeedbackVector>(heap_obj)->ClearSlots(isolate());
        } else if (IsJSTypedArray(heap_obj) &&
                   Cast<JSTypedArray>(heap_obj)->is_on_heap()) {
          // Cannot call typed_array->GetBuffer() here directly because it may
          // trigger GC. Defer that call by collecting the object in a vector.
          on_heap_typed_arrays.push_back(
              handle(Cast<JSTypedArray>(heap_obj), isolate()));
        }
      }
      for (auto& typed_array : on_heap_typed_arrays) {
        // Convert the on-heap typed array into off-heap typed array, so that
        // its ArrayBuffer becomes valid and can be returned in the result.
        typed_array->GetBuffer(isolate());
      }
    }
    // We should return accurate information about live objects, so we need to
    // collect all garbage first.
    heap()->CollectAllAvailableGarbage(GarbageCollectionReason::kHeapProfiler);
    CombinedHeapObjectIterator heap_iterator(
        heap(), HeapObjectIterator::kFilterUnreachable);
    PtrComprCageBase cage_base(isolate());
    for (Tagged<HeapObject> heap_obj = heap_iterator.Next();
         !heap_obj.is_null(); heap_obj = heap_iterator.Next()) {
      if (!IsJSObject(heap_obj, cage_base) ||
          IsJSExternalObject(heap_obj, cage_base))
        continue;
      v8::Local<v8::Object> v8_obj(
          Utils::ToLocal(direct_handle(Cast<JSObject>(heap_obj), isolate())));
      if (!predicate->Filter(v8_obj)) continue;
      objects->emplace_back(reinterpret_cast<v8::Isolate*>(isolate()), v8_obj);
    }
  });
}

}  // namespace v8::internal
