Add Chromium-only Blender WebEngine parity work
This commit is contained in:
290
blender-5.2.0/tests/performance/api/bisect.py
Normal file
290
blender-5.2.0/tests/performance/api/bisect.py
Normal file
@@ -0,0 +1,290 @@
|
||||
# SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from collections.abc import Callable
|
||||
|
||||
from .environment import TestEnvironment
|
||||
from .test import Test
|
||||
|
||||
|
||||
def date_str(ts: int) -> str:
|
||||
"""Format a Unix timestamp as 'YYYY-MM-DD HH:MM:SS' in UTC."""
|
||||
return datetime.datetime.fromtimestamp(ts, tz=datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
|
||||
def passes_threshold(value: float, success: str, threshold: float) -> bool:
|
||||
"""Check if a performance value passes the given threshold.
|
||||
|
||||
Returns True when the value is on the good side of the threshold
|
||||
based on the success direction ('greater_than' or 'less_than').
|
||||
"""
|
||||
if success == 'greater_than':
|
||||
return value > threshold
|
||||
elif success == 'less_than':
|
||||
return value < threshold
|
||||
return False
|
||||
|
||||
|
||||
class BisectProgress:
|
||||
"""Tracks the current search window during bisecting."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.min_index = 0
|
||||
self.max_index = 0
|
||||
|
||||
@property
|
||||
def remaining(self) -> int:
|
||||
return self.max_index - self.min_index
|
||||
|
||||
|
||||
class Bisect:
|
||||
"""
|
||||
Bisect over a commit range.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
env: TestEnvironment,
|
||||
test_commit_cb: Callable[..., tuple[float | None, str]],
|
||||
start_ts: int,
|
||||
end_ts: int,
|
||||
) -> None:
|
||||
self.env = env
|
||||
self.test_commit_cb = test_commit_cb
|
||||
self.start_ts = start_ts
|
||||
self.end_ts = end_ts
|
||||
self.commit_status: dict[str, str] = {}
|
||||
self.last_good: str | None = None
|
||||
self.first_bad: str | None = None
|
||||
|
||||
def run(
|
||||
self,
|
||||
progress: BisectProgress | None,
|
||||
) -> None:
|
||||
self._run_per_day(progress)
|
||||
if self.first_bad is not None:
|
||||
self._run_single_day(progress)
|
||||
|
||||
def _run_per_day(
|
||||
self,
|
||||
progress: BisectProgress | None,
|
||||
) -> None:
|
||||
"""
|
||||
Walks day-by-day trying up to three commits per day to quickly locate the good/bad commit.
|
||||
"""
|
||||
SECONDS_PER_DAY = 86400
|
||||
|
||||
day_windows: list[list[tuple[str, int]]] = []
|
||||
day_ts = self.start_ts
|
||||
while day_ts < self.end_ts:
|
||||
next_day_ts = day_ts + SECONDS_PER_DAY
|
||||
day_windows.append(self.env.commits_in_window(day_ts, next_day_ts))
|
||||
day_ts = next_day_ts
|
||||
|
||||
total_commits = sum(len(w) for w in day_windows)
|
||||
self._update_progress(progress, 0, total_commits)
|
||||
|
||||
day_index = 0
|
||||
consumed = 0
|
||||
last_tested = None
|
||||
while day_index < len(day_windows):
|
||||
day_commits = day_windows[day_index]
|
||||
self._update_progress(progress, consumed, total_commits)
|
||||
consumed += len(day_commits)
|
||||
|
||||
attempts = 0
|
||||
for commit_hash, commit_ts in day_commits:
|
||||
if commit_hash == last_tested:
|
||||
continue
|
||||
if attempts >= 3:
|
||||
break
|
||||
if commit_hash in self.commit_status:
|
||||
break
|
||||
|
||||
attempts += 1
|
||||
_, status = self.test_commit_cb(commit_hash, commit_ts)
|
||||
if status in {'build_error', 'no_output', 'run_error', 'skip'}:
|
||||
continue
|
||||
|
||||
if status == 'pass':
|
||||
self.last_good = commit_hash
|
||||
self.commit_status[commit_hash] = 'pass'
|
||||
else:
|
||||
self.first_bad = commit_hash
|
||||
self.commit_status[commit_hash] = 'fail'
|
||||
last_tested = commit_hash
|
||||
break
|
||||
|
||||
if self.first_bad:
|
||||
break
|
||||
day_index += 1
|
||||
|
||||
def _run_single_day(
|
||||
self,
|
||||
progress: BisectProgress | None,
|
||||
) -> None:
|
||||
"""
|
||||
Narrows down to the exact commit with a binary search.
|
||||
"""
|
||||
all_commits = self.env.commits_in_window(self.start_ts, self.end_ts)
|
||||
commit_index = {commit_hash: index for index, (commit_hash, _) in enumerate(all_commits)}
|
||||
max_index = commit_index[self.first_bad]
|
||||
min_index = commit_index[self.last_good] + 1 if self.last_good else 0
|
||||
self._update_progress(progress, min_index, max_index)
|
||||
|
||||
while min_index < max_index:
|
||||
mid_index = (min_index + max_index) // 2
|
||||
commit_hash, commit_ts = all_commits[mid_index]
|
||||
|
||||
if commit_hash in self.commit_status:
|
||||
if self.commit_status[commit_hash] == 'pass':
|
||||
min_index = mid_index + 1
|
||||
else:
|
||||
max_index = mid_index
|
||||
self._update_progress(progress, min_index, max_index)
|
||||
continue
|
||||
|
||||
_, status = self.test_commit_cb(commit_hash, commit_ts)
|
||||
|
||||
if status == 'pass':
|
||||
self.commit_status[commit_hash] = 'pass'
|
||||
self.last_good = commit_hash
|
||||
min_index = mid_index + 1
|
||||
elif status == 'fail':
|
||||
self.commit_status[commit_hash] = 'fail'
|
||||
self.first_bad = commit_hash
|
||||
max_index = mid_index
|
||||
else:
|
||||
_, new_max = self._forward_scan(mid_index + 1, max_index, all_commits, progress)
|
||||
if new_max is None:
|
||||
break
|
||||
max_index = new_max
|
||||
self._update_progress(progress, min_index, max_index)
|
||||
continue
|
||||
|
||||
self._update_progress(progress, min_index, max_index)
|
||||
|
||||
def _forward_scan(
|
||||
self,
|
||||
start_index: int,
|
||||
max_index: int,
|
||||
commits: list[tuple[str, int]],
|
||||
progress: BisectProgress | None,
|
||||
) -> tuple[int | None, int | None]:
|
||||
for scan_index in range(start_index, max_index):
|
||||
scan_hash, scan_ts = commits[scan_index]
|
||||
if scan_hash in self.commit_status:
|
||||
if self.commit_status[scan_hash] == 'fail':
|
||||
self.first_bad = scan_hash
|
||||
self._update_progress(progress, start_index, scan_index)
|
||||
return start_index, scan_index
|
||||
continue
|
||||
_, status = self.test_commit_cb(scan_hash, scan_ts)
|
||||
if status == 'pass':
|
||||
self.commit_status[scan_hash] = 'pass'
|
||||
self.last_good = scan_hash
|
||||
self._update_progress(progress, scan_index + 1, max_index)
|
||||
return scan_index + 1, max_index
|
||||
elif status == 'fail':
|
||||
self.commit_status[scan_hash] = 'fail'
|
||||
self.first_bad = scan_hash
|
||||
self._update_progress(progress, start_index, scan_index)
|
||||
return start_index, scan_index
|
||||
return None, None
|
||||
|
||||
@staticmethod
|
||||
def run_commit(
|
||||
env: TestEnvironment,
|
||||
test: Test,
|
||||
device_id: str,
|
||||
gpu_backend: str,
|
||||
count: int,
|
||||
attribute: str,
|
||||
success: str,
|
||||
threshold: float,
|
||||
tested: set[str],
|
||||
on_progress: Callable[..., None],
|
||||
commit_hash: str,
|
||||
commit_ts: int,
|
||||
) -> tuple[float | None, str]:
|
||||
"""Build, benchmark, and evaluate a single commit.
|
||||
|
||||
Builds the given git hash, runs the test ``count`` times, averages the
|
||||
results, and checks whether the value passes the threshold.
|
||||
|
||||
Args:
|
||||
env: TestEnvironment for git/build operations.
|
||||
test: Test object to run.
|
||||
device_id: Device identifier string.
|
||||
gpu_backend: GPU backend string.
|
||||
count: Number of benchmark runs per commit.
|
||||
attribute: Name of the performance attribute to extract from output.
|
||||
success: Comparison direction ('greater_than' or 'less_than').
|
||||
threshold: Pass/fail threshold value.
|
||||
tested: Mutable set tracking already-tested commit hashes.
|
||||
on_progress: Callable ``(row_values, end)`` for printing table rows.
|
||||
commit_hash: Commit hash to test.
|
||||
commit_ts: Unix timestamp of the commit.
|
||||
|
||||
Returns:
|
||||
Tuple ``(value, status)`` where status is one of
|
||||
``'skip'``, ``'build_error'``, ``'no_output'``, ``'run_error``, ``'pass'`` or ``'fail'``.
|
||||
value can be None when status is ``'skip'``, ``'build_error'``, ``'no_output'``, ``'run_error'``.
|
||||
"""
|
||||
# During the weekends it can happen that a day doesn't have any commit. In that case a commit
|
||||
# can be selected that has already been performed.
|
||||
if commit_hash in tested:
|
||||
return None, 'skip'
|
||||
tested.add(commit_hash)
|
||||
|
||||
title = env.commit_title(commit_hash)[:70].replace('`', '\'')
|
||||
title = f'`{title}`'
|
||||
commit_hash_str = f'`{commit_hash}`'
|
||||
on_progress([commit_hash_str, date_str(commit_ts), title, '', 'building'], end='\r')
|
||||
|
||||
install_dir = env.install_dir
|
||||
ok = env.build(commit_hash, install_dir)
|
||||
if not ok:
|
||||
on_progress([commit_hash_str, date_str(commit_ts), title, 'error', 'FAIL (build)'])
|
||||
return None, 'build_error'
|
||||
|
||||
env.set_blender_executable(install_dir, {})
|
||||
|
||||
values: list[float] = []
|
||||
try:
|
||||
for run_idx in range(count):
|
||||
run_status = 'running' if count == 1 else f'run [{run_idx + 1}/{count}]'
|
||||
on_progress([commit_hash_str, date_str(commit_ts), title, '', run_status], end='\r')
|
||||
output = test.run(env, device_id, gpu_backend)
|
||||
if not output or attribute not in output:
|
||||
env.set_default_blender_executable()
|
||||
on_progress([commit_hash_str, date_str(commit_ts), title, 'error', 'run'])
|
||||
return None, 'no_output'
|
||||
values.append(output[attribute])
|
||||
except Exception as e:
|
||||
env.set_default_blender_executable()
|
||||
on_progress([commit_hash_str, date_str(commit_ts), title, 'error', str(e)[:30]])
|
||||
return None, 'run_error'
|
||||
|
||||
env.set_default_blender_executable()
|
||||
avg = sum(values) / len(values)
|
||||
|
||||
good = passes_threshold(avg, success, threshold)
|
||||
status = 'PASS' if good else 'FAIL'
|
||||
on_progress([commit_hash_str, date_str(commit_ts), title, f'{avg:.4f}', status])
|
||||
return avg, 'pass' if good else 'fail'
|
||||
|
||||
@staticmethod
|
||||
def _update_progress(
|
||||
progress: BisectProgress | None,
|
||||
min_index: int,
|
||||
max_index: int,
|
||||
) -> None:
|
||||
if progress:
|
||||
progress.min_index = min_index
|
||||
progress.max_index = max_index
|
||||
Reference in New Issue
Block a user