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