/*
 * 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.view.textclassifier;

import android.annotation.Nullable;
import android.metrics.LogMaker;
import android.util.ArrayMap;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.util.Preconditions;

import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.UUID;

A helper for logging calls to generateLinks.
@hide
/** * A helper for logging calls to generateLinks. * @hide */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public final class GenerateLinksLogger { private static final String LOG_TAG = "GenerateLinksLogger"; private static final boolean DEBUG_LOG_ENABLED = false; private static final String ZERO = "0"; private final MetricsLogger mMetricsLogger; private final Random mRng; private final int mSampleRate;
Params:
  • sampleRate – the rate at which log events are written. (e.g. 100 means there is a 0.01 chance that a call to logGenerateLinks results in an event being written). To write all events, pass 1.
/** * @param sampleRate the rate at which log events are written. (e.g. 100 means there is a 0.01 * chance that a call to logGenerateLinks results in an event being written). * To write all events, pass 1. */
public GenerateLinksLogger(int sampleRate) { mSampleRate = sampleRate; mRng = new Random(System.nanoTime()); mMetricsLogger = new MetricsLogger(); } @VisibleForTesting public GenerateLinksLogger(int sampleRate, MetricsLogger metricsLogger) { mSampleRate = sampleRate; mRng = new Random(System.nanoTime()); mMetricsLogger = metricsLogger; }
Logs statistics about a call to generateLinks.
/** Logs statistics about a call to generateLinks. */
public void logGenerateLinks(CharSequence text, TextLinks links, String callingPackageName, long latencyMs) { Preconditions.checkNotNull(text); Preconditions.checkNotNull(links); Preconditions.checkNotNull(callingPackageName); if (!shouldLog()) { return; } // Always populate the total stats, and per-entity stats for each entity type detected. final LinkifyStats totalStats = new LinkifyStats(); final Map<String, LinkifyStats> perEntityTypeStats = new ArrayMap<>(); for (TextLinks.TextLink link : links.getLinks()) { if (link.getEntityCount() == 0) continue; final String entityType = link.getEntity(0); if (entityType == null || TextClassifier.TYPE_OTHER.equals(entityType) || TextClassifier.TYPE_UNKNOWN.equals(entityType)) { continue; } totalStats.countLink(link); perEntityTypeStats.computeIfAbsent(entityType, k -> new LinkifyStats()).countLink(link); } final String callId = UUID.randomUUID().toString(); writeStats(callId, callingPackageName, null, totalStats, text, latencyMs); for (Map.Entry<String, LinkifyStats> entry : perEntityTypeStats.entrySet()) { writeStats(callId, callingPackageName, entry.getKey(), entry.getValue(), text, latencyMs); } }
Returns whether this particular event should be logged. Sampling is used to reduce the amount of logging data generated.
/** * Returns whether this particular event should be logged. * * Sampling is used to reduce the amount of logging data generated. **/
private boolean shouldLog() { if (mSampleRate <= 1) { return true; } else { return mRng.nextInt(mSampleRate) == 0; } }
Writes a log event for the given stats.
/** Writes a log event for the given stats. */
private void writeStats(String callId, String callingPackageName, @Nullable String entityType, LinkifyStats stats, CharSequence text, long latencyMs) { final LogMaker log = new LogMaker(MetricsEvent.TEXT_CLASSIFIER_GENERATE_LINKS) .setPackageName(callingPackageName) .addTaggedData(MetricsEvent.FIELD_LINKIFY_CALL_ID, callId) .addTaggedData(MetricsEvent.FIELD_LINKIFY_NUM_LINKS, stats.mNumLinks) .addTaggedData(MetricsEvent.FIELD_LINKIFY_LINK_LENGTH, stats.mNumLinksTextLength) .addTaggedData(MetricsEvent.FIELD_LINKIFY_TEXT_LENGTH, text.length()) .addTaggedData(MetricsEvent.FIELD_LINKIFY_LATENCY, latencyMs); if (entityType != null) { log.addTaggedData(MetricsEvent.FIELD_LINKIFY_ENTITY_TYPE, entityType); } mMetricsLogger.write(log); debugLog(log); } private static void debugLog(LogMaker log) { if (!DEBUG_LOG_ENABLED) return; final String callId = Objects.toString( log.getTaggedData(MetricsEvent.FIELD_LINKIFY_CALL_ID), ""); final String entityType = Objects.toString( log.getTaggedData(MetricsEvent.FIELD_LINKIFY_ENTITY_TYPE), "ANY_ENTITY"); final int numLinks = Integer.parseInt( Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_NUM_LINKS), ZERO)); final int linkLength = Integer.parseInt( Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_LINK_LENGTH), ZERO)); final int textLength = Integer.parseInt( Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_TEXT_LENGTH), ZERO)); final int latencyMs = Integer.parseInt( Objects.toString(log.getTaggedData(MetricsEvent.FIELD_LINKIFY_LATENCY), ZERO)); Log.d(LOG_TAG, String.format(Locale.US, "%s:%s %d links (%d/%d chars) %dms %s", callId, entityType, numLinks, linkLength, textLength, latencyMs, log.getPackageName())); }
Helper class for storing per-entity type statistics.
/** Helper class for storing per-entity type statistics. */
private static final class LinkifyStats { int mNumLinks; int mNumLinksTextLength; void countLink(TextLinks.TextLink link) { mNumLinks += 1; mNumLinksTextLength += link.getEnd() - link.getStart(); } } }