1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.view.textclassifier;
18 
19 import android.annotation.Nullable;
20 import android.metrics.LogMaker;
21 import android.util.ArrayMap;
22 
23 import com.android.internal.annotations.VisibleForTesting;
24 import com.android.internal.logging.MetricsLogger;
25 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
26 import com.android.internal.util.Preconditions;
27 
28 import java.util.Locale;
29 import java.util.Map;
30 import java.util.Objects;
31 import java.util.Random;
32 import java.util.UUID;
33 
34 /**
35  * A helper for logging calls to generateLinks.
36  * @hide
37  */
38 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
39 public final class GenerateLinksLogger {
40 
41     private static final String LOG_TAG = "GenerateLinksLogger";
42     private static final String ZERO = "0";
43 
44     private final MetricsLogger mMetricsLogger;
45     private final Random mRng;
46     private final int mSampleRate;
47 
48     /**
49      * @param sampleRate the rate at which log events are written. (e.g. 100 means there is a 0.01
50      *                   chance that a call to logGenerateLinks results in an event being written).
51      *                   To write all events, pass 1.
52      */
GenerateLinksLogger(int sampleRate)53     public GenerateLinksLogger(int sampleRate) {
54         mSampleRate = sampleRate;
55         mRng = new Random(System.nanoTime());
56         mMetricsLogger = new MetricsLogger();
57     }
58 
59     @VisibleForTesting
GenerateLinksLogger(int sampleRate, MetricsLogger metricsLogger)60     public GenerateLinksLogger(int sampleRate, MetricsLogger metricsLogger) {
61         mSampleRate = sampleRate;
62         mRng = new Random(System.nanoTime());
63         mMetricsLogger = metricsLogger;
64     }
65 
66     /** Logs statistics about a call to generateLinks. */
logGenerateLinks(CharSequence text, TextLinks links, String callingPackageName, long latencyMs)67     public void logGenerateLinks(CharSequence text, TextLinks links, String callingPackageName,
68             long latencyMs) {
69         Preconditions.checkNotNull(text);
70         Preconditions.checkNotNull(links);
71         Preconditions.checkNotNull(callingPackageName);
72         if (!shouldLog()) {
73             return;
74         }
75 
76         // Always populate the total stats, and per-entity stats for each entity type detected.
77         final LinkifyStats totalStats = new LinkifyStats();
78         final Map<String, LinkifyStats> perEntityTypeStats = new ArrayMap<>();
79         for (TextLinks.TextLink link : links.getLinks()) {
80             if (link.getEntityCount() == 0) continue;
81             final String entityType = link.getEntity(0);
82             if (entityType == null
83                     || TextClassifier.TYPE_OTHER.equals(entityType)
84                     || TextClassifier.TYPE_UNKNOWN.equals(entityType)) {
85                 continue;
86             }
87             totalStats.countLink(link);
88             perEntityTypeStats.computeIfAbsent(entityType, k -> new LinkifyStats()).countLink(link);
89         }
90 
91         final String callId = UUID.randomUUID().toString();
92         writeStats(callId, callingPackageName, null, totalStats, text, latencyMs);
93         for (Map.Entry<String, LinkifyStats> entry : perEntityTypeStats.entrySet()) {
94             writeStats(callId, callingPackageName, entry.getKey(), entry.getValue(), text,
95                        latencyMs);
96         }
97     }
98 
99     /**
100      * Returns whether this particular event should be logged.
101      *
102      * Sampling is used to reduce the amount of logging data generated.
103      **/
shouldLog()104     private boolean shouldLog() {
105         if (mSampleRate <= 1) {
106             return true;
107         } else {
108             return mRng.nextInt(mSampleRate) == 0;
109         }
110     }
111 
112     /** Writes a log event for the given stats. */
writeStats(String callId, String callingPackageName, @Nullable String entityType, LinkifyStats stats, CharSequence text, long latencyMs)113     private void writeStats(String callId, String callingPackageName, @Nullable String entityType,
114                             LinkifyStats stats, CharSequence text, long latencyMs) {
115         final LogMaker log = new LogMaker(MetricsEvent.TEXT_CLASSIFIER_GENERATE_LINKS)
116                 .setPackageName(callingPackageName)
117                 .addTaggedData(MetricsEvent.FIELD_LINKIFY_CALL_ID, callId)
118                 .addTaggedData(MetricsEvent.FIELD_LINKIFY_NUM_LINKS, stats.mNumLinks)
119                 .addTaggedData(MetricsEvent.FIELD_LINKIFY_LINK_LENGTH, stats.mNumLinksTextLength)
120                 .addTaggedData(MetricsEvent.FIELD_LINKIFY_TEXT_LENGTH, text.length())
121                 .addTaggedData(MetricsEvent.FIELD_LINKIFY_LATENCY, latencyMs);
122         if (entityType != null) {
123             log.addTaggedData(MetricsEvent.FIELD_LINKIFY_ENTITY_TYPE, entityType);
124         }
125         mMetricsLogger.write(log);
126         debugLog(log);
127     }
128 
debugLog(LogMaker log)129     private static void debugLog(LogMaker log) {
130         if (!Log.ENABLE_FULL_LOGGING) {
131             return;
132         }
133         final String callId = Objects.toString(
134                 log.getTaggedData(MetricsEvent.FIELD_LINKIFY_CALL_ID), "");
135         final String entityType = Objects.toString(
136                 log.getTaggedData(MetricsEvent.FIELD_LINKIFY_ENTITY_TYPE), "ANY_ENTITY");
137         final int numLinks = Integer.parseInt(
138                 Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_NUM_LINKS), ZERO));
139         final int linkLength = Integer.parseInt(
140                 Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_LINK_LENGTH), ZERO));
141         final int textLength = Integer.parseInt(
142                 Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_TEXT_LENGTH), ZERO));
143         final int latencyMs = Integer.parseInt(
144                 Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_LATENCY), ZERO));
145 
146         Log.v(LOG_TAG,
147                 String.format(Locale.US, "%s:%s %d links (%d/%d chars) %dms %s", callId, entityType,
148                         numLinks, linkLength, textLength, latencyMs, log.getPackageName()));
149     }
150 
151     /** Helper class for storing per-entity type statistics. */
152     private static final class LinkifyStats {
153         int mNumLinks;
154         int mNumLinksTextLength;
155 
countLink(TextLinks.TextLink link)156         void countLink(TextLinks.TextLink link) {
157             mNumLinks += 1;
158             mNumLinksTextLength += link.getEnd() - link.getStart();
159         }
160     }
161 }
162