Performance https://theinshotproapk.com/category/app/performance/ Download InShot Pro APK for Android, iOS, and PC Tue, 17 Feb 2026 16:00:00 +0000 en-US hourly 1 https://theinshotproapk.com/wp-content/uploads/2021/07/cropped-Inshot-Pro-APK-Logo-1-32x32.png Performance https://theinshotproapk.com/category/app/performance/ 32 32 Under the hood: Android 17’s lock-free MessageQueue https://theinshotproapk.com/under-the-hood-android-17s-lock-free-messagequeue/ Tue, 17 Feb 2026 16:00:00 +0000 https://theinshotproapk.com/under-the-hood-android-17s-lock-free-messagequeue/ Posted by Shai Barack, Android Platform Performance Lead and Charles Munger, Principal Software Engineer In Android 17, apps targeting SDK ...

Read more

The post Under the hood: Android 17’s lock-free MessageQueue appeared first on InShot Pro.

]]>

Posted by Shai Barack, Android Platform Performance Lead and Charles Munger, Principal Software Engineer


In Android 17, apps targeting SDK 37 or higher will receive a new implementation of MessageQueue where the implementation is lock-free. The new implementation improves performance and reduces missed frames, but may break clients that reflect on MessageQueue private fields and methods. To learn more about the behavior change and how you can mitigate impact, check out the MessageQueue behavior change documentation. This technical blog post provides an overview of the MessageQueue rearchitecture and how you can analyze lock contention issues using Perfetto.

The Looper drives the UI thread of every Android application. It pulls work from a MessageQueue, dispatches it to a Handler, and repeats. For two decades, MessageQueue used a single monitor lock (i.e. a synchronized code block) to protect its state.

Android 17 introduces a significant update to this component: a lock-free implementation named DeliQueue.

This post explains how locks affect UI performance, how to analyze these issues with Perfetto, and the specific algorithms and optimizations used to improve the Android main thread.

The problem: Lock Contention and Priority Inversion

The legacy MessageQueue functioned as a priority queue protected by a single lock. If a background thread posts a message while the main thread performs queue maintenance, the background thread blocks the main thread.

When two or more threads are competing for exclusive use of the same lock, this is called Lock contention. This contention can cause Priority Inversion, leading to UI jank and other performance problems.

Priority inversion can happen when a high-priority thread (like the UI thread) is made to wait for a low-priority thread. Consider this sequence:

  1. A low priority background thread acquires the MessageQueue lock to post the result of work that it did.

  2. A medium priority thread becomes runnable and the Kernel’s scheduler allocates it CPU time, preempting the low priority thread.

  3. The high priority UI thread finishes its current task and attempts to read from the queue, but is blocked because the low priority thread holds the lock.

The low-priority thread blocks the UI thread, and the medium-priority work delays it further.

A visual representation of priority inversion. It shows 'Task L' (Low) holding a lock, blocking 'Task H' (High). 'Task M' (Medium) then preempts 'Task L', effectively delaying 'Task H' for the duration of 'Task M's' execution.

Analyzing contention with Perfetto

You can diagnose these issues using Perfetto. In a standard trace, a thread blocked on a monitor lock enters the sleeping state, and Perfetto shows a slice indicating the lock owner.

When you query trace data, look for slices named “monitor contention with …” followed by the name of the thread that owns the lock and the code site where the lock was acquired.

Case study: Launcher jank

To illustrate, let’s analyze a trace where a user experienced jank while navigating home on a Pixel phone immediately after taking a photo in the camera app. Below we see a screenshot of Perfetto showing the events leading up to the missed frame:


A Perfetto trace screenshot diagnosing the Launcher jank. The 'Actual Timeline' shows a red missed frame. Coinciding with this, the main thread track contains a large green slice labeled 'monitor contention with owner BackgroundExecutor,' indicating that the UI thread was blocked because a background thread held the MessageQueue lock.

  • Symptom: The Launcher main thread missed its frame deadline. It blocked for 18ms, which exceeds the 16ms deadline required for 60Hz rendering.

  • Diagnosis: Perfetto showed the main thread blocked on the MessageQueue lock. A “BackgroundExecutor” thread owned the lock.

  • Root Cause: The BackgroundExecutor runs at Process.THREAD_PRIORITY_BACKGROUND (very low priority). It performed a non-urgent task (checking app usage limits). Simultaneously, medium priority threads were using CPU time to process data from the camera. The OS scheduler preempted the BackgroundExecutor thread to run the camera threads.

This sequence caused the Launcher’s UI thread (high priority) to become indirectly blocked by the camera worker thread (medium priority), which was keeping the Launcher’s background thread (low priority) from releasing the lock.

Querying traces with PerfettoSQL

You can use PerfettoSQL to query trace data for specific patterns. This is useful if you have a large bank of traces from user devices or tests, and you’re searching for specific traces that demonstrate a problem.

For example, this query finds MessageQueue contention coincident with dropped frames (jank):

INCLUDE PERFETTO MODULE android.monitor_contention;
INCLUDE PERFETTO MODULE android.frames.jank_type;

SELECT
  process_name,
  -- Convert duration from nanoseconds to milliseconds
  SUM(dur) / 1000000 AS sum_dur_ms,
  COUNT(*) AS count_contention
FROM android_monitor_contention
WHERE is_blocked_thread_main
AND short_blocked_method LIKE "%MessageQueue%" 

-- Only look at app processes that had jank
AND upid IN (
  SELECT DISTINCT(upid)
  FROM actual_frame_timeline_slice
  WHERE android_is_app_jank_type(jank_type) = TRUE
)
GROUP BY process_name
ORDER BY SUM(dur) DESC;

In this more complex examples, join trace data that spans multiple tables to identify MessageQueue contention during app startup:

INCLUDE PERFETTO MODULE android.monitor_contention; 
INCLUDE PERFETTO MODULE android.startup.startups; 

-- Join package and process information for startups
DROP VIEW IF EXISTS startups; 
CREATE VIEW startups AS 
SELECT startup_id, ts, dur, upid 
FROM android_startups 
JOIN android_startup_processes USING(startup_id); 

-- Intersect monitor contention with startups in the same process.
DROP TABLE IF EXISTS monitor_contention_during_startup; 
CREATE VIRTUAL TABLE monitor_contention_during_startup 
USING SPAN_JOIN(android_monitor_contention PARTITIONED upid, startups PARTITIONED upid); 

SELECT 
  process_name, 
  SUM(dur) / 1000000 AS sum_dur_ms, 
  COUNT(*) AS count_contention 
FROM monitor_contention_during_startup 
WHERE is_blocked_thread_main 
AND short_blocked_method LIKE "%MessageQueue%" 
GROUP BY process_name 
ORDER BY SUM(dur) DESC;

You can use your favorite LLM to write PerfettoSQL queries to find other patterns.

At Google, we use BigTrace to run PerfettoSQL queries across millions of traces. In doing so, we confirmed that what we saw anecdotally was, in fact, a systemic issue. The data revealed that MessageQueue lock contention impacts users across the entire ecosystem, substantiating the need for a fundamental architectural change.

Solution: lock-free concurrency

We addressed the MessageQueue contention problem by implementing a lock-free data structure, using atomic memory operations rather than exclusive locks to synchronize access to shared state. A data structure or algorithm is lock-free if at least one thread can always make progress regardless of the scheduling behavior of the other threads. This property is generally hard to achieve, and is usually not worth pursuing for most code.

The atomic primitives

Lock-free software often relies on atomic Read-Modify-Write primitives that the hardware provides.

On older generation ARM64 CPUs, atomics used a Load-Link/Store-Conditional (LL/SC) loop. The CPU loads a value and marks the address. If another thread writes to that address, the store fails, and the loop retries. Because the threads can keep trying and succeed without waiting for another thread, this operation is lock-free.

ARM64 LL/SC loop example
retry:
    ldxr    x0, [x1]        // Load exclusive from address x1 to x0
    add     x0, x0, #1      // Increment value by 1
    stxr    w2, x0, [x1]    // Store exclusive.
                            // w2 gets 0 on success, 1 on failure
    cbnz    w2, retry       // If w2 is non-zero (failed), branch to retr


(view in Compiler Explorer)


Newer ARM architectures (ARMv8.1) support Large System Extensions (LSE) which include instructions in the form of Compare-And-Swap (CAS) or Load-And-Add (demonstrated below). In Android 17 we added support to the Android Runtime (ART) compiler to detect when LSE is supported and emit optimized instructions:

/ ARMv8.1 LSE atomic example
ldadd   x0, x1, [x2]    // Atomic load-add.
                        // Faster, no loop required.

In our benchmarks, high-contention code that uses CAS achieves a ~3x speedup over the LL/SC variant.

The Java programming language offers atomic primitives via java.util.concurrent.atomic that rely on these and other specialized CPU instructions.

The Data Structure: DeliQueue

To remove lock contention from MessageQueue, our engineers designed a novel data structure called DeliQueue. DeliQueue separates Message insertion from Message processing:

  1. The list of Messages (Treiber stack): A lock-free stack. Any thread can push new Messages here without contention.

  2. The priority queue (Min-heap): A heap of Messages to handle, exclusively owned by the Looper thread (hence no synchronization or locks are needed to access).

Enqueue: pushing to a Treiber stack

The list of Messages is kept in a Treiber stack [1], a lock-free stack that uses a CAS loop to update the head pointer.

public class TreiberStack <E> {
    AtomicReference<Node<E>> top =
            new AtomicReference<Node<E>>();
    public void push(E item) {
        Node<E> newHead = new Node<E>(item);
        Node<E> oldHead;
        do {
            oldHead = top.get();
            newHead.next = oldHead;
        } while (!top.compareAndSet(oldHead, newHead));
    }

    public E pop() {
        Node<E> oldHead;
        Node<E> newHead;
        do {
            oldHead = top.get();
            if (oldHead == null) return null;
            newHead = oldHead.next;
        } while (!top.compareAndSet(oldHead, newHead));
        return oldHead.item;
    }
}

Source code based on Java Concurrency in Practice [2], available online and released to the public domain

Any producer can push new Messages to the stack at any time. This is like pulling a ticket at a deli counter – your number is determined by when you showed up, but the order you get your food in doesn’t have to match. Because it’s a linked stack, every Message is a sub-stack – you can see what the Message queue was like at any point in time by tracking the head and iterating forwards – you won’t see any new Messages pushed on top, even if they’re being added during your traversal.

Dequeue: bulk transfer to a min-heap

To find the next Message to handle, the Looper processes new Messages from the Treiber stack by walking the stack starting from the top and iterating until it finds the last Message that it previously processed. As the Looper traverses down the stack, it inserts Messages into the deadline-ordered min-heap. Since the Looper exclusively owns the heap, it orders and processes Messages without locks or atomics.

A system diagram illustrating the DeliQueue architecture. Concurrent producer threads (left) push messages onto a shared 'Lock-Free Treiber Stack' using atomic CAS operations. The single consumer 'Looper Thread' (right) claims these messages via an atomic swap, merges them into a private 'Local Min-heap' sorted by timestamp, and then executes them.


In walking down the stack, the Looper also creates links from stacked Messages back to their predecessors, thus forming a doubly-linked list. Creating the linked list is safe because links pointing down the stack are added via the Treiber stack algorithm with CAS, and links up the stack are only ever read and modified by the Looper thread. These back links are then used to remove Messages from arbitrary points in the stack in O(1) time.

This design provides O(1) insertion for producers (threads posting work to the queue) and amortized O(log N) processing for the consumer (the Looper).

Using a min-heap to order Messages also addresses a fundamental flaw in the legacy MessageQueue, where Messages were kept in a singly-linked list (rooted at the top). In the legacy implementation, removal from the head was O(1), but insertion had a worst case of O(N) – scaling poorly for overloaded queues! Conversely, insertion to and removal from the min-heap scale logarithmically, delivering competitive average performance but really excelling in tail latencies.


Legacy (locked) MessageQueue

DeliQueue

Insert

O(N)

O(1) for calling thread

O(logN) for Looper thread

Remove from head

O(1)

O(logN)

In the legacy queue implementation, producers and the consumer used a lock to coordinate exclusive access to the underlying singly-linked list. In DeliQueue, the Treiber stack handles concurrent access, and the single consumer handles ordering its work queue.

Removal: consistency via tombstones

DeliQueue is a hybrid data structure, joining a lock-free Treiber stack with a single-threaded min-heap. Keeping these two structures in sync without a global lock presents a unique challenge: a message might be physically present in the stack but logically removed from the queue.

To solve this, DeliQueue uses a technique called “tombstoning.” Each Message tracks its position in the stack via the backwards and forwards pointers, its index in the heap’s array, and a boolean flag indicating whether it has been removed. When a Message is ready to run, the Looper thread will CAS its removed flag, then remove it from the heap and stack.

When another thread needs to remove a Message, it doesn’t immediately extract it from the data structure. Instead, it performs the following steps:

  1. Logical removal: the thread uses a CAS to atomically set the Message’s removal flag from false to true. The Message remains in the data structure as evidence of its pending removal, a so-called “tombstone”. Once a Message is flagged for removal, DeliQueue treats it as if it no longer exists in the queue whenever it’s found.

  2. Deferred cleanup: The actual removal from the data structure is the responsibility of the Looper thread, and is deferred until later. Rather than modifying the stack or heap, the remover thread adds the Message to another lock-free freelist stack.

  3. Structural removal: Only the Looper can interact with the heap or remove elements from the stack. When it wakes up, it clears the freelist and processes the Messages it contained. Each Message is then unlinked from the stack and removed from the heap. 


This approach keeps all management of the heap single-threaded. It minimizes the number of concurrent operations and memory barriers required, making the critical path faster and simpler.

Traversal: benign Java memory model data races

Most concurrency APIs, such as Future in the Java standard library, or Kotlin’s Job and Deferred, include a mechanism to cancel work before it completes. An instance of one of these classes matches 1:1 with a unit of underlying work, and calling cancel on an object cancels the specific operations associated with them.


Today’s Android devices have multi-core CPUs and concurrent, generational garbage collection. But when Android was first developed, it was too expensive to allocate one object for each unit of work. Consequently, Android’s Handler supports cancellation via numerous overloads of removeMessages rather than removing a specific Message, it removes all Messages that match the specified criteria. In practice, this requires iterating through all Messages inserted before removeMessages was called and removing the ones that match.


When iterating forward, a thread only requires one ordered atomic operation, to read the current head of the stack. After that, ordinary field reads are used to find the next
Message. If the Looper thread modifies the next fields while removing Messages, the Looper’s write and another thread’s read are unsynchronized – this is a data race. Normally, a data race is a serious bug that can cause huge problems in your app – leaks, infinite loops, crashes, freezes, and more. However, under certain narrow conditions, data races can be benign within the Java Memory Model. Suppose we start with a stack of:

A diagram showing the initial state of the message stack as a linked list. The 'Head' points to 'Message A', which links sequentially to 'Message B', 'Message C', and 'Message D'.


We perform an atomic read of the head, and see A. A’s next pointer points to B. At the same time as we process B, the looper might remove B and C, by updating A to point to C and then D.

A diagram illustrating a benign data race during list traversal. The Looper thread has updated 'Message A' to point directly to 'Message D', effectively removing 'Message B' and 'Message C'. Simultaneously, a concurrent thread reads a stale 'next' pointer from A, traverses through the logically removed messages B and C, and eventually rejoins the live list at 'Message D'.


Even though B and C are logically removed, B retains its next pointer to C, and C to D. The reading thread continues traversing through the detached removed nodes and eventually rejoins the live stack at D.


By designing DeliQueue to handle races between traversal and removal, we allow for safe, lock-free iteration.

Quitting: Native refcount

Looper is backed by a native allocation that must be manually freed once the Looper has quit. If some other thread is adding Messages while the Looper is quitting, it could use the native allocation after it’s freed, a memory safety violation. We prevent this using a tagged refcount, where one bit of the atomic is used to indicate whether the Looper is quitting.


Before using the native allocation, a thread reads the refcount atomic. If the quitting bit is set, it returns that the Looper is quitting and the native allocation must not be used. If not, it attempts a CAS to increment the number of active threads using the native allocation. After doing what it needs to, it decrements the count. If the quitting bit was set after its increment but before the decrement, and the count is now zero, then it wakes up the Looper thread.

When the Looper thread is ready to quit, it uses CAS to set the quitting bit in the atomic. If the refcount was 0, it can proceed to free its native allocation. Otherwise, it parks itself, knowing that it will be woken up when the last user of the native allocation decrements the refcount. This approach does mean that the Looper thread waits for the progress of other threads, but only when it’s quitting. That only happens once and is not performance sensitive, and it keeps the other code for using the native allocation fully lock-free.

A state diagram illustrating the tagged refcount mechanism for safe termination. It defines three states based on the atomic value's layout (Bit 63 for teardown, Bits 0-62 for refcount):  Active (Green): The teardown bit is 0. Workers successfully increment and decrement the reference count.  Draining (Yellow): The Looper has set the teardown bit to 1. New worker increments fail, but existing workers continue to decrement.  Terminated (Red): Occurs when the reference count reaches 0 while draining. The Looper is signaled that it is safe to destroy the native allocation.



There’s a lot of other tricks and complexity in the implementation. You can learn more about DeliQueue by reviewing the source code.

Optimization: branchless programming

While developing and testing DeliQueue, the team ran many benchmarks and carefully profiled the new code. One issue identified using the simpleperf tool was pipeline flushes caused by the Message comparator code.

A standard comparator uses conditional jumps, with the condition for deciding which Message comes first simplified below:

static int compareMessages(@NonNull Message m1, @NonNull Message m2) {
    if (m1 == m2) {
        return 0;
    }

    // Primary queue order is by when.
    // Messages with an earlier when should come first in the queue.
    final long whenDiff = m1.when - m2.when;
    if (whenDiff > 0) return 1;
    if (whenDiff < 0) return -1;

    // Secondary queue order is by insert sequence.
    // If two messages were inserted with the same `when`, the one inserted
    // first should come first in the queue.
    final long insertSeqDiff = m1.insertSeq - m2.insertSeq;
    if (insertSeqDiff > 0) return 1;
    if (insertSeqDiff < 0) return -1;

    return 0;
}


This code compiles to conditional jumps (b.le and cbnz instructions). When the CPU encounters a conditional branch, it can’t know whether the branch is taken until the condition is computed, so it doesn’t know which instruction to read next, and has to guess, using a technique called branch prediction. In a case like binary search, the branch direction will be unpredictably different at each step, so it’s likely that half the predictions will be wrong. Branch prediction is often ineffective in searching and sorting algorithms (such as the one used in a min-heap), because the cost of guessing wrong is larger than the improvement from guessing correctly. When the branch predictor guesses wrong, it must throw away the work it did after assuming the predicted value, and start again from the path that was actually taken – this is called a pipeline flush.

To find this issue, we profiled our benchmarks using the branch-misses performance counter, which records stack traces where the branch predictor guesses wrong. We then visualized the results with Google pprof, as shown below:

A screenshot from the pprof web UI showing branch misses in MessageQueue code to compare Message instances while performing heap operations.

Recall that the original MessageQueue code used a singly-linked list for the ordered queue. Insertion would traverse the list in sorted order as a linear search, stopping at the first element that’s past the point of insertion and linking the new Message ahead of it. Removal from the head simply required unlinking the head. Whereas DeliQueue uses a min-heap, where mutations require reordering some elements (sifting up or down) with logarithmic complexity in a balanced data structure, where any comparison has an even chance of directing the traversal to a left child or to a right child. The new algorithm is asymptotically faster, but exposes a new bottleneck as the search code stalls on branch misses half the time.

Realizing that branch misses were slowing down our heap code, we optimized the code using branch-free programming:

// Branchless Logic
static int compareMessages(@NonNull Message m1, @NonNull Message m2) {
    final long when1 = m1.when;
    final long when2 = m2.when;
    final long insertSeq1 = m1.insertSeq;
    final long insertSeq2 = m2.insertSeq;

    // signum returns the sign (-1, 0, 1) of the argument,
    // and is implemented as pure arithmetic:
    // ((num >> 63) | (-num >>> 63))
    final int whenSign = Long.signum(when1 - when2);
    final int insertSeqSign = Long.signum(insertSeq1 - insertSeq2);

    // whenSign takes precedence over insertSeqSign,
    // so the formula below is such that insertSeqSign only matters
    // as a tie-breaker if whenSign is 0.
    return whenSign * 2 + insertSeqSign;
}

To understand the optimization, disassemble the two examples in Compiler Explorer and use LLVM-MCA, a CPU simulator that can generate an estimated timeline of CPU cycles.

The original code:
Index     01234567890123
[0,0]     DeER .    .  .   sub  x0, x2, x3
[0,1]     D=eER.    .  .   cmp  x0, #0
[0,2]     D==eER    .  .   cset w0, ne
[0,3]     .D==eER   .  .   cneg w0, w0, lt
[0,4]     .D===eER  .  .   cmp  w0, #0
[0,5]     .D====eER .  .   b.le #12
[0,6]     . DeE---R .  .   mov  w1, #1
[0,7]     . DeE---R .  .   b    #48
[0,8]     . D==eE-R .  .   tbz  w0, #31, #12
[0,9]     .  DeE--R .  .   mov  w1, #-1
[0,10]    .  DeE--R .  .   b    #36
[0,11]    .  D=eE-R .  .   sub  x0, x4, x5
[0,12]    .   D=eER .  .   cmp  x0, #0
[0,13]    .   D==eER.  .   cset w0, ne
[0,14]    .   D===eER  .   cneg w0, w0, lt
[0,15]    .    D===eER .   cmp  w0, #0
[0,16]    .    D====eER.   csetm        w1, lt
[0,17]    .    D===eE-R.   cmp  w0, #0
[0,18]    .    .D===eER.   csinc        w1, w1, wzr, le
[0,19]    .    .D====eER   mov  x0, x1
[0,20]    .    .DeE----R   ret

Note the one conditional branch, b.lewhich avoids comparing the insertSeq fields if the result is already known from comparing the when fields.

The branchless code:
Index     012345678
[0,0]     DeER .  .   sub       x0, x2, x3
[0,1]     DeER .  .   sub       x1, x4, x5
[0,2]     D=eER.  .   cmp       x0, #0
[0,3]     .D=eER  .   cset      w0, ne
[0,4]     .D==eER .   cneg      w0, w0, lt
[0,5]     .DeE--R .   cmp       x1, #0
[0,6]     . DeE-R .   cset      w1, ne
[0,7]     . D=eER .   cneg      w1, w1, lt
[0,8]     . D==eeER   add       w0, w1, w0, lsl #1
[0,9]     .  DeE--R   ret


Here, the branchless implementation takes fewer cycles and instructions than even the shortest path through the branchy code – it’s better in all cases. The faster implementation plus the elimination of mispredicted branches resulted in a 5x improvement in some of our benchmarks!


However, this technique is not always applicable. Branchless approaches generally require doing work that will be thrown away, and if the branch is predictable most of the time, that wasted work can slow your code down. In addition, removing a branch often introduces a data dependency. Modern CPUs execute multiple operations per cycle, but they can’t execute an instruction until its inputs from a previous instruction are ready. In contrast, a CPU can speculate about data in branches, and work ahead if a branch is predicted correctly.

Testing and Validation

Validating the correctness of lock-free algorithms is notoriously difficult!

In addition to standard unit tests for continuous validation during development, we also wrote rigorous stress tests to verify queue invariants and to attempt to induce data races if they existed. In our test labs we could run millions of test instances on emulated devices and on real hardware.

With Java ThreadSanitizer (JTSan) instrumentation, we could use the same tests to also detect some data races in our code. JTSan did not find any problematic data races in DeliQueue, but – surprisingly -actually detected two concurrency bugs in the Robolectric framework, which we promptly fixed.

To improve our debugging capabilities, we built new analysis tools. Below is an example showing an issue in Android platform code where one thread is overloading another thread with Messages, causing a large backlog, visible in Perfetto thanks to the MessageQueue instrumentation feature that we added.

A screenshot of Perfetto UI, demonstrating flows and metadata for Messages being posted to a MessageQueue and delivered to a worker thread.

To enable MessageQueue tracing in the system_server process, include the following in your Perfetto configuration:

data_sources {
  config {
    name: "track_event"
    target_buffer: 0  # Change this per your buffers configuration
    track_event_config {
      enabled_categories: "mq"
    }
  }
}

Impact

DeliQueue improves system and app performance by eliminating locks from MessageQueue.

  • Synthetic benchmarks: multi-threaded insertions into busy queues is up to 5,000x faster than the legacy MessageQueue, thanks to improved concurrency (the Treiber stack) and faster insertions (the min-heap).

  • In Perfetto traces acquired from internal beta testers, we see a reduction of 15% in app main thread time spent in lock contention.

  • On the same test devices, the reduced lock contention leads to significant improvements to the user experience, such as:

    • -4% missed frames in apps.

    • -7.7% missed frames in System UI and Launcher interactions.

    • -9.1% in time from app startup to the first frame drawn, at the 95%ile.

Next steps

DeliQueue is rolling out to apps in Android 17. App developers should review preparing your app for the new lock-free MessageQueue on the Android Developers blog to learn how to test their apps.

References

[1] Treiber, R.K., 1986. Systems programming: Coping with parallelism. International Business Machines Incorporated, Thomas J. Watson Research Center.

[2] Goetz, B., Peierls, T., Bloch, J., Bowbeer, J., Holmes, D., & Lea, D. (2006). Java Concurrency in Practice. Addison-Wesley Professional.




The post Under the hood: Android 17’s lock-free MessageQueue appeared first on InShot Pro.

]]>
Beyond the smartphone: How JioHotstar optimized its UX for foldables and tablets https://theinshotproapk.com/beyond-the-smartphone-how-jiohotstar-optimized-its-ux-for-foldables-and-tablets/ Mon, 02 Feb 2026 12:04:16 +0000 https://theinshotproapk.com/beyond-the-smartphone-how-jiohotstar-optimized-its-ux-for-foldables-and-tablets/ Posted by Prateek Batra, Developer Relations Engineer, Android Adaptive Apps Beyond Phones: How JioHotstar Built an Adaptive UX JioHotstar is ...

Read more

The post Beyond the smartphone: How JioHotstar optimized its UX for foldables and tablets appeared first on InShot Pro.

]]>
Posted by Prateek Batra, Developer Relations Engineer, Android Adaptive Apps

Beyond Phones: How JioHotstar Built an Adaptive UX
JioHotstar is a leading streaming platform in India, serving a user base exceeding 400 million. With a vast content library encompassing over 330,000 hours of video on demand (VOD) and real-time delivery of major sporting events, the platform operates at a massive scale.

To help ensure a premium experience for its vast audience, JioHotstar elevated the viewing experience by optimizing their app for foldables and tablets. They accomplished this by following Google’s adaptive app guidance and utilizing resources like  samples, codelabs, cookbooks, and documentation to help create a consistently seamless and engaging experience across all display sizes.


JioHotstar’s large screen challenge


JioHotstar offered an excellent user experience on standard phones and the team wanted to take advantage of new form factors. To start, the team evaluated their app against the large screen app quality guidelines to understand the optimizations required to extend their user experience to foldables and tablets. To achieve Tier 1 large screen app status, the team implemented two strategic updates to adapt the app across various form factors and differentiate on foldables. By addressing the unique challenges posed by foldable and tablet devices, JioHotstar aims to deliver a high-quality and immersive experience across all display sizes and aspect ratios.


What they needed to do


JioHotstar’s user interface, designed primarily for standard phone displays, encountered challenges in adapting hero image aspect ratios, menus, and show screens to the diverse screen sizes and resolutions of other form factors. This often led to image cropping, letterboxing, low resolution, and unutilized space, particularly in landscape mode. To help fully leverage the capabilities of tablets and foldables and deliver an optimized user experience across these device types, JioHotstar focused on refining the UI to ensure optimal layout flexibility, image rendering, and navigation across a wider range of devices.


What they did


For a better viewing experience on large screens, JioHotstar took the initiative to enhance its app by incorporating WindowSizeClass and creating optimized layouts for compact, medium and extended widths. This allowed the app to adapt its user interface to various screen dimensions and aspect ratios, ensuring a consistent and visually appealing UI across different devices.

JioHotstar followed this pattern using Material 3 Adaptive library to know how much space the app has available. First invoking the currentWindowAdaptiveInfo() function, then using new layouts accordingly for the three window size classes:


val sizeClass = currentWindowAdaptiveInfo().windowSizeClass

if(sizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_EXPANDED_LOWER_BOUND)) {
    showExpandedLayout()
} else if(sizeClass.isHeightAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) {
    showMediumLayout()
} else {
    showCompactLayout()
}

The breakpoints are in order, from the biggest to the smallest, as internally the API checks for with a greater or equal then, so any width that is at least greater or equal then EXPANDED will always be greater than MEDIUM.


JioHotstar is able to provide the premium experience unique to foldable devices: Tabletop Mode. This feature conveniently relocates the video player to the top half of the screen and the video controls to the bottom half when a foldable device is partially folded for a handsfree experience.



To accomplish this, also using the Material 3 Adaptive library, the same currentWindowAdaptiveInfo() can be used to query for the tabletop mode. Once the device is held in tabletop mode, a change of layout to match the top and bottom half of the posture can be done with a column to place the player in the top half and the controllers in the bottom half:

val isTabletTop = currentWindowAdaptiveInfo().windowPosture.isTabletop
if(isTabletopMode) {
   Column {
       Player(Modifier.weight(1f))
       Controls(Modifier.weight(1f))
   }
} else {
   usualPlayerLayout()
}

JioHotstar is now meeting the Large Screen app quality guidelines for Tier 1. The team leveraged adaptive app guidance, utilizing samples, codelabs, cookbooks, and documentation to incorporate these recommendations.


To further improve the user experience, JioHotstar increased touch target sizes, to the recommended 48dp, on video discovery pages, ensuring accessibility across large screen devices. Their video details page is now adaptive, adjusting to screen sizes and orientations. They moved beyond simple image scaling, instead leveraging window size classes to detect window size and density in real time and load the most appropriate hero image for each form factor, helping to enhance visual fidelity. Navigation was also improved, with layouts adapting to suit different screen sizes.


Now users can view their favorite content from JioHotstar on large screens devices with an improved and highly optimized viewing experience.

Achieving Tier 1 large screen app status with Google is a milestone that reflects the strength of our shared vision. At JioHotstar, we have always believed that optimizing for large screen devices goes beyond adaptability, it’s about elevating the viewing experience for audiences who are rapidly embracing foldables, tablets, and connected TVs.

Leveraging Google’s Jetpack libraries and guides allowed us to combine our insights on content consumption with their expertise in platform innovation. This collaboration allowed both teams to push boundaries, address gaps, and co-create a seamless, immersive experience across every screen size.

Together, we’re proud to bring this enhanced experience to millions of users and to set new benchmarks in how India and the world experience streaming.

Sonu Sanjeev
Senior Software Development Engineer

The post Beyond the smartphone: How JioHotstar optimized its UX for foldables and tablets appeared first on InShot Pro.

]]>
Get your app on the fast track with Android Performance Spotlight Week! https://theinshotproapk.com/get-your-app-on-the-fast-track-with-android-performance-spotlight-week/ Sun, 23 Nov 2025 12:00:37 +0000 https://theinshotproapk.com/get-your-app-on-the-fast-track-with-android-performance-spotlight-week/ Posted by Ben Weiss – Senior Developer Relations Engineer, Performance Paladin When working on new features, app performance often takes ...

Read more

The post Get your app on the fast track with Android Performance Spotlight Week! appeared first on InShot Pro.

]]>

Posted by Ben Weiss – Senior Developer Relations Engineer, Performance Paladin

When working on new features, app performance often takes a back seat. However, while it’s not always top of mind for developers, users can see exactly where your app’s performance lags behind. When that new feature takes a long time to load or is slow to render, your users can become frustrated. And unhappy users are more likely to abandon the feature you spent so much time on.

App performance is a core part of user experience and app quality, and recent studies and research shows that it’s highly correlated with increased user satisfaction, higher retention, and better review scores.

And we’re here to help… Welcome to Android Performance Spotlight Week! All week long, we’re providing you with low-effort, high-impact tools and guidance to get your app on the fast track to better performance. We help you lay the foundation and then dive deeper into helping your app become a better version of itself.

The R8 optimizer and Profile Guided Optimizations are foundational tools to improve overall app performance. And that’s why we just released significant improvements to Android Studio tooling for performance and with the Android Gradle Plugin 9.0 we’re introducing new APIs to make it easier for you to do the right thing when configuring the R8 Android app optimizer. Jetpack Compose version 1.10, which is now in beta, ships with several features that improve app rendering performance. In addition to these updates, we’re bringing you a refresher on improving app health and performance monitoring. Some of our partners are going to tell their performance improvement stories as well.


Stay tuned to the blog all week as we’ll be updating this post with a digest of all the content released. We’re excited to share these updates and help you improve your app’s performance.

Here’s a closer look at what we’ll be covering:

Monday: Deliberate performance optimization with R8

November 17, 2025

We’re kicking off with a deep dive into the R8 optimizer. It’s not just about shrinking your app’s size, it’s about gaining a fundamental understanding of how the R8 optimizer can improve performance in your app and why you should use it right away. We just published the largest overhaul of new technical guidance to date. The guides cover how to enable, configure and troubleshoot the R8 optimizer. On Monday you’ll also see case studies from top partners showing the real-world gains they achieved.



Read the blog post and developer guide.

Tuesday: Debugging and troubleshooting R8

November 18, 2025

We tackle the “Why does my app crash after enabling R8?” question head-on. We know advanced optimization can sometimes reveal edge cases, so we’re focusing on debugging and troubleshooting R8 related issues. We’ll show you how to use new features in Android Studio to de-obfuscate stack traces, identify common configuration problems, and implement best practices to get the most out of R8. We want you to feel confident, not just hopeful, when you flip the switch.



Read the blog post and developer guide on testing and troubleshooting R8.

Wednesday: Deeper performance considerations

November 19, 2025

Mid-week, we explore high-impact performance offerings beyond the R8 optimizer. We’ll show you how to supercharge your app’s startup and interactions using Profile Guided Optimization with Baseline Profiles and Startup Profiles. They are ready and proven to deliver another massive boost. We also have exciting news on Jetpack Compose rendering performance improvements. Plus, we’ll share how to optimize your app’s health by managing background work effectively.

Read the blog post.

Thursday: Measure and improve

November 20, 2025

It’s not an improvement if you can’t prove it. Thursday is dedicated to performance measurement. We’ll share our complete guide, starting from local measurement and debugging with tools like Jetpack Macrobenchmark and the new UiAutomator API to capture jank and startup times, all the way to monitoring your app in the wild. You’ll learn about Play Vitals and other new APIs to understand your real user performance and quantify your success.

Read the blog post.

Friday: Ask Android Live

November 21, 2025

We cap off the week with an in-depth, live conversation. This is your chance to talk directly with the engineers and Developer Relations team who build and use these tools every day. We’ll have a panel of experts from the R8 and other performance teams ready to answer your toughest questions live. Get your questions ready!

Content coming on November 21, 2025

Get notified when we go live on YouTube



📣 Take the Performance Challenge!

We’re not just sharing guidance. We’re challenging you to put it into action!

Here’s our challenge for you this week: Enable R8 full mode for your app.

  1. Follow our developer guides to get started: Enable app optimization.

  2. Then, measure the impact. Don’t just feel the difference, verify it. Measure your performance gains by using or adapting the code from our Macrobenchmark sample app on GitHub to measure your startup times before and after.

We’re confident you’ll see a meaningful improvement in your app’s performance.

While you’re at it, use the social tags #AskAndroid to bring your questions. Throughout the week our experts are monitoring and answering your questions.


The post Get your app on the fast track with Android Performance Spotlight Week! appeared first on InShot Pro.

]]>
Fully Optimized: Wrapping up Performance Spotlight Week https://theinshotproapk.com/fully-optimized-wrapping-up-performance-spotlight-week/ Fri, 21 Nov 2025 17:00:00 +0000 https://theinshotproapk.com/fully-optimized-wrapping-up-performance-spotlight-week/ Posted by Ben Weiss, Senior Developer Relations Engineer and Sara Hamilton, Product Manager We spent the past week diving deep ...

Read more

The post Fully Optimized: Wrapping up Performance Spotlight Week appeared first on InShot Pro.

]]>

Posted by Ben Weiss, Senior Developer Relations Engineer and Sara Hamilton, Product Manager




We spent the past week diving deep into sharing best practices and guidance that helps to make Android apps faster, smaller, and more stable. From the foundational powers of the R8 optimizer and Profile Guided Optimizations, to performance improvements with Jetpack Compose, to a new guide on levelling up your app’s performance, we’ve covered the low effort, high impact tools you need to build a performant app.

This post serves as your index and roadmap to revisit these resources whenever you need to optimize. Here are the five key takeaways from our journey together.

Use the R8 optimizer to speed up your app

The single most impactful, low-effort change you can make is fully enabling the R8 optimizer. It doesn’t just reduce app size; it performs deep, whole-program optimizations to fundamentally rewrite your code for efficiency. Revisit your Keep Rules and get R8 back into your engineering tasks.


Our newly updated and expanded documentation on the R8 optimizer is here to help.


Reddit observed a 40% faster cold startup and 30% fewer ANR errors after enabling R8 full mode.

You can read the full case study on our blog.


Engineers at Disney+ invest in app performance and are optimizing the app’s user experience. Sometimes even seemingly small changes can make a huge impact. While inspecting their R8 configuration, the team found that the -dontoptimize flag was being used. After enabling optimizations by removing this flag, the Disney+ team saw significant improvements in their app’s performance.

So next time someone asks you what you could do to improve app performance, just link them to this post.


Read more in our Day 1 blog: Use R8 to shrink, optimize, and fast-track your app

Guiding you to better performance


Baseline Profiles effectively remove the need for Just in Time compilation, improving startup speed, scrolling, animation and overall rendering performance. Startup Profiles make app startup more even more lightweight by bringing an intelligent order to your app’s classes.dex files.


And to learn more about just how important Baseline Profiles are for app performance, read Meta’s engineering blog where they shared how Baseline Profiles improved various critical performance metrics by up to 40% across their apps.


We continue to make Jetpack Compose more performant for you in Jetpack Compose 1.10. Features like pausable composition and the customizable cache window are crucial for maintaining zero scroll jank when dealing with complex list items.Take a look at the latest episode of #TheAndroidShow where we explain this in more detail.


Read more in our Wednesday’s blog: Deeper Performance Considerations

Measuring performance can be easy as 1, 2, 3


You can’t manage what you don’t measure. Our Performance Leveling Guide breaks down your measurement journey into five steps, starting with easily available data and building up to advanced local tooling.

Starting at level 1, we’ll teach you how to use readily available data from Android Vitals, which provides you with field data on ANRs, crashes, and excessive battery usage.


We’ll also teach you how to level up. For example, we’ll demonstrate how to reach level 3 with local performance testing using Jetpack Macrobenchmark and the new UiAutomator 2.4 API to accurately measure and verify any change in your app’s performance.


Read more in our Thursday’s blog

Debugging performance just got an upgrade


Advanced optimization shouldn’t mean unreadable crash reports. New features are designed to help you confidently debug R8 and background work:

Automatic Logcat Retrace

Starting in Android Studio Narwhal, stack traces can automatically be de-obfuscated in the Logcat window. This way you can immediately see and debug any crashes in a production-ready build.

Narrow Keep Rules

On Tuesday we demystified the Keep Rules needed to fix runtime crashes, emphasizing writing specific, member-level rules over overly-broad wildcards. And because it’s an important topic, we made you a video as well.

And with the new lint check for wide Keep Rules, the Android Studio Otter 3 Feature Drop has you covered here as well.

We also released new guidance on testing and troubleshooting your R8 configuration to help you get the configuration right with confidence.


Read more in our Tuesday’s blog: Configure and troubleshoot R8 Keep Rules

Background Work

We shared guidance on debugging common scenarios you may encounter when scheduling tasks with WorkManager.

Background Task Inspector gives you a visual representation and graph view of WorkManager tasks, helping debug why scheduled work is delayed or failed. And our refreshed Background Work documentation landing page highlights task-specific APIs that are optimized for particular use cases, helping you achieve more reliable execution.


Read more in our Wednesday’s blog: Background work performance considerations

Performance optimization is an ongoing journey

If you successfully took our challenge to enable R8 full mode this week, your next step is to integrate performance into your product roadmap using the App Performance Score. This standardized framework helps you find the highest leverage action items for continuous improvement.

We capped off the week with the #AskAndroid Live Q&A session, where engineers answered your toughest questions on R8, Profile Guided Optimizations, and more. If you missed it, look for the replay!


Thank you for joining us! Now, get building and keep that momentum going.


The post Fully Optimized: Wrapping up Performance Spotlight Week appeared first on InShot Pro.

]]>
Deeper Performance Considerations https://theinshotproapk.com/deeper-performance-considerations/ Fri, 21 Nov 2025 12:04:53 +0000 https://theinshotproapk.com/deeper-performance-considerations/ Posted by Ben Weiss – Senior Developer Relations Engineer, Breana Tate – Developer Relations Engineer, Jossi Wolf – Software Engineer ...

Read more

The post Deeper Performance Considerations appeared first on InShot Pro.

]]>

Posted by Ben Weiss – Senior Developer Relations Engineer,
Breana Tate – Developer Relations Engineer,
Jossi Wolf – Software Engineer on Compose

Compose
yourselves and let us guide you through more background on performance.

Welcome
to day 3 of Performance Spotlight Week. Today we’re continuing to share details and guidance on
important
areas of app performance. We’re covering Profile Guided Optimization, Jetpack Compose
performance
improvements and considerations on working behind the scenes. Let’s dive right in.

Profile
Guided Optimization

Baseline
Profiles

and
Startup
Profiles

are foundational to improve an Android app’s startup and runtime performance. They are part of a
group of
performance optimizations called Profile Guided Optimization.

When
an app is packaged, the d8 dexer takes classes and methods and populates your app’s
classes.dex
files. When a user opens the app, these dex files are loaded, one after the other until the app
can start.
By providing a
Startup
Profile

you let d8 know which classes and methods to pack in the first
classes.dex
files. This structure allows the app to load fewer files, which in turn improves startup
speed.

Baseline
Profiles effectively move the Just in Time (JIT) compilation steps away from user devices and
onto developer
machines. The generated Ahead Of Time (AOT) compiled code has proven to reduce startup time and
rendering
issues alike.

Trello
and Baseline Profiles

We
asked engineers on the Trello app how Baseline Profiles affected their app’s performance. After
applying
Baseline Profiles to their main user journey, Trello saw a significant 25 % reduction in app
startup
time.

Trello
was able to improve their app’s startup time by 25 % by using baseline
profiles.

Baseline
Profiles at Meta

Also,
engineers at Meta recently published an article on how they are
accelerating
their Android apps with Baseline Profiles
.

Across
Meta’s apps the teams have seen various critical metrics improve by up to 40 % after
applying Baseline
Profiles.


Technical
improvements like these help you improve user satisfaction and business success as well. Sharing
this with
your product owners, CTOs and decision makers can also help speed up your app’s
performance.

Get
started with Baseline Profiles

To
generate either a Baseline or Startup Profile, you write a
macrobenchmark
test that exercises the app. During the test profile data is collected which will be used during
app
compilation. The tests are written using the new
UiAutomator
API
,
which we’ll cover tomorrow.

Writing
a benchmark like this is straightforward and you can see the full sample on
GitHub.

@Test

fun profileGenerator() {

    rule.collect(

        packageName = TARGET_PACKAGE,

        maxIterations = 15,

        stableIterations = 3,

        includeInStartupProfile = true

    ) {

        uiAutomator {

            startApp(TARGET_PACKAGE)

        }

    }

}

Considerations

Start
by writing a macrobenchmark tests Baseline Profile and a Startup Profile for the path most
traveled by your
users. This means the main entry point that your users take into your app which usually is
after
they logged in
.
Then continue to write more test cases to capture a more complete picture only for Baseline
Profiles. You do
not need to cover everything with a Baseline Profile. Stick to the most used paths and measure
performance
in the field. More on that in tomorrow’s post.

Get
started with Profile Guided Optimization

To
learn how Baseline Profiles work under the hood, watch this video from the Android Developers
Summit:




And
check out the Android Build Time episode on Profile Guided Optimization for another in-depth
look: 




We
also have extensive guidance on
Baseline
Profiles

and
Startup
Profiles

available for further reading.

Jetpack
Compose performance improvements

The
UI framework for Android has seen the performance investment of the engineering team pay off.
From version
1.9 of Jetpack Compose, scroll jank has dropped to 0.2 % during an internal long scrolling
benchmark
test. 

These
improvements were made possible because of several features packed into the most recent
releases.

Customizable
cache window

By
default, lazy layouts only compose one item ahead of time in the direction of scrolling, and
after something
scrolls off screen it is discarded. You can now customize the amount of items to retain through
a fraction
of the viewport or dp size. This helps your app perform more work upfront, and after enabling
pausable
composition in between frames, using the available time more efficiently.

To
start using customizable cache windows, instantiate a
LazyLayoutCacheWindow
and pass it to your lazy list or lazy grid. Measure your app’s performance using different cache
window
sizes, for example 50% of the viewport. The optimal value will depend on your content’s
structure and item
size.

val
dpCacheWindow = LazyLayoutCacheWindow(ahead =
150.dp,
behind =
100.dp)

val
state = rememberLazyListState(cacheWindow = dpCacheWindow)

LazyColumn(state
= state) {

    //
column contents

}

Pausable
composition

This
feature allows compositions to be paused, and their work split up over several frames. The APIs
landed in
1.9 and it is now used by default in 1.10 in lazy layout prefetch. You should see the most
benefit with
complex items with longer composition times. 


More
Compose performance optimizations

In
the versions 1.9 and 1.10 of Compose the team also made several optimizations that are a bit
less
obvious.

Several
APIs that use coroutines under the hood have been improved. For example, when using
Draggable
and
Clickable,
developers should see faster reaction times and improved allocation counts.

Optimizations
in layout rectangle tracking have improved performance of Modifiers like
onVisibilityChanged()
and
onLayoutRectChanged().
This speeds up the layout phase, even when not explicitly using these APIs.

Another
performance improvement is using cached values when observing positions via
onPlaced().

Prefetch
text in the background

Starting
with version 1.9, Compose adds the ability to prefetch text on a background thread. This enables
you to
pre-warm caches to enable faster text layout and is relevant for app rendering performance.
During layout,
text has to be passed into the Android framework where a word cache is populated. By default
this runs on
the Ui thread. Offloading prefetching and populating the word cache onto a background thread can
speed up
layout, especially for longer texts. To prefetch on a background thread you can pass a custom
executor to
any composable that’s using
BasicText
under the hood by passing a
LocalBackgroundTextMeasurementExecutor
to a
CompositionLocalProvider
like so.

val defaultTextMeasurementExecutor = Executors.newSingleThreadExecutor()

CompositionLocalProvider(

    LocalBackgroundTextMeasurementExecutor provides DefaultTextMeasurementExecutor

) {

    BasicText(“Some text that should be measured on a background thread!”)

}

Depending
on the text, this can provide a performance boost to your text rendering. To make sure that it
improves your
app’s rendering performance, benchmark and compare the results.

Background
work performance considerations

Background
Work is an essential part of many apps. You may be using libraries like WorkManager or
JobScheduler to
perform tasks like:

  • Periodically
    uploading analytical events

  • Syncing
    data between a backend service and a database

  • Processing
    media (i.e. resizing or compressing images)

A
key challenge while executing these tasks is balancing performance and power efficiency.
WorkManager allows
you to achieve this balance. It’s designed to be power-efficient, and allow work to be deferred
to an
optimal execution window influenced by a number of factors, including constraints you specify or
constraints
imposed by the system. 

WorkManager
is not a one-size-fits-all solution, though. Android also has a number of power-optimized APIs
that are
designed specifically with certain common Core User Journeys (CUJs) in
mind.  

Reference
the
Background
Work landing page

for a list of just a few of these,  including updating a widget and getting location in the
background.

Local
Debugging tools for Background Work: Common Scenarios

To
debug Background Work and understand why a task may have been delayed or failed, you need
visibility into
how the system has scheduled your tasks. 

To
help with this, WorkManager has several related

tools to help you debug locally

and optimize performance (some of these work for JobScheduler as well)! Here are some common
scenarios you
might encounter when using WorkManager, and an explanation of tools you can use to debug
them.

Debugging
why scheduled work is not executing

Scheduled
work being delayed or not executing at all can be due to a number of factors, including
specified
constraints not being met or constraints having been
imposed
by the system

The
first step in investigating why scheduled work is not running is to
confirm
the work was successfully scheduled

After confirming the scheduling status, determine whether there are any unmet constraints or
preconditions
preventing the work from executing.

There
are several tools for debugging this scenario.

Background
Task Inspector

The
Background Task Inspector is a powerful tool integrated directly into Android Studio. It
provides a visual
representation of all WorkManager tasks and their associated states (Running, Enqueued, Failed,
Succeeded). 

To
debug why scheduled work is not executing with the Background Task Inspector, consult the listed
Work
status(es). An ‘Enqueued’ status indicates your Work was scheduled, but is still waiting to
run.

Benefits:
Aside from providing an easy way to view all tasks, this tool is especially useful if you have
chained work.
The Background Task inspector offers a graph view that can visualize if a previous task failing
may have
impacted the execution of the following task.

Background
Task Inspector list view



Background
Task Inspector graph view

adb
shell dumpsys jobscheduler

This
command
returns a list of all active JobScheduler jobs (which includes WorkManager Workers) along with
specified
constraints, and system-imposed constraints. It also returns job
history. 

Use
this if you want a different way to view your scheduled work and associated constraints. For
WorkManager
versions earlier than WorkManager 2.10.0,
adb
shell dumpsys jobscheduler

will return a list of Workers with this name:

[package
name]/androidx.work.impl.background.systemjob.SystemJobService


If
your app has multiple workers, updating to WorkManager 2.10.0 will allow you to see Worker names
and easily
distinguish between workers:

#WorkerName#@[package
name]/androidx.work.impl.background.systemjob.SystemJobService


Benefits:
This
command is useful for understanding if there were any
system-imposed
constraints,
which
you cannot determine with the Background Task Inspector. For example, this will return your
app’s
standby bucket
,
which can affect the window in which scheduled work completes.

Enable
Debug logging

You
can enable
custom
logging

to see verbose WorkManager logs, which will have
WM—
attached. 

Benefits:
This allows you to gain visibility into when work is scheduled, constraints are fulfilled, and
lifecycle
events, and you can consult these logs while developing your app.

WorkInfo.StopReason

If
you notice unpredictable performance with a specific worker, you can programmatically observe
the reason
your worker was stopped on the previous run attempt with
WorkInfo.getStopReason

It’s
a good practice to configure your app to observe WorkInfo using getWorkInfoByIdFlow to identify
if your work
is being affected by background restrictions, constraints, frequent timeouts, or even stopped by
the
user.

Benefits:
You can use WorkInfo.StopReason to collect field data about your workers’
performance.

Debugging
WorkManager-attributed high wake lock duration flagged by Android vitals

Android
vitals features an excessive partial wake locks metric, which highlights wake locks contributing
to battery
drain. You may be surprised to know that
WorkManager
acquires wake locks to execute tasks
,
and if the wake locks exceed the threshold set by Google Play, can have impacts to your app’s
visibility.
How can you debug why there is so much wake lock duration attributed to your work? You can use
the following
tools.

Android
vitals dashboard

First
confirm in the
Android
vitals excessive wake lock dashboard

that the high wake lock duration
is
from WorkManager and not an alarm or other wake lock. You can use the
Identify
wake locks created by other APIs

documentation to understand which wake locks are held due to WorkManager. 

Perfetto

Perfetto
is a tool for analyzing system traces. When using it for debugging WorkManager specifically, you
can view
the “Device State” section to see when your work started, how long it ran, and how it
contributes to power
consumption. 

Under
“Device State: Jobs” track,  you can see any workers that have been executed and their
associated wake
locks.

 

Device
State section in Perfetto, showing CleanupWorker and BlurWorker execution.

Resources

Consult
the
Debug
WorkManager page

for an overview of the available debugging methods for other scenarios you might
encounter.

And
to try some of these methods hands on and learn more about debugging WorkManager, check out
the

Advanced WorkManager and Testing

codelab.

Next
steps

Today
we moved beyond code shrinking and explored how the Android Runtime and Jetpack Compose actually
render your
app. Whether it’s pre-compiling critical paths with Baseline Profiles or smoothing out scroll
states with
the new Compose 1.9 and 1.10 features, these tools focus on the
feel
of your app. And we dove deep into best practices on debugging background work.

Ask
Android

On
Friday we’re hosting a live AMA on performance. Ask your questions now using #AskAndroid and get
them
answered by the experts. 



The
challenge

We
challenged you on Monday to enable R8. Today, we are asking you to
generate
one Baseline Profile

for your app.

With
Android
Studio Otter
,
the Baseline Profile Generator module wizard makes this easier than ever. Pick your most
critical user
journey—even if it’s just your app startup and login—and generate a profile.

Once
you have it, run a Macrobenchmark to compare
CompilationMode.None
vs.
CompilationMode.Partial.

Share
your startup time improvements on social media using
#optimizationEnabled.

Tune
in tomorrow

You
have shrunk your app with R8 and optimized your runtime with Profile Guided Optimization. But
how do you
prove
these wins to your stakeholders? And how do you catch regressions before they hit
production?

Join
us tomorrow for
Day
4: The Performance Leveling Guide
,
where we will map out exactly how to measure your success, from field data in Play Vitals to
deep local
tracing with Perfetto.

The post Deeper Performance Considerations appeared first on InShot Pro.

]]>
Leveling Guide for your Performance Journey https://theinshotproapk.com/leveling-guide-for-your-performance-journey/ Thu, 20 Nov 2025 17:00:00 +0000 https://theinshotproapk.com/leveling-guide-for-your-performance-journey/ Posted by Alice Yuan – Senior Developer Relations Engineer Welcome to day 4 of Performance Spotlight Week. Now that you’ve ...

Read more

The post Leveling Guide for your Performance Journey appeared first on InShot Pro.

]]>

Posted by Alice Yuan – Senior Developer Relations Engineer

Welcome to day 4 of Performance Spotlight Week. Now that you’ve learned about some of the awesome tools and best practices we’ve introduced recently such as the R8 Optimizer, and Profile Guided Optimization with Baseline Profiles and Startup Profiles, you might be wondering where to start your performance improvement journey. 

We’ve come up with a step-by-step performance leveling guide to meet your mobile development team where you are—whether you’re an app with a single developer looking to get started with performance, or you have an entire team dedicated to improving Android performance. 

The performance leveling guide features 5 levels. We’ll start with level 1, which introduces minimal adoption effort performance tooling, and we’ll go up to level 5, ideal for apps that have the resourcing to maintain a bespoke performance framework.

 
Feel free to jump to the level that resonates most with you:

Level 1:  Use Play Console provided field monitoring

We recommend first leveraging Android vitals within the Play Console for viewing automatically collected field monitoring data, giving you insights about your application with minimal effort.

Android vitals is Google’s initiative to automatically collect and surface this field data for you.

Here’s an explanation of how we deliver this data:

  1. Collect Data: When a user opts-in, their Android device automatically logs key performance and stability events from all apps, including yours.

  2. Aggregate Data: Google Play collects and anonymizes this data from your app’s users.

  3. Surface Insights: The data is presented to you in the Android vitals dashboard within your Google Play Console.

The Android vitals dashboard tracks many metrics, but a few are designated as Core Vitals. These are the most important because they can affect your app’s visibility and ranking on the Google Play Store.

The Core Vitals

GOOGLE PLAY’S CORE TECHNICAL QUALITY METRICS

To maximize visibility on Google Play, keep your app below the bad behavior thresholds for these metrics.

User-perceived crash rate The percentage of daily active users who experienced at least one crash that is likely to have been noticeable
User-perceived ANR rate The percentage of daily active users who experienced at least one ANR that is likely to have been noticeable
Excessive battery usage The percentage of watch face sessions where battery usage exceeds 4.44% per hour
New: Excessive partial wake locks The percentage of user sessions where cumulative, non-exempt wake lock usage exceeds 2 hours

The core vitals include user-perceived crash rate, ANR rate, excessive battery usage and the newly introduced metric on excessive partial wake locks.

User-Perceived ANR Rate

You can use the Android Vitals ANR dashboard, to see stack traces of issues that occur in the field and get insights and recommendations on how to fix the issue. 

You can drill down into a specific ANR that occurred, to see the stack trace as well as insights on what might be causing the issue.

Also, check out our ANR guidance to help you diagnose and fix the common scenarios where ANRs might occur. 

User-Perceived Crash Rate 

Use the Android vitals crflevelash dashboard to further debug crashes and view a sample of stack traces that occur within your app. 


Our documentation also has guidance around troubleshooting specific crashes. For example, the Troubleshoot foreground services guide discusses ways to identify and fix common scenarios where crashes occur.

Excessive Battery Usage 

To decrease watch face sessions with excessive battery usage on Wear OS, check out the Wear guide on how to improve and conserve battery

[new] Excessive Partial Wake Locks

We recently announced that apps that exceed the excessive partial wake locks threshold may see additional treatment starting on March 1st 2026

For mobile devices, the Android vitals metric applies to non-exempted wake locks acquired while the screen is off and the app is in the background or running a foreground service. Android vitals considers partial wake lock usage excessive if wake locks are held for at least two hours within a 24-hour period and it affects more than 5% of your app’s sessions, averaged over 28 days.

To debug and fix excessive wake lock issues, check out our technical blog post.

Consult our Android vitals documentation and continue your journey to better leverage Android vitals.

Level 2: Follow the App Performance Score action items

Next, move onto using the App Performance Score to find the high leverage action items to uplevel your app performance.

The Android App Performance Score is a standardized framework to measure your app’s technical performance. It gives you a score between 0 and 100, where a lower number indicates more room for improvement.

To get easy wins, you should first start with the Static Performance Score first. These are often configuration changes or tooling updates that provide significant performance boosts.

Step 1: Perform the Static Assessment

The static assessment evaluates your project’s configuration and tooling adoption. These are often the quickest ways to improve performance.

Navigate to the Static Score section of the scoreboard page and do the following:

  1. Assess Android Gradle Plugin (AGP) Version.

  2. Adopt R8 Minification incrementally or ideally, use R8 in full mode to minify and optimize the app code.

  3. Adopt Baseline Profiles which improves code execution speed from the first launch providing performance enhancements for every new app install and every app update.

  4. Adopt Startup Profiles to improve Dex Layout. Startup Profiles are used by the build system to further optimize the classes and methods they contain by improving the layout of code in your APK’s DEX files. 

  5. Upgrade to the newest version of Jetpack Compose

Step 2: Perform the Dynamic Assessment

Once you have applied the static easy wins, use the dynamic assessment to validate the improvements on a real device. You can first do this manually with a physical device and a stop watch.

Navigate to the Dynamic Score section of the scoreboard page and do the following:

  1. Set up your test environment with a physical device. Consider using a lower-end device to exaggerate performance issues, making them easier to spot.

  2. Measure startup time from the launcher. Cold start your app from the launcher icon and measure the time until it is interactive.

  3. Measure app startup time from a notification, with the goal to reduce notification startup time to be below a couple seconds.

  4. Measure rendering performance by scrolling through your core screens and animations.

Once you’ve completed these steps, you will receive a score between 1 – 100 for the static and dynamic scores, giving you an understanding of your app’s performance and where to focus on.

Level 3: Leverage local performance test frameworks


Once you’ve started to assess dynamic performance, you may find it too tedious to measure performance manually. Consider automating your performance testing using performance test frameworks such as Macrobenchmarks and UiAutomator.

Macrobenchmark 💚 UiAutomator

Think of Macrobenchmark and UiAutomator as two tools that work together: Macrobenchmark is the measurement tool. It’s like a stopwatch and a frame-rate counter that runs outside your app. It is responsible for starting your app, recording metrics (like startup time or dropped frames), and stopping the app. UiAutomator is the robot user. The library lets you write code to interact with the device’s screen. It can find an icon, tap a button,  scroll on a list and more.

How to write a test

When you write a test, you wrap your UiAutomator code inside a Macrobenchmark block.

  1. Define the Test: Use the @MacrobenchmarkRule

  2. Start Measuring: Call benchmarkRule.measureRepeated.

  3. Drive the UI: Inside that block, use UiAutomator code to launch your app, find UI elements, and interact with them.

Here’s an example code snippet of what it looks like to test a compose list for scrolling jank.


benchmarkRule.measureRepeated(

    // …

    metrics = listOf(

        FrameTimingMetric(),

    ),

    startupMode = StartupMode.COLD,

    iterations = 10,

) {

    // 1. Launch the app’s main activity

    startApp()

    // 2. Find the list using its resource ID and scroll down

    onElement { viewIdResourceName == “$packageName.my_list” }

        .fling(Direction.DOWN)

}

  1. Review the results: Each test run provides you with precisely measured information to give you the best data on your app’s performance.

timeToInitialDisplayMs  min  1894.4,   median 2847.4,   max  3355.6


frameOverrunMs          P50 -3.2,  P90  6.2, P95  10.4, P99  119.5

Common use cases

Macrobenchmark provides several core metrics out of the box. StartupTimingMetric allows you to accurately measure app startup. The FrameTimingMetric enables you to understand an app’s rendering performance during the test.

We have a detailed and complete guide to using Macrobenchmarks and UiAutomator alongside code samples available for you to continue learning.

Level 4: Use trace analysis tools like Perfetto 

Trace analysis tools like Perfetto are used when you need to see beyond your own application code. Unlike standard debuggers or profilers that only see your process, Perfetto captures the entire device state—kernel scheduling, CPU frequency, other processes, and system services—giving you complete context for performance issues.

Check our Performance Debugging youtube playlist for video instructions on performance debugging using system traces, Android Studio Profiler and Perfetto.

How to use Perfetto to debug performance

The general workflow for debugging performance using trace analysis tools is to record, load and analyze the trace. 

Step 1: Record a trace

You can record a system trace using several methods: 

Step 2: Load the trace

Once you have the trace file, you need to load it into the analysis tool.

  1. Open Chrome and navigate to ui.perfetto.dev.

  2. Drag and drop your .perfetto-trace (or .pftrace) file directly into the browser window.

  3. The UI will process the file and display the timeline.

Step 3: Analyze the trace

You can use Perfetto UI or Android Studio Profiler to investigate performance issues. Check out this episode of the MAD Skills series on Performance, where our performance engineer Carmen Jackson discusses the Perfetto traceviewer.

Scenarios for inspecting system traces using Perfetto

Perfetto is an expert tool and can provide information about everything that happened on the Android device while a trace was captured. This is particularly helpful when you cannot identify the root cause of a slowdown using standard logs or basic profilers.

Debugging Jank (Dropped Frames)

If your app stutters while scrolling, Perfetto can show you exactly why a specific frame missed its deadline.

If it’s due to the app, you might see your main thread running for a long duration doing heavy parsing; this indicates scenarios where you should move the work into asynchronous processing.

If it’s due to the system, you might see your main thread ready to run, but the CPU kernel scheduler gave priority to a different system service, leaving your app waiting (CPU contention). This indicates scenarios where you may need to optimize usage of platform APIs.

Analyzing Slow App Startup

Startup is complex, involving system init, process forking, and resource loading. Perfetto visualizes this timeline precisely.

You can see if you are waiting on Binder calls (inter-process communication). If your onCreate waits a long time for a response from the system PackageManager, Perfetto will show that blocked state clearly. 

You can also see if your app is doing more work than necessary during the app startup. For example, if you are creating and laying out more views than the app needs to show, you can see these operations in the trace.

Investigating Battery Drain & CPU Usage

Because Perfetto sees the whole system, it’s perfect for finding invisible power drains.

You can identify which processes are holding wake locks, preventing the device from sleeping under the “Device State” tracks. Learn more in our wake locks blog post. Also, use Perfetto to see if your background jobs are running too frequently or waking up the CPU unnecessarily.

Level 5: Build your own performance tracking framework

The final level is for apps that have teams with resourcing to maintain a performance tracking framework. 

Building a custom performance tracking framework on Android involves leveraging several system APIs to capture data throughout the application lifecycle, from startup to exit, and during specific high-load scenarios.

By using ApplicationStartInfo, ProfilingManager, and ApplicationExitInfo, you can create a robust telemetry system that reports on how your app started, detailed info on what it did while running, and why it died.

ApplicationStartInfo: Tracking how the app started

Available from Android 15 (API 35), ApplicationStartInfo provides detailed metrics about app startup in the field. The data includes whether it was a cold, warm, or hot start, and the duration of different startup phases. 

This helps you develop a baseline startup metric using production data to further optimize that might be hard to reproduce locally. You can use these metrics to run A/B tests optimizing the startup flow.

The goal is to accurately record launch metrics without manually instrumenting every initialization phase.

You can query this data lazily some time after application launch.

ProfilingManager: Capturing why it was slow

ProfilingManager (API 35) allows your app to programmatically trigger system traces on user devices. This is powerful for catching transient performance issues in the wild that you can’t reproduce locally.

The goal is to automatically record a trace when a specific highly critical user journey is detected as running slowly or experiencing performance issues.

You can register a listener that triggers when specific conditions are met or trigger it manually when you detect a performance issue such as jank, excessive memory, or battery drain.

Check our documentation on how to capture a profile,

retrieve and analyze profiling data and use debug commands.

ApplicationExitInfo: Tracking why the app died

ApplicationExitInfo (API 30) tells you why your previous process died. This is crucial for finding native crashes, ANRs, or system kills due to excessive memory usage (OOM). You’ll also be able to get a detailed tombstone trace by using the API getTraceInputStream.

The goal of the API is to understand stability issues that don’t trigger standard Java crash reporters (like Low Memory Kills).

You should trigger this API on the next app launch.

Next Steps

Improving Android performance is a step-by-step journey. We’re so excited to see how you level up your performance using these tools!

Tune in tomorrow for Ask Android

You have shrunk your app with R8 and optimized your runtime with Profile Guided Optimization. And measure your app’s performance.

Join us tomorrow for the live Ask Android session. Ask your questions now using #AskAndroid and get them answered by the experts.

The post Leveling Guide for your Performance Journey appeared first on InShot Pro.

]]>
Configure and troubleshoot R8 Keep Rules https://theinshotproapk.com/configure-and-troubleshoot-r8-keep-rules/ Tue, 18 Nov 2025 17:00:00 +0000 https://theinshotproapk.com/configure-and-troubleshoot-r8-keep-rules/ Posted by Ajesh R Pai – Developer Relations Engineer & Ben Weiss – Senior Developer Relations Engineer In modern Android ...

Read more

The post Configure and troubleshoot R8 Keep Rules appeared first on InShot Pro.

]]>

Posted by Ajesh R Pai – Developer Relations Engineer & Ben Weiss – Senior Developer Relations Engineer


In modern Android development, shipping a small, fast, and secure application is a fundamental user expectation. The Android build system’s primary tool for achieving this is the
R8 optimizer, the compiler that handles dead code and resource removal for shrinking, code renaming or minification, and app optimization.

Enabling R8 is a critical step in preparing an app for release, but it requires developers to provide guidance in the form of “Keep Rules.”

After reading this article, check out the Performance Spotlight Week video on enabling, debugging and troubleshooting the R8 optimizer on YouTube.

Why Keep Rules are needed

The need to write Keep Rules stems from a core conflict: R8 is a static analysis tool, but Android apps often rely on dynamic execution patterns like reflection or calls in and out of native code using the JNI (Java Native Interface).

R8 builds a graph of used code by analyzing direct calls. When code is accessed in a dynamic way, R8’s static analysis cannot predict that and it will identify that code as unused and remove it, leading to runtime crashes.

A keep rule is an explicit instruction to the R8 compiler, stating: “This specific class, method, or field is an entry point that will be accessed dynamically at runtime. You must keep it, even if you cannot find a direct reference to it.”

See the official guide for more details on Keep Rules.

Where to write Keep Rules

Custom Keep Rules for an application are written in text file. By convention, this file is named proguard-rules.pro and is located in the root of the app or library module. This file is then specified in your module’s build.gradle.kts file’s release build type.

release {

    isShrinkResources = true

    isMinifyEnabled = true

    proguardFiles(

        getDefaultProguardFile(“proguard-android-optimize.txt”),

        “proguard-rules.pro”,

    )

}


Use the correct default file

The getDefaultProguardFile method imports a default set of rules provided by the Android SDK. When using the wrong file your app might not be optimized. Make sure to use proguard-android-optimize.txt. This file provides the default Keep Rules for standard Android components and enables R8’s code optimizations. The outdated proguard-android.txt only provides the Keep Rules but does not enable R8’s optimizations.

Since this is a serious performance problem, we are starting to warn developers about using the wrong file, starting in Android Studio Narwhal 3 Feature Drop. And starting with the Android Gradle Plugin Version 9.0 we’re no longer supporting the outdated proguard-android.txt file. So make sure you upgrade to the optimized version.

How to write Keep Rules

A keep rule consists of three main parts:

  1. An option like -keep or -keepclassmembers

  2. Optional modifiers like allowshrinking

  3. A class specification that defines the code to match

For the complete syntax and examples, refer to the guidance to add Keep Rules.

Keep Rule anti-patterns

It’s important to know about best practices, but also about anti-patterns. These anti-patterns often arise from misunderstandings or troubleshooting shortcuts and can be catastrophic for a production build’s performance.

Global options

These flags are global toggles that should never be used in a release build. They are only for temporary debugging to isolate a problem.

Using -dontotptimize effectively disables R8’s performance optimizations leading to a slower app.

When using -dontobfuscate you disable all renaming and using -dontshrink turns off dead code removal. Both of these global rules increase app size.

Avoid using these global flags in a production environment wherever possible for a more performant app user experience.

Overly broad keep rules

The easiest way to nullify R8’s benefits is to write overly-broad Keep Rules. Keep rules like the one below instruct the R8 optimizer to not shrink, not obfuscate, and not optimize any class in this package or any of its sub-packages. This completely removes R8’s benefits for that entire package. Try to write narrow and specific Keep Rules instead.


-keep class com.example.package.** { *;} // WIDE KEEP RULES CAUSE PROBLEMS


The inversion operator (!)

The inversion operator (!) seems like a powerful way to exclude a package from a rule. But it’s not that simple. Take this example:


-keep class !com.example.my_package.** { *; } // USE WITH CAUTION

You might think that this rule means “do not keep classes in com.example.package.” But it actually means “keep every class, method and property in the entire application that is not in com.example.package.” If that came as a surprise to you, best check for any negations in your R8 configuration.

Redundant rules for Android components

Another common mistake is to manually add Keep Rules for your app’s Activities, Services, or BroadcastReceivers. This is unnecessary. The default proguard-android-optimize.txt file already includes the relevant rules for these standard Android components to work out of the box.

Also many libraries bring their own Keep Rules. So you should not have to write your own rules for these. In case there is a problem with Keep Rules from a library you’re using, it is best to reach out to the library author to see what the problem is.

Keep Rule best practices

Now that you know what not to do, let’s talk about best practices.

Write narrow Keep Rules

Good Keep Rules should be as narrow and specific as possible. They should preserve only what is necessary, allowing R8 to optimize everything else.

Rule

Quality

-keep class com.example.** { ; }

Low: Keeps an entire package and its subpackages

-keep class com.example.MyClass { ; }

Low: Keeps an entire class which is likely still too wide

-keepclassmembers class com.example.MyClass {

    private java.lang.String secretMessage;

    public void onNativeEvent(java.lang.String);

}

High: Only relevant methods and properties from a specific class are kept


Use common ancestors

Instead of writing separate Keep Rules for multiple different data models, write one rule that targets a common base class or interface. The below rule tells R8 to keep any members of classes that implement this interface and is highly scalable.


# Keep all fields of any class that implements SerializableModel

-keepclassmembers class * implements com.example.models.SerializableModel {

    <fields>;

}


Use Annotations to target multiple classes

Create a custom annotation (e.g., @Serialize) and use it to “tag” classes that need their fields preserved. This is another clean, declarative, and highly scalable pattern. You can create Keep Rules for already existing annotations from frameworks you’re using as well.

# Keep all fields of any class annotated with @Serialize

-keepclassmembers class * {

    @com.example.annotations.Serialize <fields>;

}

Choose the right Keep Option

The Keep Option is the most critical part of the rule. Choosing the wrong one can needlessly disable optimization.

Keep Option

What It Does

-keep

Prevents the class and members mentioned in the declaration from being removed or renamed.

-keepclassmembers

Prevents the specified members from being removed or renamed, but allows the class itself to be removed but only on classes which are not otherwise removed.

-keepclasseswithmembers

A combination: Keeps the class and its members, only if all the specified members are present.


You can find more about the keep option in our
documentation for Keep Options.

Allow optimization with Modifiers

Modifiers like allowshrinking and allowobfuscation relax a broad -keep rule, giving optimization power back to R8. For example, if a legacy library forces you to use -keep on an entire class, you might be able to reclaim some optimization by allowing shrinking and obfuscation:


# Keep this class, but allow R8 to remove it if it’s unused and allow R8 to rename it.

-keep,allowshrinking,allowobfuscation class com.example.LegacyClass


Add global options for additional optimization

Beyond Keep Rules, you can add global flags to your R8 configuration file to encourage even more optimization.

-repackageclasses is a powerful option that instructs R8 to move all obfuscated classes into a single package. This saves significant space in the DEX file by removing redundant package name strings.

-allowaccessmodification allows R8 to widen access (e.g., private to public) to enable more aggressive inlining. This is now enabled by default when using proguard-android-optimize.txt.

Warning: Library authors must never add these global optimization flags to their consumer rules, as they would be forcibly applied to the entire app.

And to make it even more clear, in version 9.0 of the Android Gradle Plugin we’re going to start ignoring global optimization flags from libraries altogether. 

Best practices for libraries

Every Android app relies on libraries one way or another. So let’s talk about best practices for libraries.

For library developers

If your library uses reflection or JNI, you have the responsibility to provide the necessary Keep Rules to its consumers. These rules are placed in a consumer-rules.pro file, which is then automatically bundled inside the library’s AAR file.

android {

    defaultConfig {

        consumerProguardFiles(“consumer-rules.pro”)

    }

    

}


For library consumers

Filter out problematic Keep Rules

If you must use a library that includes problematic Keep Rules, you can filter them out in your build.gradle.kts file starting with AGP 9.0 This tells R8 to ignore the rules coming from a specific dependency.


release {

    optimization.keepRules {

        // Ignore all consumer rules from this specific library

        it.ignoreFrom(“com.somelibrary:somelibrary”)

    }

}


The best Keep Rule is no Keep Rule

The ultimate R8 configuration strategy is to remove the need to write Keep Rules altogether. For many apps can be achieved by choosing modern libraries that favor code generation over reflection. With code generation, the optimizer can more easily determine what code is actually used at runtime and what code can be removed. Also not using any dynamic reflection means no “hidden” entry points, and therefore, no Keep Rules are needed. When choosing a new library, always prefer a solution that uses code generation over reflection.

For more information about how to choose libraries, check choose library wisely.

Debugging and troubleshooting your R8 configuration

When R8 removes code it should have kept, or your APK is larger than expected, use these tools to diagnose the problem.

Find duplicate and global Keep Rules

Because R8 merges rules from dozens of sources, it can be hard to know what the “final” ruleset is. Adding this flag to your proguard-rules.pro file generates a complete report:

# Outputs the final, merged set of rules to the specified file

-printconfiguration build/outputs/logs/configuration.txt


You can search this file to find redundant rules or trace a problematic rule (like -dontoptimize) back to the specific library that included it.

Ask R8: Why are you keeping this?

If a class you expected to be removed is still in your app, R8 can tell you why. Just add this rule:

# Asks R8 to explain why it’s keeping a specific class

class com.example.MyUnusedClass

-whyareyoukeeping 



During the build, R8 will print the exact chain of references that caused it to keep that class, allowing you to trace the reference and adjust your rules.

For a full guide, check out the troubleshoot R8 section.

Next steps

R8 is a powerful tool for enhancing Android app performance. Its effectiveness, depends on a correct understanding of its operation as a static analysis engine.

By writing specific, member-level rules, leveraging ancestors and annotations, and carefully choosing the right keep options, you can preserve exactly what is necessary. The most advanced practice is to eliminate the need for rules entirely by choosing modern, codegen-based libraries over their reflection-based predecessors.

As you’re following along Performance Spotlight Week, make sure to check out today’s Spotlight Week video on YouTube and continue with our R8 challenge. Use #optimizationEnabled for any questions on enabling or troubleshooting R8. We’re here to help.

It’s time to see the benefits for yourself.

We challenge you to enable R8 full mode for your app today.

  1. Follow our developer guides to get started: Enable app optimization.

  2. Check if you still use proguard-android.txt and replace it with proguard-android-optimize.txt.

  3. Then, measure the impact. Don’t just feel the difference, verify it. Measure your performance gains by adapting the code from our Macrobenchmark sample app on GitHub to measure your startup times before and after.

We’re confident you’ll see a meaningful improvement in your app’s performance.

While you’re at it, use the social tag #AskAndroid to bring your questions. Throughout the week our experts are monitoring and answering your questions.

Stay tuned for tomorrow, where we’ll talk about Profile Guided Optimization with Baseline and Startup Profiles, share how Compose rendering performance improved over the past releases and share performance considerations for background work.


The post Configure and troubleshoot R8 Keep Rules appeared first on InShot Pro.

]]>
How Reddit used the R8 optimizer for high impact performance improvements https://theinshotproapk.com/how-reddit-used-the-r8-optimizer-for-high-impact-performance-improvements/ Mon, 17 Nov 2025 18:01:00 +0000 https://theinshotproapk.com/how-reddit-used-the-r8-optimizer-for-high-impact-performance-improvements/ Posted by Ben Weiss – Senior Developer Relations Engineer In today’s world of mobile applications, a seamless user experience is ...

Read more

The post How Reddit used the R8 optimizer for high impact performance improvements appeared first on InShot Pro.

]]>

Posted by Ben Weiss – Senior Developer Relations Engineer

In today’s world of mobile applications, a seamless user experience is not just a feature—it’s a necessity. Slow load times, unresponsive interfaces, and instability can be significant barriers to user engagement and retention. During their work with the Android Developer Relations team, the engineering team at Reddit used the App Performance Score to evaluate their app. After assessing their performance, they identified significant improvement potential and decided to take the steps to enable the full power of R8, the Android app optimizer. This focused initiative led to remarkable improvements in startup times, reductions in slow or frozen frames and ANRs, and an overall increase in Play Store ratings. This case study breaks down how Reddit achieved these impressive results.


How the R8 Optimizer helped Reddit

The R8 Optimizer is a foundational tool for performance optimization on Android. It takes various steps to improve app performance.Let’s take a quick look at the most impactful ones.


  • Tree shaking is the most important step to reduce an app’s size. Here, unused code from app dependencies and the app itself is removed.

  • Method inlining replaces method calls with the actual code, making the app more performant.

  • Class merging, and other strategies are applied to make the code more compact. At this point it’s not about human readability of source code any more, but making compiled code work fast. So abstractions, such as interfaces or class hierarchies don’t matter here and will be removed.

  • Identifier minification changes the names of classes, fields, and methods to shorter, meaningless names. So instead of MyDataModel you might end up with a class called a

  • Resource shrinking removes unused resources such as xml files and drawables to further reduce app size.


Caption: Main stages of R8 Optimization


From hard data to user satisfaction: Identifying success in production

Reddit saw improved performance results immediately after a new version of the app was rolled out to users. By using Android Vitals and Crashlytics, Reddit was able to capture performance metrics on real devices with actual users, allowing them to compare the new release against previous versions.

Caption: How R8 improved Reddit’s app performance


The team observed a 40% faster cold startup, a 30% reduction in “Application Not Responding” (ANR) errors, a 25% improvement in frame rendering, and a 14% reduction in app size.

These enhancements are crucial for user satisfaction. A faster startup means less waiting and quicker access to content. Fewer ANRs lead to a more stable and reliable app, reducing user frustration. Smoother frame rendering removes UI jank, making scrolling and animations feel fluid and responsive. This positive technical impact was also clearly visible in user sentiment.

User satisfaction indicators of the optimization’s success were directly visible on the Google Play Store. Following the rollout of the R8-optimized version, the team saw a dramatic and positive shift in user sentiment and engagement.


Drew Heavner: “Enabling R8’s full potential tool less than 2 weeks”


Most impressively, this was accomplished with a focused effort. Drew Heavner, the Staff Software Engineer at Reddit who worked on this initiative, noted that implementing the changes to enable R8’s full potential took less than two weeks.

Confirming the gains: A deep dive with macrobenchmarks

After observing the significant real-world improvements, Reddit’s engineering team and the Android Developer Relations team at Google conducted detailed benchmarks to scientifically confirm the gains and experiment with further optimizations. For this analysis, Reddit engineering provided two versions of their app: one without optimizations and another that applied R8 and two more foundational performance optimization tools: Baseline Profiles, and Startup Profiles.

Baseline Profiles effectively move the Just in Time (JIT) compilation steps away from user devices and onto developer machines. The generated Ahead Of Time (AOT) compiled code has proven to reduce startup time and rendering issues alike.

When an app is packaged, the d8 dexer takes classes and methods and constructs your app’s
classes.dex files. When a user opens the app, these dex files are loaded, one after the other until the app can start. By providing a Startup Profile you let d8 know which classes and methods to pack in the first classes.dex files. This structure allows the app to load fewer files, which in turn improves startup speed.

Jetpack Macrobenchmark was the core tool for this phase, allowing for precise measurement of user interactions in a controlled environment. To simulate a typical user journey, they used the UIAutomator API to create a test that opened the app, scrolled down three times, and then scrolled back up.

In the end all that was needed to write the benchmark was this:

uiAutomator {

  startApp(REDDIT)

  repeat(3) {

    onView { isScrollable }.fling(Direction.DOWN) }

  repeat(3) {

    onView {isScrollable }.fling(Direction.UP)

  }

}

The benchmark data confirmed the field observations and provided deeper insights. The fully optimized app started 55% faster and users could begin to browse 18% sooner. The optimized app also showed a two-thirds reduction in Just in Time (JIT) compilation occurrences and a one-third decrease in JIT compilation time. Frame rendering improved, resulting in 19% more frames being rendered over the benchmarked user journey. Finally, the app’s size was reduced by over a third.

Caption: Reddit’s overall performance improvements

You can measure the JIT compilation time with a custom Macrobenchmark trace section metric like this:

val jitCompilationMetric = TraceSectionMetric(“JIT Compiling %”, label = “JIT compilation”)


Enabling the technology behind the transformation: R8

To enable R8 in full mode, you configure your app/build.gradle.kts file by setting minifyEnabled and shrinkResources to true in the release build type.

android {

    …

    buildTypes {

        release {

            isMinifyEnabled = true

            isShrinkResources = true

            proguardFiles(

                getDefaultProguardFile(“proguard-android-optimize.txt”),

                “keep-rules.pro”,

            )

        }

    }

}


This step has to be followed by holistic end to end testing, as performance optimizations can lead to unwanted behavior, which you better catch before your users do.

As shown earlier in this article, R8 performs extensive optimizations in order to maximize your performance benefits. R8 makes substantial modifications to the code including renaming, moving, and removing classes, fields and methods. If you observe that these modifications cause errors, you need to specify which parts of the code R8 shouldn’t modify by declaring those in keep rules.

Follow Reddit’s example in your app

Reddit’s success with R8 serves as a powerful case study for any development team looking to make a significant, low-effort impact on their app’s performance. The direct correlation between the technical improvements and the subsequent rise in user satisfaction underscores the value of performance optimization.

By following the blueprint laid out in this case study—using tools like the App Performance Score to identify opportunities, enabling R8’s full optimization potential, monitoring real-world data, and using benchmarks to confirm and deepen understanding—other developers can achieve similar gains.

To get started with R8 in your own app, refer to the freshly updated official documentation and guidance on enabling, configuring and troubleshooting the R8 optimizer.


The post How Reddit used the R8 optimizer for high impact performance improvements appeared first on InShot Pro.

]]>
Use R8 to shrink, optimize, and fast-track your app https://theinshotproapk.com/use-r8-to-shrink-optimize-and-fast-track-your-app/ Mon, 17 Nov 2025 17:00:00 +0000 https://theinshotproapk.com/use-r8-to-shrink-optimize-and-fast-track-your-app/ Posted by Ben Weiss – Senior Developer Relations Engineer Welcome to day one of Android Performance Spotlight Week! We’re kicking ...

Read more

The post Use R8 to shrink, optimize, and fast-track your app appeared first on InShot Pro.

]]>

Posted by Ben Weiss – Senior Developer Relations Engineer

Welcome to day one of Android Performance Spotlight Week!

We’re kicking things off with the single most impactful, low-effort change you can make to improve your app’s performance: enabling the R8 optimizer in full mode.

You probably already know R8 as a tool to shrink your app’s size. It does a fantastic job of removing unused code and resources, reducing your app’s size. But its real power, the one it’s really g-R8 at, is as an optimizer.

When you enable full mode and allow optimizations, R8 performs deep, whole-program optimizations, rewriting your code to be fundamentally more efficient. This isn’t just a minor tweak.

After reading this article, check out the Performance Spotlight Week introduction to the R8 optimizer on YouTube.

How R8 makes your app more performant

Let’s shine a spotlight on the largest steps that the R8 optimizer takes to improve app performance.

Tree shaking is the most important step to reduce app size. During this phase the R8 optimizer removes unused code from libraries that your app depends on as well as dead code from your own codebase.

Method inlining replaces a method call with the actual code, which improves runtime performance.

Class merging, and other strategies are applied to make the code more compact. All your beautiful abstractions, such as interfaces and class hierarchies don’t matter at this point and are likely to be removed.

Code minification is used to change the names of classes, fields, and methods to shorter, meaningless ones. So instead of MyDataModel you might end up with a class called

a. This is what causes the most confusion when reading stack traces from an R8 optimized app. (Note that we have improved this in AGP 9.0!)

Resource shrinking further reduces an app’s size by removing unused resources such as xml files and drawables.

By applying these steps the R8 optimizer improves app startup times, enables smoother UI rendering, with fewer slow and frozen frames and improves overall on-device resource usage.

Case Study: Reddit’s performance improvements with R8

As one example of the performance improvements that R8 can bring, let’s take a look at an example from Reddit. After enabling R8 in full mode, the Reddit for Android app saw significant performance improvements in various areas.

Caption: How R8 improved Reddit’s app performance


The team observed a 40% faster cold startup, a 30% reduction in “Application Not Responding” (ANR) errors, a 25% improvement in frame rendering, and a 14% reduction in app size

.


These enhancements are crucial for user satisfaction. A faster startup means less waiting and quicker access to content. Fewer ANRs lead to a more stable and reliable app, reducing user frustration. Smoother frame rendering removes UI jank, making scrolling and animations feel fluid and responsive. This positive technical impact was also clearly visible in user sentiment.

You can read more about their improvements on our blog.


Non-technical side effects of using R8

During our work with partners we have seen that these technical improvements have a direct impact on user satisfaction and can be reflected in user retention, engagement and session length. User stickiness, which can be measured with daily, weekly or monthly active users, has also been positively affected by technical performance improvements. And we’ve seen app ratings on the Play Store rise in correlation with R8 adoption. Sharing this with your product owners, CTOs and decision makers can help speed up your app’s performance.



So let’s call it what it is: Deliberate performance optimization is a virtue.

Guiding you to a more performant app

We heard that our developer guidance for R8 needed to be improved. So we went to work. The developer guidance for the R8 optimizer now is much more actionable and provides comprehensive guidance to enable and debug R8.

The documentation guides you on the high-level strategy for adoption, emphasizing the importance of choosing optimization-friendly libraries and, crucially, adopting R8’s features incrementally to ensure stability. This phased approach allows you to safely unlock the benefits of R8 while providing you with guidance on difficult-to-debug issues.

We have significantly expanded our guidance on Keep Rules, which are the primary mechanism for controlling the R8 optimizer. We now provide a section on what Keep Rules are, how to apply them and guide you with best practices for writing and maintaining them. We also provide practical and actionable use cases and examples, helping you understand how to correctly prevent R8 from removing code that is needed at runtime, such as code accessed via reflection or use of the JNI native interface.

The documentation now also covers essential follow-up steps and advanced scenarios. We added a section on testing and troubleshooting, so you can verify the performance gains and debug any potential issues that arise. The advanced configurations

section explains how to target specific build variants, customize which resources are kept or removed, and offers special optimization instructions for library authors, ensuring you can provide an optimized and R8-friendly package for other developers to use.

Enable the R8 optimizer’s full potential

The R8 optimizer defaults to using “full mode” since version 8.0 of the Android Gradle Plugin. If your project has been developed over many years, it might still include a legacy flag to disable it. Check your gradle.properties file for this line and remove it.

android.enableR8.fullMode=false // delete this line to enable R8’s full potential

Now check whether you have enabled R8 in your app’s build.gradle.kts file for the release variant. It’s enabled by setting isMinifyEnabled and isShrinkResources to true. You can also pass default and custom configuration files at this step.

release {

   isMinifyEnabled = true

   isShrinkResources = true

   proguardFiles(

       getDefaultProguardFile(“proguard-android-optimize.txt”),

       “keep-rules.pro”

   )

}


Case Study: Disney+ performance improvements

Engineers at Disney+ invest in app performance and are optimizing the app’s user experience. Sometimes even seemingly small changes can make a huge impact. While inspecting their R8 configuration, the team found that the -dontoptimize flag was being used. It was brought in by a default configuration file, which is still used in many apps today.


After replacing proguard-android.txt

with proguard-android-optimize.txt, the Disney+ team saw significant improvements in their app’s performance.

After a new version of the app containing this change was rolled out to users, Disney+ saw 30% faster app startup and 25% fewer user-perceived ANRs. 

Today many apps still use the proguard-android.txt file which contains the -dontoptimize flag. And that’s where our tooling improvements come in.

Tooling support

Starting with Android Studio Narwhal 3 Feature Drop, you will see a lint warning when using proguard-android.txt 

And from AGP 9.0 onwards we are entirely dropping support for the file. This means you will have to migrate to proguard-android-optimize.txt.

We’ve also invested in new Android Studio features

to make debugging R8-optimized code easier than ever. Starting in AGP 9.0 you can now automatically de-obfuscate stack traces within Android Studio’s logcat for R8-processed builds, helping you pinpoint the exact line of code causing an issue, even in a fully optimized app. This will be covered in more depth in tomorrow’s blog post on this Android Performance Spotlight Week.

Next Steps

Check out the Performance Spotlight Week introduction to the R8 optimizer on YouTube.


📣 Take the Performance Challenge!

It’s time to see the benefits for yourself.

We challenge you to enable R8 full mode for your app today.

  1. Follow our developer guides to get started: Enable app optimization.

  2. Check if you still use proguard-android.txt and replace it with proguard-android-optimize.txt.

  3. Then, measure the impact. Don’t just feel the difference, verify it. Measure your performance gains by adapting the code from our

    Macrobenchmark sample app on GitHub to measure your startup times before and after.

We’re confident you’ll see a meaningful improvement in your app’s performance. Use #optimizationEnabled for any questions on enabling or troubleshooting R8. We’re here to help.

Bring your questions for the Ask Android session on Friday

Use the social tag #AskAndroid to bring any performance questions. Throughout the week we are monitoring your questions and will answer several in the Ask Android session on performance on Friday, November 21. Stay tuned for tomorrow, where we’ll dive even deeper into debugging and troubleshooting. But for now, get started with R8 and get your app on the fast track.


The post Use R8 to shrink, optimize, and fast-track your app appeared first on InShot Pro.

]]>
Optimize your app battery using Android vitals wake lock metric https://theinshotproapk.com/optimize-your-app-battery-using-android-vitals-wake-lock-metric/ Thu, 02 Oct 2025 16:00:00 +0000 https://theinshotproapk.com/optimize-your-app-battery-using-android-vitals-wake-lock-metric/ Written by Alice Yuan, Senior Developer Relations Engineer Most of the content of this post is also available in video ...

Read more

The post Optimize your app battery using Android vitals wake lock metric appeared first on InShot Pro.

]]>

Written by Alice Yuan, Senior Developer Relations Engineer


Most of the content of this post is also available in video format, go give it a watch!

Battery life is a crucial aspect of user experience and wake locks play a major role. Are you using them excessively? In this blog post we’ll explore what wake locks are, what are some best practices for using them and how you can better understand your own app’s behavior with the Play Console metric.

Excessive partial wake lock usage in Android Vitals

The Play Console now monitors battery drain, with a focus on excessive partial wake lock usage, as a key performance indicator.

This feature elevates the importance of battery efficiency alongside existing core metric stability indicators: excessive user-perceived crashes and ANRs. Currently, an app exceeding the threshold will not be less discoverable on Google Play.

The excessive wake lock warning in the Android vitals overview.

For mobile devices, the Android vitals metric applies to non-exempted wake locks acquired while the screen is off and the app is in the background or running a foreground service. Android vitals considers partial wake lock usage excessive if:

  • Wake locks are held for at least two hours within a 24-hour period.

  • It affects more than 5% of your app’s sessions, averaged over 28 days.

Wake locks created by audio, location, and JobScheduler user initiated APIs are exempted from the wake lock calculation.

Understanding wake locks

A wake lock is a mechanism that allows an app to keep a device’s CPU running even when the user isn’t actively interacting with it. 

A partial wake lock keeps the CPU running even if the screen is off, preventing the CPU from entering a low-power “suspend” state. A full wake lock keeps both the screen and the CPU running.

There are 2 methods partial wake locks are acquired:

  • The app manually acquires and releases the wake lock using PowerManager APIs for a specific use case, often this is acquired in conjunction with a Foreground Service – a platform lifecycle API intended for user-perceptible operation.

  • Alternatively, the wake lock is acquired by another API, and attributed to the app due to usage of the API, more on this in the best practices section.

While wake locks are necessary for tasks like completing a user-initiated download of a large file, their excessive or improper use can lead to significant battery drain. We’ve seen cases where apps hold wake locks for hours or fail to release them properly, leading to user complaints about significant battery drain even when they’re not interacting with the app.

Best Practices for Wake Lock Usage

Before we go over how to debug excessive wake lock usage, ensure you’re following wake lock best practices. 

Consider these four critical questions.

1. Have you considered alternative wake lock options?

Before considering acquiring a manual partial wake lock, follow this decision-making flowchart:

Flowchart to decide when to manually acquire a wake lock

  1. Does the screen need to stay on?

  • Is the application running a foreground service? 

    • No: You don’t need to manually acquire a wake lock.

  • Is it detrimental to the user experience if the device suspends? 

    • No: For instance, updating a notification after the device wakes up doesn’t require a wake lock. 

    • Yes: If it’s critical to prevent the device from suspending, like ongoing communication with an external device, proceed.

  • Is there already an API keeping the device awake on your behalf?

    • You can leverage the documentation Identify wake locks created by other APIs to identify scenarios where wake locks created by other APIs to identify scenarios where wake locks are created by other APIs such as LocationManager.

    • If no APIs exist, proceed to the final question.

  • If you’ve answered all these questions and determined no alternative exists, you should proceed with manually acquiring a wake lock.

  • 2. Are you naming the wake lock correctly?

    When manually acquiring wake locks, proper naming is important for debugging:

    • Leave out any Personally Identifiable Information (PII) in the name like email addresses. If PII is detected, the wake lock is logged as _UNKNOWN, hindering debugging.

    • Don’t name your wake lock programmatically using class or method names, as these can be obfuscated by tools like Proguard. Instead, use a hard-coded string.

    • Do not add counters or unique identifiers to wake lock tags. The same tag should be used every time the wake lock runs to allow the system to aggregate usage by name, making abnormal behavior easier to detect.

    3. Is the acquired wake lock always released?

    If you’re acquiring a wake lock manually, ensure the wake lock release always executes. Failing to release a wake lock can cause significant battery drain. 

    For example, if an uncaught exception is thrown during processingWork(), the release() call might never happen. Instead, you can use a try-finally block to guarantee the wake lock is released, even if an exception occurs.

    Additionally, you can add a timeout to the wake lock to ensure it releases after a specific period, preventing it from being held indefinitely.

    fun processingWork() {
        wakeLock.apply {
            try {
                acquire(60 * 10 * 1000) // timeout after 10 minutes
                doTheWork()
            } finally {
                release()
            }
        }
    }


    4. Can you reduce the wake-up frequency?

    For periodic data requests, reducing how often your app wakes up the device is key to battery optimization. Some examples of reducing wake-up frequency include:

    You can view more details in the wake lock best practices documentation.

    Debugging excessive wake lock usage

    Even with the best intentions, excessive wake lock usage can occur. If your app is flagged in the Play Console, here’s how to debug it:
    Initial identification with Play Console

    The Android vitals excessive partial wake lock dashboard provides breakdowns of non-exempted wake lock names associated with your app, showing affected sessions and durations. Reminder to use the documentation to help you identify if the wake lock name is app-held or held by another API.

    The Android vitals excessive partial wake lock dashboard scrolled down to the breakdowns section to view excessive wake lock tags.

    Debugging excessive wake locks held by workers/jobs


    You can identify worker-held wake locks with this wake lock name:

    *job*/<package_name>/androidx.work.impl.background.systemjob.SystemJobService

    The full list of variations of worker-held wake lock names is available in documentation. To debug these wake locks, you can use Background Task Inspector to debug locally, or leverage getStopReason to debug issues in the field. 


    Android Studio Background Task Inspector

    Screen capture of the Background Task Inspector, where it has been able to identify a worker “WeatherSyncWorker” that has frequently retried and failed.

    For local debugging of WorkManager issues, use this tool on an emulator or connected device (API level 26+). It shows a list of workers and their statuses (finished, executing, enqueued), allowing you to inspect details and understand worker chains. 

    For instance, it can reveal if a worker is frequently failing or retrying due to hitting system limitations. 

    See Background Task Inspector documentation for more details.

    WorkManager getStopReason

    For in-field debugging of workers with excessive wake locks, use WorkInfo.getStopReason() on WorkManager 2.9.0+ or for JobScheduler, JobParameters.getStopReason() available on SDK 31+. 

    This API helps log the reason why a worker stopped (e.g., STOP_REASON_TIMEOUT, STOP_REASON_QUOTA), pinpointing issues like frequent timeouts due to exhausting runtime duration.

    backgroundScope.launch {
        WorkManager.getInstance(context)
            .getWorkInfoByIdFlow(workRequest.id)
            .collect { workInfo ->
                logStopReason(workRequest.id, workInfo?.stopReason)
            }
    }
    

    Debugging other types of excessive wake locks

    For more complex scenarios involving manually held wake locks or APIs holding the wake lock, we recommend you use system trace collection to debug.

    System trace collection

    A system trace is a powerful debugging tool that captures a detailed record of system activity over a period, providing insights into CPU state, thread activity, network activity, and battery-related metrics like job duration and wake lock usage.

    You can capture a system trace using several methods: 

    Enable “power:PowerManagement” Atrace category in the Perfetto UI under the Android apps & svcs tab. 

    Regardless of the chosen method, it’s crucial to ensure that you are collecting the “power:PowerManagement” Atrace category to enable viewing of device state tracks. 

    Perfetto UI inspection and SQL analysis

    System traces can be opened and inspected in the Perfetto UI. When you open the trace, you will see a visualization of various processes on a timeline. The tracks we will be focused on in this guide are the ones under “Device State”.

    Pin the tracks under “Device State” such as “Top app”, “Screen state”, “Long Wake locks”, and “Jobs” tracks to visually identify long-running wake lock slices.

    Each block lists the name of the event, when the event started, and when it ended. In Perfetto, this is called a slice.

    For scalable analysis of multiple traces, you can use Perfetto’s SQL analysis. A SQL query can find all wake locks sorted by duration, helping identify the top contributors to excessive usage.

    Here’s an example query summing all the wake lock tags that occurred in the system trace, ordered by total duration:

    SELECT slice.name as name, track.name as track_name,
    SUM(dur / 100000) as total_dur_ms
    FROM slice
    JOIN track ON slice.track_id = track.id
    WHERE track.name = 'WakeLocks'
    GROUP BY slice.name, track.name
    ORDER BY total_dur_ms DESC
    


    Use ProfilingManager for in-field trace collection

    For hard-to-reproduce issues, ProfilingManager (added in SDK 35) is a programmatic API that allows developers to collect system traces in the field with start and end triggers. It offers more control over the start and end trigger points for profile collection and enforces system-level rate limiting to prevent device performance impact. 

    Check out the ProfilingManager documentation for further steps on how to implement in field system trace collection which include how to programmatically capture a trace, analyze profiling data, and use local debug commands.

    The system traces collected using ProfilingManager will look similar to the ones collected manually, but system processes and other app processes are redacted from the trace.

    Conclusion

    The excessive partial wake lock metric in Android vitals is only a small part of our ongoing commitment to supporting developers in reducing battery drain and improving app quality. 

    By understanding and properly implementing wake locks, you can significantly optimize your app’s battery performance. Leveraging alternative APIs, adhering to wake lock best practices, and using powerful debugging tools such as Background Task Inspector, system traces and ProfilingManager are key to ensuring your app’s success on Google Play.


    The post Optimize your app battery using Android vitals wake lock metric appeared first on InShot Pro.

    ]]>