// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/arc/tracing/arc_tracing_graphics_model.h"

#include <inttypes.h>

#include <algorithm>
#include <set>
#include <utility>

#include "base/base64.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/no_destructor.h"
#include "base/strings/string_tokenizer.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "base/trace_event/common/trace_event_common.h"
#include "chrome/browser/ash/arc/tracing/arc_graphics_jank_detector.h"
#include "chrome/browser/ash/arc/tracing/arc_tracing_event.h"
#include "chrome/browser/ash/arc/tracing/arc_tracing_event_matcher.h"
#include "chrome/browser/ash/arc/tracing/arc_tracing_model.h"
#include "chrome/browser/ash/arc/tracing/present_frames_tracer.h"
#include "chromeos/ash/experiences/arc/arc_util.h"

namespace arc {

namespace {

using BufferEvent = ArcTracingGraphicsModel::BufferEvent;
using BufferEvents = ArcTracingGraphicsModel::BufferEvents;
using EventType = ArcTracingGraphicsModel::EventType;

constexpr char kUnknownActivity[] = "unknown";

constexpr char kKeyActivity[] = "activity";
constexpr char kKeyBuffers[] = "buffers";
constexpr char kKeyChrome[] = "chrome";
constexpr char kKeyAppFps[] = "app_fps";
constexpr char kKeyGlobalEvents[] = "global_events";
constexpr char kKeyIcon[] = "icon";
constexpr char kKeyViews[] = "views";
constexpr char kKeyPlatform[] = "platform";
constexpr char kKeySystem[] = "system";
constexpr char kKeyTaskId[] = "task_id";
constexpr char kKeyTimestamp[] = "timestamp";
constexpr char kKeyTitle[] = "title";

constexpr char kDequeueBufferQuery[] = "android:dequeueBuffer";
constexpr char kQueueBufferQuery[] = "android:queueBuffer";

constexpr char kFrameDisplayedQuery[] =
    "benchmark,viz," TRACE_DISABLED_BY_DEFAULT(
        "display.framedisplayed") ":Display::FrameDisplayed";

// Helper factory class that produces graphic buffer events from the giving
// |ArcTracingEvent| generic events. Each |ArcTracingEvent| may produce graphics
// event |ArcTracingGraphicsModel::BufferEvent| on start or/and on finish of the
// event |ArcTracingEvent|. This is organized in form of map
// |ArcTracingEventMatcher| to the pair of |EventType| which indicates
// what to generate on start and on finish of the event.
class BufferGraphicsEventMapper {
 public:
  struct MappingRule {
    using EventTimeCallback =
        base::RepeatingCallback<uint64_t(const ArcTracingEventMatcher&,
                                         const ArcTracingEvent& event)>;

    MappingRule(std::unique_ptr<ArcTracingEventMatcher> matcher,
                EventType map_start,
                EventType map_finish,
                EventTimeCallback start_time_callback = EventTimeCallback())
        : matcher(std::move(matcher)),
          map_start(map_start),
          map_finish(map_finish),
          event_start_time_callback(start_time_callback) {}

    bool Produce(const ArcTracingEvent& event,
                 ArcTracingGraphicsModel::BufferEvents* collector) const {
      if (!matcher->Match(event)) {
        return false;
      }

      if (map_start != EventType::kNone) {
        uint64_t start_timestamp = event.GetTimestamp();
        if (event_start_time_callback) {
          start_timestamp = event_start_time_callback.Run(*matcher, event);
        }

        collector->emplace_back(map_start, start_timestamp);
      }
      if (map_finish != EventType::kNone) {
        collector->emplace_back(map_finish, event.GetEndTimestamp());
      }

      return true;
    }

    std::unique_ptr<ArcTracingEventMatcher> matcher;
    EventType map_start;
    EventType map_finish;
    EventTimeCallback event_start_time_callback;
  };
  using MappingRules = std::vector<MappingRule>;

  BufferGraphicsEventMapper() {
    // android rules
    rules_.emplace_back(
        std::make_unique<ArcTracingEventMatcher>(kDequeueBufferQuery),
        EventType::kBufferQueueDequeueStart,
        EventType::kBufferQueueDequeueDone);
    rules_.emplace_back(
        std::make_unique<ArcTracingEventMatcher>(kQueueBufferQuery),
        EventType::kBufferQueueQueueStart, EventType::kBufferQueueQueueDone);
    rules_.emplace_back(
        std::make_unique<ArcTracingEventMatcher>("android:acquireBuffer"),
        EventType::kBufferQueueAcquire, EventType::kNone);
    rules_.emplace_back(
        std::make_unique<ArcTracingEventMatcher>("android:releaseBuffer"),
        EventType::kNone, EventType::kBufferQueueReleased);
    rules_.emplace_back(std::make_unique<ArcTracingEventMatcher>(
                            "android:handleMessageInvalidate"),
                        EventType::kSurfaceFlingerInvalidationStart,
                        EventType::kSurfaceFlingerInvalidationDone);
    rules_.emplace_back(std::make_unique<ArcTracingEventMatcher>(
                            "android:handleMessageRefresh"),
                        EventType::kSurfaceFlingerCompositionStart,
                        EventType::kSurfaceFlingerCompositionDone);
  }

  BufferGraphicsEventMapper(const BufferGraphicsEventMapper&) = delete;
  BufferGraphicsEventMapper& operator=(const BufferGraphicsEventMapper&) =
      delete;

  ~BufferGraphicsEventMapper() = default;

  void Produce(const ArcTracingEvent& event,
               ArcTracingGraphicsModel::BufferEvents* collector) const {
    for (const auto& rule : rules_) {
      if (rule.Produce(event, collector)) {
        return;
      }
    }
    LOG(ERROR) << "Unsupported event: " << event.ToString();
  }

 private:
  MappingRules rules_;
};

// Maps particular buffer to its events.
using BufferToEvents =
    std::map<std::string, ArcTracingGraphicsModel::BufferEvents>;

bool SortByTimestampPred(const ArcTracingGraphicsModel::BufferEvent& a,
                         const ArcTracingGraphicsModel::BufferEvent& b) {
  if (a.timestamp != b.timestamp) {
    return a.timestamp < b.timestamp;
  }
  return static_cast<int>(a.type) < static_cast<int>(b.type);
}

void SortBufferEventsByTimestamp(BufferEvents* events) {
  std::sort(events->begin(), events->end(), SortByTimestampPred);
}

std::string RouteToSelector(const std::vector<const ArcTracingEvent*>& route) {
  std::string result;
  for (const ArcTracingEvent* segment : route) {
    result = result + "/" + segment->GetCategory() + ":" + segment->GetName();
  }
  return result;
}

void DetermineHierarchy(std::vector<const ArcTracingEvent*>* route,
                        const ArcTracingEvent* event,
                        const ArcTracingEventMatcher& matcher,
                        std::string* out_query) {
  if (!out_query->empty()) {
    return;
  }

  route->emplace_back(event);

  if (matcher.Match(*event)) {
    *out_query = RouteToSelector(*route);
  } else {
    for (const auto& child : event->children()) {
      DetermineHierarchy(route, child.get(), matcher, out_query);
    }
  }

  route->pop_back();
}

// Adds jank events into |ArcTracingGraphicsModel::EventsContainer|.
// |pulse_event_type| defines the type of the event that should appear
// periodically. Once it is missed in analyzed buffer events, new jank event is
// added. |jank_event_type| defines the type of jank.
void AddJanks(std::vector<BufferEvent>* events,
              EventType pulse_event_type,
              EventType jank_event_type) {
  // Detect rate first.
  std::vector<base::Time> pulse_events;

  for (const auto& ev : *events) {
    if (ev.type == pulse_event_type) {
      pulse_events.emplace_back(base::Time::FromDeltaSinceWindowsEpoch(
          base::Microseconds(ev.timestamp)));
    }
  }
  if (!ArcGraphicsJankDetector::IsEnoughSamplesToDetect(pulse_events.size())) {
    LOG(WARNING) << "Not enough samples to detect jank: "
                 << pulse_events.size();
    return;
  }
  std::sort(pulse_events.begin(), pulse_events.end());

  ArcGraphicsJankDetector jank_detector(base::BindRepeating(
      [](EventType jank_event_type, BufferEvents* out_janks,
         base::Time timestamp) {
        out_janks->emplace_back(
            jank_event_type,
            timestamp.ToDeltaSinceWindowsEpoch().InMicroseconds());
      },
      jank_event_type, events));

  for (const auto& it : pulse_events) {
    jank_detector.OnSample(it);
    if (jank_detector.stage() == ArcGraphicsJankDetector::Stage::kActive) {
      break;
    }
  }
  // At this point, no janks should be reported. We are detecting the rate.
  if (jank_detector.stage() != ArcGraphicsJankDetector::Stage::kActive) {
    LOG(ERROR) << "Jank detector was not able to determine rate";
    return;
  }

  // Period is defined. Pass all samples to detect janks.
  jank_detector.SetPeriodFixed(jank_detector.period());
  for (const auto& it : pulse_events) {
    jank_detector.OnSample(it);
  }

  SortBufferEventsByTimestamp(events);
}

// Helper that performs query in |common_model| for top level Chrome GPU events
// and returns bands of sorted list of built events.
void GetChromeTopLevelEvents(const ArcTracingModel& common_model,
                             ArcTracingGraphicsModel::EventsContainer* result) {
  result->buffer_events().resize(1);
  for (const ArcTracingEvent* event :
       common_model.Select(kFrameDisplayedQuery)) {
    result->buffer_events()[0].emplace_back(EventType::kChromeOSSwapDone,
                                            event->GetTimestamp());
  }

  SortBufferEventsByTimestamp(&result->buffer_events()[0]);
}

// Helper that serializes events |events| to the |base::ListValue|.
base::ListValue SerializeEvents(
    const ArcTracingGraphicsModel::BufferEvents& events) {
  base::ListValue list;
  for (const auto& event : events) {
    base::ListValue event_value;
    event_value.Append(static_cast<int>(event.type));
    event_value.Append(static_cast<double>(event.timestamp));
    if (!event.content.empty()) {
      event_value.Append(base::Value(event.content));
    }
    list.Append(std::move(event_value));
  }
  return list;
}

// Helper that serializes |events| to the |base::DictValue|.
base::DictValue SerializeEventsContainer(
    const ArcTracingGraphicsModel::EventsContainer& events) {
  base::DictValue dictionary;

  base::ListValue buffer_list;
  for (auto& buffer : events.buffer_events()) {
    buffer_list.Append(SerializeEvents(buffer));
  }

  dictionary.Set(kKeyBuffers, std::move(buffer_list));
  dictionary.Set(kKeyGlobalEvents, SerializeEvents(events.global_events()));

  return dictionary;
}

bool IsInRange(EventType type,
               EventType type_from_inclusive,
               EventType type_to_inclusive) {
  return type >= type_from_inclusive && type <= type_to_inclusive;
}

// Helper that loads events from |base::Value|. Returns true in case events were
// read successfully. Events must be sorted and be known.
bool LoadEvents(const base::ListValue* value,
                ArcTracingGraphicsModel::BufferEvents* out_events) {
  DCHECK(out_events);
  if (!value) {
    return false;
  }
  int64_t previous_timestamp = 0;
  for (const auto& item : *value) {
    if (!item.is_list()) {
      return false;
    }
    const base::ListValue& entry = item.GetList();
    if (entry.size() < 2) {
      return false;
    }
    if (!entry[0].is_int()) {
      return false;
    }
    const EventType type = static_cast<EventType>(entry[0].GetInt());

    if (!IsInRange(type, EventType::kBufferQueueDequeueStart,
                   EventType::kBufferFillJank) &&
        !IsInRange(type, EventType::kExoSurfaceAttach,
                   EventType::kExoLastEvent) &&
        !IsInRange(type, EventType::kChromeBarrierOrder,
                   EventType::kChromeBarrierFlush) &&
        !IsInRange(type, EventType::kSurfaceFlingerVsyncHandler,
                   EventType::kVsyncTimestamp) &&
        !IsInRange(type, EventType::kChromeOSDraw,
                   EventType::kChromeOSLastEvent) &&
        !IsInRange(type, EventType::kCustomEvent, EventType::kCustomEvent) &&
        !IsInRange(type, EventType::kInputEventCreated,
                   EventType::kInputEventDeliverEnd)) {
      return false;
    }

    if (!entry[1].is_double() && !entry[1].is_int()) {
      return false;
    }
    const int64_t timestamp = entry[1].GetDouble();
    if (timestamp < previous_timestamp) {
      return false;
    }
    if (entry.size() == 3) {
      if (!entry[2].is_string()) {
        return false;
      }
      out_events->emplace_back(type, timestamp, entry[2].GetString());
    } else {
      out_events->emplace_back(type, timestamp);
    }
    previous_timestamp = timestamp;
  }
  return true;
}

bool LoadEventsContainer(const base::DictValue* dict,
                         ArcTracingGraphicsModel::EventsContainer* out_events) {
  DCHECK(out_events->buffer_events().empty());
  DCHECK(out_events->global_events().empty());

  if (!dict) {
    return false;
  }

  const base::ListValue* buffer_entries = dict->FindList(kKeyBuffers);
  if (!buffer_entries) {
    return false;
  }

  for (const auto& buffer_entry : *buffer_entries) {
    BufferEvents events;
    if (!LoadEvents(buffer_entry.GetIfList(), &events)) {
      return false;
    }
    out_events->buffer_events().emplace_back(std::move(events));
  }

  const base::ListValue* const global_events = dict->FindList(kKeyGlobalEvents);
  if (!LoadEvents(global_events, &out_events->global_events())) {
    return false;
  }

  return true;
}

bool ReadDuration(const base::DictValue* root, uint32_t* duration) {
  const base::Value* duration_value = root->Find(kKeyDuration);
  if (!duration_value ||
      (!duration_value->is_double() && !duration_value->is_int())) {
    return false;
  }

  *duration = duration_value->GetDouble();
  if (*duration < 0) {
    return false;
  }

  return true;
}

}  // namespace

ArcTracingGraphicsModel::BufferEvent::BufferEvent(EventType type,
                                                  int64_t timestamp)
    : type(type), timestamp(timestamp) {}

ArcTracingGraphicsModel::BufferEvent::BufferEvent(EventType type,
                                                  int64_t timestamp,
                                                  const std::string& content)
    : type(type), timestamp(timestamp), content(content) {}

bool ArcTracingGraphicsModel::BufferEvent::operator==(
    const BufferEvent& other) const {
  return type == other.type && timestamp == other.timestamp &&
         content == other.content;
}

ArcTracingGraphicsModel::ViewId::ViewId(int task_id,
                                        const std::string& activity)
    : task_id(task_id), activity(activity) {}

bool ArcTracingGraphicsModel::ViewId::operator<(const ViewId& other) const {
  if (task_id != other.task_id) {
    return task_id < other.task_id;
  }
  return activity.compare(other.activity) < 0;
}

bool ArcTracingGraphicsModel::ViewId::operator==(const ViewId& other) const {
  return task_id == other.task_id && activity == other.activity;
}

ArcTracingGraphicsModel::ArcTracingGraphicsModel() = default;

ArcTracingGraphicsModel::~ArcTracingGraphicsModel() = default;

bool ArcTracingGraphicsModel::Build(const ArcTracingModel& common_model,
                                    const PresentFramesTracer& present_frames) {
  Reset();

  // TODO(b/296595454): Remove the mapping mechanism as it was only needed
  // for arc-graphics-tracing, and use callbacks to only get buffer updates
  // for a single task.
  // Note that JS code for arc-overview-tracing conflates the buffers for all
  // view IDs when calculating app commit time and FPS (see
  // getAppCommitEvents), so we don't gain anything by generating unique view
  // IDs here.
  auto& buffer_events =
      view_buffers_[ViewId(1 /* task_id */, kUnknownActivity)].buffer_events();
  buffer_events.emplace_back();
  for (int64_t ticks : present_frames.commits()) {
    buffer_events[0].emplace_back(EventType::kExoSurfaceCommit, ticks);
  }
  AddJanks(&buffer_events[0], EventType::kExoSurfaceCommit,
           EventType::kExoSurfaceCommitJank);

  for (int64_t ticks : present_frames.presents()) {
    chrome_top_level_.global_events().emplace_back(
        EventType::kChromeOSPresentationDone, ticks);
  }
  AddJanks(&chrome_top_level_.global_events(),
           EventType::kChromeOSPresentationDone,
           EventType::kChromeOSPerceivedJank);

  GetChromeTopLevelEvents(common_model, &chrome_top_level_);
  if (chrome_top_level_.buffer_events().empty()) {
    LOG(ERROR) << "No Chrome top events";
    if (!skip_structure_validation_) {
      return false;
    }
  }
  AddJanks(&chrome_top_level_.buffer_events()[0], EventType::kChromeOSSwapDone,
           EventType::kChromeOSSwapJank);

  system_model_.CopyFrom(common_model.system_model());

  NormalizeTimestamps();

  perceived_fps_ = present_frames.presents().size() * 1'000'000.0 / duration_;
  app_fps_ = present_frames.commits().size() * 1'000'000.0 / duration_;

  system_model_.CloseRangeForValueEvents(duration_ - 1 /* max_timestamp */);

  return true;
}

void ArcTracingGraphicsModel::NormalizeTimestamps() {
  std::vector<BufferEvents*> all_buffers;
  for (auto& view : view_buffers_) {
    for (auto& buffer : view.second.buffer_events()) {
      all_buffers.emplace_back(&buffer);
    }
    all_buffers.emplace_back(&view.second.global_events());
  }

  for (auto& buffer : chrome_top_level_.buffer_events()) {
    all_buffers.emplace_back(&buffer);
  }
  all_buffers.emplace_back(&chrome_top_level_.global_events());

  uint64_t min = std::numeric_limits<uint64_t>::max();
  uint64_t max = std::numeric_limits<uint64_t>::min();
  for (const BufferEvents* buffer : all_buffers) {
    if (!buffer->empty()) {
      min = std::min(min, buffer->front().timestamp);
      max = std::max(max, buffer->back().timestamp);
    }
  }

  for (const auto& cpu_events : system_model_.all_cpu_events()) {
    if (!cpu_events.empty()) {
      min = std::min(min, cpu_events.front().timestamp);
      max = std::max(max, cpu_events.back().timestamp);
    }
  }

  if (!system_model_.memory_events().empty()) {
    min = std::min(min, system_model_.memory_events().front().timestamp);
    max = std::max(max, system_model_.memory_events().back().timestamp);
  }

  duration_ = max - min + 1;

  for (BufferEvents* buffer : all_buffers) {
    for (auto& event : *buffer) {
      event.timestamp -= min;
    }
  }

  for (auto& cpu_events : system_model_.all_cpu_events()) {
    for (auto& cpu_event : cpu_events) {
      cpu_event.timestamp -= min;
    }
  }

  for (auto& memory_event : system_model_.memory_events()) {
    memory_event.timestamp -= min;
  }
}

void ArcTracingGraphicsModel::Reset() {
  chrome_top_level_.Reset();
  view_buffers_.clear();
  system_model_.Reset();
  duration_ = 0;
  app_fps_ = 0;
  perceived_fps_ = 0;
  app_title_ = std::string();
  app_icon_png_.clear();
  platform_ = std::string();
  timestamp_ = base::Time();
}

base::DictValue ArcTracingGraphicsModel::Serialize() const {
  base::DictValue root;

  // Views
  base::ListValue view_list;
  for (auto& view : view_buffers_) {
    base::DictValue view_value = SerializeEventsContainer(view.second);
    view_value.Set(kKeyActivity, view.first.activity);
    view_value.Set(kKeyTaskId, view.first.task_id);
    view_list.Append(std::move(view_value));
  }
  root.Set(kKeyViews, std::move(view_list));

  // Chrome top events
  root.Set(kKeyChrome, SerializeEventsContainer(chrome_top_level_));

  // System.
  root.Set(kKeySystem, system_model_.Serialize());

  // Information
  base::DictValue information;
  information.Set(kKeyDuration, static_cast<double>(duration_));
  information.Set(kKeyPerceivedFps, perceived_fps_);
  information.Set(kKeyAppFps, app_fps_);
  if (!platform_.empty()) {
    information.Set(kKeyPlatform, platform_);
  }
  if (!timestamp_.is_null()) {
    information.Set(kKeyTimestamp, timestamp_.InMillisecondsFSinceUnixEpoch());
  }
  if (!app_title_.empty()) {
    information.Set(kKeyTitle, app_title_);
  }
  if (!app_icon_png_.empty()) {
    const std::string png_data_as_string(
        reinterpret_cast<const char*>(&app_icon_png_[0]), app_icon_png_.size());
    information.Set(kKeyIcon, base::Base64Encode(png_data_as_string));
  }
  root.Set(kKeyInformation, std::move(information));

  return root;
}

std::string ArcTracingGraphicsModel::SerializeToJson() const {
  base::DictValue root = Serialize();
  std::string output;
  if (!base::JSONWriter::WriteWithOptions(
          root, base::JSONWriter::OPTIONS_PRETTY_PRINT, &output)) {
    LOG(ERROR) << "Failed to serialize model";
  }
  return output;
}

bool ArcTracingGraphicsModel::LoadFromJson(const std::string& json_data) {
  Reset();
  std::optional<base::DictValue> root = base::JSONReader::ReadDict(
      json_data, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
  if (!root) {
    return false;
  }
  return LoadFromValue(*root);
}

bool ArcTracingGraphicsModel::LoadFromValue(const base::DictValue& root) {
  Reset();

  const base::ListValue* view_list = root.FindList(kKeyViews);
  if (!view_list || view_list->empty()) {
    // Views are optional for overview tracing.
    if (!skip_structure_validation_) {
      return false;
    }
  } else {
    for (const auto& item : *view_list) {
      const base::DictValue* view_entry = item.GetIfDict();
      if (!view_entry) {
        return false;
      }
      const std::string* activity = view_entry->FindString(kKeyActivity);
      std::optional<int> task_id = view_entry->FindInt(kKeyTaskId);
      if (!activity || !task_id) {
        return false;
      }
      const ViewId view_id(*task_id, *activity);
      if (view_buffers_.find(view_id) != view_buffers_.end()) {
        return false;
      }

      if (!LoadEventsContainer(view_entry, &view_buffers_[view_id])) {
        return false;
      }
    }
  }

  if (!LoadEventsContainer(root.FindDict(kKeyChrome), &chrome_top_level_)) {
    return false;
  }

  if (!system_model_.Load(root.Find(kKeySystem))) {
    return false;
  }

  const base::DictValue* informaton = root.FindDict(kKeyInformation);
  if (informaton) {
    if (!ReadDuration(informaton, &duration_)) {
      return false;
    }

    const auto perceived_fps_value = informaton->FindDouble(kKeyPerceivedFps);
    if (perceived_fps_value.has_value()) {
      perceived_fps_ = *perceived_fps_value;
    }
    const auto app_fps_value = informaton->FindDouble(kKeyAppFps);
    if (app_fps_value.has_value()) {
      app_fps_ = *perceived_fps_value;
    }

    const std::string* platform_value = informaton->FindString(kKeyPlatform);
    if (platform_value) {
      platform_ = *platform_value;
    }
    const std::string* title_value = informaton->FindString(kKeyTitle);
    if (title_value) {
      app_title_ = *title_value;
    }
    const std::string* icon_value = informaton->FindString(kKeyIcon);
    if (icon_value) {
      std::string icon_content;
      if (!base::Base64Decode(*icon_value, &icon_content)) {
        return false;
      }
      app_icon_png_ =
          std::vector<unsigned char>(icon_content.begin(), icon_content.end());
    }
    std::optional<double> timestamp_value =
        informaton->FindDouble(kKeyTimestamp);
    if (timestamp_value) {
      timestamp_ = base::Time::FromMillisecondsSinceUnixEpoch(*timestamp_value);
    }
  } else {
    if (!ReadDuration(&root, &duration_)) {
      return false;
    }
  }

  return true;
}

ArcTracingGraphicsModel::EventsContainer::EventsContainer() = default;

ArcTracingGraphicsModel::EventsContainer::~EventsContainer() = default;

void ArcTracingGraphicsModel::EventsContainer::Reset() {
  buffer_events_.clear();
  global_events_.clear();
}

bool ArcTracingGraphicsModel::EventsContainer::operator==(
    const EventsContainer& other) const {
  return buffer_events() == other.buffer_events() &&
         global_events() == other.global_events();
}

std::ostream& operator<<(std::ostream& os,
                         ArcTracingGraphicsModel::EventType event_type) {
  return os << std::to_underlying(event_type);
}

}  // namespace arc
