/*
|
* Copyright (C) 2016 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.server.connectivity;
|
|
import android.content.Context;
|
import android.net.ConnectivityMetricsEvent;
|
import android.net.IIpConnectivityMetrics;
|
import android.net.INetdEventCallback;
|
import android.net.metrics.ApfProgramEvent;
|
import android.net.metrics.IpConnectivityLog;
|
import android.os.Binder;
|
import android.os.Process;
|
import android.provider.Settings;
|
import android.text.TextUtils;
|
import android.text.format.DateUtils;
|
import android.util.ArrayMap;
|
import android.util.Base64;
|
import android.util.Log;
|
|
import com.android.internal.annotations.GuardedBy;
|
import com.android.internal.annotations.VisibleForTesting;
|
import com.android.internal.util.RingBuffer;
|
import com.android.internal.util.TokenBucket;
|
import com.android.server.LocalServices;
|
import com.android.server.SystemService;
|
import com.android.server.connectivity.metrics.nano.IpConnectivityLogClass.IpConnectivityEvent;
|
|
import java.io.FileDescriptor;
|
import java.io.IOException;
|
import java.io.PrintWriter;
|
import java.util.ArrayList;
|
import java.util.Arrays;
|
import java.util.List;
|
import java.util.function.ToIntFunction;
|
|
/**
|
* Event buffering service for core networking and connectivity metrics.
|
*
|
* {@hide}
|
*/
|
final public class IpConnectivityMetrics extends SystemService {
|
private static final String TAG = IpConnectivityMetrics.class.getSimpleName();
|
private static final boolean DBG = false;
|
|
// The logical version numbers of ipconnectivity.proto, corresponding to the
|
// "version" field of IpConnectivityLog.
|
private static final int NYC = 0;
|
private static final int NYC_MR1 = 1;
|
private static final int NYC_MR2 = 2;
|
public static final int VERSION = NYC_MR2;
|
|
private static final String SERVICE_NAME = IpConnectivityLog.SERVICE_NAME;
|
|
// Default size of the event rolling log for bug report dumps.
|
private static final int DEFAULT_LOG_SIZE = 500;
|
// Default size of the event buffer for metrics reporting.
|
// Once the buffer is full, incoming events are dropped.
|
private static final int DEFAULT_BUFFER_SIZE = 2000;
|
// Maximum size of the event buffer.
|
private static final int MAXIMUM_BUFFER_SIZE = DEFAULT_BUFFER_SIZE * 10;
|
|
private static final int MAXIMUM_CONNECT_LATENCY_RECORDS = 20000;
|
|
private static final int ERROR_RATE_LIMITED = -1;
|
|
// Lock ensuring that concurrent manipulations of the event buffers are correct.
|
// There are three concurrent operations to synchronize:
|
// - appending events to the buffer.
|
// - iterating throught the buffer.
|
// - flushing the buffer content and replacing it by a new buffer.
|
private final Object mLock = new Object();
|
|
// Implementation instance of IIpConnectivityMetrics.aidl.
|
@VisibleForTesting
|
public final Impl impl = new Impl();
|
// Subservice listening to Netd events via INetdEventListener.aidl.
|
@VisibleForTesting
|
NetdEventListenerService mNetdListener;
|
|
// Rolling log of the most recent events. This log is used for dumping
|
// connectivity events in bug reports.
|
@GuardedBy("mLock")
|
private final RingBuffer<ConnectivityMetricsEvent> mEventLog =
|
new RingBuffer(ConnectivityMetricsEvent.class, DEFAULT_LOG_SIZE);
|
// Buffer of connectivity events used for metrics reporting. This buffer
|
// does not rotate automatically and instead saturates when it becomes full.
|
// It is flushed at metrics reporting.
|
@GuardedBy("mLock")
|
private ArrayList<ConnectivityMetricsEvent> mBuffer;
|
// Total number of events dropped from mBuffer since last metrics reporting.
|
@GuardedBy("mLock")
|
private int mDropped;
|
// Capacity of mBuffer
|
@GuardedBy("mLock")
|
private int mCapacity;
|
// A list of rate limiting counters keyed by connectivity event types for
|
// metrics reporting mBuffer.
|
@GuardedBy("mLock")
|
private final ArrayMap<Class<?>, TokenBucket> mBuckets = makeRateLimitingBuckets();
|
|
private final ToIntFunction<Context> mCapacityGetter;
|
|
@VisibleForTesting
|
final DefaultNetworkMetrics mDefaultNetworkMetrics = new DefaultNetworkMetrics();
|
|
public IpConnectivityMetrics(Context ctx, ToIntFunction<Context> capacityGetter) {
|
super(ctx);
|
mCapacityGetter = capacityGetter;
|
initBuffer();
|
}
|
|
public IpConnectivityMetrics(Context ctx) {
|
this(ctx, READ_BUFFER_SIZE);
|
}
|
|
@Override
|
public void onStart() {
|
if (DBG) Log.d(TAG, "onStart");
|
}
|
|
@Override
|
public void onBootPhase(int phase) {
|
if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
|
if (DBG) Log.d(TAG, "onBootPhase");
|
mNetdListener = new NetdEventListenerService(getContext());
|
|
publishBinderService(SERVICE_NAME, impl);
|
publishBinderService(mNetdListener.SERVICE_NAME, mNetdListener);
|
|
LocalServices.addService(Logger.class, new LoggerImpl());
|
}
|
}
|
|
@VisibleForTesting
|
public int bufferCapacity() {
|
return mCapacityGetter.applyAsInt(getContext());
|
}
|
|
private void initBuffer() {
|
synchronized (mLock) {
|
mDropped = 0;
|
mCapacity = bufferCapacity();
|
mBuffer = new ArrayList<>(mCapacity);
|
}
|
}
|
|
private int append(ConnectivityMetricsEvent event) {
|
if (DBG) Log.d(TAG, "logEvent: " + event);
|
synchronized (mLock) {
|
mEventLog.append(event);
|
final int left = mCapacity - mBuffer.size();
|
if (event == null) {
|
return left;
|
}
|
if (isRateLimited(event)) {
|
// Do not count as a dropped event. TODO: consider adding separate counter
|
return ERROR_RATE_LIMITED;
|
}
|
if (left == 0) {
|
mDropped++;
|
return 0;
|
}
|
mBuffer.add(event);
|
return left - 1;
|
}
|
}
|
|
private boolean isRateLimited(ConnectivityMetricsEvent event) {
|
TokenBucket tb = mBuckets.get(event.data.getClass());
|
return (tb != null) && !tb.get();
|
}
|
|
private String flushEncodedOutput() {
|
final ArrayList<ConnectivityMetricsEvent> events;
|
final int dropped;
|
synchronized (mLock) {
|
events = mBuffer;
|
dropped = mDropped;
|
initBuffer();
|
}
|
|
final List<IpConnectivityEvent> protoEvents = IpConnectivityEventBuilder.toProto(events);
|
|
mDefaultNetworkMetrics.flushEvents(protoEvents);
|
|
if (mNetdListener != null) {
|
mNetdListener.flushStatistics(protoEvents);
|
}
|
|
final byte[] data;
|
try {
|
data = IpConnectivityEventBuilder.serialize(dropped, protoEvents);
|
} catch (IOException e) {
|
Log.e(TAG, "could not serialize events", e);
|
return "";
|
}
|
|
return Base64.encodeToString(data, Base64.DEFAULT);
|
}
|
|
/**
|
* Clear the event buffer and prints its content as a protobuf serialized byte array
|
* inside a base64 encoded string.
|
*/
|
private void cmdFlush(PrintWriter pw) {
|
pw.print(flushEncodedOutput());
|
}
|
|
/**
|
* Print the content of the rolling event buffer in human readable format.
|
* Also print network dns/connect statistics and recent default network events.
|
*/
|
private void cmdList(PrintWriter pw) {
|
pw.println("metrics events:");
|
final List<ConnectivityMetricsEvent> events = getEvents();
|
for (ConnectivityMetricsEvent ev : events) {
|
pw.println(ev.toString());
|
}
|
pw.println("");
|
if (mNetdListener != null) {
|
mNetdListener.list(pw);
|
}
|
pw.println("");
|
mDefaultNetworkMetrics.listEvents(pw);
|
}
|
|
/*
|
* Print the content of the rolling event buffer in text proto format.
|
*/
|
private void cmdListAsProto(PrintWriter pw) {
|
final List<ConnectivityMetricsEvent> events = getEvents();
|
for (IpConnectivityEvent ev : IpConnectivityEventBuilder.toProto(events)) {
|
pw.print(ev.toString());
|
}
|
if (mNetdListener != null) {
|
mNetdListener.listAsProtos(pw);
|
}
|
mDefaultNetworkMetrics.listEventsAsProto(pw);
|
}
|
|
/*
|
* Return a copy of metrics events stored in buffer for metrics uploading.
|
*/
|
private List<ConnectivityMetricsEvent> getEvents() {
|
synchronized (mLock) {
|
return Arrays.asList(mEventLog.toArray());
|
}
|
}
|
|
public final class Impl extends IIpConnectivityMetrics.Stub {
|
// Dump and flushes the metrics event buffer in base64 encoded serialized proto output.
|
static final String CMD_FLUSH = "flush";
|
// Dump the rolling buffer of metrics event in human readable proto text format.
|
static final String CMD_PROTO = "proto";
|
// Dump the rolling buffer of metrics event and pretty print events using a human readable
|
// format. Also print network dns/connect statistics and default network event time series.
|
static final String CMD_LIST = "list";
|
// By default any other argument will fall into the default case which is the equivalent
|
// of calling both the "list" and "ipclient" commands. This includes most notably bug
|
// reports collected by dumpsys.cpp with the "-a" argument.
|
static final String CMD_DEFAULT = "";
|
|
@Override
|
public int logEvent(ConnectivityMetricsEvent event) {
|
enforceConnectivityInternalPermission();
|
return append(event);
|
}
|
|
@Override
|
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
|
enforceDumpPermission();
|
if (DBG) Log.d(TAG, "dumpsys " + TextUtils.join(" ", args));
|
final String cmd = (args.length > 0) ? args[0] : CMD_DEFAULT;
|
switch (cmd) {
|
case CMD_FLUSH:
|
cmdFlush(pw);
|
return;
|
case CMD_PROTO:
|
cmdListAsProto(pw);
|
return;
|
case CMD_LIST:
|
default:
|
cmdList(pw);
|
return;
|
}
|
}
|
|
private void enforceConnectivityInternalPermission() {
|
enforcePermission(android.Manifest.permission.CONNECTIVITY_INTERNAL);
|
}
|
|
private void enforceDumpPermission() {
|
enforcePermission(android.Manifest.permission.DUMP);
|
}
|
|
private void enforcePermission(String what) {
|
getContext().enforceCallingOrSelfPermission(what, "IpConnectivityMetrics");
|
}
|
|
private void enforceNetdEventListeningPermission() {
|
final int uid = Binder.getCallingUid();
|
if (uid != Process.SYSTEM_UID) {
|
throw new SecurityException(String.format("Uid %d has no permission to listen for"
|
+ " netd events.", uid));
|
}
|
}
|
|
@Override
|
public boolean addNetdEventCallback(int callerType, INetdEventCallback callback) {
|
enforceNetdEventListeningPermission();
|
if (mNetdListener == null) {
|
return false;
|
}
|
return mNetdListener.addNetdEventCallback(callerType, callback);
|
}
|
|
@Override
|
public boolean removeNetdEventCallback(int callerType) {
|
enforceNetdEventListeningPermission();
|
if (mNetdListener == null) {
|
// if the service is null, we aren't registered anyway
|
return true;
|
}
|
return mNetdListener.removeNetdEventCallback(callerType);
|
}
|
};
|
|
private static final ToIntFunction<Context> READ_BUFFER_SIZE = (ctx) -> {
|
int size = Settings.Global.getInt(ctx.getContentResolver(),
|
Settings.Global.CONNECTIVITY_METRICS_BUFFER_SIZE, DEFAULT_BUFFER_SIZE);
|
if (size <= 0) {
|
return DEFAULT_BUFFER_SIZE;
|
}
|
return Math.min(size, MAXIMUM_BUFFER_SIZE);
|
};
|
|
private static ArrayMap<Class<?>, TokenBucket> makeRateLimitingBuckets() {
|
ArrayMap<Class<?>, TokenBucket> map = new ArrayMap<>();
|
// one token every minute, 50 tokens max: burst of ~50 events every hour.
|
map.put(ApfProgramEvent.class, new TokenBucket((int)DateUtils.MINUTE_IN_MILLIS, 50));
|
return map;
|
}
|
|
/** Direct non-Binder interface for event producer clients within the system servers. */
|
public interface Logger {
|
DefaultNetworkMetrics defaultNetworkMetrics();
|
}
|
|
private class LoggerImpl implements Logger {
|
public DefaultNetworkMetrics defaultNetworkMetrics() {
|
return mDefaultNetworkMetrics;
|
}
|
}
|
}
|