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
15 changes: 8 additions & 7 deletions evalscope/perf/utils/perf_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,14 +434,15 @@ def sort_key(self) -> float:
def name(self) -> str:
"""Human-readable run label.

* ``parallel_<N>_number_<M>`` directories → "Parallel N / Number M"
* ``rate_<R>_number_<M>`` directories → "Rate R rps / Number M"
* closed-loop (``parallel_<N>_number_<M>``) → "Parallel N / Number M"
* open-loop with a fixed rate → "Rate R rps / Number M"
* open-loop with no fixed rate (rate=-1) → "Open-loop / Number M"
"""
import re
m = re.match(r'^rate_([\d.]+)_number_(\d+)$', self.dir_name)
if m:
return f'Rate {m.group(1)} rps / Number {m.group(2)}'
return f'Parallel {self.parallel} / Number {self.number}'
if not self.is_open_loop:
return f'Parallel {self.parallel} / Number {self.number}'
if self.rate is not None and self.rate < 0:
return f'Open-loop / Number {self.number}'
return f'Rate {self.rate} rps / Number {self.number}'

@property
def success_rate(self) -> float:
Expand Down
4 changes: 1 addition & 3 deletions evalscope/perf/utils/report/perf_charts.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,6 @@ def histogram(
# X-axis helper: adapts to open-loop (rate) vs closed-loop (concurrency)
# ---------------------------------------------------------------------------

_RATE_DIR_RE = re.compile(r'^rate_[\d.]+_number_\d+$')


def _x_axis(runs: 'List[RunData]'):
"""Return *(xs, x_title)* appropriate for the run mode.
Expand All @@ -191,7 +189,7 @@ def _x_axis(runs: 'List[RunData]'):
* closed-loop (``parallel_*_number_*`` dirs) → xs = concurrency integers,
x_title = ``'Concurrency'``
"""
is_open = any(_RATE_DIR_RE.match(r.dir_name) for r in runs)
is_open = any(r.is_open_loop for r in runs)
if is_open:
return [r.summary.request_rate for r in runs], 'Rate (req/s)'
return [r.parallel for r in runs], 'Concurrency'
Expand Down
2 changes: 1 addition & 1 deletion evalscope/perf/utils/report/perf_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

# Patterns for recognized run directory names
_PARALLEL_RE = re.compile(r'^parallel_(\d+)_number_(\d+)$')
_RATE_RE = re.compile(r'^rate_([\d.]+)_number_(\d+)$')
_RATE_RE = re.compile(r'^rate_(-?[\d.]+)_number_(\d+)$')


class RunLoader:
Expand Down
80 changes: 80 additions & 0 deletions tests/perf/test_perf_report_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Copyright (c) Alibaba, Inc. and its affiliates.
"""Unit tests for perf HTML-report run discovery and labeling.

Network-free: builds minimal fake run sub-directories on disk and exercises
``RunLoader.load_all`` plus the ``RunData``/chart helpers directly.

Regression for open-loop runs with no fixed rate (``rate_-1.0_number_<M>``),
which were previously dropped by the loader regex so no HTML report was
generated.
"""
import json
import os
import tempfile
import unittest


def _write_json(path: str, obj: object) -> None:
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w', encoding='utf-8') as f:
json.dump(obj, f)


def _make_run(base: str, dir_name: str, *, request_rate: float) -> None:
"""Create a minimal run sub-directory recognized by ``RunLoader``."""
sub = os.path.join(base, dir_name)
_write_json(os.path.join(sub, 'benchmark_summary.json'), {'Request rate': request_rate})
_write_json(os.path.join(sub, 'benchmark_percentile.json'), [])
_write_json(os.path.join(sub, 'benchmark_args.json'), {'api': 'openai'})


class TestPerfReportLoader(unittest.TestCase):

def setUp(self):
self.tmp = tempfile.mkdtemp()
_make_run(self.tmp, 'parallel_2_number_5', request_rate=-1.0)
_make_run(self.tmp, 'rate_1.5_number_4', request_rate=1.5)
_make_run(self.tmp, 'rate_-1.0_number_30', request_rate=-1.0)

def tearDown(self):
import shutil
shutil.rmtree(self.tmp, ignore_errors=True)

def _by_dir(self):
from evalscope.perf.utils.report.perf_data import RunLoader
runs = RunLoader.load_all(self.tmp, with_requests=False)
return {r.dir_name: r for r in runs}

def test_open_loop_no_fixed_rate_is_discovered(self):
"""rate_-1.0_number_30 must be loaded (previously silently dropped)."""
runs = self._by_dir()
self.assertIn('rate_-1.0_number_30', runs)
run = runs['rate_-1.0_number_30']
self.assertTrue(run.is_open_loop)
self.assertEqual(run.name, 'Open-loop / Number 30')

def test_fixed_rate_run_labeled_by_rate(self):
run = self._by_dir()['rate_1.5_number_4']
self.assertTrue(run.is_open_loop)
self.assertEqual(run.name, 'Rate 1.5 rps / Number 4')

def test_closed_loop_run_unaffected(self):
run = self._by_dir()['parallel_2_number_5']
self.assertFalse(run.is_open_loop)
self.assertEqual(run.name, 'Parallel 2 / Number 5')

def test_x_axis_detects_open_loop(self):
from evalscope.perf.utils.report import perf_charts
run = self._by_dir()['rate_-1.0_number_30']
_, x_title = perf_charts._x_axis([run])
self.assertEqual(x_title, 'Rate (req/s)')

def test_x_axis_closed_loop(self):
from evalscope.perf.utils.report import perf_charts
run = self._by_dir()['parallel_2_number_5']
_, x_title = perf_charts._x_axis([run])
self.assertEqual(x_title, 'Concurrency')


if __name__ == '__main__':
unittest.main()
Loading