[epilogue] Optimize time and memory usage of epilogue backends (#8190)

This commit is contained in:
Sam Carlberg
2025-08-30 23:15:22 -04:00
committed by GitHub
parent 45db0fd45e
commit 129cbbe11d
7 changed files with 354 additions and 69 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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