blob: 6a6c7678ef7d9130a9d798f22545dd822a35462b [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
20#include <memory>
Austin Schuh094d09b2020-11-20 23:26:52 -080021#include <errno.h>
James Kuszmaul7daef362019-12-31 18:28:17 -080022
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*/,
72 PyObject * /*kwds*/) {
73 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
120PyObject *LogReader_get_data_for_channel(LogReaderType *self,
121 PyObject *args,
122 PyObject *kwds) {
123 const char *kwlist[] = {"name", "type", nullptr};
124
125 const char *name;
126 const char *type;
127 if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss",
128 const_cast<char **>(kwlist), &name, &type)) {
129 return nullptr;
130 }
131
132 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
133
134 if (!tools->processed) {
135 PyErr_SetString(PyExc_RuntimeError,
136 "Called get_data_for_bus before calling process().");
137 return nullptr;
138 }
139
140 for (const auto &channel : tools->channel_data) {
141 if (channel.name == name && channel.type == type) {
142 PyObject *list = PyList_New(channel.messages.size());
143 for (size_t ii = 0; ii < channel.messages.size(); ++ii)
144 {
145 const auto &message = channel.messages[ii];
146 PyObject *monotonic_time = PyLong_FromLongLong(
147 std::chrono::duration_cast<std::chrono::nanoseconds>(
148 message.monotonic_sent_time.time_since_epoch())
149 .count());
150 PyObject *realtime_time = PyLong_FromLongLong(
151 std::chrono::duration_cast<std::chrono::nanoseconds>(
152 message.realtime_sent_time.time_since_epoch())
153 .count());
154 PyObject *json_data = PyUnicode_FromStringAndSize(
155 message.json_data.data(), message.json_data.size());
156 PyObject *entry =
157 PyTuple_Pack(3, monotonic_time, realtime_time, json_data);
158 if (PyList_SetItem(list, ii, entry) != 0) {
159 return nullptr;
160 }
161 }
162 return list;
163 }
164 }
165 PyErr_SetString(PyExc_ValueError,
166 "The provided channel was never subscribed to.");
167 return nullptr;
168}
169
170PyObject *LogReader_subscribe(LogReaderType *self, PyObject *args,
171 PyObject *kwds) {
172 const char *kwlist[] = {"name", "type", nullptr};
173
174 const char *name;
175 const char *type;
176 if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss",
177 const_cast<char **>(kwlist), &name, &type)) {
178 return nullptr;
179 }
180
181 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
182
183 if (tools->processed) {
184 PyErr_SetString(PyExc_RuntimeError,
185 "Called subscribe after calling process().");
186 return nullptr;
187 }
188
189 const aos::Channel *const channel = aos::configuration::GetChannel(
190 tools->reader->configuration(), name, type, "", nullptr);
191 if (channel == nullptr) {
192 return Py_False;
193 }
194 const int index = tools->channel_data.size();
195 tools->channel_data.push_back(
196 {.name = name, .type = type, .messages = {}});
197 tools->event_loop->MakeRawWatcher(
198 channel, [channel, index, tools](const aos::Context &context,
199 const void *message) {
200 tools->channel_data[index].messages.push_back(
201 {.monotonic_sent_time = context.monotonic_event_time,
202 .realtime_sent_time = context.realtime_event_time,
203 .json_data = aos::FlatbufferToJson(
204 channel->schema(), static_cast<const uint8_t *>(message))});
205 });
206 return Py_True;
207}
208
209static PyObject *LogReader_process(LogReaderType *self,
210 PyObject *Py_UNUSED(ignored)) {
211 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
212
213 if (tools->processed) {
214 PyErr_SetString(PyExc_RuntimeError, "process() may only be called once.");
215 return nullptr;
216 }
217
218 tools->processed = true;
219
James Kuszmaul84ff3e52020-01-03 19:48:53 -0800220 tools->reader->event_loop_factory()->Run();
James Kuszmaul7daef362019-12-31 18:28:17 -0800221
222 Py_RETURN_NONE;
223}
224
225static PyObject *LogReader_configuration(LogReaderType *self,
226 PyObject *Py_UNUSED(ignored)) {
227 LogReaderTools *tools = CHECK_NOTNULL(self->tools);
228
229 // I have no clue if the Configuration that we get from the log reader is in a
230 // contiguous chunk of memory, and I'm too lazy to either figure it out or
231 // figure out how to extract the actual data buffer + offset.
232 // Instead, copy the flatbuffer and return a copy of the new buffer.
233 aos::FlatbufferDetachedBuffer<aos::Configuration> buffer =
234 aos::CopyFlatBuffer(tools->reader->configuration());
235
236 return PyBytes_FromStringAndSize(
Austin Schuhadd6eb32020-11-09 21:24:26 -0800237 reinterpret_cast<const char *>(buffer.span().data()),
238 buffer.span().size());
James Kuszmaul7daef362019-12-31 18:28:17 -0800239}
240
241static PyMethodDef LogReader_methods[] = {
242 {"configuration", (PyCFunction)LogReader_configuration, METH_NOARGS,
243 "Return a bytes buffer for the Configuration of the logfile."},
244 {"process", (PyCFunction)LogReader_process, METH_NOARGS,
245 "Processes the logfile and all the subscribed to channels."},
246 {"subscribe", (PyCFunction)LogReader_subscribe,
247 METH_VARARGS | METH_KEYWORDS,
248 "Attempts to subscribe to the provided channel name + type. Returns True "
249 "if successful."},
250 {"get_data_for_channel", (PyCFunction)LogReader_get_data_for_channel,
251 METH_VARARGS | METH_KEYWORDS,
252 "Returns the logged data for a given channel. Raises an exception if you "
253 "did not subscribe to the provided channel. Returned data is a list of "
254 "tuples where each tuple is of the form (monotonic_nsec, realtime_nsec, "
255 "json_message_data)."},
256 {nullptr, 0, 0, nullptr} /* Sentinel */
257};
258
259static PyTypeObject LogReaderType = {
260 PyVarObject_HEAD_INIT(NULL, 0).tp_name = "py_log_reader.LogReader",
261 .tp_doc = "LogReader objects",
262 .tp_basicsize = sizeof(LogReaderType),
263 .tp_itemsize = 0,
264 .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
265 .tp_new = LogReader_new,
266 .tp_init = (initproc)LogReader_init,
267 .tp_dealloc = (destructor)LogReader_dealloc,
268 .tp_methods = LogReader_methods,
269};
270
271static PyModuleDef log_reader_module = {
272 PyModuleDef_HEAD_INIT,
273 .m_name = "py_log_reader",
274 .m_doc = "Example module that creates an extension type.",
275 .m_size = -1,
276};
277
278PyObject *InitModule() {
279 PyObject *m;
280 if (PyType_Ready(&LogReaderType) < 0) return nullptr;
281
282 m = PyModule_Create(&log_reader_module);
283 if (m == nullptr) return nullptr;
284
285 Py_INCREF(&LogReaderType);
286 if (PyModule_AddObject(m, "LogReader", (PyObject *)&LogReaderType) < 0) {
287 Py_DECREF(&LogReaderType);
288 Py_DECREF(m);
289 return nullptr;
290 }
291
292 return m;
293}
294
295} // namespace
296} // namespace analysis
297} // namespace frc971
298
299PyMODINIT_FUNC PyInit_py_log_reader(void) {
300 return frc971::analysis::InitModule();
301}