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 org.wpilib.epilogue.logging; 006 007import static org.wpilib.util.ErrorMessages.requireNonNullParam; 008 009import java.util.HashMap; 010import java.util.HashSet; 011import java.util.Map; 012import java.util.Set; 013import java.util.function.BiFunction; 014import org.wpilib.datalog.BooleanArrayLogEntry; 015import org.wpilib.datalog.BooleanLogEntry; 016import org.wpilib.datalog.DataLog; 017import org.wpilib.datalog.DataLogEntry; 018import org.wpilib.datalog.DoubleArrayLogEntry; 019import org.wpilib.datalog.DoubleLogEntry; 020import org.wpilib.datalog.FloatArrayLogEntry; 021import org.wpilib.datalog.FloatLogEntry; 022import org.wpilib.datalog.IntegerArrayLogEntry; 023import org.wpilib.datalog.IntegerLogEntry; 024import org.wpilib.datalog.ProtobufLogEntry; 025import org.wpilib.datalog.RawLogEntry; 026import org.wpilib.datalog.StringArrayLogEntry; 027import org.wpilib.datalog.StringLogEntry; 028import org.wpilib.datalog.StructArrayLogEntry; 029import org.wpilib.datalog.StructLogEntry; 030import org.wpilib.util.protobuf.Protobuf; 031import org.wpilib.util.struct.Struct; 032import us.hebi.quickbuf.ProtoMessage; 033 034/** A backend implementation that saves information to a WPILib {@link DataLog} file on disk. */ 035public class FileBackend implements EpilogueBackend { 036 private final DataLog m_dataLog; 037 private final Map<String, DataLogEntry> m_entries = new HashMap<>(); 038 private final Map<String, NestedBackend> m_subLoggers = new HashMap<>(); 039 private final Set<Struct<?>> m_seenSchemas = new HashSet<>(); 040 private final Set<Protobuf<?, ?>> m_seenProtos = new HashSet<>(); 041 042 /** 043 * Creates a new file-based backend. 044 * 045 * @param dataLog the data log to save data to 046 */ 047 public FileBackend(DataLog dataLog) { 048 this.m_dataLog = requireNonNullParam(dataLog, "dataLog", "FileBackend"); 049 } 050 051 @Override 052 public EpilogueBackend getNested(String path) { 053 if (!m_subLoggers.containsKey(path)) { 054 var nested = new NestedBackend(path, this); 055 m_subLoggers.put(path, nested); 056 return nested; 057 } 058 059 return m_subLoggers.get(path); 060 } 061 062 @SuppressWarnings("unchecked") 063 private <E extends DataLogEntry> E getEntry( 064 String identifier, BiFunction<DataLog, String, ? extends E> ctor) { 065 if (m_entries.get(identifier) != null) { 066 return (E) m_entries.get(identifier); 067 } 068 069 var entry = ctor.apply(m_dataLog, identifier); 070 m_entries.put(identifier, entry); 071 return entry; 072 } 073 074 @Override 075 public void log(String identifier, int value) { 076 getEntry(identifier, IntegerLogEntry::new).append(value); 077 } 078 079 @Override 080 public void log(String identifier, long value) { 081 getEntry(identifier, IntegerLogEntry::new).append(value); 082 } 083 084 @Override 085 public void log(String identifier, float value) { 086 getEntry(identifier, FloatLogEntry::new).append(value); 087 } 088 089 @Override 090 public void log(String identifier, double value) { 091 getEntry(identifier, DoubleLogEntry::new).append(value); 092 } 093 094 @Override 095 public void log(String identifier, boolean value) { 096 getEntry(identifier, BooleanLogEntry::new).append(value); 097 } 098 099 @Override 100 public void log(String identifier, byte[] value) { 101 getEntry(identifier, RawLogEntry::new).append(value); 102 } 103 104 @Override 105 @SuppressWarnings("PMD.UnnecessaryCastRule") 106 public void log(String identifier, int[] value) { 107 long[] widened = new long[value.length]; 108 for (int i = 0; i < value.length; i++) { 109 widened[i] = (long) value[i]; 110 } 111 getEntry(identifier, IntegerArrayLogEntry::new).append(widened); 112 } 113 114 @Override 115 public void log(String identifier, long[] value) { 116 getEntry(identifier, IntegerArrayLogEntry::new).append(value); 117 } 118 119 @Override 120 public void log(String identifier, float[] value) { 121 getEntry(identifier, FloatArrayLogEntry::new).append(value); 122 } 123 124 @Override 125 public void log(String identifier, double[] value) { 126 getEntry(identifier, DoubleArrayLogEntry::new).append(value); 127 } 128 129 @Override 130 public void log(String identifier, boolean[] value) { 131 getEntry(identifier, BooleanArrayLogEntry::new).append(value); 132 } 133 134 @Override 135 public void log(String identifier, String value) { 136 getEntry(identifier, StringLogEntry::new).append(value); 137 } 138 139 @Override 140 public void log(String identifier, String[] value) { 141 getEntry(identifier, StringArrayLogEntry::new).append(value); 142 } 143 144 @Override 145 @SuppressWarnings("unchecked") 146 public <S> void log(String identifier, S value, Struct<S> struct) { 147 // DataLog.addSchema has checks that we're able to skip, avoiding allocations 148 if (m_seenSchemas.add(struct)) { 149 m_dataLog.addSchema(struct); 150 } 151 152 if (!m_entries.containsKey(identifier)) { 153 m_entries.put(identifier, StructLogEntry.create(m_dataLog, identifier, struct)); 154 } 155 156 ((StructLogEntry<S>) m_entries.get(identifier)).append(value); 157 } 158 159 @Override 160 @SuppressWarnings("unchecked") 161 public <S> void log(String identifier, S[] value, Struct<S> struct) { 162 // DataLog.addSchema has checks that we're able to skip, avoiding allocations 163 if (m_seenSchemas.add(struct)) { 164 m_dataLog.addSchema(struct); 165 } 166 167 if (!m_entries.containsKey(identifier)) { 168 m_entries.put(identifier, StructArrayLogEntry.create(m_dataLog, identifier, struct)); 169 } 170 171 ((StructArrayLogEntry<S>) m_entries.get(identifier)).append(value); 172 } 173 174 @Override 175 @SuppressWarnings("unchecked") 176 public <P, M extends ProtoMessage<M>> void log(String identifier, P value, Protobuf<P, M> proto) { 177 // DataLog.addSchema has checks that we're able to skip, avoiding allocations 178 if (m_seenProtos.add(proto)) { 179 m_dataLog.addSchema(proto); 180 } 181 182 if (!m_entries.containsKey(identifier)) { 183 m_entries.put(identifier, ProtobufLogEntry.create(m_dataLog, identifier, proto)); 184 } 185 186 ((ProtobufLogEntry<P>) m_entries.get(identifier)).append(value); 187 } 188}