Benchmark: Add device name to EEVEE tests

Adding the actual GPU device name and host CPU to the test results.
For Cycles the CPU isn't that important when using GPUs as it would
mostly perform the scheduling, however when running EEVEE tests the
CPU is more involved.

This PR will extract the correct GPU device names and adds the CPU name
to a separate identifier. The used CPU is then added to the report, when
found in the results and different than the test device.

Pull Request: https://projects.blender.org/blender/blender/pulls/155419
This commit is contained in:
Jeroen Bakker 2026-03-10 13:04:02 +01:00
parent 257696a8dd
commit b0e2dbcc6a
5 changed files with 88 additions and 19 deletions

View file

@ -30,6 +30,7 @@ class TestEntry:
device_type: str = 'CPU'
device_id: str = 'CPU'
device_name: str = 'Unknown CPU'
device_cpu: str = ''
status: str = 'queued'
# Short, single-line error.
error_msg: str = ''
@ -272,6 +273,9 @@ class TestConfig:
entry.executable = executable
entry.benchmark_type = self.benchmark_type
entry.date = date
entry.device_name = device.name
if device.cpu:
entry.device_cpu = device.cpu
if entry.status in {'done', 'failed'}:
entry.status = 'outdated'
else:
@ -287,6 +291,7 @@ class TestConfig:
device_type=device.type,
device_id=device.id,
device_name=device.name,
device_cpu=device.cpu,
benchmark_type=self.benchmark_type)
entries.append(entry)

View file

@ -4,6 +4,9 @@
import platform
import subprocess
import logging
logger = logging.getLogger(__name__)
def get_cpu_name() -> str:
@ -22,13 +25,13 @@ def get_cpu_name() -> str:
return "Unknown CPU"
def get_gpu_device(args: None) -> list:
def get_gpu_device_cycles(args: None) -> dict:
# Get the list of available Cycles GPU devices.
import bpy
prefs = bpy.context.preferences
if 'cycles' not in prefs.addons.keys():
return []
return {'devices': []}
cprefs = prefs.addons['cycles'].preferences
result = []
@ -45,30 +48,73 @@ def get_gpu_device(args: None) -> list:
if device.type in {"OPTIX"}:
result.append({'type': f"{device.type}-OSL", 'name': device.name, 'index': index})
index += 1
return {'devices': result}
# Get GPU backends
# TODO: Add support for Vulkan device selection even when backend isn't active.
# TODO: Cannot retrieve actual GPU name as the gpu module isn't initialized when run in background mode
# !152683 adds support to use gpu module in background mode.
original_gpu_backend = prefs.system.gpu_backend
for gpu_backend in prefs.system.bl_rna.properties['gpu_backend'].enum_items:
def get_gpu_device_backend(args: dict) -> dict:
import bpy
import gpu
result = []
prefs = bpy.context.preferences
gpu_backend = args['gpu_backend'].upper()
try:
gpu.init()
except AttributeError:
# `gpu.init` has been introduced in March 2026, previous versions are not able to access gpu module and require a fallback.
original_gpu_backend = prefs.system.gpu_backend
try:
prefs.system.gpu_backend = gpu_backend.identifier
prefs.system.gpu_backend = gpu_backend
except TypeError:
# GPU backend isn't available.
pass
else:
result.append({'type': gpu_backend.identifier, 'name': gpu_backend.name})
prefs.system.gpu_backend = original_gpu_backend
result.append({'type': gpu_backend, 'name': gpu_backend})
prefs.system.gpu_backend = original_gpu_backend
else:
if gpu.platform.backend_type_get() == args['gpu_backend'].upper():
result.append({'type': gpu.platform.backend_type_get(), 'name': gpu.platform.renderer_get()})
return {'devices': result}
def get_gpu_devices(env) -> list:
"""
Return a list of devices available in default blender executable.
"""
from api.environment import TestFailure
result = []
try:
cycles_devices, _ = env.run_in_blender(get_gpu_device_cycles, {})
except TestFailure as failure:
logger.error("Unable to receive cycles device list", exc_info=failure)
else:
result += cycles_devices.get('devices', [])
for backend in ['vulkan', 'opengl', 'metal']:
try:
backend_devices, _ = env.run_in_blender(
get_gpu_device_backend, {
'gpu_backend': backend}, gpu_backend=backend)
except TestFailure as failure:
logger.error("Unable to receive device list for '{backend}'", exc_info=failure)
else:
result += backend_devices.get('devices', [])
return result
class TestDevice:
def __init__(self, device_type: str, device_id: str, name: str, operating_system: str):
def __init__(self, device_type: str, device_id: str, name: str, cpu: str, operating_system: str):
self.type = device_type
self.id = device_id
self.name = name
self.cpu = cpu
self.operating_system = operating_system
@ -76,19 +122,20 @@ class TestMachine:
def __init__(self, env, need_gpus: bool):
operating_system = platform.system()
self.devices = [TestDevice('CPU', 'CPU', get_cpu_name(), operating_system),
TestDevice('CPU-OSL', 'CPU-OSL', get_cpu_name(), operating_system)]
device_cpu = get_cpu_name()
self.devices = [TestDevice('CPU', 'CPU', device_cpu, device_cpu, operating_system),
TestDevice('CPU-OSL', 'CPU-OSL', device_cpu, device_cpu, operating_system)]
self.has_gpus = need_gpus
if need_gpus and env.blender_executable:
gpu_devices, _ = env.run_in_blender(get_gpu_device, {})
gpu_devices = get_gpu_devices(env)
for gpu_device in gpu_devices:
device_type = gpu_device['type']
device_name = gpu_device['name']
device_id = device_type
if 'index' in gpu_device:
device_id += "_" + str(gpu_device['index'])
self.devices.append(TestDevice(device_type, device_id, device_name, operating_system))
self.devices.append(TestDevice(device_type, device_id, device_name, device_cpu, operating_system))
def cpu_device(self) -> TestDevice:
return self.devices[0]

View file

@ -36,6 +36,7 @@ class TestGraph:
# Gather used categories.
categories = {}
device_cpu = ''
for entry in device_entries:
category = entry.category
if category in categories.keys():
@ -43,6 +44,9 @@ class TestGraph:
else:
categories[category] = [entry]
if device_cpu == '':
device_cpu = entry.device_cpu
# Sort categories alphabetically.
sorted_categories = sorted(categories.items(), key=lambda item: item[0])
# Generate one graph for every device x category x result key combination.
@ -60,11 +64,11 @@ class TestGraph:
for output in sorted(outputs, reverse=True):
chart_name = f"{category} ({output})"
data.append(self.chart(device_name, chart_name, entries, chart_type, output))
data.append(self.chart(device_name, device_cpu, chart_name, entries, chart_type, output))
self.json = json.dumps(data, indent=2)
def chart(self, device_name: str, chart_name: str, entries: list, chart_type: str, output: str) -> dict:
def chart(self, device_name: str, device_cpu, chart_name: str, entries: list, chart_type: str, output: str) -> dict:
# Gather used tests.
tests = {}
for entry in entries:
@ -128,7 +132,12 @@ class TestGraph:
datasets[test_index]['data'][revision_index] = output_value
data = {'labels': labels, 'datasets': datasets}
return {'device': device_name, 'name': chart_name, 'data': data, 'chart_type': chart_type}
return {
'device': device_name,
'device_cpu': device_cpu,
'name': chart_name,
'data': data,
'chart_type': chart_type}
def write(self, filepath: pathlib.Path) -> None:
# Write HTML page with JSON graph data embedded.

View file

@ -409,6 +409,12 @@
subtitle_h4.textContent = benchmark['device'];
section.appendChild(subtitle_h4);
if (benchmark['device_cpu'] && benchmark['device_cpu'] != benchmark['device']) {
const subtitle_h4 = document.createElement('h4');
subtitle_h4.textContent = benchmark['device_cpu'];
section.appendChild(subtitle_h4);
}
/* Create chart container. */
const chart_container = document.createElement('div');
chart_container.style.position = 'relative';

View file

@ -13,6 +13,7 @@ import api
import argparse
import fnmatch
import glob
import logging
import pathlib
import shutil
import sys
@ -359,6 +360,7 @@ def cmd_graph(argv: list):
def main():
logging.basicConfig()
usage = ('benchmark <command> [<args>]\n'
'\n'
'Commands:\n'