// 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 "components/ui_devtools/tracing_agent.h"

#include <algorithm>
#include <memory>
#include <unordered_set>
#include <vector>

#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/process/process.h"
#include "base/strings/cstring_view.h"
#include "base/strings/string_view_util.h"
#include "base/timer/timer.h"
#include "base/trace_event/trace_event.h"
#include "base/trace_event/traced_value.h"
#include "components/ui_devtools/connector_delegate.h"
#include "components/ui_devtools/devtools_base_agent.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/system/data_pipe_drainer.h"
#include "services/tracing/public/cpp/perfetto/perfetto_config.h"
#include "services/tracing/public/cpp/tracing_features.h"
#include "services/tracing/public/mojom/constants.mojom.h"
#include "services/tracing/public/mojom/perfetto_service.mojom.h"
#include "third_party/inspector_protocol/crdtp/json.h"

namespace ui_devtools {

using ui_devtools::protocol::Response;

namespace {

// Minimum reporting interval for the buffer usage status.
const double kMinimumReportingInterval = 250.0;

// We currently don't support concurrent tracing sessions, but are planning to.
// For the time being, we're using this flag as a workaround to prevent devtools
// users from accidentally starting two concurrent sessions.
// TODO(eseckler): Remove once we add support for concurrent sessions to the
// perfetto backend.
static bool g_any_agent_tracing = false;

base::trace_event::TraceConfig::ProcessFilterConfig CreateProcessFilterConfig(
    base::ProcessId gpu_pid) {
  base::ProcessId browser_pid = base::Process::Current().Pid();
  std::unordered_set<base::ProcessId> included_process_ids({browser_pid});

  if (gpu_pid != base::kNullProcessId)
    included_process_ids.insert(gpu_pid);

  return base::trace_event::TraceConfig::ProcessFilterConfig(
      included_process_ids);
}

}  // namespace

// This class is passed to StopTracing() and receives the trace data followed by
// a notification that data collection is over.
class TracingAgent::DevToolsTraceEndpointProxy
    : public base::RefCountedThreadSafe<DevToolsTraceEndpointProxy> {
 public:
  explicit DevToolsTraceEndpointProxy(base::WeakPtr<TracingAgent> tracing_agent)
      : tracing_agent_(tracing_agent) {}

  void ReceiveTraceChunk(std::unique_ptr<std::string> chunk) {
    if (TracingAgent* h = tracing_agent_.get())
      h->OnTraceDataCollected(std::move(chunk));
  }
  void ReceiveTraceFinalContents() {
    if (TracingAgent* h = tracing_agent_.get())
      h->OnTraceComplete();
  }

 protected:
  friend class base::RefCountedThreadSafe<DevToolsTraceEndpointProxy>;

 private:
  ~DevToolsTraceEndpointProxy() = default;
  base::WeakPtr<TracingAgent> tracing_agent_;
};

// Class used to communicate with the Perfetto Consumer interface.
class TracingAgent::PerfettoTracingSession
    : public tracing::mojom::TracingSessionClient,
      public mojo::DataPipeDrainer::Client {
 public:
  explicit PerfettoTracingSession(ConnectorDelegate* connector)
      : connector_(connector) {}

  ~PerfettoTracingSession() override { DCHECK(!tracing_active_); }

  // TracingAgent::TracingSession implementation:
  // Make request and set up perfetto for tracing.
  void EnableTracing(const base::trace_event::TraceConfig& chrome_config,
                     base::OnceClosure on_recording_enabled_callback) {
    DCHECK(!tracing_session_host_);
    DCHECK(!tracing_active_);
    tracing_active_ = true;
    connector_->BindTracingConsumerHost(
        consumer_host_.BindNewPipeAndPassReceiver());
    DCHECK(consumer_host_);
    perfetto::TraceConfig perfetto_config =
        CreatePerfettoConfiguration(chrome_config);

    mojo::PendingRemote<tracing::mojom::TracingSessionClient>
        tracing_session_client;
    receiver_.Bind(tracing_session_client.InitWithNewPipeAndPassReceiver());
    receiver_.set_disconnect_handler(
        base::BindOnce(&PerfettoTracingSession::OnTracingSessionFailed,
                       base::Unretained(this)));

    on_recording_enabled_callback_ = std::move(on_recording_enabled_callback);
    consumer_host_->EnableTracing(
        tracing_session_host_.BindNewPipeAndPassReceiver(),
        std::move(tracing_session_client), std::move(perfetto_config),
        base::File());

    tracing_session_host_.set_disconnect_handler(
        base::BindOnce(&PerfettoTracingSession::OnTracingSessionFailed,
                       base::Unretained(this)));
  }

  void OnTracingEnabled() override {
    if (on_recording_enabled_callback_) {
      std::move(on_recording_enabled_callback_).Run();
    }
  }

  void OnTracingDisabled(bool) override {
    // Since we're converting the tracing data to JSON, we will receive the
    // tracing data via ConsumerHost::DisableTracingAndEmitJson().
  }

  void DisableTracing(
      const std::string& agent_label,
      const scoped_refptr<DevToolsTraceEndpointProxy>& endpoint) {
    agent_label_ = agent_label;
    endpoint_ = endpoint;
    tracing_active_ = false;

    if (!tracing_session_host_) {
      if (endpoint_) {
        // Will delete |this|.
        endpoint_->ReceiveTraceFinalContents();
      }
      return;
    }

    mojo::ScopedDataPipeProducerHandle producer_handle;
    mojo::ScopedDataPipeConsumerHandle consumer_handle;

    MojoResult result =
        mojo::CreateDataPipe(nullptr, producer_handle, consumer_handle);
    if (result != MOJO_RESULT_OK) {
      OnTracingSessionFailed();
      return;
    }
    drainer_ = std::make_unique<mojo::DataPipeDrainer>(
        this, std::move(consumer_handle));
    tracing_session_host_->DisableTracingAndEmitJson(
        agent_label_, std::move(producer_handle),
        base::BindOnce(&PerfettoTracingSession::OnReadBuffersComplete,
                       base::Unretained(this)));
  }

  void GetBufferUsage(base::OnceCallback<void(float percent_full,
                                              size_t approximate_event_count)>
                          on_buffer_usage_callback) {
    if (!tracing_session_host_) {
      std::move(on_buffer_usage_callback).Run(0.0f, 0);
      return;
    }
    DCHECK(on_buffer_usage_callback);
    tracing_session_host_->RequestBufferUsage(base::BindOnce(
        &PerfettoTracingSession::OnBufferUsage, base::Unretained(this),
        std::move(on_buffer_usage_callback)));
  }

  bool HasTracingFailed() { return tracing_active_ && !tracing_session_host_; }

  bool HasDataLossOccurred() { return data_loss_; }

 private:
  perfetto::TraceConfig CreatePerfettoConfiguration(
      const base::trace_event::TraceConfig& chrome_config) {
#if DCHECK_IS_ON()
    base::trace_event::TraceConfig processfilter_stripped_config(chrome_config);
    processfilter_stripped_config.SetProcessFilterConfig(
        base::trace_event::TraceConfig::ProcessFilterConfig());

    // Ensure that the process filter is the only thing that gets changed
    // in a configuration during a tracing session.
    DCHECK((last_config_for_perfetto_.ToString() ==
            base::trace_event::TraceConfig().ToString()) ||
           (last_config_for_perfetto_.ToString() ==
            processfilter_stripped_config.ToString()));
    last_config_for_perfetto_ = std::move(processfilter_stripped_config);
#endif

    return tracing::GetDefaultPerfettoConfig(
        chrome_config,
        /*privacy_filtering_enabled=*/false,
        /*convert_to_legacy_json=*/false);
  }

  void OnTracingSessionFailed() {
    tracing_session_host_.reset();
    receiver_.reset();
    drainer_.reset();

    if (on_recording_enabled_callback_)
      std::move(on_recording_enabled_callback_).Run();

    if (pending_disable_tracing_task_)
      std::move(pending_disable_tracing_task_).Run();

    if (endpoint_) {
      endpoint_->ReceiveTraceFinalContents();
    }
  }

  // Notify client about data loss during tracing.
  void OnBufferUsage(base::OnceCallback<void(float percent_full,
                                             size_t approximate_event_count)>
                         on_buffer_usage_callback,
                     bool success,
                     float percent_full,
                     bool data_loss) {
    if (!success) {
      std::move(on_buffer_usage_callback).Run(0.0f, 0);
      return;
    }
    data_loss_ |= data_loss;
    std::move(on_buffer_usage_callback).Run(percent_full, 0);
  }

  // mojo::DataPipeDrainer::Client implementation:
  void OnDataAvailable(base::span<const uint8_t> data) override {
    auto data_string =
        std::make_unique<std::string>(base::as_string_view(data));
    endpoint_->ReceiveTraceChunk(std::move(data_string));
  }

  void OnDataComplete() override {
    data_complete_ = true;
    MaybeTraceComplete();
  }

  void OnReadBuffersComplete() {
    read_buffers_complete_ = true;
    MaybeTraceComplete();
  }

  void MaybeTraceComplete() {
    if (read_buffers_complete_ && data_complete_) {
      // Request stats to check if data loss occurred.
      GetBufferUsage(base::BindOnce(&PerfettoTracingSession::OnFinalBufferUsage,
                                    base::Unretained(this)));
    }
  }

  void OnFinalBufferUsage(float percent_full, size_t approximate_event_count) {
    if (!endpoint_)
      return;
    // Will delete |this|.
    endpoint_->ReceiveTraceFinalContents();
  }

  mojo::Receiver<tracing::mojom::TracingSessionClient> receiver_{this};
  mojo::Remote<tracing::mojom::TracingSessionHost> tracing_session_host_;

  mojo::Remote<tracing::mojom::ConsumerHost> consumer_host_;
  raw_ptr<ConnectorDelegate> connector_;

  std::string agent_label_;
  base::OnceClosure on_recording_enabled_callback_;
  base::OnceClosure pending_disable_tracing_task_;
  scoped_refptr<DevToolsTraceEndpointProxy> endpoint_;
  std::unique_ptr<mojo::DataPipeDrainer> drainer_;
  bool data_complete_ = false;
  bool read_buffers_complete_ = false;
  bool tracing_active_ = false;
  bool data_loss_ = false;

#if DCHECK_IS_ON()
  base::trace_event::TraceConfig last_config_for_perfetto_;
#endif
};

TracingAgent::TracingAgent(std::unique_ptr<ConnectorDelegate> connector)
    : connector_(std::move(connector)) {}

TracingAgent::~TracingAgent() = default;

namespace {
class TracingNotification : public crdtp::Serializable {
 public:
  explicit TracingNotification(std::string json) : json_(std::move(json)) {}

  void AppendSerialized(std::vector<uint8_t>* out) const override {
    crdtp::Status status =
        crdtp::json::ConvertJSONToCBOR(crdtp::SpanFrom(json_), out);
    DCHECK(status.ok()) << status.ToASCIIString();
  }

 private:
  std::string json_;
};
}  // namespace

void TracingAgent::OnTraceDataCollected(
    std::unique_ptr<std::string> trace_fragment) {
  const std::string valid_trace_fragment =
      UpdateTraceDataBuffer(*trace_fragment);
  if (valid_trace_fragment.empty())
    return;

  // Hand-craft protocol notification message so we can substitute JSON
  // that we already got as string as a bare object, not a quoted string.
  std::string message(
      "{ \"method\": \"Tracing.dataCollected\", \"params\": { \"value\": [");
  const size_t messageSuffixSize = 10;
  message.reserve(message.size() + valid_trace_fragment.size() +
                  messageSuffixSize - trace_data_buffer_state_.offset);
  message.append(base::cstring_view(valid_trace_fragment)
                     .substr(trace_data_buffer_state_.offset)
                     .data());
  message += "] } }";
  frontend()->sendRawNotification(
      std::make_unique<TracingNotification>(std::move(message)));
}

void TracingAgent::OnTraceComplete() {
  if (!trace_data_buffer_state_.data.empty())
    OnTraceDataCollected(std::make_unique<std::string>(""));

  DCHECK(trace_data_buffer_state_.data.empty());
  DCHECK_EQ(0u, trace_data_buffer_state_.pos);
  DCHECK_EQ(0, trace_data_buffer_state_.open_braces);
  DCHECK(!trace_data_buffer_state_.in_string);
  DCHECK(!trace_data_buffer_state_.slashed);

  bool data_loss = perfetto_session_->HasDataLossOccurred();
  perfetto_session_.reset();
  frontend()->tracingComplete(data_loss);
}

void TracingAgent::start(std::optional<std::string> categories,
                         std::optional<std::string> options,
                         std::optional<double> buffer_usage_reporting_interval,
                         std::unique_ptr<StartCallback> callback) {
  if (g_any_agent_tracing) {
    callback->sendFailure(Response::ServerError("Tracing is already started"));
    return;
  }

  if (!categories.has_value() && !options.has_value()) {
    callback->sendFailure(
        Response::InvalidParams("categories+options should be specified."));
    return;
  }

  did_initiate_recording_ = true;
  buffer_usage_reporting_interval_ =
      buffer_usage_reporting_interval.value_or(0);

  // Since we want minimum changes to the devtools frontend, enable the
  // tracing categories for ui_devtools here.
  std::string ui_devtools_categories =
      "disabled-by-default-devtools.timeline,disabled-by-default-devtools."
      "timeline.frame,views,latency,toplevel,"
      "benchmark,cc,viz,input,latency,gpu,rail,viz,ui";
  trace_config_ = base::trace_event::TraceConfig(ui_devtools_categories,
                                                 options.value_or(""));
  StartTracing(std::move(callback));
}

Response TracingAgent::end() {
  if (!perfetto_session_)
    return Response::ServerError("Tracing is not started");

  if (perfetto_session_->HasTracingFailed())
    return Response::ServerError("Tracing failed");

  scoped_refptr<DevToolsTraceEndpointProxy> endpoint;
  // Reset the trace data buffer state.
  trace_data_buffer_state_ = TracingAgent::TraceDataBufferState();
  endpoint = new DevToolsTraceEndpointProxy(weak_factory_.GetWeakPtr());
  StopTracing(endpoint, tracing::mojom::kChromeTraceEventLabel);

  return Response::Success();
}

void TracingAgent::StartTracing(std::unique_ptr<StartCallback> callback) {
  trace_config_.SetProcessFilterConfig(CreateProcessFilterConfig(gpu_pid_));

  perfetto_session_ =
      std::make_unique<PerfettoTracingSession>(connector_.get());
  perfetto_session_->EnableTracing(
      trace_config_,
      base::BindOnce(&TracingAgent::OnRecordingEnabled,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
  g_any_agent_tracing = true;
}

void TracingAgent::OnRecordingEnabled(std::unique_ptr<StartCallback> callback) {
  EditTraceDataForFrontend();
  callback->sendSuccess();
  SetupTimer(buffer_usage_reporting_interval_);
}

void TracingAgent::EditTraceDataForFrontend() {
  // TracingStartedInBrowser is used to enter _processInspectorTrace,
  // which is the logic flow that browser devtools uses for the performance
  // panel. Without this trace event, devtools will use the generic trace, which
  // does not handle fps logic or use the color scheme defined by the
  // category-event map. The processes that are of interest in ui_devtools needs
  // to be specified in the data[frame] property of TracingStartedInBrowser
  auto process_data = std::make_unique<base::trace_event::TracedValue>();
  process_data->SetBoolean("persistentIds", true);
  process_data->BeginArray("frames");

  process_data->BeginDictionary();
  process_data->SetString("frame", "ui_devtools_browser_frame");
  process_data->SetString("name", "Browser");
  process_data->SetInteger("processId", base::Process::Current().Pid());
  process_data->EndDictionary();

  process_data->BeginDictionary();
  process_data->SetString("frame", "ui_devtools_gpu_frame");
  process_data->SetString("name", "Gpu");
  process_data->SetInteger("processId", gpu_pid_);
  process_data->EndDictionary();

  process_data->EndArray();
  TRACE_EVENT_INSTANT(TRACE_DISABLED_BY_DEFAULT("devtools.timeline"),
                      "TracingStartedInBrowser", "data",
                      std::move(process_data));

  // Browser devtools make sure the SetLayerTreeId trace event has the same
  // layertreeid as the layertreeid from the frame trace events 'DrawFrame' and
  // 'BeginFrame'. There is only 1 layer tree in ui_devtools, so the layertreeid
  // for all frames in ui_devtools is 1. This is used to get the frames, which
  // is later used for the fps metrics.
  auto layer_tree_data = std::make_unique<base::trace_event::TracedValue>();
  layer_tree_data->SetString("frame", "ui_devtools_browser_frame");
  layer_tree_data->SetInteger("layerTreeId", 1);
  TRACE_EVENT_INSTANT(TRACE_DISABLED_BY_DEFAULT("devtools.timeline"),
                      "SetLayerTreeId", "data", std::move(layer_tree_data));
}

void TracingAgent::SetupTimer(double usage_reporting_interval) {
  if (usage_reporting_interval == 0)
    return;

  if (usage_reporting_interval < kMinimumReportingInterval)
    usage_reporting_interval = kMinimumReportingInterval;

  base::TimeDelta interval =
      base::Milliseconds(std::ceil(usage_reporting_interval));
  buffer_usage_poll_timer_ = std::make_unique<base::RepeatingTimer>();
  buffer_usage_poll_timer_->Start(
      FROM_HERE, interval,
      base::BindRepeating(&TracingAgent::UpdateBufferUsage,
                          weak_factory_.GetWeakPtr()));
}

void TracingAgent::OnBufferUsage(float percent_full,
                                 size_t approximate_event_count) {
  if (!did_initiate_recording_)
    return;
  frontend()->bufferUsage(percent_full, approximate_event_count, percent_full);
}

void TracingAgent::UpdateBufferUsage() {
  perfetto_session_->GetBufferUsage(
      base::BindOnce(&TracingAgent::OnBufferUsage, weak_factory_.GetWeakPtr()));
}

void TracingAgent::StopTracing(
    const scoped_refptr<DevToolsTraceEndpointProxy>& endpoint,
    const std::string& agent_label) {
  DCHECK(perfetto_session_);
  buffer_usage_poll_timer_.reset();
  perfetto_session_->DisableTracing(agent_label, endpoint);
  did_initiate_recording_ = false;
  g_any_agent_tracing = false;
}

std::string TracingAgent::UpdateTraceDataBuffer(
    const std::string& trace_fragment) {
  size_t end = 0;
  size_t last_open = 0;
  TraceDataBufferState& state = trace_data_buffer_state_;
  state.offset = 0;
  bool update_offset = state.open_braces == 0;
  for (; state.pos < trace_fragment.size(); ++state.pos) {
    char c = trace_fragment[state.pos];
    switch (c) {
      case '{':
        if (!state.in_string && !state.slashed) {
          state.open_braces++;
          if (state.open_braces == 1) {
            last_open = state.data.size() + state.pos;
            if (update_offset) {
              state.offset = last_open;
              update_offset = false;
            }
          }
        }
        break;
      case '}':
        if (!state.in_string && !state.slashed) {
          DCHECK_GT(state.open_braces, 0);
          state.open_braces--;
          if (state.open_braces == 0)
            end = state.data.size() + state.pos + 1;
        }
        break;
      case '"':
        if (!state.slashed)
          state.in_string = !state.in_string;
        break;
      case 'u':
        if (state.slashed)
          state.pos += 4;
        break;
    }

    if (state.in_string && c == '\\') {
      state.slashed = !state.slashed;
    } else {
      state.slashed = false;
    }
  }

  // Next starting position is usually 0 except when we are in the middle of
  // processing a unicode character, i.e. \uxxxx.
  state.pos -= trace_fragment.size();

  std::string complete_str = state.data + trace_fragment;
  state.data = complete_str.substr(std::max(end, last_open));

  complete_str.resize(end);
  return complete_str;
}

}  // namespace ui_devtools
