Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"name": "microshift-ci",
"source": "./plugins/microshift-ci",
"description": "MicroShift CI Automation",
"version": "1.4.3"
"version": "1.5.0"
},
{
"name": "microshift-dev",
Expand Down
2 changes: 1 addition & 1 deletion plugins/microshift-ci/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "microshift-ci",
"description": "MicroShift CI Automation",
"version": "1.4.3",
"version": "1.5.0",
"author": {
"name": "ggiguash"
},
Expand Down
13 changes: 9 additions & 4 deletions plugins/microshift-ci/scripts/pcp-graphs/create-pcp-dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,25 +315,27 @@ def escape_json_for_script(json_str):
return json_str.replace("</", "<\\/").replace("<!--", "<\\!--")


def build_html(chartjs_src, pcp_charts_src, data_json, scenarios_json, timezone):
def build_html(chartjs_src, pcp_charts_src, data_json, scenarios_json, timezone,
title="PCP Performance Dashboard"):
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
"""Assemble the complete HTML document."""
safe_data = escape_json_for_script(data_json)
safe_scenarios = escape_json_for_script(scenarios_json)
safe_tz = html.escape(timezone)
safe_title = html.escape(title)

return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PCP Performance Dashboard</title>
<title>{safe_title}</title>
<style>
{CSS}
</style>
</head>
<body>
<div class="sidebar">
<h1>PCP Dashboard</h1>
<h1>{safe_title}</h1>
<div style="padding:4px 16px;font-size:0.75em;color:#8888aa;">Timezone: {safe_tz}</div>
<div id="sidebar-items"></div>
</div>
Expand Down Expand Up @@ -368,6 +370,8 @@ def main():
help="Timezone label for display (default: UTC)")
parser.add_argument("--output",
help="Output HTML file (default: <workdir>/pcp-dashboard.html)")
parser.add_argument("--title", default="PCP Performance Dashboard",
help="HTML <title> for the dashboard")
args = parser.parse_args()

dashboard_dir = os.path.join(args.workdir, "pcp-dashboard")
Expand All @@ -385,7 +389,8 @@ def main():
data_json = json.dumps(data, separators=(",", ":"))
scenarios_json = json.dumps(scenarios, separators=(",", ":"))

html = build_html(chartjs_src, pcp_charts_src, data_json, scenarios_json, args.timezone)
html = build_html(chartjs_src, pcp_charts_src, data_json, scenarios_json, args.timezone,
title=args.title)

with open(output, "w") as f:
f.write(html)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,18 @@ def find_scenario_dirs(artifacts_root):
"""Yield (build_id, scenario_name, scenario_dir) tuples."""
if not os.path.isdir(artifacts_root):
return
seen = set()
for build_id in os.listdir(artifacts_root):
build_dir = os.path.join(artifacts_root, build_id)
if not os.path.isdir(build_dir):
continue
for root, dirs, _files in os.walk(build_dir):
for root, dirs, _files in os.walk(build_dir, followlinks=True):
st = os.stat(root)
ident = (st.st_dev, st.st_ino)
if ident in seen:
dirs.clear()
continue
seen.add(ident)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
if os.path.basename(root) != "scenario-info":
continue
for scenario in sorted(os.listdir(root)):
Expand Down
78 changes: 58 additions & 20 deletions plugins/microshift-ci/scripts/pcp-graphs/generate-dashboard.sh
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
#!/usr/bin/bash
# Generate an interactive PCP performance dashboard from a Prow job URL.
# Generate an interactive PCP performance dashboard.
#
# Downloads artifacts from GCS, extracts per-VM PCP archives, and produces
# an interactive HTML dashboard.
# Two modes:
# --url <prow-url> Download artifacts from GCS (default)
# --local <path> Use a local scenario-info/ directory
#
# Usage: generate-dashboard.sh --url <prow-url> [--parallel N] [--timezone TZ]
# Usage:
# generate-dashboard.sh --url <prow-url> [--parallel N] [--timezone TZ] [--output FILE] [--title TITLE]
# generate-dashboard.sh --local <path> [--parallel N] [--timezone TZ] [--output FILE] [--title TITLE]
#
# Prerequisites: gsutil, python3, and one of:
# Prerequisites: python3, and one of:
# - pcp-export-pcp2json (native)
# - podman (container fallback)

Expand All @@ -16,15 +19,24 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SHARED_SCRIPTS="$(cd "${SCRIPT_DIR}/../../../shared/scripts" && pwd)"

URL=""
LOCAL_PATH=""
OUTPUT=""
TITLE=""
PARALLEL=6
TIMEZONE="UTC"
PCP2JSON_MODE="" # "native" or "container"
CONTAINER_RT="" # "podman"
CONTAINER_IMAGE="pcp2json-tool"

usage() {
echo "Usage: ${0} --url <prow-url> [--parallel N] [--timezone TZ]" >&2
echo " --url URL : Prow job URL (required)" >&2
echo "Usage:" >&2
echo " ${0} --url <prow-url> [OPTIONS]" >&2
echo " ${0} --local <path> [OPTIONS]" >&2
echo "" >&2
echo " --url URL : Prow job URL (mutually exclusive with --local)" >&2
echo " --local PATH : path to scenario-info/ directory (mutually exclusive with --url)" >&2
echo " --output FILE : output HTML file path (default: <workdir>/pcp-dashboard.html)" >&2
echo " --title TITLE : HTML <title> for the dashboard" >&2
echo " --parallel N : number of parallel extraction jobs (default: 6)" >&2
echo " --timezone TZ : IANA timezone for timestamps (default: UTC)" >&2
exit 1
Expand All @@ -35,6 +47,15 @@ while [[ $# -gt 0 ]]; do
--url)
[[ $# -lt 2 ]] && { echo "Error: --url requires a URL" >&2; usage; }
URL="$2"; shift 2 ;;
--local)
[[ $# -lt 2 ]] && { echo "Error: --local requires a path" >&2; usage; }
LOCAL_PATH="$2"; shift 2 ;;
--output)
[[ $# -lt 2 ]] && { echo "Error: --output requires a file path" >&2; usage; }
OUTPUT="$2"; shift 2 ;;
--title)
[[ $# -lt 2 ]] && { echo "Error: --title requires a value" >&2; usage; }
TITLE="$2"; shift 2 ;;
--parallel)
[[ $# -lt 2 ]] && { echo "Error: --parallel requires a number" >&2; usage; }
[[ "$2" =~ ^[1-9][0-9]*$ ]] || { echo "Error: --parallel must be a positive integer" >&2; usage; }
Expand All @@ -47,20 +68,34 @@ while [[ $# -gt 0 ]]; do
esac
done

if [[ -z "${URL}" ]]; then
echo "Error: --url is required" >&2
if [[ -n "${URL}" && -n "${LOCAL_PATH}" ]]; then
echo "Error: --url and --local are mutually exclusive" >&2
usage
fi

if [[ -z "${URL}" && -z "${LOCAL_PATH}" ]]; then
echo "Error: --url or --local is required" >&2
usage
fi

# ---------------------------------------------------------------------------
# URL parsing and artifact download (delegates to shared download-jobs.sh)
# Set up WORKDIR depending on mode
# ---------------------------------------------------------------------------

# Extract build ID (last path segment of the URL) to compute workdir
BUILD_ID=$(basename "${URL%/}")
WORKDIR="/tmp/microshift-job-pcp-dashboard.${BUILD_ID}"

bash "${SHARED_SCRIPTS}/download-jobs.sh" --workdir "${WORKDIR}" --url "${URL}"
if [[ -n "${LOCAL_PATH}" ]]; then
# Local mode: symlink the local scenario-info into a temp workdir
LOCAL_PATH="$(cd "${LOCAL_PATH}" && pwd)"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
BUILD_ID="local"
WORKDIR=$(mktemp -d "/tmp/microshift-pcp-local.XXXXXX")
mkdir -p "${WORKDIR}/artifacts/${BUILD_ID}"
ln -s "${LOCAL_PATH}" "${WORKDIR}/artifacts/${BUILD_ID}/scenario-info"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
echo "Local mode: ${LOCAL_PATH} -> ${WORKDIR}/artifacts/${BUILD_ID}/scenario-info" >&2
else
# URL mode: download artifacts via shared script
BUILD_ID=$(basename "${URL%/}")
WORKDIR="/tmp/microshift-job-pcp-dashboard.${BUILD_ID}"
bash "${SHARED_SCRIPTS}/download-jobs.sh" --workdir "${WORKDIR}" --url "${URL}"
fi

# ---------------------------------------------------------------------------
# pcp2json detection: native or container fallback
Expand Down Expand Up @@ -121,7 +156,7 @@ run_pcp2json() {
# ---------------------------------------------------------------------------

find_pcp_tarballs() {
find "${WORKDIR}/artifacts" -name "pcp-archives.tar" -path "*/vms/*/pcp/*" \
find -L "${WORKDIR}/artifacts" -name "pcp-archives.tar" -path "*/vms/*/pcp/*" \
2>/dev/null | sort
}

Expand Down Expand Up @@ -223,7 +258,7 @@ process_tarball() {
# ---------------------------------------------------------------------------

find_hypervisor_pcp_dirs() {
find "${WORKDIR}/artifacts" -name "Latest" -path "*pmlogs*" \
find -L "${WORKDIR}/artifacts" -name "Latest" -path "*pmlogs*" \
-exec dirname {} \; 2>/dev/null | sort
}

Expand Down Expand Up @@ -327,10 +362,13 @@ python3 "${SCRIPT_DIR}/extract_scenarios.py" --workdir "${WORKDIR}"

# Generate the HTML dashboard
echo "Generating HTML dashboard..." >&2
python3 "${SCRIPT_DIR}/create-pcp-dashboard.py" \
--workdir "${WORKDIR}" --timezone "${TIMEZONE}"
dashboard_args=(--workdir "${WORKDIR}" --timezone "${TIMEZONE}")
[[ -n "${OUTPUT}" ]] && dashboard_args+=(--output "${OUTPUT}")
[[ -n "${TITLE}" ]] && dashboard_args+=(--title "${TITLE}")

python3 "${SCRIPT_DIR}/create-pcp-dashboard.py" "${dashboard_args[@]}"

output="${WORKDIR}/pcp-dashboard.html"
output="${OUTPUT:-${WORKDIR}/pcp-dashboard.html}"
if [[ -f "${output}" ]]; then
echo "Dashboard: ${output}" >&2
else
Expand Down