1 /* 2 * Copyright (C) 2024 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 com.android.server.vibrator; 18 19 import android.os.SystemClock; 20 import android.util.IndentingPrintWriter; 21 import android.util.SparseArray; 22 import android.util.proto.ProtoOutputStream; 23 24 import java.util.ArrayDeque; 25 26 /** 27 * A generic grouped list of aggregated log records to be printed in dumpsys. 28 * 29 * <p>This can be used to dump history of operations or requests to the vibrator services, e.g. 30 * vibration requests grouped by usage or vibration parameters sent to the vibrator control service. 31 * 32 * @param <T> The type of log entries aggregated in this record. 33 */ 34 abstract class GroupedAggregatedLogRecords<T extends GroupedAggregatedLogRecords.SingleLogRecord> { 35 private final SparseArray<ArrayDeque<AggregatedLogRecord<T>>> mGroupedRecords; 36 private final int mSizeLimit; 37 private final int mAggregationTimeLimitMs; 38 GroupedAggregatedLogRecords(int sizeLimit, int aggregationTimeLimitMs)39 GroupedAggregatedLogRecords(int sizeLimit, int aggregationTimeLimitMs) { 40 mGroupedRecords = new SparseArray<>(); 41 mSizeLimit = sizeLimit; 42 mAggregationTimeLimitMs = aggregationTimeLimitMs; 43 } 44 45 /** Prints a header to identify the group to be logged. */ dumpGroupHeader(IndentingPrintWriter pw, int groupKey)46 abstract void dumpGroupHeader(IndentingPrintWriter pw, int groupKey); 47 48 /** Returns the {@link ProtoOutputStream} repeated field id to log records of this group. */ findGroupKeyProtoFieldId(int groupKey)49 abstract long findGroupKeyProtoFieldId(int groupKey); 50 51 /** 52 * Adds given entry to this record list, dropping the oldest record if size limit was reached 53 * for its group. 54 * 55 * @param record The new {@link SingleLogRecord} to be recorded. 56 * @return The oldest {@link AggregatedLogRecord} entry being dropped from the group list if 57 * it's full, null otherwise. 58 */ add(T record)59 final synchronized AggregatedLogRecord<T> add(T record) { 60 int groupKey = record.getGroupKey(); 61 if (!mGroupedRecords.contains(groupKey)) { 62 mGroupedRecords.put(groupKey, new ArrayDeque<>(mSizeLimit)); 63 } 64 ArrayDeque<AggregatedLogRecord<T>> records = mGroupedRecords.get(groupKey); 65 if (mAggregationTimeLimitMs > 0 && !records.isEmpty()) { 66 AggregatedLogRecord<T> lastAggregatedRecord = records.getLast(); 67 if (lastAggregatedRecord.mayAggregate(record, mAggregationTimeLimitMs)) { 68 lastAggregatedRecord.record(record); 69 return null; 70 } 71 } 72 AggregatedLogRecord<T> removedRecord = null; 73 if (records.size() >= mSizeLimit) { 74 removedRecord = records.removeFirst(); 75 } 76 records.addLast(new AggregatedLogRecord<>(record)); 77 return removedRecord; 78 } 79 dump(IndentingPrintWriter pw)80 final synchronized void dump(IndentingPrintWriter pw) { 81 for (int i = 0; i < mGroupedRecords.size(); i++) { 82 dumpGroupHeader(pw, mGroupedRecords.keyAt(i)); 83 pw.increaseIndent(); 84 for (AggregatedLogRecord<T> records : mGroupedRecords.valueAt(i)) { 85 records.dump(pw); 86 } 87 pw.decreaseIndent(); 88 pw.println(); 89 } 90 } 91 dump(ProtoOutputStream proto)92 final synchronized void dump(ProtoOutputStream proto) { 93 for (int i = 0; i < mGroupedRecords.size(); i++) { 94 long fieldId = findGroupKeyProtoFieldId(mGroupedRecords.keyAt(i)); 95 for (AggregatedLogRecord<T> records : mGroupedRecords.valueAt(i)) { 96 records.dump(proto, fieldId); 97 } 98 } 99 } 100 101 /** 102 * Represents an aggregation of log record entries that can be printed in a compact manner. 103 * 104 * <p>The aggregation is controlled by a time limit on the difference between the creation time 105 * of two consecutive entries that {@link SingleLogRecord#mayAggregate}. 106 * 107 * @param <T> The type of log entries aggregated in this record. 108 */ 109 static final class AggregatedLogRecord<T extends SingleLogRecord> { 110 private final T mFirst; 111 private T mLatest; 112 private int mCount; 113 AggregatedLogRecord(T record)114 AggregatedLogRecord(T record) { 115 mLatest = mFirst = record; 116 mCount = 1; 117 } 118 getLatest()119 T getLatest() { 120 return mLatest; 121 } 122 mayAggregate(T record, long timeLimitMs)123 synchronized boolean mayAggregate(T record, long timeLimitMs) { 124 long timeDeltaMs = Math.abs(mLatest.getCreateUptimeMs() - record.getCreateUptimeMs()); 125 return mLatest.mayAggregate(record) && timeDeltaMs < timeLimitMs; 126 } 127 record(T record)128 synchronized void record(T record) { 129 mLatest = record; 130 mCount++; 131 } 132 dump(IndentingPrintWriter pw)133 synchronized void dump(IndentingPrintWriter pw) { 134 mFirst.dump(pw); 135 if (mCount == 1) { 136 return; 137 } 138 if (mCount > 2) { 139 pw.println("-> Skipping " + (mCount - 2) + " aggregated entries, latest:"); 140 } 141 mLatest.dump(pw); 142 } 143 dump(ProtoOutputStream proto, long fieldId)144 synchronized void dump(ProtoOutputStream proto, long fieldId) { 145 mFirst.dump(proto, fieldId); 146 if (mCount > 1) { 147 mLatest.dump(proto, fieldId); 148 } 149 } 150 } 151 152 /** 153 * Represents a single log entry that can be grouped and aggregated for compact logging. 154 * 155 * <p>Entries are first grouped by an integer group key, and then aggregated with consecutive 156 * entries of same group within a limited timespan. 157 */ 158 interface SingleLogRecord { 159 160 /** The group identifier for this record (e.g. vibration usage). */ getGroupKey()161 int getGroupKey(); 162 163 /** 164 * The timestamp in millis that should be used for aggregation of close entries. 165 * 166 * <p>Should be {@link SystemClock#uptimeMillis()} to be used for calculations. 167 */ getCreateUptimeMs()168 long getCreateUptimeMs(); 169 170 /** 171 * Returns true if this record can be aggregated with the given one (e.g. the represent the 172 * same vibration request from the same process client). 173 */ mayAggregate(SingleLogRecord record)174 boolean mayAggregate(SingleLogRecord record); 175 176 /** Writes this record into given {@link IndentingPrintWriter}. */ dump(IndentingPrintWriter pw)177 void dump(IndentingPrintWriter pw); 178 179 /** Writes this record into given {@link ProtoOutputStream} field. */ dump(ProtoOutputStream proto, long fieldId)180 void dump(ProtoOutputStream proto, long fieldId); 181 } 182 } 183