From 5c8df3131c621b5b19cebe92c44fb1d71c9e6239 Mon Sep 17 00:00:00 2001 From: Kweku Adams Date: Wed, 3 Nov 2021 15:18:02 -0700 Subject: [PATCH] Thermally throttle jobs based on priorities. Since low and min priority jobs can be deferred more, they can be throttled for thermal reasons earlier. Bug: 203621220 Test: atest CtsJobSchedulerTestCases:JobThrottlingTest Test: atest FrameworksMockingServicesTests:ThermalStatusRestrictionTest Change-Id: Idb77372cea6ac393c4b9d5de4a08d4e86727e5b6 --- .../ThermalStatusRestriction.java | 39 ++- .../ThermalStatusRestrictionTest.java | 241 ++++++++++++++++++ 2 files changed, 270 insertions(+), 10 deletions(-) create mode 100644 services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java index a9b046507f3d3..40244e8cc3862 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java +++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java @@ -16,28 +16,30 @@ package com.android.server.job.restrictions; +import android.app.job.JobInfo; import android.app.job.JobParameters; import android.os.PowerManager; import android.os.PowerManager.OnThermalStatusChangedListener; import android.util.IndentingPrintWriter; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.job.JobSchedulerService; import com.android.server.job.controllers.JobStatus; public class ThermalStatusRestriction extends JobRestriction { private static final String TAG = "ThermalStatusRestriction"; - /** The threshold at which we start restricting non-EJ jobs. */ - private static final int REGULAR_JOB_THRESHOLD = PowerManager.THERMAL_STATUS_SEVERE; + /** The threshold at which we start restricting low and min priority jobs. */ + private static final int LOW_PRIORITY_THRESHOLD = PowerManager.THERMAL_STATUS_LIGHT; + /** The threshold at which we start restricting higher priority jobs. */ + private static final int HIGHER_PRIORITY_THRESHOLD = PowerManager.THERMAL_STATUS_MODERATE; /** The lowest threshold at which we start restricting jobs. */ - private static final int LOWER_THRESHOLD = REGULAR_JOB_THRESHOLD; + private static final int LOWER_THRESHOLD = LOW_PRIORITY_THRESHOLD; /** The threshold at which we start restricting ALL jobs. */ - private static final int UPPER_THRESHOLD = PowerManager.THERMAL_STATUS_CRITICAL; + private static final int UPPER_THRESHOLD = PowerManager.THERMAL_STATUS_SEVERE; private volatile int mThermalStatus = PowerManager.THERMAL_STATUS_NONE; - private PowerManager mPowerManager; - public ThermalStatusRestriction(JobSchedulerService service) { super(service, JobParameters.STOP_REASON_DEVICE_STATE, JobParameters.INTERNAL_STOP_REASON_DEVICE_THERMAL); @@ -45,9 +47,10 @@ public class ThermalStatusRestriction extends JobRestriction { @Override public void onSystemServicesReady() { - mPowerManager = mService.getContext().getSystemService(PowerManager.class); + final PowerManager powerManager = + mService.getTestableContext().getSystemService(PowerManager.class); // Use MainExecutor - mPowerManager.addThermalStatusListener(new OnThermalStatusChangedListener() { + powerManager.addThermalStatusListener(new OnThermalStatusChangedListener() { @Override public void onThermalStatusChanged(int status) { // This is called on the main thread. Do not do any slow operations in it. @@ -83,12 +86,28 @@ public class ThermalStatusRestriction extends JobRestriction { if (mThermalStatus >= UPPER_THRESHOLD) { return true; } - if (mThermalStatus >= REGULAR_JOB_THRESHOLD) { - return !job.shouldTreatAsExpeditedJob(); + final int priority = job.getEffectivePriority(); + if (mThermalStatus >= HIGHER_PRIORITY_THRESHOLD) { + // For moderate throttling, only let expedited jobs and high priority regular jobs that + // are already running run. + return !job.shouldTreatAsExpeditedJob() + && !(priority == JobInfo.PRIORITY_HIGH + && mService.isCurrentlyRunningLocked(job)); + } + if (mThermalStatus >= LOW_PRIORITY_THRESHOLD) { + // For light throttling, throttle all min priority jobs and all low priority jobs that + // aren't already running. + return (priority == JobInfo.PRIORITY_LOW && !mService.isCurrentlyRunningLocked(job)) + || priority == JobInfo.PRIORITY_MIN; } return false; } + @VisibleForTesting + int getThermalStatus() { + return mThermalStatus; + } + @Override public void dumpConstants(IndentingPrintWriter pw) { pw.print("Thermal status: "); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java b/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java new file mode 100644 index 0000000000000..ab08573356ffa --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.job.restrictions; + +import static android.os.PowerManager.THERMAL_STATUS_CRITICAL; +import static android.os.PowerManager.THERMAL_STATUS_EMERGENCY; +import static android.os.PowerManager.THERMAL_STATUS_LIGHT; +import static android.os.PowerManager.THERMAL_STATUS_MODERATE; +import static android.os.PowerManager.THERMAL_STATUS_NONE; +import static android.os.PowerManager.THERMAL_STATUS_SEVERE; +import static android.os.PowerManager.THERMAL_STATUS_SHUTDOWN; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.app.job.JobInfo; +import android.content.ComponentName; +import android.content.Context; +import android.os.PowerManager; +import android.provider.DeviceConfig; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.controllers.JobStatus; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +@RunWith(AndroidJUnit4.class) +public class ThermalStatusRestrictionTest { + private static final int CALLING_UID = 1000; + private static final String SOURCE_PACKAGE = "com.android.frameworks.mockingservicestests"; + private static final int SOURCE_USER_ID = 0; + + private ThermalStatusRestriction mThermalStatusRestriction; + private PowerManager.OnThermalStatusChangedListener mStatusChangedListener; + + private MockitoSession mMockingSession; + @Mock + private Context mContext; + @Mock + private JobSchedulerService mJobSchedulerService; + + @Before + public void setUp() { + mMockingSession = mockitoSession() + .initMocks(this) + .strictness(Strictness.LENIENT) + .spyStatic(DeviceConfig.class) + .mockStatic(LocalServices.class) + .startMocking(); + + when(mJobSchedulerService.getTestableContext()).thenReturn(mContext); + when(mJobSchedulerService.getLock()).thenReturn(mJobSchedulerService); + + PowerManager powerManager = mock(PowerManager.class); + when(mContext.getSystemService(PowerManager.class)).thenReturn(powerManager); + // Initialize real objects. + // Capture the listeners. + ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(PowerManager.OnThermalStatusChangedListener.class); + mThermalStatusRestriction = new ThermalStatusRestriction(mJobSchedulerService); + mThermalStatusRestriction.onSystemServicesReady(); + + verify(powerManager).addThermalStatusListener(listenerCaptor.capture()); + mStatusChangedListener = listenerCaptor.getValue(); + } + + @After + public void tearDown() { + if (mMockingSession != null) { + mMockingSession.finishMocking(); + } + } + + @Test + public void testStatusChanges() { + InOrder inOrder = inOrder(mJobSchedulerService); + + mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_NONE); + inOrder.verify(mJobSchedulerService, never()).onControllerStateChanged(any()); + assertEquals(THERMAL_STATUS_NONE, mThermalStatusRestriction.getThermalStatus()); + + // Moving within LOW and UPPER thresholds + mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_LIGHT); + inOrder.verify(mJobSchedulerService).onControllerStateChanged(any()); + assertEquals(THERMAL_STATUS_LIGHT, mThermalStatusRestriction.getThermalStatus()); + + mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_MODERATE); + inOrder.verify(mJobSchedulerService).onControllerStateChanged(any()); + assertEquals(THERMAL_STATUS_MODERATE, mThermalStatusRestriction.getThermalStatus()); + + mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_SEVERE); + inOrder.verify(mJobSchedulerService).onControllerStateChanged(any()); + assertEquals(THERMAL_STATUS_SEVERE, mThermalStatusRestriction.getThermalStatus()); + + // Changing outside of range + mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_CRITICAL); + inOrder.verify(mJobSchedulerService, never()).onControllerStateChanged(any()); + assertEquals(THERMAL_STATUS_CRITICAL, mThermalStatusRestriction.getThermalStatus()); + + mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_EMERGENCY); + inOrder.verify(mJobSchedulerService, never()).onControllerStateChanged(any()); + assertEquals(THERMAL_STATUS_EMERGENCY, mThermalStatusRestriction.getThermalStatus()); + + mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_SHUTDOWN); + inOrder.verify(mJobSchedulerService, never()).onControllerStateChanged(any()); + assertEquals(THERMAL_STATUS_SHUTDOWN, mThermalStatusRestriction.getThermalStatus()); + + // Cross values we care about + mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_NONE); + inOrder.verify(mJobSchedulerService).onControllerStateChanged(any()); + assertEquals(THERMAL_STATUS_NONE, mThermalStatusRestriction.getThermalStatus()); + + mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_EMERGENCY); + inOrder.verify(mJobSchedulerService).onControllerStateChanged(any()); + assertEquals(THERMAL_STATUS_EMERGENCY, mThermalStatusRestriction.getThermalStatus()); + } + + @Test + public void testIsJobRestricted() { + mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_NONE); + + final JobStatus jobMinPriority = createJobStatus("testIsJobRestricted", + createJobBuilder(1).setPriority(JobInfo.PRIORITY_MIN).build()); + final JobStatus jobLowPriority = createJobStatus("testIsJobRestricted", + createJobBuilder(2).setPriority(JobInfo.PRIORITY_LOW).build()); + final JobStatus jobLowPriorityRunning = createJobStatus("testIsJobRestricted", + createJobBuilder(3).setPriority(JobInfo.PRIORITY_LOW).build()); + final JobStatus jobDefaultPriority = createJobStatus("testIsJobRestricted", + createJobBuilder(4).setPriority(JobInfo.PRIORITY_DEFAULT).build()); + final JobStatus jobHighPriority = createJobStatus("testIsJobRestricted", + createJobBuilder(5).setPriority(JobInfo.PRIORITY_HIGH).build()); + final JobStatus jobHighPriorityRunning = createJobStatus("testIsJobRestricted", + createJobBuilder(6).setPriority(JobInfo.PRIORITY_HIGH).build()); + final JobStatus ejDowngraded = createJobStatus("testIsJobRestricted", + createJobBuilder(7).setExpedited(true).build()); + final JobStatus ej = spy(createJobStatus("testIsJobRestricted", + createJobBuilder(8).setExpedited(true).build())); + when(ej.shouldTreatAsExpeditedJob()).thenReturn(true); + when(mJobSchedulerService.isCurrentlyRunningLocked(jobLowPriorityRunning)).thenReturn(true); + when(mJobSchedulerService.isCurrentlyRunningLocked(jobHighPriorityRunning)) + .thenReturn(true); + + assertFalse(mThermalStatusRestriction.isJobRestricted(jobMinPriority)); + assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriority)); + assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning)); + assertFalse(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority)); + assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriority)); + assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning)); + assertFalse(mThermalStatusRestriction.isJobRestricted(ej)); + assertFalse(mThermalStatusRestriction.isJobRestricted(ejDowngraded)); + + mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_LIGHT); + + assertTrue(mThermalStatusRestriction.isJobRestricted(jobMinPriority)); + assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriority)); + assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning)); + assertFalse(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority)); + assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriority)); + assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning)); + assertFalse(mThermalStatusRestriction.isJobRestricted(ejDowngraded)); + assertFalse(mThermalStatusRestriction.isJobRestricted(ej)); + + mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_MODERATE); + + assertTrue(mThermalStatusRestriction.isJobRestricted(jobMinPriority)); + assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriority)); + assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning)); + assertTrue(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority)); + assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriority)); + assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning)); + assertTrue(mThermalStatusRestriction.isJobRestricted(ejDowngraded)); + assertFalse(mThermalStatusRestriction.isJobRestricted(ej)); + + mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_SEVERE); + + assertTrue(mThermalStatusRestriction.isJobRestricted(jobMinPriority)); + assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriority)); + assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning)); + assertTrue(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority)); + assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriority)); + assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning)); + assertTrue(mThermalStatusRestriction.isJobRestricted(ejDowngraded)); + assertTrue(mThermalStatusRestriction.isJobRestricted(ej)); + + mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_CRITICAL); + + assertTrue(mThermalStatusRestriction.isJobRestricted(jobMinPriority)); + assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriority)); + assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning)); + assertTrue(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority)); + assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriority)); + assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning)); + assertTrue(mThermalStatusRestriction.isJobRestricted(ejDowngraded)); + assertTrue(mThermalStatusRestriction.isJobRestricted(ej)); + } + + private JobInfo.Builder createJobBuilder(int jobId) { + return new JobInfo.Builder(jobId, + new ComponentName(mContext, "ThermalStatusRestrictionTest")); + } + + private JobStatus createJobStatus(String testTag, JobInfo jobInfo) { + return JobStatus.createFromJobInfo( + jobInfo, CALLING_UID, SOURCE_PACKAGE, SOURCE_USER_ID, testTag); + } +}