Skip to content

File trace_recorder.cpp

File List > lib > utilities > trace_recorder.cpp

Go to the documentation of this file

#include "utilities/trace_recorder.hpp"

#include <algorithm>
#include <chrono>
#include <cstdio>
#include <fstream>
#include <mutex>
#include <nlohmann/json.hpp>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>
#include <thread>
#include <vector>

#include "utilities/env.hpp"

namespace blaze::trace {

using json = nlohmann::json;

namespace {

constexpr int k_pid = 1;

struct DiagnosticScopeFrame {
  uint64_t scope_id = 0;
  std::string name;
  std::string file;
  int line = 0;
  std::string function;
  std::optional<std::string> call_file;
  std::optional<int> call_line;
  std::optional<std::string> call_function;
};

std::vector<json> g_events;
std::mutex g_events_mutex;
uint64_t g_next_scope_id = 1;
std::chrono::steady_clock::time_point g_origin{std::chrono::steady_clock::now()};
int g_recording_depth = 0;

thread_local std::vector<DiagnosticScopeFrame> g_scope_stack;

const char* trace_output_path() {
  static const char* path = []() -> const char* {
    const char* value = blaze::get_env("BLAZE_TRACE");
    return (value != nullptr && value[0] != '\0') ? value : nullptr;
  }();
  return path;
}

uint64_t timestamp_us() {
  const auto now = std::chrono::steady_clock::now();
  return static_cast<uint64_t>(
      std::chrono::duration_cast<std::chrono::microseconds>(now - g_origin).count());
}

uint64_t thread_id() {
  static thread_local const uint64_t id =
      static_cast<uint64_t>(std::hash<std::thread::id>{}(std::this_thread::get_id()));
  return id;
}

std::string format_scope_id(const uint64_t scope_id) {
  std::ostringstream ss;
  ss << "0x" << std::hex << scope_id;
  return ss.str();
}

std::string basename_path(const char* path) {
  if (path == nullptr || path[0] == '\0') {
    return "unknown";
  }
  std::string_view view(path);
  const size_t pos = view.find_last_of("/\\");
  if (pos != std::string_view::npos) {
    view = view.substr(pos + 1);
  }
  return std::string(view);
}

void append_trace_event(json event) {
  if (!enabled()) {
    return;
  }
  event["ts"] = timestamp_us();
  std::lock_guard lock(g_events_mutex);
  if (g_events.empty()) {
    g_events.push_back(
        json{{"name", "process_name"}, {"ph", "M"}, {"pid", k_pid}, {"args", {{"name", "Blaze"}}}});
  }
  g_events.push_back(std::move(event));
}

void append_scope_event(const char phase, const std::string& name, const uint64_t scope_id,
                        json args) {
  json event = {{"name", name}, {"cat", "progress"},  {"ph", std::string(1, phase)},
                {"pid", k_pid}, {"tid", thread_id()}, {"id", format_scope_id(scope_id)}};
  if (!args.empty()) {
    event["args"] = std::move(args);
  }
  append_trace_event(std::move(event));
}

json scope_begin_args(const double range_start, const double range_end,
                      const std::source_location& location, const std::source_location* call_site) {
  json args{{"depth", static_cast<int>(g_scope_stack.size())},
            {"scope_id", ""},
            {"range_start", range_start},
            {"range_end", range_end},
            {"function", location.function_name()},
            {"file", basename_path(location.file_name())},
            {"line", location.line()}};
  if (call_site != nullptr) {
    args["call_function"] = call_site->function_name();
    args["call_file"] = basename_path(call_site->file_name());
    args["call_line"] = call_site->line();
  }
  return args;
}

void push_scope_frame(const uint64_t scope_id, const std::string& name,
                      const std::source_location& location, const std::source_location* call_site) {
  DiagnosticScopeFrame frame{};
  frame.scope_id = scope_id;
  frame.name = name;
  frame.file = basename_path(location.file_name());
  frame.line = static_cast<int>(location.line());
  frame.function = location.function_name();
  if (call_site != nullptr) {
    frame.call_file = basename_path(call_site->file_name());
    frame.call_line = static_cast<int>(call_site->line());
    frame.call_function = call_site->function_name();
  }
  g_scope_stack.push_back(std::move(frame));
}

DiagnosticScopeFrame* find_scope_frame(const uint64_t scope_id) {
  for (DiagnosticScopeFrame& frame : g_scope_stack) {
    if (frame.scope_id == scope_id) {
      return &frame;
    }
  }
  return nullptr;
}

void pop_scope_frame(const uint64_t scope_id) {
  if (g_scope_stack.empty()) {
    return;
  }
  if (g_scope_stack.back().scope_id == scope_id) {
    g_scope_stack.pop_back();
    return;
  }
  const auto it = std::find_if(
      g_scope_stack.rbegin(), g_scope_stack.rend(),
      [scope_id](const DiagnosticScopeFrame& frame) { return frame.scope_id == scope_id; });
  if (it != g_scope_stack.rend()) {
    g_scope_stack.erase(it.base() - 1, g_scope_stack.end());
  }
}

std::string scope_name_or_location(const std::source_location& location, const std::string& name) {
  if (!name.empty()) {
    return name;
  }
  return basename_path(location.file_name()) + ':' + std::to_string(location.line());
}

void patch_scope_begin(const uint64_t scope_id, const std::string& name,
                       const std::source_location& location, const DiagnosticScopeFrame* frame) {
  const std::string id_str = format_scope_id(scope_id);
  std::lock_guard lock(g_events_mutex);
  for (json& event : g_events) {
    if (event.value("ph", "") != "B" || event.value("id", "") != id_str) {
      continue;
    }
    event["name"] = name;
    json args = scope_begin_args(event["args"].value("range_start", 0.0),
                                 event["args"].value("range_end", 1.0), location, nullptr);
    args["scope_id"] = id_str;
    if (event["args"].contains("depth")) {
      args["depth"] = event["args"]["depth"];
    }
    if (frame != nullptr && frame->call_function.has_value()) {
      args["call_function"] = *frame->call_function;
      args["call_file"] = *frame->call_file;
      args["call_line"] = *frame->call_line;
    } else if (event["args"].contains("call_function")) {
      args["call_function"] = event["args"]["call_function"];
      args["call_file"] = event["args"]["call_file"];
      args["call_line"] = event["args"]["call_line"];
    }
    event["args"] = std::move(args);
    return;
  }
}

}  // namespace

bool enabled() { return g_recording_depth > 0 || trace_output_path() != nullptr; }

std::string format_active_scopes() {
  if (g_scope_stack.empty()) {
    return {};
  }
  std::ostringstream ss;
  ss << "Active progress scopes:";
  for (size_t i = 0; i < g_scope_stack.size(); ++i) {
    const DiagnosticScopeFrame& frame = g_scope_stack[i];
    ss << "\n  [" << i << "] " << frame.name << " (" << frame.function << " @ " << frame.file << ':'
       << frame.line << ')';
    if (frame.call_function.has_value()) {
      ss << " [called from " << *frame.call_function << " @ " << *frame.call_file << ':'
         << *frame.call_line << ']';
    }
  }
  return ss.str();
}

RecordTrace::RecordTrace(fs::path path) : m_path(std::move(path)) {
  if (g_recording_depth++ == 0) {
    std::lock_guard lock(g_events_mutex);
    g_events.clear();
    g_next_scope_id = 1;
    g_origin = std::chrono::steady_clock::now();
  }
}

RecordTrace::~RecordTrace() {
  if (g_recording_depth == 0) {
    return;
  }
  --g_recording_depth;
  if (g_recording_depth > 0) {
    return;
  }
  if (!m_path.parent_path().empty()) {
    fs::create_directories(m_path.parent_path());
  }
  write_chrome_trace(m_path);
}

uint64_t register_progress_scope(const std::source_location& location, const double range_start,
                                 const double range_end, const std::string& name,
                                 const std::source_location* call_site) {
  const uint64_t scope_id = g_next_scope_id++;
  const std::string scope_name = scope_name_or_location(location, name);
  const std::source_location* effective_call_site = call_site;
  if (call_site == nullptr && name.empty()) {
    effective_call_site = &location;
  }
  if (enabled()) {
    json args = scope_begin_args(range_start, range_end, location, effective_call_site);
    args["scope_id"] = format_scope_id(scope_id);
    append_scope_event('B', scope_name, scope_id, std::move(args));
    push_scope_frame(scope_id, scope_name, location, effective_call_site);
  }
  return scope_id;
}

void progress_scope_set_display(const uint64_t scope_id, const std::string& name,
                                const std::source_location& callee_location) {
  if (!enabled()) {
    return;
  }
  DiagnosticScopeFrame* frame = find_scope_frame(scope_id);
  if (frame != nullptr) {
    frame->name = name;
    frame->file = basename_path(callee_location.file_name());
    frame->line = static_cast<int>(callee_location.line());
    frame->function = callee_location.function_name();
  }
  patch_scope_begin(scope_id, name, callee_location, frame);
}

void progress_end(const uint64_t scope_id, const double proportion) {
  if (enabled()) {
    const int depth = static_cast<int>(g_scope_stack.size()) - 1;
    append_scope_event('E', "", scope_id,
                       json{{"scope_id", format_scope_id(scope_id)},
                            {"depth", depth < 0 ? 0 : depth},
                            {"proportion", proportion}});
  }
  pop_scope_frame(scope_id);
}

void memory_counters(const uint64_t total_bytes, const uint64_t las_bytes,
                     const uint64_t grid_bytes) {
  constexpr double BYTES_PER_GB = 1e9;
  const auto emit_counter = [](const char* counter_name, const uint64_t bytes) {
    append_trace_event(json{{"name", counter_name},
                            {"cat", "memory"},
                            {"ph", "C"},
                            {"pid", k_pid},
                            {"tid", thread_id()},
                            {"args", {{"value", static_cast<double>(bytes) / BYTES_PER_GB}}}});
  };
  emit_counter("memory_total", total_bytes);
  emit_counter("memory_las", las_bytes);
  emit_counter("memory_grid", grid_bytes);
}

void write_chrome_trace(const fs::path& path) {
  std::vector<json> events;
  {
    std::lock_guard lock(g_events_mutex);
    events = g_events;
  }
  std::sort(events.begin(), events.end(),
            [](const json& a, const json& b) { return a.value("ts", 0ULL) < b.value("ts", 0ULL); });

  json trace_events = json::array();
  for (json& event : events) {
    trace_events.push_back(std::move(event));
  }

  const json output = {{"traceEvents", std::move(trace_events)}, {"displayTimeUnit", "ms"}};

  std::ofstream out(path);
  if (!out) {
    std::fprintf(stderr, "Failed to write trace to %s\n", path.string().c_str());
    return;
  }
  out << output.dump() << '\n';
}

void write_chrome_trace_if_configured() {
  const char* path = trace_output_path();
  if (path == nullptr) {
    return;
  }
  write_chrome_trace(path);
}

}  // namespace blaze::trace