diff mbox series

[v2,1/2] scripts/performance: Add list_fn_callees.py script

Message ID 20200716103921.6605-2-ahmedkhaledkaraman@gmail.com
State New
Headers show
Series [v2,1/2] scripts/performance: Add list_fn_callees.py script | expand

Commit Message

Ahmed Karaman July 16, 2020, 10:39 a.m. UTC
Python script that prints the callees of a given list of QEMU
functions.

Syntax:
list_fn_callees.py [-h] -f FUNCTION [FUNCTION ...] -- \
               <qemu executable> [<qemu executable options>] \
               <target executable> [<target executable options>]

[-h] - Print the script arguments help message.
-f FUNCTION [FUNCTION ...] - List of function names

Example of usage:
list_fn_callees.py -f helper_float_sub_d helper_float_mul_d -- \
                      qemu-mips coulomb_double-mips -n10

Example output:
 Total number of instructions: 108,952,851

 Callees of helper_float_sub_d:

 No. Instructions Percentage  Calls Ins/Call Function Name Source File
 --- ------------ ---------- ------ -------- ------------- ---------------
   1      153,160     0.141%  1,305     117  float64_sub   <qemu>/fpu/softfloat.c

 Callees of helper_float_mul_d:

 No. Instructions Percentage  Calls Ins/Call Function Name Source File
 --- ------------ ---------- ------ -------- ------------- ---------------
   1      131,137     0.120%  1,014      129 float64_mul   <qemu>/fpu/softfloat.c

Signed-off-by: Ahmed Karaman <ahmedkhaledkaraman@gmail.com>
---
 scripts/performance/list_fn_callees.py | 228 +++++++++++++++++++++++++
 1 file changed, 228 insertions(+)
 create mode 100755 scripts/performance/list_fn_callees.py
diff mbox series

Patch

diff --git a/scripts/performance/list_fn_callees.py b/scripts/performance/list_fn_callees.py
new file mode 100755
index 0000000000..f0ec5c8e81
--- /dev/null
+++ b/scripts/performance/list_fn_callees.py
@@ -0,0 +1,228 @@ 
+#!/usr/bin/env python3
+
+#  Print the callees of a given list of QEMU functions.
+#
+#  Syntax:
+#  list_fn_callees.py [-h] -f FUNCTION [FUNCTION ...] -- \
+#                 <qemu executable> [<qemu executable options>] \
+#                 <target executable> [<target executable options>]
+#
+#  [-h] - Print the script arguments help message.
+#  -f FUNCTION [FUNCTION ...] - List of function names
+#
+#  Example of usage:
+#  list_fn_callees.py -f helper_float_sub_d helper_float_mul_d -- \
+#                        qemu-mips coulomb_double-mips
+#
+#  This file is a part of the project "TCG Continuous Benchmarking".
+#
+#  Copyright (C) 2020  Ahmed Karaman <ahmedkhaledkaraman@gmail.com>
+#  Copyright (C) 2020  Aleksandar Markovic <aleksandar.qemu.devel@gmail.com>
+#
+#  This program is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 2 of the License, or
+#  (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import argparse
+import os
+import subprocess
+import sys
+import tempfile
+
+
+def find_function_lines(function_name, callgrind_data):
+    """
+    Search for the line with the function name in the
+    callgrind_annotate output when ran using --tre=calling.
+    All the function callees should be listed after that line.
+
+    Parameters:
+    function_name (string): The desired function name to print its callees
+    callgrind_data (list): callgrind_annotate output
+
+    Returns:
+    (list): List of function line numbers
+    """
+    lines = []
+    for i in range(len(callgrind_data)):
+        split_line = callgrind_data[i].split()
+        if len(split_line) > 2 and \
+                split_line[1] == "*" and \
+                split_line[2].split(":")[-1] == function_name:
+            # Function might be in the callgrind_annotate output more than
+            # once, so don't break after finding an instance
+            if callgrind_data[i + 1] != "\n":
+                # Only append the line number if the found instance has
+                # callees
+                lines.append(i)
+    return lines
+
+
+def get_function_calles(function_lines, callgrind_data):
+    """
+    Get all callees data for a function given its list of line numbers in
+    callgrind_annotate output.
+
+    Parameters:
+    function_lines (list): Line numbers of the function to get its callees
+    callgrind_data (list): callgrind_annotate output
+
+    Returns:
+    (list):[[number_of_instructions(int), callee_name(str),
+             number_of_calls(int), source_file(str)]]
+    """
+    callees = []
+    for function_line in function_lines:
+        next_callee = function_line + 1
+        while (callgrind_data[next_callee] != "\n"):
+            split_line = callgrind_data[next_callee].split()
+            number_of_instructions = int(split_line[0].replace(",", ""))
+            source_file = split_line[2].split(":")[0]
+            callee_name = split_line[2].split(":")[1]
+            number_of_calls = int(split_line[3][1:-2])
+            callees.append([number_of_instructions, callee_name,
+                            number_of_calls, source_file])
+            next_callee += 1
+    return sorted(callees, reverse=True)
+
+
+def main():
+    # Parse the command line arguments
+    parser = argparse.ArgumentParser(
+        usage="list_fn_callees.py [-h] -f FUNCTION [FUNCTION ...] -- "
+        "<qemu executable> [<qemu executable options>] "
+        "<target executable> [<target executable options>]")
+
+    parser.add_argument("-f", dest="function", type=str,
+                        nargs="+", required=True,
+                        help="list of function names to print their callees")
+
+    parser.add_argument("command", type=str, nargs="+", help=argparse.SUPPRESS)
+
+    args = parser.parse_args()
+
+    # Extract the needed variables from the args
+    command = args.command
+    function_names = args.function
+
+    # Insure that valgrind is installed
+    check_valgrind = subprocess.run(
+        ["which", "valgrind"], stdout=subprocess.DEVNULL)
+    if check_valgrind.returncode:
+        sys.exit("Please install valgrind before running the script.")
+
+    # Save all intermediate files in a temporary directory
+    with tempfile.TemporaryDirectory() as tmpdirname:
+        # callgrind output file path
+        data_path = os.path.join(tmpdirname, "callgrind.data")
+        # callgrind_annotate output file path
+        annotate_out_path = os.path.join(tmpdirname, "callgrind_annotate.out")
+
+        # Run callgrind
+        callgrind = subprocess.run((["valgrind",
+                                     "--tool=callgrind",
+                                     "--callgrind-out-file=" + data_path]
+                                    + command),
+                                   stdout=subprocess.DEVNULL,
+                                   stderr=subprocess.PIPE)
+        if callgrind.returncode:
+            sys.exit(callgrind.stderr.decode("utf-8"))
+
+        # Save callgrind_annotate output
+        with open(annotate_out_path, "w") as output:
+            callgrind_annotate = subprocess.run(
+                ["callgrind_annotate", data_path,
+                    "--threshold=100", "--tree=calling"],
+                stdout=output,
+                stderr=subprocess.PIPE)
+            if callgrind_annotate.returncode:
+                sys.exit(callgrind_annotate.stderr.decode("utf-8"))
+
+        # Read the callgrind_annotate output to callgrind_data[]
+        callgrind_data = []
+        with open(annotate_out_path, "r") as data:
+            callgrind_data = data.readlines()
+
+        # Line number with the total number of instructions
+        total_instructions_line_number = 20
+        # Get the total number of instructions
+        total_instructions_line_data = \
+            callgrind_data[total_instructions_line_number]
+        total_instructions = total_instructions_line_data.split()[0]
+
+        print("Total number of instructions: {}\n".format(total_instructions))
+
+        # Remove commas and convert to int
+        total_instructions = int(total_instructions.replace(",", ""))
+
+        for function_name in function_names:
+            # Line numbers with the desired function
+            function_lines = find_function_lines(function_name, callgrind_data)
+
+            if len(function_lines) == 0:
+                print("Couldn't locate function: {}.\n".format(
+                    function_name))
+                continue
+
+            # Get function callees
+            function_callees = get_function_calles(
+                function_lines, callgrind_data)
+
+            print("Callees of {}:\n".format(function_name))
+
+            # Print table header
+            print("{:>4}  {:>15}  {:>10}  {:>15}  {:>10}  {:<25}  {}".
+                  format(
+                      "No.",
+                      "Instructions",
+                      "Percentage",
+                      "Calls",
+                      "Ins/Call",
+                      "Function Name",
+                      "Source File")
+                  )
+
+            print("{:>4}  {:>15}  {:>10}  {:>15}  {:>10}  {:<25}  {}".
+                  format(
+                      "-" * 4,
+                      "-" * 15,
+                      "-" * 10,
+                      "-" * 15,
+                      "-" * 10,
+                      "-" * 25,
+                      "-" * 30)
+                  )
+
+            for (index, callee) in enumerate(function_callees, start=1):
+                instructions = callee[0]
+                percentage = (callee[0] / total_instructions) * 100
+                calls = callee[2]
+                instruction_per_call = int(callee[0] / callee[2])
+                function_name = callee[1]
+                source_file = callee[3]
+                # Print extracted data
+                print("{:>4}  {:>15}  {:>9.3f}%  {:>15}  {:>10}  {:<25}  {}".
+                      format(
+                          index,
+                          format(instructions, ","),
+                          round(percentage, 3),
+                          format(calls, ","),
+                          format(instruction_per_call, ","),
+                          function_name,
+                          source_file)
+                      )
+
+            print("\n")
+
+
+if __name__ == "__main__":
+    main()