From 4ca0ba8f86f657f42ebae2d389f7a6f92251c01c Mon Sep 17 00:00:00 2001 From: Lorenzo Colitti Date: Wed, 12 Jul 2017 15:48:07 +0900 Subject: [PATCH] Fetch tethering offload stats. Make tethering offload register an ITetheringStatsProvider and fetch tethering stats from the hardware. Currently we fetch stats in the following cases: 1. Just after changing upstreams, we fetch stats from the previous upstream. 2. When we are polled by NetworkStatsService. (cherry-picked from commit 5a7dea1a8eb2cf53fc1d5a52004647de94150e62) Bug: 29337859 Bug: 32163131 Test: builds, boots Test: stats appear in tethering logs Change-Id: If744f2e06cb6a3095a40199936b9afb76eff7b56 Merged-In: If744f2e06cb6a3095a40199936b9afb76eff7b56 --- .../com/android/server/connectivity/Tethering.java | 2 +- .../connectivity/tethering/OffloadController.java | 88 +++++++++++++++- .../android/server/connectivity/TetheringTest.java | 1 + .../tethering/OffloadControllerTest.java | 116 ++++++++++++++++++--- 4 files changed, 190 insertions(+), 17 deletions(-) diff --git a/services/core/java/com/android/server/connectivity/Tethering.java b/services/core/java/com/android/server/connectivity/Tethering.java index 3a4e07e96ad7..60071b143724 100644 --- a/services/core/java/com/android/server/connectivity/Tethering.java +++ b/services/core/java/com/android/server/connectivity/Tethering.java @@ -211,7 +211,7 @@ public class Tethering extends BaseNetworkObserver { final Handler smHandler = mTetherMasterSM.getHandler(); mOffloadController = new OffloadController(smHandler, deps.getOffloadHardwareInterface(smHandler, mLog), - mContext.getContentResolver(), + mContext.getContentResolver(), mNMService, mLog); mUpstreamNetworkMonitor = new UpstreamNetworkMonitor( mContext, mTetherMasterSM, mLog, TetherMasterSM.EVENT_UPSTREAM_CALLBACK); diff --git a/services/core/java/com/android/server/connectivity/tethering/OffloadController.java b/services/core/java/com/android/server/connectivity/tethering/OffloadController.java index b47386705a36..28d390f35202 100644 --- a/services/core/java/com/android/server/connectivity/tethering/OffloadController.java +++ b/services/core/java/com/android/server/connectivity/tethering/OffloadController.java @@ -16,24 +16,36 @@ package com.android.server.connectivity.tethering; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.TrafficStats.UID_TETHERING; import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED; import android.content.ContentResolver; +import android.net.ITetheringStatsProvider; import android.net.IpPrefix; import android.net.LinkAddress; import android.net.LinkProperties; +import android.net.NetworkStats; import android.net.RouteInfo; import android.net.util.SharedLog; import android.os.Handler; +import android.os.INetworkManagementService; +import android.os.RemoteException; +import android.os.SystemClock; import android.provider.Settings; +import android.text.TextUtils; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.Objects; import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; /** * A class to encapsulate the business logic of programming the tethering @@ -44,6 +56,8 @@ import java.util.Set; public class OffloadController { private static final String TAG = OffloadController.class.getSimpleName(); + private static final int STATS_FETCH_TIMEOUT_MS = 1000; + private final Handler mHandler; private final OffloadHardwareInterface mHwInterface; private final ContentResolver mContentResolver; @@ -59,14 +73,25 @@ public class OffloadController { // prefixes representing only the locally-assigned IP addresses. private Set mLastLocalPrefixStrs; + // Maps upstream interface names to offloaded traffic statistics. + private HashMap + mForwardedStats = new HashMap<>(); + public OffloadController(Handler h, OffloadHardwareInterface hwi, - ContentResolver contentResolver, SharedLog log) { + ContentResolver contentResolver, INetworkManagementService nms, SharedLog log) { mHandler = h; mHwInterface = hwi; mContentResolver = contentResolver; mLog = log.forSubComponent(TAG); mExemptPrefixes = new HashSet<>(); mLastLocalPrefixStrs = new HashSet<>(); + + try { + nms.registerTetheringStatsProvider( + new OffloadTetheringStatsProvider(), getClass().getSimpleName()); + } catch (RemoteException e) { + mLog.e("Cannot register offload stats provider: " + e); + } } public void start() { @@ -138,6 +163,7 @@ public class OffloadController { public void stop() { final boolean wasStarted = started(); + updateStatsForCurrentUpstream(); mUpstreamLinkProperties = null; mHwInterface.stopOffloadControl(); mControlInitialized = false; @@ -145,16 +171,76 @@ public class OffloadController { if (wasStarted) mLog.log("tethering offload stopped"); } + private class OffloadTetheringStatsProvider extends ITetheringStatsProvider.Stub { + @Override + public NetworkStats getTetherStats() { + NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 0); + CountDownLatch latch = new CountDownLatch(1); + + mHandler.post(() -> { + try { + NetworkStats.Entry entry = new NetworkStats.Entry(); + entry.set = SET_DEFAULT; + entry.tag = TAG_NONE; + entry.uid = UID_TETHERING; + + updateStatsForCurrentUpstream(); + + for (String iface : mForwardedStats.keySet()) { + entry.iface = iface; + entry.rxBytes = mForwardedStats.get(iface).rxBytes; + entry.txBytes = mForwardedStats.get(iface).txBytes; + stats.addValues(entry); + } + } finally { + latch.countDown(); + } + }); + + try { + latch.await(STATS_FETCH_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + mLog.e("Tethering stats fetch timed out after " + STATS_FETCH_TIMEOUT_MS + "ms"); + } + + return stats; + } + } + + private void maybeUpdateStats(String iface) { + if (TextUtils.isEmpty(iface)) { + return; + } + + if (!mForwardedStats.containsKey(iface)) { + mForwardedStats.put(iface, new OffloadHardwareInterface.ForwardedStats()); + } + mForwardedStats.get(iface).add(mHwInterface.getForwardedStats(iface)); + } + + private void updateStatsForCurrentUpstream() { + if (mUpstreamLinkProperties != null) { + maybeUpdateStats(mUpstreamLinkProperties.getInterfaceName()); + } + } + public void setUpstreamLinkProperties(LinkProperties lp) { if (!started() || Objects.equals(mUpstreamLinkProperties, lp)) return; + String prevUpstream = (mUpstreamLinkProperties != null) ? + mUpstreamLinkProperties.getInterfaceName() : null; + mUpstreamLinkProperties = (lp != null) ? new LinkProperties(lp) : null; + // TODO: examine return code and decide what to do if programming // upstream parameters fails (probably just wait for a subsequent // onOffloadEvent() callback to tell us offload is available again and // then reapply all state). computeAndPushLocalPrefixes(); pushUpstreamParameters(); + + // Update stats after we've told the hardware to change routing so we don't miss packets. + maybeUpdateStats(prevUpstream); } public void setLocalPrefixes(Set localPrefixes) { diff --git a/tests/net/java/com/android/server/connectivity/TetheringTest.java b/tests/net/java/com/android/server/connectivity/TetheringTest.java index 460d53e8f522..6f048e2ccd2f 100644 --- a/tests/net/java/com/android/server/connectivity/TetheringTest.java +++ b/tests/net/java/com/android/server/connectivity/TetheringTest.java @@ -178,6 +178,7 @@ public class TetheringTest { mTethering = new Tethering(mServiceContext, mNMService, mStatsService, mPolicyManager, mLooper.getLooper(), mSystemProperties, mTetheringDependencies); + verify(mNMService).registerTetheringStatsProvider(any(), anyString()); } @After diff --git a/tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java b/tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java index 789ce6c3092a..45525e624d90 100644 --- a/tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java +++ b/tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java @@ -16,26 +16,38 @@ package com.android.server.connectivity.tethering; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.TrafficStats.UID_TETHERING; import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED; +import static com.android.server.connectivity.tethering.OffloadHardwareInterface.ForwardedStats; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; import android.content.pm.ApplicationInfo; +import android.net.ITetheringStatsProvider; import android.net.IpPrefix; import android.net.LinkAddress; import android.net.LinkProperties; +import android.net.NetworkStats; import android.net.RouteInfo; import android.net.util.SharedLog; +import android.os.Handler; +import android.os.Looper; +import android.os.INetworkManagementService; import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; @@ -66,11 +78,14 @@ public class OffloadControllerTest { @Mock private OffloadHardwareInterface mHardware; @Mock private ApplicationInfo mApplicationInfo; @Mock private Context mContext; + @Mock private INetworkManagementService mNMService; private final ArgumentCaptor mStringArrayCaptor = ArgumentCaptor.forClass(ArrayList.class); + private final ArgumentCaptor mTetherStatsProviderCaptor = + ArgumentCaptor.forClass(ITetheringStatsProvider.Stub.class); private MockContentResolver mContentResolver; - @Before public void setUp() throws Exception { + @Before public void setUp() { MockitoAnnotations.initMocks(this); when(mContext.getApplicationInfo()).thenReturn(mApplicationInfo); when(mContext.getPackageName()).thenReturn("OffloadControllerTest"); @@ -90,15 +105,24 @@ public class OffloadControllerTest { when(mHardware.initOffloadConfig()).thenReturn(true); when(mHardware.initOffloadControl(any(OffloadHardwareInterface.ControlCallback.class))) .thenReturn(true); + when(mHardware.getForwardedStats(any())).thenReturn(new ForwardedStats()); } private void enableOffload() { Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 0); } + private OffloadController makeOffloadController() throws Exception { + OffloadController offload = new OffloadController(new Handler(Looper.getMainLooper()), + mHardware, mContentResolver, mNMService, new SharedLog("test")); + verify(mNMService).registerTetheringStatsProvider( + mTetherStatsProviderCaptor.capture(), anyString()); + return offload; + } + // TODO: Restore when FakeSettingsProvider.clearSettingsProvider() is available. // @Test - public void testNoSettingsValueDefaultDisabledDoesNotStart() { + public void testNoSettingsValueDefaultDisabledDoesNotStart() throws Exception { setupFunctioningHardwareInterface(); when(mHardware.getDefaultTetherOffloadDisabled()).thenReturn(1); try { @@ -106,8 +130,7 @@ public class OffloadControllerTest { fail(); } catch (SettingNotFoundException expected) {} - final OffloadController offload = - new OffloadController(null, mHardware, mContentResolver, new SharedLog("test")); + final OffloadController offload = makeOffloadController(); offload.start(); final InOrder inOrder = inOrder(mHardware); @@ -120,7 +143,7 @@ public class OffloadControllerTest { // TODO: Restore when FakeSettingsProvider.clearSettingsProvider() is available. // @Test - public void testNoSettingsValueDefaultEnabledDoesStart() { + public void testNoSettingsValueDefaultEnabledDoesStart() throws Exception { setupFunctioningHardwareInterface(); when(mHardware.getDefaultTetherOffloadDisabled()).thenReturn(0); try { @@ -128,8 +151,7 @@ public class OffloadControllerTest { fail(); } catch (SettingNotFoundException expected) {} - final OffloadController offload = - new OffloadController(null, mHardware, mContentResolver, new SharedLog("test")); + final OffloadController offload = makeOffloadController(); offload.start(); final InOrder inOrder = inOrder(mHardware); @@ -141,12 +163,11 @@ public class OffloadControllerTest { } @Test - public void testSettingsAllowsStart() { + public void testSettingsAllowsStart() throws Exception { setupFunctioningHardwareInterface(); Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 0); - final OffloadController offload = - new OffloadController(null, mHardware, mContentResolver, new SharedLog("test")); + final OffloadController offload = makeOffloadController(); offload.start(); final InOrder inOrder = inOrder(mHardware); @@ -158,12 +179,11 @@ public class OffloadControllerTest { } @Test - public void testSettingsDisablesStart() { + public void testSettingsDisablesStart() throws Exception { setupFunctioningHardwareInterface(); Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 1); - final OffloadController offload = - new OffloadController(null, mHardware, mContentResolver, new SharedLog("test")); + final OffloadController offload = makeOffloadController(); offload.start(); final InOrder inOrder = inOrder(mHardware); @@ -178,8 +198,7 @@ public class OffloadControllerTest { setupFunctioningHardwareInterface(); enableOffload(); - final OffloadController offload = - new OffloadController(null, mHardware, mContentResolver, new SharedLog("test")); + final OffloadController offload = makeOffloadController(); offload.start(); final InOrder inOrder = inOrder(mHardware); @@ -244,6 +263,7 @@ public class OffloadControllerTest { inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture()); inOrder.verify(mHardware, times(1)).setUpstreamParameters( eq(testIfName), eq(ipv4Addr), eq(null), eq(null)); + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName)); inOrder.verifyNoMoreInteractions(); final String ipv4Gateway = "192.0.2.1"; @@ -253,6 +273,7 @@ public class OffloadControllerTest { inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture()); inOrder.verify(mHardware, times(1)).setUpstreamParameters( eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), eq(null)); + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName)); inOrder.verifyNoMoreInteractions(); final String ipv6Gw1 = "fe80::cafe"; @@ -262,6 +283,7 @@ public class OffloadControllerTest { inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture()); inOrder.verify(mHardware, times(1)).setUpstreamParameters( eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName)); ArrayList v6gws = mStringArrayCaptor.getValue(); assertEquals(1, v6gws.size()); assertTrue(v6gws.contains(ipv6Gw1)); @@ -274,6 +296,7 @@ public class OffloadControllerTest { inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture()); inOrder.verify(mHardware, times(1)).setUpstreamParameters( eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName)); v6gws = mStringArrayCaptor.getValue(); assertEquals(2, v6gws.size()); assertTrue(v6gws.contains(ipv6Gw1)); @@ -291,6 +314,7 @@ public class OffloadControllerTest { inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture()); inOrder.verify(mHardware, times(1)).setUpstreamParameters( eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName)); v6gws = mStringArrayCaptor.getValue(); assertEquals(2, v6gws.size()); assertTrue(v6gws.contains(ipv6Gw1)); @@ -325,6 +349,7 @@ public class OffloadControllerTest { assertEquals(2, v6gws.size()); assertTrue(v6gws.contains(ipv6Gw1)); assertTrue(v6gws.contains(ipv6Gw2)); + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName)); inOrder.verifyNoMoreInteractions(); // Completely identical LinkProperties updates are de-duped. @@ -335,4 +360,65 @@ public class OffloadControllerTest { anyObject(), anyObject(), anyObject(), anyObject()); inOrder.verifyNoMoreInteractions(); } + + private void assertNetworkStats(String iface, ForwardedStats stats, NetworkStats.Entry entry) { + assertEquals(iface, entry.iface); + assertEquals(stats.rxBytes, entry.rxBytes); + assertEquals(stats.txBytes, entry.txBytes); + assertEquals(SET_DEFAULT, entry.set); + assertEquals(TAG_NONE, entry.tag); + assertEquals(UID_TETHERING, entry.uid); + } + + @Test + public void testGetForwardedStats() throws Exception { + setupFunctioningHardwareInterface(); + enableOffload(); + + final OffloadController offload = makeOffloadController(); + offload.start(); + + final String ethernetIface = "eth1"; + final String mobileIface = "rmnet_data0"; + + ForwardedStats ethernetStats = new ForwardedStats(); + ethernetStats.rxBytes = 12345; + ethernetStats.txBytes = 54321; + + ForwardedStats mobileStats = new ForwardedStats(); + mobileStats.rxBytes = 999; + mobileStats.txBytes = 99999; + + when(mHardware.getForwardedStats(eq(ethernetIface))).thenReturn(ethernetStats); + when(mHardware.getForwardedStats(eq(mobileIface))).thenReturn(mobileStats); + + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(ethernetIface); + offload.setUpstreamLinkProperties(lp); + + lp.setInterfaceName(mobileIface); + offload.setUpstreamLinkProperties(lp); + + lp.setInterfaceName(ethernetIface); + offload.setUpstreamLinkProperties(lp); + + ethernetStats.rxBytes = 100000; + ethernetStats.txBytes = 100000; + offload.setUpstreamLinkProperties(null); + + NetworkStats stats = mTetherStatsProviderCaptor.getValue().getTetherStats(); + assertEquals(2, stats.size()); + + NetworkStats.Entry entry = null; + int ethernetPosition = ethernetIface.equals(stats.getValues(0, entry).iface) ? 0 : 1; + int mobilePosition = 1 - ethernetPosition; + + entry = stats.getValues(mobilePosition, entry); + assertNetworkStats(mobileIface, mobileStats, entry); + + ethernetStats.rxBytes = 12345 + 100000; + ethernetStats.txBytes = 54321 + 100000; + entry = stats.getValues(ethernetPosition, entry); + assertNetworkStats(ethernetIface, ethernetStats, entry); + } } -- 2.11.0