#!/bin/bash
#
# kcbench - kernel compile benchmark
# Copyright (c) 2007 Thorsten Leemhuis <fedora@leemhuis.info>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#

# this is me
myprog_name=kcbench
myprog_version=0.1.1

# set some defaults -- called before cmdoptions are parsed
kcbench_init ()
{
	# verbose?
	verboselevel=2

	# results are normally stable, thus 3 runs after filling the cache should normally be enough
	default_number_of_iterations=3

	# used together with make -j 
	# number of CPUs * 2
	default_number_of_jobs=$(($(grep '^processor' < /proc/cpuinfo  | wc -l)*2))

	# we search for kernels to compile here
	# note that I didn't use /usr/src/ on purpose as localtion -- kernels there 
	# might be modified
	default_sources_dir="/usr/share/kcbench-data/"

	# optional: retrieve settings from file
	if [[ -e "${HOME}/.kcbench" ]]; then
		source "${HOME}/.kcbench"
	fi
}


# check everthing before starting
kcbench_startupchecks()
{
	# check for tools we need
	# thinkaboutme: there are likely more needed
	for tool in make gcc ld ; do
		if ! which ${tool} &> /dev/null ; then
			echo "Could not find ${tool}"
			exit 2
		fi
	done

	# check if cron or other stuff runs and give a warning
	if [[ ! "${ignore_running_apps}" ]]; then
		local runningtasks="$(echo $(ps -A | grep --word -e crond -e httpd -e atd -e sendmail -e smbd | awk '{print $4}' | sort | uniq))"
		if [[ "${runningtasks}" ]] ; then
			echo "WARNING: The following daemons run and might disturb the benchmark: ${runningtasks}; use '--ignore-running-apps' to disabled this warning" >&2
			sleep 5
		fi
	fi


	# find a srctree to compile!
	if [[ "${compile_srctree}" ]]; then
		# user provided some informations what kernel to use
		if [[ -e "${compile_srctree}/include/linux/kernel.h" ]] ; then
			# is a local tree -- but we need the full path!
			if [[ "${compile_srctree}" == "${compile_srctree##/}" ]]; then
				dir_sources="${PWD}/${compile_srctree}"
			else
				dir_sources="${compile_srctree}"
			fi
		elif [[ -e "${default_sources_dir}/linux-${compile_srctree}/include/linux/kernel.h" ]]; then
			# user meant one of our trees by just giving version number
			dir_sources="${default_sources_dir%%/}/linux-${compile_srctree%%/}/"
		else
			echo "Could neither find directory ${compile_srctree} nor ${default_sources_dir%%/}/linux-${compile_srctree%%/}/" >&2
			exit 2
		fi
	else
		# we are own our own

		# without our std-dir it doesn't make sense to continue
		if [[ ! -d "${default_sources_dir}" ]]; then
			echo "${default_sources_dir} not found." >&2
			exit 2
		fi

		# use the latest one by default
		dir_sources="${default_sources_dir%%/}/$(ls -r "${default_sources_dir}" | head -n 1)"
		if [[ ! -e "${dir_sources}/include/linux/kernel.h" ]]; then
			echo "Found ${default_sources_dir}, but doesn't look like a kernel srctree." >&2
			exit 2
		fi
	fi


	# on testsystems the date often is set incorectly, thus check it to prevent misscompiles
	if (( $(date -r "${dir_sources}/Makefile" "+%s") > $(date "+%s") )); then
		echo "Makefile is younger then current date; please set your system time properly." >&2
		exit 2
	fi


	# create tempdir and remove it on exit
	trap 'kcbench_exit 127' 1 2 15
	if [[ "${dir_topoutput}" ]]; then
		# dir_topoutput present?
		if [[ ! -d "${dir_topoutput}" ]]; then
			echo "Could not find ${dir_topoutput}" >&2
			exit 2
		fi
		if [[ "${dir_topoutput}" == "${dir_topoutput##/}" ]]; then
			# we need the full path
			dir_topoutput="${PWD}/${dir_topoutput}"
		fi

		# if our dir in dir_topoutput exist already just use it
		if [[ -d "${dir_topoutput%%/}/${myprog_name}" ]]; then
			readonly dir_outputtmp="${dir_topoutput%%/}/${myprog_name}"
		else
			readonly dir_outputtmp=$(mktemp -d ${dir_topoutput%%/}/${myprog_name})
		fi
	else
		# use a tmp dir
		readonly dir_outputtmp=$(mktemp -d -t ${myprog_name}.XXXXXXXXX)
	fi

	# did dir get created?
	local returncode=$?
	if (( "${returncode}" > 0 )) || [[ ! -d "${dir_outputtmp}" ]]; then
		echo "Could not create temporary output directory" >&2 
		exit 2
	fi

	if [[ "${savefailedlogs}" ]] && [[ ! -d "${savefailedlogs}" ]] ; then
		echo "Could not find ${savefailedlogs}." >&2 
		exit 2
	fi


	# how many jobs?
	if [[ ! "${number_of_jobs}" ]]; then
		# use default
		number_of_jobs=${default_number_of_jobs}
	else
		# user provided number of jobs; but check user input
		for number in ${number_of_jobs}; do
			# is number_of_jobs a real number?
			if (( ! ${number} > 0 )) ; then
				echo "Please provide a real number together with --jobs" >&2
				exit 2
			fi
		done
	fi


	# is number_of_iterations a real number?
	if [[ ! "${number_of_iterations}" ]] ; then
		# use default
		if [[ "${run_infinite}" ]]; then 
			# when running infinite just 1
			number_of_iterations=1
		else
			number_of_iterations=${default_number_of_iterations}
		fi
	elif (( ! ${number_of_iterations} > 0 )) ; then
		echo "Please provide a real number together with --ilterations" >&2
		exit 2
	fi


	# create a logdirectory in dir_outputtmp
	[[ ! -d "${dir_outputtmp}/kcbench" ]] && mkdir "${dir_outputtmp}/kcbench"
	kcbench_logdir="${dir_outputtmp}/kcbench/"
	kcbench_logfile="${dir_outputtmp}/kcbench/log"
	touch "${kcbench_logfile}"



	# disable sceensaver (should we reset this? how?)
	setterm -blank 0
}


kcbench_exit()
{
	kcbench_echo 1 4 "Removing ${dir_outputtmp}"

	if [[ "${dir_outputtmp}" ]] && [[ -d "${dir_outputtmp}" ]]; then
		# the directory should contain our name, thus check it to be on the safe side
		if echo "${dir_outputtmp}" | grep "${myprog_name}" &> /dev/null ; then
			rm -rf "${dir_outputtmp}"
		else
			echo "Leaving ${dir_outputtmp} behind" >&2
		fi
	fi

	exit ${1}
}

kcbench_echo ()
{
	# where to output
	local this_fd=${1}
	shift

	# verboselevel
	local this_verbose=${1}
	shift

	# output
	if (( ${verboselevel} >= ${this_verbose} )); then
		echo "$@" >&${this_fd}
		echo "$@" >> "${kcbench_logfile}"
	fi
}

kcbench_main()
{
	# info
	kcebench_sysinfo 2

	# create defconfig
	kcbench_echo 1 3 "Creating default configuration with 'make defconfig'."
	make O="${dir_outputtmp}" -C "${dir_sources}" -j ${default_number_of_jobs} defconfig >> "${kcbench_logdir}"/make_defconfig 2>&1

	# prepration run
	if [[ ! "${no_cachefill}" ]]; then
		# Note: I tried to use something like this first:
		# find "${dir_sources}" -type f | grep -e '.h$'  -e '.c$' -e 'Makefile' -e 'Kconfig' -e 'Kbuild' -e '.S$' | xargs cat > /dev/null
		# But it does not work as good as a thrown-away compile-run

		# only print result in verbose mode
		(( ${verboselevel} == 2 )) && kcbench_echo 1 2 -n "Filling caches:     This might take a while..."
		kcbench_compile_kernel 3 ${default_number_of_jobs} run-0-fillcaches "Filling caches:     "
		kcbench_echo 1 3 "-------------------------------------------------------------------------------"
		# is there a better way to move the cursor to the left?
		(( ${verboselevel} == 2 )) && kcbench_echo 1 2 -e "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\bFilling caches:     Done                                                      "
	fi

	# go
	local totalruns=1
	local errorruns=0
	while : ; do
		for number in ${number_of_jobs}; do
			for ((run=1; run <= number_of_iterations ; run++)) ; do
				kcbench_compile_kernel 1 ${number} "run${totalruns}" "$(printf "%-20s" "Run ${totalruns} (-j ${number}):")"
				local returncode=$?
				if (( ${returncode} > 0 )); then
					let errorruns++
				fi
				let totalruns++
			done
		done

		if (( ${errorruns} > 0 )) ; then
			kcbench_echo 1 1 "Total erroruns:     ${errorruns}"
		fi

		if [[ ! "${run_infinite}" ]]; then
			# get out of the while loop
			break
		fi
	done
}


kcbench_compile_kernel()
{
	local this_verboselevel="${1}"
	local this_nrjobs="${2}"
	local this_logfile="${kcbench_logdir%%/}/${3}"
	local this_msgstart="${4}"

	echo "make O=${dir_outputtmp} -C ${dir_sources} -j ${this_nrjobs} vmlinux" > "${this_logfile}"
	kcbench_echo 1 4 "Running 'make O=${dir_outputtmp} -C ${dir_sources} -j ${this_nrjobs} vmlinux'"

	kcbench_echo 1 ${this_verboselevel} -n "${this_msgstart}"


	if /usr/bin/time -o "${this_logfile}.time" -f "P:%P R:%e U:%U S:%S" make O="${dir_outputtmp}" -C "${dir_sources}" -j ${this_nrjobs} vmlinux >> "${this_logfile}" 2>&1 ; then
		local time_points="$(echo 1000000/$(cut -d ' ' -f 2 < "${this_logfile}.time" | cut -d ':' -f 2) | /usr/bin/bc)"
		local time_details="$(cat "${this_logfile}.time")"
		kcbench_echo 1 ${this_verboselevel} "${time_points} (${time_details})"
	else
		kcbench_echo 2 1 "Failed ($(date))."

		if [[ "${savefailedlogs}" ]]; then
			# safe log and continue
			kcbench_echo 2 2 "Saving logfile to ${savefailedlogs}/kcbench-$(basename ${this_logfile})"
			cp "${this_logfile}" "${savefailedlogs}/kcbench-$(basename ${this_logfile})"
		elif [[ "${run_infinite}" ]]; then
			return 1
		else
			# what to do with the logfile?
			read -n 1 -s -t 300 -p "Hit 'l' within 300 seconds to view log-file." answer
			echo

			if [[ "${answer}" == [lL] ]]; then
				less "${this_logfile}" 
			fi

			# end
			kcbench_exit 2
		fi
	fi

	# cleanup
	kcbench_echo 1 3 "Cleaning up builddir"
	make O="${dir_outputtmp}" -C "${dir_sources}" -j ${default_number_of_jobs} clean &> "${kcbench_logdir}/make_clean"
}

# basic information about the system
kcebench_sysinfo()
{
	kcbench_echo 1 ${1} "Linux running:      $(uname -r)"
	kcbench_echo 1 ${1} "Compiler:           $(gcc --version | head -n 1)"
	kcbench_echo 1 ${1} "CPU:                $(grep '^processor' < /proc/cpuinfo  | wc -l) x $(grep 'model name' /proc/cpuinfo  | sed 's!model name\t: !!' | sort | uniq)"
	kcbench_echo 1 ${1} "Memory:             $(( $(awk '/MemTotal:/ { print $2}' /proc/meminfo) / 1024  )) MByte"
	kcbench_echo 1 ${1} "Linux compiled:     $(basename "${dir_sources}") (${dir_sources})"
}

myprog_help()
{
	echo "Usage: ${myprog_name} [options]"
	echo $'\n'"Compiles a kernel and messures the time it takes"
	echo $'\n'"Available options:"
	echo " --compiledir <path>      -- use <path>/kcbench for compile results (O=)"
	echo " --ignore-running-apps    -- Do not warn if cron or other daemons run"
	echo " --infinite               -- run endlessly"
	echo " --iterations <int>       -- number or iterations"
	echo " --jobs <int>             -- number of jobs to use ('make -j #') (*)"
	echo " --no-cachefill           -- omit the initial kernel compile to fill caches"
	echo " --verbose                -- increase verboselevel (*)"
	echo " --savefailedlogs <path>  -- save log of failed compile runs in <path>"
	echo " --src (<path>|<version>) -- take sources in <path> or from"
	echo "                             /usr/share/kcdata/linux-<version>"
	echo
	echo " --help                   -- this text"
	echo " --version                -- output program version"
	echo
	echo "(*) -- option can be past multiple times"
}


# set defaults which might get overwritten my command line parameters
kcbench_init


# parse cmdline options
while [ "${1}" ] ; do
	case "${1}" in
		--compiledir)
			shift
			dir_topoutput="${1}"
			shift
			;;
		--jobs)
			shift
			# without quotes, to make --jobs "4 8 16 32" possible
			number_of_jobs="${number_of_jobs} ${1}"
			shift
			;;
		--ignore-running-apps)
			shift
			ignore_running_apps="true"
			;;
		--infinite)
			shift
			run_infinite="true"
			;;
		--iterations)
			shift
			number_of_iterations="${1}"
			shift
			;;
		--no-cachefill)
			shift
			no_cachefill="true"
			;;
		--quiet)
			verboselevel="1"
			shift
			;;
		--savefailedlogs)
			shift
			savefailedlogs="${1}"
			shift
			;;
		--src)
			shift
			compile_srctree="${1}"
			shift
			;;
		--verbose)
			shift
			let verboselevel++
			;;
		--help)
			myprog_help
			exit 0
			;;
		--version)
			echo "${myprog_name} ${myprog_version}"
			exit 0
			;;
		--*)
			echo "Error: Unknown option '${1}'." >&2
			myprog_help >&2
			exit 2
			;;
		*)
			list_of_packages="${list_of_packages} ${1}"
			shift
			;;
	esac
done

# startup checks
kcbench_startupchecks

# go
kcbench_main

# cleanup
kcbench_exit

exit 0
