本篇博客以开源代码RT-Thread为例,描述了如何使用python扫描统计代码中频繁修改的函数,帮助我们发现系统中需求变化和BUG制造的重灾区。
需求背景
最近在学习设计模式时,印象深刻的一句话就是“要将设计模式应用在不稳定、频繁修改的地方,在变化处应用招式”,那么什么样的地方是频繁变化的?找出变化点最好的办法就是模块或者系统的架构师可以根据经验预测识别出系统的需求热点、发展趋势,指导我们进行重构。但是我想有没有其他办法,统计出我们可能注意不到的潜在变化热点呢?这些函数可能预示着我们的代码质量不佳、频繁出现BUG,或者这块代码需求经常变更,面临剧烈的修改冲击。这样的代码,我们要把它找出来,主动思考进行合理的架构设计来抵御需求变化或者BUG产生。
关键技术
要实现上面的功能,我们要摸底下面的几个关键信息能不能搞定:
- 代码修改记录,有了修改记录才能统计热点。
- 代码修改记录筛选,我们只需要精确到函数级别,精确到某一行代码是否频繁修改没有意义,一般修改不会只改一行。
- 自动化统计手段,依靠人工统计是不现实的。
上面的三个问题,提前尝试了一下解决方法如下:
-
代码记录自然想到从git获取,git一般查询git log命令获取修改记录,尝试了一下如下:
$ git logcommit cc2d54ff9fa0b4e958d4a46dacc1106abac9432e (HEAD -> master, origin/master)Merge: 31b6533ba f94ffe28eAuthor: Bernard Xiong <bernard.xiong@gmail.com>Date: Thu Jun 27 15:47:17 2019 +0800Merge pull request #2781 from jinsheng20/Timer增加基础定时器驱动commit 31b6533baa1d52f070ce43fecaf0183af9ef7299Merge: ef6a4aee9 7d0907185Author: Bernard Xiong <bernard.xiong@gmail.com>Date: Thu Jun 27 13:49:16 2019 +0800Merge pull request #2811 from enkiller/nfs[components][dfs][nfs] 修复连接 Linux NFS服务器认证错误的问题可以看到,git log可以看到提交的修改记录,每个修改记录都有一个独一无二的commit id。但是不幸的是,我们并不能从这里面看出具体修改了什么,没关系,使用git show即可,git show commit_id filename还可以指定特定文件。
$ git show 7d0907185837152b918bd4a4b749d453171f6a9fcommit 7d0907185837152b918bd4a4b749d453171f6a9fAuthor: tangyuxin <462747508@qq.com>Date: Wed Jun 26 11:33:41 2019 +0800[components][dfs][nfs] ÐÞ¸´Linux NFS·þÎñÆ÷ÈÏÖ¤ÎÊÌâdiff --git a/components/dfs/filesystems/nfs/SConscript b/components/dfs/filesystems/nfs/SConscriptems/nfs/SConscriptindex 8f1e6defc..f830dfc75 100644--- a/components/dfs/filesystems/nfs/SConscript+++ b/components/dfs/filesystems/nfs/SConscript@@ -6,6 +6,8 @@ cwd = GetCurrentDir()src = Glob('*.c') + Glob('rpc/*.c')CPPPATH = [cwd]+SrcRemove(src, ['rpc/auth_none.c'])+group = DefineGroup('Filesystem', src, depend = ['RT_USING_DFS', 'RT_USING_DFS_NFS'], CPPPATH = CPPPATH)Return('group')diff --git a/components/dfs/filesystems/nfs/dfs_nfs.c b/components/dfs/filesystems/nfs/dfs_nfs.cindex f36531456..e60f6f98c 100644--- a/components/dfs/filesystems/nfs/dfs_nfs.c+++ b/components/dfs/filesystems/nfs/dfs_nfs.c@@ -225,7 +225,7 @@ static nfs_fh3 *get_dir_handle(nfs_filesystem *nfs, const char *name)copy_handle(handle, &nfs->current_handle);}- while ((file = strtok_r(NULL, "/", &path)) != NULL && path[0] != 0)+ while ((file = strtok_r(NULL, "/", &path)) != NULL && path && path[0] != 0) -
有了记录之后,我们要从中筛选出我们感兴趣的函数名,准确从文本中识别出函数名个人感觉不是一件容易的事情,函数名字的形式和参数都多变,不过仔细观察上面的比较记录,会发现git自己能够识别出函数名,并专门使用@@标示出来了,如下的void SystemClock_Config(void)函数,我们只需要筛选符合@@的行就可以了。例子里给了一个
@@.*@@\s((\w+)\s+)+[\*,&]*\s*(\w+)\s*\(,效果不是特别好,你可以自己根据函数特点修改,修改后可能需要重新改下最后拼接res_str的地方。@@ -160,14 +165,14 @@ void SystemClock_Config(void)RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0};- /**Configure LSE Drive Capability+ /** Configure LSE Drive Capability*/HAL_PWR_EnableBkUpAccess();- /**Configure the main internal regulator output voltage+ /** Configure the main internal regulator output voltage*/__HAL_RCC_PWR_CLK_ENABLE();__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);- /**Initializes the CPU, AHB and APB busses clocks+ /** Initializes the CPU, AHB and APB busses clocks*/RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSI|RCC_OSCILLATORTYPE_HSE;RCC_OscInitStruct.HSEState = RCC_HSE_ON; -
自动化工具方面这种文本处理的肯定使用python是最快的,git可以使用gitpython库,使用pip install gitpython安装即可,避免使用原生git命令行的繁琐。文本筛选肯定使用正则表达式最方便,可以实现我们想要的功能。
具体脚本
# -- coding: utf-8 --
# author: tanxiaoyaoimport thread
import gitimport reimport sysimport threadingimport time
reload(sys)sys.setdefaultencoding("utf-8")
# 全局变量定义thread_num = 10 # 使用的CPU线程数commit_num = 1000 # 需要遍历的commit数量repo = git.Repo.init("D:\\02.Code\\02.Reference\\02.OS\\rt-thread") # 仓库路径,根据自己的实际填写folder_path = "." # 需要扫描的子文件夹路径diff_regex = r"@@.*@@\s((\w+)\s+)+[\*,&]*\s*(\w+)\s*\(" # 修改函数名匹配正则表达式,根据自己的需要修改max_modified_filenum = 30 # 允许的单次提交修改的最大文件数,排除分支合并的commit
# 正则匹配commit_regex = r"commit\s(\w{40})" # commit匹配正则表达式,当前简单匹配40个hash码
# 全局线程互斥变量fun_dict= {}lock = threading.Lock()global final_thread_countfinal_thread_count = 0
# 获取正则匹配结果def get_regex_match(src_str, regex_str): partten = re.compile(regex_str) match = partten.findall(src_str) return match
# 获取小于特定修改文件数的commit,支持多线程def get_commit_lessthan(match, thread_id): fo = open("fo" + str(thread_id) + ".txt", "w") ok_list = [] count = 0
# 筛选修改文件数符合要求的commit for commit in match: filelist_str = repo.git.show(commit, stat=True) name_patt = re.compile(commit_regex) name_match = name_patt.findall(filelist_str) if len(name_match) <= max_modified_filenum: ok_list.append(commit)
# 获取每个commit的具体修改内容 for commit in ok_list: if commit is "": continue diff_str = get_commit_content(commit,"*") # 从内容中提取修改的函数 change_fun_list = get_change_funcs(diff_str) fo.write(diff_str) count += 1
lock.acquire() try: # 添加到全局数组 add_funlist2dict(change_fun_list) print "Thd" + str(thread_id) + "\t" + commit + "_" + str(count) + "/" + str(len(ok_list)) finally: lock.release()
fo.close() global final_thread_count final_thread_count += 1
# 按指定数量拆分数组def chunks(l, n): for i in xrange(0, len(l), n): yield l[i:i+n]
# 获取commit列表def get_commit_list(): log = repo.git.log(folder_path) all_commit = get_regex_match(log, commit_regex) commit_groups = list(chunks(all_commit, commit_num / thread_num)) print commit_groups
# 开始多个线程扫描修改commit,修改函数名 try: for index in range(0, min(thread_num, len(commit_groups))): thread.start_new_thread(get_commit_lessthan, (commit_groups[index], index,) ) except: print "start thread fail!"
return
# 获取COMMIT中文件的修改内容def get_commit_content(commit_id, file_name): return repo.git.show(commit_id, file_name)
# 提取差异中的函数def get_change_funcs(diff_log): return get_regex_match(diff_log, diff_regex)
# 将函数加入全局dictdef add_funlist2dict(list): for fun in list: if fun_dict.has_key(fun): fun_dict[fun] = fun_dict[fun] + 1 else: fun_dict[fun] = 1
# dict按照value值排序def sort_funcdict(dict): items = dict.items() backitems = [[v[1], v[0]] for v in items] backitems.sort(None, None, True) return [[v[1], v[0]] for v in backitems]
if __name__ == '__main__': # 开始处理 get_commit_list() # 等待所有线程处理完毕 while(final_thread_count < thread_num): time.sleep(1)
# 输出结果 resfile = open("result.txt", "w") sorted_list = sort_funcdict(fun_dict) for func_name, times in sorted_list: res_str = " times:" + str(times) + "\t" + func_name[1].strip() + " " + func_name[2].strip() + "\n" resfile.write(res_str) resfile.close()运行效果
提示:依赖gitpython库,使用pip install gitpython安装。运行后结果输出在脚本同级目录的result.txt中
times:77 void SystemClock_Config times:50 if GetDepend times:36 int main times:19 void MX_GPIO_Init times:18 void stm32_dma_config times:18 typedef void times:18 HAL_StatusTypeDef HAL_RCC_OscConfig times:17 void HAL_MspInit times:16 typedef struct times:14 rt_err_t es32f0x_configure times:14 int rt_hw_spi_bus_init times:13 void rt_schedule times:13 void HAL_UART_MspDeInit times:13 void HAL_SMARTCARD_IRQHandler times:13 rt_err_t drv_control times:12 void uart_isr times:12 HAL_StatusTypeDef SMBUS_Slave_ISR times:11 void phy_monitor_thread_entry times:11 void MX_USART1_UART_Init times:11 def PrepareBuilding times:11 HAL_StatusTypeDef HAL_SPI_TransmitReceive times:10 void HAL_UART_MspInit times:10 void HAL_CAN_IRQHandler times:10 rt_err_t rt_sensor_open times:10 rt_err_t rt1052_pin_irq_enable如果仓库修改记录很多,运行时间会比较长,可以在脚本中限制一下统计次数,运行速度主要消耗在git比较两个commit上。