/* * Copyright 2011 Google Inc. * * 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.google.ipc.invalidation.ticl; import com.google.ipc.invalidation.common.CommonProtos2; import com.google.ipc.invalidation.external.client.SystemResources.Logger; import com.google.ipc.invalidation.external.client.types.SimplePair; import com.google.ipc.invalidation.util.InternalBase; import com.google.ipc.invalidation.util.Marshallable; import com.google.ipc.invalidation.util.TextBuilder; import com.google.ipc.invalidation.util.TypedUtil; import com.google.protos.ipc.invalidation.ClientProtocol.PropertyRecord; import com.google.protos.ipc.invalidation.JavaClient.StatisticsState; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Statistics for the Ticl, e.g., number of registration calls, number of token mismatches, etc. * */ public class Statistics extends InternalBase implements Marshallable<StatisticsState> { // Implementation: To classify the statistics a bit better, we have a few enums to track different // types of statistics, e.g., sent message types, errors, etc. For each statistic type, we create // a map and provide a method to record an event for each type of statistic. /** Types of messages sent to the server: {@code ClientToServerMessage} for their description. */ public enum SentMessageType { INFO, INITIALIZE, INVALIDATION_ACK, REGISTRATION, REGISTRATION_SYNC, TOTAL, // Refers to the actual ClientToServerMessage message sent on the network. } /** * Types of messages received from the server: {@code ServerToClientMessage} for their * description. */ public enum ReceivedMessageType { INFO_REQUEST, INVALIDATION, REGISTRATION_STATUS, REGISTRATION_SYNC_REQUEST, TOKEN_CONTROL, ERROR, CONFIG_CHANGE, TOTAL, // Refers to the actual ServerToClientMessage messages received from the network. } /** Interesting API calls coming from the application ({@code InvalidationClient}). */ public enum IncomingOperationType { ACKNOWLEDGE, REGISTRATION, UNREGISTRATION, } /** Different types of events issued by the {@code InvalidationListener}). */ public enum ListenerEventType { INFORM_ERROR, INFORM_REGISTRATION_FAILURE, INFORM_REGISTRATION_STATUS, INVALIDATE, INVALIDATE_ALL, INVALIDATE_UNKNOWN, REISSUE_REGISTRATIONS, } /** Different types of errors observed by the Ticl. */ public enum ClientErrorType { /** Acknowledge call received from client with a bad handle. */ ACKNOWLEDGE_HANDLE_FAILURE, /** Incoming message dropped due to parsing, validation problems. */ INCOMING_MESSAGE_FAILURE, /** Tried to send an outgoing message that was invalid. */ OUTGOING_MESSAGE_FAILURE, /** Persistent state failed to deserialize correctly. */ PERSISTENT_DESERIALIZATION_FAILURE, /** Read of blob from persistent state failed. */ PERSISTENT_READ_FAILURE, /** Write of blob from persistent state failed. */ PERSISTENT_WRITE_FAILURE, /** Message received with incompatible protocol version. */ PROTOCOL_VERSION_FAILURE, /** * Registration at client and server is different, e.g., client thinks it is registered while * the server says it is unregistered (of course, sync will fix it). */ REGISTRATION_DISCREPANCY, /** The nonce from the server did not match the current nonce by the client. */ NONCE_MISMATCH, /** The current token at the client is different from the token in the incoming message. */ TOKEN_MISMATCH, /** No message sent due to token missing. */ TOKEN_MISSING_FAILURE, /** Received a message with a token (transient) failure. */ TOKEN_TRANSIENT_FAILURE, } // Names of statistics types. Do not rely on reflection to determine type names because Proguard // may change them for Android clients. private static final String SENT_MESSAGE_TYPE_NAME = "SentMessageType"; private static final String INCOMING_OPERATION_TYPE_NAME = "IncomingOperationType"; private static final String RECEIVED_MESSAGE_TYPE_NAME = "ReceivedMessageType"; private static final String LISTENER_EVENT_TYPE_NAME = "ListenerEventType"; private static final String CLIENT_ERROR_TYPE_NAME = "ClientErrorType"; // Map from stats enum names to values. Used in place of Enum.valueOf() because this method // invokes Enum.values() via reflection, and that method may be renamed by Proguard. private static final Map<String, SentMessageType> SENT_MESSAGE_TYPE_NAME_TO_VALUE_MAP = createValueOfMap(SentMessageType.values()); private static final Map<String, IncomingOperationType> INCOMING_OPERATION_TYPE_NAME_TO_VALUE_MAP = createValueOfMap(IncomingOperationType.values()); private static final Map<String, ReceivedMessageType> RECEIVED_MESSAGE_TYPE_NAME_TO_VALUE_MAP = createValueOfMap(ReceivedMessageType.values()); private static final Map<String, ListenerEventType> LISTENER_EVENT_TYPE_NAME_TO_VALUE_MAP = createValueOfMap(ListenerEventType.values()); private static final Map<String, ClientErrorType> CLIENT_ERROR_TYPE_NAME_TO_VALUE_MAP = createValueOfMap(ClientErrorType.values()); // Maps for each type of Statistic to keep track of how many times each event has occurred. private final Map<SentMessageType, Integer> sentMessageTypes = new HashMap<SentMessageType, Integer>(); private final Map<ReceivedMessageType, Integer> receivedMessageTypes = new HashMap<ReceivedMessageType, Integer>(); private final Map<IncomingOperationType, Integer> incomingOperationTypes = new HashMap<IncomingOperationType, Integer>(); private final Map<ListenerEventType, Integer> listenerEventTypes = new HashMap<ListenerEventType, Integer>(); private final Map<ClientErrorType, Integer> clientErrorTypes = new HashMap<ClientErrorType, Integer>(); public Statistics() { initializeMap(sentMessageTypes, SentMessageType.values()); initializeMap(receivedMessageTypes, ReceivedMessageType.values()); initializeMap(incomingOperationTypes, IncomingOperationType.values()); initializeMap(listenerEventTypes, ListenerEventType.values()); initializeMap(clientErrorTypes, ClientErrorType.values()); } /** Returns a copy of this. */ public Statistics getCopyForTest() { Statistics statistics = new Statistics(); statistics.sentMessageTypes.putAll(sentMessageTypes); statistics.receivedMessageTypes.putAll(receivedMessageTypes); statistics.incomingOperationTypes.putAll(incomingOperationTypes); statistics.listenerEventTypes.putAll(listenerEventTypes); statistics.clientErrorTypes.putAll(clientErrorTypes); return statistics; } /** Returns the counter value for {@code clientErrorType}. */ int getClientErrorCounterForTest(ClientErrorType clientErrorType) { return TypedUtil.mapGet(clientErrorTypes, clientErrorType); } /** Returns the counter value for {@code sentMessageType}. */ int getSentMessageCounterForTest(SentMessageType sentMessageType) { return TypedUtil.mapGet(sentMessageTypes, sentMessageType); } /** Returns the counter value for {@code receivedMessageType}. */ int getReceivedMessageCounterForTest(ReceivedMessageType receivedMessageType) { return TypedUtil.mapGet(receivedMessageTypes, receivedMessageType); } /** Records the fact that a message of type {@code sentMessageType} has been sent. */ public void recordSentMessage(SentMessageType sentMessageType) { incrementValue(sentMessageTypes, sentMessageType); } /** Records the fact that a message of type {@code receivedMessageType} has been received. */ public void recordReceivedMessage(ReceivedMessageType receivedMessageType) { incrementValue(receivedMessageTypes, receivedMessageType); } /** * Records the fact that the application has made a call of type * {@code incomingOperationType}. */ public void recordIncomingOperation(IncomingOperationType incomingOperationType) { incrementValue(incomingOperationTypes, incomingOperationType); } /** Records the fact that the listener has issued an event of type {@code listenerEventType}. */ public void recordListenerEvent(ListenerEventType listenerEventType) { incrementValue(listenerEventTypes, listenerEventType); } /** Records the fact that the client has observed an error of type {@code clientErrorType}. */ public void recordError(ClientErrorType clientErrorType) { incrementValue(clientErrorTypes, clientErrorType); } /** * Modifies {@code performanceCounters} to contain all the statistics that are non-zero. Each pair * has the name of the statistic event and the number of times that event has occurred since the * client started. */ public void getNonZeroStatistics(List<SimplePair<String, Integer>> performanceCounters) { // Add the non-zero values from the different maps to performanceCounters. fillWithNonZeroStatistics(sentMessageTypes, performanceCounters, SENT_MESSAGE_TYPE_NAME); fillWithNonZeroStatistics(receivedMessageTypes, performanceCounters, RECEIVED_MESSAGE_TYPE_NAME); fillWithNonZeroStatistics(incomingOperationTypes, performanceCounters, INCOMING_OPERATION_TYPE_NAME); fillWithNonZeroStatistics(listenerEventTypes, performanceCounters, LISTENER_EVENT_TYPE_NAME); fillWithNonZeroStatistics(clientErrorTypes, performanceCounters, CLIENT_ERROR_TYPE_NAME); } /** Modifies {@code result} to contain those statistics from {@code map} whose value is > 0. */ private static <Key extends Enum<Key>> void fillWithNonZeroStatistics(Map<Key, Integer> map, List<SimplePair<String, Integer>> destination, String typeName) { String prefix = typeName + "."; for (Map.Entry<Key, Integer> entry : map.entrySet()) { if (entry.getValue() > 0) { destination.add(SimplePair.of(prefix + entry.getKey().name(), entry.getValue())); } } } /** Initializes a map from enum names to values of the given {@code keys}. */ private static <Key extends Enum<Key>> Map<String, Key> createValueOfMap(Key[] keys) { HashMap<String, Key> map = new HashMap<String, Key>(); for (Key key : keys) { map.put(key.name(), key); } return map; } /** Increments the value of {@code map}[{@code key}] by 1. */ private static <Key> void incrementValue(Map<Key, Integer> map, Key key) { map.put(key, TypedUtil.mapGet(map, key) + 1); } /** Initializes all values for {@code keys} in {@code map} to be 0. */ private static <Key> void initializeMap(Map<Key, Integer> map, Key[] keys) { for (Key key : keys) { map.put(key, 0); } } @Override public void toCompactString(TextBuilder builder) { List<SimplePair<String, Integer>> nonZeroValues = new ArrayList<SimplePair<String, Integer>>(); getNonZeroStatistics(nonZeroValues); builder.appendFormat("Client Statistics: %s\n", nonZeroValues); } @Override public StatisticsState marshal() { // Get all the non-zero counters, convert them to proto PropertyRecord messages, and return // a StatisticsState containing the records. StatisticsState.Builder builder = StatisticsState.newBuilder(); List<SimplePair<String, Integer>> counters = new ArrayList<SimplePair<String, Integer>>(); getNonZeroStatistics(counters); for (SimplePair<String, Integer> counter : counters) { builder.addCounter(CommonProtos2.newPropertyRecord(counter.getFirst(), counter.getSecond())); } return builder.build(); } /** * Given the serialized {@code performanceCounters} of the client statistics, returns a Statistics * object with the performance counter values from {@code performanceCounters}. */ public static Statistics deserializeStatistics(Logger logger, Collection<PropertyRecord> performanceCounters) { Statistics statistics = new Statistics(); // For each counter, parse out the counter name and value. for (PropertyRecord performanceCounter : performanceCounters) { String counterName = performanceCounter.getName(); String[] parts = counterName.split("\\."); if (parts.length != 2) { logger.warning("Perf counter name must of form: class.value, skipping: %s", counterName); continue; } String className = parts[0]; String fieldName = parts[1]; int counterValue = performanceCounter.getValue(); // Call the relevant method in a loop (i.e., depending on the type of the class). if (TypedUtil.<String>equals(className, SENT_MESSAGE_TYPE_NAME)) { incrementPerformanceCounterValue(logger, SENT_MESSAGE_TYPE_NAME_TO_VALUE_MAP, statistics.sentMessageTypes, fieldName, counterValue); } else if (TypedUtil.<String>equals(className, INCOMING_OPERATION_TYPE_NAME)) { incrementPerformanceCounterValue(logger, INCOMING_OPERATION_TYPE_NAME_TO_VALUE_MAP, statistics.incomingOperationTypes, fieldName, counterValue); } else if (TypedUtil.<String>equals(className, RECEIVED_MESSAGE_TYPE_NAME)) { incrementPerformanceCounterValue(logger, RECEIVED_MESSAGE_TYPE_NAME_TO_VALUE_MAP, statistics.receivedMessageTypes, fieldName, counterValue); } else if (TypedUtil.<String>equals(className, LISTENER_EVENT_TYPE_NAME)) { incrementPerformanceCounterValue(logger, LISTENER_EVENT_TYPE_NAME_TO_VALUE_MAP, statistics.listenerEventTypes, fieldName, counterValue); } else if (TypedUtil.<String>equals(className, CLIENT_ERROR_TYPE_NAME)) { incrementPerformanceCounterValue(logger, CLIENT_ERROR_TYPE_NAME_TO_VALUE_MAP, statistics.clientErrorTypes, fieldName, counterValue); } else { logger.warning("Skipping unknown enum class name %s", className); } } return statistics; } /** * Looks for an enum value with the given {@code fieldName} in {@code valueOfMap} and increments * the corresponding entry in {@code counts} by {@code counterValue}. Call to update statistics * for a single performance counter. */ private static <Key extends Enum<Key>> void incrementPerformanceCounterValue(Logger logger, Map<String, Key> valueOfMap, Map<Key, Integer> counts, String fieldName, int counterValue) { Key type = TypedUtil.mapGet(valueOfMap, fieldName); if (type != null) { int currentValue = TypedUtil.mapGet(counts, type); counts.put(type, currentValue + counterValue); } else { logger.warning("Skipping unknown enum value name %s", fieldName); } } }