Lua中如何实现类似gdb的断点调试--05优化断点信息数据结构
在上一篇04优化钩子事件处理中,我们在钩子函数中引入了call和return事件的处理,对性能进行了优化。
细心的同学可能已经发现了,我们的hook函数中call事件和line都需要对整个断点表进行遍历,这其中其实是存在着一些冗余的。因为call事件只关心函数是否有断点,而line事件则只关心本函数内有哪些断点。所以我们可以想办法优化一下断点信息的数据结构,进一步提升性能。
源码已经上传Github,欢迎watch/star。
本博客已迁移至CatBro's Blog,那里是我自己搭建的个人博客,欢迎关注。
实现分析
原来的断点表status.bptable
是以断点id为键的数组,我们需要通过断点id来快速删除断点,所以status.bptable
表还是保留。
但是在钩子函数中,我们并不关心断点的id,相反我们关心断点属于哪个函数。只有当程序执行到有断点的函数时,我们才需要处理line事件。所以,很自然地可以想到,我们应该新增一个以函数为键的断点表。我们把这个新表定义如下:
status.funcbpt = {} -- 以函数为键的断点表
那么新表的值应该是什么呢?因为一个函数中可能有多个断点,而在line事件中我们又需要比较当前行是否是断点行,所以我们把新表status.funcbpt
的值也设计成一个表,该表的键是断点行号,值为断点id。因为断点行号并不连续,所以status.funcbpt[func]
表不是一个序列,为了快速获取断点个数,我们在表中额外加了一个特殊的num
字段保存该函数中的断点个数。
这样我们就可以在call事件中快速地判断当前函数是否有断点,在line事件中快速地判断当前行是否是断点行。
另外,之所以将断点id作为status.funcbpt[func]
表的值,则是为了保留从status.funcbpt
访问status.bptable
中对应断点的能力(status.bptable
表中元素因为包含了func
和line
字段,所以也可以访问到status.funcbpt
对应断点)。
这么说可能不直观,我们来看个例子。
假设我们的bptable
表中func函数添加了两个断点如下:
bptable[1] = {func = func, line = 10}
bptable[2] = {func = func, line = 20}
对应的在funcbpt
表中的操作如下:
funcbpt[func] = {} -- 构造表
funcbpt[func][10] = 1 -- 函数func,行号10,断点id为1
funcbpt[func].num = 1 -- 该函数第一个断点
funcbpt[func][20] = 2 -- 函数func,行号20,断点id为2
funcbpt[func].num = funcbpt[func].num + 1 -- 断点个数+1
OK,理清楚了status.funcbpt
的数据结构设计,实现起来就简单了。
添加断点
我们先从设置断点函数入手修改代码。新增的代码已经用中括号括起来了,有两个部分。其中前面的部分先根据函数和行号检查是否已经设置过同一个断点了,如果是的话,直接返回之前设置的断点id。
后面的部分,在断点保存到s.bptable
表之后,也要保存到我们新增的表s.funcbpt
表中,行号line作为键,新断点id作为值。如果设置的是该函数的第一个断点,需要先进行表的初始化构造,所以操作稍有不同。
local function setbreakpoint(func, line)
local s = status
if type(func) ~= "function" or type(line) ~= "number" then
return nil
end
[[ 新增代码开始 ]]
-- 已经设置了相同的断点
if s.funcbpt[func] and s.funcbpt[func][line] then
return s.funcbpt[func][line]
end
[[ 新增代码结束 ]]
s.bpid = s.bpid + 1
s.bpnum = s.bpnum + 1
s.bptable[s.bpid] = {func = func, line = line}
[[ 新增代码开始 ]]
if s.funcbpt[func] then -- 该函数已经有断点了
s.funcbpt[func].num = s.funcbpt[func].num + 1
s.funcbpt[func][line] = s.bpid
else -- 该函数第一个断点
s.funcbpt[func] = {}
s.funcbpt[func].num = 1
s.funcbpt[func][line] = s.bpid
end
[[ 新增代码结束 ]]
if s.bpnum == 1 then -- 全局第一个断点
debug.sethook(hook, "c") -- 设置钩子"call"事件
end
return s.bpid --> 返回断点id
end
删除断点
相应地,在删除断点的时候,我们也需要把s.funcbpt
表中对应的断点删除。首先根据断点id,从s.bptable
表中获取到断点的函数和行号,从而找到s.funcbpt
表中对应的断点。现将断点个数减1,然后将对应断点删除(防止后续line事件找到该断点)。如果该函数已经没有断点了,那么将s.funcbpt[func]
表本身也删除(防止后续call事件以为该函数还有断点)。
local function removebreakpoint(id)
local s = status
if s.bptable[id] == nil then
return
end
[[ 新增代码开始 ]]
local func = s.bptable[id].func
local line = s.bptable[id].line
s.funcbpt[func].num = s.funcbpt[func].num - 1
s.funcbpt[func][line] = nil
if s.funcbpt[func].num == 0 then
s.funcbpt[func] = nil
end
[[ 新增代码结束 ]]
s.bptable[id] = nil
s.bpnum = s.bpnum - 1
if s.bpnum == 0 then
debug.sethook() -- 移除钩子
end
end
钩子函数
然后来修改钩子函数的处理,钩子的函数的修改主要有两处,分别是call事件和line事件。我们先来看call事件的修改:
local function hook (event, line)
local s = status
if event == "call" or event == "tail call" then
local func = debug.getinfo(2, "f").func
- for _, v in pairs(s.bptable) do
- -- found breakpoint in current function
- if v.func == func then
- if event == "call" then
- s.stackdepth = s.stackdepth + 1
- end
- s.stackinfos[s.stackdepth] =
- {func = func, hasbreak = true}
- debug.sethook(hook, "crl") -- 添加"line"事件
- return
- end
- end
- -- 没有断点
- if event == "call" then
+ if event == "call" then -- 对于尾调用,直接覆盖
s.stackdepth = s.stackdepth + 1
end
- s.stackinfos[s.stackdepth] = {func = func, hasbreak = false}
- debug.sethook(hook, "cr") -- 去掉"line"事件
+ -- found breakpoint in current function
+ if s.funcbpt[func] then
+ s.stackinfos[s.stackdepth] = {func = func, hasbreak = true}
+ debug.sethook(hook, "crl") -- 添加"line"事件
+ else -- no breakpoints found
+ s.stackinfos[s.stackdepth] = {func = func, hasbreak = false}
+ debug.sethook(hook, "cr") -- 去掉"line"事件
+ end
elseif event == "return" or event == "tail return" then
主要的改动就是从原本的遍历s.bptable
表来查找是否有断点,改成了直接通过检查s.funcbpt[func]
是否不为nil
来判断是否有断点。这里直接从遍历一个表,优化成了一个查表操作。
第二处是line事件的修改,下面是修改前的代码:
-- 省略
elseif event == "line" then
for _, v in pairs(s.bptable) do
if v.func == s.stackinfos[s.stackdepth].func
and v.line == line then
if not s.funcinfos[v.func] then
s.funcinfos[v.func] = debug.getinfo(2, "nS")
end
local info = s.funcinfos[v.func]
local prompt = string.format("%s (%s)%s %s:%d\n",
info.what, info.namewhat, info.name, info.short_src, line)
io.write(prompt)
debug.debug()
end
end
end
-- 省略
下面是修改后的代码,除了增加了一些局部变量简化代码之外,也是将原本的遍历s.bptable
表来判断当前行是否是断点行,改成了直接通过检查s.funcbpt[curfunc][line]
是否不为nil
来判断当前行是否是断点行。这里也将遍历一个表的动作,优化成了一个查表操作。
elseif event == "line" then
local curfunc = s.stackinfos[s.stackdepth].func
local funcbp = s.funcbpt[curfunc]
assert(funcbp)
if funcbp[line] then
if not s.funcinfos[curfunc] then
s.funcinfos[curfunc] = debug.getinfo(2, "nS")
end
local info = s.funcinfos[curfunc]
local prompt = string.format("%s (%s)%s %s:%d\n",
info.what, info.namewhat, info.name, info.short_src, line)
io.write(prompt)
debug.debug()
end
end
复杂度分析
我们仍然假设代码执行的总行数为L
,断点数N=n*b
,其中n
为有断点的函数个数,b
为平均每个函数的断点数,断点所在函数平均行数为l
,断点所在函数平均调用次数为c
,总的函数调用次数C
。
完全没优化前复杂度为O(L*N)
,上一篇的事件处理优化后的复杂度为O(C*N+c*l*N)
,而本篇的数据结构优化之后复杂度进一步缩小为O(C+c*l)
。
测试多函数多断点
先来测试一下修改之后断点功能是否正常,编写一个测试脚本如下。我们对foo函数和bar函数分别添加了两个断点,其中foo函数第一个断点添加了两次用于测试重复添加的情况。
local ldb = require "luadebug"
local setbp = ldb.setbreakpoint
local rmbp = ldb.removebreakpoint
pv = ldb.printvarvalue
sv = ldb.setvarvalue
ptb = ldb.printtraceback
local function foo ()
local a = 1
end
local function bar ()
local b = 1
end
local id1 = setbp(foo, 9)
assert(id1 == 1)
local id1 = setbp(foo, 9)
assert(id1 == 1)
local id2 = setbp(foo, 10)
local id3 = setbp(bar, 13)
local id4 = setbp(bar, 14)
foo()
bar()
rmbp(id1)
rmbp(id2)
rmbp(id3)
rmbp(id4)
foo()
bar()
运行脚本,4个断点情况都能正常跑到。删除断点后再调用foo和bar函数,不再碰到断点。
$ lua test.lua
Lua (local)foo test.lua:9
lua_debug> cont
Lua (local)foo test.lua:10
lua_debug> cont
Lua (local)bar test.lua:13
lua_debug> cont
Lua (local)bar test.lua:14
lua_debug> cont
$
性能比对
我们再来做个简单的测试,看看我们优化的效果。编写如下测试脚本,foo函数模拟较长的程序,然后在另一个函数上加个断点(为了设置hook)。我们分别使用优化前的luadebug.lua
和优化后的luadebug.lua
进行测试。
local ldb = require "luadebug"
local setbp = ldb.setbreakpoint
local rmbp = ldb.removebreakpoint
local function foo ()
local a = 1
for i = 1, 10000000 do
a = a + 1
end
end
local function bar ()
end
local id1 = setbp(bar, 13)
foo()
优化前
使用优化前的实现,运行这个脚本用了40s
$ time lua test2.lua
lua test2.lua 39.54s user 0.16s system 99% cpu 39.957 total
优化后
使用优化后的实现,则只花了0.109s,相差了接近400倍,可见我们的优化效果还是很明显的。
$ time lua test2.lua
lua test2.lua 0.10s user 0.00s system 97% cpu 0.109 total
Lua中如何实现类似gdb的断点调试--05优化断点信息数据结构的更多相关文章
- Lua中如何实现类似gdb的断点调试—07支持通过函数名称添加断点
我们之前已经支持了通过函数来添加断点,并且已经支持了行号的检查和自动修正.但是通过函数来添加断点有一些限制,如果在当前的位置无法访问目标函数,那我们就无法对其添加断点. 于是,本篇我们将扩展断点设置的 ...
- Lua中如何实现类似gdb的断点调试--01最小实现
说到Lua代码调试,最常用的方法应该就是加一堆print进行打印.print大法虽好,但其缺点也是显而易见的.比如效率低下,需要修改原有函数内部代码,在每个需要的地方添加print语句,运行一次只能获 ...
- Lua中如何实现类似gdb的断点调试—09支持动态添加和删除断点
前面已经支持了几种不同的方式添加断点,但是必须事先在代码中添加断点,在使用上不是那么灵活方便.本文将支持动态增删断点,只需要开一开始引入调试库即可,后续可以在调试过程中动态的添加和删除断点.事不宜迟, ...
- Lua中如何实现类似gdb的断点调试--02通用变量打印
在前一篇01最小实现中,我们实现了Lua断点调试的的一个最小实现.我们编写了一个模块,提供了两个基本的接口:设置断点和删除断点. 虽然我们已经支持在断点进行变量的打印,但是需要自己指定层数以及变量索引 ...
- Lua中如何实现类似gdb的断点调试—08支持通过包名称添加断点
在前一篇中我们支持了通过函数名称来添加断点,我们同时也提到了在Lua中一个函数的名称的并不是确定的.准确的说,Lua中的函数并没有名称,所谓名称其实是保存这个函数值的变量的名称. 于是通过函数名称添加 ...
- Lua中如何实现类似gdb的断点调试--04优化钩子事件处理
在第一篇的01最小实现中,我们实现了一个断点调试的最小实现,在设置钩子函数时只加了line事件,显然这会对性能有很大的影响.而后来两篇02通用变量打印和03通用变量修改及调用栈回溯则是提供了一些辅助的 ...
- Lua中如何实现类似gdb的断点调试—06断点行号检查与自动修正
前面两篇我们对性能做了一个优化,接下来继续来丰富调试器的特性. 我们前面提到过,函数内并不是所有行都是有效行,空行和注释行就不是有效行.我们之前在添加断点的时候,并没有对行号进行检查,任何行号都能成功 ...
- Lua中如何实现类似gdb的断点调试--03通用变量修改及调用栈回溯
在前面两篇01最小实现及02通用变量打印中,我们已经实现了设置断点.删除断点及通用变量打印接口. 本篇将继续新增两个辅助的调试接口:调用栈回溯打印接口.通用变量设置接口.前者打印调用栈的回溯信息,后者 ...
- 2018-8-10-VisualStduio-打断点调试和不打断点调试有什么区别
title author date CreateTime categories VisualStduio 打断点调试和不打断点调试有什么区别 lindexi 2018-08-10 19:16:52 + ...
随机推荐
- Nacos服务注册与发现的2种实现方法!
Spring Cloud Alibaba 技术体系中的 Nacos,提供了两个重要的功能:注册中心(服务注册与发现)功能和配置中心功能. 其中注册中心解决了微服务调用中,服务提供者和服务调用者的解耦, ...
- 「NOI十联测」黑暗
「NOI十联测」黑暗 \(n\) 个点的无向图,每条边都可能存在,一个图的权值是连通块个数的 \(m\) 次方,求所有可能的图的权值和.(n≤30000,m≤15) 令\(ans[n][m]\)为n个 ...
- Java Thread.currentThread()和This的区别
感谢原文作者:王婷婷-Smily 原文链接:https://blog.csdn.net/dfshsdr/article/details/92760135 缘由 很多人认为多线程中的Thread.cur ...
- 通过ANT生成MANIFEST.MF中的Class-Path属性
原文地址:http://reason2003.iteye.com/blog/1627353 之前做一个项目,主程序打包成一个jar文件,因为用到了很多第三方的lib包,所以直接通过java命令运行ja ...
- 《Effective Python》笔记——第1章 用Pythonic方式来思考
一. 遵循PEP8风格指南. PEP8是对python代码格式而编订的风格指南.地址:https://www.python.org/dev/peps/pep-0008/ 个人觉得不一定完全按照PEP8 ...
- 第11、12章等待方法和alter框处理
11.等待方法 time sleep webdriver implicitly_wait() 设置浏览器等待时间 WebDriverWait 等待条件满足或超时后退出 12.alter对话框处理 ...
- tarjan2
反过来调过去,我还是感觉没学明白缩点 讲一个有向图中的所有强连通分量缩成一个点后,构成的新图是一个DAG. 一个点所在的强连通分量一定被该点所在DFS搜索树所包含 树上的边大致分为:树枝边,前向边(从 ...
- 3、架构--cp、scp、rsync、实时监控与同步
笔记 1.晨考 1.VPN的搭建步骤 2.vpn中的iptables是什么作用? 网络转发 2.昨日问题 1.yum源问题 2.VPN链接正常,但是没办法通过172 3.VPN链接时,出现了DNS错误 ...
- 37、python并发编程之协程
目录: 一 引子 二 协程介绍 三 Greenlet 四 Gevent介绍 五 Gevent之同步与异步 六 Gevent之应用举例一 七 Gevent之应用举例二 一 引子 本节的主题是基于单线程来 ...
- tomcat的基本使用及项目部署
tomcat介绍 我们在学习Javaweb的时候,最普遍使用的服务器j就是阿帕奇的tomcat,主要是用来处理jsp和servlet的请求以及响应 tomcat的启动和关闭 在我们安装完tomcat后 ...