blob: 4485cca1fcf1518bee9d9af4bc8cd8550dbada71 [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>
19
Tyler Chatowbf0609c2021-07-31 16:13:27 -070020#include <cerrno>
James Kuszmaul7daef362019-12-31 18:28:17 -080021#include <memory>
22
23#include "aos/configuration.h"
Austin Schuhb06f03b2021-02-17 22:00:37 -080024#include "aos/events/logging/log_reader.h"
James Kuszmaul7daef362019-12-31 18:28:17 -080025#include "aos/events/simulated_event_loop.h"
26#include "aos/flatbuffer_merge.h"
Austin Schuh094d09b2020-11-20 23:26:52 -080027#include "aos/init.h"
James Kuszmaul7daef362019-12-31 18:28:17 -080028#include "aos/json_to_flatbuffer.h"
29
30namespace frc971 {
31namespace analysis {
32namespace {
33
34// All the data corresponding to a single message.
35struct MessageData {
36 aos::monotonic_clock::time_point monotonic_sent_time;
37 aos::realtime_clock::time_point realtime_sent_time;
38 // JSON representation of the message.
39 std::string json_data;
40};
41
42// Data corresponding to an entire channel.
43struct ChannelData {
44 std::string name;
45 std::string type;
46 // Each message published on the channel, in order by monotonic time.
47 std::vector<MessageData> messages;
48};
49
50// All the objects that we need for managing reading a logfile.
51struct LogReaderTools {
52 std::unique_ptr<aos::logger::LogReader> reader;
James Kuszmaul7daef362019-12-31 18:28:17 -080053 // Event loop to use for subscribing to buses.
54 std::unique_ptr<aos::EventLoop> event_loop;
55 std::vector<ChannelData> channel_data;
56 // Whether we have called process() on the reader yet.
57 bool processed = false;
58};
59
60struct LogReaderType {
61 PyObject_HEAD;
62 LogReaderTools *tools = nullptr;
63};
64
65void LogReader_dealloc(LogReaderType *self) {
66 LogReaderTools *tools = self->tools;
James Kuszmaul7daef362019-12-31 18:28:17 -080067 delete tools;
68 Py_TYPE(self)->tp_free((PyObject *)self);
69}
70
71PyObject *LogReader_new(PyTypeObject *type, PyObject * /*args*/,
Tyler Chatowbf0609c2021-07-31 16:13:27 -070072 PyObject * /*kwds*/) {
James Kuszmaul7daef362019-12-31 18:28:17 -080073 LogReaderType *self;
74 self = (LogReaderType *)type->tp_alloc(type, 0);
75 if (self != nullptr) {
76 self->tools = new LogReaderTools();
77 if (self->tools == nullptr) {
78 return nullptr;
79 }
80 }
81 return (PyObject *)self;
82}
83
84int LogReader_init(LogReaderType *self, PyObject *args, PyObject *kwds) {
Austin Schuh094d09b2020-11-20 23:26:52 -080085 int count = 1;
86 if (!aos::IsInitialized()) {
87 // Fake out argc and argv to let InitGoogle run properly to instrument
88 // malloc, setup glog, and such.
89 char *name = program_invocation_name;
90 char **argv = &name;
91 aos::InitGoogle(&count, &argv);
92 }
93
James Kuszmaul7daef362019-12-31 18:28:17 -080094 const char *kwlist[] = {"log_file_name", nullptr};
95
96 const char *log_file_name;
97 if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast<char **>(kwlist),
98 &log_file_name)) {
99 return -1;
100 }
101
102 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
103 tools->reader = std::make_unique<aos::logger::LogReader>(log_file_name);
James Kuszmaul84ff3e52020-01-03 19:48:53 -0800104 tools->reader->Register();
James Kuszmaul7daef362019-12-31 18:28:17 -0800105
Austin Schuh9fe5d202020-02-29 15:09:53 -0800106 if (aos::configuration::MultiNode(tools->reader->configuration())) {
107 tools->event_loop = tools->reader->event_loop_factory()->MakeEventLoop(
108 "data_fetcher",
109 aos::configuration::GetNode(tools->reader->configuration(), "roborio"));
110 } else {
111 tools->event_loop =
112 tools->reader->event_loop_factory()->MakeEventLoop("data_fetcher");
113 }
James Kuszmaul7daef362019-12-31 18:28:17 -0800114 tools->event_loop->SkipTimingReport();
Tyler Chatow67ddb032020-01-12 14:30:04 -0800115 tools->event_loop->SkipAosLog();
James Kuszmaul7daef362019-12-31 18:28:17 -0800116
117 return 0;
118}
119
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700120PyObject *LogReader_get_data_for_channel(LogReaderType *self, PyObject *args,
121 PyObject *kwds) {
James Kuszmaul7daef362019-12-31 18:28:17 -0800122 const char *kwlist[] = {"name", "type", nullptr};
123
124 const char *name;
125 const char *type;
126 if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss",
127 const_cast<char **>(kwlist), &name, &type)) {
128 return nullptr;
129 }
130
131 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
132
133 if (!tools->processed) {
134 PyErr_SetString(PyExc_RuntimeError,
135 "Called get_data_for_bus before calling process().");
136 return nullptr;
137 }
138
139 for (const auto &channel : tools->channel_data) {
140 if (channel.name == name && channel.type == type) {
141 PyObject *list = PyList_New(channel.messages.size());
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700142 for (size_t ii = 0; ii < channel.messages.size(); ++ii) {
James Kuszmaul7daef362019-12-31 18:28:17 -0800143 const auto &message = channel.messages[ii];
144 PyObject *monotonic_time = PyLong_FromLongLong(
145 std::chrono::duration_cast<std::chrono::nanoseconds>(
146 message.monotonic_sent_time.time_since_epoch())
147 .count());
148 PyObject *realtime_time = PyLong_FromLongLong(
149 std::chrono::duration_cast<std::chrono::nanoseconds>(
150 message.realtime_sent_time.time_since_epoch())
151 .count());
152 PyObject *json_data = PyUnicode_FromStringAndSize(
153 message.json_data.data(), message.json_data.size());
154 PyObject *entry =
155 PyTuple_Pack(3, monotonic_time, realtime_time, json_data);
156 if (PyList_SetItem(list, ii, entry) != 0) {
157 return nullptr;
158 }
159 }
160 return list;
161 }
162 }
163 PyErr_SetString(PyExc_ValueError,
164 "The provided channel was never subscribed to.");
165 return nullptr;
166}
167
168PyObject *LogReader_subscribe(LogReaderType *self, PyObject *args,
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700169 PyObject *kwds) {
James Kuszmaul7daef362019-12-31 18:28:17 -0800170 const char *kwlist[] = {"name", "type", nullptr};
171
172 const char *name;
173 const char *type;
174 if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss",
175 const_cast<char **>(kwlist), &name, &type)) {
176 return nullptr;
177 }
178
179 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
180
181 if (tools->processed) {
182 PyErr_SetString(PyExc_RuntimeError,
183 "Called subscribe after calling process().");
184 return nullptr;
185 }
186
187 const aos::Channel *const channel = aos::configuration::GetChannel(
188 tools->reader->configuration(), name, type, "", nullptr);
189 if (channel == nullptr) {
190 return Py_False;
191 }
192 const int index = tools->channel_data.size();
Tyler Chatowbf0609c2021-07-31 16:13:27 -0700193 tools->channel_data.push_back({.name = name, .type = type, .messages = {}});
James Kuszmaul7daef362019-12-31 18:28:17 -0800194 tools->event_loop->MakeRawWatcher(
195 channel, [channel, index, tools](const aos::Context &context,
196 const void *message) {
197 tools->channel_data[index].messages.push_back(
198 {.monotonic_sent_time = context.monotonic_event_time,
199 .realtime_sent_time = context.realtime_event_time,
200 .json_data = aos::FlatbufferToJson(
201 channel->schema(), static_cast<const uint8_t *>(message))});
202 });
203 return Py_True;
204}
205
206static PyObject *LogReader_process(LogReaderType *self,
207 PyObject *Py_UNUSED(ignored)) {
208 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
209
210 if (tools->processed) {
211 PyErr_SetString(PyExc_RuntimeError, "process() may only be called once.");
212 return nullptr;
213 }
214
215 tools->processed = true;
216
James Kuszmaul84ff3e52020-01-03 19:48:53 -0800217 tools->reader->event_loop_factory()->Run();
James Kuszmaul7daef362019-12-31 18:28:17 -0800218
219 Py_RETURN_NONE;
220}
221
222static PyObject *LogReader_configuration(LogReaderType *self,
223 PyObject *Py_UNUSED(ignored)) {
224 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
225
226 // I have no clue if the Configuration that we get from the log reader is in a
227 // contiguous chunk of memory, and I'm too lazy to either figure it out or
228 // figure out how to extract the actual data buffer + offset.
229 // Instead, copy the flatbuffer and return a copy of the new buffer.
230 aos::FlatbufferDetachedBuffer<aos::Configuration> buffer =
231 aos::CopyFlatBuffer(tools->reader->configuration());
232
233 return PyBytes_FromStringAndSize(
Austin Schuhadd6eb32020-11-09 21:24:26 -0800234 reinterpret_cast<const char *>(buffer.span().data()),
235 buffer.span().size());
James Kuszmaul7daef362019-12-31 18:28:17 -0800236}
237
238static PyMethodDef LogReader_methods[] = {
239 {"configuration", (PyCFunction)LogReader_configuration, METH_NOARGS,
240 "Return a bytes buffer for the Configuration of the logfile."},
241 {"process", (PyCFunction)LogReader_process, METH_NOARGS,
242 "Processes the logfile and all the subscribed to channels."},
243 {"subscribe", (PyCFunction)LogReader_subscribe,
244 METH_VARARGS | METH_KEYWORDS,
245 "Attempts to subscribe to the provided channel name + type. Returns True "
246 "if successful."},
247 {"get_data_for_channel", (PyCFunction)LogReader_get_data_for_channel,
248 METH_VARARGS | METH_KEYWORDS,
249 "Returns the logged data for a given channel. Raises an exception if you "
250 "did not subscribe to the provided channel. Returned data is a list of "
251 "tuples where each tuple is of the form (monotonic_nsec, realtime_nsec, "
252 "json_message_data)."},
253 {nullptr, 0, 0, nullptr} /* Sentinel */
254};
255
256static PyTypeObject LogReaderType = {
257 PyVarObject_HEAD_INIT(NULL, 0).tp_name = "py_log_reader.LogReader",
258 .tp_doc = "LogReader objects",
259 .tp_basicsize = sizeof(LogReaderType),
260 .tp_itemsize = 0,
261 .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
262 .tp_new = LogReader_new,
263 .tp_init = (initproc)LogReader_init,
264 .tp_dealloc = (destructor)LogReader_dealloc,
265 .tp_methods = LogReader_methods,
266};
267
268static PyModuleDef log_reader_module = {
269 PyModuleDef_HEAD_INIT,
270 .m_name = "py_log_reader",
271 .m_doc = "Example module that creates an extension type.",
272 .m_size = -1,
273};
274
275PyObject *InitModule() {
276 PyObject *m;
277 if (PyType_Ready(&LogReaderType) < 0) return nullptr;
278
279 m = PyModule_Create(&log_reader_module);
280 if (m == nullptr) return nullptr;
281
282 Py_INCREF(&LogReaderType);
283 if (PyModule_AddObject(m, "LogReader", (PyObject *)&LogReaderType) < 0) {
284 Py_DECREF(&LogReaderType);
285 Py_DECREF(m);
286 return nullptr;
287 }
288
289 return m;
290}
291
292} // namespace
293} // namespace analysis
294} // namespace frc971
295
296PyMODINIT_FUNC PyInit_py_log_reader(void) {
297 return frc971::analysis::InitModule();
298}