Skip to content
Merged
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
41 changes: 37 additions & 4 deletions src/dvsim/cli/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,45 @@ def cli() -> None:
"""


@cli.group()
def dashboard() -> None:
"""Dashboard helper commands."""


@dashboard.command("gen")
@click.argument(
"json_path",
type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path),
)
@click.argument(
"output_dir",
type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
)
@click.option(
"--base-url",
default=None,
type=str,
)
def dashboard_gen(json_path: Path, output_dir: Path, base_url: str | None) -> None:
"""Generate a dashboard from a existing results JSON."""
from dvsim.sim.dashboard import gen_dashboard # noqa: PLC0415
from dvsim.sim.data import SimResultsSummary # noqa: PLC0415

results: SimResultsSummary = SimResultsSummary.load(path=json_path)

gen_dashboard(
summary=results,
path=output_dir,
base_url=base_url,
)


@cli.group()
def report() -> None:
"""Reporting helper commands."""


@report.command()
@report.command("gen")
@click.argument(
"json_path",
type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path),
Expand All @@ -36,10 +69,10 @@ def report() -> None:
"output_dir",
type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
)
def gen(json_path: Path, output_dir: Path) -> None:
def report_gen(json_path: Path, output_dir: Path) -> None:
"""Generate a report from a existing results JSON."""
from dvsim.sim.data import SimResultsSummary
from dvsim.sim.report import gen_reports
from dvsim.sim.data import SimResultsSummary # noqa: PLC0415
from dvsim.sim.report import gen_reports # noqa: PLC0415

summary: SimResultsSummary = SimResultsSummary.load(path=json_path)
flow_results = summary.load_flow_results(
Expand Down
83 changes: 83 additions & 0 deletions src/dvsim/report/artifacts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Copyright lowRISC contributors (OpenTitan project).
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

"""Reporting artifacts."""

from collections.abc import Callable, Iterable
from pathlib import Path
from typing import TypeAlias

from dvsim.templates.render import render_static

__all__ = (
"ReportArtifacts",
"display_report",
"render_static_content",
"write_report",
)

# Report rendering returns mappings of relative report paths to (string) contents.
ReportArtifacts: TypeAlias = dict[str, str]


def write_report(files: ReportArtifacts, root: Path) -> None:
"""Write rendered report artifacts to the file system, relative to a given path.

Args:
files: the output report artifacts from rendering simulation results.
root: the path to write the report files relative to.

"""
for relative_path, content in files.items():
path = root / relative_path
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)


def display_report(
files: ReportArtifacts, sink: Callable[[str], None] = print, *, with_headers: bool = False
) -> None:
"""Emit the report artifacts to some textual sink.

Prints to stdout by default, but can also write to a logger by overriding the sink.

Args:
files: the output report artifacts from rendering simulation results.
sink: a callable that accepts a string. Default is `print` to stdout.
with_headers: a boolean controlling whether to emit artifact path names as headers.

"""
for path, content in files.items():
header = f"\n--- {path} ---\n" if with_headers else ""
sink(header + content + "\n")


def render_static_content(
static_files: Iterable[str],
outdir: Path | None = None,
) -> ReportArtifacts:
"""Render static artifacts.

These are files are just copied over as they don't need to be templated.
Where an outdir is specified the rendered artifacts are saved to that
directory eagerly as each file is rendered.

Args:
static_files: iterable of relative file paths as strings
outdir: optional output directory

Returns:
Report artifacts that have been rendered.

"""
artifacts = {}

for name in static_files:
artifacts[name] = render_static(path=name)
if outdir is not None:
artifact_path = outdir / name
artifact_path.parent.mkdir(parents=True, exist_ok=True)
artifact_path.write_text(artifacts[name])

return artifacts
61 changes: 61 additions & 0 deletions src/dvsim/sim/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright lowRISC contributors (OpenTitan project).
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

"""Generate dashboard.

The dashboard is a cut down version of the full report where a simpler summary
is required than the full report simulation summary. This is intended to be used
on a separate website and links back to the detailed report if required.

This is intended to generate a dashboard that could be used on the OpenTitan
and automatically i.e. https://opentitan.org/dashboard/index.html
"""

from pathlib import Path

from dvsim.logging import log
from dvsim.report.artifacts import render_static_content
from dvsim.sim.data import SimResultsSummary
from dvsim.templates.render import render_template

__all__ = ("gen_dashboard",)


def gen_dashboard(
summary: SimResultsSummary,
path: Path,
base_url: str | None = None,
) -> None:
"""Generate a summary dashboard.

Args:
summary: overview of the block results
path: output directory path
base_url: override the base URL for links

"""
log.debug("generating results dashboard")

path.parent.mkdir(parents=True, exist_ok=True)

# Generate the JS and CSS files
render_static_content(
static_files=[
"css/style.css",
"css/bootstrap.min.css",
"js/bootstrap.bundle.min.js",
"js/htmx.min.js",
],
outdir=path,
)

(path / "dashboard.html").write_text(
render_template(
path="dashboard/dashboard.html",
data={
"summary": summary,
"base_url": base_url,
},
)
)
77 changes: 15 additions & 62 deletions src/dvsim/sim/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@
"""Generate reports."""

from collections import defaultdict
from collections.abc import Callable, Collection, Iterable, Mapping
from collections.abc import Collection, Iterable, Mapping
from datetime import datetime
from pathlib import Path
from typing import Any, Protocol, TypeAlias
from typing import Any, Protocol

from tabulate import tabulate

from dvsim.logging import log
from dvsim.report.artifacts import ReportArtifacts, display_report, render_static_content
from dvsim.report.data import IPMeta
from dvsim.sim.data import SimFlowResults, SimResultsSummary
from dvsim.templates.render import render_static, render_template
from dvsim.templates.render import render_template
from dvsim.utils import TS_FORMAT_LONG
from dvsim.utils.fs import relative_to

Expand All @@ -24,9 +25,7 @@
"JsonReportRenderer",
"MarkdownReportRenderer",
"ReportRenderer",
"display_report",
"gen_reports",
"write_report",
)


Expand All @@ -41,10 +40,6 @@ def _indent_by_levels(lines: Iterable[tuple[int, str]], indent_spaces: int = 4)
return "\n".join(" " * lvl * indent_spaces + msg for lvl, msg in lines)


# Report rendering returns mappings of relative report paths to (string) contents.
ReportArtifacts: TypeAlias = dict[str, str]


class ReportRenderer(Protocol):
"""Renders/formats result reports, returning mappings of relative paths to (string) content."""

Expand Down Expand Up @@ -135,27 +130,17 @@ def render(
(outdir / "index.html").write_text(artifacts["index.html"])

# Generate other static site contents
artifacts.update(self.render_static_content(outdir))

return artifacts

def render_static_content(self, outdir: Path | None = None) -> ReportArtifacts:
"""Render static CSS / JS artifacts for HTML report generation."""
static_files = [
"css/style.css",
"css/bootstrap.min.css",
"js/bootstrap.bundle.min.js",
"js/htmx.min.js",
]

artifacts = {}

for name in static_files:
artifacts[name] = render_static(path=name)
if outdir is not None:
artifact_path = outdir / name
artifact_path.parent.mkdir(parents=True, exist_ok=True)
artifact_path.write_text(artifacts[name])
artifacts.update(
render_static_content(
static_files=[
"css/style.css",
"css/bootstrap.min.css",
"js/bootstrap.bundle.min.js",
"js/htmx.min.js",
],
outdir=outdir,
)
)

return artifacts

Expand Down Expand Up @@ -449,38 +434,6 @@ def render_summary(self, summary: SimResultsSummary) -> ReportArtifacts:
return {"report.md": report_md}


def write_report(files: ReportArtifacts, root: Path) -> None:
"""Write rendered report artifacts to the file system, relative to a given path.

Args:
files: the output report artifacts from rendering simulation results.
root: the path to write the report files relative to.

"""
for relative_path, content in files.items():
path = root / relative_path
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)


def display_report(
files: ReportArtifacts, sink: Callable[[str], None] = print, *, with_headers: bool = False
) -> None:
"""Emit the report artifacts to some textual sink.

Prints to stdout by default, but can also write to a logger by overriding the sink.

Args:
files: the output report artifacts from rendering simulation results.
sink: a callable that accepts a string. Default is `print` to stdout.
with_headers: a boolean controlling whether to emit artifact path names as headers.

"""
for path, content in files.items():
header = f"\n--- {path} ---\n" if with_headers else ""
sink(header + content + "\n")


def gen_reports(
summary: SimResultsSummary,
flow_results: Mapping[str, SimFlowResults],
Expand Down
Loading
Loading