#!/bin/bash
#
# kcbench - kernel compile benchmark
# Copyright (c) 2007-2018 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.4.1
set -e

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

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

	# used together with make -j
	# number of CPUs * 2
	default_number_of_jobs=$(($(nproc --all)*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_sys="/usr/share/kcbench-data/"
	default_sources_dir_home="${HOME}/.cache/kcbench/"

	# use gcc by default
	compiler="gcc"

	# Download this version by default:
	default_linux_to_compile="4.19"

	# retrieve settings from file (undocumented)
	if [[ -e "${HOME}/.config/kcbench" ]]; then
		source "${HOME}/.config/kcbench"
	fi
}


# check everthing before starting
kcbench_startupchecks()
{
	# do we have all the tools we need?
	kcbench_checktools make ld awk /usr/bin/time "${compiler}" pkg-config
	if ! pkg-config --modversion libelf &> /dev/null; then
		echo "Please install libelf-dev, libelf-devel or elfutils-libelf-devel. Aboring" >&2
		exit 2		
	fi
	
	# decide which version to compile by default
	kcbench_decideversion

	# find a tree to compile
	kcbench_findsrctree

	# check date prevent errors on testsystems where date sometimes is set incorectly
	if (( $(date -r "${dir_sources}/Makefile" "+%s" 2> /dev/null) > $(date "+%s") )); then
		echo "Makefile is younger then current date; please set your system time properly." >&2
		exit 2
	fi


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

		# if the kcbench dir exists in compile_directory already just use it
		if [[ ! -d "${compile_directory%%/}/${myprog_name}" ]]; then
			if ! mkdir ${compile_directory%%/}/${myprog_name}; then
				echo "Could not create ${compile_directory%%/}/${myprog_name}" >&2
				exit 2
			fi
		fi
		readonly dir_outputtmp="${compile_directory%%/}/${myprog_name}"

	else
		# create a tmp dir
		readonly dir_outputtmp=$(mktemp -d -t ${myprog_name}.XXXXXXXXX)
	fi

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

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


	# how many jobs to use?
	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}"

}

kcbench_checktools()
{
	# check for tools we need
	for tool in $@ ; do
		if ! type ${tool} &> /dev/null ; then
			echo "${tool} needed, but could not be found. Aboring" >&2
			exit 2
		fi
	done
}

kcbench_decideversion()
{
	# user provided us with something
	if [[ "${compile_srctree}" ]] ; then
		if [[ -d "${compile_srctree}" ]]; then
			# not needed when a patch was specified
			unset linux_to_compile
		else
			linux_to_compile="${compile_srctree}"
		fi
	else
		case $(gcc --version | head -n 1 | cut -d ' ' -f 3 2>/dev/null | cut -d . -f 1 2>/dev/null) in
		# this is known to only work with gcc right now
		9)
			linux_to_compile="4.19"
			;;
		4|5|6|7|8)
			linux_to_compile="4.14"
			;;
		*)
			echo "Warning: Could not find a default linux version to compile with your compiler"
			echo "Please report to linux@leemhuis.info"
			echo "Using ${default_linux_to_compile}"
			linux_to_compile=${default_linux_to_compile}
		esac;
	fi
}

# find a srctree to compile!
kcbench_findsrctree()
{
	# use user provided information if avilable
	if [[ -d "${compile_srctree}" ]]; then
		if [[ -e "${compile_srctree}/include/linux/kernel.h" ]] ; then
			# user provided a local directory. Make sure we have 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_sys}/linux-${compile_srctree}/include/linux/kernel.h" ]]; then
			# user meant a tree prepared for kcbench by just giving version number
			dir_sources="${default_sources_dir_sys%%/}/linux-${compile_srctree%%/}/"
		elif [[ -e "${default_sources_dir_home%%/}/linux-${compile_srctree}/include/linux/kernel.h" ]]; then
			# user meant a tree prepared for kcbench by just giving version number
			dir_sources="${default_sources_dir_home%%/}/linux-${compile_srctree%%/}/"
		else
			echo "Could neither find directory ${compile_srctree}, ${default_sources_dir_sys%%/}/linux-${compile_srctree%%/}/, or ${default_sources_dir_home%%/}/linux-${compile_srctree%%/}/" >&2
			exit 2
		fi	
		return 0
	fi


	# something already available in homedir?
	if [[ -d "${default_sources_dir_home%%/}"/linux-"${linux_to_compile}" ]]; then
		dir_sources="${default_sources_dir_home%%/}"/linux-"${linux_to_compile}"
		return 0
	fi

	# something available system wide?
	if [[ -d "${default_sources_dir_sys%%/}"/linux-"${linux_to_compile}" ]]; then
		dir_sources="${default_sources_dir_sys%%/}"/linux-"${linux_to_compile}"
		return 0
	fi

	# only remaining option: download
	if [[ "${noautodownloads}" ]]; then
		echo "Could not find a source tree to compile. Please specify one or allow auto-downloads" >&2
		exit 2
	fi

	if ! mkdir -p "${default_sources_dir_home}"; then
		echo "Could not create ${sourcesdir_autolocal}; aborting."  >&2 
		exit 2
	fi

	# remove partial downloads
	if [[ -e "${default_sources_dir_home}downloading" ]]; then
		rm -rf "${default_sources_dir_home}downloading"
	fi

	if ! mkdir -p "${default_sources_dir_home}downloading" ; then
		echo Could not create "${default_sources_dir_home}downloading" >&2
		exit 2
	fi


	# use pre-loaded sources to speed up local testing during development
	if [[ -e "${default_sources_dir_home}"/local/linux-"${linux_to_compile}".tar.xz ]]; then 
		local downloadurl="file://${default_sources_dir_home}"/local/linux-"${linux_to_compile}".tar.xz
	else
		local downloadurl=https://cdn.kernel.org/pub/linux/kernel/v"${linux_to_compile%%\.*}".x/linux-"${linux_to_compile}".tar.xz
	fi

	# find a downloadtools
	if type curl &> /dev/null ; then
		local downloadtool="curl --silent"
	elif type wget &> /dev/null ; then
		local downloadtool="wget -O - --quiet"
	else
		echo "Need either curl or wget for dowload" >&2
		exit 2
	fi

	# download and extract
	echo -n "Downloading source: This might take a while... "
	if ! ${downloadtool} ${downloadurl}| unxz | tar -C "${default_sources_dir_home}downloading" -x ; then
		rm -rf "${default_sources_dir_home}downloading"
		echo
		echo "Downloading ${downloadurl} or extraction failed" >&2 
		exit 2
	fi
	echo "Done."

	mv "${default_sources_dir_home}downloading/"* "${default_sources_dir_home}" || exit 2
	rmdir "${default_sources_dir_home}downloading/" || exit 2

	dir_sources="${default_sources_dir_home%%/}"/linux-"${linux_to_compile}"
}


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()
{
	# infos
	kcebench_sysinfo 2

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

	# prepration run
	if [[ "${cachefillrun}" ]]; 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
		if [[ "${show_result_from_cache_free_run}" ]]; then
			kcbench_compile_kernel 2 ${default_number_of_jobs} run-0-fillcaches "Filling caches:     "
		else
			kcbench_echo 1 2 -n "Filling caches:     This might take a while... "
			kcbench_compile_kernel 3 ${default_number_of_jobs} run-0-fillcaches "Fill cache run (${default_number_of_jobs})"
			kcbench_echo 1 3 "-------------------------------------------------------------------------------"
		fi
	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 # of errors:  ${errorruns}"
		fi

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

parse_results ()
{
	local this_resultfile="${1}"
	local this_verboselevel="${2}"
	local this_runtype="${3}"

	while read format value; do
		case "${format}" in
			E)
				local tm_elapsed="${value}"
				;;
			e)
				local tm_elapsed_s="${value}"
				;;
			S)
				local tm_kernel_s="${value}"
				;;
			U)
				local tm_user_s="${value}"
				;;
			P)
				local tm_cpu_p="${value}"
				;;
			M)
				local tm_mem_max="${value}"
				;;
			F)
				local tm_pgfault_maj="${value}"
				;;
			R)
				local tm_pgfault_min="${value}"
				;;
			W)
				local tm_swapped="${value}"
				;;
			c)
				local tm_cswitched="${value}"
				;;
			w)
				local tm_waits="${value}"
				;;
			I)
				local tm_file_in="${value}"
				;;
			O)
				local tm_file_out="${value}"
				;;
			x)
				local tm_exit="${value}"
				;;
		esac
	done < "${this_resultfile}"

	if [[ "${this_runtype}" != "run-0-fillcaches" ]]; then
		local time_points="$(echo 1000000/${tm_elapsed_s} | /usr/bin/bc)"
		kcbench_echo 1 ${this_verboselevel} "${time_points} points  (e:${tm_elapsed_s} sec  P:${tm_cpu_p}  U:${tm_user_s} sec  S:${tm_kernel_s} sec)"
		if [[ "${print_detailed_results}" ]]; then
			echo "  Elapsed Time(E): ${tm_elapsed} (${tm_elapsed_s} seconds)"
			echo "  Kernel time (S): ${tm_kernel_s} seconds"
			echo "  User time (U): ${tm_user_s} seconds"
			echo "  CPU usage (P): ${tm_cpu_p}"
			echo "  Major page faults (F): ${tm_pgfault_maj}"
			echo "  Minor page faults (R): ${tm_pgfault_min}"
			echo "  Context switches involuntarily (c): ${tm_cswitched}"
			echo "  Context switches voluntarily (w): ${tm_waits}"
		fi
	else
		if [[ "${show_result_from_cache_free_run}" ]]; then
			kcbench_echo 1 2 "Done (-j ${default_number_of_jobs}, e: ${tm_elapsed_s})"
		else
			kcbench_echo 1 2 "Done"
		fi
	fi
}

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

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

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

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

	if /usr/bin/time -o "${this_logfile}.time" -f 'E %E\ne %e\nS %S\nU %U\nP %P\nM %M\nF %F\nR %R\nW %W\nc %c\nw %w\nI %I\nO %O\nx %x' make O="${dir_outputtmp}" CC=${compiler} -C "${dir_sources}" -j ${this_nrjobs} vmlinux >> "${this_logfile}" 2>&1 ; then
	#if /usr/bin/time -o "${this_logfile}.time" -f 'E %E\ne %e\nS %S\nU %U\nP %P\nM %M\nF %F\nR %R\nW %W\nc %c\nw %w\nI %I\nO %O\nx %x' dd if=/dev/urandom bs=10M count=1 2>/dev/null | md5sum >> "${this_logfile}" 2>&1 ; then	
		parse_results "${this_logfile}.time" ${this_verboselevel} "${this_runtype}"
		kcbench_cleanup_builddir
	else
		kcbench_echo 2 1 "Failed ($(date))."
		kcbench_cleanup_builddir

		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})"
		fi

		if [[ "${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
}

# basic information about the system
kcebench_sysinfo()
{
	kcbench_echo 1 ${1} "Linux running:      $(uname -r) on $(uname -i) ($(uname -n))"
	kcbench_echo 1 ${1} "Processor Cores:    $(nproc --all) -- $(grep 'model name' /proc/cpuinfo  | sed 's!model name\t: !!' | sort | uniq)"
	kcbench_echo 1 ${1} "Compiler:           $(${compiler} --version | head -n 1)"
	[[ -r /proc/memory ]] && kcbench_echo 1 ${1} "Memory:             $(( $(awk '/MemTotal:/ { print $2}' /proc/meminfo) / 1024  )) MByte"
	kcbench_echo 1 ${1} "Linux compiled:     $(basename "${dir_sources}" | sed 's!linux-!!') (${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 " -d, --compiledir <dir>       -- compile in <path>/kcbench (make 'O=#')"
	echo " -r, --detailedresults        -- print more detailed results"
	echo " -i, --infinite               -- run endlessly"
	echo " -n, --iterations <int>       -- number or iterations"
	echo " -j, --jobs <int>             -- number of jobs to use ('make -j #') (*)"
	echo " -q, --quiet                  -- quiet"
	echo " -v, --verbose                -- increase verboselevel (*)"
	echo " -s, --src (<dir>|<version>)  -- take sources in <dir> or from either"
	echo "                                 /usr/share/kcdata/linux-<version> or "
	echo "                                 ~/.cache/kcbench/linux-<version>; download"
	echo "                                 ~/.cache/kcbench/linux-<version>; download"
	echo "                                 them automatically if not found."
	echo " -l, --savefailedlogs <dir>   -- save log of failed compile runs in <dir>"
	echo "     --cachefill              -- omit results from initial compile"
	echo "     --compiler (<exec>)      -- use specified compiler"
	echo "     --no-download            -- do not download sources automatically"
	echo
	echo " -h, --help                   -- this text"
	echo " -V, --version                -- output program version"
	echo
	echo "(*) -- option can be passed multiple times"
}


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


# parse cmdline options
while [ "${1}" ] ; do
	case "${1}" in
		-d|--compiledir)
			shift
			compile_directory="${1}"
			shift
			;;
		-r|--detailedresults)
			shift
			print_detailed_results="true"
			;;
		-j|--jobs)
			shift
			# without quotes, to make --jobs "4 8 16 32" possible
			number_of_jobs="${number_of_jobs} ${1}"
			shift
			;;
		-i|--infinite)
			shift
			run_infinite="true"
			;;
		-n|--iterations)
			shift
			number_of_iterations="${1}"
			shift
			;;
		-c|--no-cachefill)
			shift
			# old option ( <= 0.4), ignored, as it's the default these days
			;;
		--cachefill)
			shift
			cachefillrun="true"
			;;
		-q|--quiet)
			verboselevel="1"
			shift
			;;
		-v|--verbose)
			shift
			let verboselevel++
			;;
		-l|--savefailedlogs)
			shift
			savefailedlogs="${1}"
			shift
			;;
		-s|--src)
			shift
			compile_srctree="${1}"
			shift
			;;
		--compiler)
			shift
			compiler="${1}"
			shift
			;;
		--no-download)
			noautodownloads="true"
			shift
			;;
		-h|--help)
			myprog_help
			exit 0
			;;
		-V|--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
