Firefox OS runs on some severely memory-constrained devices, and it's easy for apps to exhaust the memory available on such systems. When a process exhausts the memory available on the system, the kernel must kill some other processes in order to free up memory. This article describes how the low memory killer and low memory notifications work — the two systems on the device that control what processes are killed to keep the main system running when it has run out of memory.
FxOS operation involves multiple processes — one "main process" running basic system services and potentially many "child processes". In general every app runs in its own child process. Since in the FxOS environment applications are rarely closed by the user the system automatically manages their lifetime to make room for new apps or for existing apps requiring additional memory.
Two subsystems are used to manage this: the Low memory killer (LMK) and Low memory notifications.
The LMK is a subsystem of the Android kernel that automatically kills processes to make room for memory requests. In order to choose which process is killed first for freeing up memory, each process is assigned a priority via the /proc/<pid>/oom_adj or /proc/<pid>/oom_score_adj files. A process's priority is known as its adjustment score, or
oom_adj values correspond to higher priority processes.
In general, the larger the adjustment score is, the more likely a process is to be killed. The LMK provides multiple levels, each corresponding to a certain amount of free memory and a minimum adjustment score. Whenever the amount of free memory in the system drops below a certain level, all processes with an adjustment score higher than the minimum specified for that level are eligible to be killed. The LMK will start killing these processes, larger ones first, and keep going until it has freed enough memory to go above the threshold again.
Note: The process killed when the device runs out of memory isn't necessarily the one that "caused" the out-of-memory condition.
In Firefox OS apps are killed in the following priority order policy, which is enforced by giving each application a priority level and associating an OOM adjustment score to these levels (the current values are set in prefs):
- The first apps to be killed will be the background apps, starting with the least recently used.
- The homescreen application is killed next.
- Background applications that are perceivable by the user are killed next (for example, a music player playing audio in the background or an app holding a
cpuwakelock and having a registered handler for system messages.)
- If the keyboard application is running, it will be killed next.
- Foreground applications will be killed next.
- Finally, foreground applications that have requested
cpuwakelocks are the last to get killed.
Note: Most child processes run with
oom_adj 2 while they're in the foreground. Child processes in the background run with
6 (inclusive). Exactly what
oom_adj a child process has while in the background depends on a number of factors, such as whether it's playing sound, whether it's the homescreen app, etc.
There's a couple of exceptions to these rules:
- The main process is never killed by the LMK as doing so would kill all child processes and restart the operating system. The main process runs with
- We keep a process around to speed up the startup of new applications called the preallocated process. This process is usually always kept alive because it consumes very little memory and significantly speeds up application startup. The only case under which it can be killed is if there's not enough memory available for the main process to keep running after having killed all other processes.
The second mechanism we use to free memory is low memory notifications. The LMK provides a special threshold that, when crossed, can send notifications to the userspace that is running low on memory. Both the system application and regular user apps continuously wait for this condition and will react upon it by sending a
memory-pressure event via the observer service. This event is visible only to C++ and chrome JS code and not directly by an application. Through the Gecko codebase we use this event to free as much memory as we can — normally by purging internal caches (images, DNS, sqlite, etc.), dropping assets that can be recreated (WebGL contexts for example) and running the garbage collector and cycle collector.
When encountering a low memory condition the first
memory-pressure event that will be sent will have the
low-memory payload. If after a predefined time (5s) we're still in a low memory condition another
memory-pressure event will be fired, but this time with the
low-memory-ongoing payload. This payload is used when we continue to experience low-memory conditions and we want to flush caches and do other cheap forms of memory minimization, but know that heavy handed approaches like a GC are unlikely to succeed.
Currently the low memory threshold is set above the LMK level for background applications but below the one for the homescreen. So the aggregated action of the LMK and low memory notifications is as follows when a deivce is running out of memory:
- Kill background apps in least-recently-used order.
- If we didn't free enough memory, send
memory-pressureevents to all remaining applications.
- If the condition persists resend a
memory-pressureevent every 5 seconds, but mark them as ongoing so the GC/CC doesn't react to them.
- Kill the homescreen.
- Kill perceivable or high-priority background applications
- Kill the keyboard app, if it is running.
- Kill foreground applications.
- Kill high priority foreground applications.
- Kill the preallocated process.