187 lines
5.8 KiB
Python
187 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
# -----------------------------------------------------------------------------
|
|
# Copyright 2020-2022 Arm Limited
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
# use this file except in compliance with the License. You may obtain a copy
|
|
# of the License at:
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
# -----------------------------------------------------------------------------
|
|
"""
|
|
A simple wrapper utility to run a callgrind profile over a test image, and
|
|
post-process the output into an call graph image.
|
|
|
|
Only runs on Linux and requires the following tools available on the PATH:
|
|
|
|
* valgrind
|
|
* gprof2dot
|
|
* dot
|
|
"""
|
|
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import subprocess as sp
|
|
import sys
|
|
|
|
def postprocess_cga(lines, outfile):
|
|
"""
|
|
Postprocess the output of callgrind_annotate.
|
|
|
|
Args:
|
|
lines ([str]): The output of callgrind_annotate.
|
|
outfile (str): The output file path to write.
|
|
"""
|
|
pattern = re.compile("^\s*([0-9,]+)\s+\([ 0-9.]+%\)\s+Source/(\S+):(\S+)\(.*\).*$")
|
|
|
|
totalCost = 0.0
|
|
functionTable = []
|
|
functionMap = {}
|
|
|
|
for line in lines:
|
|
line = line.strip()
|
|
match = pattern.match(line)
|
|
if not match:
|
|
continue
|
|
|
|
cost = float(match.group(1).replace(",", ""))
|
|
sourceFile = match.group(2)
|
|
function = match.group(3)
|
|
|
|
# Filter out library code we don't want to change
|
|
if function.startswith("stbi__"):
|
|
continue
|
|
|
|
totalCost += cost
|
|
|
|
# Accumulate the scores from functions in multiple call chains
|
|
if function in functionMap:
|
|
index = functionMap[function]
|
|
functionTable[index][1] += cost
|
|
functionTable[index][2] += cost
|
|
# Else add new functions to the end of the table
|
|
else:
|
|
functionMap[function] = len(functionTable)
|
|
functionTable.append([function, cost, cost])
|
|
|
|
# Sort the table by accumulated cost
|
|
functionTable.sort(key=lambda x: 101.0 - x[2])
|
|
|
|
for function in functionTable:
|
|
function[2] /= totalCost
|
|
function[2] *= 100.0
|
|
|
|
with open(outfile, "w") as fileHandle:
|
|
|
|
totals = 0.0
|
|
for function in functionTable:
|
|
# Omit entries less than 1% load
|
|
if function[2] < 1:
|
|
break
|
|
|
|
totals += function[2]
|
|
fileHandle.write("%5.2f%% %s\n" % (function[2], function[0]))
|
|
|
|
fileHandle.write("======\n")
|
|
fileHandle.write(f"{totals:5.2f}%\n")
|
|
|
|
|
|
def run_pass(image, noStartup, encoder, blocksize, quality):
|
|
"""
|
|
Run Valgrind on a single binary.
|
|
|
|
Args:
|
|
image (str): The path of the image to compress.
|
|
noStartup (bool): Exclude startup from reported data.
|
|
encoder (str): The name of the encoder variant to run.
|
|
blocksize (str): The block size to use.
|
|
quality (str): The encoding quality to use.
|
|
|
|
Raises:
|
|
CalledProcessException: Any subprocess failed.
|
|
"""
|
|
binary = "./bin/astcenc-%s" % encoder
|
|
args = ["valgrind", "--tool=callgrind", "--callgrind-out-file=callgrind.txt",
|
|
binary, "-cl", image, "out.astc", blocksize, quality, "-j", "1"]
|
|
|
|
result = sp.run(args, check=True, universal_newlines=True)
|
|
|
|
args = ["callgrind_annotate", "callgrind.txt"]
|
|
ret = sp.run(args, stdout=sp.PIPE, check=True, encoding="utf-8")
|
|
lines = ret.stdout.splitlines()
|
|
with open("perf_%s_cga.txt" % quality.replace("-", ""), "w") as handle:
|
|
handle.write("\n".join(lines))
|
|
|
|
postprocess_cga(lines, "perf_%s.txt" % quality.replace("-", ""))
|
|
|
|
if noStartup:
|
|
args = ["gprof2dot", "--format=callgrind", "--output=out.dot", "callgrind.txt",
|
|
"-s", "-z", "compress_block(astcenc_contexti const&, image_block const&, unsigned char*, compression_working_buffers&)"]
|
|
else:
|
|
args = ["gprof2dot", "--format=callgrind", "--output=out.dot", "callgrind.txt",
|
|
"-s", "-z", "main"]
|
|
|
|
result = sp.run(args, check=True, universal_newlines=True)
|
|
|
|
args = ["dot", "-Tpng", "out.dot", "-o", "perf_%s.png" % quality.replace("-", "")]
|
|
result = sp.run(args, check=True, universal_newlines=True)
|
|
|
|
os.remove("out.astc")
|
|
os.remove("out.dot")
|
|
os.remove("callgrind.txt")
|
|
|
|
|
|
def parse_command_line():
|
|
"""
|
|
Parse the command line.
|
|
|
|
Returns:
|
|
Namespace: The parsed command line container.
|
|
"""
|
|
parser = argparse.ArgumentParser()
|
|
|
|
parser.add_argument("img", type=argparse.FileType("r"),
|
|
help="The image file to test")
|
|
|
|
encoders = ["sse2", "sse4.1", "avx2"]
|
|
parser.add_argument("--encoder", dest="encoder", default="avx2",
|
|
choices=encoders, help="select encoder variant")
|
|
|
|
testquant = [str(x) for x in range (0, 101, 10)]
|
|
testqual = ["-fastest", "-fast", "-medium", "-thorough", "-exhaustive"]
|
|
qualities = testqual + testquant
|
|
parser.add_argument("--test-quality", dest="quality", default="medium",
|
|
choices=qualities, help="select compression quality")
|
|
|
|
parser.add_argument("--no-startup", dest="noStartup", default=False,
|
|
action="store_true", help="Exclude init")
|
|
|
|
args = parser.parse_args()
|
|
|
|
return args
|
|
|
|
|
|
def main():
|
|
"""
|
|
The main function.
|
|
|
|
Returns:
|
|
int: The process return code.
|
|
"""
|
|
args = parse_command_line()
|
|
run_pass(args.img.name, args.noStartup, args.encoder, "6x6", args.quality)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|