// Copyright 2019 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/memory-measurement.h"

#include "include/v8.h"
#include "src/api/api-inl.h"
#include "src/execution/isolate-inl.h"
#include "src/heap/factory-inl.h"
#include "src/heap/incremental-marking.h"
#include "src/heap/marking-worklist.h"
#include "src/logging/counters.h"
#include "src/objects/js-array-buffer-inl.h"
#include "src/objects/js-promise-inl.h"
#include "src/tasks/task-utils.h"

namespace v8 {
namespace internal {

namespace {
class MemoryMeasurementResultBuilder {
 public:
  MemoryMeasurementResultBuilder(Isolate* isolate, Factory* factory)
      : isolate_(isolate), factory_(factory) {
    result_ = NewJSObject();
  }
  void AddTotal(size_t estimate, size_t lower_bound, size_t upper_bound) {
    AddProperty(result_, factory_->total_string(),
                NewResult(estimate, lower_bound, upper_bound));
  }
  void AddCurrent(size_t estimate, size_t lower_bound, size_t upper_bound) {
    detailed_ = true;
    AddProperty(result_, factory_->current_string(),
                NewResult(estimate, lower_bound, upper_bound));
  }
  void AddOther(size_t estimate, size_t lower_bound, size_t upper_bound) {
    detailed_ = true;
    other_.push_back(NewResult(estimate, lower_bound, upper_bound));
  }
  Handle<JSObject> Build() {
    if (detailed_) {
      int length = static_cast<int>(other_.size());
      Handle<FixedArray> other = factory_->NewFixedArray(length);
      for (int i = 0; i < length; i++) {
        other->set(i, *other_[i]);
      }
      AddProperty(result_, factory_->other_string(),
                  factory_->NewJSArrayWithElements(other));
    }
    return result_;
  }

 private:
  Handle<JSObject> NewResult(size_t estimate, size_t lower_bound,
                             size_t upper_bound) {
    Handle<JSObject> result = NewJSObject();
    Handle<Object> estimate_obj = NewNumber(estimate);
    AddProperty(result, factory_->jsMemoryEstimate_string(), estimate_obj);
    Handle<Object> range = NewRange(lower_bound, upper_bound);
    AddProperty(result, factory_->jsMemoryRange_string(), range);
    return result;
  }
  Handle<Object> NewNumber(size_t value) {
    return factory_->NewNumberFromSize(value);
  }
  Handle<JSObject> NewJSObject() {
    return factory_->NewJSObject(isolate_->object_function());
  }
  Handle<JSArray> NewRange(size_t lower_bound, size_t upper_bound) {
    Handle<Object> lower = NewNumber(lower_bound);
    Handle<Object> upper = NewNumber(upper_bound);
    Handle<FixedArray> elements = factory_->NewFixedArray(2);
    elements->set(0, *lower);
    elements->set(1, *upper);
    return factory_->NewJSArrayWithElements(elements);
  }
  void AddProperty(Handle<JSObject> object, Handle<String> name,
                   Handle<Object> value) {
    JSObject::AddProperty(isolate_, object, name, value, NONE);
  }
  Isolate* isolate_;
  Factory* factory_;
  Handle<JSObject> result_;
  std::vector<Handle<JSObject>> other_;
  bool detailed_ = false;
};
}  // anonymous namespace

class V8_EXPORT_PRIVATE MeasureMemoryDelegate
    : public v8::MeasureMemoryDelegate {
 public:
  MeasureMemoryDelegate(Isolate* isolate, Handle<NativeContext> context,
                        Handle<JSPromise> promise, v8::MeasureMemoryMode mode);
  ~MeasureMemoryDelegate() override;

  // v8::MeasureMemoryDelegate overrides:
  bool ShouldMeasure(v8::Local<v8::Context> context) override;
  void MeasurementComplete(
      const std::vector<std::pair<v8::Local<v8::Context>, size_t>>&
          context_sizes_in_bytes,
      size_t unattributed_size_in_bytes) override;

 private:
  Isolate* isolate_;
  Handle<JSPromise> promise_;
  Handle<NativeContext> context_;
  v8::MeasureMemoryMode mode_;
};

MeasureMemoryDelegate::MeasureMemoryDelegate(Isolate* isolate,
                                             Handle<NativeContext> context,
                                             Handle<JSPromise> promise,
                                             v8::MeasureMemoryMode mode)
    : isolate_(isolate), mode_(mode) {
  context_ = isolate->global_handles()->Create(*context);
  promise_ = isolate->global_handles()->Create(*promise);
}

MeasureMemoryDelegate::~MeasureMemoryDelegate() {
  isolate_->global_handles()->Destroy(promise_.location());
  isolate_->global_handles()->Destroy(context_.location());
}

bool MeasureMemoryDelegate::ShouldMeasure(v8::Local<v8::Context> context) {
  Handle<NativeContext> native_context =
      Handle<NativeContext>::cast(Utils::OpenHandle(*context));
  return context_->security_token() == native_context->security_token();
}

void MeasureMemoryDelegate::MeasurementComplete(
    const std::vector<std::pair<v8::Local<v8::Context>, size_t>>&
        context_sizes_in_bytes,
    size_t shared_size) {
  v8::Local<v8::Context> v8_context =
      Utils::Convert<HeapObject, v8::Context>(context_);
  v8::Context::Scope scope(v8_context);
  size_t total_size = 0;
  size_t current_size = 0;
  for (const auto& context_and_size : context_sizes_in_bytes) {
    total_size += context_and_size.second;
    if (*Utils::OpenHandle(*context_and_size.first) == *context_) {
      current_size = context_and_size.second;
    }
  }
  MemoryMeasurementResultBuilder result_builder(isolate_, isolate_->factory());
  result_builder.AddTotal(total_size, total_size, total_size + shared_size);

  if (mode_ == v8::MeasureMemoryMode::kDetailed) {
    result_builder.AddCurrent(current_size, current_size,
                              current_size + shared_size);
    for (const auto& context_and_size : context_sizes_in_bytes) {
      if (*Utils::OpenHandle(*context_and_size.first) != *context_) {
        size_t other_size = context_and_size.second;
        result_builder.AddOther(other_size, other_size,
                                other_size + shared_size);
      }
    }
  }

  Handle<JSObject> result = result_builder.Build();
  JSPromise::Resolve(promise_, result).ToHandleChecked();
}

MemoryMeasurement::MemoryMeasurement(Isolate* isolate)
    : isolate_(isolate), random_number_generator_() {
  if (FLAG_random_seed) {
    random_number_generator_.SetSeed(FLAG_random_seed);
  }
}

bool MemoryMeasurement::EnqueueRequest(
    std::unique_ptr<v8::MeasureMemoryDelegate> delegate,
    v8::MeasureMemoryExecution execution,
    const std::vector<Handle<NativeContext>> contexts) {
  int length = static_cast<int>(contexts.size());
  Handle<WeakFixedArray> weak_contexts =
      isolate_->factory()->NewWeakFixedArray(length);
  for (int i = 0; i < length; ++i) {
    weak_contexts->Set(i, HeapObjectReference::Weak(*contexts[i]));
  }
  Handle<WeakFixedArray> global_weak_contexts =
      isolate_->global_handles()->Create(*weak_contexts);
  Request request = {std::move(delegate),
                     global_weak_contexts,
                     std::vector<size_t>(length),
                     0u,
                     {}};
  request.timer.Start();
  received_.push_back(std::move(request));
  ScheduleGCTask(execution);
  return true;
}

std::vector<Address> MemoryMeasurement::StartProcessing() {
  if (received_.empty()) return {};
  std::unordered_set<Address> unique_contexts;
  DCHECK(processing_.empty());
  processing_ = std::move(received_);
  for (const auto& request : processing_) {
    Handle<WeakFixedArray> contexts = request.contexts;
    for (int i = 0; i < contexts->length(); i++) {
      HeapObject context;
      if (contexts->Get(i).GetHeapObject(&context)) {
        unique_contexts.insert(context.ptr());
      }
    }
  }
  return std::vector<Address>(unique_contexts.begin(), unique_contexts.end());
}

void MemoryMeasurement::FinishProcessing(const NativeContextStats& stats) {
  if (processing_.empty()) return;

  while (!processing_.empty()) {
    Request request = std::move(processing_.front());
    processing_.pop_front();
    for (int i = 0; i < static_cast<int>(request.sizes.size()); i++) {
      HeapObject context;
      if (!request.contexts->Get(i).GetHeapObject(&context)) {
        continue;
      }
      request.sizes[i] = stats.Get(context.ptr());
    }
    request.shared = stats.Get(MarkingWorklists::kSharedContext);
    done_.push_back(std::move(request));
  }
  ScheduleReportingTask();
}

void MemoryMeasurement::ScheduleReportingTask() {
  if (reporting_task_pending_) return;
  reporting_task_pending_ = true;
  auto taskrunner = V8::GetCurrentPlatform()->GetForegroundTaskRunner(
      reinterpret_cast<v8::Isolate*>(isolate_));
  taskrunner->PostTask(MakeCancelableTask(isolate_, [this] {
    reporting_task_pending_ = false;
    ReportResults();
  }));
}

bool MemoryMeasurement::IsGCTaskPending(v8::MeasureMemoryExecution execution) {
  DCHECK(execution == v8::MeasureMemoryExecution::kEager ||
         execution == v8::MeasureMemoryExecution::kDefault);
  return execution == v8::MeasureMemoryExecution::kEager
             ? eager_gc_task_pending_
             : delayed_gc_task_pending_;
}

void MemoryMeasurement::SetGCTaskPending(v8::MeasureMemoryExecution execution) {
  DCHECK(execution == v8::MeasureMemoryExecution::kEager ||
         execution == v8::MeasureMemoryExecution::kDefault);
  if (execution == v8::MeasureMemoryExecution::kEager) {
    eager_gc_task_pending_ = true;
  } else {
    delayed_gc_task_pending_ = true;
  }
}

void MemoryMeasurement::SetGCTaskDone(v8::MeasureMemoryExecution execution) {
  DCHECK(execution == v8::MeasureMemoryExecution::kEager ||
         execution == v8::MeasureMemoryExecution::kDefault);
  if (execution == v8::MeasureMemoryExecution::kEager) {
    eager_gc_task_pending_ = false;
  } else {
    delayed_gc_task_pending_ = false;
  }
}

void MemoryMeasurement::ScheduleGCTask(v8::MeasureMemoryExecution execution) {
  if (execution == v8::MeasureMemoryExecution::kLazy) return;
  if (IsGCTaskPending(execution)) return;
  SetGCTaskPending(execution);
  auto taskrunner = V8::GetCurrentPlatform()->GetForegroundTaskRunner(
      reinterpret_cast<v8::Isolate*>(isolate_));
  auto task = MakeCancelableTask(isolate_, [this, execution] {
    SetGCTaskDone(execution);
    if (received_.empty()) return;
    Heap* heap = isolate_->heap();
    if (FLAG_incremental_marking) {
      if (heap->incremental_marking()->IsStopped()) {
        heap->StartIncrementalMarking(Heap::kNoGCFlags,
                                      GarbageCollectionReason::kMeasureMemory);
      } else {
        if (execution == v8::MeasureMemoryExecution::kEager) {
          heap->FinalizeIncrementalMarkingAtomically(
              GarbageCollectionReason::kMeasureMemory);
        }
        ScheduleGCTask(execution);
      }
    } else {
      heap->CollectGarbage(OLD_SPACE, GarbageCollectionReason::kMeasureMemory);
    }
  });
  if (execution == v8::MeasureMemoryExecution::kEager) {
    taskrunner->PostTask(std::move(task));
  } else {
    taskrunner->PostDelayedTask(std::move(task), NextGCTaskDelayInSeconds());
  }
}

int MemoryMeasurement::NextGCTaskDelayInSeconds() {
  return kGCTaskDelayInSeconds +
         random_number_generator_.NextInt(kGCTaskDelayInSeconds);
}

void MemoryMeasurement::ReportResults() {
  while (!done_.empty()) {
    Request request = std::move(done_.front());
    done_.pop_front();
    HandleScope handle_scope(isolate_);
    std::vector<std::pair<v8::Local<v8::Context>, size_t>> sizes;
    DCHECK_EQ(request.sizes.size(),
              static_cast<size_t>(request.contexts->length()));
    for (int i = 0; i < request.contexts->length(); i++) {
      HeapObject raw_context;
      if (!request.contexts->Get(i).GetHeapObject(&raw_context)) {
        continue;
      }
      v8::Local<v8::Context> context = Utils::Convert<HeapObject, v8::Context>(
          handle(raw_context, isolate_));
      sizes.push_back(std::make_pair(context, request.sizes[i]));
    }
    request.delegate->MeasurementComplete(sizes, request.shared);
    isolate_->counters()->measure_memory_delay_ms()->AddSample(
        static_cast<int>(request.timer.Elapsed().InMilliseconds()));
  }
}

std::unique_ptr<v8::MeasureMemoryDelegate> MemoryMeasurement::DefaultDelegate(
    Isolate* isolate, Handle<NativeContext> context, Handle<JSPromise> promise,
    v8::MeasureMemoryMode mode) {
  return std::make_unique<MeasureMemoryDelegate>(isolate, context, promise,
                                                 mode);
}

bool NativeContextInferrer::InferForContext(Isolate* isolate, Context context,
                                            Address* native_context) {
  Map context_map = context.map(kAcquireLoad);
  Object maybe_native_context =
      TaggedField<Object, Map::kConstructorOrBackPointerOrNativeContextOffset>::
          Acquire_Load(isolate, context_map);
  if (maybe_native_context.IsNativeContext()) {
    *native_context = maybe_native_context.ptr();
    return true;
  }
  return false;
}

bool NativeContextInferrer::InferForJSFunction(Isolate* isolate,
                                               JSFunction function,
                                               Address* native_context) {
  Object maybe_context =
      TaggedField<Object, JSFunction::kContextOffset>::Acquire_Load(isolate,
                                                                    function);
  // The context may be a smi during deserialization.
  if (maybe_context.IsSmi()) {
    DCHECK_EQ(maybe_context, Deserializer::uninitialized_field_value());
    return false;
  }
  if (!maybe_context.IsContext()) {
    // The function does not have a context.
    return false;
  }
  return InferForContext(isolate, Context::cast(maybe_context), native_context);
}

bool NativeContextInferrer::InferForJSObject(Isolate* isolate, Map map,
                                             JSObject object,
                                             Address* native_context) {
  if (map.instance_type() == JS_GLOBAL_OBJECT_TYPE) {
    Object maybe_context =
        JSGlobalObject::cast(object).native_context_unchecked(isolate);
    if (maybe_context.IsNativeContext()) {
      *native_context = maybe_context.ptr();
      return true;
    }
  }
  // The maximum number of steps to perform when looking for the context.
  const int kMaxSteps = 3;
  Object maybe_constructor = map.TryGetConstructor(isolate, kMaxSteps);
  if (maybe_constructor.IsJSFunction()) {
    return InferForJSFunction(isolate, JSFunction::cast(maybe_constructor),
                              native_context);
  }
  return false;
}

void NativeContextStats::Clear() { size_by_context_.clear(); }

void NativeContextStats::Merge(const NativeContextStats& other) {
  for (const auto& it : other.size_by_context_) {
    size_by_context_[it.first] += it.second;
  }
}

void NativeContextStats::IncrementExternalSize(Address context, Map map,
                                               HeapObject object) {
  InstanceType instance_type = map.instance_type();
  size_t external_size = 0;
  if (instance_type == JS_ARRAY_BUFFER_TYPE) {
    external_size = JSArrayBuffer::cast(object).allocation_length();
  } else {
    DCHECK(InstanceTypeChecker::IsExternalString(instance_type));
    external_size = ExternalString::cast(object).ExternalPayloadSize();
  }
  size_by_context_[context] += external_size;
}

}  // namespace internal
}  // namespace v8
