From 4c7fe73f69cfa4b5e73558964d6f2c80e10e9196 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Sun, 21 Jul 2024 13:08:15 -0700 Subject: [PATCH] [wpiutil] DataLog: Add last value and change detection (#6674) Update() checks/updates the last value and appends only if changed. GetLastValue() gets the last value. Also add OutputStream support to Java DataLogWriter. --- .../util/datalog/BooleanArrayLogEntry.java | 77 ++ .../first/util/datalog/BooleanLogEntry.java | 56 ++ .../edu/wpi/first/util/datalog/DataLog.java | 12 +- .../wpi/first/util/datalog/DataLogJNI.java | 18 + .../wpi/first/util/datalog/DataLogWriter.java | 53 ++ .../util/datalog/DoubleArrayLogEntry.java | 77 ++ .../first/util/datalog/DoubleLogEntry.java | 56 ++ .../util/datalog/FloatArrayLogEntry.java | 77 ++ .../wpi/first/util/datalog/FloatLogEntry.java | 56 ++ .../util/datalog/IntegerArrayLogEntry.java | 77 ++ .../first/util/datalog/IntegerLogEntry.java | 56 ++ .../first/util/datalog/ProtobufLogEntry.java | 123 +++ .../wpi/first/util/datalog/RawLogEntry.java | 205 ++++- .../util/datalog/StringArrayLogEntry.java | 77 ++ .../first/util/datalog/StringLogEntry.java | 54 ++ .../util/datalog/StructArrayLogEntry.java | 247 +++++- .../first/util/datalog/StructLogEntry.java | 115 +++ wpiutil/src/main/native/cpp/DataLog.cpp | 102 +++ .../src/main/native/cpp/jni/DataLogJNI.cpp | 66 +- wpiutil/src/main/native/include/wpi/DataLog.h | 604 ++++++++++++- .../main/native/include/wpi/DataLogWriter.h | 7 + .../wpi/first/util/datalog/DataLogTest.java | 799 ++++++++++++++++++ wpiutil/src/test/native/cpp/DataLogTest.cpp | 424 +++++++++- 23 files changed, 3398 insertions(+), 40 deletions(-) create mode 100644 wpiutil/src/test/java/edu/wpi/first/util/datalog/DataLogTest.java diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanArrayLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanArrayLogEntry.java index 21ac596359..76b85f518c 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanArrayLogEntry.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanArrayLogEntry.java @@ -4,6 +4,8 @@ package edu.wpi.first.util.datalog; +import java.util.Arrays; + /** Log array of boolean values. */ public class BooleanArrayLogEntry extends DataLogEntry { /** The data type for boolean array values. */ @@ -71,4 +73,79 @@ public class BooleanArrayLogEntry extends DataLogEntry { public void append(boolean[] value) { m_log.appendBooleanArray(m_entry, value, 0); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + * @param timestamp Time stamp (0 to indicate now) + */ + public synchronized void update(boolean[] value, long timestamp) { + if (!equalsLast(value)) { + copyToLast(value); + append(value, timestamp); + } + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + */ + public void update(boolean[] value) { + update(value, 0); + } + + /** + * Gets whether there is a last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return True if last value exists, false otherwise. + */ + public synchronized boolean hasLastValue() { + return m_lastValue != null; + } + + /** + * Gets the last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return Last value, or null if none. + */ + @SuppressWarnings("PMD.ReturnEmptyCollectionRatherThanNull") + public synchronized boolean[] getLastValue() { + if (m_lastValue == null) { + return null; + } + return Arrays.copyOf(m_lastValue, m_lastValueLen); + } + + private boolean equalsLast(boolean[] value) { + if (m_lastValue == null || m_lastValueLen != value.length) { + return false; + } + return Arrays.equals(m_lastValue, 0, value.length, value, 0, value.length); + } + + private void copyToLast(boolean[] value) { + if (m_lastValue == null || m_lastValue.length < value.length) { + m_lastValue = Arrays.copyOf(value, value.length); + } else { + System.arraycopy(value, 0, m_lastValue, 0, value.length); + } + m_lastValueLen = value.length; + } + + private boolean[] m_lastValue; + private int m_lastValueLen; } diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanLogEntry.java index c413bfa42a..ba1783e161 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanLogEntry.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/BooleanLogEntry.java @@ -71,4 +71,60 @@ public class BooleanLogEntry extends DataLogEntry { public void append(boolean value) { m_log.appendBoolean(m_entry, value, 0); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + * @param timestamp Time stamp (0 to indicate now) + */ + public synchronized void update(boolean value, long timestamp) { + if (!m_hasLastValue || m_lastValue != value) { + m_lastValue = value; + m_hasLastValue = true; + append(value, timestamp); + } + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + */ + public void update(boolean value) { + update(value, 0); + } + + /** + * Gets whether there is a last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return True if last value exists, false otherwise. + */ + public synchronized boolean hasLastValue() { + return m_hasLastValue; + } + + /** + * Gets the last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return Last value, or false if none. + */ + public synchronized boolean getLastValue() { + return m_lastValue; + } + + private boolean m_hasLastValue; + private boolean m_lastValue; } diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLog.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLog.java index bec3a7bde6..4a89542cb7 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLog.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLog.java @@ -269,6 +269,12 @@ public class DataLog implements AutoCloseable { setMetadata(entry, metadata, 0); } + @Override + public void close() { + DataLogJNI.close(m_impl); + m_impl = 0; + } + /** * Appends a raw record to the log. * @@ -318,12 +324,6 @@ public class DataLog implements AutoCloseable { DataLogJNI.appendRaw(m_impl, entry, data, start, len, timestamp); } - @Override - public void close() { - DataLogJNI.close(m_impl); - m_impl = 0; - } - /** * Appends a boolean record to the log. * diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogJNI.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogJNI.java index ea040edb71..ee868931d9 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogJNI.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogJNI.java @@ -46,6 +46,14 @@ public class DataLogJNI extends WPIUtilJNI { */ static native long fgCreate(String filename, String extraHeader) throws IOException; + /** + * Create a new Data Log foreground writer to a memory buffer. + * + * @param extraHeader extra header data + * @return data log writer implementation handle + */ + static native long fgCreateMemory(String extraHeader); + /** * Explicitly flushes the log data to disk. * @@ -53,6 +61,16 @@ public class DataLogJNI extends WPIUtilJNI { */ static native void flush(long impl); + /** + * Flushes the log data to a memory buffer (only valid with fgCreateMemory data logs). + * + * @param impl data log background writer implementation handle + * @param buf output data buffer + * @param pos position in write buffer to start copying from + * @return Number of bytes written to buffer; 0 if no more to copy + */ + static native int copyWriteBuffer(long impl, byte[] buf, int pos); + /** * 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. diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogWriter.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogWriter.java index 2b11b9a4d2..058d6dd8a4 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogWriter.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DataLogWriter.java @@ -5,6 +5,7 @@ package edu.wpi.first.util.datalog; import java.io.IOException; +import java.io.OutputStream; /** A data log writer that flushes the data log to a file when flush() is called. */ public class DataLogWriter extends DataLog { @@ -17,6 +18,8 @@ public class DataLogWriter extends DataLog { */ public DataLogWriter(String filename, String extraHeader) throws IOException { super(DataLogJNI.fgCreate(filename, extraHeader)); + m_os = null; + m_buf = null; } /** @@ -28,4 +31,54 @@ public class DataLogWriter extends DataLog { public DataLogWriter(String filename) throws IOException { this(filename, ""); } + + /** + * Construct a new Data Log with an output stream. Prefer the filename version if possible; this + * is much slower! + * + * @param os output stream + * @param extraHeader extra header data + */ + public DataLogWriter(OutputStream os, String extraHeader) { + super(DataLogJNI.fgCreateMemory(extraHeader)); + m_os = os; + m_buf = new byte[kBufferSize]; + } + + /** + * Construct a new Data Log with an output stream. + * + * @param os output stream + */ + public DataLogWriter(OutputStream os) { + this(os, ""); + } + + /** Explicitly flushes the log data to disk. */ + @Override + public void flush() { + DataLogJNI.flush(m_impl); + if (m_os == null) { + return; + } + try { + int pos = 0; + for (; ; ) { + int qty = DataLogJNI.copyWriteBuffer(m_impl, m_buf, pos); + if (qty == 0) { + break; + } + pos += qty; + m_os.write(m_buf, 0, qty); + } + m_os.flush(); + } catch (IOException e) { + // ignore + } + } + + private static final int kBufferSize = 16 * 1024; + + private final OutputStream m_os; + private final byte[] m_buf; } diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleArrayLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleArrayLogEntry.java index 485a9c8c82..2fe528f1bd 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleArrayLogEntry.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleArrayLogEntry.java @@ -4,6 +4,8 @@ package edu.wpi.first.util.datalog; +import java.util.Arrays; + /** Log array of double values. */ public class DoubleArrayLogEntry extends DataLogEntry { /** The data type for double array values. */ @@ -71,4 +73,79 @@ public class DoubleArrayLogEntry extends DataLogEntry { public void append(double[] value) { m_log.appendDoubleArray(m_entry, value, 0); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + * @param timestamp Time stamp (0 to indicate now) + */ + public synchronized void update(double[] value, long timestamp) { + if (!equalsLast(value)) { + copyToLast(value); + append(value, timestamp); + } + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + */ + public void update(double[] value) { + update(value, 0); + } + + /** + * Gets whether there is a last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return True if last value exists, false otherwise. + */ + public synchronized boolean hasLastValue() { + return m_lastValue != null; + } + + /** + * Gets the last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return Last value, or false if none. + */ + @SuppressWarnings("PMD.ReturnEmptyCollectionRatherThanNull") + public synchronized double[] getLastValue() { + if (m_lastValue == null) { + return null; + } + return Arrays.copyOf(m_lastValue, m_lastValueLen); + } + + private boolean equalsLast(double[] value) { + if (m_lastValue == null || m_lastValueLen != value.length) { + return false; + } + return Arrays.equals(m_lastValue, 0, value.length, value, 0, value.length); + } + + private void copyToLast(double[] value) { + if (m_lastValue == null || m_lastValue.length < value.length) { + m_lastValue = Arrays.copyOf(value, value.length); + } else { + System.arraycopy(value, 0, m_lastValue, 0, value.length); + } + m_lastValueLen = value.length; + } + + private double[] m_lastValue; + private int m_lastValueLen; } diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleLogEntry.java index a089df2cd8..2484063aba 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleLogEntry.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/DoubleLogEntry.java @@ -71,4 +71,60 @@ public class DoubleLogEntry extends DataLogEntry { public void append(double value) { m_log.appendDouble(m_entry, value, 0); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + * @param timestamp Time stamp (0 to indicate now) + */ + public synchronized void update(double value, long timestamp) { + if (!m_hasLastValue || m_lastValue != value) { + m_lastValue = value; + m_hasLastValue = true; + append(value, timestamp); + } + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + */ + public void update(double value) { + update(value, 0); + } + + /** + * Gets whether there is a last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return True if last value exists, false otherwise. + */ + public synchronized boolean hasLastValue() { + return m_hasLastValue; + } + + /** + * Gets the last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return Last value, or 0 if none. + */ + public synchronized double getLastValue() { + return m_lastValue; + } + + boolean m_hasLastValue; + double m_lastValue; } diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatArrayLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatArrayLogEntry.java index be25970ffc..136b7e5b42 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatArrayLogEntry.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatArrayLogEntry.java @@ -4,6 +4,8 @@ package edu.wpi.first.util.datalog; +import java.util.Arrays; + /** Log array of float values. */ public class FloatArrayLogEntry extends DataLogEntry { /** The data type for float array values. */ @@ -71,4 +73,79 @@ public class FloatArrayLogEntry extends DataLogEntry { public void append(float[] value) { m_log.appendFloatArray(m_entry, value, 0); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + * @param timestamp Time stamp (0 to indicate now) + */ + public synchronized void update(float[] value, long timestamp) { + if (!equalsLast(value)) { + copyToLast(value); + append(value, timestamp); + } + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + */ + public void update(float[] value) { + update(value, 0); + } + + /** + * Gets whether there is a last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return True if last value exists, false otherwise. + */ + public synchronized boolean hasLastValue() { + return m_lastValue != null; + } + + /** + * Gets the last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return Last value, or false if none. + */ + @SuppressWarnings("PMD.ReturnEmptyCollectionRatherThanNull") + public synchronized float[] getLastValue() { + if (m_lastValue == null) { + return null; + } + return Arrays.copyOf(m_lastValue, m_lastValueLen); + } + + private boolean equalsLast(float[] value) { + if (m_lastValue == null || m_lastValueLen != value.length) { + return false; + } + return Arrays.equals(m_lastValue, 0, value.length, value, 0, value.length); + } + + private void copyToLast(float[] value) { + if (m_lastValue == null || m_lastValue.length < value.length) { + m_lastValue = Arrays.copyOf(value, value.length); + } else { + System.arraycopy(value, 0, m_lastValue, 0, value.length); + } + m_lastValueLen = value.length; + } + + private float[] m_lastValue; + private int m_lastValueLen; } diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatLogEntry.java index 28f83cb6a6..8cb02cfafd 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatLogEntry.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/FloatLogEntry.java @@ -71,4 +71,60 @@ public class FloatLogEntry extends DataLogEntry { public void append(float value) { m_log.appendFloat(m_entry, value, 0); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + * @param timestamp Time stamp (0 to indicate now) + */ + public synchronized void update(float value, long timestamp) { + if (!m_hasLastValue || m_lastValue != value) { + m_lastValue = value; + m_hasLastValue = true; + append(value, timestamp); + } + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + */ + public void update(float value) { + update(value, 0); + } + + /** + * Gets whether there is a last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return True if last value exists, false otherwise. + */ + public synchronized boolean hasLastValue() { + return m_hasLastValue; + } + + /** + * Gets the last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return Last value, or 0 if none. + */ + public synchronized float getLastValue() { + return m_lastValue; + } + + boolean m_hasLastValue; + float m_lastValue; } diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerArrayLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerArrayLogEntry.java index d2f8f0ea52..80e2be36d9 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerArrayLogEntry.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerArrayLogEntry.java @@ -4,6 +4,8 @@ package edu.wpi.first.util.datalog; +import java.util.Arrays; + /** Log array of integer values. */ public class IntegerArrayLogEntry extends DataLogEntry { /** The data type for integer array values. */ @@ -71,4 +73,79 @@ public class IntegerArrayLogEntry extends DataLogEntry { public void append(long[] value) { m_log.appendIntegerArray(m_entry, value, 0); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + * @param timestamp Time stamp (0 to indicate now) + */ + public synchronized void update(long[] value, long timestamp) { + if (!equalsLast(value)) { + copyToLast(value); + append(value, timestamp); + } + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + */ + public void update(long[] value) { + update(value, 0); + } + + /** + * Gets whether there is a last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return True if last value exists, false otherwise. + */ + public synchronized boolean hasLastValue() { + return m_lastValue != null; + } + + /** + * Gets the last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return Last value, or false if none. + */ + @SuppressWarnings("PMD.ReturnEmptyCollectionRatherThanNull") + public synchronized long[] getLastValue() { + if (m_lastValue == null) { + return null; + } + return Arrays.copyOf(m_lastValue, m_lastValueLen); + } + + private boolean equalsLast(long[] value) { + if (m_lastValue == null || m_lastValueLen != value.length) { + return false; + } + return Arrays.equals(m_lastValue, 0, value.length, value, 0, value.length); + } + + private void copyToLast(long[] value) { + if (m_lastValue == null || m_lastValue.length < value.length) { + m_lastValue = Arrays.copyOf(value, value.length); + } else { + System.arraycopy(value, 0, m_lastValue, 0, value.length); + } + m_lastValueLen = value.length; + } + + private long[] m_lastValue; + private int m_lastValueLen; } diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerLogEntry.java index 395a208f74..25f491cc03 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerLogEntry.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/IntegerLogEntry.java @@ -71,4 +71,60 @@ public class IntegerLogEntry extends DataLogEntry { public void append(long value) { m_log.appendInteger(m_entry, value, 0); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + * @param timestamp Time stamp (0 to indicate now) + */ + public synchronized void update(long value, long timestamp) { + if (!m_hasLastValue || m_lastValue != value) { + m_lastValue = value; + m_hasLastValue = true; + append(value, timestamp); + } + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + */ + public void update(long value) { + update(value, 0); + } + + /** + * Gets whether there is a last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return True if last value exists, false otherwise. + */ + public synchronized boolean hasLastValue() { + return m_hasLastValue; + } + + /** + * Gets the last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return Last value, or 0 if none. + */ + public synchronized long getLastValue() { + return m_lastValue; + } + + boolean m_hasLastValue; + long m_lastValue; } diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/ProtobufLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/ProtobufLogEntry.java index 9e7fa43b34..9108e56bbf 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/ProtobufLogEntry.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/ProtobufLogEntry.java @@ -20,6 +20,8 @@ public final class ProtobufLogEntry extends DataLogEntry { DataLog log, String name, Protobuf proto, String metadata, long timestamp) { super(log, name, proto.getTypeString(), metadata, timestamp); m_buf = ProtobufBuffer.create(proto); + m_immutable = proto.isImmutable(); + m_cloneable = proto.isCloneable(); log.addSchema(proto, timestamp); } @@ -113,5 +115,126 @@ public final class ProtobufLogEntry extends DataLogEntry { append(value, 0); } + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + * @param timestamp Time stamp (0 to indicate now) + */ + public void update(T value, long timestamp) { + try { + synchronized (m_buf) { + if (m_immutable || m_cloneable) { + if (value.equals(m_lastValue)) { + return; + } + try { + if (m_immutable) { + m_lastValue = value; + } else { + m_lastValue = m_buf.getProto().clone(value); + } + ByteBuffer bb = m_buf.write(value); + m_log.appendRaw(m_entry, bb, 0, bb.position(), timestamp); + return; + } catch (CloneNotSupportedException e) { + // fall through + } + } + doUpdate(m_buf.write(value), timestamp); + } + } catch (IOException e) { + // ignore + } + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + */ + public void update(T value) { + update(value, 0); + } + + /** + * Gets whether there is a last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return True if last value exists, false otherwise. + */ + public boolean hasLastValue() { + synchronized (m_buf) { + if (m_immutable) { + return m_lastValue != null; + } else if (m_cloneable && m_lastValue != null) { + return true; + } + return m_lastValueBuf != null; + } + } + + /** + * Gets the last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return Last value, or null if none. + */ + public T getLastValue() { + synchronized (m_buf) { + if (m_immutable) { + return m_lastValue; + } else if (m_cloneable && m_lastValue != null) { + try { + return m_buf.getProto().clone(m_lastValue); + } catch (CloneNotSupportedException e) { + // fall through + } + } + if (m_lastValueBuf == null) { + return null; + } + try { + T val = m_buf.read(m_lastValueBuf); + m_lastValueBuf.position(0); + return val; + } catch (IOException e) { + return null; + } + } + } + + private void doUpdate(ByteBuffer bb, long timestamp) { + int len = bb.position(); + bb.limit(len); + bb.position(0); + if (m_lastValueBuf == null || !bb.equals(m_lastValueBuf)) { + // update last value + if (m_lastValueBuf == null || m_lastValueBuf.limit() < len) { + m_lastValueBuf = ByteBuffer.allocate(len); + } + bb.get(m_lastValueBuf.array(), 0, len); + bb.position(0); + m_lastValueBuf.limit(len); + + // append to log + m_log.appendRaw(m_entry, bb, 0, len, timestamp); + } + } + private final ProtobufBuffer m_buf; + private ByteBuffer m_lastValueBuf; + private final boolean m_immutable; + private final boolean m_cloneable; + private T m_lastValue; } diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/RawLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/RawLogEntry.java index a9e3373335..9ab59e9659 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/RawLogEntry.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/RawLogEntry.java @@ -5,6 +5,7 @@ package edu.wpi.first.util.datalog; import java.nio.ByteBuffer; +import java.util.Arrays; /** Log raw byte array values. */ public class RawLogEntry extends DataLogEntry { @@ -125,7 +126,7 @@ public class RawLogEntry extends DataLogEntry { /** * Appends a record to the log. * - * @param value Data to record; will send from value.position() to value.capacity() + * @param value Data to record; will send from value.position() to value.limit() * @param timestamp Time stamp (0 to indicate now) */ public void append(ByteBuffer value, long timestamp) { @@ -135,7 +136,7 @@ public class RawLogEntry extends DataLogEntry { /** * Appends a record to the log. * - * @param value Data to record; will send from value.position() to value.capacity() + * @param value Data to record; will send from value.position() to value.limit() */ public void append(ByteBuffer value) { append(value, 0); @@ -163,4 +164,204 @@ public class RawLogEntry extends DataLogEntry { public void append(ByteBuffer value, int start, int len) { append(value, start, len, 0); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record; will send entire array contents + * @param timestamp Time stamp (0 to indicate now) + */ + public synchronized void update(byte[] value, long timestamp) { + if (!equalsLast(value, 0, value.length)) { + copyToLast(value, 0, value.length); + append(value, timestamp); + } + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record; will send entire array contents + */ + public void update(byte[] value) { + update(value, 0); + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Data to record + * @param start Start position of data (in byte array) + * @param len Length of data (must be less than or equal to value.length - offset) + * @param timestamp Time stamp (0 to indicate now) + */ + public synchronized void update(byte[] value, int start, int len, long timestamp) { + if (!equalsLast(value, start, len)) { + copyToLast(value, start, len); + append(value, start, len, timestamp); + } + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Data to record + * @param start Start position of data (in byte array) + * @param len Length of data (must be less than or equal to value.length - offset) + */ + public void update(byte[] value, int start, int len) { + update(value, start, len, 0); + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Data to record; will send from value.position() to value.limit() + * @param timestamp Time stamp (0 to indicate now) + */ + public synchronized void update(ByteBuffer value, long timestamp) { + if (!equalsLast(value)) { + int start = value.position(); + int len = value.limit() - start; + copyToLast(value, start, len); + append(value, start, len, timestamp); + } + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Data to record; will send from value.position() to value.limit() + */ + public void update(ByteBuffer value) { + update(value, 0); + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Data to record + * @param start Start position of data (in value buffer) + * @param len Length of data (must be less than or equal to value.length - offset) + * @param timestamp Time stamp (0 to indicate now) + */ + public synchronized void update(ByteBuffer value, int start, int len, long timestamp) { + if (!equalsLast(value, start, len)) { + copyToLast(value, start, len); + append(value, start, len, timestamp); + } + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Data to record + * @param start Start position of data (in value buffer) + * @param len Length of data (must be less than or equal to value.length - offset) + */ + public void update(ByteBuffer value, int start, int len) { + update(value, start, len, 0); + } + + /** + * Gets whether there is a last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return True if last value exists, false otherwise. + */ + public synchronized boolean hasLastValue() { + return m_lastValue != null; + } + + /** + * Gets the last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return Last value, or null if none. + */ + @SuppressWarnings("PMD.ReturnEmptyCollectionRatherThanNull") + public synchronized byte[] getLastValue() { + if (m_lastValue == null) { + return null; + } + return Arrays.copyOf(m_lastValue.array(), m_lastValue.limit()); + } + + private boolean equalsLast(byte[] value, int start, int len) { + if (m_lastValue == null || m_lastValue.limit() != len) { + return false; + } + return Arrays.equals(m_lastValue.array(), 0, len, value, start, start + len); + } + + @SuppressWarnings("PMD.SimplifyBooleanReturns") + private boolean equalsLast(ByteBuffer value) { + if (m_lastValue == null) { + return false; + } + return value.equals(m_lastValue); + } + + private boolean equalsLast(ByteBuffer value, int start, int len) { + if (m_lastValue == null || m_lastValue.limit() != len) { + return false; + } + int origpos = value.position(); + value.position(start); + int origlimit = value.limit(); + value.limit(start + len); + boolean eq = value.equals(m_lastValue); + value.position(origpos); + value.limit(origlimit); + return eq; + } + + private void copyToLast(byte[] value, int start, int len) { + if (m_lastValue == null || m_lastValue.limit() < len) { + m_lastValue = ByteBuffer.allocate(len); + } + System.arraycopy(value, start, m_lastValue.array(), 0, len); + m_lastValue.limit(len); + } + + private void copyToLast(ByteBuffer value, int start, int len) { + if (m_lastValue == null || m_lastValue.limit() < len) { + m_lastValue = ByteBuffer.allocate(len); + } + int origpos = value.position(); + value.position(start); + value.get(m_lastValue.array(), 0, len); + value.position(origpos); + m_lastValue.limit(len); + } + + private ByteBuffer m_lastValue; } diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringArrayLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringArrayLogEntry.java index f0a6dde737..0218502fc0 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringArrayLogEntry.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringArrayLogEntry.java @@ -4,6 +4,8 @@ package edu.wpi.first.util.datalog; +import java.util.Arrays; + /** Log array of string values. */ public class StringArrayLogEntry extends DataLogEntry { /** The data type for string array values. */ @@ -71,4 +73,79 @@ public class StringArrayLogEntry extends DataLogEntry { public void append(String[] value) { m_log.appendStringArray(m_entry, value, 0); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + * @param timestamp Time stamp (0 to indicate now) + */ + public synchronized void update(String[] value, long timestamp) { + if (!equalsLast(value)) { + copyToLast(value); + append(value, timestamp); + } + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + */ + public void update(String[] value) { + update(value, 0); + } + + /** + * Gets whether there is a last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return True if last value exists, false otherwise. + */ + public synchronized boolean hasLastValue() { + return m_lastValue != null; + } + + /** + * Gets the last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return Last value, or false if none. + */ + @SuppressWarnings("PMD.ReturnEmptyCollectionRatherThanNull") + public synchronized String[] getLastValue() { + if (m_lastValue == null) { + return null; + } + return Arrays.copyOf(m_lastValue, m_lastValueLen); + } + + private boolean equalsLast(String[] value) { + if (m_lastValue == null || m_lastValueLen != value.length) { + return false; + } + return Arrays.equals(m_lastValue, 0, value.length, value, 0, value.length); + } + + private void copyToLast(String[] value) { + if (m_lastValue == null || m_lastValue.length < value.length) { + m_lastValue = Arrays.copyOf(value, value.length); + } else { + System.arraycopy(value, 0, m_lastValue, 0, value.length); + } + m_lastValueLen = value.length; + } + + private String[] m_lastValue; + private int m_lastValueLen; } diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringLogEntry.java index 27c8aefef5..523abd6646 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringLogEntry.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StringLogEntry.java @@ -96,4 +96,58 @@ public class StringLogEntry extends DataLogEntry { public void append(String value) { m_log.appendString(m_entry, value, 0); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + * @param timestamp Time stamp (0 to indicate now) + */ + public synchronized void update(String value, long timestamp) { + if (m_lastValue == null || !value.equals(m_lastValue)) { + m_lastValue = value; + append(value, timestamp); + } + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + */ + public void update(String value) { + update(value, 0); + } + + /** + * Gets whether there is a last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return True if last value exists, false otherwise. + */ + public synchronized boolean hasLastValue() { + return m_lastValue != null; + } + + /** + * Gets the last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return Last value, or null if none. + */ + public synchronized String getLastValue() { + return m_lastValue; + } + + private String m_lastValue; } diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructArrayLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructArrayLogEntry.java index e208f2c0d0..43d69b10f1 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructArrayLogEntry.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructArrayLogEntry.java @@ -6,7 +6,9 @@ package edu.wpi.first.util.datalog; import edu.wpi.first.util.struct.Struct; import edu.wpi.first.util.struct.StructBuffer; +import java.lang.reflect.Array; import java.nio.ByteBuffer; +import java.util.Arrays; import java.util.Collection; /** @@ -19,6 +21,8 @@ public final class StructArrayLogEntry extends DataLogEntry { DataLog log, String name, Struct struct, String metadata, long timestamp) { super(log, name, struct.getTypeString() + "[]", metadata, timestamp); m_buf = StructBuffer.create(struct); + m_immutable = struct.isImmutable(); + m_cloneable = struct.isCloneable(); log.addSchema(struct, timestamp); } @@ -87,7 +91,7 @@ public final class StructArrayLogEntry extends DataLogEntry { * @param nelem number of elements */ public void reserve(int nelem) { - synchronized (this) { + synchronized (m_buf) { m_buf.reserve(nelem); } } @@ -99,7 +103,7 @@ public final class StructArrayLogEntry extends DataLogEntry { * @param timestamp Time stamp (0 to indicate now) */ public void append(T[] value, long timestamp) { - synchronized (this) { + synchronized (m_buf) { ByteBuffer bb = m_buf.writeArray(value); m_log.appendRaw(m_entry, bb, 0, bb.position(), timestamp); } @@ -121,7 +125,7 @@ public final class StructArrayLogEntry extends DataLogEntry { * @param timestamp Time stamp (0 to indicate now) */ public void append(Collection value, long timestamp) { - synchronized (this) { + synchronized (m_buf) { ByteBuffer bb = m_buf.writeArray(value); m_log.appendRaw(m_entry, bb, 0, bb.position(), timestamp); } @@ -136,5 +140,242 @@ public final class StructArrayLogEntry extends DataLogEntry { append(value, 0); } + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + * @param timestamp Time stamp (0 to indicate now) + */ + public void update(T[] value, long timestamp) { + synchronized (m_buf) { + if (m_immutable || m_cloneable) { + if (m_lastValue != null + && m_lastValueLen == value.length + && Arrays.equals(m_lastValue, 0, value.length, value, 0, value.length)) { + return; + } + try { + copyToLast(value); + ByteBuffer bb = m_buf.writeArray(value); + m_log.appendRaw(m_entry, bb, 0, bb.position(), timestamp); + return; + } catch (CloneNotSupportedException e) { + // fall through + } + } + doUpdate(m_buf.writeArray(value), timestamp); + } + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + */ + public void update(T[] value) { + update(value, 0); + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + * @param timestamp Time stamp (0 to indicate now) + */ + public void update(Collection value, long timestamp) { + synchronized (m_buf) { + if (m_immutable || m_cloneable) { + if (equalsLast(value)) { + return; + } + try { + copyToLast(value); + ByteBuffer bb = m_buf.writeArray(value); + m_log.appendRaw(m_entry, bb, 0, bb.position(), timestamp); + return; + } catch (CloneNotSupportedException e) { + // fall through + } + } + doUpdate(m_buf.writeArray(value), timestamp); + } + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + */ + public void update(Collection value) { + update(value, 0); + } + + /** + * Gets whether there is a last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return True if last value exists, false otherwise. + */ + public boolean hasLastValue() { + synchronized (m_buf) { + if (m_immutable) { + return m_lastValue != null; + } else if (m_cloneable && m_lastValue != null) { + return true; + } + return m_lastValueBuf != null; + } + } + + /** + * Gets the last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return Last value, or null if none. + */ + @SuppressWarnings("PMD.ReturnEmptyCollectionRatherThanNull") + public T[] getLastValue() { + synchronized (m_buf) { + if (m_immutable) { + if (m_lastValue == null) { + return null; + } + return Arrays.copyOf(m_lastValue, m_lastValueLen); + } else if (m_cloneable && m_lastValue != null) { + try { + return cloneArray(m_lastValue, m_lastValueLen); + } catch (CloneNotSupportedException e) { + // fall through + } + } + if (m_lastValueBuf == null) { + return null; + } + T[] val = m_buf.readArray(m_lastValueBuf); + m_lastValueBuf.position(0); + return val; + } + } + + private void doUpdate(ByteBuffer bb, long timestamp) { + int len = bb.position(); + bb.limit(len); + bb.position(0); + if (m_lastValueBuf == null || !bb.equals(m_lastValueBuf)) { + // update last value + if (m_lastValueBuf == null || m_lastValueBuf.limit() < len) { + m_lastValueBuf = ByteBuffer.allocate(len); + } + bb.get(m_lastValueBuf.array(), 0, len); + bb.position(0); + m_lastValueBuf.limit(len); + + // append to log + m_log.appendRaw(m_entry, bb, 0, len, timestamp); + } + } + + private boolean equalsLast(Collection arr) { + if (m_lastValue == null) { + return false; + } + if (m_lastValueLen != arr.size()) { + return false; + } + int i = 0; + for (T elem : arr) { + if (!m_lastValue[i].equals(elem)) { + return false; + } + i++; + } + return true; + } + + private T[] cloneArray(T[] in, int len) throws CloneNotSupportedException { + Struct s = m_buf.getStruct(); + @SuppressWarnings("unchecked") + T[] arr = (T[]) Array.newInstance(s.getTypeClass(), len); + for (int i = 0; i < len; i++) { + arr[i] = s.clone(in[i]); + } + return arr; + } + + private T[] cloneArray(Collection in) throws CloneNotSupportedException { + Struct s = m_buf.getStruct(); + @SuppressWarnings("unchecked") + T[] arr = (T[]) Array.newInstance(s.getTypeClass(), in.size()); + int i = 0; + for (T elem : in) { + arr[i++] = s.clone(elem); + } + return arr; + } + + private void copyToLast(T[] value) throws CloneNotSupportedException { + if (m_lastValue == null || m_lastValue.length < value.length) { + if (m_immutable) { + m_lastValue = Arrays.copyOf(value, value.length); + } else { + m_lastValue = cloneArray(value, value.length); + } + } else { + if (m_immutable) { + System.arraycopy(value, 0, m_lastValue, 0, value.length); + } else { + Struct s = m_buf.getStruct(); + for (int i = 0; i < value.length; i++) { + m_lastValue[i] = s.clone(value[i]); + } + } + } + m_lastValueLen = value.length; + } + + private void copyToLast(Collection value) throws CloneNotSupportedException { + if (m_lastValue == null || m_lastValue.length < value.size()) { + if (m_immutable) { + Struct s = m_buf.getStruct(); + @SuppressWarnings("unchecked") + T[] arr = (T[]) Array.newInstance(s.getTypeClass(), value.size()); + int i = 0; + for (T elem : value) { + arr[i++] = elem; + } + } else { + m_lastValue = cloneArray(value); + } + } else { + Struct s = m_buf.getStruct(); + int i = 0; + for (T elem : value) { + m_lastValue[i++] = m_immutable ? elem : s.clone(elem); + } + } + m_lastValueLen = value.size(); + } + private final StructBuffer m_buf; + private ByteBuffer m_lastValueBuf; + private final boolean m_immutable; + private final boolean m_cloneable; + private T[] m_lastValue; + private int m_lastValueLen; } diff --git a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructLogEntry.java b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructLogEntry.java index 0d09182315..c7bdf0a858 100644 --- a/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructLogEntry.java +++ b/wpiutil/src/main/java/edu/wpi/first/util/datalog/StructLogEntry.java @@ -18,6 +18,8 @@ public final class StructLogEntry extends DataLogEntry { DataLog log, String name, Struct struct, String metadata, long timestamp) { super(log, name, struct.getTypeString(), metadata, timestamp); m_buf = StructBuffer.create(struct); + m_immutable = struct.isImmutable(); + m_cloneable = struct.isCloneable(); log.addSchema(struct, timestamp); } @@ -102,5 +104,118 @@ public final class StructLogEntry extends DataLogEntry { append(value, 0); } + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + * @param timestamp Time stamp (0 to indicate now) + */ + public void update(T value, long timestamp) { + synchronized (m_buf) { + if (m_immutable || m_cloneable) { + if (value.equals(m_lastValue)) { + return; + } + try { + if (m_immutable) { + m_lastValue = value; + } else { + m_lastValue = m_buf.getStruct().clone(value); + } + ByteBuffer bb = m_buf.write(value); + m_log.appendRaw(m_entry, bb, 0, bb.position(), timestamp); + return; + } catch (CloneNotSupportedException e) { + // fall through + } + } + doUpdate(m_buf.write(value), timestamp); + } + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + *

Note: the last value is local to this class instance; using update() with two instances + * pointing to the same underlying log entry name will likely result in unexpected results. + * + * @param value Value to record + */ + public void update(T value) { + update(value, 0); + } + + /** + * Gets whether there is a last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return True if last value exists, false otherwise. + */ + public boolean hasLastValue() { + synchronized (m_buf) { + if (m_immutable) { + return m_lastValue != null; + } else if (m_cloneable && m_lastValue != null) { + return true; + } + return m_lastValueBuf != null; + } + } + + /** + * Gets the last value. + * + *

Note: the last value is local to this class instance and updated only with update(), not + * append(). + * + * @return Last value, or null if none. + */ + public T getLastValue() { + synchronized (m_buf) { + if (m_immutable) { + return m_lastValue; + } else if (m_cloneable && m_lastValue != null) { + try { + return m_buf.getStruct().clone(m_lastValue); + } catch (CloneNotSupportedException e) { + // fall through + } + } + if (m_lastValueBuf == null) { + return null; + } + T val = m_buf.read(m_lastValueBuf); + m_lastValueBuf.position(0); + return val; + } + } + + private void doUpdate(ByteBuffer bb, long timestamp) { + int len = bb.position(); + bb.limit(len); + bb.position(0); + if (m_lastValueBuf == null || !bb.equals(m_lastValueBuf)) { + // update last value + if (m_lastValueBuf == null || m_lastValueBuf.limit() < len) { + m_lastValueBuf = ByteBuffer.allocate(len); + } + bb.get(m_lastValueBuf.array(), 0, len); + bb.position(0); + m_lastValueBuf.limit(len); + + // append to log + m_log.appendRaw(m_entry, bb, 0, len, timestamp); + } + } + private final StructBuffer m_buf; + private ByteBuffer m_lastValueBuf; + private final boolean m_immutable; + private final boolean m_cloneable; + private T m_lastValue; } diff --git a/wpiutil/src/main/native/cpp/DataLog.cpp b/wpiutil/src/main/native/cpp/DataLog.cpp index 5234c8e705..dd717cc6c0 100644 --- a/wpiutil/src/main/native/cpp/DataLog.cpp +++ b/wpiutil/src/main/native/cpp/DataLog.cpp @@ -4,6 +4,7 @@ #include "wpi/DataLog.h" +#include #include #include #include @@ -618,6 +619,107 @@ void DataLog::AppendStringArray(int entry, } } +template +inline bool UpdateImpl(std::optional>& lastValue, + std::span data) { + if (!lastValue || !std::equal(data.begin(), data.end(), lastValue->begin(), + lastValue->end())) { + if (lastValue) { + lastValue->assign(data.begin(), data.end()); + } else { + lastValue = std::vector{data.begin(), data.end()}; + } + return true; + } + return false; +} + +template +inline bool UpdateImpl(std::optional>& lastValue, + std::span data) { + if (!lastValue || !std::equal(data.begin(), data.end(), lastValue->begin(), + lastValue->end(), [](auto a, auto b) { + return a == static_cast(b); + })) { + if (lastValue) { + lastValue->assign(data.begin(), data.end()); + } else { + lastValue = std::vector{data.begin(), data.end()}; + } + return true; + } + return false; +} + +void RawLogEntry::Update(std::span data, int64_t timestamp) { + std::scoped_lock lock{m_mutex}; + if (UpdateImpl(m_lastValue, data)) { + Append(data, timestamp); + } +} + +void BooleanArrayLogEntry::Update(std::span arr, + int64_t timestamp) { + std::scoped_lock lock{m_mutex}; + if (UpdateImpl(m_lastValue, arr)) { + Append(arr, timestamp); + } +} + +void BooleanArrayLogEntry::Update(std::span arr, int64_t timestamp) { + std::scoped_lock lock{m_mutex}; + if (UpdateImpl(m_lastValue, arr)) { + Append(arr, timestamp); + } +} + +void BooleanArrayLogEntry::Update(std::span arr, + int64_t timestamp) { + std::scoped_lock lock{m_mutex}; + if (UpdateImpl(m_lastValue, arr)) { + Append(arr, timestamp); + } +} + +void IntegerArrayLogEntry::Update(std::span arr, + int64_t timestamp) { + std::scoped_lock lock{m_mutex}; + if (UpdateImpl(m_lastValue, arr)) { + Append(arr, timestamp); + } +} + +void FloatArrayLogEntry::Update(std::span arr, int64_t timestamp) { + std::scoped_lock lock{m_mutex}; + if (UpdateImpl(m_lastValue, arr)) { + Append(arr, timestamp); + } +} + +void DoubleArrayLogEntry::Update(std::span arr, + int64_t timestamp) { + std::scoped_lock lock{m_mutex}; + if (UpdateImpl(m_lastValue, arr)) { + Append(arr, timestamp); + } +} + +void StringArrayLogEntry::Update(std::span arr, + int64_t timestamp) { + std::scoped_lock lock{m_mutex}; + if (UpdateImpl(m_lastValue, arr)) { + Append(arr, timestamp); + } +} + +void StringArrayLogEntry::Update(std::span arr, + int64_t timestamp) { + std::scoped_lock lock{m_mutex}; + if (UpdateImpl(m_lastValue, arr)) { + Append(arr, timestamp); + } +} + extern "C" { void WPI_DataLog_Release(struct WPI_DataLog* datalog) { diff --git a/wpiutil/src/main/native/cpp/jni/DataLogJNI.cpp b/wpiutil/src/main/native/cpp/jni/DataLogJNI.cpp index 9ac6b3b58a..e98c13f8df 100644 --- a/wpiutil/src/main/native/cpp/jni/DataLogJNI.cpp +++ b/wpiutil/src/main/native/cpp/jni/DataLogJNI.cpp @@ -4,6 +4,7 @@ #include +#include #include #include @@ -18,6 +19,18 @@ using namespace wpi::java; using namespace wpi::log; +namespace { +class buf_ostream : public wpi::raw_uvector_ostream { + private: + std::vector data; + + public: + buf_ostream() : raw_uvector_ostream{data} {} + + void clear() { data.clear(); } +}; +} // namespace + extern "C" { /* @@ -96,6 +109,24 @@ Java_edu_wpi_first_util_datalog_DataLogJNI_fgCreate return reinterpret_cast(writer); } +/* + * Class: edu_wpi_first_util_datalog_DataLogJNI + * Method: fgCreateMemory + * Signature: (Ljava/lang/String;)J + */ +JNIEXPORT jlong JNICALL +Java_edu_wpi_first_util_datalog_DataLogJNI_fgCreateMemory + (JNIEnv* env, jclass, jstring extraHeader) +{ + if (!extraHeader) { + wpi::ThrowNullPointerException(env, "extraHeader is null"); + return 0; + } + auto writer = new DataLogWriter{std::make_unique(), + JStringRef{env, extraHeader}}; + return reinterpret_cast(writer); +} + /* * Class: edu_wpi_first_util_datalog_DataLogJNI * Method: flush @@ -109,7 +140,34 @@ Java_edu_wpi_first_util_datalog_DataLogJNI_flush wpi::ThrowNullPointerException(env, "impl is null"); return; } - reinterpret_cast(impl)->Flush(); + reinterpret_cast(impl)->Flush(); +} + +/* + * Class: edu_wpi_first_util_datalog_DataLogJNI + * Method: copyWriteBuffer + * Signature: (J[BI)I + */ +JNIEXPORT jint JNICALL +Java_edu_wpi_first_util_datalog_DataLogJNI_copyWriteBuffer + (JNIEnv* env, jclass, jlong impl, jbyteArray buf, jint start) +{ + if (impl == 0) { + wpi::ThrowNullPointerException(env, "impl is null"); + return 0; + } + auto writer = reinterpret_cast(impl); + writer->Flush(); + auto& stream = static_cast(writer->GetStream()); + JSpan jbuf{env, buf}; + auto arr = stream.array(); + if (start < 0 || static_cast(start) >= arr.size()) { + stream.clear(); + return 0; + } + size_t qty = (std::min)(jbuf.size(), arr.size() - start); + std::copy(arr.begin(), arr.begin() + qty, jbuf.begin()); + return qty; } /* @@ -125,7 +183,7 @@ Java_edu_wpi_first_util_datalog_DataLogJNI_pause wpi::ThrowNullPointerException(env, "impl is null"); return; } - reinterpret_cast(impl)->Pause(); + reinterpret_cast(impl)->Pause(); } /* @@ -141,7 +199,7 @@ Java_edu_wpi_first_util_datalog_DataLogJNI_resume wpi::ThrowNullPointerException(env, "impl is null"); return; } - reinterpret_cast(impl)->Resume(); + reinterpret_cast(impl)->Resume(); } /* @@ -157,7 +215,7 @@ Java_edu_wpi_first_util_datalog_DataLogJNI_stop wpi::ThrowNullPointerException(env, "impl is null"); return; } - reinterpret_cast(impl)->Stop(); + reinterpret_cast(impl)->Stop(); } /* diff --git a/wpiutil/src/main/native/include/wpi/DataLog.h b/wpiutil/src/main/native/include/wpi/DataLog.h index 9fa209b244..4834b65dd0 100644 --- a/wpiutil/src/main/native/include/wpi/DataLog.h +++ b/wpiutil/src/main/native/include/wpi/DataLog.h @@ -6,8 +6,10 @@ #include +#include #include #include +#include #include #include #include @@ -575,10 +577,60 @@ class DataLogEntry { int m_entry = 0; }; +template +class DataLogValueEntryImpl : public DataLogEntry { + protected: + DataLogValueEntryImpl() = default; + DataLogValueEntryImpl(DataLog& log, std::string_view name, + std::string_view type, std::string_view metadata = {}, + int64_t timestamp = 0) + : DataLogEntry{log, name, type, metadata, timestamp} {} + + public: + DataLogValueEntryImpl(DataLogValueEntryImpl&& rhs) + : DataLogEntry{std::move(rhs)} { + std::scoped_lock lock{rhs.m_mutex}; + m_lastValue = std::move(rhs.m_lastValue); + } + DataLogValueEntryImpl& operator=(DataLogValueEntryImpl&& rhs) { + DataLogEntry::operator=(std::move(rhs)); + std::scoped_lock lock{m_mutex, rhs.m_mutex}; + m_lastValue = std::move(rhs.m_lastValue); + return *this; + } + + /** + * Gets whether there is a last value. + * + * @note The last value is local to this class instance and updated only with + * Update(), not Append(). + * + * @return True if last value exists, false otherwise. + */ + bool HasLastValue() const { return m_lastValue.has_value(); } + + /** + * Gets the last value. + * + * @note The last value is local to this class instance and updated only with + * Update(), not Append(). + * + * @return Last value (empty if no last value) + */ + std::optional GetLastValue() const { + std::scoped_lock lock{m_mutex}; + return m_lastValue; + } + + protected: + mutable wpi::mutex m_mutex; + std::optional m_lastValue; +}; + /** * Log arbitrary byte data. */ -class RawLogEntry : public DataLogEntry { +class RawLogEntry : public DataLogValueEntryImpl> { public: static constexpr std::string_view kDataType = "raw"; @@ -590,7 +642,7 @@ class RawLogEntry : public DataLogEntry { : 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} {} + : DataLogValueEntryImpl{log, name, type, metadata, timestamp} {} /** * Appends a record to the log. @@ -601,12 +653,24 @@ class RawLogEntry : public DataLogEntry { void Append(std::span data, int64_t timestamp = 0) { m_log->AppendRaw(m_entry, data, timestamp); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param data Data to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(std::span data, int64_t timestamp = 0); }; /** * Log boolean values. */ -class BooleanLogEntry : public DataLogEntry { +class BooleanLogEntry : public DataLogValueEntryImpl { public: static constexpr std::string_view kDataType = "boolean"; @@ -615,7 +679,7 @@ class BooleanLogEntry : public DataLogEntry { : 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} {} + : DataLogValueEntryImpl{log, name, kDataType, metadata, timestamp} {} /** * Appends a record to the log. @@ -626,12 +690,30 @@ class BooleanLogEntry : public DataLogEntry { void Append(bool value, int64_t timestamp = 0) { m_log->AppendBoolean(m_entry, value, timestamp); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param value Value to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(bool value, int64_t timestamp = 0) { + std::scoped_lock lock{m_mutex}; + if (m_lastValue != value) { + m_lastValue = value; + Append(value, timestamp); + } + } }; /** * Log integer values. */ -class IntegerLogEntry : public DataLogEntry { +class IntegerLogEntry : public DataLogValueEntryImpl { public: static constexpr std::string_view kDataType = "int64"; @@ -640,7 +722,7 @@ class IntegerLogEntry : public DataLogEntry { : 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} {} + : DataLogValueEntryImpl{log, name, kDataType, metadata, timestamp} {} /** * Appends a record to the log. @@ -651,12 +733,30 @@ class IntegerLogEntry : public DataLogEntry { void Append(int64_t value, int64_t timestamp = 0) { m_log->AppendInteger(m_entry, value, timestamp); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param value Value to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(int64_t value, int64_t timestamp = 0) { + std::scoped_lock lock{m_mutex}; + if (m_lastValue != value) { + m_lastValue = value; + Append(value, timestamp); + } + } }; /** * Log float values. */ -class FloatLogEntry : public DataLogEntry { +class FloatLogEntry : public DataLogValueEntryImpl { public: static constexpr std::string_view kDataType = "float"; @@ -665,7 +765,7 @@ class FloatLogEntry : public DataLogEntry { : 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} {} + : DataLogValueEntryImpl{log, name, kDataType, metadata, timestamp} {} /** * Appends a record to the log. @@ -676,12 +776,30 @@ class FloatLogEntry : public DataLogEntry { void Append(float value, int64_t timestamp = 0) { m_log->AppendFloat(m_entry, value, timestamp); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param value Value to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(float value, int64_t timestamp = 0) { + std::scoped_lock lock{m_mutex}; + if (m_lastValue != value) { + m_lastValue = value; + Append(value, timestamp); + } + } }; /** * Log double values. */ -class DoubleLogEntry : public DataLogEntry { +class DoubleLogEntry : public DataLogValueEntryImpl { public: static constexpr std::string_view kDataType = "double"; @@ -690,7 +808,7 @@ class DoubleLogEntry : public DataLogEntry { : 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} {} + : DataLogValueEntryImpl{log, name, kDataType, metadata, timestamp} {} /** * Appends a record to the log. @@ -701,12 +819,30 @@ class DoubleLogEntry : public DataLogEntry { void Append(double value, int64_t timestamp = 0) { m_log->AppendDouble(m_entry, value, timestamp); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param value Value to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(double value, int64_t timestamp = 0) { + std::scoped_lock lock{m_mutex}; + if (m_lastValue != value) { + m_lastValue = value; + Append(value, timestamp); + } + } }; /** * Log string values. */ -class StringLogEntry : public DataLogEntry { +class StringLogEntry : public DataLogValueEntryImpl { public: static constexpr const char* kDataType = "string"; @@ -718,7 +854,7 @@ class StringLogEntry : public DataLogEntry { : 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} {} + : DataLogValueEntryImpl{log, name, type, metadata, timestamp} {} /** * Appends a record to the log. @@ -729,12 +865,30 @@ class StringLogEntry : public DataLogEntry { void Append(std::string_view value, int64_t timestamp = 0) { m_log->AppendString(m_entry, value, timestamp); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param value Value to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(std::string value, int64_t timestamp = 0) { + std::scoped_lock lock{m_mutex}; + if (m_lastValue != value) { + m_lastValue = value; + Append(value, timestamp); + } + } }; /** * Log array of boolean values. */ -class BooleanArrayLogEntry : public DataLogEntry { +class BooleanArrayLogEntry : public DataLogValueEntryImpl> { public: static constexpr const char* kDataType = "boolean[]"; @@ -744,7 +898,7 @@ class BooleanArrayLogEntry : public DataLogEntry { : 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} {} + : DataLogValueEntryImpl{log, name, kDataType, metadata, timestamp} {} /** * Appends a record to the log. For find functions to work, timestamp @@ -796,12 +950,77 @@ class BooleanArrayLogEntry : public DataLogEntry { void Append(std::span arr, int64_t timestamp = 0) { m_log->AppendBooleanArray(m_entry, arr, timestamp); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param arr Values to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(std::span arr, int64_t timestamp = 0); + + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param arr Values to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(std::initializer_list arr, int64_t timestamp = 0) { + Update(std::span{arr.begin(), arr.end()}, timestamp); + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param arr Values to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(std::span arr, int64_t timestamp = 0); + + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param arr Values to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(std::initializer_list arr, int64_t timestamp = 0) { + Update(std::span{arr.begin(), arr.end()}, timestamp); + } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param arr Values to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(std::span arr, int64_t timestamp = 0); }; /** * Log array of integer values. */ -class IntegerArrayLogEntry : public DataLogEntry { +class IntegerArrayLogEntry + : public DataLogValueEntryImpl> { public: static constexpr const char* kDataType = "int64[]"; @@ -811,7 +1030,7 @@ class IntegerArrayLogEntry : public DataLogEntry { : 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} {} + : DataLogValueEntryImpl{log, name, kDataType, metadata, timestamp} {} /** * Appends a record to the log. @@ -832,12 +1051,38 @@ class IntegerArrayLogEntry : public DataLogEntry { void Append(std::initializer_list arr, int64_t timestamp = 0) { Append({arr.begin(), arr.end()}, timestamp); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param arr Values to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(std::span arr, int64_t timestamp = 0); + + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param arr Values to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(std::initializer_list arr, int64_t timestamp = 0) { + Update({arr.begin(), arr.end()}, timestamp); + } }; /** * Log array of float values. */ -class FloatArrayLogEntry : public DataLogEntry { +class FloatArrayLogEntry : public DataLogValueEntryImpl> { public: static constexpr const char* kDataType = "float[]"; @@ -846,7 +1091,7 @@ class FloatArrayLogEntry : public DataLogEntry { : 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} {} + : DataLogValueEntryImpl{log, name, kDataType, metadata, timestamp} {} /** * Appends a record to the log. @@ -867,12 +1112,38 @@ class FloatArrayLogEntry : public DataLogEntry { void Append(std::initializer_list arr, int64_t timestamp = 0) { Append({arr.begin(), arr.end()}, timestamp); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param arr Values to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(std::span arr, int64_t timestamp = 0); + + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param arr Values to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(std::initializer_list arr, int64_t timestamp = 0) { + Update({arr.begin(), arr.end()}, timestamp); + } }; /** * Log array of double values. */ -class DoubleArrayLogEntry : public DataLogEntry { +class DoubleArrayLogEntry : public DataLogValueEntryImpl> { public: static constexpr const char* kDataType = "double[]"; @@ -882,7 +1153,7 @@ class DoubleArrayLogEntry : public DataLogEntry { : 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} {} + : DataLogValueEntryImpl{log, name, kDataType, metadata, timestamp} {} /** * Appends a record to the log. @@ -903,12 +1174,39 @@ class DoubleArrayLogEntry : public DataLogEntry { void Append(std::initializer_list arr, int64_t timestamp = 0) { Append({arr.begin(), arr.end()}, timestamp); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param arr Values to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(std::span arr, int64_t timestamp = 0); + + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param arr Values to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(std::initializer_list arr, int64_t timestamp = 0) { + Update({arr.begin(), arr.end()}, timestamp); + } }; /** * Log array of string values. */ -class StringArrayLogEntry : public DataLogEntry { +class StringArrayLogEntry + : public DataLogValueEntryImpl> { public: static constexpr const char* kDataType = "string[]"; @@ -918,7 +1216,7 @@ class StringArrayLogEntry : public DataLogEntry { : 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} {} + : DataLogValueEntryImpl{log, name, kDataType, metadata, timestamp} {} /** * Appends a record to the log. @@ -951,6 +1249,46 @@ class StringArrayLogEntry : public DataLogEntry { Append(std::span{arr.begin(), arr.end()}, timestamp); } + + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param arr Values to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(std::span arr, int64_t timestamp = 0); + + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param arr Values to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(std::span arr, int64_t timestamp = 0); + + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param arr Values to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(std::initializer_list arr, + int64_t timestamp = 0) { + Update(std::span{arr.begin(), arr.end()}, + timestamp); + } }; /** @@ -974,6 +1312,19 @@ class StructLogEntry : public DataLogEntry { m_entry = log.Start(name, S::GetTypeString(info...), metadata, timestamp); } + StructLogEntry(StructLogEntry&& rhs) + : DataLogEntry{std::move(rhs)}, m_info{std::move(rhs.m_info)} { + std::scoped_lock lock{rhs.m_mutex}; + m_lastValue = std::move(rhs.m_lastValue); + } + StructLogEntry& operator=(StructLogEntry&& rhs) { + DataLogEntry::operator=(std::move(rhs)); + m_info = std::move(rhs.m_info); + std::scoped_lock lock{m_mutex, rhs.m_mutex}; + m_lastValue = std::move(rhs.m_lastValue); + return *this; + } + /** * Appends a record to the log. * @@ -995,7 +1346,74 @@ class StructLogEntry : public DataLogEntry { m_log->AppendRaw(m_entry, buf, timestamp); } + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param data Data to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(const T& data, int64_t timestamp = 0) { + if constexpr (sizeof...(I) == 0) { + if constexpr (wpi::is_constexpr([] { S::GetSize(); })) { + uint8_t buf[S::GetSize()]; + S::Pack(buf, data); + std::scoped_lock lock{m_mutex}; + if (m_lastValue.empty() || + !std::equal(buf, buf + S::GetSize(), m_lastValue.begin(), + m_lastValue.end())) { + m_lastValue.assign(buf, buf + S::GetSize()); + m_log->AppendRaw(m_entry, buf, timestamp); + } + return; + } + } + wpi::SmallVector buf; + buf.resize_for_overwrite(std::apply(S::GetSize, m_info)); + std::apply([&](const I&... info) { S::Pack(buf, data, info...); }, m_info); + std::scoped_lock lock{m_mutex}; + if (m_lastValue.empty() || + !std::equal(buf.begin(), buf.end(), m_lastValue.begin(), + m_lastValue.end())) { + m_lastValue.assign(buf.begin(), buf.end()); + m_log->AppendRaw(m_entry, buf, timestamp); + } + } + + /** + * Gets whether there is a last value. + * + * @note The last value is local to this class instance and updated only with + * Update(), not Append(). + * + * @return True if last value exists, false otherwise. + */ + bool HasLastValue() const { return !m_lastValue.empty(); } + + /** + * Gets the last value. + * + * @note The last value is local to this class instance and updated only with + * Update(), not Append(). + * + * @return Last value (empty if no last value) + */ + std::optional GetLastValue() const { + std::scoped_lock lock{m_mutex}; + if (m_lastValue.empty()) { + return std::nullopt; + } + return std::apply( + [&](const I&... info) { return S::Unpack(m_lastValue, info...); }, + m_info); + } + private: + mutable wpi::mutex m_mutex; + std::vector m_lastValue; [[no_unique_address]] std::tuple m_info; }; @@ -1024,6 +1442,22 @@ class StructArrayLogEntry : public DataLogEntry { metadata, timestamp); } + StructArrayLogEntry(StructArrayLogEntry&& rhs) + : DataLogEntry{std::move(rhs)}, + m_buf{std::move(rhs.m_buf)}, + m_info{std::move(rhs.m_info)} { + std::scoped_lock lock{rhs.m_mutex}; + m_lastValue = std::move(rhs.m_lastValue); + } + StructArrayLogEntry& operator=(StructArrayLogEntry&& rhs) { + DataLogEntry::operator=(std::move(rhs)); + m_buf = std::move(rhs.m_buf); + m_info = std::move(rhs.m_info); + std::scoped_lock lock{m_mutex, rhs.m_mutex}; + m_lastValue = std::move(rhs.m_lastValue); + return *this; + } + /** * Appends a record to the log. * @@ -1063,8 +1497,81 @@ class StructArrayLogEntry : public DataLogEntry { m_info); } + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param data Data to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(std::span data, int64_t timestamp = 0) { + std::apply( + [&](const I&... info) { + m_buf.Write( + data, + [&](auto bytes) { + std::scoped_lock lock{m_mutex}; + if (!m_lastValue.has_value()) { + m_lastValue = std::vector(bytes.begin(), bytes.end()); + m_log->AppendRaw(m_entry, bytes, timestamp); + } else if (!std::equal(bytes.begin(), bytes.end(), + m_lastValue->begin(), + m_lastValue->end())) { + m_lastValue->assign(bytes.begin(), bytes.end()); + m_log->AppendRaw(m_entry, bytes, timestamp); + } + }, + info...); + }, + m_info); + } + + /** + * Gets whether there is a last value. + * + * @note The last value is local to this class instance and updated only with + * Update(), not Append(). + * + * @return True if last value exists, false otherwise. + */ + bool HasLastValue() const { return m_lastValue.has_value(); } + + /** + * Gets the last value. + * + * @note The last value is local to this class instance and updated only with + * Update(), not Append(). + * + * @return Last value (empty if no last value) + */ + std::optional> GetLastValue() const { + std::scoped_lock lock{m_mutex}; + if (!m_lastValue.has_value()) { + return std::nullopt; + } + auto& lastValue = m_lastValue.value(); + size_t size = std::apply(S::GetSize, m_info); + std::vector rv; + rv.reserve(lastValue.size() / size); + for (auto in = lastValue.begin(), end = lastValue.end(); in < end; + in += size) { + std::apply( + [&](const I&... info) { + rv.emplace_back(S::Unpack( + std::span{std::to_address(in), size}, info...)); + }, + m_info); + } + return rv; + } + private: + mutable wpi::mutex m_mutex; StructArrayBuffer m_buf; + std::optional> m_lastValue; [[no_unique_address]] std::tuple m_info; }; @@ -1102,9 +1609,60 @@ class ProtobufLogEntry : public DataLogEntry { m_log->AppendRaw(m_entry, buf, timestamp); } + /** + * Updates the last value and appends a record to the log if it has changed. + * + * @note The last value is local to this class instance; using Update() with + * two instances pointing to the same underlying log entry name will likely + * result in unexpected results. + * + * @param data Data to record + * @param timestamp Time stamp (may be 0 to indicate now) + */ + void Update(const T& data, int64_t timestamp = 0) { + std::scoped_lock lock{m_mutex}; + wpi::SmallVector buf; + m_msg.Pack(buf, data); + if (!m_lastValue.has_value()) { + m_lastValue = std::vector(buf.begin(), buf.end()); + m_log->AppendRaw(m_entry, buf, timestamp); + } else if (!std::equal(buf.begin(), buf.end(), m_lastValue->begin(), + m_lastValue->end())) { + m_lastValue->assign(buf.begin(), buf.end()); + m_log->AppendRaw(m_entry, buf, timestamp); + } + } + + /** + * Gets whether there is a last value. + * + * @note The last value is local to this class instance and updated only with + * Update(), not Append(). + * + * @return True if last value exists, false otherwise. + */ + bool HasLastValue() const { return m_lastValue.has_value(); } + + /** + * Gets the last value. + * + * @note The last value is local to this class instance and updated only with + * Update(), not Append(). + * + * @return Last value (empty if no last value) + */ + std::optional GetLastValue() const { + std::scoped_lock lock{m_mutex}; + if (!m_lastValue.has_value()) { + return std::nullopt; + } + return m_msg.Unpack(m_lastValue); + } + private: - wpi::mutex m_mutex; + mutable wpi::mutex m_mutex; ProtobufMessage m_msg; + std::optional> m_lastValue; }; } // namespace wpi::log diff --git a/wpiutil/src/main/native/include/wpi/DataLogWriter.h b/wpiutil/src/main/native/include/wpi/DataLogWriter.h index 15e106e94f..f69c7b5b25 100644 --- a/wpiutil/src/main/native/include/wpi/DataLogWriter.h +++ b/wpiutil/src/main/native/include/wpi/DataLogWriter.h @@ -81,6 +81,13 @@ class DataLogWriter final : public DataLog { */ void Stop() final; + /** + * Gets the output stream. + * + * @return output stream + */ + wpi::raw_ostream& GetStream() { return *m_os; } + private: bool BufferFull() final; diff --git a/wpiutil/src/test/java/edu/wpi/first/util/datalog/DataLogTest.java b/wpiutil/src/test/java/edu/wpi/first/util/datalog/DataLogTest.java new file mode 100644 index 0000000000..c84d2846c4 --- /dev/null +++ b/wpiutil/src/test/java/edu/wpi/first/util/datalog/DataLogTest.java @@ -0,0 +1,799 @@ +// 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. + +package edu.wpi.first.util.datalog; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import edu.wpi.first.util.struct.Struct; +import edu.wpi.first.util.struct.StructSerializable; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.Objects; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("AvoidEscapedUnicodeCharacters") +class DataLogTest { + static class ImmutableThingStruct implements Struct { + @Override + public Class getTypeClass() { + return ImmutableThing.class; + } + + @Override + public String getTypeString() { + return "struct:Thing"; + } + + @Override + public int getSize() { + return 1; + } + + @Override + public String getSchema() { + return "uint8 value"; + } + + @Override + public ImmutableThing unpack(ByteBuffer bb) { + return new ImmutableThing(bb.get()); + } + + @Override + public void pack(ByteBuffer bb, ImmutableThing value) { + bb.put(value.m_x); + } + + @Override + public boolean isImmutable() { + return true; + } + } + + static class ImmutableThing implements StructSerializable { + byte m_x; + + ImmutableThing(int x) { + m_x = (byte) x; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof ImmutableThing other && other.m_x == m_x; + } + + @Override + public int hashCode() { + return Objects.hash(m_x); + } + + public static final ImmutableThingStruct struct = new ImmutableThingStruct(); + } + + static class CloneableThingStruct implements Struct { + @Override + public Class getTypeClass() { + return CloneableThing.class; + } + + @Override + public String getTypeString() { + return "struct:Thing"; + } + + @Override + public int getSize() { + return 1; + } + + @Override + public String getSchema() { + return "uint8 value"; + } + + @Override + public CloneableThing unpack(ByteBuffer bb) { + return new CloneableThing(bb.get()); + } + + @Override + public void pack(ByteBuffer bb, CloneableThing value) { + bb.put(value.m_x); + } + + @Override + public boolean isCloneable() { + return true; + } + + @Override + public CloneableThing clone(CloneableThing obj) throws CloneNotSupportedException { + return obj.clone(); + } + } + + @SuppressWarnings("MemberName") + private static int cloneCalls; + + static class CloneableThing implements StructSerializable, Cloneable { + byte m_x; + + CloneableThing(int x) { + m_x = (byte) x; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof CloneableThing other && other.m_x == m_x; + } + + @Override + public int hashCode() { + return Objects.hash(m_x); + } + + @Override + public CloneableThing clone() throws CloneNotSupportedException { + CloneableThing thing = (CloneableThing) super.clone(); + cloneCalls++; + return thing; + } + + public static final CloneableThingStruct struct = new CloneableThingStruct(); + } + + static class ThingStruct implements Struct { + @Override + public Class getTypeClass() { + return Thing.class; + } + + @Override + public String getTypeString() { + return "struct:Thing"; + } + + @Override + public int getSize() { + return 1; + } + + @Override + public String getSchema() { + return "uint8 value"; + } + + @Override + public Thing unpack(ByteBuffer bb) { + return new Thing(bb.get()); + } + + @Override + public void pack(ByteBuffer bb, Thing value) { + bb.put(value.m_x); + } + } + + static class Thing implements StructSerializable { + byte m_x; + + Thing(int x) { + m_x = (byte) x; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Thing other && other.m_x == m_x; + } + + @Override + public int hashCode() { + return Objects.hash(m_x); + } + + public static final ThingStruct struct = new ThingStruct(); + } + + @SuppressWarnings("MemberName") + private ByteArrayOutputStream data; + + @SuppressWarnings("MemberName") + private DataLog log; + + @BeforeEach + public void init() { + data = new ByteArrayOutputStream(); + log = new DataLogWriter(data); + cloneCalls = 0; + } + + @AfterEach + public void shutdown() { + log.close(); + } + + @Test + void testSimpleInt() { + int entry = log.start("test", "int64", "", 1); + log.appendInteger(entry, 1, 2); + log.flush(); + assertEquals(54, data.size()); + } + + @Test + void testBooleanAppend() { + BooleanLogEntry entry = new BooleanLogEntry(log, "a", 5); + entry.append(false, 7); + log.flush(); + assertEquals(46, data.size()); + } + + @Test + void testBooleanUpdate() { + BooleanLogEntry entry = new BooleanLogEntry(log, "a", 5); + assertFalse(entry.hasLastValue()); + entry.update(false, 7); + log.flush(); + assertEquals(46, data.size()); + assertTrue(entry.hasLastValue()); + assertFalse(entry.getLastValue()); + entry.update(false, 8); + log.flush(); + assertEquals(46, data.size()); + entry.update(true, 9); + log.flush(); + assertEquals(51, data.size()); + assertTrue(entry.hasLastValue()); + assertTrue(entry.getLastValue()); + } + + @Test + void testIntegerAppend() { + IntegerLogEntry entry = new IntegerLogEntry(log, "a", 5); + entry.append(5, 7); + log.flush(); + assertEquals(51, data.size()); + } + + @Test + void testIntegerUpdate() { + IntegerLogEntry entry = new IntegerLogEntry(log, "a", 5); + assertFalse(entry.hasLastValue()); + entry.update(0, 7); + log.flush(); + assertEquals(51, data.size()); + assertTrue(entry.hasLastValue()); + assertEquals(0, entry.getLastValue()); + entry.update(0, 8); + log.flush(); + assertEquals(51, data.size()); + entry.update(2, 9); + log.flush(); + assertEquals(63, data.size()); + assertTrue(entry.hasLastValue()); + assertEquals(2, entry.getLastValue()); + } + + @Test + void testFloatAppend() { + FloatLogEntry entry = new FloatLogEntry(log, "a", 5); + entry.append(5.0f, 7); + log.flush(); + assertEquals(47, data.size()); + } + + @Test + void testFloatUpdate() { + FloatLogEntry entry = new FloatLogEntry(log, "a", 5); + assertFalse(entry.hasLastValue()); + entry.update(0.0f, 7); + log.flush(); + assertEquals(47, data.size()); + assertTrue(entry.hasLastValue()); + assertEquals(0.0f, entry.getLastValue()); + entry.update(0.0f, 8); + log.flush(); + assertEquals(47, data.size()); + entry.update(0.1f, 9); + log.flush(); + assertEquals(55, data.size()); + assertTrue(entry.hasLastValue()); + assertEquals(0.1f, entry.getLastValue()); + } + + @Test + void testDoubleAppend() { + DoubleLogEntry entry = new DoubleLogEntry(log, "a", 5); + entry.append(5.0, 7); + log.flush(); + assertEquals(52, data.size()); + } + + @Test + void testDoubleUpdate() { + DoubleLogEntry entry = new DoubleLogEntry(log, "a", 5); + assertFalse(entry.hasLastValue()); + entry.update(0.0, 7); + log.flush(); + assertEquals(52, data.size()); + assertTrue(entry.hasLastValue()); + assertEquals(0.0, entry.getLastValue()); + entry.update(0.0, 8); + log.flush(); + assertEquals(52, data.size()); + entry.update(0.1, 9); + log.flush(); + assertEquals(64, data.size()); + assertTrue(entry.hasLastValue()); + assertEquals(0.1, entry.getLastValue()); + } + + @Test + void testStringAppend() { + StringLogEntry entry = new StringLogEntry(log, "a", 5); + entry.append("x", 7); + log.flush(); + assertEquals(45, data.size()); + } + + @Test + void testStringUpdate() { + StringLogEntry entry = new StringLogEntry(log, "a", 5); + assertFalse(entry.hasLastValue()); + + entry.update("x", 7); + log.flush(); + assertEquals(45, data.size()); + assertTrue(entry.hasLastValue()); + assertEquals("x", entry.getLastValue()); + + entry.update("x", 8); + log.flush(); + assertEquals(45, data.size()); + + entry.update("y", 9); + log.flush(); + assertEquals(50, data.size()); + assertTrue(entry.hasLastValue()); + assertEquals("y", entry.getLastValue()); + + entry.update("yy", 10); + log.flush(); + assertEquals(56, data.size()); + assertTrue(entry.hasLastValue()); + assertEquals("yy", entry.getLastValue()); + + entry.update("", 11); + log.flush(); + assertEquals(60, data.size()); + assertTrue(entry.hasLastValue()); + assertEquals("", entry.getLastValue()); + } + + @Test + void testRawAppend() { + RawLogEntry entry = new RawLogEntry(log, "a", 5); + entry.append(new byte[] {5}, 7); + log.flush(); + assertEquals(42, data.size()); + } + + @Test + void testRawUpdate() { + RawLogEntry entry = new RawLogEntry(log, "a", 5); + assertFalse(entry.hasLastValue()); + + entry.update(new byte[] {5}, 7); + log.flush(); + assertEquals(42, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new byte[] {5}, entry.getLastValue()); + + entry.update(new byte[] {5}, 8); + log.flush(); + assertEquals(42, data.size()); + + entry.update(new byte[] {6}, 9); + log.flush(); + assertEquals(47, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new byte[] {6}, entry.getLastValue()); + + entry.update(new byte[] {6, 6}, 10); + log.flush(); + assertEquals(53, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new byte[] {6, 6}, entry.getLastValue()); + + entry.update(new byte[] {}, 11); + log.flush(); + assertEquals(57, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new byte[] {}, entry.getLastValue()); + } + + @Test + void testBooleanArrayAppendEmpty() { + BooleanArrayLogEntry entry = new BooleanArrayLogEntry(log, "a", 5); + entry.append(new boolean[] {}, 7); + log.flush(); + assertEquals(47, data.size()); + } + + @Test + void testBooleanArrayAppend() { + BooleanArrayLogEntry entry = new BooleanArrayLogEntry(log, "a", 5); + entry.append(new boolean[] {false}, 7); + log.flush(); + assertEquals(48, data.size()); + } + + @Test + void testBooleanArrayUpdate() { + BooleanArrayLogEntry entry = new BooleanArrayLogEntry(log, "a", 5); + assertFalse(entry.hasLastValue()); + entry.update(new boolean[] {false}, 7); + log.flush(); + assertEquals(48, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new boolean[] {false}, entry.getLastValue()); + entry.update(new boolean[] {false}, 8); + log.flush(); + assertEquals(48, data.size()); + entry.update(new boolean[] {true}, 9); + log.flush(); + assertEquals(53, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new boolean[] {true}, entry.getLastValue()); + entry.update(new boolean[] {}, 10); + log.flush(); + assertEquals(57, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new boolean[] {}, entry.getLastValue()); + } + + @Test + void testIntegerArrayAppendEmpty() { + IntegerArrayLogEntry entry = new IntegerArrayLogEntry(log, "a", 5); + entry.append(new long[] {}, 7); + log.flush(); + assertEquals(45, data.size()); + } + + @Test + void testIntegerArrayAppend() { + IntegerArrayLogEntry entry = new IntegerArrayLogEntry(log, "a", 5); + entry.append(new long[] {1}, 7); + log.flush(); + assertEquals(53, data.size()); + } + + @Test + void testIntegerArrayUpdate() { + IntegerArrayLogEntry entry = new IntegerArrayLogEntry(log, "a", 5); + assertFalse(entry.hasLastValue()); + entry.update(new long[] {1}, 7); + log.flush(); + assertEquals(53, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new long[] {1}, entry.getLastValue()); + entry.update(new long[] {1}, 8); + log.flush(); + assertEquals(53, data.size()); + entry.update(new long[] {2}, 9); + log.flush(); + assertEquals(65, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new long[] {2}, entry.getLastValue()); + entry.update(new long[] {}, 10); + log.flush(); + assertEquals(69, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new long[] {}, entry.getLastValue()); + } + + @Test + void testDoubleArrayAppendEmpty() { + DoubleArrayLogEntry entry = new DoubleArrayLogEntry(log, "a", 5); + entry.append(new double[] {}, 7); + log.flush(); + assertEquals(46, data.size()); + } + + @Test + void testDoubleArrayAppend() { + DoubleArrayLogEntry entry = new DoubleArrayLogEntry(log, "a", 5); + entry.append(new double[] {1.0}, 7); + log.flush(); + assertEquals(54, data.size()); + } + + @Test + void testDoubleArrayUpdate() { + DoubleArrayLogEntry entry = new DoubleArrayLogEntry(log, "a", 5); + assertFalse(entry.hasLastValue()); + entry.update(new double[] {1.0}, 7); + log.flush(); + assertEquals(54, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new double[] {1.0}, entry.getLastValue()); + entry.update(new double[] {1.0}, 8); + log.flush(); + assertEquals(54, data.size()); + entry.update(new double[] {2.0}, 9); + log.flush(); + assertEquals(66, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new double[] {2}, entry.getLastValue()); + entry.update(new double[] {}, 10); + log.flush(); + assertEquals(70, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new double[] {}, entry.getLastValue()); + } + + @Test + void testFloatArrayAppendEmpty() { + FloatArrayLogEntry entry = new FloatArrayLogEntry(log, "a", 5); + entry.append(new float[] {}, 7); + log.flush(); + assertEquals(45, data.size()); + } + + @Test + void testFloatArrayAppend() { + FloatArrayLogEntry entry = new FloatArrayLogEntry(log, "a", 5); + entry.append(new float[] {1.0f}, 7); + log.flush(); + assertEquals(49, data.size()); + } + + @Test + void testFloatArrayUpdate() { + FloatArrayLogEntry entry = new FloatArrayLogEntry(log, "a", 5); + assertFalse(entry.hasLastValue()); + entry.update(new float[] {1.0f}, 7); + log.flush(); + assertEquals(49, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new float[] {1.0f}, entry.getLastValue()); + entry.update(new float[] {1.0f}, 8); + log.flush(); + assertEquals(49, data.size()); + entry.update(new float[] {2.0f}, 9); + log.flush(); + assertEquals(57, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new float[] {2.0f}, entry.getLastValue()); + entry.update(new float[] {}, 10); + log.flush(); + assertEquals(61, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new float[] {}, entry.getLastValue()); + } + + @Test + void testStringArrayAppendEmpty() { + StringArrayLogEntry entry = new StringArrayLogEntry(log, "a", 5); + entry.append(new String[] {}, 7); + entry.append(new String[] {}, 7); + log.flush(); + assertEquals(58, data.size()); + } + + @Test + void testStringArrayAppend() { + StringArrayLogEntry entry = new StringArrayLogEntry(log, "a", 5); + entry.append(new String[] {"x"}, 7); + log.flush(); + assertEquals(55, data.size()); + } + + @Test + void testStringArrayUpdate() { + StringArrayLogEntry entry = new StringArrayLogEntry(log, "a", 5); + assertFalse(entry.hasLastValue()); + entry.update(new String[] {"x"}, 7); + log.flush(); + assertEquals(55, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new String[] {"x"}, entry.getLastValue()); + entry.update(new String[] {"x"}, 8); + log.flush(); + assertEquals(55, data.size()); + entry.update(new String[] {"y"}, 9); + log.flush(); + assertEquals(68, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new String[] {"y"}, entry.getLastValue()); + entry.update(new String[] {}, 10); + log.flush(); + assertEquals(76, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new String[] {}, entry.getLastValue()); + } + + @Test + void testStruct() { + StructLogEntry entry = StructLogEntry.create(log, "a", Thing.struct, 5); + entry.append(new Thing((byte) 1), 6); + entry.append(new Thing((byte) 0), 7); + } + + @Test + void testStructUpdate() { + StructLogEntry entry = StructLogEntry.create(log, "a", Thing.struct, 5); + assertFalse(entry.hasLastValue()); + + entry.update(new Thing(1), 7); + log.flush(); + assertEquals(120, data.size()); + assertTrue(entry.hasLastValue()); + assertEquals(new Thing(1), entry.getLastValue()); + + entry.update(new Thing(1), 8); + log.flush(); + assertEquals(120, data.size()); + + entry.update(new Thing(2), 9); + log.flush(); + assertEquals(125, data.size()); + assertTrue(entry.hasLastValue()); + assertEquals(new Thing(2), entry.getLastValue()); + } + + @Test + void testCloneableStructUpdate() { + StructLogEntry entry = + StructLogEntry.create(log, "a", CloneableThing.struct, 5); + assertFalse(entry.hasLastValue()); + + entry.update(new CloneableThing(1), 7); + assertEquals(1, cloneCalls); + log.flush(); + assertEquals(120, data.size()); + assertTrue(entry.hasLastValue()); + assertEquals(new CloneableThing(1), entry.getLastValue()); + assertEquals(2, cloneCalls); + + entry.update(new CloneableThing(1), 8); + assertEquals(2, cloneCalls); + log.flush(); + assertEquals(120, data.size()); + + entry.update(new CloneableThing(2), 9); + assertEquals(3, cloneCalls); + log.flush(); + assertEquals(125, data.size()); + assertTrue(entry.hasLastValue()); + assertEquals(new CloneableThing(2), entry.getLastValue()); + assertEquals(4, cloneCalls); + } + + @Test + void testImmutableStructUpdate() { + StructLogEntry entry = + StructLogEntry.create(log, "a", ImmutableThing.struct, 5); + assertFalse(entry.hasLastValue()); + + entry.update(new ImmutableThing(1), 7); + log.flush(); + assertEquals(120, data.size()); + assertTrue(entry.hasLastValue()); + assertEquals(new ImmutableThing(1), entry.getLastValue()); + + entry.update(new ImmutableThing(1), 8); + log.flush(); + assertEquals(120, data.size()); + + entry.update(new ImmutableThing(2), 9); + log.flush(); + assertEquals(125, data.size()); + assertTrue(entry.hasLastValue()); + assertEquals(new ImmutableThing(2), entry.getLastValue()); + } + + @Test + void testStructArrayUpdate() { + StructArrayLogEntry entry = StructArrayLogEntry.create(log, "a", Thing.struct, 5); + assertFalse(entry.hasLastValue()); + + entry.update(new Thing[] {new Thing(1), new Thing(2)}, 7); + log.flush(); + assertEquals(123, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new Thing[] {new Thing(1), new Thing(2)}, entry.getLastValue()); + + entry.update(new Thing[] {new Thing(1), new Thing(2)}, 8); + log.flush(); + assertEquals(123, data.size()); + + entry.update(new Thing[] {new Thing(1), new Thing(3)}, 9); + log.flush(); + assertEquals(129, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new Thing[] {new Thing(1), new Thing(3)}, entry.getLastValue()); + + entry.update(new Thing[] {}, 10); + log.flush(); + assertEquals(133, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new Thing[] {}, entry.getLastValue()); + } + + @Test + void testCloneableStructArrayUpdate() { + StructArrayLogEntry entry = + StructArrayLogEntry.create(log, "a", CloneableThing.struct, 5); + assertFalse(entry.hasLastValue()); + + entry.update(new CloneableThing[] {new CloneableThing(1), new CloneableThing(2)}, 7); + assertEquals(2, cloneCalls); + log.flush(); + assertEquals(123, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals( + new CloneableThing[] {new CloneableThing(1), new CloneableThing(2)}, entry.getLastValue()); + assertEquals(4, cloneCalls); + + entry.update(new CloneableThing[] {new CloneableThing(1), new CloneableThing(2)}, 8); + assertEquals(4, cloneCalls); + log.flush(); + assertEquals(123, data.size()); + + entry.update(new CloneableThing[] {new CloneableThing(1), new CloneableThing(3)}, 9); + assertEquals(6, cloneCalls); + log.flush(); + assertEquals(129, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals( + new CloneableThing[] {new CloneableThing(1), new CloneableThing(3)}, entry.getLastValue()); + assertEquals(8, cloneCalls); + + entry.update(new CloneableThing[] {}, 10); + assertEquals(8, cloneCalls); + log.flush(); + assertEquals(133, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new CloneableThing[] {}, entry.getLastValue()); + assertEquals(8, cloneCalls); + } + + @Test + void testImmutableStructArrayUpdate() { + StructArrayLogEntry entry = + StructArrayLogEntry.create(log, "a", ImmutableThing.struct, 5); + assertFalse(entry.hasLastValue()); + + entry.update(new ImmutableThing[] {new ImmutableThing(1), new ImmutableThing(2)}, 7); + log.flush(); + assertEquals(123, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals( + new ImmutableThing[] {new ImmutableThing(1), new ImmutableThing(2)}, entry.getLastValue()); + + entry.update(new ImmutableThing[] {new ImmutableThing(1), new ImmutableThing(2)}, 8); + log.flush(); + assertEquals(123, data.size()); + + entry.update(new ImmutableThing[] {new ImmutableThing(1), new ImmutableThing(3)}, 9); + log.flush(); + assertEquals(129, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals( + new ImmutableThing[] {new ImmutableThing(1), new ImmutableThing(3)}, entry.getLastValue()); + + entry.update(new ImmutableThing[] {}, 10); + log.flush(); + assertEquals(133, data.size()); + assertTrue(entry.hasLastValue()); + assertArrayEquals(new ImmutableThing[] {}, entry.getLastValue()); + } +} diff --git a/wpiutil/src/test/native/cpp/DataLogTest.cpp b/wpiutil/src/test/native/cpp/DataLogTest.cpp index f4389abcb0..aa0a79188f 100644 --- a/wpiutil/src/test/native/cpp/DataLogTest.cpp +++ b/wpiutil/src/test/native/cpp/DataLogTest.cpp @@ -16,6 +16,10 @@ struct ThingA { int x = 0; }; +inline bool operator==(const ThingA& a, const ThingA& b) { + return a.x == b.x; +} + struct ThingB { int x = 0; }; @@ -130,10 +134,376 @@ class DataLogTest : public ::testing::Test { }; TEST_F(DataLogTest, SimpleInt) { - int entry = log.Start("test", "int64"); - log.AppendInteger(entry, 1, 0); + int entry = log.Start("test", "int64", "", 1); + log.AppendInteger(entry, 1, 2); + log.Flush(); + ASSERT_EQ(data.size(), 54u); +} + +TEST_F(DataLogTest, BooleanAppend) { + wpi::log::BooleanLogEntry entry{log, "a", 5}; + entry.Append(false, 7); + log.Flush(); + ASSERT_EQ(data.size(), 46u); +} + +TEST_F(DataLogTest, BooleanUpdate) { + wpi::log::BooleanLogEntry entry{log, "a", 5}; + ASSERT_FALSE(entry.GetLastValue().has_value()); + entry.Update(false, 7); + log.Flush(); + ASSERT_EQ(data.size(), 46u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), false); + entry.Update(false, 8); + log.Flush(); + ASSERT_EQ(data.size(), 46u); + entry.Update(true, 9); + log.Flush(); + ASSERT_EQ(data.size(), 51u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), true); +} + +TEST_F(DataLogTest, IntegerAppend) { + wpi::log::IntegerLogEntry entry{log, "a", 5}; + entry.Append(5, 7); + log.Flush(); + ASSERT_EQ(data.size(), 51u); +} + +TEST_F(DataLogTest, IntegerUpdate) { + wpi::log::IntegerLogEntry entry{log, "a", 5}; + ASSERT_FALSE(entry.GetLastValue().has_value()); + entry.Update(0, 7); + log.Flush(); + ASSERT_EQ(data.size(), 51u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), 0); + entry.Update(0, 8); + log.Flush(); + ASSERT_EQ(data.size(), 51u); + entry.Update(2, 9); + log.Flush(); + ASSERT_EQ(data.size(), 63u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), 2); +} + +TEST_F(DataLogTest, FloatAppend) { + wpi::log::FloatLogEntry entry{log, "a", 5}; + entry.Append(5.0, 7); + log.Flush(); + ASSERT_EQ(data.size(), 47u); +} + +TEST_F(DataLogTest, FloatUpdate) { + wpi::log::FloatLogEntry entry{log, "a", 5}; + ASSERT_FALSE(entry.GetLastValue().has_value()); + entry.Update(0.0f, 7); + log.Flush(); + ASSERT_EQ(data.size(), 47u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), 0.0f); + entry.Update(0.0f, 8); + log.Flush(); + ASSERT_EQ(data.size(), 47u); + entry.Update(0.1f, 9); + log.Flush(); + ASSERT_EQ(data.size(), 55u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), 0.1f); +} + +TEST_F(DataLogTest, DoubleAppend) { + wpi::log::DoubleLogEntry entry{log, "a", 5}; + entry.Append(5.0, 7); + log.Flush(); + ASSERT_EQ(data.size(), 52u); +} + +TEST_F(DataLogTest, DoubleUpdate) { + wpi::log::DoubleLogEntry entry{log, "a", 5}; + ASSERT_FALSE(entry.GetLastValue().has_value()); + entry.Update(0.0, 7); + log.Flush(); + ASSERT_EQ(data.size(), 52u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), 0.0); + entry.Update(0.0, 8); + log.Flush(); + ASSERT_EQ(data.size(), 52u); + entry.Update(0.1, 9); + log.Flush(); + ASSERT_EQ(data.size(), 64u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), 0.1); +} + +TEST_F(DataLogTest, StringAppend) { + wpi::log::StringLogEntry entry{log, "a", 5}; + entry.Append("x", 7); + log.Flush(); + ASSERT_EQ(data.size(), 45u); +} + +TEST_F(DataLogTest, StringUpdate) { + wpi::log::StringLogEntry entry{log, "a", 5}; + ASSERT_FALSE(entry.HasLastValue()); + + entry.Update("x", 7); + log.Flush(); + ASSERT_EQ(data.size(), 45u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), "x"); + + entry.Update("x", 8); + log.Flush(); + ASSERT_EQ(data.size(), 45u); + + entry.Update("y", 9); + log.Flush(); + ASSERT_EQ(data.size(), 50u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), "y"); + + entry.Update("yy", 10); + log.Flush(); + ASSERT_EQ(data.size(), 56u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), "yy"); + + entry.Update("", 11); + log.Flush(); + ASSERT_EQ(data.size(), 60u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), ""); +} + +TEST_F(DataLogTest, RawAppend) { + wpi::log::RawLogEntry entry{log, "a", 5}; + entry.Append({{5}}, 7); + log.Flush(); + ASSERT_EQ(data.size(), 42u); +} + +TEST_F(DataLogTest, RawUpdate) { + wpi::log::RawLogEntry entry{log, "a", 5}; + ASSERT_FALSE(entry.HasLastValue()); + + entry.Update({{5}}, 7); + log.Flush(); + ASSERT_EQ(data.size(), 42u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), std::vector{5}); + + entry.Update({{5}}, 8); + log.Flush(); + ASSERT_EQ(data.size(), 42u); + + entry.Update({{6}}, 9); + log.Flush(); + ASSERT_EQ(data.size(), 47u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), std::vector{6}); + + entry.Update({{6, 6}}, 9); + log.Flush(); + ASSERT_EQ(data.size(), 53u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), (std::vector{6, 6})); + + entry.Update(std::span{}, 10); + log.Flush(); + ASSERT_EQ(data.size(), 57u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), std::vector{}); +} + +TEST_F(DataLogTest, BooleanArrayAppendEmpty) { + wpi::log::BooleanArrayLogEntry entry{log, "a", 5}; + entry.Append(std::span{}, 7); + log.Flush(); + ASSERT_EQ(data.size(), 47u); +} + +TEST_F(DataLogTest, BooleanArrayAppend) { + wpi::log::BooleanArrayLogEntry entry{log, "a", 5}; + entry.Append({false}, 7); + log.Flush(); + ASSERT_EQ(data.size(), 48u); +} + +TEST_F(DataLogTest, BooleanArrayUpdate) { + wpi::log::BooleanArrayLogEntry entry{log, "a", 5}; + ASSERT_FALSE(entry.GetLastValue().has_value()); + entry.Update({false}, 7); + log.Flush(); + ASSERT_EQ(data.size(), 48u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), std::vector{false}); + entry.Update({false}, 8); + log.Flush(); + ASSERT_EQ(data.size(), 48u); + entry.Update({true}, 9); + log.Flush(); + ASSERT_EQ(data.size(), 53u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), std::vector{true}); + entry.Update(std::span{}, 10); + log.Flush(); + ASSERT_EQ(data.size(), 57u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), std::vector{}); +} + +TEST_F(DataLogTest, IntegerArrayAppendEmpty) { + wpi::log::IntegerArrayLogEntry entry{log, "a", 5}; + entry.Append(std::span{}, 7); + log.Flush(); + ASSERT_EQ(data.size(), 45u); +} + +TEST_F(DataLogTest, IntegerArrayAppend) { + wpi::log::IntegerArrayLogEntry entry{log, "a", 5}; + entry.Append({1}, 7); + log.Flush(); + ASSERT_EQ(data.size(), 53u); +} + +TEST_F(DataLogTest, IntegerArrayUpdate) { + wpi::log::IntegerArrayLogEntry entry{log, "a", 5}; + ASSERT_FALSE(entry.GetLastValue().has_value()); + entry.Update({1}, 7); + log.Flush(); + ASSERT_EQ(data.size(), 53u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), std::vector{1}); + entry.Update({1}, 8); + log.Flush(); + ASSERT_EQ(data.size(), 53u); + entry.Update({2}, 9); + log.Flush(); + ASSERT_EQ(data.size(), 65u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), std::vector{2}); + entry.Update(std::span{}, 10); + log.Flush(); + ASSERT_EQ(data.size(), 69u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), std::vector{}); +} + +TEST_F(DataLogTest, DoubleArrayAppendEmpty) { + wpi::log::DoubleArrayLogEntry entry{log, "a", 5}; + entry.Append(std::span{}, 7); + log.Flush(); + ASSERT_EQ(data.size(), 46u); +} + +TEST_F(DataLogTest, DoubleArrayAppend) { + wpi::log::DoubleArrayLogEntry entry{log, "a", 5}; + entry.Append({1.0}, 7); + log.Flush(); + ASSERT_EQ(data.size(), 54u); +} + +TEST_F(DataLogTest, DoubleArrayUpdate) { + wpi::log::DoubleArrayLogEntry entry{log, "a", 5}; + ASSERT_FALSE(entry.GetLastValue().has_value()); + entry.Update({1.0}, 7); + log.Flush(); + ASSERT_EQ(data.size(), 54u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), std::vector{1.0}); + entry.Update({1.0}, 8); + log.Flush(); + ASSERT_EQ(data.size(), 54u); + entry.Update({2.0}, 9); log.Flush(); ASSERT_EQ(data.size(), 66u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), std::vector{2}); + entry.Update(std::span{}, 10); + log.Flush(); + ASSERT_EQ(data.size(), 70u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), std::vector{}); +} + +TEST_F(DataLogTest, FloatArrayAppendEmpty) { + wpi::log::FloatArrayLogEntry entry{log, "a", 5}; + entry.Append(std::span{}, 7); + log.Flush(); + ASSERT_EQ(data.size(), 45u); +} + +TEST_F(DataLogTest, FloatArrayAppend) { + wpi::log::FloatArrayLogEntry entry{log, "a", 5}; + entry.Append({1.0f}, 7); + log.Flush(); + ASSERT_EQ(data.size(), 49u); +} + +TEST_F(DataLogTest, FloatArrayUpdate) { + wpi::log::FloatArrayLogEntry entry{log, "a", 5}; + ASSERT_FALSE(entry.GetLastValue().has_value()); + entry.Update({1.0f}, 7); + log.Flush(); + ASSERT_EQ(data.size(), 49u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), std::vector{1.0f}); + entry.Update({1.0f}, 8); + log.Flush(); + ASSERT_EQ(data.size(), 49u); + entry.Update({2.0f}, 9); + log.Flush(); + ASSERT_EQ(data.size(), 57u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), std::vector{2.0f}); + entry.Update(std::span{}, 10); + log.Flush(); + ASSERT_EQ(data.size(), 61u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), std::vector{}); +} + +TEST_F(DataLogTest, StringArrayAppendEmpty) { + wpi::log::StringArrayLogEntry entry{log, "a", 5}; + entry.Append(std::span{}, 7); + entry.Append(std::span{}, 7); + log.Flush(); + ASSERT_EQ(data.size(), 58u); +} + +TEST_F(DataLogTest, StringArrayAppend) { + wpi::log::StringArrayLogEntry entry{log, "a", 5}; + entry.Append({"x"}, 7); + log.Flush(); + ASSERT_EQ(data.size(), 55u); +} + +TEST_F(DataLogTest, StringArrayUpdate) { + wpi::log::StringArrayLogEntry entry{log, "a", 5}; + ASSERT_FALSE(entry.GetLastValue().has_value()); + entry.Update({"x"}, 7); + log.Flush(); + ASSERT_EQ(data.size(), 55u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), std::vector{"x"}); + entry.Update({"x"}, 8); + log.Flush(); + ASSERT_EQ(data.size(), 55u); + entry.Update({"y"}, 9); + log.Flush(); + ASSERT_EQ(data.size(), 68u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), std::vector{"y"}); + entry.Update(std::span{}, 10); + log.Flush(); + ASSERT_EQ(data.size(), 76u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), std::vector{}); } TEST_F(DataLogTest, StructA) { @@ -144,6 +514,27 @@ TEST_F(DataLogTest, StructA) { entry.Append(ThingA{}, 7); } +TEST_F(DataLogTest, StructUpdate) { + wpi::log::StructLogEntry entry{log, "a", 5}; + ASSERT_FALSE(entry.GetLastValue().has_value()); + + entry.Update(ThingA{}, 7); + log.Flush(); + ASSERT_EQ(data.size(), 122u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), ThingA{}); + + entry.Update(ThingA{}, 8); + log.Flush(); + ASSERT_EQ(data.size(), 122u); + + entry.Update(ThingA{.x = 1}, 9); + log.Flush(); + ASSERT_EQ(data.size(), 127u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), ThingA{.x = 1}); +} + TEST_F(DataLogTest, StructArrayA) { [[maybe_unused]] wpi::log::StructArrayLogEntry entry0; @@ -152,6 +543,35 @@ TEST_F(DataLogTest, StructArrayA) { entry.Append({{ThingA{}, ThingA{}}}, 7); } +TEST_F(DataLogTest, StructArrayUpdate) { + wpi::log::StructArrayLogEntry entry{log, "a", 5}; + ASSERT_FALSE(entry.GetLastValue().has_value()); + + entry.Update({{ThingA{}, ThingA{.x = 1}}}, 7); + log.Flush(); + ASSERT_EQ(data.size(), 125u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), + (std::vector{ThingA{}, ThingA{.x = 1}})); + + entry.Update({{ThingA{}, ThingA{.x = 1}}}, 8); + log.Flush(); + ASSERT_EQ(data.size(), 125u); + + entry.Update({{ThingA{}, ThingA{.x = 2}}}, 9); + log.Flush(); + ASSERT_EQ(data.size(), 131u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), + (std::vector{ThingA{}, ThingA{.x = 2}})); + + entry.Update(std::span{}, 10); + log.Flush(); + ASSERT_EQ(data.size(), 135u); + ASSERT_TRUE(entry.GetLastValue().has_value()); + ASSERT_EQ(entry.GetLastValue().value(), std::vector{}); +} + TEST_F(DataLogTest, StructFixedArrayA) { [[maybe_unused]] wpi::log::StructArrayLogEntry> entry0;