Check isZramMaintenanceSupported on ZramMaintenance
Sending doZramMaintenanceAsync() request to mmd on the devices which
does not support zram writeback and zram recompression is useless.
Check the zram status before the first doZramMaintenanceAsync() and stop
the job scheduler loop of ZramMaintenance if the device does not support
zram maintenance.
Bug: 375432472
Test: atest FrameworksServicesTests:ZramMaintenanceTest
Change-Id: Idc76bae87f6f974a6c276cd030135b050d444dc1
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 19676eb..6045f63 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -946,7 +946,6 @@
refreshZramSettings();
if (mmdEnabled()) {
- // TODO: b/375432472 - Start zram maintenance only when zram is enabled.
ZramMaintenance.startZramMaintenance(mContext);
} else {
// Schedule zram writeback unless zram is disabled by persist.sys.zram_enabled
diff --git a/services/core/java/com/android/server/ZramMaintenance.java b/services/core/java/com/android/server/ZramMaintenance.java
index f1e6e85..ca1eba9 100644
--- a/services/core/java/com/android/server/ZramMaintenance.java
+++ b/services/core/java/com/android/server/ZramMaintenance.java
@@ -24,11 +24,14 @@
import android.content.Context;
import android.os.IBinder;
import android.os.IMmd;
+import android.os.PersistableBundle;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemProperties;
import android.util.Slog;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.time.Duration;
/**
@@ -46,9 +49,12 @@
private static final String TAG = ZramMaintenance.class.getName();
// Job id must be unique across all clients of the same uid. ZramMaintenance uses the bug number
// as the job id.
- private static final int JOB_ID = 375432472;
+ @VisibleForTesting
+ public static final int JOB_ID = 375432472;
private static final ComponentName sZramMaintenance =
new ComponentName("android", ZramMaintenance.class.getName());
+ @VisibleForTesting
+ public static final String KEY_CHECK_STATUS = "check_status";
private static final String FIRST_DELAY_SECONDS_PROP =
"mm.zram.maintenance.first_delay_seconds";
@@ -69,20 +75,18 @@
@Override
public boolean onStartJob(JobParameters params) {
- IBinder binder = ServiceManager.getService("mmd");
- if (binder != null) {
- IMmd mmd = IMmd.Stub.asInterface(binder);
- try {
- mmd.doZramMaintenanceAsync();
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to doZramMaintenance", e);
+ new Thread("ZramMaintenance") {
+ @Override
+ public void run() {
+ try {
+ IBinder binder = ServiceManager.getService("mmd");
+ IMmd mmd = IMmd.Stub.asInterface(binder);
+ startJob(ZramMaintenance.this, params, mmd);
+ } finally {
+ jobFinished(params, false);
+ }
}
- } else {
- Slog.w(TAG, "binder not found");
- }
- Duration delay = Duration.ofSeconds(SystemProperties.getLong(PERIODIC_DELAY_SECONDS_PROP,
- DEFAULT_PERIODIC_DELAY_SECONDS));
- scheduleZramMaintenance(this, delay);
+ }.start();
return true;
}
@@ -92,18 +96,55 @@
}
/**
+ * This is public to test ZramMaintenance logic.
+ *
+ * <p>
+ * We need to pass mmd as parameter because we can't mock "IMmd.Stub.asInterface".
+ *
+ * <p>
+ * Since IMmd.isZramMaintenanceSupported() is blocking call, this method should be executed on
+ * a worker thread.
+ */
+ @VisibleForTesting
+ public static void startJob(Context context, JobParameters params, IMmd mmd) {
+ boolean checkStatus = params.getExtras().getBoolean(KEY_CHECK_STATUS);
+ if (mmd != null) {
+ try {
+ if (checkStatus && !mmd.isZramMaintenanceSupported()) {
+ Slog.i(TAG, "zram maintenance is not supported");
+ return;
+ }
+ // Status check is required before the first doZramMaintenanceAsync() call once.
+ checkStatus = false;
+
+ mmd.doZramMaintenanceAsync();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to binder call to mmd", e);
+ }
+ } else {
+ Slog.w(TAG, "binder not found");
+ }
+ Duration delay = Duration.ofSeconds(SystemProperties.getLong(PERIODIC_DELAY_SECONDS_PROP,
+ DEFAULT_PERIODIC_DELAY_SECONDS));
+ scheduleZramMaintenance(context, delay, checkStatus);
+ }
+
+ /**
* Starts periodical zram maintenance.
*/
public static void startZramMaintenance(Context context) {
Duration delay = Duration.ofSeconds(
SystemProperties.getLong(FIRST_DELAY_SECONDS_PROP, DEFAULT_FIRST_DELAY_SECONDS));
- scheduleZramMaintenance(context, delay);
+ scheduleZramMaintenance(context, delay, true);
}
- private static void scheduleZramMaintenance(Context context, Duration delay) {
+ private static void scheduleZramMaintenance(Context context, Duration delay,
+ boolean checkStatus) {
JobScheduler js = context.getSystemService(JobScheduler.class);
if (js != null) {
+ final PersistableBundle bundle = new PersistableBundle();
+ bundle.putBoolean(KEY_CHECK_STATUS, checkStatus);
js.schedule(new JobInfo.Builder(JOB_ID, sZramMaintenance)
.setMinimumLatency(delay.toMillis())
.setRequiresDeviceIdle(
@@ -112,6 +153,7 @@
.setRequiresBatteryNotLow(
SystemProperties.getBoolean(REQUIRE_BATTERY_NOT_LOW_PROP,
DEFAULT_REQUIRE_BATTERY_NOT_LOW))
+ .setExtras(bundle)
.build());
}
}
diff --git a/services/tests/servicestests/src/com/android/server/zram/ZramMaintenanceTest.kt b/services/tests/servicestests/src/com/android/server/zram/ZramMaintenanceTest.kt
new file mode 100644
index 0000000..5448a05
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/zram/ZramMaintenanceTest.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2024 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.zram
+
+import android.app.job.JobInfo
+import android.app.job.JobParameters
+import android.app.job.JobScheduler
+import android.os.IMmd
+import android.os.PersistableBundle
+import android.os.RemoteException
+import android.testing.TestableContext
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+
+import com.android.server.ZramMaintenance
+import com.google.common.truth.Truth.assertThat
+
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+private fun generateJobParameters(jobId: Int, extras: PersistableBundle): JobParameters {
+ return JobParameters(
+ null, "", jobId, extras, null, null, 0, false, false, false, null, null, null
+ )
+}
+
+@SmallTest
+@RunWith(JUnit4::class)
+class ZramMaintenanceTest {
+ private val context = TestableContext(InstrumentationRegistry.getInstrumentation().context)
+
+ @Captor
+ private lateinit var jobInfoCaptor: ArgumentCaptor<JobInfo>
+
+ @Mock
+ private lateinit var mockJobScheduler: JobScheduler
+
+ @Mock
+ private lateinit var mockMmd: IMmd
+
+ @Before
+ @Throws(RemoteException::class)
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ context.addMockSystemService(JobScheduler::class.java, mockJobScheduler)
+ }
+
+ @Test
+ fun startZramMaintenance() {
+ ZramMaintenance.startZramMaintenance(context)
+
+ verify(mockJobScheduler, times(1)).schedule(jobInfoCaptor.capture())
+ val job = jobInfoCaptor.value
+ assertThat(job.id).isEqualTo(ZramMaintenance.JOB_ID)
+ assertThat(job.extras.getBoolean(ZramMaintenance.KEY_CHECK_STATUS)).isTrue()
+ }
+
+ @Test
+ fun startJobForFirstTime() {
+ val extras = PersistableBundle()
+ extras.putBoolean(ZramMaintenance.KEY_CHECK_STATUS, true)
+ val params = generateJobParameters(
+ ZramMaintenance.JOB_ID,
+ extras,
+ )
+ `when`(mockMmd.isZramMaintenanceSupported()).thenReturn(true)
+
+ ZramMaintenance.startJob(context, params, mockMmd)
+
+ verify(mockMmd, times(1)).isZramMaintenanceSupported()
+ verify(mockMmd, times(1)).doZramMaintenanceAsync()
+ verify(mockJobScheduler, times(1)).schedule(jobInfoCaptor.capture())
+ val nextJob = jobInfoCaptor.value
+ assertThat(nextJob.id).isEqualTo(ZramMaintenance.JOB_ID)
+ assertThat(nextJob.extras.getBoolean(ZramMaintenance.KEY_CHECK_STATUS)).isFalse()
+ }
+
+ @Test
+ fun startJobWithoutCheckStatus() {
+ val extras = PersistableBundle()
+ extras.putBoolean(ZramMaintenance.KEY_CHECK_STATUS, false)
+ val params = generateJobParameters(
+ ZramMaintenance.JOB_ID,
+ extras,
+ )
+
+ ZramMaintenance.startJob(context, params, mockMmd)
+
+ verify(mockMmd, never()).isZramMaintenanceSupported()
+ verify(mockMmd, times(1)).doZramMaintenanceAsync()
+ verify(mockJobScheduler, times(1)).schedule(jobInfoCaptor.capture())
+ val nextJob = jobInfoCaptor.value
+ assertThat(nextJob.id).isEqualTo(ZramMaintenance.JOB_ID)
+ assertThat(nextJob.extras.getBoolean(ZramMaintenance.KEY_CHECK_STATUS)).isFalse()
+ }
+
+ @Test
+ fun startJobZramIsDisabled() {
+ val extras = PersistableBundle()
+ extras.putBoolean(ZramMaintenance.KEY_CHECK_STATUS, true)
+ val params = generateJobParameters(
+ ZramMaintenance.JOB_ID,
+ extras,
+ )
+ `when`(mockMmd.isZramMaintenanceSupported()).thenReturn(false)
+
+ ZramMaintenance.startJob(context, params, mockMmd)
+
+ verify(mockMmd, times(1)).isZramMaintenanceSupported()
+ verify(mockMmd, never()).doZramMaintenanceAsync()
+ verify(mockJobScheduler, never()).schedule(any())
+ }
+
+ @Test
+ fun startJobMmdIsNotReadyYet() {
+ val extras = PersistableBundle()
+ extras.putBoolean(ZramMaintenance.KEY_CHECK_STATUS, true)
+ val params = generateJobParameters(
+ ZramMaintenance.JOB_ID,
+ extras,
+ )
+
+ ZramMaintenance.startJob(context, params, null)
+
+ verify(mockJobScheduler, times(1)).schedule(jobInfoCaptor.capture())
+ val nextJob = jobInfoCaptor.value
+ assertThat(nextJob.id).isEqualTo(ZramMaintenance.JOB_ID)
+ assertThat(nextJob.extras.getBoolean(ZramMaintenance.KEY_CHECK_STATUS)).isTrue()
+ }
+}
\ No newline at end of file