content: /*
   * Copyright (C) 2019 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;
  
  import android.app.job.JobInfo;
  import android.app.job.JobParameters;
  import android.app.job.JobScheduler;
  import android.app.job.JobService;
  import android.content.ComponentName;
  import android.content.Context;
  import android.os.FileUtils;
  import android.os.SystemProperties;
  import android.util.Slog;
  
  import java.io.File;
  import java.io.IOException;
  import java.util.concurrent.TimeUnit;
  
  /**
   * Schedules jobs for triggering zram writeback.
   */
  public final class ZramWriteback extends JobService {
      private static final String TAG = "ZramWriteback";
      private static final boolean DEBUG = false;
  
      private static final ComponentName sZramWriteback =
              new ComponentName("android", ZramWriteback.class.getName());
  
      private static final int MARK_IDLE_JOB_ID = 811;
      private static final int WRITEBACK_IDLE_JOB_ID = 812;
  
      private static final int MAX_ZRAM_DEVICES = 256;
      private static int sZramDeviceId = 0;
  
      private static final String IDLE_SYS = "/sys/block/zram%d/idle";
      private static final String IDLE_SYS_ALL_PAGES = "all";
  
      private static final String WB_SYS = "/sys/block/zram%d/writeback";
      private static final String WB_SYS_IDLE_PAGES = "idle";
  
      private static final String WB_STATS_SYS = "/sys/block/zram%d/bd_stat";
      private static final int WB_STATS_MAX_FILE_SIZE = 128;
  
      private static final String BDEV_SYS = "/sys/block/zram%d/backing_dev";
  
      private static final String MARK_IDLE_DELAY_PROP = "ro.zram.mark_idle_delay_mins";
      private static final String FIRST_WB_DELAY_PROP = "ro.zram.first_wb_delay_mins";
      private static final String PERIODIC_WB_DELAY_PROP = "ro.zram.periodic_wb_delay_hours";
  
      private void markPagesAsIdle() {
          String idlePath = String.format(IDLE_SYS, sZramDeviceId);
          try {
              FileUtils.stringToFile(new File(idlePath), IDLE_SYS_ALL_PAGES);
          } catch (IOException e) {
              Slog.e(TAG, "Failed to write to " + idlePath);
          }
      }
  
      private void flushIdlePages() {
          if (DEBUG) Slog.d(TAG, "Start writing back idle pages to disk");
          String wbPath = String.format(WB_SYS, sZramDeviceId);
          try {
              FileUtils.stringToFile(new File(wbPath), WB_SYS_IDLE_PAGES);
          } catch (IOException e) {
              Slog.e(TAG, "Failed to write to " + wbPath);
          }
          if (DEBUG) Slog.d(TAG, "Finished writeback back idle pages");
      }
  
      private int getWrittenPageCount() {
          String wbStatsPath = String.format(WB_STATS_SYS, sZramDeviceId);
          try {
              String wbStats = FileUtils
                      .readTextFile(new File(wbStatsPath), WB_STATS_MAX_FILE_SIZE, "");
              return Integer.parseInt(wbStats.trim().split("\\s+")[2], 10);
          } catch (IOException e) {
              Slog.e(TAG, "Failed to read writeback stats from " + wbStatsPath);
          }
  
          return -1;
      }
  
      private void markAndFlushPages() {
          int pageCount = getWrittenPageCount();
  
          flushIdlePages();
          markPagesAsIdle();
  
          if (pageCount != -1) {
              Slog.i(TAG, "Total pages written to disk is " + (getWrittenPageCount() - pageCount));
          }
      }
  
      private static boolean isWritebackEnabled() {
          try {
              String backingDev = FileUtils
                      .readTextFile(new File(String.format(BDEV_SYS, sZramDeviceId)), 128, "");
              if (!"none".equals(backingDev.trim())) {
                  return true;
              } else {
                  Slog.w(TAG, "Writeback device is not set");
              }
          } catch (IOException e) {
              Slog.w(TAG, "Writeback is not enabled on zram");
          }
          return false;
      }
  
      private static void schedNextWriteback(Context context) {
          int nextWbDelay = SystemProperties.getInt(PERIODIC_WB_DELAY_PROP, 24);
          JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
  
          js.schedule(new JobInfo.Builder(WRITEBACK_IDLE_JOB_ID, sZramWriteback)
                          .setMinimumLatency(TimeUnit.HOURS.toMillis(nextWbDelay))
                          .setRequiresDeviceIdle(true)
                          .build());
      }
  
      @Override
      public boolean onStartJob(JobParameters params) {
  
          if (!isWritebackEnabled()) {
              jobFinished(params, false);
              return false;
          }
  
          if (params.getJobId() == MARK_IDLE_JOB_ID) {
              markPagesAsIdle();
              jobFinished(params, false);
              return false;
          } else {
              new Thread("ZramWriteback_WritebackIdlePages") {
                  @Override
                  public void run() {
                      markAndFlushPages();
                      schedNextWriteback(ZramWriteback.this);
                      jobFinished(params, false);
                  }
              }.start();
          }
          return true;
      }
  
      @Override
      public boolean onStopJob(JobParameters params) {
          // The thread that triggers the writeback is non-interruptible
          return false;
      }
  
      /**
       * Schedule the zram writeback job to trigger a writeback when idle
       */
      public static void scheduleZramWriteback(Context context) {
          int markIdleDelay = SystemProperties.getInt(MARK_IDLE_DELAY_PROP, 20);
          int firstWbDelay = SystemProperties.getInt(FIRST_WB_DELAY_PROP, 180);
  
          JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
  
          // Schedule a one time job to mark pages as idle. These pages will be written
          // back at later point if they remain untouched.
          js.schedule(new JobInfo.Builder(MARK_IDLE_JOB_ID, sZramWriteback)
                          .setMinimumLatency(TimeUnit.MINUTES.toMillis(markIdleDelay))
                          .setOverrideDeadline(TimeUnit.MINUTES.toMillis(markIdleDelay))
                          .build());
  
          // Schedule a one time job to flush idle pages to disk.
          // After the initial writeback, subsequent writebacks are done at interval set
          // by ro.zram.periodic_wb_delay_hours.
          js.schedule(new JobInfo.Builder(WRITEBACK_IDLE_JOB_ID, sZramWriteback)
                          .setMinimumLatency(TimeUnit.MINUTES.toMillis(firstWbDelay))
                          .setRequiresDeviceIdle(true)
                          .build());
      }
  }
  