Loading common/netlinkclient/src/android/net/netlink/NetlinkConstants.java +8 −0 Original line number Diff line number Diff line Loading @@ -35,6 +35,14 @@ public class NetlinkConstants { private NetlinkConstants() {} public static final int NLA_ALIGNTO = 4; /** * Flag for dumping struct tcp_info. * Corresponding to enum definition in external/strace/linux/inet_diag.h. */ public static final int INET_DIAG_MEMINFO = 1; public static final int SOCKDIAG_MSG_HEADER_SIZE = StructNlMsgHdr.STRUCT_SIZE + StructInetDiagMsg.STRUCT_SIZE; public static final int alignedLengthOf(short length) { final int intLength = (int) length & 0xffff; Loading src/android/net/util/DataStallUtils.java +1 −1 Original line number Diff line number Diff line Loading @@ -98,7 +98,7 @@ public class DataStallUtils { * Type: int * Valid values: 0 to 100. */ public static final String CONFIG_TCP_PACKETS_FAIL_RATE = "tcp_packets_fail_rate"; public static final String CONFIG_TCP_PACKETS_FAIL_PERCENTAGE = "tcp_packets_fail_percentage"; /** Corresponds to enum from bionic/libc/include/netinet/tcp.h. */ public static final int TCP_ESTABLISHED = 1; Loading src/com/android/networkstack/netlink/TcpSocketTracker.java +100 −52 Original line number Diff line number Diff line Loading @@ -16,11 +16,14 @@ package com.android.networkstack.netlink; import static android.net.netlink.InetDiagMessage.InetDiagReqV2; import static android.net.netlink.NetlinkConstants.INET_DIAG_MEMINFO; import static android.net.netlink.NetlinkConstants.NLA_ALIGNTO; import static android.net.netlink.NetlinkConstants.NLMSG_DONE; import static android.net.netlink.NetlinkConstants.SOCKDIAG_MSG_HEADER_SIZE; import static android.net.netlink.StructNlMsgHdr.NLM_F_DUMP; import static android.net.netlink.StructNlMsgHdr.NLM_F_REQUEST; import static android.net.util.DataStallUtils.CONFIG_MIN_PACKETS_THRESHOLD; import static android.net.util.DataStallUtils.CONFIG_TCP_PACKETS_FAIL_RATE; import static android.net.util.DataStallUtils.CONFIG_TCP_PACKETS_FAIL_PERCENTAGE; import static android.net.util.DataStallUtils.DEFAULT_DATA_STALL_MIN_PACKETS_THRESHOLD; import static android.net.util.DataStallUtils.DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE; import static android.net.util.DataStallUtils.TCP_MONITOR_STATE_FILTER; Loading @@ -35,13 +38,15 @@ import static android.system.OsConstants.SOCK_DGRAM; import static android.system.OsConstants.SOL_SOCKET; import static android.system.OsConstants.SO_SNDTIMEO; import android.content.Context; import android.net.netlink.NetlinkSocket; import android.net.netlink.StructInetDiagMsg; import android.net.netlink.StructNlMsgHdr; import android.net.util.NetworkStackUtils; import android.net.util.SocketUtils; import android.os.Build; import android.os.AsyncTask; import android.os.SystemClock; import android.provider.DeviceConfig; import android.system.ErrnoException; import android.system.Os; import android.system.StructTimeval; Loading @@ -53,7 +58,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.networkstack.apishim.ShimUtils; import java.io.FileDescriptor; import java.io.InterruptedIOException; Loading @@ -65,7 +69,7 @@ import java.util.List; /** * Class for NetworkStack to send a SockDiag request and parse the returned tcp info. * * This should be only access from the NetworkMonitor statemahcine thread. * This is not thread-safe. This should be only accessed from one thread. */ public class TcpSocketTracker { private static final String TAG = "TcpSocketTracker"; Loading @@ -75,16 +79,6 @@ public class TcpSocketTracker { private static final int DEFAULT_RECV_BUFSIZE = 60_000; // Default I/O timeout time in ms of the socket request. private static final long IO_TIMEOUT = 3_000L; // Map to definition in bionic/libc/kernel/uapi/linux/netlink.h. private static final int NLMSG_ALIGNTO = 4; /** * Flag for dumping struct tcp_info. * Corresponding to enum definition in external/strace/linux/inet_diag.h. */ private static final int INET_DIAG_MEMINFO = 1; @VisibleForTesting public static final int SOCKDIAG_MSG_HEADER_SIZE = StructNlMsgHdr.STRUCT_SIZE + StructInetDiagMsg.STRUCT_SIZE; /** Cookie offset of an InetMagMessage header. */ private static final int IDIAG_COOKIE_OFFSET = 44; /** Loading @@ -98,7 +92,9 @@ public class TcpSocketTracker { // Number of packets sent since the last received packet private int mSentSinceLastRecv; // The latest fail rate calculated by the latest tcp info. private int mLatestPacketFailRate; private int mLatestPacketFailPercentage; // Number of packets received in the latest polling cycle. private int mLatestReceivedCount; /** * Request to send to kernel to request tcp info. * Loading @@ -106,15 +102,30 @@ public class TcpSocketTracker { * Value: Bytes array represent the {@Code InetDiagReqV2}. */ private final SparseArray<byte[]> mSockDiagMsg = new SparseArray<>(); private final Dependencies mDependencies; private int mMinPacketsThreshold = DEFAULT_DATA_STALL_MIN_PACKETS_THRESHOLD; private int mTcpPacketsFailRateThreshold = DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE; @VisibleForTesting public final Dependencies mDependencies; public TcpSocketTracker(Dependencies dps) { protected final DeviceConfig.OnPropertiesChangedListener mConfigListener = new DeviceConfig.OnPropertiesChangedListener() { @Override public void onPropertiesChanged(DeviceConfig.Properties properties) { mMinPacketsThreshold = mDependencies.getDeviceConfigPropertyInt( NAMESPACE_CONNECTIVITY, CONFIG_MIN_PACKETS_THRESHOLD, DEFAULT_DATA_STALL_MIN_PACKETS_THRESHOLD); mTcpPacketsFailRateThreshold = mDependencies.getDeviceConfigPropertyInt( NAMESPACE_CONNECTIVITY, CONFIG_TCP_PACKETS_FAIL_PERCENTAGE, DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE); } }; public TcpSocketTracker(@NonNull final Dependencies dps) { mDependencies = dps; // Request tcp info from NetworkStack directly needs extra SELinux permission added after Q // release. mDependencies = dps; if (!mDependencies.isTcpInfoParsingSupported()) return; // Build SocketDiag messages. for (final int family : ADDRESS_FAMILIES) { mSockDiagMsg.put( Loading @@ -128,14 +139,14 @@ public class TcpSocketTracker { 1 << INET_DIAG_MEMINFO /* idiagExt */, TCP_MONITOR_STATE_FILTER)); } mDependencies.addDeviceConfigChangedListener(mConfigListener); } /** * Request to send a SockDiag Netlink request. Receive and parse the returned message. This * function should only be called from statemachine thread of NetworkMonitor. * function is not thread-safe and should only be called from only one thread. * * @Return if this polling request executes successfully or not. * * TODO: Need to filter socket info based on the target network. */ public boolean pollSocketsInfo() { Loading Loading @@ -163,6 +174,10 @@ public class TcpSocketTracker { while (enoughBytesRemainForValidNlMsg(bytes)) { final StructNlMsgHdr nlmsghdr = StructNlMsgHdr.parse(bytes); if (nlmsghdr == null) { Log.e(TAG, "Badly formatted data."); break; } final int nlmsgLen = nlmsghdr.nlmsg_len; log("pollSocketsInfo: nlmsghdr=" + nlmsghdr); if (nlmsghdr.nlmsg_type == NLMSG_DONE) break; Loading @@ -175,19 +190,20 @@ public class TcpSocketTracker { // It's stored in native with 2 int. Parse it as long for convenience. final long cookie = bytes.getLong(); // Skip the rest part of StructInetDiagMsg. bytes.position(bytes.position() + 5 * Integer.BYTES); final SocketInfo info = parseSockInfo(bytes, family, nlmsgLen, time); bytes.position(bytes.position() + StructInetDiagMsg.STRUCT_SIZE - IDIAG_COOKIE_OFFSET - Long.BYTES); final SocketInfo info = parseSockInfo(bytes, family, nlmsgLen, time); // Update TcpStats based on previous and current socket info. stat.accumulate(calculateLatestPacketsStat(info, mSocketInfos.get(cookie))); mSocketInfos.put(cookie, info); } } } // Calculate mSentSinceLastRecv and mLatestPacketFailRate. // Calculate mLatestReceiveCount, mSentSinceLastRecv and mLatestPacketFailPercentage. mSentSinceLastRecv = (stat.receivedCount == 0) ? (mSentSinceLastRecv + stat.sentCount) : 0; mLatestPacketFailRate = ((stat.sentCount != 0) mLatestReceivedCount = stat.receivedCount; mLatestPacketFailPercentage = ((stat.sentCount != 0) ? ((stat.retransmitCount + stat.lostCount) * 100 / stat.sentCount) : 0); // Remove out-of-date socket info. Loading Loading @@ -252,20 +268,25 @@ public class TcpSocketTracker { */ public boolean isDataStallSuspected() { if (!mDependencies.isTcpInfoParsingSupported()) return false; return (getLatestPacketFailRate() >= getTcpPacketsFailRateThreshold()); return (getLatestPacketFailPercentage() >= getTcpPacketsFailRateThreshold()); } /** Calculate the change between the {@param current} and {@param previous}. */ @Nullable private TcpStat calculateLatestPacketsStat(@NonNull final SocketInfo current, @Nullable final SocketInfo previous) { final TcpStat stat = new TcpStat(); if (current.tcpInfo != null) { if (current.tcpInfo == null) { log("Current tcpInfo is null."); return null; } stat.sentCount = current.tcpInfo.getValue(TcpInfo.Field.SEGS_OUT).intValue(); stat.receivedCount = current.tcpInfo.getValue(TcpInfo.Field.SEGS_IN).intValue(); stat.lostCount = current.tcpInfo.getValue(TcpInfo.Field.LOST).intValue(); stat.retransmitCount = current.tcpInfo.getValue(TcpInfo.Field.RETRANSMITS).intValue(); } if (previous != null && previous.tcpInfo != null) { stat.sentCount -= previous.tcpInfo.getValue(TcpInfo.Field.SEGS_OUT).intValue(); stat.receivedCount -= previous.tcpInfo.getValue(TcpInfo.Field.SEGS_IN).intValue(); Loading @@ -278,12 +299,14 @@ public class TcpSocketTracker { /** * Get tcp connection fail rate based on packet lost and retransmission count. * * @return the latest packet fail percentage. -1 denotes that there is no available data. */ public int getLatestPacketFailRate() { if (!mDependencies.isTcpInfoParsingSupported()) return 0; public int getLatestPacketFailPercentage() { if (!mDependencies.isTcpInfoParsingSupported()) return -1; // Only return fail rate if device sent enough packets. if (getSentSinceLastRecv() < getMinPacketsThreshold()) return 0; return mLatestPacketFailRate; if (getSentSinceLastRecv() < getMinPacketsThreshold()) return -1; return mLatestPacketFailPercentage; } /** Loading @@ -291,18 +314,14 @@ public class TcpSocketTracker { * between each polling period, not an accurate number. */ public int getSentSinceLastRecv() { if (!mDependencies.isTcpInfoParsingSupported()) return 0; if (!mDependencies.isTcpInfoParsingSupported()) return -1; return mSentSinceLastRecv; } private int getMinPacketsThreshold() { return mDependencies.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY, CONFIG_MIN_PACKETS_THRESHOLD, DEFAULT_DATA_STALL_MIN_PACKETS_THRESHOLD); } private int getTcpPacketsFailRateThreshold() { return mDependencies.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY, CONFIG_TCP_PACKETS_FAIL_RATE, DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE); /** Return the number of the packets received in the latest polling cycle. */ public int getLatestReceivedCount() { if (!mDependencies.isTcpInfoParsingSupported()) return -1; return mLatestReceivedCount; } /** Check if the length and position of the given ByteBuffer is valid for a nlmsghdr message. */ Loading @@ -315,6 +334,14 @@ public class TcpSocketTracker { return nlMsgLen >= SOCKDIAG_MSG_HEADER_SIZE; } private int getMinPacketsThreshold() { return mMinPacketsThreshold; } private int getTcpPacketsFailRateThreshold() { return mTcpPacketsFailRateThreshold; } /** * Method to skip the remaining attributes bytes. * Corresponds to NLMSG_NEXT in bionic/libc/kernel/uapi/linux/netlink.h. Loading @@ -324,12 +351,12 @@ public class TcpSocketTracker { */ private void skipRemainingAttributesBytesAligned(@NonNull final ByteBuffer buffer, final int len) { // Data in {@Code RoutingAttribute} is followed after header with size {@Code NLMSG_ALIGNTO} // Data in {@Code RoutingAttribute} is followed after header with size {@Code NLA_ALIGNTO} // bytes long for each block. Next attribute will start after the padding bytes if any. // If all remaining bytes after header are valid in a data block, next attr will just start // after valid bytes. // // E.g. With NLMSG_ALIGNTO(4), an attr struct with length 5 means 1 byte valid data remains // E.g. With NLA_ALIGNTO(4), an attr struct with length 5 means 1 byte valid data remains // after header and 3(4-1) padding bytes. Next attr with length 8 will start after the // padding bytes and contain 4(8-4) valid bytes of data. The next attr start after the // valid bytes, like: Loading @@ -337,7 +364,7 @@ public class TcpSocketTracker { // [HEADER(L=5)][ 4-Bytes DATA ][ HEADER(L=8) ][4 bytes DATA][Next attr] // [ 5 valid bytes ][3 padding bytes ][ 8 valid bytes ] ... final int cur = buffer.position(); buffer.position(cur + ((len + NLMSG_ALIGNTO - 1) & ~(NLMSG_ALIGNTO - 1))); buffer.position(cur + ((len + NLA_ALIGNTO - 1) & ~(NLA_ALIGNTO - 1))); } private void log(final String str) { Loading Loading @@ -423,7 +450,9 @@ public class TcpSocketTracker { public int retransmitCount; public int receivedCount; void accumulate(final TcpStat stat) { void accumulate(@Nullable final TcpStat stat) { if (stat == null) return; sentCount += stat.sentCount; lostCount += stat.lostCount; receivedCount += stat.receivedCount; Loading @@ -431,12 +460,19 @@ public class TcpSocketTracker { } } /** * Dependencies class for testing. */ @VisibleForTesting public static class Dependencies { private final Context mContext; private final boolean mIsTcpInfoParsingSupported; public Dependencies(final Context context, final boolean tcpSupport) { mContext = context; mIsTcpInfoParsingSupported = tcpSupport; } /** * Connect to kernel via netlink socket. * Loading @@ -451,6 +487,7 @@ public class TcpSocketTracker { return fd; } /** * Send composed message request to kernel. * @param fd see {@Code FileDescriptor} Loading Loading @@ -484,7 +521,7 @@ public class TcpSocketTracker { public boolean isTcpInfoParsingSupported() { // Request tcp info from NetworkStack directly needs extra SELinux permission added // after Q release. return ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q); return mIsTcpInfoParsingSupported; } /** Loading @@ -494,5 +531,16 @@ public class TcpSocketTracker { throws ErrnoException, InterruptedIOException { return NetlinkSocket.recvMessage(fd, DEFAULT_RECV_BUFSIZE, IO_TIMEOUT); } public Context getContext() { return mContext; } /** Add device config change listener */ public void addDeviceConfigChangedListener( @NonNull final DeviceConfig.OnPropertiesChangedListener listener) { DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_CONNECTIVITY, AsyncTask.THREAD_POOL_EXECUTOR, listener); } } } src/com/android/server/connectivity/NetworkMonitor.java +9 −6 Original line number Diff line number Diff line Loading @@ -126,6 +126,7 @@ import com.android.internal.util.State; import com.android.internal.util.StateMachine; import com.android.internal.util.TrafficStatsConstants; import com.android.networkstack.R; import com.android.networkstack.apishim.ShimUtils; import com.android.networkstack.metrics.DataStallDetectionStats; import com.android.networkstack.metrics.DataStallStatsUtils; import com.android.networkstack.netlink.TcpSocketTracker; Loading Loading @@ -389,13 +390,15 @@ public class NetworkMonitor extends StateMachine { public NetworkMonitor(Context context, INetworkMonitorCallbacks cb, Network network, SharedLog validationLog) { this(context, cb, network, new IpConnectivityLog(), validationLog, Dependencies.DEFAULT, new DataStallStatsUtils()); Dependencies.DEFAULT, new DataStallStatsUtils(), new TcpSocketTracker( new TcpSocketTracker.Dependencies(context, ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q)))); } @VisibleForTesting public NetworkMonitor(Context context, INetworkMonitorCallbacks cb, Network network, IpConnectivityLog logger, SharedLog validationLogs, Dependencies deps, DataStallStatsUtils detectionStatsUtils) { Dependencies deps, DataStallStatsUtils detectionStatsUtils, TcpSocketTracker tst) { // Add suffix indicating which NetworkMonitor we're talking about. super(TAG + "/" + network.toString()); Loading Loading @@ -442,7 +445,7 @@ public class NetworkMonitor extends StateMachine { mDataStallMinEvaluateTime = getDataStallMinEvaluateTime(); mDataStallValidDnsTimeThreshold = getDataStallValidDnsTimeThreshold(); mDataStallEvaluationType = getDataStallEvaluationType(); mTcpTracker = new TcpSocketTracker(new TcpSocketTracker.Dependencies()); mTcpTracker = tst; // Provide empty LinkProperties and NetworkCapabilities to make sure they are never null, // even before notifyNetworkConnected. Loading Loading @@ -2139,7 +2142,7 @@ public class NetworkMonitor extends StateMachine { // 2. Accumulate enough packets count. // TODO: Need to filter per target network. if (dataStallEvaluateTypeEnabled(DATA_STALL_EVALUATION_TYPE_TCP)) { if (getTcpSocketTracker().getSentSinceLastRecv() > 0) { if (getTcpSocketTracker().getLatestReceivedCount() > 0) { result = false; } else if (getTcpSocketTracker().isDataStallSuspected()) { result = true; Loading @@ -2160,8 +2163,8 @@ public class NetworkMonitor extends StateMachine { if (VDBG_STALL) { log("isDataStall: result=" + result + ", consecutive dns timeout count=" + mDnsStallDetector.getConsecutiveTimeoutCount() + ", tcp packets received=" + getTcpSocketTracker().getSentSinceLastRecv() + ", tcp fail rate=" + getTcpSocketTracker().getLatestPacketFailRate()); + ", tcp packets received=" + getTcpSocketTracker().getLatestReceivedCount() + ", tcp fail rate=" + getTcpSocketTracker().getLatestPacketFailPercentage()); } return (result == null) ? false : result; Loading tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java +20 −23 Original line number Diff line number Diff line Loading @@ -16,13 +16,12 @@ package com.android.networkstack.netlink; import static android.net.util.DataStallUtils.CONFIG_TCP_PACKETS_FAIL_RATE; import static android.net.netlink.NetlinkConstants.SOCKDIAG_MSG_HEADER_SIZE; import static android.net.util.DataStallUtils.CONFIG_TCP_PACKETS_FAIL_PERCENTAGE; import static android.net.util.DataStallUtils.DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE; import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY; import static android.system.OsConstants.AF_INET; import static com.android.networkstack.netlink.TcpSocketTracker.SOCKDIAG_MSG_HEADER_SIZE; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; Loading Loading @@ -111,7 +110,7 @@ public class TcpSocketTrackerTest { "00000000" + // data "0008" + // len = 8 "000F" + // type = 15(INET_DIAG_MARK) "000C0064" + // data, socket mark=786532 "000C1A85" + // data, socket mark=793221 "00AC" + // len = 172 "0002" + // type = 2(INET_DIAG_INFO) // tcp_info Loading @@ -129,7 +128,7 @@ public class TcpSocketTrackerTest { "00000218" + // rcvMss = 536 "00000000" + // unsacked = 0 "00000000" + // acked = 0 "00000005" + // lost = 5 "00000000" + // lost = 0 "00000000" + // retrans = 0 "00000000" + // fackets = 0 "000000BB" + // lastDataSent = 187 Loading Loading @@ -185,7 +184,7 @@ public class TcpSocketTrackerTest { when(mDependencies.connectToKernel()).thenReturn(mMockFd); when(mDependencies.getDeviceConfigPropertyInt( eq(NAMESPACE_CONNECTIVITY), eq(CONFIG_TCP_PACKETS_FAIL_RATE), eq(CONFIG_TCP_PACKETS_FAIL_PERCENTAGE), anyInt())).thenReturn(DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE); } Loading @@ -211,7 +210,7 @@ public class TcpSocketTrackerTest { expected.put(TcpInfo.Field.RCV_MSS, 536); expected.put(TcpInfo.Field.UNACKED, 0); expected.put(TcpInfo.Field.SACKED, 0); expected.put(TcpInfo.Field.LOST, 5); expected.put(TcpInfo.Field.LOST, 0); expected.put(TcpInfo.Field.RETRANS, 0); expected.put(TcpInfo.Field.FACKETS, 0); expected.put(TcpInfo.Field.LAST_DATA_SENT, 187); Loading Loading @@ -242,7 +241,7 @@ public class TcpSocketTrackerTest { expected.put(TcpInfo.Field.DELIVERY_RATE, 0L); assertEquals(parsed.tcpInfo, new TcpInfo(expected)); assertEquals(parsed.fwmark, 786532); assertEquals(parsed.fwmark, 793221); assertEquals(parsed.updateTime, 100); assertEquals(parsed.ipFamily, AF_INET); } Loading @@ -261,18 +260,6 @@ public class TcpSocketTrackerTest { assertFalse(TcpSocketTracker.enoughBytesRemainForValidNlMsg(buffer)); } @Test public void testIsDataStallSuspected() { when(mDependencies.isTcpInfoParsingSupported()).thenReturn(false); final TcpSocketTracker tst = new TcpSocketTracker(mDependencies); assertFalse(tst.isDataStallSuspected()); when(mDependencies.isTcpInfoParsingSupported()).thenReturn(true); assertFalse(tst.isDataStallSuspected()); when(mDependencies.getDeviceConfigPropertyInt(any(), eq(CONFIG_TCP_PACKETS_FAIL_RATE), anyInt())).thenReturn(0); assertTrue(tst.isDataStallSuspected()); } @Test public void testPollSocketsInfo() throws Exception { when(mDependencies.isTcpInfoParsingSupported()).thenReturn(false); Loading @@ -284,20 +271,30 @@ public class TcpSocketTrackerTest { final ByteBuffer invalidBuffer = ByteBuffer.allocate(1); when(mDependencies.recvMesssage(any())).thenReturn(invalidBuffer); assertTrue(tst.pollSocketsInfo()); assertEquals(0, tst.getLatestPacketFailRate()); assertEquals(-1, tst.getLatestPacketFailPercentage()); assertEquals(0, tst.getSentSinceLastRecv()); // Header only. final ByteBuffer headerBuffer = ByteBuffer.wrap(SOCK_DIAG_MSG_BYTES); when(mDependencies.recvMesssage(any())).thenReturn(headerBuffer); assertTrue(tst.pollSocketsInfo()); assertEquals(-1, tst.getLatestPacketFailPercentage()); assertEquals(0, tst.getSentSinceLastRecv()); assertEquals(0, tst.getLatestPacketFailRate()); final ByteBuffer tcpBuffer = ByteBuffer.wrap(TEST_RESPONSE_BYTES); when(mDependencies.recvMesssage(any())).thenReturn(tcpBuffer); assertTrue(tst.pollSocketsInfo()); assertEquals(10, tst.getSentSinceLastRecv()); assertEquals(100, tst.getLatestPacketFailRate()); assertEquals(50, tst.getLatestPacketFailPercentage()); assertFalse(tst.isDataStallSuspected()); // Lower the threshold. when(mDependencies.getDeviceConfigPropertyInt(any(), eq(CONFIG_TCP_PACKETS_FAIL_PERCENTAGE), anyInt())).thenReturn(40); // No device config change. Using cache value. assertFalse(tst.isDataStallSuspected()); // Trigger a config update tst.mConfigListener.onPropertiesChanged(null /* properties */); assertTrue(tst.isDataStallSuspected()); } } Loading
common/netlinkclient/src/android/net/netlink/NetlinkConstants.java +8 −0 Original line number Diff line number Diff line Loading @@ -35,6 +35,14 @@ public class NetlinkConstants { private NetlinkConstants() {} public static final int NLA_ALIGNTO = 4; /** * Flag for dumping struct tcp_info. * Corresponding to enum definition in external/strace/linux/inet_diag.h. */ public static final int INET_DIAG_MEMINFO = 1; public static final int SOCKDIAG_MSG_HEADER_SIZE = StructNlMsgHdr.STRUCT_SIZE + StructInetDiagMsg.STRUCT_SIZE; public static final int alignedLengthOf(short length) { final int intLength = (int) length & 0xffff; Loading
src/android/net/util/DataStallUtils.java +1 −1 Original line number Diff line number Diff line Loading @@ -98,7 +98,7 @@ public class DataStallUtils { * Type: int * Valid values: 0 to 100. */ public static final String CONFIG_TCP_PACKETS_FAIL_RATE = "tcp_packets_fail_rate"; public static final String CONFIG_TCP_PACKETS_FAIL_PERCENTAGE = "tcp_packets_fail_percentage"; /** Corresponds to enum from bionic/libc/include/netinet/tcp.h. */ public static final int TCP_ESTABLISHED = 1; Loading
src/com/android/networkstack/netlink/TcpSocketTracker.java +100 −52 Original line number Diff line number Diff line Loading @@ -16,11 +16,14 @@ package com.android.networkstack.netlink; import static android.net.netlink.InetDiagMessage.InetDiagReqV2; import static android.net.netlink.NetlinkConstants.INET_DIAG_MEMINFO; import static android.net.netlink.NetlinkConstants.NLA_ALIGNTO; import static android.net.netlink.NetlinkConstants.NLMSG_DONE; import static android.net.netlink.NetlinkConstants.SOCKDIAG_MSG_HEADER_SIZE; import static android.net.netlink.StructNlMsgHdr.NLM_F_DUMP; import static android.net.netlink.StructNlMsgHdr.NLM_F_REQUEST; import static android.net.util.DataStallUtils.CONFIG_MIN_PACKETS_THRESHOLD; import static android.net.util.DataStallUtils.CONFIG_TCP_PACKETS_FAIL_RATE; import static android.net.util.DataStallUtils.CONFIG_TCP_PACKETS_FAIL_PERCENTAGE; import static android.net.util.DataStallUtils.DEFAULT_DATA_STALL_MIN_PACKETS_THRESHOLD; import static android.net.util.DataStallUtils.DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE; import static android.net.util.DataStallUtils.TCP_MONITOR_STATE_FILTER; Loading @@ -35,13 +38,15 @@ import static android.system.OsConstants.SOCK_DGRAM; import static android.system.OsConstants.SOL_SOCKET; import static android.system.OsConstants.SO_SNDTIMEO; import android.content.Context; import android.net.netlink.NetlinkSocket; import android.net.netlink.StructInetDiagMsg; import android.net.netlink.StructNlMsgHdr; import android.net.util.NetworkStackUtils; import android.net.util.SocketUtils; import android.os.Build; import android.os.AsyncTask; import android.os.SystemClock; import android.provider.DeviceConfig; import android.system.ErrnoException; import android.system.Os; import android.system.StructTimeval; Loading @@ -53,7 +58,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.networkstack.apishim.ShimUtils; import java.io.FileDescriptor; import java.io.InterruptedIOException; Loading @@ -65,7 +69,7 @@ import java.util.List; /** * Class for NetworkStack to send a SockDiag request and parse the returned tcp info. * * This should be only access from the NetworkMonitor statemahcine thread. * This is not thread-safe. This should be only accessed from one thread. */ public class TcpSocketTracker { private static final String TAG = "TcpSocketTracker"; Loading @@ -75,16 +79,6 @@ public class TcpSocketTracker { private static final int DEFAULT_RECV_BUFSIZE = 60_000; // Default I/O timeout time in ms of the socket request. private static final long IO_TIMEOUT = 3_000L; // Map to definition in bionic/libc/kernel/uapi/linux/netlink.h. private static final int NLMSG_ALIGNTO = 4; /** * Flag for dumping struct tcp_info. * Corresponding to enum definition in external/strace/linux/inet_diag.h. */ private static final int INET_DIAG_MEMINFO = 1; @VisibleForTesting public static final int SOCKDIAG_MSG_HEADER_SIZE = StructNlMsgHdr.STRUCT_SIZE + StructInetDiagMsg.STRUCT_SIZE; /** Cookie offset of an InetMagMessage header. */ private static final int IDIAG_COOKIE_OFFSET = 44; /** Loading @@ -98,7 +92,9 @@ public class TcpSocketTracker { // Number of packets sent since the last received packet private int mSentSinceLastRecv; // The latest fail rate calculated by the latest tcp info. private int mLatestPacketFailRate; private int mLatestPacketFailPercentage; // Number of packets received in the latest polling cycle. private int mLatestReceivedCount; /** * Request to send to kernel to request tcp info. * Loading @@ -106,15 +102,30 @@ public class TcpSocketTracker { * Value: Bytes array represent the {@Code InetDiagReqV2}. */ private final SparseArray<byte[]> mSockDiagMsg = new SparseArray<>(); private final Dependencies mDependencies; private int mMinPacketsThreshold = DEFAULT_DATA_STALL_MIN_PACKETS_THRESHOLD; private int mTcpPacketsFailRateThreshold = DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE; @VisibleForTesting public final Dependencies mDependencies; public TcpSocketTracker(Dependencies dps) { protected final DeviceConfig.OnPropertiesChangedListener mConfigListener = new DeviceConfig.OnPropertiesChangedListener() { @Override public void onPropertiesChanged(DeviceConfig.Properties properties) { mMinPacketsThreshold = mDependencies.getDeviceConfigPropertyInt( NAMESPACE_CONNECTIVITY, CONFIG_MIN_PACKETS_THRESHOLD, DEFAULT_DATA_STALL_MIN_PACKETS_THRESHOLD); mTcpPacketsFailRateThreshold = mDependencies.getDeviceConfigPropertyInt( NAMESPACE_CONNECTIVITY, CONFIG_TCP_PACKETS_FAIL_PERCENTAGE, DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE); } }; public TcpSocketTracker(@NonNull final Dependencies dps) { mDependencies = dps; // Request tcp info from NetworkStack directly needs extra SELinux permission added after Q // release. mDependencies = dps; if (!mDependencies.isTcpInfoParsingSupported()) return; // Build SocketDiag messages. for (final int family : ADDRESS_FAMILIES) { mSockDiagMsg.put( Loading @@ -128,14 +139,14 @@ public class TcpSocketTracker { 1 << INET_DIAG_MEMINFO /* idiagExt */, TCP_MONITOR_STATE_FILTER)); } mDependencies.addDeviceConfigChangedListener(mConfigListener); } /** * Request to send a SockDiag Netlink request. Receive and parse the returned message. This * function should only be called from statemachine thread of NetworkMonitor. * function is not thread-safe and should only be called from only one thread. * * @Return if this polling request executes successfully or not. * * TODO: Need to filter socket info based on the target network. */ public boolean pollSocketsInfo() { Loading Loading @@ -163,6 +174,10 @@ public class TcpSocketTracker { while (enoughBytesRemainForValidNlMsg(bytes)) { final StructNlMsgHdr nlmsghdr = StructNlMsgHdr.parse(bytes); if (nlmsghdr == null) { Log.e(TAG, "Badly formatted data."); break; } final int nlmsgLen = nlmsghdr.nlmsg_len; log("pollSocketsInfo: nlmsghdr=" + nlmsghdr); if (nlmsghdr.nlmsg_type == NLMSG_DONE) break; Loading @@ -175,19 +190,20 @@ public class TcpSocketTracker { // It's stored in native with 2 int. Parse it as long for convenience. final long cookie = bytes.getLong(); // Skip the rest part of StructInetDiagMsg. bytes.position(bytes.position() + 5 * Integer.BYTES); final SocketInfo info = parseSockInfo(bytes, family, nlmsgLen, time); bytes.position(bytes.position() + StructInetDiagMsg.STRUCT_SIZE - IDIAG_COOKIE_OFFSET - Long.BYTES); final SocketInfo info = parseSockInfo(bytes, family, nlmsgLen, time); // Update TcpStats based on previous and current socket info. stat.accumulate(calculateLatestPacketsStat(info, mSocketInfos.get(cookie))); mSocketInfos.put(cookie, info); } } } // Calculate mSentSinceLastRecv and mLatestPacketFailRate. // Calculate mLatestReceiveCount, mSentSinceLastRecv and mLatestPacketFailPercentage. mSentSinceLastRecv = (stat.receivedCount == 0) ? (mSentSinceLastRecv + stat.sentCount) : 0; mLatestPacketFailRate = ((stat.sentCount != 0) mLatestReceivedCount = stat.receivedCount; mLatestPacketFailPercentage = ((stat.sentCount != 0) ? ((stat.retransmitCount + stat.lostCount) * 100 / stat.sentCount) : 0); // Remove out-of-date socket info. Loading Loading @@ -252,20 +268,25 @@ public class TcpSocketTracker { */ public boolean isDataStallSuspected() { if (!mDependencies.isTcpInfoParsingSupported()) return false; return (getLatestPacketFailRate() >= getTcpPacketsFailRateThreshold()); return (getLatestPacketFailPercentage() >= getTcpPacketsFailRateThreshold()); } /** Calculate the change between the {@param current} and {@param previous}. */ @Nullable private TcpStat calculateLatestPacketsStat(@NonNull final SocketInfo current, @Nullable final SocketInfo previous) { final TcpStat stat = new TcpStat(); if (current.tcpInfo != null) { if (current.tcpInfo == null) { log("Current tcpInfo is null."); return null; } stat.sentCount = current.tcpInfo.getValue(TcpInfo.Field.SEGS_OUT).intValue(); stat.receivedCount = current.tcpInfo.getValue(TcpInfo.Field.SEGS_IN).intValue(); stat.lostCount = current.tcpInfo.getValue(TcpInfo.Field.LOST).intValue(); stat.retransmitCount = current.tcpInfo.getValue(TcpInfo.Field.RETRANSMITS).intValue(); } if (previous != null && previous.tcpInfo != null) { stat.sentCount -= previous.tcpInfo.getValue(TcpInfo.Field.SEGS_OUT).intValue(); stat.receivedCount -= previous.tcpInfo.getValue(TcpInfo.Field.SEGS_IN).intValue(); Loading @@ -278,12 +299,14 @@ public class TcpSocketTracker { /** * Get tcp connection fail rate based on packet lost and retransmission count. * * @return the latest packet fail percentage. -1 denotes that there is no available data. */ public int getLatestPacketFailRate() { if (!mDependencies.isTcpInfoParsingSupported()) return 0; public int getLatestPacketFailPercentage() { if (!mDependencies.isTcpInfoParsingSupported()) return -1; // Only return fail rate if device sent enough packets. if (getSentSinceLastRecv() < getMinPacketsThreshold()) return 0; return mLatestPacketFailRate; if (getSentSinceLastRecv() < getMinPacketsThreshold()) return -1; return mLatestPacketFailPercentage; } /** Loading @@ -291,18 +314,14 @@ public class TcpSocketTracker { * between each polling period, not an accurate number. */ public int getSentSinceLastRecv() { if (!mDependencies.isTcpInfoParsingSupported()) return 0; if (!mDependencies.isTcpInfoParsingSupported()) return -1; return mSentSinceLastRecv; } private int getMinPacketsThreshold() { return mDependencies.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY, CONFIG_MIN_PACKETS_THRESHOLD, DEFAULT_DATA_STALL_MIN_PACKETS_THRESHOLD); } private int getTcpPacketsFailRateThreshold() { return mDependencies.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY, CONFIG_TCP_PACKETS_FAIL_RATE, DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE); /** Return the number of the packets received in the latest polling cycle. */ public int getLatestReceivedCount() { if (!mDependencies.isTcpInfoParsingSupported()) return -1; return mLatestReceivedCount; } /** Check if the length and position of the given ByteBuffer is valid for a nlmsghdr message. */ Loading @@ -315,6 +334,14 @@ public class TcpSocketTracker { return nlMsgLen >= SOCKDIAG_MSG_HEADER_SIZE; } private int getMinPacketsThreshold() { return mMinPacketsThreshold; } private int getTcpPacketsFailRateThreshold() { return mTcpPacketsFailRateThreshold; } /** * Method to skip the remaining attributes bytes. * Corresponds to NLMSG_NEXT in bionic/libc/kernel/uapi/linux/netlink.h. Loading @@ -324,12 +351,12 @@ public class TcpSocketTracker { */ private void skipRemainingAttributesBytesAligned(@NonNull final ByteBuffer buffer, final int len) { // Data in {@Code RoutingAttribute} is followed after header with size {@Code NLMSG_ALIGNTO} // Data in {@Code RoutingAttribute} is followed after header with size {@Code NLA_ALIGNTO} // bytes long for each block. Next attribute will start after the padding bytes if any. // If all remaining bytes after header are valid in a data block, next attr will just start // after valid bytes. // // E.g. With NLMSG_ALIGNTO(4), an attr struct with length 5 means 1 byte valid data remains // E.g. With NLA_ALIGNTO(4), an attr struct with length 5 means 1 byte valid data remains // after header and 3(4-1) padding bytes. Next attr with length 8 will start after the // padding bytes and contain 4(8-4) valid bytes of data. The next attr start after the // valid bytes, like: Loading @@ -337,7 +364,7 @@ public class TcpSocketTracker { // [HEADER(L=5)][ 4-Bytes DATA ][ HEADER(L=8) ][4 bytes DATA][Next attr] // [ 5 valid bytes ][3 padding bytes ][ 8 valid bytes ] ... final int cur = buffer.position(); buffer.position(cur + ((len + NLMSG_ALIGNTO - 1) & ~(NLMSG_ALIGNTO - 1))); buffer.position(cur + ((len + NLA_ALIGNTO - 1) & ~(NLA_ALIGNTO - 1))); } private void log(final String str) { Loading Loading @@ -423,7 +450,9 @@ public class TcpSocketTracker { public int retransmitCount; public int receivedCount; void accumulate(final TcpStat stat) { void accumulate(@Nullable final TcpStat stat) { if (stat == null) return; sentCount += stat.sentCount; lostCount += stat.lostCount; receivedCount += stat.receivedCount; Loading @@ -431,12 +460,19 @@ public class TcpSocketTracker { } } /** * Dependencies class for testing. */ @VisibleForTesting public static class Dependencies { private final Context mContext; private final boolean mIsTcpInfoParsingSupported; public Dependencies(final Context context, final boolean tcpSupport) { mContext = context; mIsTcpInfoParsingSupported = tcpSupport; } /** * Connect to kernel via netlink socket. * Loading @@ -451,6 +487,7 @@ public class TcpSocketTracker { return fd; } /** * Send composed message request to kernel. * @param fd see {@Code FileDescriptor} Loading Loading @@ -484,7 +521,7 @@ public class TcpSocketTracker { public boolean isTcpInfoParsingSupported() { // Request tcp info from NetworkStack directly needs extra SELinux permission added // after Q release. return ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q); return mIsTcpInfoParsingSupported; } /** Loading @@ -494,5 +531,16 @@ public class TcpSocketTracker { throws ErrnoException, InterruptedIOException { return NetlinkSocket.recvMessage(fd, DEFAULT_RECV_BUFSIZE, IO_TIMEOUT); } public Context getContext() { return mContext; } /** Add device config change listener */ public void addDeviceConfigChangedListener( @NonNull final DeviceConfig.OnPropertiesChangedListener listener) { DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_CONNECTIVITY, AsyncTask.THREAD_POOL_EXECUTOR, listener); } } }
src/com/android/server/connectivity/NetworkMonitor.java +9 −6 Original line number Diff line number Diff line Loading @@ -126,6 +126,7 @@ import com.android.internal.util.State; import com.android.internal.util.StateMachine; import com.android.internal.util.TrafficStatsConstants; import com.android.networkstack.R; import com.android.networkstack.apishim.ShimUtils; import com.android.networkstack.metrics.DataStallDetectionStats; import com.android.networkstack.metrics.DataStallStatsUtils; import com.android.networkstack.netlink.TcpSocketTracker; Loading Loading @@ -389,13 +390,15 @@ public class NetworkMonitor extends StateMachine { public NetworkMonitor(Context context, INetworkMonitorCallbacks cb, Network network, SharedLog validationLog) { this(context, cb, network, new IpConnectivityLog(), validationLog, Dependencies.DEFAULT, new DataStallStatsUtils()); Dependencies.DEFAULT, new DataStallStatsUtils(), new TcpSocketTracker( new TcpSocketTracker.Dependencies(context, ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q)))); } @VisibleForTesting public NetworkMonitor(Context context, INetworkMonitorCallbacks cb, Network network, IpConnectivityLog logger, SharedLog validationLogs, Dependencies deps, DataStallStatsUtils detectionStatsUtils) { Dependencies deps, DataStallStatsUtils detectionStatsUtils, TcpSocketTracker tst) { // Add suffix indicating which NetworkMonitor we're talking about. super(TAG + "/" + network.toString()); Loading Loading @@ -442,7 +445,7 @@ public class NetworkMonitor extends StateMachine { mDataStallMinEvaluateTime = getDataStallMinEvaluateTime(); mDataStallValidDnsTimeThreshold = getDataStallValidDnsTimeThreshold(); mDataStallEvaluationType = getDataStallEvaluationType(); mTcpTracker = new TcpSocketTracker(new TcpSocketTracker.Dependencies()); mTcpTracker = tst; // Provide empty LinkProperties and NetworkCapabilities to make sure they are never null, // even before notifyNetworkConnected. Loading Loading @@ -2139,7 +2142,7 @@ public class NetworkMonitor extends StateMachine { // 2. Accumulate enough packets count. // TODO: Need to filter per target network. if (dataStallEvaluateTypeEnabled(DATA_STALL_EVALUATION_TYPE_TCP)) { if (getTcpSocketTracker().getSentSinceLastRecv() > 0) { if (getTcpSocketTracker().getLatestReceivedCount() > 0) { result = false; } else if (getTcpSocketTracker().isDataStallSuspected()) { result = true; Loading @@ -2160,8 +2163,8 @@ public class NetworkMonitor extends StateMachine { if (VDBG_STALL) { log("isDataStall: result=" + result + ", consecutive dns timeout count=" + mDnsStallDetector.getConsecutiveTimeoutCount() + ", tcp packets received=" + getTcpSocketTracker().getSentSinceLastRecv() + ", tcp fail rate=" + getTcpSocketTracker().getLatestPacketFailRate()); + ", tcp packets received=" + getTcpSocketTracker().getLatestReceivedCount() + ", tcp fail rate=" + getTcpSocketTracker().getLatestPacketFailPercentage()); } return (result == null) ? false : result; Loading
tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java +20 −23 Original line number Diff line number Diff line Loading @@ -16,13 +16,12 @@ package com.android.networkstack.netlink; import static android.net.util.DataStallUtils.CONFIG_TCP_PACKETS_FAIL_RATE; import static android.net.netlink.NetlinkConstants.SOCKDIAG_MSG_HEADER_SIZE; import static android.net.util.DataStallUtils.CONFIG_TCP_PACKETS_FAIL_PERCENTAGE; import static android.net.util.DataStallUtils.DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE; import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY; import static android.system.OsConstants.AF_INET; import static com.android.networkstack.netlink.TcpSocketTracker.SOCKDIAG_MSG_HEADER_SIZE; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; Loading Loading @@ -111,7 +110,7 @@ public class TcpSocketTrackerTest { "00000000" + // data "0008" + // len = 8 "000F" + // type = 15(INET_DIAG_MARK) "000C0064" + // data, socket mark=786532 "000C1A85" + // data, socket mark=793221 "00AC" + // len = 172 "0002" + // type = 2(INET_DIAG_INFO) // tcp_info Loading @@ -129,7 +128,7 @@ public class TcpSocketTrackerTest { "00000218" + // rcvMss = 536 "00000000" + // unsacked = 0 "00000000" + // acked = 0 "00000005" + // lost = 5 "00000000" + // lost = 0 "00000000" + // retrans = 0 "00000000" + // fackets = 0 "000000BB" + // lastDataSent = 187 Loading Loading @@ -185,7 +184,7 @@ public class TcpSocketTrackerTest { when(mDependencies.connectToKernel()).thenReturn(mMockFd); when(mDependencies.getDeviceConfigPropertyInt( eq(NAMESPACE_CONNECTIVITY), eq(CONFIG_TCP_PACKETS_FAIL_RATE), eq(CONFIG_TCP_PACKETS_FAIL_PERCENTAGE), anyInt())).thenReturn(DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE); } Loading @@ -211,7 +210,7 @@ public class TcpSocketTrackerTest { expected.put(TcpInfo.Field.RCV_MSS, 536); expected.put(TcpInfo.Field.UNACKED, 0); expected.put(TcpInfo.Field.SACKED, 0); expected.put(TcpInfo.Field.LOST, 5); expected.put(TcpInfo.Field.LOST, 0); expected.put(TcpInfo.Field.RETRANS, 0); expected.put(TcpInfo.Field.FACKETS, 0); expected.put(TcpInfo.Field.LAST_DATA_SENT, 187); Loading Loading @@ -242,7 +241,7 @@ public class TcpSocketTrackerTest { expected.put(TcpInfo.Field.DELIVERY_RATE, 0L); assertEquals(parsed.tcpInfo, new TcpInfo(expected)); assertEquals(parsed.fwmark, 786532); assertEquals(parsed.fwmark, 793221); assertEquals(parsed.updateTime, 100); assertEquals(parsed.ipFamily, AF_INET); } Loading @@ -261,18 +260,6 @@ public class TcpSocketTrackerTest { assertFalse(TcpSocketTracker.enoughBytesRemainForValidNlMsg(buffer)); } @Test public void testIsDataStallSuspected() { when(mDependencies.isTcpInfoParsingSupported()).thenReturn(false); final TcpSocketTracker tst = new TcpSocketTracker(mDependencies); assertFalse(tst.isDataStallSuspected()); when(mDependencies.isTcpInfoParsingSupported()).thenReturn(true); assertFalse(tst.isDataStallSuspected()); when(mDependencies.getDeviceConfigPropertyInt(any(), eq(CONFIG_TCP_PACKETS_FAIL_RATE), anyInt())).thenReturn(0); assertTrue(tst.isDataStallSuspected()); } @Test public void testPollSocketsInfo() throws Exception { when(mDependencies.isTcpInfoParsingSupported()).thenReturn(false); Loading @@ -284,20 +271,30 @@ public class TcpSocketTrackerTest { final ByteBuffer invalidBuffer = ByteBuffer.allocate(1); when(mDependencies.recvMesssage(any())).thenReturn(invalidBuffer); assertTrue(tst.pollSocketsInfo()); assertEquals(0, tst.getLatestPacketFailRate()); assertEquals(-1, tst.getLatestPacketFailPercentage()); assertEquals(0, tst.getSentSinceLastRecv()); // Header only. final ByteBuffer headerBuffer = ByteBuffer.wrap(SOCK_DIAG_MSG_BYTES); when(mDependencies.recvMesssage(any())).thenReturn(headerBuffer); assertTrue(tst.pollSocketsInfo()); assertEquals(-1, tst.getLatestPacketFailPercentage()); assertEquals(0, tst.getSentSinceLastRecv()); assertEquals(0, tst.getLatestPacketFailRate()); final ByteBuffer tcpBuffer = ByteBuffer.wrap(TEST_RESPONSE_BYTES); when(mDependencies.recvMesssage(any())).thenReturn(tcpBuffer); assertTrue(tst.pollSocketsInfo()); assertEquals(10, tst.getSentSinceLastRecv()); assertEquals(100, tst.getLatestPacketFailRate()); assertEquals(50, tst.getLatestPacketFailPercentage()); assertFalse(tst.isDataStallSuspected()); // Lower the threshold. when(mDependencies.getDeviceConfigPropertyInt(any(), eq(CONFIG_TCP_PACKETS_FAIL_PERCENTAGE), anyInt())).thenReturn(40); // No device config change. Using cache value. assertFalse(tst.isDataStallSuspected()); // Trigger a config update tst.mConfigListener.onPropertiesChanged(null /* properties */); assertTrue(tst.isDataStallSuspected()); } }