NetdEventListener: add rolling log of connect and dns stats
This patch adds a rolling time order logs of basic statistics of
connect and dns, grouped by network id and including:
- average error rate
- average latency
- max latency
- total number of operations
The log is segmented in buckets of 5 minutes, and covers the last 4
hours.
Note that only blocking connect() calls latencies are recorded with the
current netd logging infrastructure.
Example of logs:
05:30:00.000: {netId=100, WIFI, dns avg=92ms max=525ms err=00.0% tot=37, connect avg=0ms max=23ms err=00.0% tot=25}
05:35:00.000: {netId=100, WIFI, dns avg=94ms max=537ms err=05.3% tot=57, connect avg=0ms max=0ms err=00.0% tot=69}
05:40:00.000: {netId=100, WIFI, dns avg=220ms max=350ms err=00.0% tot=4, connect avg=0ms max=0ms err=00.0% tot=2}
05:45:00.000: {netId=100, WIFI, dns avg=112ms max=113ms err=00.0% tot=1, connect avg=0ms max=0ms err=00.0% tot=1}
05:50:00.000: {netId=100, WIFI, dns avg=131ms max=269ms err=00.0% tot=2, connect avg=0ms max=0ms err=00.0% tot=1}
Bug: 65700460
Test: runtest frameworks-net
Change-Id: I54e76d18fbaaa92639fb675f93ea90b7615fd6bf
diff --git a/core/java/android/net/metrics/ConnectStats.java b/core/java/android/net/metrics/ConnectStats.java
index 30b2656..2495cab 100644
--- a/core/java/android/net/metrics/ConnectStats.java
+++ b/core/java/android/net/metrics/ConnectStats.java
@@ -20,6 +20,7 @@
import android.system.OsConstants;
import android.util.IntArray;
import android.util.SparseIntArray;
+
import com.android.internal.util.BitUtils;
import com.android.internal.util.TokenBucket;
@@ -43,6 +44,8 @@
public final TokenBucket mLatencyTb;
/** Maximum number of latency values recorded. */
public final int mMaxLatencyRecords;
+ /** Total count of events */
+ public int eventCount = 0;
/** Total count of successful connects. */
public int connectCount = 0;
/** Total count of successful connects done in blocking mode. */
@@ -57,12 +60,15 @@
mMaxLatencyRecords = maxLatencyRecords;
}
- public void addEvent(int errno, int latencyMs, String ipAddr) {
+ boolean addEvent(int errno, int latencyMs, String ipAddr) {
+ eventCount++;
if (isSuccess(errno)) {
countConnect(errno, ipAddr);
countLatency(errno, latencyMs);
+ return true;
} else {
countError(errno);
+ return false;
}
}
@@ -101,7 +107,7 @@
return (errno == 0) || isNonBlocking(errno);
}
- private static boolean isNonBlocking(int errno) {
+ static boolean isNonBlocking(int errno) {
// On non-blocking TCP sockets, connect() immediately returns EINPROGRESS.
// On non-blocking TCP sockets that are connecting, connect() immediately returns EALREADY.
return (errno == EINPROGRESS) || (errno == EALREADY);
@@ -117,6 +123,7 @@
for (int t : BitUtils.unpackBits(transports)) {
builder.append(NetworkCapabilities.transportNameOf(t)).append(", ");
}
+ builder.append(String.format("%d events, ", eventCount));
builder.append(String.format("%d success, ", connectCount));
builder.append(String.format("%d blocking, ", connectBlockingCount));
builder.append(String.format("%d IPv6 dst", ipv6ConnectCount));
diff --git a/core/java/android/net/metrics/DnsEvent.java b/core/java/android/net/metrics/DnsEvent.java
index a4970e4..81b098b 100644
--- a/core/java/android/net/metrics/DnsEvent.java
+++ b/core/java/android/net/metrics/DnsEvent.java
@@ -17,11 +17,13 @@
package android.net.metrics;
import android.net.NetworkCapabilities;
-import java.util.Arrays;
+
import com.android.internal.util.BitUtils;
+import java.util.Arrays;
+
/**
- * A DNS event recorded by NetdEventListenerService.
+ * A batch of DNS events recorded by NetdEventListenerService for a specific network.
* {@hide}
*/
final public class DnsEvent {
@@ -38,6 +40,8 @@
// the eventTypes, returnCodes, and latenciesMs arrays have the same length and the i-th event
// is spread across the three array at position i.
public int eventCount;
+ // The number of successful DNS queries recorded.
+ public int successCount;
// The types of DNS queries as defined in INetdEventListener.
public byte[] eventTypes;
// Current getaddrinfo codes go from 1 to EAI_MAX = 15. gethostbyname returns errno, but there
@@ -54,10 +58,11 @@
latenciesMs = new int[initialCapacity];
}
- public void addResult(byte eventType, byte returnCode, int latencyMs) {
+ boolean addResult(byte eventType, byte returnCode, int latencyMs) {
+ boolean isSuccess = (returnCode == 0);
if (eventCount >= SIZE_LIMIT) {
// TODO: implement better rate limiting that does not biases metrics.
- return;
+ return isSuccess;
}
if (eventCount == eventTypes.length) {
resize((int) (1.4 * eventCount));
@@ -66,6 +71,10 @@
returnCodes[eventCount] = returnCode;
latenciesMs[eventCount] = latencyMs;
eventCount++;
+ if (isSuccess) {
+ successCount++;
+ }
+ return isSuccess;
}
public void resize(int newLength) {
@@ -80,6 +89,8 @@
for (int t : BitUtils.unpackBits(transports)) {
builder.append(NetworkCapabilities.transportNameOf(t)).append(", ");
}
- return builder.append(eventCount).append(" events)").toString();
+ builder.append(String.format("%d events, ", eventCount));
+ builder.append(String.format("%d success)", successCount));
+ return builder.toString();
}
}
diff --git a/core/java/android/net/metrics/NetworkMetrics.java b/core/java/android/net/metrics/NetworkMetrics.java
new file mode 100644
index 0000000..2b662a0
--- /dev/null
+++ b/core/java/android/net/metrics/NetworkMetrics.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2017 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 android.net.metrics;
+
+import android.net.NetworkCapabilities;
+
+import com.android.internal.util.BitUtils;
+import com.android.internal.util.TokenBucket;
+
+import java.util.StringJoiner;
+
+/**
+ * A class accumulating network metrics received from Netd regarding dns queries and
+ * connect() calls on a given network.
+ *
+ * This class also accumulates running sums of dns and connect latency stats and
+ * error counts for bug report logging.
+ *
+ * @hide
+ */
+public class NetworkMetrics {
+
+ private static final int INITIAL_DNS_BATCH_SIZE = 100;
+ private static final int CONNECT_LATENCY_MAXIMUM_RECORDS = 20000;
+
+ // The network id of the Android Network.
+ public final int netId;
+ // The transport types bitmap of the Android Network, as defined in NetworkCapabilities.java.
+ public final long transports;
+ // Accumulated metrics for connect events.
+ public final ConnectStats connectMetrics;
+ // Accumulated metrics for dns events.
+ public final DnsEvent dnsMetrics;
+ // Running sums of latencies and error counts for connect and dns events.
+ public final Summary summary;
+ // Running sums of the most recent latencies and error counts for connect and dns events.
+ // Starts null until some events are accumulated.
+ // Allows to collect periodic snapshot of the running summaries for a given network.
+ public Summary pendingSummary;
+
+ public NetworkMetrics(int netId, long transports, TokenBucket tb) {
+ this.netId = netId;
+ this.transports = transports;
+ this.connectMetrics =
+ new ConnectStats(netId, transports, tb, CONNECT_LATENCY_MAXIMUM_RECORDS);
+ this.dnsMetrics = new DnsEvent(netId, transports, INITIAL_DNS_BATCH_SIZE);
+ this.summary = new Summary(netId, transports);
+ }
+
+ /**
+ * Get currently pending Summary statistics, if any, for this NetworkMetrics, merge them
+ * into the long running Summary statistics of this NetworkMetrics, and also clear them.
+ */
+ public Summary getPendingStats() {
+ Summary s = pendingSummary;
+ pendingSummary = null;
+ if (s != null) {
+ summary.merge(s);
+ }
+ return s;
+ }
+
+ /** Accumulate a dns query result reported by netd. */
+ public void addDnsResult(int eventType, int returnCode, int latencyMs) {
+ if (pendingSummary == null) {
+ pendingSummary = new Summary(netId, transports);
+ }
+ boolean isSuccess = dnsMetrics.addResult((byte) eventType, (byte) returnCode, latencyMs);
+ pendingSummary.dnsLatencies.count(latencyMs);
+ pendingSummary.dnsErrorRate.count(isSuccess ? 0 : 1);
+ }
+
+ /** Accumulate a connect query result reported by netd. */
+ public void addConnectResult(int error, int latencyMs, String ipAddr) {
+ if (pendingSummary == null) {
+ pendingSummary = new Summary(netId, transports);
+ }
+ boolean isSuccess = connectMetrics.addEvent(error, latencyMs, ipAddr);
+ pendingSummary.connectErrorRate.count(isSuccess ? 0 : 1);
+ if (ConnectStats.isNonBlocking(error)) {
+ pendingSummary.connectLatencies.count(latencyMs);
+ }
+ }
+
+ /** Represents running sums for dns and connect average error counts and average latencies. */
+ public static class Summary {
+
+ public final int netId;
+ public final long transports;
+ // DNS latencies measured in milliseconds.
+ public final Metrics dnsLatencies = new Metrics();
+ // DNS error rate measured in percentage points.
+ public final Metrics dnsErrorRate = new Metrics();
+ // Blocking connect latencies measured in milliseconds.
+ public final Metrics connectLatencies = new Metrics();
+ // Blocking and non blocking connect error rate measured in percentage points.
+ public final Metrics connectErrorRate = new Metrics();
+
+ public Summary(int netId, long transports) {
+ this.netId = netId;
+ this.transports = transports;
+ }
+
+ void merge(Summary that) {
+ dnsLatencies.merge(that.dnsLatencies);
+ dnsErrorRate.merge(that.dnsErrorRate);
+ connectLatencies.merge(that.connectLatencies);
+ connectErrorRate.merge(that.connectErrorRate);
+ }
+
+ @Override
+ public String toString() {
+ StringJoiner j = new StringJoiner(", ", "{", "}");
+ j.add("netId=" + netId);
+ for (int t : BitUtils.unpackBits(transports)) {
+ j.add(NetworkCapabilities.transportNameOf(t));
+ }
+ j.add(String.format("dns avg=%dms max=%dms err=%.1f%% tot=%d",
+ (int) dnsLatencies.average(), (int) dnsLatencies.max,
+ 100 * dnsErrorRate.average(), dnsErrorRate.count));
+ j.add(String.format("connect avg=%dms max=%dms err=%.1f%% tot=%d",
+ (int) connectLatencies.average(), (int) connectLatencies.max,
+ 100 * connectErrorRate.average(), connectErrorRate.count));
+ return j.toString();
+ }
+ }
+
+ /** Tracks a running sum and returns the average of a metric. */
+ static class Metrics {
+ public double sum;
+ public double max = Double.MIN_VALUE;
+ public int count;
+
+ void merge(Metrics that) {
+ this.count += that.count;
+ this.sum += that.sum;
+ this.max = Math.max(this.max, that.max);
+ }
+
+ void count(double value) {
+ count++;
+ sum += value;
+ max = Math.max(max, value);
+ }
+
+ double average() {
+ double a = sum / (double) count;
+ if (Double.isNaN(a)) {
+ a = 0;
+ }
+ return a;
+ }
+ }
+}