介绍食人鱼:自动删除过时代码的开源工具

介绍食人鱼:自动删除过时代码的开源工具

在优步,我们使用功能标志来定制我们的移动应用程序执行,为不同的用户提供不同的功能。例如,这些标志让我们能够在不同的运营区域本地化用户体验,更重要的是,逐步向用户推出新功能实验具有相同功能的不同变体。

然而,当一个功能向我们的用户100%推出后,或者一个实验性的功能被认为是不成功的,代码中的功能标志就会过时。这些非功能性特征标志代表了技术债务,使得开发人员很难在代码库上工作,并且会使我们的应用程序膨胀,需要一些影响最终用户性能的不必要操作,并可能影响整个应用程序的可靠性。

对于我们的工程师来说,消除这种债务可能需要大量时间,从而阻碍他们开发新的功能。

我们开发了寻求自动化此过程水虎鱼,它扫描源代码以删除与陈旧或过时的特性标志相关的代码,从而生成更干净、更安全、更性能和更可维护的代码库。我们正在为我们的Android和iOS代码库运行Uber的Piranha,并使用它删除了大约2000个陈旧的功能标志及其相关代码。

我们相信,Piranha为那些在应用部署中使用特性标志的组织提供了很好的实用工具开放的它。Piranha目前主要用于Objective-C、Swift和Java程序,开源贡献者可能希望将其应用于其他语言或提高其执行深度代码重构的能力。

特色标志的生死

为了引入标志,开发者需要在优步的标志管理系统中创建一个条目,并输入诸如标志的名称、标志的类型、目标推广百分比、目标平台和标志运行的地理位置等属性。此外,在源代码中手动引入标志,这在我们的实验平台上的标志和移动应用实例之间建立了一个连贯的关联。从那时起,这个标志作为一个变量在代码中管理应用程序的行为。

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

在最简单的情况下,当逐步推出一个特性时,我们有一个控制条件(功能未启用),以及治疗条件(功能启用的地方)。我们倾向于一开始将治疗条件应用于一小部分用户,如果推广成功,我们将逐步扩大治疗应用,以涵盖所有与该功能相关的用户(例如,特定地理位置的所有用户)。如果在推出期间出现问题,我们可以停止并回滚,确保对用户的影响最小。

我们的系统还可以处理相同特征的各种不同的实现,例如在不同的用户组上测试不同的接口(例如A / B测试)。

许多功能最终会向全球100%的用户推出。有时,我们希望在代码中保留特性标志来保护该功能,以充当非关键应用程序功能的安全终止开关。通过这种方式,一个小功能的潜在漏洞或崩溃(否则可能导致整个应用崩溃)可以通过关闭之前普遍推出的功能标志来轻松缓解服务器端问题。但是,因为大多数特性都嵌套在其他特性之下,所以这种有意的终止开关并不是大多数特性标志的常见结束状态。

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

由于过期的旗帜而产生的技术债务

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

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

自动删除与陈旧标志相关的代码

为了解决由于过时的特性标志而导致的技术债务问题,我们设计并实现了Piranha,这是一个自动的源代码到源代码重构工具,用于自动生成微分修正(句话说,差异)来删除与过时特性标志对应的代码。Piranha将标记的名称、期望的处理行为和标记作者的名称作为输入。分析了抽象语法树diff被分配给标记的作者以供进一步检查,然后他可以按原样着陆(提交给主服务器),或者在着陆之前执行任何额外的重构。我们还围绕Piranha构建了工作流,以一种可配置的方式定期删除过时的代码。

功能标记示例

让我们通过一个简单的例子来说明优步源代码中特性标志的基本用法。

首先,我们定义一个名为的新标志RIDES_NEW_FEATURE在一个旗帜列表中RidesExpName,并在国旗管理系统中注册它。随后,我们使用功能标志API将标志写入代码,istreated,并分别在IF和其他分支下提供治疗/控制行为的实现:

公共枚举RidesExpName实现ExpName {
RIDES_NEW_FEATURE,

}

如果(experiments.isTreated (RIDES_NEW_FEATURE)) {
//执行的治疗(对)行为
其他}{
//实现控制(关闭)行为
}

要使用各种标志值测试代码,对于每个单元测试,我们可以添加注释以指定要素标志的值。以下,test_new_feature对象中考虑的标志时运行治疗状态:

@Test
@RidesExpTest(治疗= RidesExpName.RIDES_NEW_FEATURE)
Public void test_new_feature() {

}

RIDES_NEW_FEATURE如果变得陈旧,所有与之相关的代码都需要从代码库中删除。这包括:

    1. 定义RidesExpName。
    2. 它的用法istreatedAPI。
    3. 注释@RidesExpTest。

此外,必须删除实现现在不可到达的控制行为的else分支的内容。我们还想删除任何包含此删除行为的测试的代码。

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

自动化的挑战

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

国旗过时

令人惊讶的是,确定一个标志是否陈旧并不简单。首先,无论是作为治疗还是作为对照,旗帜都应该被完全铺开。一面尚未完全铺开的国旗可能意味着它的实验仍在进行中。即使它已经推出,开发人员可能还没有准备好取消这个标志。例如,标志可以用作终止开关或用于监视调试信息。因此,即使标志完全展开,它们仍然可能被使用。

国旗的所有权

在优步的早期增长期间,确定陈旧标志的所有权信息变得很困难。即使我们可以很好地确定标志作者身份,有问题的作者可能已经转移到另一个团队或离开了组织。

编码风格

缺乏与特征标志相关的代码有关的任何限制会增加自动工具设计的复杂性。例如,标志相关代码的辅助函数不能轻易区分代码中的任何其他功能。此外,开发人员允许手动状态对标志的变化来引入的复杂性可以限制执行全面清理的工具。例如,当与标志相关的代码是测试的,有时尚不清楚测试是否可以全部丢弃,因为删除了功能,或者需要删除测试身体内的特定状态变化,以便剩余的功能可以继续进行测试。

使用静态分析构建食人鱼

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

我们确定了执行清理工作的三个关键方面:

    • 删除的代码立即围绕着特性标志api。
    • 删除由于执行上一步而变得不可访问的代码。我们称之为深层清洁
    • 删除与特性标志相关的测试。

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

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

我们观察到有三种标志API:

    • 布尔api返回一个布尔值,用于确定执行所采取的控制路径。
    • 更新的api更新正在运行的系统中的特性标志值。
    • 参数API.返回一个非布尔原始值(整型、双型等),对应于从后端控制的实验值。

我们的重构技术解析了输入源代码的ASTS,以检测使用所考虑的标志的特征标志API的存在。对于布尔API,我们执行简单的布尔表达式简化。如果生成的值是一个布尔常数,我们会适当地重构代码。例如,如果布尔API作为一部分发生如果并化简为真的,我们通过删除整个代码来重构代码>如果语句,将其替换为然后条款。

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

由于我们注意到boolean api不需要总是在条件保护中使用,所以我们为重构设计了第二个通道。我们确定分配,其中右手边是一个布尔API, Piranha已简化为一个常量,并跟踪受让人变量。类似地,我们跟踪返回布尔API的包装器方法,该API被简化为一个常量。随后,我们确定在条件保护中使用受让人变量或包装器方法来执行重构。

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

总而言之,食人鱼将以下信息作为输入:考虑的失效标志、处理行为和标志的所有者。它分析在预定义的特性标志api中使用此标志的代码,并根据处理行为对其进行重构以删除代码路径。

在优步使用食人鱼

我们用Piranha重构Objective-C, Swift和Java程序。Piranhajava.重构Java应用程序中过时的特性标志相关代码,特别是那些针对Android平台的代码。它是在Java上实现的容易出错作为易于插件的错误。PiranhaSwift是用Swift实现的吗SwiftSyntax用于重构Swift代码。Piranhaobjc.用于清理Objective-C程序中的代码,并以C ++实现为aClang插件,使用AST匹配者和重写器内部解析和重写ast。

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

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

图1所示。在我们的Piranha工作流中,标志管理系统定期向Piranha发送一个可能过时的标志列表,该列表生成一个diff并将其发送给原始标志作者。然后,作者可以决定是否着陆的差异。

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

Piranha管道使用启发式方法将标志管理系统中超过特定时期(例如8周)未修改的标志视为过时的,并为这些标志生成差异。负责处理食人鱼输出差异的各个团队配置了标志过期的确切时间。我们观察到,使用食人鱼产生差异的当前时间不到3分钟。

在您自己的代码中使用Piranha

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

    • 广泛使用特性标志
    • 是否有特定的api来控制特性标志的行为
    • 是用Java, Swift还是Objective-C实现的

为Codebase设置Piranha非常简单:定义属性文件中的功能标志与相关的API和预期行为,然后运行Piranha,名称为陈旧标志和预期输出行为。看看文档有关如何在每种语言中使用食人鱼的更多细节。

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

如果你有兴趣参加我们的编程系统团队工作的其他令人兴奋的项目在编程语言,编译和软件工程,并有学术或行业经验的程序分析,编译和相关领域,请联系我们的团队

一个详细的雷竞技是骗人的研究论文《食人鱼》将在韩国首尔举行的软件工程、软件工程和实践国际会议(ICSE-SEIP ' 20)上发表。

注释

没有帖子显示