blob: 70096288daae4c352194f7e652a68d9112f81ec3 [file] [log] [blame]
James Kuszmaulcf324122023-01-14 14:07:17 -08001// Copyright (c) FIRST and other WPILib contributors.
2// Open Source Software; you can modify and/or share it under the terms of
3// the WPILib BSD license file in the root directory of this project.
4
5#include "wpi/DataLog.h"
6
7#include "wpi/Synchronization.h"
8
9#ifndef _WIN32
10#include <unistd.h>
11#endif
12
13#ifdef _WIN32
14#ifndef WIN32_LEAN_AND_MEAN
15#define WIN32_LEAN_AND_MEAN
16#endif
17
18#include <windows.h> // NOLINT(build/include_order)
19
20#endif
21
22#include <atomic>
23#include <cstdio>
24#include <cstdlib>
25#include <cstring>
26#include <random>
27#include <vector>
28
29#include "fmt/format.h"
30#include "wpi/Endian.h"
31#include "wpi/Logger.h"
32#include "wpi/MathExtras.h"
33#include "wpi/fs.h"
34#include "wpi/timestamp.h"
35
36using namespace wpi::log;
37
38static constexpr size_t kBlockSize = 16 * 1024;
39static constexpr size_t kRecordMaxHeaderSize = 17;
40
41template <typename T>
42static unsigned int WriteVarInt(uint8_t* buf, T val) {
43 unsigned int len = 0;
44 do {
45 *buf++ = static_cast<unsigned int>(val) & 0xff;
46 ++len;
47 val >>= 8;
48 } while (val != 0);
49 return len;
50}
51
52// min size: 4, max size: 17
53static unsigned int WriteRecordHeader(uint8_t* buf, uint32_t entry,
54 uint64_t timestamp,
55 uint32_t payloadSize) {
56 uint8_t* origbuf = buf++;
57
58 unsigned int entryLen = WriteVarInt(buf, entry);
59 buf += entryLen;
60 unsigned int payloadLen = WriteVarInt(buf, payloadSize);
61 buf += payloadLen;
62 unsigned int timestampLen =
63 WriteVarInt(buf, timestamp == 0 ? wpi::Now() : timestamp);
64 buf += timestampLen;
65 *origbuf =
66 ((timestampLen - 1) << 4) | ((payloadLen - 1) << 2) | (entryLen - 1);
67 return buf - origbuf;
68}
69
70class DataLog::Buffer {
71 public:
72 explicit Buffer(size_t alloc = kBlockSize)
73 : m_buf{new uint8_t[alloc]}, m_maxLen{alloc} {}
74 ~Buffer() { delete[] m_buf; }
75
76 Buffer(const Buffer&) = delete;
77 Buffer& operator=(const Buffer&) = delete;
78
79 Buffer(Buffer&& oth)
80 : m_buf{oth.m_buf}, m_len{oth.m_len}, m_maxLen{oth.m_maxLen} {
81 oth.m_buf = nullptr;
82 oth.m_len = 0;
83 oth.m_maxLen = 0;
84 }
85
86 Buffer& operator=(Buffer&& oth) {
87 if (m_buf) {
88 delete[] m_buf;
89 }
90 m_buf = oth.m_buf;
91 m_len = oth.m_len;
92 m_maxLen = oth.m_maxLen;
93 oth.m_buf = nullptr;
94 oth.m_len = 0;
95 oth.m_maxLen = 0;
96 return *this;
97 }
98
99 uint8_t* Reserve(size_t size) {
100 assert(size <= GetRemaining());
101 uint8_t* rv = m_buf + m_len;
102 m_len += size;
103 return rv;
104 }
105
106 void Unreserve(size_t size) { m_len -= size; }
107
108 void Clear() { m_len = 0; }
109
110 size_t GetRemaining() const { return m_maxLen - m_len; }
111
112 std::span<uint8_t> GetData() { return {m_buf, m_len}; }
113 std::span<const uint8_t> GetData() const { return {m_buf, m_len}; }
114
115 private:
116 uint8_t* m_buf;
117 size_t m_len = 0;
118 size_t m_maxLen;
119};
120
121static void DefaultLog(unsigned int level, const char* file, unsigned int line,
122 const char* msg) {
123 if (level > wpi::WPI_LOG_INFO) {
124 fmt::print(stderr, "DataLog: {}\n", msg);
125 } else if (level == wpi::WPI_LOG_INFO) {
126 fmt::print("DataLog: {}\n", msg);
127 }
128}
129
130static wpi::Logger defaultMessageLog{DefaultLog};
131
132DataLog::DataLog(std::string_view dir, std::string_view filename, double period,
133 std::string_view extraHeader)
134 : DataLog{defaultMessageLog, dir, filename, period, extraHeader} {}
135
136DataLog::DataLog(wpi::Logger& msglog, std::string_view dir,
137 std::string_view filename, double period,
138 std::string_view extraHeader)
139 : m_msglog{msglog},
140 m_period{period},
141 m_extraHeader{extraHeader},
142 m_newFilename{filename},
143 m_thread{[this, dir = std::string{dir}] { WriterThreadMain(dir); }} {}
144
145DataLog::DataLog(std::function<void(std::span<const uint8_t> data)> write,
146 double period, std::string_view extraHeader)
147 : DataLog{defaultMessageLog, std::move(write), period, extraHeader} {}
148
149DataLog::DataLog(wpi::Logger& msglog,
150 std::function<void(std::span<const uint8_t> data)> write,
151 double period, std::string_view extraHeader)
152 : m_msglog{msglog},
153 m_period{period},
154 m_extraHeader{extraHeader},
155 m_thread{[this, write = std::move(write)] {
156 WriterThreadMain(std::move(write));
157 }} {}
158
159DataLog::~DataLog() {
160 {
161 std::scoped_lock lock{m_mutex};
162 m_active = false;
163 m_doFlush = true;
164 }
165 m_cond.notify_all();
166 m_thread.join();
167}
168
169void DataLog::SetFilename(std::string_view filename) {
170 {
171 std::scoped_lock lock{m_mutex};
172 m_newFilename = filename;
173 }
174 m_cond.notify_all();
175}
176
177void DataLog::Flush() {
178 {
179 std::scoped_lock lock{m_mutex};
180 m_doFlush = true;
181 }
182 m_cond.notify_all();
183}
184
185void DataLog::Pause() {
186 std::scoped_lock lock{m_mutex};
187 m_paused = true;
188}
189
190void DataLog::Resume() {
191 std::scoped_lock lock{m_mutex};
192 m_paused = false;
193}
194
195static void WriteToFile(fs::file_t f, std::span<const uint8_t> data,
196 std::string_view filename, wpi::Logger& msglog) {
197 do {
198#ifdef _WIN32
199 DWORD ret;
200 if (!WriteFile(f, data.data(), data.size(), &ret, nullptr)) {
201 WPI_ERROR(msglog, "Error writing to log file '{}': {}", filename,
202 GetLastError());
203 break;
204 }
205#else
206 ssize_t ret = ::write(f, data.data(), data.size());
207 if (ret < 0) {
208 // If it's a recoverable error, swallow it and retry the write
209 if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) {
210 continue;
211 }
212
213 // Otherwise it's a non-recoverable error; quit trying
214 WPI_ERROR(msglog, "Error writing to log file '{}': {}", filename,
215 std::strerror(errno));
216 break;
217 }
218#endif
219
220 // The write may have written some or all of the data
221 data = data.subspan(ret);
222 } while (data.size() > 0);
223}
224
225static std::string MakeRandomFilename() {
226 // build random filename
227 static std::random_device dev;
228 static std::mt19937 rng(dev());
229 std::uniform_int_distribution<int> dist(0, 15);
230 const char* v = "0123456789abcdef";
231 std::string filename = "wpilog_";
232 for (int i = 0; i < 16; i++) {
233 filename += v[dist(rng)];
234 }
235 filename += ".wpilog";
236 return filename;
237}
238
239void DataLog::WriterThreadMain(std::string_view dir) {
240 std::chrono::duration<double> periodTime{m_period};
241
242 std::error_code ec;
243 fs::path dirPath{dir};
244 std::string filename;
245
246 {
247 std::scoped_lock lock{m_mutex};
248 filename = std::move(m_newFilename);
249 m_newFilename.clear();
250 }
251
252 if (filename.empty()) {
253 filename = MakeRandomFilename();
254 }
255
256 // try preferred filename, or randomize it a few times, before giving up
257 fs::file_t f;
258 for (int i = 0; i < 5; ++i) {
259 // open file for append
260#ifdef _WIN32
261 // WIN32 doesn't allow combination of CreateNew and Append
262 f = fs::OpenFileForWrite(dirPath / filename, ec, fs::CD_CreateNew,
263 fs::OF_None);
264#else
265 f = fs::OpenFileForWrite(dirPath / filename, ec, fs::CD_CreateNew,
266 fs::OF_Append);
267#endif
268 if (ec) {
269 WPI_ERROR(m_msglog, "Could not open log file '{}': {}",
270 (dirPath / filename).string(), ec.message());
271 // try again with random filename
272 filename = MakeRandomFilename();
273 } else {
274 break;
275 }
276 }
277
278 if (f == fs::kInvalidFile) {
279 WPI_ERROR(m_msglog, "Could not open log file, no log being saved");
280 } else {
281 WPI_INFO(m_msglog, "Logging to '{}'", (dirPath / filename).string());
282 }
283
284 // write header (version 1.0)
285 if (f != fs::kInvalidFile) {
286 const uint8_t header[] = {'W', 'P', 'I', 'L', 'O', 'G', 0, 1};
287 WriteToFile(f, header, filename, m_msglog);
288 uint8_t extraLen[4];
289 support::endian::write32le(extraLen, m_extraHeader.size());
290 WriteToFile(f, extraLen, filename, m_msglog);
291 if (m_extraHeader.size() > 0) {
292 WriteToFile(f,
293 {reinterpret_cast<const uint8_t*>(m_extraHeader.data()),
294 m_extraHeader.size()},
295 filename, m_msglog);
296 }
297 }
298
299 std::vector<Buffer> toWrite;
300
301 std::unique_lock lock{m_mutex};
302 while (m_active) {
303 bool doFlush = false;
304 auto timeoutTime = std::chrono::steady_clock::now() + periodTime;
305 if (m_cond.wait_until(lock, timeoutTime) == std::cv_status::timeout) {
306 doFlush = true;
307 }
308
309 if (!m_newFilename.empty()) {
310 auto newFilename = std::move(m_newFilename);
311 m_newFilename.clear();
312 lock.unlock();
313 // rename
314 if (filename != newFilename) {
315 fs::rename(dirPath / filename, dirPath / newFilename, ec);
316 }
317 if (ec) {
318 WPI_ERROR(m_msglog, "Could not rename log file from '{}' to '{}': {}",
319 filename, newFilename, ec.message());
320 } else {
321 WPI_INFO(m_msglog, "Renamed log file from '{}' to '{}'", filename,
322 newFilename);
323 }
324 filename = std::move(newFilename);
325 lock.lock();
326 }
327
328 if (doFlush || m_doFlush) {
329 // flush to file
330 m_doFlush = false;
331 if (m_outgoing.empty()) {
332 continue;
333 }
334 // swap outgoing with empty vector
335 toWrite.swap(m_outgoing);
336
337 if (f != fs::kInvalidFile) {
338 lock.unlock();
339 // write buffers to file
340 for (auto&& buf : toWrite) {
341 WriteToFile(f, buf.GetData(), filename, m_msglog);
342 }
343
344 // sync to storage
345#if defined(__linux__)
346 ::fdatasync(f);
347#elif defined(__APPLE__)
348 ::fsync(f);
349#endif
350 lock.lock();
351 }
352
353 // release buffers back to free list
354 for (auto&& buf : toWrite) {
355 buf.Clear();
356 m_free.emplace_back(std::move(buf));
357 }
358 toWrite.resize(0);
359 }
360 }
361
362 if (f != fs::kInvalidFile) {
363 fs::CloseFile(f);
364 }
365}
366
367void DataLog::WriterThreadMain(
368 std::function<void(std::span<const uint8_t> data)> write) {
369 std::chrono::duration<double> periodTime{m_period};
370
371 // write header (version 1.0)
372 {
373 const uint8_t header[] = {'W', 'P', 'I', 'L', 'O', 'G', 0, 1};
374 write(header);
375 uint8_t extraLen[4];
376 support::endian::write32le(extraLen, m_extraHeader.size());
377 write(extraLen);
378 if (m_extraHeader.size() > 0) {
379 write({reinterpret_cast<const uint8_t*>(m_extraHeader.data()),
380 m_extraHeader.size()});
381 }
382 }
383
384 std::vector<Buffer> toWrite;
385
386 std::unique_lock lock{m_mutex};
387 while (m_active) {
388 bool doFlush = false;
389 auto timeoutTime = std::chrono::steady_clock::now() + periodTime;
390 if (m_cond.wait_until(lock, timeoutTime) == std::cv_status::timeout) {
391 doFlush = true;
392 }
393
394 if (doFlush || m_doFlush) {
395 // flush to file
396 m_doFlush = false;
397 if (m_outgoing.empty()) {
398 continue;
399 }
400 // swap outgoing with empty vector
401 toWrite.swap(m_outgoing);
402
403 lock.unlock();
404 // write buffers
405 for (auto&& buf : toWrite) {
406 if (!buf.GetData().empty()) {
407 write(buf.GetData());
408 }
409 }
410 lock.lock();
411
412 // release buffers back to free list
413 for (auto&& buf : toWrite) {
414 buf.Clear();
415 m_free.emplace_back(std::move(buf));
416 }
417 toWrite.resize(0);
418 }
419 }
420
421 write({}); // indicate EOF
422}
423
424// Control records use the following format:
425// 1-byte type
426// 4-byte entry
427// rest of data (depending on type)
428
429int DataLog::Start(std::string_view name, std::string_view type,
430 std::string_view metadata, int64_t timestamp) {
431 std::scoped_lock lock{m_mutex};
432 auto& entryInfo = m_entries[name];
433 if (entryInfo.id == 0) {
434 entryInfo.id = ++m_lastId;
435 }
436 auto& savedCount = m_entryCounts[entryInfo.id];
437 ++savedCount;
438 if (savedCount > 1) {
439 if (entryInfo.type != type) {
440 WPI_ERROR(m_msglog,
441 "type mismatch for '{}': was '{}', requested '{}'; ignoring",
442 name, entryInfo.type, type);
443 return 0;
444 }
445 return entryInfo.id;
446 }
447 entryInfo.type = type;
448 size_t strsize = name.size() + type.size() + metadata.size();
449 uint8_t* buf = StartRecord(0, timestamp, 5 + 12 + strsize, 5);
450 *buf++ = impl::kControlStart;
451 wpi::support::endian::write32le(buf, entryInfo.id);
452 AppendStringImpl(name);
453 AppendStringImpl(type);
454 AppendStringImpl(metadata);
455
456 return entryInfo.id;
457}
458
459void DataLog::Finish(int entry, int64_t timestamp) {
460 if (entry <= 0) {
461 return;
462 }
463 std::scoped_lock lock{m_mutex};
464 auto& savedCount = m_entryCounts[entry];
465 if (savedCount == 0) {
466 return;
467 }
468 --savedCount;
469 if (savedCount != 0) {
470 return;
471 }
472 m_entryCounts.erase(entry);
473 uint8_t* buf = StartRecord(0, timestamp, 5, 5);
474 *buf++ = impl::kControlFinish;
475 wpi::support::endian::write32le(buf, entry);
476}
477
478void DataLog::SetMetadata(int entry, std::string_view metadata,
479 int64_t timestamp) {
480 if (entry <= 0) {
481 return;
482 }
483 std::scoped_lock lock{m_mutex};
484 uint8_t* buf = StartRecord(0, timestamp, 5 + 4 + metadata.size(), 5);
485 *buf++ = impl::kControlSetMetadata;
486 wpi::support::endian::write32le(buf, entry);
487 AppendStringImpl(metadata);
488}
489
490uint8_t* DataLog::Reserve(size_t size) {
491 assert(size <= kBlockSize);
492 if (m_outgoing.empty() || size > m_outgoing.back().GetRemaining()) {
493 if (m_free.empty()) {
494 m_outgoing.emplace_back();
495 } else {
496 m_outgoing.emplace_back(std::move(m_free.back()));
497 m_free.pop_back();
498 }
499 }
500 return m_outgoing.back().Reserve(size);
501}
502
503uint8_t* DataLog::StartRecord(uint32_t entry, uint64_t timestamp,
504 uint32_t payloadSize, size_t reserveSize) {
505 uint8_t* buf = Reserve(kRecordMaxHeaderSize + reserveSize);
506 auto headerLen = WriteRecordHeader(buf, entry, timestamp, payloadSize);
507 m_outgoing.back().Unreserve(kRecordMaxHeaderSize - headerLen);
508 buf += headerLen;
509 return buf;
510}
511
512void DataLog::AppendImpl(std::span<const uint8_t> data) {
513 while (data.size() > kBlockSize) {
514 uint8_t* buf = Reserve(kBlockSize);
515 std::memcpy(buf, data.data(), kBlockSize);
516 data = data.subspan(kBlockSize);
517 }
518 uint8_t* buf = Reserve(data.size());
519 std::memcpy(buf, data.data(), data.size());
520}
521
522void DataLog::AppendStringImpl(std::string_view str) {
523 uint8_t* buf = Reserve(4);
524 wpi::support::endian::write32le(buf, str.size());
525 AppendImpl({reinterpret_cast<const uint8_t*>(str.data()), str.size()});
526}
527
528void DataLog::AppendRaw(int entry, std::span<const uint8_t> data,
529 int64_t timestamp) {
530 if (entry <= 0) {
531 return;
532 }
533 std::scoped_lock lock{m_mutex};
534 if (m_paused) {
535 return;
536 }
537 StartRecord(entry, timestamp, data.size(), 0);
538 AppendImpl(data);
539}
540
541void DataLog::AppendRaw2(int entry,
542 std::span<const std::span<const uint8_t>> data,
543 int64_t timestamp) {
544 if (entry <= 0) {
545 return;
546 }
547 std::scoped_lock lock{m_mutex};
548 if (m_paused) {
549 return;
550 }
551 size_t size = 0;
552 for (auto&& chunk : data) {
553 size += chunk.size();
554 }
555 StartRecord(entry, timestamp, size, 0);
556 for (auto chunk : data) {
557 AppendImpl(chunk);
558 }
559}
560
561void DataLog::AppendBoolean(int entry, bool value, int64_t timestamp) {
562 if (entry <= 0) {
563 return;
564 }
565 std::scoped_lock lock{m_mutex};
566 if (m_paused) {
567 return;
568 }
569 uint8_t* buf = StartRecord(entry, timestamp, 1, 1);
570 buf[0] = value ? 1 : 0;
571}
572
573void DataLog::AppendInteger(int entry, int64_t value, int64_t timestamp) {
574 if (entry <= 0) {
575 return;
576 }
577 std::scoped_lock lock{m_mutex};
578 if (m_paused) {
579 return;
580 }
581 uint8_t* buf = StartRecord(entry, timestamp, 8, 8);
582 wpi::support::endian::write64le(buf, value);
583}
584
585void DataLog::AppendFloat(int entry, float value, int64_t timestamp) {
586 if (entry <= 0) {
587 return;
588 }
589 std::scoped_lock lock{m_mutex};
590 if (m_paused) {
591 return;
592 }
593 uint8_t* buf = StartRecord(entry, timestamp, 4, 4);
594 if constexpr (wpi::support::endian::system_endianness() ==
595 wpi::support::little) {
596 std::memcpy(buf, &value, 4);
597 } else {
598 wpi::support::endian::write32le(buf, wpi::FloatToBits(value));
599 }
600}
601
602void DataLog::AppendDouble(int entry, double value, int64_t timestamp) {
603 if (entry <= 0) {
604 return;
605 }
606 std::scoped_lock lock{m_mutex};
607 if (m_paused) {
608 return;
609 }
610 uint8_t* buf = StartRecord(entry, timestamp, 8, 8);
611 if constexpr (wpi::support::endian::system_endianness() ==
612 wpi::support::little) {
613 std::memcpy(buf, &value, 8);
614 } else {
615 wpi::support::endian::write64le(buf, wpi::DoubleToBits(value));
616 }
617}
618
619void DataLog::AppendString(int entry, std::string_view value,
620 int64_t timestamp) {
621 AppendRaw(entry,
622 {reinterpret_cast<const uint8_t*>(value.data()), value.size()},
623 timestamp);
624}
625
626void DataLog::AppendBooleanArray(int entry, std::span<const bool> arr,
627 int64_t timestamp) {
628 if (entry <= 0) {
629 return;
630 }
631 std::scoped_lock lock{m_mutex};
632 if (m_paused) {
633 return;
634 }
635 StartRecord(entry, timestamp, arr.size(), 0);
636 uint8_t* buf;
637 while (arr.size() > kBlockSize) {
638 buf = Reserve(kBlockSize);
639 for (auto val : arr.subspan(0, kBlockSize)) {
640 *buf++ = val ? 1 : 0;
641 }
642 arr = arr.subspan(kBlockSize);
643 }
644 buf = Reserve(arr.size());
645 for (auto val : arr) {
646 *buf++ = val ? 1 : 0;
647 }
648}
649
650void DataLog::AppendBooleanArray(int entry, std::span<const int> arr,
651 int64_t timestamp) {
652 if (entry <= 0) {
653 return;
654 }
655 std::scoped_lock lock{m_mutex};
656 if (m_paused) {
657 return;
658 }
659 StartRecord(entry, timestamp, arr.size(), 0);
660 uint8_t* buf;
661 while (arr.size() > kBlockSize) {
662 buf = Reserve(kBlockSize);
663 for (auto val : arr.subspan(0, kBlockSize)) {
664 *buf++ = val & 1;
665 }
666 arr = arr.subspan(kBlockSize);
667 }
668 buf = Reserve(arr.size());
669 for (auto val : arr) {
670 *buf++ = val & 1;
671 }
672}
673
674void DataLog::AppendBooleanArray(int entry, std::span<const uint8_t> arr,
675 int64_t timestamp) {
676 AppendRaw(entry, arr, timestamp);
677}
678
679void DataLog::AppendIntegerArray(int entry, std::span<const int64_t> arr,
680 int64_t timestamp) {
681 if constexpr (wpi::support::endian::system_endianness() ==
682 wpi::support::little) {
683 AppendRaw(entry,
684 {reinterpret_cast<const uint8_t*>(arr.data()), arr.size() * 8},
685 timestamp);
686 } else {
687 if (entry <= 0) {
688 return;
689 }
690 std::scoped_lock lock{m_mutex};
691 if (m_paused) {
692 return;
693 }
694 StartRecord(entry, timestamp, arr.size() * 8, 0);
695 uint8_t* buf;
696 while ((arr.size() * 8) > kBlockSize) {
697 buf = Reserve(kBlockSize);
698 for (auto val : arr.subspan(0, kBlockSize / 8)) {
699 wpi::support::endian::write64le(buf, val);
700 buf += 8;
701 }
702 arr = arr.subspan(kBlockSize / 8);
703 }
704 buf = Reserve(arr.size() * 8);
705 for (auto val : arr) {
706 wpi::support::endian::write64le(buf, val);
707 buf += 8;
708 }
709 }
710}
711
712void DataLog::AppendFloatArray(int entry, std::span<const float> arr,
713 int64_t timestamp) {
714 if constexpr (wpi::support::endian::system_endianness() ==
715 wpi::support::little) {
716 AppendRaw(entry,
717 {reinterpret_cast<const uint8_t*>(arr.data()), arr.size() * 4},
718 timestamp);
719 } else {
720 if (entry <= 0) {
721 return;
722 }
723 std::scoped_lock lock{m_mutex};
724 if (m_paused) {
725 return;
726 }
727 StartRecord(entry, timestamp, arr.size() * 4, 0);
728 uint8_t* buf;
729 while ((arr.size() * 4) > kBlockSize) {
730 buf = Reserve(kBlockSize);
731 for (auto val : arr.subspan(0, kBlockSize / 4)) {
732 wpi::support::endian::write32le(buf, wpi::FloatToBits(val));
733 buf += 4;
734 }
735 arr = arr.subspan(kBlockSize / 4);
736 }
737 buf = Reserve(arr.size() * 4);
738 for (auto val : arr) {
739 wpi::support::endian::write32le(buf, wpi::FloatToBits(val));
740 buf += 4;
741 }
742 }
743}
744
745void DataLog::AppendDoubleArray(int entry, std::span<const double> arr,
746 int64_t timestamp) {
747 if constexpr (wpi::support::endian::system_endianness() ==
748 wpi::support::little) {
749 AppendRaw(entry,
750 {reinterpret_cast<const uint8_t*>(arr.data()), arr.size() * 8},
751 timestamp);
752 } else {
753 if (entry <= 0) {
754 return;
755 }
756 std::scoped_lock lock{m_mutex};
757 if (m_paused) {
758 return;
759 }
760 StartRecord(entry, timestamp, arr.size() * 8, 0);
761 uint8_t* buf;
762 while ((arr.size() * 8) > kBlockSize) {
763 buf = Reserve(kBlockSize);
764 for (auto val : arr.subspan(0, kBlockSize / 8)) {
765 wpi::support::endian::write64le(buf, wpi::DoubleToBits(val));
766 buf += 8;
767 }
768 arr = arr.subspan(kBlockSize / 8);
769 }
770 buf = Reserve(arr.size() * 8);
771 for (auto val : arr) {
772 wpi::support::endian::write64le(buf, wpi::DoubleToBits(val));
773 buf += 8;
774 }
775 }
776}
777
778void DataLog::AppendStringArray(int entry, std::span<const std::string> arr,
779 int64_t timestamp) {
780 if (entry <= 0) {
781 return;
782 }
783 // storage: 4-byte array length, each string prefixed by 4-byte length
784 // calculate total size
785 size_t size = 4;
786 for (auto&& str : arr) {
787 size += 4 + str.size();
788 }
789 std::scoped_lock lock{m_mutex};
790 if (m_paused) {
791 return;
792 }
793 uint8_t* buf = StartRecord(entry, timestamp, size, 4);
794 wpi::support::endian::write32le(buf, arr.size());
795 for (auto&& str : arr) {
796 AppendStringImpl(str);
797 }
798}
799
800void DataLog::AppendStringArray(int entry,
801 std::span<const std::string_view> arr,
802 int64_t timestamp) {
803 if (entry <= 0) {
804 return;
805 }
806 // storage: 4-byte array length, each string prefixed by 4-byte length
807 // calculate total size
808 size_t size = 4;
809 for (auto&& str : arr) {
810 size += 4 + str.size();
811 }
812 std::scoped_lock lock{m_mutex};
813 if (m_paused) {
814 return;
815 }
816 uint8_t* buf = StartRecord(entry, timestamp, size, 4);
817 wpi::support::endian::write32le(buf, arr.size());
818 for (auto sv : arr) {
819 AppendStringImpl(sv);
820 }
821}