导读:来自系统的自动消息:“你的代码死亡已超六个月,建议彻底删除。”
任何大型项目都一定会积累下“死代码”,也就是那些不再使用的模块,或者在早期开发期间存在,但已经多年没跑过的程序。
事实上,有很多项目在开发完成后都会先运行一段时间,之后可能就扔在那弃之不用。
这些死代码会继续产生成本:自动化测试系统并不知道哪些代码无需再测,负责大规模清理的人们也会把很多不再运行的代码白白移来挪去。所以虽然这些代码的生产成本很高,但它同时也需要耗费大量时间加以维护。这类维护工作不能轻易跳过,否则未来就一定会造成更大的回溯管理成本。
那么,能不能靠削减代码量来降低维护成本?代码仓库里的内容真的都有存在的必要吗?
但在这样的单一代码库的条件下,最坏最坏的情况就是不小心删掉了“源代码”,Google SRE 首席软件工程师说,这种情况“意味着谷歌使用的每个数据中心、每个工作站都会突然停止运行——不仅仅是关闭,甚至连存储都无法使用。(虽然只有在世界末日时才会发生)。”
那么,他们是怎么清理这些死代码的?谷歌最近在其博客中介绍了 Sesenmann“自动删除代码”项目,该项目的目标是自动识别出无效代码,再发送代码审查请求(变更列表)以将其删除。
Sesenmann 在德语中代表“死神”的无情收割之义。据谷歌介绍,该项目非常成功,每周可提交超过 1000 个待删除的变更列表,而且截至目前已经删除了谷歌全部 C++代码中的 5%。
假如 main1 正在使用,但 main2 的最后一次使用却是在一年多之前,那就可以构建起树状传播活动信号将 main1 及其依赖的所有内容均标记为活动。
余下的部分则可以去掉;由于 main2 依赖于 lib2,所以这次我们希望在一次变更中同时删除这两个目标:
到目前为止一切顺利,但真正的生产代码需要经过单元测试,其构建目标由测试的库决定。这就让整个遍历结构变得更加复杂:
测试基础设施会运行所有测试,包括 lib2_test,可是 lib2 从未被“真正”执行过。也就是说,我们不能单纯将测试运行作为“活跃度”信号:在这种情况下,可以误以为 lib2_test 保持活动,并导致 lib2 永远存在。只能清理未经测试的代码,而这会严重阻碍清理工作的有效进行。
根本目标是让每个测试都能共享所测试库的使用情况,所以我们可以让库和测试相互依赖来达成这个目标,据此在图中创建循环:
这样就将各个库及其测试转化成了强连接组件,可以使用与以往相同的方法标记出“活”节点,之后寻找有待删除的“死”节点集合。区别在于这次使用了 Tarjan 强连通分量算法来处理循环。
这样做很简单,但前提是能轻松看出测试及所测库之间的关系。遗憾的是,情况并不总是这么乐观。在以上示例中,由于遵循简单的命名约定,所以大家能将测试与库快速匹配起来。但这种方法在实际生产系统中往往并不奏效。
比如以下两种情况:
左边的是 LZW 压缩算法实现,分别存在单独的压缩器和解压缩器库。该测试实际上是对两者都进行测试,以确保数据在压缩和解压缩后不致损坏。在右侧,web_test 负责测试 Web 服务器库,它使用 URL 编码器库来提供支持,但实际上并不会测试 URL 编码器本身。
这就希望将左侧的 LZW 测试和两个 LZW 库视为同一连接组件,而在右侧则希望排除掉 URL 编码器,只将 web_test 和 web_lib 视为连接组件。尽管需要不同的处理方式,但这两种情况的基本结构是相同的。在实践当中,可以建议工程师将 url_encoder_lib 之类的库标记为“纯供测试”(即仅用于支持单元测试),这样就能解决 web-test 需求。
除此之外,Phil 表示目前谷歌的方法是使用测试和库名称之间的编辑距离来选择最可能与给定测试相匹配的库。至于如何识别 LZW 这类一项测试对应两个库的情况,这可能需要涉及测试覆盖率数据,谷歌并没有讨论这类方法。
作者:东方春晓
参考链接:
https://testing.googleblog.com/2023/04/sensenmann-code-deletion-at-scale.html
https://news.ycombinator.com/item?id=35755841
本篇文章为 @ 行动的大雄 创作并授权 21CTO 发布,未经许可,请勿转载。
内容授权事宜请您联系 webmaster@21cto.com或关注 21CTO 公众号。
该文观点仅代表作者本人,21CTO 平台仅提供信息存储空间服务。
请扫描二维码,使用微信支付哦。