mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-19 00:41:43 +00:00
[epilogue] Optimize time and memory usage of epilogue backends (#8190)
This commit is contained in:
@@ -106,14 +106,13 @@ public abstract class ClassSpecificLogger<T> {
|
||||
return;
|
||||
}
|
||||
|
||||
var builder =
|
||||
m_sendables.computeIfAbsent(
|
||||
sendable,
|
||||
s -> {
|
||||
var b = new LogBackedSendableBuilder(backend);
|
||||
s.initSendable(b);
|
||||
return b;
|
||||
});
|
||||
builder.update();
|
||||
if (m_sendables.containsKey(sendable)) {
|
||||
m_sendables.get(sendable).update();
|
||||
} else {
|
||||
var builder = new LogBackedSendableBuilder(backend);
|
||||
sendable.initSendable(builder);
|
||||
m_sendables.put(sendable, builder);
|
||||
builder.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@ import edu.wpi.first.util.datalog.StructArrayLogEntry;
|
||||
import edu.wpi.first.util.datalog.StructLogEntry;
|
||||
import edu.wpi.first.util.struct.Struct;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/** A backend implementation that saves information to a WPILib {@link DataLog} file on disk. */
|
||||
@@ -31,6 +33,7 @@ public class FileBackend implements EpilogueBackend {
|
||||
private final DataLog m_dataLog;
|
||||
private final Map<String, DataLogEntry> m_entries = new HashMap<>();
|
||||
private final Map<String, NestedBackend> m_subLoggers = new HashMap<>();
|
||||
private final Set<Struct<?>> m_seenSchemas = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Creates a new file-based backend.
|
||||
@@ -43,7 +46,13 @@ public class FileBackend implements EpilogueBackend {
|
||||
|
||||
@Override
|
||||
public EpilogueBackend getNested(String path) {
|
||||
return m_subLoggers.computeIfAbsent(path, k -> new NestedBackend(k, this));
|
||||
if (!m_subLoggers.containsKey(path)) {
|
||||
var nested = new NestedBackend(path, this);
|
||||
m_subLoggers.put(path, nested);
|
||||
return nested;
|
||||
}
|
||||
|
||||
return m_subLoggers.get(path);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -131,14 +140,30 @@ public class FileBackend implements EpilogueBackend {
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <S> void log(String identifier, S value, Struct<S> struct) {
|
||||
m_dataLog.addSchema(struct);
|
||||
getEntry(identifier, (log, k) -> StructLogEntry.create(log, k, struct)).append(value);
|
||||
// DataLog.addSchema has checks that we're able to skip, avoiding allocations
|
||||
if (m_seenSchemas.add(struct)) {
|
||||
m_dataLog.addSchema(struct);
|
||||
}
|
||||
|
||||
if (!m_entries.containsKey(identifier)) {
|
||||
m_entries.put(identifier, StructLogEntry.create(m_dataLog, identifier, struct));
|
||||
}
|
||||
|
||||
((StructLogEntry<S>) m_entries.get(identifier)).append(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <S> void log(String identifier, S[] value, Struct<S> struct) {
|
||||
m_dataLog.addSchema(struct);
|
||||
getEntry(identifier, (log, k) -> StructArrayLogEntry.create(log, k, struct)).append(value);
|
||||
// DataLog.addSchema has checks that we're able to skip, avoiding allocations
|
||||
if (m_seenSchemas.add(struct)) {
|
||||
m_dataLog.addSchema(struct);
|
||||
}
|
||||
|
||||
if (!m_entries.containsKey(identifier)) {
|
||||
m_entries.put(identifier, StructArrayLogEntry.create(m_dataLog, identifier, struct));
|
||||
}
|
||||
|
||||
((StructArrayLogEntry<S>) m_entries.get(identifier)).append(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,13 @@ public class LazyBackend implements EpilogueBackend {
|
||||
|
||||
@Override
|
||||
public EpilogueBackend getNested(String path) {
|
||||
return m_subLoggers.computeIfAbsent(path, k -> new NestedBackend(k, this));
|
||||
if (!m_subLoggers.containsKey(path)) {
|
||||
var nested = new NestedBackend(path, this);
|
||||
m_subLoggers.put(path, nested);
|
||||
return nested;
|
||||
}
|
||||
|
||||
return m_subLoggers.get(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -24,7 +24,13 @@ public class MultiBackend implements EpilogueBackend {
|
||||
|
||||
@Override
|
||||
public EpilogueBackend getNested(String path) {
|
||||
return m_nestedBackends.computeIfAbsent(path, k -> new NestedBackend(k, this));
|
||||
if (!m_nestedBackends.containsKey(path)) {
|
||||
var nested = new NestedBackend(path, this);
|
||||
m_nestedBackends.put(path, nested);
|
||||
return nested;
|
||||
}
|
||||
|
||||
return m_nestedBackends.get(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,7 +21,10 @@ import edu.wpi.first.networktables.StructArrayPublisher;
|
||||
import edu.wpi.first.networktables.StructPublisher;
|
||||
import edu.wpi.first.util.struct.Struct;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* A backend implementation that sends data over network tables. Be careful when using this, since
|
||||
@@ -32,61 +35,81 @@ public class NTEpilogueBackend implements EpilogueBackend {
|
||||
|
||||
private final Map<String, Publisher> m_publishers = new HashMap<>();
|
||||
private final Map<String, NestedBackend> m_nestedBackends = new HashMap<>();
|
||||
private final Set<Struct<?>> m_seenSchemas = new HashSet<>();
|
||||
private final Function<String, IntegerPublisher> m_createIntPublisher;
|
||||
private final Function<String, FloatPublisher> m_createFloatPublisher;
|
||||
private final Function<String, DoublePublisher> m_createDoublePublisher;
|
||||
private final Function<String, BooleanPublisher> m_createBooleanPublisher;
|
||||
private final Function<String, RawPublisher> m_createRawPublisher;
|
||||
private final Function<String, IntegerArrayPublisher> m_createIntegerArrayPublisher;
|
||||
private final Function<String, FloatArrayPublisher> m_createFloatArrayPublisher;
|
||||
private final Function<String, DoubleArrayPublisher> m_createDoubleArrayPublisher;
|
||||
private final Function<String, BooleanArrayPublisher> m_createBooleanArrayPublisher;
|
||||
private final Function<String, StringPublisher> m_createStringPublisher;
|
||||
private final Function<String, StringArrayPublisher> m_createStringArrayPublisher;
|
||||
|
||||
/**
|
||||
* Creates a logging backend that sends information to NetworkTables.
|
||||
*
|
||||
* @param nt the NetworkTable instance to use to send data to
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public NTEpilogueBackend(NetworkTableInstance nt) {
|
||||
this.m_nt = nt;
|
||||
m_createIntPublisher = identifier -> m_nt.getIntegerTopic(identifier).publish();
|
||||
m_createFloatPublisher = identifier -> m_nt.getFloatTopic(identifier).publish();
|
||||
m_createDoublePublisher = identifier -> m_nt.getDoubleTopic(identifier).publish();
|
||||
m_createBooleanPublisher = identifier -> m_nt.getBooleanTopic(identifier).publish();
|
||||
m_createRawPublisher = identifier -> m_nt.getRawTopic(identifier).publish("raw");
|
||||
m_createIntegerArrayPublisher = identifier -> m_nt.getIntegerArrayTopic(identifier).publish();
|
||||
m_createFloatArrayPublisher = identifier -> m_nt.getFloatArrayTopic(identifier).publish();
|
||||
m_createDoubleArrayPublisher = identifier -> m_nt.getDoubleArrayTopic(identifier).publish();
|
||||
m_createBooleanArrayPublisher = identifier -> m_nt.getBooleanArrayTopic(identifier).publish();
|
||||
m_createStringPublisher = identifier -> m_nt.getStringTopic(identifier).publish();
|
||||
m_createStringArrayPublisher = identifier -> m_nt.getStringArrayTopic(identifier).publish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EpilogueBackend getNested(String path) {
|
||||
return m_nestedBackends.computeIfAbsent(path, k -> new NestedBackend(k, this));
|
||||
if (!m_nestedBackends.containsKey(path)) {
|
||||
var nested = new NestedBackend(path, this);
|
||||
m_nestedBackends.put(path, nested);
|
||||
return nested;
|
||||
}
|
||||
|
||||
return m_nestedBackends.get(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, int value) {
|
||||
((IntegerPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getIntegerTopic(k).publish()))
|
||||
.set(value);
|
||||
((IntegerPublisher) m_publishers.computeIfAbsent(identifier, m_createIntPublisher)).set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, long value) {
|
||||
((IntegerPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getIntegerTopic(k).publish()))
|
||||
.set(value);
|
||||
((IntegerPublisher) m_publishers.computeIfAbsent(identifier, m_createIntPublisher)).set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, float value) {
|
||||
((FloatPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getFloatTopic(k).publish()))
|
||||
.set(value);
|
||||
((FloatPublisher) m_publishers.computeIfAbsent(identifier, m_createFloatPublisher)).set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, double value) {
|
||||
((DoublePublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getDoubleTopic(k).publish()))
|
||||
((DoublePublisher) m_publishers.computeIfAbsent(identifier, m_createDoublePublisher))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, boolean value) {
|
||||
((BooleanPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getBooleanTopic(k).publish()))
|
||||
((BooleanPublisher) m_publishers.computeIfAbsent(identifier, m_createBooleanPublisher))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, byte[] value) {
|
||||
((RawPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getRawTopic(k).publish("raw")))
|
||||
.set(value);
|
||||
((RawPublisher) m_publishers.computeIfAbsent(identifier, m_createRawPublisher)).set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -100,68 +123,79 @@ public class NTEpilogueBackend implements EpilogueBackend {
|
||||
}
|
||||
|
||||
((IntegerArrayPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getIntegerArrayTopic(k).publish()))
|
||||
m_publishers.computeIfAbsent(identifier, m_createIntegerArrayPublisher))
|
||||
.set(widened);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, long[] value) {
|
||||
((IntegerArrayPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getIntegerArrayTopic(k).publish()))
|
||||
m_publishers.computeIfAbsent(identifier, m_createIntegerArrayPublisher))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, float[] value) {
|
||||
((FloatArrayPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getFloatArrayTopic(k).publish()))
|
||||
((FloatArrayPublisher) m_publishers.computeIfAbsent(identifier, m_createFloatArrayPublisher))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, double[] value) {
|
||||
((DoubleArrayPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getDoubleArrayTopic(k).publish()))
|
||||
((DoubleArrayPublisher) m_publishers.computeIfAbsent(identifier, m_createDoubleArrayPublisher))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, boolean[] value) {
|
||||
((BooleanArrayPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getBooleanArrayTopic(k).publish()))
|
||||
m_publishers.computeIfAbsent(identifier, m_createBooleanArrayPublisher))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, String value) {
|
||||
((StringPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getStringTopic(k).publish()))
|
||||
((StringPublisher) m_publishers.computeIfAbsent(identifier, m_createStringPublisher))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, String[] value) {
|
||||
((StringArrayPublisher)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getStringArrayTopic(k).publish()))
|
||||
((StringArrayPublisher) m_publishers.computeIfAbsent(identifier, m_createStringArrayPublisher))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <S> void log(String identifier, S value, Struct<S> struct) {
|
||||
m_nt.addSchema(struct);
|
||||
((StructPublisher<S>)
|
||||
m_publishers.computeIfAbsent(identifier, k -> m_nt.getStructTopic(k, struct).publish()))
|
||||
.set(value);
|
||||
// NetworkTableInstance.addSchema has checks that we're able to skip, avoiding allocations
|
||||
if (m_seenSchemas.add(struct)) {
|
||||
m_nt.addSchema(struct);
|
||||
}
|
||||
|
||||
if (m_publishers.containsKey(identifier)) {
|
||||
((StructPublisher<S>) m_publishers.get(identifier)).set(value);
|
||||
} else {
|
||||
StructPublisher<S> publisher = m_nt.getStructTopic(identifier, struct).publish();
|
||||
m_publishers.put(identifier, publisher);
|
||||
publisher.set(value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <S> void log(String identifier, S[] value, Struct<S> struct) {
|
||||
m_nt.addSchema(struct);
|
||||
((StructArrayPublisher<S>)
|
||||
m_publishers.computeIfAbsent(
|
||||
identifier, k -> m_nt.getStructArrayTopic(k, struct).publish()))
|
||||
.set(value);
|
||||
// NetworkTableInstance.addSchema has checks that we're able to skip, avoiding allocations
|
||||
if (m_seenSchemas.add(struct)) {
|
||||
m_nt.addSchema(struct);
|
||||
}
|
||||
|
||||
if (m_publishers.containsKey(identifier)) {
|
||||
((StructArrayPublisher<S>) m_publishers.get(identifier)).set(value);
|
||||
} else {
|
||||
StructArrayPublisher<S> publisher = m_nt.getStructArrayTopic(identifier, struct).publish();
|
||||
m_publishers.put(identifier, publisher);
|
||||
publisher.set(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,15 @@ public class NestedBackend implements EpilogueBackend {
|
||||
private final EpilogueBackend m_impl;
|
||||
private final Map<String, NestedBackend> m_nestedBackends = new HashMap<>();
|
||||
|
||||
// String concatenation can be expensive, especially for deeply nested hierarchies with many
|
||||
// logged fields. For example, logging a hypothetical Robot.elevator.io.getHeight() would result
|
||||
// in "/Robot/" + "elevator/" + "io/" + "getHeight"; three concatenations and string and byte
|
||||
// array allocations that need to be cleaned up by the GC. Caching the results means those
|
||||
// allocations only occur once, resulting in no GC (the strings are always referenced in the
|
||||
// cache), and minimal time costs (the String object caches its own hash code, so all we do is an
|
||||
// O(1) table lookup per concatenation)
|
||||
private final Map<String, String> m_prefixedIdentifiers = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Creates a new nested backed underneath another backend.
|
||||
*
|
||||
@@ -33,83 +42,109 @@ public class NestedBackend implements EpilogueBackend {
|
||||
this.m_impl = impl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast lookup to avoid redundant `m_prefix + identifier` concatenations. If the identifier has
|
||||
* not been seen before, we compute the concatenation and cache the result for later invocations
|
||||
* to read. This avoids redundantly recomputing the same concatenations every loop and
|
||||
* significantly cuts down on the CPU and memory overhead of the Epilogue library.
|
||||
*
|
||||
* @param identifier The identifier to prepend with {@link #m_prefix}.
|
||||
* @return The concatenated string.
|
||||
*/
|
||||
private String withPrefix(String identifier) {
|
||||
// Using computeIfAbsent would result in a new lambda object allocation on every call
|
||||
if (m_prefixedIdentifiers.containsKey(identifier)) {
|
||||
return m_prefixedIdentifiers.get(identifier);
|
||||
}
|
||||
|
||||
String result = m_prefix + identifier;
|
||||
m_prefixedIdentifiers.put(identifier, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EpilogueBackend getNested(String path) {
|
||||
return m_nestedBackends.computeIfAbsent(path, k -> new NestedBackend(k, this));
|
||||
if (!m_nestedBackends.containsKey(path)) {
|
||||
var nested = new NestedBackend(path, this);
|
||||
m_nestedBackends.put(path, nested);
|
||||
return nested;
|
||||
}
|
||||
|
||||
return m_nestedBackends.get(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, int value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
m_impl.log(withPrefix(identifier), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, long value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
m_impl.log(withPrefix(identifier), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, float value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
m_impl.log(withPrefix(identifier), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, double value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
m_impl.log(withPrefix(identifier), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, boolean value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
m_impl.log(withPrefix(identifier), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, byte[] value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
m_impl.log(withPrefix(identifier), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, int[] value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
m_impl.log(withPrefix(identifier), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, long[] value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
m_impl.log(withPrefix(identifier), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, float[] value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
m_impl.log(withPrefix(identifier), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, double[] value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
m_impl.log(withPrefix(identifier), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, boolean[] value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
m_impl.log(withPrefix(identifier), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, String value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
m_impl.log(withPrefix(identifier), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String identifier, String[] value) {
|
||||
m_impl.log(m_prefix + identifier, value);
|
||||
m_impl.log(withPrefix(identifier), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> void log(String identifier, S value, Struct<S> struct) {
|
||||
m_impl.log(m_prefix + identifier, value, struct);
|
||||
m_impl.log(withPrefix(identifier), value, struct);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> void log(String identifier, S[] value, Struct<S> struct) {
|
||||
m_impl.log(m_prefix + identifier, value, struct);
|
||||
m_impl.log(withPrefix(identifier), value, struct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
// 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.epilogue.logging;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class NestedBackendTest {
|
||||
@Test
|
||||
void prefixesAppliedAndNested() {
|
||||
var root = new TestBackend();
|
||||
var nested = new NestedBackend("/Robot", root);
|
||||
|
||||
nested.log("int", 1);
|
||||
nested.log("string", "hello");
|
||||
|
||||
var arm = nested.getNested("arm");
|
||||
arm.log("position", 2.0);
|
||||
arm.log("enabled", true);
|
||||
|
||||
assertEquals(4, root.getEntries().size());
|
||||
assertEquals("/Robot/int", root.getEntries().get(0).identifier());
|
||||
assertEquals(1, root.getEntries().get(0).value());
|
||||
|
||||
assertEquals("/Robot/string", root.getEntries().get(1).identifier());
|
||||
assertEquals("hello", root.getEntries().get(1).value());
|
||||
|
||||
assertEquals("/Robot/arm/position", root.getEntries().get(2).identifier());
|
||||
assertEquals(2.0, root.getEntries().get(2).value());
|
||||
|
||||
assertEquals("/Robot/arm/enabled", root.getEntries().get(3).identifier());
|
||||
assertEquals(true, root.getEntries().get(3).value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesTrailingSlashOnPrefix() {
|
||||
var root = new TestBackend();
|
||||
var a = new NestedBackend("/Robot", root);
|
||||
var b = new NestedBackend("/Robot/", root);
|
||||
|
||||
a.log("x", 1);
|
||||
b.log("y", 2);
|
||||
|
||||
assertEquals("/Robot/x", root.getEntries().get(0).identifier());
|
||||
assertEquals("/Robot/y", root.getEntries().get(1).identifier());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNestedIsCached() {
|
||||
var root = new TestBackend();
|
||||
var nested = new NestedBackend("/Robot", root);
|
||||
|
||||
var arm1 = nested.getNested("arm");
|
||||
var arm2 = nested.getNested("arm");
|
||||
|
||||
assertSame(arm1, arm2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void usesPrefixedIdentifierCacheForSameField() {
|
||||
var root = new TestBackend();
|
||||
var nested = new NestedBackend("/Robot", root);
|
||||
|
||||
// Same field logged multiple times - identifier object should be the same (cached)
|
||||
// We use assertSame to check that the references are identical
|
||||
nested.log("x", 0);
|
||||
nested.log("x", 1);
|
||||
|
||||
String id0 = root.getEntries().get(0).identifier();
|
||||
String id1 = root.getEntries().get(1).identifier();
|
||||
assertSame(
|
||||
id0,
|
||||
id1,
|
||||
"Identifier %s (id: %d) was not reused (new id: %d)"
|
||||
.formatted(id0, System.identityHashCode(id0), System.identityHashCode(id1)));
|
||||
|
||||
// Also verify through a nested backend path
|
||||
var arm = nested.getNested("arm");
|
||||
arm.log("position", 0.0);
|
||||
arm.log("position", 1.0);
|
||||
|
||||
String id2 = root.getEntries().get(2).identifier();
|
||||
String id3 = root.getEntries().get(3).identifier();
|
||||
assertSame(
|
||||
id2,
|
||||
id3,
|
||||
"Identifier %s (id: %d) was not reused (new id: %d)"
|
||||
.formatted(id2, System.identityHashCode(id2), System.identityHashCode(id3)));
|
||||
|
||||
// Sanity check actual full values
|
||||
assertEquals("/Robot/x", id0);
|
||||
assertEquals("/Robot/arm/position", id2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void logsAllOverloads() {
|
||||
var root = new TestBackend();
|
||||
var nested = new NestedBackend("/Robot", root);
|
||||
|
||||
// Scalars
|
||||
nested.log("int", 1);
|
||||
nested.log("long", 2L);
|
||||
nested.log("float", 3.0f);
|
||||
nested.log("double", 4.0);
|
||||
nested.log("boolean", true);
|
||||
nested.log("string", "hello");
|
||||
|
||||
// Arrays
|
||||
nested.log("bytes", new byte[] {1, 2});
|
||||
nested.log("ints", new int[] {3, 4});
|
||||
nested.log("longs", new long[] {5L, 6L});
|
||||
nested.log("floats", new float[] {7.0f, 8.0f});
|
||||
nested.log("doubles", new double[] {9.0, 10.0});
|
||||
nested.log("booleans", new boolean[] {true, false});
|
||||
nested.log("strings", new String[] {"x", "y"});
|
||||
|
||||
// Structs
|
||||
nested.log("customStruct", new CustomStruct(7), CustomStruct.struct);
|
||||
nested.log(
|
||||
"customStructs",
|
||||
new CustomStruct[] {new CustomStruct(0), new CustomStruct(1)},
|
||||
CustomStruct.struct);
|
||||
|
||||
var entries = root.getEntries();
|
||||
int idx = 0;
|
||||
|
||||
// Scalars
|
||||
assertEquals(new TestBackend.LogEntry<>("/Robot/int", 1), entries.get(idx++));
|
||||
assertEquals(new TestBackend.LogEntry<>("/Robot/long", 2L), entries.get(idx++));
|
||||
assertEquals(new TestBackend.LogEntry<>("/Robot/float", 3.0f), entries.get(idx++));
|
||||
assertEquals(new TestBackend.LogEntry<>("/Robot/double", 4.0), entries.get(idx++));
|
||||
assertEquals(new TestBackend.LogEntry<>("/Robot/boolean", true), entries.get(idx++));
|
||||
assertEquals(new TestBackend.LogEntry<>("/Robot/string", "hello"), entries.get(idx++));
|
||||
|
||||
// Arrays
|
||||
assertEquals("/Robot/bytes", entries.get(idx).identifier());
|
||||
assertArrayEquals(new byte[] {1, 2}, (byte[]) entries.get(idx++).value());
|
||||
|
||||
assertEquals("/Robot/ints", entries.get(idx).identifier());
|
||||
assertArrayEquals(new int[] {3, 4}, (int[]) entries.get(idx++).value());
|
||||
|
||||
assertEquals("/Robot/longs", entries.get(idx).identifier());
|
||||
assertArrayEquals(new long[] {5L, 6L}, (long[]) entries.get(idx++).value());
|
||||
|
||||
assertEquals("/Robot/floats", entries.get(idx).identifier());
|
||||
assertArrayEquals(new float[] {7.0f, 8.0f}, (float[]) entries.get(idx++).value());
|
||||
|
||||
assertEquals("/Robot/doubles", entries.get(idx).identifier());
|
||||
assertArrayEquals(new double[] {9.0, 10.0}, (double[]) entries.get(idx++).value());
|
||||
|
||||
assertEquals("/Robot/booleans", entries.get(idx).identifier());
|
||||
assertArrayEquals(new boolean[] {true, false}, (boolean[]) entries.get(idx++).value());
|
||||
|
||||
assertEquals("/Robot/strings", entries.get(idx).identifier());
|
||||
assertArrayEquals(new String[] {"x", "y"}, (String[]) entries.get(idx++).value());
|
||||
|
||||
// Structs are serialized to bytes
|
||||
assertEquals("/Robot/customStruct", entries.get(idx).identifier());
|
||||
assertArrayEquals(new byte[] {0x07, 0x00, 0x00, 0x00}, (byte[]) entries.get(idx++).value());
|
||||
|
||||
assertEquals("/Robot/customStructs", entries.get(idx).identifier());
|
||||
// two int32 values, little-endian
|
||||
assertArrayEquals(
|
||||
new byte[] {
|
||||
0x00, 0x00, 0x00, 0x00, // 0 (first element)
|
||||
0x01, 0x00, 0x00, 0x00, // 1 (second element)
|
||||
0x00, 0x00, 0x00, 0x00, // 0 (empty space allocated by StructBuffer)
|
||||
0x00, 0x00, 0x00, 0x00 // 0 (empty space allocated by StructBuffer)
|
||||
},
|
||||
(byte[]) entries.get(idx++).value());
|
||||
|
||||
// Ensure we covered all calls
|
||||
assertEquals(idx, entries.size());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user