#!/usr/pkg/bin/python3.13
# pylint: disable=C0103,C0114,C0116,C0209,R0914,R0912,R0915,eval-used
######################################################################

import argparse
import collections
import re
# from pprint import pprint

######################################################################


def profcfunc(filename: str) -> None:
    funcs = {}

    with open(filename, "r", encoding="utf8") as fh:

        for line in fh:
            #                  %time      cumesec   selfsec     calls     {stuff}   name
            match = re.match(
                r'^\s*([0-9.]+)\s+[0-9.]+\s+([0-9.]+)\s+([0-9.]+)\s+[^a-zA-Z_]*([a-zA-Z_].*)$',
                line)
            if match:
                pct = float(match.group(1))
                sec = float(match.group(2))
                calls = float(match.group(3))
                func = match.group(4)
                if func not in funcs:
                    funcs[func] = {'pct': 0.0, 'sec': 0.0, 'calls': 0}
                funcs[func]['pct'] += pct
                funcs[func]['sec'] += sec
                funcs[func]['calls'] += calls
                continue

            # Older gprofs have no call column for single-call functions
            #                  %time      cumesec   selfsec      {stuff}   name
            match = re.match(r'^\s*([0-9.]+)\s+[0-9.]+\s+([0-9.]+)\s+[^a-zA-Z_]*([a-zA-Z_].*)$',
                             line)
            if match:
                pct = float(match.group(1))
                sec = float(match.group(2))
                calls = 1
                func = match.group(3)
                if func not in funcs:
                    funcs[func] = {'pct': 0, 'sec': 0, 'calls': 0}
                funcs[func]['pct'] += pct
                funcs[func]['sec'] += sec
                funcs[func]['calls'] += calls
                continue

    # Find modules
    verilated_mods = {}
    for func in funcs:
        match = re.search(r'(.*)::eval(_step)?\(', func)
        if match:
            prefix = match.group(1)
            if Args.debug:
                print("-got _eval %s   prefix=%s" % (func, prefix))
            verilated_mods[prefix] = re.compile(r'^' + prefix)
    # pprint(verilated_mods)

    # Sort by Verilog name
    vfuncs = {}
    groups = {}
    groups['type'] = collections.defaultdict(lambda: 0)
    groups['design'] = collections.defaultdict(lambda: 0)
    groups['module'] = collections.defaultdict(lambda: 0)

    for func, func_item in funcs.items():
        pct = func_item['pct']
        vfunc = func

        funcarg = re.sub(r'^.*\(', '', func)

        design = None
        for vde, vde_item in verilated_mods.items():
            if vde_item.match(func) or vde_item.match(funcarg):
                design = vde
                break

        vdesign = "-"

        prof_match = re.search(r'__PROF__([a-zA-Z_0-9]+)__l?([0-9]+)\(', vfunc)
        if design and prof_match:
            linefunc = prof_match.group(1)
            lineno = int(prof_match.group(2))
            vfunc = "VBlock    %s:%d" % (linefunc, lineno)
            vdesign = design
            groups['type']["Verilog Blocks under " + design] += pct
            groups['design'][design] += pct
            groups['module'][linefunc] += pct
        elif design:
            vfunc = "VCommon   " + func
            vdesign = design
            groups['type']["Common code under " + design] += pct
            groups['design'][design] += pct
            groups['module'][design + " common code"] += pct
        elif re.match(r'(VL_[A-Z0-9_]+|_?vl_[a-zA-Z0-9_]+|Verilated)', vfunc):
            vfunc = "VLib      " + func
            groups['type']['VLib'] += pct
            groups['design']['VLib'] += pct
            groups['module']['VLib'] += pct
        elif re.match(r'^_mcount_private', vfunc):
            vfunc = "Prof      " + func
            groups['type']['Prof'] += pct
            groups['design']['Prof'] += pct
            groups['module']['Prof'] += pct
        else:
            vfunc = "C++       " + func
            groups['type']['C++'] += pct
            groups['design']['C++'] += pct
            groups['module']['C++'] += pct

        if vfunc not in vfuncs:
            vfuncs[vfunc] = func_item
            vfuncs[vfunc]['design'] = vdesign
        else:
            vfuncs[vfunc]['pct'] += func_item['pct']
            vfuncs[vfunc]['calls'] += func_item['calls']
            vfuncs[vfunc]['sec'] += func_item['sec']

    for ftype in ['type', 'design', 'module']:
        missing = 100
        for item in groups[ftype].keys():
            missing -= groups[ftype][item]
        groups[ftype]["\377Unaccounted for/rounding error"] = missing

        print("Overall summary by %s:" % ftype)
        print("  %-6s  %s" % ("% time", ftype))
        for what in sorted(groups[ftype].keys()):
            # \377 used to establish sort order
            pwhat = re.sub(r'^\377', '', what)
            print("  %6.2f  %s" % (groups[ftype][what], pwhat))
        print()

    design_width = 1
    for func, func_item in vfuncs.items():
        design_width = max(design_width, len(func_item['design']))

    print("Verilog code profile:")
    print("   These are split into three categories:")
    print("      C++:     Time in non-Verilated C++ code")
    print("      Prof:    Time in profile overhead")
    print("      VBlock:  Time attributable to a block in a" + " Verilog file and line")
    print("      VCommon: Time in a Verilated module," + " due to all parts of the design")
    print("      VLib:    Time in Verilated common libraries," + " called by the Verilated code")
    print()

    print("  %   cumulative   self              ")
    print((" time   seconds   seconds      calls   %-" + str(design_width) +
           "s  type      filename and line number") % "design")

    cume = 0.0
    for func in sorted(vfuncs.keys(), key=lambda f: vfuncs[f]['sec'], reverse=True):
        cume += vfuncs[func]['sec']
        print(("%6.2f %9.2f %8.2f %10d   %-" + str(design_width) + "s  %s") %
              (vfuncs[func]['pct'], cume, vfuncs[func]['sec'], vfuncs[func]['calls'],
               vfuncs[func]['design'], func))


######################################################################
######################################################################

parser = argparse.ArgumentParser(
    allow_abbrev=False,
    formatter_class=argparse.RawDescriptionHelpFormatter,
    description="""Read gprof report created with Verilator --prof-cfuncs

Verilator_profcfunc reads a profile report created by gprof.  The names of
the functions are then transformed, assuming the user used Verilator's
--prof-cfuncs, and a report printed showing the percentage of time, etc,
in each Verilog block.

For documentation see
https://verilator.org/guide/latest/exe_verilator_profcfunc.html""",
    epilog="""Copyright 2002-2025 by Wilson Snyder. This program is free software; you
can redistribute it and/or modify it under the terms of either the GNU
Lesser General Public License Version 3 or the Perl Artistic License
Version 2.0.

SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0""")

parser.add_argument('--debug', action='store_const', const=9, help='enable debug')
parser.add_argument('filename', help='input gprof output to process')

Args = parser.parse_args()
profcfunc(Args.filename)

######################################################################
# Local Variables:
# compile-command: "./verilator_profcfunc ../test_regress/t/t_profcfunc.gprof"
# End:
