Skip to content

Add benchmark dashboard - #169

Draft
PranjalC100 wants to merge 60 commits into
mainfrom
add-benchmark-dashboard
Draft

Add benchmark dashboard#169
PranjalC100 wants to merge 60 commits into
mainfrom
add-benchmark-dashboard

Conversation

@PranjalC100

Copy link
Copy Markdown
Member

No description provided.

- Import plot_reports.py from d_princer_micro_benchmarking branch.
- Update plot_reports.py to support both 6-field and 7-field CSV test matrix parameters.
- Add --no-auto-plot and --plot-metric-group flags to orchestrator.py and run.sh.
- Add pandas and matplotlib dependencies to requirements.txt.
- Explicitly disable plotting in kokoro_run.sh using --no-auto-plot.
Add a Python FastAPI server and Single-Page HTML/Tailwind/JS frontend under benchmark-dashboard/. The dashboard supports:
- User metadata (username and run description tracking).
- Dynamic test parameters configurator (automatically listing files from test_suites/).
- Resource-locked queue management serializing runs per target machine while running different targets concurrently.
- Expanding historical runs to view configurations and output logs.
- Cloning run parameters.
- Rendering comparative performance charts using Chart.js based on BigQuery run metrics.
- Updated Username field to Username (LDAP) for team tracking.
- Added advanced Managed Instance Group settings: single/multi-thread GCE VM templates support.
- Added '+ Add Custom CSV' endpoints and modals for test cases and mount options.
- Implemented real-time dynamic configurations previewer for selected test suites, mount config CSVs, and FIO templates.
- Defaulted artifacts bucket to 'pranjal-bucket-1'.
…Split Views, and Progress Analysis

- Restructured dashboard UI to use a Google-style clean Light Mode theme.
- Added Google LDAP sign-in overlay card saving session identity locally.
- Restructured layout to a side-by-side split screen showing the configuration form and live previews concurrently.
- Re-routed preview accordion widgets to render as direct view block inspectors.
- Added input fields for custom GCS artifacts and test data buckets.
- Implemented real-time worker-node execution progress tracker matching the progress analysis logic of 'analyse.py'.
- Added SSH project network requirement banner and documentation cards.
- Renamed UI-created FIO folder target path to 'test_suites/custom_fio_configs' for naming consistency.
- Updated root .gitignore to ignore local database caches, virtual environments, and custom benchmark templates.
- Added Pytest unit tests in tests/test_api.py covering configurations listing, traversal security guards, run submissions, and progress tracking.
- Fixed an AttributeError by adding the missing commit_hash parameter to the BenchmarkRunRequest Pydantic model.
- Fixed a SQLite UNIQUE constraint collision bug by appending a random integer suffix to enqueued benchmark IDs.
- Implemented GCE lookup resolver in main.py to dynamically locate VM projects and zones.
- Locked Project input field on the SPA UI as a read-only showcase field auto-populated by the VM resolution loop.
- Added VM zone auto-fill based on search resolver output.
- Re-aligned BQ comparator queries to dynamically read project IDs, enabling cross-project plots.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a web-based benchmark dashboard for GCSFuse, adding a FastAPI backend, a frontend SPA, and integrating it with the existing distributed micro-benchmark orchestrator. Key security and code quality feedback highlights high-severity vulnerabilities, including path traversal in file previews, stored XSS in frontend rendering, and SQL injection in BigQuery queries. Additionally, improvements are suggested to handle subprocess termination on task cancellation, utilize the native GCS client library instead of blocking shell commands, and fix a hardcoded project ID in the BigQuery history endpoint.

Comment thread benchmark-dashboard/main.py Outdated
Comment on lines +341 to +343
safe_path = (DMB_DIR / path).resolve()
if not str(safe_path).startswith(str(DMB_DIR)):
raise HTTPException(status_code=403, detail="Access denied: Path lies outside benchmark directory")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The current path validation uses startswith on the resolved path string. This is vulnerable to path traversal if there are sibling directories with similar prefixes (e.g., /home/user/gcs and /home/user/gcs_other). Using pathlib.Path.relative_to is a much more robust and secure way to ensure the path is strictly inside the target directory.

Suggested change
safe_path = (DMB_DIR / path).resolve()
if not str(safe_path).startswith(str(DMB_DIR)):
raise HTTPException(status_code=403, detail="Access denied: Path lies outside benchmark directory")
try:
safe_path = (DMB_DIR / path).resolve()
safe_path.relative_to(DMB_DIR.resolve())
except ValueError:
raise HTTPException(status_code=403, detail="Access denied: Path lies outside benchmark directory")

Comment on lines +269 to +280
item.innerHTML = `
<div class="flex justify-between items-start mb-2">
<span class="font-mono text-xs text-slate-500 font-bold">${run.benchmark_id}</span>
<span class="text-[10px] font-bold px-2 py-0.5 rounded border uppercase tracking-wider ${statusColors[run.status] || 'bg-slate-100'}">${run.status}</span>
</div>
<h4 class="font-bold text-sm text-slate-800 mb-1 truncate">${run.description}</h4>
<p class="text-xs text-slate-500 mb-2">VM: <span class="font-mono font-bold text-slate-700">${run.executor_vm}</span></p>
<div class="flex justify-between items-center text-[10px] text-slate-400">
<span>User: ${run.username}</span>
<button onclick="cancelRun(event, '${run.benchmark_id}')" class="text-red-600 hover:text-red-800 font-bold uppercase transition"><i class="fa-solid fa-ban mr-1"></i>Cancel</button>
</div>
`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

User-controlled fields like run.description and run.username are rendered directly into the DOM using innerHTML without escaping. This introduces a Stored Cross-Site Scripting (XSS) vulnerability. All user-supplied values should be escaped before being rendered in HTML.

        const escapeHtml = (str) => {
            if (!str) return '';
            return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
        };
        item.innerHTML = `
            <div class="flex justify-between items-start mb-2">
                <span class="font-mono text-xs text-slate-500 font-bold">${escapeHtml(run.benchmark_id)}</span>
                <span class="text-[10px] font-bold px-2 py-0.5 rounded border uppercase tracking-wider ${statusColors[run.status] || 'bg-slate-100'}">${escapeHtml(run.status)}</span>
            </div>
            <h4 class="font-bold text-sm text-slate-800 mb-1 truncate">${escapeHtml(run.description)}</h4>
            <p class="text-xs text-slate-500 mb-2">VM: <span class="font-mono font-bold text-slate-700">${escapeHtml(run.executor_vm)}</span></p>
            <div class="flex justify-between items-center text-[10px] text-slate-400">
                <span>User: ${escapeHtml(run.username)}</span>
                <button onclick="cancelRun(event, '${escapeHtml(run.benchmark_id)}')" class="text-red-600 hover:text-red-800 font-bold uppercase transition"><i class="fa-solid fa-ban mr-1"></i>Cancel</button>
            </div>
        `;

Comment on lines +143 to +162
try:
# Open local log file to stream subprocess output
with open(log_file_path, "w") as log_f:
process = await asyncio.create_subprocess_exec(
"python3", *args,
cwd=str(DMB_DIR),
stdout=log_f,
stderr=subprocess.STDOUT if hasattr(subprocess, 'STDOUT') else log_f
)

# Wait for execution to finish
exit_code = await process.wait()

if exit_code == 0:
logger.info(f"Subprocess finished successfully for {benchmark_id}")
db.update_run_status(benchmark_id, "completed", completed_at=datetime.utcnow().isoformat())
else:
logger.error(f"Subprocess failed with exit code {exit_code} for {benchmark_id}")
db.update_run_status(benchmark_id, "failed", completed_at=datetime.utcnow().isoformat())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If the execute_orchestrator task is cancelled (e.g., during shutdown or manual cancellation), the await process.wait() call will raise asyncio.CancelledError. However, the spawned subprocess will continue running in the background as an orphaned process. Wrapping the subprocess wait in a try...except asyncio.CancelledError block ensures that the subprocess is properly terminated when the task is cancelled.

    process = None
    try:
        # Open local log file to stream subprocess output
        with open(log_file_path, "w") as log_f:
            process = await asyncio.create_subprocess_exec(
                "python3", *args,
                cwd=str(DMB_DIR),
                stdout=log_f,
                stderr=subprocess.STDOUT if hasattr(subprocess, 'STDOUT') else log_f
            )
            
            # Wait for execution to finish
            exit_code = await process.wait()
            
            if exit_code == 0:
                logger.info(f"Subprocess finished successfully for {benchmark_id}")
                db.update_run_status(benchmark_id, "completed", completed_at=datetime.utcnow().isoformat())
            else:
                logger.error(f"Subprocess failed with exit code {exit_code} for {benchmark_id}")
                db.update_run_status(benchmark_id, "failed", completed_at=datetime.utcnow().isoformat())
    except asyncio.CancelledError:
        if process and process.returncode is None:
            logger.warning(f"Orchestrator process for {benchmark_id} was cancelled. Terminating subprocess...")
            try:
                process.terminate()
                await process.wait()
            except Exception as e:
                logger.error(f"Failed to terminate subprocess: {e}")
        raise

Comment thread benchmark-dashboard/main.py Outdated
Comment on lines +697 to +699
# Resolve GCE project dynamically from DB, fallback to default
run_config = db.get_run(rid)
proj = run_config.get("project", project_id) if run_config else project_id

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The proj variable is retrieved from run_config (which contains user-supplied values stored in the SQLite database) and is directly interpolated into the BigQuery SQL query string. This introduces a SQL injection vulnerability. Validating the project ID against a strict regular expression before query execution is highly recommended.

Suggested change
# Resolve GCE project dynamically from DB, fallback to default
run_config = db.get_run(rid)
proj = run_config.get("project", project_id) if run_config else project_id
import re
run_config = db.get_run(rid)
proj = run_config.get("project", project_id) if run_config else project_id
if not re.match(r"^[a-z0-9\-]{6,30}$", proj):
raise HTTPException(status_code=400, detail="Invalid project ID format")

Comment on lines +671 to +679
if run["status"] == "running":
# Create cancel flag in GCS for workers to detect
try:
import subprocess
cancel_path = f"gs://{run['artifacts_bucket']}/{run_id}/cancel"
subprocess.run(['gcloud', 'storage', 'cp', '-', cancel_path], input=b'cancelled', check=True)
logger.info(f"GCS cancellation flag written for {run_id}")
except Exception as e:
logger.error(f"Failed to create cancel flag in GCS: {e}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Spawning a synchronous gcloud storage cp subprocess to write a cancellation flag to GCS is inefficient and blocks the threadpool. Since the google-cloud-storage library is already imported and used in this file, you should use the native GCS client library instead.

Suggested change
if run["status"] == "running":
# Create cancel flag in GCS for workers to detect
try:
import subprocess
cancel_path = f"gs://{run['artifacts_bucket']}/{run_id}/cancel"
subprocess.run(['gcloud', 'storage', 'cp', '-', cancel_path], input=b'cancelled', check=True)
logger.info(f"GCS cancellation flag written for {run_id}")
except Exception as e:
logger.error(f"Failed to create cancel flag in GCS: {e}")
if run["status"] == "running":
# Create cancel flag in GCS for workers to detect
try:
client = storage.Client()
bucket = client.bucket(run['artifacts_bucket'])
blob = bucket.blob(f"{run_id}/cancel")
blob.upload_from_string("cancelled")
logger.info(f"GCS cancellation flag written for {run_id}")
except Exception as e:
logger.error(f"Failed to create cancel flag in GCS: {e}")

Comment on lines +445 to +453
query = """
SELECT DISTINCT benchmark_id, run_timestamp, commit, mount_args, io_type
FROM `gcs-fuse-test-ml.periodic_benchmarks.kokoro_run_*`
UNION DISTINCT
SELECT DISTINCT benchmark_id, run_timestamp, commit, mount_args, io_type
FROM `gcs-fuse-test-ml.adhoc_benchmarks.local_run_*`
ORDER BY run_timestamp DESC
LIMIT 100
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The get_bq_history endpoint accepts a project_id parameter but hardcodes gcs-fuse-test-ml in the SQL query, rendering the parameter useless. The query should safely interpolate the validated project_id parameter.

Suggested change
query = """
SELECT DISTINCT benchmark_id, run_timestamp, commit, mount_args, io_type
FROM `gcs-fuse-test-ml.periodic_benchmarks.kokoro_run_*`
UNION DISTINCT
SELECT DISTINCT benchmark_id, run_timestamp, commit, mount_args, io_type
FROM `gcs-fuse-test-ml.adhoc_benchmarks.local_run_*`
ORDER BY run_timestamp DESC
LIMIT 100
"""
import re
if not re.match(r"^[a-z0-9\-]{6,30}$", project_id):
raise HTTPException(status_code=400, detail="Invalid project ID format")
query = f"""
SELECT DISTINCT benchmark_id, run_timestamp, commit, mount_args, io_type
FROM `{project_id}.periodic_benchmarks.kokoro_run_*`
UNION DISTINCT
SELECT DISTINCT benchmark_id, run_timestamp, commit, mount_args, io_type
FROM `{project_id}.adhoc_benchmarks.local_run_*`
ORDER BY run_timestamp DESC
LIMIT 100
"""

- Disabled the Zone input field by default and styled it read-only on startup.
- Configured frontend GCE VM resolver to auto-fill the target Zone and keep the field locked when successfully resolved.
- Implemented automatic fallback to unlock the Zone field for manual entry if GCE name discovery yields no zone context.
- Increased max-height on preview code pre blocks from max-h-36 (144px) to max-h-72 (288px) to reduce vertical scrolling when reviewing large test matrix CSV files.
- Added python -u flag to the create_subprocess_exec launcher to disable stdout buffering when output is redirected to log files.
- Ensures benchmark log outputs are immediately flushed to orchestrator.log and streamed to the UI in real-time.
…ve dropdown selections during template addition
…l test logic for a simpler, faster dashboard
…render full dynamic configs CSV mount options table in PDF report
…mma-separated FIO parameters format with format description labels
@PranjalC100
PranjalC100 force-pushed the add-benchmark-dashboard branch 6 times, most recently from 5181129 to 3de4261 Compare July 7, 2026 04:08
@PranjalC100
PranjalC100 force-pushed the add-benchmark-dashboard branch from aaff610 to 8c1f797 Compare July 13, 2026 11:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant