blob: 6a8c15f2d5112e6982e45ab1d1b45841a5d4aa78 [file]
# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
# SPDX-License-Identifier: Apache-2.0
'''
Process a build folder and generate a comprehensive HTML dashboard
including memory use, Kconfig values, devicetree browser, sys-init
ordering, and other general details.
Data sources
============
The following build artifacts (read from <build_dir>/zephyr/ unless noted)
are used by this script:
<build_dir>/build_info.yml
Build metadata produced by CMake: application source path, board
name, west build command, and workspace top directory.
<zephyr_base>/VERSION
Zephyr source version (major, minor, patchlevel, extraversion).
<build_dir>/CMakeFiles/*/CMakeCCompiler.cmake
CMake compiler description file, parsed to extract the toolchain
name and version string.
zephyr/<kernel>.elf
ELF image of the built kernel. Used by pyelftools to compute the
memory summary (text/rodata/rwdata/bss section sizes), to check
sys-init ordering via check_init_priorities, and by the
scripts/footprint/size_report script to generate memory reports.
zephyr/<kernel>.bin
Binary image. Only its file size is read, reported as "Bin Size"
on the build summary page.
zephyr/.config-trace.pickle
Pickled output of the traceconfig script. Contains the list of
Kconfig symbols that were evaluated during the build, together
with their values and source locations, used to populate the
Kconfig browser tab.
zephyr/edt.pickle
Pickled edtlib.EDT object representing the fully resolved
devicetree for the build. Used in two ways:
1. General devicetree browser — the full DT node tree is rendered
as a searchable, expandable view in the Devicetree tab.
2. Per-region memory reports — all nodes whose "compatible"
includes "zephyr,memory-region" and whose status is "okay" are
enumerated. For each such node the "zephyr,memory-region"
property provides the region name and the first "reg" entry
provides its base address and size. If the ELF contains at
least one allocated section that overlaps the region's address
range, a report is created and shown as an additional tab in the
Memory Report view.
zephyr/zephyr.dts
Raw DTS source file, syntax-highlighted with Pygments and
displayed verbatim in the Devicetree tab.
'''
import argparse
import contextlib
import glob
import html
import io
import json
import logging
import os
import pickle
import re
import shutil
import subprocess
import sys
import webbrowser
from datetime import datetime
from functools import partial
from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
import jinja2
import yaml
from elftools.elf.constants import SH_FLAGS
from elftools.elf.elffile import ELFFile
from pygments import highlight
from pygments.formatters import HtmlFormatter # pylint: disable=E0611
from pygments.lexers import DevicetreeLexer # pylint: disable=E0611
from pygments.styles import get_style_by_name
sys.path.append(str(Path(__file__).parents[1] / "build"))
from check_init_priorities import Validator
sys.path.append(str(Path(__file__).parents[1] / "dts" / "python-devicetree" / "src" / "devicetree"))
from dtlib import DT
sys.path.append(str(Path(__file__).parents[1] / "footprint"))
try:
from plot import generate_figure
from plotly.offline.offline import get_plotlyjs
except ImportError:
generate_figure = None
logger = logging.getLogger('dashboard')
# Online path to the kconfig browser.
KCONFIG_BROWSER = 'https://docs.zephyrproject.org/latest/kconfig.html'
# Set to match the CSS used in the HTML as it is set via CSS variable
# that we cannot access from Python script.
CSS_BS_TABLE_BG = '#f8f9fa'
# Height of the memory report plot.
MEMORY_PLOT_HEIGHT = 600
# Constants for converting from bytes to human-friendly sizes.
MEMORY_UNIT_SIZES = {
1073741824: 'GB',
1048576: 'MB',
1024: 'KB',
1: 'Bytes',
}
def display_size(byte_cnt):
'''
Display a number of bytes in human-readable form with a size indicator.
'''
if byte_cnt in [None, '']:
return ''
compressed_bytes = byte_cnt
unit = 'Bytes'
for unit_size, unit_str in MEMORY_UNIT_SIZES.items():
if abs(byte_cnt) >= unit_size:
compressed_bytes = float(byte_cnt) / unit_size
unit = unit_str
break
return f"{compressed_bytes:.1f} {unit}".replace('.0', '')
def elf_memory_summary(elf_file):
'''
Get a summary of memory use from the given ELF file.
This function will parse the given elf file and determine the total memory
used by read-only, read-write, and executable regions.
:param elf_file: Path to an elf file.
:return: A dictionary with keys "bss", "rwdata", "rodata", "text", "other"
'''
report = {'bss': 0, 'rodata': 0, 'rwdata': 0, 'text': 0, 'other': 0}
with ELFFile.load_from_path(elf_file) as elf:
for section in elf.iter_sections():
flags = section.header['sh_flags']
size = section.header['sh_size']
sh_type = section.header['sh_type']
if sh_type == 'SHT_NOBITS':
report['bss'] += size
elif sh_type == 'SHT_PROGBITS':
if flags & SH_FLAGS.SHF_EXECINSTR:
report['text'] += size
elif flags & SH_FLAGS.SHF_WRITE:
report['rwdata'] += size
elif flags & SH_FLAGS.SHF_ALLOC:
report['rodata'] += size
else:
report['other'] += size
else:
report['other'] += size
return report
class ZephyrVersion:
'''
Class representing the version of Zephyr kernel. See
https://docs.zephyrproject.org/latest/project/release_process.html
for what the fields represent.
Data is loaded from the VERSION file in the Zephyr base.
'''
FIELDS = ['version_major', 'version_minor', 'patchlevel', 'version_tweak', 'extraversion']
def __init__(self, zephyr_base):
self.version_major = None
self.version_minor = None
self.patchlevel = None
self.version_tweak = None
self.extraversion = None
version_file = zephyr_base / 'VERSION'
try:
with open(version_file, encoding='utf-8') as f:
version_data = f.readlines()
except OSError as e:
logger.error(f"Unable to read version information from {version_file}: {e}")
return
for version_line in version_data:
field, value = version_line.split('=')
field = field.strip().lower()
value = value.strip()
if field in ZephyrVersion.FIELDS:
setattr(self, field, value)
def __str__(self):
version = f"{self.version_major}.{self.version_minor}"
if self.patchlevel and self.patchlevel != '0':
version += f'.{self.patchlevel}'
if self.extraversion and self.extraversion != '0':
version += f'-{self.extraversion}'
if self.version_tweak and self.version_tweak != '0':
version += f'+{self.version_tweak}'
return version
class ZephyrToolchain:
'''
Container for the Zephyr toolchain information.
Extracted from the CMakeCCompiler.cmake file.
'''
def __init__(self, build_path):
self.cmake_c_compiler_id = None
self.cmake_c_compiler_version = None
cmake_glob = build_path / 'CMakeFiles' / '*' / 'CMakeCCompiler.cmake'
cmake_file = glob.glob(str(cmake_glob))
if cmake_file:
cmake_file = cmake_file[0]
else:
return
try:
with open(cmake_file, encoding='utf-8') as f:
lines = f.readlines()
except OSError as e:
logger.error(f"Unable to load CMakeCCompiler.cmake file {cmake_file}: {e}")
return
cmake_c_compiler_id_re = re.compile(r'\s*set\(CMAKE_C_COMPILER_ID\s+"(.*)"\)')
cmake_c_compiler_version_re = re.compile(r'\s*set\(CMAKE_C_COMPILER_VERSION\s+"(.*)"\)')
for line in lines:
if match := cmake_c_compiler_id_re.match(line):
self.cmake_c_compiler_id = match.group(1)
if match := cmake_c_compiler_version_re.match(line):
self.cmake_c_compiler_version = match.group(1)
if self.cmake_c_compiler_id and self.cmake_c_compiler_version:
break
def __str__(self):
return f"{self.cmake_c_compiler_id} {self.cmake_c_compiler_version}"
class KconfigSymbol:
'''
Class representing a KconfigSymbol and its value in a given build.
'''
def __init__(self, name, visible, sym_type, value, src, loc, build):
self.sym_type = sym_type
self.visible = bool(visible == 'y')
self.name = name
self.value = value
if sym_type == "string" and self.value is not None:
self.value = f'"{self.value}"'
self.src = src
self.loc = loc
self.build = build
def loc_html(self):
'''
Return an HTML representation of the symbol location.
'''
if self.loc and self.src in ['default', 'assign']:
fn = os.path.relpath(self.loc[0], self.build.zephyr_base)
disp_fn = os.path.normpath(fn).replace("../", "")
sym_loc = f'{disp_fn}:{self.loc[1]}'
elif self.loc and self.src in ['select', 'imply']:
if len(self.loc) > 1:
sym_loc = " || ".join(f"({html.escape(loc)})" for loc in self.loc)
else:
sym_loc = html.escape(self.loc[0])
sym_loc = f"<code>{sym_loc}</code>"
elif self.loc is None and self.src == "default":
sym_loc = "<i>(implicit)</i>"
else:
sym_loc = ''
return sym_loc
def src_html(self):
'''
Return an HTML representation of the symbol source tag.
'''
if self.src == "unset":
return ''
if self.src == "default":
return '<span class="badge bg-secondary">default</span>'
if self.src == "assign":
return '<span class="badge bg-primary">assigned</span>'
if self.src == "select":
return '<span class="badge bg-info">selected</span>'
if self.src == "imply":
return '<span class="badge bg-warning">implied</span>'
return self.src
def _build_memory_report_tree(report):
'''
Convert a size_report JSON dict into the tree structure expected by the
JS tree widget.
Returns a dict with keys "size" (total bytes) and "tree" (list of nodes).
Each node has "expanded", "data" (name/size/displaySize/memoryType), and
optionally "children".
'''
def create_node(symbol, expanded=False):
size = symbol['size']
return (
{
"expanded": expanded,
"data": {
"name": symbol['name'],
"size": size,
"displaySize": display_size(size),
"memoryType": [loc.upper() for loc in symbol.get('loc', [])],
},
}
if size
else None
)
def add_children(node, children):
if not children:
return
node['children'] = []
for symbol in sorted(children, key=lambda c: c['name']):
child_node = create_node(symbol)
if not child_node:
continue
node['children'].append(child_node)
add_children(child_node, symbol.get('children'))
tree = {}
symbol = report.get('symbols')
if symbol:
tree['size'] = symbol['size']
tree['tree'] = []
node = create_node(symbol, expanded=True)
tree['tree'].append(node)
add_children(node, symbol.get('children'))
return tree
class ZephyrDashboard:
'''
Class for collecting and processing Zephyr build artifacts to create the
HTML dashboard.
'''
def __init__(
self,
zephyr_base,
build_path,
output_path,
kernel_bin_name="zephyr",
skip_memory_report=False,
):
self.zephyr_base = Path(zephyr_base)
self.kernel_bin_name = kernel_bin_name
self.build_path = Path(build_path) if build_path else self.zephyr_base / "build"
self.output_path = Path(output_path) if output_path else self.build_path / "dashboard"
self.elf_file = self.build_path / "zephyr" / f"{self.kernel_bin_name}.elf"
self.dts_file = self.build_path / "zephyr" / "zephyr.dts"
self.kconfig_file = self.build_path / "zephyr" / ".config"
# Read details from the build_info.yml file.
with open(self.build_path / "build_info.yml", encoding='utf-8') as fd:
self.build_info = yaml.safe_load(fd)
try:
self.application = self.build_info['cmake']['application']['source-dir']
base_prefix = '^' + str(self.zephyr_base.absolute()).replace('\\', '/') + '/'
self.application = re.sub(base_prefix, '', self.application)
except KeyError:
self.application = None
try:
self.board = self.build_info['cmake']['board']['name']
except KeyError:
self.board = None
try:
self.command = self.build_info['west']['command']
self.command = re.sub(r'\S*west', 'west', self.command)
except KeyError:
self.command = None
try:
self.topdir = Path(self.build_info['west']['topdir'])
except KeyError:
self.topdir = self.zephyr_base
# Get some details on the build binary file - may not exist in all
# builds (like QEMU).
try:
bin_file = self.build_path / "zephyr" / f"{self.kernel_bin_name}.bin"
bin_stats = bin_file.stat()
self.bin_size_str = display_size(bin_stats.st_size)
except OSError:
self.bin_size_str = None
# Get some details on the build ELF file.
elf_stats = self.elf_file.stat()
self.elf_date_str = datetime.fromtimestamp(elf_stats.st_mtime).strftime("%Y-%m-%d %H:%M:%S")
self.elf_size_str = display_size(elf_stats.st_size)
self.toolchain = ZephyrToolchain(self.build_path)
self.zephyr_version = ZephyrVersion(self.zephyr_base)
self._init_kconfigs()
self._init_sysinit()
self.memory_summary = elf_memory_summary(self.elf_file)
# Create the memory reports if they are stale.
self.skip_memory_report = skip_memory_report
self.memory_regions = []
if not self.skip_memory_report:
memory_report = self.output_path / "all_report.json"
if not os.path.isfile(memory_report) or os.path.getmtime(
memory_report
) < os.path.getmtime(self.elf_file):
self._create_memory_reports()
def _init_sysinit(self):
'''
Get the sys-init levels and validate against DT dependencies.
'''
validator_log = io.StringIO()
stream_handler = logging.StreamHandler(validator_log)
stream_handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
validator_logger = logging.getLogger('check_init_priorities')
validator_logger.propagate = False
validator_logger.addHandler(stream_handler)
validator_logger.setLevel(logging.WARNING)
with open(self.elf_file, 'rb') as fd:
validator = Validator(self.elf_file, 'edt.pickle', validator_logger, fd)
validator.check_edt()
self.sys_init_errors = validator_log.getvalue().splitlines()
self.sys_init_levels = validator.initlevels
def _init_kconfigs(self):
'''
Load the kconfig trace information from the pickled data file and convert
to a list of KconfigSymbol objects.
'''
pickle_path = self.build_path / "zephyr" / ".config-trace.pickle"
with open(pickle_path, 'rb') as f:
trace_data = pickle.load(f)
self.kconfigs = [KconfigSymbol(*sym, build=self) for sym in trace_data]
self.kconfigs.sort(key=lambda kc: kc.name)
def _get_dt_memory_regions(self):
'''
Return a list of DT memory regions from edt.pickle.
Each entry is a dict with "name" (str), "addr" (int), "size" (int).
Only nodes with compatible "zephyr,memory-region", status "okay", a
valid "zephyr,memory-region" name property, and at least one reg are
included.
'''
edt_pickle_path = self.build_path / "zephyr" / "edt.pickle"
with open(edt_pickle_path, "rb") as f:
edt = pickle.load(f)
regions = []
for node in edt.compat2nodes.get("zephyr,memory-region", []):
if node.status != "okay":
continue
name_prop = node.props.get("zephyr,memory-region")
if not name_prop:
continue
name = name_prop.val
if not node.regs:
continue
reg = node.regs[0]
if reg.addr is None or reg.size is None:
continue
regions.append({"name": name, "addr": reg.addr, "size": reg.size, "path": node.path})
return regions
def _has_sections_in_range(self, addr, size):
'''
Return True if the ELF contains any allocated, non-empty section
whose address range overlaps [addr, addr+size).
'''
end = addr + size
with ELFFile.load_from_path(self.elf_file) as elf:
for section in elf.iter_sections():
if section["sh_size"] == 0:
continue
if section["sh_flags"] & SH_FLAGS.SHF_ALLOC == 0:
continue
sec_start = section["sh_addr"]
sec_end = sec_start + section["sh_size"]
if sec_start < end and sec_end > addr:
return True
return False
def _create_memory_reports(self):
'''
Use the footprint size_report tool to create the ram/rom/all JSON data.
'''
logger.info("Creating memory reports (may take a few minutes).")
# Create the output location for the report files.
self.output_path.mkdir(parents=True, exist_ok=True)
cmd = [
sys.executable,
str(self.zephyr_base / "scripts" / "footprint" / "size_report"),
'-k',
str(self.elf_file),
'-z',
str(self.zephyr_base.absolute()),
f'--workspace={self.topdir}',
'--json',
str(self.output_path / '{target}_report.json'),
'--quiet',
'--output',
'.',
'rom',
'ram',
'all',
]
logger.debug(' '.join(cmd))
try:
subprocess.check_call(cmd)
except subprocess.CalledProcessError as e:
logger.error(
f"Failed generating memory size report (exit code {e.returncode}). "
+ f"Command:\n{' '.join(e.cmd)}"
)
# Generate per-region reports for non-empty DT memory regions.
try:
regions = self._get_dt_memory_regions()
except Exception as e:
logger.warning(f"Could not load DT memory regions: {e}")
regions = []
for region in regions:
if not self._has_sections_in_range(region["addr"], region["size"]):
continue
region_cmd = [
sys.executable,
str(self.zephyr_base / "scripts" / "footprint" / "size_report"),
'-k',
str(self.elf_file),
'-z',
str(self.zephyr_base.absolute()),
f'--workspace={self.topdir}',
'--json',
str(self.output_path / f"{region['name']}_report.json"),
'--filter-address-range',
str(region["addr"]),
str(region["size"]),
'--quiet',
'--output',
'.',
'all',
]
logger.debug(' '.join(region_cmd))
try:
subprocess.check_call(region_cmd)
self.memory_regions.append(region)
except subprocess.CalledProcessError as e:
logger.error(
f"Failed generating memory region report for {region['name']} "
f"(exit code {e.returncode}). Command:\n{' '.join(e.cmd)}"
)
def _load_memory_report(self, mem_type):
'''
Load the memory report of the given type from an existing json file.
:param type: Which report, "ram", "rom", or "all"
:return: Dictionary of the loaded memory report.
'''
fname = self.output_path / f"{mem_type}_report.json"
if not os.path.isfile(fname):
logger.error(f'Memory report file "{fname}" not found.')
return None
with open(fname, encoding="utf-8") as fd:
return json.load(fd)
def _symbols_by_size(self, report, limit=None):
'''
Sort the symbols from a memory report by their size.
:param report: A memory report as created by the Zephyr size_report script.
:param limit: How many results to return.
:return: A list of all the symbols with the largest first.
'''
if not report:
return []
symbol_list = []
def flatten(symbols, lst):
for symbol in symbols:
children = symbol.get('children')
if children:
flatten(children, lst)
elif symbol['name'][0] != '(':
lst.append(symbol)
flatten(report['symbols']['children'], symbol_list)
symbol_list = sorted(symbol_list, key=lambda s: s['size'], reverse=True)
if limit:
symbol_list = symbol_list[0:limit]
return symbol_list
def _memory_report_template_context(self, mem_type, ctxt):
'''
Add the HTML template context for the memory report of the given type.
'''
report = self._load_memory_report(mem_type)
if report:
tree = _build_memory_report_tree(report)
ctxt[f'{mem_type}_report'] = json.dumps(tree)
ctxt[f'{mem_type}_report_size'] = tree['size']
if generate_figure:
figure = generate_figure(report)
figure.update_layout(paper_bgcolor=CSS_BS_TABLE_BG, height=MEMORY_PLOT_HEIGHT)
ctxt[f'{mem_type}_plot'] = figure.to_html(full_html=False, include_plotlyjs=False)
else:
ctxt[f'{mem_type}_plot'] = None
else:
ctxt[f'{mem_type}_report'] = None
ctxt[f'{mem_type}_plot'] = None
if mem_type == 'all':
ctxt['top_ten'] = self._symbols_by_size(report, 10)
def _region_reports_template_context(self, ctxt):
'''
Add the HTML template context for per-region memory reports.
Populates ctxt["memory_regions"] with a list of dicts, one per
non-empty DT memory region, each containing:
name - region name (e.g. "ITCM")
tab_id - sanitized lowercase id safe for use in HTML attributes
report - JSON-encoded tree data for the JS tree widget
report_size - total size in bytes (for percentage display)
'''
region_ctxt_list = []
for region in self.memory_regions:
name = region['name']
tab_id = re.sub(r'[^a-zA-Z0-9]', '_', name).lower()
fname = self.output_path / f"{name}_report.json"
if not os.path.isfile(fname):
logger.warning(f'Region report file "{fname}" not found.')
continue
with open(fname, encoding="utf-8") as fd:
report = json.load(fd)
if not report:
continue
tree = _build_memory_report_tree(report)
if generate_figure:
# When doing a memory-region report, the total size of the children
# may exceed the region size by a few bytes due to how the linker
# deals with alignment padding at a section boundary. plotly
# barfs if they do not match, so force them to be the same.
saved_size = report['symbols']['size']
report['symbols']['size'] = sum(
node['size'] for node in report['symbols']['children']
)
figure = generate_figure(report)
figure.update_layout(paper_bgcolor=CSS_BS_TABLE_BG, height=MEMORY_PLOT_HEIGHT)
plot = figure.to_html(full_html=False, include_plotlyjs=False)
report['symbols']['size'] = saved_size
else:
plot = None
region_ctxt_list.append(
{
'name': name,
'tab_id': tab_id,
'path': region['path'],
'region_size': region['size'],
'report': json.dumps(tree),
'report_size': tree.get('size', 0),
'plot': plot,
}
)
ctxt['memory_regions'] = region_ctxt_list
def _safe_relpath(self, filename):
'''
Returns a relative path to the file from the top dir, or
the absolute path if a relative path cannot be established
(on Windows with files in different drives, for example).
'''
try:
return os.path.relpath(filename, start=self.topdir)
except ValueError:
return filename
def _edt_fancytree(self):
'''
Create the Fancytree data object for the EDT tree.
'''
edt_pickle_path = self.build_path / "zephyr" / 'edt.pickle'
with open(edt_pickle_path, "rb") as f:
edt = pickle.load(f)
# Load the same DTS file into a dtlib object so we can iterate over
# all properties (not just ones that edtlib tracks).
dt = DT(edt.dts_path, base_dir=self.topdir)
label2path = {}
def create_tree_node(dt_node, expanded=False):
edt_node = edt.get_node(dt_node.path)
if edt_node and edt_node.binding_path:
binding_path = self._safe_relpath(edt_node.binding_path)
else:
binding_path = ""
return {
"id": dt_node.path,
"expanded": expanded,
"edtNode": {
"isProperty": False,
"name": dt_node.name,
"labels": dt_node.labels,
"filename": self._safe_relpath(dt_node.filename),
"lineno": dt_node.lineno,
"description": edt_node.description if edt_node else "",
"bindingPath": binding_path,
},
}
def create_prop_tree_node(dt_prop, edt_prop):
# Extract the original text value from the string conversion of the
# property. No other way to get it from this object. Also remove
# the trailing semi-colon and extra spaces around <>.
value = re.sub('^.*= ', '', str(dt_prop))[:-1].replace('< ', '<').replace(' >', '>')
# Find all the label references and create a look-up table to the node path.
# This will be used on the render side to create links.
for m in re.finditer(r'&(\w+)', value):
try:
label2path[m.group(1)] = dt.label2node[m.group(1)].path
except KeyError:
continue
return {
"id": dt_prop.node.path + f':{dt_prop.name}',
"edtNode": {
"isProperty": True,
"name": dt_prop.name,
"value": value,
"filename": self._safe_relpath(dt_prop.filename),
"lineno": dt_prop.lineno,
"description": edt_prop.description if edt_prop else "",
"typeSpec": edt_prop.type if edt_prop else "",
"bindingPath": "",
},
}
def add_children(tree_node, parent):
tree_node['children'] = []
edt_node = edt.get_node(parent.path)
for name, dt_prop in parent.props.items():
prop_node = create_prop_tree_node(dt_prop, edt_node.props.get(name))
tree_node['children'].append(prop_node)
for _, dt_node in parent.nodes.items():
child_tree_node = create_tree_node(dt_node)
tree_node['children'].append(child_tree_node)
add_children(child_tree_node, dt_node)
tree = {'tree': [], 'label2path': label2path}
tree_node = create_tree_node(dt.root, expanded=True)
tree['tree'].append(tree_node)
add_children(tree_node, dt.root)
return tree
def create_html(self):
'''
Generate all the HTML output.
'''
templates_path = Path(__file__).parents[0] / "templates"
template_loader = jinja2.FileSystemLoader(searchpath=str(templates_path))
env = jinja2.Environment(
loader=template_loader, autoescape=True, trim_blocks=True, lstrip_blocks=True
)
env.filters['display_size'] = display_size
# Create the output directory tree.
self.output_path.mkdir(parents=True, exist_ok=True)
# Copy the static tree from the dashboard location as well as
# doc/_static for the logo images.
rpt_static = self.output_path / "static"
doc_static = self.zephyr_base / "doc" / "_static"
shutil.copytree(Path(__file__).parent.resolve() / 'static', rpt_static, dirs_exist_ok=True)
img_dir = rpt_static / "img"
img_dir.mkdir(exist_ok=True)
shutil.copy2(doc_static / "images" / "logo.svg", rpt_static / "img")
shutil.copy2(doc_static / "images" / "favicon.png", rpt_static / "img")
shutil.copy2(doc_static / "css" / "light.css", rpt_static / "css")
# Create the pygments CSS file.
pygments_style = get_style_by_name('default')
pygments_formatter = HtmlFormatter(style=pygments_style)
pygments_css = pygments_formatter.get_style_defs('.highlight')
with open(rpt_static / "css" / "pygments.css", 'w', encoding="utf-8") as fd:
fd.write(pygments_css)
# Create the plotly.js file if plotly was found.
if generate_figure:
with open(rpt_static / "js" / "plotly.js", 'w', encoding="utf-8") as fd:
fd.write(get_plotlyjs())
# ---------------------------------
# HTML: index.html
# ---------------------------------
template = env.get_template("index.html")
context = {'build': self, 'title': 'Build Summary', 'current': 'home'}
fname = self.output_path / "index.html"
logger.info(f"Creating {fname}")
with open(fname, 'w', encoding="utf-8") as fd:
fd.write(template.render(**context))
# ---------------------------------
# HTML: kconfig.html
# ---------------------------------
template = env.get_template("kconfig.html")
context = {
'build': self,
'title': 'Kconfig',
'current': 'kconfig',
'kconfig_browser': KCONFIG_BROWSER,
}
fname = os.path.join(self.output_path, 'kconfig.html')
logger.debug(f"Creating {fname}")
with open(fname, 'w', encoding="utf-8") as fd:
fd.write(template.render(**context))
# ---------------------------------
# HTML: sysinit.html
# ---------------------------------
template = env.get_template("sysinit.html")
context = {'build': self, 'title': 'Sys Init', 'current': 'sysinit'}
fname = os.path.join(self.output_path, 'sysinit.html')
logger.debug(f"Creating {fname}")
with open(fname, 'w', encoding="utf-8") as fd:
fd.write(template.render(**context))
# ---------------------------------
# HTML: dts.html
# ---------------------------------
try:
with open(self.dts_file, encoding="utf-8") as fd:
dts_contents = fd.read()
except OSError as e:
logger.warning(f'Unable to load device tree file at {self.dts_file}: {e}')
dts_contents = None
if dts_contents:
dts_contents = highlight(dts_contents, DevicetreeLexer(), pygments_formatter)
edt_tree = self._edt_fancytree()
template = env.get_template("dts.html")
context = {
'build': self,
'title': 'Device Tree',
'current': 'dts',
'file_contents': dts_contents,
'file_path': self.dts_file.absolute(),
'edt_tree': json.dumps(edt_tree),
}
fname = self.output_path / "dts.html"
logger.debug(f"Creating {fname}")
with open(fname, 'w', encoding='utf-8') as fd:
fd.write(template.render(**context))
# ---------------------------------
# HTML: memoryreport.html
# ---------------------------------
if not self.skip_memory_report:
template = env.get_template("memoryreport.html")
context = {
'build': self,
'title': 'Memory Report',
'current': 'memory',
'include_plotly_js': bool(generate_figure),
}
self._memory_report_template_context('all', context)
self._memory_report_template_context('ram', context)
self._memory_report_template_context('rom', context)
self._region_reports_template_context(context)
fname = self.output_path / 'memoryreport.html'
logger.debug(f"Creating {fname}")
with open(fname, 'w', encoding="utf-8") as fd:
fd.write(template.render(**context))
# ---------------------------------
# HTML: elfstats.html
# ---------------------------------
fname = self.build_path / "zephyr" / f"{self.kernel_bin_name}.stat"
try:
with open(fname, encoding="utf-8") as fd:
file_contents = fd.read()
except OSError as e:
logger.warning(f'Unable to load zephyr elf stat file at {fname}: {e}')
file_contents = ''
template = env.get_template("textviewer.html")
context = {
'build': self,
'title': 'ELF Stats',
'current': 'elfstats',
'file_contents': file_contents,
'file_path': fname.absolute(),
}
fname = self.output_path / "elfstats.html"
logger.debug(f"Creating {fname}")
with open(fname, 'w', encoding='utf-8') as fd:
fd.write(template.render(**context))
def clean_up(self):
'''
Clean-up any temporary files.
'''
for mem_type in ['ram', 'rom', 'all']:
fname = self.output_path / f"{mem_type}_report.json"
fname.unlink(missing_ok=True)
for region in self.memory_regions:
fname = self.output_path / f"{region['name']}_report.json"
fname.unlink(missing_ok=True)
def open_browser(self):
'''
Open the default web browser to the index page of the dashboard.
Uses a one-shot HTTP server so that the browser loads from
http://localhost, which works reliably across most platforms.
'''
handler = partial(SimpleHTTPRequestHandler, directory=str(self.output_path))
server = HTTPServer(("127.0.0.1", 0), handler)
url = f"http://127.0.0.1:{server.server_port}/index.html"
logger.info("Serving dashboard at %s (Ctrl+C to stop)", url)
webbrowser.open(url)
with contextlib.suppress(KeyboardInterrupt):
server.serve_forever()
def parse_args():
"""
Parse command line arguments.
"""
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument("build", help="Build directory", nargs='?', default="build")
parser.add_argument("--output", help="Output directory for index.html")
parser.add_argument("--zephyr-base", default=".", help="Zephyr base directory")
parser.add_argument("--kernel-bin-name", default="zephyr", help="Kernel binary name")
parser.add_argument(
"--skip-memory-report",
action="store_true",
help="Skip the memory report to generate faster",
)
parser.add_argument(
"--no-clean", action="store_true", help="Do not remove temporary memory map json data"
)
parser.add_argument(
"-v", "--verbose", action="store_true", help="Print extra debugging information"
)
parser.add_argument("-q", "--quiet", action="store_true", help="Print nothing to the console.")
parser.add_argument(
"--open", action="store_true", help="Open the default web browser to the dashboard."
)
return parser.parse_args()
def main():
'''
Main function when invoked from the command-line.
'''
args = parse_args()
if args.verbose:
logging.basicConfig(level=logging.DEBUG)
logger.setLevel(logging.DEBUG)
elif not args.quiet:
logging.basicConfig(level=logging.INFO)
logger.setLevel(logging.INFO)
output_path = args.output
zephyr_base = args.zephyr_base
build_path = args.build
build = ZephyrDashboard(
zephyr_base,
build_path,
output_path,
kernel_bin_name=args.kernel_bin_name,
skip_memory_report=args.skip_memory_report,
)
build.create_html()
if not args.no_clean:
build.clean_up()
if args.open:
build.open_browser()
if __name__ == "__main__":
main()