404 lines
14 KiB
Python
Executable File
404 lines
14 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import datetime
|
|
import json
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
|
|
SCRIPT_PATH = os.path.realpath(__file__)
|
|
SCRIPT_DIR = os.path.dirname(SCRIPT_PATH)
|
|
|
|
RESULT_JSON_SCHEMA_VERSION = 2
|
|
|
|
import foundation
|
|
from foundation import (benchrunner,
|
|
buildbot,
|
|
config,
|
|
context,
|
|
logger,
|
|
system_info,
|
|
util)
|
|
|
|
|
|
########################################
|
|
# Parser helpers.
|
|
########################################
|
|
|
|
|
|
def configureArgumentParser():
|
|
parser = argparse.ArgumentParser(
|
|
description="Cycles benchmark helper script.")
|
|
parser.add_argument("-b", "--blender",
|
|
help="Full file path to Blender's binary " +
|
|
"to use for rendering",
|
|
default="")
|
|
parser.add_argument("-d", "--scenes-dir",
|
|
help="Directory with scenes",
|
|
default="")
|
|
parser.add_argument('-c', '--scenes',
|
|
nargs='+',
|
|
help='Scenes to be rendered',
|
|
default=[])
|
|
parser.add_argument('-t', '--device-type',
|
|
help="Type of the device to render on",
|
|
default="")
|
|
parser.add_argument('-n', '--device-name',
|
|
help="Device name to render on",
|
|
default="")
|
|
parser.add_argument('-o', '--device-name-override',
|
|
help="Override compute device name with this",
|
|
default="")
|
|
parser.add_argument('-s', '--device-single',
|
|
help="Use single device when multiple matches",
|
|
action='store_true',
|
|
default=False)
|
|
parser.add_argument('-v', '--verbose',
|
|
help="Do verbose logging",
|
|
action='store_true',
|
|
default=False)
|
|
parser.add_argument('-i', '--system-info',
|
|
help="Only dump system info to the screen",
|
|
action='store_true',
|
|
default=False)
|
|
return parser
|
|
|
|
########################################
|
|
# Configuration helpers.
|
|
########################################
|
|
|
|
|
|
def injectDefaultConfiguration(config):
|
|
"""
|
|
For a specified configuration object, set all possible properties to their
|
|
default value.
|
|
"""
|
|
root_dir = util.getBundleRootDirectory()
|
|
section = {
|
|
"scenes": "",
|
|
"scenes_dir": os.path.join(root_dir, "scenes"),
|
|
"device_name": "",
|
|
"device_single": "False",
|
|
"device_name_override": "",
|
|
}
|
|
config['farm'] = section
|
|
|
|
|
|
def injectArgparseConfiguration(config, args):
|
|
"""
|
|
Override settings wit harguments passed from the command line.
|
|
"""
|
|
section = config['farm']
|
|
if args.blender:
|
|
section['blender'] = args.blender
|
|
if args.scenes:
|
|
section['scenes'] = ",".join(args.scenes)
|
|
if args.scenes_dir:
|
|
section['scenes_dir'] = args.scenes_dir
|
|
if args.device_type:
|
|
section['device_type'] = args.device_type
|
|
if args.device_name:
|
|
section['device_name'] = args.device_name
|
|
if args.device_name_override:
|
|
section['device_name_override'] = args.device_name_override
|
|
if args.device_single:
|
|
section['device_single'] = args.device_single
|
|
|
|
|
|
def readConfiguration(args):
|
|
"""
|
|
Read configuration file and return BenchmarkConfig with all the settings
|
|
we will need to use.
|
|
"""
|
|
config = foundation.config.BenchmarkConfig()
|
|
injectDefaultConfiguration(config)
|
|
read_configs = config.readGlobalConfig("farm")
|
|
if read_configs:
|
|
logger.INFO("Configuration was read from:")
|
|
for cfg in read_configs:
|
|
print(" " + util.stripSensitiveInfo(cfg))
|
|
injectArgparseConfiguration(config, args)
|
|
return config
|
|
|
|
|
|
def checkConfiguration(config):
|
|
"""
|
|
Check whether configuration is complete and usable.
|
|
"""
|
|
required_keys = ('device_type', )
|
|
known_device_types = ('CPU', 'GPU', 'CUDA', 'OPENCL', )
|
|
logger.INFO("Validating configuration...")
|
|
# Check whether section exists.
|
|
if 'farm' not in config.sections():
|
|
logger.INFO(" Missing configuration section for 'farm'.")
|
|
return False
|
|
section = config['farm']
|
|
# Check whether required keys exists.
|
|
for required_key in required_keys:
|
|
if required_key not in section:
|
|
logger.INFO(" Missing configuration key {}" .
|
|
format(required_key))
|
|
return False
|
|
# Check whether device configuration is correct.
|
|
device_type = section['device_type']
|
|
if device_type not in known_device_types:
|
|
logger.INFO(" Unknown device type {}" . format(device_type))
|
|
return False
|
|
if device_type != 'CPU':
|
|
if 'device_name' not in section or not section['device_name']:
|
|
logger.INFO(" Need to explicitly specify device name.")
|
|
return False
|
|
# Check whether directories are correct.
|
|
if not os.path.exists(section["scenes_dir"]):
|
|
logger.INFO(" Scenes directory does not exist.")
|
|
return False
|
|
if "output_dir" not in section:
|
|
logger.INFO(" Missing configuration for output directory.")
|
|
return False
|
|
return True
|
|
|
|
|
|
########################################
|
|
# Temporary directories helpers.
|
|
########################################
|
|
|
|
def createTempDirectory(config):
|
|
"""
|
|
Usually creates an unique temporary directory and returns full path to it.
|
|
|
|
In the development environment uses pre-defined directory, where all data
|
|
might already be existing (for example, blender might be already downloaded
|
|
and unpacked).
|
|
"""
|
|
temp_dir = tempfile.mkdtemp(prefix="blender-benchmark-")
|
|
return temp_dir
|
|
|
|
|
|
def deleteTempDirectory(config, temp_dir):
|
|
"""
|
|
Get rid of automatically created temp directory/
|
|
"""
|
|
shutil.rmtree(temp_dir)
|
|
|
|
|
|
########################################
|
|
# Latets Blender helper script.
|
|
########################################
|
|
|
|
def downloadLatestBlender(directory):
|
|
"""
|
|
Download latets Blender from buildbot to given directory.
|
|
"""
|
|
platform = sys.platform
|
|
latest_blender_url = buildbot.buildbotGetLatetsVersion(platform, "64bit")
|
|
if not latest_blender_url:
|
|
logger.ERROR("Unable to figure out latest Blender version")
|
|
return
|
|
blender_filename = latest_blender_url.split('/')[-1]
|
|
logger.INFO("Found latest Blender: {}" . format(blender_filename))
|
|
local_blender = os.path.join(directory, blender_filename)
|
|
logger.INFO("Downloading Blender...")
|
|
util.downloadFile(latest_blender_url, local_blender)
|
|
return local_blender
|
|
|
|
|
|
def findBlenderDirInDirectory(directory):
|
|
"""
|
|
Find Blender directory in given folder.
|
|
"""
|
|
for filename in os.listdir(directory):
|
|
if filename.startswith("blender"):
|
|
full_filename = os.path.join(directory, filename)
|
|
if os.path.isdir(full_filename):
|
|
return full_filename
|
|
return None
|
|
|
|
|
|
def findBlenderInDirectory(directory):
|
|
"""
|
|
Find blender executable file in given directory. Will recurse into
|
|
Blender directory in there
|
|
"""
|
|
blender_dir = findBlenderDirInDirectory(directory)
|
|
platform = sys.platform
|
|
if platform == 'linux':
|
|
return os.path.join(blender_dir, "blender")
|
|
elif platform == "win32":
|
|
return os.path.join(blender_dir, "blender.exe")
|
|
else:
|
|
raise Exception("Need to support your OS!")
|
|
|
|
|
|
def getLatetsBlenderBinary(config, temp_dir):
|
|
"""
|
|
Get full file path to latest Blender executable which will be used for
|
|
actual benchmark.
|
|
|
|
It will either use Blender from already specified location or it will
|
|
download latest Blender from buildbot.
|
|
"""
|
|
# Firts try to use Blender specified in configuration.
|
|
if 'blender' in config['farm']:
|
|
return config['farm']['blender']
|
|
# Well, download and unpack the latest Blender from buildbot.
|
|
logger.INFO("Will get latest Blender from buildbot.")
|
|
blender_archive = downloadLatestBlender(temp_dir)
|
|
logger.INFO("Unpacking Blender...")
|
|
util.unpackArchive(blender_archive, temp_dir)
|
|
return findBlenderInDirectory(temp_dir)
|
|
|
|
########################################
|
|
# Results output.
|
|
########################################
|
|
|
|
|
|
def latestDirGet(path):
|
|
"""
|
|
Get directory with bigger number in the given path
|
|
"""
|
|
max_file = None
|
|
for f in os.listdir(path):
|
|
if f.endswith(".DS_Store"):
|
|
continue
|
|
x = int(f)
|
|
if not max_file or x > max_file:
|
|
max_file = x
|
|
return max_file
|
|
|
|
|
|
def ensureOutputDir(config):
|
|
output_dir = config['farm']['output_dir']
|
|
if not os.path.exists(output_dir):
|
|
os.makedirs(output_dir)
|
|
max_dir = latestDirGet(output_dir)
|
|
if not max_dir:
|
|
max_dir = 0
|
|
new_dir = str(max_dir + 1).zfill(8)
|
|
new_full_dir = os.path.join(output_dir, new_dir)
|
|
os.mkdir(new_full_dir)
|
|
return new_full_dir
|
|
|
|
|
|
def ensureImageOutputDir(results_output_dir):
|
|
images_output_dir = os.path.join(results_output_dir, "images")
|
|
os.mkdir(images_output_dir)
|
|
return images_output_dir
|
|
|
|
|
|
def getResultJSONString(ctx, results):
|
|
# Convert custom classes to dictionaries for easier JSON dump.
|
|
results['schema_version'] = RESULT_JSON_SCHEMA_VERSION
|
|
stats = results['stats']
|
|
for scene in ctx.scenes:
|
|
if scene not in stats:
|
|
continue
|
|
if stats[scene]:
|
|
stats[scene] = stats[scene].asDict()
|
|
stats[scene]['result'] = 'OK'
|
|
else:
|
|
stats[scene] = {'result': 'CRASH'}
|
|
return json.dumps(results, sort_keys=True, indent=2)
|
|
|
|
|
|
def saveResults(ctx, results, output_dir):
|
|
json_string = getResultJSONString(ctx, results)
|
|
results_file = os.path.join(output_dir, "results.json")
|
|
with open(results_file, "w") as f:
|
|
f.write(json_string)
|
|
|
|
########################################
|
|
# Main logic.
|
|
########################################
|
|
|
|
|
|
def main():
|
|
parser = configureArgumentParser()
|
|
args = parser.parse_args()
|
|
logger.VERBOSE = args.verbose
|
|
logger.init()
|
|
logger.HEADER("Cycles Benchmark Suite v{}, farm edition" .
|
|
format(foundation.VERSION))
|
|
# Some platform independent directories to helper scripts.
|
|
script_directory = os.path.dirname(os.path.realpath(__file__))
|
|
configure_script = os.path.join(script_directory, "configure.py")
|
|
# Read configuration file, so we know what we will be doing.
|
|
config = readConfiguration(args)
|
|
if not checkConfiguration(config):
|
|
logger.ERROR("Configuration is not complete or valid, aborting.")
|
|
return False
|
|
logger.INFO("Configuration looks reasonable, continuing.")
|
|
# Create temporary directory, all runtime files will be stored there.
|
|
temp_dir = createTempDirectory(config)
|
|
results_output_dir = ensureOutputDir(config)
|
|
images_output_dir = ensureImageOutputDir(results_output_dir)
|
|
try:
|
|
blender_binary = getLatetsBlenderBinary(config, temp_dir)
|
|
logger.INFO("Will benchmark the following Blender: {}".
|
|
format(util.stripSensitiveInfo(blender_binary)))
|
|
# Bechmark context initialization.
|
|
farm_config = config['farm']
|
|
ctx = context.Context()
|
|
ctx.blender = blender_binary
|
|
ctx.configure_script = configure_script
|
|
ctx.scenes_dir = farm_config['scenes_dir']
|
|
ctx.device_type = farm_config['device_type']
|
|
ctx.device_name = farm_config['device_name']
|
|
if farm_config['device_single'].lower() == "true":
|
|
ctx.device_single = True
|
|
else:
|
|
ctx.device_single = False
|
|
ctx.image_output_dir = images_output_dir
|
|
# Don't do any scenes when only querying for system information.
|
|
if args.system_info:
|
|
ctx.scenes = []
|
|
else:
|
|
if farm_config['scenes']:
|
|
ctx.scenes = farm_config['scenes'].split(",")
|
|
else:
|
|
ctx.scenes = ctx.listAllScenes(ctx.scenes_dir)
|
|
# Print additional informaiton.
|
|
if farm_config["device_name_override"]:
|
|
print("Using device name override: {}" .
|
|
format(farm_config["device_name_override"]))
|
|
# Print prelmiinary information.
|
|
blender_device_info = benchrunner.benchmarkGetDeviceInfo(ctx)
|
|
if not blender_device_info['device_type']:
|
|
logger.ERROR("Requested device can not be enabled in Blender.")
|
|
logger.INFO("Requested device details:")
|
|
logger.INFO(" Device type: {}" . format(ctx.device_type))
|
|
logger.INFO(" Device name: {}" . format(ctx.device_name))
|
|
all_stats = None
|
|
else:
|
|
logger.INFO("Configured device details:")
|
|
logger.INFO(" Device type: {}" .
|
|
format(blender_device_info["device_type"]))
|
|
logger.INFO(" Compute devices:")
|
|
for compute_device in blender_device_info["compute_devices"]:
|
|
logger.INFO(" {}" . format(compute_device))
|
|
# Run benchmark.
|
|
all_stats = benchrunner.benchmarkAll(ctx)
|
|
# Gather all information together.
|
|
timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
results = {
|
|
"timestamp": timestamp,
|
|
"blender_version": system_info.getBlenderVersion(ctx),
|
|
"system_info": system_info.gatherSystemInfo(ctx),
|
|
"device_info": blender_device_info,
|
|
"stats": all_stats if all_stats else {}
|
|
}
|
|
if farm_config["device_name_override"]:
|
|
results["device_name_override"] = farm_config["device_name_override"]
|
|
saveResults(ctx, results, results_output_dir)
|
|
return all_stats is not None
|
|
finally:
|
|
if not args.system_info:
|
|
deleteTempDirectory(config, temp_dir)
|
|
return True
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if not main():
|
|
sys.exit(1)
|