[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.
This commit is contained in:
Peter Johnson
2024-07-21 13:08:15 -07:00
committed by GitHub
parent 57205c8d15
commit 4c7fe73f69
23 changed files with 3398 additions and 40 deletions

View File

@@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -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.
*

View File

@@ -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.

View File

@@ -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;
}

View File

@@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -20,6 +20,8 @@ public final class ProtobufLogEntry<T> extends DataLogEntry {
DataLog log, String name, Protobuf<T, ?> 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<T> extends DataLogEntry {
append(value, 0);
}
/**
* Updates the last value and appends a record to the log if it has changed.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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<T, ?> m_buf;
private ByteBuffer m_lastValueBuf;
private final boolean m_immutable;
private final boolean m_cloneable;
private T m_lastValue;
}

View File

@@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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;
}

View File

@@ -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<T> extends DataLogEntry {
DataLog log, String name, Struct<T> 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<T> 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<T> 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<T> extends DataLogEntry {
* @param timestamp Time stamp (0 to indicate now)
*/
public void append(Collection<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);
}
@@ -136,5 +140,242 @@ public final class StructArrayLogEntry<T> extends DataLogEntry {
append(value, 0);
}
/**
* Updates the last value and appends a record to the log if it has changed.
*
* <p>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.
*
* <p>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.
*
* <p>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<T> 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.
*
* <p>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<T> value) {
update(value, 0);
}
/**
* Gets whether there is a last value.
*
* <p>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.
*
* <p>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<T> 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<T> 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<T> in) throws CloneNotSupportedException {
Struct<T> 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<T> 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<T> value) throws CloneNotSupportedException {
if (m_lastValue == null || m_lastValue.length < value.size()) {
if (m_immutable) {
Struct<T> 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<T> 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<T> m_buf;
private ByteBuffer m_lastValueBuf;
private final boolean m_immutable;
private final boolean m_cloneable;
private T[] m_lastValue;
private int m_lastValueLen;
}

View File

@@ -18,6 +18,8 @@ public final class StructLogEntry<T> extends DataLogEntry {
DataLog log, String name, Struct<T> 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<T> extends DataLogEntry {
append(value, 0);
}
/**
* Updates the last value and appends a record to the log if it has changed.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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<T> m_buf;
private ByteBuffer m_lastValueBuf;
private final boolean m_immutable;
private final boolean m_cloneable;
private T m_lastValue;
}

View File

@@ -4,6 +4,7 @@
#include "wpi/DataLog.h"
#include <algorithm>
#include <bit>
#include <cstdio>
#include <cstdlib>
@@ -618,6 +619,107 @@ void DataLog::AppendStringArray(int entry,
}
}
template <typename V1, typename V2>
inline bool UpdateImpl(std::optional<std::vector<V1>>& lastValue,
std::span<const V2> 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<V1>{data.begin(), data.end()};
}
return true;
}
return false;
}
template <typename V1>
inline bool UpdateImpl(std::optional<std::vector<V1>>& lastValue,
std::span<const bool> data) {
if (!lastValue || !std::equal(data.begin(), data.end(), lastValue->begin(),
lastValue->end(), [](auto a, auto b) {
return a == static_cast<bool>(b);
})) {
if (lastValue) {
lastValue->assign(data.begin(), data.end());
} else {
lastValue = std::vector<V1>{data.begin(), data.end()};
}
return true;
}
return false;
}
void RawLogEntry::Update(std::span<const uint8_t> data, int64_t timestamp) {
std::scoped_lock lock{m_mutex};
if (UpdateImpl(m_lastValue, data)) {
Append(data, timestamp);
}
}
void BooleanArrayLogEntry::Update(std::span<const bool> arr,
int64_t timestamp) {
std::scoped_lock lock{m_mutex};
if (UpdateImpl(m_lastValue, arr)) {
Append(arr, timestamp);
}
}
void BooleanArrayLogEntry::Update(std::span<const int> arr, int64_t timestamp) {
std::scoped_lock lock{m_mutex};
if (UpdateImpl(m_lastValue, arr)) {
Append(arr, timestamp);
}
}
void BooleanArrayLogEntry::Update(std::span<const uint8_t> arr,
int64_t timestamp) {
std::scoped_lock lock{m_mutex};
if (UpdateImpl(m_lastValue, arr)) {
Append(arr, timestamp);
}
}
void IntegerArrayLogEntry::Update(std::span<const int64_t> arr,
int64_t timestamp) {
std::scoped_lock lock{m_mutex};
if (UpdateImpl(m_lastValue, arr)) {
Append(arr, timestamp);
}
}
void FloatArrayLogEntry::Update(std::span<const float> arr, int64_t timestamp) {
std::scoped_lock lock{m_mutex};
if (UpdateImpl(m_lastValue, arr)) {
Append(arr, timestamp);
}
}
void DoubleArrayLogEntry::Update(std::span<const double> arr,
int64_t timestamp) {
std::scoped_lock lock{m_mutex};
if (UpdateImpl(m_lastValue, arr)) {
Append(arr, timestamp);
}
}
void StringArrayLogEntry::Update(std::span<const std::string> arr,
int64_t timestamp) {
std::scoped_lock lock{m_mutex};
if (UpdateImpl(m_lastValue, arr)) {
Append(arr, timestamp);
}
}
void StringArrayLogEntry::Update(std::span<const std::string_view> 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) {

View File

@@ -4,6 +4,7 @@
#include <jni.h>
#include <algorithm>
#include <vector>
#include <fmt/format.h>
@@ -18,6 +19,18 @@
using namespace wpi::java;
using namespace wpi::log;
namespace {
class buf_ostream : public wpi::raw_uvector_ostream {
private:
std::vector<uint8_t> 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<jlong>(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<buf_ostream>(),
JStringRef{env, extraHeader}};
return reinterpret_cast<jlong>(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<DataLogBackgroundWriter*>(impl)->Flush();
reinterpret_cast<DataLog*>(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<DataLogWriter*>(impl);
writer->Flush();
auto& stream = static_cast<buf_ostream&>(writer->GetStream());
JSpan<jbyte> jbuf{env, buf};
auto arr = stream.array();
if (start < 0 || static_cast<size_t>(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<DataLogBackgroundWriter*>(impl)->Pause();
reinterpret_cast<DataLog*>(impl)->Pause();
}
/*
@@ -141,7 +199,7 @@ Java_edu_wpi_first_util_datalog_DataLogJNI_resume
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
reinterpret_cast<DataLogBackgroundWriter*>(impl)->Resume();
reinterpret_cast<DataLog*>(impl)->Resume();
}
/*
@@ -157,7 +215,7 @@ Java_edu_wpi_first_util_datalog_DataLogJNI_stop
wpi::ThrowNullPointerException(env, "impl is null");
return;
}
reinterpret_cast<DataLogBackgroundWriter*>(impl)->Stop();
reinterpret_cast<DataLog*>(impl)->Stop();
}
/*

View File

@@ -6,8 +6,10 @@
#include <stdint.h>
#include <algorithm>
#include <concepts>
#include <initializer_list>
#include <optional>
#include <ranges>
#include <span>
#include <string>
@@ -575,10 +577,60 @@ class DataLogEntry {
int m_entry = 0;
};
template <typename T>
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<T> GetLastValue() const {
std::scoped_lock lock{m_mutex};
return m_lastValue;
}
protected:
mutable wpi::mutex m_mutex;
std::optional<T> m_lastValue;
};
/**
* Log arbitrary byte data.
*/
class RawLogEntry : public DataLogEntry {
class RawLogEntry : public DataLogValueEntryImpl<std::vector<uint8_t>> {
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<const uint8_t> 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<const uint8_t> data, int64_t timestamp = 0);
};
/**
* Log boolean values.
*/
class BooleanLogEntry : public DataLogEntry {
class BooleanLogEntry : public DataLogValueEntryImpl<bool> {
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<int64_t> {
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<float> {
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<double> {
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<std::string> {
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<std::vector<int>> {
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<const uint8_t> 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<const bool> 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<bool> 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<const int> 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<int> 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<const uint8_t> arr, int64_t timestamp = 0);
};
/**
* Log array of integer values.
*/
class IntegerArrayLogEntry : public DataLogEntry {
class IntegerArrayLogEntry
: public DataLogValueEntryImpl<std::vector<int64_t>> {
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<int64_t> 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<const int64_t> 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<int64_t> arr, int64_t timestamp = 0) {
Update({arr.begin(), arr.end()}, timestamp);
}
};
/**
* Log array of float values.
*/
class FloatArrayLogEntry : public DataLogEntry {
class FloatArrayLogEntry : public DataLogValueEntryImpl<std::vector<float>> {
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<float> 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<const float> 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<float> arr, int64_t timestamp = 0) {
Update({arr.begin(), arr.end()}, timestamp);
}
};
/**
* Log array of double values.
*/
class DoubleArrayLogEntry : public DataLogEntry {
class DoubleArrayLogEntry : public DataLogValueEntryImpl<std::vector<double>> {
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<double> 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<const double> 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<double> arr, int64_t timestamp = 0) {
Update({arr.begin(), arr.end()}, timestamp);
}
};
/**
* Log array of string values.
*/
class StringArrayLogEntry : public DataLogEntry {
class StringArrayLogEntry
: public DataLogValueEntryImpl<std::vector<std::string>> {
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<const std::string_view>{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<const std::string> 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<const std::string_view> 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<std::string_view> arr,
int64_t timestamp = 0) {
Update(std::span<const std::string_view>{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<uint8_t, 128> 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<T> 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<uint8_t> m_lastValue;
[[no_unique_address]]
std::tuple<I...> 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<const T> 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<std::vector<T>> 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<T> 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<const uint8_t>{std::to_address(in), size}, info...));
},
m_info);
}
return rv;
}
private:
mutable wpi::mutex m_mutex;
StructArrayBuffer<T, I...> m_buf;
std::optional<std::vector<uint8_t>> m_lastValue;
[[no_unique_address]]
std::tuple<I...> 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<uint8_t, 128> 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<T> 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<T> m_msg;
std::optional<std::vector<uint8_t>> m_lastValue;
};
} // namespace wpi::log

View File

@@ -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;

View File

@@ -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<ImmutableThing> {
@Override
public Class<ImmutableThing> 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<CloneableThing> {
@Override
public Class<CloneableThing> 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<Thing> {
@Override
public Class<Thing> 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<Thing> 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<Thing> 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<CloneableThing> 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<ImmutableThing> 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<Thing> 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<CloneableThing> 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<ImmutableThing> 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());
}
}

View File

@@ -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<uint8_t>{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<uint8_t>{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<uint8_t>{6, 6}));
entry.Update(std::span<const uint8_t>{}, 10);
log.Flush();
ASSERT_EQ(data.size(), 57u);
ASSERT_TRUE(entry.GetLastValue().has_value());
ASSERT_EQ(entry.GetLastValue().value(), std::vector<uint8_t>{});
}
TEST_F(DataLogTest, BooleanArrayAppendEmpty) {
wpi::log::BooleanArrayLogEntry entry{log, "a", 5};
entry.Append(std::span<const bool>{}, 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<int>{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<int>{true});
entry.Update(std::span<const bool>{}, 10);
log.Flush();
ASSERT_EQ(data.size(), 57u);
ASSERT_TRUE(entry.GetLastValue().has_value());
ASSERT_EQ(entry.GetLastValue().value(), std::vector<int>{});
}
TEST_F(DataLogTest, IntegerArrayAppendEmpty) {
wpi::log::IntegerArrayLogEntry entry{log, "a", 5};
entry.Append(std::span<const int64_t>{}, 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<int64_t>{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<int64_t>{2});
entry.Update(std::span<const int64_t>{}, 10);
log.Flush();
ASSERT_EQ(data.size(), 69u);
ASSERT_TRUE(entry.GetLastValue().has_value());
ASSERT_EQ(entry.GetLastValue().value(), std::vector<int64_t>{});
}
TEST_F(DataLogTest, DoubleArrayAppendEmpty) {
wpi::log::DoubleArrayLogEntry entry{log, "a", 5};
entry.Append(std::span<const double>{}, 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<double>{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<double>{2});
entry.Update(std::span<const double>{}, 10);
log.Flush();
ASSERT_EQ(data.size(), 70u);
ASSERT_TRUE(entry.GetLastValue().has_value());
ASSERT_EQ(entry.GetLastValue().value(), std::vector<double>{});
}
TEST_F(DataLogTest, FloatArrayAppendEmpty) {
wpi::log::FloatArrayLogEntry entry{log, "a", 5};
entry.Append(std::span<const float>{}, 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<float>{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<float>{2.0f});
entry.Update(std::span<const float>{}, 10);
log.Flush();
ASSERT_EQ(data.size(), 61u);
ASSERT_TRUE(entry.GetLastValue().has_value());
ASSERT_EQ(entry.GetLastValue().value(), std::vector<float>{});
}
TEST_F(DataLogTest, StringArrayAppendEmpty) {
wpi::log::StringArrayLogEntry entry{log, "a", 5};
entry.Append(std::span<const std::string>{}, 7);
entry.Append(std::span<const std::string_view>{}, 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<std::string>{"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<std::string>{"y"});
entry.Update(std::span<const std::string_view>{}, 10);
log.Flush();
ASSERT_EQ(data.size(), 76u);
ASSERT_TRUE(entry.GetLastValue().has_value());
ASSERT_EQ(entry.GetLastValue().value(), std::vector<std::string>{});
}
TEST_F(DataLogTest, StructA) {
@@ -144,6 +514,27 @@ TEST_F(DataLogTest, StructA) {
entry.Append(ThingA{}, 7);
}
TEST_F(DataLogTest, StructUpdate) {
wpi::log::StructLogEntry<ThingA> 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<ThingA> entry0;
@@ -152,6 +543,35 @@ TEST_F(DataLogTest, StructArrayA) {
entry.Append({{ThingA{}, ThingA{}}}, 7);
}
TEST_F(DataLogTest, StructArrayUpdate) {
wpi::log::StructArrayLogEntry<ThingA> 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{}, 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{}, ThingA{.x = 2}}));
entry.Update(std::span<const ThingA>{}, 10);
log.Flush();
ASSERT_EQ(data.size(), 135u);
ASSERT_TRUE(entry.GetLastValue().has_value());
ASSERT_EQ(entry.GetLastValue().value(), std::vector<ThingA>{});
}
TEST_F(DataLogTest, StructFixedArrayA) {
[[maybe_unused]]
wpi::log::StructArrayLogEntry<std::array<ThingA, 2>> entry0;