diff options
author | Remi NGUYEN VAN <reminv@google.com> | 2020-07-06 03:11:34 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2020-07-06 03:11:34 +0000 |
commit | a0428bfc7c2b1eb1b2a9f9252378e5e93ff64ba1 (patch) | |
tree | 7b435896c128df15885d91598c357e646f504e04 /src | |
parent | 4412473e2679937dc5b0632da60827fe5a5111b0 (diff) | |
parent | e1b31f105824614c6a506a99faed954d0d6214d3 (diff) |
Merge "Injecting network validation stats into statsd" into rvc-dev am: e1b31f1058
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/modules/NetworkStack/+/11952741
Change-Id: I3f2d48f967d60236dd86e4c025bf017b5d066472
Diffstat (limited to 'src')
-rw-r--r-- | src/com/android/networkstack/metrics/NetworkValidationMetrics.java | 220 | ||||
-rwxr-xr-x | src/com/android/server/connectivity/NetworkMonitor.java | 94 |
2 files changed, 302 insertions, 12 deletions
diff --git a/src/com/android/networkstack/metrics/NetworkValidationMetrics.java b/src/com/android/networkstack/metrics/NetworkValidationMetrics.java new file mode 100644 index 0000000..3789c5c --- /dev/null +++ b/src/com/android/networkstack/metrics/NetworkValidationMetrics.java @@ -0,0 +1,220 @@ +/* + * 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.metrics; + +import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; +import static android.net.NetworkCapabilities.TRANSPORT_LOWPAN; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE; + +import static java.lang.System.currentTimeMillis; + +import android.net.INetworkMonitor; +import android.net.NetworkCapabilities; +import android.net.captiveportal.CaptivePortalProbeResult; +import android.net.metrics.ValidationProbeEvent; +import android.net.util.NetworkStackUtils; +import android.net.util.Stopwatch; +import android.stats.connectivity.ProbeResult; +import android.stats.connectivity.ProbeType; +import android.stats.connectivity.TransportType; +import android.stats.connectivity.ValidationResult; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.networkstack.apishim.common.CaptivePortalDataShim; + +/** + * Class to record the network validation into statsd. + * 1. Fill in NetworkValidationReported proto. + * 2. Write the NetworkValidationReported proto into statsd. + * @hide + */ + +public class NetworkValidationMetrics { + private final NetworkValidationReported.Builder mStatsBuilder = + NetworkValidationReported.newBuilder(); + private final ProbeEvents.Builder mProbeEventsBuilder = ProbeEvents.newBuilder(); + private final CapportApiData.Builder mCapportApiDataBuilder = CapportApiData.newBuilder(); + private final Stopwatch mWatch = new Stopwatch(); + private int mValidationIndex = 0; + // Define a maximum size that can store events. + public static final int MAX_PROBE_EVENTS_COUNT = 20; + + /** + * Reset this NetworkValidationMetrics. + */ + public void reset(@Nullable NetworkCapabilities nc) { + mStatsBuilder.clear(); + mProbeEventsBuilder.clear(); + mCapportApiDataBuilder.clear(); + mWatch.restart(); + mStatsBuilder.setTransportType(getTransportTypeFromNC(nc)); + mValidationIndex++; + } + + /** + * Returns the enum TransportType + * + * @param NetworkCapabilities + * @return the TransportType which is defined in + * core/proto/android/stats/connectivity/network_stack.proto + */ + @VisibleForTesting + public static TransportType getTransportTypeFromNC( + @Nullable NetworkCapabilities nc) { + if (nc == null) return TransportType.TT_UNKNOWN; + boolean hasCellular = nc.hasTransport(TRANSPORT_CELLULAR); + boolean hasWifi = nc.hasTransport(TRANSPORT_WIFI); + boolean hasBT = nc.hasTransport(TRANSPORT_BLUETOOTH); + boolean hasEthernet = nc.hasTransport(TRANSPORT_ETHERNET); + boolean hasVpn = nc.hasTransport(TRANSPORT_VPN); + boolean hasWifiAware = nc.hasTransport(TRANSPORT_WIFI_AWARE); + boolean hasLopan = nc.hasTransport(TRANSPORT_LOWPAN); + + if (hasCellular && hasWifi && hasVpn) return TransportType.TT_WIFI_CELLULAR_VPN; + if (hasWifi) return hasVpn ? TransportType.TT_WIFI_VPN : TransportType.TT_WIFI; + if (hasCellular) return hasVpn ? TransportType.TT_CELLULAR_VPN : TransportType.TT_CELLULAR; + if (hasBT) return hasVpn ? TransportType.TT_BLUETOOTH_VPN : TransportType.TT_BLUETOOTH; + if (hasEthernet) return hasVpn ? TransportType.TT_ETHERNET_VPN : TransportType.TT_ETHERNET; + if (hasWifiAware) return TransportType.TT_WIFI_AWARE; + if (hasLopan) return TransportType.TT_LOWPAN; + return TransportType.TT_UNKNOWN; + } + + /** + * Map {@link ValidationProbeEvent} to {@link ProbeType}. + */ + public static ProbeType probeTypeToEnum(final int probeType) { + switch(probeType) { + case ValidationProbeEvent.PROBE_DNS: + return ProbeType.PT_DNS; + case ValidationProbeEvent.PROBE_HTTP: + return ProbeType.PT_HTTP; + case ValidationProbeEvent.PROBE_HTTPS: + return ProbeType.PT_HTTPS; + case ValidationProbeEvent.PROBE_PAC: + return ProbeType.PT_PAC; + case ValidationProbeEvent.PROBE_FALLBACK: + return ProbeType.PT_FALLBACK; + case ValidationProbeEvent.PROBE_PRIVDNS: + return ProbeType.PT_PRIVDNS; + default: + return ProbeType.PT_UNKNOWN; + } + } + + /** + * Map {@link CaptivePortalProbeResult} to {@link ProbeResult}. + */ + public static ProbeResult httpProbeResultToEnum(final CaptivePortalProbeResult result) { + if (result == null) return ProbeResult.PR_UNKNOWN; + + if (result.isSuccessful()) { + return ProbeResult.PR_SUCCESS; + } else if (result.isDnsPrivateIpResponse()) { + return ProbeResult.PR_PRIVATE_IP_DNS; + } else if (result.isFailed()) { + return ProbeResult.PR_FAILURE; + } else if (result.isPortal()) { + return ProbeResult.PR_PORTAL; + } else { + return ProbeResult.PR_UNKNOWN; + } + } + + /** + * Map validation result (as per INetworkMonitor) to {@link ValidationResult}. + */ + @VisibleForTesting + public static ValidationResult validationResultToEnum(int result, String redirectUrl) { + if ((result & INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID) != 0) { + return ValidationResult.VR_SUCCESS; + } else if (redirectUrl != null) { + return ValidationResult.VR_PORTAL; + } else if ((result & INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL) != 0) { + return ValidationResult.VR_PARTIAL; + } else { + return ValidationResult.VR_FAILURE; + } + } + + /** + * Write each network probe event to mProbeEventsBuilder. + */ + public void setProbeEvent(final ProbeType type, final long durationUs, final ProbeResult result, + @Nullable final CaptivePortalDataShim capportData) { + // When the number of ProbeEvents of mProbeEventsBuilder exceeds + // MAX_PROBE_EVENTS_COUNT, stop adding ProbeEvent. + if (mProbeEventsBuilder.getProbeEventCount() >= MAX_PROBE_EVENTS_COUNT) return; + + int latencyUs = NetworkStackUtils.saturatedCast(durationUs); + + final ProbeEvent.Builder probeEventBuilder = ProbeEvent.newBuilder() + .setLatencyMicros(latencyUs) + .setProbeType(type) + .setProbeResult(result); + + if (capportData != null) { + final long secondsRemaining = + (capportData.getExpiryTimeMillis() - currentTimeMillis()) / 1000; + mCapportApiDataBuilder + .setRemainingTtlSecs(NetworkStackUtils.saturatedCast(secondsRemaining)) + .setRemainingBytes(NetworkStackUtils.saturatedCast(capportData.getByteLimit())) + .setHasPortalUrl((capportData.getUserPortalUrl() != null)) + .setHasVenueInfo((capportData.getVenueInfoUrl() != null)); + probeEventBuilder.setCapportApiData(mCapportApiDataBuilder); + } + + mProbeEventsBuilder.addProbeEvent(probeEventBuilder); + } + + /** + * Write the network validation info to mStatsBuilder. + */ + public void setValidationResult(int result, String redirectUrl) { + mStatsBuilder.setValidationResult(validationResultToEnum(result, redirectUrl)); + } + + /** + * Write the NetworkValidationReported proto to statsd. + */ + public NetworkValidationReported sendValidationStats() { + if (!mWatch.isStarted()) return null; + mStatsBuilder.setProbeEvents(mProbeEventsBuilder); + mStatsBuilder.setLatencyMicros(NetworkStackUtils.saturatedCast(mWatch.stop())); + mStatsBuilder.setValidationIndex(mValidationIndex); + // write a random value(0 ~ 999) for sampling. + mStatsBuilder.setRandomNumber((int) (Math.random() * 1000)); + final NetworkValidationReported mStats = mStatsBuilder.build(); + final byte[] probeEvents = mStats.getProbeEvents().toByteArray(); + + NetworkStackStatsLog.write(NetworkStackStatsLog.NETWORK_VALIDATION_REPORTED, + mStats.getTransportType().getNumber(), + probeEvents, + mStats.getValidationResult().getNumber(), + mStats.getLatencyMicros(), + mStats.getValidationIndex(), + mStats.getRandomNumber()); + mWatch.reset(); + return mStats; + } +} diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java index b6b1fff..47c910a 100755 --- a/src/com/android/server/connectivity/NetworkMonitor.java +++ b/src/com/android/server/connectivity/NetworkMonitor.java @@ -129,6 +129,8 @@ import android.os.SystemClock; import android.os.UserHandle; import android.provider.DeviceConfig; import android.provider.Settings; +import android.stats.connectivity.ProbeResult; +import android.stats.connectivity.ProbeType; import android.telephony.AccessNetworkConstants; import android.telephony.CellIdentityNr; import android.telephony.CellInfo; @@ -155,6 +157,7 @@ import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.annotation.VisibleForTesting; +import com.android.internal.annotations.GuardedBy; import com.android.internal.util.RingBufferIndices; import com.android.internal.util.State; import com.android.internal.util.StateMachine; @@ -168,6 +171,7 @@ import com.android.networkstack.apishim.common.ShimUtils; import com.android.networkstack.apishim.common.UnsupportedApiLevelException; import com.android.networkstack.metrics.DataStallDetectionStats; import com.android.networkstack.metrics.DataStallStatsUtils; +import com.android.networkstack.metrics.NetworkValidationMetrics; import com.android.networkstack.netlink.TcpSocketTracker; import com.android.networkstack.util.DnsUtils; import com.android.server.NetworkStackService.NetworkStackServiceManager; @@ -500,6 +504,10 @@ public class NetworkMonitor extends StateMachine { private final boolean mPrivateIpNoInternetEnabled; + @GuardedBy("mNetworkValidationMetrics") + private final NetworkValidationMetrics mNetworkValidationMetrics = + new NetworkValidationMetrics(); + private int getCallbackVersion(INetworkMonitorCallbacks cb) { int version; try { @@ -775,6 +783,31 @@ public class NetworkMonitor extends StateMachine { } } + private void recordMetricsReset(@Nullable NetworkCapabilities nc) { + synchronized (mNetworkValidationMetrics) { + mNetworkValidationMetrics.reset(nc); + } + } + + private void recordMetricsProbeEvent(ProbeType type, long latencyMicros, ProbeResult result, + CaptivePortalDataShim capportData) { + synchronized (mNetworkValidationMetrics) { + mNetworkValidationMetrics.setProbeEvent(type, latencyMicros, result, capportData); + } + } + + private void recordMetricsValidationResult(int result, String redirectUrl) { + synchronized (mNetworkValidationMetrics) { + mNetworkValidationMetrics.setValidationResult(result, redirectUrl); + } + } + + private void recordMetricsValidationStats() { + synchronized (mNetworkValidationMetrics) { + mNetworkValidationMetrics.sendValidationStats(); + } + } + // DefaultState is the parent of all States. It exists only to handle CMD_* messages but // does not entail any real state (hence no enter() or exit() routines). private class DefaultState extends State { @@ -787,6 +820,7 @@ public class NetworkMonitor extends StateMachine { transitionTo(mEvaluatingState); return HANDLED; case CMD_NETWORK_DISCONNECTED: + recordMetricsValidationStats(); logNetworkEvent(NetworkEvent.NETWORK_DISCONNECTED); quit(); return HANDLED; @@ -938,6 +972,7 @@ public class NetworkMonitor extends StateMachine { initSocketTrackingIfRequired(); // start periodical polling. sendTcpPollingEvent(); + recordMetricsValidationStats(); } private void initSocketTrackingIfRequired() { @@ -957,6 +992,9 @@ public class NetworkMonitor extends StateMachine { transitionTo(mValidatedState); break; case CMD_EVALUATE_PRIVATE_DNS: + // TODO: this causes reevaluation of a single probe that is not counted in + // metrics. Add support for such reevaluation probes in metrics, and log them + // separately. transitionTo(mEvaluatingPrivateDnsState); break; case EVENT_DNS_NOTIFICATION: @@ -1285,6 +1323,7 @@ public class NetworkMonitor extends StateMachine { sendMessageDelayed(CMD_CAPTIVE_PORTAL_RECHECK, 0 /* no UID */, CAPTIVE_PORTAL_REEVALUATE_DELAY_MS); mValidations++; + recordMetricsValidationStats(); } @Override @@ -1316,6 +1355,10 @@ public class NetworkMonitor extends StateMachine { notifyPrivateDnsConfigResolved(); } else { handlePrivateDnsEvaluationFailure(); + // The private DNS probe fails-fast if the server hostname cannot + // be resolved. Record it as a failure with zero latency. + recordMetricsProbeEvent(ProbeType.PT_PRIVDNS, 0 /* latency */, + ProbeResult.PR_FAILURE, null /* capportData */); break; } } @@ -1425,6 +1468,8 @@ public class NetworkMonitor extends StateMachine { validationLog(PROBE_PRIVDNS, host, String.format("%dus - Error: %s", time, uhe.getMessage())); } + recordMetricsProbeEvent(ProbeType.PT_PRIVDNS, time, success ? ProbeResult.PR_SUCCESS : + ProbeResult.PR_FAILURE, null /* capportData */); logValidationProbe(time, PROBE_PRIVDNS, success ? DNS_SUCCESS : DNS_FAILURE); return success; } @@ -1435,6 +1480,8 @@ public class NetworkMonitor extends StateMachine { @Override public void enter() { + recordMetricsValidationStats(); + recordMetricsReset(mNetworkCapabilities); if (mEvaluateAttempts >= BLAME_FOR_EVALUATION_ATTEMPTS) { //Don't continue to blame UID forever. TrafficStats.clearThreadStatsUid(); @@ -1516,6 +1563,7 @@ public class NetworkMonitor extends StateMachine { private class WaitingForNextProbeState extends State { @Override public void enter() { + recordMetricsValidationStats(); scheduleNextProbe(); } @@ -2265,6 +2313,8 @@ public class NetworkMonitor extends StateMachine { // network validation (the HTTPS probe, which would likely fail anyway) or the PAC probe. if (mPrivateIpNoInternetEnabled && probeType == ValidationProbeEvent.PROBE_HTTP && (proxy == null) && hasPrivateIpAddress(resolvedAddr)) { + recordMetricsProbeEvent(NetworkValidationMetrics.probeTypeToEnum(probeType), + 0 /* latency */, ProbeResult.PR_PRIVATE_IP_DNS, null /* capportData */); return CaptivePortalProbeResult.PRIVATE_IP; } return sendHttpProbe(url, probeType, null); @@ -2296,6 +2346,9 @@ public class NetworkMonitor extends StateMachine { result = ValidationProbeEvent.DNS_FAILURE; } final long latency = watch.stop(); + recordMetricsProbeEvent(ProbeType.PT_DNS, latency, + (result == ValidationProbeEvent.DNS_SUCCESS) ? ProbeResult.PR_SUCCESS : + ProbeResult.PR_FAILURE, null /* capportData */); logValidationProbe(latency, ValidationProbeEvent.PROBE_DNS, result); return addresses; } @@ -2413,12 +2466,17 @@ public class NetworkMonitor extends StateMachine { } logValidationProbe(probeTimer.stop(), probeType, httpResponseCode); + final CaptivePortalProbeResult probeResult; if (probeSpec == null) { - return new CaptivePortalProbeResult(httpResponseCode, redirectUrl, url.toString(), - 1 << probeType); + probeResult = new CaptivePortalProbeResult(httpResponseCode, redirectUrl, + url.toString(), 1 << probeType); } else { - return probeSpec.getResult(httpResponseCode, redirectUrl); + probeResult = probeSpec.getResult(httpResponseCode, redirectUrl); } + recordMetricsProbeEvent(NetworkValidationMetrics.probeTypeToEnum(probeType), + probeTimer.stop(), NetworkValidationMetrics.httpProbeResultToEnum(probeResult), + null /* capportData */); + return probeResult; } @VisibleForTesting @@ -2570,8 +2628,7 @@ public class NetworkMonitor extends StateMachine { super(deps, proxy, url, captivePortalApiUrl); } - private CaptivePortalDataShim tryCapportApiProbe() { - if (mCaptivePortalApiUrl == null) return null; + private CaptivePortalDataShim sendCapportApiProbe() { validationLog("Fetching captive portal data from " + mCaptivePortalApiUrl); final String apiContent; @@ -2610,26 +2667,38 @@ public class NetworkMonitor extends StateMachine { try { final JSONObject info = new JSONObject(apiContent); - return CaptivePortalDataShimImpl.fromJson(info); + final CaptivePortalDataShim capportData = CaptivePortalDataShimImpl.fromJson(info); + if (capportData != null && capportData.isCaptive() + && capportData.getUserPortalUrl() == null) { + validationLog("Missing user-portal-url from capport response"); + return null; + } + return capportData; } catch (JSONException e) { validationLog("Could not parse capport API JSON: " + e.getMessage()); return null; } catch (UnsupportedApiLevelException e) { + // This should never happen because LinkProperties would not have a capport URL + // before R. validationLog("Platform API too low to support capport API"); return null; } } + private CaptivePortalDataShim tryCapportApiProbe() { + if (mCaptivePortalApiUrl == null) return null; + final Stopwatch capportApiWatch = new Stopwatch().start(); + final CaptivePortalDataShim capportData = sendCapportApiProbe(); + recordMetricsProbeEvent(ProbeType.PT_CAPPORT_API, capportApiWatch.stop(), + capportData == null ? ProbeResult.PR_FAILURE : ProbeResult.PR_SUCCESS, + capportData); + return capportData; + } + @Override protected CaptivePortalProbeResult sendProbe() { final CaptivePortalDataShim capportData = tryCapportApiProbe(); if (capportData != null && capportData.isCaptive()) { - if (capportData.getUserPortalUrl() == null) { - validationLog("Missing user-portal-url from capport response"); - return new CapportApiProbeResult( - sendDnsAndHttpProbes(mProxy, mUrl, ValidationProbeEvent.PROBE_HTTP), - null /* capportData */); - } final String loginUrlString = capportData.getUserPortalUrl().toString(); // Starting from R (where CaptivePortalData was introduced), the captive portal app // delegates to NetworkMonitor for verifying when the network validates instead of @@ -3343,6 +3412,7 @@ public class NetworkMonitor extends StateMachine { p.redirectUrl = redirectUrl; p.timestampMillis = SystemClock.elapsedRealtime(); notifyNetworkTested(p); + recordMetricsValidationResult(result, redirectUrl); } @VisibleForTesting |