#!/usr/local/bin/python2
#
#  win_repo_bench.py: run repository / server performance tests on Windows.
#
#  Subversion is a tool for revision control.
#  See http://subversion.apache.org for more information.
#
# ====================================================================
#    Licensed to the Apache Software Foundation (ASF) under one
#    or more contributor license agreements.  See the NOTICE file
#    distributed with this work for additional information
#    regarding copyright ownership.  The ASF licenses this file
#    to you under the Apache License, Version 2.0 (the
#    "License"); you may not use this file except in compliance
#    with the License.  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing,
#    software distributed under the License is distributed on an
#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
#    KIND, either express or implied.  See the License for the
#    specific language governing permissions and limitations
#    under the License.
######################################################################

# General modules
import os
import shutil
import sys
import subprocess
import time

from win32com.shell import shell, shellcon

# Adapt these paths to your needs

# Contains all the REPOSITORIES
repo_parent = "C:\\repos"

# Where to create working copies
wc_path = "C:\\wc"
exe_path = "C:\\develop\\Subversion\\trunk\\Release"
apache_path = "C:\\develop\\Subversion"

# Test these repositories and in this order.
# Actual repository names have numbers 0 .. REPETITIONS-1 append to them
repositories = ["ruby-f6-nonpacked", "ruby-f7-nonpacked",
                "ruby-f6-packed",    "ruby-f7-packed",
                "bsd-f6-nonpacked",  "bsd-f7-nonpacked",
                "bsd-f6-packed",     "bsd-f7-packed"]

# Basically lists the RA backends to test but as long as all repositories
# can be accessed using any of them, arbitrary URLs are possible.
prefixes = ["svn://localhost/", "http://localhost/svn/", "file:///C:/repos/"]

# Number of time to repeat the tests. For each iteration, there must be
# a separate copy of all repositories.
repetitions = 3

# Server configurations to test
configurations = ['slow', 'medium', 'fast']
svnserve_params = {
  'slow':"",
  'medium':"-M 256" ,
  'fast':"-M 1024 -c 0 --cache-revprops yes --block-read yes --client-speed 1000"
}


def clear_memory():
  """ Clear in-RAM portion of the file / disk cache """
  subprocess.call(["ClearMemory.exe"])

def start_server(prefix, config):
  """ Depending on the url PREFIX, start the corresponding server with the
      given CONFIGuration.  file: and http: access will actually have been
      configured by set_config(). """

  if prefix[:4] == "svn:":
    exe = os.path.join(exe_path, "svnserve.exe")
    command = "cmd.exe /c start " + exe + " -dr " + repo_parent + \
              " " + svnserve_params[config]
    subprocess.call(command)
    time.sleep(2)
  elif prefix[:5] == "http:":
    exe = os.path.join(apache_path, 'bin', 'httpd.exe')
    subprocess.call(exe + " -k start")
    time.sleep(2)

def stop_server(prefix):
  """ Depending on the url PREFIX, stop / kill the corresponding server. """

  if prefix[:4] == "svn:":
    subprocess.call("cmd.exe /c taskkill /im svnserve.exe /f > nul 2>&1")
    time.sleep(1)
  elif prefix[:5] == "http:":
    exe = os.path.join(apache_path, 'bin', 'httpd.exe')
    subprocess.call(exe + " -k stop")
    time.sleep(1)

def run_cs_command(state, config, repository, prefix, args):
  """ Run the client-side command given in ARGS.  Log the STATE of the
      caches, the CONFIG we are using, the REPOSITORY, the url PREFIX
      and finally the execution times. """

  # Make sure we can create a new working copy if we want to.
  if os.path.exists(wc_path):
    shutil.rmtree(wc_path)

  # Select the client to use.
  if ('null-export' in args) or ('null-log' in args):
    exe = os.path.join(exe_path, "svn-bench.exe")
  else:
    exe = os.path.join(exe_path, "svn.exe")

  # Display the operation
  repo_title = repository.replace('nonpacked', 'nopack')
  print state, "\t", repo_title, "\t", prefix, "\t", config, "\t",
  sys.stdout.flush()

  # Execute the command and show the execution times
  subprocess.call(["TimeWin.exe", exe] + args)


def run_test_cs_sequence(config, repository, run, prefix, command, args):
  """ Run the client-side COMMAND with the given ARGS in various stages
      of cache heat-up.  Execute the test with server CONFIG on REPOSITORY
      with the given url PREFIX. """

  # Build the full URL to use.  Exports operate on the main dev line only.
  url = prefix + repository + str(run)
  if (command == 'export') or (command == 'null-export'):
    if repository[:3] == 'bsd':
      url += '/head'
    else:
      url += '/trunk'

  # Full set of command arguments
  args = [command, url] + args

  # Free up caches best we can.
  clear_memory()

  # Caches are quite cool now and ready to take up new data
  start_server(prefix, config)
  run_cs_command("Cold", config, repository, prefix, args)
  stop_server(prefix)

  # OS caches are quite hot now.
  # Run operation from hot OS caches but cold SVN caches.
  start_server(prefix, config)
  run_cs_command("WarmOS", config, repository, prefix, args)
  stop_server(prefix)

  # OS caches may be even hotter now.
  # Run operation from hot OS caches but cold SVN caches.
  start_server(prefix, config)
  run_cs_command("HotOS", config, repository, prefix, args)

  # Keep server process and thus the warmed up SVN caches.
  # Run operation from hot OS and SVN caches.
  run_cs_command("WrmSVN", config, repository, prefix, args)
  run_cs_command("HotSVN", config, repository, prefix, args)
  stop_server(prefix)


def set_config(config):
  """ Switch configuration files to CONFIG.  This overwrites the client
      config file with config.$CONFIG and the server config file with
      subversion.$CONFIG.conf. """

  appdata = shell.SHGetFolderPath(0, shellcon.CSIDL_APPDATA, None, 0)
  svn_config_folder = os.path.join(appdata, 'Subversion')
  svn_config_file = os.path.join(svn_config_folder, 'config')
  svn_config_template = svn_config_file + '.' + config

  shutil.copyfile(svn_config_template, svn_config_file)

  apache_config_folder = os.path.join(apache_path, 'conf', 'extra')
  apache_config_file = os.path.join(apache_config_folder, 'subversion.conf')
  apache_config_template = os.path.join(apache_config_folder,
                                        'subversion.' + config + '.conf')

  shutil.copyfile(apache_config_template, apache_config_file)


def run_test_cs_configurations(command, args):
  """ Run client COMMAND with basic arguments ARGS in all configurations
      repeatedly with all servers on all repositories. """

  print
  print command
  print

  for config in configurations:
    set_config(config)
    for prefix in prefixes:
      # These two must be the innermost loops and must be in that order.
      # It gives us the coldest caches and the least temporal favoritism.
      for run in range(0, repetitions):
        for repository in repositories:
          run_test_cs_sequence(config, repository, run, prefix, command, args)

def run_admin_command(state, config, repository, args):
  """ Run the svnadmin command given in ARGS.  Log the STATE of the
      caches, the CONFIG we are using, the REPOSITORY and finally
      the execution times. """

  exe = os.path.join(exe_path, "svnadmin.exe")

  if config == 'medium':
    extra = ['-M', '256']
  elif config == 'fast':
    extra = ['-M', '1024']
  else:
    extra = []

  print state, "\t", repository, "\t", config, "\t",
  sys.stdout.flush()
  subprocess.call(["TimeWin.exe", exe] + args + extra)

def run_test_admin_sequence(config, repository, run, command, args):
  """ Run the svnadmin COMMAND with the given ARGS in various stages
      of cache heat-up.  Execute the test with server CONFIG on
      REPOSITORY. """

  # Full set of command arguments
  path = os.path.join(repo_parent, repository + str(run))
  args = [command, path] + args

  # Free up caches best we can.
  clear_memory()

  # svnadmin runs can be quite costly and are usually CPU-bound.
  # Test with "cold" and "hot" CPU caches only.
  run_admin_command("Cold", config, repository, args)
  run_admin_command("Hot", config, repository, args)


def run_test_admin_configurations(command, args):
  """ Run svnadmin COMMAND with basic arguments ARGS in all configurations
      repeatedly on all repositories. """

  print
  print command
  print

  for config in configurations:
    # These two must be the innermost loops and must be in that order.
    # It gives us the coldest caches and the least temporal favoritism.
    for run in range(0, repetitions):
      for repository in repositories:
        run_test_admin_sequence(config, repository, run, command, args)


def bench():
  """ Run all performance tests. """

  run_test_cs_configurations('log', ['-v', '--limit', '50000'])
  run_test_cs_configurations('export', [wc_path, '-q'])

  run_test_cs_configurations('null-log', ['-v', '--limit', '50000', '-q'])
  run_test_cs_configurations('null-export', ['-q'])

  run_test_admin_configurations('dump', ['-q'])

# main function
bench()
