Support active session metrics sampling
This commit introduces a feature to monitor and upload active
tethering connection metrics. This will help us understand
the usage of tethering and improve the user experience.
The feature is disabled by default and will be gradually
rolled out.
Test: atest TetheringTests:com.android.networkstack.tethering.BpfCoordinatorTest
Test: manual test with:
adb shell device_config put tethering tether_active_sessions_metrics 3
m statsd_testdrive && statsd_testdrive -e 925
Bug: 354619988
Change-Id: I3b3f5ae068b127192c35541c6d9439b7367d54bd
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 80eb042..89e06da 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -87,6 +87,7 @@
import com.android.net.module.util.netlink.NetlinkUtils;
import com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim;
import com.android.networkstack.tethering.util.TetheringUtils.ForwardedStats;
+import com.android.server.ConnectivityStatsLog;
import java.io.IOException;
import java.net.Inet4Address;
@@ -151,6 +152,13 @@
@VisibleForTesting
static final int CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS = 60_000;
+ // The interval is set to 5 minutes to strike a balance between minimizing
+ // the amount of metrics data uploaded and providing sufficient resolution
+ // to track changes in forwarding rules. This choice considers the minimum
+ // push metrics sampling interval of 5 minutes and the 3-minute timeout
+ // for forwarding rules.
+ @VisibleForTesting
+ static final int CONNTRACK_METRICS_UPDATE_INTERVAL_MS = 300_000;
@VisibleForTesting
static final int NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED = 432_000;
@VisibleForTesting
@@ -319,6 +327,12 @@
private final boolean mSupportActiveSessionsMetrics;
+ // Runnable that used by scheduling next refreshing of conntrack metrics sampling.
+ private final Runnable mScheduledConntrackMetricsSampling = () -> {
+ uploadConntrackMetricsSample();
+ scheduleConntrackMetricsSampling();
+ };
+
// TODO: add BpfMap<TetherDownstream64Key, TetherDownstream64Value> retrieving function.
@VisibleForTesting
public abstract static class Dependencies {
@@ -481,6 +495,12 @@
}
}
+ /** Send a TetheringActiveSessionsReported event. */
+ public void sendTetheringActiveSessionsReported(int lastMaxSessionCount) {
+ ConnectivityStatsLog.write(ConnectivityStatsLog.TETHERING_ACTIVE_SESSIONS_REPORTED,
+ lastMaxSessionCount);
+ }
+
/**
* @see DeviceConfigUtils#isTetheringFeatureEnabled
*/
@@ -530,38 +550,48 @@
}
/**
- * Start BPF tethering offload stats and conntrack timeout polling.
+ * Start BPF tethering offload stats and conntrack polling.
* Note that this can be only called on handler thread.
*/
- private void startStatsAndConntrackTimeoutPolling() {
+ private void startStatsAndConntrackPolling() {
schedulePollingStats();
scheduleConntrackTimeoutUpdate();
+ if (mSupportActiveSessionsMetrics) {
+ scheduleConntrackMetricsSampling();
+ }
mLog.i("Polling started.");
}
/**
- * Stop BPF tethering offload stats and conntrack timeout polling.
+ * Stop BPF tethering offload stats and conntrack polling.
* The data limit cleanup and the tether stats maps cleanup are not implemented here.
* These cleanups rely on all IpServers calling #removeIpv6DownstreamRule. After the
* last rule is removed from the upstream, #removeIpv6DownstreamRule does the cleanup
* functionality.
* Note that this can be only called on handler thread.
*/
- private void stopStatsAndConntrackTimeoutPolling() {
+ private void stopStatsAndConntrackPolling() {
// Stop scheduled polling conntrack timeout.
if (mHandler.hasCallbacks(mScheduledConntrackTimeoutUpdate)) {
mHandler.removeCallbacks(mScheduledConntrackTimeoutUpdate);
}
- // Clear counters in case there is any counter unsync problem
+ // Stop scheduled polling conntrack metrics sampling and
+ // clear counters in case there is any counter unsync problem
// previously due to possible bpf failures.
// Normally this won't happen because all clients are cleared before
// reaching here. See IpServer.BaseServingState#exit().
if (mSupportActiveSessionsMetrics) {
+ if (mHandler.hasCallbacks(mScheduledConntrackMetricsSampling)) {
+ mHandler.removeCallbacks(mScheduledConntrackMetricsSampling);
+ }
final int currentCount = mBpfConntrackEventConsumer.getCurrentConnectionCount();
if (currentCount != 0) {
Log.wtf(TAG, "Unexpected CurrentConnectionCount: " + currentCount);
}
+ // Avoid sending metrics when tethering is about to close.
+ // This leads to a missing final sample before disconnect
+ // but avoids possibly duplicating the last metric in the upload.
mBpfConntrackEventConsumer.clearConnectionCounters();
}
// Stop scheduled polling stats and poll the latest stats from BPF maps.
@@ -897,7 +927,7 @@
// Start monitoring and polling when the first IpServer is added.
if (mServedIpServers.isEmpty()) {
- startStatsAndConntrackTimeoutPolling();
+ startStatsAndConntrackPolling();
startConntrackMonitoring();
mIpNeighborMonitor.start();
mLog.i("Neighbor monitoring started.");
@@ -920,7 +950,7 @@
// Stop monitoring and polling when the last IpServer is removed.
if (mServedIpServers.isEmpty()) {
- stopStatsAndConntrackTimeoutPolling();
+ stopStatsAndConntrackPolling();
stopConntrackMonitoring();
mIpNeighborMonitor.stop();
mLog.i("Neighbor monitoring stopped.");
@@ -2579,6 +2609,11 @@
});
}
+ private void uploadConntrackMetricsSample() {
+ mDeps.sendTetheringActiveSessionsReported(
+ mBpfConntrackEventConsumer.getLastMaxConnectionAndResetToCurrent());
+ }
+
private void schedulePollingStats() {
if (mHandler.hasCallbacks(mScheduledPollingStats)) {
mHandler.removeCallbacks(mScheduledPollingStats);
@@ -2596,6 +2631,15 @@
CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS);
}
+ private void scheduleConntrackMetricsSampling() {
+ if (mHandler.hasCallbacks(mScheduledConntrackMetricsSampling)) {
+ mHandler.removeCallbacks(mScheduledConntrackMetricsSampling);
+ }
+
+ mHandler.postDelayed(mScheduledConntrackMetricsSampling,
+ CONNTRACK_METRICS_UPDATE_INTERVAL_MS);
+ }
+
// Return IPv6 downstream forwarding rule map. This is used for testing only.
// Note that this can be only called on handler thread.
@NonNull
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 dfa9936..5d22977 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -48,6 +48,7 @@
import static com.android.net.module.util.netlink.StructNdMsg.NUD_FAILED;
import static com.android.net.module.util.netlink.StructNdMsg.NUD_REACHABLE;
import static com.android.net.module.util.netlink.StructNdMsg.NUD_STALE;
+import static com.android.networkstack.tethering.BpfCoordinator.CONNTRACK_METRICS_UPDATE_INTERVAL_MS;
import static com.android.networkstack.tethering.BpfCoordinator.CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS;
import static com.android.networkstack.tethering.BpfCoordinator.INVALID_MTU;
import static com.android.networkstack.tethering.BpfCoordinator.NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED;
@@ -573,6 +574,11 @@
}
@Override
+ public void sendTetheringActiveSessionsReported(int lastMaxSessionCount) {
+ // No-op.
+ }
+
+ @Override
public boolean isFeatureEnabled(Context context, String name) {
return mFeatureFlags.getOrDefault(name, false);
}
@@ -2166,6 +2172,70 @@
assertConsumerCountersEquals(0);
}
+ @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS)
+ // BPF IPv4 forwarding only supports on S+.
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ @Test
+ public void testSendActiveSessionsReported_metricsEnabled() throws Exception {
+ doTestSendActiveSessionsReported(true);
+ }
+
+ @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS, enabled = false)
+ @Test
+ public void testSendActiveSessionsReported_metricsDisabled() throws Exception {
+ doTestSendActiveSessionsReported(false);
+ }
+
+ private void doTestSendActiveSessionsReported(final boolean supportActiveSessionsMetrics)
+ throws Exception {
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ initBpfCoordinatorForRule4(coordinator);
+ resetNetdAndBpfMaps();
+ assertConsumerCountersEquals(0);
+
+ // Prepare the counter value.
+ for (int i = 0; i < 5; i++) {
+ mConsumer.accept(new TestConntrackEvent.Builder().setMsgType(
+ IPCTNL_MSG_CT_NEW).setProto(IPPROTO_TCP).setRemotePort(i).build());
+ }
+
+ // Then delete some 3 rules, 2 rules remaining.
+ // The max count is 5 while current rules count is 2.
+ for (int i = 0; i < 3; i++) {
+ mConsumer.accept(new TestConntrackEvent.Builder().setMsgType(
+ IPCTNL_MSG_CT_DELETE).setProto(IPPROTO_TCP).setRemotePort(i).build());
+ }
+
+ // Verify the method is not invoked when timer is not expired.
+ waitForIdle();
+ verify(mDeps, never()).sendTetheringActiveSessionsReported(anyInt());
+
+ // Verify metrics will be sent upon timer expiry.
+ mTestLooper.moveTimeForward(CONNTRACK_METRICS_UPDATE_INTERVAL_MS);
+ waitForIdle();
+ if (supportActiveSessionsMetrics) {
+ verify(mDeps).sendTetheringActiveSessionsReported(5);
+ } else {
+ verify(mDeps, never()).sendTetheringActiveSessionsReported(anyInt());
+ }
+
+ // Verify next uploaded metrics will reflect the decreased rules count.
+ mTestLooper.moveTimeForward(CONNTRACK_METRICS_UPDATE_INTERVAL_MS);
+ waitForIdle();
+ if (supportActiveSessionsMetrics) {
+ verify(mDeps).sendTetheringActiveSessionsReported(2);
+ } else {
+ verify(mDeps, never()).sendTetheringActiveSessionsReported(anyInt());
+ }
+
+ // Verify no metrics uploaded if polling stopped.
+ clearInvocations(mDeps);
+ coordinator.removeIpServer(mIpServer);
+ mTestLooper.moveTimeForward(CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS);
+ waitForIdle();
+ verify(mDeps, never()).sendTetheringActiveSessionsReported(anyInt());
+ }
+
private void setElapsedRealtimeNanos(long nanoSec) {
mElapsedRealtimeNanos = nanoSec;
}