// Copyright (c) 2013, 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/service.h"

#include <cstdint>
#include <memory>
#include <utility>

#include "include/dart_api.h"
#include "include/dart_native_api.h"
#include "platform/globals.h"

#include "platform/unicode.h"
#include "platform/utils.h"
#include "vm/base64.h"
#include "vm/canonical_tables.h"
#include "vm/closure_functions_cache.h"
#include "vm/compiler/jit/compiler.h"
#include "vm/cpu.h"
#include "vm/dart_api_impl.h"
#include "vm/dart_api_message.h"
#include "vm/dart_api_state.h"
#include "vm/dart_entry.h"
#include "vm/debugger.h"
#include "vm/heap/safepoint.h"
#include "vm/isolate.h"
#include "vm/json_stream.h"
#include "vm/kernel.h"
#include "vm/kernel_isolate.h"
#include "vm/lockers.h"
#include "vm/message.h"
#include "vm/message_handler.h"
#include "vm/message_snapshot.h"
#include "vm/microtask_mirror_queues.h"
#include "vm/native_arguments.h"
#include "vm/native_entry.h"
#include "vm/native_symbol.h"
#include "vm/object.h"
#include "vm/object_graph.h"
#include "vm/object_id_ring.h"
#include "vm/object_store.h"
#include "vm/parser.h"
#include "vm/port.h"
#include "vm/profiler.h"
#include "vm/profiler_service.h"
#include "vm/raw_object_fields.h"
#include "vm/resolver.h"
#include "vm/reusable_handles.h"
#include "vm/service_event.h"
#include "vm/service_isolate.h"
#include "vm/source_report.h"
#include "vm/stack_frame.h"
#include "vm/symbols.h"
#include "vm/timeline.h"
#include "vm/version.h"
#include "vm/visitor.h"

#if defined(SUPPORT_PERFETTO)
#include "vm/perfetto_utils.h"
#endif  // defined(SUPPORT_PERFETTO)

#if defined(DART_HOST_OS_WINDOWS)
#include <psapi.h>
#endif  // defined(DART_HOST_OS_WINDOWS)

namespace dart {

#define Z (T->zone())

DECLARE_FLAG(bool, trace_service);
DECLARE_FLAG(bool, trace_service_pause_events);
// This flag is defined in "runtime/vm/microtask_mirror_queues.cc".
DECLARE_FLAG(bool, profile_microtasks);
DECLARE_FLAG(bool, profile_vm);
DEFINE_FLAG(charp,
            vm_name,
            "vm",
            "The default name of this vm as reported by the VM service "
            "protocol");

DEFINE_FLAG(bool,
            warn_on_pause_with_no_debugger,
            false,
            "Print a message when an isolate is paused but there is no "
            "debugger attached.");

DEFINE_FLAG(
    charp,
    log_service_response_sizes,
    nullptr,
    "Log sizes of service responses and events to a file in CSV format.");

void* Service::service_response_size_log_file_ = nullptr;

void Service::LogResponseSize(const char* method, JSONStream* js) {
  if (service_response_size_log_file_ == nullptr) {
    return;
  }
  Dart_FileWriteCallback file_write = Dart::file_write_callback();
  char* entry =
      OS::SCreate(nullptr, "%s, %" Pd "\n", method, js->buffer()->length());
  (*file_write)(entry, strlen(entry), service_response_size_log_file_);
  free(entry);
}

void Service::Init() {
  if (FLAG_log_service_response_sizes == nullptr) {
    return;
  }
  Dart_FileOpenCallback file_open = Dart::file_open_callback();
  Dart_FileWriteCallback file_write = Dart::file_write_callback();
  Dart_FileCloseCallback file_close = Dart::file_close_callback();
  if ((file_open == nullptr) || (file_write == nullptr) ||
      (file_close == nullptr)) {
    OS::PrintErr("Error: Could not access file callbacks.");
    UNREACHABLE();
  }
  ASSERT(service_response_size_log_file_ == nullptr);
  service_response_size_log_file_ =
      (*file_open)(FLAG_log_service_response_sizes, true);
  if (service_response_size_log_file_ == nullptr) {
    OS::PrintErr("Warning: Failed to open service response size log file: %s\n",
                 FLAG_log_service_response_sizes);
    return;
  }
}

void Service::Cleanup() {
  if (service_response_size_log_file_ == nullptr) {
    return;
  }
  Dart_FileCloseCallback file_close = Dart::file_close_callback();
  (*file_close)(service_response_size_log_file_);
  service_response_size_log_file_ = nullptr;
}

static void PrintInvalidParamError(JSONStream* js, const char* param) {
#if !defined(PRODUCT)
  js->PrintError(kInvalidParams, "%s: invalid '%s' parameter: %s", js->method(),
                 param, js->LookupParam(param));
#endif
}

// TODO(johnmccutchan): Split into separate file and write unit tests.
class MethodParameter {
 public:
  MethodParameter(const char* name, bool required)
      : name_(name), required_(required) {}

  virtual ~MethodParameter() {}

  virtual bool Validate(const char* value) const { return true; }

  virtual bool ValidateObject(const Object& value) const { return true; }

  const char* name() const { return name_; }

  bool required() const { return required_; }

  virtual void PrintError(const char* name,
                          const char* value,
                          JSONStream* js) const {
    PrintInvalidParamError(js, name);
  }

  virtual void PrintErrorObject(const char* name,
                                const Object& value,
                                JSONStream* js) const {
    PrintInvalidParamError(js, name);
  }

 private:
  const char* name_;
  bool required_;
};

class NoSuchParameter : public MethodParameter {
 public:
  explicit NoSuchParameter(const char* name) : MethodParameter(name, false) {}

  virtual bool Validate(const char* value) const { return (value == nullptr); }

  virtual bool ValidateObject(const Object& value) const {
    return value.IsNull();
  }
};

#define ISOLATE_PARAMETER new IdParameter("isolateId", true)
#define ISOLATE_GROUP_PARAMETER new IdParameter("isolateGroupId", true)
#define NO_ISOLATE_PARAMETER new NoSuchParameter("isolateId")
#define RUNNABLE_ISOLATE_PARAMETER new RunnableIsolateParameter("isolateId")
#define OBJECT_PARAMETER new IdParameter("objectId", true)
#define NATIVE_MEMORY_ADDRESS_PARAMETER new StringParameter("address", true)
#define NATIVE_MEMORY_SIZE_PARAMETER new UIntParameter("size", true)

static bool ValidateUIntParameter(const char* value) {
  if (value == nullptr) {
    return false;
  }
  for (const char* cp = value; *cp != '\0'; cp++) {
    if (*cp < '0' || *cp > '9') {
      return false;
    }
  }
  return true;
}

class UIntParameter : public MethodParameter {
 public:
  UIntParameter(const char* name, bool required)
      : MethodParameter(name, required) {}

  virtual bool Validate(const char* value) const {
    return ValidateUIntParameter(value);
  }

  static uintptr_t Parse(const char* value) {
    if (value == nullptr) {
      return -1;
    }
    char* end_ptr = nullptr;
    uintptr_t result = strtoul(value, &end_ptr, 10);
    ASSERT(*end_ptr == '\0');  // Parsed full string
    return result;
  }
};

class EnumListParameter : public MethodParameter {
 public:
  EnumListParameter(const char* name, bool required, const char* const* enums)
      : MethodParameter(name, required), enums_(enums) {}

  virtual bool Validate(const char* value) const {
    return ElementCount(value) >= 0;
  }

  const char** Parse(char* value) const {
    const char* kJsonChars = " \t\r\n[,]";

    // Make a writeable copy of the value.
    intptr_t element_count = ElementCount(value);
    if (element_count < 0) {
      return nullptr;
    }
    intptr_t element_pos = 0;

    // Allocate our element array.  +1 for nullptr terminator.
    // The caller is responsible for deleting this memory.
    char** elements = new char*[element_count + 1];
    elements[element_count] = nullptr;

    // Parse the string destructively.  Build the list of elements.
    while (element_pos < element_count) {
      // Skip to the next element.
      value += strspn(value, kJsonChars);

      intptr_t len = strcspn(value, kJsonChars);
      ASSERT(len > 0);  // We rely on the parameter being validated already.
      value[len] = '\0';
      elements[element_pos++] = value;

      // Advance.  +1 for null terminator.
      value += (len + 1);
    }
    return const_cast<const char**>(elements);
  }

 private:
  // For now VM service enums are ascii letters plus underscore.
  static bool IsEnumChar(char c) {
    return (((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')) ||
            (c == '_'));
  }

  // Returns number of elements in the list.  -1 on parse error.
  intptr_t ElementCount(const char* value) const {
    const char* kJsonWhitespaceChars = " \t\r\n";
    if (value == nullptr) {
      return -1;
    }
    const char* cp = value;
    cp += strspn(cp, kJsonWhitespaceChars);
    if (*cp++ != '[') {
      // Missing initial [.
      return -1;
    }
    bool closed = false;
    bool element_allowed = true;
    intptr_t element_count = 0;
    while (true) {
      // Skip json whitespace.
      cp += strspn(cp, kJsonWhitespaceChars);
      switch (*cp) {
        case '\0':
          return closed ? element_count : -1;
        case ']':
          closed = true;
          cp++;
          break;
        case ',':
          if (element_allowed) {
            return -1;
          }
          element_allowed = true;
          cp++;
          break;
        default:
          if (!element_allowed) {
            return -1;
          }
          bool valid_enum = false;
          const char* id_start = cp;
          while (IsEnumChar(*cp)) {
            cp++;
          }
          if (cp == id_start) {
            // Empty identifier, something like this [,].
            return -1;
          }
          intptr_t id_len = cp - id_start;
          if (enums_ != nullptr) {
            for (intptr_t i = 0; enums_[i] != nullptr; i++) {
              intptr_t len = strlen(enums_[i]);
              if (len == id_len && strncmp(id_start, enums_[i], len) == 0) {
                element_count++;
                valid_enum = true;
                element_allowed = false;  // we need a comma first.
                break;
              }
            }
          }
          if (!valid_enum) {
            return -1;
          }
          break;
      }
    }
  }

  const char* const* enums_;
};

#if defined(SUPPORT_TIMELINE)
static const char* const timeline_streams_enum_names[] = {
    "all",
#define DEFINE_NAME(name, ...) #name,
    TIMELINE_STREAM_LIST(DEFINE_NAME)
#undef DEFINE_NAME
        nullptr};

static const MethodParameter* const set_vm_timeline_flags_params[] = {
    NO_ISOLATE_PARAMETER,
    new EnumListParameter("recordedStreams",
                          false,
                          timeline_streams_enum_names),
    nullptr,
};

static bool HasStream(const char** recorded_streams, const char* stream) {
  while (*recorded_streams != nullptr) {
    if ((strstr(*recorded_streams, "all") != nullptr) ||
        (strstr(*recorded_streams, stream) != nullptr)) {
      return true;
    }
    recorded_streams++;
  }
  return false;
}

bool Service::EnableTimelineStreams(char* categories_list) {
  const EnumListParameter* recorded_streams_param =
      static_cast<const EnumListParameter*>(set_vm_timeline_flags_params[1]);
  const char** streams = recorded_streams_param->Parse(categories_list);
  if (streams == nullptr) {
    return false;
  }

#define SET_ENABLE_STREAM(name, ...)                                           \
  Timeline::SetStream##name##Enabled(HasStream(streams, #name));
  TIMELINE_STREAM_LIST(SET_ENABLE_STREAM);
#undef SET_ENABLE_STREAM

  delete[] streams;

#if !defined(PRODUCT)
  // Notify clients that the set of subscribed streams has been updated.
  if (Service::timeline_stream.enabled()) {
    ServiceEvent event(ServiceEvent::kTimelineStreamSubscriptionsUpdate);
    Service::HandleEvent(&event);
  }
#endif

  return true;
}
#endif  // defined(SUPPORT_TIMELINE)

#ifndef PRODUCT
// The name of this of this vm as reported by the VM service protocol.
static char* vm_name = nullptr;

static const char* GetVMName() {
  if (vm_name == nullptr) {
    return FLAG_vm_name;
  }
  return vm_name;
}

ServiceIdZone::ServiceIdZone(intptr_t id, ObjectIdRing::IdPolicy policy)
    : id_(id), policy_(policy) {}

ServiceIdZone::~ServiceIdZone() {}

intptr_t ServiceIdZone::StringIdToInt(const char* id_string) {
  if (Utils::StrStartsWith(id_string, "zones/") &&
      ValidateUIntParameter(id_string + 6)) {
    return UIntParameter::Parse(id_string + 6);
  }
  return -1;
}

RingServiceIdZone::RingServiceIdZone(intptr_t id,
                                     ObjectIdRing::IdPolicy policy,
                                     int32_t capacity)
    : ServiceIdZone(id, policy), ring_(capacity) {}

RingServiceIdZone::~RingServiceIdZone() {}

int32_t RingServiceIdZone::GetIdForObject(const ObjectPtr obj) {
  return ring_.GetIdForObject(obj, policy());
}

ObjectPtr RingServiceIdZone::GetObjectForId(int32_t id,
                                            ObjectIdRing::LookupResult* kind) {
  return ring_.GetObjectForId(id, kind);
}

char* RingServiceIdZone::GetServiceId(const Object& obj) {
  Thread* thread = Thread::Current();
  Zone* zone = thread->zone();
  ASSERT(zone != nullptr);
  if (obj.IsSmi()) {
    return zone->PrintToString("objects/int-%" Pd, Smi::Cast(obj).Value());
  }
  const intptr_t object_part_of_service_id = GetIdForObject(obj.ptr());
  return zone->PrintToString("objects/%" Pd "/%" Pd, object_part_of_service_id,
                             id());
}

void RingServiceIdZone::Invalidate() {
  ring_.Invalidate();
}

void RingServiceIdZone::VisitPointers(ObjectPointerVisitor* visitor) const {
  ring_.VisitPointers(visitor);
}

void RingServiceIdZone::PrintJSON(JSONStream& js) const {
  JSONObject jsobj(&js);
  jsobj.AddProperty("type", "IdZone");
  jsobj.AddPropertyF("id", "zones/%" Pd, id());
  jsobj.AddProperty("backingBufferKind", "Ring");
  switch (policy()) {
    case dart::ObjectIdRing::IdPolicy::kAllocateId:
      jsobj.AddProperty("idAssignmentPolicy", "AlwaysAllocate");
      break;
    case dart::ObjectIdRing::IdPolicy::kReuseId:
      jsobj.AddProperty("idAssignmentPolicy", "ReuseExisting");
      break;
  }
}

// TODO(johnmccutchan): Unify embedder service handler lists and their APIs.
EmbedderServiceHandler* Service::isolate_service_handler_head_ = nullptr;
EmbedderServiceHandler* Service::root_service_handler_head_ = nullptr;
struct ServiceMethodDescriptor;
const ServiceMethodDescriptor* FindMethod(const char* method_name);

// Support for streams defined in embedders.
Dart_ServiceStreamListenCallback Service::stream_listen_callback_ = nullptr;
Dart_ServiceStreamCancelCallback Service::stream_cancel_callback_ = nullptr;
Dart_EmbedderInformationCallback Service::embedder_information_callback_ =
    nullptr;

// These are the set of streams known to the core VM.
StreamInfo Service::vm_stream("VM");
StreamInfo Service::isolate_stream("Isolate");
StreamInfo Service::debug_stream("Debug");
StreamInfo Service::gc_stream("GC");
StreamInfo Service::echo_stream("_Echo");
StreamInfo Service::heapsnapshot_stream("HeapSnapshot");
StreamInfo Service::logging_stream("Logging");
StreamInfo Service::timer_stream("Timer");
StreamInfo Service::extension_stream("Extension");
StreamInfo Service::timeline_stream("Timeline");
StreamInfo Service::profiler_stream("Profiler");

const uint8_t* Service::dart_library_kernel_ = nullptr;
intptr_t Service::dart_library_kernel_len_ = 0;

// Keep streams_ in sync with the protected streams in
// lib/developer/extension.dart
static StreamInfo* const streams_[] = {
    &Service::vm_stream,        &Service::isolate_stream,
    &Service::debug_stream,     &Service::gc_stream,
    &Service::echo_stream,      &Service::heapsnapshot_stream,
    &Service::logging_stream,   &Service::timer_stream,
    &Service::extension_stream, &Service::timeline_stream,
    &Service::profiler_stream,
};

bool Service::ListenStream(const char* stream_id,
                           bool include_private_members) {
  if (FLAG_trace_service) {
    OS::PrintErr("vm-service: starting stream '%s'\n", stream_id);
  }
  intptr_t num_streams = sizeof(streams_) / sizeof(streams_[0]);
  for (intptr_t i = 0; i < num_streams; i++) {
    if (strcmp(stream_id, streams_[i]->id()) == 0) {
      streams_[i]->set_enabled(true);
      streams_[i]->set_include_private_members(include_private_members);
      return true;
    }
  }
  if (stream_listen_callback_ != nullptr) {
    Thread* T = Thread::Current();
    TransitionVMToNative transition(T);
    return (*stream_listen_callback_)(stream_id);
  }
  return false;
}

void Service::CancelStream(const char* stream_id) {
  if (FLAG_trace_service) {
    OS::PrintErr("vm-service: stopping stream '%s'\n", stream_id);
  }
  intptr_t num_streams = sizeof(streams_) / sizeof(streams_[0]);
  for (intptr_t i = 0; i < num_streams; i++) {
    if (strcmp(stream_id, streams_[i]->id()) == 0) {
      streams_[i]->set_enabled(false);
      return;
    }
  }
  if (stream_cancel_callback_ != nullptr) {
    Thread* T = Thread::Current();
    TransitionVMToNative transition(T);
    return (*stream_cancel_callback_)(stream_id);
  }
}

static void PrintSuccess(JSONStream* js) {
  JSONObject jsobj(js);
  jsobj.AddProperty("type", "Success");
}

static bool CheckDebuggerDisabled(Thread* thread, JSONStream* js) {
#if defined(DART_PRECOMPILED_RUNTIME)
  js->PrintError(kFeatureDisabled, "Debugger is disabled in AOT mode.");
  return true;
#else
  if (thread->isolate()->debugger() == nullptr) {
    js->PrintError(kFeatureDisabled, "Debugger is disabled.");
    return true;
  }
  return false;
#endif
}

static bool CheckProfilerDisabled(Thread* thread, JSONStream* js) {
  if (!Profiler::IsRunning()) {
    js->PrintError(kFeatureDisabled, "Profiler is disabled.");
    return true;
  }
  return false;
}

static bool GetIntegerId(const char* s, intptr_t* id, int base = 10) {
  if ((s == nullptr) || (*s == '\0')) {
    // Empty string.
    return false;
  }
  if (id == nullptr) {
    // No id pointer.
    return false;
  }
  intptr_t r = 0;
  char* end_ptr = nullptr;
#if defined(ARCH_IS_32_BIT)
  r = strtol(s, &end_ptr, base);
#else
  r = strtoll(s, &end_ptr, base);
#endif
  if (end_ptr == s) {
    // String was not advanced at all, cannot be valid.
    return false;
  }
  *id = r;
  return true;
}

static bool GetUnsignedIntegerId(const char* s, uintptr_t* id, int base = 10) {
  if ((s == nullptr) || (*s == '\0')) {
    // Empty string.
    return false;
  }
  if (id == nullptr) {
    // No id pointer.
    return false;
  }
  uintptr_t r = 0;
  char* end_ptr = nullptr;
#if defined(ARCH_IS_32_BIT)
  r = strtoul(s, &end_ptr, base);
#else
  r = strtoull(s, &end_ptr, base);
#endif
  if (end_ptr == s) {
    // String was not advanced at all, cannot be valid.
    return false;
  }
  *id = r;
  return true;
}

static bool GetInteger64Id(const char* s, int64_t* id, int base = 10) {
  if ((s == nullptr) || (*s == '\0')) {
    // Empty string.
    return false;
  }
  if (id == nullptr) {
    // No id pointer.
    return false;
  }
  int64_t r = 0;
  char* end_ptr = nullptr;
  r = strtoll(s, &end_ptr, base);
  if (end_ptr == s) {
    // String was not advanced at all, cannot be valid.
    return false;
  }
  *id = r;
  return true;
}

// Scans the string until the '-' character. Returns pointer to string
// at '-' character. Returns nullptr if not found.
static const char* ScanUntilDash(const char* s) {
  if ((s == nullptr) || (*s == '\0')) {
    // Empty string.
    return nullptr;
  }
  while (*s != '\0') {
    if (*s == '-') {
      return s;
    }
    s++;
  }
  return nullptr;
}

static bool GetCodeId(const char* s, int64_t* timestamp, uword* address) {
  if ((s == nullptr) || (*s == '\0')) {
    // Empty string.
    return false;
  }
  if ((timestamp == nullptr) || (address == nullptr)) {
    // Bad arguments.
    return false;
  }
  // Extract the timestamp.
  if (!GetInteger64Id(s, timestamp, 16) || (*timestamp < 0)) {
    return false;
  }
  s = ScanUntilDash(s);
  if (s == nullptr) {
    return false;
  }
  // Skip the dash.
  s++;
  // Extract the PC.
  if (!GetUnsignedIntegerId(s, address, 16)) {
    return false;
  }
  return true;
}

// Verifies that |s| begins with |prefix| and then calls |GetIntegerId| on
// the remainder of |s|.
static bool GetPrefixedIntegerId(const char* s,
                                 const char* prefix,
                                 intptr_t* service_id) {
  if (s == nullptr) {
    return false;
  }
  ASSERT(prefix != nullptr);
  const intptr_t kInputLen = strlen(s);
  const intptr_t kPrefixLen = strlen(prefix);
  ASSERT(kPrefixLen > 0);
  if (kInputLen <= kPrefixLen) {
    return false;
  }
  if (strncmp(s, prefix, kPrefixLen) != 0) {
    return false;
  }
  // Prefix satisfied. Move forward.
  s += kPrefixLen;
  // Attempt to read integer id.
  return GetIntegerId(s, service_id);
}

static bool IsValidClassId(Isolate* isolate, intptr_t cid) {
  ASSERT(isolate != nullptr);
  ClassTable* class_table = isolate->group()->class_table();
  ASSERT(class_table != nullptr);
  return class_table->IsValidIndex(cid) && class_table->HasValidClassAt(cid);
}

static ClassPtr GetClassForId(Isolate* isolate, intptr_t cid) {
  ASSERT(isolate == Isolate::Current());
  ASSERT(isolate != nullptr);
  ClassTable* class_table = isolate->group()->class_table();
  ASSERT(class_table != nullptr);
  return class_table->At(cid);
}

class DartStringParameter : public MethodParameter {
 public:
  DartStringParameter(const char* name, bool required)
      : MethodParameter(name, required) {}

  virtual bool ValidateObject(const Object& value) const {
    return value.IsString();
  }
};

class DartListParameter : public MethodParameter {
 public:
  DartListParameter(const char* name, bool required)
      : MethodParameter(name, required) {}

  virtual bool ValidateObject(const Object& value) const {
    return value.IsArray() || value.IsGrowableObjectArray();
  }
};

class BoolParameter : public MethodParameter {
 public:
  BoolParameter(const char* name, bool required)
      : MethodParameter(name, required) {}

  virtual bool Validate(const char* value) const {
    if (value == nullptr) {
      return false;
    }
    return (strcmp("true", value) == 0) || (strcmp("false", value) == 0);
  }

  virtual bool ValidateObject(const Object& value) const {
    return value.IsBool();
  }

  static bool Parse(const char* value, bool default_value = false) {
    if (value == nullptr) {
      return default_value;
    }
    return strcmp("true", value) == 0;
  }
};

class Int64Parameter : public MethodParameter {
 public:
  Int64Parameter(const char* name, bool required)
      : MethodParameter(name, required) {}

  virtual bool Validate(const char* value) const {
    if (value == nullptr) {
      return false;
    }
    for (const char* cp = value; *cp != '\0'; cp++) {
      if ((*cp < '0' || *cp > '9') && (*cp != '-')) {
        return false;
      }
    }
    return true;
  }

  static int64_t Parse(const char* value, int64_t default_value = -1) {
    if ((value == nullptr) || (*value == '\0')) {
      return default_value;
    }
    char* end_ptr = nullptr;
    int64_t result = strtoll(value, &end_ptr, 10);
    ASSERT(*end_ptr == '\0');  // Parsed full string
    return result;
  }
};

class UInt64Parameter : public MethodParameter {
 public:
  UInt64Parameter(const char* name, bool required)
      : MethodParameter(name, required) {}

  virtual bool Validate(const char* value) const {
    if (value == nullptr) {
      return false;
    }
    for (const char* cp = value; *cp != '\0'; cp++) {
      if ((*cp < '0' || *cp > '9') && (*cp != '-')) {
        return false;
      }
    }
    return true;
  }

  static uint64_t Parse(const char* value, uint64_t default_value = 0) {
    if ((value == nullptr) || (*value == '\0')) {
      return default_value;
    }
    char* end_ptr = nullptr;
    uint64_t result = strtoull(value, &end_ptr, 10);
    ASSERT(*end_ptr == '\0');  // Parsed full string
    return result;
  }
};

class IdParameter : public MethodParameter {
 public:
  IdParameter(const char* name, bool required)
      : MethodParameter(name, required) {}

  virtual bool Validate(const char* value) const { return (value != nullptr); }
};

class StringParameter : public MethodParameter {
 public:
  StringParameter(const char* name, bool required)
      : MethodParameter(name, required) {}

  virtual bool Validate(const char* value) const { return (value != nullptr); }
};

class RunnableIsolateParameter : public MethodParameter {
 public:
  explicit RunnableIsolateParameter(const char* name)
      : MethodParameter(name, true) {}

  virtual bool Validate(const char* value) const {
    // We assume the request has been forwarded to the current isolate and
    // isolateId matches it.
    return (value != nullptr) && ValidateCurrentIsolate();
  }

  virtual bool ValidateObject(const Object& value) const {
    // We assume the request has been forwarded to the current isolate and
    // isolateId matches it.
    return value.IsString() && ValidateCurrentIsolate();
  }

  virtual void PrintError(const char* name,
                          const char* value,
                          JSONStream* js) const {
    js->PrintError(kIsolateMustBeRunnable,
                   "Isolate must be runnable before this request is made.");
  }

 private:
  static bool ValidateCurrentIsolate() {
    Isolate* isolate = Isolate::Current();
    return (isolate != nullptr) && (isolate->is_runnable());
  }
};

class EnumParameter : public MethodParameter {
 public:
  EnumParameter(const char* name, bool required, const char* const* enums)
      : MethodParameter(name, required), enums_(enums) {}

  virtual bool Validate(const char* value) const {
    if (value == nullptr) {
      return true;
    }
    for (intptr_t i = 0; enums_[i] != nullptr; i++) {
      if (strcmp(value, enums_[i]) == 0) {
        return true;
      }
    }
    return false;
  }

 private:
  const char* const* enums_;
};

// If the key is not found, this function returns the last element in the
// values array. This can be used to encode the default value.
template <typename T>
T EnumMapper(const char* value, const char* const* enums, T* values) {
  ASSERT(value != nullptr);
  intptr_t i = 0;
  for (i = 0; enums[i] != nullptr; i++) {
    if (strcmp(value, enums[i]) == 0) {
      return values[i];
    }
  }
  // Default value.
  return values[i];
}

typedef void (*ServiceMethodEntry)(Thread* thread, JSONStream* js);

struct ServiceMethodDescriptor {
  const char* name;
  const ServiceMethodEntry entry;
  const MethodParameter* const* parameters;
};

static void PrintMissingParamError(JSONStream* js, const char* param) {
  js->PrintError(kInvalidParams, "%s expects the '%s' parameter", js->method(),
                 param);
}

static void PrintUnrecognizedMethodError(JSONStream* js) {
  js->PrintError(kMethodNotFound, nullptr);
}

// TODO(johnmccutchan): Do we reject unexpected parameters?
static bool ValidateParameters(const MethodParameter* const* parameters,
                               JSONStream* js) {
  if (parameters == nullptr) {
    return true;
  }
  if (js->NumObjectParameters() > 0) {
    Object& value = Object::Handle();
    for (intptr_t i = 0; parameters[i] != nullptr; i++) {
      const MethodParameter* parameter = parameters[i];
      const char* name = parameter->name();
      const bool required = parameter->required();
      value = js->LookupObjectParam(name);
      const bool has_parameter = !value.IsNull();
      if (required && !has_parameter) {
        PrintMissingParamError(js, name);
        return false;
      }
      if (has_parameter && !parameter->ValidateObject(value)) {
        parameter->PrintErrorObject(name, value, js);
        return false;
      }
    }
  } else {
    for (intptr_t i = 0; parameters[i] != nullptr; i++) {
      const MethodParameter* parameter = parameters[i];
      const char* name = parameter->name();
      const bool required = parameter->required();
      const char* value = js->LookupParam(name);
      const bool has_parameter = (value != nullptr);
      if (required && !has_parameter) {
        PrintMissingParamError(js, name);
        return false;
      }
      if (has_parameter && !parameter->Validate(value)) {
        parameter->PrintError(name, value, js);
        return false;
      }
    }
  }
  return true;
}

void Service::PostError(const String& method_name,
                        const Array& parameter_keys,
                        const Array& parameter_values,
                        const Instance& reply_port,
                        const Instance& id,
                        const Error& error) {
  Thread* T = Thread::Current();
  StackZone zone(T);
  JSONStream js;
  js.Setup(zone.GetZone(), SendPort::Cast(reply_port).Id(), id, method_name,
           parameter_keys, parameter_values);
  js.PrintError(kExtensionError, "Error in extension `%s`: %s", js.method(),
                error.ToErrorCString());
  js.PostReply();
}

ErrorPtr Service::InvokeMethod(Isolate* I, const Array& msg) {
  Thread* T = Thread::Current();
  ASSERT(I == T->isolate());
  ASSERT(I != nullptr);
  ASSERT(T->execution_state() == Thread::kThreadInVM);
  ASSERT(!msg.IsNull());
  ASSERT(msg.Length() == 7);

  {
    StackZone zone(T);

    Isolate& isolate = *T->isolate();
    Instance& reply_port = Instance::Handle(Z);
    Instance& seq = String::Handle(Z);
    String& method_name = String::Handle(Z);
    Bool& parameters_are_dart_objects = Bool::Handle(Z);
    Array& param_keys = Array::Handle(Z);
    Array& param_values = Array::Handle(Z);
    reply_port ^= msg.At(1);
    seq ^= msg.At(2);
    method_name ^= msg.At(3);
    parameters_are_dart_objects ^= msg.At(4);
    param_keys ^= msg.At(5);
    param_values ^= msg.At(6);

    ASSERT(!method_name.IsNull());
    ASSERT(seq.IsNull() || seq.IsString() || seq.IsNumber());
    ASSERT(!param_keys.IsNull());
    ASSERT(!param_values.IsNull());
    ASSERT(param_keys.Length() == param_values.Length());

    // We expect a reply port unless there is a null sequence id,
    // which indicates that no reply should be sent.  We use this in
    // tests.
    if (!seq.IsNull() && !reply_port.IsSendPort()) {
      FATAL("SendPort expected.");
    }

    JSONStream js;
    Dart_Port reply_port_id =
        (reply_port.IsNull() ? ILLEGAL_PORT : SendPort::Cast(reply_port).Id());
    js.Setup(zone.GetZone(), reply_port_id, seq, method_name, param_keys,
             param_values, parameters_are_dart_objects.value());

    // |id_zone| is the zone that will be stored into the |JSONStream| that we
    // are about to create, meaning that it is where temporary Service IDs may
    // be allocated by the RPC currently being handled.
    RingServiceIdZone* id_zone = &isolate.EnsureDefaultServiceIdZone();
    const char* id_zone_id_arg = js.LookupParam("idZoneId");
    if (id_zone_id_arg != nullptr) {
      intptr_t id_zone_id = ServiceIdZone::StringIdToInt(id_zone_id_arg);
      if (id_zone_id != -1) {
        id_zone = isolate.GetServiceIdZone(id_zone_id);
      } else {
        id_zone = nullptr;
      }
    }

    if (id_zone == nullptr) {
      PrintInvalidParamError(&js, "idZoneId");
      js.PostReply();
      return T->StealStickyError();
    }
    js.set_id_zone(*id_zone);

    const char* c_method_name = method_name.ToCString();

    const ServiceMethodDescriptor* method = FindMethod(c_method_name);
    if (method != nullptr) {
      if (!ValidateParameters(method->parameters, &js)) {
        js.PostReply();
        return T->StealStickyError();
      }
      method->entry(T, &js);
      Service::LogResponseSize(c_method_name, &js);
      js.PostReply();
      return T->StealStickyError();
    }

    EmbedderServiceHandler* handler = FindIsolateEmbedderHandler(c_method_name);
    if (handler == nullptr) {
      handler = FindRootEmbedderHandler(c_method_name);
    }

    if (handler != nullptr) {
      EmbedderHandleMessage(handler, &js);
      return T->StealStickyError();
    }

    const Instance& extension_handler =
        Instance::Handle(Z, I->LookupServiceExtensionHandler(method_name));
    if (!extension_handler.IsNull()) {
      ScheduleExtensionHandler(extension_handler, method_name, param_keys,
                               param_values, reply_port, seq);
      // Schedule was successful. Extension code will post a reply
      // asynchronously.
      return T->StealStickyError();
    }

    PrintUnrecognizedMethodError(&js);
    js.PostReply();
    return T->StealStickyError();
  }
}

ErrorPtr Service::HandleRootMessage(const Array& msg_instance) {
  Isolate* isolate = Isolate::Current();
  return InvokeMethod(isolate, msg_instance);
}

ErrorPtr Service::HandleIsolateMessage(Isolate* isolate, const Array& msg) {
  ASSERT(isolate != nullptr);
  const Error& error = Error::Handle(InvokeMethod(isolate, msg));
  return MaybePause(isolate, error);
}

static void Finalizer(void* isolate_callback_data, void* buffer) {
  free(buffer);
}

void Service::SendEvent(const char* stream_id,
                        const char* event_type,
                        uint8_t* bytes,
                        intptr_t bytes_length) {
  Thread* thread = Thread::Current();
  Isolate* isolate = thread->isolate();
  ASSERT(isolate != nullptr);

  if (FLAG_trace_service) {
    OS::PrintErr(
        "vm-service: Pushing ServiceEvent(isolate='%s', "
        "isolateId='" ISOLATE_SERVICE_ID_FORMAT_STRING
        "', kind='%s',"
        " len=%" Pd ") to stream %s\n",
        isolate->name(), static_cast<int64_t>(isolate->main_port()), event_type,
        bytes_length, stream_id);
  }

  bool result;
  {
    Dart_CObject cbytes;
    cbytes.type = Dart_CObject_kExternalTypedData;
    cbytes.value.as_external_typed_data.type = Dart_TypedData_kUint8;
    cbytes.value.as_external_typed_data.length = bytes_length;
    cbytes.value.as_external_typed_data.data = bytes;
    cbytes.value.as_external_typed_data.peer = bytes;
    cbytes.value.as_external_typed_data.callback = Finalizer;

    Dart_CObject cstream_id;
    cstream_id.type = Dart_CObject_kString;
    cstream_id.value.as_string = const_cast<char*>(stream_id);

    Dart_CObject* elements[2];
    elements[0] = &cstream_id;
    elements[1] = &cbytes;
    Dart_CObject message;
    message.type = Dart_CObject_kArray;
    message.value.as_array.length = 2;
    message.value.as_array.values = elements;

    std::unique_ptr<Message> msg =
        WriteApiMessage(thread->zone(), &message, ServiceIsolate::Port(),
                        Message::kNormalPriority);
    if (msg == nullptr) {
      result = false;
    } else {
      result = PortMap::PostMessage(std::move(msg));
    }
  }

  if (!result) {
    free(bytes);
  }
}

void Service::SendEventWithData(const char* stream_id,
                                const char* event_type,
                                intptr_t reservation,
                                const char* metadata,
                                intptr_t metadata_size,
                                uint8_t* data,
                                intptr_t data_size) {
  ASSERT(kInt32Size + metadata_size <= reservation);
  // Using a SPACE creates valid JSON. Our goal here is to prevent the memory
  // overhead of copying to concatenate metadata and payload together by
  // over-allocating to underlying buffer before we know how long the metadata
  // will be.
  memset(data, ' ', reservation);
  reinterpret_cast<uint32_t*>(data)[0] = reservation;
  memmove(&(reinterpret_cast<uint32_t*>(data)[1]), metadata, metadata_size);
  Service::SendEvent(stream_id, event_type, data, data_size);
}

static void ReportPauseOnConsole(ServiceEvent* event) {
  const char* name = event->isolate()->name();
  const int64_t main_port = static_cast<int64_t>(event->isolate()->main_port());
  switch (event->kind()) {
    case ServiceEvent::kPauseStart:
      OS::PrintErr("vm-service: isolate(%" Pd64
                   ") '%s' has no debugger attached and is paused at start.",
                   main_port, name);
      break;
    case ServiceEvent::kPauseExit:
      OS::PrintErr("vm-service: isolate(%" Pd64
                   ")  '%s' has no debugger attached and is paused at exit.",
                   main_port, name);
      break;
    case ServiceEvent::kPauseException:
      OS::PrintErr(
          "vm-service: isolate (%" Pd64
          ") '%s' has no debugger attached and is paused due to exception.",
          main_port, name);
      break;
    case ServiceEvent::kPauseInterrupted:
      OS::PrintErr(
          "vm-service: isolate (%" Pd64
          ") '%s' has no debugger attached and is paused due to interrupt.",
          main_port, name);
      break;
    case ServiceEvent::kPauseBreakpoint:
      OS::PrintErr("vm-service: isolate (%" Pd64
                   ") '%s' has no debugger attached and is paused.",
                   main_port, name);
      break;
    case ServiceEvent::kPausePostRequest:
      OS::PrintErr("vm-service: isolate (%" Pd64
                   ") '%s' has no debugger attached and is paused post reload.",
                   main_port, name);
      break;
    default:
      UNREACHABLE();
      break;
  }
  if (!ServiceIsolate::IsRunning()) {
    OS::PrintErr("  Start the vm-service to debug.\n");
  } else if (ServiceIsolate::server_address() == nullptr) {
    OS::PrintErr("  Connect to the Dart VM service to debug.\n");
  } else {
    OS::PrintErr("  Connect to the Dart VM service at %s to debug.\n",
                 ServiceIsolate::server_address());
  }
  const Error& err = Error::Handle(Thread::Current()->sticky_error());
  if (!err.IsNull()) {
    OS::PrintErr("%s\n", err.ToErrorCString());
  }
}

void Service::HandleEvent(ServiceEvent* event, bool enter_safepoint) {
  if (event->stream_info() != nullptr && !event->stream_info()->enabled()) {
    if (FLAG_warn_on_pause_with_no_debugger && event->IsPause()) {
      // If we are about to pause a running program which has no
      // debugger connected, tell the user about it.
      ReportPauseOnConsole(event);
    }
    // Ignore events when no one is listening to the event stream.
    return;
  } else if (event->stream_info() != nullptr &&
             FLAG_warn_on_pause_with_no_debugger && event->IsPause()) {
    ReportPauseOnConsole(event);
  }
  if (!ServiceIsolate::IsRunning()) {
    return;
  }
  JSONStream js;
  // When a Service event needs to include temporary object IDs, these IDs will
  // be allocated in the default ID zone of the isolate associated with the
  // Service event.
  if (event->isolate() != nullptr) {
    js.set_id_zone(event->isolate()->EnsureDefaultServiceIdZone());
  }
  if (event->stream_info() != nullptr) {
    js.set_include_private_members(
        event->stream_info()->include_private_members());
  }
  const char* stream_id = event->stream_id();
  ASSERT(stream_id != nullptr);
  {
    JSONObject jsobj(&js);
    jsobj.AddProperty("jsonrpc", "2.0");
    jsobj.AddProperty("method", "streamNotify");
    JSONObject params(&jsobj, "params");
    params.AddProperty("streamId", stream_id);
    params.AddProperty("event", event);
  }
  PostEvent(event->isolate_group(), event->isolate(), stream_id,
            event->KindAsCString(), &js, enter_safepoint);
}

void Service::PostEvent(IsolateGroup* isolate_group,
                        Isolate* isolate,
                        const char* stream_id,
                        const char* kind,
                        JSONStream* event,
                        bool enter_safepoint) {
  if (enter_safepoint) {
    // Enter a safepoint so we don't block the mutator while processing
    // large events.
    TransitionToNative transition(Thread::Current());
    PostEventImpl(isolate_group, isolate, stream_id, kind, event);
    return;
  }
  PostEventImpl(isolate_group, isolate, stream_id, kind, event);
}

void Service::PostEventImpl(IsolateGroup* isolate_group,
                            Isolate* isolate,
                            const char* stream_id,
                            const char* kind,
                            JSONStream* event) {
  ASSERT(stream_id != nullptr);
  ASSERT(kind != nullptr);
  ASSERT(event != nullptr);

  if (FLAG_trace_service) {
    if (isolate != nullptr) {
      ASSERT(isolate_group != nullptr);
      OS::PrintErr(
          "vm-service: Pushing "
          "ServiceEvent(isolateGroupId='" ISOLATE_GROUP_SERVICE_ID_FORMAT_STRING
          "', isolate='%s', isolateId='" ISOLATE_SERVICE_ID_FORMAT_STRING
          "', kind='%s') to stream %s\n",
          isolate_group->id(), isolate->name(),
          static_cast<int64_t>(isolate->main_port()), kind, stream_id);
    } else if (isolate_group != nullptr) {
      OS::PrintErr(
          "vm-service: Pushing "
          "ServiceEvent(isolateGroupId='" ISOLATE_GROUP_SERVICE_ID_FORMAT_STRING
          "', kind='%s') to stream %s\n",
          isolate_group->id(), kind, stream_id);
    } else {
      OS::PrintErr(
          "vm-service: Pushing ServiceEvent(isolate='<no current isolate>', "
          "kind='%s') to stream %s\n",
          kind, stream_id);
    }
  }

  Service::LogResponseSize(kind, event);

  // Message is of the format [<stream id>, <json string>].
  //
  // Build the event message in the C heap to avoid dart heap
  // allocation.  This method can be called while we have acquired a
  // direct pointer to typed data, so we can't allocate here.
  Dart_CObject list_cobj;
  Dart_CObject* list_values[2];
  list_cobj.type = Dart_CObject_kArray;
  list_cobj.value.as_array.length = 2;
  list_cobj.value.as_array.values = list_values;

  Dart_CObject stream_id_cobj;
  stream_id_cobj.type = Dart_CObject_kString;
  stream_id_cobj.value.as_string = const_cast<char*>(stream_id);
  list_values[0] = &stream_id_cobj;

  Dart_CObject json_cobj;
  json_cobj.type = Dart_CObject_kString;
  json_cobj.value.as_string = const_cast<char*>(event->ToCString());
  list_values[1] = &json_cobj;

  AllocOnlyStackZone zone;
  std::unique_ptr<Message> msg =
      WriteApiMessage(zone.GetZone(), &list_cobj, ServiceIsolate::Port(),
                      Message::kNormalPriority);
  if (msg != nullptr) {
    PortMap::PostMessage(std::move(msg));
  }
}

class EmbedderServiceHandler {
 public:
  explicit EmbedderServiceHandler(const char* name)
      : name_(nullptr),
        callback_(nullptr),
        user_data_(nullptr),
        next_(nullptr) {
    ASSERT(name != nullptr);
    name_ = Utils::StrDup(name);
  }

  ~EmbedderServiceHandler() { free(name_); }

  const char* name() const { return name_; }

  Dart_ServiceRequestCallback callback() const { return callback_; }
  void set_callback(Dart_ServiceRequestCallback callback) {
    callback_ = callback;
  }

  void* user_data() const { return user_data_; }
  void set_user_data(void* user_data) { user_data_ = user_data; }

  EmbedderServiceHandler* next() const { return next_; }
  void set_next(EmbedderServiceHandler* next) { next_ = next; }

 private:
  char* name_;
  Dart_ServiceRequestCallback callback_;
  void* user_data_;
  EmbedderServiceHandler* next_;
};

void Service::EmbedderHandleMessage(EmbedderServiceHandler* handler,
                                    JSONStream* js) {
  ASSERT(handler != nullptr);
  Dart_ServiceRequestCallback callback = handler->callback();
  ASSERT(callback != nullptr);
  const char* response = nullptr;
  bool success;
  {
    TransitionVMToNative transition(Thread::Current());
    success = callback(js->method(), js->param_keys(), js->param_values(),
                       js->num_params(), handler->user_data(), &response);
  }
  ASSERT(response != nullptr);
  if (!success) {
    js->SetupError();
  }
  js->buffer()->AddString(response);
  js->PostReply();
  free(const_cast<char*>(response));
}

void Service::RegisterIsolateEmbedderCallback(
    const char* name,
    Dart_ServiceRequestCallback callback,
    void* user_data) {
  if (name == nullptr) {
    return;
  }
  EmbedderServiceHandler* handler = FindIsolateEmbedderHandler(name);
  if (handler != nullptr) {
    // Update existing handler entry.
    handler->set_callback(callback);
    handler->set_user_data(user_data);
    return;
  }
  // Create a new handler.
  handler = new EmbedderServiceHandler(name);
  handler->set_callback(callback);
  handler->set_user_data(user_data);

  // Insert into isolate_service_handler_head_ list.
  handler->set_next(isolate_service_handler_head_);
  isolate_service_handler_head_ = handler;
}

EmbedderServiceHandler* Service::FindIsolateEmbedderHandler(const char* name) {
  EmbedderServiceHandler* current = isolate_service_handler_head_;
  while (current != nullptr) {
    if (strcmp(name, current->name()) == 0) {
      return current;
    }
    current = current->next();
  }
  return nullptr;
}

void Service::RegisterRootEmbedderCallback(const char* name,
                                           Dart_ServiceRequestCallback callback,
                                           void* user_data) {
  if (name == nullptr) {
    return;
  }
  EmbedderServiceHandler* handler = FindRootEmbedderHandler(name);
  if (handler != nullptr) {
    // Update existing handler entry.
    handler->set_callback(callback);
    handler->set_user_data(user_data);
    return;
  }
  // Create a new handler.
  handler = new EmbedderServiceHandler(name);
  handler->set_callback(callback);
  handler->set_user_data(user_data);

  // Insert into root_service_handler_head_ list.
  handler->set_next(root_service_handler_head_);
  root_service_handler_head_ = handler;
}

void Service::SetEmbedderStreamCallbacks(
    Dart_ServiceStreamListenCallback listen_callback,
    Dart_ServiceStreamCancelCallback cancel_callback) {
  stream_listen_callback_ = listen_callback;
  stream_cancel_callback_ = cancel_callback;
}

void Service::SetEmbedderInformationCallback(
    Dart_EmbedderInformationCallback callback) {
  embedder_information_callback_ = callback;
}

int64_t Service::CurrentRSS() {
  if (embedder_information_callback_ == nullptr) {
    return -1;
  }
  Dart_EmbedderInformation info = {
      0,        // version
      nullptr,  // name
      0,        // max_rss
      0         // current_rss
  };
  embedder_information_callback_(&info);
  ASSERT(info.version == DART_EMBEDDER_INFORMATION_CURRENT_VERSION);
  return info.current_rss;
}

int64_t Service::MaxRSS() {
  if (embedder_information_callback_ == nullptr) {
    return -1;
  }
  Dart_EmbedderInformation info = {
      0,        // version
      nullptr,  // name
      0,        // max_rss
      0         // current_rss
  };
  embedder_information_callback_(&info);
  ASSERT(info.version == DART_EMBEDDER_INFORMATION_CURRENT_VERSION);
  return info.max_rss;
}

void Service::SetDartLibraryKernelForSources(const uint8_t* kernel_bytes,
                                             intptr_t kernel_length) {
  dart_library_kernel_ = kernel_bytes;
  dart_library_kernel_len_ = kernel_length;
}

EmbedderServiceHandler* Service::FindRootEmbedderHandler(const char* name) {
  EmbedderServiceHandler* current = root_service_handler_head_;
  while (current != nullptr) {
    if (strcmp(name, current->name()) == 0) {
      return current;
    }
    current = current->next();
  }
  return nullptr;
}

void Service::ScheduleExtensionHandler(const Instance& handler,
                                       const String& method_name,
                                       const Array& parameter_keys,
                                       const Array& parameter_values,
                                       const Instance& reply_port,
                                       const Instance& id) {
  ASSERT(!handler.IsNull());
  ASSERT(!method_name.IsNull());
  ASSERT(!parameter_keys.IsNull());
  ASSERT(!parameter_values.IsNull());
  ASSERT(!reply_port.IsNull());
  Isolate* isolate = Isolate::Current();
  ASSERT(isolate != nullptr);
  isolate->AppendServiceExtensionCall(handler, method_name, parameter_keys,
                                      parameter_values, reply_port, id);
}

static const MethodParameter* const get_isolate_params[] = {
    ISOLATE_PARAMETER,
    nullptr,
};

static void GetIsolate(Thread* thread, JSONStream* js) {
  thread->isolate()->PrintJSON(js, false);
}

static const MethodParameter* const get_isolate_group_params[] = {
    ISOLATE_GROUP_PARAMETER,
    nullptr,
};

enum SentinelType {
  kCollectedSentinel,
  kExpiredSentinel,
  kFreeSentinel,
};

static void PrintSentinel(JSONStream* js, SentinelType sentinel_type) {
  JSONObject jsobj(js);
  jsobj.AddProperty("type", "Sentinel");
  switch (sentinel_type) {
    case kCollectedSentinel:
      jsobj.AddProperty("kind", "Collected");
      jsobj.AddProperty("valueAsString", "<collected>");
      break;
    case kExpiredSentinel:
      jsobj.AddProperty("kind", "Expired");
      jsobj.AddProperty("valueAsString", "<expired>");
      break;
    case kFreeSentinel:
      jsobj.AddProperty("kind", "Free");
      jsobj.AddProperty("valueAsString", "<free>");
      break;
    default:
      UNIMPLEMENTED();
      break;
  }
}

static const MethodParameter* const
    set_stream_include_private_members_params[] = {
        NO_ISOLATE_PARAMETER,
        new BoolParameter("includePrivateMembers", true),
        nullptr,
};

static void SetStreamIncludePrivateMembers(Thread* thread, JSONStream* js) {
  const char* stream_id = js->LookupParam("streamId");
  if (stream_id == nullptr) {
    PrintMissingParamError(js, "streamId");
    return;
  }
  bool include_private_members =
      BoolParameter::Parse(js->LookupParam("includePrivateMembers"), false);
  intptr_t num_streams = sizeof(streams_) / sizeof(streams_[0]);
  for (intptr_t i = 0; i < num_streams; i++) {
    if (strcmp(stream_id, streams_[i]->id()) == 0) {
      streams_[i]->set_include_private_members(include_private_members);
      break;
    }
  }
  PrintSuccess(js);
}

static void ActOnIsolateGroup(JSONStream* js,
                              std::function<void(IsolateGroup*)> visitor) {
  const String& prefix =
      String::Handle(String::New(ISOLATE_GROUP_SERVICE_ID_PREFIX));

  const String& s =
      String::Handle(String::New(js->LookupParam("isolateGroupId")));
  if (!s.StartsWith(prefix)) {
    PrintInvalidParamError(js, "isolateGroupId");
    return;
  }
  Dart_Port isolate_group_id = Int64Parameter::Parse(
      String::Handle(String::SubString(s, prefix.Length())).ToCString());
  IsolateGroup::RunWithIsolateGroup(
      isolate_group_id,
      [&visitor](IsolateGroup* isolate_group) { visitor(isolate_group); },
      /*not_found=*/[&js]() { PrintSentinel(js, kExpiredSentinel); });
}

static void GetIsolateGroup(Thread* thread, JSONStream* js) {
  ActOnIsolateGroup(js, [&](IsolateGroup* isolate_group) {
    isolate_group->PrintJSON(js, false);
  });
}

static const MethodParameter* const get_memory_usage_params[] = {
    ISOLATE_PARAMETER,
    nullptr,
};

static void GetMemoryUsage(Thread* thread, JSONStream* js) {
  thread->isolate()->PrintMemoryUsageJSON(js);
}

static const MethodParameter* const get_isolate_group_memory_usage_params[] = {
    ISOLATE_GROUP_PARAMETER,
    nullptr,
};

static void GetIsolateGroupMemoryUsage(Thread* thread, JSONStream* js) {
  ActOnIsolateGroup(js, [&](IsolateGroup* isolate_group) {
    isolate_group->PrintMemoryUsageJSON(js);
  });
}

static const MethodParameter* const get_isolate_pause_event_params[] = {
    ISOLATE_PARAMETER,
    nullptr,
};

static void GetIsolatePauseEvent(Thread* thread, JSONStream* js) {
  thread->isolate()->PrintPauseEventJSON(js);
}

static const MethodParameter* const get_scripts_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void GetScripts(Thread* thread, JSONStream* js) {
  auto object_store = thread->isolate_group()->object_store();
  Zone* zone = thread->zone();

  const auto& libs =
      GrowableObjectArray::Handle(zone, object_store->libraries());
  intptr_t num_libs = libs.Length();

  Library& lib = Library::Handle(zone);
  Array& scripts = Array::Handle(zone);
  Script& script = Script::Handle(zone);

  JSONObject jsobj(js);
  {
    jsobj.AddProperty("type", "ScriptList");
    JSONArray script_array(&jsobj, "scripts");
    for (intptr_t i = 0; i < num_libs; i++) {
      lib ^= libs.At(i);
      ASSERT(!lib.IsNull());
      scripts = lib.LoadedScripts();
      for (intptr_t j = 0; j < scripts.Length(); j++) {
        script ^= scripts.At(j);
        ASSERT(!script.IsNull());
        script_array.AddValue(script);
      }
    }
  }
}

static const MethodParameter* const get_stack_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new UIntParameter("limit", false),
    nullptr,
};

static void GetStack(Thread* thread, JSONStream* js) {
  if (CheckDebuggerDisabled(thread, js)) {
    return;
  }
  intptr_t limit = 0;
  bool has_limit = js->HasParam("limit");
  if (has_limit) {
    limit = UIntParameter::Parse(js->LookupParam("limit"));
    if (limit < 0) {
      PrintInvalidParamError(js, "limit");
      return;
    }
  }
  Isolate* isolate = thread->isolate();
  DebuggerStackTrace* stack = isolate->debugger()->StackTrace();
  DebuggerStackTrace* async_awaiter_stack =
      isolate->debugger()->AsyncAwaiterStackTrace();

  // Do we want the complete script object and complete local variable objects?
  // This is true for dump requests.
  JSONObject jsobj(js);
  jsobj.AddProperty("type", "Stack");
  {
    JSONArray jsarr(&jsobj, "frames");

    intptr_t num_frames =
        has_limit ? Utils::Minimum(stack->Length(), limit) : stack->Length();

    for (intptr_t i = 0; i < num_frames; i++) {
      ActivationFrame* frame = stack->FrameAt(i);
      JSONObject jsobj(&jsarr);
      frame->PrintToJSONObject(&jsobj);
      jsobj.AddProperty("index", i);
    }
  }

  if (async_awaiter_stack != nullptr) {
    JSONArray jsarr(&jsobj, "asyncCausalFrames");
    intptr_t num_frames =
        has_limit ? Utils::Minimum(async_awaiter_stack->Length(), limit)
                  : async_awaiter_stack->Length();
    for (intptr_t i = 0; i < num_frames; i++) {
      ActivationFrame* frame = async_awaiter_stack->FrameAt(i);
      JSONObject jsobj(&jsarr);
      frame->PrintToJSONObject(&jsobj);
      jsobj.AddProperty("index", i);
    }
  }

  const bool truncated =
      (has_limit &&
       (limit < stack->Length() || (async_awaiter_stack != nullptr &&
                                    limit < async_awaiter_stack->Length())));
  jsobj.AddProperty("truncated", truncated);

  {
    // Deprecated and always empty since protocol version 4.22.
    JSONArray jsarr(&jsobj, "messages");
  }
}

static void HandleCommonEcho(JSONObject* jsobj, JSONStream* js) {
  jsobj->AddProperty("type", "_EchoResponse");
  if (js->HasParam("text")) {
    jsobj->AddProperty("text", js->LookupParam("text"));
  }
}

void Service::SendEchoEvent(Isolate* isolate, const char* text) {
  JSONStream js;
  {
    JSONObject jsobj(&js);
    jsobj.AddProperty("jsonrpc", "2.0");
    jsobj.AddProperty("method", "streamNotify");
    {
      JSONObject params(&jsobj, "params");
      params.AddProperty("streamId", echo_stream.id());
      {
        JSONObject event(&params, "event");
        event.AddProperty("type", "Event");
        event.AddProperty("kind", "_Echo");
        event.AddProperty("isolate", isolate);
        if (text != nullptr) {
          event.AddProperty("text", text);
        }
        event.AddPropertyTimeMillis("timestamp", OS::GetCurrentTimeMillis());
      }
    }
  }

  intptr_t reservation = js.buffer()->length() + sizeof(int32_t);
  intptr_t data_size = reservation + 3;
  uint8_t* data = reinterpret_cast<uint8_t*>(malloc(data_size));
  data[reservation + 0] = 0;
  data[reservation + 1] = 128;
  data[reservation + 2] = 255;
  SendEventWithData(echo_stream.id(), "_Echo", reservation,
                    js.buffer()->buffer(), js.buffer()->length(), data,
                    data_size);
}

static void TriggerEchoEvent(Thread* thread, JSONStream* js) {
  if (Service::echo_stream.enabled()) {
    Service::SendEchoEvent(thread->isolate(), js->LookupParam("text"));
  }
  JSONObject jsobj(js);
  HandleCommonEcho(&jsobj, js);
}

static void Echo(Thread* thread, JSONStream* js) {
  JSONObject jsobj(js);
  HandleCommonEcho(&jsobj, js);
}

static bool ContainsNonInstance(const Object& obj) {
  if (obj.IsArray()) {
    const Array& array = Array::Cast(obj);
    Object& element = Object::Handle();
    for (intptr_t i = 0; i < array.Length(); ++i) {
      element = array.At(i);
      if (!(element.IsInstance() || element.IsNull())) {
        return true;
      }
    }
    return false;
  } else if (obj.IsGrowableObjectArray()) {
    const GrowableObjectArray& array = GrowableObjectArray::Cast(obj);
    Object& element = Object::Handle();
    for (intptr_t i = 0; i < array.Length(); ++i) {
      element = array.At(i);
      if (!(element.IsInstance() || element.IsNull())) {
        return true;
      }
    }
    return false;
  } else {
    return !(obj.IsInstance() || obj.IsNull());
  }
}

static ObjectPtr LookUpObjectByServiceId(
    Thread* thread,
    const char* id_zone_part_of_service_id_as_string,
    const char* object_part_of_service_id_as_string,
    ObjectIdRing::LookupResult* kind) {
  ASSERT(thread != nullptr);

  // First, check if the provided ID is a fixed Service ID.
  *kind = ObjectIdRing::kValid;
  if (strncmp(object_part_of_service_id_as_string, "int-", 4) == 0) {
    object_part_of_service_id_as_string += 4;
    int64_t value = 0;
    if (!OS::StringToInt64(object_part_of_service_id_as_string, &value) ||
        !Smi::IsValid(value)) {
      *kind = ObjectIdRing::kInvalid;
      return Object::null();
    }
    const Integer& obj =
        Integer::Handle(thread->zone(), Smi::New(static_cast<intptr_t>(value)));
    return obj.ptr();
  } else if (strcmp(object_part_of_service_id_as_string, "bool-true") == 0) {
    return Bool::True().ptr();
  } else if (strcmp(object_part_of_service_id_as_string, "bool-false") == 0) {
    return Bool::False().ptr();
  } else if (strcmp(object_part_of_service_id_as_string, "null") == 0) {
    return Object::null();
  }

  // The provided ID is not a fixed Service ID, now we will interpret the ID as
  // a temporary Service ID. Temporary Service IDs of objects look like
  // "objects/1123/4", where 4 is the ID of the ID Zone where the ID was
  // allocated, and 1123 is the part that identifies an object within zone 4.
  intptr_t id_zone_part_of_service_id = -1;
  if (!GetIntegerId(id_zone_part_of_service_id_as_string,
                    &id_zone_part_of_service_id)) {
    *kind = ObjectIdRing::kInvalid;
    return Object::null();
  }
  intptr_t object_part_of_service_id = -1;
  if (!GetIntegerId(object_part_of_service_id_as_string,
                    &object_part_of_service_id)) {
    *kind = ObjectIdRing::kInvalid;
    return Object::null();
  }

  ASSERT(thread->isolate() != nullptr);
  const Isolate& isolate = *thread->isolate();
  RingServiceIdZone* id_zone =
      isolate.GetServiceIdZone(id_zone_part_of_service_id);
  if (id_zone == nullptr) {
    *kind = ObjectIdRing::kInvalid;
    return Object::null();
  }
  return id_zone->GetObjectForId(object_part_of_service_id, kind);
}

static ObjectPtr LookupClassMembers(Thread* thread,
                                    const Class& klass,
                                    char** parts,
                                    int num_parts) {
  auto zone = thread->zone();

  if (num_parts != 4) {
    return Object::sentinel().ptr();
  }

  const char* encoded_id = parts[3];
  auto& id = String::Handle(String::New(encoded_id));
  id = String::DecodeIRI(id);
  if (id.IsNull()) {
    return Object::sentinel().ptr();
  }

  if (strcmp(parts[2], "fields") == 0) {
    // Field ids look like: "classes/17/fields/name"
    const auto& field = Field::Handle(klass.LookupField(id));
    if (field.IsNull()) {
      return Object::sentinel().ptr();
    }
    return field.ptr();
  }
  if (strcmp(parts[2], "field_inits") == 0) {
    // Field initializer ids look like: "classes/17/field_inits/name"
    const auto& field = Field::Handle(klass.LookupField(id));
    if (field.IsNull() || (field.is_late() && !field.has_initializer())) {
      return Object::sentinel().ptr();
    }
    const auto& function = Function::Handle(field.EnsureInitializerFunction());
    if (function.IsNull()) {
      return Object::sentinel().ptr();
    }
    return function.ptr();
  }
  if (strcmp(parts[2], "functions") == 0) {
    // Function ids look like: "classes/17/functions/name"

    const auto& function =
        Function::Handle(Resolver::ResolveFunction(zone, klass, id));
    if (function.IsNull()) {
      return Object::sentinel().ptr();
    }
    return function.ptr();
  }
  if (strcmp(parts[2], "implicit_closures") == 0) {
    // Function ids look like: "classes/17/implicit_closures/11"
    intptr_t id;
    if (!GetIntegerId(parts[3], &id)) {
      return Object::sentinel().ptr();
    }
    const auto& func =
        Function::Handle(zone, klass.ImplicitClosureFunctionFromIndex(id));
    if (func.IsNull()) {
      return Object::sentinel().ptr();
    }
    return func.ptr();
  }
  if (strcmp(parts[2], "dispatchers") == 0) {
    // Dispatcher Function ids look like: "classes/17/dispatchers/11"
    intptr_t id;
    if (!GetIntegerId(parts[3], &id)) {
      return Object::sentinel().ptr();
    }
    const auto& func =
        Function::Handle(zone, klass.InvocationDispatcherFunctionFromIndex(id));
    if (func.IsNull()) {
      return Object::sentinel().ptr();
    }
    return func.ptr();
  }
  if (strcmp(parts[2], "closures") == 0) {
    // Closure ids look like: "classes/17/closures/11"
    intptr_t id;
    if (!GetIntegerId(parts[3], &id)) {
      return Object::sentinel().ptr();
    }
    Function& func = Function::Handle(zone);
    func = ClosureFunctionsCache::ClosureFunctionFromIndex(id);
    if (func.IsNull()) {
      return Object::sentinel().ptr();
    }
    return func.ptr();
  }

  UNREACHABLE();
  return Object::sentinel().ptr();
}

static ObjectPtr LookupHeapObjectLibraries(IsolateGroup* isolate_group,
                                           char** parts,
                                           int num_parts) {
  // Library ids look like "libraries/35"
  if (num_parts < 2) {
    return Object::sentinel().ptr();
  }
  const auto& libs =
      GrowableObjectArray::Handle(isolate_group->object_store()->libraries());
  ASSERT(!libs.IsNull());
  const String& id = String::Handle(String::New(parts[1]));
  // Scan for private key.
  String& private_key = String::Handle();
  Library& lib = Library::Handle();
  bool lib_found = false;
  for (intptr_t i = 0; i < libs.Length(); i++) {
    lib ^= libs.At(i);
    ASSERT(!lib.IsNull());
    private_key = lib.private_key();
    if (private_key.Equals(id)) {
      lib_found = true;
      break;
    }
  }
  if (!lib_found) {
    return Object::sentinel().ptr();
  }

  const auto& klass = Class::Handle(lib.toplevel_class());
  ASSERT(!klass.IsNull());

  if (num_parts == 2) {
    return lib.ptr();
  }
  if (strcmp(parts[2], "fields") == 0) {
    // Library field ids look like: "libraries/17/fields/name"
    return LookupClassMembers(Thread::Current(), klass, parts, num_parts);
  }
  if (strcmp(parts[2], "field_inits") == 0) {
    // Library field ids look like: "libraries/17/field_inits/name"
    return LookupClassMembers(Thread::Current(), klass, parts, num_parts);
  }
  if (strcmp(parts[2], "functions") == 0) {
    // Library function ids look like: "libraries/17/functions/name"
    return LookupClassMembers(Thread::Current(), klass, parts, num_parts);
  }
  if (strcmp(parts[2], "closures") == 0) {
    // Library function ids look like: "libraries/17/closures/name"
    return LookupClassMembers(Thread::Current(), klass, parts, num_parts);
  }
  if (strcmp(parts[2], "implicit_closures") == 0) {
    // Library function ids look like: "libraries/17/implicit_closures/name"
    return LookupClassMembers(Thread::Current(), klass, parts, num_parts);
  }

  if (strcmp(parts[2], "scripts") == 0) {
    // Script ids look like "libraries/35/scripts/library%2Furl.dart/12345"
    if (num_parts != 5) {
      return Object::sentinel().ptr();
    }
    const String& id = String::Handle(String::New(parts[3]));
    ASSERT(!id.IsNull());
    // The id is the url of the script % encoded, decode it.
    const String& requested_url = String::Handle(String::DecodeIRI(id));

    // Each script id is tagged with a load time.
    int64_t timestamp;
    if (!GetInteger64Id(parts[4], &timestamp, 16) || (timestamp < 0)) {
      return Object::sentinel().ptr();
    }

    Script& script = Script::Handle();
    String& script_url = String::Handle();
    const Array& loaded_scripts = Array::Handle(lib.LoadedScripts());
    ASSERT(!loaded_scripts.IsNull());
    intptr_t i;
    for (i = 0; i < loaded_scripts.Length(); i++) {
      script ^= loaded_scripts.At(i);
      ASSERT(!script.IsNull());
      script_url = script.url();
      if (script_url.Equals(requested_url) &&
          (timestamp == script.load_timestamp())) {
        return script.ptr();
      }
    }
  }

  // Not found.
  return Object::sentinel().ptr();
}

static ObjectPtr LookupHeapObjectClasses(Thread* thread,
                                         char** parts,
                                         int num_parts) {
  // Class ids look like: "classes/17"
  if (num_parts < 2) {
    return Object::sentinel().ptr();
  }
  Zone* zone = thread->zone();
  auto table = thread->isolate_group()->class_table();
  intptr_t id;
  if (!GetIntegerId(parts[1], &id) || !table->IsValidIndex(id)) {
    return Object::sentinel().ptr();
  }
  Class& cls = Class::Handle(zone, table->At(id));
  if (num_parts == 2) {
    return cls.ptr();
  }
  if (strcmp(parts[2], "closures") == 0) {
    // Closure ids look like: "classes/17/closures/11"
    return LookupClassMembers(thread, cls, parts, num_parts);
  } else if (strcmp(parts[2], "field_inits") == 0) {
    // Field initializer ids look like: "classes/17/field_inits/name"
    return LookupClassMembers(thread, cls, parts, num_parts);
  } else if (strcmp(parts[2], "fields") == 0) {
    // Field ids look like: "classes/17/fields/name"
    return LookupClassMembers(thread, cls, parts, num_parts);
  } else if (strcmp(parts[2], "functions") == 0) {
    // Function ids look like: "classes/17/functions/name"
    return LookupClassMembers(thread, cls, parts, num_parts);
  } else if (strcmp(parts[2], "implicit_closures") == 0) {
    // Function ids look like: "classes/17/implicit_closures/11"
    return LookupClassMembers(thread, cls, parts, num_parts);
  } else if (strcmp(parts[2], "dispatchers") == 0) {
    // Dispatcher Function ids look like: "classes/17/dispatchers/11"
    return LookupClassMembers(thread, cls, parts, num_parts);
  } else if (strcmp(parts[2], "types") == 0) {
    // Type ids look like: "classes/17/types/11"
    if (num_parts != 4) {
      return Object::sentinel().ptr();
    }
    intptr_t id;
    if (!GetIntegerId(parts[3], &id)) {
      return Object::sentinel().ptr();
    }
    if (id != 0) {
      return Object::sentinel().ptr();
    }
    const Type& type = Type::Handle(zone, cls.DeclarationType());
    if (!type.IsNull()) {
      return type.ptr();
    }
  }

  // Not found.
  return Object::sentinel().ptr();
}

static ObjectPtr LookupHeapObjectTypeArguments(Thread* thread,
                                               char** parts,
                                               int num_parts) {
  // TypeArguments ids look like: "typearguments/17"
  if (num_parts < 2) {
    return Object::sentinel().ptr();
  }
  intptr_t id;
  if (!GetIntegerId(parts[1], &id)) {
    return Object::sentinel().ptr();
  }
  ObjectStore* object_store = thread->isolate_group()->object_store();
  const Array& table =
      Array::Handle(thread->zone(), object_store->canonical_type_arguments());
  ASSERT(table.Length() > 0);
  const intptr_t table_size = table.Length() - 1;
  if ((id < 0) || (id >= table_size) || (table.At(id) == Object::null())) {
    return Object::sentinel().ptr();
  }
  return table.At(id);
}

static ObjectPtr LookupHeapObjectCode(char** parts, int num_parts) {
  if (num_parts != 2) {
    return Object::sentinel().ptr();
  }
  uword pc;
  const char* const kCollectedPrefix = "collected-";
  const intptr_t kCollectedPrefixLen = strlen(kCollectedPrefix);
  const char* const kNativePrefix = "native-";
  const intptr_t kNativePrefixLen = strlen(kNativePrefix);
  const char* const kReusedPrefix = "reused-";
  const intptr_t kReusedPrefixLen = strlen(kReusedPrefix);
  const char* id = parts[1];
  if (strncmp(kCollectedPrefix, id, kCollectedPrefixLen) == 0) {
    if (!GetUnsignedIntegerId(&id[kCollectedPrefixLen], &pc, 16)) {
      return Object::sentinel().ptr();
    }
    // TODO(turnidge): Return "collected" instead.
    return Object::null();
  }
  if (strncmp(kNativePrefix, id, kNativePrefixLen) == 0) {
    if (!GetUnsignedIntegerId(&id[kNativePrefixLen], &pc, 16)) {
      return Object::sentinel().ptr();
    }
    // TODO(johnmccutchan): Support native Code.
    return Object::null();
  }
  if (strncmp(kReusedPrefix, id, kReusedPrefixLen) == 0) {
    if (!GetUnsignedIntegerId(&id[kReusedPrefixLen], &pc, 16)) {
      return Object::sentinel().ptr();
    }
    // TODO(turnidge): Return "expired" instead.
    return Object::null();
  }
  int64_t timestamp = 0;
  if (!GetCodeId(id, &timestamp, &pc) || (timestamp < 0)) {
    return Object::sentinel().ptr();
  }
  Code& code = Code::Handle(Code::FindCode(pc, timestamp));
  if (!code.IsNull()) {
    return code.ptr();
  }
#if defined(DART_DYNAMIC_MODULES)
  const auto& bytecode = Bytecode::Handle(Bytecode::FindBytecode(pc));
  if (!bytecode.IsNull()) {
    return bytecode.ptr();
  }
#endif

  // Not found.
  return Object::sentinel().ptr();
}

static ObjectPtr LookupHeapObject(Thread* thread,
                                  const char* id_original,
                                  ObjectIdRing::LookupResult* result) {
  char* id = thread->zone()->MakeCopyOfString(id_original);

  // Parse the id by splitting at each '/'.
  const int MAX_PARTS = 8;
  char* parts[MAX_PARTS];
  int num_parts = 0;
  int i = 0;
  int start_pos = 0;
  while (id[i] != '\0') {
    if (id[i] == '/') {
      id[i++] = '\0';
      parts[num_parts++] = &id[start_pos];
      if (num_parts == MAX_PARTS) {
        break;
      }
      start_pos = i;
    } else {
      i++;
    }
  }
  if (num_parts < MAX_PARTS) {
    parts[num_parts++] = &id[start_pos];
  }

  if (result != nullptr) {
    *result = ObjectIdRing::kValid;
  }

  Isolate* isolate = thread->isolate();
  if (strcmp(parts[0], "objects") == 0) {
    Object& obj = Object::Handle(thread->zone());
    ObjectIdRing::LookupResult lookup_result;
    obj = LookUpObjectByServiceId(thread, parts[2], parts[1], &lookup_result);
    if (lookup_result != ObjectIdRing::kValid) {
      if (result != nullptr) {
        *result = lookup_result;
      }
      return Object::sentinel().ptr();
    }
    return obj.ptr();

  } else if (strcmp(parts[0], "libraries") == 0) {
    return LookupHeapObjectLibraries(isolate->group(), parts, num_parts);
  } else if (strcmp(parts[0], "classes") == 0) {
    return LookupHeapObjectClasses(thread, parts, num_parts);
  } else if (strcmp(parts[0], "typearguments") == 0) {
    return LookupHeapObjectTypeArguments(thread, parts, num_parts);
  } else if (strcmp(parts[0], "code") == 0) {
    return LookupHeapObjectCode(parts, num_parts);
  }

  // Not found.
  return Object::sentinel().ptr();
}

static Breakpoint* LookupBreakpoint(Isolate* isolate,
                                    const char* id,
                                    ObjectIdRing::LookupResult* result) {
  *result = ObjectIdRing::kInvalid;
  size_t end_pos = strcspn(id, "/");
  if (end_pos == strlen(id)) {
    return nullptr;
  }
  const char* rest = id + end_pos + 1;  // +1 for '/'.
  if (strncmp("breakpoints", id, end_pos) == 0) {
    intptr_t bpt_id = 0;
    Breakpoint* bpt = nullptr;
    if (GetIntegerId(rest, &bpt_id)) {
      bpt = isolate->debugger()->GetBreakpointById(bpt_id);
      if (bpt != nullptr) {
        *result = ObjectIdRing::kValid;
        return bpt;
      }
      if (bpt_id < isolate->debugger()->limitBreakpointId()) {
        *result = ObjectIdRing::kCollected;
        return nullptr;
      }
    }
  }
  return nullptr;
}

static inline void AddParentFieldToResponseBasedOnRecord(
    Thread* thread,
    Array* field_names_handle,
    String* name_handle,
    const JSONObject& jsresponse,
    const Record& record,
    const intptr_t field_slot_offset) {
  *field_names_handle = record.GetFieldNames(thread);
  const intptr_t num_positional_fields =
      record.num_fields() - field_names_handle->Length();
  const intptr_t field_index =
      (field_slot_offset - Record::field_offset(0)) / Record::kBytesPerElement;
  if (field_index < num_positional_fields) {
    jsresponse.AddProperty("parentField", field_index);
  } else {
    *name_handle ^= field_names_handle->At(field_index - num_positional_fields);
    jsresponse.AddProperty("parentField", name_handle->ToCString());
  }
}

static void PrintInboundReferences(Thread* thread,
                                   Object* target,
                                   intptr_t limit,
                                   JSONStream* js) {
  ObjectGraph graph(thread);
  Array& path = Array::Handle(Array::New(limit * 2));
  intptr_t length = graph.InboundReferences(target, path);
  OffsetsTable offsets_table(thread->zone());
  JSONObject jsobj(js);
  jsobj.AddProperty("type", "InboundReferences");
  {
    JSONArray elements(&jsobj, "references");
    Object& source = Object::Handle();
    Smi& slot_offset = Smi::Handle();
    Array& field_names = Array::Handle();
    String& name = String::Handle();
    Class& source_class = Class::Handle();
    Field& field = Field::Handle();
    Array& parent_field_map = Array::Handle();
    limit = Utils::Minimum(limit, length);
    for (intptr_t i = 0; i < limit; ++i) {
      JSONObject jselement(&elements);
      source = path.At(i * 2);
      slot_offset ^= path.At((i * 2) + 1);

      jselement.AddProperty("source", source);
      if (source.IsArray()) {
        intptr_t element_index =
            (slot_offset.Value() - Array::element_offset(0)) /
            Array::kBytesPerElement;
        jselement.AddProperty("parentListIndex", element_index);
        jselement.AddProperty("parentField", element_index);
      } else if (source.IsRecord()) {
        AddParentFieldToResponseBasedOnRecord(thread, &field_names, &name,
                                              jselement, Record::Cast(source),
                                              slot_offset.Value());
      } else {
        if (source.IsInstance()) {
          source_class = source.clazz();
          parent_field_map = source_class.OffsetToFieldMap();
          intptr_t index = slot_offset.Value() >> kCompressedWordSizeLog2;
          if (index > 0 && index < parent_field_map.Length()) {
            field ^= parent_field_map.At(index);
            if (!field.IsNull()) {
              jselement.AddProperty("parentField", field);
              continue;
            }
          }
        }
        const char* field_name = offsets_table.FieldNameForOffset(
            source.GetClassId(), slot_offset.Value());
        if (field_name != nullptr) {
          jselement.AddProperty("_parentWordOffset", slot_offset.Value());
          // TODO(vm-service): Adjust RPC type to allow returning a field name
          // without a field object, or reify the fields described by
          // raw_object_fields.cc
          // jselement.AddProperty("_parentFieldName", field_name);
        } else if (source.IsContext()) {
          intptr_t element_index =
              (slot_offset.Value() - Context::variable_offset(0)) /
              Context::kBytesPerElement;
          jselement.AddProperty("parentListIndex", element_index);
          jselement.AddProperty("parentField", element_index);
        } else {
          jselement.AddProperty("_parentWordOffset", slot_offset.Value());
        }
      }
    }
  }

  // We nil out the array after generating the response to prevent
  // reporting spurious references when repeatedly looking for the
  // references to an object.
  for (intptr_t i = 0; i < path.Length(); i++) {
    path.SetAt(i, Object::null_object());
  }
}

static const MethodParameter* const get_inbound_references_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void GetInboundReferences(Thread* thread, JSONStream* js) {
  const char* target_id = js->LookupParam("targetId");
  if (target_id == nullptr) {
    PrintMissingParamError(js, "targetId");
    return;
  }
  const char* limit_cstr = js->LookupParam("limit");
  if (limit_cstr == nullptr) {
    PrintMissingParamError(js, "limit");
    return;
  }
  intptr_t limit;
  if (!GetIntegerId(limit_cstr, &limit)) {
    PrintInvalidParamError(js, "limit");
    return;
  }

  Object& obj = Object::Handle(thread->zone());
  ObjectIdRing::LookupResult lookup_result;
  {
    HANDLESCOPE(thread);
    obj = LookupHeapObject(thread, target_id, &lookup_result);
  }
  if (obj.ptr() == Object::sentinel().ptr()) {
    if (lookup_result == ObjectIdRing::kCollected) {
      PrintSentinel(js, kCollectedSentinel);
    } else if (lookup_result == ObjectIdRing::kExpired) {
      PrintSentinel(js, kExpiredSentinel);
    } else {
      PrintInvalidParamError(js, "targetId");
    }
    return;
  }
  PrintInboundReferences(thread, &obj, limit, js);
}

static void PrintRetainingPath(Thread* thread,
                               Object* obj,
                               intptr_t limit,
                               JSONStream* js) {
  ObjectGraph graph(thread);
  Array& path = Array::Handle(Array::New(limit * 2));
  auto result = graph.RetainingPath(obj, path);
  intptr_t length = result.length;
  JSONObject jsobj(js);
  jsobj.AddProperty("type", "RetainingPath");
  jsobj.AddProperty("length", length);
  jsobj.AddProperty("gcRootType", result.gc_root_type);
  JSONArray elements(&jsobj, "elements");
  Object& element = Object::Handle();
  Smi& slot_offset = Smi::Handle();
  Array& field_names = Array::Handle();
  String& name = String::Handle();
  Class& element_class = Class::Handle();
  Array& element_field_map = Array::Handle();
  Map& map = Map::Handle();
  Array& map_data = Array::Handle();
  Field& field = Field::Handle();
  WeakProperty& wp = WeakProperty::Handle();
  limit = Utils::Minimum(limit, length);
  OffsetsTable offsets_table(thread->zone());
  for (intptr_t i = 0; i < limit; ++i) {
    JSONObject jselement(&elements);
    element = path.At(i * 2);
    jselement.AddProperty("value", element);
    // Interpret the word offset from parent as list index, map key,
    // weak property, or instance field.
    if (i > 0) {
      slot_offset ^= path.At((i * 2) - 1);
      if (element.IsArray() || element.IsGrowableObjectArray()) {
        intptr_t element_index =
            (slot_offset.Value() - Array::element_offset(0)) /
            Array::kBytesPerElement;
        jselement.AddProperty("parentListIndex", element_index);
        jselement.AddProperty("parentField", element_index);
      } else if (element.IsRecord()) {
        AddParentFieldToResponseBasedOnRecord(thread, &field_names, &name,
                                              jselement, Record::Cast(element),
                                              slot_offset.Value());
      } else if (element.IsMap()) {
        map = static_cast<MapPtr>(path.At(i * 2));
        map_data = map.data();
        intptr_t element_index =
            (slot_offset.Value() - Array::element_offset(0)) /
            Array::kBytesPerElement;
        Map::Iterator iterator(map);
        while (iterator.MoveNext()) {
          if (iterator.CurrentKey() == map_data.At(element_index) ||
              iterator.CurrentValue() == map_data.At(element_index)) {
            element = iterator.CurrentKey();
            jselement.AddProperty("parentMapKey", element);
            break;
          }
        }
      } else if (element.IsWeakProperty()) {
        wp ^= static_cast<WeakPropertyPtr>(element.ptr());
        element = wp.key();
        jselement.AddProperty("parentMapKey", element);
      } else {
        if (element.IsInstance()) {
          element_class = element.clazz();
          element_field_map = element_class.OffsetToFieldMap();
          intptr_t index = slot_offset.Value() >> kCompressedWordSizeLog2;
          if ((index > 0) && (index < element_field_map.Length())) {
            field ^= element_field_map.At(index);
            if (!field.IsNull()) {
              name ^= field.name();
              jselement.AddProperty("parentField", name.ToCString());
              continue;
            }
          }
        }
        const char* field_name = offsets_table.FieldNameForOffset(
            element.GetClassId(), slot_offset.Value());
        if (field_name != nullptr) {
          jselement.AddProperty("parentField", field_name);
        } else if (element.IsContext()) {
          intptr_t element_index =
              (slot_offset.Value() - Context::variable_offset(0)) /
              Context::kBytesPerElement;
          jselement.AddProperty("parentListIndex", element_index);
          jselement.AddProperty("parentField", element_index);
        } else {
          jselement.AddProperty("_parentWordOffset", slot_offset.Value());
        }
      }
    }
  }

  // We nil out the array after generating the response to prevent
  // reporting spurious references when looking for inbound references
  // after looking for a retaining path.
  for (intptr_t i = 0; i < path.Length(); i++) {
    path.SetAt(i, Object::null_object());
  }
}

static const MethodParameter* const get_retaining_path_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void GetRetainingPath(Thread* thread, JSONStream* js) {
  const char* target_id = js->LookupParam("targetId");
  if (target_id == nullptr) {
    PrintMissingParamError(js, "targetId");
    return;
  }
  const char* limit_cstr = js->LookupParam("limit");
  if (limit_cstr == nullptr) {
    PrintMissingParamError(js, "limit");
    return;
  }
  intptr_t limit;
  if (!GetIntegerId(limit_cstr, &limit)) {
    PrintInvalidParamError(js, "limit");
    return;
  }

  Object& obj = Object::Handle(thread->zone());
  ObjectIdRing::LookupResult lookup_result;
  {
    HANDLESCOPE(thread);
    obj = LookupHeapObject(thread, target_id, &lookup_result);
  }
  if (obj.ptr() == Object::sentinel().ptr()) {
    if (lookup_result == ObjectIdRing::kCollected) {
      PrintSentinel(js, kCollectedSentinel);
    } else if (lookup_result == ObjectIdRing::kExpired) {
      PrintSentinel(js, kExpiredSentinel);
    } else {
      PrintInvalidParamError(js, "targetId");
    }
    return;
  }
  PrintRetainingPath(thread, &obj, limit, js);
}

static const MethodParameter* const get_retained_size_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new IdParameter("targetId", true),
    nullptr,
};

static void GetRetainedSize(Thread* thread, JSONStream* js) {
  const char* target_id = js->LookupParam("targetId");
  ASSERT(target_id != nullptr);
  ObjectIdRing::LookupResult lookup_result;
  Object& obj =
      Object::Handle(LookupHeapObject(thread, target_id, &lookup_result));
  if (obj.ptr() == Object::sentinel().ptr()) {
    if (lookup_result == ObjectIdRing::kCollected) {
      PrintSentinel(js, kCollectedSentinel);
    } else if (lookup_result == ObjectIdRing::kExpired) {
      PrintSentinel(js, kExpiredSentinel);
    } else {
      PrintInvalidParamError(js, "targetId");
    }
    return;
  }
  // TODO(rmacnak): There is no way to get the size retained by a class object.
  // SizeRetainedByClass should be a separate RPC.
  if (obj.IsClass()) {
    const Class& cls = Class::Cast(obj);
    ObjectGraph graph(thread);
    intptr_t retained_size = graph.SizeRetainedByClass(cls.id());
    const Object& result = Object::Handle(Integer::New(retained_size));
    result.PrintJSON(js, true);
    return;
  }

  ObjectGraph graph(thread);
  intptr_t retained_size = graph.SizeRetainedByInstance(obj);
  const Object& result = Object::Handle(Integer::New(retained_size));
  result.PrintJSON(js, true);
}

static const MethodParameter* const get_reachable_size_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new IdParameter("targetId", true),
    nullptr,
};

static void GetReachableSize(Thread* thread, JSONStream* js) {
  const char* target_id = js->LookupParam("targetId");
  ASSERT(target_id != nullptr);
  ObjectIdRing::LookupResult lookup_result;
  Object& obj =
      Object::Handle(LookupHeapObject(thread, target_id, &lookup_result));
  if (obj.ptr() == Object::sentinel().ptr()) {
    if (lookup_result == ObjectIdRing::kCollected) {
      PrintSentinel(js, kCollectedSentinel);
    } else if (lookup_result == ObjectIdRing::kExpired) {
      PrintSentinel(js, kExpiredSentinel);
    } else {
      PrintInvalidParamError(js, "targetId");
    }
    return;
  }
  // TODO(rmacnak): There is no way to get the size retained by a class object.
  // SizeRetainedByClass should be a separate RPC.
  if (obj.IsClass()) {
    const Class& cls = Class::Cast(obj);
    ObjectGraph graph(thread);
    intptr_t retained_size = graph.SizeReachableByClass(cls.id());
    const Object& result = Object::Handle(Integer::New(retained_size));
    result.PrintJSON(js, true);
    return;
  }

  ObjectGraph graph(thread);
  intptr_t retained_size = graph.SizeReachableByInstance(obj);
  const Object& result = Object::Handle(Integer::New(retained_size));
  result.PrintJSON(js, true);
}

static const MethodParameter* const invalidate_id_zone_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void InvalidateIdZone(Thread* thread, JSONStream* js) {
  ASSERT(thread != nullptr);
  ASSERT(js != nullptr);

  Isolate* isolate = thread->isolate();
  ASSERT(isolate != nullptr);

  js->id_zone().Invalidate();

  PrintSuccess(js);
}

static const MethodParameter* const invoke_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void Invoke(Thread* thread, JSONStream* js) {
  const char* receiver_id = js->LookupParam("targetId");
  if (receiver_id == nullptr) {
    PrintMissingParamError(js, "targetId");
    return;
  }
  const char* selector_cstr = js->LookupParam("selector");
  if (selector_cstr == nullptr) {
    PrintMissingParamError(js, "selector");
    return;
  }
  const char* argument_ids = js->LookupParam("argumentIds");
  if (argument_ids == nullptr) {
    PrintMissingParamError(js, "argumentIds");
    return;
  }

#if !defined(DART_PRECOMPILED_RUNTIME)
  bool disable_breakpoints =
      BoolParameter::Parse(js->LookupParam("disableBreakpoints"), false);
  DisableBreakpointsScope db(thread->isolate()->debugger(),
                             disable_breakpoints);
#endif

  Zone* zone = thread->zone();
  ObjectIdRing::LookupResult lookup_result;
  Object& receiver = Object::Handle(
      zone, LookupHeapObject(thread, receiver_id, &lookup_result));
  if (receiver.ptr() == Object::sentinel().ptr()) {
    if (lookup_result == ObjectIdRing::kCollected) {
      PrintSentinel(js, kCollectedSentinel);
    } else if (lookup_result == ObjectIdRing::kExpired) {
      PrintSentinel(js, kExpiredSentinel);
    } else {
      PrintInvalidParamError(js, "targetId");
    }
    return;
  }

  const GrowableObjectArray& growable_args =
      GrowableObjectArray::Handle(zone, GrowableObjectArray::New());

  bool is_instance = (receiver.IsInstance() || receiver.IsNull()) &&
                     !ContainsNonInstance(receiver);
  if (is_instance) {
    growable_args.Add(receiver);
  }

  intptr_t n = strlen(argument_ids);
  if ((n < 2) || (argument_ids[0] != '[') || (argument_ids[n - 1] != ']')) {
    PrintInvalidParamError(js, "argumentIds");
    return;
  }
  if (n > 2) {
    intptr_t start = 1;
    while (start < n) {
      intptr_t end = start;
      while ((argument_ids[end + 1] != ',') && (argument_ids[end + 1] != ']')) {
        end++;
      }
      if (end == start) {
        // Empty element.
        PrintInvalidParamError(js, "argumentIds");
        return;
      }

      const char* argument_id =
          zone->MakeCopyOfStringN(&argument_ids[start], end - start + 1);

      ObjectIdRing::LookupResult lookup_result;
      Object& argument = Object::Handle(
          zone, LookupHeapObject(thread, argument_id, &lookup_result));
      // Invoke only accepts Instance arguments.
      if (!(argument.IsInstance() || argument.IsNull()) ||
          ContainsNonInstance(argument)) {
        PrintInvalidParamError(js, "argumentIds");
        return;
      }
      if (argument.ptr() == Object::sentinel().ptr()) {
        if (lookup_result == ObjectIdRing::kCollected) {
          PrintSentinel(js, kCollectedSentinel);
        } else if (lookup_result == ObjectIdRing::kExpired) {
          PrintSentinel(js, kExpiredSentinel);
        } else {
          PrintInvalidParamError(js, "argumentIds");
        }
        return;
      }
      growable_args.Add(argument);

      start = end + 3;
    }
  }

  const String& selector = String::Handle(zone, String::New(selector_cstr));
  const Array& args =
      Array::Handle(zone, Array::MakeFixedLength(growable_args));
  const Array& arg_names = Object::empty_array();
  // For debugging calls via vm-service, don't require entry point annotations.
  const bool check_is_entrypoint = false;

  if (receiver.IsLibrary()) {
    const Library& lib = Library::Cast(receiver);
    const Object& result = Object::Handle(
        zone, lib.Invoke(selector, args, arg_names, check_is_entrypoint));
    result.PrintJSON(js, true);
    return;
  }
  if (receiver.IsClass()) {
    const Class& cls = Class::Cast(receiver);
    const Object& result = Object::Handle(
        zone, cls.Invoke(selector, args, arg_names, check_is_entrypoint));
    result.PrintJSON(js, true);
    return;
  }
  if (is_instance) {
    // We don't use Instance::Cast here because it doesn't allow null.
    Instance& instance = Instance::Handle(zone);
    instance ^= receiver.ptr();
    const Object& result = Object::Handle(
        zone, instance.Invoke(selector, args, arg_names, check_is_entrypoint));
    result.PrintJSON(js, true);
    return;
  }
  js->PrintError(kInvalidParams,
                 "%s: invalid 'targetId' parameter: "
                 "Cannot invoke against a VM-internal object",
                 js->method());
}

static const MethodParameter* const evaluate_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static bool IsAlpha(char c) {
  return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
}
static bool IsAlphaOrDollar(char c) {
  return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '$');
}
static bool IsAlphaNum(char c) {
  return (c >= '0' && c <= '9') || IsAlpha(c);
}
static bool IsAlphaNumOrDollar(char c) {
  return (c >= '0' && c <= '9') || IsAlphaOrDollar(c);
}
static bool IsWhitespace(char c) {
  return c <= ' ';
}
static bool IsObjectIdChar(char c) {
  return IsAlphaNum(c) || c == '/' || c == '-' || c == '@' || c == '%';
}

// TODO(vm-service): Consider whether we should pass structured objects in
// service messages instead of always flattening them to C strings.
static bool ParseScope(const char* scope,
                       GrowableArray<const char*>* names,
                       GrowableArray<const char*>* ids) {
  Zone* zone = Thread::Current()->zone();
  const char* c = scope;
  if (*c++ != '{') return false;

  for (;;) {
    while (IsWhitespace(*c)) {
      c++;
    }

    if (*c == '}') return true;

    const char* name = c;
    if (!IsAlphaOrDollar(*c)) return false;
    while (IsAlphaNumOrDollar(*c)) {
      c++;
    }
    names->Add(zone->MakeCopyOfStringN(name, c - name));

    while (IsWhitespace(*c)) {
      c++;
    }

    if (*c++ != ':') return false;

    while (IsWhitespace(*c)) {
      c++;
    }

    const char* id = c;
    if (!IsObjectIdChar(*c)) return false;
    while (IsObjectIdChar(*c)) {
      c++;
    }
    ids->Add(zone->MakeCopyOfStringN(id, c - id));

    while (IsWhitespace(*c)) {
      c++;
    }
    if (*c == ',') c++;
  }

  return false;
}

static bool BuildScope(Thread* thread,
                       JSONStream* js,
                       const GrowableObjectArray& names,
                       const GrowableObjectArray& values) {
  const char* scope = js->LookupParam("scope");
  GrowableArray<const char*> cnames;
  GrowableArray<const char*> cids;
  if (scope != nullptr) {
    if (!ParseScope(scope, &cnames, &cids)) {
      PrintInvalidParamError(js, "scope");
      return true;
    }
    String& name = String::Handle();
    Object& obj = Object::Handle();
    for (intptr_t i = 0; i < cids.length(); i++) {
      ObjectIdRing::LookupResult lookup_result;
      obj = LookupHeapObject(thread, cids[i], &lookup_result);
      if (obj.ptr() == Object::sentinel().ptr()) {
        if (lookup_result == ObjectIdRing::kCollected) {
          PrintSentinel(js, kCollectedSentinel);
        } else if (lookup_result == ObjectIdRing::kExpired) {
          PrintSentinel(js, kExpiredSentinel);
        } else {
          PrintInvalidParamError(js, "targetId");
        }
        return true;
      }
      if ((!obj.IsInstance() && !obj.IsNull()) || ContainsNonInstance(obj)) {
        js->PrintError(kInvalidParams,
                       "%s: invalid scope 'targetId' parameter: "
                       "Cannot evaluate against a VM-internal object",
                       js->method());
        return true;
      }
      name = String::New(cnames[i]);
      names.Add(name);
      values.Add(obj);
    }
  }
  return false;
}

static void Evaluate(Thread* thread, JSONStream* js) {
  // If a compilation service is available, this RPC invocation will have been
  // intercepted by RunningIsolates.routeRequest.
  js->PrintError(
      kExpressionCompilationError,
      "%s: No compilation service available; cannot evaluate from source.",
      js->method());
}

static const MethodParameter* const build_expression_evaluation_scope_params[] =
    {
        RUNNABLE_ISOLATE_PARAMETER,
        new IdParameter("frameIndex", false),
        new IdParameter("targetId", false),
        nullptr,
};

static void CollectStringifiedType(Thread* thread,
                                   Zone* zone,
                                   const AbstractType& type,
                                   const GrowableObjectArray& output) {
  Instance& instance = Instance::Handle(zone);
  if (type.IsFunctionType()) {
    // The closure class
    // (IsolateGroup::Current()->object_store()->closure_class())
    // is statically typed weird (the call method redirects to itself)
    // and the type is therefore not useful for the CFE. We use null instead.
    output.Add(instance);
    return;
  }
  if (type.IsRecordType()) {
    const auto& record = RecordType::Cast(type);
    const intptr_t num_fields = record.NumFields();
    const Array& field_names =
        Array::Handle(zone, record.GetFieldNames(thread));
    const intptr_t num_positional_fields = num_fields - field_names.Length();

    // Records have their own encoding:
    // "record" <nullability> <num fields> <num positional fields>
    // <field names> <encoding of field>
    instance ^= String::New("record");
    output.Add(instance);
    instance ^= Smi::New((intptr_t)type.nullability());
    output.Add(instance);
    instance ^= Smi::New(num_fields);
    output.Add(instance);
    instance ^= Smi::New(num_positional_fields);
    output.Add(instance);

    String& name = String::Handle(zone);
    for (intptr_t i = 0, n = field_names.Length(); i < n; ++i) {
      name ^= field_names.At(i);
      output.Add(name);
    }

    AbstractType& field_type = AbstractType::Handle(zone);
    for (intptr_t i = 0, n = num_fields; i < n; ++i) {
      field_type = record.FieldTypeAt(i);
      CollectStringifiedType(thread, zone, field_type, output);
    }
    return;
  }
  if (type.IsDynamicType()) {
    // Dynamic is weird in that it seems to have a class with no name and a
    // library called something like '7189777121420'. We use null instead.
    output.Add(instance);
    return;
  }
  if (type.IsTypeParameter()) {
    // Calling type_class on a type parameter will crash the VM.
    // We use null instead.
    output.Add(instance);
    return;
  }
  ASSERT(type.IsType());

  const Class& cls = Class::Handle(type.type_class());
  const Library& lib = Library::Handle(zone, cls.library());

  instance ^= lib.url();
  output.Add(instance);

  instance ^= cls.ScrubbedName();
  output.Add(instance);

  instance ^= Smi::New((intptr_t)type.nullability());
  output.Add(instance);

  const TypeArguments& type_arguments =
      TypeArguments::Handle(Type::Cast(type).arguments());
  if (!type_arguments.IsNull()) {
    instance ^= Smi::New(type_arguments.Length());
    output.Add(instance);
    AbstractType& src_type = AbstractType::Handle();
    for (intptr_t i = 0; i < type_arguments.Length(); i++) {
      src_type = type_arguments.TypeAt(i);
      CollectStringifiedType(thread, zone, src_type, output);
    }
  } else {
    const intptr_t num_type_parameters = cls.NumTypeParameters(thread);
    instance ^= Smi::New(num_type_parameters);
    output.Add(instance);
    const AbstractType& dynamic_type =
        AbstractType::Handle(Type::DynamicType());
    for (intptr_t i = 0; i < num_type_parameters; i++) {
      CollectStringifiedType(thread, zone, dynamic_type, output);
    }
  }
}

static void BuildExpressionEvaluationScope(Thread* thread, JSONStream* js) {
  if (CheckDebuggerDisabled(thread, js)) {
    return;
  }

  Isolate* isolate = thread->isolate();
  DebuggerStackTrace* stack = isolate->debugger()->StackTrace();
  intptr_t framePos = UIntParameter::Parse(js->LookupParam("frameIndex"));
  if (framePos >= stack->Length()) {
    PrintInvalidParamError(js, "frameIndex");
    return;
  }

  Zone* zone = thread->zone();
  const GrowableObjectArray& param_names =
      GrowableObjectArray::Handle(zone, GrowableObjectArray::New());
  const GrowableObjectArray& param_values =
      GrowableObjectArray::Handle(zone, GrowableObjectArray::New());
  const GrowableObjectArray& type_params_names =
      GrowableObjectArray::Handle(zone, GrowableObjectArray::New());
  const GrowableObjectArray& type_params_bounds =
      GrowableObjectArray::Handle(zone, GrowableObjectArray::New());
  const GrowableObjectArray& type_params_defaults =
      GrowableObjectArray::Handle(zone, GrowableObjectArray::New());
  String& klass_name = String::Handle(zone);
  String& method_name = String::Handle(zone);
  String& library_uri = String::Handle(zone);
  bool isStatic = false;
  String& script_uri = String::Handle(zone);
  TokenPosition token_pos = TokenPosition::kNoSource;

  if (BuildScope(thread, js, param_names, param_values)) {
    return;
  }

  if (js->HasParam("frameIndex")) {
    // building scope in the context of a given frame
    DebuggerStackTrace* stack = isolate->debugger()->StackTrace();
    intptr_t framePos = UIntParameter::Parse(js->LookupParam("frameIndex"));
    if (framePos >= stack->Length()) {
      PrintInvalidParamError(js, "frameIndex");
      return;
    }

    ActivationFrame* frame = stack->FrameAt(framePos);
    script_uri = frame->SourceUrl();
    token_pos = frame->TokenPos();
    frame->BuildParameters(param_names, param_values, type_params_names,
                           type_params_bounds, type_params_defaults);

    if (frame->function().is_static()) {
      const Class& cls = Class::Handle(zone, frame->function().Owner());
      if (!cls.IsTopLevel()) {
        klass_name = cls.UserVisibleName();
      }
      library_uri = Library::Handle(zone, cls.library()).url();
      method_name = frame->function().UserVisibleName();
      isStatic = true;
    } else {
      Class& method_cls = Class::Handle(zone, frame->function().Owner());
      method_cls = method_cls.Mixin();
      library_uri = Library::Handle(zone, method_cls.library()).url();
      klass_name = method_cls.UserVisibleName();
      method_name = frame->function().UserVisibleName();
      isStatic = false;
    }
  } else {
    // building scope in the context of a given object
    if (!js->HasParam("targetId")) {
      js->PrintError(kInvalidParams,
                     "Either targetId or frameIndex has to be provided.");
      return;
    }
    const char* target_id = js->LookupParam("targetId");

    ObjectIdRing::LookupResult lookup_result;
    Object& obj = Object::Handle(
        zone, LookupHeapObject(thread, target_id, &lookup_result));
    if (obj.ptr() == Object::sentinel().ptr()) {
      PrintInvalidParamError(js, "targetId");
      return;
    }
    if (obj.IsLibrary()) {
      const Library& lib = Library::Cast(obj);
      library_uri = lib.url();
      isStatic = true;
    } else if (obj.IsClass() || ((obj.IsInstance() || obj.IsNull()) &&
                                 !ContainsNonInstance(obj))) {
      Class& cls = Class::Handle(zone);
      if (obj.IsClass()) {
        cls ^= obj.ptr();
        isStatic = true;
      } else {
        Instance& instance = Instance::Handle(zone);
        instance ^= obj.ptr();
        cls = instance.clazz();
        cls = cls.Mixin();
        isStatic = false;
      }
      if (!cls.IsTopLevel() &&
          (IsInternalOnlyClassId(cls.id()) || cls.id() == kTypeArgumentsCid)) {
        js->PrintError(
            kInvalidParams,
            "Expressions can be evaluated only with regular Dart instances");
        return;
      }

      if (!cls.IsTopLevel()) {
        klass_name = cls.UserVisibleName();
      }
      library_uri = Library::Handle(zone, cls.library()).url();
    } else {
      js->PrintError(kInvalidParams,
                     "%s: invalid 'targetId' parameter: "
                     "Cannot evaluate against a VM-internal object",
                     js->method());
      return;
    }
  }

  JSONObject report(js);
  {
    JSONArray jsonParamNames(&report, "param_names");

    String& param_name = String::Handle(zone);
    for (intptr_t i = 0; i < param_names.Length(); i++) {
      param_name ^= param_names.At(i);
      jsonParamNames.AddValue(param_name.ToCString());
    }
  }
  {
    const JSONArray jsonParamTypes(&report, "param_types");
    Object& obj = Object::Handle();
    Instance& instance = Instance::Handle();
    const GrowableObjectArray& param_types =
        GrowableObjectArray::Handle(zone, GrowableObjectArray::New());
    AbstractType& type = AbstractType::Handle();
    for (intptr_t i = 0; i < param_names.Length(); i++) {
      obj = param_values.At(i);
      if (obj.IsNull()) {
        param_types.Add(obj);
      } else if (obj.IsInstance()) {
        instance ^= param_values.At(i);
        type = instance.GetType(Heap::kNew);
        CollectStringifiedType(thread, zone, type, param_types);
      }
    }
    for (intptr_t i = 0; i < param_types.Length(); i++) {
      instance ^= param_types.At(i);
      jsonParamTypes.AddValue(instance.ToCString());
    }
  }

  {
    JSONArray jsonTypeParamsNames(&report, "type_params_names");
    String& type_param_name = String::Handle(zone);
    for (intptr_t i = 0; i < type_params_names.Length(); i++) {
      type_param_name ^= type_params_names.At(i);
      jsonTypeParamsNames.AddValue(type_param_name.ToCString());
    }
  }
  {
    const JSONArray jsonParamTypes(&report, "type_params_bounds");
    const GrowableObjectArray& type_params_bounds_strings =
        GrowableObjectArray::Handle(zone, GrowableObjectArray::New());
    AbstractType& type = AbstractType::Handle();
    for (intptr_t i = 0; i < type_params_bounds.Length(); i++) {
      type ^= type_params_bounds.At(i);
      CollectStringifiedType(thread, zone, type, type_params_bounds_strings);
    }
    Instance& instance = Instance::Handle();
    for (intptr_t i = 0; i < type_params_bounds_strings.Length(); i++) {
      instance ^= type_params_bounds_strings.At(i);
      jsonParamTypes.AddValue(instance.ToCString());
    }
  }
  {
    const JSONArray jsonParamTypes(&report, "type_params_defaults");
    const GrowableObjectArray& type_params_defaults_strings =
        GrowableObjectArray::Handle(zone, GrowableObjectArray::New());
    AbstractType& type = AbstractType::Handle();
    for (intptr_t i = 0; i < type_params_defaults.Length(); i++) {
      type ^= type_params_defaults.At(i);
      CollectStringifiedType(thread, zone, type, type_params_defaults_strings);
    }
    Instance& instance = Instance::Handle();
    for (intptr_t i = 0; i < type_params_defaults_strings.Length(); i++) {
      instance ^= type_params_defaults_strings.At(i);
      jsonParamTypes.AddValue(instance.ToCString());
    }
  }
  report.AddProperty("libraryUri", library_uri.ToCString());
  if (!klass_name.IsNull()) {
    report.AddProperty("klass", klass_name.ToCString());
  }
  if (!method_name.IsNull()) {
    report.AddProperty("method", method_name.ToCString());
  }
  report.AddProperty("tokenPos", token_pos);
  if (!script_uri.IsNull()) {
    report.AddProperty("scriptUri", script_uri.ToCString());
  }
  report.AddProperty("isStatic", isStatic);

  const Library& root_lib =
      Library::Handle(isolate->group()->object_store()->root_library());
  report.AddProperty("rootLibraryUri",
                     String::Handle(root_lib.url()).ToCString());
}

#if !defined(DART_PRECOMPILED_RUNTIME)
// Parse comma-separated list of values, put them into values
static bool ParseCSVList(const char* csv_list,
                         const GrowableObjectArray& values) {
  Zone* zone = Thread::Current()->zone();
  String& s = String::Handle(zone);
  const char* c = csv_list;
  if (*c++ != '[') return false;
  while (IsWhitespace(*c) && *c != '\0') {
    c++;
  }
  while (*c != '\0') {
    const char* value = c;
    while (*c != '\0' && *c != ']' && *c != ',' && !IsWhitespace(*c)) {
      c++;
    }
    if (c > value) {
      s = String::New(zone->MakeCopyOfStringN(value, c - value));
      values.Add(s);
    }
    switch (*c) {
      case '\0':
        return false;
      case ',':
        c++;
        break;
      case ']':
        return true;
    }
    while (IsWhitespace(*c) && *c != '\0') {
      c++;
    }
  }
  return false;
}
#endif

static const MethodParameter* const compile_expression_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new StringParameter("expression", true),
    new StringParameter("definitions", false),
    new StringParameter("definitionTypes", false),
    new StringParameter("typeDefinitions", false),
    new StringParameter("typeBounds", false),
    new StringParameter("typeDefaults", false),
    new StringParameter("libraryUri", true),
    new StringParameter("klass", false),
    new BoolParameter("isStatic", false),
    new StringParameter("method", false),
    new Int64Parameter("tokenPos", false),
    // TODO(jensj): Uncomment this line when DDS has rolled into flutter
    // (https://dart-review.googlesource.com/c/sdk/+/329322).
    // new StringParameter("scriptUri", false),
    nullptr,
};

static void CompileExpression(Thread* thread, JSONStream* js) {
#if defined(DART_PRECOMPILED_RUNTIME)
  js->PrintError(kFeatureDisabled, "Debugger is disabled in AOT mode.");
#else
  if (CheckDebuggerDisabled(thread, js)) {
    return;
  }

  if (!KernelIsolate::IsRunning() && !KernelIsolate::Start()) {
    js->PrintError(
        kExpressionCompilationError,
        "%s: No compilation service available; cannot evaluate from source.",
        js->method());
    return;
  }

  const char* klass = js->LookupParam("klass");
  bool is_static =
      BoolParameter::Parse(js->LookupParam("isStatic"), (klass == nullptr));
  int64_t token_pos = Int64Parameter::Parse(js->LookupParam("tokenPos"));

  const GrowableObjectArray& params =
      GrowableObjectArray::Handle(thread->zone(), GrowableObjectArray::New());
  if (!ParseCSVList(js->LookupParam("definitions"), params)) {
    PrintInvalidParamError(js, "definitions");
    return;
  }
  const GrowableObjectArray& param_types =
      GrowableObjectArray::Handle(thread->zone(), GrowableObjectArray::New());
  if (!ParseCSVList(js->LookupParam("definitionTypes"), param_types)) {
    PrintInvalidParamError(js, "definitionTypes");
    return;
  }

  const GrowableObjectArray& type_params =
      GrowableObjectArray::Handle(thread->zone(), GrowableObjectArray::New());
  if (!ParseCSVList(js->LookupParam("typeDefinitions"), type_params)) {
    PrintInvalidParamError(js, "typedDefinitions");
    return;
  }
  const GrowableObjectArray& type_bounds =
      GrowableObjectArray::Handle(thread->zone(), GrowableObjectArray::New());
  if (!ParseCSVList(js->LookupParam("typeBounds"), type_bounds)) {
    PrintInvalidParamError(js, "typeBounds");
    return;
  }
  const GrowableObjectArray& type_defaults =
      GrowableObjectArray::Handle(thread->zone(), GrowableObjectArray::New());
  if (!ParseCSVList(js->LookupParam("typeDefaults"), type_defaults)) {
    PrintInvalidParamError(js, "typeDefaults");
    return;
  }

  const uint8_t* kernel_buffer = Service::dart_library_kernel();
  const intptr_t kernel_buffer_len = Service::dart_library_kernel_length();

  Dart_KernelCompilationResult compilation_result =
      KernelIsolate::CompileExpressionToKernel(
          kernel_buffer, kernel_buffer_len, js->LookupParam("expression"),
          Array::Handle(Array::MakeFixedLength(params)),
          Array::Handle(Array::MakeFixedLength(param_types)),
          Array::Handle(Array::MakeFixedLength(type_params)),
          Array::Handle(Array::MakeFixedLength(type_bounds)),
          Array::Handle(Array::MakeFixedLength(type_defaults)),
          js->LookupParam("libraryUri"), js->LookupParam("klass"),
          js->LookupParam("method"), TokenPosition::Deserialize(token_pos),
          js->LookupParam("scriptUri"), is_static);

  if (compilation_result.status != Dart_KernelCompilationStatus_Ok) {
    js->PrintError(kExpressionCompilationError, "%s", compilation_result.error);
    free(compilation_result.error);
    return;
  }

  const uint8_t* kernel_bytes = compilation_result.kernel;
  intptr_t kernel_length = compilation_result.kernel_size;
  ASSERT(kernel_bytes != nullptr);

  JSONObject report(js);
  report.AddPropertyBase64("kernelBytes", kernel_bytes, kernel_length);
#endif
}

static const MethodParameter* const evaluate_compiled_expression_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new UIntParameter("frameIndex", false),
    new IdParameter("targetId", false),
    new StringParameter("kernelBytes", true),
    nullptr,
};

ExternalTypedDataPtr DecodeKernelBuffer(const char* kernel_buffer_base64) {
  intptr_t kernel_length;
  uint8_t* kernel_buffer = DecodeBase64(kernel_buffer_base64, &kernel_length);
  return ExternalTypedData::NewFinalizeWithFree(kernel_buffer, kernel_length);
}

static void EvaluateCompiledExpression(Thread* thread, JSONStream* js) {
  if (CheckDebuggerDisabled(thread, js)) {
    return;
  }

  Isolate* isolate = thread->isolate();

  bool disable_breakpoints =
      BoolParameter::Parse(js->LookupParam("disableBreakpoints"), false);
  DisableBreakpointsScope db(isolate->debugger(), disable_breakpoints);

  DebuggerStackTrace* stack = isolate->debugger()->StackTrace();
  intptr_t frame_pos = UIntParameter::Parse(js->LookupParam("frameIndex"));
  if (frame_pos >= stack->Length()) {
    PrintInvalidParamError(js, "frameIndex");
    return;
  }
  Zone* zone = thread->zone();
  const GrowableObjectArray& param_names =
      GrowableObjectArray::Handle(zone, GrowableObjectArray::New());
  const GrowableObjectArray& param_values =
      GrowableObjectArray::Handle(zone, GrowableObjectArray::New());
  if (BuildScope(thread, js, param_names, param_values)) {
    return;
  }
  const GrowableObjectArray& type_params_names =
      GrowableObjectArray::Handle(zone, GrowableObjectArray::New());
  const GrowableObjectArray& type_params_bounds =
      GrowableObjectArray::Handle(zone, GrowableObjectArray::New());
  const GrowableObjectArray& type_params_defaults =
      GrowableObjectArray::Handle(zone, GrowableObjectArray::New());

  const ExternalTypedData& kernel_data = ExternalTypedData::Handle(
      zone, DecodeKernelBuffer(js->LookupParam("kernelBytes")));

  if (js->HasParam("frameIndex")) {
    DebuggerStackTrace* stack = isolate->debugger()->StackTrace();
    intptr_t frame_pos = UIntParameter::Parse(js->LookupParam("frameIndex"));
    if (frame_pos >= stack->Length()) {
      PrintInvalidParamError(js, "frameIndex");
      return;
    }

    ActivationFrame* frame = stack->FrameAt(frame_pos);
    TypeArguments& type_arguments = TypeArguments::Handle(
        zone,
        frame->BuildParameters(param_names, param_values, type_params_names,
                               type_params_bounds, type_params_defaults));

    const Object& result = Object::Handle(
        zone,
        frame->EvaluateCompiledExpression(
            kernel_data,
            Array::Handle(zone, Array::MakeFixedLength(type_params_names)),
            Array::Handle(zone, Array::MakeFixedLength(param_values)),
            type_arguments));
    result.PrintJSON(js, true);
  } else {
    // evaluating expression in the context of a given object
    if (!js->HasParam("targetId")) {
      js->PrintError(kInvalidParams,
                     "Either targetId or frameIndex has to be provided.");
      return;
    }
    const char* target_id = js->LookupParam("targetId");
    ObjectIdRing::LookupResult lookup_result;
    Object& obj = Object::Handle(
        zone, LookupHeapObject(thread, target_id, &lookup_result));
    if (obj.ptr() == Object::sentinel().ptr()) {
      if (lookup_result == ObjectIdRing::kCollected) {
        PrintSentinel(js, kCollectedSentinel);
      } else if (lookup_result == ObjectIdRing::kExpired) {
        PrintSentinel(js, kExpiredSentinel);
      } else {
        PrintInvalidParamError(js, "targetId");
      }
      return;
    }
    const auto& type_params_names_fixed =
        Array::Handle(zone, Array::MakeFixedLength(type_params_names));
    const auto& param_values_fixed =
        Array::Handle(zone, Array::MakeFixedLength(param_values));

    TypeArguments& type_arguments = TypeArguments::Handle(zone);
    if (obj.IsLibrary()) {
      const auto& lib = Library::Cast(obj);
      const auto& result = Object::Handle(
          zone,
          lib.EvaluateCompiledExpression(kernel_data, type_params_names_fixed,
                                         param_values_fixed, type_arguments));
      result.PrintJSON(js, true);
      return;
    }
    if (obj.IsClass()) {
      const auto& cls = Class::Cast(obj);
      const auto& result = Object::Handle(
          zone,
          cls.EvaluateCompiledExpression(kernel_data, type_params_names_fixed,
                                         param_values_fixed, type_arguments));
      result.PrintJSON(js, true);
      return;
    }
    if ((obj.IsInstance() || obj.IsNull()) && !ContainsNonInstance(obj)) {
      const auto& instance =
          Instance::Handle(zone, Instance::RawCast(obj.ptr()));
      const auto& receiver_cls = Class::Handle(zone, instance.clazz());
      const auto& result = Object::Handle(
          zone, instance.EvaluateCompiledExpression(
                    receiver_cls, kernel_data, type_params_names_fixed,
                    param_values_fixed, type_arguments));
      result.PrintJSON(js, true);
      return;
    }
    js->PrintError(kInvalidParams,
                   "%s: invalid 'targetId' parameter: "
                   "Cannot evaluate against a VM-internal object",
                   js->method());
  }
}

static const MethodParameter* const evaluate_in_frame_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new UIntParameter("frameIndex", true),
    new MethodParameter("expression", true),
    nullptr,
};

static void EvaluateInFrame(Thread* thread, JSONStream* js) {
  // If a compilation service is available, this RPC invocation will have been
  // intercepted by RunningIsolates.routeRequest.
  js->PrintError(
      kExpressionCompilationError,
      "%s: No compilation service available; cannot evaluate from source.",
      js->method());
}

static void MarkClasses(const Class& root,
                        bool include_subclasses,
                        bool include_implementors) {
  Thread* thread = Thread::Current();
  HANDLESCOPE(thread);
  ClassTable* table = thread->isolate()->group()->class_table();
  GrowableArray<const Class*> worklist;
  table->SetCollectInstancesFor(root.id(), true);
  worklist.Add(&root);
  GrowableObjectArray& subclasses = GrowableObjectArray::Handle();
  GrowableObjectArray& implementors = GrowableObjectArray::Handle();
  while (!worklist.is_empty()) {
    const Class& cls = *worklist.RemoveLast();
    // All subclasses are implementors, but they are not included in
    // `direct_implementors`.
    if (include_subclasses || include_implementors) {
      subclasses = cls.direct_subclasses_unsafe();
      if (!subclasses.IsNull()) {
        for (intptr_t j = 0; j < subclasses.Length(); j++) {
          Class& subclass = Class::Handle();
          subclass ^= subclasses.At(j);
          if (!table->CollectInstancesFor(subclass.id())) {
            table->SetCollectInstancesFor(subclass.id(), true);
            worklist.Add(&subclass);
          }
        }
      }
    }
    if (include_implementors) {
      implementors = cls.direct_implementors_unsafe();
      if (!implementors.IsNull()) {
        for (intptr_t j = 0; j < implementors.Length(); j++) {
          Class& implementor = Class::Handle();
          implementor ^= implementors.At(j);
          if (!table->CollectInstancesFor(implementor.id())) {
            table->SetCollectInstancesFor(implementor.id(), true);
            worklist.Add(&implementor);
          }
        }
      }
    }
  }
}

static void UnmarkClasses() {
  ClassTable* table = IsolateGroup::Current()->class_table();
  for (intptr_t i = 1; i < table->NumCids(); i++) {
    table->SetCollectInstancesFor(i, false);
  }
}

class GetInstancesVisitor : public ObjectGraph::Visitor {
 public:
  GetInstancesVisitor(ZoneGrowableHandlePtrArray<Object>* storage,
                      intptr_t limit)
      : table_(IsolateGroup::Current()->class_table()),
        storage_(storage),
        limit_(limit),
        count_(0) {}

  virtual Direction VisitObject(ObjectGraph::StackIterator* it) {
    ObjectPtr raw_obj = it->Get();
    if (raw_obj->IsPseudoObject()) {
      return kProceed;
    }
    if (table_->CollectInstancesFor(raw_obj->GetClassIdOfHeapObject())) {
      if (count_ < limit_) {
        storage_->Add(Object::Handle(raw_obj));
      }
      ++count_;
    }
    return kProceed;
  }

  intptr_t count() const { return count_; }

 private:
  ClassTable* const table_;
  ZoneGrowableHandlePtrArray<Object>* storage_;
  const intptr_t limit_;
  intptr_t count_;
};

static const MethodParameter* const get_instances_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new IdParameter("objectId", /*required=*/true),
    new UIntParameter("limit", /*required=*/true),
    new BoolParameter("includeSubclasses", /*required=*/false),
    new BoolParameter("includeImplementers", /*required=*/false),
    nullptr,
};

static void GetInstances(Thread* thread, JSONStream* js) {
  const char* object_id = js->LookupParam("objectId");
  const intptr_t limit = UIntParameter::Parse(js->LookupParam("limit"));
  const bool include_subclasses =
      BoolParameter::Parse(js->LookupParam("includeSubclasses"), false);
  const bool include_implementers =
      BoolParameter::Parse(js->LookupParam("includeImplementers"), false);

  const Object& obj =
      Object::Handle(LookupHeapObject(thread, object_id, nullptr));
  if (obj.ptr() == Object::sentinel().ptr() || !obj.IsClass()) {
    PrintInvalidParamError(js, "objectId");
    return;
  }
  const Class& cls = Class::Cast(obj);

  // Ensure the array and handles created below are promptly destroyed.
  StackZone zone(thread);

  ZoneGrowableHandlePtrArray<Object> storage(thread->zone(), limit);
  GetInstancesVisitor visitor(&storage, limit);
  {
    ObjectGraph graph(thread);
    HeapIterationScope iteration_scope(thread, true);
    MarkClasses(cls, include_subclasses, include_implementers);
    graph.IterateObjects(&visitor);
    UnmarkClasses();
  }
  intptr_t count = visitor.count();
  JSONObject jsobj(js);
  jsobj.AddProperty("type", "InstanceSet");
  jsobj.AddProperty("totalCount", count);
  {
    JSONArray samples(&jsobj, "instances");
    for (int i = 0; (i < limit) && (i < count); i++) {
      samples.AddValue(storage.At(i));
    }
  }
}

static const MethodParameter* const get_instances_as_list_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new IdParameter("objectId", /*required=*/true),
    new BoolParameter("includeSubclasses", /*required=*/false),
    new BoolParameter("includeImplementers", /*required=*/false),
    nullptr,
};

static void GetInstancesAsList(Thread* thread, JSONStream* js) {
  const char* object_id = js->LookupParam("objectId");
  bool include_subclasses =
      BoolParameter::Parse(js->LookupParam("includeSubclasses"), false);
  bool include_implementers =
      BoolParameter::Parse(js->LookupParam("includeImplementers"), false);

  const Object& obj =
      Object::Handle(LookupHeapObject(thread, object_id, nullptr));
  if (obj.ptr() == Object::sentinel().ptr() || !obj.IsClass()) {
    PrintInvalidParamError(js, "objectId");
    return;
  }
  const Class& cls = Class::Cast(obj);

  Array& instances = Array::Handle();
  {
    // Ensure the |ZoneGrowableHandlePtrArray| and handles created below are
    // promptly destroyed.
    StackZone zone(thread);

    ZoneGrowableHandlePtrArray<Object> storage(thread->zone(), 1024);
    GetInstancesVisitor visitor(&storage, kSmiMax);
    {
      ObjectGraph graph(thread);
      HeapIterationScope iteration_scope(thread, true);
      MarkClasses(cls, include_subclasses, include_implementers);
      graph.IterateObjects(&visitor);
      UnmarkClasses();
    }
    intptr_t count = visitor.count();
    instances = Array::New(count);
    for (intptr_t i = 0; i < count; i++) {
      instances.SetAt(i, storage.At(i));
    }
  }
  instances.PrintJSON(js, /*ref=*/true);
}

template <typename Adder>
static intptr_t ParseJSONCollection(Thread* thread,
                                    const char* str,
                                    const Adder& add) {
  ASSERT(str != nullptr);
  ASSERT(thread != nullptr);
  intptr_t n = strlen(str);
  if (n < 2) {
    return -1;
  } else if (n == 2) {
    return 0;
  }
  // The JSON string array looks like [abc, def]. There are no quotes around the
  // strings, but there is a space after the comma. start points to the first
  // character of the element. end points to the separator after the element
  // (']' or ',').
  intptr_t start = 1;
  while (start < n) {
    intptr_t end = start;
    while (end < n) {
      const char c = str[end];
      if (c == ',' || c == ']') {
        break;
      }
      ++end;
    }
    add(&str[start], end - start);
    start = end + 2;
  }
  return 0;
}

intptr_t ParseJSONArray(Thread* thread,
                        const char* str,
                        const GrowableObjectArray& elements) {
  Zone* zone = thread->zone();
  return ParseJSONCollection(
      thread, str, [zone, &elements](const char* start, intptr_t length) {
        String& element = String::Handle(
            zone,
            String::FromUTF8(reinterpret_cast<const uint8_t*>(start), length));
        elements.Add(element);
      });
}

#if !defined(DART_PRECOMPILED_RUNTIME)
static intptr_t ParseJSONSet(Thread* thread,
                             const char* str,
                             ZoneCStringSet* elements) {
  Zone* zone = thread->zone();
  return ParseJSONCollection(
      thread, str, [zone, elements](const char* start, intptr_t length) {
        elements->Insert(zone->MakeCopyOfStringN(start, length));
      });
}
#endif

static const MethodParameter* const get_ports_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void GetPorts(Thread* thread, JSONStream* js) {
  // Ensure the array and handles created below are promptly destroyed.
  StackZone zone(thread);
  const GrowableObjectArray& ports = GrowableObjectArray::Handle(
      GrowableObjectArray::RawCast(DartLibraryCalls::LookupOpenPorts()));
  JSONObject jsobj(js);
  jsobj.AddProperty("type", "PortList");
  {
    ReceivePort& port = ReceivePort::Handle(zone.GetZone());
    JSONArray arr(&jsobj, "ports");
    for (int i = 0; i < ports.Length(); ++i) {
      port ^= ports.At(i);
      ASSERT(port.is_open());
      if (port.keep_isolate_alive()) {
        arr.AddValue(port);
      }
    }
  }
}

static const MethodParameter* const get_queued_microtasks_params[] = {
    ISOLATE_PARAMETER,
    nullptr,
};

static void GetQueuedMicrotasks(Thread* thread, JSONStream* js) {
  if (!FLAG_profile_microtasks) {
    js->PrintError(kFeatureDisabled,
                   "getQueuedMicrotaks is only available when the VM is "
                   "started with the flag --profile-microtasks.");
    return;
  }

  const MicrotaskMirrorQueue& queue =
      *MicrotaskMirrorQueues::GetQueue(thread->isolate()->main_port());

  if (queue.is_disabled()) {
    js->PrintError(
        kCannotGetQueuedMicrotasks,
        "An exception has gone unhandled in the specified isolate, so "
        "information about the microtasks queued in it cannot be retrieved.");
  } else {
    queue.PrintJSON(*js);
  }
}

#if !defined(DART_PRECOMPILED_RUNTIME)
static const char* const report_enum_names[] = {
    SourceReport::kCallSitesStr,           SourceReport::kCoverageStr,
    SourceReport::kPossibleBreakpointsStr, SourceReport::kProfileStr,
    SourceReport::kBranchCoverageStr,      nullptr,
};
#endif

static const MethodParameter* const get_source_report_params[] = {
#if !defined(DART_PRECOMPILED_RUNTIME)
    RUNNABLE_ISOLATE_PARAMETER,
    new EnumListParameter("reports", true, report_enum_names),
    new IdParameter("scriptId", false),
    new UIntParameter("tokenPos", false),
    new UIntParameter("endTokenPos", false),
    new BoolParameter("forceCompile", false),
#endif
    nullptr,
};

static void GetSourceReport(Thread* thread, JSONStream* js) {
#if defined(DART_PRECOMPILED_RUNTIME)
  js->PrintError(kFeatureDisabled, "disabled in AOT mode and PRODUCT.");
#else
  char* reports_str = Utils::StrDup(js->LookupParam("reports"));
  const EnumListParameter* reports_parameter =
      static_cast<const EnumListParameter*>(get_source_report_params[1]);
  const char** reports = reports_parameter->Parse(reports_str);
  const char** riter = reports;
  intptr_t report_set = 0;
  while (*riter != nullptr) {
    if (strcmp(*riter, SourceReport::kCallSitesStr) == 0) {
      report_set |= SourceReport::kCallSites;
    } else if (strcmp(*riter, SourceReport::kCoverageStr) == 0) {
      report_set |= SourceReport::kCoverage;
    } else if (strcmp(*riter, SourceReport::kPossibleBreakpointsStr) == 0) {
      report_set |= SourceReport::kPossibleBreakpoints;
    } else if (strcmp(*riter, SourceReport::kProfileStr) == 0) {
      report_set |= SourceReport::kProfile;
    } else if (strcmp(*riter, SourceReport::kBranchCoverageStr) == 0) {
      report_set |= SourceReport::kBranchCoverage;
    }
    riter++;
  }
  if (reports != nullptr) {
    delete[] reports;
  }
  free(reports_str);

  SourceReport::CompileMode compile_mode = SourceReport::kNoCompile;
  if (BoolParameter::Parse(js->LookupParam("forceCompile"), false)) {
    compile_mode = SourceReport::kForceCompile;
  }

  bool report_lines =
      BoolParameter::Parse(js->LookupParam("reportLines"), false);

  Script& script = Script::Handle();
  intptr_t start_pos = UIntParameter::Parse(js->LookupParam("tokenPos"));
  intptr_t end_pos = UIntParameter::Parse(js->LookupParam("endTokenPos"));

  if (js->HasParam("scriptId")) {
    // Get the target script.
    const char* script_id_param = js->LookupParam("scriptId");
    const Object& obj =
        Object::Handle(LookupHeapObject(thread, script_id_param, nullptr));
    if (obj.ptr() == Object::sentinel().ptr() || !obj.IsScript()) {
      PrintInvalidParamError(js, "scriptId");
      return;
    }
    script ^= obj.ptr();
  } else {
    if (js->HasParam("tokenPos")) {
      js->PrintError(
          kInvalidParams,
          "%s: the 'tokenPos' parameter requires the 'scriptId' parameter",
          js->method());
      return;
    }
    if (js->HasParam("endTokenPos")) {
      js->PrintError(
          kInvalidParams,
          "%s: the 'endTokenPos' parameter requires the 'scriptId' parameter",
          js->method());
      return;
    }
  }

  const char* library_filters_param = js->LookupParam("libraryFilters");
  GrowableObjectArray& library_filters = GrowableObjectArray::Handle();
  if (library_filters_param != nullptr) {
    library_filters = GrowableObjectArray::New();
    intptr_t library_filters_length =
        ParseJSONArray(thread, library_filters_param, library_filters);
    if (library_filters_length < 0) {
      PrintInvalidParamError(js, "library_filters");
      return;
    }
  }

  const char* libraries_already_compiled_param =
      js->LookupParam("librariesAlreadyCompiled");
  Zone* zone = thread->zone();
  ZoneCStringSet* libraries_already_compiled = nullptr;
  if (libraries_already_compiled_param != nullptr) {
    libraries_already_compiled = new (zone) ZoneCStringSet(zone);
    intptr_t libraries_already_compiled_length = ParseJSONSet(
        thread, libraries_already_compiled_param, libraries_already_compiled);
    if (libraries_already_compiled_length < 0) {
      PrintInvalidParamError(js, "libraries_already_compiled");
      return;
    }
  }

  SourceReport report(report_set, library_filters, libraries_already_compiled,
                      compile_mode, report_lines);
  report.PrintJSON(js, script, TokenPosition::Deserialize(start_pos),
                   TokenPosition::Deserialize(end_pos));
#endif  // !DART_PRECOMPILED_RUNTIME
}

static const MethodParameter* const reload_kernel_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new BoolParameter("force", false),
    new BoolParameter("pause", false),
    new DartStringParameter("kernelFilePath", true),
    nullptr,
};

/// Replaces the program running in the isolate group containing the isolate
/// specified by |js->LookupParam("isolateId")| with the program defined by
/// |js->LookupParam("kernelFilePath")|.
static void ReloadKernel(Thread* thread, JSONStream* js) {
#if defined(DART_PRECOMPILED_RUNTIME)
  js->PrintError(kFeatureDisabled, "Compiler is disabled in AOT mode.");
#else
  IsolateGroup* isolate_group = thread->isolate_group();
  if (isolate_group->library_tag_handler() == nullptr) {
    js->PrintError(kFeatureDisabled,
                   "A library tag handler must be installed.");
    return;
  }
  Isolate* isolate = thread->isolate();
  if ((isolate->sticky_error() != Error::null()) ||
      (Thread::Current()->sticky_error() != Error::null())) {
    js->PrintError(kIsolateReloadBarred,
                   "The specified isolate cannot reload from a kernel file "
                   "anymore because it encountered an unhandled exception. "
                   "Restart the isolate.");
    return;
  }
  if (isolate_group->IsReloading()) {
    js->PrintError(kIsolateIsReloading,
                   "The specified isolate group is already in the process of "
                   "being reloaded.");
    return;
  }
  if (!isolate_group->CanReload()) {
    js->PrintError(kFeatureDisabled,
                   "The specified isolate group cannot reload from a kernel "
                   "file right now.");
    return;
  }

  Dart_FileOpenCallback file_open = Dart::file_open_callback();
  Dart_FileReadCallback file_read = Dart::file_read_callback();
  Dart_FileCloseCallback file_close = Dart::file_close_callback();
  if ((file_open == nullptr) || (file_read == nullptr) ||
      (file_close == nullptr)) {
    js->PrintError(kInternalError,
                   "An internal error occurred when trying to read the "
                   "specified kernel file.");
    return;
  }

  const String& kernel_file_path = String::CheckedHandle(
      thread->zone(), js->LookupObjectParam("kernelFilePath"));
  void* file = (*file_open)(kernel_file_path.ToCString(), /*write=*/false);
  if (file == nullptr) {
    js->PrintError(kIsolateReloadBarred,
                   "The specified kernel file could not be read. Please ensure "
                   "that the provided 'kernelFilePath' argument is correct.");
    return;
  }

  uint8_t* kernel_buffer = nullptr;
  intptr_t kernel_buffer_size = -1;
  (*file_read)(&kernel_buffer, &kernel_buffer_size, file);

  const bool force_reload =
      js->LookupObjectParam("force") == Bool::True().ptr();
  isolate_group->ReloadKernel(js, force_reload, kernel_buffer,
                              kernel_buffer_size);

  Service::CheckForPause(isolate, js);
#endif  // defined(DART_PRECOMPILED_RUNTIME)
}

static const MethodParameter* const reload_sources_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new BoolParameter("force", false),
    new BoolParameter("pause", false),
    new DartStringParameter("rootLibUri", false),
    new DartStringParameter("packagesUri", false),
    nullptr,
};

static void ReloadSources(Thread* thread, JSONStream* js) {
#if defined(DART_PRECOMPILED_RUNTIME)
  js->PrintError(kFeatureDisabled, "Compiler is disabled in AOT mode.");
#else
  IsolateGroup* isolate_group = thread->isolate_group();
  if (isolate_group->library_tag_handler() == nullptr) {
    js->PrintError(kFeatureDisabled,
                   "A library tag handler must be installed.");
    return;
  }
  // TODO(dartbug.com/36097): We need to change the "reloadSources" service-api
  // call to accept an isolate group instead of an isolate.
  Isolate* isolate = thread->isolate();
  if ((isolate->sticky_error() != Error::null()) ||
      (Thread::Current()->sticky_error() != Error::null())) {
    js->PrintError(kIsolateReloadBarred,
                   "This isolate cannot reload sources anymore because there "
                   "was an unhandled exception error. Restart the isolate.");
    return;
  }
  if (isolate_group->IsReloading()) {
    js->PrintError(kIsolateIsReloading, "This isolate is being reloaded.");
    return;
  }
  if (!isolate_group->CanReload()) {
    js->PrintError(kFeatureDisabled,
                   "This isolate cannot reload sources right now.");
    return;
  }
  const bool force_reload =
      js->LookupObjectParam("force") == Bool::True().ptr();

  String& root_lib_uri = String::Handle(thread->zone());
  root_lib_uri ^= js->LookupObjectParam("rootLibUri");

  String& packages_uri = String::Handle(thread->zone());
  packages_uri ^= js->LookupObjectParam("packagesUri");

  isolate_group->ReloadSources(
      js, force_reload,
      root_lib_uri.IsNull() ? nullptr : root_lib_uri.ToCString(),
      packages_uri.IsNull() ? nullptr : packages_uri.ToCString());

  Service::CheckForPause(isolate, js);

#endif
}

void Service::CheckForPause(Isolate* isolate, JSONStream* stream) {
  // Should we pause?
  isolate->set_should_pause_post_service_request(
      stream->LookupObjectParam("pause") == Bool::True().ptr());
}

ErrorPtr Service::MaybePause(Isolate* isolate, const Error& error) {
  const bool should_pause_post_current_service_request =
      isolate->should_pause_post_service_request();
  if (should_pause_post_current_service_request) {
    // Ensure that we do not accidentally pause post the next service request.
    isolate->set_should_pause_post_service_request(false);
  }
  // Don't pause twice.
  if (!isolate->IsPaused()) {
    if (should_pause_post_current_service_request) {
      if (!error.IsNull()) {
        // Before pausing, restore the sticky error. The debugger will return it
        // from PausePostRequest.
        Thread::Current()->set_sticky_error(error);
      }
      return isolate->PausePostRequest();
    }
  }
  return error.ptr();
}

static void AddBreakpointCommon(Thread* thread,
                                JSONStream* js,
                                const String& script_uri) {
  if (CheckDebuggerDisabled(thread, js)) {
    return;
  }

  const char* line_param = js->LookupParam("line");
  intptr_t line = UIntParameter::Parse(line_param);
  const char* col_param = js->LookupParam("column");
  intptr_t col = -1;
  if (col_param != nullptr) {
    col = UIntParameter::Parse(col_param);
    if (col == 0) {
      // Column number is 1-based.
      PrintInvalidParamError(js, "column");
      return;
    }
  }
  ASSERT(!script_uri.IsNull());
  Breakpoint* bpt = nullptr;
  const Error& error =
      Error::Handle(thread->isolate()->debugger()->SetBreakpointAtLineCol(
          script_uri, line, col, &bpt));
  if (!error.IsNull()) {
    if (col_param != nullptr) {
      js->PrintError(
          kCannotAddBreakpoint,
          "%s: Cannot add breakpoint at %s:%s. Error occurred when resolving "
          "breakpoint location: %s.",
          js->method(), line_param, col_param, error.ToErrorCString());
    } else {
      js->PrintError(kCannotAddBreakpoint,
                     "%s: Cannot add breakpoint at line %s. Error occurred "
                     "when resolving breakpoint location: %s.",
                     js->method(), line_param, error.ToErrorCString());
    }
    return;
  }
  ASSERT(bpt != nullptr);
  bpt->PrintJSON(js);
}

static const MethodParameter* const add_breakpoint_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new IdParameter("scriptId", true),
    new UIntParameter("line", true),
    new UIntParameter("column", false),
    nullptr,
};

static void AddBreakpoint(Thread* thread, JSONStream* js) {
  if (CheckDebuggerDisabled(thread, js)) {
    return;
  }

  const char* script_id_param = js->LookupParam("scriptId");
  Object& obj =
      Object::Handle(LookupHeapObject(thread, script_id_param, nullptr));
  if (obj.ptr() == Object::sentinel().ptr() || !obj.IsScript()) {
    PrintInvalidParamError(js, "scriptId");
    return;
  }
  const Script& script = Script::Cast(obj);
  const String& script_uri = String::Handle(script.url());
  ASSERT(!script_uri.IsNull());
  AddBreakpointCommon(thread, js, script_uri);
}

static const MethodParameter* const add_breakpoint_with_script_uri_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new IdParameter("scriptUri", true),
    new UIntParameter("line", true),
    new UIntParameter("column", false),
    nullptr,
};

static void AddBreakpointWithScriptUri(Thread* thread, JSONStream* js) {
  if (CheckDebuggerDisabled(thread, js)) {
    return;
  }

  const char* script_uri_param = js->LookupParam("scriptUri");
  const String& script_uri = String::Handle(String::New(script_uri_param));
  AddBreakpointCommon(thread, js, script_uri);
}

static const MethodParameter* const add_breakpoint_at_entry_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new IdParameter("functionId", true),
    nullptr,
};

static void AddBreakpointAtEntry(Thread* thread, JSONStream* js) {
  if (CheckDebuggerDisabled(thread, js)) {
    return;
  }

  const char* function_id = js->LookupParam("functionId");
  Object& obj = Object::Handle(LookupHeapObject(thread, function_id, nullptr));
  if (obj.ptr() == Object::sentinel().ptr() || !obj.IsFunction()) {
    PrintInvalidParamError(js, "functionId");
    return;
  }
  const Function& function = Function::Cast(obj);
  Breakpoint* bpt = nullptr;
  const Error& error =
      Error::Handle(thread->isolate()->debugger()->SetBreakpointAtEntry(
          function, false, &bpt));
  if (!error.IsNull()) {
    js->PrintError(kCannotAddBreakpoint,
                   "%s: Cannot add breakpoint at function '%s'. Error occurred "
                   "when resolving breakpoint location: %s.",
                   js->method(), function.ToCString(), error.ToErrorCString());
    return;
  }
  ASSERT(bpt != nullptr);
  bpt->PrintJSON(js);
}

static const MethodParameter* const add_breakpoint_at_activation_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new IdParameter("objectId", true),
    nullptr,
};

static void AddBreakpointAtActivation(Thread* thread, JSONStream* js) {
  if (CheckDebuggerDisabled(thread, js)) {
    return;
  }

  const char* object_id = js->LookupParam("objectId");
  Object& obj = Object::Handle(LookupHeapObject(thread, object_id, nullptr));
  if (obj.ptr() == Object::sentinel().ptr() || !obj.IsInstance()) {
    PrintInvalidParamError(js, "objectId");
    return;
  }
  const Instance& closure = Instance::Cast(obj);
  Breakpoint* bpt;
  const Error& error =
      Error::Handle(thread->isolate()->debugger()->SetBreakpointAtActivation(
          closure, /*single_shot=*/false, &bpt));
  if (!error.IsNull()) {
    js->PrintError(kCannotAddBreakpoint,
                   "%s: Cannot add breakpoint at activation. Error occurred "
                   "when resolving breakpoint location: %s.",
                   js->method(), error.ToErrorCString());
    return;
  }
  ASSERT(bpt != nullptr);
  bpt->PrintJSON(js);
}

static const MethodParameter* const remove_breakpoint_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void RemoveBreakpoint(Thread* thread, JSONStream* js) {
  if (CheckDebuggerDisabled(thread, js)) {
    return;
  }

  if (!js->HasParam("breakpointId")) {
    PrintMissingParamError(js, "breakpointId");
    return;
  }
  const char* bpt_id = js->LookupParam("breakpointId");
  ObjectIdRing::LookupResult lookup_result;
  Isolate* isolate = thread->isolate();
  Breakpoint* bpt = LookupBreakpoint(isolate, bpt_id, &lookup_result);
  // TODO(turnidge): Should we return a different error for bpts which
  // have been already removed?
  if (bpt == nullptr) {
    PrintInvalidParamError(js, "breakpointId");
    return;
  }
  isolate->debugger()->RemoveBreakpoint(bpt->id());
  PrintSuccess(js);
}

static void HandleNativeMetricsList(Thread* thread, JSONStream* js) {
  JSONObject obj(js);
  obj.AddProperty("type", "MetricList");
  {
    JSONArray metrics(&obj, "metrics");

    auto isolate = thread->isolate();
#define ADD_METRIC(type, variable, name, unit)                                 \
  metrics.AddValue(isolate->Get##variable##Metric());
    ISOLATE_METRIC_LIST(ADD_METRIC);
#undef ADD_METRIC

    auto isolate_group = thread->isolate_group();
#define ADD_METRIC(type, variable, name, unit)                                 \
  metrics.AddValue(isolate_group->Get##variable##Metric());
    ISOLATE_GROUP_METRIC_LIST(ADD_METRIC);
#undef ADD_METRIC
  }
}

static void HandleNativeMetric(Thread* thread, JSONStream* js, const char* id) {
  auto isolate = thread->isolate();
#define ADD_METRIC(type, variable, name, unit)                                 \
  if (strcmp(id, name) == 0) {                                                 \
    isolate->Get##variable##Metric()->PrintJSON(js);                           \
    return;                                                                    \
  }
  ISOLATE_METRIC_LIST(ADD_METRIC);
#undef ADD_METRIC

  auto isolate_group = thread->isolate_group();
#define ADD_METRIC(type, variable, name, unit)                                 \
  if (strcmp(id, name) == 0) {                                                 \
    isolate_group->Get##variable##Metric()->PrintJSON(js);                     \
    return;                                                                    \
  }
  ISOLATE_GROUP_METRIC_LIST(ADD_METRIC);
#undef ADD_METRIC

  PrintInvalidParamError(js, "metricId");
}

static const MethodParameter* const get_isolate_metric_list_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void GetIsolateMetricList(Thread* thread, JSONStream* js) {
  if (js->HasParam("type")) {
    if (!js->ParamIs("type", "Native")) {
      PrintInvalidParamError(js, "type");
      return;
    }
  } else {
    PrintMissingParamError(js, "type");
    return;
  }
  HandleNativeMetricsList(thread, js);
}

static const MethodParameter* const get_isolate_metric_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void GetIsolateMetric(Thread* thread, JSONStream* js) {
  const char* metric_id = js->LookupParam("metricId");
  if (metric_id == nullptr) {
    PrintMissingParamError(js, "metricId");
    return;
  }
  // Verify id begins with "metrics/native/".
  static const char* const kNativeMetricIdPrefix = "metrics/native/";
  static intptr_t kNativeMetricIdPrefixLen = strlen(kNativeMetricIdPrefix);
  if (strncmp(metric_id, kNativeMetricIdPrefix, kNativeMetricIdPrefixLen) !=
      0) {
    PrintInvalidParamError(js, "metricId");
    return;
  }
  const char* id = metric_id + kNativeMetricIdPrefixLen;
  HandleNativeMetric(thread, js, id);
}

enum TimelineOrSamplesResponseFormat : bool { JSON = false, Perfetto = true };

static void GetCpuSamplesCommon(TimelineOrSamplesResponseFormat format,
                                Thread* thread,
                                JSONStream* js) {
  const int64_t time_origin_micros =
      Int64Parameter::Parse(js->LookupParam("timeOriginMicros"));
  const int64_t time_extent_micros =
      Int64Parameter::Parse(js->LookupParam("timeExtentMicros"));
  const bool include_code_samples =
      BoolParameter::Parse(js->LookupParam("_code"), false);
  if (CheckProfilerDisabled(thread, js)) {
    return;
  }

  if (format == TimelineOrSamplesResponseFormat::JSON) {
    ProfilerService::PrintJSON(js, time_origin_micros, time_extent_micros,
                               include_code_samples);
  } else if (format == TimelineOrSamplesResponseFormat::Perfetto) {
#if defined(SUPPORT_PERFETTO)
    // This branch will never be reached when SUPPORT_PERFETTO is not defined,
    // because |GetPerfettoCpuSamples| is not defined when SUPPORT_PERFETTO is
    // not defined.
    ProfilerService::PrintPerfetto(js, time_origin_micros, time_extent_micros);
#else
    UNREACHABLE();
#endif  // defined(SUPPORT_PERFETTO)
  }
}

inline void GetVMTimelineCommon(TimelineOrSamplesResponseFormat format,
                                Thread* thread,
                                JSONStream* js) {
  Isolate* isolate = thread->isolate();
  ASSERT(isolate != nullptr);
  StackZone zone(thread);
  TimelineEventRecorder* timeline_recorder = Timeline::recorder();
  ASSERT(timeline_recorder != nullptr);
  const char* name = timeline_recorder->name();
  if (strcmp(name, CALLBACK_RECORDER_NAME) == 0) {
    js->PrintError(kInvalidTimelineRequest,
                   "A recorder of type \"%s\" is currently in use. As a "
                   "result, timeline events are handled by the embedder rather "
                   "than the VM.",
                   timeline_recorder->name());
    return;
  } else if (strcmp(name, FUCHSIA_RECORDER_NAME) == 0 ||
             strcmp(name, SYSTRACE_RECORDER_NAME) == 0 ||
             strcmp(name, MACOS_RECORDER_NAME) == 0) {
    js->PrintError(
        kInvalidTimelineRequest,
        "A recorder of type \"%s\" is currently in use. As a result, timeline "
        "events are handled by the OS rather than the VM. See the VM service "
        "documentation for more details on where timeline events can be found "
        "for this recorder type.",
        timeline_recorder->name());
    return;
  } else if (strcmp(name, FILE_RECORDER_NAME) == 0 ||
             strcmp(name, PERFETTO_FILE_RECORDER_NAME) == 0) {
    js->PrintError(kInvalidTimelineRequest,
                   "A recorder of type \"%s\" is currently in use. As a "
                   "result, timeline events are written directly to a file and "
                   "thus cannot be retrieved through the VM Service.",
                   timeline_recorder->name());
    return;
  }
  int64_t time_origin_micros =
      Int64Parameter::Parse(js->LookupParam("timeOriginMicros"));
  int64_t time_extent_micros =
      Int64Parameter::Parse(js->LookupParam("timeExtentMicros"));
  TimelineEventFilter filter(time_origin_micros, time_extent_micros);
  if (format == TimelineOrSamplesResponseFormat::JSON) {
    timeline_recorder->PrintJSON(js, &filter);
  } else if (format == TimelineOrSamplesResponseFormat::Perfetto) {
#if defined(SUPPORT_PERFETTO)
    // This branch will never be reached when SUPPORT_PERFETTO is not defined,
    // because |GetPerfettoVMTimeline| is not defined when SUPPORT_PERFETTO is
    // not defined.
    timeline_recorder->PrintPerfettoTimeline(js, filter);
#else
    UNREACHABLE();
#endif  // defined(SUPPORT_PERFETTO)
  }
}

#if defined(SUPPORT_PERFETTO)
static void GetPerfettoCpuSamples(Thread* thread, JSONStream* js) {
  GetCpuSamplesCommon(TimelineOrSamplesResponseFormat::Perfetto, thread, js);
}

static void GetPerfettoVMTimeline(Thread* thread, JSONStream* js) {
  GetVMTimelineCommon(TimelineOrSamplesResponseFormat::Perfetto, thread, js);
}
#endif  // defined(SUPPORT_PERFETTO)

static void SetVMTimelineFlags(Thread* thread, JSONStream* js) {
#if !defined(SUPPORT_TIMELINE)
  PrintSuccess(js);
#else
  Isolate* isolate = thread->isolate();
  ASSERT(isolate != nullptr);
  StackZone zone(thread);

  char* recorded_streams = Utils::StrDup(js->LookupParam("recordedStreams"));
  Service::EnableTimelineStreams(recorded_streams);
  free(recorded_streams);

  PrintSuccess(js);
#endif
}

static const MethodParameter* const get_vm_timeline_flags_params[] = {
    NO_ISOLATE_PARAMETER,
    nullptr,
};

static void GetVMTimelineFlags(Thread* thread, JSONStream* js) {
#if !defined(SUPPORT_TIMELINE)
  JSONObject obj(js);
  obj.AddProperty("type", "TimelineFlags");
#else
  Isolate* isolate = thread->isolate();
  ASSERT(isolate != nullptr);
  StackZone zone(thread);
  Timeline::PrintFlagsToJSON(js);
#endif
}

static const MethodParameter* const get_vm_timeline_micros_params[] = {
    NO_ISOLATE_PARAMETER,
    nullptr,
};

static void GetVMTimelineMicros(Thread* thread, JSONStream* js) {
  JSONObject obj(js);
  obj.AddProperty("type", "Timestamp");
  obj.AddPropertyTimeMicros("timestamp", OS::GetCurrentMonotonicMicros());
}

static const MethodParameter* const clear_vm_timeline_params[] = {
    NO_ISOLATE_PARAMETER,
    nullptr,
};

static void ClearVMTimeline(Thread* thread, JSONStream* js) {
  Isolate* isolate = thread->isolate();
  ASSERT(isolate != nullptr);
  StackZone zone(thread);

  Timeline::Clear();

  PrintSuccess(js);
}

static const MethodParameter* const get_vm_timeline_params[] = {
    NO_ISOLATE_PARAMETER,
    new Int64Parameter("timeOriginMicros", /*required=*/false),
    new Int64Parameter("timeExtentMicros", /*required=*/false),
    nullptr,
};

static void GetVMTimeline(Thread* thread, JSONStream* js) {
  GetVMTimelineCommon(TimelineOrSamplesResponseFormat::JSON, thread, js);
}

static const char* const step_enum_names[] = {
    "None", "Into", "Over", "Out", "Rewind", "OverAsyncSuspension", nullptr,
};

static const Debugger::ResumeAction step_enum_values[] = {
    Debugger::kContinue,   Debugger::kStepInto,
    Debugger::kStepOver,   Debugger::kStepOut,
    Debugger::kStepRewind, Debugger::kStepOverAsyncSuspension,
    Debugger::kContinue,  // Default value
};

static const MethodParameter* const resume_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new EnumParameter("step", false, step_enum_names),
    new UIntParameter("frameIndex", false),
    nullptr,
};

static void Resume(Thread* thread, JSONStream* js) {
  const char* step_param = js->LookupParam("step");
  Debugger::ResumeAction step = Debugger::kContinue;
  if (step_param != nullptr) {
    step = EnumMapper(step_param, step_enum_names, step_enum_values);
  }
  intptr_t frame_index = 1;
  const char* frame_index_param = js->LookupParam("frameIndex");
  if (frame_index_param != nullptr) {
    if (step != Debugger::kStepRewind) {
      // Only rewind supports the frameIndex parameter.
      js->PrintError(
          kInvalidParams,
          "%s: the 'frameIndex' parameter can only be used when rewinding",
          js->method());
      return;
    }
    frame_index = UIntParameter::Parse(js->LookupParam("frameIndex"));
  }
  Isolate* isolate = thread->isolate();
  if (isolate->message_handler()->is_paused_on_start()) {
    // If the user is issuing a 'Over' or an 'Out' step, that is the
    // same as a regular resume request.
    if (step == Debugger::kStepInto) {
      isolate->debugger()->EnterSingleStepMode();
    }
    isolate->message_handler()->set_should_pause_on_start(false);
    isolate->SetResumeRequest();
    if (Service::debug_stream.enabled()) {
      ServiceEvent event(isolate, ServiceEvent::kResume);
      Service::HandleEvent(&event);
    }
    PrintSuccess(js);
    return;
  }
  if (isolate->message_handler()->should_pause_on_start()) {
    isolate->message_handler()->set_should_pause_on_start(false);
    isolate->SetResumeRequest();
    if (Service::debug_stream.enabled()) {
      ServiceEvent event(isolate, ServiceEvent::kResume);
      Service::HandleEvent(&event);
    }
    PrintSuccess(js);
    return;
  }
  if (isolate->message_handler()->is_paused_on_exit()) {
    isolate->message_handler()->set_should_pause_on_exit(false);
    isolate->SetResumeRequest();
    // We don't send a resume event because we will be exiting.
    PrintSuccess(js);
    return;
  }
  if (isolate->debugger()->PauseEvent() == nullptr) {
    js->PrintError(kIsolateMustBePaused, nullptr);
    return;
  }

  const char* error = nullptr;
  if (!isolate->debugger()->SetResumeAction(step, frame_index, &error)) {
    js->PrintError(kCannotResume, "%s", error);
    return;
  }
  isolate->SetResumeRequest();
  PrintSuccess(js);
}

static const MethodParameter* const kill_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void Kill(Thread* thread, JSONStream* js) {
  const String& msg =
      String::Handle(String::New("isolate terminated by Kill service request"));
  const UnwindError& error = UnwindError::Handle(UnwindError::New(msg));
  error.set_is_user_initiated(true);
  Thread::Current()->set_sticky_error(error);
  PrintSuccess(js);
}

static const MethodParameter* const pause_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void Pause(Thread* thread, JSONStream* js) {
  // TODO(turnidge): This interrupt message could have been sent from
  // the service isolate directly, but would require some special case
  // code.  That would prevent this isolate getting double-interrupted
  // with OOB messages.
  Isolate* isolate = thread->isolate();
  isolate->SendInternalLibMessage(Isolate::kInterruptMsg,
                                  isolate->pause_capability());
  PrintSuccess(js);
}

static const MethodParameter* const enable_profiler_params[] = {
    nullptr,
};

static void EnableProfiler(Thread* thread, JSONStream* js) {
  Dart_StartProfiling();
  PrintSuccess(js);
}

static const MethodParameter* const get_tag_profile_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void GetTagProfile(Thread* thread, JSONStream* js) {
  JSONObject miniProfile(js);
  miniProfile.AddProperty("type", "TagProfile");
  thread->isolate()->vm_tag_counters()->PrintToJSONObject(&miniProfile);
}

static const MethodParameter* const get_cpu_samples_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new Int64Parameter("timeOriginMicros", /*required=*/false),
    new Int64Parameter("timeExtentMicros", /*required=*/false),
    nullptr,
};

static void GetCpuSamples(Thread* thread, JSONStream* js) {
  GetCpuSamplesCommon(TimelineOrSamplesResponseFormat::JSON, thread, js);
}

static const MethodParameter* const get_allocation_traces_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new IdParameter("classId", false),
    new Int64Parameter("timeOriginMicros", false),
    new Int64Parameter("timeExtentMicros", false),
    nullptr,
};

static void GetAllocationTraces(Thread* thread, JSONStream* js) {
  int64_t time_origin_micros =
      Int64Parameter::Parse(js->LookupParam("timeOriginMicros"));
  int64_t time_extent_micros =
      Int64Parameter::Parse(js->LookupParam("timeExtentMicros"));
  Isolate* isolate = thread->isolate();

  // Return only allocations for objects with classId.
  if (js->HasParam("classId")) {
    const char* class_id = js->LookupParam("classId");
    intptr_t cid = -1;
    GetPrefixedIntegerId(class_id, "classes/", &cid);
    if (IsValidClassId(isolate, cid)) {
      if (CheckProfilerDisabled(thread, js)) {
        return;
      }
      const Class& cls = Class::Handle(GetClassForId(isolate, cid));
      ProfilerService::PrintAllocationJSON(js, cls, time_origin_micros,
                                           time_extent_micros);
    } else {
      PrintInvalidParamError(js, "classId");
    }
  } else {
    // Otherwise, return allocations for all traced class IDs.
    if (CheckProfilerDisabled(thread, js)) {
      return;
    }
    ProfilerService::PrintAllocationJSON(js, time_origin_micros,
                                         time_extent_micros);
  }
}

static const MethodParameter* const clear_cpu_samples_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void ClearCpuSamples(Thread* thread, JSONStream* js) {
  ProfilerService::ClearSamples();
  PrintSuccess(js);
}

static void GetAllocationProfileImpl(Thread* thread,
                                     JSONStream* js,
                                     bool internal) {
  bool should_reset_accumulator = false;
  bool should_collect = false;
  if (js->HasParam("reset")) {
    if (js->ParamIs("reset", "true")) {
      should_reset_accumulator = true;
    } else {
      PrintInvalidParamError(js, "reset");
      return;
    }
  }
  if (js->HasParam("gc")) {
    if (js->ParamIs("gc", "true")) {
      should_collect = true;
    } else {
      PrintInvalidParamError(js, "gc");
      return;
    }
  }
  auto isolate_group = thread->isolate_group();
  if (should_reset_accumulator) {
    isolate_group->UpdateLastAllocationProfileAccumulatorResetTimestamp();
  }
  if (should_collect) {
    isolate_group->UpdateLastAllocationProfileGCTimestamp();
    isolate_group->heap()->CollectAllGarbage(GCReason::kDebugging);
  }
  isolate_group->class_table()->AllocationProfilePrintJSON(js, internal);
}

static const MethodParameter* const get_allocation_profile_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void GetAllocationProfilePublic(Thread* thread, JSONStream* js) {
  GetAllocationProfileImpl(thread, js, false);
}

static void GetAllocationProfile(Thread* thread, JSONStream* js) {
  GetAllocationProfileImpl(thread, js, true);
}

static const MethodParameter* const collect_all_garbage_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void CollectAllGarbage(Thread* thread, JSONStream* js) {
  auto heap = thread->isolate_group()->heap();
  heap->CollectAllGarbage(GCReason::kDebugging);
  PrintSuccess(js);
}

static const MethodParameter* const get_heap_map_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void GetHeapMap(Thread* thread, JSONStream* js) {
  auto isolate_group = thread->isolate_group();
  if (js->HasParam("gc")) {
    if (js->ParamIs("gc", "scavenge")) {
      isolate_group->heap()->CollectGarbage(thread, GCType::kScavenge,
                                            GCReason::kDebugging);
    } else if (js->ParamIs("gc", "mark-sweep")) {
      isolate_group->heap()->CollectGarbage(thread, GCType::kMarkSweep,
                                            GCReason::kDebugging);
    } else if (js->ParamIs("gc", "mark-compact")) {
      isolate_group->heap()->CollectGarbage(thread, GCType::kMarkCompact,
                                            GCReason::kDebugging);
    } else {
      PrintInvalidParamError(js, "gc");
      return;
    }
  }
  isolate_group->heap()->PrintHeapMapToJSONStream(isolate_group, js);
}

static const MethodParameter* const request_heap_snapshot_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void RequestHeapSnapshot(Thread* thread, JSONStream* js) {
  if (Service::heapsnapshot_stream.enabled()) {
    VmServiceHeapSnapshotChunkedWriter vmservice_writer(thread);
    HeapSnapshotWriter writer(thread, &vmservice_writer);
    writer.Write();
  }
  // TODO(koda): Provide some id that ties this request to async response(s).
  PrintSuccess(js);
}

#if defined(DART_HOST_OS_LINUX) || defined(DART_HOST_OS_ANDROID)
struct VMMapping {
  char path[256];
  size_t size;
};

static void AddVMMappings(JSONArray* rss_children) {
  FILE* fp = fopen("/proc/self/smaps", "r");
  if (fp == nullptr) {
    return;
  }

  MallocGrowableArray<VMMapping> mappings(10);
  char* line = nullptr;
  size_t line_buffer_size = 0;
  char path[256];
  char property[32];
  size_t start, end, size;
  while (getline(&line, &line_buffer_size, fp) > 0) {
    if (sscanf(line, "%zx-%zx", &start, &end) == 2) {
      // Each line has the following format:
      //
      //   start-end flags offset dev inode path
      //
      // We want to skip 4 fields and get to the last one (path).
      // Note that we can't scan backwards because path might contain white
      // space.
      const intptr_t kPathFieldIndex = 5;

      char* path_start = line;
      intptr_t current_field = 0;
      while (*path_start != '\0') {
        // Field separator.
        if (*path_start == ' ') {
          // Skip to the first non-space.
          while (*path_start == ' ') {
            path_start++;
          }
          current_field++;
          if (current_field == kPathFieldIndex) {
            break;
          }
          continue;
        }
        path_start++;
      }
      if (current_field != kPathFieldIndex) {
        continue;  // Malformed input.
      }

      strncpy(path, path_start, sizeof(path) - 1);
      path[sizeof(path) - 1] = '\0';
      int len = strlen(path);
      if ((len > 0) && path[len - 1] == '\n') {
        path[len - 1] = 0;
      }
    } else if (sscanf(line, "%s%zd", property, &size) == 2) {
      // Property line.
      // Skipping a few paths to avoid double counting:
      // (deleted) - memfd dual mapping in Dart heap
      // [heap] - sbrk area, should already included with malloc
      // <empty> - anonymous mappings, mostly in Dart heap (Linux)
      // [anon:dart-*] - as labelled (Android)
      if ((strcmp(property, "Rss:") == 0) && (size != 0) &&
          (strcmp(path, "(deleted)") != 0) && (strcmp(path, "[heap]") != 0) &&
          (strcmp(path, "") != 0) && (strcmp(path, "[anon:dart-heap]") != 0) &&
          (strcmp(path, "[anon:dart-code]") != 0) &&
          (strcmp(path, "[anon:dart-profiler]") != 0) &&
          (strcmp(path, "[anon:dart-timeline]") != 0) &&
          (strcmp(path, "[anon:dart-zone]") != 0)) {
        bool updated = false;
        for (intptr_t i = 0; i < mappings.length(); i++) {
          if (strcmp(mappings[i].path, path) == 0) {
            mappings[i].size += size;
            updated = true;
            break;
          }
        }
        if (!updated) {
          VMMapping mapping;
#if defined(__GNUC__) && !defined(__clang__)
#pragma GCC diagnostic ignored "-Wstringop-truncation"
#endif
          strncpy(mapping.path, path, sizeof(mapping.path) - 1);
          mapping.path[sizeof(mapping.path) - 1] = '\0';
          mapping.size = size;
          mappings.Add(mapping);
        }
      }
    }
  }
  fclose(fp);
  free(line);  // Free buffer allocated by getline.

  for (intptr_t i = 0; i < mappings.length(); i++) {
    JSONObject mapping(rss_children);
    mapping.AddProperty("name", mappings[i].path);
    mapping.AddProperty("description",
                        "Mapped file / shared library / executable");
    mapping.AddProperty64("size", mappings[i].size * KB);
    JSONArray(&mapping, "children");
  }
}
#endif

#if defined(DART_HOST_OS_WINDOWS) && !defined(DART_TARGET_OS_WINDOWS_UWP)
class VMMappingTrait {
 public:
  struct Pair {
    char* path;
    size_t size;
  };
  using Key = char*;
  using Value = size_t;

  static Key KeyOf(const Pair& kv) { return kv.path; }
  static Value ValueOf(const Pair& kv) { return kv.size; }
  static uword Hash(Key key) { return Utils::StringHash(key, strlen(key)); }
  static bool IsKeyEqual(const Pair& kv, Key key) {
    return strcmp(kv.path, key) == 0;
  }
};

static void AddVMMappings(JSONArray* rss_children) {
  MallocDirectChainedHashMap<VMMappingTrait> mappings;

  MEMORY_BASIC_INFORMATION mbi;
  uint8_t* address = nullptr;
  while (VirtualQuery(address, &mbi, sizeof(mbi)) == sizeof(mbi)) {
    if ((mbi.State == MEM_COMMIT) &&
        (mbi.Type == MEM_IMAGE || mbi.Type == MEM_MAPPED)) {
      char path_buffer[MAX_PATH];
      if (GetMappedFileNameA(GetCurrentProcess(), address, path_buffer,
                             MAX_PATH) != 0) {
        auto lookup_result = mappings.Lookup(path_buffer);
        if (lookup_result != nullptr) {
          lookup_result->size += mbi.RegionSize;
        } else {
          char* path = strdup(path_buffer);
          mappings.Insert({path, mbi.RegionSize});
        }
      }
    }
    if (address == nullptr) {
      address = reinterpret_cast<uint8_t*>(mbi.RegionSize);
    } else {
      address += mbi.RegionSize;
    }
  }

  auto it = mappings.GetIterator();
  for (auto* pair = it.Next(); pair != nullptr; pair = it.Next()) {
    JSONObject mapping(rss_children);
    mapping.AddProperty("name", pair->path);
    mapping.AddProperty("description",
                        "Mapped file / shared library / executable");
    mapping.AddProperty64("size", pair->size);
    JSONArray(&mapping, "children");
    free(pair->path);
  }
}
#endif

static intptr_t GetProcessMemoryUsageHelper(JSONStream* js) {
  JSONObject response(js);
  response.AddProperty("type", "ProcessMemoryUsage");

  JSONObject rss(&response, "root");
  rss.AddPropertyF("name", "Process %" Pd "", OS::ProcessId());
  rss.AddProperty("description", "Resident set size");
  rss.AddProperty64("size", Service::CurrentRSS());
  JSONArray rss_children(&rss, "children");

  intptr_t vm_size = 0;
  {
    JSONObject vm(&rss_children);
    {
      JSONArray vm_children(&vm, "children");

      {
        JSONObject profiler(&vm_children);
        profiler.AddProperty("name", "Profiler");
        profiler.AddProperty("description",
                             "Samples from the Dart VM's profiler");
        intptr_t size = Profiler::Size();
        vm_size += size;
        profiler.AddProperty64("size", size);
        JSONArray(&profiler, "children");
      }

      {
        JSONObject timeline(&vm_children);
        timeline.AddProperty("name", "Timeline");
        timeline.AddProperty(
            "description",
            "Timeline events from dart:developer and Dart_RecordTimelineEvent");
        intptr_t size = Timeline::recorder()->Size();
        vm_size += size;
        timeline.AddProperty64("size", size);
        JSONArray(&timeline, "children");
      }

      {
        JSONObject zone(&vm_children);
        zone.AddProperty("name", "Zone");
        zone.AddProperty("description", "Arena allocation in the Dart VM");
        intptr_t size = Zone::Size();
        vm_size += size;
        zone.AddProperty64("size", size);
        JSONArray(&zone, "children");
      }

      {
        JSONObject semi(&vm_children);
        semi.AddProperty("name", "Page Cache");
        semi.AddProperty("description", "Cached heap regions");
        intptr_t size = Page::CachedSize();
        vm_size += size;
        semi.AddProperty64("size", size);
        JSONArray(&semi, "children");
      }

      IsolateGroup::ForEach([&vm_children,
                             &vm_size](IsolateGroup* isolate_group) {
        int64_t capacity =
            (isolate_group->heap()->new_space()->CapacityInWords() +
             isolate_group->heap()->old_space()->CapacityInWords()) *
            kWordSize;
        // The more precise UsedInWords for new-space iterates pages and
        // potentially accesses Thread::top_/end_, which is not thread-safe
        // here. CapacityInWords is similar enough for purposes of service stats
        // for new-space, differing only up to the as-yet-unused portion of
        // active TLABs, unlike old-space where it can differ greatly in a
        // highly fragmented heap.
        int64_t used = (isolate_group->heap()->new_space()->CapacityInWords() +
                        isolate_group->heap()->old_space()->UsedInWords()) *
                       kWordSize;

        int64_t free = capacity - used;

        JSONObject group(&vm_children);
        group.AddPropertyF("name", "IsolateGroup %s",
                           isolate_group->source()->name);
        group.AddProperty("description", "Dart heap capacity");
        vm_size += capacity;
        group.AddProperty64("size", capacity);
        JSONArray group_children(&group, "children");

        {
          JSONObject jsused(&group_children);
          jsused.AddProperty("name", "Used");
          jsused.AddProperty("description", "");
          jsused.AddProperty64("size", used);
          JSONArray(&jsused, "children");
        }

        {
          JSONObject jsfree(&group_children);
          jsfree.AddProperty("name", "Free");
          jsfree.AddProperty("description", "");
          jsfree.AddProperty64("size", free);
          JSONArray(&jsfree, "children");
        }
      });
    }  // vm_children

    vm.AddProperty("name", "Dart VM");
    vm.AddProperty("description", "");
    vm.AddProperty64("size", vm_size);
  }

#if defined(DART_HOST_OS_LINUX) || defined(DART_HOST_OS_ANDROID)
  AddVMMappings(&rss_children);
#elif defined(DART_HOST_OS_WINDOWS) && !defined(DART_TARGET_OS_WINDOWS_UWP)
  AddVMMappings(&rss_children);
#endif
  // TODO(46166): Implement for other operating systems.

  return vm_size;
}

static const MethodParameter* const get_process_memory_usage_params[] = {
    nullptr,
};

static void GetProcessMemoryUsage(Thread* thread, JSONStream* js) {
  GetProcessMemoryUsageHelper(js);
}

void Service::SendInspectEvent(Isolate* isolate, const Object& inspectee) {
  if (!Service::debug_stream.enabled()) {
    return;
  }
  ServiceEvent event(isolate, ServiceEvent::kInspect);
  event.set_inspectee(&inspectee);
  Service::HandleEvent(&event);
}

void Service::SendEmbedderEvent(Isolate* isolate,
                                const char* stream_id,
                                const char* event_kind,
                                const uint8_t* bytes,
                                intptr_t bytes_len) {
  ServiceEvent event(isolate, ServiceEvent::kEmbedder);
  event.set_embedder_kind(event_kind);
  event.set_embedder_stream_id(stream_id);
  event.set_bytes(bytes, bytes_len);
  Service::HandleEvent(&event, /*enter_safepoint=*/false);
}

void Service::SendLogEvent(Isolate* isolate,
                           int64_t sequence_number,
                           int64_t timestamp,
                           intptr_t level,
                           const String& name,
                           const String& message,
                           const Instance& zone,
                           const Object& error,
                           const Instance& stack_trace) {
  if (!Service::logging_stream.enabled()) {
    return;
  }
  ServiceEvent::LogRecord log_record;
  log_record.sequence_number = sequence_number;
  log_record.timestamp = timestamp;
  log_record.level = level;
  log_record.name = &name;
  log_record.message = &message;
  log_record.zone = &zone;
  log_record.error = &error;
  log_record.stack_trace = &stack_trace;
  ServiceEvent event(isolate, ServiceEvent::kLogging);
  event.set_log_record(log_record);
  Service::HandleEvent(&event);
}

void Service::SendTimerEvent(Isolate* isolate, intptr_t milliseconds_overdue) {
  if (!Service::timer_stream.enabled()) {
    return;
  }
  ServiceEvent event(isolate, ServiceEvent::kTimerSignificantlyOverdue);
  event.set_milliseconds_overdue(milliseconds_overdue);
  Service::HandleEvent(&event);
}

void Service::SendExtensionEvent(Isolate* isolate,
                                 const String& event_kind,
                                 const String& event_data) {
  ServiceEvent::ExtensionEvent extension_event;
  extension_event.event_kind = &event_kind;
  extension_event.event_data = &event_data;
  ServiceEvent event(isolate, ServiceEvent::kExtension);
  event.set_extension_event(extension_event);
  Service::HandleEvent(&event);
}

static const MethodParameter* const get_persistent_handles_params[] = {
    ISOLATE_PARAMETER,
    nullptr,
};

class PersistentHandleVisitor : public HandleVisitor {
 public:
  explicit PersistentHandleVisitor(Zone* zone)
      : HandleVisitor(), zone_(zone), objects_(zone, 0) {}

  void Serialize(JSONArray* array) {
    for (intptr_t i = 0, n = objects_.length(); i < n; ++i) {
      JSONObject js_obj(array);
      js_obj.AddProperty("type", "_PersistentHandle");
      js_obj.AddProperty("object", *objects_[i]);
    }
  }

 protected:
  void VisitHandle(uword addr) override {
    auto* const handle = reinterpret_cast<PersistentHandle*>(addr);
    const Object& obj = Object::Handle(zone_, handle->ptr());
    objects_.Add(&obj);
  }

  Zone* const zone_;
  ZoneGrowableArray<const Object*> objects_;
};

class WeakPersistentHandleVisitor : public HandleVisitor {
 public:
  explicit WeakPersistentHandleVisitor(Zone* zone)
      : HandleVisitor(),
        zone_(zone),
        objects_(zone, 0),
        peers_(zone, 0),
        callbacks_(zone, 0),
        names_(zone, 0),
        external_sizes_(zone, 0) {}

  void Serialize(JSONArray* array) {
    for (intptr_t i = 0, n = objects_.length(); i < n; ++i) {
      JSONObject js_obj(array);
      js_obj.AddProperty("type", "_WeakPersistentHandle");
      js_obj.AddProperty("object", *objects_[i]);
      js_obj.AddPropertyF("peer", "0x%" Px "", peers_[i]);
      js_obj.AddPropertyF("callbackAddress", "0x%" Px "", callbacks_[i]);
      js_obj.AddProperty("callbackSymbolName", names_[i]);
      js_obj.AddPropertyF("externalSize", "%" Pd "", external_sizes_[i]);
    }
  }

 protected:
  void VisitHandle(uword addr) override {
    auto* const handle = reinterpret_cast<FinalizablePersistentHandle*>(addr);
    if (!handle->ptr()->IsHeapObject()) {
      return;  // Free handle.
    }
    const Object& obj = Object::Handle(zone_, handle->ptr());
    objects_.Add(&obj);
    peers_.Add(reinterpret_cast<uword>(handle->peer()));
    auto const callback = reinterpret_cast<uword>(handle->callback());
    callbacks_.Add(callback);
    // Attempt to include a native symbol name.
    if (auto* const name =
            NativeSymbolResolver::LookupSymbolName(callback, nullptr)) {
      names_.Add(OS::SCreate(zone_, "%s", name));
      NativeSymbolResolver::FreeSymbolName(name);
    } else {
      names_.Add("");
    }
    external_sizes_.Add(handle->external_size());
  }

  Zone* const zone_;
  ZoneGrowableArray<const Object*> objects_;
  ZoneGrowableArray<uword> peers_;
  ZoneGrowableArray<uword> callbacks_;
  ZoneGrowableArray<const char*> names_;
  ZoneGrowableArray<intptr_t> external_sizes_;
};

static void GetPersistentHandles(Thread* thread, JSONStream* js) {
  Isolate* isolate = thread->isolate();
  ASSERT(isolate != nullptr);

  ApiState* api_state = isolate->group()->api_state();
  ASSERT(api_state != nullptr);

  auto* const zone = thread->zone();
  {
    JSONObject obj(js);
    obj.AddProperty("type", "_PersistentHandles");

    // For each set of handles, separate out retrieval and serialization,
    // since the former requires a no safepoint scope and the latter may
    // cause allocation.
    {
      PersistentHandleVisitor visitor(zone);
      api_state->RunWithLockedPersistentHandles(
          [&](PersistentHandles& handles) { handles.Visit(&visitor); });
      JSONArray persistent_handles(&obj, "persistentHandles");
      visitor.Serialize(&persistent_handles);
    }

    {
      WeakPersistentHandleVisitor visitor(zone);
      api_state->RunWithLockedWeakPersistentHandles(
          [&](FinalizablePersistentHandles& handles) {
            handles.VisitHandles(&visitor);
          });
      JSONArray weak_persistent_handles(&obj, "weakPersistentHandles");
      visitor.Serialize(&weak_persistent_handles);
    }
  }
}

static const MethodParameter* const get_ports_private_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void GetPortsPrivate(Thread* thread, JSONStream* js) {
  MessageHandler* message_handler = thread->isolate()->message_handler();
  PortMap::PrintPortsForMessageHandler(message_handler, js);
}

static void RespondWithMalformedJson(Thread* thread, JSONStream* js) {
  JSONObject jsobj(js);
  jsobj.AddProperty("a", "a");
  JSONObject jsobj1(js);
  jsobj1.AddProperty("a", "a");
  JSONObject jsobj2(js);
  jsobj2.AddProperty("a", "a");
  JSONObject jsobj3(js);
  jsobj3.AddProperty("a", "a");
}

static void RespondWithMalformedObject(Thread* thread, JSONStream* js) {
  JSONObject jsobj(js);
  jsobj.AddProperty("bart", "simpson");
}

// Returns |true| if a heap object with the specified ID was successfully found,
// and |false| otherwise. If an object was found, it will be stored at address
// |obj|.
// This function should be used to handle shared logic between |GetObject| and
// |GetImplementationFields|.
static bool GetHeapObjectCommon(Thread* thread,
                                JSONStream* js,
                                const char* id,
                                Object* obj,
                                ObjectIdRing::LookupResult* lookup_result) {
  *obj = LookupHeapObject(thread, id, lookup_result);
  ASSERT(obj != nullptr);
  ASSERT(lookup_result != nullptr);
  if (obj->ptr() != Object::sentinel().ptr()) {
#if !defined(DART_PRECOMPILED_RUNTIME)
    // If obj is a script from dart:* and doesn't have source loaded, try and
    // load the source before sending the response.
    if (obj->IsScript()) {
      const Script& script = Script::Cast(*obj);
      if (!script.HasSource() && script.IsPartOfDartColonLibrary() &&
          Service::HasDartLibraryKernelForSources()) {
        const uint8_t* kernel_buffer = Service::dart_library_kernel();
        const intptr_t kernel_buffer_len =
            Service::dart_library_kernel_length();
        script.LoadSourceFromKernel(kernel_buffer, kernel_buffer_len);
      }
    }
#endif  // !defined(DART_PRECOMPILED_RUNTIME)
    // We found a heap object for this id.
    return true;
  }

  return false;
}

static const MethodParameter* const create_id_zone_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new StringParameter("backingBufferKind", true),
    new StringParameter("idAssignmentPolicy", true),
    new UIntParameter("capacity", false),
    nullptr,
};

static void CreateIdZone(Thread* thread, JSONStream* js) {
  ASSERT(thread != nullptr);
  ASSERT(js != nullptr);

  Isolate* isolate = thread->isolate();
  ASSERT(isolate != nullptr);

  const char* backing_buffer_kind_as_string =
      js->LookupParam("backingBufferKind");
  ASSERT(backing_buffer_kind_as_string != nullptr);
  ObjectIdRing::BackingBufferKind backing_buffer_kind;
  if (strcmp(backing_buffer_kind_as_string, "Ring") == 0) {
    backing_buffer_kind = ObjectIdRing::BackingBufferKind::kRing;
  } else {
    PrintInvalidParamError(js, "backingBufferKind");
    return;
  }

  const char* id_assignment_policy_as_string =
      js->LookupParam("idAssignmentPolicy");
  ASSERT(id_assignment_policy_as_string != nullptr);
  ObjectIdRing::IdPolicy id_assignment_policy;
  if (strcmp(id_assignment_policy_as_string, "AlwaysAllocate") == 0) {
    id_assignment_policy = ObjectIdRing::IdPolicy::kAllocateId;
  } else if (strcmp(id_assignment_policy_as_string, "ReuseExisting") == 0) {
    id_assignment_policy = ObjectIdRing::IdPolicy::kReuseId;
  } else {
    PrintInvalidParamError(js, "idAssignmentPolicy");
    return;
  }

  int32_t capacity = RingServiceIdZone::kFallbackCapacityForCreateIdZone;
  if (js->HasParam("capacity")) {
    intptr_t value = UIntParameter::Parse(js->LookupParam("capacity"));
    if (value < 0 || value > INT32_MAX) {
      PrintInvalidParamError(js, "capacity");
      return;
    }
    capacity = value;
  }
  isolate->AddServiceIdZone(backing_buffer_kind, id_assignment_policy, capacity)
      .PrintJSON(*js);
}

static const MethodParameter* const delete_id_zone_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void DeleteIdZone(Thread* thread, JSONStream* js) {
  ASSERT(thread != nullptr);
  ASSERT(js != nullptr);

  Isolate* isolate = thread->isolate();
  ASSERT(isolate != nullptr);

  const char* id_zone_id_arg = js->LookupParam("idZoneId");
  if (id_zone_id_arg == nullptr) {
    PrintMissingParamError(js, "idZoneId");
  }

  // If the `idZoneId` argument is not missing, we know that some properties of
  // |id_zone_id| have already been checked in |InvokeMethod| (search for
  // `js.set_id_zone(*id_zone)` to find the checks), so we can assert these
  // properties to be true below.

  intptr_t id_zone_id = ServiceIdZone::StringIdToInt(id_zone_id_arg);
  ASSERT(id_zone_id != -1);
  ASSERT(isolate->GetServiceIdZone(id_zone_id) != nullptr);

  isolate->DeleteServiceIdZone(id_zone_id);

  PrintSuccess(js);
}

static const MethodParameter* const get_object_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new UIntParameter("offset", false),
    new UIntParameter("count", false),
    nullptr,
};

static void GetObject(Thread* thread, JSONStream* js) {
  const char* id = js->LookupParam("objectId");
  if (id == nullptr) {
    PrintMissingParamError(js, "objectId");
    return;
  }
  if (js->HasParam("offset")) {
    intptr_t value = UIntParameter::Parse(js->LookupParam("offset"));
    if (value < 0) {
      PrintInvalidParamError(js, "offset");
      return;
    }
    js->set_offset(value);
  }
  if (js->HasParam("count")) {
    intptr_t value = UIntParameter::Parse(js->LookupParam("count"));
    if (value < 0) {
      PrintInvalidParamError(js, "count");
      return;
    }
    js->set_count(value);
  }

  // Handle heap objects.
  Object& obj = Object::Handle();
  ObjectIdRing::LookupResult lookup_result;
  if (GetHeapObjectCommon(thread, js, id, &obj, &lookup_result)) {
    obj.PrintJSON(js, false);
    return;
  } else if (lookup_result == ObjectIdRing::kCollected) {
    PrintSentinel(js, kCollectedSentinel);
    return;
  } else if (lookup_result == ObjectIdRing::kExpired) {
    PrintSentinel(js, kExpiredSentinel);
    return;
  }

  // Handle non-heap objects.
  Breakpoint* bpt = LookupBreakpoint(thread->isolate(), id, &lookup_result);
  if (bpt != nullptr) {
    bpt->PrintJSON(js);
    return;
  } else if (lookup_result == ObjectIdRing::kCollected) {
    PrintSentinel(js, kCollectedSentinel);
    return;
  }

  PrintInvalidParamError(js, "objectId");
}

static const MethodParameter* const get_implementation_fields_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    OBJECT_PARAMETER,
    nullptr,
};

static void GetImplementationFields(Thread* thread, JSONStream* js) {
  const char* id = js->LookupParam("objectId");

  // Handle heap objects.
  Object& obj = Object::Handle();
  ObjectIdRing::LookupResult lookup_result;
  if (GetHeapObjectCommon(thread, js, id, &obj, &lookup_result)) {
    obj.PrintImplementationFields(js);
    return;
  } else if (lookup_result == ObjectIdRing::kCollected) {
    PrintSentinel(js, kCollectedSentinel);
    return;
  } else if (lookup_result == ObjectIdRing::kExpired) {
    PrintSentinel(js, kExpiredSentinel);
    return;
  }

  // Handle non-heap objects.
  Breakpoint* bpt = LookupBreakpoint(thread->isolate(), id, &lookup_result);
  if (bpt != nullptr) {
    const JSONObject jsobj(js);
    jsobj.AddProperty("type", "ImplementationFields");
    JSONArray jsarr_fields(&jsobj, "fields");
    return;
  } else if (lookup_result == ObjectIdRing::kCollected) {
    PrintSentinel(js, kCollectedSentinel);
    return;
  }

  PrintInvalidParamError(js, "objectId");
}

static const MethodParameter* const get_object_store_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void GetObjectStore(Thread* thread, JSONStream* js) {
  JSONObject jsobj(js);
  thread->isolate_group()->object_store()->PrintToJSONObject(&jsobj);
}

static const MethodParameter* const get_isolate_object_store_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void GetIsolateObjectStore(Thread* thread, JSONStream* js) {
  JSONObject jsobj(js);
  thread->isolate()->isolate_object_store()->PrintToJSONObject(&jsobj);
}

static const MethodParameter* const get_class_list_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void GetClassList(Thread* thread, JSONStream* js) {
  ClassTable* table = thread->isolate_group()->class_table();
  JSONObject jsobj(js);
  table->PrintToJSONObject(&jsobj);
}

static const MethodParameter* const get_type_arguments_list_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    nullptr,
};

static void GetTypeArgumentsList(Thread* thread, JSONStream* js) {
  bool only_with_instantiations = false;
  if (js->ParamIs("onlyWithInstantiations", "true")) {
    only_with_instantiations = true;
  }
  Zone* zone = thread->zone();
  ObjectStore* object_store = thread->isolate_group()->object_store();
  CanonicalTypeArgumentsSet typeargs_table(
      zone, object_store->canonical_type_arguments());
  const intptr_t table_size = typeargs_table.NumEntries();
  const intptr_t table_used = typeargs_table.NumOccupied();
  const Array& typeargs_array =
      Array::Handle(zone, HashTables::ToArray(typeargs_table, false));
  ASSERT(typeargs_array.Length() == table_used);
  TypeArguments& typeargs = TypeArguments::Handle(zone);
  JSONObject jsobj(js);
  jsobj.AddProperty("type", "TypeArgumentsList");
  jsobj.AddProperty("canonicalTypeArgumentsTableSize", table_size);
  jsobj.AddProperty("canonicalTypeArgumentsTableUsed", table_used);
  JSONArray members(&jsobj, "typeArguments");
  for (intptr_t i = 0; i < table_used; i++) {
    typeargs ^= typeargs_array.At(i);
    if (!typeargs.IsNull()) {
      if (!only_with_instantiations || typeargs.HasInstantiations()) {
        members.AddValue(typeargs);
      }
    }
  }
  typeargs_table.Release();
}

static const MethodParameter* const get_version_params[] = {
    NO_ISOLATE_PARAMETER,
    nullptr,
};

static void GetVersion(Thread* thread, JSONStream* js) {
  JSONObject jsobj(js);
  jsobj.AddProperty("type", "Version");
  jsobj.AddProperty("major",
                    static_cast<intptr_t>(SERVICE_PROTOCOL_MAJOR_VERSION));
  jsobj.AddProperty("minor",
                    static_cast<intptr_t>(SERVICE_PROTOCOL_MINOR_VERSION));
  jsobj.AddProperty("_privateMajor", static_cast<intptr_t>(0));
  jsobj.AddProperty("_privateMinor", static_cast<intptr_t>(0));
}

class ServiceIsolateVisitor : public IsolateVisitor {
 public:
  explicit ServiceIsolateVisitor(JSONArray* jsarr) : jsarr_(jsarr) {}
  virtual ~ServiceIsolateVisitor() {}

  void VisitIsolate(Isolate* isolate) {
    if (!IsSystemIsolate(isolate) && isolate->is_service_registered()) {
      jsarr_->AddValue(isolate);
    }
  }

 private:
  JSONArray* jsarr_;
};

class SystemServiceIsolateVisitor : public IsolateVisitor {
 public:
  explicit SystemServiceIsolateVisitor(JSONArray* jsarr) : jsarr_(jsarr) {}
  virtual ~SystemServiceIsolateVisitor() {}

  void VisitIsolate(Isolate* isolate) {
    if (IsSystemIsolate(isolate)) {
      jsarr_->AddValue(isolate);
    }
  }

 private:
  JSONArray* jsarr_;
};

static const MethodParameter* const get_vm_params[] = {
    NO_ISOLATE_PARAMETER,
    nullptr,
};

void Service::PrintJSONForEmbedderInformation(JSONObject* jsobj) {
  if (embedder_information_callback_ != nullptr) {
    Dart_EmbedderInformation info = {
        0,        // version
        nullptr,  // name
        -1,       // max_rss
        -1        // current_rss
    };
    embedder_information_callback_(&info);
    ASSERT(info.version == DART_EMBEDDER_INFORMATION_CURRENT_VERSION);
    if (info.name != nullptr) {
      jsobj->AddProperty("_embedder", info.name);
    }
    if (info.max_rss >= 0) {
      jsobj->AddProperty64("_maxRSS", info.max_rss);
    }
    if (info.current_rss >= 0) {
      jsobj->AddProperty64("_currentRSS", info.current_rss);
    }
  }
}

void Service::PrintJSONForVM(JSONStream* js, bool ref) {
  JSONObject jsobj(js);
  jsobj.AddProperty("type", (ref ? "@VM" : "VM"));
  jsobj.AddProperty("name", GetVMName());
  if (ref) {
    return;
  }
  jsobj.AddProperty("architectureBits", static_cast<intptr_t>(kBitsPerWord));
  jsobj.AddProperty("hostCPU", HostCPUFeatures::hardware());
  jsobj.AddProperty("operatingSystem", OS::Name());
  jsobj.AddProperty("targetCPU", CPU::Id());
  jsobj.AddProperty("version", Version::String());
#if defined(DART_PRECOMPILED_RUNTIME)
  Snapshot::Kind kind = Snapshot::kFullAOT;
#else
  Snapshot::Kind kind = Snapshot::kFullJIT;
#endif
  char* features_string = Dart::FeaturesString(nullptr, kind);
  jsobj.AddProperty("_features", features_string);
  free(features_string);
  jsobj.AddProperty("_profilerMode", FLAG_profile_vm ? "VM" : "Dart");
  jsobj.AddProperty64("pid", OS::ProcessId());
  jsobj.AddPropertyTimeMillis(
      "startTime", OS::GetCurrentTimeMillis() - Dart::UptimeMillis());
  PrintJSONForEmbedderInformation(&jsobj);
  // Construct the isolate and isolate_groups list.
  {
    JSONArray jsarr(&jsobj, "isolates");
    ServiceIsolateVisitor visitor(&jsarr);
    Isolate::VisitIsolates(&visitor);
  }
  {
    JSONArray jsarr(&jsobj, "systemIsolates");
    SystemServiceIsolateVisitor visitor(&jsarr);
    Isolate::VisitIsolates(&visitor);
  }
  {
    JSONArray jsarr_isolate_groups(&jsobj, "isolateGroups");
    IsolateGroup::ForEach([&jsarr_isolate_groups](IsolateGroup* isolate_group) {
      if (!isolate_group->is_system_isolate_group()) {
        jsarr_isolate_groups.AddValue(isolate_group);
      }
    });
  }
  {
    JSONArray jsarr_isolate_groups(&jsobj, "systemIsolateGroups");
    IsolateGroup::ForEach([&jsarr_isolate_groups](IsolateGroup* isolate_group) {
      if (isolate_group->is_system_isolate_group()) {
        jsarr_isolate_groups.AddValue(isolate_group);
      }
    });
  }
  {
    JSONStream discard_js;
    intptr_t vm_memory = GetProcessMemoryUsageHelper(&discard_js);
    jsobj.AddProperty("_currentMemory", vm_memory);
  }
}

static void GetVM(Thread* thread, JSONStream* js) {
  Service::PrintJSONForVM(js, false);
}

class UriMappingTraits {
 public:
  static const char* Name() { return "UriMappingTraits"; }
  static bool ReportStats() { return false; }

  static bool IsMatch(const Object& a, const Object& b) {
    const String& a_str = String::Cast(a);
    const String& b_str = String::Cast(b);

    ASSERT(a_str.HasHash() && b_str.HasHash());
    return a_str.Equals(b_str);
  }

  static uword Hash(const Object& key) { return String::Cast(key).Hash(); }

  static ObjectPtr NewKey(const String& str) { return str.ptr(); }
};

typedef UnorderedHashMap<UriMappingTraits> UriMapping;

static void PopulateUriMappings(Thread* thread) {
  Zone* zone = thread->zone();
  auto object_store = thread->isolate_group()->object_store();
  UriMapping uri_to_resolved_uri(HashTables::New<UriMapping>(16, Heap::kOld));
  UriMapping resolved_uri_to_uri(HashTables::New<UriMapping>(16, Heap::kOld));

  const auto& libs =
      GrowableObjectArray::Handle(zone, object_store->libraries());
  intptr_t num_libs = libs.Length();

  Library& lib = Library::Handle(zone);
  Script& script = Script::Handle(zone);
  Array& scripts = Array::Handle(zone);
  String& uri = String::Handle(zone);
  String& resolved_uri = String::Handle(zone);
#if defined(DART_HOST_OS_WINDOWS) || defined(DART_HOST_OS_MACOS)
  String& tmp = thread->StringHandle();
#endif
  for (intptr_t i = 0; i < num_libs; ++i) {
    lib ^= libs.At(i);
    scripts ^= lib.LoadedScripts();
    intptr_t num_scripts = scripts.Length();
    for (intptr_t j = 0; j < num_scripts; ++j) {
      script ^= scripts.At(j);
      uri ^= script.url();
      resolved_uri ^= script.resolved_url();
      uri_to_resolved_uri.UpdateOrInsert(uri, resolved_uri);
      resolved_uri_to_uri.UpdateOrInsert(resolved_uri, uri);

#if defined(DART_HOST_OS_WINDOWS) || defined(DART_HOST_OS_MACOS)
      // Allow for case insensitive matching on platforms that might allow for
      // case insensitive paths.
      tmp = String::ToLowerCase(uri);
      uri_to_resolved_uri.UpdateOrInsert(tmp, resolved_uri);
      tmp = String::ToLowerCase(resolved_uri);
      resolved_uri_to_uri.UpdateOrInsert(tmp, uri);
#endif
    }
  }

  object_store->set_uri_to_resolved_uri_map(uri_to_resolved_uri.Release());
  object_store->set_resolved_uri_to_uri_map(resolved_uri_to_uri.Release());
  Smi& count = Smi::Handle(zone, Smi::New(num_libs));
  object_store->set_last_libraries_count(count);
}

static void LookupScriptUrisImpl(Thread* thread,
                                 JSONStream* js,
                                 bool lookup_resolved) {
  Zone* zone = thread->zone();
  auto object_store = thread->isolate_group()->object_store();

  const auto& libs =
      GrowableObjectArray::Handle(zone, object_store->libraries());
  Smi& last_libraries_count =
      Smi::Handle(zone, object_store->last_libraries_count());
  if ((object_store->uri_to_resolved_uri_map() == Array::null()) ||
      (object_store->resolved_uri_to_uri_map() == Array::null()) ||
      (last_libraries_count.Value() != libs.Length())) {
    PopulateUriMappings(thread);
  }
  const char* uris_arg = js->LookupParam("uris");
  if (uris_arg == nullptr) {
    PrintMissingParamError(js, "uris");
    return;
  }

  const GrowableObjectArray& uris =
      GrowableObjectArray::Handle(zone, GrowableObjectArray::New());
  intptr_t uris_length = ParseJSONArray(thread, uris_arg, uris);
  if (uris_length < 0) {
    PrintInvalidParamError(js, "uris");
    return;
  }

  UriMapping map(lookup_resolved ? object_store->uri_to_resolved_uri_map()
                                 : object_store->resolved_uri_to_uri_map());
  JSONObject jsobj(js);
  jsobj.AddProperty("type", "UriList");

  {
    JSONArray uris_array(&jsobj, "uris");
    String& uri = String::Handle(zone);
    String& res = String::Handle(zone);
    for (intptr_t i = 0; i < uris.Length(); ++i) {
      uri ^= uris.At(i);
      res ^= map.GetOrNull(uri);
#if defined(DART_HOST_OS_WINDOWS) || defined(DART_HOST_OS_MACOS)
      // Windows and MacOS paths can be case insensitive, so we should allow for
      // case insensitive URI mappings on Windows and MacOS.
      if (res.IsNull()) {
        String& lower_case_uri = thread->StringHandle();
        lower_case_uri = String::ToLowerCase(uri);
        res ^= map.GetOrNull(lower_case_uri);
      }
#endif  // defined(DART_HOST_OS_WINDOWS) || defined(DART_HOST_OS_MACOS)
      if (res.IsNull()) {
        uris_array.AddValueNull();
      } else {
        uris_array.AddValue(res.ToCString());
      }
    }
  }
  map.Release();
}

static const MethodParameter* const lookup_resolved_package_uris_params[] = {
    ISOLATE_PARAMETER,
    nullptr,
};

static void LookupResolvedPackageUris(Thread* thread, JSONStream* js) {
  LookupScriptUrisImpl(thread, js, true);
}

static const MethodParameter* const lookup_package_uris_params[] = {
    ISOLATE_PARAMETER,
    nullptr,
};

static void LookupPackageUris(Thread* thread, JSONStream* js) {
  LookupScriptUrisImpl(thread, js, false);
}

static const char* const exception_pause_mode_names[] = {
    "All",
    "None",
    "Unhandled",
    nullptr,
};

static Dart_ExceptionPauseInfo exception_pause_mode_values[] = {
    kPauseOnAllExceptions,
    kNoPauseOnExceptions,
    kPauseOnUnhandledExceptions,
    kInvalidExceptionPauseInfo,
};

static const MethodParameter* const set_exception_pause_mode_params[] = {
    ISOLATE_PARAMETER,
    new EnumParameter("mode", true, exception_pause_mode_names),
    nullptr,
};

static void SetExceptionPauseMode(Thread* thread, JSONStream* js) {
  const char* mode = js->LookupParam("mode");
  if (mode == nullptr) {
    PrintMissingParamError(js, "mode");
    return;
  }
  Dart_ExceptionPauseInfo info =
      EnumMapper(mode, exception_pause_mode_names, exception_pause_mode_values);
  if (info == kInvalidExceptionPauseInfo) {
    PrintInvalidParamError(js, "mode");
    return;
  }
  Isolate* isolate = thread->isolate();
  isolate->debugger()->SetExceptionPauseInfo(info);
  if (Service::debug_stream.enabled()) {
    ServiceEvent event(isolate, ServiceEvent::kDebuggerSettingsUpdate);
    Service::HandleEvent(&event);
  }
  PrintSuccess(js);
}

static const MethodParameter* const set_isolate_pause_mode_params[] = {
    ISOLATE_PARAMETER,
    new EnumParameter("exceptionPauseMode", false, exception_pause_mode_names),
    new BoolParameter("shouldPauseOnExit", false),
    nullptr,
};

static void SetIsolatePauseMode(Thread* thread, JSONStream* js) {
  bool state_changed = false;
  const char* exception_pause_mode = js->LookupParam("exceptionPauseMode");
  if (exception_pause_mode != nullptr) {
    Dart_ExceptionPauseInfo info =
        EnumMapper(exception_pause_mode, exception_pause_mode_names,
                   exception_pause_mode_values);
    if (info == kInvalidExceptionPauseInfo) {
      PrintInvalidParamError(js, "exceptionPauseMode");
      return;
    }
    Isolate* isolate = thread->isolate();
    isolate->debugger()->SetExceptionPauseInfo(info);
    state_changed = true;
  }

  const char* pause_isolate_on_exit = js->LookupParam("shouldPauseOnExit");
  if (pause_isolate_on_exit != nullptr) {
    bool enable = BoolParameter::Parse(pause_isolate_on_exit, false);
    thread->isolate()->message_handler()->set_should_pause_on_exit(enable);
    state_changed = true;
  }

  if (state_changed && Service::debug_stream.enabled()) {
    ServiceEvent event(thread->isolate(),
                       ServiceEvent::kDebuggerSettingsUpdate);
    Service::HandleEvent(&event);
  }
  PrintSuccess(js);
}

static const MethodParameter* const set_breakpoint_state_params[] = {
    ISOLATE_PARAMETER,
    new IdParameter("breakpointId", true),
    new BoolParameter("enable", true),
    nullptr,
};

static void SetBreakpointState(Thread* thread, JSONStream* js) {
  Isolate* isolate = thread->isolate();
  const char* bpt_id = js->LookupParam("breakpointId");
  bool enable = BoolParameter::Parse(js->LookupParam("enable"), true);
  ObjectIdRing::LookupResult lookup_result;
  Breakpoint* bpt = LookupBreakpoint(isolate, bpt_id, &lookup_result);
  // TODO(bkonyi): Should we return a different error for bpts which
  // have been already removed?
  if (bpt == nullptr) {
    PrintInvalidParamError(js, "breakpointId");
    return;
  }
  if (isolate->debugger()->SetBreakpointState(bpt, enable)) {
    if (Service::debug_stream.enabled()) {
      ServiceEvent event(isolate, ServiceEvent::kBreakpointUpdated);
      event.set_breakpoint(bpt);
      Service::HandleEvent(&event);
    }
  }
  bpt->PrintJSON(js);
}

static const MethodParameter* const get_flag_list_params[] = {
    NO_ISOLATE_PARAMETER,
    nullptr,
};

static void GetFlagList(Thread* thread, JSONStream* js) {
  Flags::PrintJSON(js);
}

static const MethodParameter* const set_flags_params[] = {
    NO_ISOLATE_PARAMETER,
    nullptr,
};

static void SetFlag(Thread* thread, JSONStream* js) {
  const char* flag_name = js->LookupParam("name");
  if (flag_name == nullptr) {
    PrintMissingParamError(js, "name");
    return;
  }
  const char* flag_value = js->LookupParam("value");
  if (flag_value == nullptr) {
    PrintMissingParamError(js, "value");
    return;
  }

  if (Flags::Lookup(flag_name) == nullptr) {
    JSONObject jsobj(js);
    jsobj.AddProperty("type", "Error");
    jsobj.AddProperty("message", "Cannot set flag: flag not found");
    return;
  }

  // Changing most flags at runtime is dangerous because e.g., it may leave the
  // behavior generated code and the runtime out of sync.
  const uintptr_t kProfilePeriodIndex = 3;
  const uintptr_t kProfilerIndex = 4;
  const char* kAllowedFlags[] = {
      "pause_isolates_on_start",
      "pause_isolates_on_exit",
      "pause_isolates_on_unhandled_exceptions",
      "profile_period",
      "profiler",
  };

  bool allowed = false;
  bool profile_period = false;
  bool profiler = false;
  for (size_t i = 0; i < ARRAY_SIZE(kAllowedFlags); i++) {
    if (strcmp(flag_name, kAllowedFlags[i]) == 0) {
      allowed = true;
      profile_period = (i == kProfilePeriodIndex);
      profiler = (i == kProfilerIndex);
      break;
    }
  }

  if (!allowed) {
    JSONObject jsobj(js);
    jsobj.AddProperty("type", "Error");
    jsobj.AddProperty("message", "Cannot set flag: cannot change at runtime");
    return;
  }

  const char* error = nullptr;
  if (Flags::SetFlag(flag_name, flag_value, &error)) {
    PrintSuccess(js);
    if (profile_period || profiler) {
      // Update profiler config based on FLAG_* values.
      Profiler::SetConfig({});
    }
    if (Service::vm_stream.enabled()) {
      ServiceEvent event(ServiceEvent::kVMFlagUpdate);
      event.set_flag_name(flag_name);
      event.set_flag_new_value(flag_value);
      Service::HandleEvent(&event);
    }
  } else {
    JSONObject jsobj(js);
    jsobj.AddProperty("type", "Error");
    jsobj.AddProperty("message", error);
  }
}

static const MethodParameter* const set_library_debuggable_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new IdParameter("libraryId", true),
    new BoolParameter("isDebuggable", true),
    nullptr,
};

static void SetLibraryDebuggable(Thread* thread, JSONStream* js) {
  const char* lib_id = js->LookupParam("libraryId");
  ObjectIdRing::LookupResult lookup_result;
  Object& obj =
      Object::Handle(LookupHeapObject(thread, lib_id, &lookup_result));
  const bool is_debuggable =
      BoolParameter::Parse(js->LookupParam("isDebuggable"), false);
  if (obj.IsLibrary()) {
    const Library& lib = Library::Cast(obj);
    lib.set_debuggable(is_debuggable);
    PrintSuccess(js);
    return;
  }
  PrintInvalidParamError(js, "libraryId");
}

static const MethodParameter* const set_name_params[] = {
    ISOLATE_PARAMETER,
    new MethodParameter("name", true),
    nullptr,
};

static void SetName(Thread* thread, JSONStream* js) {
  Isolate* isolate = thread->isolate();
  isolate->set_name(js->LookupParam("name"));
  if (Service::isolate_stream.enabled()) {
    ServiceEvent event(isolate, ServiceEvent::kIsolateUpdate);
    Service::HandleEvent(&event);
  }
  PrintSuccess(js);
  return;
}

static const MethodParameter* const set_vm_name_params[] = {
    NO_ISOLATE_PARAMETER,
    new MethodParameter("name", true),
    nullptr,
};

static void SetVMName(Thread* thread, JSONStream* js) {
  const char* name_param = js->LookupParam("name");
  free(vm_name);
  vm_name = Utils::StrDup(name_param);
  if (Service::vm_stream.enabled()) {
    ServiceEvent event(ServiceEvent::kVMUpdate);
    Service::HandleEvent(&event);
  }
  PrintSuccess(js);
  return;
}

static const MethodParameter* const set_trace_class_allocation_params[] = {
    RUNNABLE_ISOLATE_PARAMETER,
    new IdParameter("classId", true),
    new BoolParameter("enable", true),
    nullptr,
};

static void SetTraceClassAllocation(Thread* thread, JSONStream* js) {
  const char* class_id = js->LookupParam("classId");
  const bool enable = BoolParameter::Parse(js->LookupParam("enable"));
  intptr_t cid = -1;
  GetPrefixedIntegerId(class_id, "classes/", &cid);
  Isolate* isolate = thread->isolate();
  if (!IsValidClassId(isolate, cid)) {
    PrintInvalidParamError(js, "classId");
    return;
  }
  const Class& cls = Class::Handle(GetClassForId(isolate, cid));
  ASSERT(!cls.IsNull());
  cls.SetTraceAllocation(enable);
  PrintSuccess(js);
}

static const MethodParameter* const get_default_classes_aliases_params[] = {
    NO_ISOLATE_PARAMETER,
    nullptr,
};

static void GetDefaultClassesAliases(Thread* thread, JSONStream* js) {
  JSONObject jsobj(js);
  jsobj.AddProperty("type", "ClassesAliasesMap");

  JSONObject map(&jsobj, "map");

#define DEFINE_ADD_VALUE_F(id)                                                 \
  internals.AddValueF("classes/%" Pd, static_cast<intptr_t>(id));
#define DEFINE_ADD_VALUE_F_CID(clazz) DEFINE_ADD_VALUE_F(k##clazz##Cid)
  {
    JSONArray internals(&map, "<VM Internals>");
    for (intptr_t id = kFirstInternalOnlyCid; id <= kLastInternalOnlyCid;
         ++id) {
      DEFINE_ADD_VALUE_F(id);
    }
    DEFINE_ADD_VALUE_F_CID(LibraryPrefix);
  }
  {
    JSONArray internals(&map, "Type");
    for (intptr_t id = kAbstractTypeCid; id <= kTypeParameterCid; ++id) {
      DEFINE_ADD_VALUE_F(id);
    }
  }
  {
    JSONArray internals(&map, "Object");
    DEFINE_ADD_VALUE_F_CID(Instance);
  }
  {
    JSONArray internals(&map, "Closure");
    DEFINE_ADD_VALUE_F_CID(Closure);
    DEFINE_ADD_VALUE_F_CID(Context);
  }
  {
    JSONArray internals(&map, "Int");
    for (intptr_t id = kIntegerCid; id <= kMintCid; ++id) {
      DEFINE_ADD_VALUE_F(id);
    }
  }
  {
    JSONArray internals(&map, "Double");
    DEFINE_ADD_VALUE_F_CID(Double);
  }
  {
    JSONArray internals(&map, "String");
    CLASS_LIST_STRINGS(DEFINE_ADD_VALUE_F_CID)
  }
  {
    JSONArray internals(&map, "List");
    CLASS_LIST_ARRAYS(DEFINE_ADD_VALUE_F_CID)
    DEFINE_ADD_VALUE_F_CID(ByteBuffer)
  }
  {
    JSONArray internals(&map, "Map");
    CLASS_LIST_MAPS(DEFINE_ADD_VALUE_F_CID)
  }

  {
    JSONArray internals(&map, "Set");
    CLASS_LIST_SETS(DEFINE_ADD_VALUE_F_CID)
  }
#define DEFINE_ADD_MAP_KEY(clazz)                                              \
  {                                                                            \
    JSONArray internals(&map, #clazz);                                         \
    DEFINE_ADD_VALUE_F_CID(TypedData##clazz)                                   \
    DEFINE_ADD_VALUE_F_CID(TypedData##clazz##View)                             \
    DEFINE_ADD_VALUE_F_CID(ExternalTypedData##clazz)                           \
    DEFINE_ADD_VALUE_F_CID(UnmodifiableTypedData##clazz##View)                 \
  }
  CLASS_LIST_TYPED_DATA(DEFINE_ADD_MAP_KEY)
#undef DEFINE_ADD_MAP_KEY
#define DEFINE_ADD_MAP_KEY(clazz)                                              \
  {                                                                            \
    JSONArray internals(&map, #clazz);                                         \
    DEFINE_ADD_VALUE_F_CID(Ffi##clazz)                                         \
  }
  CLASS_LIST_FFI(DEFINE_ADD_MAP_KEY)
#undef DEFINE_ADD_MAP_KEY
#undef DEFINE_ADD_VALUE_F_CID
#undef DEFINE_ADD_VALUE_F
}

static constexpr const char* kNativeMemoryAddressParam = "address";
static constexpr const char* kNativeMemorySizeParam = "size";
static constexpr const char* kNativeMemoryBytesKey = "bytes";
static constexpr const char* kNativeMemoryTypeKey = "type";

static void ReadNativeMemoryHelper(JSONStream* js,
                                   uintptr_t address,
                                   intptr_t size) {
  if (address == 0) {
    js->PrintError(kInvalidParams, "null pointer");
    return;
  }

  if (address > (UINTPTR_MAX - static_cast<uintptr_t>(size))) {
    js->PrintError(kInvalidParams, "address + size overflows address space");
    return;
  }

  CAllocUniquePtr<uint8_t> buffer(reinterpret_cast<uint8_t*>(malloc(size)));

  if (buffer.get() == nullptr) {
    js->PrintError(kInternalError, "failed to allocate buffer");
    return;
  }

  const char* read_error = nullptr;

#if defined(DART_HOST_OS_LINUX) || defined(DART_HOST_OS_ANDROID)
  bool ok = OS::SafeReadMemory(reinterpret_cast<void*>(address), buffer.get(),
                               size, &read_error);
#elif defined(DART_HOST_OS_MACOS) || defined(DART_HOST_OS_IOS)
  bool ok = OS::SafeReadMemory(reinterpret_cast<void*>(address), buffer.get(),
                               size, &read_error);
#elif defined(DART_HOST_OS_WINDOWS)
  bool ok = OS::SafeReadMemory(reinterpret_cast<void*>(address), buffer.get(),
                               size, &read_error);
#else
  bool ok = false;  // TODO(thenourhan): implement for other platforms
#endif

  if (!ok) {
    const char* error_msg =
        (read_error != nullptr) ? read_error : "address not readable";
    js->PrintError(kNativeMemoryReadError, "%s", error_msg);
    return;
  }

  CStringUniquePtr hex(reinterpret_cast<char*>(malloc(size * 2 + 1)));
  for (intptr_t i = 0; i < size; i++) {
    Utils::SNPrint(hex.get() + i * 2, 3, "%02x", buffer.get()[i]);
  }
  hex.get()[size * 2] = '\0';

  JSONObject response(js);
  response.AddProperty(kNativeMemoryTypeKey, "NativeMemory");
  response.AddPropertyF(kNativeMemoryAddressParam, "0x%" Px "", address);
  response.AddProperty64(kNativeMemorySizeParam, size);
  response.AddProperty(kNativeMemoryBytesKey, hex.get());
}

static const MethodParameter* const read_native_memory_params[] = {
    NATIVE_MEMORY_ADDRESS_PARAMETER,
    NATIVE_MEMORY_SIZE_PARAMETER,
    nullptr,
};

// Parameters:
//   address : string
//     Hex string without '0x' prefix (e.g. "7f3a00001000").
//
//   size : int
//     Number of bytes to read. Must be between 1 and 1048576 (1 MB).
//
// Responses:
//
//   On success:
//     {
//       "type":    "NativeMemory",
//       "address": "0x7f3a00001000",
//       "size":    8,
//       "bytes":   "0102030405060708"
//     }
//
//   On read failure (unmapped/invalid address):
//     JSON-RPC error code 1004 with OS error string as details
//          "Input/output error" (Linux EIO)
//          "ReadProcessMemory failed (error 299)" (Windows)
//          "mach_vm_read_overwrite failed" (macOS)
//
//   On invalid params (null pointer, address overflow):
//     JSON-RPC error code -32602 "Invalid params"
//
static void ReadNativeMemory(Thread* thread, JSONStream* js) {
  const char* address_str = js->LookupParam(kNativeMemoryAddressParam);
  const char* size_str = js->LookupParam(kNativeMemorySizeParam);

  if (address_str == nullptr) {
    PrintMissingParamError(js, kNativeMemoryAddressParam);
    return;
  }
  if (size_str == nullptr) {
    PrintMissingParamError(js, kNativeMemorySizeParam);
    return;
  }

  uintptr_t address = 0;
  intptr_t size = 0;

  if (!GetUnsignedIntegerId(address_str, &address, 16)) {
    PrintInvalidParamError(js, kNativeMemoryAddressParam);
    return;
  }
  if (!GetIntegerId(size_str, &size) || size <= 0 || size > 1 * MB) {
    PrintInvalidParamError(js, kNativeMemorySizeParam);
    return;
  }

  ReadNativeMemoryHelper(js, static_cast<uintptr_t>(address),
                         static_cast<intptr_t>(size));
}

// clang-format off
static const ServiceMethodDescriptor service_methods_[] = {
  { "_echo", Echo,
    nullptr },
  { "_respondWithMalformedJson", RespondWithMalformedJson,
    nullptr },
  { "_respondWithMalformedObject", RespondWithMalformedObject,
    nullptr },
  { "_triggerEchoEvent", TriggerEchoEvent,
    nullptr },
  { "addBreakpoint", AddBreakpoint,
    add_breakpoint_params },
  { "addBreakpointWithScriptUri", AddBreakpointWithScriptUri,
    add_breakpoint_with_script_uri_params },
  { "addBreakpointAtEntry", AddBreakpointAtEntry,
    add_breakpoint_at_entry_params },
  { "_addBreakpointAtActivation", AddBreakpointAtActivation,
    add_breakpoint_at_activation_params },
  { "_buildExpressionEvaluationScope", BuildExpressionEvaluationScope,
    build_expression_evaluation_scope_params },
  { "clearCpuSamples", ClearCpuSamples,
    clear_cpu_samples_params },
  { "clearVMTimeline", ClearVMTimeline,
    clear_vm_timeline_params, },
  { "_compileExpression", CompileExpression, compile_expression_params },
  { "createIdZone", CreateIdZone,
    create_id_zone_params },
  { "deleteIdZone", DeleteIdZone,
    delete_id_zone_params },
  { "_enableProfiler", EnableProfiler,
    enable_profiler_params, },
  { "evaluate", Evaluate,
    evaluate_params },
  { "evaluateInFrame", EvaluateInFrame,
    evaluate_in_frame_params },
  { "_getAllocationProfile", GetAllocationProfile,
    get_allocation_profile_params },
  { "getAllocationProfile", GetAllocationProfilePublic,
    get_allocation_profile_params },
  { "getAllocationTraces", GetAllocationTraces,
      get_allocation_traces_params },
  { "getClassList", GetClassList,
    get_class_list_params },
  { "getCpuSamples", GetCpuSamples,
    get_cpu_samples_params },
  { "getFlagList", GetFlagList,
    get_flag_list_params },
  { "_getHeapMap", GetHeapMap,
    get_heap_map_params },
  { "_getImplementationFields", GetImplementationFields,
    get_implementation_fields_params },
  { "getInboundReferences", GetInboundReferences,
    get_inbound_references_params },
  { "getInstances", GetInstances,
    get_instances_params },
  { "getInstancesAsList", GetInstancesAsList,
    get_instances_as_list_params },
#if defined(SUPPORT_PERFETTO)
  { "getPerfettoCpuSamples", GetPerfettoCpuSamples,
    get_cpu_samples_params },
  { "getPerfettoVMTimeline", GetPerfettoVMTimeline,
    get_vm_timeline_params },
#endif  // defined(SUPPORT_PERFETTO)
  { "getPorts", GetPorts,
    get_ports_params },
  { "getIsolate", GetIsolate,
    get_isolate_params },
  { "_getIsolateObjectStore", GetIsolateObjectStore,
    get_isolate_object_store_params },
  { "getIsolateGroup", GetIsolateGroup,
    get_isolate_group_params },
  { "getMemoryUsage", GetMemoryUsage,
    get_memory_usage_params },
  { "getIsolateGroupMemoryUsage", GetIsolateGroupMemoryUsage,
    get_isolate_group_memory_usage_params },
  { "_getIsolateMetric", GetIsolateMetric,
    get_isolate_metric_params },
  { "_getIsolateMetricList", GetIsolateMetricList,
    get_isolate_metric_list_params },
  { "getIsolatePauseEvent", GetIsolatePauseEvent,
    get_isolate_pause_event_params },
  { "getObject", GetObject,
    get_object_params },
  { "_getObjectStore", GetObjectStore,
    get_object_store_params },
  { "_getPersistentHandles", GetPersistentHandles,
      get_persistent_handles_params, },
  { "_getPorts", GetPortsPrivate,
    get_ports_private_params },
  { "getProcessMemoryUsage", GetProcessMemoryUsage,
    get_process_memory_usage_params },
  { "_getReachableSize", GetReachableSize,
    get_reachable_size_params },
  { "_getRetainedSize", GetRetainedSize,
    get_retained_size_params },
  { "lookupResolvedPackageUris", LookupResolvedPackageUris,
    lookup_resolved_package_uris_params },
  { "lookupPackageUris", LookupPackageUris,
    lookup_package_uris_params },
  { "getQueuedMicrotasks", GetQueuedMicrotasks,
    get_queued_microtasks_params },
  { "getRetainingPath", GetRetainingPath,
    get_retaining_path_params },
  { "getScripts", GetScripts,
    get_scripts_params },
  { "getSourceReport", GetSourceReport,
    get_source_report_params },
  { "getStack", GetStack,
    get_stack_params },
  { "_getTagProfile", GetTagProfile,
    get_tag_profile_params },
  { "_getTypeArgumentsList", GetTypeArgumentsList,
    get_type_arguments_list_params },
  { "getVersion", GetVersion,
    get_version_params },
  { "getVM", GetVM,
    get_vm_params },
  { "getVMTimeline", GetVMTimeline,
    get_vm_timeline_params },
  { "getVMTimelineFlags", GetVMTimelineFlags,
    get_vm_timeline_flags_params },
  { "getVMTimelineMicros", GetVMTimelineMicros,
    get_vm_timeline_micros_params },
  { "invalidateIdZone", InvalidateIdZone,
    invalidate_id_zone_params },
  { "invoke", Invoke, invoke_params },
  { "kill", Kill, kill_params },
  { "pause", Pause,
    pause_params },
  { "removeBreakpoint", RemoveBreakpoint,
    remove_breakpoint_params },
  { "_reloadKernel", ReloadKernel,
    reload_kernel_params },
  { "reloadSources", ReloadSources,
    reload_sources_params },
  { "_reloadSources", ReloadSources,
    reload_sources_params },
  { "resume", Resume,
    resume_params },
  { "requestHeapSnapshot", RequestHeapSnapshot,
    request_heap_snapshot_params },
  { "_evaluateCompiledExpression", EvaluateCompiledExpression,
    evaluate_compiled_expression_params },
  { "setBreakpointState", SetBreakpointState,
    set_breakpoint_state_params },
  { "setExceptionPauseMode", SetExceptionPauseMode,
    set_exception_pause_mode_params },
  { "setIsolatePauseMode", SetIsolatePauseMode,
    set_isolate_pause_mode_params },
  { "setFlag", SetFlag,
    set_flags_params },
  { "setLibraryDebuggable", SetLibraryDebuggable,
    set_library_debuggable_params },
  { "setName", SetName,
    set_name_params },
  { "_setStreamIncludePrivateMembers", SetStreamIncludePrivateMembers,
    set_stream_include_private_members_params },
  { "setTraceClassAllocation", SetTraceClassAllocation,
    set_trace_class_allocation_params },
  { "setVMName", SetVMName,
    set_vm_name_params },
  { "setVMTimelineFlags", SetVMTimelineFlags,
    set_vm_timeline_flags_params },
  { "_collectAllGarbage", CollectAllGarbage,
    collect_all_garbage_params },
  { "_getDefaultClassesAliases", GetDefaultClassesAliases,
    get_default_classes_aliases_params },
  { "_readNativeMemory", ReadNativeMemory, read_native_memory_params },
};
// clang-format on

const ServiceMethodDescriptor* FindMethod(const char* method_name) {
  intptr_t num_methods = sizeof(service_methods_) / sizeof(service_methods_[0]);
  for (intptr_t i = 0; i < num_methods; i++) {
    const ServiceMethodDescriptor& method = service_methods_[i];
    if (strcmp(method_name, method.name) == 0) {
      return &method;
    }
  }
  return nullptr;
}

#endif  // !PRODUCT

}  // namespace dart
