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