随着移动互联网的快速发展,人工智能在移动端上的应用越来越广泛,集团内端智能在图像识别、视频检测、数据计算等核心场景发挥着重要作用。而在开发阶段,Python 毋庸置疑是算法进行研发的首选语言。但在移动端上,进行算法的部署、调试、验证,仍处在“刀耕火种”的时代,目前算法主要通过在代码中插入日志,验证程序的运行逻辑和结果。
通过打日志当然也能验证结果和定位问题,但一旦工程稍微复杂点,生产效率会非常低。因此,在 MNN 工作台之中(点击文末阅读原文,前往 MNN 官网:www.mnn.zone 下载)嵌入了端侧 Python 调试能力。经常使用Python的同学一定熟悉pdb模块,它是Python官方标准库提供的交互式代码调试器,和任何一门语言提供的调试能力一样,pdb提供了源代码行级别的设置断点、单步执行等常规调试能力,是Python开发的一个很重要的工具模块。
今天就让我们来重点分析下官方pdb模块源码,看看其调试功能的底层技术原理。
原理
从cpython源码中可以看到,pdb模块并非c实现的内置模块,而是纯Python实现和封装的模块。核心文件是pdb.py,它继承自bdb和cmd模块:
class Pdb(bdb.Bdb, cmd.Cmd): ...
复制代码
基本原理:利用cmd模块定义和实现一系列的调试命令的交互式输入,基于sys.settrace插桩跟踪代码运行的栈帧,针对不同的调试命令控制代码的运行和断点状态,并向控制台输出对应的信息。
cmd模块主要是提供一个控制台的命令交互能力,通过raw_input/readline这些阻塞的方法实现输入等待,然后将命令交给子类处理决定是否继续循环输入下去,就和他主要的方法名runloop一样。
cmd是一个常用的模块,并非为pdb专门设计的,pdb使用了cmd的框架从而实现了交互式自定义调试。
bdb提供了调试的核心框架,依赖sys.settrace进行代码的单步运行跟踪,然后分发对应的事件(call/line/return/exception)交给子类(pdb)处理。bdb的核心逻辑在对于调试命令的中断控制,比如输入一个单步运行的”s“命令,决定是否需要继续跟踪运行还是中断等待交互输入,中断到哪一帧等。
基本流程
- pdb启动,当前frame绑定跟踪函数trace_dispatch
def trace_dispatch(self, frame, event, arg):
if self.quitting:
return # None
if event == 'line':
return self.dispatch_line(frame)
if event == 'call':
return self.dispatch_call(frame, arg)
if event == 'return':
return self.dispatch_return(frame, arg)
if event == 'exception':
...
复制代码
- 每一帧的不同事件的处理都会经过中断控制逻辑,主要是stop_here(line事件还会经过break_here)函数,处理后决定代码是否中断,需要中断到哪一行
- 如需要中断,触发子类方法user_#event,子类通过interaction实现栈帧信息更新,并在控制台打印对应的信息,然后执行cmdloop让控制台处于等待交互输入
def interaction(self, frame, traceback):
self.setup(frame, traceback) # 当前栈、frame、local vars
self.print_stack_entry(self.stack[self.curindex])
self.cmdloop()
self.forget()
复制代码
- 用户输入调试命令如“next”并回车,首先会调用set_#命令,对stopframe、returnframe、stoplineno进行设置,它会影响中断控制```stop_here``的逻辑,从而决定运行到下一帧的中断结果.
def _set_stopinfo(self, stopframe, returnframe, stoplineno=0):
self.stopframe = stopframe
self.returnframe = returnframe
self.quitting = 0
# stoplineno >= 0 means: stop at line >= the stoplineno
# stoplineno -1 means: don't stop at all
self.stoplineno = stoplineno
复制代码
- 对于调试过程控制类的命令,一般do_#命令都会返回1,这样本次runloop立马结束,下次运行到某一帧触发中断会再次启动runloop(见步骤3);对于信息获取类的命令,do_#命令都没有返回值,保持当前的中断状态。
- 代码运行到下一帧,重复步骤3 <