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