{'fraction_non_alphanumeric': 0.02528769694275695, 'fraction_numerical': 0.003160962117844619, 'mean_word_length': 5.1788220933780895, 'pattern_counts': {'":': 0, '<': 0, '<?xml version=': 0, '>': 0, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 3, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
Quantifying Daily Evolution of Mobile Software Based on Memory Allocator Churn

The pace and volume of code churn necessary to evolve modern software systems present challenges for analyzing the performance impact of any set of code changes. Traditional methods used in performance analysis rely on extensive data collection and profiling, which often takes days. For large organizations utilizing Continuous Integration (CI) and Continuous Deployment (CD), these traditional techniques often fail to provide timely and actionable data. A different impact analysis method that allows for more efficient detection of performance regressions is needed. We propose the utilization of user mode memory allocator churn as a novel approach to performance engineering. User mode allocator churn acts as a proxy metric to evaluate the relative change in the cost of specific tasks. We prototyped the memory allocation churn methodology while engaged in performance engineering for a major iOS application. We find that calculating and analyzing memory allocator churn (a) results in deterministic measurements, (b) is efficient for determining the presence of both individual performance regressions and general performance-related trends, and (c) is a suitable alternative to measuring the task completion time.


INTRODUCTION
Software evolution is a well-researched topic [21]. The commonly agreed-upon definition of software evolution is a process of continual change from a lesser or worse state to a higher or better state [1]. The how view of software evolution focuses on practical aspects "that provide means to direct, implement and control software evolution" [16, p. 4]. We apply principles of software evolution to tackle the problem of understanding how the performance of key user scenarios in mobile applications changes daily.
Current industry practices for popular mobile applications, such as Facebook or Instagram, use CI/CD [11,17]. Performance regressions are an evolution of software in an undesired direction. Users of mobile applications are sensitive to performance regressions. Poor performance increases the likelihood of application abandonment [26]. Optimizing performance characteristics is the key to user satisfaction and prolonged engagement with the application [8]. Given the frequency of releases in a CI/CD environment, engineers need to have the means to detect, diagnose, and fix performance regressions within a limited time frame. Moreover, traditional techniques used in performance analysis rely on extensive amounts of data and profiling [7,10]. The data collection and profiling can take days, rendering the initial diagnosis obsolete.
To analyze the performance of an application more efficiently, we propose a simplified heuristic of using memory allocator churn. Churn is the number of calls to the memory allocator and the size of objects allocated and released in the heap of the current process during a specific period. We use the churn as a proxy metric to (a) determine the presence of performance regressions, (b) evaluate their severity, and (c) rank the order of performance investigations. This paper is based on our experience prototyping the usage of allocator churn for six months while working on performance engineering for an iOS version of application X.

BACKGROUND AND MOTIVATION 2.1 Contemporary software development
Modern software development methodologies such as CI/CD strive towards frequent release of updates. That approach is different from the waterfall model, which has been dominant for decades [2,18].
A final version of software within the waterfall model is released only after a long development cycle followed by a thorough testing period. However, popular mobile and web applications such as Facebook, Instagram, and Twitter use CI/CD [11,17].
Each application has a core codebase and several dependencies. The development of all these components can involve hundreds to thousands of engineers. Each engineer may contribute code to their respective codebases daily. Any code change can modify the application's behavior, dependency graph, or performance .
Every application has a set of critical scenarios and phases of execution which can influence its popularity and usage. For example, boot time is a key performance metric for the operating system (OS), responsiveness during composing and publishing a tweet is one of the critical scenarios for Twitter, and application start time is a crucial metric for Facebook [22]. Research shows that responsiveness, or time the application takes to react to a user's command is critical to the application's reputation and overall success [25].

Software evolution and performance
The waterfall model dominant in the past meant that engineers had time to understand the current state of the product and conduct a performance analysis on it. Given the increased focus on the single trunk development model, the ability to control the inclusion of code changes in the specific daily builds of the application diminishes [24]. With CI/CD, (a) the amount of daily code churn is high, (b) application release cadence is frequent, and (c) the time frame to fix the performance-related issues has decreased. Our research question becomes: RQ1: What data and metrics can we use to understand the software's evolution since the last daily build [15]?
Insights from those metrics can then be used to isolate the code changes responsible for performance regressions. Understanding the root cause of regressions enables us to make required modifications to software to optimize its behavior in the target environment and for the critical scenarios.

DATA AVAILABLE FOR ANALYSIS 3.1 Code changes, commit descriptions, and work items
Modern source control systems such as Git and Mercurial use the distributed model. Engineers commit and test the code changes locally. When new code is ready for integration into the main development branch, either the code collaboration tool (e.g., CodeFlow, Critique, Gerrit, or Phabricator) merges them after passing the code review, or changes are manually pushed to the target branch. One solution to analyze the performance impact is to read through each code change, commit description, and resolved work item since the last daily build. This approach is impractical for most engineers working on complex systems due to the sheer volume of data processing. Our empirical observations about commit descriptions indicate that they are succinct and do not describe every single change in detail. Moreover, reading through every code change in each component is not time-effective.

General performance characteristics
A typical build validation process executes several automated test cases on each candidate build daily. Some of these test cases measure general performance-related characteristics of the application under a set of common usage scenarios. For example, active thread count, maximum memory usage, and the number of modules loaded. These metrics, however, indicate only generally the trend of an application's behavior. We can approximate whether an application is "doing more work" or utilizing more resources for performance engineering purposes, but we cannot pinpoint specifics.

Specific observable performance characteristics
In software performance engineering [7], we can distinguish between the following broad categories of metrics: • CPU. It is critical to know how long a particular operation takes for metrics that measure user experience (e.g., responsiveness). To measure the duration of an operation, it is common to use elapsed time (wall-clock time). A typical OS (not real-time) scheduler operates on a per-thread basis and makes decisions about quantum (allowance of CPU time) assignments based on the behavior of all executing OS threads. This scheduling approach makes conducting deterministic measurements time-consuming (due to several iterations required) and in most cases requires a variety of hardware and in-depth profiler data to interpret the results correctly. • I/O. I/O refers to the interactions with the storage device such as a disk, inter-process communication, and network traffic. Slow I/O directly contributes to increased wall clock time, impacting the user experience. Measuring the amount of I/O and speed is highly dependent on the environment (e.g., file system cache behavior on a particular OS), the hardware used, and application usage scenarios. • Memory. Reducing memory consumption is a well-known optimization technique for mobile software [8]. Decreasing the amount of memory an application consumes reduces the application's launch time [13]. Metrics such as overall consumption, allocation rate, fragmentation, and page faults, are the basis of evaluation to determine the cost of scenarios. However, because of a variety of different classifications, even interpreting the exact meaning of a metric and impact to application's performance is an involved and costly process. • Battery or power. Battery consumption is a metric relevant mainly to software running on mobile devices. Measuring power utilization correctly for intervals lasting tens or hundreds of milliseconds typically requires special hardware and access to facilities of the OS, which enable exposing that data. We find that this approach is not practical for daily performance analysis due to the investment of time required. Gathering valid data about all these categories is a time-consuming process. To cover a wide range of target environments, the performance testing needs to take place on multiple versions of OS executing on different hardware versions.
A key observation, based on our industry experience when conducting performance analysis on a variety of products on different OSs is the following: Most types of "work" done by any application require interaction with the memory manager. The efficiency of the allocator influences software performance [14]. Memory allocator churn can be a deterministic way to measure the amount of work an application must do and is independent of CPU execution speed.

MEASURING MEMORY ALLOCATOR CHURN 4.1 Overview
Lower-level languages, such as C, C++, and earlier versions of Objective-C require explicit memory management. Source code gives the reader details about memory allocation and release. In higher-level programming languages such as JavaScript, PHP, or Python, engineers are not required to manage memory explicitly. An engineer may use data types like strings or hash tables and never allocate or free memory associated with them directly. The performance costs for calling frameworks or utilizing language constructs (e.g., blocks in Objective-C) are not immediately visible based on the code. Therefore, it is hard for both the author and reviewer to understand the performance impact of changes.
For this paper, we consider the standard POSIX memory allocation functions (free(), calloc(), malloc(), and realloc()) as an interface to allocator [9]. The runtime for a particular language ends up eventually translating various language constructs to use system libraries which consequently call these functions.

Metrics
We define the critical parts of application execution as two marker points in the execution timeline of a specific thread: and +1 . For example, placing a starting marker at the beginning of the function and the ending marker before the function returns. Similarly, this approach is extendable to cover more involved user scenarios like the start of rendering some content and when the rendering finishes. Each marker can contain other child markers, and the markers can overlap.
Tasks like manipulating strings, loading modules, processing images, calling other APIs, and marshaling data all involve allocating and deallocating memory. We can quantify how expensive a particular critical phase is by counting the amount of allocator churn during it. To measure the allocator churn, we need to gather data about allocator usage. Multiple ways to intercept calls to the allocator exist. Using callbacks [20, p. 977], hooks [6], preloading the shim [12], and debug memory libraries [23] are all valid options.
We can quantify the allocator churn by looking at the following values: • Total churn in bytes. Allocating more memory will cause the allocator to execute more CPU instructions. For example, coalescing neighboring blocks, paging out some memory to the disk, and making syscalls. • Total number of function calls to the allocator. Calling more allocator functions means similarly execution of extra CPU instructions to validate the input, manage the stack frame, and acquire necessary synchronization primitives.
Anecdotally, we observe that both values are an indication of the amount of work the caller's code will perform. Putting the two observations above together lead us to propose calculating the cost between two markers and +1 on a specific thread as where is the number of memory allocation related functions called between two markers, is a function calculating the cost of a function , and is the number of bytes the function operates on.

Relative costs of allocator functions
We define ( , ) = ( ) · log 2 where is a weight for a function and is the number of bytes the function operates on. Defining the weight function depends on several variables: OS and its version, types of compiler optimizations enabled, or memory allocator used. A simplified model assumes that allocating memory costs more than freeing it, and reallocation is even more costlier than either allocation or deallocation.

Issues associated with valid measurements
This section describes the conceptual issues associated with gathering valid measurements for the allocator churn. Single thread versus multiple threads. An application can have multiple threads executing at the same time. Figure 1 shows a sample snapshot of process execution. Decisions made by the OS scheduler depend on variables an application cannot fully control. For example, the behavior of other applications executing at the same time, high-priority tasks executed by the OS drivers, or attempts to avoid priority inversion.
It is possible to capture all the allocator-related activity during the execution of a critical phase for an entire process. We find that this approach is fragile and noisy. Some of the critical phases we measure last only tens or hundreds of milliseconds. Executing even one unrelated callback thread in parallel with the critical phase we measure can change the outcome of measurements by orders of magnitude. We find it optimal to measure parts of the critical phases on a per-thread basis and later merge the results.
Custom memory managers. Each OS provides a default user mode memory manager. There are highly specialized applications and environments where performance is of the utmost importance (e.g., a database engine or a stock trading system) [5]. Those applications benefit from custom memory allocators [3,4]. We can use a custom memory allocator to increase the application's performance. Configuration for a custom allocator can be tuned to optimize a specific application's usage patterns. Custom allocators can use various techniques to intercept and replace the calls to the memory manager. Those techniques may not be compatible with an interception of function calls to a default allocator we use. We need to use the intercept methods specific to a custom allocator.

IMPLEMENTATION RELATED PROBLEMS
When prototyping this methodology on application X, we encountered the following categories of technical challenges: • Keeping track of allocations. To keep track of relevant statistics, an application will need to store the memory usage patterns. We found that using data structures of a fixed size, or thread-local storage are functional solutions. If threadlocal storage entries themselves are allocated lazily using malloc(), then explicit avoidance of recursion is also required. • Avoiding performance impact. To gather representative data, internal testing is not enough, and it is desirable to collect the metrics from the production environment. We observe that depending on a scenario, the number of interactions with the allocator may reach up to thousands or tens of thousands of events per second. • Interactions with other intercept mechanisms. Using multiple interceptors at the same time causes problems. Tools like AddressSanitizer [19], which engineers employ to detect memory corruptions, use their mechanisms to intercept memory allocation functions. Similar problems are associated with using profilers such as Xcode's Instruments.

QUESTIONS TO RESEARCH COMMUNITY
Our findings have raised several questions which we seek answers to: • Relationship between memory allocator churn and other performance metrics. Allocating memory is a process that causes the execution of CPU instructions. Depending on the OS, it may also cause noticeable delays in I/O operations in case of page faults. Our primary experience comes from iOS, where writable pages are never stored on the disk. Therefore, we have limited data about associating memory allocator churn with the cost of I/O. • Determining OS and allocator-specific cost functions. Based on our observations, the relative cost differs depending on OS, its version, compiler used, compiler options applied, or memory manager settings. We need more comprehensive testing and potentially benchmarks to evaluate how the weight of each of those functions depends on a specific environment.
• Replicating our work in different environments. Our work is specific only to the environments where an increase in tens or hundreds of milliseconds is critical. We theorize that our proposed approach is also practical on products where the performance constraints are more relaxed.

THREATS TO VALIDITY
The threats associated with construct validity are caused by not interpreting or correctly measuring the theoretical constructs discussed in the study. We intercept only a specific set of functions related to memory management. Applications can always use lowerlevel functions directly and bypass the runtime library exposing the memory management functionality. One of the concerns for internal validity is the interpretation of results and if the conclusions we present can be really drawn from the data available. Because of confidentiality reasons, we are not able to share either the specific performance regressions or their correlation with other application-specific data with the public. We reach our conclusions based on analyzing the data gathered from the production environment, correlating it with other performance related metrics, and discussions with performance engineering experts working on this topic.
Threats to external validity are related to the application of our findings in other contexts. We have used this technique in the context of only one mobile OS, a single application (albeit having hundreds of millions of users), and a small set of programming languages (C, C++, and Objective-C). As a result, we cannot conclusively state that this approach works for other environments.

CONCLUSIONS AND FUTURE WORK
We present an approach agnostic of CPU execution speed to understanding the evolution of critical phases of software based on analyzing the memory allocator churn. The proposed technique enables comparison between execution intervals where median duration in the production environment is from tens to hundreds of milliseconds. Using memory allocator churn is a more deterministic indicator of performance regressions than elapsed wall-clock time. We use this technique for six months on an iOS version of application X. Findings from the memory allocator churn help identify individual performance regressions and confirm (or disprove) the presence of problems uniformly impacting the entire application.
Our future work will focus on (a) the toolset necessary for preemptively detecting the change in the allocator churn and notifying the engineers during the development phase, and (b) determining the ways to rank the cost of functions based on their memory churn and provide engineers with means to assess the potential cost in the production environment.