| # Copyright lowRISC contributors. | |
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| r""" | |
| Class describing synthesis configuration object | |
| """ | |
| import logging as log | |
| from pathlib import Path | |
| import hjson | |
| from tabulate import tabulate | |
| from OneShotCfg import OneShotCfg | |
| from utils import print_msg_list, subst_wildcards | |
| class SynCfg(OneShotCfg): | |
| """Derivative class for synthesis purposes. | |
| """ | |
| def __init__(self, flow_cfg_file, proj_root, args): | |
| super().__init__(flow_cfg_file, proj_root, args) | |
| def __post_init__(self): | |
| super().__post_init__() | |
| # Set the title for synthesis results. | |
| self.results_title = self.name.upper() + " Synthesis Results" | |
| def gen_results_summary(self): | |
| ''' | |
| Gathers the aggregated results from all sub configs | |
| ''' | |
| # Generate results table for runs. | |
| log.info("Create summary of synthesis results") | |
| results_str = "## " + self.results_title + " (Summary)\n\n" | |
| results_str += "### " + self.timestamp_long + "\n" | |
| if self.revision_string: | |
| results_str += "### " + self.revision_string + "\n" | |
| results_str += "\n" | |
| self.results_summary_md = results_str + "\nNot supported yet.\n" | |
| print(self.results_summary_md) | |
| # Return only the tables | |
| return self.results_summary_md | |
| def _gen_results(self): | |
| # ''' | |
| # The function is called after the regression has completed. It looks | |
| # for a regr_results.hjson file with aggregated results from the | |
| # synthesis run. The hjson needs to have the following (potentially | |
| # empty) fields | |
| # | |
| # results = { | |
| # "tool": "dc", | |
| # "top" : <name of toplevel>, | |
| # | |
| # "messages": { | |
| # "flow_errors" : [], | |
| # "flow_warnings" : [], | |
| # "analyze_errors" : [], | |
| # "analyze_warnings" : [], | |
| # "elab_errors" : [], | |
| # "elab_warnings" : [], | |
| # "compile_errors" : [], | |
| # "compile_warnings" : [], | |
| # }, | |
| # | |
| # "timing": { | |
| # # per timing group (ususally a clock domain) | |
| # # in nano seconds | |
| # <group> : { | |
| # "tns" : <value>, | |
| # "wns" : <value>, | |
| # "period" : <value>, | |
| # ... | |
| # } | |
| # }, | |
| # | |
| # "area": { | |
| # # gate equivalent of a NAND2 gate | |
| # "ge" : <value>, | |
| # | |
| # # summary report, in GE | |
| # "comb" : <value>, | |
| # "buf" : <value>, | |
| # "reg" : <value>, | |
| # "macro" : <value>, | |
| # "total" : <value>, | |
| # | |
| # # hierchical report of first submodule level | |
| # "instances" : { | |
| # <name> : { | |
| # "comb" : <value>, | |
| # "buf" : <value>, | |
| # "reg" : <value>, | |
| # "macro" : <value>, | |
| # "total" : <value>, | |
| # }, | |
| # ... | |
| # }, | |
| # }, | |
| # | |
| # "power": { | |
| # "net" : <value>, | |
| # "int" : <value>, | |
| # "leak" : <value>, | |
| # }, | |
| # | |
| # "units": { | |
| # "voltage" : <value>, | |
| # "capacitance" : <value>, | |
| # "time" : <value>, | |
| # "dynamic" : <value>, | |
| # "static" : <value>, | |
| # } | |
| # } | |
| # | |
| # note that if this is a primary config, the results will | |
| # be generated using the _gen_results_summary function | |
| # ''' | |
| def _create_entry(val, norm=1.0, total=None, perctag="%"): | |
| """ | |
| Create normalized entry with an optional | |
| percentage appended in brackets. | |
| """ | |
| if val is not None and norm is not None: | |
| if total is not None: | |
| perc = float(val) / float(total) * 100.0 | |
| entry = "%2.1f %s" % (perc, perctag) | |
| else: | |
| value = float(val) / norm | |
| entry = "%2.1f" % (value) | |
| else: | |
| entry = "--" | |
| return entry | |
| self.result = {} | |
| # Generate results table for runs. | |
| results_str = "## " + self.results_title + "\n\n" | |
| results_str += "### " + self.timestamp_long + "\n" | |
| if self.revision_string: | |
| results_str += "### " + self.revision_string + "\n" | |
| results_str += "### Synthesis Tool: " + self.tool.upper() + "\n\n" | |
| # TODO: extend this to support multiple build modes | |
| for mode in self.build_modes: | |
| # results_str += "## Build Mode: " + mode.name + "\n\n" | |
| result_data = Path( | |
| subst_wildcards(self.build_dir, {"build_mode": mode.name}) + | |
| '/results.hjson') | |
| log.info("looking for result data file at %s", result_data) | |
| try: | |
| with result_data.open() as results_file: | |
| self.result = hjson.load(results_file, use_decimal=True) | |
| except IOError as err: | |
| log.warning("%s", err) | |
| self.result = { | |
| "messages": { | |
| "flow_errors": ["IOError: %s" % err], | |
| "flow_warnings": [], | |
| "analyze_errors": [], | |
| "analyze_warnings": [], | |
| "elab_errors": [], | |
| "elab_warnings": [], | |
| "compile_errors": [], | |
| "compile_warnings": [], | |
| }, | |
| } | |
| # Message summary | |
| # results_str += "### Tool Message Summary\n\n" | |
| if "messages" in self.result: | |
| header = [ | |
| "Build Mode", "Flow Warnings", "Flow Errors", | |
| "Analyze Warnings", "Analyze Errors", "Elab Warnings", | |
| "Elab Errors", "Compile Warnings", "Compile Errors" | |
| ] | |
| colalign = ("left", ) + ("center", ) * (len(header) - 1) | |
| table = [header] | |
| messages = self.result["messages"] | |
| table.append([ | |
| mode.name, | |
| str(len(messages["flow_warnings"])) + " W ", | |
| str(len(messages["flow_errors"])) + " E ", | |
| str(len(messages["analyze_warnings"])) + " W ", | |
| str(len(messages["analyze_errors"])) + " E ", | |
| str(len(messages["elab_warnings"])) + " W ", | |
| str(len(messages["elab_errors"])) + " E ", | |
| str(len(messages["compile_warnings"])) + " W ", | |
| str(len(messages["compile_errors"])) + " E ", | |
| ]) | |
| if len(table) > 1: | |
| results_str += tabulate(table, | |
| headers="firstrow", | |
| tablefmt="pipe", | |
| colalign=colalign) + "\n\n" | |
| else: | |
| results_str += "No messages found\n\n" | |
| else: | |
| results_str += "No messages found\n\n" | |
| # Hierarchical Area report | |
| results_str += "### Circuit Complexity in [kGE]\n\n" | |
| if "area" in self.result: | |
| header = [ | |
| "Instance", "Comb ", "Buf/Inv", "Regs", "Macros", "Total", | |
| "Total [%]" | |
| ] | |
| colalign = ("left", ) + ("center", ) * (len(header) - 1) | |
| table = [header] | |
| # print top-level summary first | |
| row = ["**" + self.result["top"] + "**"] | |
| try: | |
| kge = float(self.result["area"]["ge"]) * 1000.0 | |
| for field in ["comb", "buf", "reg", "macro", "total"]: | |
| row += [ | |
| "**" + | |
| _create_entry(self.result["area"][field], kge) + | |
| "**" | |
| ] | |
| row += ["**--**"] | |
| table.append(row) | |
| # go through submodules | |
| for name in self.result["area"]["instances"].keys(): | |
| if name == self.result["top"]: | |
| continue | |
| row = [name] | |
| for field in ["comb", "buf", "reg", "macro", "total"]: | |
| row += [ | |
| _create_entry( | |
| self.result["area"]["instances"][name] | |
| [field], kge) | |
| ] | |
| # add percentage of total | |
| row += [ | |
| _create_entry( | |
| self.result["area"]["instances"][name][field], | |
| kge, self.result["area"]["total"], "%u") | |
| ] | |
| table.append(row) | |
| except TypeError: | |
| results_str += "Gate equivalent is not properly defined\n\n" | |
| if len(table) > 1: | |
| results_str += tabulate(table, | |
| headers="firstrow", | |
| tablefmt="pipe", | |
| colalign=colalign) + "\n\n" | |
| else: | |
| results_str += "No area report found\n\n" | |
| else: | |
| results_str += "No area report found\n\n" | |
| # Timing report | |
| results_str += "### Timing in [ns]\n\n" | |
| if "timing" in self.result and "units" in self.result: | |
| header = ["Clock", "Period", "WNS", "TNS"] | |
| colalign = ("left", ) + ("center", ) * (len(header) - 1) | |
| table = [header] | |
| for clock in self.result["timing"].keys(): | |
| row = [clock] | |
| row += [ | |
| _create_entry( | |
| self.result["timing"][clock]["period"], | |
| 1.0E-09 / float(self.result["units"]["time"])), | |
| _create_entry( | |
| self.result["timing"][clock]["wns"], 1.0E-09 / | |
| float(self.result["units"]["time"])) + " EN", | |
| _create_entry( | |
| self.result["timing"][clock]["tns"], 1.0E-09 / | |
| float(self.result["units"]["time"])) + " EN" | |
| ] | |
| table.append(row) | |
| if len(table) > 1: | |
| results_str += tabulate(table, | |
| headers="firstrow", | |
| tablefmt="pipe", | |
| colalign=colalign) + "\n\n" | |
| else: | |
| results_str += "No timing report found\n\n" | |
| else: | |
| results_str += "No timing report found\n\n" | |
| # Power report | |
| results_str += "### Power Estimates in [mW]\n\n" | |
| if "power" in self.result and "units" in self.result: | |
| header = ["Network", "Internal", "Leakage", "Total"] | |
| colalign = ("center", ) * len(header) | |
| table = [header] | |
| try: | |
| self.result["power"]["net"] | |
| power = [ | |
| float(self.result["power"]["net"]) * | |
| float(self.result["units"]["dynamic"]), | |
| float(self.result["power"]["int"]) * | |
| float(self.result["units"]["dynamic"]), | |
| float(self.result["power"]["leak"]) * | |
| float(self.result["units"]["static"]) | |
| ] | |
| total_power = sum(power) | |
| row = [_create_entry(power[0], 1.0E-3) + " / " + | |
| _create_entry(power[0], 1.0E-3, total_power), | |
| _create_entry(power[1], 1.0E-3) + " / " + | |
| _create_entry(power[1], 1.0E-3, total_power), | |
| _create_entry(power[2], 1.0E-3) + " / " + | |
| _create_entry(power[2], 1.0E-3, total_power), | |
| _create_entry(total_power, 1.0E-3)] | |
| table.append(row) | |
| # in case fp values are NoneType | |
| except TypeError: | |
| results_str += "No power report found\n\n" | |
| if len(table) > 1: | |
| results_str += tabulate(table, | |
| headers="firstrow", | |
| tablefmt="pipe", | |
| colalign=colalign) + "\n\n" | |
| else: | |
| results_str += "No power report found\n\n" | |
| # Append detailed messages if they exist | |
| # Note that these messages are omitted in publication mode | |
| hdr_key_pairs = [("Flow Warnings", "flow_warnings"), | |
| ("Flow Errors", "flow_errors"), | |
| ("Analyze Warnings", "analyze_warnings"), | |
| ("Analyze Errors", "analyze_errors"), | |
| ("Elab Warnings", "elab_warnings"), | |
| ("Elab Errors", "elab_errors"), | |
| ("Compile Warnings", "compile_warnings"), | |
| ("Compile Errors", "compile_errors")] | |
| # Synthesis fails if any warning or error message has occurred | |
| self.errors_seen = False | |
| fail_msgs = "" | |
| for _, key in hdr_key_pairs: | |
| if key in self.result['messages']: | |
| if self.result['messages'].get(key): | |
| self.errors_seen = True | |
| break | |
| if self.errors_seen: | |
| fail_msgs += "\n### Errors and Warnings for Build Mode `'" + mode.name + "'`\n" | |
| for hdr, key in hdr_key_pairs: | |
| msgs = self.result['messages'].get(key) | |
| fail_msgs += print_msg_list("#### " + hdr, msgs, self.max_msg_count) | |
| # the email and published reports will default to self.results_md if they are | |
| # empty. in case they need to be sanitized, override them and do not append | |
| # detailed messages. | |
| if self.sanitize_email_results: | |
| self.email_results_md = results_str | |
| if self.sanitize_publish_results: | |
| self.publish_results_md = results_str | |
| # locally generated result always contains all details | |
| self.results_md = results_str + fail_msgs | |
| # TODO: add support for pie / bar charts for area splits and | |
| # QoR history | |
| # Write results to the scratch area | |
| self.results_file = self.scratch_path + "/results_" + self.timestamp + ".md" | |
| log.info("Detailed results are available at %s", self.results_file) | |
| with open(self.results_file, 'w') as f: | |
| f.write(self.results_md) | |
| return self.results_md |