Mobile Development 22 min read

Why Setting a Low Thread Priority on Android Can Crash the Main Thread

An in‑depth investigation reveals that calling Thread.start() before setPriority() on Android can unintentionally lower the main thread’s priority and increase TimerSlack, causing sleep/wait delays, video stutter, and a major WeChat outage, with root causes traced to timing issues and WebView initialization.

WeChat Client Technology Team
WeChat Client Technology Team
WeChat Client Technology Team
Why Setting a Low Thread Priority on Android Can Crash the Main Thread

Running the following code on Android's main thread:

Thread t = new Thread();</code><code>t.start();</code><code>t.setPriority(3);

We expect only the child thread t to have a low priority.

In reality, both the child thread and the main thread end up with low priority, causing a live fault in the WeChat Android client.

(Skip to section 5.3 for correct and incorrect examples of setting thread priority.)

1. Case Occurrence

After a new WeChat Android version was released, users reported video stutter and audio‑video desynchronization in public accounts and Moments. Initial suspicion fell on the player or CDN, but testing pinpointed the Matrix performance‑monitoring tool upgrade as the cause.

2. Scene

Investigation showed that methods like pthread_cond_timedwait and sleep/wait were about 40 ms longer than requested. The following code reproduces the issue:

long startTime = System.currentTimeMillis();</code><code>Thread.sleep(10);</code><code>Log.i("Matrix","duration = " + (System.currentTimeMillis() - startTime)); // result: duration = 50

Further binary search in Matrix commits identified a change that swapped the order of setting thread priority and starting the thread.

The modification set the thread priority before the thread was fully created, causing the main thread’s priority to be altered as well.

Thread t = new Thread();</code><code>t.start();</code><code>t.setPriority(3);</code><code></code><code>long startTime = System.currentTimeMillis();</code><code>Thread.sleep(10);</code><code>Log.i("Matrix","duration = " + (System.currentTimeMillis() - startTime)); // result: duration = 50ms

If setPriority is called immediately after start with a priority lower than the default (5), sleep/wait methods take dozens of milliseconds longer.

Because many video‑playback loops rely on sleep/wait or pthread_cond_timedwait for audio‑video sync, the extra delay leads to noticeable stutter and desynchronization, which triggered the WeChat outage.

3. Analysis

The issue propagates to the main thread and all threads created by it.

3.1 Root Cause: TimerSlackHigh

Thread.setPriority eventually calls PaletteSchedSetPriority in system/libartpalette/palette_android.cc, which maps the Java priority to a Linux nice value using the kNiceValues array.

palette_status_t PaletteSchedSetPriority(int32_t tid, int32_t managed_priority) { ... }

If the resulting nice value is ≥ ANDROID_PRIORITY_BACKGROUND (10), the system applies the task profile SCHED_SP_BACKGROUND, which includes the TimerSlackHigh action that sets a large timer slack (≈40 ms).

When setPriority is invoked before the native thread ID (tid) is initialized, GetTid() returns 0, causing setpriority to affect the calling (main) thread instead of the intended child thread.

3.2 Misunderstanding of setPriority

In the ART runtime, Thread::SetNativePriority calls PaletteSchedSetPriority(GetTid(), new_priority). If GetTid() is still 0, the native setpriority call changes the main thread’s nice value and TimerSlack.

3.3 The Accomplice: WebView

Creating a WebView before setting thread priority forces the main thread’s nice value to -4 (high priority) via Chromium’s base::PlatformThread::SetCurrentThreadPriority, but it does not reset TimerSlack. Subsequent child threads inherit the main thread’s nice value (< -4) and the high TimerSlack, leading to the observed fault.

Thread t = new Thread();</code><code>t.start();</code><code>t.setPriority(3);</code><code></code><code>long startTime = System.currentTimeMillis();</code><code>Thread.sleep(10);</code><code>Log.i("Matrix","MainThread duration = " + (System.currentTimeMillis() - startTime)); // 50</code><code></code><code>new WebView(context);</code><code></code><code>new Thread(new Runnable() { public void run() { long startTime = System.currentTimeMillis(); Thread.sleep(10); Log.i("Matrix","Thread duration = " + (System.currentTimeMillis() - startTime)); // 50 } }).start();

The WebView initialization raises the main thread’s priority without lowering TimerSlack, so child threads inherit the high TimerSlack while being considered foreground threads.

4. Monitoring Mechanism

After understanding the root cause, a monitoring hook can be added to prevent the main thread from being set to a low priority or a large TimerSlack.

int (*original_setpriority)(int which, id_t who, int priority);
int my_setpriority(int which, id_t who, int priority) {
    if (priority <= 0) return original_setpriority(which, who, priority);
    if (who == 0 && getpid() == gettid()) { /* crash */ }
    else if (who == getpid()) { /* crash */ }
    return original_setpriority(which, who, priority);
}
int (*original_prctl)(int option, unsigned long arg2, unsigned long arg3,
                      unsigned long arg4, unsigned long arg5);
int my_prctl(int option, unsigned long arg2, unsigned long arg3,
             unsigned long arg4, unsigned long arg5) {
    if (option == PR_SET_TIMERSLACK && gettid()==getpid() && arg2 > 50000) {
        /* crash */
    }
    return original_prctl(option, arg2, arg3, arg4, arg5);
}

These hooks can be enabled in debug builds of WeChat to catch improper priority or TimerSlack changes before they reach production.

5. Additional Conclusions

5.1 Dual Priority Standards

Java’s Thread.setPriority uses a 0‑10 scale where larger numbers mean higher priority; this is mapped to Linux nice values via kNiceValues. In contrast, android.os.Process.setThreadPriority directly sets the nice value on a -20‑20 scale, where larger numbers mean lower priority.

5.2 Correctly Setting HandlerThread Priority

Calling HandlerThread.setPriority only changes the parent Thread field and has no effect. The proper way is to pass the desired nice value to the constructor:

HandlerThread ht = new HandlerThread("My-Thread", 13);
ht.start();

Note that the value 13 corresponds to a nice value of 13 (low priority).

5.3 Some Cases

Summary of correct and incorrect ways to set a thread’s priority to 3 (nice = 13):

// Case 1: Correct
Thread thread = new Thread();
thread.setPriority(3);
thread.start();

// Case 2: Correct
Thread thread = new Thread(() -> {
    Thread.currentThread().setPriority(3);
    // or Process.setThreadPriority(13);
});
thread.start();

// Case 3: Wrong – may affect current thread
Thread thread = new Thread();
thread.start();
thread.setPriority(3);

// Case 4: Wrong – same as Case 3
HandlerThread thread = new HandlerThread("My-Thread");
thread.start();
thread.setPriority(3);

// Case 5: Wrong – priority not applied
HandlerThread thread = new HandlerThread("My-Thread");
thread.setPriority(3);
thread.start();

// Case 6: Correct
HandlerThread thread = new HandlerThread("My-Thread", 13);
thread.start();

The seemingly harmless three‑line priority change can cause a production outage, highlighting the need for careful handling of thread timing and priority in multithreaded Android applications.

Share, bookmark, like, and follow for more insights!

AndroidWebViewThread PriorityTimerSlack
WeChat Client Technology Team
Written by

WeChat Client Technology Team

Official account of the WeChat mobile client development team, sharing development experience, cutting‑edge tech, and little‑known stories across Android, iOS, macOS, Windows Phone, and Windows.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.