[wpiutil] Add high speed data logging

This commit is contained in:
Peter Johnson
2020-03-21 20:43:34 -07:00
parent 5a89575b3a
commit 9b500df0d9
31 changed files with 4963 additions and 19 deletions

View File

@@ -0,0 +1,821 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include "wpi/DataLog.h"
#include "wpi/Synchronization.h"
#ifndef _WIN32
#include <unistd.h>
#endif
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h> // NOLINT(build/include_order)
#endif
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <random>
#include <vector>
#include "fmt/format.h"
#include "wpi/Endian.h"
#include "wpi/Logger.h"
#include "wpi/MathExtras.h"
#include "wpi/fs.h"
#include "wpi/timestamp.h"
using namespace wpi::log;
static constexpr size_t kBlockSize = 16 * 1024;
static constexpr size_t kRecordMaxHeaderSize = 17;
template <typename T>
static unsigned int WriteVarInt(uint8_t* buf, T val) {
unsigned int len = 0;
do {
*buf++ = static_cast<unsigned int>(val) & 0xff;
++len;
val >>= 8;
} while (val != 0);
return len;
}
// min size: 4, max size: 17
static unsigned int WriteRecordHeader(uint8_t* buf, uint32_t entry,
uint64_t timestamp,
uint32_t payloadSize) {
uint8_t* origbuf = buf++;
unsigned int entryLen = WriteVarInt(buf, entry);
buf += entryLen;
unsigned int payloadLen = WriteVarInt(buf, payloadSize);
buf += payloadLen;
unsigned int timestampLen =
WriteVarInt(buf, timestamp == 0 ? wpi::Now() : timestamp);
buf += timestampLen;
*origbuf =
((timestampLen - 1) << 4) | ((payloadLen - 1) << 2) | (entryLen - 1);
return buf - origbuf;
}
class DataLog::Buffer {
public:
explicit Buffer(size_t alloc = kBlockSize)
: m_buf{new uint8_t[alloc]}, m_maxLen{alloc} {}
~Buffer() { delete[] m_buf; }
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
Buffer(Buffer&& oth)
: m_buf{oth.m_buf}, m_len{oth.m_len}, m_maxLen{oth.m_maxLen} {
oth.m_buf = nullptr;
oth.m_len = 0;
oth.m_maxLen = 0;
}
Buffer& operator=(Buffer&& oth) {
if (m_buf) {
delete[] m_buf;
}
m_buf = oth.m_buf;
m_len = oth.m_len;
m_maxLen = oth.m_maxLen;
oth.m_buf = nullptr;
oth.m_len = 0;
oth.m_maxLen = 0;
return *this;
}
uint8_t* Reserve(size_t size) {
assert(size <= GetRemaining());
uint8_t* rv = m_buf + m_len;
m_len += size;
return rv;
}
void Unreserve(size_t size) { m_len -= size; }
void Clear() { m_len = 0; }
size_t GetRemaining() const { return m_maxLen - m_len; }
wpi::span<uint8_t> GetData() { return {m_buf, m_len}; }
wpi::span<const uint8_t> GetData() const { return {m_buf, m_len}; }
private:
uint8_t* m_buf;
size_t m_len = 0;
size_t m_maxLen;
};
static void DefaultLog(unsigned int level, const char* file, unsigned int line,
const char* msg) {
if (level > wpi::WPI_LOG_INFO) {
fmt::print(stderr, "DataLog: {}\n", msg);
} else if (level == wpi::WPI_LOG_INFO) {
fmt::print("DataLog: {}\n", msg);
}
}
static wpi::Logger defaultMessageLog{DefaultLog};
DataLog::DataLog(std::string_view dir, std::string_view filename, double period,
std::string_view extraHeader)
: DataLog{defaultMessageLog, dir, filename, period, extraHeader} {}
DataLog::DataLog(wpi::Logger& msglog, std::string_view dir,
std::string_view filename, double period,
std::string_view extraHeader)
: m_msglog{msglog},
m_period{period},
m_extraHeader{extraHeader},
m_newFilename{filename},
m_thread{[this, dir = std::string{dir}] { WriterThreadMain(dir); }} {}
DataLog::DataLog(std::function<void(wpi::span<const uint8_t> data)> write,
double period, std::string_view extraHeader)
: DataLog{defaultMessageLog, std::move(write), period, extraHeader} {}
DataLog::DataLog(wpi::Logger& msglog,
std::function<void(wpi::span<const uint8_t> data)> write,
double period, std::string_view extraHeader)
: m_msglog{msglog},
m_period{period},
m_extraHeader{extraHeader},
m_thread{[this, write = std::move(write)] {
WriterThreadMain(std::move(write));
}} {}
DataLog::~DataLog() {
{
std::scoped_lock lock{m_mutex};
m_active = false;
m_doFlush = true;
}
m_cond.notify_all();
m_thread.join();
}
void DataLog::SetFilename(std::string_view filename) {
{
std::scoped_lock lock{m_mutex};
m_newFilename = filename;
}
m_cond.notify_all();
}
void DataLog::Flush() {
{
std::scoped_lock lock{m_mutex};
m_doFlush = true;
}
m_cond.notify_all();
}
void DataLog::Pause() {
std::scoped_lock lock{m_mutex};
m_paused = true;
}
void DataLog::Resume() {
std::scoped_lock lock{m_mutex};
m_paused = false;
}
static void WriteToFile(fs::file_t f, wpi::span<const uint8_t> data,
std::string_view filename, wpi::Logger& msglog) {
do {
#ifdef _WIN32
DWORD ret;
if (!WriteFile(f, data.data(), data.size(), &ret, nullptr)) {
WPI_ERROR(msglog, "Error writing to log file '{}': {}", filename,
GetLastError());
break;
}
#else
ssize_t ret = ::write(f, data.data(), data.size());
if (ret < 0) {
// If it's a recoverable error, swallow it and retry the write
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) {
continue;
}
// Otherwise it's a non-recoverable error; quit trying
WPI_ERROR(msglog, "Error writing to log file '{}': {}", filename,
std::strerror(errno));
break;
}
#endif
// The write may have written some or all of the data
data = data.subspan(ret);
} while (data.size() > 0);
}
static std::string MakeRandomFilename() {
// build random filename
static std::random_device dev;
static std::mt19937 rng(dev());
std::uniform_int_distribution<int> dist(0, 15);
const char* v = "0123456789abcdef";
std::string filename = "wpilog_";
for (int i = 0; i < 16; i++) {
filename += v[dist(rng)];
}
filename += ".wpilog";
return filename;
}
void DataLog::WriterThreadMain(std::string_view dir) {
std::chrono::duration<double> periodTime{m_period};
std::error_code ec;
fs::path dirPath{dir};
std::string filename;
{
std::scoped_lock lock{m_mutex};
filename = std::move(m_newFilename);
m_newFilename.clear();
}
if (filename.empty()) {
filename = MakeRandomFilename();
}
// try preferred filename, or randomize it a few times, before giving up
fs::file_t f;
for (int i = 0; i < 5; ++i) {
// open file for append
#ifdef _WIN32
// WIN32 doesn't allow combination of CreateNew and Append
f = fs::OpenFileForWrite(dirPath / filename, ec, fs::CD_CreateNew,
fs::OF_None);
#else
f = fs::OpenFileForWrite(dirPath / filename, ec, fs::CD_CreateNew,
fs::OF_Append);
#endif
if (ec) {
WPI_ERROR(m_msglog, "Could not open log file '{}': {}",
(dirPath / filename).string(), ec.message());
// try again with random filename
filename = MakeRandomFilename();
} else {
break;
}
}
if (f == fs::kInvalidFile) {
WPI_ERROR(m_msglog, "{}", "Could not open log file, no log being saved");
} else {
WPI_INFO(m_msglog, "Logging to '{}'", (dirPath / filename).string());
}
// write header (version 1.0)
if (f != fs::kInvalidFile) {
const uint8_t header[] = {'W', 'P', 'I', 'L', 'O', 'G', 0, 1};
WriteToFile(f, header, filename, m_msglog);
uint8_t extraLen[4];
support::endian::write32le(extraLen, m_extraHeader.size());
WriteToFile(f, extraLen, filename, m_msglog);
if (m_extraHeader.size() > 0) {
WriteToFile(f,
{reinterpret_cast<const uint8_t*>(m_extraHeader.data()),
m_extraHeader.size()},
filename, m_msglog);
}
}
std::vector<Buffer> toWrite;
std::unique_lock lock{m_mutex};
while (m_active) {
bool doFlush = false;
auto timeoutTime = std::chrono::steady_clock::now() + periodTime;
if (m_cond.wait_until(lock, timeoutTime) == std::cv_status::timeout) {
doFlush = true;
}
if (!m_newFilename.empty()) {
auto newFilename = std::move(m_newFilename);
m_newFilename.clear();
lock.unlock();
// rename
if (filename != newFilename) {
fs::rename(dirPath / filename, dirPath / newFilename, ec);
}
if (ec) {
WPI_ERROR(m_msglog, "Could not rename log file from '{}' to '{}': {}",
filename, newFilename, ec.message());
} else {
WPI_INFO(m_msglog, "Renamed log file from '{}' to '{}'", filename,
newFilename);
}
filename = std::move(newFilename);
lock.lock();
}
if (doFlush || m_doFlush) {
// flush to file
m_doFlush = false;
if (m_outgoing.empty()) {
continue;
}
// swap outgoing with empty vector
toWrite.swap(m_outgoing);
if (f != fs::kInvalidFile) {
lock.unlock();
// write buffers to file
for (auto&& buf : toWrite) {
WriteToFile(f, buf.GetData(), filename, m_msglog);
}
// sync to storage
#if defined(__linux__)
::fdatasync(f);
#elif defined(__APPLE__)
::fsync(f);
#endif
lock.lock();
}
// release buffers back to free list
for (auto&& buf : toWrite) {
buf.Clear();
m_free.emplace_back(std::move(buf));
}
toWrite.resize(0);
}
}
if (f != fs::kInvalidFile) {
fs::CloseFile(f);
}
}
void DataLog::WriterThreadMain(
std::function<void(wpi::span<const uint8_t> data)> write) {
std::chrono::duration<double> periodTime{m_period};
// write header (version 1.0)
{
const uint8_t header[] = {'W', 'P', 'I', 'L', 'O', 'G', 0, 1};
write(header);
uint8_t extraLen[4];
support::endian::write32le(extraLen, m_extraHeader.size());
write(extraLen);
if (m_extraHeader.size() > 0) {
write({reinterpret_cast<const uint8_t*>(m_extraHeader.data()),
m_extraHeader.size()});
}
}
std::vector<Buffer> toWrite;
std::unique_lock lock{m_mutex};
while (m_active) {
bool doFlush = false;
auto timeoutTime = std::chrono::steady_clock::now() + periodTime;
if (m_cond.wait_until(lock, timeoutTime) == std::cv_status::timeout) {
doFlush = true;
}
if (doFlush || m_doFlush) {
// flush to file
m_doFlush = false;
if (m_outgoing.empty()) {
continue;
}
// swap outgoing with empty vector
toWrite.swap(m_outgoing);
lock.unlock();
// write buffers
for (auto&& buf : toWrite) {
if (!buf.GetData().empty()) {
write(buf.GetData());
}
}
lock.lock();
// release buffers back to free list
for (auto&& buf : toWrite) {
buf.Clear();
m_free.emplace_back(std::move(buf));
}
toWrite.resize(0);
}
}
write({}); // indicate EOF
}
// Control records use the following format:
// 1-byte type
// 4-byte entry
// rest of data (depending on type)
int DataLog::Start(std::string_view name, std::string_view type,
std::string_view metadata, int64_t timestamp) {
std::scoped_lock lock{m_mutex};
auto& entryInfo = m_entries[name];
if (entryInfo.id == 0) {
entryInfo.id = ++m_lastId;
}
auto& savedCount = m_entryCounts[entryInfo.id];
++savedCount;
if (savedCount > 1) {
if (entryInfo.type != type) {
WPI_ERROR(m_msglog,
"type mismatch for '{}': was '{}', requested '{}'; ignoring",
name, entryInfo.type, type);
return 0;
}
return entryInfo.id;
}
entryInfo.type = type;
size_t strsize = name.size() + type.size() + metadata.size();
uint8_t* buf = StartRecord(0, timestamp, 5 + 12 + strsize, 5);
*buf++ = impl::kControlStart;
wpi::support::endian::write32le(buf, entryInfo.id);
AppendStringImpl(name);
AppendStringImpl(type);
AppendStringImpl(metadata);
return entryInfo.id;
}
void DataLog::Finish(int entry, int64_t timestamp) {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
auto& savedCount = m_entryCounts[entry];
if (savedCount == 0) {
return;
}
--savedCount;
if (savedCount != 0) {
return;
}
m_entryCounts.erase(entry);
uint8_t* buf = StartRecord(0, timestamp, 5, 5);
*buf++ = impl::kControlFinish;
wpi::support::endian::write32le(buf, entry);
}
void DataLog::SetMetadata(int entry, std::string_view metadata,
int64_t timestamp) {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
uint8_t* buf = StartRecord(entry, timestamp, 5 + 4 + metadata.size(), 5);
*buf++ = impl::kControlSetMetadata;
wpi::support::endian::write32le(buf, entry);
AppendStringImpl(metadata);
}
uint8_t* DataLog::Reserve(size_t size) {
assert(size <= kBlockSize);
if (m_outgoing.empty() || size > m_outgoing.back().GetRemaining()) {
if (m_free.empty()) {
m_outgoing.emplace_back();
} else {
m_outgoing.emplace_back(std::move(m_free.back()));
m_free.pop_back();
}
}
return m_outgoing.back().Reserve(size);
}
uint8_t* DataLog::StartRecord(uint32_t entry, uint64_t timestamp,
uint32_t payloadSize, size_t reserveSize) {
uint8_t* buf = Reserve(kRecordMaxHeaderSize + reserveSize);
auto headerLen = WriteRecordHeader(buf, entry, timestamp, payloadSize);
m_outgoing.back().Unreserve(kRecordMaxHeaderSize - headerLen);
buf += headerLen;
return buf;
}
void DataLog::AppendImpl(wpi::span<const uint8_t> data) {
while (data.size() > kBlockSize) {
uint8_t* buf = Reserve(kBlockSize);
std::memcpy(buf, data.data(), kBlockSize);
data = data.subspan(kBlockSize);
}
uint8_t* buf = Reserve(data.size());
std::memcpy(buf, data.data(), data.size());
}
void DataLog::AppendStringImpl(std::string_view str) {
uint8_t* buf = Reserve(4);
wpi::support::endian::write32le(buf, str.size());
AppendImpl({reinterpret_cast<const uint8_t*>(str.data()), str.size()});
}
void DataLog::AppendRaw(int entry, wpi::span<const uint8_t> data,
int64_t timestamp) {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
return;
}
StartRecord(entry, timestamp, data.size(), 0);
AppendImpl(data);
}
void DataLog::AppendRaw2(int entry,
wpi::span<const wpi::span<const uint8_t>> data,
int64_t timestamp) {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
return;
}
size_t size = 0;
for (auto&& chunk : data) {
size += chunk.size();
}
StartRecord(entry, timestamp, size, 0);
for (auto chunk : data) {
AppendImpl(chunk);
}
}
void DataLog::AppendBoolean(int entry, bool value, int64_t timestamp) {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
return;
}
uint8_t* buf = StartRecord(entry, timestamp, 1, 1);
buf[0] = value ? 1 : 0;
}
void DataLog::AppendInteger(int entry, int64_t value, int64_t timestamp) {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
return;
}
uint8_t* buf = StartRecord(entry, timestamp, 8, 8);
wpi::support::endian::write64le(buf, value);
}
void DataLog::AppendFloat(int entry, float value, int64_t timestamp) {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
return;
}
uint8_t* buf = StartRecord(entry, timestamp, 4, 4);
if constexpr (wpi::support::endian::system_endianness() ==
wpi::support::little) {
std::memcpy(buf, &value, 4);
} else {
wpi::support::endian::write32le(buf, wpi::FloatToBits(value));
}
}
void DataLog::AppendDouble(int entry, double value, int64_t timestamp) {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
return;
}
uint8_t* buf = StartRecord(entry, timestamp, 8, 8);
if constexpr (wpi::support::endian::system_endianness() ==
wpi::support::little) {
std::memcpy(buf, &value, 8);
} else {
wpi::support::endian::write64le(buf, wpi::DoubleToBits(value));
}
}
void DataLog::AppendString(int entry, std::string_view value,
int64_t timestamp) {
AppendRaw(entry,
{reinterpret_cast<const uint8_t*>(value.data()), value.size()},
timestamp);
}
void DataLog::AppendBooleanArray(int entry, wpi::span<const bool> arr,
int64_t timestamp) {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
return;
}
StartRecord(entry, timestamp, arr.size(), 0);
uint8_t* buf;
while (arr.size() > kBlockSize) {
buf = Reserve(kBlockSize);
for (auto val : arr.subspan(0, kBlockSize)) {
*buf++ = val ? 1 : 0;
}
arr = arr.subspan(kBlockSize);
}
buf = Reserve(arr.size());
for (auto val : arr) {
*buf++ = val ? 1 : 0;
}
}
void DataLog::AppendBooleanArray(int entry, wpi::span<const int> arr,
int64_t timestamp) {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
return;
}
StartRecord(entry, timestamp, arr.size(), 0);
uint8_t* buf;
while (arr.size() > kBlockSize) {
buf = Reserve(kBlockSize);
for (auto val : arr.subspan(0, kBlockSize)) {
*buf++ = val & 1;
}
arr = arr.subspan(kBlockSize);
}
buf = Reserve(arr.size());
for (auto val : arr) {
*buf++ = val & 1;
}
}
void DataLog::AppendBooleanArray(int entry, wpi::span<const uint8_t> arr,
int64_t timestamp) {
AppendRaw(entry, arr, timestamp);
}
void DataLog::AppendIntegerArray(int entry, wpi::span<const int64_t> arr,
int64_t timestamp) {
if constexpr (wpi::support::endian::system_endianness() ==
wpi::support::little) {
AppendRaw(entry,
{reinterpret_cast<const uint8_t*>(arr.data()), arr.size() * 8},
timestamp);
} else {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
return;
}
StartRecord(entry, timestamp, arr.size() * 8, 0);
uint8_t* buf;
while ((arr.size() * 8) > kBlockSize) {
buf = Reserve(kBlockSize);
for (auto val : arr.subspan(0, kBlockSize / 8)) {
wpi::support::endian::write64le(buf, val);
buf += 8;
}
arr = arr.subspan(kBlockSize / 8);
}
buf = Reserve(arr.size() * 8);
for (auto val : arr) {
wpi::support::endian::write64le(buf, val);
buf += 8;
}
}
}
void DataLog::AppendFloatArray(int entry, wpi::span<const float> arr,
int64_t timestamp) {
if constexpr (wpi::support::endian::system_endianness() ==
wpi::support::little) {
AppendRaw(entry,
{reinterpret_cast<const uint8_t*>(arr.data()), arr.size() * 4},
timestamp);
} else {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
return;
}
StartRecord(entry, timestamp, arr.size() * 4, 0);
uint8_t* buf;
while ((arr.size() * 4) > kBlockSize) {
buf = Reserve(kBlockSize);
for (auto val : arr.subspan(0, kBlockSize / 4)) {
wpi::support::endian::write32le(buf, wpi::FloatToBits(val));
buf += 4;
}
arr = arr.subspan(kBlockSize / 4);
}
buf = Reserve(arr.size() * 4);
for (auto val : arr) {
wpi::support::endian::write32le(buf, wpi::FloatToBits(val));
buf += 4;
}
}
}
void DataLog::AppendDoubleArray(int entry, wpi::span<const double> arr,
int64_t timestamp) {
if constexpr (wpi::support::endian::system_endianness() ==
wpi::support::little) {
AppendRaw(entry,
{reinterpret_cast<const uint8_t*>(arr.data()), arr.size() * 8},
timestamp);
} else {
if (entry <= 0) {
return;
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
return;
}
StartRecord(entry, timestamp, arr.size() * 8, 0);
uint8_t* buf;
while ((arr.size() * 8) > kBlockSize) {
buf = Reserve(kBlockSize);
for (auto val : arr.subspan(0, kBlockSize / 8)) {
wpi::support::endian::write64le(buf, wpi::DoubleToBits(val));
buf += 8;
}
arr = arr.subspan(kBlockSize / 8);
}
buf = Reserve(arr.size() * 8);
for (auto val : arr) {
wpi::support::endian::write64le(buf, wpi::DoubleToBits(val));
buf += 8;
}
}
}
void DataLog::AppendStringArray(int entry, wpi::span<const std::string> arr,
int64_t timestamp) {
if (entry <= 0) {
return;
}
// storage: 4-byte array length, each string prefixed by 4-byte length
// calculate total size
size_t size = 4;
for (auto&& str : arr) {
size += 4 + str.size();
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
return;
}
uint8_t* buf = StartRecord(entry, timestamp, size, 4);
wpi::support::endian::write32le(buf, arr.size());
for (auto&& str : arr) {
AppendStringImpl(str);
}
}
void DataLog::AppendStringArray(int entry,
wpi::span<const std::string_view> arr,
int64_t timestamp) {
if (entry <= 0) {
return;
}
// storage: 4-byte array length, each string prefixed by 4-byte length
// calculate total size
size_t size = 4;
for (auto&& str : arr) {
size += 4 + str.size();
}
std::scoped_lock lock{m_mutex};
if (m_paused) {
return;
}
uint8_t* buf = StartRecord(entry, timestamp, size, 4);
wpi::support::endian::write32le(buf, arr.size());
for (auto sv : arr) {
AppendStringImpl(sv);
}
}

View File

@@ -0,0 +1,307 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include "wpi/DataLogReader.h"
#include "wpi/DataLog.h"
#include "wpi/Endian.h"
#include "wpi/MathExtras.h"
using namespace wpi::log;
static bool ReadString(wpi::span<const uint8_t>* buf, std::string_view* str) {
if (buf->size() < 4) {
*str = {};
return false;
}
uint32_t len = wpi::support::endian::read32le(buf->data());
if (len > (buf->size() - 4)) {
*str = {};
return false;
}
*str = {reinterpret_cast<const char*>(buf->data() + 4), len};
*buf = buf->subspan(len + 4);
return true;
}
bool DataLogRecord::IsStart() const {
return m_entry == 0 && m_data.size() >= 17 &&
m_data[0] == impl::kControlStart;
}
bool DataLogRecord::IsFinish() const {
return m_entry == 0 && m_data.size() == 5 &&
m_data[0] == impl::kControlFinish;
}
bool DataLogRecord::IsSetMetadata() const {
return m_entry == 0 && m_data.size() >= 9 &&
m_data[0] == impl::kControlSetMetadata;
}
bool DataLogRecord::GetStartData(StartRecordData* out) const {
if (!IsStart()) {
return false;
}
out->entry = wpi::support::endian::read32le(&m_data[1]);
auto buf = m_data.subspan(5);
if (!ReadString(&buf, &out->name)) {
return false;
}
if (!ReadString(&buf, &out->type)) {
return false;
}
if (!ReadString(&buf, &out->metadata)) {
return false;
}
return true;
}
bool DataLogRecord::GetFinishEntry(int* out) const {
if (!IsFinish()) {
return false;
}
*out = wpi::support::endian::read32le(&m_data[1]);
return true;
}
bool DataLogRecord::GetSetMetadataData(MetadataRecordData* out) const {
if (!IsSetMetadata()) {
return false;
}
out->entry = wpi::support::endian::read32le(&m_data[1]);
auto buf = m_data.subspan(5);
return ReadString(&buf, &out->metadata);
}
bool DataLogRecord::GetBoolean(bool* value) const {
if (m_data.size() != 1) {
return false;
}
*value = m_data[0] != 0;
return true;
}
bool DataLogRecord::GetInteger(int64_t* value) const {
if (m_data.size() != 8) {
return false;
}
*value = wpi::support::endian::read64le(m_data.data());
return true;
}
bool DataLogRecord::GetFloat(float* value) const {
if (m_data.size() != 4) {
return false;
}
*value = wpi::BitsToFloat(wpi::support::endian::read32le(m_data.data()));
return true;
}
bool DataLogRecord::GetDouble(double* value) const {
if (m_data.size() != 8) {
return false;
}
*value = wpi::BitsToDouble(wpi::support::endian::read64le(m_data.data()));
return true;
}
bool DataLogRecord::GetString(std::string_view* value) const {
*value = {reinterpret_cast<const char*>(m_data.data()), m_data.size()};
return true;
}
bool DataLogRecord::GetBooleanArray(std::vector<int>* arr) const {
arr->clear();
arr->reserve(m_data.size());
for (auto v : m_data) {
arr->push_back(v);
}
return true;
}
bool DataLogRecord::GetIntegerArray(std::vector<int64_t>* arr) const {
arr->clear();
if ((m_data.size() % 8) != 0) {
return false;
}
arr->reserve(m_data.size() / 8);
for (size_t pos = 0; pos < m_data.size(); pos += 8) {
arr->push_back(wpi::support::endian::read64le(&m_data[pos]));
}
return true;
}
bool DataLogRecord::GetFloatArray(std::vector<float>* arr) const {
arr->clear();
if ((m_data.size() % 4) != 0) {
return false;
}
arr->reserve(m_data.size() / 4);
for (size_t pos = 0; pos < m_data.size(); pos += 4) {
arr->push_back(
wpi::BitsToFloat(wpi::support::endian::read32le(&m_data[pos])));
}
return true;
}
bool DataLogRecord::GetDoubleArray(std::vector<double>* arr) const {
arr->clear();
if ((m_data.size() % 8) != 0) {
return false;
}
arr->reserve(m_data.size() / 8);
for (size_t pos = 0; pos < m_data.size(); pos += 8) {
arr->push_back(
wpi::BitsToDouble(wpi::support::endian::read64le(&m_data[pos])));
}
return true;
}
bool DataLogRecord::GetStringArray(std::vector<std::string_view>* arr) const {
arr->clear();
if (m_data.size() < 4) {
return false;
}
uint32_t size = wpi::support::endian::read32le(m_data.data());
// sanity check size
if (size > ((m_data.size() - 4) / 4)) {
return false;
}
auto buf = m_data.subspan(4);
arr->reserve(size);
for (uint32_t i = 0; i < size; ++i) {
std::string_view str;
if (!ReadString(&buf, &str)) {
arr->clear();
return false;
}
arr->push_back(str);
}
// any left over? treat as corrupt
if (!buf.empty()) {
arr->clear();
return false;
}
return true;
}
DataLogReader::DataLogReader(std::unique_ptr<MemoryBuffer> buffer)
: m_buf{std::move(buffer)} {}
bool DataLogReader::IsValid() const {
if (!m_buf) {
return false;
}
auto buf = m_buf->GetBuffer();
return buf.size() >= 12 &&
std::string_view{reinterpret_cast<const char*>(buf.data()), 6} ==
"WPILOG" &&
wpi::support::endian::read16le(&buf[6]) >= 0x0100;
}
uint16_t DataLogReader::GetVersion() const {
if (!m_buf) {
return 0;
}
auto buf = m_buf->GetBuffer();
if (buf.size() < 12) {
return 0;
}
return wpi::support::endian::read16le(&buf[6]);
}
std::string_view DataLogReader::GetExtraHeader() const {
if (!m_buf) {
return {};
}
auto buf = m_buf->GetBuffer();
if (buf.size() < 8) {
return {};
}
std::string_view rv;
buf = buf.subspan(8);
ReadString(&buf, &rv);
return rv;
}
DataLogReader::iterator DataLogReader::begin() const {
if (!m_buf) {
return end();
}
auto buf = m_buf->GetBuffer();
if (buf.size() < 12) {
return end();
}
uint32_t size = wpi::support::endian::read32le(&buf[8]);
if (buf.size() < (12 + size)) {
return end();
}
return DataLogIterator{this, 12 + size};
}
static uint64_t ReadVarInt(wpi::span<const uint8_t> buf) {
uint64_t val = 0;
int shift = 0;
for (auto v : buf) {
val |= static_cast<uint64_t>(v) << shift;
shift += 8;
}
return val;
}
bool DataLogReader::GetRecord(size_t* pos, DataLogRecord* out) const {
if (!m_buf) {
return false;
}
auto buf = m_buf->GetBuffer();
if (*pos >= buf.size()) {
return false;
}
buf = buf.subspan(*pos);
if (buf.size() < 4) { // minimum header length
return false;
}
unsigned int entryLen = (buf[0] & 0x3) + 1;
unsigned int sizeLen = ((buf[0] >> 2) & 0x3) + 1;
unsigned int timestampLen = ((buf[0] >> 4) & 0x7) + 1;
unsigned int headerLen = 1 + entryLen + sizeLen + timestampLen;
if (buf.size() < headerLen) {
return false;
}
int entry = ReadVarInt(buf.subspan(1, entryLen));
uint32_t size = ReadVarInt(buf.subspan(1 + entryLen, sizeLen));
if (size > (buf.size() - headerLen)) {
return false;
}
int64_t timestamp =
ReadVarInt(buf.subspan(1 + entryLen + sizeLen, timestampLen));
*out = DataLogRecord{entry, timestamp, buf.subspan(headerLen, size)};
*pos += headerLen + size;
return true;
}
bool DataLogReader::GetNextRecord(size_t* pos) const {
if (!m_buf) {
return false;
}
auto buf = m_buf->GetBuffer();
if (buf.size() < (*pos + 4)) { // minimum header length
return false;
}
unsigned int entryLen = (buf[*pos] & 0x3) + 1;
unsigned int sizeLen = ((buf[*pos] >> 2) & 0x3) + 1;
unsigned int timestampLen = ((buf[*pos] >> 4) & 0x7) + 1;
unsigned int headerLen = 1 + entryLen + sizeLen + timestampLen;
if (buf.size() < (*pos + headerLen)) {
return false;
}
uint32_t size = ReadVarInt(buf.subspan(*pos + 1 + entryLen, sizeLen));
// check this way to avoid overflow
if (size >= (buf.size() - *pos - headerLen)) {
return false;
}
*pos += headerLen + size;
return true;
}

View File

@@ -82,17 +82,6 @@ namespace fs {
const file_t kInvalidFile = INVALID_HANDLE_VALUE;
static DWORD nativeDisposition(CreationDisposition Disp, OpenFlags Flags) {
// This is a compatibility hack. Really we should respect the creation
// disposition, but a lot of old code relied on the implicit assumption that
// OF_Append implied it would open an existing file. Since the disposition is
// now explicit and defaults to CD_CreateAlways, this assumption would cause
// any usage of OF_Append to append to a new file, even if the file already
// existed. A better solution might have two new creation dispositions:
// CD_AppendAlways and CD_AppendNew. This would also address the problem of
// OF_Append being used on a read-only descriptor, which doesn't make sense.
if (Flags & OF_Append)
return OPEN_ALWAYS;
switch (Disp) {
case CD_CreateAlways:
return CREATE_ALWAYS;
@@ -251,12 +240,6 @@ static int nativeOpenFlags(CreationDisposition Disp, OpenFlags Flags,
Result |= O_RDWR;
}
// This is for compatibility with old code that assumed F_Append implied
// would open an existing file. See Windows/Path.inc for a longer comment.
if (Flags & F_Append) {
Disp = CD_OpenAlways;
}
if (Disp == CD_CreateNew) {
Result |= O_CREAT; // Create if it doesn't exist.
Result |= O_EXCL; // Fail if it does.

View File

@@ -0,0 +1,357 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#include <jni.h>
#include "edu_wpi_first_util_datalog_DataLogJNI.h"
#include "wpi/DataLog.h"
#include "wpi/jni_util.h"
using namespace wpi::java;
using namespace wpi::log;
extern "C" {
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: create
* Signature: (Ljava/lang/String;Ljava/lang/String;DLjava/lang/String;)J
*/
JNIEXPORT jlong JNICALL
Java_edu_wpi_first_util_datalog_DataLogJNI_create
(JNIEnv* env, jclass, jstring dir, jstring filename, jdouble period,
jstring extraHeader)
{
return reinterpret_cast<jlong>(new DataLog{JStringRef{env, dir},
JStringRef{env, filename}, period,
JStringRef{env, extraHeader}});
}
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: setFilename
* Signature: (JLjava/lang/String;)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_util_datalog_DataLogJNI_setFilename
(JNIEnv* env, jclass, jlong impl, jstring filename)
{
if (impl == 0) {
return;
}
reinterpret_cast<DataLog*>(impl)->SetFilename(JStringRef{env, filename});
}
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: flush
* Signature: (J)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_util_datalog_DataLogJNI_flush
(JNIEnv*, jclass, jlong impl)
{
if (impl == 0) {
return;
}
reinterpret_cast<DataLog*>(impl)->Flush();
}
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: pause
* Signature: (J)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_util_datalog_DataLogJNI_pause
(JNIEnv*, jclass, jlong impl)
{
if (impl == 0) {
return;
}
reinterpret_cast<DataLog*>(impl)->Pause();
}
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: resume
* Signature: (J)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_util_datalog_DataLogJNI_resume
(JNIEnv*, jclass, jlong impl)
{
if (impl == 0) {
return;
}
reinterpret_cast<DataLog*>(impl)->Resume();
}
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: start
* Signature: (JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;J)I
*/
JNIEXPORT jint JNICALL
Java_edu_wpi_first_util_datalog_DataLogJNI_start
(JNIEnv* env, jclass, jlong impl, jstring name, jstring type,
jstring metadata, jlong timestamp)
{
if (impl == 0) {
return 0;
}
return reinterpret_cast<DataLog*>(impl)->Start(
JStringRef{env, name}, JStringRef{env, type}, JStringRef{env, metadata},
timestamp);
}
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: finish
* Signature: (JIJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_util_datalog_DataLogJNI_finish
(JNIEnv*, jclass, jlong impl, jint entry, jlong timestamp)
{
if (impl == 0) {
return;
}
reinterpret_cast<DataLog*>(impl)->Finish(entry, timestamp);
}
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: setMetadata
* Signature: (JILjava/lang/String;J)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_util_datalog_DataLogJNI_setMetadata
(JNIEnv* env, jclass, jlong impl, jint entry, jstring metadata,
jlong timestamp)
{
if (impl == 0) {
return;
}
reinterpret_cast<DataLog*>(impl)->SetMetadata(
entry, JStringRef{env, metadata}, timestamp);
}
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: close
* Signature: (J)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_util_datalog_DataLogJNI_close
(JNIEnv*, jclass, jlong impl)
{
delete reinterpret_cast<DataLog*>(impl);
}
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: appendRaw
* Signature: (JI[BJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_util_datalog_DataLogJNI_appendRaw
(JNIEnv* env, jclass, jlong impl, jint entry, jbyteArray value,
jlong timestamp)
{
if (impl == 0) {
return;
}
JByteArrayRef cvalue{env, value};
reinterpret_cast<DataLog*>(impl)->AppendRaw(
entry,
{reinterpret_cast<const uint8_t*>(cvalue.array().data()), cvalue.size()},
timestamp);
}
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: appendBoolean
* Signature: (JIZJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_util_datalog_DataLogJNI_appendBoolean
(JNIEnv*, jclass, jlong impl, jint entry, jboolean value, jlong timestamp)
{
if (impl == 0) {
return;
}
reinterpret_cast<DataLog*>(impl)->AppendBoolean(entry, value, timestamp);
}
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: appendInteger
* Signature: (JIJJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_util_datalog_DataLogJNI_appendInteger
(JNIEnv*, jclass, jlong impl, jint entry, jlong value, jlong timestamp)
{
if (impl == 0) {
return;
}
reinterpret_cast<DataLog*>(impl)->AppendInteger(entry, value, timestamp);
}
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: appendFloat
* Signature: (JIFJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_util_datalog_DataLogJNI_appendFloat
(JNIEnv*, jclass, jlong impl, jint entry, jfloat value, jlong timestamp)
{
if (impl == 0) {
return;
}
reinterpret_cast<DataLog*>(impl)->AppendFloat(entry, value, timestamp);
}
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: appendDouble
* Signature: (JIDJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_util_datalog_DataLogJNI_appendDouble
(JNIEnv*, jclass, jlong impl, jint entry, jdouble value, jlong timestamp)
{
if (impl == 0) {
return;
}
reinterpret_cast<DataLog*>(impl)->AppendDouble(entry, value, timestamp);
}
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: appendString
* Signature: (JILjava/lang/String;J)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_util_datalog_DataLogJNI_appendString
(JNIEnv* env, jclass, jlong impl, jint entry, jstring value, jlong timestamp)
{
if (impl == 0) {
return;
}
reinterpret_cast<DataLog*>(impl)->AppendString(entry, JStringRef{env, value},
timestamp);
}
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: appendBooleanArray
* Signature: (JI[ZJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_util_datalog_DataLogJNI_appendBooleanArray
(JNIEnv* env, jclass, jlong impl, jint entry, jbooleanArray value,
jlong timestamp)
{
if (impl == 0) {
return;
}
reinterpret_cast<DataLog*>(impl)->AppendBooleanArray(
entry, JBooleanArrayRef{env, value}, timestamp);
}
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: appendIntegerArray
* Signature: (JI[JJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_util_datalog_DataLogJNI_appendIntegerArray
(JNIEnv* env, jclass, jlong impl, jint entry, jlongArray value,
jlong timestamp)
{
if (impl == 0) {
return;
}
JLongArrayRef jarr{env, value};
if constexpr (sizeof(jlong) == sizeof(int64_t)) {
reinterpret_cast<DataLog*>(impl)->AppendIntegerArray(
entry,
{reinterpret_cast<const int64_t*>(jarr.array().data()),
jarr.array().size()},
timestamp);
} else {
wpi::SmallVector<int64_t, 16> arr;
arr.reserve(jarr.size());
for (auto v : jarr.array()) {
arr.push_back(v);
}
reinterpret_cast<DataLog*>(impl)->AppendIntegerArray(entry, arr, timestamp);
}
}
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: appendFloatArray
* Signature: (JI[FJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_util_datalog_DataLogJNI_appendFloatArray
(JNIEnv* env, jclass, jlong impl, jint entry, jfloatArray value,
jlong timestamp)
{
if (impl == 0) {
return;
}
reinterpret_cast<DataLog*>(impl)->AppendFloatArray(
entry, JFloatArrayRef{env, value}, timestamp);
}
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: appendDoubleArray
* Signature: (JI[DJ)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_util_datalog_DataLogJNI_appendDoubleArray
(JNIEnv* env, jclass, jlong impl, jint entry, jdoubleArray value,
jlong timestamp)
{
if (impl == 0) {
return;
}
reinterpret_cast<DataLog*>(impl)->AppendDoubleArray(
entry, JDoubleArrayRef{env, value}, timestamp);
}
/*
* Class: edu_wpi_first_util_datalog_DataLogJNI
* Method: appendStringArray
* Signature: (JI[Ljava/lang/Object;J)V
*/
JNIEXPORT void JNICALL
Java_edu_wpi_first_util_datalog_DataLogJNI_appendStringArray
(JNIEnv* env, jclass, jlong impl, jint entry, jobjectArray value,
jlong timestamp)
{
if (impl == 0) {
return;
}
size_t len = env->GetArrayLength(value);
std::vector<std::string> arr;
arr.reserve(len);
for (size_t i = 0; i < len; ++i) {
JLocal<jstring> elem{
env, static_cast<jstring>(env->GetObjectArrayElement(value, i))};
if (!elem) {
return;
}
arr.emplace_back(JStringRef{env, elem}.str());
}
reinterpret_cast<DataLog*>(impl)->AppendStringArray(entry, arr, timestamp);
}
} // extern "C"

View File

@@ -0,0 +1,680 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#pragma once
#include <stdint.h>
#include <functional>
#include <initializer_list>
#include <memory>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
#include "wpi/DenseMap.h"
#include "wpi/StringMap.h"
#include "wpi/condition_variable.h"
#include "wpi/mutex.h"
#include "wpi/span.h"
namespace wpi {
class Logger;
} // namespace wpi
namespace wpi::log {
namespace impl {
enum ControlRecordType {
kControlStart = 0,
kControlFinish,
kControlSetMetadata
};
} // namespace impl
/**
* A data log. The log file is created immediately upon construction with a
* temporary filename. The file may be renamed at any time using the
* SetFilename() function.
*
* The lifetime of the data log object must be longer than any data log entry
* objects that refer to it.
*
* The data log is periodically flushed to disk. It can also be explicitly
* flushed to disk by using the Flush() function.
*/
class DataLog final {
public:
/**
* Construct a new Data Log. The log will be initially created with a
* temporary filename.
*
* @param dir directory to store the log
* @param filename filename to use; if none provided, a random filename is
* generated of the form "wpilog_{}.wpilog"
* @param period time between automatic flushes to disk, in seconds;
* this is a time/storage tradeoff
* @param extraHeader extra header data
*/
explicit DataLog(std::string_view dir = "", std::string_view filename = "",
double period = 0.25, std::string_view extraHeader = "");
/**
* Construct a new Data Log. The log will be initially created with a
* temporary filename.
*
* @param msglog message logger (will be called from separate thread)
* @param dir directory to store the log
* @param filename filename to use; if none provided, a random filename is
* generated of the form "wpilog_{}.wpilog"
* @param period time between automatic flushes to disk, in seconds;
* this is a time/storage tradeoff
* @param extraHeader extra header data
*/
explicit DataLog(wpi::Logger& msglog, std::string_view dir = "",
std::string_view filename = "", double period = 0.25,
std::string_view extraHeader = "");
/**
* Construct a new Data Log that passes its output to the provided function
* rather than a file. The write function will be called on a separate
* background thread and may block. The write function is called with an
* empty data array when the thread is terminating.
*
* @param write write function
* @param period time between automatic calls to write, in seconds;
* this is a time/storage tradeoff
* @param extraHeader extra header data
*/
explicit DataLog(std::function<void(wpi::span<const uint8_t> data)> write,
double period = 0.25, std::string_view extraHeader = "");
/**
* Construct a new Data Log that passes its output to the provided function
* rather than a file. The write function will be called on a separate
* background thread and may block. The write function is called with an
* empty data array when the thread is terminating.
*
* @param msglog message logger (will be called from separate thread)
* @param write write function
* @param period time between automatic calls to write, in seconds;
* this is a time/storage tradeoff
* @param extraHeader extra header data
*/
explicit DataLog(wpi::Logger& msglog,
std::function<void(wpi::span<const uint8_t> data)> write,
double period = 0.25, std::string_view extraHeader = "");
~DataLog();
DataLog(const DataLog&) = delete;
DataLog& operator=(const DataLog&) = delete;
DataLog(DataLog&&) = delete;
DataLog& operator=(const DataLog&&) = delete;
/**
* Change log filename.
*
* @param filename filename
*/
void SetFilename(std::string_view filename);
/**
* Explicitly flushes the log data to disk.
*/
void Flush();
/**
* Pauses appending of data records to the log. While paused, no data records
* are saved (e.g. AppendX is a no-op). Has no effect on entry starts /
* finishes / metadata changes.
*/
void Pause();
/**
* Resumes appending of data records to the log.
*/
void Resume();
/**
* Start an entry. Duplicate names are allowed (with the same type), and
* result in the same index being returned (Start/Finish are reference
* counted). A duplicate name with a different type will result in an error
* message being printed to the console and 0 being returned (which will be
* ignored by the Append functions).
*
* @param name Name
* @param type Data type
* @param metadata Initial metadata (e.g. data properties)
* @param timestamp Time stamp (may be 0 to indicate now)
*
* @return Entry index
*/
int Start(std::string_view name, std::string_view type,
std::string_view metadata = {}, int64_t timestamp = 0);
/**
* Finish an entry.
*
* @param entry Entry index
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Finish(int entry, int64_t timestamp = 0);
/**
* Updates the metadata for an entry.
*
* @param entry Entry index
* @param metadata New metadata for the entry
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void SetMetadata(int entry, std::string_view metadata, int64_t timestamp = 0);
/**
* Appends a record to the log.
*
* @param entry Entry index, as returned by Start()
* @param data Data to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void AppendRaw(int entry, wpi::span<const uint8_t> data, int64_t timestamp);
/**
* Appends a record to the log.
*
* @param entry Entry index, as returned by Start()
* @param data Data to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void AppendRaw2(int entry, wpi::span<const wpi::span<const uint8_t>> data,
int64_t timestamp);
void AppendBoolean(int entry, bool value, int64_t timestamp);
void AppendInteger(int entry, int64_t value, int64_t timestamp);
void AppendFloat(int entry, float value, int64_t timestamp);
void AppendDouble(int entry, double value, int64_t timestamp);
void AppendString(int entry, std::string_view value, int64_t timestamp);
void AppendBooleanArray(int entry, wpi::span<const bool> arr,
int64_t timestamp);
void AppendBooleanArray(int entry, wpi::span<const int> arr,
int64_t timestamp);
void AppendBooleanArray(int entry, wpi::span<const uint8_t> arr,
int64_t timestamp);
void AppendIntegerArray(int entry, wpi::span<const int64_t> arr,
int64_t timestamp);
void AppendFloatArray(int entry, wpi::span<const float> arr,
int64_t timestamp);
void AppendDoubleArray(int entry, wpi::span<const double> arr,
int64_t timestamp);
void AppendStringArray(int entry, wpi::span<const std::string> arr,
int64_t timestamp);
void AppendStringArray(int entry, wpi::span<const std::string_view> arr,
int64_t timestamp);
private:
void WriterThreadMain(std::string_view dir);
void WriterThreadMain(
std::function<void(wpi::span<const uint8_t> data)> write);
// must be called with m_mutex held
uint8_t* StartRecord(uint32_t entry, uint64_t timestamp, uint32_t payloadSize,
size_t reserveSize);
uint8_t* Reserve(size_t size);
void AppendImpl(wpi::span<const uint8_t> data);
void AppendStringImpl(std::string_view str);
wpi::Logger& m_msglog;
mutable wpi::mutex m_mutex;
wpi::condition_variable m_cond;
bool m_active{true};
bool m_doFlush{false};
bool m_paused{false};
double m_period;
std::string m_extraHeader;
std::string m_newFilename;
class Buffer;
std::vector<Buffer> m_free;
std::vector<Buffer> m_outgoing;
struct EntryInfo {
std::string type;
int id{0};
};
wpi::StringMap<EntryInfo> m_entries;
wpi::DenseMap<int, unsigned int> m_entryCounts;
int m_lastId = 0;
std::thread m_thread;
};
/**
* Log entry base class.
*/
class DataLogEntry {
protected:
DataLogEntry() = default;
DataLogEntry(DataLog& log, std::string_view name, std::string_view type,
std::string_view metadata = {}, int64_t timestamp = 0)
: m_log{&log}, m_entry{log.Start(name, type, metadata, timestamp)} {}
public:
DataLogEntry(const DataLogEntry&) = delete;
DataLogEntry& operator=(const DataLogEntry&) = delete;
DataLogEntry(DataLogEntry&& rhs) : m_log{rhs.m_log}, m_entry{rhs.m_entry} {
rhs.m_log = nullptr;
}
DataLogEntry& operator=(DataLogEntry&& rhs) {
if (m_log) {
m_log->Finish(m_entry);
}
m_log = rhs.m_log;
rhs.m_log = nullptr;
m_entry = rhs.m_entry;
return *this;
}
explicit operator bool() const { return m_log != nullptr; }
/**
* Updates the metadata for the entry.
*
* @param metadata New metadata for the entry
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void SetMetadata(std::string_view metadata, int64_t timestamp = 0) {
m_log->SetMetadata(m_entry, metadata, timestamp);
}
/**
* Finishes the entry.
*
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Finish(int64_t timestamp = 0) { m_log->Finish(m_entry, timestamp); }
protected:
DataLog* m_log = nullptr;
int m_entry = 0;
};
/**
* Log arbitrary byte data.
*/
class RawLogEntry : public DataLogEntry {
public:
static constexpr std::string_view kDataType = "raw";
RawLogEntry() = default;
RawLogEntry(DataLog& log, std::string_view name, int64_t timestamp = 0)
: RawLogEntry{log, name, {}, kDataType, timestamp} {}
RawLogEntry(DataLog& log, std::string_view name, std::string_view metadata,
int64_t timestamp = 0)
: RawLogEntry{log, name, metadata, kDataType, timestamp} {}
RawLogEntry(DataLog& log, std::string_view name, std::string_view metadata,
std::string_view type, int64_t timestamp = 0)
: DataLogEntry{log, name, type, metadata, timestamp} {}
/**
* Appends a record to the log.
*
* @param data Data to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Append(wpi::span<const uint8_t> data, int64_t timestamp = 0) {
m_log->AppendRaw(m_entry, data, timestamp);
}
};
/**
* Log boolean values.
*/
class BooleanLogEntry : public DataLogEntry {
public:
static constexpr std::string_view kDataType = "boolean";
BooleanLogEntry() = default;
BooleanLogEntry(DataLog& log, std::string_view name, int64_t timestamp = 0)
: BooleanLogEntry{log, name, {}, timestamp} {}
BooleanLogEntry(DataLog& log, std::string_view name,
std::string_view metadata, int64_t timestamp = 0)
: DataLogEntry{log, name, kDataType, metadata, timestamp} {}
/**
* Appends a record to the log.
*
* @param value Value to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Append(bool value, int64_t timestamp = 0) {
m_log->AppendBoolean(m_entry, value, timestamp);
}
};
/**
* Log integer values.
*/
class IntegerLogEntry : public DataLogEntry {
public:
static constexpr std::string_view kDataType = "int64";
IntegerLogEntry() = default;
IntegerLogEntry(DataLog& log, std::string_view name, int64_t timestamp = 0)
: IntegerLogEntry{log, name, {}, timestamp} {}
IntegerLogEntry(DataLog& log, std::string_view name,
std::string_view metadata, int64_t timestamp = 0)
: DataLogEntry{log, name, kDataType, metadata, timestamp} {}
/**
* Appends a record to the log.
*
* @param value Value to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Append(int64_t value, int64_t timestamp = 0) {
m_log->AppendInteger(m_entry, value, timestamp);
}
};
/**
* Log float values.
*/
class FloatLogEntry : public DataLogEntry {
public:
static constexpr std::string_view kDataType = "float";
FloatLogEntry() = default;
FloatLogEntry(DataLog& log, std::string_view name, int64_t timestamp = 0)
: FloatLogEntry{log, name, {}, timestamp} {}
FloatLogEntry(DataLog& log, std::string_view name, std::string_view metadata,
int64_t timestamp = 0)
: DataLogEntry{log, name, kDataType, metadata, timestamp} {}
/**
* Appends a record to the log.
*
* @param value Value to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Append(float value, int64_t timestamp = 0) {
m_log->AppendFloat(m_entry, value, timestamp);
}
};
/**
* Log double values.
*/
class DoubleLogEntry : public DataLogEntry {
public:
static constexpr std::string_view kDataType = "double";
DoubleLogEntry() = default;
DoubleLogEntry(DataLog& log, std::string_view name, int64_t timestamp = 0)
: DoubleLogEntry{log, name, {}, timestamp} {}
DoubleLogEntry(DataLog& log, std::string_view name, std::string_view metadata,
int64_t timestamp = 0)
: DataLogEntry{log, name, kDataType, metadata, timestamp} {}
/**
* Appends a record to the log.
*
* @param value Value to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Append(double value, int64_t timestamp = 0) {
m_log->AppendDouble(m_entry, value, timestamp);
}
};
/**
* Log string values.
*/
class StringLogEntry : public DataLogEntry {
public:
static constexpr const char* kDataType = "string";
StringLogEntry() = default;
StringLogEntry(DataLog& log, std::string_view name, int64_t timestamp = 0)
: StringLogEntry{log, name, {}, kDataType, timestamp} {}
StringLogEntry(DataLog& log, std::string_view name, std::string_view metadata,
int64_t timestamp = 0)
: StringLogEntry{log, name, metadata, kDataType, timestamp} {}
StringLogEntry(DataLog& log, std::string_view name, std::string_view metadata,
std::string_view type, int64_t timestamp = 0)
: DataLogEntry{log, name, type, metadata, timestamp} {}
/**
* Appends a record to the log.
*
* @param value Value to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Append(std::string_view value, int64_t timestamp = 0) {
m_log->AppendString(m_entry, value, timestamp);
}
};
/**
* Log array of boolean values.
*/
class BooleanArrayLogEntry : public DataLogEntry {
public:
static constexpr const char* kDataType = "boolean[]";
BooleanArrayLogEntry() = default;
BooleanArrayLogEntry(DataLog& log, std::string_view name,
int64_t timestamp = 0)
: BooleanArrayLogEntry{log, name, {}, timestamp} {}
BooleanArrayLogEntry(DataLog& log, std::string_view name,
std::string_view metadata, int64_t timestamp = 0)
: DataLogEntry{log, name, kDataType, metadata, timestamp} {}
/**
* Appends a record to the log. For find functions to work, timestamp
* must be monotonically increasing.
*
* @param arr Values to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Append(wpi::span<const bool> arr, int64_t timestamp = 0) {
m_log->AppendBooleanArray(m_entry, arr, timestamp);
}
/**
* Appends a record to the log.
*
* @param arr Values to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Append(std::initializer_list<bool> arr, int64_t timestamp = 0) {
Append(wpi::span{arr.begin(), arr.end()}, timestamp);
}
/**
* Appends a record to the log.
*
* @param arr Values to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Append(wpi::span<const int> arr, int64_t timestamp = 0) {
m_log->AppendBooleanArray(m_entry, arr, timestamp);
}
/**
* Appends a record to the log.
*
* @param arr Values to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Append(std::initializer_list<int> arr, int64_t timestamp = 0) {
Append(wpi::span{arr.begin(), arr.end()}, timestamp);
}
/**
* Appends a record to the log.
*
* @param arr Values to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Append(wpi::span<const uint8_t> arr, int64_t timestamp = 0) {
m_log->AppendBooleanArray(m_entry, arr, timestamp);
}
};
/**
* Log array of integer values.
*/
class IntegerArrayLogEntry : public DataLogEntry {
public:
static constexpr const char* kDataType = "int64[]";
IntegerArrayLogEntry() = default;
IntegerArrayLogEntry(DataLog& log, std::string_view name,
int64_t timestamp = 0)
: IntegerArrayLogEntry{log, name, {}, timestamp} {}
IntegerArrayLogEntry(DataLog& log, std::string_view name,
std::string_view metadata, int64_t timestamp = 0)
: DataLogEntry{log, name, kDataType, metadata, timestamp} {}
/**
* Appends a record to the log.
*
* @param arr Values to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Append(wpi::span<const int64_t> arr, int64_t timestamp = 0) {
m_log->AppendIntegerArray(m_entry, arr, timestamp);
}
/**
* Appends a record to the log.
*
* @param arr Values to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Append(std::initializer_list<int64_t> arr, int64_t timestamp = 0) {
Append({arr.begin(), arr.end()}, timestamp);
}
};
/**
* Log array of float values.
*/
class FloatArrayLogEntry : public DataLogEntry {
public:
static constexpr const char* kDataType = "float[]";
FloatArrayLogEntry() = default;
FloatArrayLogEntry(DataLog& log, std::string_view name, int64_t timestamp = 0)
: FloatArrayLogEntry{log, name, {}, timestamp} {}
FloatArrayLogEntry(DataLog& log, std::string_view name,
std::string_view metadata, int64_t timestamp = 0)
: DataLogEntry{log, name, kDataType, metadata, timestamp} {}
/**
* Appends a record to the log.
*
* @param arr Values to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Append(wpi::span<const float> arr, int64_t timestamp = 0) {
m_log->AppendFloatArray(m_entry, arr, timestamp);
}
/**
* Appends a record to the log.
*
* @param arr Values to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Append(std::initializer_list<float> arr, int64_t timestamp = 0) {
Append({arr.begin(), arr.end()}, timestamp);
}
};
/**
* Log array of double values.
*/
class DoubleArrayLogEntry : public DataLogEntry {
public:
static constexpr const char* kDataType = "double[]";
DoubleArrayLogEntry() = default;
DoubleArrayLogEntry(DataLog& log, std::string_view name,
int64_t timestamp = 0)
: DoubleArrayLogEntry{log, name, {}, timestamp} {}
DoubleArrayLogEntry(DataLog& log, std::string_view name,
std::string_view metadata, int64_t timestamp = 0)
: DataLogEntry{log, name, kDataType, metadata, timestamp} {}
/**
* Appends a record to the log.
*
* @param arr Values to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Append(wpi::span<const double> arr, int64_t timestamp = 0) {
m_log->AppendDoubleArray(m_entry, arr, timestamp);
}
/**
* Appends a record to the log.
*
* @param arr Values to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Append(std::initializer_list<double> arr, int64_t timestamp = 0) {
Append({arr.begin(), arr.end()}, timestamp);
}
};
/**
* Log array of string values.
*/
class StringArrayLogEntry : public DataLogEntry {
public:
static constexpr const char* kDataType = "string[]";
StringArrayLogEntry() = default;
StringArrayLogEntry(DataLog& log, std::string_view name,
int64_t timestamp = 0)
: StringArrayLogEntry{log, name, {}, timestamp} {}
StringArrayLogEntry(DataLog& log, std::string_view name,
std::string_view metadata, int64_t timestamp = 0)
: DataLogEntry{log, name, kDataType, metadata, timestamp} {}
/**
* Appends a record to the log.
*
* @param arr Values to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Append(wpi::span<const std::string> arr, int64_t timestamp = 0) {
m_log->AppendStringArray(m_entry, arr, timestamp);
}
/**
* Appends a record to the log.
*
* @param arr Values to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Append(wpi::span<const std::string_view> arr, int64_t timestamp = 0) {
m_log->AppendStringArray(m_entry, arr, timestamp);
}
/**
* Appends a record to the log.
*
* @param arr Values to record
* @param timestamp Time stamp (may be 0 to indicate now)
*/
void Append(std::initializer_list<std::string_view> arr,
int64_t timestamp = 0) {
Append(wpi::span<const std::string_view>{arr.begin(), arr.end()},
timestamp);
}
};
} // namespace wpi::log

View File

@@ -0,0 +1,369 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
#pragma once
#include <stdint.h>
#include <iterator>
#include <memory>
#include <utility>
#include <vector>
#include "wpi/MemoryBuffer.h"
#include "wpi/span.h"
namespace wpi::log {
/**
* Data contained in a start control record as created by DataLog::Start() when
* writing the log. This can be read by calling DataLogRecord::GetStartData().
*/
struct StartRecordData {
/** Entry ID; this will be used for this entry in future records. */
int entry;
/** Entry name. */
std::string_view name;
/** Type of the stored data for this entry, as a string, e.g. "double". */
std::string_view type;
/** Initial metadata. */
std::string_view metadata;
};
/**
* Data contained in a set metadata control record as created by
* DataLog::SetMetadata(). This can be read by calling
* DataLogRecord::GetSetMetadataData().
*/
struct MetadataRecordData {
/** Entry ID. */
int entry;
/** New metadata for the entry. */
std::string_view metadata;
};
/**
* A record in the data log. May represent either a control record (entry == 0)
* or a data record. Used only for reading (e.g. with DataLogReader).
*/
class DataLogRecord {
public:
DataLogRecord() = default;
DataLogRecord(int entry, int64_t timestamp, wpi::span<const uint8_t> data)
: m_timestamp{timestamp}, m_data{data}, m_entry{entry} {}
/**
* Gets the entry ID.
*
* @return entry ID
*/
int GetEntry() const { return m_entry; }
/**
* Gets the record timestamp.
*
* @return Timestamp, in integer microseconds
*/
int64_t GetTimestamp() const { return m_timestamp; }
/**
* Gets the size of the raw data.
*
* @return size
*/
size_t GetSize() const { return m_data.size(); }
/**
* Gets the raw data. Use the GetX functions to decode based on the data type
* in the entry's start record.
*/
wpi::span<const uint8_t> GetRaw() const { return m_data; }
/**
* Returns true if the record is a control record.
*
* @return True if control record, false if normal data record.
*/
bool IsControl() const { return m_entry == 0; }
/**
* Returns true if the record is a start control record. Use GetStartData()
* to decode the contents.
*
* @return True if start control record, false otherwise.
*/
bool IsStart() const;
/**
* Returns true if the record is a finish control record. Use GetFinishEntry()
* to decode the contents.
*
* @return True if finish control record, false otherwise.
*/
bool IsFinish() const;
/**
* Returns true if the record is a set metadata control record. Use
* GetSetMetadataData() to decode the contents.
*
* @return True if set metadata control record, false otherwise.
*/
bool IsSetMetadata() const;
/**
* Decodes a start control record.
*
* @param[out] out start record decoded data (if successful)
* @return True on success, false on error
*/
bool GetStartData(StartRecordData* out) const;
/**
* Decodes a finish control record.
*
* @param[out] out finish record entry ID (if successful)
* @return True on success, false on error
*/
bool GetFinishEntry(int* out) const;
/**
* Decodes a set metadata control record.
*
* @param[out] out set metadata record decoded data (if successful)
* @return True on success, false on error
*/
bool GetSetMetadataData(MetadataRecordData* out) const;
/**
* Decodes a data record as a boolean. Note if the data type (as indicated in
* the corresponding start control record for this entry) is not "boolean",
* invalid results may be returned.
*
* @param[out] value boolean value (if successful)
* @return True on success, false on error
*/
bool GetBoolean(bool* value) const;
/**
* Decodes a data record as an integer. Note if the data type (as indicated in
* the corresponding start control record for this entry) is not "int64",
* invalid results may be returned.
*
* @param[out] value integer value (if successful)
* @return True on success, false on error
*/
bool GetInteger(int64_t* value) const;
/**
* Decodes a data record as a float. Note if the data type (as indicated in
* the corresponding start control record for this entry) is not "float",
* invalid results may be returned.
*
* @param[out] value float value (if successful)
* @return True on success, false on error
*/
bool GetFloat(float* value) const;
/**
* Decodes a data record as a double. Note if the data type (as indicated in
* the corresponding start control record for this entry) is not "double",
* invalid results may be returned.
*
* @param[out] value double value (if successful)
* @return True on success, false on error
*/
bool GetDouble(double* value) const;
/**
* Decodes a data record as a string. Note if the data type (as indicated in
* the corresponding start control record for this entry) is not "string",
* invalid results may be returned.
*
* @param[out] value string value
* @return True (never fails)
*/
bool GetString(std::string_view* value) const;
/**
* Decodes a data record as a boolean array. Note if the data type (as
* indicated in the corresponding start control record for this entry) is not
* "boolean[]", invalid results may be returned.
*
* @param[out] arr boolean array
* @return True (never fails)
*/
bool GetBooleanArray(std::vector<int>* arr) const;
/**
* Decodes a data record as an integer array. Note if the data type (as
* indicated in the corresponding start control record for this entry) is not
* "int64[]", invalid results may be returned.
*
* @param[out] arr integer array (if successful)
* @return True on success, false on error
*/
bool GetIntegerArray(std::vector<int64_t>* arr) const;
/**
* Decodes a data record as a float array. Note if the data type (as
* indicated in the corresponding start control record for this entry) is not
* "float[]", invalid results may be returned.
*
* @param[out] arr float array (if successful)
* @return True on success, false on error
*/
bool GetFloatArray(std::vector<float>* arr) const;
/**
* Decodes a data record as a double array. Note if the data type (as
* indicated in the corresponding start control record for this entry) is not
* "double[]", invalid results may be returned.
*
* @param[out] arr double array (if successful)
* @return True on success, false on error
*/
bool GetDoubleArray(std::vector<double>* arr) const;
/**
* Decodes a data record as a string array. Note if the data type (as
* indicated in the corresponding start control record for this entry) is not
* "string[]", invalid results may be returned.
*
* @param[out] arr string array (if successful)
* @return True on success, false on error
*/
bool GetStringArray(std::vector<std::string_view>* arr) const;
private:
int64_t m_timestamp{0};
wpi::span<const uint8_t> m_data;
int m_entry{-1};
};
class DataLogReader;
/** DataLogReader iterator. */
class DataLogIterator {
public:
using iterator_category = std::forward_iterator_tag;
using value_type = DataLogRecord;
using pointer = const value_type*;
using reference = const value_type&;
DataLogIterator(const DataLogReader* reader, size_t pos)
: m_reader{reader}, m_pos{pos} {}
bool operator==(const DataLogIterator& oth) const {
return m_reader == oth.m_reader && m_pos == oth.m_pos;
}
bool operator!=(const DataLogIterator& oth) const {
return !this->operator==(oth);
}
bool operator<(const DataLogIterator& oth) const { return m_pos < oth.m_pos; }
bool operator>(const DataLogIterator& oth) const {
return !this->operator<(oth) && !this->operator==(oth);
}
bool operator<=(const DataLogIterator& oth) const {
return !this->operator>(oth);
}
bool operator>=(const DataLogIterator& oth) const {
return !this->operator<(oth);
}
DataLogIterator& operator++();
DataLogIterator operator++(int) {
DataLogIterator tmp = *this;
++*this;
return tmp;
}
reference operator*() const;
pointer operator->() const { return &this->operator*(); }
private:
const DataLogReader* m_reader;
size_t m_pos;
mutable bool m_valid = false;
mutable DataLogRecord m_value;
};
/** Data log reader (reads logs written by the DataLog class). */
class DataLogReader {
friend class DataLogIterator;
public:
using iterator = DataLogIterator;
/** Constructs from a memory buffer. */
explicit DataLogReader(std::unique_ptr<MemoryBuffer> buffer);
/** Returns true if the data log is valid (e.g. has a valid header). */
explicit operator bool() const { return IsValid(); }
/** Returns true if the data log is valid (e.g. has a valid header). */
bool IsValid() const;
/**
* Gets the data log version. Returns 0 if data log is invalid.
*
* @return Version number; most significant byte is major, least significant
* is minor (so version 1.0 will be 0x0100)
*/
uint16_t GetVersion() const;
/**
* Gets the extra header data.
*
* @return Extra header data
*/
std::string_view GetExtraHeader() const;
/**
* Gets the buffer identifier, typically the filename.
*
* @return Identifier string
*/
std::string_view GetBufferIdentifier() const {
return m_buf ? m_buf->GetBufferIdentifier() : "Invalid";
}
/** Returns iterator to first record. */
iterator begin() const;
/** Returns end iterator. */
iterator end() const { return DataLogIterator{this, SIZE_MAX}; }
private:
std::unique_ptr<MemoryBuffer> m_buf;
bool GetRecord(size_t* pos, DataLogRecord* out) const;
bool GetNextRecord(size_t* pos) const;
};
inline DataLogIterator& DataLogIterator::operator++() {
if (!m_reader->GetNextRecord(&m_pos)) {
m_pos = SIZE_MAX;
}
m_valid = false;
return *this;
}
inline DataLogIterator::reference DataLogIterator::operator*() const {
if (!m_valid) {
size_t pos = m_pos;
if (m_reader->GetRecord(&pos, &m_value)) {
m_valid = true;
}
}
return m_value;
}
} // namespace wpi::log