An open-source SDK for finding dead code
ON THIS PAGE
- How Reaper works: Runtime Analysis
- Why deleting code is important
- Using Reaper for iOS
- Using Reaper for Android
- Concluding Thoughts
Writing code is easier to do than ever. We want to make deleting code easier than ever – introducing Reaper for iOS and Android.
Reaper was an Emerge Tools product that helped companies like Duolingo delete 1% of their iOS codebase. And just like with Emerge Tools’ Launch Booster, we’re making Reaper open-source for anyone to use.
In this post, we’ll explain what Reaper is, why you should care about dead code, and how Reaper works on both platforms.
Resources
How Reaper works: Runtime AnalysisHow Reaper works: Runtime Analysis
Static analysis is when you inspect your code without running it to find bugs, enforce rules, discover insights, etc. A compiler can analyze your project and delete or strip out code for you. Tools like Periphery use static analysis to identify dead code.
Static analysis can be very effective, but it can only go so far. The compiler won’t determine that a feature flag is stale because there are strong references to it.
Reaper finds dead code via runtime analysis – it monitors how users are actually using your app to find code that is never touched. While there are OS-level nuances, Reaper’s premise is the same on iOS or Android: let users tell you the code that should be deleted.
The Reaper SDK automatically detects what code is hit during a user’s session. This data can then be aggregated by version to show you types that haven’t been executed.
Data can then be compared across versions – code that hasn’t been hit in the previous 5 versions is likely safe to delete.
Why deleting code is importantWhy deleting code is important
Some people will tell you all code is bad code. This sentiment is (probably) not meant to be dogma, but there is evidence to the downstream impacts of the number of lines of code. More code means:
Using Reaper for iOSUsing Reaper for iOS
Reaper for iOS is available as an open source Swift package. As part of this change we have updated the API to allow custom handling of the detected types, so you can upload them to your own server.
Using Reaper is a two-step process. First, you run a script that statically analyzes your app binary to generate a set of all types that Reaper can track. Next, you aggregate reports from production users. The differences between these sets are the types that were never used in production, and can be deleted from your codebase.
How it works under the hoodHow it works under the hood
At its core, Reaper works by inspecting the Objective-C and Swift runtime for metadata that is already used to track the lifecycle of types. This way it adds zero overhead while your app is running, and only inspects pre-existing fields at the upload time.
For Objective-C classes this uses the RW_INITIALIZED
bit that is set by the runtime the first time a class is accessed. This flag is how the runtime knows to call +(initialize) only once per class. Checking this boils down to these three lines:
objc_class *metaClass = (__bridge objc_class *) object_getClass((__bridge id) classPtr); class_rw_t *writableClassData = metaClass->bits.data();
BOOL isInitialized = !!(writableClassData->flags & (1<<29) /* RW_INITIALIZED */);
The process is more complicated for Swift types. Some Swift types set the RW_INITIALIZED bit, but others bypass the ObjC runtime entirely. However, some that bypass the ObjC runtime are still attributable due to other flags used by the Swift runtime. When you run our provided scripts to determine which types Reaper supports, the Swift binary metadata is inspected to see which types will be reliably trackable at runtime.
Using Reaper for AndroidUsing Reaper for Android
The Android Reaper SDK has always been open source, but you can now use it with your own backend by editing the Android manifest for your app:
<meta-data
android:name="com.emergetools.OVERRIDE_BASE_URL"
android:value="https://example.com/foo" />
The app will now send reports to `https://example.com/foo/report` and any errors with Reaper to `https://example.com/foo/reaper/error`. Here is an example commit showing how to do this using our demo app Hacker News.
As with iOS, using Reaper on Android involves subtracting the set of classes seen in production from those observed at build time to find classes that are unused.
How it works under the hoodHow it works under the hood
Unlike iOS on Android we don’t have access to runtime for metadata that tracks the lifecycle of type. Instead we manually instrument classes at build time. This is done in the Emerge Tools gradle plugin.
The JVM operates on bytecode ‘classes’. Classes in Java and Kotlin map to JVM classes however many high-level constructs from those languages (lambdas, inner classes, continuations, etc) will also generate unique JVM classes. Here is the prefix of a typical Android class in the .smali format:
.class public final Lcom/emergetools/hackernews/HNApplication;
.super Landroid/app/Application;
.source "SourceFile"
.method public static constructor <clinit>()V
.registers 2
return-void
.end method
.method public constructor <init>()V
.registers 3
invoke-direct {p0}, Landroid/app/Application;-><init>()V
return-void
.end method
# virtual methods
.method public final onCreate()V
.registers 3
invoke-super {p0}, Landroid/app/Application;->onCreate()V
[...snip…]
This shows two ‘magic’ methods: <clinit>
and <init>
. <clinit>
is the static initializer. It’s called once for each class before that class is used. <init>
is the class initializer. It’s called any time the class is instantiated.
For (most) JVM classes at buildtime we:
Compute the SHA256 hash of the class signature and extract the top 64 bits
Then for each of
<clinit>
and<init>
We inject a call to
logMethodEntry
passing the top bits from the hash.
After the instrumentation is added the smali looks like this:
.class public final Lcom/emergetools/hackernews/HNApplication;
.super Landroid/app/Application;
.source "SourceFile"
.method public static constructor <clinit>()V
.registers 2
sget-object v0, Lcom/emergetools/reaper/ReaperInternal;->INSTANCE:Lcom/emergetools/reaper/ReaperInternal;
const-wide v0, 0x3203966e22ecd0dcL
invoke-static {v0, v1}, Lcom/emergetools/reaper/ReaperInternal;->logMethodEntry(J)V
return-void
.end method
.method public constructor <init>()V
.registers 3
sget-object v0, Lcom/emergetools/reaper/ReaperInternal;->INSTANCE:Lcom/emergetools/reaper/ReaperInternal;
const-wide v0, 0x3203966e22ecd0dcL
invoke-static {v0, v1}, Lcom/emergetools/reaper/ReaperInternal;->logMethodEntry(J)V
invoke-direct {p0}, Landroid/app/Application;-><init>()V
return-void
.end method
# virtual methods
.method public final onCreate()V
.registers 3
invoke-super {p0}, Landroid/app/Application;->onCreate()V
[...snip…]
At runtime (if Reaper is enabled) the hashes are inserted into a thread local cache, periodically swept into a set for the whole application and finally, when the session ends, queued to be reported to the server in a worker job.
We avoid instrumenting a number of core libraries and the Reaper SDK itself to avoid issues with performance and reentrancy. We also don’t add a <clinit> or <init> method if one was not already present in the class. This means we do not instrument most interfaces or abstract classes. Large apps typically have 50-100k classes and around 85% of these have a <clinit> method, an <init> method, or both.
In our sample repo we have provided:
A script for extracting the instrumented classes and hashes from .aab files
A demo backend for receiving and reports
The script can be used like this:
git clone https://github.com/getsentry/reaper-server.git
cd reaper-server
./reaper.py myapp.aab -o reaper.tsv
Concluding ThoughtsConcluding Thoughts
Most reports indicate that generative AI is speeding up developer velocity. Google Dora’s “Impact of Generative AI in Software Development” states as such, but it also notes that AI adoption has had a “negative impact on delivery stability”. GitClear's AI Copilot Code Quality report notes higher-than-expected code additions, but also a sharp rise in code duplication.
This isn’t to downplay what gen AI can do (see Seer). The tools to write code are improving every day and it's important that the tools to monitor and maintain code don’t fall behind. We hope open-sourcing Reaper helps teams take a significant step forward in managing their iOS and Android codebases and will leave you with this classic software wisdom: