blob: b5628f7be8768f80538327a2f176faaf936cedaf [file] [log] [blame]
Michele Di Giorgiod9eaf612020-07-08 11:12:57 +01001# Copyright (c) 2019 Arm Limited.
SiCong Li8b4c7302019-09-19 12:18:15 +01002#
3# SPDX-License-Identifier: MIT
4#
5# Permission is hereby granted, free of charge, to any person obtaining a copy
6# of this software and associated documentation files (the "Software"), to
7# deal in the Software without restriction, including without limitation the
8# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9# sell copies of the Software, and to permit persons to whom the Software is
10# furnished to do so, subject to the following conditions:
11#
12# The above copyright notice and this permission notice shall be included in all
13# copies or substantial portions of the Software.
14#
15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21# SOFTWARE.
22
23#!/bin/sh
24
25# Global: Global variables and global settings {{{
26# Treat unset variables as an error when substituting
27set -u
28
29CMD=$( basename $0 )
30
31# All supported strategy options
SiCong Liea803482019-09-26 16:55:49 +010032ALL_STRATEGY_OPTIONS=("native" "reshaped_rhs_only" "reshaped")
SiCong Li8b4c7302019-09-19 12:18:15 +010033
34# Names of example binary for each strategy
SiCong Liea803482019-09-26 16:55:49 +010035EXAMPLE_BIN_NATIVE="benchmark_cl_gemm_native"
SiCong Li8b4c7302019-09-19 12:18:15 +010036EXAMPLE_BIN_RESHAPED_RHS_ONLY="benchmark_cl_gemm_reshaped_rhs_only"
SiCong Libc166d52019-09-26 14:58:53 +010037EXAMPLE_BIN_RESHAPED="benchmark_cl_gemm_reshaped"
SiCong Li8b4c7302019-09-19 12:18:15 +010038
Eren Kopuz6977b372020-07-13 12:37:06 +010039# Default data type
40DEFAULT_DATA_TYPE="F32"
41
SiCong Li8b4c7302019-09-19 12:18:15 +010042# Default output directory
43DEFAULT_OUT_DIR="out"
44
45# Number of iterations for each benchmark run
SiCong Libc166d52019-09-26 14:58:53 +010046NUM_ITERATION=5
SiCong Li8b4c7302019-09-19 12:18:15 +010047# Global }}}
48
49# Functions {{{
50#######################################
51# Print gemm shape file help message
52# Globals:
53# None
54# Arguments:
55# None
56# Returns:
57# None
58#######################################
59function help_gemm_shape_file() {
60 cat >&2 << EOF
61Gemm shape file:
SiCong Lib10181b2020-08-11 13:00:20 +010062 Gemm shape file is a headerless csv file with fields separated by commas
SiCong Li8abbabd2020-04-03 12:39:41 +010063
SiCong Li8b4c7302019-09-19 12:18:15 +010064 A gemm shape is a list of 4 positive integers <M, N, K, B> describing the shapes of the two matrices (LHS and RHS)
65 with:
66 M - Number of lhs matrix rows
67 N - Number of rhs matrix columns
68 K - Number of lhs matrix columns/rhs matrix rows
69 B - Batch size
70
71 An example gemm shape file looks like:
72 100,100,30,1
73 100,100,30,3
74 ...
75
76EOF
77}
78
79#######################################
SiCong Liea803482019-09-26 16:55:49 +010080# Print gemm config file for native help message
81# Globals:
82# None
83# Arguments:
84# None
85# Returns:
86# None
87#######################################
88function help_gemm_config_file_native() {
89 cat >&2 << EOF
90Gemm config file (Strategy native):
SiCong Lib10181b2020-08-11 13:00:20 +010091 Gemm config file is a headerless csv file with fields separated by commas
SiCong Li8abbabd2020-04-03 12:39:41 +010092
93 A gemm config is a list of 3 positive integers <m0, n0, k0>, with:
SiCong Liea803482019-09-26 16:55:49 +010094 m0 - Number of rows processed by the matrix multiplication
95 n0 - Number of columns processed by the matrix multiplication
96 k0 - Number of partial accumulations performed by the matrix multiplication
97
98 Only the following configurations of M0, N0 and K0 are currently supported:
99 M0 = 1, 2, 3, 4, 5, 6, 7, 8
100 N0 = 2, 3, 4, 8, 16
101 K0 = 2, 3, 4, 8, 16
102
103 An example gemm config file looks like:
104 1,4,4
105 2,3,8
106 ...
107
108EOF
109}
110
111#######################################
SiCong Libc166d52019-09-26 14:58:53 +0100112# Print gemm config file for reshaped_rhs_only help message
SiCong Li8b4c7302019-09-19 12:18:15 +0100113# Globals:
114# None
115# Arguments:
116# None
117# Returns:
118# None
119#######################################
120function help_gemm_config_file_reshaped_rhs_only() {
121 cat >&2 << EOF
122Gemm config file (Strategy reshaped_rhs_only):
SiCong Lib10181b2020-08-11 13:00:20 +0100123 Gemm config file is a headerless csv file with fields separated by commas.
SiCong Li8abbabd2020-04-03 12:39:41 +0100124
125 Note also comments and extraneous empty lines are not permitted.
126
SiCong Lib10181b2020-08-11 13:00:20 +0100127 A gemm config is a list of 4 positive integers <m0, n0, k0, h0> and 3 boolean values:
SiCong Li8b4c7302019-09-19 12:18:15 +0100128 m0 - Number of rows processed by the matrix multiplication
129 n0 - Number of columns processed by the matrix multiplication
130 k0 - Number of partial accumulations performed by the matrix multiplication
131 h0 - Number of horizontal blocks of size (k0xn0) stored on the same output row
132 interleave_rhs - Interleave rhs matrix (1) / Do not interleave rhs matrix (0)
SiCong Libc166d52019-09-26 14:58:53 +0100133 transpose_rhs - Transpose rhs matrix (1) / Do not transpose rhs matrix (0)
SiCong Lib10181b2020-08-11 13:00:20 +0100134 export_to_cl_image_rhs - Export rhs matrix to cl_image (1) / Do not export rhs matrix to cl_image (0). Can only be true
135 with certain combinations of the GEMMParams and other configs. Please refer to CLGEMMReshapeRHSMatrixKernel
136 for more details
SiCong Li8b4c7302019-09-19 12:18:15 +0100137
138 Only the following configurations of M0, N0 and K0 are currently supported:
139 M0 = 1, 2, 3, 4, 5, 6, 7, 8
140 N0 = 2, 3, 4, 8, 16
141 K0 = 2, 3, 4, 8, 16
142 H0 >= 1
143
144 An example gemm config file looks like:
Eren Kopuz15205d92020-07-17 15:13:39 +0100145 4,4,4,1,1,1,0
146 4,4,4,3,1,0,1
SiCong Libc166d52019-09-26 14:58:53 +0100147 ...
148
149EOF
150}
151
152#######################################
153# Print gemm config file for reshaped help message
154# Globals:
155# None
156# Arguments:
157# None
158# Returns:
159# None
160#######################################
161function help_gemm_config_file_reshaped() {
162 cat >&2 << EOF
163Gemm config file (Strategy reshaped):
SiCong Lib10181b2020-08-11 13:00:20 +0100164 Gemm config file is a headerless csv file with fields separated by commas
SiCong Li8abbabd2020-04-03 12:39:41 +0100165
SiCong Lib10181b2020-08-11 13:00:20 +0100166 A gemm config is a list of 5 positive integers <m0, n0, k0, v0, h0> and 4 boolean values:
SiCong Libc166d52019-09-26 14:58:53 +0100167 m0 - Number of rows processed by the matrix multiplication
168 n0 - Number of columns processed by the matrix multiplication
169 k0 - Number of partial accumulations performed by the matrix multiplication
170 v0 - Number of vertical blocks of size (m0xk0) stored on the same output row
171 h0 - Number of horizontal blocks of size (k0xn0) stored on the same output row
172 interleave_lhs - Interleave lhs matrix (1) / Do not interleave lhs matrix (0)
173 interleave_rhs - Interleave rhs matrix (1) / Do not interleave rhs matrix (0)
174 transpose_rhs - Transpose rhs matrix but not lhs matrix (1) / Do not transpose rhs matrix but do transpose lhs matrix (0)
SiCong Lib10181b2020-08-11 13:00:20 +0100175 export_to_cl_image_rhs - Export rhs matrix to cl_image (1) / Do not export rhs matrix to cl_image (0). Can only be true
176 with certain combinations of the GEMMParams and other configs. Please refer to CLGEMMReshapeRHSMatrixKernel
177 for more details
SiCong Libc166d52019-09-26 14:58:53 +0100178
179 If rhs matrix is transposed only the following configurations are currently supported:
180 M0 = 2, 3, 4, 5, 6, 7, 8
181 N0 = 2, 3, 4, 8, 16
182 K0 = 2, 3, 4, 8, 16
183 V0 >= 1
184 H0 >= 1
185
186 If lhs matrix is transposed only the following configurations are currently supported:
187 M0 = 2, 3, 4, 8
188 N0 = 2, 3, 4, 8, 16
189 K0 = 2, 3, 4, 8, 16
190 V0 >= 1
191 H0 >= 1
192
193 An example gemm config file looks like:
Eren Kopuz15205d92020-07-17 15:13:39 +0100194 4,4,4,1,3,1,1,1,0
195 4,4,4,3,3,1,1,0,1
SiCong Li8b4c7302019-09-19 12:18:15 +0100196 ...
197
198EOF
199}
200
201#######################################
202# Print usage of this program and exit with Error
203# Globals:
204# Assumes all globals are required
205# Arguments:
206# None
207# Returns:
208# Error(1)
209#######################################
210function usage() {
211 cat >&2 << EOF
212Run gemm examples of a selected strategy, over provided tunable configurationsa and gemm shapes.
213Save the benchmark results to json files in an output directory.
214
SiCong Lib10181b2020-08-11 13:00:20 +0100215Usage: ${CMD} [-h] -s <strategy> -e <example_binary_dir> -g <gemm_shape_file> -c <gemm_config_file> [-d <data_type>] [-o <out_dir>]
SiCong Li8b4c7302019-09-19 12:18:15 +0100216
217Options:
218 -h
SiCong Libc166d52019-09-26 14:58:53 +0100219 Print help messages. If a strategy is specified with -s <strategy>, then only display messages relevant to that
220 strategy. Otherwise if no strategy is specified, display messages for all available strategies.
221
222 -s <strategy>
223 Strategy option.
224 Options: ${ALL_STRATEGY_OPTIONS[@]}.
SiCong Li8b4c7302019-09-19 12:18:15 +0100225
226 -e <example_binary_dir>
227 Path to directory that holds all example binaries
228
229 -g <gemm_shape_file>
230 Path to gemm shape csv file
231
232 -c <gemm_config_file>
233 Path to gemm config csv file
234
Eren Kopuz6977b372020-07-13 12:37:06 +0100235 -d <data_type>
236 Data type option with which to run benchmark examples
237 Default: ${DEFAULT_DATA_TYPE}
Eren Kopuz15205d92020-07-17 15:13:39 +0100238 Supported options:
239 Strategy : Data Types
240 Native : F32
241 Reshaped : F16, F32
242 Reshaped RHS Only : F16, F32
Eren Kopuz6977b372020-07-13 12:37:06 +0100243
SiCong Li8b4c7302019-09-19 12:18:15 +0100244 -o <out_dir>
245 Path to output directory that holds output json files
246 Default: ${DEFAULT_OUT_DIR}
247
248EOF
249# Print help messages about gemm shapes and various gemm configs
250$HELP && help_gemm_shape_file
SiCong Liea803482019-09-26 16:55:49 +0100251$HELP && ( [ "${STRATEGY_OPTION}" == "" ] || [ "${STRATEGY_OPTION}" == "native" ] ) && help_gemm_config_file_native
SiCong Li8b4c7302019-09-19 12:18:15 +0100252$HELP && ( [ "${STRATEGY_OPTION}" == "" ] || [ "${STRATEGY_OPTION}" == "reshaped_rhs_only" ] ) && help_gemm_config_file_reshaped_rhs_only
SiCong Libc166d52019-09-26 14:58:53 +0100253$HELP && ( [ "${STRATEGY_OPTION}" == "" ] || [ "${STRATEGY_OPTION}" == "reshaped" ] ) && help_gemm_config_file_reshaped
SiCong Li8b4c7302019-09-19 12:18:15 +0100254exit 1
255}
256
257#######################################
258# Print error message and exit with Error.
259# Globals:
260# None
261# Arguments:
262# $1 - Error message
263# Returns:
264# None
265#######################################
266function error_msg() {
267 echo "Error: $1" 1>&2
268 exit 1
269}
270
271#######################################
272# Convert string to lower-case
273# Globals:
274# None
275# Arguments:
276# target - String
277# Returns:
278# (stdout) - String in lowercase
279#######################################
280function to_lower() {
281 local target=$1
282 echo "$target" | tr '[:upper:]' '[:lower:]'
283}
284
285#######################################
286# Test if the argument is an integer
287# Globals:
288# None
289# Arguments:
290# in - Input
291# Returns:
292# true/false
293#######################################
294function is_integer() {
295 local in=$1
296 [ "$in" -eq "$in" ] 2> /dev/null
297}
298
299#######################################
SiCong Libc166d52019-09-26 14:58:53 +0100300# Test if a string is in an array of strings
301# Globals:
302# None
303# Arguments:
304# target - String to test
305# array - Array of strings to search
306# Returns:
307# true/false
308#######################################
309function arr_contains() {
310 local target=$1
311 shift
312 local array
313 array=("$@")
314 for s in "${array[@]}"
315 do
316 [ "$s" == "${target}" ] && return
317 done
318 false
319}
320
321#######################################
SiCong Lie36b5262019-10-01 19:26:00 +0100322# Run a single example with all tunable gemm configurations on all gemm parameters
SiCong Li8b4c7302019-09-19 12:18:15 +0100323# Globals:
324# OUT_DIR
SiCong Lie36b5262019-10-01 19:26:00 +0100325# OUT_EXTENSION
SiCong Li8b4c7302019-09-19 12:18:15 +0100326# EXAMPLE_BIN_DIR
327# NUM_ITERATION
328# GEMM_CONFIGS_FILE
329# GEMM_SHAPES_FILE
330# Arguments:
331# example_bin Name of the example binary to run
332# Returns:
333# None
334#######################################
335function run() {
336 local example_bin=$1
337 echo "Running all configs for ${example_bin}" 1>&2
338 local example_args
SiCong Lie36b5262019-10-01 19:26:00 +0100339 local expr_count=1
340 # Total number of experiment runs scheduled for this session
341 local total_num_experiment
342 local num_params
343 local num_configs
Gian Marco Iodiced72bd122020-08-12 18:39:55 +0100344 local match_expression_shape="^([^,]*,){3}[^,]*$"
345 local match_expression_config="^(\s*[0-9]+\s*,)+\s*[0-9]\s*$"
Eren Kopuz1e2a4512020-06-17 19:23:20 +0100346 # Don't count empty lines and lines starting with # (comments)
Gian Marco Iodiced72bd122020-08-12 18:39:55 +0100347 num_params=$( grep -E "$match_expression_shape" "${GEMM_SHAPES_FILE}" | wc -l | cut -d " " -f 1)
348 num_configs=$( grep -E "$match_expression_config" "${GEMM_CONFIGS_FILE}" | wc -l | cut -d " " -f 1)
SiCong Lie36b5262019-10-01 19:26:00 +0100349 (( total_num_experiment=${num_params} * ${num_configs} ))
350 # Time elapsed since the beginning in seconds
351 local time_elapsed_s
352 # Time estimated to finish in seconds
353 local time_est_s
354 echo "Running a total number of ${total_num_experiment} experiments" 1>&2
355
SiCong Li8b4c7302019-09-19 12:18:15 +0100356 while read gemm_shape
357 do
358 while read gemm_config
359 do
Eren Kopuz1e2a4512020-06-17 19:23:20 +0100360 # Ignore empty lines and lines starting with # (comments)
Gian Marco Iodiced72bd122020-08-12 18:39:55 +0100361 if echo "$gemm_shape" | grep -Eq "$match_expression_shape" && echo "$gemm_config" | grep -Eq "$match_expression_config";then
Eren Kopuz1e2a4512020-06-17 19:23:20 +0100362 echo "Running..." 1>&2
Eren Kopuz6977b372020-07-13 12:37:06 +0100363 example_args="${gemm_shape},${gemm_config},--type=${DATA_TYPE}"
Eren Kopuz1e2a4512020-06-17 19:23:20 +0100364 # Run experiment
365 ${EXAMPLE_BIN_DIR}/${example_bin} --example_args=${example_args} --iterations=${NUM_ITERATION} --json-file=${OUT_DIR}/${expr_count}.${OUT_EXTENSION} --instruments=OPENCL_TIMER_MS
366 # Print progress
367 print_progress ${expr_count} ${total_num_experiment}
368 # Print time statistics
369 time_elapsed_s=$SECONDS
370 echo "Time elapsed since beginning: $(( $time_elapsed_s / 60 ))m $(( $time_elapsed_s % 60 ))s" 1>&2
371 (( time_est_s=(${total_num_experiment} - ${expr_count}) * ${time_elapsed_s} / ${expr_count} ))
372 echo "Time estimated to finish: $(( $time_est_s / 60 ))m $(( $time_est_s % 60 ))s" 1>&2
373 (( expr_count++ ))
374 echo "Done." 1>&2
375 fi
SiCong Li8b4c7302019-09-19 12:18:15 +0100376 done < "${GEMM_CONFIGS_FILE}"
377 done < "${GEMM_SHAPES_FILE}"
378 echo "Finished running all configs for ${example_bin}" 1>&2
379 echo "All results saved to ${OUT_DIR}" 1>&2
380}
381
SiCong Lie36b5262019-10-01 19:26:00 +0100382#######################################
383# Print the progress of the current session
384# Globals:
385# None
386# Arguments:
387# current Current number of items
388# total Total number of items
389# Returns:
390# None
391#######################################
392function print_progress() {
393 local current
394 local total
395 current=$1
396 total=$2
397 # Width of progress bar
398 local width
399 width=20
400 (( current_width= $width * current / total ))
401 echo -n -e "Progress [" 1>&2
402 for i in $(seq 1 ${width}); do
403 if [[ $i -le ${current_width} ]]; then
404 echo -n "#" 1>&2
405 else
406 echo -n " " 1>&2
407 fi
408 done
409 echo "] $current / $total Experiments" 1>&2
410}
411
SiCong Li8b4c7302019-09-19 12:18:15 +0100412# Functions }}}
413
414# Main: Main script {{{
415# Path to directory containing all benchmark examples binaries
416EXAMPLE_BIN_DIR=""
417# Path to gemm shapes file
418GEMM_SHAPES_FILE=""
419# Path to gemm configs file
420GEMM_CONFIGS_FILE=""
SiCong Libc166d52019-09-26 14:58:53 +0100421STRATEGY_OPTION=""
Eren Kopuz6977b372020-07-13 12:37:06 +0100422# Data type to use
423DATA_TYPE=${DEFAULT_DATA_TYPE}
SiCong Li8b4c7302019-09-19 12:18:15 +0100424# Path to output directory
425OUT_DIR=${DEFAULT_OUT_DIR}
SiCong Lie36b5262019-10-01 19:26:00 +0100426# Output benchmark result file extension
427OUT_EXTENSION="gemmtuner_benchmark"
SiCong Li8b4c7302019-09-19 12:18:15 +0100428# Toggle help
429HELP=false
430
431# Obtain options
Eren Kopuz6977b372020-07-13 12:37:06 +0100432while getopts "hs:e:g:c:d:o:" opt; do
SiCong Li8b4c7302019-09-19 12:18:15 +0100433 case "$opt" in
SiCong Libc166d52019-09-26 14:58:53 +0100434 h) HELP=true ;;
SiCong Li8b4c7302019-09-19 12:18:15 +0100435 s) STRATEGY_OPTION=$(to_lower "${OPTARG}");;
SiCong Libc166d52019-09-26 14:58:53 +0100436 e) EXAMPLE_BIN_DIR="${OPTARG}";;
SiCong Li8b4c7302019-09-19 12:18:15 +0100437 g) GEMM_SHAPES_FILE="${OPTARG}";;
438 c) GEMM_CONFIGS_FILE="${OPTARG}";;
Eren Kopuz6977b372020-07-13 12:37:06 +0100439 d) DATA_TYPE="${OPTARG}";;
SiCong Li8b4c7302019-09-19 12:18:15 +0100440 o) OUT_DIR="${OPTARG}";;
441 esac
442done
443shift $((OPTIND - 1))
444
445# Lazily print usage (after arguments have been parsed)
446$HELP &&
447 usage
448
449# Parse and validate options
SiCong Libc166d52019-09-26 14:58:53 +0100450# Verify all compulsory arguments are passed in
451( [ ! -z "${STRATEGY_OPTION}" ] && [ ! -z "${EXAMPLE_BIN_DIR}" ] && [ ! -z "${GEMM_SHAPES_FILE}" ] && [ ! -z "${GEMM_CONFIGS_FILE}" ] ) ||
SiCong Li8b4c7302019-09-19 12:18:15 +0100452 usage
453
454# Verify example binaries directory exists
455[ -d "${EXAMPLE_BIN_DIR}" ] ||
456 error_msg "${EXAMPLE_BIN_DIR} does not exist."
457
458# Verify all benchmark example binaries exist
459[ -f "${EXAMPLE_BIN_DIR}/${EXAMPLE_BIN_RESHAPED_RHS_ONLY}" ] ||
460 error_msg "Cannot find ${EXAMPLE_BIN_RESHAPED_RHS_ONLY} at ${EXAMPLE_BIN_DIR}"
461
462# Verify Gemm shapes file exists
463[ -f "${GEMM_SHAPES_FILE}" ] ||
464 error_msg "Cannot find gemm shapes file ${GEMM_SHAPES_FILE}"
465
SiCong Libc166d52019-09-26 14:58:53 +0100466# Verify Gemm configs file exists
SiCong Li8b4c7302019-09-19 12:18:15 +0100467[ -f "${GEMM_CONFIGS_FILE}" ] ||
468 error_msg "Cannot find gemm configs file ${GEMM_CONFIGS_FILE}"
469
470# Verify strategy option is valid
SiCong Libc166d52019-09-26 14:58:53 +0100471arr_contains "${STRATEGY_OPTION}" "${ALL_STRATEGY_OPTIONS[@]}" ||
SiCong Li8b4c7302019-09-19 12:18:15 +0100472 error_msg "Does not support strategy ${STRATEGY_OPTION}"
473
474# Make sure existing benchmark outputs are not overwritten
475[ ! -d "${OUT_DIR}" ] ||
476 error_msg "Output directory ${OUT_DIR} already exists!"
477
478# Make output directory
SiCong Liac4c0302020-07-28 12:24:45 +0100479echo "Making output directory ${OUT_DIR}" 1>&2
480mkdir -p ${OUT_DIR} || error_msg "Failed to make output directory ${OUT_DIR}"
481date +%s > ${OUT_DIR}/start_time_unix_seconds
SiCong Li8b4c7302019-09-19 12:18:15 +0100482
483# Run selected strategy with all configurations
SiCong Lie36b5262019-10-01 19:26:00 +0100484# Restart the built-in timer
485SECONDS=0
SiCong Liea803482019-09-26 16:55:49 +0100486[ "${STRATEGY_OPTION}" == "native" ] && run $EXAMPLE_BIN_NATIVE
SiCong Li8b4c7302019-09-19 12:18:15 +0100487[ "${STRATEGY_OPTION}" == "reshaped_rhs_only" ] && run $EXAMPLE_BIN_RESHAPED_RHS_ONLY
SiCong Libc166d52019-09-26 14:58:53 +0100488[ "${STRATEGY_OPTION}" == "reshaped" ] && run $EXAMPLE_BIN_RESHAPED
SiCong Liac4c0302020-07-28 12:24:45 +0100489
490date +%s > ${OUT_DIR}/end_time_unix_seconds
SiCong Li8b4c7302019-09-19 12:18:15 +0100491# Main: Main script }}}