Skip to content
academia.sh

Lesson 07 / 14

Background Processing Limits

Background processing limits describe the rules the operating system sets for running work while the app is not visible. The lesson measures that media backup is deferred and live location is disabled when background capability is restricted.

Published Updated

In this lesson

  • Explains the background task mechanisms of the two platforms and the limits the operating system imposes
  • Distinguishes low power mode as a condition, not an operability level
  • Measures that media backup is deferred when background capability is restricted
  • Shows that deferred work does not run but is not lost, while disabled work is not left for later
Contents

In the previous lesson, changes made offline entered a queue and were to be sent when the network returned. Who starts the sending was not asked. The user may have closed the app, or the phone may be in a pocket. For an app to run work while it is not visible, the operating system has to allow it, and the operating system does not give that permission generously. This lesson explains the background processing mechanisms of the two platforms and the limits they bring, then measures how much those limits affect a feature.

The course question stays the same: how well does this feature work under this condition? The nine records in the opening lesson came out as five full, one degraded, two deferred, and one disabled. This lesson does not change that count; it measures only its own slice.

The Operating System Decides

Working in the background is the operating system’s decision, not the app’s. The app asks for a job; the operating system looks at conditions such as battery, network, the user’s habits, and the load from other apps, and decides when the job runs, or even whether it runs at all. So a background job never means “run now”; it means “run at a suitable moment”.

On iOS the app registers a task request, and when the system finds it suitable, it wakes the app for a short time. The app does not set the wake time. On Android the job is handed to a work manager together with the conditions it needs; the manager runs the job when the conditions are met and the device’s power management allows it. With both mechanisms the app has no exact clock. The limits on duration and frequency are set by the system version and the user’s settings; this lesson does not write these limits as numbers, because they change with every version and every device.

The iOS snippet below shows requesting a backup task. The snippet is not measured.

// Platform code: iOS background task
func scheduleBackup() {
    let request = BGProcessingTaskRequest(identifier: "app.media-backup")
    request.requiresNetworkConnectivity = true
    request.requiresExternalPower = false
    try? BGTaskScheduler.shared.submit(request)
}

On Android the same request is handed to the work manager together with its conditions. The snippet is not measured.

// Platform code: Android background task
fun scheduleBackup(context: Context) {
    val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .build()
    val request = OneTimeWorkRequestBuilder<BackupWorker>()
        .setConstraints(constraints)
        .build()
    WorkManager.getInstance(context).enqueue(request)
}

The two snippets ask for the same thing: a backup job that runs while there is a network. Both create a registration; the job does not run right away. Whether it runs is up to the system’s answer.

Low Power Mode

The user or the system can turn on low power mode to save battery. While the mode is on, the system cuts or thins out background work: refreshing stops and scheduled jobs run later. While the app is open, the network and storage are still there; what disappears is the permission to run in the background.

This lesson treats low power mode as a condition. A condition is the set of capabilities present at that moment: in low power mode, network and storage are present and background is not. Under the same condition one feature can come out full, another deferred, and a third disabled.

Building the Features

Measuring needs the course core. The core is familiar from the first lesson and is given again in the same form in every lesson; this lesson does not touch it.

// operability.mjs — classifies how fully a feature works under a condition
export const LEVELS = ["full", "degraded", "deferred", "disabled"];
const FALLBACKS = ["degraded", "deferred"];

export function feature(name, needs, fallback = {}) {
  return { name, needs, fallback };
}

export function condition(name, has) {
  return { name, has };
}

function judge(item, cond) {
  const missing = item.needs.filter((need) => !cond.has.includes(need)).sort();
  let level = "full";
  for (const need of missing) {
    const given = FALLBACKS.includes(item.fallback[need]) ? item.fallback[need] : "disabled";
    if (LEVELS.indexOf(given) > LEVELS.indexOf(level)) level = given;
  }
  return { level, feature: item.name, condition: cond.name, missing };
}

export function assess(features, conditions) {
  const records = [];
  for (const cond of conditions) {
    for (const item of features) records.push(judge(item, cond));
  }
  return records;
}

export function printLevels(records) {
  for (const r of records) {
    const missing = r.missing.length === 0 ? "-" : r.missing.join(",");
    console.log(`${r.level}: ${r.feature} @ ${r.condition} missing=${missing}`);
  }
}

export function countLevels(records) {
  for (const name of LEVELS) {
    console.log(`${name}: ${records.filter((r) => r.level === name).length}`);
  }
  console.log(`affected: ${records.filter((r) => r.level !== "full").length}`);
}

The lesson’s helper file builds three features. MediaBackup backs up photos; it needs network and background, and when background is missing it postpones its work, so its fallback is deferred. LiveLocation shares location continuously; it needs the same two capabilities and has no fallback, because sharing a stale location is meaningless. Catalog is a product list and needs only network. stateOf(link) builds a condition from the connection and background state.

// background.mjs — builds the background features and conditions
import { feature, condition } from "./operability.mjs";

export const mediaBackup = feature("MediaBackup", ["network", "background"], { background: "deferred" });
export const liveLocation = feature("LiveLocation", ["network", "background"]);
export const catalog = feature("Catalog", ["network"]);

export function stateOf(link) {
  const has = [];
  if (link.network) has.push("network");
  if (link.background) has.push("background");
  return condition(link.name, has);
}

The model of the scheduler is in a second helper file. schedule looks at a record’s operability: if it is full, the job runs; if it is deferred, the job is added to the waiting list; if it is disabled, the job does not run and does not enter the waiting list. runWindow imitates a window given by the system: the window runs a certain number of jobs from the waiting list.

// scheduler.mjs — builds background work according to the operating system's window
export function schedule(record, pending, job) {
  if (record.level === "deferred") {
    pending.push({ feature: record.feature, job });
    return "waiting";
  }
  return record.level === "full" ? "running" : "skipped";
}

export function runWindow(pending, slots) {
  return pending.splice(0, slots).length;
}

The first measurement runs backup and the catalog in two states: the app in the foreground and background restricted.

// main.mjs
import { assess, printLevels } from "./operability.mjs";
import { mediaBackup, catalog, stateOf } from "./background.mjs";
import { schedule } from "./scheduler.mjs";

const states = [
  { name: "foreground", network: true, background: true },
  { name: "background-restricted", network: true, background: false },
];
const records = assess([mediaBackup, catalog], states.map(stateOf));
printLevels(records);
const pending = [];
for (const r of records.filter((x) => x.feature === "MediaBackup")) {
  console.log(`${r.condition}: ${schedule(r, pending, "photo-1")}`);
}
console.log(`waiting: ${pending.length}`);
full: MediaBackup @ foreground missing=-
full: Catalog @ foreground missing=-
deferred: MediaBackup @ background-restricted missing=background
full: Catalog @ background-restricted missing=-
foreground: running
background-restricted: waiting
waiting: 1

While the app is in the foreground, backup is full and the job runs right away. While background is restricted, backup is deferred and the job enters the waiting list. Catalog is full in both states, because it depends only on the network.

OS3. The model builds background as a single capability: either it is there or it is not. A real system gives background work short and unpredictable windows; when the job runs, how many windows it takes to finish, and whether it ever runs are the system’s decision. In the model, runWindow takes the size of the window as a job count chosen for the lesson; that number says nothing about the behavior of a system. The model builds this operability’s rule; the model is not the framework itself.

Deferred Work Is Not Lost

Deferred work has not run, but it has not been lost. It stays in the waiting list and runs when the system gives a window. The second measurement puts three backup jobs in the queue under the restricted condition and gives two windows. Under the same condition LiveLocation asks for three location updates.

// main.mjs
import { assess, printLevels } from "./operability.mjs";
import { mediaBackup, liveLocation, stateOf } from "./background.mjs";
import { schedule, runWindow } from "./scheduler.mjs";

const states = [
  { name: "background-restricted", network: true, background: false },
  { name: "foreground", network: true, background: true },
];
const records = assess([mediaBackup, liveLocation], states.map(stateOf));
const restricted = records.filter((r) => r.condition === "background-restricted");
printLevels(restricted);
const pending = [];
for (const job of ["photo-1", "photo-2", "photo-3"]) schedule(restricted[0], pending, job);
for (const job of ["location-1", "location-2", "location-3"]) schedule(restricted[1], pending, job);
console.log(`first window: ${runWindow(pending, 2)} ran, ${pending.length} waiting`);
console.log(`second window: ${runWindow(pending, 2)} ran, ${pending.length} waiting`);
deferred: MediaBackup @ background-restricted missing=background
disabled: LiveLocation @ background-restricted missing=background
first window: 2 ran, 1 waiting
second window: 1 ran, 0 waiting

The three backup jobs finished in two windows: in the first, two ran and one waited; in the second, the remaining one ran. The location updates did not appear in any window, because schedule does not add the job of a disabled record to the waiting list. A disabled feature does not run either, but its work is not left for later; the user cannot share their location in the background, and that sharing is not done at the next opportunity.

Low Power Mode Is Not a Level

The third measurement puts low power mode side by side with the app-in-foreground condition. Three features run under two conditions, and only the low power records are printed.

// main.mjs
import { assess, printLevels } from "./operability.mjs";
import { catalog, mediaBackup, liveLocation, stateOf } from "./background.mjs";

const states = [
  { name: "foreground", network: true, background: true },
  { name: "low-power", network: true, background: false },
];
const records = assess([catalog, mediaBackup, liveLocation], states.map(stateOf));
printLevels(records.filter((r) => r.condition === "low-power"));
full: Catalog @ low-power missing=-
deferred: MediaBackup @ low-power missing=background
disabled: LiveLocation @ low-power missing=background

With low power mode on, three features fell to three different levels. Low power mode is a condition; degraded is an operability level, and the two words are not the same thing. The mode says which capability decreased; the level is decided by the fallback the feature defined for that missing capability.

The Bounding Measurement

This lesson’s focus record is MediaBackup @ background-restricted. The last measurement runs three features under three conditions and prints the records of the background-restricted condition, the count, and the waiting jobs.

// main.mjs
import { assess, printLevels, countLevels } from "./operability.mjs";
import { mediaBackup, liveLocation, catalog, stateOf } from "./background.mjs";
import { schedule } from "./scheduler.mjs";

const states = [
  { name: "foreground", network: true, background: true },
  { name: "background-restricted", network: true, background: false },
  { name: "low-power", network: true, background: false },
];
const records = assess([mediaBackup, liveLocation, catalog], states.map(stateOf));
const restricted = records.filter((r) => r.condition === "background-restricted");
printLevels(restricted);
countLevels(records);
const pending = [];
for (const r of restricted) {
  for (const job of ["job-1", "job-2", "job-3"]) schedule(r, pending, job);
}
const waiting = (name) => pending.filter((p) => p.feature === name).length;
console.log(`waiting: MediaBackup ${waiting("MediaBackup")}, LiveLocation ${waiting("LiveLocation")}`);
deferred: MediaBackup @ background-restricted missing=background
disabled: LiveLocation @ background-restricted missing=background
full: Catalog @ background-restricted missing=-
full: 5
degraded: 0
deferred: 2
disabled: 2
affected: 4
waiting: MediaBackup 3, LiveLocation 0

The first line carries the lesson’s operability: when background capability is restricted, backup came out deferred. Of the nine records, five are full, two are deferred, and two are disabled; four are affected. The two deferred records are two conditions of the same feature: restricted and low power.

The last line shows how this differs from the question of M12/K04. That lesson asked whether a piece ran in this request. Here neither backup nor location sharing ran in the background, but their situations are different. Deferred backup has not run and has not been lost: three jobs wait. Disabled location sharing has not run either, but it has no waiting job. Operability does not classify whether a job ran; it classifies what happens to the job when it did not run.

Summary

  • This lesson’s operability: deferred. MediaBackup @ background-restricted came out deferred because background is missing and a deferred fallback is defined; LiveLocation stayed disabled under the same condition because it has no fallback.
  • Working in the background is the operating system’s decision. On iOS the system wakes the app when it finds the task request suitable, and on Android the work manager runs the job when the conditions are met; the limits on duration and frequency change with the version and the settings.
  • Low power mode is a condition, not an operability level: under the same mode Catalog came out full, MediaBackup deferred, and LiveLocation disabled.
  • Deferred work has not run but has not been lost: three backup jobs finished in two windows. The work of a disabled feature does not enter the waiting list.
  • Of the nine records, five are full, two are deferred, and two are disabled; four are affected.

Next Step

In this lesson, background capability was either there or not; the connection was always there. In reality the connection also drops, slows down, and comes back. The next lesson, Connectivity State Management, covers monitoring the connection, a weak connection, retry with exponential backoff, and showing the connection state to the user. It then measures how well an image gallery works on a weak connection.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close