Piranha:一个自动删除陈旧代码的开源工具

2020-06-15 07:34:45

在优步,我们使用功能标志来定制我们的移动应用执行,为不同的用户提供不同的功能。例如,这些标志允许我们在我们运营的不同地区本地化用户体验,更重要的是,我们可以逐步向用户推出功能,并试验相同功能的不同变体。

但是,在一个功能已经100%向我们的用户推出或者一个试验功能被认为不成功之后,代码中的功能标志就会变得过时。这些不起作用的功能标志代表技术债务,使开发人员难以处理代码库,并可能使我们的应用程序膨胀,需要进行不必要的操作,从而影响最终用户的性能,并可能影响应用程序的整体可靠性。

消除这一债务对我们的工程师来说可能是时间密集型的,使他们无法开发新功能。

为了使这个过程自动化,我们开发了Piranha,这是一个扫描源代码的工具,可以删除与陈旧或过时的功能标志相关的代码,从而获得更干净、更安全、性能更好和更可维护的代码库。我们正在为我们的Android和iOS代码库运行Uber的食人鱼,并用它删除了大约2000个陈旧的功能标志及其相关代码。

我们相信Piranha为在应用程序部署中使用功能标志的组织提供了很好的实用程序,因此已经将其开源。目前为Objective-C、Swift和Java程序实现的开源贡献者可能希望将食人鱼应用于其他语言或提高其执行深度代码重构的能力。

为了引入旗帜,开发人员在Uber的旗帜管理系统中创建一个条目,并输入诸如旗帜名称、旗帜类型、目标推出百分比、目标平台和旗帜可操作的地理位置等属性。此外,该标志是在源代码中手动引入的,这在我们的实验平台上的标志和移动应用实例之间建立了一致的关联。从那时起,该标志作为代码中的变量来管理应用程序行为。

从运行的应用程序的角度来看,功能标志是单个键,映射到两个或多个条件中的一个,例如开/关、颜色值、大小和复制文本。在启动时,我们的移动应用程序通过网络查询我们的旗帜管理系统,并检索当前应用程序实例的每个旗帜的特定处理条件。返回值确定应用程序中功能的存在和行为。

在最简单的情况下,当逐步推出单个功能时,我们有一个控制条件(功能未启用)和一个治疗条件(功能启用)。我们倾向于最初将治疗条件应用于一小部分用户,如果推出被证明是成功的,则逐渐增加治疗的应用以涵盖与该功能相关的所有用户(例如,特定地理位置的每个人)。如果在推广过程中出现问题,我们可以停止并回滚,确保对用户的影响降至最低。

我们的系统还可以处理同一功能的各种不同实现,例如在不同的用户集上试验要测试的不同接口(例如,A/B测试)。

许多功能最终在全球范围内向100%的用户推出。有时,我们希望在代码中保留保护该功能的功能标志,以充当非关键应用程序功能的安全终止开关。这样,通过关闭之前普遍推出的功能标志,服务器端就可以很容易地缓解一个小功能中的潜在错误或崩溃,否则可能会导致整个应用程序瘫痪。但是,由于大多数功能都嵌套在其他功能下,因此此类故意取消开关不是大多数功能标志的共同结束状态。

我们认为与100%推出的功能相关的大多数功能标志都是“陈旧的”,这意味着标志本身不再起作用,可以通过硬编码完全推出的功能版本来替代。类似的情况发生在实验或治疗条件中,这些实验或治疗条件已经100%回滚到它们的控制(即没有特征)条件。

当标志变得陈旧时,应该在功能标志管理系统中禁用它,并且需要从源代码中删除与该标志相关的所有代码工件,包括现在无法访问的功能替代版本的实现。这确保了改进的代码卫生,并避免了技术债务。

在实践中,开发人员并不总是执行这个简单的后期清理过程,而是在与过时标志相关的代码中留下,从而导致技术债务的累积。与这些不必要的标志相关的代码的存在可能会影响跨多个维度的软件开发。首先,开发人员必须对与这些过时标志相关的控制流进行推理,并在monorepo中处理大量无法访问的代码。其次,在意外情况下(例如,由于标志管理后端错误),这样的代码可能仍然是可执行的,从而降低了应用程序的整体可靠性。第三,必须努力维护这些不必要路径的测试覆盖。最后,死代码和测试的存在会影响整个构建和测试时间,从而影响开发人员的工作效率。

为了解决由于过时的功能标志而导致的技术债务问题,我们设计并实现了Piranha,这是一个自动化的源代码到源代码重构工具,用于自动生成差异修订(换句话说,差异),以删除与过时的功能标志相对应的代码。食人鱼接受旗帜的名称、预期的治疗行为和旗帜作者的姓名作为输入。它分析程序的抽象语法树(AST)以生成适当的重构,并将这些重构封装到DIFF中。diff被分配给标志的作者进行进一步检查,然后他可以按原样登陆(提交给master),或者在登陆之前执行任何额外的重构。我们还围绕Piranha构建了工作流,以便以可配置的方式定期删除陈旧的代码。

让我们来看一个简单的示例,说明Uber源代码中功能标志的基本用法。

最初,我们在RidesExpName的标志列表中定义一个名为RIDES_NEW_FEATURE的新标志,并将其注册到标志管理系统中。随后,我们使用功能标志API isTreated将标志写入代码,并分别在if和Else分支下提供处理/控制行为的实现:

if(实验性s.isTreated(RIDES_NEW_FEATURE)){为治疗(开)行为提供//实现}否则{为控制(关闭)行为提供//实现}。

要使用各种标志值测试代码,对于每个单元测试,我们可以添加一个注释来指定特性标志值。下面,当考虑的标志处于已处理状态时,TEST_NEW_FEATURE运行:

当RIDES_NEW_FEATURE变得陈旧时,需要从代码库中删除与其相关的所有代码。这包括:

此外,必须删除实现现在无法访问的控件行为的Else-BRANCH的内容。我们还希望删除涉及此删除行为的任何测试的代码。

不删除这些代码工件会逐渐增加源代码的复杂性和总体可维护性。

不出所料,在自动检测过时标志和相关代码删除方面存在许多困难。范围从确定标志是否正在使用到谁拥有该标志,再到其代码是如何编写的细节。克服这些挑战是食人鱼发展的关键。

确定标志是否陈旧并不是一件轻而易举的事,这一点令人惊讶。首先,这面旗帜应该百分之百地展开,要么作为治疗,要么作为对照。一面没有百分之百铺开的旗帜可能意味着它的实验仍在进行中。即使当旗帜推出时,开发商也可能还没有准备好取消旗帜。例如,标志可以用作终止开关或用于监控调试信息。因此,即使旗帜完全铺开,也可能仍在使用。

在优步成长初期,确定陈旧旗帜的所有权信息颇具挑战性。即使我们可以完美地确定旗帜的作者身份,有问题的作者也可能已经跳槽到另一个团队或离开了组织。

与功能标志相关的代码缺乏任何限制,增加了自动化工具设计的复杂性。例如,与标志相关的代码的帮助器函数不容易与代码中的任何其他函数区分开来。此外,开发人员允许手动更改标志的测试引入的复杂性可能会限制工具执行全面清理。例如,在单元测试与标志相关的代码时,有时不清楚是否可以完全丢弃测试,因为功能被移除,或者测试主体中的特定状态更改需要被移除,以便可以继续测试剩余的功能。

考虑到我们在程序分析方面的共同背景,我们设想通过应用静态分析来删除由于陈旧标志而不必要的代码,可以有效地解决这个问题。

删除由于执行上一步而变得无法访问的代码。我们把这称为深度清洗。

为了在所有三个维度上执行精确清理,有必要执行可达性分析以识别变得不可达的代码区域,并实现算法以识别与测试功能标志相关的测试。虽然这在理想情况下将确保完全自动化,其中开发人员只需要检查删除内容并将更改放入主控文件中,但它需要克服两个挑战:确保执行清理的底层分析是健全和完整的,以及实现和扩展此类分析以在有用的时间框架内处理数百万行代码所需的工程工作是可用的。

由于以健全和完整的方式确定可达性通常是不切实际的,因此我们决定不构建一个复杂的分析,因为清理后开发人员干预的量是未知的,工程投资回报也是不清楚的。相反,我们选择了一种基于代码库中观察到的编码模式迭代设计技术的实用方法。

返回布尔值并用于确定执行所采用的控制路径的布尔API。

返回非布尔原始值(整数、双精度等)的参数API,该原始值对应于从后端控制的实验值。

我们的重构技术解析输入源代码的AST,以检测是否存在使用正在考虑的标志的特性标志API。对于布尔API,我们执行一个简单的布尔表达式简化。如果结果值是布尔常量,我们将适当地重构代码。例如,如果布尔API作为if语句的一部分出现,并且简化为true,则我们通过删除整个>;if语句,将其替换为THEN子句中的语句来重构代码。

对于更新API,我们只需删除相应的语句即可。我们不处理参数API,因为解决它们所需的工程工作量要大得多,并且它们在代码库中出现的频率要低得多。

因为我们观察到布尔API不需要总是在条件保护中使用,所以我们为重构设计了第二遍。我们识别右侧是布尔型API的赋值,Piranha将其简化为常量,并跟踪被赋值变量。类似地,我们跟踪返回简化为常量的布尔API的包装器方法。随后,我们确定在条件保护中使用被赋值变量或包装器方法来执行重构。

最后,如果标记注释与输入处理行为不匹配,我们将通过丢弃整个测试来处理测试的标记注释。否则,我们只需删除测试的注释。

总而言之,Piranha接受以下内容作为输入:正在考虑的陈旧标志、处理行为和标志的所有者。它分析在预定义的功能标志API中使用此标志的代码,并根据处理行为对其进行重构以删除代码路径。

我们实现了Piranha来重构Objective-C、SWIFT和Java程序。PiranhaJava重构Java应用程序中与陈旧功能标志相关的代码,特别是那些针对Android平台的应用程序。它是在易出错的基础上用Java实现的,是一个易出错的插件。PiranhaSwift在SWIFT中实施,使用SWIFT语法重构SWIFT代码。PiranhaObjC用于清理Objective-C程序中的代码,并在C++中作为Clang插件实现,在内部使用AST匹配器和重写器来解析和重写AST。

虽然Piranha作为一个独立的工具可以执行代码重构,但是开发人员并不总是优先考虑标志清理,因此可能不会像需要的那样频繁地使用它。就像食人鱼自动清除旗帜一样,我们需要一个系统来自动启动这些清除。

在内部,我们构建了一个工作流管道,它定期(在我们的例子中是每周)生成差异和任务来清理过时的特性标志。Piranha管道向标志管理系统查询过时标志的列表,并且对于这些标志中的每一个,它分别调用Piranha,提供过时标志的名称、其所有者和预期的输出行为(处理或控制)作为输入。

上面的图1显示了食人鱼管道的架构图。食人鱼生成一个diff(即拉请求),并将其放入我们的代码审查系统中,并将标志的原始作者作为默认审查者。作者既可以按原样接受差异,根据需要修改它,也可以拒绝该标志并将其标记为未过时。管道还在我们的任务管理系统中生成一个清理任务,以跟踪每个生成的差异的状态。由于开发人员可能并不总是及时对这些不同之处采取行动,我们还引入了一个名为PiranhaTidy的提醒机器人,它可以定期添加与打开的食人鱼相关任务的提醒。

食人鱼管道使用试探法将在标志管理系统中超过特定时间段(例如,8周)未修改的标志视为陈旧的,并为这些标志生成差异。负责处理食人鱼输出差异的各个团队配置标志失效的确切时间段。我们观察到,目前使用食人鱼生成差异所用的时间不到3分钟。

我们很高兴地宣布,食人鱼现在对所有三种受支持的语言都是开源的。如果您的代码满足以下标准,我们相信该工具将对您的团队有用:

为您的代码库设置Piranha相当简单:在属性文件中定义与特性标志相关的API和预期行为,然后使用stale标志和预期输出行为的名称运行Piranha。有关如何为每种语言使用食人鱼的更多详细信息,请参阅文档。

我们欢迎开发者对食人鱼的贡献。欢迎各种能力的开发人员,并且致力于Piranha的实现对于该领域的非专家来说可能是理解程序分析的细微差别的一种很好的方式。有许多有趣的项目涉及改进Piranha生成的代码重构、将Piranha扩展到其他语言(例如,Kotlin、Go等),以及设计和实现其他与功能标志相关的程序分析。请下载食人鱼的源代码开始使用。

如果您有兴趣加入我们的编程系统团队,从事编程语言、编译器和软件工程方面的其他令人兴奋的项目,并且在程序分析、编译器和相关领域有学术或行业经验,请联系我们的团队。

关于食人鱼的详细研究论文将在韩国汉城举行的软件工程、软件工程和实践轨道国际会议(ICSE-SEIP‘20)上发表。