[NFCT.TETHER.3] Migrate tetherOffloadGetStats from netd to mainline
A preparation for updating BPF map in mainline module.
Test: TetheringCoverageTests
Change-Id: Ie73f7b4d9b191e62cfdfe2cfa3360cc7210f17e8
diff --git a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
index 5cf0384..e21325e 100644
--- a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
@@ -17,14 +17,18 @@
package com.android.networkstack.tethering.apishim.api30;
import android.net.INetd;
+import android.net.TetherStatsParcel;
import android.net.util.SharedLog;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
+import android.util.SparseArray;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.TetherStatsValue;
/**
* Bpf coordinator class for API shims.
@@ -61,6 +65,36 @@
};
@Override
+ @Nullable
+ public SparseArray<TetherStatsValue> tetherOffloadGetStats() {
+ final TetherStatsParcel[] tetherStatsList;
+ try {
+ // The reported tether stats are total data usage for all currently-active upstream
+ // interfaces since tethering start. There will only ever be one entry for a given
+ // interface index.
+ tetherStatsList = mNetd.tetherOffloadGetStats();
+ } catch (RemoteException | ServiceSpecificException e) {
+ mLog.e("Fail to fetch tethering stats from netd: " + e);
+ return null;
+ }
+
+ return toTetherStatsValueSparseArray(tetherStatsList);
+ }
+
+ @NonNull
+ private SparseArray<TetherStatsValue> toTetherStatsValueSparseArray(
+ @NonNull final TetherStatsParcel[] parcels) {
+ final SparseArray<TetherStatsValue> tetherStatsList = new SparseArray<TetherStatsValue>();
+
+ for (TetherStatsParcel p : parcels) {
+ tetherStatsList.put(p.ifIndex, new TetherStatsValue(p.rxPackets, p.rxBytes,
+ 0 /* rxErrors */, p.txPackets, p.txBytes, 0 /* txErrors */));
+ }
+
+ return tetherStatsList;
+ }
+
+ @Override
public String toString() {
return "Netd used";
}
diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
index 03616ca..d00ccc2 100644
--- a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
@@ -18,6 +18,7 @@
import android.net.util.SharedLog;
import android.system.ErrnoException;
+import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -27,6 +28,8 @@
import com.android.networkstack.tethering.BpfMap;
import com.android.networkstack.tethering.TetherIngressKey;
import com.android.networkstack.tethering.TetherIngressValue;
+import com.android.networkstack.tethering.TetherStatsKey;
+import com.android.networkstack.tethering.TetherStatsValue;
/**
* Bpf coordinator class for API shims.
@@ -43,14 +46,19 @@
@Nullable
private final BpfMap<TetherIngressKey, TetherIngressValue> mBpfIngressMap;
+ // BPF map of tethering statistics of the upstream interface since tethering startup.
+ @Nullable
+ private final BpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap;
+
public BpfCoordinatorShimImpl(@NonNull final Dependencies deps) {
mLog = deps.getSharedLog().forSubComponent(TAG);
mBpfIngressMap = deps.getBpfIngressMap();
+ mBpfStatsMap = deps.getBpfStatsMap();
}
@Override
public boolean isInitialized() {
- return mBpfIngressMap != null;
+ return mBpfIngressMap != null && mBpfStatsMap != null;
}
@Override
@@ -71,9 +79,28 @@
}
@Override
+ @Nullable
+ public SparseArray<TetherStatsValue> tetherOffloadGetStats() {
+ if (!isInitialized()) return null;
+
+ final SparseArray<TetherStatsValue> tetherStatsList = new SparseArray<TetherStatsValue>();
+ try {
+ // The reported tether stats are total data usage for all currently-active upstream
+ // interfaces since tethering start.
+ mBpfStatsMap.forEach((key, value) -> tetherStatsList.put((int) key.ifindex, value));
+ } catch (ErrnoException e) {
+ mLog.e("Fail to fetch tethering stats from BPF map: ", e);
+ return null;
+ }
+ return tetherStatsList;
+ }
+
+ @Override
public String toString() {
return "mBpfIngressMap{"
- + (mBpfIngressMap != null ? "initialized" : "not initialized") + "} "
+ + (mBpfIngressMap != null ? "initialized" : "not initialized") + "}, "
+ + "mBpfStatsMap{"
+ + (mBpfStatsMap != null ? "initialized" : "not initialized") + "} "
+ "}";
}
}
diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
index bcb644c..97d3402 100644
--- a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
+++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
@@ -16,10 +16,14 @@
package com.android.networkstack.tethering.apishim.common;
+import android.util.SparseArray;
+
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.TetherStatsValue;
/**
* Bpf coordinator class for API shims.
@@ -54,5 +58,15 @@
* @param rule The rule to add or update.
*/
public abstract boolean tetherOffloadRuleAdd(@NonNull Ipv6ForwardingRule rule);
+
+ /**
+ * Return BPF tethering offload statistics.
+ *
+ * @return an array of TetherStatsValue's, where each entry contains the upstream interface
+ * index and its tethering statistics since tethering was first started.
+ * There will only ever be one entry for a given interface index.
+ */
+ @Nullable
+ public abstract SparseArray<TetherStatsValue> tetherOffloadGetStats();
}
diff --git a/Tethering/src/android/net/util/TetheringUtils.java b/Tethering/src/android/net/util/TetheringUtils.java
index 53b54f7..706d78c 100644
--- a/Tethering/src/android/net/util/TetheringUtils.java
+++ b/Tethering/src/android/net/util/TetheringUtils.java
@@ -21,6 +21,8 @@
import androidx.annotation.NonNull;
+import com.android.networkstack.tethering.TetherStatsValue;
+
import java.io.FileDescriptor;
import java.net.Inet6Address;
import java.net.SocketException;
@@ -91,6 +93,13 @@
txPackets = tetherStats.txPackets;
}
+ public ForwardedStats(@NonNull TetherStatsValue tetherStats) {
+ rxBytes = tetherStats.rxBytes;
+ rxPackets = tetherStats.rxPackets;
+ txBytes = tetherStats.txBytes;
+ txPackets = tetherStats.txPackets;
+ }
+
public ForwardedStats(@NonNull ForwardedStats other) {
rxBytes = other.rxBytes;
rxPackets = other.rxPackets;
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index d890e08..4afbed2 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -78,6 +78,8 @@
private static final int DUMP_TIMEOUT_MS = 10_000;
private static final String TETHER_INGRESS_FS_PATH =
"/sys/fs/bpf/map_offload_tether_ingress_map";
+ private static final String TETHER_STATS_MAP_PATH =
+ "/sys/fs/bpf/map_offload_tether_stats_map";
@VisibleForTesting
enum StatsType {
@@ -157,7 +159,7 @@
// Runnable that used by scheduling next polling of stats.
private final Runnable mScheduledPollingTask = () -> {
- updateForwardedStatsFromNetd();
+ updateForwardedStats();
maybeSchedulePollingStats();
};
@@ -199,6 +201,17 @@
return null;
}
}
+
+ /** Get stats BPF map. */
+ @Nullable public BpfMap<TetherStatsKey, TetherStatsValue> getBpfStatsMap() {
+ try {
+ return new BpfMap<>(TETHER_STATS_MAP_PATH,
+ BpfMap.BPF_F_RDWR, TetherStatsKey.class, TetherStatsValue.class);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot create stats map: " + e);
+ return null;
+ }
+ }
}
@VisibleForTesting
@@ -261,7 +274,7 @@
if (mHandler.hasCallbacks(mScheduledPollingTask)) {
mHandler.removeCallbacks(mScheduledPollingTask);
}
- updateForwardedStatsFromNetd();
+ updateForwardedStats();
mPollingStarted = false;
mLog.i("Polling stopped");
@@ -344,8 +357,14 @@
try {
final TetherStatsParcel stats =
mNetd.tetherOffloadGetAndClearStats(upstreamIfindex);
+ SparseArray<TetherStatsValue> tetherStatsList =
+ new SparseArray<TetherStatsValue>();
+ tetherStatsList.put(stats.ifIndex, new TetherStatsValue(stats.rxPackets,
+ stats.rxBytes, 0 /* rxErrors */, stats.txPackets, stats.txBytes,
+ 0 /* txErrors */));
+
// Update the last stats delta and delete the local cache for a given upstream.
- updateQuotaAndStatsFromSnapshot(new TetherStatsParcel[] {stats});
+ updateQuotaAndStatsFromSnapshot(tetherStatsList);
mStats.remove(upstreamIfindex);
} catch (RemoteException | ServiceSpecificException e) {
Log.wtf(TAG, "Exception when cleanup tether stats for upstream index "
@@ -743,10 +762,11 @@
}
private void updateQuotaAndStatsFromSnapshot(
- @NonNull final TetherStatsParcel[] tetherStatsList) {
+ @NonNull final SparseArray<TetherStatsValue> tetherStatsList) {
long usedAlertQuota = 0;
- for (TetherStatsParcel tetherStats : tetherStatsList) {
- final Integer ifIndex = tetherStats.ifIndex;
+ for (int i = 0; i < tetherStatsList.size(); i++) {
+ final Integer ifIndex = tetherStatsList.keyAt(i);
+ final TetherStatsValue tetherStats = tetherStatsList.valueAt(i);
final ForwardedStats curr = new ForwardedStats(tetherStats);
final ForwardedStats base = mStats.get(ifIndex);
final ForwardedStats diff = (base != null) ? curr.subtract(base) : curr;
@@ -778,16 +798,15 @@
// TODO: Count the used limit quota for notifying data limit reached.
}
- private void updateForwardedStatsFromNetd() {
- final TetherStatsParcel[] tetherStatsList;
- try {
- // The reported tether stats are total data usage for all currently-active upstream
- // interfaces since tethering start.
- tetherStatsList = mNetd.tetherOffloadGetStats();
- } catch (RemoteException | ServiceSpecificException e) {
- mLog.e("Problem fetching tethering stats: ", e);
+ private void updateForwardedStats() {
+ final SparseArray<TetherStatsValue> tetherStatsList =
+ mBpfCoordinatorShim.tetherOffloadGetStats();
+
+ if (tetherStatsList == null) {
+ mLog.e("Problem fetching tethering stats");
return;
}
+
updateQuotaAndStatsFromSnapshot(tetherStatsList);
}
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfMap.java b/Tethering/src/com/android/networkstack/tethering/BpfMap.java
index 69ad1b6..78d212c 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfMap.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfMap.java
@@ -23,6 +23,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.Struct;
import java.nio.ByteBuffer;
@@ -76,6 +77,21 @@
mValueSize = Struct.getSize(value);
}
+ /**
+ * Constructor for testing only.
+ * The derived class implements an internal mocked map. It need to implement all functions
+ * which are related with the native BPF map because the BPF map handler is not initialized.
+ * See BpfCoordinatorTest#TestBpfMap.
+ */
+ @VisibleForTesting
+ protected BpfMap(final Class<K> key, final Class<V> value) {
+ mMapFd = -1;
+ mKeyClass = key;
+ mValueClass = value;
+ mKeySize = Struct.getSize(key);
+ mValueSize = Struct.getSize(value);
+ }
+
/**
* Update an existing or create a new key -> value entry in an eBbpf map.
*/
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java b/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java
new file mode 100644
index 0000000..5442480
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/** The key of BpfMap which is used for tethering stats. */
+public class TetherStatsKey extends Struct {
+ @Field(order = 0, type = Type.U32)
+ public final long ifindex; // upstream interface index
+
+ public TetherStatsKey(final long ifindex) {
+ this.ifindex = ifindex;
+ }
+
+ // TODO: remove equals, hashCode and toString once aosp/1536721 is merged.
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+
+ if (!(obj instanceof TetherStatsKey)) return false;
+
+ final TetherStatsKey that = (TetherStatsKey) obj;
+
+ return ifindex == that.ifindex;
+ }
+
+ @Override
+ public int hashCode() {
+ return Long.hashCode(ifindex);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ifindex: %d", ifindex);
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java b/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java
new file mode 100644
index 0000000..844d2e8
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/** The key of BpfMap which is used for tethering stats. */
+public class TetherStatsValue extends Struct {
+ // Use the signed long variable to store the uint64 stats from stats BPF map.
+ // U63 is enough for each data element even at 5Gbps for ~468 years.
+ // 2^63 / (5 * 1000 * 1000 * 1000) * 8 / 86400 / 365 = 468.
+ @Field(order = 0, type = Type.U63)
+ public final long rxPackets;
+ @Field(order = 1, type = Type.U63)
+ public final long rxBytes;
+ @Field(order = 2, type = Type.U63)
+ public final long rxErrors;
+ @Field(order = 3, type = Type.U63)
+ public final long txPackets;
+ @Field(order = 4, type = Type.U63)
+ public final long txBytes;
+ @Field(order = 5, type = Type.U63)
+ public final long txErrors;
+
+ public TetherStatsValue(final long rxPackets, final long rxBytes, final long rxErrors,
+ final long txPackets, final long txBytes, final long txErrors) {
+ this.rxPackets = rxPackets;
+ this.rxBytes = rxBytes;
+ this.rxErrors = rxErrors;
+ this.txPackets = txPackets;
+ this.txBytes = txBytes;
+ this.txErrors = txErrors;
+ }
+
+ // TODO: remove equals, hashCode and toString once aosp/1536721 is merged.
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+
+ if (!(obj instanceof TetherStatsValue)) return false;
+
+ final TetherStatsValue that = (TetherStatsValue) obj;
+
+ return rxPackets == that.rxPackets
+ && rxBytes == that.rxBytes
+ && rxErrors == that.rxErrors
+ && txPackets == that.txPackets
+ && txBytes == that.txBytes
+ && txErrors == that.txErrors;
+ }
+
+ @Override
+ public int hashCode() {
+ return Long.hashCode(rxPackets) ^ Long.hashCode(rxBytes) ^ Long.hashCode(rxErrors)
+ ^ Long.hashCode(txPackets) ^ Long.hashCode(txBytes) ^ Long.hashCode(txErrors);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("rxPackets: %s, rxBytes: %s, rxErrors: %s, txPackets: %s, "
+ + "txBytes: %s, txErrors: %s", rxPackets, rxBytes, rxErrors, txPackets,
+ txBytes, txErrors);
+ }
+}
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index dae19b7..4ae1552 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -106,6 +106,8 @@
import com.android.networkstack.tethering.PrivateAddressCoordinator;
import com.android.networkstack.tethering.TetherIngressKey;
import com.android.networkstack.tethering.TetherIngressValue;
+import com.android.networkstack.tethering.TetherStatsKey;
+import com.android.networkstack.tethering.TetherStatsValue;
import com.android.networkstack.tethering.TetheringConfiguration;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
@@ -169,6 +171,7 @@
@Mock private NetworkStatsManager mStatsManager;
@Mock private TetheringConfiguration mTetherConfig;
@Mock private BpfMap<TetherIngressKey, TetherIngressValue> mBpfIngressMap;
+ @Mock private BpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap;
@Captor private ArgumentCaptor<DhcpServingParamsParcel> mDhcpParamsCaptor;
@@ -293,6 +296,11 @@
public BpfMap<TetherIngressKey, TetherIngressValue> getBpfIngressMap() {
return mBpfIngressMap;
}
+
+ @Nullable
+ public BpfMap<TetherStatsKey, TetherStatsValue> getBpfStatsMap() {
+ return mBpfStatsMap;
+ }
};
mBpfCoordinator = spy(new BpfCoordinator(mBpfDeps));
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index b920fa8..058e199 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -61,6 +61,7 @@
import android.os.Build;
import android.os.Handler;
import android.os.test.TestLooper;
+import android.system.ErrnoException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -68,6 +69,7 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.Struct;
import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.TestableNetworkStatsProviderCbBinder;
@@ -85,7 +87,10 @@
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -97,6 +102,32 @@
private static final MacAddress MAC_A = MacAddress.fromString("00:00:00:00:00:0a");
private static final MacAddress MAC_B = MacAddress.fromString("11:22:33:00:00:0b");
+ // The test fake BPF map class is needed because the test has no privilege to access the BPF
+ // map. All member functions which eventually call JNI to access the real native BPF map need
+ // to be overridden.
+ // TODO: consider moving to an individual file.
+ private class TestBpfMap<K extends Struct, V extends Struct> extends BpfMap<K, V> {
+ private final HashMap<K, V> mMap = new HashMap<K, V>();
+
+ TestBpfMap(final Class<K> key, final Class<V> value) {
+ super(key, value);
+ }
+
+ @Override
+ public void forEach(BiConsumer<K, V> action) throws ErrnoException {
+ // TODO: consider using mocked #getFirstKey and #getNextKey to iterate. It helps to
+ // implement the entry deletion in the iteration if required.
+ for (Map.Entry<K, V> entry : mMap.entrySet()) {
+ action.accept(entry.getKey(), entry.getValue());
+ }
+ }
+
+ @Override
+ public void updateEntry(K key, V value) throws ErrnoException {
+ mMap.put(key, value);
+ }
+ };
+
@Mock private NetworkStatsManager mStatsManager;
@Mock private INetd mNetd;
@Mock private IpServer mIpServer;
@@ -109,6 +140,8 @@
private final ArgumentCaptor<ArrayList> mStringArrayCaptor =
ArgumentCaptor.forClass(ArrayList.class);
private final TestLooper mTestLooper = new TestLooper();
+ private final TestBpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap =
+ spy(new TestBpfMap<>(TetherStatsKey.class, TetherStatsValue.class));
private BpfCoordinator.Dependencies mDeps =
spy(new BpfCoordinator.Dependencies() {
@NonNull
@@ -140,6 +173,11 @@
public BpfMap<TetherIngressKey, TetherIngressValue> getBpfIngressMap() {
return mBpfIngressMap;
}
+
+ @Nullable
+ public BpfMap<TetherStatsKey, TetherStatsValue> getBpfStatsMap() {
+ return mBpfStatsMap;
+ }
});
@Before public void setUp() {
@@ -190,14 +228,44 @@
return parcel;
}
- // Set up specific tether stats list and wait for the stats cache is updated by polling thread
+ // Update a stats entry or create if not exists.
+ private void updateStatsEntry(@NonNull TetherStatsParcel stats) throws Exception {
+ if (mDeps.isAtLeastS()) {
+ final TetherStatsKey key = new TetherStatsKey(stats.ifIndex);
+ final TetherStatsValue value = new TetherStatsValue(stats.rxPackets, stats.rxBytes,
+ 0L /* rxErrors */, stats.txPackets, stats.txBytes, 0L /* txErrors */);
+ mBpfStatsMap.updateEntry(key, value);
+ } else {
+ when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[] {stats});
+ }
+ }
+
+ // Update specific tether stats list and wait for the stats cache is updated by polling thread
// in the coordinator. Beware of that it is only used for the default polling interval.
- private void setTetherOffloadStatsList(TetherStatsParcel[] tetherStatsList) throws Exception {
- when(mNetd.tetherOffloadGetStats()).thenReturn(tetherStatsList);
+ // Note that the mocked tetherOffloadGetStats of netd replaces all stats entries because it
+ // doesn't store the previous entries.
+ private void updateStatsEntriesAndWaitForUpdate(@NonNull TetherStatsParcel[] tetherStatsList)
+ throws Exception {
+ if (mDeps.isAtLeastS()) {
+ for (TetherStatsParcel stats : tetherStatsList) {
+ updateStatsEntry(stats);
+ }
+ } else {
+ when(mNetd.tetherOffloadGetStats()).thenReturn(tetherStatsList);
+ }
+
mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
waitForIdle();
}
+ private void clearStatsInvocations() {
+ if (mDeps.isAtLeastS()) {
+ clearInvocations(mBpfStatsMap);
+ } else {
+ clearInvocations(mNetd);
+ }
+ }
+
private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
if (inOrder != null) {
return inOrder.verify(t);
@@ -206,6 +274,22 @@
}
}
+ private void verifyTetherOffloadGetStats() throws Exception {
+ if (mDeps.isAtLeastS()) {
+ verify(mBpfStatsMap).forEach(any());
+ } else {
+ verify(mNetd).tetherOffloadGetStats();
+ }
+ }
+
+ private void verifyNeverTetherOffloadGetStats() throws Exception {
+ if (mDeps.isAtLeastS()) {
+ verify(mBpfStatsMap, never()).forEach(any());
+ } else {
+ verify(mNetd, never()).tetherOffloadGetStats();
+ }
+ }
+
private void verifyTetherOffloadRuleAdd(@Nullable InOrder inOrder,
@NonNull Ipv6ForwardingRule rule) throws Exception {
if (mDeps.isAtLeastS()) {
@@ -224,6 +308,11 @@
}
}
+ // S+ and R api minimum tests.
+ // The following tests are used to provide minimum checking for the APIs on different flow.
+ // The auto merge is not enabled on mainline prod. The code flow R may be verified at the
+ // late stage by manual cherry pick. It is risky if the R code flow has broken and be found at
+ // the last minute.
// TODO: remove once presubmit tests on R even the code is submitted on S.
private void checkTetherOffloadRuleAdd(boolean usingApiS) throws Exception {
setupFunctioningNetdInterface();
@@ -255,6 +344,43 @@
checkTetherOffloadRuleAdd(true /* S+ */);
}
+ // TODO: remove once presubmit tests on R even the code is submitted on S.
+ private void checkTetherOffloadGetStats(boolean usingApiS) throws Exception {
+ setupFunctioningNetdInterface();
+
+ doReturn(usingApiS).when(mDeps).isAtLeastS();
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ coordinator.startPolling();
+
+ final String mobileIface = "rmnet_data0";
+ final Integer mobileIfIndex = 100;
+ coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+
+ updateStatsEntriesAndWaitForUpdate(new TetherStatsParcel[] {
+ buildTestTetherStatsParcel(mobileIfIndex, 1000, 100, 2000, 200)});
+
+ final NetworkStats expectedIfaceStats = new NetworkStats(0L, 1)
+ .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 1000, 100, 2000, 200));
+
+ final NetworkStats expectedUidStats = new NetworkStats(0L, 1)
+ .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 1000, 100, 2000, 200));
+
+ mTetherStatsProvider.pushTetherStats();
+ mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStats, expectedUidStats);
+ }
+
+ // TODO: remove once presubmit tests on R even the code is submitted on S.
+ @Test
+ public void testTetherOffloadGetStatsSdkR() throws Exception {
+ checkTetherOffloadGetStats(false /* R */);
+ }
+
+ // TODO: remove once presubmit tests on R even the code is submitted on S.
+ @Test
+ public void testTetherOffloadGetStatsAtLeastSdkS() throws Exception {
+ checkTetherOffloadGetStats(true /* S+ */);
+ }
+
@Test
public void testGetForwardedStats() throws Exception {
setupFunctioningNetdInterface();
@@ -275,7 +401,7 @@
// [1] Both interface stats are changed.
// Setup the tether stats of wlan and mobile interface. Note that move forward the time of
// the looper to make sure the new tether stats has been updated by polling update thread.
- setTetherOffloadStatsList(new TetherStatsParcel[] {
+ updateStatsEntriesAndWaitForUpdate(new TetherStatsParcel[] {
buildTestTetherStatsParcel(wlanIfIndex, 1000, 100, 2000, 200),
buildTestTetherStatsParcel(mobileIfIndex, 3000, 300, 4000, 400)});
@@ -296,7 +422,7 @@
// [2] Only one interface stats is changed.
// The tether stats of mobile interface is accumulated and The tether stats of wlan
// interface is the same.
- setTetherOffloadStatsList(new TetherStatsParcel[] {
+ updateStatsEntriesAndWaitForUpdate(new TetherStatsParcel[] {
buildTestTetherStatsParcel(wlanIfIndex, 1000, 100, 2000, 200),
buildTestTetherStatsParcel(mobileIfIndex, 3010, 320, 4030, 440)});
@@ -317,12 +443,12 @@
// Shutdown the coordinator and clear the invocation history, especially the
// tetherOffloadGetStats() calls.
coordinator.stopPolling();
- clearInvocations(mNetd);
+ clearStatsInvocations();
// Verify the polling update thread stopped.
mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
waitForIdle();
- verify(mNetd, never()).tetherOffloadGetStats();
+ verifyNeverTetherOffloadGetStats();
}
@Test
@@ -342,16 +468,14 @@
mTetherStatsProviderCb.expectNotifyAlertReached();
// Verify that notifyAlertReached never fired if quota is not yet reached.
- when(mNetd.tetherOffloadGetStats()).thenReturn(
- new TetherStatsParcel[] {buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0)});
+ updateStatsEntry(buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
mTetherStatsProvider.onSetAlert(100);
mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
waitForIdle();
mTetherStatsProviderCb.assertNoCallback();
// Verify that notifyAlertReached fired when quota is reached.
- when(mNetd.tetherOffloadGetStats()).thenReturn(
- new TetherStatsParcel[] {buildTestTetherStatsParcel(mobileIfIndex, 50, 0, 50, 0)});
+ updateStatsEntry(buildTestTetherStatsParcel(mobileIfIndex, 50, 0, 50, 0));
mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
waitForIdle();
mTetherStatsProviderCb.expectNotifyAlertReached();
@@ -606,7 +730,7 @@
// The tether stats polling task should not be scheduled.
mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
waitForIdle();
- verify(mNetd, never()).tetherOffloadGetStats();
+ verifyNeverTetherOffloadGetStats();
// The interface name lookup table can't be added.
final String iface = "rmnet_data0";
@@ -670,6 +794,15 @@
}
@Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testBpfDisabledbyNoBpfStatsMap() throws Exception {
+ setupFunctioningNetdInterface();
+ doReturn(null).when(mDeps).getBpfStatsMap();
+
+ checkBpfDisabled();
+ }
+
+ @Test
public void testTetheringConfigSetPollingInterval() throws Exception {
setupFunctioningNetdInterface();
@@ -703,18 +836,18 @@
// Start on a new polling time slot.
mTestLooper.moveTimeForward(pollingInterval);
waitForIdle();
- clearInvocations(mNetd);
+ clearStatsInvocations();
// Move time forward to 90% polling interval time. Expect that the polling thread has not
// scheduled yet.
mTestLooper.moveTimeForward((long) (pollingInterval * 0.9));
waitForIdle();
- verify(mNetd, never()).tetherOffloadGetStats();
+ verifyNeverTetherOffloadGetStats();
// Move time forward to the remaining 10% polling interval time. Expect that the polling
// thread has scheduled.
mTestLooper.moveTimeForward((long) (pollingInterval * 0.1));
waitForIdle();
- verify(mNetd).tetherOffloadGetStats();
+ verifyTetherOffloadGetStats();
}
}