Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 0699cf98 authored by Hugo Benichi's avatar Hugo Benichi
Browse files

Connectivity metrics: log DnsEvents in-band

This patch changes how DnsEvents are logged in IpConnectivityMetrics.
The following changes are made:
 - DnsEventBatch are not logged after 100 queries on the same network
    - this allows to merge DnsEvent and DnsEventBatch into one class
 - DnsEventBatch are not logged after a network disconnect
    - this allows to remove the NetworkCallback
 - DnsEvent are now logged similarly to ConnectStats when statistics are
   flushed, in a direct call from IpConnectivityMetrics into
   NetdEventListenerService, in a direct call from IpConnectivityMetrics
   into NetdEventListenerService.
    - this allows to remove the Parcelable implementation of DnsEvent
 - transports information is added to DnsEvent.

Test: - simplified NetdEventListenerServiceTest covering dns logging
      - updated IpConnectivityEventBuilderTest
      - updated IpConnectivityMetricsTest
      - $ runtest frameworks-net passes
      - manually verified $ adb shell dumpsys connmetrics list proto
Bug: 34901696

Change-Id: Ia4b33fd4212741152662a2adbb0533bd1b4902ee
parent df456e13
Loading
Loading
Loading
Loading
+41 −44
Original line number Diff line number Diff line
@@ -16,67 +16,64 @@

package android.net.metrics;

import android.os.Parcel;
import android.os.Parcelable;
import java.util.Arrays;

/**
 * A DNS event recorded by NetdEventListenerService.
 * {@hide}
 */
final public class DnsEvent implements Parcelable {
    public final int netId;
final public class DnsEvent {

    private static final int SIZE_LIMIT = 20000;

    // The event type is currently only 1 or 2, so we store it as a byte.
    public final byte[] eventTypes;
    // Network id of the network associated with the event, or 0 if unspecified.
    public final int netId;
    // Transports of the network associated with the event, as defined in NetworkCapabilities.
    // It is the caller responsability to ensure the value of transports does not change between
    // calls to addResult.
    public final long transports;
    // The number of DNS queries recorded. Queries are stored in the structure-of-array style where
    // 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 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
    // are fewer than 255 errno values. So we store the result code in a byte as well.
    public final byte[] returnCodes;
    // The latency is an integer because a) short arrays aren't parcelable and b) a short can only
    // store a maximum latency of 32757 or 65535 ms, which is too short for pathologically slow
    // queries.
    public final int[] latenciesMs;
    public byte[] returnCodes;
    // Latencies in milliseconds of queries, stored as ints.
    public int[] latenciesMs;

    public DnsEvent(int netId, byte[] eventTypes, byte[] returnCodes, int[] latenciesMs) {
    public DnsEvent(int netId, long transports, int initialCapacity) {
        this.netId = netId;
        this.eventTypes = eventTypes;
        this.returnCodes = returnCodes;
        this.latenciesMs = latenciesMs;
        this.transports = transports;
        eventTypes = new byte[initialCapacity];
        returnCodes = new byte[initialCapacity];
        latenciesMs = new int[initialCapacity];
    }

    private DnsEvent(Parcel in) {
        this.netId = in.readInt();
        this.eventTypes = in.createByteArray();
        this.returnCodes = in.createByteArray();
        this.latenciesMs = in.createIntArray();
    public void addResult(byte eventType, byte returnCode, int latencyMs) {
        if (eventCount >= SIZE_LIMIT) {
            // TODO: implement better rate limiting that does not biases metrics.
            return;
        }

    @Override
    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(netId);
        out.writeByteArray(eventTypes);
        out.writeByteArray(returnCodes);
        out.writeIntArray(latenciesMs);
        if (eventCount == eventTypes.length) {
            resize((int) (1.4 * eventCount));
        }

    @Override
    public int describeContents() {
        return 0;
        eventTypes[eventCount] = eventType;
        returnCodes[eventCount] = returnCode;
        latenciesMs[eventCount] = latencyMs;
        eventCount++;
    }

    @Override
    public String toString() {
        return String.format("DnsEvent(%d, %d events)", netId, eventTypes.length);
    }

    public static final Parcelable.Creator<DnsEvent> CREATOR = new Parcelable.Creator<DnsEvent>() {
        @Override
        public DnsEvent createFromParcel(Parcel in) {
            return new DnsEvent(in);
    public void resize(int newLength) {
        eventTypes = Arrays.copyOf(eventTypes, newLength);
        returnCodes = Arrays.copyOf(returnCodes, newLength);
        latenciesMs = Arrays.copyOf(latenciesMs, newLength);
    }

    @Override
        public DnsEvent[] newArray(int size) {
            return new DnsEvent[size];
    public String toString() {
        return String.format("DnsEvent(%d events)", eventCount);
    }
    };
}
+15 −15
Original line number Diff line number Diff line
@@ -91,6 +91,21 @@ final public class IpConnectivityEventBuilder {
        return out;
    }

    public static IpConnectivityEvent toProto(DnsEvent in) {
        final IpConnectivityEvent out = new IpConnectivityEvent();
        IpConnectivityLogClass.DNSLookupBatch dnsLookupBatch =
                new IpConnectivityLogClass.DNSLookupBatch();
        in.resize(in.eventCount);
        dnsLookupBatch.eventTypes = bytesToInts(in.eventTypes);
        dnsLookupBatch.returnCodes = bytesToInts(in.returnCodes);
        dnsLookupBatch.latenciesMs = in.latenciesMs;
        out.setDnsLookupBatch(dnsLookupBatch);
        out.networkId = in.netId;
        out.transports = in.transports;
        inferLinkLayer(out);
        return out;
    }

    private static boolean setEvent(IpConnectivityEvent out, Parcelable in) {
        if (in instanceof DhcpErrorEvent) {
            setDhcpErrorEvent(out, (DhcpErrorEvent) in);
@@ -102,11 +117,6 @@ final public class IpConnectivityEventBuilder {
            return true;
        }

        if (in instanceof DnsEvent) {
            setDnsEvent(out, (DnsEvent) in);
            return true;
        }

        if (in instanceof IpManagerEvent) {
            setIpManagerEvent(out, (IpManagerEvent) in);
            return true;
@@ -163,16 +173,6 @@ final public class IpConnectivityEventBuilder {
        out.setDhcpEvent(dhcpEvent);
    }

    private static void setDnsEvent(IpConnectivityEvent out, DnsEvent in) {
        IpConnectivityLogClass.DNSLookupBatch dnsLookupBatch =
                new IpConnectivityLogClass.DNSLookupBatch();
        dnsLookupBatch.networkId = netIdOf(in.netId);
        dnsLookupBatch.eventTypes = bytesToInts(in.eventTypes);
        dnsLookupBatch.returnCodes = bytesToInts(in.returnCodes);
        dnsLookupBatch.latenciesMs = in.latenciesMs;
        out.setDnsLookupBatch(dnsLookupBatch);
    }

    private static void setIpManagerEvent(IpConnectivityEvent out, IpManagerEvent in) {
        IpConnectivityLogClass.IpProvisioningEvent ipProvisioningEvent =
                new IpConnectivityLogClass.IpProvisioningEvent();
+2 −1
Original line number Diff line number Diff line
@@ -76,7 +76,8 @@ final public class IpConnectivityMetrics extends SystemService {

    @VisibleForTesting
    public final Impl impl = new Impl();
    private NetdEventListenerService mNetdListener;
    @VisibleForTesting
    NetdEventListenerService mNetdListener;

    @GuardedBy("mLock")
    private ArrayList<ConnectivityMetricsEvent> mBuffer;
+51 −93
Original line number Diff line number Diff line
@@ -18,10 +18,9 @@ package com.android.server.connectivity;

import android.content.Context;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.INetdEventCallback;
import android.net.Network;
import android.net.NetworkRequest;
import android.net.NetworkCapabilities;
import android.net.metrics.ConnectStats;
import android.net.metrics.DnsEvent;
import android.net.metrics.INetdEventListener;
@@ -29,17 +28,16 @@ import android.net.metrics.IpConnectivityLog;
import android.os.RemoteException;
import android.text.format.DateUtils;
import android.util.Log;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.BitUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.TokenBucket;
import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.ConnectStatistics;
import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * Implementation of the INetdEventListener interface.
@@ -52,7 +50,7 @@ public class NetdEventListenerService extends INetdEventListener.Stub {
    private static final boolean DBG = false;
    private static final boolean VDBG = false;

    private static final int MAX_LOOKUPS_PER_DNS_EVENT = 100;
    private static final int INITIAL_DNS_BATCH_SIZE = 100;

    // Rate limit connect latency logging to 1 measurement per 15 seconds (5760 / day) with maximum
    // bursts of 5000 measurements.
@@ -60,74 +58,11 @@ public class NetdEventListenerService extends INetdEventListener.Stub {
    private static final int CONNECT_LATENCY_FILL_RATE    = 15 * (int) DateUtils.SECOND_IN_MILLIS;
    private static final int CONNECT_LATENCY_MAXIMUM_RECORDS = 20000;

    // Stores the results of a number of consecutive DNS lookups on the same network.
    // This class is not thread-safe and it is the responsibility of the service to call its methods
    // on one thread at a time.
    private class DnsEventBatch {
        private final int mNetId;

        private final byte[] mEventTypes = new byte[MAX_LOOKUPS_PER_DNS_EVENT];
        private final byte[] mReturnCodes = new byte[MAX_LOOKUPS_PER_DNS_EVENT];
        private final int[] mLatenciesMs = new int[MAX_LOOKUPS_PER_DNS_EVENT];
        private int mEventCount;

        public DnsEventBatch(int netId) {
            mNetId = netId;
        }

        public void addResult(byte eventType, byte returnCode, int latencyMs) {
            mEventTypes[mEventCount] = eventType;
            mReturnCodes[mEventCount] = returnCode;
            mLatenciesMs[mEventCount] = latencyMs;
            mEventCount++;
            if (mEventCount == MAX_LOOKUPS_PER_DNS_EVENT) {
                logAndClear();
            }
        }

        public void logAndClear() {
            // Did we lose a race with addResult?
            if (mEventCount == 0) {
                return;
            }

            // Only log as many events as we actually have.
            byte[] eventTypes = Arrays.copyOf(mEventTypes, mEventCount);
            byte[] returnCodes = Arrays.copyOf(mReturnCodes, mEventCount);
            int[] latenciesMs = Arrays.copyOf(mLatenciesMs, mEventCount);
            mMetricsLog.log(new DnsEvent(mNetId, eventTypes, returnCodes, latenciesMs));
            maybeLog("Logging %d results for netId %d", mEventCount, mNetId);
            mEventCount = 0;
        }

        // For debugging and unit tests only.
        public String toString() {
            return String.format("%s %d %d", getClass().getSimpleName(), mNetId, mEventCount);
        }
    }

    // Only sorted for ease of debugging. Because we only typically have a handful of networks up
    // at any given time, performance is not a concern.
    // Sparse array of DNS events, grouped by net id.
    @GuardedBy("this")
    private final SortedMap<Integer, DnsEventBatch> mEventBatches = new TreeMap<>();
    private final SparseArray<DnsEvent> mDnsEvents = new SparseArray<>();

    // We register a NetworkCallback to ensure that when a network disconnects, we flush the DNS
    // queries we've logged on that network. Because we do not do this periodically, we might lose
    // up to MAX_LOOKUPS_PER_DNS_EVENT lookup stats on each network when the system is shutting
    // down. We believe this to be sufficient for now.
    private final ConnectivityManager mCm;
    private final IpConnectivityLog mMetricsLog;
    private final NetworkCallback mNetworkCallback = new NetworkCallback() {
        @Override
        public void onLost(Network network) {
            synchronized (NetdEventListenerService.this) {
                DnsEventBatch batch = mEventBatches.remove(network.netId);
                if (batch != null) {
                    batch.logAndClear();
                }
            }
        }
    };

    @GuardedBy("this")
    private final TokenBucket mConnectTb =
@@ -151,16 +86,13 @@ public class NetdEventListenerService extends INetdEventListener.Stub {
    }

    public NetdEventListenerService(Context context) {
        this(context.getSystemService(ConnectivityManager.class), new IpConnectivityLog());
        this(context.getSystemService(ConnectivityManager.class));
    }

    @VisibleForTesting
    public NetdEventListenerService(ConnectivityManager cm, IpConnectivityLog log) {
    public NetdEventListenerService(ConnectivityManager cm) {
        // We are started when boot is complete, so ConnectivityService should already be running.
        mCm = cm;
        mMetricsLog = log;
        final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build();
        mCm.registerNetworkCallback(request, mNetworkCallback);
    }

    @Override
@@ -171,16 +103,16 @@ public class NetdEventListenerService extends INetdEventListener.Stub {
            throws RemoteException {
        maybeVerboseLog("onDnsEvent(%d, %d, %d, %dms)", netId, eventType, returnCode, latencyMs);

        DnsEventBatch batch = mEventBatches.get(netId);
        if (batch == null) {
            batch = new DnsEventBatch(netId);
            mEventBatches.put(netId, batch);
        DnsEvent dnsEvent = mDnsEvents.get(netId);
        if (dnsEvent == null) {
            dnsEvent = makeDnsEvent(netId);
            mDnsEvents.put(netId, dnsEvent);
        }
        batch.addResult((byte) eventType, (byte) returnCode, latencyMs);
        dnsEvent.addResult((byte) eventType, (byte) returnCode, latencyMs);

        if (mNetdEventCallback != null) {
            mNetdEventCallback.onDnsEvent(hostname, ipAddresses, ipAddressesCount,
                    System.currentTimeMillis(), uid);
            long timestamp = System.currentTimeMillis();
            mNetdEventCallback.onDnsEvent(hostname, ipAddresses, ipAddressesCount, timestamp, uid);
        }
    }

@@ -199,21 +131,29 @@ public class NetdEventListenerService extends INetdEventListener.Stub {
    }

    public synchronized void flushStatistics(List<IpConnectivityEvent> events) {
        events.add(flushConnectStats());
        // TODO: migrate DnsEventBatch to IpConnectivityLogClass.DNSLatencies
        flushConnectStats(events);
        flushDnsStats(events);
    }

    private IpConnectivityEvent connectStatsProto() {
    private static IpConnectivityEvent connectStatsProto(ConnectStats connectStats) {
        // TODO: add transport information
        IpConnectivityEvent ev = new IpConnectivityEvent();
        ev.setConnectStatistics(mConnectStats.toProto());
        ev.setConnectStatistics(connectStats.toProto());
        return ev;
    }

    private IpConnectivityEvent flushConnectStats() {
        IpConnectivityEvent ev = connectStatsProto();
    private void flushConnectStats(List<IpConnectivityEvent> events) {
        events.add(connectStatsProto(mConnectStats));
        mConnectStats = makeConnectStats();
        return ev;
    }

    private void flushDnsStats(List<IpConnectivityEvent> events) {
        // TODO: migrate DnsEventBatch to IpConnectivityLogClass.DNSLatencies
        for (int i = 0; i < mDnsEvents.size(); i++) {
            IpConnectivityEvent ev = IpConnectivityEventBuilder.toProto(mDnsEvents.valueAt(i));
            events.add(ev);
        }
        mDnsEvents.clear();
    }

    public synchronized void dump(PrintWriter writer) {
@@ -225,20 +165,38 @@ public class NetdEventListenerService extends INetdEventListener.Stub {
    }

    public synchronized void list(PrintWriter pw) {
        for (DnsEventBatch batch : mEventBatches.values()) {
            pw.println(batch.toString());
        for (int i = 0; i < mDnsEvents.size(); i++) {
            pw.println(mDnsEvents.valueAt(i).toString());
        }
        pw.println(mConnectStats.toString());
    }

    public synchronized void listAsProtos(PrintWriter pw) {
        pw.println(connectStatsProto().toString());
        for (int i = 0; i < mDnsEvents.size(); i++) {
            IpConnectivityEvent ev = IpConnectivityEventBuilder.toProto(mDnsEvents.valueAt(i));
            pw.println(ev.toString());
        }
        pw.println(connectStatsProto(mConnectStats).toString());
    }

    private ConnectStats makeConnectStats() {
        return new ConnectStats(mConnectTb, CONNECT_LATENCY_MAXIMUM_RECORDS);
    }

    private DnsEvent makeDnsEvent(int netId) {
        long transports = getTransports(netId);
        return new DnsEvent(netId, transports, INITIAL_DNS_BATCH_SIZE);
    }

    private long getTransports(int netId) {
        // TODO: directly query ConnectivityService instead of going through Binder interface.
        NetworkCapabilities nc = mCm.getNetworkCapabilities(new Network(netId));
        if (nc == null) {
            return 0;
        }
        return BitUtils.packBits(nc.getTransportTypes());
    }

    private static void maybeLog(String s, Object... args) {
        if (DBG) Log.d(TAG, String.format(s, args));
    }
+34 −95

File changed.

Preview size limit exceeded, changes collapsed.

Loading