// 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/overview_tracing_handler.h"

#include <map>
#include <optional>
#include <string_view>
#include <vector>

#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/json/json_writer.h"
#include "base/linux_util.h"
#include "base/process/process_iterator.h"
#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/strings/string_tokenizer.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/task/thread_pool.h"
#include "base/time/time_override.h"
#include "base/timer/timer.h"
#include "base/trace_event/common/trace_event_common.h"
#include "base/trace_event/trace_config.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/ash/arc/tracing/arc_system_stat_collector.h"
#include "chrome/browser/ash/arc/tracing/arc_tracing_graphics_model.h"
#include "chrome/browser/ash/arc/tracing/arc_tracing_model.h"
#include "chrome/browser/ash/arc/tracing/present_frames_tracer.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chromeos/ash/experiences/arc/arc_features.h"
#include "chromeos/ash/experiences/arc/arc_util.h"
#include "components/exo/shell_surface_util.h"
#include "components/exo/surface.h"
#include "components/exo/wm_helper.h"
#include "content/public/browser/browser_thread.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_rep.h"

namespace arc {

struct OverviewTracingHandler::ActiveTrace {
  arc::ArcTracingGraphicsModel model;

  base::FilePath save_model_path;

  // Time filter for tracing, since ARC++ window was activated last until
  // tracing is stopped.
  base::TimeTicks time_min;
  base::TimeTicks time_max;

  // Collects system stat runtime.
  arc::ArcSystemStatCollector system_stat_collector;

  // Information about active task, title and icon.
  std::string task_title;
  std::vector<unsigned char> task_icon_png;
  base::Time timestamp;

  // This must be destructed on the UI thread, so make it manually-destructable
  // with std::optional.
  std::optional<base::OneShotTimer> stop_timer;

  arc::PresentFramesTracer present_frames;
};

namespace {

class ProcessFilterPassAll : public base::ProcessFilter {
 public:
  ProcessFilterPassAll() = default;

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

  ~ProcessFilterPassAll() override = default;

  // base::ProcessFilter:
  bool Includes(const base::ProcessEntry& process) const override {
    return true;
  }
};

// Reads name of thread from /proc/pid/task/tid/status.
bool ReadNameFromStatus(pid_t pid, pid_t tid, std::string* out_name) {
  std::string status;
  if (!base::ReadFileToString(base::FilePath(base::StringPrintf(
                                  "/proc/%d/task/%d/status", pid, tid)),
                              &status)) {
    return false;
  }
  base::StringTokenizer tokenizer(status, "\n");
  while (tokenizer.GetNext()) {
    std::string_view value_str(tokenizer.token_piece());
    if (!base::StartsWith(value_str, "Name:")) {
      continue;
    }
    std::vector<std::string_view> split_value_str = base::SplitStringPiece(
        value_str, "\t", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
    DCHECK_EQ(2U, split_value_str.size());
    *out_name = std::string(split_value_str[1]);
    return true;
  }

  return false;
}

// Helper that clarifies thread and process names. Tracing events may not have
// enough data for this. Also it determines the process pid the thread belongs
// to.
void UpdateThreads(arc::ArcSystemModel::ThreadMap* threads) {
  ProcessFilterPassAll filter_pass_all;
  base::ProcessIterator process_iterator(&filter_pass_all);

  std::vector<pid_t> tids;
  std::string name;
  for (const auto& process : process_iterator.Snapshot()) {
    tids.clear();
    base::GetThreadsForProcess(process.pid(), &tids);
    bool process_in_use = threads->find(process.pid()) != threads->end();
    for (pid_t tid : tids) {
      if (threads->find(tid) != threads->end()) {
        process_in_use = true;
        (*threads)[tid].pid = process.pid();
        if (!ReadNameFromStatus(process.pid(), tid, &(*threads)[tid].name)) {
          LOG(WARNING) << "Failed to update thread name " << tid;
        }
      }
    }
    if (process_in_use) {
      (*threads)[process.pid()].pid = process.pid();
      if (!ReadNameFromStatus(process.pid(), process.pid(),
                              &(*threads)[process.pid()].name)) {
        LOG(WARNING) << "Failed to update process name " << process.pid();
      }
    }
  }
}

std::unique_ptr<OverviewTracingResult> BuildGraphicsModel(
    const std::string& data,
    std::unique_ptr<OverviewTracingHandler::ActiveTrace> trace,
    const base::FilePath& model_path) {
  DCHECK(trace);

  if (base::FeatureList::IsEnabled(arc::kSaveRawFilesOnTracing)) {
    const base::FilePath raw_path =
        model_path.DirName().Append(model_path.BaseName().value() + "_raw");
    const base::FilePath system_path =
        model_path.DirName().Append(model_path.BaseName().value() + "_system");
    if (!base::WriteFile(base::FilePath(raw_path), data)) {
      LOG(ERROR) << "Failed to save raw trace model to " << raw_path.value();
    }
    const std::string system_raw =
        trace->system_stat_collector.SerializeToJson();
    if (!base::WriteFile(base::FilePath(system_path), system_raw)) {
      LOG(ERROR) << "Failed to save system model to " << system_path.value();
    }
  }

  arc::ArcTracingModel common_model;
  const base::TimeTicks time_min_clamped =
      std::max(trace->time_min,
               trace->time_max - trace->system_stat_collector.max_interval());
  common_model.SetMinMaxTime(
      (time_min_clamped - base::TimeTicks()).InMicroseconds(),
      (trace->time_max - base::TimeTicks()).InMicroseconds());

  if (!common_model.Build(data)) {
    return std::make_unique<OverviewTracingResult>(
        base::Value(), base::FilePath(), "Failed to process tracing data");
  }

  trace->system_stat_collector.Flush(trace->time_min, trace->time_max,
                                     &common_model.system_model());

  trace->model.set_skip_structure_validation();
  if (!trace->model.Build(common_model, trace->present_frames)) {
    return std::make_unique<OverviewTracingResult>(
        base::Value(), base::FilePath(), "Failed to build tracing model");
  }

  UpdateThreads(&trace->model.system_model().thread_map());
  trace->model.set_app_title(trace->task_title);
  trace->model.set_app_icon_png(trace->task_icon_png);
  trace->model.set_platform(base::GetLinuxDistro());
  trace->model.set_timestamp(trace->timestamp);
  base::DictValue model = trace->model.Serialize();

  std::string json_content;
  base::JSONWriter::WriteWithOptions(
      model, base::JSONWriter::OPTIONS_PRETTY_PRINT, &json_content);
  DCHECK(!json_content.empty());

  if (!base::WriteFile(model_path, json_content)) {
    LOG(ERROR) << "Failed serialize model to " << model_path.value() << ".";
  }

  return std::make_unique<OverviewTracingResult>(
      base::Value(std::move(model)), model_path, "Tracing model is ready");
}

base::trace_event::TraceConfig GetTracingConfig() {
  base::trace_event::TraceConfig config(
      "-*," TRACE_DISABLED_BY_DEFAULT("display.framedisplayed"),
      base::trace_event::RECORD_CONTINUOUSLY);
  config.EnableSystrace();
  config.EnableSystraceEvent("i915:intel_gpu_freq_change");
  config.EnableSystraceEvent("drm_msm_gpu:msm_gpu_freq_change");
  return config;
}

}  // namespace

std::string OverviewTracingHandler::GetModelBaseNameFromTitle(
    std::string_view title,
    base::Time timestamp) {
  constexpr size_t kMaxNameSize = 31;
  std::string normalized_name;
  normalized_name.reserve(kMaxNameSize);
  for (char c : title) {
    if (normalized_name.size() >= kMaxNameSize) {
      break;
    }

    c = base::ToLowerASCII(c);
    if (c == ' ') {
      normalized_name.push_back('_');
      continue;
    }
    if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) {
      normalized_name.push_back(c);
    }
  }

  base::Time::Exploded exploded;
  timestamp.LocalExplode(&exploded);

  std::string time_str = base::StringPrintf(
      "%04d-%02d-%02d_%02d-%02d-%02d", exploded.year, exploded.month,
      exploded.day_of_month, exploded.hour, exploded.minute, exploded.second);
  return base::StrCat(
      {"overview_tracing_", normalized_name, "_", time_str, ".json"});
}

OverviewTracingHandler::OverviewTracingHandler(
    ArcWindowFocusChangeCb arc_window_focus_change)
    : arc_window_focus_change_(arc_window_focus_change),
      wm_helper_(exo::WMHelper::HasInstance() ? exo::WMHelper::GetInstance()
                                              : nullptr) {
  DCHECK(wm_helper_);

  aura::Window* const current_active = wm_helper_->GetActiveWindow();
  if (current_active) {
    OnWindowActivated(ActivationReason::ACTIVATION_CLIENT /* not used */,
                      current_active, nullptr);
  }
  wm_helper_->AddActivationObserver(this);
}

OverviewTracingHandler::~OverviewTracingHandler() {
  wm_helper_->RemoveActivationObserver(this);
  DiscardActiveArcWindow();
}

void OverviewTracingHandler::OnWindowActivated(ActivationReason reason,
                                               aura::Window* gained_active,
                                               aura::Window* lost_active) {
  // Handle ARC current active window if any. This stops any ongoing trace.
  DiscardActiveArcWindow();

  if (!gained_active) {
    return;
  }

  if (!arc::GetWindowTaskId(gained_active).has_value()) {
    return;
  }

  arc_active_window_ = gained_active;
  arc_active_window_->AddObserver(this);
  if (arc_window_focus_change_) {
    arc_window_focus_change_.Run(arc_active_window_);
  }

  exo::Surface* const surface = exo::GetShellRootSurface(arc_active_window_);
  CHECK(surface);

  // We observe the _root_ window surface rather than the layer that receives
  // the buffer attachment. This is because it makes locating the surface
  // much easier. We will not have a noticeable delay in monitoring this
  // higher-level window since the wl_surface_commit calls by the client
  // percolate up a single thread's call stack. The root surface's commit is
  // at [0], and the ApplyPending...Changes calls there are committing
  // subsurfaces.
  // [0]
  // https://source.corp.google.com/h/googleplex-android/platform/superproject/base/+/rvc-arc:vendor/google_arc/libs/wayland_service/wayland_layer_container_window.cpp;l=73-79;drc=7bf4887dc1838167c63ae6c0fc514aaab9551e2a
  //
  // We do not need to observe the Attach event since that immediately precedes
  // the commit, and we don't care about the buffer ID.

  // TODO(matvore): can the root surface change after the window is created?
  surface->AddSurfaceObserver(this);
}

void OverviewTracingHandler::OnWindowPropertyChanged(aura::Window* window,
                                                     const void* key,
                                                     intptr_t old) {
  DCHECK_EQ(arc_active_window_, window);
  if (key != aura::client::kAppIconKey) {
    return;
  }

  if (active_trace_) {
    UpdateActiveArcWindowInfo();
  }
}

void OverviewTracingHandler::OnWindowDestroying(aura::Window* window) {
  DCHECK_EQ(arc_active_window_, window);
  DiscardActiveArcWindow();
}

void OverviewTracingHandler::OnSurfaceDestroying(exo::Surface* surface) {
  DiscardActiveArcWindow();
}

void OverviewTracingHandler::OnCommit(exo::Surface* surface) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (!active_trace_) {
    return;
  }

  active_trace_->present_frames.AddCommit(SystemTicksNow());
  active_trace_->present_frames.ListenForPresent(surface);
}

void OverviewTracingHandler::UpdateActiveArcWindowInfo() {
  DCHECK(arc_active_window_);
  DCHECK(active_trace_);

  active_trace_->task_title =
      base::UTF16ToASCII(arc_active_window_->GetTitle());
  active_trace_->task_icon_png.clear();

  const gfx::ImageSkia* app_icon =
      arc_active_window_->GetProperty(aura::client::kAppIconKey);
  if (app_icon) {
    std::optional<std::vector<uint8_t>> data =
        gfx::PNGCodec::EncodeBGRASkBitmap(
            app_icon->GetRepresentation(1.0f).GetBitmap(),
            false /* discard_transparency */);
    if (data) {
      active_trace_->task_icon_png = std::move(data).value();
    }
  }
}

void OverviewTracingHandler::DiscardActiveArcWindow() {
  if (active_trace_) {
    StopTracing();
  }

  if (!arc_active_window_) {
    return;
  }

  exo::Surface* const surface = exo::GetShellRootSurface(arc_active_window_);
  if (surface) {
    surface->RemoveSurfaceObserver(this);
  }

  if (arc_window_focus_change_) {
    arc_window_focus_change_.Run(nullptr);
  }
  arc_active_window_->RemoveObserver(this);
  arc_active_window_ = nullptr;
}

base::Time OverviewTracingHandler::Now() {
  return base::Time::Now();
}

void OverviewTracingHandler::StartTracingOnController(
    const base::trace_event::TraceConfig& trace_config,
    content::TracingController::StartTracingDoneCallback after_start) {
  content::TracingController::GetInstance()->StartTracing(
      trace_config, std::move(after_start));
}

void OverviewTracingHandler::StopTracingOnController(
    content::TracingController::CompletionCallback after_stop) {
  auto* const controller = content::TracingController::GetInstance();

  if (!controller->IsTracing()) {
    LOG(WARNING) << "TracingController has already stopped tracing";
    return;
  }

  controller->StopTracing(
      content::TracingController::CreateStringEndpoint(std::move(after_stop)));
}

OverviewTracingHandler::AppWindowList OverviewTracingHandler::AllAppWindows()
    const {
  auto* app_service = apps::AppServiceProxyFactory::GetForProfile(
      ProfileManager::GetPrimaryUserProfile());
  AppWindowList windows;
  if (app_service) {
    app_service->InstanceRegistry().ForEachInstance(
        [&windows](const auto& update) {
          windows.emplace_back(update.Window());
        });
  }
  return windows;
}

void OverviewTracingHandler::StartTracing(const base::FilePath& save_path,
                                          base::TimeDelta max_time) {
  active_trace_ = std::make_unique<ActiveTrace>();

  active_trace_->save_model_path = save_path;
  active_trace_->system_stat_collector.Start(max_time);
  active_trace_->timestamp = Now();
  UpdateActiveArcWindowInfo();
  active_trace_->time_min = SystemTicksNow();

  StartTracingOnController(
      GetTracingConfig(),
      base::BindOnce(&OverviewTracingHandler::OnTracingStarted,
                     weak_ptr_factory_.GetWeakPtr()));
}

void OverviewTracingHandler::StopTracing() {
  if (start_build_model_) {
    start_build_model_.Run();
  }

  // |stop_timer| will already be nullopt in the case that the window was or
  // deactivated before OnTracingStarted was invoked.
  if (!active_trace_->stop_timer) {
    active_trace_.reset();
    return;
  }
  active_trace_->stop_timer.reset();

  active_trace_->time_max = SystemTicksNow();
  active_trace_->system_stat_collector.Stop();

  StopTracingOnController(
      base::BindOnce(&OverviewTracingHandler::OnTracingStopped,
                     weak_ptr_factory_.GetWeakPtr(), std::move(active_trace_)));
}

OverviewTracingHandler::AppWindowList
OverviewTracingHandler::NonTraceTargetWindows() const {
  auto windows = AllAppWindows();
  auto arc_window_pos =
      std::find(windows.begin(), windows.end(), arc_active_window_);
  if (arc_window_pos != windows.end()) {
    windows.erase(arc_window_pos);
  }

  return windows;
}

bool OverviewTracingHandler::is_tracing() const {
  return active_trace_ != nullptr;
}

bool OverviewTracingHandler::arc_window_is_active() const {
  return arc_active_window_ != nullptr;
}

base::TimeTicks OverviewTracingHandler::SystemTicksNow() {
  return TRACE_TIME_TICKS_NOW();
}

void OverviewTracingHandler::OnTracingStarted() {
  // This is an asynchronous call and it may arrive after tracing is actually
  // stopped.
  if (!active_trace_) {
    return;
  }

  active_trace_->stop_timer.emplace();
  active_trace_->stop_timer->Start(
      FROM_HERE, active_trace_->system_stat_collector.max_interval(),
      base::BindOnce(&OverviewTracingHandler::StopTracing,
                     base::Unretained(this)));
}

void OverviewTracingHandler::OnTracingStopped(
    std::unique_ptr<ActiveTrace> trace,
    std::unique_ptr<std::string> trace_data) {
  std::string string_data;
  string_data.swap(*trace_data);

  const base::FilePath model_path = trace->save_model_path.AppendASCII(
      GetModelBaseNameFromTitle(trace->task_title, trace->timestamp));

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
      base::BindOnce(&BuildGraphicsModel, std::move(string_data),
                     std::move(trace), model_path),
      base::BindOnce(graphics_model_ready_));
}

}  // namespace arc
