Skip to content

Commit bf6639b

Browse files
committed
view-results-json.sh
This script uses jq to display the values from one or more lockhammer result json files. The output is in a table format, or a json array. Flags to the script control filtering and sorting of the results. Change-Id: I91bd705381a232a35bbe7167b380cf004d587d2b
1 parent 8e8b03a commit bf6639b

File tree

1 file changed

+290
-0
lines changed

1 file changed

+290
-0
lines changed
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
#!/bin/bash
2+
3+
# SPDX-FileCopyrightText: Copyright 2019-2025 Arm Limited and/or its affiliates <open-source-office@arm.com>
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
6+
# This script displays the values from one or more lockhammer json files in a table format using jq.
7+
8+
# XXX: can't differentiate between ns vs. inst for crit/par; please select only using the same units!
9+
10+
SORT_STRING='.num_threads'
11+
REVERSE=0
12+
DUMP_DATA=0
13+
14+
declare -a CRIT
15+
declare -a PAR
16+
declare -a NUM_THREADS
17+
declare -a VARIANT_NAMES
18+
19+
usage() {
20+
cat<<"USAGE"
21+
22+
./view-results-json.sh [options] json [json ...]
23+
24+
select options:
25+
-c crit nominal critical time/inst parameter (repeatable)
26+
-p par nominal parallel time/inst parameter (repeatable)
27+
-t num_threads number of threads (repeatable)
28+
-v variant_name variant name (repeatable)
29+
30+
sort options:
31+
-s sort_string sort string (default is by '.num_threads')
32+
-s help print short header to .key mapping
33+
-r reverse the sort
34+
35+
output options:
36+
-D dump the records in a json array
37+
38+
-h print this usage help message
39+
40+
41+
Example:
42+
43+
# list all data with threads=8, parallel=1000 or parallel=500, and critical=0
44+
# from files *osq_lock*.json, sort by overhead %
45+
46+
./view-results-json.sh -s overhead_% -t 8 -p 1000 -p 500 -c 0 *osq_lock*.json
47+
48+
USAGE
49+
exit 1
50+
}
51+
52+
53+
shopt -s extglob
54+
55+
while getopts ":c:p:t:v:s:rDh" name; do
56+
case "${name}" in
57+
c) CRIT+=(${OPTARG})
58+
;;
59+
p) PAR+=(${OPTARG})
60+
;;
61+
t) NUM_THREADS+=(${OPTARG})
62+
;;
63+
v) VARIANT_NAMES+=(${OPTARG})
64+
;;
65+
s) SORT_STRING=${OPTARG}
66+
;;
67+
r) REVERSE=1
68+
;;
69+
D) DUMP_DATA=1
70+
;;
71+
h) usage
72+
;;
73+
:) >&2 echo "ERROR: flag -$OPTARG required an argument, but none was given"
74+
usage
75+
;;
76+
*) echo "ERROR: unknown flag name=$name, OPTARG=$OPTARG"
77+
usage
78+
;;
79+
esac
80+
done
81+
82+
shift $((OPTIND-1))
83+
84+
FILES="$@"
85+
86+
if [ -z "$FILES" ]; then
87+
echo "no json files given; run with -h for usage help"
88+
exit -1
89+
fi
90+
91+
# -----------------------------------------------------------------------------
92+
# jq filter stages. Write as separate single-quoted strings so that escapes are not needed (i.e., do not use escapes!).
93+
#
94+
# reducer - puts data from all the json into an array with some modifications
95+
# selector - selects the data from the array that match the command line criteria
96+
# sorter - sort the selected data by the sorting criteria
97+
# filter - convert the sorted data into formatted output
98+
99+
# ----------------------------------
100+
# Reducer gets the .results[] array from each json, and, for each results
101+
# element/object, deletes the pinorder and per_thread_sets, and adds an
102+
# .input_filename to the object. The output is a single array of results
103+
# elements.
104+
105+
REDUCER='reduce inputs as $s ([]; . += [$s.results[] | del(.pinorder) | del(.per_thread_stats) | . += {"input_filename":input_filename}])'
106+
107+
108+
# ----------------------------------
109+
# Select the records with the requested element values
110+
111+
make_selector() {
112+
local NAME="$1"
113+
shift
114+
local AS_STRING=0
115+
if [ "$1" = "as_string" ]; then
116+
AS_STRING=1
117+
shift
118+
elif [ "$1" = "as_number" ]; then
119+
AS_STRING=0
120+
shift
121+
fi
122+
123+
local ARRAY=("$@")
124+
local ARRAY_SELECTOR=
125+
126+
if [ ${#ARRAY[@]} -eq 0 ]; then
127+
return
128+
fi
129+
130+
for a in ${ARRAY[@]}; do
131+
if [ -n "$ARRAY_SELECTOR" ]; then ARRAY_SELECTOR+=" or "; fi
132+
if [ $AS_STRING -eq 1 ]; then
133+
ARRAY_SELECTOR+=".${NAME}==\"${a}\""
134+
else
135+
ARRAY_SELECTOR+=".${NAME}==${a}"
136+
fi
137+
done
138+
139+
echo " and ($ARRAY_SELECTOR)"
140+
}
141+
142+
SELECTOR_ARGLIST="true"
143+
SELECTOR_ARGLIST+=$(make_selector nominal_parallel "${PAR[@]}")
144+
SELECTOR_ARGLIST+=$(make_selector nominal_critical "${CRIT[@]}")
145+
SELECTOR_ARGLIST+=$(make_selector num_threads "${NUM_THREADS[@]}")
146+
SELECTOR_ARGLIST+=$(make_selector variant_name as_string "${VARIANT_NAMES[@]}")
147+
148+
SELECTOR=' [.[] | select('$SELECTOR_ARGLIST')] '
149+
150+
151+
# ----------------------------------
152+
# Sort; output is an array
153+
154+
# for -s sort_string flag, map it to these fields. TODO: reverse SPECIAL_HEADER array instead of hard-coding
155+
declare -A SHORT_HEADER
156+
SHORT_HEADER[cputime_ns/lock]=".cputime_ns_per_lock_acquire"
157+
SHORT_HEADER[cpu_ns/lock]=".cputime_ns_per_lock_acquire"
158+
SHORT_HEADER[wall_ns/lock]=".wall_elapsed_ns_per_lock_acquire"
159+
SHORT_HEADER[fcf]=".full_concurrency_fraction"
160+
SHORT_HEADER[nom_par]=".nominal_parallel"
161+
SHORT_HEADER[nom_crit]=".nominal_critical"
162+
SHORT_HEADER[par_ns]=".avg_parallel_ns_per_loop"
163+
SHORT_HEADER[crit_ns]=".avg_critical_ns_per_loop"
164+
SHORT_HEADER[overhead_ns]=".avg_lock_overhead_cputime_ns"
165+
SHORT_HEADER[overhead_%]=".lock_overhead_cputime_percent"
166+
SHORT_HEADER[locks/wall_sec]=".total_lock_acquires_per_second"
167+
SHORT_HEADER[num_threads]=".num_threads"
168+
SHORT_HEADER[json]=".input_filename"
169+
SHORT_HEADER[host]=".hostname"
170+
SHORT_HEADER[lasom]=".lock_acquires_stddev_over_mean"
171+
172+
# print SHORT_HEADER as a table
173+
if [[ $SORT_STRING == "help" ]]; then
174+
(echo "sort_key sort_string";
175+
for key in "${!SHORT_HEADER[@]}" ; do
176+
echo "$key ${SHORT_HEADER[$key]}"
177+
done) | column -t
178+
exit -1
179+
fi
180+
181+
if [[ -v SHORT_HEADER[$SORT_STRING] ]]; then
182+
SORT_STRING="${SHORT_HEADER[$SORT_STRING]}"
183+
elif [[ ! $SORT_STRING =~ ^\. ]]; then
184+
# we check for this to allow for complex multikey comma-separated sort string to be passed in as an argument.
185+
echo "ERROR: SORT_STRING does not being with a . and is not one of the SHORT_HEADER keys, so it's probably not referring to a results variable."
186+
exit -1
187+
fi
188+
189+
#SORTER='sort_by(.cputime_ns_per_lock_acquire) '
190+
#SORTER='sort_by(.num_threads) '
191+
SORTER='sort_by('$SORT_STRING')'
192+
if [ $REVERSE -eq 1 ]; then
193+
SORTER+=' | reverse'
194+
fi
195+
196+
# json output from jq
197+
if [ $DUMP_DATA -eq 1 ]; then
198+
exec jq -n -r "$REDUCER | $SELECTOR | $SORTER | . " $FILES
199+
fi
200+
201+
202+
# the rest of this is for the tabulated output
203+
204+
# ----------------------------------
205+
# Construct KEY_LIST, an array defining the order of the columns.
206+
# These are typically keynames from entries in the .results[] of a json or, if there's a corresponding entry in SPECIAL_HEADER or SPECIAL_FILTER, what to show instead.
207+
# If the row begins with #, the metric is omitted.
208+
read -r -d '' -a KEY_LIST <<'EOF_KEY_LIST'
209+
test_name
210+
variant_name
211+
num_threads
212+
nominal_critical
213+
nominal_parallel
214+
cputime_ns_per_lock_acquire
215+
avg_critical_ns_per_loop
216+
avg_parallel_ns_per_loop
217+
avg_lock_overhead_cputime_ns
218+
lock_overhead_cputime_percent
219+
full_concurrency_fraction
220+
lock_acquires_stddev_over_mean
221+
host
222+
#json
223+
wall_elapsed_ns_per_lock_acquire
224+
total_lock_acquires_per_second
225+
EOF_KEY_LIST
226+
227+
# SPECIAL_HEADER is what to print in the header for a key name. If the key does not exist, then the key name is used as the header.
228+
declare -A SPECIAL_HEADER
229+
SPECIAL_HEADER[cputime_ns_per_lock_acquire]="cpu_ns/lock"
230+
SPECIAL_HEADER[wall_elapsed_ns_per_lock_acquire]="wall_ns/lock"
231+
SPECIAL_HEADER[full_concurrency_fraction]="fcf"
232+
SPECIAL_HEADER[avg_parallel_ns_per_loop]="par_ns"
233+
SPECIAL_HEADER[avg_critical_ns_per_loop]="crit_ns"
234+
SPECIAL_HEADER[avg_lock_overhead_cputime_ns]="overhead_ns"
235+
SPECIAL_HEADER[lock_overhead_cputime_percent]="overhead_%"
236+
SPECIAL_HEADER[total_lock_acquires_per_second]="locks/wall_sec"
237+
SPECIAL_HEADER[lock_acquires_stddev_over_mean]="lasom"
238+
SPECIAL_HEADER[nominal_critical]="nom_crit"
239+
SPECIAL_HEADER[nominal_parallel]="nom_par"
240+
241+
# SPECIAL_FILTER is how to have jq format the element. If the key does not exist, then .key is used for the filter.
242+
declare -A SPECIAL_FILTER
243+
SPECIAL_FILTER[cputime_ns_per_lock_acquire]='\(.cputime_ns_per_lock_acquire|round)'
244+
SPECIAL_FILTER[wall_elapsed_ns_per_lock_acquire]='\(.wall_elapsed_ns_per_lock_acquire|round)'
245+
SPECIAL_FILTER[full_concurrency_fraction]='\(.full_concurrency_fraction * 100 | round / 100)'
246+
SPECIAL_FILTER[host]='\(.hostname | split(".") | .[0])'
247+
SPECIAL_FILTER[json]='\(.input_filename | split(".") | .[:-1] | join("."))'
248+
SPECIAL_FILTER[avg_critical_ns_per_loop]='\(.avg_critical_ns_per_loop | round)'
249+
SPECIAL_FILTER[avg_parallel_ns_per_loop]='\(.avg_parallel_ns_per_loop | round)'
250+
SPECIAL_FILTER[avg_lock_overhead_cputime_ns]='\(.avg_lock_overhead_cputime_ns | round)'
251+
SPECIAL_FILTER[lock_overhead_cputime_percent]='\(.lock_overhead_cputime_percent | round)'
252+
SPECIAL_FILTER[total_lock_acquires_per_second]='\(.total_lock_acquires_per_second|round)'
253+
SPECIAL_FILTER[lock_acquires_stddev_over_mean]='\(.lock_acquires_stddev_over_mean*10000|round/10000)'
254+
255+
# constructs the header or filter
256+
make_special() {
257+
local -n pointer="$1" # name reference to associative array, needs bash 4.2 or later
258+
local normal_format_pre_eval=$2
259+
local normal_format
260+
local key
261+
local list=
262+
for key in "${KEY_LIST[@]}"
263+
do
264+
if [[ $key =~ ^\# ]]; then
265+
continue
266+
fi
267+
if [ -n "$list" ]; then
268+
list="$list\t"
269+
fi
270+
271+
normal_format=$(eval "echo \"$normal_format_pre_eval\"")
272+
273+
if [[ -v pointer[$key] ]]; then
274+
list+="${pointer[$key]}"
275+
else
276+
list+=$normal_format
277+
fi
278+
done
279+
echo "$list"
280+
}
281+
282+
HEADER=$(make_special SPECIAL_HEADER '$key')
283+
FILTER=$(make_special SPECIAL_FILTER '\(.${key})')
284+
285+
# ----------------------------------
286+
# finally invoke jq for tabulated output using 'column' to pretty print.
287+
(
288+
echo -e "$HEADER"
289+
jq -n -r "$REDUCER | $SELECTOR | $SORTER | .[] | \"$FILTER\" " $FILES
290+
) | column -t -o " "

0 commit comments

Comments
 (0)