blob: c190acb6602bdcd85b09cbc3f25c7f15623852f8 [file] [log] [blame]
James Kuszmaul7daef362019-12-31 18:28:17 -08001// This file provides a Python module for reading logfiles. See
2// log_reader_test.py for usage.
3//
James Kuszmauld3a6d8a2021-06-23 21:08:22 -07004// NOTE: This code has not been maintained recently, and so is missing key
5// features to support reading multi-node logfiles (namely, it assumes the the
6// logfile is just a single file). Updating this code should not be difficult,
7// but hasn't been needed thus far.
8//
James Kuszmaul7daef362019-12-31 18:28:17 -08009// This reader works by having the user specify exactly what channels they want
10// data for. We then process the logfile and store all the data on that channel
11// into a list of timestamps + JSON message data. The user can then use an
12// accessor method (get_data_for_channel) to retrieve the cached data.
13
14// Defining PY_SSIZE_T_CLEAN seems to be suggested by most of the Python
15// documentation.
16#define PY_SSIZE_T_CLEAN
17// Note that Python.h needs to be included before anything else.
18#include <Python.h>
Stephan Pleines31f98da2024-05-22 17:31:23 -070019#include <stddef.h>
20#include <stdint.h>
James Kuszmaul7daef362019-12-31 18:28:17 -080021
Stephan Pleines31f98da2024-05-22 17:31:23 -070022#include <algorithm>
Tyler Chatowbf0609c2021-07-31 16:13:27 -070023#include <cerrno>
Stephan Pleines31f98da2024-05-22 17:31:23 -070024#include <chrono>
James Kuszmaul7daef362019-12-31 18:28:17 -080025#include <memory>
Stephan Pleines31f98da2024-05-22 17:31:23 -070026#include <string>
27#include <vector>
28
29#include "absl/types/span.h"
30#include "glog/logging.h"
James Kuszmaul7daef362019-12-31 18:28:17 -080031
32#include "aos/configuration.h"
Stephan Pleines31f98da2024-05-22 17:31:23 -070033#include "aos/events/context.h"
34#include "aos/events/event_loop.h"
Austin Schuhb06f03b2021-02-17 22:00:37 -080035#include "aos/events/logging/log_reader.h"
James Kuszmaul7daef362019-12-31 18:28:17 -080036#include "aos/events/simulated_event_loop.h"
37#include "aos/flatbuffer_merge.h"
Stephan Pleines31f98da2024-05-22 17:31:23 -070038#include "aos/flatbuffers.h"
Austin Schuh094d09b2020-11-20 23:26:52 -080039#include "aos/init.h"
James Kuszmaul7daef362019-12-31 18:28:17 -080040#include "aos/json_to_flatbuffer.h"
Stephan Pleines31f98da2024-05-22 17:31:23 -070041#include "aos/time/time.h"
James Kuszmaul7daef362019-12-31 18:28:17 -080042
Stephan Pleines9e40c8e2024-02-07 20:58:28 -080043namespace aos::analysis {
James Kuszmaul7daef362019-12-31 18:28:17 -080044namespace {
45
46// All the data corresponding to a single message.
47struct MessageData {
48 aos::monotonic_clock::time_point monotonic_sent_time;
49 aos::realtime_clock::time_point realtime_sent_time;
50 // JSON representation of the message.
51 std::string json_data;
52};
53
54// Data corresponding to an entire channel.
55struct ChannelData {
56 std::string name;
57 std::string type;
58 // Each message published on the channel, in order by monotonic time.
59 std::vector<MessageData> messages;
60};
61
62// All the objects that we need for managing reading a logfile.
63struct LogReaderTools {
64 std::unique_ptr<aos::logger::LogReader> reader;
James Kuszmaul7daef362019-12-31 18:28:17 -080065 // Event loop to use for subscribing to buses.
66 std::unique_ptr<aos::EventLoop> event_loop;
67 std::vector<ChannelData> channel_data;
68 // Whether we have called process() on the reader yet.
69 bool processed = false;
70};
71
72struct LogReaderType {
73 PyObject_HEAD;
74 LogReaderTools *tools = nullptr;
75};
76
77void LogReader_dealloc(LogReaderType *self) {
78 LogReaderTools *tools = self->tools;
James Kuszmaul7daef362019-12-31 18:28:17 -080079 delete tools;
80 Py_TYPE(self)->tp_free((PyObject *)self);
81}
82
83PyObject *LogReader_new(PyTypeObject *type, PyObject * /*args*/,
Tyler Chatowbf0609c2021-07-31 16:13:27 -070084 PyObject * /*kwds*/) {
James Kuszmaul7daef362019-12-31 18:28:17 -080085 LogReaderType *self;
86 self = (LogReaderType *)type->tp_alloc(type, 0);
87 if (self != nullptr) {
88 self->tools = new LogReaderTools();
89 if (self->tools == nullptr) {
90 return nullptr;
91 }
92 }
93 return (PyObject *)self;
94}
95
96int LogReader_init(LogReaderType *self, PyObject *args, PyObject *kwds) {
Austin Schuh094d09b2020-11-20 23:26:52 -080097 int count = 1;
98 if (!aos::IsInitialized()) {
99 // Fake out argc and argv to let InitGoogle run properly to instrument
100 // malloc, setup glog, and such.
101 char *name = program_invocation_name;
102 char **argv = &name;
103 aos::InitGoogle(&count, &argv);
104 }
105
James Kuszmaul7daef362019-12-31 18:28:17 -0800106 const char *kwlist[] = {"log_file_name", nullptr};
107
108 const char *log_file_name;
109 if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast<char **>(kwlist),
110 &log_file_name)) {
111 return -1;
112 }
113
Austin Schuh6bdcc372024-06-27 14:49:11 -0700114 LogReaderTools *tools = self->tools;
115 CHECK(tools != nullptr);
James Kuszmaul7daef362019-12-31 18:28:17 -0800116 tools->reader = std::make_unique<aos::logger::LogReader>(log_file_name);
James Kuszmaul84ff3e52020-01-03 19:48:53 -0800117 tools->reader->Register();
James Kuszmaul7daef362019-12-31 18:28:17 -0800118
Austin Schuh9fe5d202020-02-29 15:09:53 -0800119 if (aos::configuration::MultiNode(tools->reader->configuration())) {
120 tools->event_loop = tools->reader->event_loop_factory()->MakeEventLoop(
121 "data_fetcher",
122 aos::configuration::GetNode(tools->reader->configuration(), "roborio"));
123 } else {
124 tools->event_loop =
125 tools->reader->event_loop_factory()->MakeEventLoop("data_fetcher");
126 }
James Kuszmaul7daef362019-12-31 18:28:17 -0800127 tools->event_loop->SkipTimingReport();
Tyler Chatow67ddb032020-01-12 14:30:04 -0800128 tools->event_loop->SkipAosLog();
James Kuszmaul7daef362019-12-31 18:28:17 -0800129
130 return 0;
131}
132
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700133PyObject *LogReader_get_data_for_channel(LogReaderType *self, PyObject *args,
134 PyObject *kwds) {
James Kuszmaul7daef362019-12-31 18:28:17 -0800135 const char *kwlist[] = {"name", "type", nullptr};
136
137 const char *name;
138 const char *type;
139 if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss",
140 const_cast<char **>(kwlist), &name, &type)) {
141 return nullptr;
142 }
143
Austin Schuh6bdcc372024-06-27 14:49:11 -0700144 LogReaderTools *tools = self->tools;
145 CHECK(tools != nullptr);
James Kuszmaul7daef362019-12-31 18:28:17 -0800146
147 if (!tools->processed) {
148 PyErr_SetString(PyExc_RuntimeError,
149 "Called get_data_for_bus before calling process().");
150 return nullptr;
151 }
152
153 for (const auto &channel : tools->channel_data) {
154 if (channel.name == name && channel.type == type) {
155 PyObject *list = PyList_New(channel.messages.size());
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700156 for (size_t ii = 0; ii < channel.messages.size(); ++ii) {
James Kuszmaul7daef362019-12-31 18:28:17 -0800157 const auto &message = channel.messages[ii];
158 PyObject *monotonic_time = PyLong_FromLongLong(
159 std::chrono::duration_cast<std::chrono::nanoseconds>(
160 message.monotonic_sent_time.time_since_epoch())
161 .count());
162 PyObject *realtime_time = PyLong_FromLongLong(
163 std::chrono::duration_cast<std::chrono::nanoseconds>(
164 message.realtime_sent_time.time_since_epoch())
165 .count());
166 PyObject *json_data = PyUnicode_FromStringAndSize(
167 message.json_data.data(), message.json_data.size());
168 PyObject *entry =
169 PyTuple_Pack(3, monotonic_time, realtime_time, json_data);
170 if (PyList_SetItem(list, ii, entry) != 0) {
171 return nullptr;
172 }
173 }
174 return list;
175 }
176 }
177 PyErr_SetString(PyExc_ValueError,
178 "The provided channel was never subscribed to.");
179 return nullptr;
180}
181
182PyObject *LogReader_subscribe(LogReaderType *self, PyObject *args,
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700183 PyObject *kwds) {
James Kuszmaul7daef362019-12-31 18:28:17 -0800184 const char *kwlist[] = {"name", "type", nullptr};
185
186 const char *name;
187 const char *type;
188 if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss",
189 const_cast<char **>(kwlist), &name, &type)) {
190 return nullptr;
191 }
192
Austin Schuh6bdcc372024-06-27 14:49:11 -0700193 LogReaderTools *tools = self->tools;
194 CHECK(tools != nullptr);
James Kuszmaul7daef362019-12-31 18:28:17 -0800195
196 if (tools->processed) {
197 PyErr_SetString(PyExc_RuntimeError,
198 "Called subscribe after calling process().");
199 return nullptr;
200 }
201
202 const aos::Channel *const channel = aos::configuration::GetChannel(
203 tools->reader->configuration(), name, type, "", nullptr);
204 if (channel == nullptr) {
205 return Py_False;
206 }
207 const int index = tools->channel_data.size();
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700208 tools->channel_data.push_back({.name = name, .type = type, .messages = {}});
James Kuszmaul7daef362019-12-31 18:28:17 -0800209 tools->event_loop->MakeRawWatcher(
210 channel, [channel, index, tools](const aos::Context &context,
211 const void *message) {
212 tools->channel_data[index].messages.push_back(
213 {.monotonic_sent_time = context.monotonic_event_time,
214 .realtime_sent_time = context.realtime_event_time,
215 .json_data = aos::FlatbufferToJson(
216 channel->schema(), static_cast<const uint8_t *>(message))});
217 });
218 return Py_True;
219}
220
221static PyObject *LogReader_process(LogReaderType *self,
222 PyObject *Py_UNUSED(ignored)) {
Austin Schuh6bdcc372024-06-27 14:49:11 -0700223 LogReaderTools *tools = self->tools;
224 CHECK(tools != nullptr);
James Kuszmaul7daef362019-12-31 18:28:17 -0800225
226 if (tools->processed) {
227 PyErr_SetString(PyExc_RuntimeError, "process() may only be called once.");
228 return nullptr;
229 }
230
231 tools->processed = true;
232
James Kuszmaul84ff3e52020-01-03 19:48:53 -0800233 tools->reader->event_loop_factory()->Run();
James Kuszmaul7daef362019-12-31 18:28:17 -0800234
235 Py_RETURN_NONE;
236}
237
238static PyObject *LogReader_configuration(LogReaderType *self,
239 PyObject *Py_UNUSED(ignored)) {
Austin Schuh6bdcc372024-06-27 14:49:11 -0700240 LogReaderTools *tools = self->tools;
241 CHECK(tools != nullptr);
James Kuszmaul7daef362019-12-31 18:28:17 -0800242
243 // I have no clue if the Configuration that we get from the log reader is in a
244 // contiguous chunk of memory, and I'm too lazy to either figure it out or
245 // figure out how to extract the actual data buffer + offset.
246 // Instead, copy the flatbuffer and return a copy of the new buffer.
247 aos::FlatbufferDetachedBuffer<aos::Configuration> buffer =
248 aos::CopyFlatBuffer(tools->reader->configuration());
249
250 return PyBytes_FromStringAndSize(
Austin Schuhadd6eb32020-11-09 21:24:26 -0800251 reinterpret_cast<const char *>(buffer.span().data()),
252 buffer.span().size());
James Kuszmaul7daef362019-12-31 18:28:17 -0800253}
254
255static PyMethodDef LogReader_methods[] = {
256 {"configuration", (PyCFunction)LogReader_configuration, METH_NOARGS,
257 "Return a bytes buffer for the Configuration of the logfile."},
258 {"process", (PyCFunction)LogReader_process, METH_NOARGS,
259 "Processes the logfile and all the subscribed to channels."},
260 {"subscribe", (PyCFunction)LogReader_subscribe,
261 METH_VARARGS | METH_KEYWORDS,
262 "Attempts to subscribe to the provided channel name + type. Returns True "
263 "if successful."},
264 {"get_data_for_channel", (PyCFunction)LogReader_get_data_for_channel,
265 METH_VARARGS | METH_KEYWORDS,
266 "Returns the logged data for a given channel. Raises an exception if you "
267 "did not subscribe to the provided channel. Returned data is a list of "
268 "tuples where each tuple is of the form (monotonic_nsec, realtime_nsec, "
269 "json_message_data)."},
270 {nullptr, 0, 0, nullptr} /* Sentinel */
271};
272
Brian Silverman4c7235a2021-11-17 19:04:37 -0800273#ifdef __clang__
274// These extensions to C++ syntax do surprising things in C++, but for these
275// uses none of them really matter I think, and the alternatives are really
276// annoying.
277#pragma clang diagnostic ignored "-Wc99-designator"
278#endif
279
James Kuszmaul7daef362019-12-31 18:28:17 -0800280static PyTypeObject LogReaderType = {
Brian Silverman4c7235a2021-11-17 19:04:37 -0800281 PyVarObject_HEAD_INIT(NULL, 0)
282 // The previous macro initializes some fields, leave a comment to help
283 // clang-format not make this uglier.
284 .tp_name = "py_log_reader.LogReader",
James Kuszmaul7daef362019-12-31 18:28:17 -0800285 .tp_basicsize = sizeof(LogReaderType),
286 .tp_itemsize = 0,
James Kuszmaul7daef362019-12-31 18:28:17 -0800287 .tp_dealloc = (destructor)LogReader_dealloc,
Brian Silverman4c7235a2021-11-17 19:04:37 -0800288 .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
289 .tp_doc = "LogReader objects",
James Kuszmaul7daef362019-12-31 18:28:17 -0800290 .tp_methods = LogReader_methods,
Brian Silverman4c7235a2021-11-17 19:04:37 -0800291 .tp_init = (initproc)LogReader_init,
292 .tp_new = LogReader_new,
James Kuszmaul7daef362019-12-31 18:28:17 -0800293};
294
295static PyModuleDef log_reader_module = {
296 PyModuleDef_HEAD_INIT,
297 .m_name = "py_log_reader",
298 .m_doc = "Example module that creates an extension type.",
299 .m_size = -1,
300};
301
302PyObject *InitModule() {
303 PyObject *m;
304 if (PyType_Ready(&LogReaderType) < 0) return nullptr;
305
306 m = PyModule_Create(&log_reader_module);
307 if (m == nullptr) return nullptr;
308
309 Py_INCREF(&LogReaderType);
310 if (PyModule_AddObject(m, "LogReader", (PyObject *)&LogReaderType) < 0) {
311 Py_DECREF(&LogReaderType);
312 Py_DECREF(m);
313 return nullptr;
314 }
315
316 return m;
317}
318
319} // namespace
Stephan Pleines9e40c8e2024-02-07 20:58:28 -0800320} // namespace aos::analysis
James Kuszmaul7daef362019-12-31 18:28:17 -0800321
322PyMODINIT_FUNC PyInit_py_log_reader(void) {
Stephan Pleines9e40c8e2024-02-07 20:58:28 -0800323 return aos::analysis::InitModule();
James Kuszmaul7daef362019-12-31 18:28:17 -0800324}