001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package edu.wpi.first.epilogue.logging;
006
007import edu.wpi.first.util.protobuf.Protobuf;
008import edu.wpi.first.util.struct.Struct;
009import java.util.HashMap;
010import java.util.Map;
011import us.hebi.quickbuf.ProtoMessage;
012
013/**
014 * A backend that logs to an underlying backend, prepending all logged data with a specific prefix.
015 * Useful for logging nested data structures.
016 */
017public class NestedBackend implements EpilogueBackend {
018  private final String m_prefix;
019  private final EpilogueBackend m_impl;
020  private final Map<String, NestedBackend> m_nestedBackends = new HashMap<>();
021
022  // String concatenation can be expensive, especially for deeply nested hierarchies with many
023  // logged fields. For example, logging a hypothetical Robot.elevator.io.getHeight() would result
024  // in "/Robot/" + "elevator/" + "io/" + "getHeight"; three concatenations and string and byte
025  // array allocations that need to be cleaned up by the GC. Caching the results means those
026  // allocations only occur once, resulting in no GC (the strings are always referenced in the
027  // cache), and minimal time costs (the String object caches its own hash code, so all we do is an
028  // O(1) table lookup per concatenation)
029  private final Map<String, String> m_prefixedIdentifiers = new HashMap<>();
030
031  /**
032   * Creates a new nested backed underneath another backend.
033   *
034   * @param prefix the prefix to append to all data logged in the nested backend
035   * @param impl the backend to log to
036   */
037  public NestedBackend(String prefix, EpilogueBackend impl) {
038    // Add a trailing slash if not already present
039    if (prefix.endsWith("/")) {
040      this.m_prefix = prefix;
041    } else {
042      this.m_prefix = prefix + "/";
043    }
044    this.m_impl = impl;
045  }
046
047  /**
048   * Fast lookup to avoid redundant `m_prefix + identifier` concatenations. If the identifier has
049   * not been seen before, we compute the concatenation and cache the result for later invocations
050   * to read. This avoids redundantly recomputing the same concatenations every loop and
051   * significantly cuts down on the CPU and memory overhead of the Epilogue library.
052   *
053   * @param identifier The identifier to prepend with {@link #m_prefix}.
054   * @return The concatenated string.
055   */
056  private String withPrefix(String identifier) {
057    // Using computeIfAbsent would result in a new lambda object allocation on every call
058    if (m_prefixedIdentifiers.containsKey(identifier)) {
059      return m_prefixedIdentifiers.get(identifier);
060    }
061
062    String result = m_prefix + identifier;
063    m_prefixedIdentifiers.put(identifier, result);
064    return result;
065  }
066
067  @Override
068  public EpilogueBackend getNested(String path) {
069    if (!m_nestedBackends.containsKey(path)) {
070      var nested = new NestedBackend(path, this);
071      m_nestedBackends.put(path, nested);
072      return nested;
073    }
074
075    return m_nestedBackends.get(path);
076  }
077
078  @Override
079  public void log(String identifier, int value) {
080    m_impl.log(withPrefix(identifier), value);
081  }
082
083  @Override
084  public void log(String identifier, long value) {
085    m_impl.log(withPrefix(identifier), value);
086  }
087
088  @Override
089  public void log(String identifier, float value) {
090    m_impl.log(withPrefix(identifier), value);
091  }
092
093  @Override
094  public void log(String identifier, double value) {
095    m_impl.log(withPrefix(identifier), value);
096  }
097
098  @Override
099  public void log(String identifier, boolean value) {
100    m_impl.log(withPrefix(identifier), value);
101  }
102
103  @Override
104  public void log(String identifier, byte[] value) {
105    m_impl.log(withPrefix(identifier), value);
106  }
107
108  @Override
109  public void log(String identifier, int[] value) {
110    m_impl.log(withPrefix(identifier), value);
111  }
112
113  @Override
114  public void log(String identifier, long[] value) {
115    m_impl.log(withPrefix(identifier), value);
116  }
117
118  @Override
119  public void log(String identifier, float[] value) {
120    m_impl.log(withPrefix(identifier), value);
121  }
122
123  @Override
124  public void log(String identifier, double[] value) {
125    m_impl.log(withPrefix(identifier), value);
126  }
127
128  @Override
129  public void log(String identifier, boolean[] value) {
130    m_impl.log(withPrefix(identifier), value);
131  }
132
133  @Override
134  public void log(String identifier, String value) {
135    m_impl.log(withPrefix(identifier), value);
136  }
137
138  @Override
139  public void log(String identifier, String[] value) {
140    m_impl.log(withPrefix(identifier), value);
141  }
142
143  @Override
144  public <S> void log(String identifier, S value, Struct<S> struct) {
145    m_impl.log(withPrefix(identifier), value, struct);
146  }
147
148  @Override
149  public <S> void log(String identifier, S[] value, Struct<S> struct) {
150    m_impl.log(withPrefix(identifier), value, struct);
151  }
152
153  @Override
154  public <P, M extends ProtoMessage<M>> void log(String identifier, P value, Protobuf<P, M> proto) {
155    m_impl.log(m_prefix + identifier, value, proto);
156  }
157}