Graphics, storage, network & power

Work through bounded subsystem cases and identify where deeper specialization begins.

15 min readChapter 48 of 50Advanced practice
In this chapter

Android subsystems cross framework, native services, HALs, kernel, and vendor implementation. The goal here is not to memorize component names: it is to trace a symptom to a boundary, construct a model that can be falsified, and stop where evidence stops. Each casework uses a small synthetic dataset, a pinned Android 14 source path and symbol to inspect, a worked reasoning model, and an extension. Source establishes implementation facts in that tag; it does not establish runtime behavior on a device.

Before you start#

The four investigations are source-reading/data exercises; no device is required. Optional runtime observations must use an authorized disposable target and remain within the read-only or explicitly disclosed temporary-output steps in the relevant lesson. Vendor source and hardware behavior may be unavailable.

Learning goals#

For one graphics, storage/encryption, networking, and power symptom, you will identify the owning layer and a pinned source entry point; distinguish framework policy from vendor/kernel execution; analyze a tiny supplied dataset without inventing measurements; choose the next evidence that could falsify a favored explanation; and write a bounded conclusion that names unknowns. These caseworks are specialist samplers, not comprehensive training or proof of expertise in all Android subsystems.

Practice#

Casework A — Graphics: a late frame is not automatically a slow GPU

A frame crosses many boundaries
App buffer production
App CPU/rendering work and submitted buffers cover only part of the interaction.
SurfaceFlinger / CompositionEngine
Coordinates outputs and composition; the selected path requires runtime evidence.
Composer / display HAL and vendor code
Hardware composition, fences, and device-specific implementation may be unavailable.
GPU / display driver / panel
Presentation timing needs matching measurements; low app/compositor totals do not prove an on-time frame.
Conceptual model. Conceptual graphics boundaries, not a full runtime call chain. Fences, queueing, scheduling, and deadlines can add delay. Public source does not establish physical GPU/panel behavior.

Question. A screen animation intermittently misses its intended presentation time. Is app rendering, SurfaceFlinger scheduling/composition, a display HAL path, or physical GPU/display work responsible?

Pinned source trail (verified against the tag). Android 14 SurfaceFlinger.cpp implements SurfaceFlinger::composite(TimePoint, VsyncId), which constructs CompositionRefreshArgs, calls mCompositionEngine->present(refreshArgs), and then consumes composition results. In the pinned CompositionEngine.cpp, CompositionEngine::present(CompositionRefreshArgs&) calls preComposition(args), prepares each output, then calls each output's present(args). Compact source excerpt (one blank line omitted):

cpp
// SurfaceFlinger.cpp, SurfaceFlinger::composite
mCompositionEngine->present(refreshArgs);
moveSnapshotsFromCompositionArgs(refreshArgs, layers);

// CompositionEngine.cpp, CompositionEngine::present
preComposition(args);
for (const auto& output : args.outputs) {
    output->prepare(args, latchedLayers);
}
for (const auto& output : args.outputs) {
    output->present(args);
}

These exact call sites establish a caller/callee edge in this tag, not which composition path a product selected at runtime. See pinned SurfaceFlinger.cpp and CompositionEngine.cpp. HWC/display HAL and GPU-driver implementation may be vendor-owned or closed.

Synthetic teaching dataset (not captured trace data):

Frameapp work (ms)compositor work (ms)presentation-late flag
15.03.1no
27.23.0no
35.43.2yes
45.18.8yes
55.33.3no

This table suggests frame 4 has an unusually long compositor interval; it does not explain why. Frame 3 is late without high recorded app/compositor work, so the dataset itself argues against a single simplistic GPU explanation. Possible causes include scheduling delay, fence wait, prior queueing, display deadline/presentation timing, measurement-window mismatch, or hidden hardware work. A frame's app and compositor slices do not necessarily cover the entire pipeline.

Investigation exercise. Write a layer diagram from app buffer production through SurfaceFlinger/composition to HWC/display. For each arrow mark source-confirmed, runtime-observed, vendor-dependent, or unknown. Propose one test: correlate frame timeline/vsync/fence/scheduler intervals for late and on-time frames under the same animation and refresh mode. State a falsifier—for example, if late frames have no longer compositor work or scheduling delay but a fence wait grows, the “SurfaceFlinger CPU composition is the bottleneck” hypothesis weakens. Do not infer panel refresh rate from a software trace alone.

Model answer. Source confirms SurfaceFlinger coordinates composition and delegates through composition machinery; it does not prove which composition path the runtime selected. The synthetic sample makes frame 4 a candidate for compositor-side delay and frame 3 evidence that elapsed deadline miss can occur without the supplied work totals increasing. Next collect a real, short frame-timeline trace on an identified target, compare the same interaction and refresh conditions, and inspect fence/scheduling timing. If the target's HWC/GPU internals are closed, stop at that boundary and report it. A Cuttlefish result cannot establish physical display/GPU behavior.

Source-reading prompt. In pinned SurfaceFlinger.cpp, identify SurfaceFlinger::composite, follow CompositionEngine::present into its output loop, and record the argument passed plus one callee. State what the source path cannot show: whether a particular layer was client-composed, which vendor composer implementation ran, or whether the physical panel presented on time.

For the mechanisms behind this case, continue to Graphics buffers and fences: producer/consumer ownership, gralloc handles, acquire/release dependencies, HWC versus client composition, and a worked deadline calculation. “Fence wait” should become a specific dependency you can explain, not another unexplained label for a late frame.

Casework B — Storage and FBE: a mount result is not an encryption diagnosis

A read can race preparation

Synthetic teaching data, not a device capture.

  1. 0 ms: unlock request accepted

    Acceptance alone does not prove the app’s CE storage is ready.

  2. 18 ms: preparation starts

    Storage/key preparation and user-started state are distinct.

  3. 43 ms: app read returns EACCES

    DAC, SELinux, key state, or app-level handling remain possible.

  4. 76 ms completion; 111 ms retry succeeds

    Consistent with a readiness race, but timestamps alone do not establish causality.

Synthetic teaching data. Ordered events from the fictional storage table, not proof of an encryption cause. Correlate user/path, caller identity, labels, mounts, and key state to distinguish alternatives.

Question. After a user unlock, an app's credential-encrypted data appears unavailable for a short period. Is user state not unlocked, a mount/volume transition delayed, a filesystem operation blocked, or the app observing stale state?

Pinned source trail (verified against the tag). In VoldNativeService.cpp, VoldNativeService::prepareUserStorage(...) checks system/root identity, validates UUID, acquires the crypto lock, then calls fscrypt_prepare_user_storage(...). The separate lifecycle entry point VoldNativeService::onUserStarted(int32_t) calls VolumeManager::onUserStarted(userId); VolumeManager.cpp implements that method by creating emulated volumes for a not-yet-started user, inserting the ID into mStartedUsers, calling createPendingDisksIfNeeded(), and returning. These are distinct flows; “user started” is not synonymous with “credential-encrypted storage is ready.” Per the file-based encryption documentation, credential-encrypted (CE) storage is the default and is only available after the user unlocks the device.

cpp
// VoldNativeService::prepareUserStorage
ACQUIRE_CRYPT_LOCK;
return translateBool(fscrypt_prepare_user_storage(uuid_, userId, userSerial, flags));

// VoldNativeService::onUserStarted
return translate(VolumeManager::Instance()->onUserStarted(userId));

This source verifies the calls and sequencing, not whether an app's path was mounted/accessed at a specific runtime instant. Use Verified Boot and encryption and official file-based encryption documentation for DE/CE distinctions. Vendor filesystem/crypto path and exact mount configuration are product-specific.

Synthetic teaching dataset (not real logcat):

eventsynthetic time after unlock (ms)
unlock request accepted0
user storage preparation started18
app read returns EACCES43
preparation completion observed76
retry succeeds111

The sequence is consistent with a race between app access and storage preparation, but timestamps alone do not establish the owning process, identity, mount state, or whether EACCES came from DAC, SELinux, encryption/key state, or app-level handling. “Encrypted” is not a generic explanation for every access denial. A repeated retry that succeeds could also hide a policy bug.

Investigation exercise. Draw the state transition and list the minimum evidence to distinguish “user key/storage not ready” from “permission denied after storage became ready.” Include the caller UID, relevant AVC if present, service event order, and exact app path/state. Avoid asking learners to unlock a bootloader, clear user data, alter encryption metadata, or remount a phone. Falsify the key-readiness hypothesis by showing the relevant storage state was ready before the denied access and the denial carries a different permission cause.

Model answer. Pinned source can show the native service request/state flow but not this synthetic runtime event. A useful next packet correlates user ID and request IDs/timestamps across framework/vold observations, the path's mount/access state, calling UID, and AVC context. If no AVC exists, that does not prove SELinux had no role; logging may be absent or inaccessible. On a disposable virtual target, use supported read-only state/log evidence only. Report product-specific key/mount implementation as unknown unless source and runtime both establish it.

Source-reading prompt. Compare those two named service calls. Identify which one invokes filesystem/key preparation. Do not assume its return proves a particular app path is accessible; find the relevant access state or label it outside the snippet. This distinguishes “request accepted” from “ready for this app.”

Casework C — Networking: “no internet” spans policy, routing, and radio

Available is not connected
App request and socket
App UID, process-bound network/VPN, DNS result, and stale socket state.
Connectivity framework
Network selection, capabilities, callbacks, and per-request policy.
Native network configuration
netd, routing/resolver state, kernel sockets, and per-UID rules.
Vendor / radio / upstream / endpoint
Correct local selection still cannot guarantee remote response. Avoid collecting payloads or credentials.
Conceptual model. Investigation boundaries, not a direct call edge from network-agent registration to networkCreatePhysical. The case’s source snippets do not supply that plumbing.

Question. A connectivity transition shows a network available, but one app request times out. Did the framework select the wrong network, did netd fail to apply routing/DNS policy, did the app use a stale socket, or did the upstream network fail?

Pinned source trail (verified against the tag). Android 14 ConnectivityService.java is in the modular Connectivity repository at packages/modules/Connectivity/service/src/com/android/server/ConnectivityService.java, not frameworks/base/services/core. ConnectivityService.handleRegisterNetworkAgent(...) registers the network agent, updates its link properties/capabilities, and calls updateNetworkInfo(nai, networkInfo). At a separate native daemon boundary, NetdNativeService.cpp implements NetdNativeService::networkCreatePhysical(...); after checking network-stack permissions, it calls gCtls->netCtrl.createPhysicalNetwork(...) and translates the result. These are verified caller/callee edges, but do not imply a direct call from network-agent registration to this netd method; Binder/interface plumbing and other paths intervene.

java
// ConnectivityService.handleRegisterNetworkAgent
updateNetworkInfo(nai, networkInfo);
cpp
// NetdNativeService::networkCreatePhysical
int ret = gCtls->netCtrl.createPhysicalNetwork(netId, convertPermission(permission),
                                               false /* local */);
return statusFromErrcode(ret);

Neither source includes the carrier, Wi-Fi access point, or necessarily the device vendor's radio implementation. DNS, VPN, per-UID policy, and socket lifetime add other layers.

Synthetic teaching dataset (invented, not a packet capture):

requestframework network stateapp DNS resultconnect resultduration
Anetwork N availablecached addresstimeout3000 ms
Bnetwork N availablecached addresssuccess82 ms
Cnetwork M selectedfresh addresssuccess95 ms

One failure out of three cannot identify a framework bug. The table omits UID policy, VPN, actual resolver/server, socket reuse, endpoint, packet path, and whether the app retried. It is compatible with a transient upstream problem or a stale connection on request A just as much as a framework-selection race.

Investigation exercise. Propose a privacy-safe reproduction that records target build, app UID, selected network/callback order, DNS/network result, endpoint class (redacted), and timing without recording payloads or credentials. Identify one falsifier for “ConnectivityService chose the wrong network”: show the request used the expected network and routing/DNS state at the time of connect. Do not capture packet payloads or change global routing on a daily-use device.

Model answer. Source tracing establishes that framework connectivity policy and native daemon operations are distinct; it cannot prove either ran for this request. Correlate request identity with network callback and per-UID network selection, then inspect the narrowest authorized diagnostics for route/DNS outcome. If the route is correct but the endpoint times out, investigate upstream/server/radio/vendor boundaries rather than patching selection policy. A synthetic table provides no evidence of actual network state.

A useful negative case. If an app has an explicitly selected process-bound network or VPN, compare that binding with the default network rather than assuming the global “available” callback describes the app's socket. Confirm the application UID and request lifecycle; do not expose destination addresses or packet content in a public report. A source declaration of a callback API does not show which callback ran in the incident.

Casework D — Power: wake lock, suspend, and energy are different evidence

Wake lock, suspend, energy
Framework wake lock
Who requested a power-policy constraint, and for how long? Source/state observations address attribution.
Kernel suspend / resume
Did the system enter suspend? Needs permitted lower-level event evidence, not just framework state.
Physical energy
How much energy was consumed? Needs a calibrated/validated hardware method and controlled workload.
Conceptual model. Relate these using matching intervals and ownership, not a single snapshot. A virtual-device battery estimate is not physical energy measurement.

Question. A virtual device's battery estimate falls during an idle interval. Is system_server holding a partial wake lock, kernel suspend being prevented elsewhere, a background workload consuming CPU, or the estimate merely noisy?

Pinned source trail (verified against the tag). In Android 14 PowerManagerService.java, acquireWakeLockInternal(...) changes framework wake-lock state and calls updatePowerStateLocked(). That method calls updateWakeLockSummaryLocked(dirtyPhase1) before later power-state updates:

java
// PowerManagerService.updatePowerStateLocked
updateWakeLockSummaryLocked(dirtyPhase1);
updateUserActivitySummaryLocked(now, dirtyPhase1);

The caller edge is also explicit in acquireWakeLockInternal(...):

java
mDirty |= DIRTY_WAKE_LOCKS;
updatePowerStateLocked();

The lock is updated under mLock; the method names alone do not show suspend entry or power consumed.

This is framework-side policy/state, not direct measurement of kernel suspend or energy. Kernel suspend blockers, power HAL behavior, fuel-gauge estimates, and board power rails may be outside public AOSP source.

Synthetic teaching dataset (fabricated):

intervalscreenframework wake-lock summaryCPU busy estimatebattery estimate delta
A, 5 minoffone partial lock, owner unknown4%−1%
B, 5 minoffno reported partial lock3%−1%
C, 5 minonone partial lock, known test app11%−2%

This cannot estimate physical energy. Battery percentage is quantized/filtered, and the intervals differ in screen state and possibly temperature, radio, charging, workload, and calibration. A framework-reported lock is a clue about a policy request, not proof that the CPU stayed out of suspend or that the owner caused measured drain.

Investigation exercise. Define a controlled idle comparison on a disposable target, stating what is held constant and what data is available. Distinguish a framework wake-lock attribution question from “did the kernel enter suspend?” and from actual energy measurement. Give a falsifier for “the test app's partial wake lock explains the drain.”

Model answer. Inspect the wake-lock owner/tag and acquisition/release lifetime, then correlate with permitted suspend/resume evidence and workload. For an energy claim, use an appropriate calibrated/validated measurement method on hardware; a virtual battery estimate is not physical power. If lock intervals do not overlap the measured drain or the same pattern occurs without the lock, weaken the attribution. Do not disable power management or force suspend as a tutorial exercise.

Interpretation boundary. A wakelock is a request/state managed through Android power policy; a suspend blocker or kernel wake source is a lower-level mechanism, and actual energy is a physical quantity. These observations can be related, but are not interchangeable. A missing framework lock in a snapshot might miss a short-lived acquisition, while a present lock can be held by a component that is not the energy bottleneck. Preserve interval boundaries and ownership rather than ranking processes from a single aggregate dump.

Extension: one unfamiliar path, independently

Choose a subsystem not covered above—such as input, audio, camera, or thermal policy. Begin at the user-visible symptom, find the Android 14 framework service/API and a caller, identify the next contract boundary (HAL/AIDL/HIDL/kernel/vendor), and stop at unavailable implementation. Submit a three-column evidence ledger: claim / pinned path-symbol or observation / what it cannot prove. Add two competing hypotheses, one discriminating measurement or test, and one security/privacy concern. Do not select a source symbol merely because its name sounds right: follow a registration/call site in the pinned tag. If the implementation is closed, “unknown at this boundary” is a valid finding.

Knowledge check#

  1. A SurfaceFlinger source path is traced. Does that establish the HWC/GPU implementation used at runtime?
  2. A storage read returns EACCES immediately after unlock. Name three distinct causes that require different evidence.
  3. A network is reported available while an app request times out. What evidence separates framework selection from endpoint failure?
  4. Does one framework wake lock prove the CPU remained awake for the entire interval or caused battery drain?
  5. What should you conclude when the required vendor implementation is closed?

Answers#

  1. No. The pinned source establishes an implementation path in that tag, not runtime selection, successful HAL registration, or vendor/GPU behavior. Correlate a target-specific trace and interface/runtime evidence; stop at closed boundaries.
  2. User/key/storage preparation not complete; DAC/UID ownership/mode denial; SELinux denial (or another service/app-level access decision). Correlate state and identity, inspect relevant permitted AVC/log evidence, and do not infer from errno alone.
  3. Correlate app request and UID with network callback/selection and route/DNS state at connect time; compare endpoint result and socket reuse. Redact endpoint identifiers. “Available” alone does not prove a usable path to a particular server.
  4. No. It records a framework-side request/ownership state; kernel suspend behavior and energy attribution require separate evidence. Battery percentage is especially weak at short intervals.
  5. State exactly where evidence stops, which public contract is known, and which runtime facts remain unknown. Do not fill the gap with a generic device assumption.

Checkpoint#

Choose one casework and deliver a one-page record with target/revision scope, symptom and synthetic-data label, source trace with exact file and symbol, layer diagram, claim/evidence/limit table, competing hypotheses, falsifier, and safe next test. Ready: you stop at evidence boundaries, distinguish synthetic from observed data, and the proposed test separates hypotheses. Revise: your answer names a tool but not the question it answers, or treats source declarations as runtime proof. Not yet: the plan alters security, encryption, routing, suspend, or a physical device without authorization and recovery. Remediation: revisit HAL/Treble, framework service, and Perfetto/performance; redo the evidence ledger using only the supplied dataset.

These four samplers do not make a learner an expert in graphics, storage, networking, or power. Genuine specialization requires repeated work across product branches, vendor implementations, compatible test hardware, domain-specific tests, and peer review. A bounded, accurate conclusion is a stronger result than a confident unsupported diagnosis.

Sources#

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.