1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tradefed.postprocessor;
18 
19 import com.android.tradefed.config.Option;
20 import com.android.tradefed.config.OptionClass;
21 import com.android.tradefed.log.LogUtil.CLog;
22 import com.android.tradefed.metrics.proto.MetricMeasurement.DataType;
23 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
24 import com.android.tradefed.result.LogFile;
25 import com.android.tradefed.result.TestDescription;
26 import com.android.tradefed.util.FileUtil;
27 import com.android.tradefed.util.ZipUtil;
28 import com.android.tradefed.util.ZipUtil2;
29 import com.android.tradefed.util.proto.TfMetricProtoUtil;
30 
31 import com.google.common.annotations.VisibleForTesting;
32 import com.google.gson.Gson;
33 import com.google.gson.JsonElement;
34 import com.google.gson.JsonObject;
35 import com.google.gson.JsonSyntaxException;
36 import com.google.protobuf.Descriptors.FieldDescriptor;
37 import com.google.protobuf.Message;
38 import com.google.protobuf.TextFormat;
39 import com.google.protobuf.TextFormat.ParseException;
40 
41 import org.apache.commons.compress.archivers.zip.ZipFile;
42 
43 import java.io.BufferedReader;
44 import java.io.File;
45 import java.io.FileInputStream;
46 import java.io.FileReader;
47 import java.io.IOException;
48 import java.util.ArrayList;
49 import java.util.HashMap;
50 import java.util.HashSet;
51 import java.util.LinkedHashMap;
52 import java.util.List;
53 import java.util.Map;
54 import java.util.Map.Entry;
55 import java.util.Optional;
56 import java.util.Set;
57 import java.util.concurrent.TimeUnit;
58 import java.util.regex.Pattern;
59 import java.util.stream.Collectors;
60 
61 import perfetto.protos.PerfettoMergedMetrics.TraceMetrics;
62 
63 /**
64  * A post processor that processes text/binary metric perfetto proto file into key-value pairs by
65  * recursively expanding the proto messages and fields with string values until the field with
66  * numeric value is encountered. Treats enum and boolean as string values while constructing the
67  * keys.
68  *
69  * <p>It optionally supports indexing list fields when there are duplicates while constructing the
70  * keys. For example
71  *
72  * <p>"perfetto-indexed-list-field" - perfetto.protos.AndroidStartupMetric.Startup
73  * <p>"perfetto-prefix-key-field" - perfetto.protos.ProcessRenderInfo.process_name
74  *
75  * <p>android_startup-startup#1-package_name-com.calculator-to_first_frame-dur_ns: 300620342
76  * android_startup-startup#2-package_name-com.nexuslauncher-to_first_frame-dur_ns: 49257713
77  * android_startup-startup#3-package_name-com.calculator-to_first_frame-dur_ns: 261382005
78  */
79 @OptionClass(alias = "perfetto-generic-processor")
80 public class PerfettoGenericPostProcessor extends BasePostProcessor {
81 
82     private static final String METRIC_SEP = "-";
83     @VisibleForTesting static final String RUNTIME_METRIC_KEY = "perfetto_post_processor_runtime";
84 
85     public enum METRIC_FILE_FORMAT {
86         text,
87         binary,
88         json,
89     }
90 
91     public enum AlternativeParseFormat {
92         json,
93         none,
94     }
95 
96     @Option(
97             name = "perfetto-proto-file-prefix",
98             description = "Prefix for identifying a perfetto metric file name.")
99     private Set<String> mPerfettoProtoMetricFilePrefix = new HashSet<>();
100 
101     @Option(
102             name = "perfetto-indexed-list-field",
103             description = "List fields in perfetto proto metric file that has to be indexed.")
104     private Set<String> mPerfettoIndexedListFields = new HashSet<>();
105 
106     @Option(
107             name = "perfetto-prefix-key-field",
108             description = "String value field need to be prefixed with the all the other"
109                     + "numeric value field keys in the proto message.")
110     private Set<String> mPerfettoPrefixKeyFields = new HashSet<>();
111 
112     @Option(
113             name = "perfetto-prefix-inner-message-key-field",
114             description = "String value field need to be prefixed with the all the other"
115                     + "numeric value field keys outside of the current proto message.")
116     private Set<String> mPerfettoPrefixInnerMessagePrefixFields = new HashSet<>();
117 
118     @Option(
119             name = "perfetto-include-all-metrics",
120             description =
121                     "If this flag is turned on, all the metrics parsed from the perfetto file will"
122                             + " be included in the final result map and ignores the regex passed"
123                             + " in the filters.")
124     private boolean mPerfettoIncludeAllMetrics = false;
125 
126     @Option(
127             name = "perfetto-metric-filter-regex",
128             description =
129                     "Regular expression that will be used for filtering the metrics parsed"
130                             + " from the perfetto proto metric file.")
131     private Set<String> mPerfettoMetricFilterRegEx = new HashSet<>();
132 
133     @Option(
134             name = "trace-processor-output-format",
135             description = "Trace processor output format. One of [binary|text|json]")
136     private METRIC_FILE_FORMAT mTraceProcessorOutputFormat = METRIC_FILE_FORMAT.text;
137 
138     @Option(
139             name = "decompress-perfetto-timeout",
140             description = "Timeout to decompress perfetto compressed file.",
141             isTimeVal = true)
142     private long mDecompressTimeoutMs = TimeUnit.MINUTES.toMillis(20);
143 
144     @Deprecated
145     @Option(
146             name = "processed-metric",
147             description =
148                     "True if the metric is final and shouldn't be processed any more,"
149                             + " false if the metric can be handled by another post-processor.")
150     private boolean mProcessedMetric = true;
151 
152     @Option(name = "perfetto-metric-replace-prefix", description = "Replace the prefix in metrics"
153             + "from the metric proto file. Key is the prefix to look for in the metric"
154             + "keys parsed and value is be the replacement string.")
155     private Map<String, String> mReplacePrefixMap = new LinkedHashMap<String, String>();
156 
157     @Option(
158             name = "perfetto-all-metric-prefix",
159             description = "Prefix to be used with the metrics collected from perfetto."
160                     + "This will be applied before any other prefixes to metrics.")
161     private String mAllMetricPrefix = "perfetto";
162 
163     @Option(
164             name = "perfetto-alternative-parse-format",
165             description =
166                     "Parse the metrics as key/value pair or JSON when corresponding proto "
167                             + "definition is not found. One of [json|none]")
168     private AlternativeParseFormat mAlternativeParseFormat = AlternativeParseFormat.none;
169 
170     // Matches 1.73, 1.73E+2
171     private Pattern mNumberWithExponentPattern =
172             Pattern.compile("[-+]?[0-9]*[\\.]?[0-9]+([eE][-+]?[0-9]+)?");
173 
174     // Matches numbers without exponent format.
175     private Pattern mNumberPattern = Pattern.compile("[-+]?[0-9]*[\\.]?[0-9]+");
176 
177     private List<Pattern> mMetricPatterns = new ArrayList<>();
178 
179     private String mPrefixFromInnerMessage = "";
180 
181     @Override
processTestMetricsAndLogs( TestDescription testDescription, HashMap<String, Metric> testMetrics, Map<String, LogFile> testLogs)182     public Map<String, Metric.Builder> processTestMetricsAndLogs(
183             TestDescription testDescription,
184             HashMap<String, Metric> testMetrics,
185             Map<String, LogFile> testLogs) {
186         buildMetricFilterPatterns();
187         return processPerfettoMetrics(filterPerfeticMetricFiles(testLogs));
188     }
189 
190     @Override
processRunMetricsAndLogs( HashMap<String, Metric> rawMetrics, Map<String, LogFile> runLogs)191     public Map<String, Metric.Builder> processRunMetricsAndLogs(
192             HashMap<String, Metric> rawMetrics, Map<String, LogFile> runLogs) {
193         buildMetricFilterPatterns();
194         return processPerfettoMetrics(filterPerfeticMetricFiles(runLogs));
195     }
196 
197     /**
198      * Filter the perfetto metric file based on the prefix.
199      *
200      * @param logs
201      * @return files matched the prefix.
202      */
filterPerfeticMetricFiles(Map<String, LogFile> logs)203     private List<File> filterPerfeticMetricFiles(Map<String, LogFile> logs) {
204         List<File> perfettoMetricFiles = new ArrayList<>();
205         for (String key : logs.keySet()) {
206             Optional<String> reportPrefix =
207                     mPerfettoProtoMetricFilePrefix
208                             .stream()
209                             .filter(prefix -> key.startsWith(prefix))
210                             .findAny();
211 
212             if (!reportPrefix.isPresent()) {
213                 continue;
214             }
215             perfettoMetricFiles.add(new File(logs.get(key).getPath()));
216         }
217         return perfettoMetricFiles;
218     }
219 
220     /**
221      * Process perfetto metric files into key, value pairs.
222      *
223      * @param perfettoMetricFiles perfetto metric files to be processed.
224      * @return key, value pairs processed from the metrics.
225      */
processPerfettoMetrics(List<File> perfettoMetricFiles)226     private Map<String, Metric.Builder> processPerfettoMetrics(List<File> perfettoMetricFiles) {
227         Map<String, Metric.Builder> parsedMetrics = new HashMap<>();
228         long startTime = System.currentTimeMillis();
229         File uncompressedDir = null;
230         for (File perfettoMetricFile : perfettoMetricFiles) {
231             // Text files by default are compressed before uploading. Decompress the text proto
232             // file before post processing.
233             try {
234                 if (!(mTraceProcessorOutputFormat == METRIC_FILE_FORMAT.binary) &&
235                         ZipUtil.isZipFileValid(perfettoMetricFile, true)) {
236                     ZipFile perfettoZippedFile = new ZipFile(perfettoMetricFile);
237                     uncompressedDir = FileUtil.createTempDir("uncompressed_perfetto_metric");
238                     ZipUtil2.extractZip(perfettoZippedFile, uncompressedDir);
239                     perfettoMetricFile = uncompressedDir.listFiles()[0];
240                     perfettoZippedFile.close();
241                 }
242             } catch (IOException e) {
243                 CLog.e(
244                         "IOException happened when unzipping the perfetto metric proto"
245                                 + " file."
246                                 + e.getMessage());
247             }
248 
249             // Parse the perfetto proto file.
250             try (BufferedReader bufferedReader =
251                     new BufferedReader(new FileReader(perfettoMetricFile))) {
252                 switch (mTraceProcessorOutputFormat) {
253                     case text:
254                         TraceMetrics.Builder builder = TraceMetrics.newBuilder();
255                         TextFormat.merge(bufferedReader, builder);
256                         parsedMetrics.putAll(
257                                 handlePrefixForProcessedMetrics(
258                                         convertPerfettoProtoMessage(builder.build())));
259                         break;
260                     case binary:
261                         TraceMetrics metricProto = null;
262                         metricProto =
263                                 TraceMetrics.parseFrom(new FileInputStream(perfettoMetricFile));
264                         parsedMetrics.putAll(
265                                 handlePrefixForProcessedMetrics(
266                                         convertPerfettoProtoMessage(metricProto)));
267                         break;
268                     case json:
269                         CLog.w("JSON perfetto metric file processing not supported.");
270                 }
271             } catch (ParseException e) {
272                 if (AlternativeParseFormat.none == mAlternativeParseFormat) {
273                     CLog.e("Failed to merge the perfetto metric file. " + e.getMessage());
274                 } else {
275                     CLog.w("Failed to merge the perfetto metric file, trying alternative");
276                     parsedMetrics.putAll(
277                             handlePrefixForProcessedMetrics(
278                                     processPerfettoMetricsWithAlternativeMethods(
279                                             perfettoMetricFile)));
280                 }
281             } catch (IOException ioe) {
282                 CLog.e(
283                         "IOException happened when reading the perfetto metric file. "
284                                 + ioe.getMessage());
285             } finally {
286                 // Delete the uncompressed perfetto metric proto file directory.
287                 FileUtil.recursiveDelete(uncompressedDir);
288             }
289         }
290 
291         if (parsedMetrics.size() > 0) {
292             parsedMetrics.put(
293                     RUNTIME_METRIC_KEY,
294                     TfMetricProtoUtil.stringToMetric(
295                             Long.toString(System.currentTimeMillis() - startTime))
296                             .toBuilder());
297         }
298 
299         return parsedMetrics;
300     }
301 
302     /**
303      * Process perfetto metric files that does not have proto defined in TraceMetrics into key,
304      * value pairs.
305      *
306      * @param perfettoMetricFile perfetto metric file to be processed.
307      * @return key, value pairs processed from the metrics.
308      */
processPerfettoMetricsWithAlternativeMethods( File perfettoMetricFile)309     private Map<String, Metric.Builder> processPerfettoMetricsWithAlternativeMethods(
310             File perfettoMetricFile) {
311         CLog.w("Entering processPerfettoMetricsWithAlternativeMethods");
312         Map<String, Metric.Builder> result = new HashMap<>();
313         try (BufferedReader bufferedReader =
314                 new BufferedReader(new FileReader(perfettoMetricFile))) {
315             if (AlternativeParseFormat.json == mAlternativeParseFormat) {
316                 JsonObject node = new Gson().fromJson(bufferedReader, JsonObject.class);
317                 node.entrySet().forEach(nested -> flattenJson(result, nested, new ArrayList<>()));
318                 return result;
319             }
320         } catch (JsonSyntaxException jse) {
321             CLog.e(
322                     "JsonSyntaxException happened when parsing perfetto metric file. "
323                             + jse.getMessage());
324         } catch (IOException ioe) {
325             CLog.e(
326                     "IOException happened when reading the perfetto metric file. "
327                             + ioe.getMessage());
328         }
329 
330         return result;
331     }
332 
333     /**
334      * Flatten a json into key, value pairs where key is the concatenation of keys in each level of
335      * json, and the value is the value of the leaf node.
336      *
337      * @param result the map to store the result
338      * @param node the node to process from
339      * @names the list of the names that has been added so far above the current node.
340      * @return key, value pairs of the flattened json.
341      */
flattenJson( Map<String, Metric.Builder> result, Entry<String, JsonElement> node, List<String> names)342     private Map<String, Metric.Builder> flattenJson(
343             Map<String, Metric.Builder> result,
344             Entry<String, JsonElement> node,
345             List<String> names) {
346         names.add(node.getKey());
347         if (node.getValue().isJsonObject()) {
348             node.getValue()
349                     .getAsJsonObject()
350                     .entrySet()
351                     .forEach(nested -> flattenJson(result, nested, new ArrayList<>(names)));
352         } else {
353             String name = names.stream().collect(Collectors.joining(METRIC_SEP));
354             result.put(
355                     name,
356                     TfMetricProtoUtil.stringToMetric(node.getValue().getAsString()).toBuilder());
357         }
358 
359         return result;
360     }
361 
handlePrefixForProcessedMetrics( Map<String, Metric.Builder> processedMetrics)362     private Map<String, Metric.Builder> handlePrefixForProcessedMetrics(
363             Map<String, Metric.Builder> processedMetrics) {
364         Map<String, Metric.Builder> result = new HashMap<>();
365         result.putAll(filterMetrics(processedMetrics));
366         replacePrefix(result);
367         // Generic prefix string is applied to all the metrics parsed from perfetto trace file.
368         replaceAllMetricPrefix(result);
369         return result;
370     }
371 
372     /**
373      * Replace the prefix in the metric key parsed from the proto file with the given string.
374      *
375      * @param processPerfettoMetrics metrics parsed from the perfetto proto file.
376      */
replacePrefix(Map<String, Metric.Builder> processPerfettoMetrics)377     private void replacePrefix(Map<String, Metric.Builder> processPerfettoMetrics) {
378         if (mReplacePrefixMap.isEmpty()) {
379             return;
380         }
381         Map<String, Metric.Builder> finalMetrics = new HashMap<String, Metric.Builder>();
382         for (Map.Entry<String, Metric.Builder> metric : processPerfettoMetrics.entrySet()) {
383             boolean isReplaced = false;
384             for (Map.Entry<String, String> replaceEntry : mReplacePrefixMap.entrySet()) {
385                 if (metric.getKey().startsWith(replaceEntry.getKey())) {
386                     String newKey = metric.getKey().replaceFirst(replaceEntry.getKey(),
387                             replaceEntry.getValue());
388                     finalMetrics.put(newKey, metric.getValue());
389                     isReplaced = true;
390                     break;
391                 }
392             }
393             // If key is not replaced put the original key and value in the final metrics.
394             if (!isReplaced) {
395                 finalMetrics.put(metric.getKey(), metric.getValue());
396             }
397         }
398         processPerfettoMetrics.clear();
399         processPerfettoMetrics.putAll(finalMetrics);
400     }
401 
402     /**
403      * Prefix all the metrics key with given string.
404      *
405      * @param processPerfettoMetrics metrics parsed from the perfetto proto file.
406      */
replaceAllMetricPrefix(Map<String, Metric.Builder> processPerfettoMetrics)407     private void replaceAllMetricPrefix(Map<String, Metric.Builder> processPerfettoMetrics) {
408         if (mAllMetricPrefix == null || mAllMetricPrefix.isEmpty()) {
409             return;
410         }
411         Map<String, Metric.Builder> finalMetrics = new HashMap<String, Metric.Builder>();
412         for (Map.Entry<String, Metric.Builder> metric : processPerfettoMetrics.entrySet()) {
413             String newKey = String.format("%s_%s", mAllMetricPrefix, metric.getKey());
414             finalMetrics.put(newKey, metric.getValue());
415             CLog.d("Perfetto trace metric: key: %s value: %s", newKey, metric.getValue());
416         }
417         processPerfettoMetrics.clear();
418         processPerfettoMetrics.putAll(finalMetrics);
419     }
420 
421     /**
422      * Expands the metric proto file as tree structure and converts it into key, value pairs by
423      * recursively constructing the key using the message name, proto fields with string values
424      * until the numeric proto field is encountered.
425      *
426      * <p>android_startup-startup-package_name-com.calculator-to_first_frame-dur_ns: 300620342
427      * android_startup-startup-package_name-com.nexuslauncher-to_first_frame-dur_ns: 49257713
428      *
429      * <p>It also supports indexing the list proto fields optionally. This will be used if the list
430      * generates duplicate key's when recursively expanding the messages to prevent overriding the
431      * results.
432      *
433      * <p>"perfetto-indexed-list-field" - perfetto.protos.AndroidStartupMetric.Startup
434      *
435      * <p><android_startup-startup#1-package_name-com.calculator-to_first_frame-dur_ns: 300620342
436      * android_startup-startup#2-package_name-com.nexuslauncher-to_first_frame-dur_ns: 49257713
437      * android_startup-startup#3-package_name-com.calculator-to_first_frame-dur_ns: 261382005
438      *
439      * <p>"perfetto-prefix-key-field" - perfetto.protos.ProcessRenderInfo.process_name
440      * android_hwui_metric-process_info-process_name-system_server-cache_miss_avg
441      *
442      */
convertPerfettoProtoMessage(Message reportMessage)443     private Map<String, Metric.Builder> convertPerfettoProtoMessage(Message reportMessage) {
444         Map<FieldDescriptor, Object> fields = reportMessage.getAllFields();
445         Map<String, Metric.Builder> convertedMetrics = new HashMap<String, Metric.Builder>();
446         List<String> keyPrefixes = new ArrayList<String>();
447 
448         // Key that will be used to prefix the other keys in the same proto message.
449         String keyPrefixOtherFields = "";
450         // If the flag is set then the prefix is set from the current message. Used
451         // to clear the prefix text after all the metrics are prefixed.
452         boolean prefixSetInCurrentMessage = false;
453 
454         // TODO(b/15014555): Cleanup the parsing logic.
455         for (Entry<FieldDescriptor, Object> entry : fields.entrySet()) {
456             if (!(entry.getValue() instanceof Message) && !(entry.getValue() instanceof List)) {
457                 if (isNumeric(entry.getValue().toString())) {
458                     // Check if the current field has to be used as prefix for other fields
459                     // and add it to the list of prefixes.
460                     if (mPerfettoPrefixKeyFields.contains(entry.getKey().toString())) {
461                         if (!keyPrefixOtherFields.isEmpty()) {
462                             keyPrefixOtherFields = keyPrefixOtherFields.concat("-");
463                         }
464                         keyPrefixOtherFields = keyPrefixOtherFields.concat(String.format("%s-%s",
465                                 entry.getKey().getName().toString(), entry.getValue().toString()));
466                         continue;
467                     }
468 
469                     if (mPerfettoPrefixInnerMessagePrefixFields.contains(
470                             entry.getKey().toString())) {
471                         if (!mPrefixFromInnerMessage.isEmpty()) {
472                             mPrefixFromInnerMessage = mPrefixFromInnerMessage.concat("-");
473                         }
474                         mPrefixFromInnerMessage = mPrefixFromInnerMessage.concat(String.format(
475                                 "%s-%s", entry.getKey().getName().toString(),
476                                 entry.getValue().toString()));
477                         prefixSetInCurrentMessage = true;
478                         continue;
479                     }
480 
481                     // Otherwise treat this numeric field as metric.
482                     if (mNumberPattern.matcher(entry.getValue().toString()).matches()) {
483                         convertedMetrics.put(
484                                 entry.getKey().getName(),
485                                 TfMetricProtoUtil.stringToMetric(entry.getValue().toString())
486                                         .toBuilder());
487                     } else {
488                         // Parse the exponent notation of string before adding it to metric.
489                         convertedMetrics.put(
490                                 entry.getKey().getName(),
491                                 TfMetricProtoUtil.stringToMetric(
492                                         Long.toString(
493                                                 Double.valueOf(entry.getValue().toString())
494                                                         .longValue()))
495                                         .toBuilder());
496                     }
497                 } else {
498                     // Add to prefix list if string value is encountered.
499                     keyPrefixes.add(
500                             String.join(
501                                     METRIC_SEP,
502                                     entry.getKey().getName().toString(),
503                                     entry.getValue().toString()));
504                     if (mPerfettoPrefixKeyFields.contains(entry.getKey().toString())) {
505                         if (!keyPrefixOtherFields.isEmpty()) {
506                             keyPrefixOtherFields = keyPrefixOtherFields.concat("-");
507                         }
508                         keyPrefixOtherFields =  keyPrefixOtherFields.concat(String.format("%s-%s",
509                                 entry.getKey().getName().toString(), entry.getValue().toString()));
510                     }
511 
512                     if (mPerfettoPrefixInnerMessagePrefixFields.contains(
513                             entry.getKey().toString())) {
514                         if (!mPrefixFromInnerMessage.isEmpty()) {
515                             mPrefixFromInnerMessage = mPrefixFromInnerMessage.concat("-");
516                         }
517                         mPrefixFromInnerMessage = mPrefixFromInnerMessage.concat(String.format(
518                                 "%s-%s", entry.getKey().getName().toString(),
519                                 entry.getValue().toString()));
520                         prefixSetInCurrentMessage = true;
521                         continue;
522                     }
523                 }
524             }
525         }
526 
527         // Recursively expand the proto messages and repeated fields(i.e list).
528         // Recursion when there are no messages or list with in the current message.
529         // Used to cache the message prefix.
530         String innerMessagePrefix = "";
531         for (Entry<FieldDescriptor, Object> entry : fields.entrySet()) {
532             if (entry.getValue() instanceof Message) {
533                 Map<String, Metric.Builder> messageMetrics =
534                         convertPerfettoProtoMessage((Message) entry.getValue());
535                 if (!mPrefixFromInnerMessage.isEmpty()) {
536                   innerMessagePrefix = mPrefixFromInnerMessage;
537                 }
538                 for (Entry<String, Metric.Builder> metricEntry : messageMetrics.entrySet()) {
539                     // Add prefix to the metrics parsed from this message.
540                     for (String prefix : keyPrefixes) {
541                         convertedMetrics.put(
542                                 String.join(
543                                         METRIC_SEP,
544                                         prefix,
545                                         entry.getKey().getName(),
546                                         metricEntry.getKey()),
547                                 metricEntry.getValue());
548                     }
549                     if (keyPrefixes.isEmpty()) {
550                         convertedMetrics.put(
551                                 String.join(
552                                         METRIC_SEP, entry.getKey().getName(), metricEntry.getKey()),
553                                 metricEntry.getValue());
554                     }
555                 }
556             } else if (entry.getValue() instanceof List) {
557                 List<? extends Object> listMetrics = (List) entry.getValue();
558                 for (int i = 0; i < listMetrics.size(); i++) {
559                     String metricKeyRoot;
560                     // Use indexing if the current field is chosen for indexing.
561                     // Use it if metrics keys generated has duplicates to prevent overriding.
562                     if (mPerfettoIndexedListFields.contains(entry.getKey().toString())) {
563                         metricKeyRoot =
564                                 String.join(
565                                         METRIC_SEP,
566                                         entry.getKey().getName(),
567                                         String.valueOf(i + 1));
568                     } else {
569                         metricKeyRoot = String.join(METRIC_SEP, entry.getKey().getName());
570                     }
571                     if (listMetrics.get(i) instanceof Message) {
572                         Map<String, Metric.Builder> messageMetrics =
573                                 convertPerfettoProtoMessage((Message) listMetrics.get(i));
574                         if (!mPrefixFromInnerMessage.isEmpty()) {
575                             innerMessagePrefix = mPrefixFromInnerMessage;
576                         }
577                         for (Entry<String, Metric.Builder> metricEntry :
578                                 messageMetrics.entrySet()) {
579                             for (String prefix : keyPrefixes) {
580                                 convertedMetrics.put(
581                                         String.join(
582                                                 METRIC_SEP,
583                                                 prefix,
584                                                 metricKeyRoot,
585                                                 metricEntry.getKey()),
586                                         metricEntry.getValue());
587                             }
588                             if (keyPrefixes.isEmpty()) {
589                                 convertedMetrics.put(
590                                         String.join(
591                                                 METRIC_SEP, metricKeyRoot, metricEntry.getKey()),
592                                         metricEntry.getValue());
593                             }
594                         }
595                     } else {
596                         convertedMetrics.put(
597                                 metricKeyRoot,
598                                 TfMetricProtoUtil.stringToMetric(listMetrics.get(i).toString())
599                                         .toBuilder());
600                     }
601                 }
602             }
603         }
604 
605         // Add prefix key to all the keys in current proto message which has numeric values.
606         Map<String, Metric.Builder> additionalConvertedMetrics =
607                 new HashMap<String, Metric.Builder>();
608         if (!keyPrefixOtherFields.isEmpty()) {
609             for (Map.Entry<String, Metric.Builder> currentMetric : convertedMetrics.entrySet()) {
610                 additionalConvertedMetrics.put(String.format("%s-%s", keyPrefixOtherFields,
611                         currentMetric.getKey()), currentMetric.getValue());
612             }
613         }
614 
615         if (!mPrefixFromInnerMessage.isEmpty() || !innerMessagePrefix.isEmpty()) {
616           String prefixToUse = !mPrefixFromInnerMessage.isEmpty()
617                   ? mPrefixFromInnerMessage : innerMessagePrefix;
618           for (Map.Entry<String, Metric.Builder> currentMetric : convertedMetrics.entrySet()) {
619             additionalConvertedMetrics.put(
620                 String.format("%s-%s", prefixToUse, currentMetric.getKey()),
621                 currentMetric.getValue());
622           }
623         }
624 
625         if (!prefixSetInCurrentMessage) {
626           mPrefixFromInnerMessage = "";
627         }
628 
629         // Not cleaning up the other metrics without prefix fields.
630         convertedMetrics.putAll(additionalConvertedMetrics);
631 
632         return convertedMetrics;
633     }
634 
635     /**
636      * Check if the given string is number. It matches the string with exponent notation as well.
637      *
638      * <p>For example returns true for Return true for 1.73, 1.73E+2
639      */
isNumeric(String strNum)640     private boolean isNumeric(String strNum) {
641         if (strNum == null) {
642             return false;
643         }
644         return mNumberWithExponentPattern.matcher(strNum).matches();
645     }
646 
647     /** Build regular expression patterns to filter the metrics. */
buildMetricFilterPatterns()648     private void buildMetricFilterPatterns() {
649         if (!mPerfettoMetricFilterRegEx.isEmpty() && mMetricPatterns.isEmpty()) {
650             for (String regEx : mPerfettoMetricFilterRegEx) {
651                 mMetricPatterns.add(Pattern.compile(regEx));
652             }
653         }
654     }
655 
656     /**
657      * Filter parsed metrics from the proto metric files based on the regular expression. If
658      * "mPerfettoIncludeAllMetrics" is enabled then filters will be ignored and returns all the
659      * parsed metrics.
660      */
filterMetrics(Map<String, Metric.Builder> parsedMetrics)661     private Map<String, Metric.Builder> filterMetrics(Map<String, Metric.Builder> parsedMetrics) {
662         if (mPerfettoIncludeAllMetrics) {
663             return parsedMetrics;
664         }
665         Map<String, Metric.Builder> filteredMetrics = new HashMap<>();
666         for (Entry<String, Metric.Builder> metricEntry : parsedMetrics.entrySet()) {
667             for (Pattern pattern : mMetricPatterns) {
668                 if (pattern.matcher(metricEntry.getKey()).matches()) {
669                     filteredMetrics.put(metricEntry.getKey(), metricEntry.getValue());
670                     break;
671                 }
672             }
673         }
674         return filteredMetrics;
675     }
676 
677     /**
678      * Set the metric type to RAW metric.
679      */
680     @Override
getMetricType()681     protected DataType getMetricType() {
682         return DataType.RAW;
683     }
684 }
685