blob: 340140c4740b0a9b748c7cf4342b35150b40a634 [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
114 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
115 tools->reader = std::make_unique<aos::logger::LogReader>(log_file_name);
James Kuszmaul84ff3e52020-01-03 19:48:53 -0800116 tools->reader->Register();
James Kuszmaul7daef362019-12-31 18:28:17 -0800117
Austin Schuh9fe5d202020-02-29 15:09:53 -0800118 if (aos::configuration::MultiNode(tools->reader->configuration())) {
119 tools->event_loop = tools->reader->event_loop_factory()->MakeEventLoop(
120 "data_fetcher",
121 aos::configuration::GetNode(tools->reader->configuration(), "roborio"));
122 } else {
123 tools->event_loop =
124 tools->reader->event_loop_factory()->MakeEventLoop("data_fetcher");
125 }
James Kuszmaul7daef362019-12-31 18:28:17 -0800126 tools->event_loop->SkipTimingReport();
Tyler Chatow67ddb032020-01-12 14:30:04 -0800127 tools->event_loop->SkipAosLog();
James Kuszmaul7daef362019-12-31 18:28:17 -0800128
129 return 0;
130}
131
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700132PyObject *LogReader_get_data_for_channel(LogReaderType *self, PyObject *args,
133 PyObject *kwds) {
James Kuszmaul7daef362019-12-31 18:28:17 -0800134 const char *kwlist[] = {"name", "type", nullptr};
135
136 const char *name;
137 const char *type;
138 if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss",
139 const_cast<char **>(kwlist), &name, &type)) {
140 return nullptr;
141 }
142
143 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
144
145 if (!tools->processed) {
146 PyErr_SetString(PyExc_RuntimeError,
147 "Called get_data_for_bus before calling process().");
148 return nullptr;
149 }
150
151 for (const auto &channel : tools->channel_data) {
152 if (channel.name == name && channel.type == type) {
153 PyObject *list = PyList_New(channel.messages.size());
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700154 for (size_t ii = 0; ii < channel.messages.size(); ++ii) {
James Kuszmaul7daef362019-12-31 18:28:17 -0800155 const auto &message = channel.messages[ii];
156 PyObject *monotonic_time = PyLong_FromLongLong(
157 std::chrono::duration_cast<std::chrono::nanoseconds>(
158 message.monotonic_sent_time.time_since_epoch())
159 .count());
160 PyObject *realtime_time = PyLong_FromLongLong(
161 std::chrono::duration_cast<std::chrono::nanoseconds>(
162 message.realtime_sent_time.time_since_epoch())
163 .count());
164 PyObject *json_data = PyUnicode_FromStringAndSize(
165 message.json_data.data(), message.json_data.size());
166 PyObject *entry =
167 PyTuple_Pack(3, monotonic_time, realtime_time, json_data);
168 if (PyList_SetItem(list, ii, entry) != 0) {
169 return nullptr;
170 }
171 }
172 return list;
173 }
174 }
175 PyErr_SetString(PyExc_ValueError,
176 "The provided channel was never subscribed to.");
177 return nullptr;
178}
179
180PyObject *LogReader_subscribe(LogReaderType *self, PyObject *args,
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700181 PyObject *kwds) {
James Kuszmaul7daef362019-12-31 18:28:17 -0800182 const char *kwlist[] = {"name", "type", nullptr};
183
184 const char *name;
185 const char *type;
186 if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss",
187 const_cast<char **>(kwlist), &name, &type)) {
188 return nullptr;
189 }
190
191 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
192
193 if (tools->processed) {
194 PyErr_SetString(PyExc_RuntimeError,
195 "Called subscribe after calling process().");
196 return nullptr;
197 }
198
199 const aos::Channel *const channel = aos::configuration::GetChannel(
200 tools->reader->configuration(), name, type, "", nullptr);
201 if (channel == nullptr) {
202 return Py_False;
203 }
204 const int index = tools->channel_data.size();
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700205 tools->channel_data.push_back({.name = name, .type = type, .messages = {}});
James Kuszmaul7daef362019-12-31 18:28:17 -0800206 tools->event_loop->MakeRawWatcher(
207 channel, [channel, index, tools](const aos::Context &context,
208 const void *message) {
209 tools->channel_data[index].messages.push_back(
210 {.monotonic_sent_time = context.monotonic_event_time,
211 .realtime_sent_time = context.realtime_event_time,
212 .json_data = aos::FlatbufferToJson(
213 channel->schema(), static_cast<const uint8_t *>(message))});
214 });
215 return Py_True;
216}
217
218static PyObject *LogReader_process(LogReaderType *self,
219 PyObject *Py_UNUSED(ignored)) {
220 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
221
222 if (tools->processed) {
223 PyErr_SetString(PyExc_RuntimeError, "process() may only be called once.");
224 return nullptr;
225 }
226
227 tools->processed = true;
228
James Kuszmaul84ff3e52020-01-03 19:48:53 -0800229 tools->reader->event_loop_factory()->Run();
James Kuszmaul7daef362019-12-31 18:28:17 -0800230
231 Py_RETURN_NONE;
232}
233
234static PyObject *LogReader_configuration(LogReaderType *self,
235 PyObject *Py_UNUSED(ignored)) {
236 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
237
238 // I have no clue if the Configuration that we get from the log reader is in a
239 // contiguous chunk of memory, and I'm too lazy to either figure it out or
240 // figure out how to extract the actual data buffer + offset.
241 // Instead, copy the flatbuffer and return a copy of the new buffer.
242 aos::FlatbufferDetachedBuffer<aos::Configuration> buffer =
243 aos::CopyFlatBuffer(tools->reader->configuration());
244
245 return PyBytes_FromStringAndSize(
Austin Schuhadd6eb32020-11-09 21:24:26 -0800246 reinterpret_cast<const char *>(buffer.span().data()),
247 buffer.span().size());
James Kuszmaul7daef362019-12-31 18:28:17 -0800248}
249
250static PyMethodDef LogReader_methods[] = {
251 {"configuration", (PyCFunction)LogReader_configuration, METH_NOARGS,
252 "Return a bytes buffer for the Configuration of the logfile."},
253 {"process", (PyCFunction)LogReader_process, METH_NOARGS,
254 "Processes the logfile and all the subscribed to channels."},
255 {"subscribe", (PyCFunction)LogReader_subscribe,
256 METH_VARARGS | METH_KEYWORDS,
257 "Attempts to subscribe to the provided channel name + type. Returns True "
258 "if successful."},
259 {"get_data_for_channel", (PyCFunction)LogReader_get_data_for_channel,
260 METH_VARARGS | METH_KEYWORDS,
261 "Returns the logged data for a given channel. Raises an exception if you "
262 "did not subscribe to the provided channel. Returned data is a list of "
263 "tuples where each tuple is of the form (monotonic_nsec, realtime_nsec, "
264 "json_message_data)."},
265 {nullptr, 0, 0, nullptr} /* Sentinel */
266};
267
Brian Silverman4c7235a2021-11-17 19:04:37 -0800268#ifdef __clang__
269// These extensions to C++ syntax do surprising things in C++, but for these
270// uses none of them really matter I think, and the alternatives are really
271// annoying.
272#pragma clang diagnostic ignored "-Wc99-designator"
273#endif
274
James Kuszmaul7daef362019-12-31 18:28:17 -0800275static PyTypeObject LogReaderType = {
Brian Silverman4c7235a2021-11-17 19:04:37 -0800276 PyVarObject_HEAD_INIT(NULL, 0)
277 // The previous macro initializes some fields, leave a comment to help
278 // clang-format not make this uglier.
279 .tp_name = "py_log_reader.LogReader",
James Kuszmaul7daef362019-12-31 18:28:17 -0800280 .tp_basicsize = sizeof(LogReaderType),
281 .tp_itemsize = 0,
James Kuszmaul7daef362019-12-31 18:28:17 -0800282 .tp_dealloc = (destructor)LogReader_dealloc,
Brian Silverman4c7235a2021-11-17 19:04:37 -0800283 .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
284 .tp_doc = "LogReader objects",
James Kuszmaul7daef362019-12-31 18:28:17 -0800285 .tp_methods = LogReader_methods,
Brian Silverman4c7235a2021-11-17 19:04:37 -0800286 .tp_init = (initproc)LogReader_init,
287 .tp_new = LogReader_new,
James Kuszmaul7daef362019-12-31 18:28:17 -0800288};
289
290static PyModuleDef log_reader_module = {
291 PyModuleDef_HEAD_INIT,
292 .m_name = "py_log_reader",
293 .m_doc = "Example module that creates an extension type.",
294 .m_size = -1,
295};
296
297PyObject *InitModule() {
298 PyObject *m;
299 if (PyType_Ready(&LogReaderType) < 0) return nullptr;
300
301 m = PyModule_Create(&log_reader_module);
302 if (m == nullptr) return nullptr;
303
304 Py_INCREF(&LogReaderType);
305 if (PyModule_AddObject(m, "LogReader", (PyObject *)&LogReaderType) < 0) {
306 Py_DECREF(&LogReaderType);
307 Py_DECREF(m);
308 return nullptr;
309 }
310
311 return m;
312}
313
314} // namespace
Stephan Pleines9e40c8e2024-02-07 20:58:28 -0800315} // namespace aos::analysis
James Kuszmaul7daef362019-12-31 18:28:17 -0800316
317PyMODINIT_FUNC PyInit_py_log_reader(void) {
Stephan Pleines9e40c8e2024-02-07 20:58:28 -0800318 return aos::analysis::InitModule();
James Kuszmaul7daef362019-12-31 18:28:17 -0800319}