Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ dependencies = [
# Tool schema validation.
"jsonschema>=4.26.0",
"types-jsonschema>=4.26.0",
# Used for generating utilisation plots
"plotly>=6.9.0",
"numpy>=2.2.6",
]

[tool.setuptools]
Expand Down
18 changes: 18 additions & 0 deletions util/utilisation_reporting/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!--
# Copyright lowRISC contributors (COSMIC project).
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
-->

Generates:
- a text report of FPGA resource utilisation, organised hierarchically
- a HTML sunburst diagram showing BRAM utilisation by instance
- a HTML sunburst diagram showing LUT+FF utilisation by instance

The sunburst diagrams are interactive -- click on a wedge to explore its children.

To generate, return to the repository root and run:
```
./util/utilisation_reporting/report_utilisation.sh
```
Expect this to take a minute or two as it will need to open the design in Vivado.
17 changes: 17 additions & 0 deletions util/utilisation_reporting/get_reports.tcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright lowRISC contributors (COSMIC project).
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

# Open the implementated design
open_run impl_1

# Report utilisation and dump to a file
report_utilization -hierarchical -hierarchical_depth 0 -file scratch/util_full.txt
# depth of 0 is interpreted as infinite

# If you're reading this file, you may be interested in some other options:

# Generate a spreadsheet report instead of text file (only works in GUI mode):
#report_utilization -hierarchical -spreadsheet_depth 0 -spreadsheet_file util.xlsx
# Generate a hierarchical report with less depth (useful for large designs):
#report_utilization -hierarchical -spreadsheet_depth 3 -spreadsheet_file util_depth3.xlsx
72 changes: 72 additions & 0 deletions util/utilisation_reporting/parse_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Copyright lowRISC contributors (COSMIC project).
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

from dataclasses import dataclass, field
from pathlib import Path
from typing import NamedTuple


class Utilisation(NamedTuple):
total_luts: int
logic_luts: int
lutrams: int
srls: int
ffs: int
ramb36: int
ramb18: int
dsp: int


@dataclass
class Node:
name: str # instance name, e.g. "u_ddr3_mig" or "(u_ddr3_mig)" for self-logic
module: str # module/cell type, e.g. "u_xlnx_mig_7_ddr3"
util: Utilisation # this row's own reported utilisation
children: list = field(default_factory=list)


def extract_utilisation_table(path: str) -> str:
"""Thin wrapper that finds the 'utilisation by Hierarchy' table in a raw Vivado
report_utilization file and returns just its data rows, stripping the file header,
table of contents, and footnotes."""
with Path(path).open(encoding="utf8") as f:
lines = f.readlines()
header = next(i for i, j in enumerate(lines) if "Instance" in j and "Module" in j)
data_start = header + 2 # skip the header row and the border beneath it
data_end = next(i for i in range(data_start, len(lines)) if lines[i].startswith("+"))
return "".join(lines[data_start:data_end])


def parse_utilisation_report(path: str) -> Node:
"""Parse a Vivado `report_utilization -hierarchical` text report into a Node tree."""
rows = []
for line in extract_utilisation_table(path).splitlines():
parts = line.split("|")[1:-1]
# Vivado should emit instance name, module name, and eight resource-count columns:
if len(parts) != len(Utilisation._fields) + 2:
continue
instance, module, *nums = parts
try:
nums = [int(n.strip()) for n in nums]
except ValueError:
continue # stray blank/border line
depth = (
len(instance) - len(instance.lstrip(" "))
) // 2 # each 2 leading spaces is one level of hierarchical depth
rows.append((depth, instance.strip(), module.strip(), Utilisation(*nums)))

root = None
stack = [] # list of (depth, node), innermost last
for depth, name, module, util in rows:
if "(" in name:
continue # skip self-logic rows, which are redundant with their parent
node = Node(name, module, util)
while stack and stack[-1][0] >= depth:
stack.pop()
if stack:
stack[-1][1].children.append(node)
else:
root = node
stack.append((depth, node))
return root
18 changes: 18 additions & 0 deletions util/utilisation_reporting/report_utilisation.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env -S bash -eu
# Copyright lowRISC contributors (COSMIC project).
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

# This should be run from the root of the repository

if [ ! -d "./build/lowrisc_mocha_chip_mocha_genesys2_0" ]; then
echo "[report_utilisation.sh] ERROR: ./build/lowrisc_mocha_chip_mocha_genesys2_0 directory does not exist"
echo "Did you:"
echo " 1. Build the design first?"
echo " 2. Run util/utilisation_reporting/report_utilisation.sh from the root of the repository?"
exit 1
fi

mkdir -p scratch
vivado -mode batch -source util/utilisation_reporting/get_reports.tcl ./build/lowrisc_mocha_chip_mocha_genesys2_0/synth-vivado/lowrisc_mocha_chip_mocha_genesys2_0.xpr
uv run python3 util/utilisation_reporting/sunburst.py scratch/util_full.txt
156 changes: 156 additions & 0 deletions util/utilisation_reporting/sunburst.py
Comment thread
thomas6785 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Copyright lowRISC contributors (COSMIC project).
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

import plotly.graph_objects as go
from parse_report import Node, parse_utilisation_report


def name2id(name: str) -> str:
"""Convert a node name to a unique ID for use in the sunburst chart."""
replacements = {
"[": "_",
"]": "_",
"(": "_",
")": "_",
" ": "_",
".": "_",
}
for old, new in replacements.items():
name = name.replace(old, new)
return name


def build_sunburst_arrays(root: Node, value_getter=lambda n: n.util.total_luts):
"""Plotly expects a sunburst chart to be defined by four parallel arrays: ids, labels, parents,
and values. This function walks the Node tree and builds those arrays."""
ids = []
labels = []
parents = []
values = []

def walk(node: Node, path: str):
node_id = path

# Recurse into children (and tally their utilisation as we go)
child_util_sum = 0
for child in node.children:
child_path = f"{node_id}.{name2id(child.name)}"
child_util_sum += walk(child, child_path)

# Add this node to the arrays too
ids.append(node_id)
labels.append(node.name)
this_util = max(
value_getter(node), child_util_sum
)
# if the node's own utilisation is less than the sum of its children, use the sum of its
# children instead. Vivado will occasionally have a parent equal to less than the sum of its
# children due to cross-boundary optimisation. We will take the sum of the children in that
# case, otherwise we can't display it
values.append(this_util)

# Root has no parent
if "." in node_id:
parents.append(node_id.rsplit(".", 1)[0])
else:
parents.append("") # Plotly root marker

return this_util # return the total utilisation as this will be useful for the parent

walk(root, root.name)
return ids, labels, parents, values


def ff_getter(node: Node):
"""Return the number of flip-flops in this node's own reported utilisation."""
return node.util.ffs


def lut_getter(node: Node):
"""Return the number of LUT's in this node's own reported utilisation."""
return node.util.total_luts


def ff_plus_lut_getter(node: Node):
"""Return the number of flip-flops and LUT's in this node's own reported utilisation."""
return node.util.ffs + node.util.total_luts


def bram_getter(node: Node):
"""Return the number of BRAM18-equivalent blocks in this node's own reported utilisation."""
return node.util.ramb18 + node.util.ramb36 * 2


def generate_sunburst(root, value_getter, outfile, title):
"""
Given the root of a Node tree, generate a sunburst plot and output to a HTML file.
The value_getter function is used to extract the value to be plotted from each node
(e.g. number of LUTs, number of flip-flops, etc.)
"""
raw_data = build_sunburst_arrays(root, value_getter=value_getter)
ids, labels, parents, values = raw_data

# depth visible at any one time, to avoid clutter. Users can zoom in to see more detail
plot_maxdepth = 4

# "total" means the value of a parent node INCLUDES the values of its children. Usually it the
# parent will be slightly larger than the sum of its children, but note that occasionally a
# parent will actually be smaller than the sum of its children due to Vivado's cross-boundary
# optimisation; Plotly will work around this if it occurs
branch_value_style = "total"

# Create the sunburst plot
fig = go.Figure(
go.Sunburst(
ids=ids,
labels=labels,
parents=parents,
values=values,
branchvalues=branch_value_style,
maxdepth=plot_maxdepth,
)
)

# Set some behaviours for interacting with it
fig.update_traces(
marker={"colors":None}, # force Plotly to regenerate colours
leaf={"opacity":1}, # ensures leaves get distinct colours
)
fig.update_layout(
title=title,
title_x=0.5,
title_font_size=22,
title_font_color="darkblue"
)

fig.write_html(outfile)


if __name__ == "__main__":
# If we are run as a script, take the first command line argument as a path
# to a Vivado `report_utilization -hierarchical` text report, parse it, and
# generate two sunburst plots: one for BRAM utilisation and one for FF+LUT
# utilisation.
import sys

if len(sys.argv) < 2:
print("Usage: python sunburst.py <path_to_vivado_report_utilisation.txt>")
sys.exit(1)

tree_root = parse_utilisation_report(sys.argv[1])

generate_sunburst(
tree_root,
value_getter=bram_getter,
outfile="bram_util.html",
title="BRAM Utilisation"
)
generate_sunburst(
tree_root,
value_getter=ff_plus_lut_getter,
outfile="ff_lut_util.html",
title="FF+LUT Utilisation"
)

print("Sunburst plots generated: bram_util.html and ff_lut_util.html")
Loading