1 package com.android.internal.protolog; 2 3 import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.GROUPS; 4 import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.ID; 5 import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.Group.NAME; 6 import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MESSAGES; 7 import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE; 8 import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.MESSAGE_ID; 9 import static android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.GROUP_ID; 10 11 import android.annotation.NonNull; 12 import android.annotation.Nullable; 13 import android.util.LongSparseArray; 14 import android.util.proto.ProtoInputStream; 15 16 import com.android.internal.protolog.common.ILogger; 17 18 import java.io.IOException; 19 import java.util.Map; 20 import java.util.Objects; 21 import java.util.Set; 22 import java.util.TreeMap; 23 24 public class ProtoLogViewerConfigReader { 25 @NonNull 26 private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider; 27 @NonNull 28 private final Map<String, Set<Long>> mGroupHashes = new TreeMap<>(); 29 @NonNull 30 private final LongSparseArray<String> mLogMessageMap = new LongSparseArray<>(); 31 ProtoLogViewerConfigReader( @onNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider)32 public ProtoLogViewerConfigReader( 33 @NonNull ViewerConfigInputStreamProvider viewerConfigInputStreamProvider) { 34 this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider; 35 } 36 37 /** 38 * Returns message format string for its hash or null if unavailable 39 * or the viewer config is not loaded into memory. 40 */ 41 @Nullable getViewerString(long messageHash)42 public String getViewerString(long messageHash) { 43 return mLogMessageMap.get(messageHash); 44 } 45 46 /** 47 * Load the viewer configs for the target groups into memory. 48 * Only viewer configs loaded into memory can be required. So this must be called for all groups 49 * we want to query before we query their viewer config. 50 * 51 * @param groups Groups to load the viewer configs from file into memory. 52 */ loadViewerConfig(@onNull String[] groups)53 public synchronized void loadViewerConfig(@NonNull String[] groups) { 54 loadViewerConfig(groups, (message) -> {}); 55 } 56 57 /** 58 * Loads the viewer config into memory. No-op if already loaded in memory. 59 */ loadViewerConfig(@onNull String[] groups, @NonNull ILogger logger)60 public synchronized void loadViewerConfig(@NonNull String[] groups, @NonNull ILogger logger) { 61 for (String group : groups) { 62 if (mGroupHashes.containsKey(group)) { 63 continue; 64 } 65 66 try { 67 Map<Long, String> mappings = loadViewerConfigMappingForGroup(group); 68 mGroupHashes.put(group, mappings.keySet()); 69 for (Long key : mappings.keySet()) { 70 mLogMessageMap.put(key, mappings.get(key)); 71 } 72 73 logger.log("Loaded " + mLogMessageMap.size() + " log definitions"); 74 } catch (IOException e) { 75 logger.log("Unable to load log definitions: " 76 + "IOException while processing viewer config" + e); 77 } 78 } 79 } 80 unloadViewerConfig(@onNull String[] groups)81 public synchronized void unloadViewerConfig(@NonNull String[] groups) { 82 unloadViewerConfig(groups, (message) -> {}); 83 } 84 85 /** 86 * Unload the viewer config from memory. 87 */ unloadViewerConfig(@onNull String[] groups, @NonNull ILogger logger)88 public synchronized void unloadViewerConfig(@NonNull String[] groups, @NonNull ILogger logger) { 89 for (String group : groups) { 90 if (!mGroupHashes.containsKey(group)) { 91 continue; 92 } 93 94 final Set<Long> hashes = mGroupHashes.get(group); 95 for (Long hash : hashes) { 96 logger.log("Unloading viewer config hash " + hash); 97 mLogMessageMap.remove(hash); 98 } 99 mGroupHashes.remove(group); 100 } 101 } 102 103 /** 104 * Return whether or not the viewer config file contains a message with the specified hash. 105 * @param messageHash The hash message we are looking for in the viewer config file 106 * @return True iff the message with message hash is contained in the viewer config. 107 * @throws IOException if there was an issue reading the viewer config file. 108 */ messageHashIsAvailableInFile(long messageHash)109 public boolean messageHashIsAvailableInFile(long messageHash) 110 throws IOException { 111 try (var pisWrapper = mViewerConfigInputStreamProvider.getInputStream()) { 112 final var pis = pisWrapper.get(); 113 while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { 114 if (pis.getFieldNumber() == (int) MESSAGES) { 115 final long inMessageToken = pis.start(MESSAGES); 116 117 while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { 118 if (pis.getFieldNumber() == (int) MESSAGE_ID) { 119 if (pis.readLong(MESSAGE_ID) == messageHash) { 120 return true; 121 } 122 } 123 } 124 125 pis.end(inMessageToken); 126 } 127 } 128 } 129 130 return false; 131 } 132 133 @NonNull loadViewerConfigMappingForGroup(@onNull String group)134 private Map<Long, String> loadViewerConfigMappingForGroup(@NonNull String group) 135 throws IOException { 136 long targetGroupId = loadGroupId(group); 137 138 final Map<Long, String> hashesForGroup = new TreeMap<>(); 139 try (var pisWrapper = mViewerConfigInputStreamProvider.getInputStream()) { 140 final var pis = pisWrapper.get(); 141 while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { 142 if (pis.getFieldNumber() == (int) MESSAGES) { 143 final long inMessageToken = pis.start(MESSAGES); 144 145 long messageId = 0; 146 String message = null; 147 int groupId = 0; 148 while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { 149 switch (pis.getFieldNumber()) { 150 case (int) MESSAGE_ID: 151 messageId = pis.readLong(MESSAGE_ID); 152 break; 153 case (int) MESSAGE: 154 message = pis.readString(MESSAGE); 155 break; 156 case (int) GROUP_ID: 157 groupId = pis.readInt(GROUP_ID); 158 break; 159 } 160 } 161 162 if (groupId == 0) { 163 throw new IOException("Failed to get group id"); 164 } 165 166 if (messageId == 0) { 167 throw new IOException("Failed to get message id"); 168 } 169 170 if (message == null) { 171 throw new IOException("Failed to get message string"); 172 } 173 174 if (groupId == targetGroupId) { 175 hashesForGroup.put(messageId, message); 176 } 177 178 pis.end(inMessageToken); 179 } 180 } 181 } 182 183 return hashesForGroup; 184 } 185 loadGroupId(@onNull String group)186 private long loadGroupId(@NonNull String group) throws IOException { 187 try (var pisWrapper = mViewerConfigInputStreamProvider.getInputStream()) { 188 final var pis = pisWrapper.get(); 189 190 while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { 191 if (pis.getFieldNumber() == (int) GROUPS) { 192 final long inMessageToken = pis.start(GROUPS); 193 194 long groupId = 0; 195 String groupName = null; 196 while (pis.nextField() != ProtoInputStream.NO_MORE_FIELDS) { 197 switch (pis.getFieldNumber()) { 198 case (int) ID: 199 groupId = pis.readInt(ID); 200 break; 201 case (int) NAME: 202 groupName = pis.readString(NAME); 203 break; 204 } 205 } 206 207 if (Objects.equals(groupName, group)) { 208 return groupId; 209 } 210 211 pis.end(inMessageToken); 212 } 213 } 214 } 215 216 throw new RuntimeException("Group " + group + " not found in viewer config"); 217 } 218 } 219