Create plots of k6 test result
[infra/cicd.git] / jjb / onap / cps / prepare-k6-performance-tests-plots.sh
1 #!/bin/bash
2 #
3 # Copyright 2024 Nordix Foundation.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 #     http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16 #
17
18 set -o errexit  # Exit on most errors
19 set -o nounset  # Disallow expansion of unset variables
20 set -o pipefail # Use last non-zero exit code in a pipeline
21 #set -o xtrace   # Trace logging - disabled to avoid producing gigabytes of logs
22
23 #############################################################################################################################
24 ################################################ F U N C T I O N S ##########################################################
25 #############################################################################################################################
26
27 # Define the list of "Test Case + Condition" k6 test names to search for in the logs.
28 # The Test Case and Condition concatenated with \s* to represent zero or more whitespace characters.
29 # Later in this script can use Perl-compatible regular expressions (PCRE) can match to pattern with grep -P flag.
30 # The tests which measure http_req_failed excluded since nothing to plot if the rate is always zero.
31 k6_test_names=(
32     "1-create-cmhandles.js\s*http_req_duration"
33     "2-wait-for-cmhandles-to-be-ready.js\s*iteration_duration"
34     "3-passthrough-read.js\s*ncmp_overhead"
35     "4-id-search-no-filter.js\s*http_req_duration"
36     "5-search-no-filter.js\s*http_req_duration"
37     "6-id-search-public-property.js\s*http_req_duration"
38     "7-search-public-property.js\s*http_req_duration"
39     "8-id-search-module.js\s*http_req_duration"
40     "9-search-module.js\s*http_req_duration"
41     "10-mixed-load-test.js\s*http_req_duration{scenario:id_search_module}"
42     "10-mixed-load-test.js\s*http_req_duration{scenario:passthrough_read}"
43     "10-mixed-load-test.js\s*http_req_duration{scenario:cm_search_module}"
44     "11-delete-cmhandles.js\s*http_req_duration"
45 )
46
47 JENKINS_JOB_URL="https://jenkins.nordix.org/job/onap-cps-performance-test-k6"
48
49 latestBuildToRecord=""
50 consoleText=""
51 latestRecordedBuild=""
52 timestampOfLatestRecordedBuild=""
53
54 # Get last completed build number
55 # The number has not been plotted on the graphs yet
56 getLastCompletedBuildNumber() {
57   curl -s "${JENKINS_JOB_URL}/lastCompletedBuild/buildNumber"
58 }
59
60 # Get the last recorded build number from local workspace of the jenkins
61 # The number has already been plotted on the graphs
62 getLastRecordedBuildNumber() {
63   cd "$WORKSPACE"
64   local file_name="1-create-cmhandles_http_req_duration.txt"
65
66   # Check if the file exists
67   if [ -f "$file_name" ]; then
68     # Get the last line from the file
69     local last_line=$(tail -n 1 "$file_name")
70     local left_side=$(echo "$last_line" | cut -d ',' -f 2)
71     echo "$left_side"
72   else
73     echo "0"
74   fi
75 }
76
77 # Get all builds numbers
78 getAllBuildNumbers() {
79   curl -s "${JENKINS_JOB_URL}/api/json?tree=allBuilds\[id\]" | jq -r '.allBuilds[].id' | sort -n
80 }
81
82 # Get the console text of k6 performance job
83 getConsoleText() {
84   buildToRead=$1
85   consoleURL="${JENKINS_JOB_URL}/${buildToRead}/consoleText"
86   consoleText=$(curl -s "$consoleURL")
87 }
88
89 # Replace special characters with underscore
90 # The return value will be used to create a file for each test combination
91 replace_special_characters() {
92   local pattern=".js\s*"
93   local text="$1"
94
95   # Escape special characters in the pattern
96   local escaped_pattern=$(echo "$pattern" | sed 's/[\.*^$]/\\&/g')
97
98   # Replace the pattern with underscores using sed
99   local result=$(echo "$text" | sed "s/$escaped_pattern/_/g")
100
101   echo "$result"
102 }
103
104 # Get and record the latest k6-job-results with the build number for all tests
105 getAndRecordPerformanceJobResultForBuild() {
106   buildNumber="$1"
107   getConsoleText "$buildNumber"
108   # Loop through each test names
109   for k6_test_name in "${k6_test_names[@]}"; do
110     getAndRecordDataResults "$consoleText" "$k6_test_name" "$(replace_special_characters "$k6_test_name").txt" "$buildNumber"
111   done
112 }
113
114 # Get and record the latest k6-job-results for a single test
115 getAndRecordDataResults() {
116   consoleText=$1
117   patternToMatch=$2
118   dataFile=$3
119   buildNumber=$4
120
121   matched_line=""
122   limit_value=""
123   actual=""
124
125   # Find the text combination (Test Case + Condition) in the console log
126   if matched_line=$(echo "$consoleText" | grep -P "${patternToMatch}"); then
127     # Find the first-occurred number in the line matched (limit)
128     limit_value=$(echo "$matched_line" | awk '{for(i=1;i<=NF;i++) if($i ~ /^[0-9]+$/) {print $i; exit}}')
129     # Find the second-occurred number in the line matched (actual)
130     actual=$(echo "$matched_line" | awk '{count=0; for(i=1; i<=NF; i++) if($i ~ /^[0-9]+$/) {count++; if(count==2) {print $i; exit}}}')
131   fi
132
133   # Add a new entry which is a combination of limit, build-number, and actual into related file
134   touch "$dataFile"
135   lastLine=$(tail -n 1 "$dataFile")
136   newLine="$limit_value,$buildNumber,$actual"
137   if [ -z "$actual" ]; then
138     # No entry found for this build, the default added as zero
139     echo "$limit_value,$buildNumber,0" >>"$dataFile"
140     recordLatestRecordedBuild "$buildNumber"
141   elif [ "$newLine" == "$lastLine" ]; then
142     # Entry already exists
143     recordLatestRecordedBuild "$buildNumber"
144   else
145     # New entry added to the file
146     echo "$limit_value,$buildNumber,$actual" >>"$dataFile"
147     recordLatestRecordedBuild "$buildNumber"
148   fi
149 }
150
151 # Format the date and time of the latest build
152 recordLatestRecordedBuild() {
153   latestBuildToRecord="$1"
154   timestampOfLatestRecordedBuild=$(curl -s "${JENKINS_JOB_URL}/${latestBuildToRecord}/api/json?tree=timestamp" | jq -r '.timestamp')
155   formattedTimestampOfLatestRecordedBuild=$(date -d "@$((timestampOfLatestRecordedBuild / 1000))" "+%B %e, %Y at %H:%M")
156   latestRecordedBuild=$latestBuildToRecord
157 }
158
159 # Plot the image (graph) in png format
160 buildPlotImage() {
161   dataFile="$1"
162   chartFileName="$2"
163
164   # Read the first line of the file
165   first_line=$(head -n 1 "$dataFile")
166
167   # Set upper limit of the graphs to %20 to have more space above the plot
168   ten_percent=1.2
169   limit=$(echo "$first_line" | cut -d ',' -f 1)
170   limit=$(expr "$limit * $ten_percent" | bc)
171
172   # Create a temporary Gnuplot script
173   cat <<EOT >gnuplot_script.gp
174 set datafile separator ","
175 set terminal pngcairo size 1500,600
176 set output "${chartFileName}"
177 set xlabel "Build"
178 set ylabel "Limit (ms)"
179 set yrange [0 : ${limit} < *]
180 plot '$dataFile' using 2:3:xtic(sprintf("%d", column(2))) with linespoints title "measured", \
181      '$dataFile' using 2:1 with lines linestyle 2 title "limit"
182 EOT
183
184   # Run the temporary Gnuplot script
185   gnuplot gnuplot_script.gp
186
187   # Remove the temporary Gnuplot script
188   rm gnuplot_script.gp
189 }
190
191 # Builds html file
192 buildHtmlReport() {
193   # use indirect expansion to get all elements of the array
194   categoryName=("${!1}")
195   reportTitle="$2"
196   outputFile="$3"
197   cat <<EOT >"$outputFile"
198     <!DOCTYPE html>
199     <html>
200     <head>
201     <title>$reportTitle</title>
202     </head>
203     <body>
204       <h1 style="text-align: center;">$reportTitle</h1>
205       <h4>Last updated for performance job build no. $latestRecordedBuild on $formattedTimestampOfLatestRecordedBuild</h4>
206       <table align="center">
207 EOT
208 # Loop through the tests to generate the HTML rows which consists of the plot-image
209 for test_name_in_category in "${categoryName[@]}"; do
210     test_name_in_category="$(replace_special_characters "$test_name_in_category")"
211     cat <<EOF >>"$outputFile"
212         <tr> <!-- Row for $test_name_in_category -->
213             <td align="center" style="padding: 10px;">
214                 <figcaption>"$test_name_in_category"</figcaption>
215                 <img src="$test_name_in_category.png" width="750" height="300">
216             </td>
217         </tr>
218 EOF
219 done
220 # Close the HTML file
221 cat <<EOT >>"$outputFile"
222       </table>
223         <p>The k6 job at 7:15 a.m. once a day, providing performance metrics.</p>
224         <p>The following graphs being updated at 7:45 a.m. once a day.</p>
225         <p>Successful performance tests job build adds new data, but even if a build fails, existing data is retained.</p>
226     </body>
227   </html>
228 EOT
229 }
230
231 #############################################################################################################################
232 ################################################ M A I N ####################################################################
233 #############################################################################################################################
234
235 # Install dependencies
236 sudo apt-get install -y bc gnuplot jq
237
238 # Download data from CPS performance Jenkins job
239 cd "$WORKSPACE"
240 if [ -z "$(ls -A)" ]; then
241   # If workspace is empty, pull data from all previous performance job runs
242   for buildNumber in $(getAllBuildNumbers); do
243     getAndRecordPerformanceJobResultForBuild "$buildNumber"
244   done
245 else
246   # Append new data from latest jobs run
247   lastCompletedBuildNumber=$(getLastCompletedBuildNumber)
248   lastRecordedBuildNumber=$(getLastRecordedBuildNumber)
249   # Check if last completed build number is greater than last recorded build number
250   if [ "$lastCompletedBuildNumber" -gt "$lastRecordedBuildNumber" ]; then
251     for ((i = lastRecordedBuildNumber + 1; i <= lastCompletedBuildNumber; i++)); do
252       # Process the new builds which hasn't been recorded yet
253       getAndRecordPerformanceJobResultForBuild "$i"
254     done
255   else
256     echo "No new builds to process."
257   fi
258 fi
259
260 # Limit the plots to last 100 builds for each test
261 for k6_test_name in "${k6_test_names[@]}"; do
262   k6_file_name="$(replace_special_characters "$k6_test_name")"
263   tail -n 100 "$k6_file_name.txt" > file.tmp && mv file.tmp "$k6_file_name.txt"
264 done
265
266 # Plot image files in png format
267 for k6_test_name in "${k6_test_names[@]}"; do
268   k6_file_name="$(replace_special_characters "$k6_test_name")"
269   buildPlotImage "$k6_file_name.txt" "$k6_file_name.png"
270 done
271
272 # Build html page
273 buildHtmlReport k6_test_names[@] "k6 tests performance review" "k6TestsPerformanceReview.html"