我试图减少Pylint内存使用量

2020-10-12 22:28:13

通过工作,我必须处理相当大的代码库,并在我们的持续集成过程中在其上运行Pylint。它总是有点慢,但最近我也注意到它消耗了大量内存,如果我们试图过多地并行运行,就会导致OOM失败。

这是我如何处理这两个问题的日志,作为将来我在Python中进行其他分析工作时的参考。

从入口点(pylint/__main__.py)查看Pylint,最后我向下挖掘,找到了您在检查一系列文件错误的程序中所期望的";QuintEssential";for循环:

Def_check_files(self,get_ast,file_descrs):#in pylint/lint/pylinter.py with self._astid_module_checkker()as check_astid_module:for name,filepath,modname in file_descrs:sel._check_file(get_ast,check_asterid_module,name,filepath,modname)。

首先,我只是在其中添加了一条print(";HI&34;)语句,以确认这确实是我在pylint my_code时正在运行的代码,并且运行起来没有任何故障。

首先是尝试找出内存中保存了什么内容,所以我用Heapy做了一个非常基本的堆转储,看看有没有什么特别的。

FROM GUPY IMPORT hp=HPY()i=0 for name,filepath,modname in file_descrs:sel._check_file(get_ast,check_astid_module,name,filepath,modname)i+=1 if I%10=0:print(";heap";)print(hp.heap())if i=100:提高值错误(";Done";)。

我的堆配置文件最终几乎全部是帧类型(出于某种原因,我有点预料到了这一点),但也保留了一些回溯。我告诉自己的这类物品已经够多了,这肯定比所需的要多。

一组2751394个对象的分区。总大小=436618350字节。索引计数%大小%累计%种类(类别/字典)0 429084 16 220007072 50 220007072 50类型.框架类型1 535810 19 30005360 7 250012432 57类型.TracebackType 2 516282 19 29719488 7 279731920 64元组3 101904 4 29004928 7 308736848 71集合4 185568 7 21556360 5 330293208 76字典(无所有者)5 206170 7 16304240 4 346597448 79列表6 117531 4 9998322 2 356595770 82 str 7 38582 1 9661040 2 6256810字典.8 76755 3 64440 2 303711285 tokenize.TokenInfo Info6 117531 4 9998322 2 356595770 82 str 7 38582 1 9661040 2 6256810 dict Astid.8 76755 3 64440 2 303711285 tokize.TokenInfo Info(无所有者)5 206170 7 16304240 4 346597448 79列表6 117531 4 9998322 2 356595770 82 str 7 38582 1 9661040 2 6256810 dict。

就是在这一点上,我发现了希比的个人资料浏览器,它似乎是一种更好的查看这些数据的方式。

我将堆机制转换为每10次迭代转储到一个文件,然后用图表表示整个运行时行为。

对于文件描述中的名称、文件路径、modname:sel._check_file(get_ast,check_astid_module,name,filepath,modname)I+=1 if I%10==0:hp.heap().stat.dump(";/tmp/linting.stats";)if I==100:hp.pb(";/tmp/linting.stats";)引发值错误(";完成";)。

我得到以下结果,确认帧和回溯在此Pylint运行中非常重要:

我的下一步是实际查看Frame对象。因为Python是被引用的,所以东西被保存在内存中,因为有东西指向它们,所以我要试着找出是哪一个。

非常棒的objgraph库利用Python内存管理器,允许您查看内存中的所有对象,以及查找对对象的引用。

事实上,你能做到这一点是非常令人惊讶的。只需有一个对象引用,您就可以找到指向该引用的所有内容(C扩展周围有一些星号,但距离足够近)。这是一项非常棒的调试技术,也是使Python成为一件令人愉快的工作的另一件事,这要归功于CPython的内部有多少是为这类工具公开的。

起初,我很难找到物品,因为objgraph.by_type(';types.TracebackType';)没有找到任何物品,尽管我有50万件物品的记录。原来要用的名字是回溯。没有弄清楚为什么.。

给了我一个随机回溯。Show_backrefs将绘制引用此回溯的对象图。

因此,在100次迭代之后,我将一个导入pdb;pdb.set_trace()放入for循环,而不是仅仅引发异常,并且我开始查看随机回溯。

最初我只是看到了一系列的回溯,所以我去了100度的深度,然后。

所以有一堆回溯引用了其他回溯。凉爽的。他们中的很多人都是这样的。

我试了很多次都失败了,所以我找了第二个目标:帧类型。这些在我看来也很可疑,我最终发现的情况如下所示:

所以回溯保持在帧上(因此有一些类似的基数)。这一切都乱七八糟,但框架至少指向了几行代码。这让我意识到了一些愚蠢的事情:我从来没有费心去实际查看占用了这么多内存的数据。我完全应该只看回溯对象本身。

因此,我最终以最复杂的方式完成了这项工作,即查看objgraph图像转储中的地址,查看内存地址,在Web上搜索如何从其地址获取Python对象,最后找到我已经注意到的巧妙技巧:

Ipdb>;import ctypesiddb>;ctyes.cast(0x7f187d22b880,ctyes.py_object)py_object(0x7f187d22b880>;处的<;traceback对象)ipdb>;ctyes.cast(0x7f187d22b880,ctyes.py_object).value<;在0x7f187d22b880>;ipdb>;my_tb=ctyes.cast(0x7f187d22b880,ctyes.py_object).valueipdb>;traceback.print_tb(my_tb,Limit=20)。

你可以告诉Python,嘿,看看这个内存,它肯定至少是一个Python对象,它会工作的。

后来我意识到我通过objgraph引用了这些对象,可以直接使用它。

看起来Astroid正在通过异常处理代码到处建立回溯。我觉得大多数很酷的把戏都是因为忘记了做某事的简单方法,所以我不会抱怨太多。

回溯本身有一串行通过Astroid,AST解析器支持Pylint。进步了!Asterid看起来确实像是一个可以将内存中的大量内容作为其解析文件的程序。

啊哈!我告诉自己。这就是我要找的东西!某种会产生超长回溯的异常链。再加上它的文件解析,这样就有了回溯,等等。从其他东西中举起东西会把它们锁在一起!

我把这个从前男友和....中去掉。没什么。内存使用量仍然基本相同,回溯仍然在这里。

我记得异常在回溯中保存它们的本地绑定,所以ex仍然是可访问的。现在还不能收集垃圾!

我进行了一系列重构,试图在很大程度上删除EXCEPT块,至少是为了避免引用EX。再说一遍,什么都没有!

对于我的生活,我不能得到这些垃圾收集的追溯,尽管没有它们的引用。至少我认为这是剩下的参考资料。

不过,说真的,这都是在转移视线。我不知道这是否真的与我的内存泄漏有关,因为在某一点上我开始注意到我没有证据支持整个异常链理论。只有一堆预感,外加50万的回溯。

我开始随机地查看回溯,寻找任何额外的线索。我试着用手向上搜索参考文献链,但一无所获。

然后我发现:这些追溯都是堆叠在一起的,但将会有一个高于所有其他追溯的追溯。有一个回溯不只是另一个回溯指向它!

引用是通过TB_NEXT属性作为一个简单的链进行的。所以我决定在他们各自的链条末端寻找回溯:

其中之一有一种神奇之处--在50万个堆栈跟踪中排成一行,找到你真正关心的那个人。

这让我找到了我实际上正在寻找的东西:Python首先真正想要保留这些对象的原因。

基本上,Astroid将缓存加载模块的结果,因此,如果代码的某些部分再次请求相同的模块/文件,它只能提供以前的结果。它还将通过保留抛出的异常来重现错误。

这是我做了一点判断的地方。缓存错误是有意义的,但对我来说,它根本不值得保留我们的代码生成的回溯次数。

相反,我决定丢弃异常,存储一个自定义错误类,然后在需要异常时重新构建它们。细节可以在这个公关上找到,尽管它不是特别有趣。

最终结果是:对于我正在使用的代码库,我们的内存使用量从500MB增加到100MB。

公关本身,我不确定它是否真的会被合并。这不仅仅是性能上的改变,我认为它可能会使堆栈跟踪在某些情况下变得不那么有用。从所有方面来看,这都是一个相当残酷的变化,即使测试套件仍然通过。

Python中的内存可见性相当惊人,我应该在调试时更频繁地使用它。

写下当前的目标,这样你就不会在调试时完全迷失情节。这最终花了我几个小时,我走进了太多与我的实际问题无关的兔子洞。

保存您的WIP代码片段(甚至更好:提交到Git)非常有用。删除内容可能很有诱惑力,但您可能需要查看一下以后是如何得到结果的,特别是对于更注重绩效的工作。

然而,有一件事让我笑了;在写这个回顾展的时候,我意识到我已经忘记了很多关于我是如何得出某些结论的,所以最终重新测试了我的一些片段。然后,我在一个不相关的代码库上运行我的测量,发现奇怪的内存使用只发生在一个代码库上。我花了很多时间来调试这个问题,但极有可能这只是一种病态的行为,只影响了一小部分用户。

即使你做了所有这些测量,也总是很难对表现做出概括性的陈述。

我也会尝试将这一经验带到其他项目中。我的理论是,考虑到社区对这一领域的关注相对较少(除了仅仅是C扩展类项目的库之外),开源Python项目的性能相关问题有很多容易摘到的果实。