1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# 4# Copyright (c) 2023 Huawei Device Co., Ltd. 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 18import os 19import shutil 20import subprocess 21import sys 22import time 23import traceback 24import zipfile 25 26import json5 27 28import performance_config 29 30 31class PerformanceBuild(): 32 def __init__(self, config_input, mail_obj): 33 self.config = None 34 self.first_line_in_avg_excel = "" 35 self.preview_all_time_dic = {} 36 self.preview_avg_time_dic = {} 37 self.time_avg_dic = {} 38 self.all_time_dic = {} 39 self.size_avg_dic = {} 40 self.all_size_dic = {} 41 self.mail_helper = None 42 self.mail_msg = '' 43 self.mail_helper = mail_obj 44 self.config = config_input 45 self.prj_name = '' 46 self.timeout = 1800 47 self.error_log_str = '' 48 49 @staticmethod 50 def append_into_dic(key, value, dic): 51 if key not in dic: 52 dic[key] = [] 53 dic[key].append(value) 54 55 @staticmethod 56 def add_code(code_path, start_pos, end_pos, code_str, lines): 57 with open(code_path, 'r+', encoding='UTF-8') as modified_file: 58 content = modified_file.read() 59 add_str_end_pos = content.find(end_pos) 60 if add_str_end_pos == -1: 61 print(f'Can not find code : {end_pos} in {code_path}, please check config') 62 return 63 add_str_start_pos = content.find(start_pos) 64 if add_str_start_pos == -1: 65 if lines == 0: 66 return 67 add_str_start_pos = add_str_end_pos 68 content_add = "" 69 for i in range(lines, 0, -1): 70 if "%d" in code_str: 71 content_add = content_add + code_str % i 72 else: 73 content_add = content_add + code_str 74 content = content[:add_str_start_pos] + content_add + content[add_str_end_pos:] 75 modified_file.seek(0) 76 modified_file.write(content) 77 modified_file.truncate() 78 79 @staticmethod 80 def add_row(context): 81 return rf'<tr align="center">{context}</tr>' 82 83 @staticmethod 84 def add_td(context): 85 return rf'<td>{context}</td>' 86 87 @staticmethod 88 def add_th(context): 89 return rf'<th width="30%">{context}</th>' 90 91 @staticmethod 92 def test_type_title(context): 93 return rf'<tr><th bgcolor="PaleGoldenRod" align="center" colspan="3">{context}</th></tr>' 94 95 @staticmethod 96 def app_title(context): 97 return rf'<th bgcolor="SkyBlue" colspan="3"><font size="4">{context}</font></th>' 98 99 def start(self): 100 self.init() 101 self.start_test() 102 self.write_mail_msg() 103 os.chdir(self.config.project_path) 104 105 def init(self): 106 if self.config.ide == performance_config.IdeType.DevEco: 107 os.environ['path'] = self.config.node_js_path + ";" + os.environ['path'] 108 os.chdir(self.config.project_path) 109 os.environ['path'] = os.path.join(self.config.jbr_path, "bin") + ";" + os.environ['path'] 110 os.environ['JAVA_HOME'] = self.config.jbr_path 111 self.config.cmd_prefix = os.path.join(self.config.project_path, self.config.cmd_prefix) 112 self.config.debug_package_path = os.path.join(self.config.project_path, self.config.debug_package_path) 113 self.config.release_package_path = os.path.join(self.config.project_path, self.config.release_package_path) 114 self.config.incremental_code_path = os.path.join(self.config.project_path, self.config.incremental_code_path) 115 self.config.json5_path = os.path.join(self.config.project_path, self.config.json5_path) 116 if self.config.developing_test_data_path: 117 self.config.build_times = 3 118 else: 119 cmd = [self.config.node_js_path, self.config.cmd_prefix, "--stop-daemon"] 120 print(f'cmd: {cmd}') 121 subprocess.Popen(cmd, stderr=sys.stderr, 122 stdout=sys.stdout).communicate(timeout=self.timeout) 123 124 def add_incremental_code(self, lines): 125 PerformanceBuild.add_code(self.config.incremental_code_path, 126 self.config.incremental_code_start_pos, 127 self.config.incremental_code_end_pos, 128 self.config.incremental_code_str, 129 lines) 130 131 def revert_incremental_code(self): 132 self.add_incremental_code(0) 133 134 def reset(self): 135 self.first_line_in_avg_excel = "" 136 self.preview_all_time_dic = {} 137 self.preview_avg_time_dic = {} 138 self.time_avg_dic = {} 139 self.all_time_dic = {} 140 self.size_avg_dic = {} 141 self.all_size_dic = {} 142 self.error_log_str = '' 143 self.revert_incremental_code() 144 145 def clean_project(self): 146 if not self.config.developing_test_data_path: 147 cmd = [self.config.node_js_path, self.config.cmd_prefix, "clean"] 148 print(f'cmd: {cmd}') 149 subprocess.Popen(cmd, stderr=sys.stderr, 150 stdout=sys.stdout).communicate(timeout=self.timeout) 151 152 def get_bytecode_size(self, is_debug): 153 if self.config.developing_test_data_path: 154 # test data for size 155 PerformanceBuild.append_into_dic("ets/mudules.abc rawSize", 44444, self.all_size_dic) 156 PerformanceBuild.append_into_dic("ets/mudules.abc Compress_size", 33333, self.all_size_dic) 157 PerformanceBuild.append_into_dic("ets/mudules2.abc rawSize", 44444, self.all_size_dic) 158 PerformanceBuild.append_into_dic("ets/mudules2.abc Compress_size", 33333, self.all_size_dic) 159 return 160 package_path = self.config.debug_package_path if is_debug else self.config.release_package_path 161 package = zipfile.ZipFile(package_path) 162 extension_name = ".abc" if self.config.ide == performance_config.IdeType.DevEco else ".dex" 163 for info in package.infolist(): 164 if info.filename.endswith(extension_name): 165 name_str1 = info.filename + " rawSize" 166 name_str2 = info.filename + " compress_size" 167 PerformanceBuild.append_into_dic(name_str1, info.file_size, self.all_size_dic) 168 PerformanceBuild.append_into_dic(name_str2, info.compress_size, self.all_size_dic) 169 170 def get_build_time(self, report_path, time_dic): 171 event_obj = None 172 with open(report_path, 'r+', encoding='UTF-8') as report: 173 event_obj = json5.load(report)['events'] 174 if not event_obj: 175 raise Exception('Open report json failed') 176 found_error = False 177 for node in event_obj: 178 if node['head']['type'] == "log" and node['additional']['logType'] == 'error': 179 self.error_log_str = self.error_log_str + node['head']['name'] 180 found_error = True 181 if found_error: 182 continue 183 184 build_time = 0 185 task_name = node['head']['name'] 186 if node['head']['type'] == "mark": 187 if node['additional']['markType'] == 'history': 188 build_time = (node['body']['endTime'] - node['body']['startTime']) / 1000000000 189 task_name = "total build cost" 190 else: 191 continue 192 elif node['head']['type'] == "continual": 193 build_time = node['additional']['totalTime'] / 1000000000 194 else: 195 continue 196 PerformanceBuild.append_into_dic(task_name, build_time, time_dic) 197 if found_error: 198 raise Exception('Build Failed') 199 200 def collect_preview_build_data(self, report_path): 201 self.get_build_time(report_path, self.preview_all_time_dic) 202 203 def collect_build_data(self, is_debug, report_path): 204 self.get_build_time(report_path, self.all_time_dic) 205 self.get_bytecode_size(is_debug) 206 207 def get_report_path(self, cmd_suffix): 208 reports_before = [] 209 report_dir = '.hvigor/report' 210 if os.path.exists(report_dir): 211 reports_before = os.listdir(report_dir) 212 cmd = [self.config.node_js_path, self.config.cmd_prefix, *cmd_suffix] 213 print(f'cmd: {cmd}') 214 subprocess.Popen(cmd, stderr=sys.stderr, 215 stdout=sys.stdout).communicate(timeout=self.timeout) 216 report_path = (set(os.listdir(report_dir)) - set(reports_before)).pop() 217 return os.path.join(report_dir, report_path) 218 219 def preview_build(self, is_debug): 220 cache_path = os.path.join(self.config.project_path, *self.config.preview_cache_path) 221 if os.path.exists(cache_path): 222 shutil.rmtree(cache_path) 223 cmd_suffix = self.config.preview_debug_suffix if is_debug else self.config.preview_release_suffix 224 report_path = self.get_report_path(cmd_suffix) 225 self.collect_preview_build_data(report_path) 226 227 def build(self, is_debug): 228 cmd_suffix = self.config.cmd_debug_suffix if is_debug else self.config.cmd_release_suffix 229 report_path = self.get_report_path(cmd_suffix) 230 self.collect_build_data(is_debug, report_path) 231 232 def start_build(self, is_debug): 233 if self.config.developing_test_data_path: 234 # test data 235 self.collect_build_data(is_debug, os.path.join(os.path.dirname(__file__), 236 self.config.developing_test_data_path)) 237 return True 238 239 self.build(is_debug) 240 return True 241 242 def get_millisecond(self, time_string): 243 if self.config.ide != performance_config.IdeType.DevEco and not self.config.developing_test_data_path: 244 return int(time_string) 245 else: 246 cost_time = 0 247 res = time_string.split(" min ") 248 target_str = "" 249 if len(res) > 1: 250 cost_time = int(res[0]) * 60000 251 target_str = res[1] 252 else: 253 target_str = res[0] 254 res = target_str.split(" s ") 255 if len(res) > 1: 256 cost_time = cost_time + int(res[0]) * 1000 257 target_str = res[1] 258 else: 259 target_str = res[0] 260 res = target_str.split(" ms") 261 if len(res) > 1: 262 cost_time = cost_time + int(res[0]) 263 return cost_time 264 265 def cal_preview_build_avg_time(self): 266 self.first_line_in_avg_excel = self.first_line_in_avg_excel + "\n" 267 for key in self.preview_all_time_dic: 268 task_count = len(self.preview_all_time_dic[key]) 269 has_task = True 270 if task_count != 2 * self.config.build_times: 271 if task_count == self.config.build_times: 272 has_task = False 273 else: 274 continue 275 # average of first build 276 sum_build_time = 0 277 for i in range(0, self.config.build_times): 278 index = i * 2 279 if not has_task: 280 self.preview_all_time_dic[key].insert(index + 1, 0) 281 sum_build_time = sum_build_time + self.preview_all_time_dic[key][index] 282 cost = round(sum_build_time / self.config.build_times, 2) 283 PerformanceBuild.append_into_dic(key, cost, self.preview_avg_time_dic) 284 # average of incremental build 285 sum_build_time = 0 286 for i in range(1, len(self.preview_all_time_dic[key]), 2): 287 sum_build_time = sum_build_time + self.preview_all_time_dic[key][i] 288 cost = round(sum_build_time / self.config.build_times, 2) 289 PerformanceBuild.append_into_dic(key, cost, self.preview_avg_time_dic) 290 291 def cal_incremental_avg_time(self, all_time_dic, avg_time_dic): 292 self.first_line_in_avg_excel = self.first_line_in_avg_excel + "\n" 293 for key in all_time_dic: 294 task_count = len(all_time_dic[key]) 295 has_task = True 296 if task_count != 2 * self.config.build_times: 297 if task_count == self.config.build_times: 298 has_task = False 299 else: 300 continue 301 # average of first build 302 sum_build_time = 0 303 for i in range(0, self.config.build_times): 304 index = i * 2 305 if not has_task: 306 all_time_dic[key].insert(index + 1, 0) 307 sum_build_time = sum_build_time + all_time_dic[key][index] 308 cost = round(sum_build_time / self.config.build_times, 2) 309 PerformanceBuild.append_into_dic(key, cost, avg_time_dic) 310 # average of incremental build 311 sum_build_time = 0 312 for i in range(1, len(all_time_dic[key]), 2): 313 sum_build_time = sum_build_time + all_time_dic[key][i] 314 cost = round(sum_build_time / self.config.build_times, 2) 315 PerformanceBuild.append_into_dic(key, cost, avg_time_dic) 316 317 def cal_incremental_avg_size(self): 318 total_raw_size = [] 319 total_compressed_size = [] 320 for i in range(0, self.config.build_times * 2): 321 total_raw_size.append(0) 322 total_compressed_size.append(0) 323 for key in self.all_size_dic: 324 if "raw" in key: 325 total_raw_size[i] += self.all_size_dic[key][i] 326 else: 327 total_compressed_size[i] += self.all_size_dic[key][i] 328 self.all_size_dic["total_raw_size"] = total_raw_size 329 self.all_size_dic["total_compressed_size"] = total_compressed_size 330 for key in self.all_size_dic: 331 # sizes should be the same, just check 332 full_first_size = self.all_size_dic[key][0] 333 for i in range(0, len(self.all_size_dic[key]), 2): 334 if full_first_size != self.all_size_dic[key][i]: 335 full_first_size = -1 336 break 337 PerformanceBuild.append_into_dic(key, full_first_size, self.size_avg_dic) 338 339 incremental_first_size = self.all_size_dic[key][1] 340 for i in range(1, len(self.all_size_dic[key]), 2): 341 if incremental_first_size != self.all_size_dic[key][i]: 342 incremental_first_size = -1 343 break 344 PerformanceBuild.append_into_dic(key, incremental_first_size, self.size_avg_dic) 345 346 def cal_incremental_avg(self): 347 if self.config.preview_build: 348 self.cal_incremental_avg_time(self.preview_all_time_dic, self.preview_avg_time_dic) 349 self.cal_incremental_avg_time(self.all_time_dic, self.time_avg_dic) 350 self.cal_incremental_avg_size() 351 352 def add_preview_build_time_pic_data(self, dic, is_debug): 353 for key in dic: 354 if "total" in key: 355 preview_build_time = dic[key][0] 356 break 357 self.mail_helper.add_pic_data(self.config.name, is_debug, [preview_build_time], 2) 358 359 def add_time_pic_data(self, dic, is_debug): 360 for key in dic: 361 if "total" in key: 362 full_time = dic[key][0] 363 incremental_time = dic[key][1] 364 break 365 self.mail_helper.add_pic_data(self.config.name, is_debug, [full_time, incremental_time], 0) 366 367 def add_size_pic_data(self, dic, is_debug): 368 for key in dic: 369 full_size = dic[key][0] 370 self.mail_helper.add_pic_data(self.config.name, is_debug, [full_size], 3) 371 372 def write_mail_files(self, dic): 373 if not hasattr(self.config, 'show_time_detail_filter'): 374 return '' 375 msg = '' 376 rows = '' 377 first_row = "" 378 first_line_res = self.first_line_in_avg_excel.replace("\n", "").split(",") 379 for i in first_line_res: 380 first_row += PerformanceBuild.add_th(i) 381 rows += PerformanceBuild.add_row(first_row) 382 383 show_dic = [] 384 for k in self.config.show_time_detail_filter: 385 if k in dic: 386 show_dic.append(k) 387 for key in show_dic: 388 content_row = PerformanceBuild.add_th(key) 389 for v in dic[key]: 390 content_row += PerformanceBuild.add_td(f'{v} s') 391 rows += PerformanceBuild.add_row(content_row) 392 msg += rows 393 return msg 394 395 def write_from_dic(self, file_path, first_line, dic): 396 content_list = [] 397 if first_line: 398 content_list.append(first_line) 399 for key in dic: 400 content_list.append(key) 401 for v in dic[key]: 402 content_list.append(",") 403 content_list.append(str(v)) 404 content_list.append("\n") 405 content = "".join(content_list) 406 self.mail_helper.add_logs_file(file_path, content.encode()) 407 408 def write_logs_from_dic(self, path_prefix, log_filename, source_dic, need_first_line): 409 file_path = self.config.output_split.join((path_prefix, log_filename)) 410 file_path = os.path.join(self.prj_name, file_path) 411 first_line = self.first_line_in_avg_excel if need_first_line else None 412 self.write_from_dic(file_path, first_line, source_dic) 413 return 414 415 def generate_full_and_incremental_results(self, is_debug): 416 path_prefix = self.config.output_split.join( 417 (self.config.ide_filename[self.config.ide - 1], 418 self.config.debug_or_release[0 if is_debug else 1], 419 self.config.build_type_of_log[0]) 420 ) 421 temp_mail_msg = "" 422 # write all build time log 423 self.write_logs_from_dic(path_prefix, self.config.log_filename[2], self.all_time_dic, False) 424 # write avg build time, html msg and picture data 425 self.write_logs_from_dic(path_prefix, self.config.log_filename[3], self.time_avg_dic, True) 426 temp_mail_msg += self.write_mail_files(self.time_avg_dic) 427 # write preview avg build time 428 if self.config.preview_build: 429 self.add_preview_build_time_pic_data(self.preview_avg_time_dic, is_debug) 430 self.add_time_pic_data(self.time_avg_dic, is_debug) 431 # write all size of abc log 432 self.write_logs_from_dic(path_prefix, self.config.log_filename[0], self.all_size_dic, False) 433 # write avg abc size, html msg and picture data 434 self.write_logs_from_dic(path_prefix, self.config.log_filename[1], self.size_avg_dic, True) 435 self.add_size_pic_data(self.size_avg_dic, is_debug) 436 437 # write html message 438 if self.config.send_mail and hasattr(self.config, 'show_time_detail_filter'): 439 temp_mail_msg = '<table width="100%" border=1 cellspacing=0 cellpadding=0 align="center">' + \ 440 PerformanceBuild.app_title(self.config.name + (' Debug' if is_debug else ' Release')) + \ 441 temp_mail_msg + '</table>' 442 self.mail_msg += temp_mail_msg 443 444 def error_handle(self, is_debug, log_type): 445 build_mode = 'Debug' if is_debug else 'Release' 446 log_type_str = 'full_build' if log_type == performance_config.LogType.FULL else 'incremental_build' 447 self.mail_helper.add_failed_project(self.prj_name, build_mode, log_type_str) 448 save_name = build_mode + '_' + os.path.basename(self.config.error_filename) 449 print(self.error_log_str) 450 self.mail_helper.add_logs_file(os.path.join(self.prj_name, save_name), 451 self.error_log_str) 452 453 def full_and_incremental_build(self, is_debug): 454 log_type = performance_config.LogType.FULL 455 try: 456 self.reset() 457 self.prj_name = os.path.basename(self.config.project_path) 458 self.first_line_in_avg_excel = self.first_line_in_avg_excel + ",first build,incremental build" 459 for i in range(self.config.build_times): 460 self.clean_project() 461 print(f"fullbuild: {'Debug' if is_debug else 'Release'}, {i + 1}/{self.config.build_times}") 462 log_type = performance_config.LogType.FULL 463 if self.config.preview_build: 464 self.preview_build(is_debug) 465 self.start_build(is_debug) 466 self.add_incremental_code(1) 467 print(f"incremental: {'Debug' if is_debug else 'Release'}, {i + 1}/{self.config.build_times}") 468 log_type = performance_config.LogType.INCREMENTAL 469 self.start_build(is_debug) 470 self.revert_incremental_code() 471 self.cal_incremental_avg() 472 self.generate_full_and_incremental_results(is_debug) 473 except Exception as e: 474 err_msg = traceback.format_exc() 475 self.error_log_str = f'error:\n{self.error_log_str}\n{err_msg}' 476 self.error_handle(is_debug, log_type) 477 478 def start_test(self): 479 self.full_and_incremental_build(True) 480 self.full_and_incremental_build(False) 481 self.reset() 482 483 def write_mail_msg(self): 484 if self.config.send_mail: 485 self.mail_helper.add_msg(self.mail_msg) 486 487 488def run(config_input, mail_obj): 489 start_time = time.time() 490 PerformanceBuild(config_input, mail_obj).start() 491 print("Test [%s] finished at: %s\n" \ 492 "total cost: %ds" 493 % (os.path.basename(config_input.project_path), 494 time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), 495 time.time() - start_time))