Skip to content
Snippets Groups Projects
cycles_timeit.py 6.54 KiB
#!/usr/bin/env python3

import argparse
import re
import shutil
import subprocess
import sys
import time

class COLORS:
    HEADER = '\033[95m'
    OKBLUE = '\033[94m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'

VERBOSE = False

#########################################
# Generic helper functions.

def logVerbose(*args):
    if VERBOSE:
        print(*args)


def logHeader(*args):
    print(COLORS.HEADER + COLORS.BOLD, end="")
    print(*args, end="")
    print(COLORS.ENDC)


def logWarning(*args):
    print(COLORS.WARNING + COLORS.BOLD, end="")
    print(*args, end="")
    print(COLORS.ENDC)


def logOk(*args):
    print(COLORS.OKGREEN + COLORS.BOLD, end="")
    print(*args, end="")
    print(COLORS.ENDC)


def progress(count, total, prefix="", suffix=""):
    if VERBOSE:
        return

    size = shutil.get_terminal_size((80, 20))

    if prefix != "":
        prefix = prefix + "    "
    if suffix != "":
        suffix = "    " + suffix

    bar_len = size.columns - len(prefix) - len(suffix) - 10
    filled_len = int(round(bar_len * count / float(total)))

    percents = round(100.0 * count / float(total), 1)
    bar = '=' * filled_len + '-' * (bar_len - filled_len)

    sys.stdout.write('%s[%s] %s%%%s\r' % (prefix, bar, percents, suffix))
    sys.stdout.flush()


def progressClear():
    if VERBOSE:
        return

    size = shutil.get_terminal_size((80, 20))
    sys.stdout.write(" " * size.columns + "\r")
    sys.stdout.flush()


def humanReadableTimeDifference(seconds):
    hours = int(seconds) // 60 // 60
    seconds = seconds - hours * 60 * 60
    minutes = int(seconds) // 60
    seconds = seconds - minutes * 60
    if hours == 0:
        return "%02d:%05.2f" % (minutes, seconds)
    else:
        return "%02d:%02d:%05.2f" % (hours, minutes, seconds)


def humanReadableTimeToSeconds(time):
    tokens = time.split(".")
    result = 0
    if len(tokens) == 2:
        result = float("0." + tokens[1])
    mult = 1
    for token in reversed(tokens[0].split(":")):
        result += int(token) * mult
        mult *= 60
    return result

#########################################
# Benchmark specific helper functions.

def configureArgumentParser():
    parser = argparse.ArgumentParser(
        description="Cycles benchmark helper script.")
    parser.add_argument("-b", "--binary",
                        help="Full file path to Blender's binary " +
                             "to use for rendering",
                        default="blender")
    parser.add_argument("-f", "--files", nargs='+')
    parser.add_argument("-v", "--verbose",
                        help="Perform fully verbose communication",
                        action="store_true",
                        default=False)
    return parser


def benchmarkFile(blender, blendfile, stats):
    logHeader("Begin benchmark of file {}" . format(blendfile))
    # Prepare some regex for parsing
    re_path_tracing = re.compile(".*Path Tracing Tile ([0-9]+)/([0-9]+)$")
    re_total_render_time = re.compile(".*Total render time: ([0-9]+(\.[0-9]+)?)")
    re_render_time_no_sync = re.compile(
        ".*Render time \(without synchronization\): ([0-9]+(\.[0-9]+)?)")
    re_pipeline_time = re.compile("Time: ([0-9:\.]+) \(Saving: ([0-9:\.]+)\)")
    # Prepare output folder.
    # TODO(sergey): Use some proper output folder.
    output_folder = "/tmp/"
    # Configure command for the current file.
    command = (blender,
               "--background",
               "-noaudio",
               "--factory-startup",
               blendfile,
               "--engine", "CYCLES",
               "--debug-cycles",
               "--render-output", output_folder,
               "--render-format", "PNG",
               "-f", "1")
    # Run Blender with configured command line.
    logVerbose("About to execuet command: {}" . format(command))
    start_time = time.time()
    process = subprocess.Popen(command,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.STDOUT)
    # Keep reading status while Blender is alive.
    total_render_time = "N/A"
    render_time_no_sync = "N/A"
    pipeline_render_time = "N/A"
    while True:
        line = process.stdout.readline()
        if line == b"" and process.poll() is not None:
            break
        line = line.decode().strip()
        if line == "":
            continue
        logVerbose("Line from stdout: {}" . format(line))
        match = re_path_tracing.match(line)
        if match:
            current_tiles = int(match.group(1))
            total_tiles = int(match.group(2))
            elapsed_time = time.time() - start_time
            elapsed_time_str = humanReadableTimeDifference(elapsed_time)
            progress(current_tiles,
                     total_tiles,
                     prefix="Path Tracing Tiles {}" . format(elapsed_time_str))
        match = re_total_render_time.match(line)
        if match:
            total_render_time = float(match.group(1))
        match = re_render_time_no_sync.match(line)
        if match:
            render_time_no_sync = float(match.group(1))
        match = re_pipeline_time.match(line)
        if match:
            pipeline_render_time = humanReadableTimeToSeconds(match.group(1))

    if process.returncode != 0:
        return False

    # Clear line used by progress.
    progressClear()
    print("Total pipeline render time: {} ({} sec)"
          . format(humanReadableTimeDifference(pipeline_render_time),
                   pipeline_render_time))
    print("Total Cycles render time: {} ({} sec)"
          . format(humanReadableTimeDifference(total_render_time),
                   total_render_time))
    print("Pure Cycles render time (without sync): {} ({} sec)"
          . format(humanReadableTimeDifference(render_time_no_sync),
                   render_time_no_sync))
    logOk("Successfully rendered")
    stats[blendfile] = {'PIPELINE_TOTAL': pipeline_render_time,
                        'CYCLES_TOTAL': total_render_time,
                        'CYCLES_NO_SYNC': render_time_no_sync}
    return True


def benchmarkAll(blender, files):
    stats = {}
    for blendfile in files:
        try:
            benchmarkFile(blender, blendfile, stats)
        except KeyboardInterrupt:
            print("")
            logWarning("Rendering aborted!")
            return


def main():
    parser = configureArgumentParser()
    args = parser.parse_args()
    if args.verbose:
        global VERBOSE
        VERBOSE = True
    benchmarkAll(args.binary, args.files)


if __name__ == "__main__":
    main()