1#!/usr/bin/env python3 2# SPDX-License-Identifier: Apache-2.0 3# ----------------------------------------------------------------------------- 4# Copyright 2020-2022 Arm Limited 5# 6# Licensed under the Apache License, Version 2.0 (the "License"); you may not 7# use this file except in compliance with the License. You may obtain a copy 8# of the License at: 9# 10# http://www.apache.org/licenses/LICENSE-2.0 11# 12# Unless required by applicable law or agreed to in writing, software 13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15# License for the specific language governing permissions and limitations 16# under the License. 17# ----------------------------------------------------------------------------- 18""" 19A simple wrapper utility to run a callgrind profile over a test image, and 20post-process the output into an call graph image. 21 22Only runs on Linux and requires the following tools available on the PATH: 23 24 * valgrind 25 * gprof2dot 26 * dot 27""" 28 29 30import argparse 31import os 32import re 33import subprocess as sp 34import sys 35 36def postprocess_cga(lines, outfile): 37 """ 38 Postprocess the output of callgrind_annotate. 39 40 Args: 41 lines ([str]): The output of callgrind_annotate. 42 outfile (str): The output file path to write. 43 """ 44 pattern = re.compile("^\s*([0-9,]+)\s+\([ 0-9.]+%\)\s+Source/(\S+):(\S+)\(.*\).*$") 45 46 totalCost = 0.0 47 functionTable = [] 48 functionMap = {} 49 50 for line in lines: 51 line = line.strip() 52 match = pattern.match(line) 53 if not match: 54 continue 55 56 cost = float(match.group(1).replace(",", "")) 57 sourceFile = match.group(2) 58 function = match.group(3) 59 60 # Filter out library code we don't want to change 61 if function.startswith("stbi__"): 62 continue 63 64 totalCost += cost 65 66 # Accumulate the scores from functions in multiple call chains 67 if function in functionMap: 68 index = functionMap[function] 69 functionTable[index][1] += cost 70 functionTable[index][2] += cost 71 # Else add new functions to the end of the table 72 else: 73 functionMap[function] = len(functionTable) 74 functionTable.append([function, cost, cost]) 75 76 # Sort the table by accumulated cost 77 functionTable.sort(key=lambda x: 101.0 - x[2]) 78 79 for function in functionTable: 80 function[2] /= totalCost 81 function[2] *= 100.0 82 83 with open(outfile, "w") as fileHandle: 84 85 totals = 0.0 86 for function in functionTable: 87 # Omit entries less than 1% load 88 if function[2] < 1: 89 break 90 91 totals += function[2] 92 fileHandle.write("%5.2f%% %s\n" % (function[2], function[0])) 93 94 fileHandle.write("======\n") 95 fileHandle.write(f"{totals:5.2f}%\n") 96 97 98def run_pass(image, noStartup, encoder, blocksize, quality): 99 """ 100 Run Valgrind on a single binary. 101 102 Args: 103 image (str): The path of the image to compress. 104 noStartup (bool): Exclude startup from reported data. 105 encoder (str): The name of the encoder variant to run. 106 blocksize (str): The block size to use. 107 quality (str): The encoding quality to use. 108 109 Raises: 110 CalledProcessException: Any subprocess failed. 111 """ 112 binary = "./bin/astcenc-%s" % encoder 113 args = ["valgrind", "--tool=callgrind", "--callgrind-out-file=callgrind.txt", 114 binary, "-cl", image, "out.astc", blocksize, quality, "-j", "1"] 115 116 result = sp.run(args, check=True, universal_newlines=True) 117 118 args = ["callgrind_annotate", "callgrind.txt"] 119 ret = sp.run(args, stdout=sp.PIPE, check=True, encoding="utf-8") 120 lines = ret.stdout.splitlines() 121 with open("perf_%s_cga.txt" % quality.replace("-", ""), "w") as handle: 122 handle.write("\n".join(lines)) 123 124 postprocess_cga(lines, "perf_%s.txt" % quality.replace("-", "")) 125 126 if noStartup: 127 args = ["gprof2dot", "--format=callgrind", "--output=out.dot", "callgrind.txt", 128 "-s", "-z", "compress_block(astcenc_contexti const&, image_block const&, physical_compressed_block&, compression_working_buffers&)"] 129 else: 130 args = ["gprof2dot", "--format=callgrind", "--output=out.dot", "callgrind.txt", 131 "-s", "-z", "main"] 132 133 result = sp.run(args, check=True, universal_newlines=True) 134 135 args = ["dot", "-Tpng", "out.dot", "-o", "perf_%s.png" % quality.replace("-", "")] 136 result = sp.run(args, check=True, universal_newlines=True) 137 138 os.remove("out.astc") 139 os.remove("out.dot") 140 os.remove("callgrind.txt") 141 142 143def parse_command_line(): 144 """ 145 Parse the command line. 146 147 Returns: 148 Namespace: The parsed command line container. 149 """ 150 parser = argparse.ArgumentParser() 151 152 parser.add_argument("img", type=argparse.FileType("r"), 153 help="The image file to test") 154 155 encoders = ["sse2", "sse4.1", "avx2"] 156 parser.add_argument("--encoder", dest="encoder", default="avx2", 157 choices=encoders, help="select encoder variant") 158 159 testquant = [str(x) for x in range (0, 101, 10)] 160 testqual = ["-fastest", "-fast", "-medium", "-thorough", "-exhaustive"] 161 qualities = testqual + testquant 162 parser.add_argument("--test-quality", dest="quality", default="medium", 163 choices=qualities, help="select compression quality") 164 165 parser.add_argument("--no-startup", dest="noStartup", default=False, 166 action="store_true", help="Exclude init") 167 168 args = parser.parse_args() 169 170 return args 171 172 173def main(): 174 """ 175 The main function. 176 177 Returns: 178 int: The process return code. 179 """ 180 args = parse_command_line() 181 run_pass(args.img.name, args.noStartup, args.encoder, "6x6", args.quality) 182 return 0 183 184 185if __name__ == "__main__": 186 sys.exit(main()) 187