| # 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. | |
| """ | |
| flow = 'syn' | |
| def __init__(self, flow_cfg_file, hjson_data, args, mk_config): | |
| super().__init__(flow_cfg_file, hjson_data, args, mk_config) | |
| # 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: | |
| results_str += "### " + self.revision + "\n" | |
| results_str += "### Branch: " + self.branch + "\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, results): | |
| # ''' | |
| # 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: | |
| if total <= 0.0: | |
| entry = "--" | |
| else: | |
| perc = float(val) / float(total) * 100.0 | |
| entry = "%2.1f %s" % (perc, perctag) | |
| else: | |
| value = float(val) / norm | |
| if value < 1.0: | |
| entry = "%2.2f" % (value) | |
| else: | |
| 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: | |
| results_str += "### " + self.revision + "\n" | |
| results_str += "### Branch: " + self.branch + "\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": None, | |
| "analyze_errors": None, | |
| "analyze_warnings": None, | |
| "elab_errors": None, | |
| "elab_warnings": None, | |
| "compile_errors": None, | |
| "compile_warnings": None, | |
| }, | |
| } | |
| # 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] | |
| msg_list = [ | |
| ("flow_warnings", " W "), | |
| ("flow_errors", " E "), | |
| ("analyze_warnings", " W "), | |
| ("analyze_errors", " E "), | |
| ("elab_warnings", " W "), | |
| ("elab_errors", " E "), | |
| ("compile_warnings", " W "), | |
| ("compile_errors", " E "), | |
| ] | |
| msgs = [mode.name] | |
| for key, sev in msg_list: | |
| if self.result["messages"][key] is None: | |
| msgs.append("--") | |
| else: | |
| msgs.append(str(len( | |
| self.result["messages"][key])) + sev) | |
| table.append(msgs) | |
| 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", "Logic", "Macros", "Total", | |
| "Logic [%]", "Macro [%]", "Total [%]" | |
| ] | |
| colalign = ("left", ) + ("center", ) * (len(header) - 1) | |
| table = [header] | |
| try: | |
| kge = float(self.result["area"]["ge"]) * 1000.0 | |
| # go through submodules. this assumes that the top-level | |
| # is listed before any other modules | |
| totals = [0] * 3 | |
| for name in self.result["area"]["instances"].keys(): | |
| row = [] | |
| is_top = (self.result["area"]["instances"][name]["depth"] == 0) | |
| row = ["**" + name + "**"] if is_top else [name] | |
| for field in ["comb", "buf", "reg", "logic", "macro", "total"]: | |
| entry = _create_entry( | |
| self.result["area"]["instances"][name] | |
| [field], kge) | |
| entry = "**" + entry + "**" if is_top else entry | |
| row.append(entry) | |
| for k, field in enumerate(["logic", "macro", "total"]): | |
| if is_top: | |
| row.append("**--**") | |
| totals[k] = self.result["area"]["instances"][name][field] | |
| else: | |
| row.append( | |
| _create_entry( | |
| self.result["area"]["instances"][name][field], | |
| kge, totals[k], "%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 = ["Path Group", "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", False), | |
| ("Flow Errors", "flow_errors", True), | |
| ("Analyze Warnings", "analyze_warnings", False), | |
| ("Analyze Errors", "analyze_errors", True), | |
| ("Elab Warnings", "elab_warnings", False), | |
| ("Elab Errors", "elab_errors", True), | |
| ("Compile Warnings", "compile_warnings", False), | |
| ("Compile Errors", "compile_errors", True)] | |
| # helper function | |
| def _getlen(x): | |
| return len(x) if x is not None else 0 | |
| # Synthesis fails if any error messages have occurred | |
| self.errors_seen = 0 | |
| msgs_seen = 0 | |
| fail_msgs = "" | |
| for _, key, fail in hdr_key_pairs: | |
| if key in self.result['messages']: | |
| num_msgs = _getlen(self.result['messages'][key]) | |
| msgs_seen += num_msgs | |
| self.errors_seen += num_msgs if fail else 0 | |
| if msgs_seen > 0: | |
| 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) | |
| # Th published report will default to self.results_md if they are | |
| # empty. In case it needs need to be sanitized, override it and do | |
| # not append detailed messages. | |
| 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 | |
| return self.results_md |