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