| # 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 |