5 min read

Discovering iOS memory leaks III: Automating with Github Action

Automating Leak Detection with Github Action
Discovering iOS memory leaks III: Automating with Github Action

Context

💡
This is part 3 of the blog: Discovering iOS memory leaks a case study with Firefox App, where I discuss my experience fixing memory leaks and contributing to their app.

In the first two parts of this series, Discovering iOS Memory Leaks, we explored the leaks command-line tool and successfully contributed to fixing a memory leak in the Firefox iOS repository.

  1. Discovering iOS memory leaks: A case study with Firefox app
  2. Discovering iOS memory leaks: A case study with Firefox app II

While that addressed the issue, there’s no assurance that new leaks haven’t surfaced in more recent versions. To maintain confidence in any codebase, we need a solution that sets a baseline and detects leaks proactively before release. This can be solved by automating leak detection as part of our CI pipeline, which we'll achieve through GitHub Actions in this post.

Github action would detect any memory leaks in the flow and would be failing the job, let's discuss the different stages of this Github Action.

Steps in Github Action

The Github Action is divided into the following parts:

  1. Setting up environment
  2. Building and Installing the app
  3. Launching App and running the maestro test.
  4. Running leaks command
  5. Upload leak report as artifacts

Setting up environment

We use macOS-13 runner. We set up this runner's environment to run the maestro test, build the code, and use a simulator. Here are the steps we perform:

  1. Checkout repository code in the runner
  2. Select xcode version 15.2
  3. Install maestro CLI
  4. Boot a simulator, we use the iPhone 14 and iOS 17.2. Here is the script we use to boot the simulator: boot_simulator.sh.
  5. Ensure the simulator is booted before we install the app.
- name: Checkout
  uses: actions/checkout@v4.1.7

- name: Select Xcode
  run: sudo xcode-select -switch /Applications/Xcode_15.2.0.app && /usr/bin/xcodebuild -version

- name: Install Maestro
  run: |
    curl -Ls --retry 3 --retry-all-errors "https://get.maestro.mobile.dev"| bash
    echo "${HOME}/.maestro/bin" >> $GITHUB_PATH

- name: Print Maestro version
  run: maestro --version

- name: Check xcodebuild
  run: xcodebuild -version

- name: List xcode runtimes
  run: xcrun simctl list runtimes

- name: Set permission to scripts
  run: chmod +x .github/scripts/boot_simulator.sh

- name: Boot Simulator
  run: ./.github/scripts/boot_simulator.sh
     
- name: Check Simulator Status
  run: xcrun simctl list

Build and Install Firefox iOS

Now we will be building the Firefox Fennec scheme and install on the booted simulator.

- name: Build Fennec Client
  run: |
    ./checkout.sh
    ./bootstrap.sh --force
    xcodebuild \
      build-for-testing \
      -scheme Fennec \
      -project ./firefox-ios/Client.xcodeproj \
      -destination "generic/platform=iOS Simulator" \
      -derivedDataPath ./build \
      ARCHS="x86_64"
        
- name: Install App
  run: |
    xcrun simctl install booted ./build/Build/Products/Fennec_Testing-iphonesimulator/Client.app

Launching the App and Running the Maestro Test

We used MallocStackLogging flag during launch as we discussed in Part 1 and we wrote a maestro test for the suggestions flow. We will be exporting the process ID in the GitHub environment so that we can invoke leaks command with it.

 - name: Launch App
   run: |
     export SIMCTL_CHILD_MallocStackLogging=1
     OUTPUT=$(xcrun simctl launch booted org.mozilla.ios.Fennec)
     PID=$(echo "$OUTPUT" | awk '{print $2}')
     echo "PID: $PID"
     echo "PID=$PID" >> $GITHUB_ENV

- name: Run test
  run: maestro test ./.github/scripts/suggestion_flow.yaml

Running leaks command and Upload leaks report

Now that we have the process ID and assume the maestro test will be successful we can invoke the leaks command and upload the report:

- name: Run leaks command
  run: sudo leaks ${{ env.PID }} >> ~/leaks.txt

- name: Upload leaks output
  uses: actions/upload-artifact@v4
  if: failure()
    with:
      name: leaks_output
      path: ~/leaks.txt
      retention-days: 1

Here is the full source code for the GitHub Action: publish_leaks_report.yaml.

firefox-ios/.github/workflows/publish_leaks_report.yml at feat-leaks-ci-integration · amanjeetsingh150/firefox-ios
Firefox for iOS. Contribute to amanjeetsingh150/firefox-ios development by creating an account on GitHub.

Ensuring No New leaks on the flow

We have an automation pipeline for getting leak reports. However, to ensure there are no new leaks, we have to create a baseline for the known leaks so that they only fail when a new leak is introduced.

To exclude the known leaks, leaks command line offers an --exclude option for excluding the traces with a symbol. As an example, if you want to exclude the following leak:

STACK OF 1 INSTANCE OF 'ROOT CYCLE: <NSBlockOperation>':
26  dyld                                  start + 1896
25  dyld_sim                              start_sim + 10
24  org.mozilla.ios.Fennec                main + 1014  main.swift:25
23  com.apple.UIKitCore                   UIApplicationMain + 123
22  com.apple.UIKitCore                   [UIApplication _run] + 936
21  com.apple.UIKitCore                   [UIApplication _compellApplicationLaunchToCompleteUnconditionally] + 59
20  com.apple.UIKitCore                   -[_UISceneLifecycleMultiplexer completeApplicationLaunchWithFBSScene:transitionContext:] + 181
19  com.apple.UIKitCore                   -[UIApplication _runWithMainScene:transitionContext:completion:] + 1241
18  com.apple.UIKitCore                   -[UIApplication _callInitializationDelegatesWithActions:forCanvas:payload:fromOriginatingProcess:] + 4253
17  com.apple.UIKitCore                   -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 184
16  org.mozilla.ios.Fennec                @objc AppDelegate.application(_:willFinishLaunchingWithOptions:) + 183  <compiler-generated>:0
15  org.mozilla.ios.Fennec               AppDelegate.application(_:willFinishLaunchingWithOptions:) + 1567  AppDelegate.swift:86
14  org.mozilla.ios.Fennec               AppLaunchUtil.setUpPreLaunchDependencies() + 1429  AppLaunchUtil.swift:62
13  org.mozilla.ios.Fennec                AppLaunchUtil.initializeExperiments() + 42  AppLaunchUtil.swift:142
12  org.mozilla.ios.Fennec                static Experiments.intialize() + 43  Experiments.swift:228
11  org.mozilla.ios.Fennec                 Experiments.shared.unsafeMutableAddressor + 49  Experiments.swift:137
10  libdispatch.dylib                      _dispatch_once_callout + 20
9   libdispatch.dylib                      _dispatch_client_callout + 8
8   org.mozilla.ios.Fennec                 one-time initialization function for shared + 9  Experiments.swift:137
7   org.mozilla.ios.Fennec                closure #1 in variable initialization expression of static Experiments.shared + 1632  Experiments.swift:160
6   org.mozilla.ios.Fennec                static Experiments.buildNimbus(dbPath:errorReporter:initialExperiments:isFirstRun:) + 1212  Experiments.swift:213
5   org.mozilla.ios.RustMozillaAppServices   NimbusBuilder.build(appInfo:) + 2172
4   org.mozilla.ios.RustMozillaAppServices  protocol witness for NimbusStartup.applyLocalExperiments(fileURL:) in conformance Nimbus + 161
3   org.mozilla.ios.RustMozillaAppServices       `Nimbus.applyLocalExperiments(getString:) + 93`
2   libobjc.A.dylib                       _objc_rootAllocWithZone + 47
1   libsystem_malloc.dylib                 _malloc_type_calloc_outlined + 105
0   libsystem_malloc.dylib         _malloc_zone_calloc_instrumented_or_legacy + 204 

You can select a unique symbol from the stack trace along with this option, so for this example, it could be: --exclude Nimbus.applyLocalExperiments(getString:)

Conclusion

You can add this GitHub Action to your workflow, either on pull requests or nightly runs, depending on what suits your team best.

Here is an example run from the repository.

There’s still one big question: the action takes a long time to run, which makes the feedback loop pretty slow. It got me thinking—can we run the memory leak detection with unit tests? We’ll dive into this further in Part 4.

Have you tried automating memory leak detection? I’d love to hear your thoughts and learn from your experience. Reach out to me at @droid_singh and let me know.


Photo by Joe Zlomek on Unsplash