Merge "Add app compaction throttling."
This commit is contained in:
committed by
Android (Google) Code Review
commit
a6a688fefd
@@ -49,6 +49,7 @@ class ActivityManagerDebugConfig {
|
||||
static final boolean DEBUG_BROADCAST_BACKGROUND = DEBUG_BROADCAST || false;
|
||||
static final boolean DEBUG_BROADCAST_LIGHT = DEBUG_BROADCAST || false;
|
||||
static final boolean DEBUG_BROADCAST_DEFERRAL = DEBUG_BROADCAST || false;
|
||||
static final boolean DEBUG_COMPACTION = DEBUG_ALL || false;
|
||||
static final boolean DEBUG_LRU = DEBUG_ALL || false;
|
||||
static final boolean DEBUG_MU = DEBUG_ALL || false;
|
||||
static final boolean DEBUG_NETWORK = DEBUG_ALL || false;
|
||||
|
||||
@@ -18,6 +18,9 @@ package com.android.server.am;
|
||||
|
||||
import static android.os.Process.THREAD_PRIORITY_FOREGROUND;
|
||||
|
||||
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_COMPACTION;
|
||||
import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.ActivityThread;
|
||||
import android.os.Debug;
|
||||
@@ -30,6 +33,7 @@ import android.provider.DeviceConfig;
|
||||
import android.provider.DeviceConfig.OnPropertyChangedListener;
|
||||
import android.text.TextUtils;
|
||||
import android.util.EventLog;
|
||||
import android.util.Slog;
|
||||
import android.util.StatsLog;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
@@ -39,7 +43,12 @@ import com.android.server.ServiceThread;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
public final class AppCompactor {
|
||||
|
||||
@@ -55,6 +64,12 @@ public final class AppCompactor {
|
||||
@VisibleForTesting static final String KEY_COMPACT_THROTTLE_6 = "compact_throttle_6";
|
||||
@VisibleForTesting static final String KEY_COMPACT_STATSD_SAMPLE_RATE =
|
||||
"compact_statsd_sample_rate";
|
||||
@VisibleForTesting static final String KEY_COMPACT_FULL_RSS_THROTTLE_KB =
|
||||
"compact_full_rss_throttle_kb";
|
||||
@VisibleForTesting static final String KEY_COMPACT_FULL_DELTA_RSS_THROTTLE_KB =
|
||||
"compact_full_delta_rss_throttle_kb";
|
||||
@VisibleForTesting static final String KEY_COMPACT_PROC_STATE_THROTTLE =
|
||||
"compact_proc_state_throttle";
|
||||
|
||||
// Phenotype sends int configurations and we map them to the strings we'll use on device,
|
||||
// preventing a weird string value entering the kernel.
|
||||
@@ -79,6 +94,11 @@ public final class AppCompactor {
|
||||
@VisibleForTesting static final long DEFAULT_COMPACT_THROTTLE_6 = 10 * 60 * 1000;
|
||||
// The sampling rate to push app compaction events into statsd for upload.
|
||||
@VisibleForTesting static final float DEFAULT_STATSD_SAMPLE_RATE = 0.1f;
|
||||
@VisibleForTesting static final long DEFAULT_COMPACT_FULL_RSS_THROTTLE_KB = 12_000L;
|
||||
@VisibleForTesting static final long DEFAULT_COMPACT_FULL_DELTA_RSS_THROTTLE_KB = 8_000L;
|
||||
// Format of this string should be a comma separated list of integers.
|
||||
@VisibleForTesting static final String DEFAULT_COMPACT_PROC_STATE_THROTTLE =
|
||||
String.valueOf(ActivityManager.PROCESS_STATE_RECEIVER);
|
||||
|
||||
@VisibleForTesting
|
||||
interface PropertyChangedCallbackForTest {
|
||||
@@ -123,6 +143,12 @@ public final class AppCompactor {
|
||||
updateCompactionThrottles();
|
||||
} else if (KEY_COMPACT_STATSD_SAMPLE_RATE.equals(name)) {
|
||||
updateStatsdSampleRate();
|
||||
} else if (KEY_COMPACT_FULL_RSS_THROTTLE_KB.equals(name)) {
|
||||
updateFullRssThrottle();
|
||||
} else if (KEY_COMPACT_FULL_DELTA_RSS_THROTTLE_KB.equals(name)) {
|
||||
updateFullDeltaRssThrottle();
|
||||
} else if (KEY_COMPACT_PROC_STATE_THROTTLE.equals(name)) {
|
||||
updateProcStateThrottle();
|
||||
}
|
||||
}
|
||||
if (mTestCallback != null) {
|
||||
@@ -154,18 +180,42 @@ public final class AppCompactor {
|
||||
@VisibleForTesting volatile long mCompactThrottlePersistent = DEFAULT_COMPACT_THROTTLE_6;
|
||||
@GuardedBy("mPhenotypeFlagLock")
|
||||
private volatile boolean mUseCompaction = DEFAULT_USE_COMPACTION;
|
||||
|
||||
private final Random mRandom = new Random();
|
||||
@GuardedBy("mPhenotypeFlagLock")
|
||||
@VisibleForTesting volatile float mStatsdSampleRate = DEFAULT_STATSD_SAMPLE_RATE;
|
||||
@GuardedBy("mPhenotypeFlagLock")
|
||||
@VisibleForTesting volatile long mFullAnonRssThrottleKb =
|
||||
DEFAULT_COMPACT_FULL_RSS_THROTTLE_KB;
|
||||
@GuardedBy("mPhenoypeFlagLock")
|
||||
@VisibleForTesting volatile long mFullDeltaRssThrottleKb =
|
||||
DEFAULT_COMPACT_FULL_DELTA_RSS_THROTTLE_KB;
|
||||
@GuardedBy("mPhenoypeFlagLock")
|
||||
@VisibleForTesting final Set<Integer> mProcStateThrottle;
|
||||
|
||||
// Handler on which compaction runs.
|
||||
private Handler mCompactionHandler;
|
||||
|
||||
// Maps process ID to last compaction statistics for processes that we've fully compacted. Used
|
||||
// when evaluating throttles that we only consider for "full" compaction, so we don't store
|
||||
// data for "some" compactions.
|
||||
private Map<Integer, LastCompactionStats> mLastCompactionStats =
|
||||
new LinkedHashMap<Integer, LastCompactionStats>() {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry eldest) {
|
||||
return size() > 100;
|
||||
}
|
||||
};
|
||||
|
||||
private int mSomeCompactionCount;
|
||||
private int mFullCompactionCount;
|
||||
private int mPersistentCompactionCount;
|
||||
private int mBfgsCompactionCount;
|
||||
|
||||
public AppCompactor(ActivityManagerService am) {
|
||||
mAm = am;
|
||||
mCompactionThread = new ServiceThread("CompactionThread",
|
||||
THREAD_PRIORITY_FOREGROUND, true);
|
||||
mProcStateThrottle = new HashSet<>();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@@ -186,10 +236,12 @@ public final class AppCompactor {
|
||||
updateCompactionActions();
|
||||
updateCompactionThrottles();
|
||||
updateStatsdSampleRate();
|
||||
updateFullRssThrottle();
|
||||
updateFullDeltaRssThrottle();
|
||||
updateProcStateThrottle();
|
||||
}
|
||||
Process.setThreadGroupAndCpuset(mCompactionThread.getThreadId(),
|
||||
Process.THREAD_GROUP_SYSTEM);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,7 +264,33 @@ public final class AppCompactor {
|
||||
pw.println(" " + KEY_COMPACT_THROTTLE_2 + "=" + mCompactThrottleSomeFull);
|
||||
pw.println(" " + KEY_COMPACT_THROTTLE_3 + "=" + mCompactThrottleFullSome);
|
||||
pw.println(" " + KEY_COMPACT_THROTTLE_4 + "=" + mCompactThrottleFullFull);
|
||||
pw.println(" " + KEY_COMPACT_THROTTLE_5 + "=" + mCompactThrottleBFGS);
|
||||
pw.println(" " + KEY_COMPACT_THROTTLE_6 + "=" + mCompactThrottlePersistent);
|
||||
pw.println(" " + KEY_COMPACT_STATSD_SAMPLE_RATE + "=" + mStatsdSampleRate);
|
||||
pw.println(" " + KEY_COMPACT_FULL_RSS_THROTTLE_KB + "="
|
||||
+ mFullAnonRssThrottleKb);
|
||||
pw.println(" " + KEY_COMPACT_FULL_DELTA_RSS_THROTTLE_KB + "="
|
||||
+ mFullDeltaRssThrottleKb);
|
||||
pw.println(" " + KEY_COMPACT_PROC_STATE_THROTTLE + "="
|
||||
+ Arrays.toString(mProcStateThrottle.toArray(new Integer[0])));
|
||||
|
||||
pw.println(" " + mSomeCompactionCount + " some, " + mFullCompactionCount
|
||||
+ " full, " + mPersistentCompactionCount + " persistent, "
|
||||
+ mBfgsCompactionCount + " BFGS compactions.");
|
||||
|
||||
if (mLastCompactionStats != null) {
|
||||
pw.println(" Tracking last compaction stats for " + mLastCompactionStats.size()
|
||||
+ " processes.");
|
||||
if (DEBUG_COMPACTION) {
|
||||
for (Map.Entry<Integer, LastCompactionStats> entry
|
||||
: mLastCompactionStats.entrySet()) {
|
||||
int pid = entry.getKey();
|
||||
LastCompactionStats stats = entry.getValue();
|
||||
pw.println(" " + pid + ": "
|
||||
+ Arrays.toString(stats.getRssAfterCompaction()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,7 +355,7 @@ public final class AppCompactor {
|
||||
|
||||
/**
|
||||
* Reads the flag value from DeviceConfig to determine whether app compaction
|
||||
* should be enabled, and starts/stops the compaction thread as needed.
|
||||
* should be enabled, and starts the compaction thread if needed.
|
||||
*/
|
||||
@GuardedBy("mPhenotypeFlagLock")
|
||||
private void updateUseCompaction() {
|
||||
@@ -360,6 +438,58 @@ public final class AppCompactor {
|
||||
mStatsdSampleRate = Math.min(1.0f, Math.max(0.0f, mStatsdSampleRate));
|
||||
}
|
||||
|
||||
@GuardedBy("mPhenotypeFlagLock")
|
||||
private void updateFullRssThrottle() {
|
||||
mFullAnonRssThrottleKb = DeviceConfig.getLong(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
KEY_COMPACT_FULL_RSS_THROTTLE_KB, DEFAULT_COMPACT_FULL_RSS_THROTTLE_KB);
|
||||
|
||||
// Don't allow negative values. 0 means don't apply the throttle.
|
||||
if (mFullAnonRssThrottleKb < 0) {
|
||||
mFullAnonRssThrottleKb = DEFAULT_COMPACT_FULL_RSS_THROTTLE_KB;
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("mPhenotypeFlagLock")
|
||||
private void updateFullDeltaRssThrottle() {
|
||||
mFullDeltaRssThrottleKb = DeviceConfig.getLong(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
KEY_COMPACT_FULL_DELTA_RSS_THROTTLE_KB, DEFAULT_COMPACT_FULL_DELTA_RSS_THROTTLE_KB);
|
||||
|
||||
if (mFullDeltaRssThrottleKb < 0) {
|
||||
mFullDeltaRssThrottleKb = DEFAULT_COMPACT_FULL_DELTA_RSS_THROTTLE_KB;
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("mPhenotypeFlagLock")
|
||||
private void updateProcStateThrottle() {
|
||||
String procStateThrottleString = DeviceConfig.getString(
|
||||
DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, KEY_COMPACT_PROC_STATE_THROTTLE,
|
||||
DEFAULT_COMPACT_PROC_STATE_THROTTLE);
|
||||
if (!parseProcStateThrottle(procStateThrottleString)) {
|
||||
Slog.w(TAG_AM, "Unable to parse app compact proc state throttle \""
|
||||
+ procStateThrottleString + "\" falling back to default.");
|
||||
if (!parseProcStateThrottle(DEFAULT_COMPACT_PROC_STATE_THROTTLE)) {
|
||||
Slog.wtf(TAG_AM,
|
||||
"Unable to parse default app compact proc state throttle "
|
||||
+ DEFAULT_COMPACT_PROC_STATE_THROTTLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean parseProcStateThrottle(String procStateThrottleString) {
|
||||
String[] procStates = TextUtils.split(procStateThrottleString, ",");
|
||||
mProcStateThrottle.clear();
|
||||
for (String procState : procStates) {
|
||||
try {
|
||||
mProcStateThrottle.add(Integer.parseInt(procState));
|
||||
} catch (NumberFormatException e) {
|
||||
Slog.e(TAG_AM, "Failed to parse default app compaction proc state: "
|
||||
+ procState);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static String compactActionIntToString(int action) {
|
||||
switch(action) {
|
||||
@@ -376,6 +506,22 @@ public final class AppCompactor {
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting static String procStateListToString(Integer... processStates) {
|
||||
return Arrays.toString(processStates);
|
||||
}
|
||||
|
||||
private static final class LastCompactionStats {
|
||||
private final long[] mRssAfterCompaction;
|
||||
|
||||
LastCompactionStats(long[] rss) {
|
||||
mRssAfterCompaction = rss;
|
||||
}
|
||||
|
||||
long[] getRssAfterCompaction() {
|
||||
return mRssAfterCompaction;
|
||||
}
|
||||
}
|
||||
|
||||
private final class MemCompactionHandler extends Handler {
|
||||
private MemCompactionHandler() {
|
||||
super(mCompactionThread.getLooper());
|
||||
@@ -392,24 +538,34 @@ public final class AppCompactor {
|
||||
final String name;
|
||||
int pendingAction, lastCompactAction;
|
||||
long lastCompactTime;
|
||||
LastCompactionStats lastCompactionStats;
|
||||
int lastOomAdj = msg.arg1;
|
||||
int procState = msg.arg2;
|
||||
synchronized (mAm) {
|
||||
proc = mPendingCompactionProcesses.remove(0);
|
||||
|
||||
pendingAction = proc.reqCompactAction;
|
||||
pid = proc.pid;
|
||||
name = proc.processName;
|
||||
|
||||
// don't compact if the process has returned to perceptible
|
||||
// and this is only a cached/home/prev compaction
|
||||
if ((pendingAction == COMPACT_PROCESS_SOME
|
||||
|| pendingAction == COMPACT_PROCESS_FULL)
|
||||
&& (proc.setAdj <= ProcessList.PERCEPTIBLE_APP_ADJ)) {
|
||||
if (DEBUG_COMPACTION) {
|
||||
Slog.d(TAG_AM,
|
||||
"Skipping compaction as process " + name + " is "
|
||||
+ "now perceptible.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
pid = proc.pid;
|
||||
name = proc.processName;
|
||||
|
||||
lastCompactAction = proc.lastCompactAction;
|
||||
lastCompactTime = proc.lastCompactTime;
|
||||
// remove rather than get so that insertion order will be updated when we
|
||||
// put the post-compaction stats back into the map.
|
||||
lastCompactionStats = mLastCompactionStats.remove(pid);
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
@@ -431,6 +587,12 @@ public final class AppCompactor {
|
||||
|| (lastCompactAction == COMPACT_PROCESS_FULL
|
||||
&& (start - lastCompactTime
|
||||
< mCompactThrottleSomeFull))) {
|
||||
if (DEBUG_COMPACTION) {
|
||||
Slog.d(TAG_AM, "Skipping some compaction for " + name
|
||||
+ ": too soon. throttle=" + mCompactThrottleSomeSome
|
||||
+ "/" + mCompactThrottleSomeFull + " last="
|
||||
+ (start - lastCompactTime) + "ms ago");
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if (pendingAction == COMPACT_PROCESS_FULL) {
|
||||
@@ -439,18 +601,35 @@ public final class AppCompactor {
|
||||
|| (lastCompactAction == COMPACT_PROCESS_FULL
|
||||
&& (start - lastCompactTime
|
||||
< mCompactThrottleFullFull))) {
|
||||
if (DEBUG_COMPACTION) {
|
||||
Slog.d(TAG_AM, "Skipping full compaction for " + name
|
||||
+ ": too soon. throttle=" + mCompactThrottleFullSome
|
||||
+ "/" + mCompactThrottleFullFull + " last="
|
||||
+ (start - lastCompactTime) + "ms ago");
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if (pendingAction == COMPACT_PROCESS_PERSISTENT) {
|
||||
if (start - lastCompactTime < mCompactThrottlePersistent) {
|
||||
if (DEBUG_COMPACTION) {
|
||||
Slog.d(TAG_AM, "Skipping persistent compaction for " + name
|
||||
+ ": too soon. throttle=" + mCompactThrottlePersistent
|
||||
+ " last=" + (start - lastCompactTime) + "ms ago");
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if (pendingAction == COMPACT_PROCESS_BFGS) {
|
||||
if (start - lastCompactTime < mCompactThrottleBFGS) {
|
||||
if (DEBUG_COMPACTION) {
|
||||
Slog.d(TAG_AM, "Skipping bfgs compaction for " + name
|
||||
+ ": too soon. throttle=" + mCompactThrottleBFGS
|
||||
+ " last=" + (start - lastCompactTime) + "ms ago");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (pendingAction) {
|
||||
case COMPACT_PROCESS_SOME:
|
||||
action = mCompactActionSome;
|
||||
@@ -470,12 +649,77 @@ public final class AppCompactor {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mProcStateThrottle.contains(procState)) {
|
||||
if (DEBUG_COMPACTION) {
|
||||
Slog.d(TAG_AM, "Skipping full compaction for process " + name
|
||||
+ "; proc state is " + procState);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
long[] rssBefore = Process.getRss(pid);
|
||||
long anonRssBefore = rssBefore[2];
|
||||
|
||||
if (rssBefore[0] == 0 && rssBefore[1] == 0 && rssBefore[2] == 0
|
||||
&& rssBefore[3] == 0) {
|
||||
if (DEBUG_COMPACTION) {
|
||||
Slog.d(TAG_AM, "Skipping compaction for" + "process " + pid
|
||||
+ " with no memory usage. Dead?");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.equals(COMPACT_ACTION_FULL) || action.equals(COMPACT_ACTION_ANON)) {
|
||||
if (mFullAnonRssThrottleKb > 0L
|
||||
&& anonRssBefore < mFullAnonRssThrottleKb) {
|
||||
if (DEBUG_COMPACTION) {
|
||||
Slog.d(TAG_AM, "Skipping full compaction for process "
|
||||
+ name + "; anon RSS is too small: " + anonRssBefore
|
||||
+ "KB.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastCompactionStats != null && mFullDeltaRssThrottleKb > 0L) {
|
||||
long[] lastRss = lastCompactionStats.getRssAfterCompaction();
|
||||
long absDelta = Math.abs(rssBefore[1] - lastRss[1])
|
||||
+ Math.abs(rssBefore[2] - lastRss[2])
|
||||
+ Math.abs(rssBefore[3] - lastRss[3]);
|
||||
if (absDelta <= mFullDeltaRssThrottleKb) {
|
||||
if (DEBUG_COMPACTION) {
|
||||
Slog.d(TAG_AM, "Skipping full compaction for process "
|
||||
+ name + "; abs delta is too small: " + absDelta
|
||||
+ "KB.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now we've passed through all the throttles and are going to compact, update
|
||||
// bookkeeping.
|
||||
switch (pendingAction) {
|
||||
case COMPACT_PROCESS_SOME:
|
||||
mSomeCompactionCount++;
|
||||
break;
|
||||
case COMPACT_PROCESS_FULL:
|
||||
mFullCompactionCount++;
|
||||
break;
|
||||
case COMPACT_PROCESS_PERSISTENT:
|
||||
mPersistentCompactionCount++;
|
||||
break;
|
||||
case COMPACT_PROCESS_BFGS:
|
||||
mBfgsCompactionCount++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Compact "
|
||||
+ ((pendingAction == COMPACT_PROCESS_SOME) ? "some" : "full")
|
||||
+ ": " + name);
|
||||
long zramFreeKbBefore = Debug.getZramFreeKb();
|
||||
long[] rssBefore = Process.getRss(pid);
|
||||
FileOutputStream fos = new FileOutputStream("/proc/" + pid + "/reclaim");
|
||||
fos.write(action.getBytes());
|
||||
fos.close();
|
||||
@@ -487,8 +731,9 @@ public final class AppCompactor {
|
||||
rssBefore[0], rssBefore[1], rssBefore[2], rssBefore[3],
|
||||
rssAfter[0] - rssBefore[0], rssAfter[1] - rssBefore[1],
|
||||
rssAfter[2] - rssBefore[2], rssAfter[3] - rssBefore[3], time,
|
||||
lastCompactAction, lastCompactTime, msg.arg1, msg.arg2,
|
||||
lastCompactAction, lastCompactTime, lastOomAdj, procState,
|
||||
zramFreeKbBefore, zramFreeKbAfter - zramFreeKbBefore);
|
||||
|
||||
// Note that as above not taking mPhenoTypeFlagLock here to avoid locking
|
||||
// on every single compaction for a flag that will seldom change and the
|
||||
// impact of reading the wrong value here is low.
|
||||
@@ -496,17 +741,23 @@ public final class AppCompactor {
|
||||
StatsLog.write(StatsLog.APP_COMPACTED, pid, name, pendingAction,
|
||||
rssBefore[0], rssBefore[1], rssBefore[2], rssBefore[3],
|
||||
rssAfter[0], rssAfter[1], rssAfter[2], rssAfter[3], time,
|
||||
lastCompactAction, lastCompactTime, msg.arg1,
|
||||
ActivityManager.processStateAmToProto(msg.arg2),
|
||||
lastCompactAction, lastCompactTime, lastOomAdj,
|
||||
ActivityManager.processStateAmToProto(procState),
|
||||
zramFreeKbBefore, zramFreeKbAfter);
|
||||
}
|
||||
|
||||
synchronized (mAm) {
|
||||
proc.lastCompactTime = end;
|
||||
proc.lastCompactAction = pendingAction;
|
||||
}
|
||||
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
|
||||
|
||||
if (action.equals(COMPACT_ACTION_FULL)
|
||||
|| action.equals(COMPACT_ACTION_ANON)) {
|
||||
mLastCompactionStats.put(pid, new LastCompactionStats(rssAfter));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// nothing to do, presumably the process died
|
||||
} finally {
|
||||
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.platform.test.annotations.Presubmit;
|
||||
import android.provider.DeviceConfig;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.server.appop.AppOpsService;
|
||||
import com.android.server.testables.TestableDeviceConfig;
|
||||
@@ -38,6 +39,8 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -103,6 +106,22 @@ public final class AppCompactorTest {
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_4);
|
||||
assertThat(mCompactorUnderTest.mStatsdSampleRate).isEqualTo(
|
||||
AppCompactor.DEFAULT_STATSD_SAMPLE_RATE);
|
||||
assertThat(mCompactorUnderTest.mFullAnonRssThrottleKb).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_FULL_RSS_THROTTLE_KB);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleBFGS).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_5);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottlePersistent).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_6);
|
||||
assertThat(mCompactorUnderTest.mFullAnonRssThrottleKb).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_FULL_RSS_THROTTLE_KB);
|
||||
assertThat(mCompactorUnderTest.mFullDeltaRssThrottleKb).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_FULL_DELTA_RSS_THROTTLE_KB);
|
||||
|
||||
Set<Integer> expected = new HashSet<>();
|
||||
for (String s : TextUtils.split(AppCompactor.DEFAULT_COMPACT_PROC_STATE_THROTTLE, ",")) {
|
||||
expected.add(Integer.parseInt(s));
|
||||
}
|
||||
assertThat(mCompactorUnderTest.mProcStateThrottle).containsExactlyElementsIn(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -139,6 +158,14 @@ public final class AppCompactorTest {
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_STATSD_SAMPLE_RATE,
|
||||
Float.toString(AppCompactor.DEFAULT_STATSD_SAMPLE_RATE + 0.1f), false);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_FULL_RSS_THROTTLE_KB,
|
||||
Long.toString(AppCompactor.DEFAULT_COMPACT_FULL_RSS_THROTTLE_KB + 1), false);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_FULL_DELTA_RSS_THROTTLE_KB,
|
||||
Long.toString(AppCompactor.DEFAULT_COMPACT_FULL_DELTA_RSS_THROTTLE_KB + 1), false);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_PROC_STATE_THROTTLE, "1,2,3", false);
|
||||
|
||||
// Then calling init will read and set that flag.
|
||||
mCompactorUnderTest.init();
|
||||
@@ -157,12 +184,19 @@ public final class AppCompactorTest {
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_3 + 1);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleFullFull).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_4 + 1);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleBFGS).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_5 + 1);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottlePersistent).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_6 + 1);
|
||||
assertThat(mCompactorUnderTest.mStatsdSampleRate).isEqualTo(
|
||||
AppCompactor.DEFAULT_STATSD_SAMPLE_RATE + 0.1f);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleBFGS).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_5 + 1);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottlePersistent).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_6 + 1);
|
||||
assertThat(mCompactorUnderTest.mFullAnonRssThrottleKb).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_FULL_RSS_THROTTLE_KB + 1);
|
||||
assertThat(mCompactorUnderTest.mProcStateThrottle).containsExactly(1, 2, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -321,6 +355,10 @@ public final class AppCompactorTest {
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_3);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleFullFull).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_4);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleBFGS).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_5);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottlePersistent).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_6);
|
||||
|
||||
// Repeat for each of the throttle keys.
|
||||
mCountDown = new CountDownLatch(1);
|
||||
@@ -335,6 +373,10 @@ public final class AppCompactorTest {
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_3);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleFullFull).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_4);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleBFGS).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_5);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottlePersistent).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_6);
|
||||
|
||||
mCountDown = new CountDownLatch(1);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
@@ -348,6 +390,10 @@ public final class AppCompactorTest {
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_3);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleFullFull).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_4);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleBFGS).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_5);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottlePersistent).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_6);
|
||||
|
||||
mCountDown = new CountDownLatch(1);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
@@ -361,13 +407,51 @@ public final class AppCompactorTest {
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_3);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleFullFull).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_4);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleBFGS).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_5);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottlePersistent).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_6);
|
||||
|
||||
mCountDown = new CountDownLatch(1);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_THROTTLE_5, "foo", false);
|
||||
assertThat(mCountDown.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleSomeSome).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_1);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleSomeFull).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_2);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleFullSome).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_3);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleFullFull).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_4);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleBFGS).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_5);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottlePersistent).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_6);
|
||||
|
||||
mCountDown = new CountDownLatch(1);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_THROTTLE_6, "foo", false);
|
||||
assertThat(mCountDown.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleSomeSome).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_1);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleSomeFull).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_2);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleFullSome).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_3);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleFullFull).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_4);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottleBFGS).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_5);
|
||||
assertThat(mCompactorUnderTest.mCompactThrottlePersistent).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_THROTTLE_6);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void statsdSampleRate_listensToDeviceConfigChanges() throws InterruptedException {
|
||||
mCompactorUnderTest.init();
|
||||
|
||||
// When we override mStatsdSampleRate with a reasonable values ...
|
||||
// When we override mStatsdSampleRate with a reasonable value ...
|
||||
mCountDown = new CountDownLatch(1);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_STATSD_SAMPLE_RATE,
|
||||
@@ -380,11 +464,11 @@ public final class AppCompactorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void statsdSanokeRate_listensToDeviceConfigChangesBadValues()
|
||||
public void statsdSampleRate_listensToDeviceConfigChangesBadValues()
|
||||
throws InterruptedException {
|
||||
mCompactorUnderTest.init();
|
||||
|
||||
// When we override mStatsdSampleRate with a reasonable values ...
|
||||
// When we override mStatsdSampleRate with an unreasonable value ...
|
||||
mCountDown = new CountDownLatch(1);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_STATSD_SAMPLE_RATE, "foo", false);
|
||||
@@ -396,7 +480,7 @@ public final class AppCompactorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void statsdSanokeRate_listensToDeviceConfigChangesOutOfRangeValues()
|
||||
public void statsdSampleRate_listensToDeviceConfigChangesOutOfRangeValues()
|
||||
throws InterruptedException {
|
||||
mCompactorUnderTest.init();
|
||||
|
||||
@@ -420,6 +504,147 @@ public final class AppCompactorTest {
|
||||
assertThat(mCompactorUnderTest.mStatsdSampleRate).isEqualTo(1.0f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fullCompactionRssThrottleKb_listensToDeviceConfigChanges()
|
||||
throws InterruptedException {
|
||||
mCompactorUnderTest.init();
|
||||
|
||||
// When we override mStatsdSampleRate with a reasonable value ...
|
||||
mCountDown = new CountDownLatch(1);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_FULL_RSS_THROTTLE_KB,
|
||||
Long.toString(AppCompactor.DEFAULT_COMPACT_FULL_RSS_THROTTLE_KB + 1), false);
|
||||
assertThat(mCountDown.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
|
||||
// Then that override is reflected in the compactor.
|
||||
assertThat(mCompactorUnderTest.mFullAnonRssThrottleKb).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_FULL_RSS_THROTTLE_KB + 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fullCompactionRssThrottleKb_listensToDeviceConfigChangesBadValues()
|
||||
throws InterruptedException {
|
||||
mCompactorUnderTest.init();
|
||||
|
||||
// When we override mStatsdSampleRate with an unreasonable value ...
|
||||
mCountDown = new CountDownLatch(1);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_FULL_RSS_THROTTLE_KB, "foo", false);
|
||||
assertThat(mCountDown.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
|
||||
// Then that override is reflected in the compactor.
|
||||
assertThat(mCompactorUnderTest.mFullAnonRssThrottleKb).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_FULL_RSS_THROTTLE_KB);
|
||||
|
||||
mCountDown = new CountDownLatch(1);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_FULL_RSS_THROTTLE_KB, "-100", false);
|
||||
assertThat(mCountDown.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
|
||||
// Then that override is reflected in the compactor.
|
||||
assertThat(mCompactorUnderTest.mFullAnonRssThrottleKb).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_FULL_RSS_THROTTLE_KB);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fullCompactionDeltaRssThrottleKb_listensToDeviceConfigChanges()
|
||||
throws InterruptedException {
|
||||
mCompactorUnderTest.init();
|
||||
|
||||
// When we override mStatsdSampleRate with a reasonable value ...
|
||||
mCountDown = new CountDownLatch(1);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_FULL_DELTA_RSS_THROTTLE_KB,
|
||||
Long.toString(AppCompactor.DEFAULT_COMPACT_FULL_DELTA_RSS_THROTTLE_KB + 1), false);
|
||||
assertThat(mCountDown.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
|
||||
// Then that override is reflected in the compactor.
|
||||
assertThat(mCompactorUnderTest.mFullDeltaRssThrottleKb).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_FULL_DELTA_RSS_THROTTLE_KB + 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fullCompactionDeltaRssThrottleKb_listensToDeviceConfigChangesBadValues()
|
||||
throws InterruptedException {
|
||||
mCompactorUnderTest.init();
|
||||
|
||||
// When we override mStatsdSampleRate with an unreasonable value ...
|
||||
mCountDown = new CountDownLatch(1);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_FULL_DELTA_RSS_THROTTLE_KB, "foo", false);
|
||||
assertThat(mCountDown.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
|
||||
// Then that override is reflected in the compactor.
|
||||
assertThat(mCompactorUnderTest.mFullDeltaRssThrottleKb).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_FULL_DELTA_RSS_THROTTLE_KB);
|
||||
|
||||
mCountDown = new CountDownLatch(1);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_FULL_DELTA_RSS_THROTTLE_KB, "-100", false);
|
||||
assertThat(mCountDown.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
|
||||
// Then that override is reflected in the compactor.
|
||||
assertThat(mCompactorUnderTest.mFullDeltaRssThrottleKb).isEqualTo(
|
||||
AppCompactor.DEFAULT_COMPACT_FULL_DELTA_RSS_THROTTLE_KB);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void procStateThrottle_listensToDeviceConfigChanges()
|
||||
throws InterruptedException {
|
||||
mCompactorUnderTest.init();
|
||||
mCountDown = new CountDownLatch(1);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_PROC_STATE_THROTTLE, "1,2,3", false);
|
||||
assertThat(mCountDown.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(mCompactorUnderTest.mProcStateThrottle).containsExactly(1, 2, 3);
|
||||
|
||||
mCountDown = new CountDownLatch(1);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_PROC_STATE_THROTTLE, "", false);
|
||||
assertThat(mCountDown.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(mCompactorUnderTest.mProcStateThrottle).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void procStateThrottle_listensToDeviceConfigChangesBadValues()
|
||||
throws InterruptedException {
|
||||
mCompactorUnderTest.init();
|
||||
|
||||
Set<Integer> expected = new HashSet<>();
|
||||
for (String s : TextUtils.split(AppCompactor.DEFAULT_COMPACT_PROC_STATE_THROTTLE, ",")) {
|
||||
expected.add(Integer.parseInt(s));
|
||||
}
|
||||
|
||||
// Not numbers
|
||||
mCountDown = new CountDownLatch(1);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_PROC_STATE_THROTTLE, "foo", false);
|
||||
assertThat(mCountDown.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(mCompactorUnderTest.mProcStateThrottle).containsExactlyElementsIn(expected);
|
||||
mCountDown = new CountDownLatch(1);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_PROC_STATE_THROTTLE, "1,foo", false);
|
||||
assertThat(mCountDown.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(mCompactorUnderTest.mProcStateThrottle).containsExactlyElementsIn(expected);
|
||||
|
||||
// Empty splits
|
||||
mCountDown = new CountDownLatch(1);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_PROC_STATE_THROTTLE, ",", false);
|
||||
assertThat(mCountDown.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(mCompactorUnderTest.mProcStateThrottle).containsExactlyElementsIn(expected);
|
||||
mCountDown = new CountDownLatch(1);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_PROC_STATE_THROTTLE, ",,3", false);
|
||||
assertThat(mCountDown.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(mCompactorUnderTest.mProcStateThrottle).containsExactlyElementsIn(expected);
|
||||
mCountDown = new CountDownLatch(1);
|
||||
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
|
||||
AppCompactor.KEY_COMPACT_PROC_STATE_THROTTLE, "1,,3", false);
|
||||
assertThat(mCountDown.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(mCompactorUnderTest.mProcStateThrottle).containsExactlyElementsIn(expected);
|
||||
}
|
||||
|
||||
private class TestInjector extends Injector {
|
||||
@Override
|
||||
public AppOpsService getAppOpsService(File file, Handler handler) {
|
||||
|
||||
Reference in New Issue
Block a user