使用line_profiler查看api接口函数每行代码执行时间

本文介绍了一种利用Python的Line_Profiler模块来优化Flask应用的方法,通过装饰器实现对接口函数中每行代码执行时间的精确测量,帮助开发者快速定位性能瓶颈。

项目情景描述

  在restful架构风格的项目交付测试的过程中,某接口出现 请求超时导致的http 502 Bad Gateway,于是开始排查具体是接口函数中的哪行代码或函数 响应时间过长导致的502错误

刚开始的解决方法:

  土鳖式的导入 time模块进行时间计算,从而查出具体响应时间过长的位置

  如下:

 1 import time import time
 2 from flask import Flask
 3 app = Flask(__name__)
 4 
 5 app.route('/line_test')
 6 def line_test():
 7     #土鳖方法
 8     first_time=time()
 9     for item in range(5):
10         time.sleep(1)
11     #土鳖方法
12     print time()-first_time
13     for item in xrange(5):
14         time.sleep(0.5)
15     #土鳖方法
16     print time()-first_time

  方法缺点:需要大量时间编写 关于 time()的代码,最后还要删除这些代码,浪费时间

现在的解决方法:

  使用python的 line_profiler 模块,此模块是用来测试 函数 每行代码的响应时间等情况

  具体思路:将 line_profiler相关函数封装在装饰器 中 进行使用,这样 在接口请求时,则会执行此装饰器并打印出结果

  windows安装方法:https://www.lfd.uci.edu/~gohlke/pythonlibs/#line_profiler  

  代码如下:

 1 #coding:utf8
 2 from flask import Flask, jsonify
 3 import time
 4 from functools import wraps
 5 from line_profiler import LineProfiler
 6 
 7 #查询接口中每行代码执行的时间
 8 def func_line_time(f):
 9     @wraps(f)
10     def decorator(*args, **kwargs):
11         func_return = f(*args, **kwargs)
12         lp = LineProfiler()
13         lp_wrap = lp(f)
14         lp_wrap(*args, **kwargs) 
15 lp.print_stats()
16 return func_return
17 return decorator
18
19
20 app = Flask(__name__)
21
22 @app.route('/line_test')
23 @func_line_time
24 def line_test():
25 for item in range(5):
26 time.sleep(1)
27 for item in xrange(5):
28 time.sleep(0.5)
29 return jsonify({'code':200})
30
31 if __name__=='__main__':
32 app.run()

 

  当浏览器请求接口时得到的结果如下:

  

 * Running on http://127.0.0.1:5000/
Timer unit: 1e-06 s

Total time: 7.50827 s
File: /home/rgc/baidu_eye/carrier/test/flask_line_profiler_test.py
Function: line_test at line 22

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    22                                           @app.route('/line_test')
    23                                           @func_line_time
    24                                           def line_test():
    25         6         33.0      5.5      0.0      for item in range(5):
    26         5    5005225.0 1001045.0     66.7          time.sleep(1)
    27         6         31.0      5.2      0.0      for item in xrange(5):
    28         5    2502696.0 500539.2     33.3          time.sleep(0.5)
    29         1        282.0    282.0      0.0      return jsonify({'code':200})

127.0.0.1 - - [05/Mar/2018 15:58:21] "GET /line_test HTTP/1.1" 200 -

 

返回结果中 具体 含义:

 Total Time:测试代码的总运行时间 

Line:代码行号
Hits:表示每行代码运行的次数  
Time:每行代码运行的总时间  
Per Hits:每行代码运行一次的时间  
% Time:每行代码运行时间的百分比

 

 从 中便可看到 具体 26行代码执行时间最长。

方法优点:只需要添加一个装饰器,再接口函数前引用即可,删除也容易,且 装饰器可以重复使用,节省大量时间。

 

其他关于line_profiler的使用方法:

在脚本中使用此方法:

 1 #coding:utf8
 2 import cgi
 3 import time
 4 from line_profiler import LineProfiler
 5 
 6 def test2():
 7     print 'hello!test2()'
 8 
 9 def test1():
10     html='''<script>alert("you are a good boy!&I like you")</scrpit>'''
11     test2()
12     escape_html=cgi.escape(html)
13     for item in range(5):
14         time.sleep(1)
15     print escape_html
16 
17 if __name__=='__main__': 18 lp=LineProfiler() 19 #同时显示函数每行所用时间和调用函数每行所用时间,加入add_function() 20 lp.add_function(test2) 21 lp_wrap=lp(test1)
#如果被测函数有入参,下面一行为 lp_wrap(被测函数入参)
22 lp_wrap() 23 lp.print_stats()

 

 直接运行显示结果:

 * Running on http://127.0.0.1:5000/
Timer unit: 1e-06 s

Total time: 7.50827 s
File: /home/rgc/baidu_eye/carrier/test/flask_line_profiler_test.py
Function: line_test at line 22

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    22                                           @app.route('/line_test')
    23                                           @func_line_time
    24                                           def line_test():
    25         6         33.0      5.5      0.0      for item in range(5):
    26         5    5005225.0 1001045.0     66.7          time.sleep(1)
    27         6         31.0      5.2      0.0      for item in xrange(5):
    28         5    2502696.0 500539.2     33.3          time.sleep(0.5)
    29         1        282.0    282.0      0.0      return jsonify({'code':200})

127.0.0.1 - - [05/Mar/2018 15:58:21] "GET /line_test HTTP/1.1" 200 -

 相对于另一种使用方法:

  1.在需要测试的函数前添加装饰器  @profile

  2.启动程序 $kernprof -l -v test.py

无需 实际运行项目时删除@profile,如需单独测试此函数,直接使用 if __name__=='__main__': 即可,这样在其他函数引用时,不会执行line_profiler相关代码。

 

相关博客:

http://blog.youkuaiyun.com/guofangxiandaihua/article/details/77825524

  

转载于:https://www.cnblogs.com/rgcLOVEyaya/p/RGC_LOVE_YAYA_603days.html

下面的代码是干什么用的,请生成说明注释,同时还有什么改进: 【# -*- coding: utf-8 -*- bl_info = { "name": "1插件启动耗时分析器 (最终色彩校正版)", "author": "提取、汉化与修正 by Gemini", "version": (9, 1, 0), "blender": (3, 0, 0), "location": "编辑 > 偏好设置 > 插件", "description": "【最终版】采用版本一的完整日志算法,确保数据完整性;结合版本二的稳定UI,并校正了UI色块颜色,使其鲜艳分明。", "warning": "此插件会直接修改Blender的安装文件,属于高风险操作,请务必在理解其原理后再使用!", "doc_url": "", "category": "Development", } import bpy import os import sys import tempfile from bpy.props import StringProperty, FloatVectorProperty, CollectionProperty from bpy.types import Operator, AddonPreferences, PropertyGroup # =================================================================================================== # 核心辅助函数 (无改动) # =================================================================================================== def find_addon_utils_py(): for path in sys.path: candidate = os.path.join(path, "addon_utils.py") if os.path.isfile(candidate): return candidate try: import bpy as _bpy blender_scripts_path = os.path.join(os.path.dirname(_bpy.__file__), "scripts", "modules", "addon_utils.py") if os.path.isfile(blender_scripts_path): return blender_scripts_path except (ImportError, AttributeError): pass return None def already_injected(lines): return any("#startup_profiler_helper" in line for line in lines) def get_version_specific_log_path(): try: config_dir = bpy.utils.user_resource('CONFIG'); return os.path.join(config_dir, "blender_startup_log.txt") except: version_str = ".".join(map(str, bpy.app.version)); return os.path.join(tempfile.gettempdir(), f"blender_startup_log_{version_str}.txt") # =================================================================================================== # 核心注入算法 (采用版本一的完整、健壮逻辑,无改动) # =================================================================================================== class TIMEPROFILER_OT_InjectCode(Operator): bl_idname = "time_profiler.inject_code";bl_label = "注入计时代码";bl_description = "【高风险操作】\n修改Blender核心文件 addon_utils.py。\n点击此按钮会先清空旧日志,然后注入代码以进行全新的记录" def execute(self, context): log_path = get_version_specific_log_path() if os.path.exists(log_path): try: os.remove(log_path) self.report({'INFO'}, f"已清空旧日志文件: {log_path}") except OSError as e: self.report({'WARNING'}, f"无法清空旧日志文件: {e}") addon_utils_path = find_addon_utils_py() if not addon_utils_path: self.report({'ERROR'}, "未能定位到核心文件 addon_utils.py!操作失败。"); return {'CANCELLED'} try: with open(addon_utils_path, "r", encoding="utf-8") as f: lines = f.readlines() except Exception as e: self.report({'ERROR'}, f"读取核心文件失败: {e}"); return {'CANCELLED'} if already_injected(lines): self.report({'INFO'}, "检测到计时代码已存在,无需重复注入。"); return {'CANCELLED'} new_lines, injected, in_enable_func, return_inserted = [], False, False, False safe_log_path = get_version_specific_log_path().replace('\\', '/') for line in lines: if not injected and line.strip().startswith("def enable("): new_lines.extend([ line, " import time #startup_profiler_helper\n", f" LOG_FILE_PATH = r'{safe_log_path}' #startup_profiler_helper\n", " start_time = time.time()#startup_profiler_helper\n"]) injected, in_enable_func = True, True; continue if in_enable_func and not return_inserted and line.strip().startswith("return mod"): new_lines.extend([ " elapsed = time.time() - start_time#startup_profiler_helper\n", " log_message = f\"{elapsed:.4f}s | {module_name}\\n\"#startup_profiler_helper\n", " print(f\"[计时分析] 耗时: {log_message.strip()}\")#startup_profiler_helper\n", " try:#startup_profiler_helper\n", " with open(LOG_FILE_PATH, 'a', encoding='utf-8') as f:#startup_profiler_helper\n", " f.write(log_message)#startup_profiler_helper\n", " except Exception:#startup_profiler_helper\n", " pass#startup_profiler_helper\n", line]) return_inserted, in_enable_func = True, False; continue new_lines.append(line) if not injected or not return_inserted: self.report({'ERROR'}, "未能在核心文件中找到合适的注入点,操作失败。"); return {'CANCELLED'} try: with open(addon_utils_path, "w", encoding="utf-8") as f: f.writelines(new_lines) self.report({'INFO'}, f"成功注入计时代码到: {addon_utils_path}") except PermissionError: self.report({'ERROR'}, "权限不足,无法写入核心文件。请尝试以管理员身份运行Blender。"); return {'CANCELLED'} except Exception as e: self.report({'ERROR'}, f"写入核心文件时发生错误: {e}"); return {'CANCELLED'} return {'FINISHED'} class TIMEPROFILER_OT_RemoveCode(Operator): bl_idname = "time_profiler.remove_code";bl_label = "移除计时代码";bl_description = "【恢复操作】\n从 addon_utils.py 中移除之前注入的计时代码" def execute(self, context): addon_utils_path = find_addon_utils_py() if not addon_utils_path: self.report({'ERROR'}, "未能定位到核心文件 addon_utils.py!操作失败。"); return {'CANCELLED'} try: with open(addon_utils_path, "r", encoding="utf-8") as f: lines = f.readlines() except Exception as e: self.report({'ERROR'}, f"读取核心文件失败: {e}"); return {'CANCELLED'} if not already_injected(lines): self.report({'INFO'}, "未检测到已注入的计时代码,无需移除。"); return {'CANCELLED'} new_lines = [line for line in lines if "#startup_profiler_helper" not in line] try: with open(addon_utils_path, "w", encoding="utf-8") as f: f.writelines(new_lines) self.report({'INFO'}, f"已成功从 {addon_utils_path} 移除计时代码。") except PermissionError: self.report({'ERROR'}, "权限不足,无法写入核心文件。请尝试以管理员身份运行Blender。"); return {'CANCELLED'} except Exception as e: self.report({'ERROR'}, f"写入核心文件时发生错误: {e}"); return {'CANCELLED'} return {'FINISHED'} class TIMEPROFILER_OT_OpenLogFolder(Operator): bl_idname = "time_profiler.open_log_folder";bl_label = "打开日志文件夹";bl_description = "在系统的文件浏览器中直接打开存放日志文件的文件夹" def execute(self, context): log_dir = os.path.dirname(get_version_specific_log_path()) if not os.path.isdir(log_dir): try: os.makedirs(log_dir, exist_ok=True) except OSError as e: self.report({'ERROR'}, f"无法创建目录: {e}"); return {'CANCELLED'} bpy.ops.wm.path_open(filepath=log_dir); return {'FINISHED'} # =================================================================================================== # 报告获取与显示 # =================================================================================================== class TIMEPROFILER_OT_FetchReport(Operator): bl_idname = "time_profiler.fetch_report";bl_label = "加载/刷新报告";bl_description = "读取累积的日志文件,并显示去重后的分析报告" def execute(self, context): prefs = context.preferences.addons[__name__].preferences log_path = get_version_specific_log_path() if not os.path.exists(log_path): prefs.report_items.clear() prefs.header_str = "未找到日志文件。请先注入代码并重启Blender以生成报告。" self.report({'WARNING'}, prefs.header_str) return {'CANCELLED'} try: with open(log_path, 'r', encoding='utf-8') as f: lines = f.readlines() unique_addons = {} for line in lines: if not line.strip() or '|' not in line: continue try: time_part, name_part = [x.strip() for x in line.strip().split('|', 1)] elapsed_time = float(time_part.rstrip('s')) if name_part not in unique_addons or elapsed_time > unique_addons[name_part][0]: unique_addons[name_part] = (elapsed_time, time_part) except (ValueError, IndexError): continue if not unique_addons: raise ValueError("日志文件为空或格式不正确。") processed_list = [(data[0], data[1], name) for name, data in unique_addons.items()] processed_list.sort(key=lambda x: x[0], reverse=True) version_str = ".".join(map(str, bpy.app.version)) prefs.header_str = f"Blender {version_str} 启动耗时报告 (共 {len(processed_list)} 个插件)" prefs.report_items.clear() for elapsed_time, time_str, name_str in processed_list: item = prefs.report_items.add() item.time_str = time_str item.name_str = name_str # ★★★ 核心修正:使用纯正、鲜艳的RGB值 ★★★ color_val = (0.7, 0.7, 0.7) # 默认 - 中灰色 if elapsed_time >= 3.0: color_val = (1.0, 0.0, 1.0) # 紫色/洋红 elif elapsed_time >= 1.0: color_val = (1.0, 0.0, 0.0) # 红色 elif elapsed_time >= 0.5: color_val = (1.0, 1.0, 0.0) # 黄色 elif elapsed_time >= 0.2: color_val = (0.0, 0.7, 1.0) # 蓝色/青色 elif elapsed_time >= 0.05: color_val = (0.0, 1.0, 0.0) # 绿色 item.color = color_val except Exception as e: prefs.report_items.clear() prefs.header_str = f"读取或解析日志时出错: {e}" self.report({'ERROR'}, prefs.header_str) return {'CANCELLED'} return {'FINISHED'} class TIMEPROFILER_ReportItem(PropertyGroup): time_str: StringProperty() name_str: StringProperty() color: FloatVectorProperty(subtype='COLOR', size=3, min=0.0, max=1.0, default=(0.7, 0.7, 0.7)) class TimeProfilerAddonPreferences(AddonPreferences): bl_idname = __name__ report_items: CollectionProperty(type=TIMEPROFILER_ReportItem) header_str: StringProperty(default="请点击“加载/刷新报告”按钮来查看。") def draw(self, context): layout = self.layout box = layout.box(); row = box.row(); row.label(text="第一步:管理计时代码", icon='MODIFIER') alert_box = box.box(); row = alert_box.row(); row.alert = True row.label(text="警告:此操作会修改Blender安装文件,请谨慎使用!", icon='ERROR') row = box.row(align=True); row.scale_y = 1.5 row.operator("time_profiler.inject_code", icon='MOD_TIME') row.operator("time_profiler.remove_code", icon='X') box2 = layout.box(); row = box2.row(align=True) row.label(text="第二步:分析报告", icon='TEXT') row.operator("time_profiler.open_log_folder", icon='FILE_FOLDER', text="") row.operator("time_profiler.fetch_report", icon='FILE_REFRESH', text="") report_box = box2.box(); report_box.label(text=self.header_str) if self.report_items: report_box.separator() main_col = report_box.column(align=True) for item in self.report_items: row = main_col.row(align=True) split = row.split(factor=0.25, align=True) left_part = split.row(align=True) left_part.prop(item, "color", text="") left_part.label(text=item.time_str) split.label(text=item.name_str) # =================================================================================================== # 注册与注销 # =================================================================================================== classes = ( TIMEPROFILER_ReportItem, TIMEPROFILER_OT_InjectCode, TIMEPROFILER_OT_RemoveCode, TIMEPROFILER_OT_OpenLogFolder, TIMEPROFILER_OT_FetchReport, TimeProfilerAddonPreferences, ) def register(): for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) if __name__ == "__main__": register()】
最新发布
09-09
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值