@@ -0,0 +1,665 @@
1+#!/usr/bin/env bash
2+
3+# Note that this script is used in the "streamly" package as well as
4+# in "streaming-benchmarks" package. Any changes to the script should be
5+# generic enough so that it works in both the cases.
6+
7+#------------------------------------------------------------------------------
8+
9+set -o pipefail
10+
11+SCRIPT_DIR=$(cd `dirname $0`; pwd)
12+
13+RUNNING_BENCHMARKS=y
14+source $SCRIPT_DIR/build-lib.sh
15+
16+print_help () {
17+ echo "Usage: $0 "
18+ echo " [--benchmarks <"bench1 bench2 ..." | help>]"
19+ echo " [--prefix <benchmark name prefix to match>"
20+ echo " [--fields <"field1 field2 ..." | help>]"
21+ echo " [--sort-by-name]"
22+ echo " [--compare]"
23+ echo " [--graphs]"
24+ echo " [--no-measure]"
25+ echo " [--append]"
26+ echo " [--long]"
27+ echo " [--slow]"
28+ echo " [--quick]"
29+ echo " [--raw]"
30+ echo " [--dev-build]"
31+ echo " [--use-nix]"
32+ echo " [--with-compiler <compiler exe name>]"
33+ echo " [--cabal-build-options <options>]"
34+ echo " [--rtsopts <opts>]"
35+ echo " [--commit-compare] [--base <commit>] [--candidate <commit>]"
36+ #echo " -- <gauge options or benchmarks>"
37+ echo
38+ echo "--benchmarks: benchmarks to run, use 'help' for list of benchmarks"
39+ echo "--compare: compare the specified benchmarks with each other"
40+ echo "--fields: measurement fields to report, use 'help' for a list"
41+ echo "--graphs: Generate graphical reports"
42+ echo "--no-measure: Don't run benchmarks, run reports from previous results"
43+ echo "--append: Don't overwrite previous results, append for comparison"
44+ echo "--long: Use much longer stream size for infinite stream benchmarks"
45+ echo "--slow: Slightly more accurate results at the expense of speed"
46+ echo "--quick: Faster results, useful for longer benchmarks"
47+ echo "--raw: Run the benchmarks but don't report them. This is useful when"
48+ echo " you only want to work with the csv files generated."
49+ echo "--cabal-build-options: Pass any cabal build options to be used for build"
50+ echo " e.g. --cabal-build-options \"--flag dev\""
51+ echo
52+ echo "When specific space complexity group is chosen then (and only then) "
53+ echo "RTS memory restrictions are used accordingly. For example, "
54+ echo "bench.sh --benchmarks Data.Parser -- Data.Parser/o-1-space "
55+ echo "restricts Heap/Stack space for O(1) characterstics"
56+ echo
57+ echo "When using --commit-compare, by default comparative chart of HEAD^ vs HEAD"
58+ echo "commit is generated, in the 'charts' directory."
59+ echo "Use --base and --candidate to select the commits to compare."
60+ echo
61+ #echo "Any arguments after a '--' are passed directly to gauge"
62+ exit
63+}
64+
65+#-----------------------------------------------------------------------------
66+# Reporting utility functions
67+#-----------------------------------------------------------------------------
68+
69+list_comparisons () {
70+ echo "Comparison groups:"
71+ for i in $COMPARISONS
72+ do
73+ echo -n "$i ["
74+ eval "echo -n \$$i"
75+ echo "]"
76+ done
77+ echo
78+}
79+
80+# chart is expensive to build and usually not required to be rebuilt
81+cabal_which_report() {
82+ local path="./bin/$1"
83+ if test -e "$path"
84+ then
85+ echo $path
86+ fi
87+}
88+
89+find_report_prog() {
90+ local prog_name="bench-report"
91+ hash -r
92+ local prog_path=$(cabal_which_report $prog_name)
93+ if test -x "$prog_path"
94+ then
95+ echo $prog_path
96+ else
97+ return 1
98+ fi
99+}
100+
101+build_report_prog() {
102+ local prog_name="bench-report"
103+ local prog_path=$(cabal_which_report $prog_name)
104+
105+ hash -r
106+ if test ! -x "$prog_path" -a "$BUILD_ONCE" = "0"
107+ then
108+ echo "Building bench-report executables"
109+ BUILD_ONCE=1
110+ pushd $BENCHMARK_DIR/bench-report
111+ local cmd
112+ cmd="$CABAL_EXECUTABLE install --installdir $SCRIPT_DIR bench-report"
113+ if test "$USE_NIX" -eq 0
114+ then
115+ $cmd || die "bench-report build failed"
116+ else
117+ nix-shell --run "$cmd" || die "bench-report build failed"
118+ fi
119+ popd
120+
121+ elif test ! -x "$prog_path"
122+ then
123+ return 1
124+ fi
125+ return 0
126+}
127+
128+build_report_progs() {
129+ if test "$RAW" = "0"
130+ then
131+ build_report_prog || exit 1
132+ local prog
133+ prog=$(find_report_prog) || \
134+ die "Cannot find bench-report executable"
135+ echo "Using bench-report executable [$prog]"
136+ fi
137+}
138+
139+# We run the benchmarks in isolation in a separate process so that different
140+# benchmarks do not interfere with other. To enable that we need to pass the
141+# benchmark exe path to gauge as an argument. Unfortunately it cannot find its
142+# own path currently.
143+
144+# The path is dependent on the architecture and cabal version.
145+
146+bench_output_file() {
147+ local bench_name=$1
148+ echo "charts/$bench_name/results.csv"
149+}
150+
151+#------------------------------------------------------------------------------
152+# Speed options
153+#------------------------------------------------------------------------------
154+
155+# tasty-bench does not like an option set twice
156+set_super_quick_mode () {
157+ echo -n super_quick
158+}
159+
160+# For certain long benchmarks if the user has not requested super quick
161+# mode we anyway use a slightly quicker mode.
162+use_quicker_mode () {
163+ if test "$QUICK_MODE" -eq 0
164+ then
165+ echo quicker
166+ fi
167+}
168+
169+#------------------------------------------------------------------------------
170+# Determine options from benchmark name
171+#------------------------------------------------------------------------------
172+
173+# Global environment passed:
174+# BENCH_EXEC_PATH: the benchmark executable
175+# RTS_OPTIONS: additional RTS options
176+# QUICK_MODE: whether we are in quick mode
177+# USE_GAUGE: whether to use gauge or tasty-bench
178+# LONG: whether to use a large stream size
179+
180+# $1: bench name
181+bench_exec_one() {
182+ local BENCH_NAME_ORIG
183+ BENCH_NAME_ORIG="$1"
184+ shift
185+
186+ local SUPER_QUICK_OPTIONS
187+ local QUICKER_OPTIONS
188+ if test "$USE_GAUGE" -eq 0
189+ then
190+ SUPER_QUICK_OPTIONS="--stdev 1000000"
191+ QUICKER_OPTIONS="--stdev 100"
192+ else
193+ # Do not keep time limit as 0 otherwise GC stats may remain 0 in some cases.
194+ SUPER_QUICK_OPTIONS="--quick --min-duration 0 --time-limit 0.01 --include-first-iter"
195+ QUICKER_OPTIONS="--min-samples 3 --time-limit 1"
196+ fi
197+
198+ local BENCH_NAME0
199+ local BENCH_NAME1
200+ local BENCH_NAME2
201+ local BENCH_NAME
202+ if test "$USE_GAUGE" -eq 0
203+ then
204+ # XXX this is a hack to make the "/" separated names used in the functions
205+ # determining options based on benchmark name. For tasty-bench the benchmark
206+ # names are separated by "." instead of "/".
207+ BENCH_NAME0=$(echo $BENCH_NAME_ORIG | sed -e s/^All\.//)
208+ BENCH_NAME1=$(echo $BENCH_NAME0 | cut -f1 -d '/')
209+ BENCH_NAME2=$(echo $BENCH_NAME0 | cut -f2- -d '/' | sed -e 's/\./\//g')
210+ BENCH_NAME="$BENCH_NAME1/$BENCH_NAME2"
211+ else
212+ BENCH_NAME=$BENCH_NAME_ORIG
213+ fi
214+
215+ local RTS_OPTIONS1
216+ RTS_OPTIONS1="\
217++RTS -T \
218+$(bench_rts_options $(basename $BENCH_EXEC_PATH) $BENCH_NAME) \
219+$RTS_OPTIONS \
220+-RTS"
221+
222+ local QUICK_MODE_TYPE
223+ QUICK_MODE_TYPE="\
224+ $(if test "$QUICK_MODE" -ne 0; then set_super_quick_mode; fi) \
225+ $(bench_speed_options $(basename $BENCH_EXEC_PATH) $BENCH_NAME)"
226+
227+ local QUICK_BENCH_OPTIONS
228+ for i in $QUICK_MODE_TYPE
229+ do
230+ case "$i" in
231+ super_quick) QUICK_BENCH_OPTIONS="$SUPER_QUICK_OPTIONS"; break ;;
232+ quicker) QUICK_BENCH_OPTIONS="$QUICKER_OPTIONS"; break ;;
233+ esac
234+ done
235+
236+ local STREAM_SIZE
237+ local STREAM_LEN
238+ local STREAM_SIZE_OPT
239+ if test "$LONG" -ne 0
240+ then
241+ STREAM_SIZE=10000000
242+ STREAM_LEN=$(env LC_ALL=en_US.UTF-8 printf "--stream-size %'.f\n" $STREAM_SIZE)
243+ STREAM_SIZE_OPT="--stream-size $STREAM_SIZE"
244+ fi
245+
246+ echo "$BENCH_NAME_ORIG: \
247+ $RTS_OPTIONS1 \
248+ $STREAM_LEN \
249+ $QUICK_BENCH_OPTIONS" \
250+ "$@"
251+
252+ #------------------------------------------------------------------------------
253+ # Run benchmark with options and collect results
254+ #------------------------------------------------------------------------------
255+
256+ local output_file
257+ output_file=$(bench_output_file $(basename $BENCH_EXEC_PATH))
258+ mkdir -p `dirname $output_file`
259+ rm -f ${output_file}.tmp
260+
261+ local BENCH_NAME_ESC
262+ if test $USE_GAUGE -eq 0
263+ then
264+ # Escape "\" and double quotes in benchmark names
265+ BENCH_NAME_ESC=$(echo "$BENCH_NAME_ORIG" | sed -e 's/\\/\\\\/g' | sed -e 's/"/\\"/g')
266+ $BENCH_EXEC_PATH \
267+ -j 1 \
268+ $RTS_OPTIONS1 \
269+ $STREAM_SIZE_OPT \
270+ $QUICK_BENCH_OPTIONS \
271+ "$@" \
272+ --csv=${output_file}.tmp \
273+ -p '$0 == "'"$BENCH_NAME_ESC"'"'
274+
275+ # Convert cpuTime field from picoseconds to seconds
276+ awk --version 2>&1 | grep -q "GNU Awk" \
277+ || die "Need GNU awk. [$(which awk)] is not GNU awk."
278+ tail -n +2 ${output_file}.tmp | \
279+ awk 'BEGIN {FPAT = "([^,]+)|(\"[^\"]+\")";OFS=","} {$2=$2/1000000000000;print}' \
280+ >> $output_file
281+ else
282+ $BENCH_EXEC_PATH \
283+ $RTS_OPTIONS1 \
284+ $STREAM_SIZE_OPT \
285+ $QUICK_BENCH_OPTIONS \
286+ "$@" \
287+ --csvraw=${output_file}.tmp \
288+ -m exact "$BENCH_NAME"
289+ tail -n +2 ${output_file}.tmp \
290+ >> $output_file
291+ fi
292+}
293+
294+invoke_gauge () {
295+ local target_prog="$1"
296+ local target_name="$2"
297+ local output_file="$3"
298+
299+ local MATCH=""
300+ if test "$LONG" -ne 0
301+ then
302+ MATCH="$target_name/o-1-space"
303+ else
304+ MATCH="$BENCH_PREFIX"
305+ fi
306+ echo "name,iters,time,cycles,cpuTime,utime,stime,maxrss,minflt,majflt,nvcsw,nivcsw,allocated,numGcs,bytesCopied,mutatorWallSeconds,mutatorCpuSeconds,gcWallSeconds,gcCpuSeconds" >> $output_file
307+ # keep only benchmark names with shortest prefix e.g. "a/b/c" and "a/b", we
308+ # should only keep "a/b" otherwise benchmarks will run multiple times. why?
309+ $target_prog -l \
310+ | grep "^$MATCH" \
311+ | while read -r name; \
312+ do bench_exec_one "$name" "${GAUGE_ARGS[@]}" || exit 1; done \
313+ || die "Benchmark execution failed."
314+}
315+
316+invoke_tasty_bench () {
317+ local target_prog="$1"
318+ local target_name="$2"
319+ local output_file="$3"
320+
321+ local MATCH=""
322+ if test "$LONG" -ne 0
323+ then
324+ MATCH="-p /$target_name\/o-1-space/"
325+ else
326+ if test -n "$BENCH_PREFIX"
327+ then
328+ # escape "/"
329+ local escaped_name=$(echo "$BENCH_PREFIX" | sed -e 's/\//\\\//g')
330+ MATCH="-p /$escaped_name/"
331+ fi
332+ fi
333+ echo "Name,cpuTime,2*Stdev (ps),Allocated,bytesCopied" >> $output_file
334+ $target_prog -l $MATCH \
335+ | grep "^All" \
336+ | while read -r name; \
337+ do bench_exec_one "$name" "${GAUGE_ARGS[@]}" || exit 1; done \
338+ || die "Benchmark execution failed."
339+}
340+
341+run_bench_target () {
342+ local package_name=$1
343+ local component=$2
344+ local target_name=$3
345+
346+ local target_prog
347+ target_prog=$(cabal_target_prog $package_name $component $target_name) || \
348+ die "Cannot find executable for target $target_name"
349+
350+ echo "Running executable $target_name ..."
351+
352+ # Needed by bench_exec_one
353+ BENCH_EXEC_PATH=$target_prog
354+
355+ local output_file=$(bench_output_file $target_name)
356+ mkdir -p `dirname $output_file`
357+ if test "$USE_GAUGE" -eq 0
358+ then invoke_tasty_bench "$target_prog" "$target_name" "$output_file"
359+ else invoke_gauge "$target_prog" "$target_name" "$output_file"
360+ fi
361+}
362+
363+# $1: package name
364+# $2: component
365+# $3: targets
366+run_bench_targets() {
367+ for i in $3
368+ do
369+ run_bench_target $1 $2 $i
370+ done
371+}
372+
373+run_benches_comparing() {
374+ local bench_list=$1
375+
376+ if test -z "$CANDIDATE"
377+ then
378+ CANDIDATE=$(git rev-parse HEAD)
379+ fi
380+ if test -z "$BASE"
381+ then
382+ # XXX Should be where the current branch is forked from master
383+ BASE="$CANDIDATE^"
384+ fi
385+ echo "Comparing baseline commit [$BASE] with candidate [$CANDIDATE]"
386+ echo "Checking out base commit [$BASE] for benchmarking"
387+ git checkout "$BASE" || die "Checkout of base commit [$BASE] failed"
388+
389+ $BUILD_BENCH || die "build failed"
390+ run_bench_targets $BENCHMARK_PACKAGE_NAME b "$bench_list" target_exe_extra_args
391+
392+ echo "Checking out candidate commit [$CANDIDATE] for benchmarking"
393+ git checkout "$CANDIDATE" || \
394+ die "Checkout of candidate [$CANDIDATE] commit failed"
395+
396+ $BUILD_BENCH || die "build failed"
397+ run_bench_targets $BENCHMARK_PACKAGE_NAME b "$bench_list" target_exe_extra_args
398+ # XXX reset back to the original commit
399+}
400+
401+backup_output_file() {
402+ local bench_name=$1
403+ local output_file=$(bench_output_file $bench_name)
404+
405+ if test -e $output_file -a "$APPEND" != 1
406+ then
407+ mv -f -v $output_file ${output_file}.prev
408+ fi
409+}
410+
411+run_measurements() {
412+ local bench_list=$1
413+
414+ for i in $bench_list
415+ do
416+ backup_output_file $i
417+ done
418+
419+ if test "$COMMIT_COMPARE" = "0"
420+ then
421+ run_bench_targets $BENCHMARK_PACKAGE_NAME b "$bench_list" target_exe_extra_args
422+ else
423+ run_benches_comparing "$bench_list"
424+ fi
425+}
426+
427+run_reports() {
428+ local prog
429+ prog=$(find_report_prog) || \
430+ die "Cannot find bench-graph executable"
431+ echo
432+
433+ for i in $1
434+ do
435+ echo "Generating reports for ${i}..."
436+ $prog \
437+ --benchmark $i \
438+ $(test "$GRAPH" = 1 && echo "--graphs") \
439+ $(test "$SORT_BY_NAME" = 1 && echo "--sort-by-name") \
440+ --fields "$FIELDS"
441+ done
442+}
443+
444+#-----------------------------------------------------------------------------
445+# Execution starts here
446+#-----------------------------------------------------------------------------
447+
448+cd $SCRIPT_DIR/..
449+
450+USE_GIT_CABAL=1 # This is used by set_common_vars
451+set_common_vars
452+
453+USE_NIX=0
454+COMPARE=0
455+COMMIT_COMPARE=0
456+BASE=
457+CANDIDATE=
458+
459+APPEND=0
460+LONG=0
461+RAW=0
462+SORT_BY_NAME=0
463+GRAPH=0
464+MEASURE=1
465+
466+GAUGE_ARGS=
467+BUILD_ONCE=0
468+
469+CABAL_BUILD_OPTIONS="--flag fusion-plugin "
470+
471+#-----------------------------------------------------------------------------
472+# Read command line
473+#-----------------------------------------------------------------------------
474+
475+while test -n "$1"
476+do
477+ case $1 in
478+ -h|--help|help) print_help ;;
479+ # options with arguments
480+ --benchmarks) shift; TARGETS=$1; shift ;;
481+ --targets) shift; TARGETS=$1; shift ;;
482+ --prefix) shift; BENCH_PREFIX="$1"; shift ;;
483+ --fields) shift; FIELDS=$1; shift ;;
484+ --base) shift; BASE=$1; shift ;;
485+ --candidate) shift; CANDIDATE=$1; shift ;;
486+ --with-compiler) shift; CABAL_WITH_COMPILER=$1; shift ;;
487+ --cabal-build-flags) shift; CABAL_BUILD_OPTIONS+=$1; shift ;;
488+ --cabal-build-options) shift; CABAL_BUILD_OPTIONS+=$1; shift ;;
489+ --rtsopts) shift; RTS_OPTIONS=$1; shift ;;
490+ --config) shift; BENCH_CONFIG_FILE=$1; shift ;;
491+ # flags
492+ --slow) SLOW=1; shift ;;
493+ --quick) QUICK_MODE=1; shift ;;
494+ --compare) COMPARE=1; shift ;;
495+ --commit-compare) COMMIT_COMPARE=1; shift ;;
496+ --raw) RAW=1; shift ;;
497+ --append) APPEND=1; shift ;;
498+ --long) LONG=1; shift ;;
499+ --sort-by-name) SORT_BY_NAME=1; shift ;;
500+ --graphs) GRAPH=1; shift ;;
501+ --no-measure) MEASURE=0; shift ;;
502+ --dev-build) RUNNING_DEVBUILD=1; shift ;;
503+ --use-nix) USE_NIX=1; shift ;;
504+ --use-gauge) USE_GAUGE=1; shift ;;
505+ --) shift; break ;;
506+ *) echo "Unknown flags: $*"; echo; print_help ;;
507+ esac
508+done
509+GAUGE_ARGS=("$@")
510+
511+if test -z "$BENCH_CONFIG_FILE"
512+then
513+ die "Please use --config to specify config file"
514+fi
515+
516+source "$BENCH_CONFIG_FILE" || \
517+ die "Failed to source config file $BENCH_CONFIG_FILE"
518+
519+# Defined in $BENCH_CONFIG_FILE
520+bench_config
521+
522+if test -z "$FIELDS"
523+then
524+ FIELDS=$DEFAULT_FIELDS
525+fi
526+
527+set_derived_vars
528+
529+#-----------------------------------------------------------------------------
530+# Determine targets
531+#-----------------------------------------------------------------------------
532+
533+only_real_benchmarks () {
534+ for i in $TARGETS
535+ do
536+ local SKIP=0
537+ for j in $COMPARISONS
538+ do
539+ if test $i == $j
540+ then
541+ SKIP=1
542+ fi
543+ done
544+ if test "$SKIP" -eq 0
545+ then
546+ echo -n "$i "
547+ fi
548+ done
549+}
550+
551+# defined in $BENCH_CONFIG_FILE
552+bench_targets
553+
554+if test "$(has_item "$TARGETS" help)" = "help"
555+then
556+ list_target_groups
557+ list_comparisons
558+ list_targets
559+ exit
560+fi
561+
562+COMMON_FIELDS="allocated bytescopied cputime maxrss"
563+if test "$USE_GAUGE" -eq 1
564+then
565+ ALL_FIELDS="$COMMON_FIELDS time cycles utime stime minflt majflt nvcsw nivcsw"
566+else
567+ ALL_FIELDS="$COMMON_FIELDS"
568+fi
569+if test "$(has_item "$FIELDS" help)" = "help"
570+then
571+ echo "Supported fields: $ALL_FIELDS"
572+ echo "Default fields: $DEFAULT_FIELDS"
573+ exit
574+fi
575+
576+if test "$LONG" -ne 0
577+then
578+ if test -n "$TARGETS"
579+ then
580+ echo "Cannot specify benchmarks [$TARGETS] with --long"
581+ exit
582+ fi
583+ TARGETS=$infinite_grp
584+fi
585+
586+DEFAULT_TARGETS="$(all_grp)"
587+TARGETS=$(set_targets)
588+
589+TARGETS_ORIG=$TARGETS
590+TARGETS=$(only_real_benchmarks)
591+
592+echo "Using benchmark suites [$TARGETS]"
593+
594+#-----------------------------------------------------------------------------
595+# Build reporting utility
596+#-----------------------------------------------------------------------------
597+
598+# We need to build the report progs first at the current (latest) commit before
599+# checking out any other commit for benchmarking.
600+build_report_progs
601+
602+#-----------------------------------------------------------------------------
603+# Build and run targets
604+#-----------------------------------------------------------------------------
605+
606+if test "$USE_GAUGE" -eq 1
607+then
608+ BUILD_FLAGS="--flag use-gauge"
609+fi
610+
611+BUILD_BENCH="$CABAL_EXECUTABLE v2-build $BUILD_FLAGS $CABAL_BUILD_OPTIONS --enable-benchmarks"
612+if test "$MEASURE" = "1"
613+then
614+ run_build "$BUILD_BENCH" $BENCHMARK_PACKAGE_NAME bench "$TARGETS"
615+ run_measurements "$TARGETS"
616+fi
617+
618+#-----------------------------------------------------------------------------
619+# Run reports
620+#-----------------------------------------------------------------------------
621+
622+# $1: var name
623+build_comparison_results () {
624+ local name
625+ local constituents
626+
627+ name=$1
628+ constituents=$(eval "echo -n \$${name}")
629+ mkdir -p "charts/$name"
630+ dest_file="charts/$name/results.csv"
631+ : > $dest_file
632+ for j in $constituents
633+ do
634+ cat "charts/$j/results.csv" >> $dest_file
635+ done
636+}
637+
638+if test "$COMPARE" -eq 1
639+then
640+ DYN_CMP_GRP="$(echo "$TARGETS" | sed -e 's/ /_/g')_cmp"
641+ eval "$DYN_CMP_GRP=\"$TARGETS\""
642+ COMPARISON_REPORTS=$DYN_CMP_GRP
643+ build_comparison_results $DYN_CMP_GRP
644+else
645+ COMPARISON_REPORTS=""
646+fi
647+
648+for i in $COMPARISONS
649+do
650+ if test "$(has_item "$TARGETS_ORIG" $i)" = $i
651+ then
652+ COMPARISON_REPORTS="$COMPARISON_REPORTS $i"
653+ build_comparison_results $i
654+ fi
655+done
656+
657+if test "$RAW" = "0"
658+then
659+ run_reports "$TARGETS"
660+ run_reports "$COMPARISON_REPORTS"
661+ if test -n "$DYN_CMP_GRP"
662+ then
663+ rm -rf "charts/$DYN_CMP_GRP"
664+ fi
665+fi