ILLIXR: Illinois Extended Reality Testbed
record_logger.hpp
1 #pragma once
2 
3 #include <any>
4 #include <atomic>
5 #include <cassert>
6 #include <chrono>
7 #include <memory>
8 #include <optional>
9 #include <spdlog/spdlog.h>
10 #include <string>
11 #include <unordered_map>
12 #include <utility>
13 #include <vector>
14 
15 #ifndef NDEBUG
16  #include <iostream>
17  #include <sstream>
18 #endif
19 
20 #include "phonebook.hpp"
21 
22 namespace ILLIXR {
23 
30 public:
31  record_header(const std::string& name_, std::vector<std::pair<std::string, const std::type_info&>> columns_)
32  : id{std::hash<std::string>{}(name_)}
33  , name{name_}
34  , columns{std::move(columns_)} { }
35 
39  bool operator==(const record_header& other) const {
40  // Check pointer first
41  if (this == &other) {
42  return true;
43  }
44 
45  if (name != other.name || columns.size() != other.columns.size() || id != other.id) {
46  return false;
47  }
48  for (std::size_t i = 0; i < columns.size(); ++i) {
49  if (columns[i] != other.columns[i]) {
50  return false;
51  }
52  }
53  return true;
54  }
55 
56  bool operator!=(const record_header& other) const {
57  return !(*this == other);
58  }
59 
60  [[nodiscard]] std::size_t get_id() const {
61  return id;
62  }
63 
64  [[nodiscard]] const std::string& get_name() const {
65  return name;
66  }
67 
68  [[nodiscard]] const std::string& get_column_name(unsigned column) const {
69  return columns[column].first;
70  }
71 
72  [[nodiscard]] const std::type_info& get_column_type(unsigned column) const {
73  return columns[column].second;
74  }
75 
76  [[nodiscard]] unsigned get_columns() const {
77  return columns.size();
78  }
79 
80  [[nodiscard]] std::string to_string() const {
81  std::string ret = std::string{"record_header "} + name + std::string{" { "};
82  for (const auto& pair : columns) {
83  ret += std::string{pair.second.name()} + std::string{" "} + pair.first + std::string{"; "};
84  }
85  ret.erase(ret.size() - 2);
86  ret += std::string{" }"};
87  return ret;
88  }
89 
90 private:
91  std::size_t id;
92  std::string name;
93  const std::vector<std::pair<std::string, const std::type_info&>> columns;
94 };
95 
102 public:
104  : used{false} { }
105 
107  : used{false} {
108  other.used = true;
109  }
110 
111  data_use_indicator& operator=(const data_use_indicator& other) {
112  if (&other != this) {
113  other.used = true;
114  used = false;
115  }
116  return *this;
117  }
118 
119  /*
120  copy constructors are just as efficient as move constructors would be,
121  so I won't define move constructors. C++ will invoke copy instead (for no loss).
122  */
123  bool is_used() const {
124  return used;
125  }
126 
127  void mark_used() const {
128  used = true;
129  }
130 
131  void mark_unused() const {
132  used = false;
133  }
134 
135 private:
136  mutable bool used;
137 };
138 
144 class record {
145 public:
146  record(const record_header& rh_, std::vector<std::any> values_)
147  : rh{rh_}
148  , values(std::move(values_)) {
149 #ifndef NDEBUG
150  assert(rh);
151  if (values.size() != rh->get().get_columns()) {
152  spdlog::get("illixr")->error("[record_logger] {} elements passed, but rh for {} only specifies {}.", values.size(),
153  rh->get().get_name(), rh->get().get_columns());
154  abort();
155  }
156  for (std::size_t column = 0; column < values.size(); ++column) {
157  if (values[column].type() != rh->get().get_column_type(column)) {
158  spdlog::get("illixr")->error("[record_logger] Caller got wrong type for column {} of {}.", column,
159  rh->get().get_name());
160  spdlog::get("illixr")->error("[record_logger] Caller passed: {}; record_header specifies: {}",
161  values[column].type().name(), rh->get().get_column_type(column).name());
162  abort();
163  }
164  }
165 #endif
166  }
167 
168  record() = default;
169 
170  ~record() {
171 #ifndef NDEBUG
172  if (rh && !data_use_indicator_.is_used()) {
173  spdlog::get("illixr")->error("[record_logger] Record was deleted without being logged.");
174  abort();
175  }
176 #endif
177  }
178 
179  template<typename T>
180  T get_value(unsigned column) const {
181 #ifndef NDEBUG
182  assert(rh);
183  data_use_indicator_.mark_used();
184  if (rh->get().get_column_type(column) != typeid(T)) {
185  std::ostringstream ss;
186  ss << "Caller column type for " << column << " of " << rh->get().get_name() << ". "
187  << "Caller passed: " << typeid(T).name() << "; "
188  << "record_header specifies: " << rh->get().get_column_type(column).name() << ". ";
189  throw std::runtime_error{ss.str()};
190  }
191 #endif
192  return std::any_cast<T>(values[column]);
193  }
194 
195  const record_header& get_record_header() const {
196  assert(rh);
197  return rh->get();
198  }
199 
200  void mark_used() const {
201 #ifndef NDEBUG
202  assert(rh);
203  data_use_indicator_.mark_used();
204 #endif
205  }
206 
207 private:
208  // Holding a pointer to a record_header is more efficient than
209  // requiring each record to hold a list of its column names
210  // and table name. This is just one pointer.
211  std::optional<std::reference_wrapper<const record_header>> rh;
212  std::vector<std::any> values;
213 #ifndef NDEBUG
214  data_use_indicator data_use_indicator_;
215 #endif
216 };
217 
227 public:
228  ~record_logger() override = default;
229 
233  virtual void log(const record& r) = 0;
234 
240  virtual void log(const std::vector<record>& rs) {
241  for (const record& r : rs) {
242  log(r);
243  }
244  }
245 };
246 
262 class gen_guid : public phonebook::service {
263 public:
267  std::size_t get(std::size_t namespace_ = 0, std::size_t subnamespace = 0, std::size_t subsubnamespace = 0) {
268  if (guid_starts[namespace_][subnamespace].count(subsubnamespace) == 0) {
269  guid_starts[namespace_][subnamespace][subsubnamespace].store(1);
270  }
271  return guid_starts[namespace_][subnamespace][subsubnamespace]++;
272  }
273 
274 private:
275  std::unordered_map<std::size_t, std::unordered_map<std::size_t, std::unordered_map<std::size_t, std::atomic<std::size_t>>>>
276  guid_starts;
277 };
278 
279 static std::chrono::milliseconds LOG_BUFFER_DELAY{1000};
280 
308 private:
309  std::shared_ptr<record_logger> logger;
310  std::chrono::time_point<std::chrono::high_resolution_clock> last_log;
311  std::vector<record> buffer;
312 
313 public:
314  explicit record_coalescer(std::shared_ptr<record_logger> logger_)
315  : logger{std::move(logger_)}
316  , last_log{std::chrono::high_resolution_clock::now()} { }
317 
318  ~record_coalescer() {
319  flush();
320  }
321 
325  void log(const record& r) {
326  if (logger) {
327  buffer.push_back(r);
328  // Log coalescer should only be used with
329  // In the common case, they will be the same pointer, quickly check the pointers.
330  // In the less common case, we check for object-structural equality.
331 #ifndef NDEBUG
332  if (&r.get_record_header() != &buffer[0].get_record_header() &&
333  r.get_record_header() == buffer[0].get_record_header()) {
334  spdlog::get("illixr")->error("[record_logger] Tried to push a record of type {} to a record logger for type {}",
335  r.get_record_header().to_string(), buffer[0].get_record_header().to_string());
336  abort();
337  }
338 #endif
339  maybe_flush();
340  }
341  }
342 
346  void maybe_flush() {
347  if (std::chrono::high_resolution_clock::now() > last_log + LOG_BUFFER_DELAY) {
348  flush();
349  }
350  }
351 
355  void flush() {
356  if (logger) {
357  std::vector<record> buffer2;
358  buffer.swap(buffer2);
359  logger->log(buffer2);
360  last_log = std::chrono::high_resolution_clock::now();
361  }
362  }
363 
364  explicit operator bool() const {
365  return bool(logger);
366  }
367 };
368 } // namespace ILLIXR
A helper class that lets one dynamically determine if some data gets used.
Definition: record_logger.hpp:101
This class generates unique IDs.
Definition: record_logger.hpp:262
std::size_t get(std::size_t namespace_=0, std::size_t subnamespace=0, std::size_t subsubnamespace=0)
Generate a number, unique from other calls to the same namespace/subnamespace/subsubnamepsace.
Definition: record_logger.hpp:267
A 'service' that can be registered in the phonebook.
Definition: phonebook.hpp:86
Coalesces logs of the same type to be written back as a single-transaction.
Definition: record_logger.hpp:307
void log(const record &r)
Appends a log to the buffer, which will eventually be written.
Definition: record_logger.hpp:325
void flush()
Flush buffer of logs to the underlying logger.
Definition: record_logger.hpp:355
void maybe_flush()
Use internal decision process, and possibly trigger flush.
Definition: record_logger.hpp:346
Schema of each record.
Definition: record_logger.hpp:29
bool operator==(const record_header &other) const
Compares two schemata.
Definition: record_logger.hpp:39
The ILLIXR logging service for structured records.
Definition: record_logger.hpp:226
virtual void log(const record &r)=0
Writes one log record.
virtual void log(const std::vector< record > &rs)
Writes many of the same type of log record.
Definition: record_logger.hpp:240
This class represents a tuple of fields which get logged by record_logger.
Definition: record_logger.hpp:144
RAC_ERRNO_MSG.
Definition: data_format.hpp:15
void abort(const std::string &msg="", [[maybe_unused]] const int error_val=1)
Exits the application during a fatal error.
Definition: error_util.hpp:61