1 /*
2  * Copyright (C) 2023 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.power.stats;
18 
19 import android.annotation.CurrentTimeMillisLong;
20 import android.annotation.DurationMillisLong;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.util.IndentingPrintWriter;
24 import android.util.Slog;
25 import android.util.TimeUtils;
26 
27 import com.android.internal.annotations.VisibleForTesting;
28 import com.android.modules.utils.TypedXmlPullParser;
29 import com.android.modules.utils.TypedXmlSerializer;
30 
31 import com.google.android.collect.Sets;
32 
33 import org.xmlpull.v1.XmlPullParser;
34 import org.xmlpull.v1.XmlPullParserException;
35 
36 import java.io.IOException;
37 import java.io.InputStream;
38 import java.io.OutputStream;
39 import java.io.StringWriter;
40 import java.nio.charset.StandardCharsets;
41 import java.time.Instant;
42 import java.time.ZoneId;
43 import java.time.format.DateTimeFormatter;
44 import java.util.ArrayList;
45 import java.util.Comparator;
46 import java.util.List;
47 import java.util.Set;
48 
49 /**
50  * Contains power stats of various kinds, aggregated over a time span.
51  */
52 public class PowerStatsSpan {
53     private static final String TAG = "PowerStatsStore";
54 
55     /**
56      * Increment VERSION when the XML format of the store changes. Also, update
57      * {@link #isCompatibleXmlFormat} to return true for all legacy versions
58      * that are compatible with the new one.
59      */
60     private static final int VERSION = 2;
61 
62     private static final String XML_TAG_METADATA = "metadata";
63     private static final String XML_ATTR_ID = "id";
64     private static final String XML_ATTR_VERSION = "version";
65     private static final String XML_TAG_TIMEFRAME = "timeframe";
66     private static final String XML_ATTR_MONOTONIC = "monotonic";
67     private static final String XML_ATTR_START_TIME = "start";
68     private static final String XML_ATTR_DURATION = "duration";
69     private static final String XML_TAG_SECTION = "section";
70     private static final String XML_ATTR_SECTION_TYPE = "type";
71 
72     private static final DateTimeFormatter DATE_FORMAT =
73             DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS").withZone(ZoneId.systemDefault());
74 
75     static class TimeFrame {
76         public final long startMonotonicTime;
77         @CurrentTimeMillisLong
78         public final long startTime;
79         @DurationMillisLong
80         public final long duration;
81 
TimeFrame(long startMonotonicTime, @CurrentTimeMillisLong long startTime, @DurationMillisLong long duration)82         TimeFrame(long startMonotonicTime, @CurrentTimeMillisLong long startTime,
83                 @DurationMillisLong long duration) {
84             this.startMonotonicTime = startMonotonicTime;
85             this.startTime = startTime;
86             this.duration = duration;
87         }
88 
write(TypedXmlSerializer serializer)89         void write(TypedXmlSerializer serializer) throws IOException {
90             serializer.startTag(null, XML_TAG_TIMEFRAME);
91             serializer.attributeLong(null, XML_ATTR_START_TIME, startTime);
92             serializer.attributeLong(null, XML_ATTR_MONOTONIC, startMonotonicTime);
93             serializer.attributeLong(null, XML_ATTR_DURATION, duration);
94             serializer.endTag(null, XML_TAG_TIMEFRAME);
95         }
96 
read(TypedXmlPullParser parser)97         static TimeFrame read(TypedXmlPullParser parser) throws XmlPullParserException {
98             return new TimeFrame(
99                     parser.getAttributeLong(null, XML_ATTR_MONOTONIC),
100                     parser.getAttributeLong(null, XML_ATTR_START_TIME),
101                     parser.getAttributeLong(null, XML_ATTR_DURATION));
102         }
103 
104         /**
105          * Prints the contents of this TimeFrame.
106          */
dump(IndentingPrintWriter pw)107         public void dump(IndentingPrintWriter pw) {
108             StringBuilder sb = new StringBuilder();
109             sb.append(DATE_FORMAT.format(Instant.ofEpochMilli(startTime)))
110                     .append(" (monotonic=").append(startMonotonicTime).append(") ")
111                     .append(" duration=");
112             String durationString = TimeUtils.formatDuration(duration);
113             if (durationString.startsWith("+")) {
114                 sb.append(durationString.substring(1));
115             } else {
116                 sb.append(durationString);
117             }
118             pw.print(sb);
119         }
120     }
121 
122     static class Metadata {
123         static final Comparator<Metadata> COMPARATOR = Comparator.comparing(Metadata::getId);
124 
125         private final long mId;
126         private final List<TimeFrame> mTimeFrames = new ArrayList<>();
127         private final List<String> mSections = new ArrayList<>();
128 
Metadata(long id)129         Metadata(long id) {
130             mId = id;
131         }
132 
getId()133         public long getId() {
134             return mId;
135         }
136 
getTimeFrames()137         public List<TimeFrame> getTimeFrames() {
138             return mTimeFrames;
139         }
140 
getSections()141         public List<String> getSections() {
142             return mSections;
143         }
144 
addTimeFrame(TimeFrame timeFrame)145         void addTimeFrame(TimeFrame timeFrame) {
146             mTimeFrames.add(timeFrame);
147         }
148 
addSection(String sectionType)149         void addSection(String sectionType) {
150             // The number of sections per span is small, so there is no need to use a Set
151             if (!mSections.contains(sectionType)) {
152                 mSections.add(sectionType);
153             }
154         }
155 
write(TypedXmlSerializer serializer)156         void write(TypedXmlSerializer serializer) throws IOException {
157             serializer.startTag(null, XML_TAG_METADATA);
158             serializer.attributeLong(null, XML_ATTR_ID, mId);
159             serializer.attributeInt(null, XML_ATTR_VERSION, VERSION);
160             for (TimeFrame timeFrame : mTimeFrames) {
161                 timeFrame.write(serializer);
162             }
163             for (String section : mSections) {
164                 serializer.startTag(null, XML_TAG_SECTION);
165                 serializer.attribute(null, XML_ATTR_SECTION_TYPE, section);
166                 serializer.endTag(null, XML_TAG_SECTION);
167             }
168             serializer.endTag(null, XML_TAG_METADATA);
169         }
170 
171         /**
172          * Reads just the header of the XML file containing metadata.
173          * Returns null if the file does not contain a compatible &lt;metadata&gt; element.
174          */
175         @Nullable
read(TypedXmlPullParser parser)176         public static Metadata read(TypedXmlPullParser parser)
177                 throws IOException, XmlPullParserException {
178             Metadata metadata = null;
179             int eventType = parser.getEventType();
180             while (eventType != XmlPullParser.END_DOCUMENT
181                    && !(eventType == XmlPullParser.END_TAG
182                         && parser.getName().equals(XML_TAG_METADATA))) {
183                 if (eventType == XmlPullParser.START_TAG) {
184                     String tagName = parser.getName();
185                     if (tagName.equals(XML_TAG_METADATA)) {
186                         int version = parser.getAttributeInt(null, XML_ATTR_VERSION);
187                         if (!isCompatibleXmlFormat(version)) {
188                             Slog.e(TAG,
189                                     "Incompatible version " + version + "; expected " + VERSION);
190                             return null;
191                         }
192 
193                         long id = parser.getAttributeLong(null, XML_ATTR_ID);
194                         metadata = new Metadata(id);
195                     } else if (metadata != null && tagName.equals(XML_TAG_TIMEFRAME)) {
196                         metadata.addTimeFrame(TimeFrame.read(parser));
197                     } else if (metadata != null && tagName.equals(XML_TAG_SECTION)) {
198                         metadata.addSection(parser.getAttributeValue(null, XML_ATTR_SECTION_TYPE));
199                     }
200                 }
201                 eventType = parser.next();
202             }
203             return metadata;
204         }
205 
206         /**
207          * Prints the metadata.
208          */
dump(IndentingPrintWriter pw)209         public void dump(IndentingPrintWriter pw) {
210             dump(pw, true);
211         }
212 
dump(IndentingPrintWriter pw, boolean includeSections)213         void dump(IndentingPrintWriter pw, boolean includeSections) {
214             pw.print("Span ");
215             if (mTimeFrames.size() > 0) {
216                 mTimeFrames.get(0).dump(pw);
217                 pw.println();
218             }
219 
220             // Sometimes, when the wall clock is adjusted in the middle of a stats session,
221             // we will have more than one time frame.
222             for (int i = 1; i < mTimeFrames.size(); i++) {
223                 TimeFrame timeFrame = mTimeFrames.get(i);
224                 pw.print("     ");      // Aligned below "Span "
225                 timeFrame.dump(pw);
226                 pw.println();
227             }
228 
229             if (includeSections) {
230                 pw.increaseIndent();
231                 for (String section : mSections) {
232                     pw.print("section", section);
233                     pw.println();
234                 }
235                 pw.decreaseIndent();
236             }
237         }
238 
239         @Override
toString()240         public String toString() {
241             StringWriter sw = new StringWriter();
242             IndentingPrintWriter ipw = new IndentingPrintWriter(sw);
243             ipw.print("id", mId);
244             for (int i = 0; i < mTimeFrames.size(); i++) {
245                 TimeFrame timeFrame = mTimeFrames.get(i);
246                 ipw.print("timeframe=[");
247                 timeFrame.dump(ipw);
248                 ipw.print("] ");
249             }
250             for (String section : mSections) {
251                 ipw.print("section", section);
252             }
253             ipw.flush();
254             return sw.toString().trim();
255         }
256     }
257 
258     /**
259      * Contains a specific type of aggregate power stats.  The contents type is determined by
260      * the section type.
261      */
262     public abstract static class Section {
263         private final String mType;
264 
Section(String type)265         Section(String type) {
266             mType = type;
267         }
268 
269         /**
270          * Returns the section type, which determines the type of data stored in the corresponding
271          * section of {@link PowerStatsSpan}
272          */
getType()273         public String getType() {
274             return mType;
275         }
276 
write(TypedXmlSerializer serializer)277         abstract void write(TypedXmlSerializer serializer) throws IOException;
278 
279         /**
280          * Prints the section type.
281          */
dump(IndentingPrintWriter ipw)282         public void dump(IndentingPrintWriter ipw) {
283             ipw.println(mType);
284         }
285     }
286 
287     /**
288      * A universal XML parser for {@link PowerStatsSpan.Section}'s.  It is aware of all
289      * supported section types as well as their corresponding XML formats.
290      */
291     public interface SectionReader {
292         /**
293          * Reads the contents of the section using the parser. The type of the object
294          * read and the corresponding XML format are determined by the section type.
295          */
read(String sectionType, TypedXmlPullParser parser)296         Section read(String sectionType, TypedXmlPullParser parser)
297                 throws IOException, XmlPullParserException;
298     }
299 
300     private final Metadata mMetadata;
301     private final List<Section> mSections = new ArrayList<>();
302 
PowerStatsSpan(long id)303     public PowerStatsSpan(long id) {
304         this(new Metadata(id));
305     }
306 
PowerStatsSpan(Metadata metadata)307     private PowerStatsSpan(Metadata metadata) {
308         mMetadata = metadata;
309     }
310 
getMetadata()311     public Metadata getMetadata() {
312         return mMetadata;
313     }
314 
getId()315     public long getId() {
316         return mMetadata.mId;
317     }
318 
addTimeFrame(long monotonicTime, @CurrentTimeMillisLong long wallClockTime, @DurationMillisLong long duration)319     void addTimeFrame(long monotonicTime, @CurrentTimeMillisLong long wallClockTime,
320             @DurationMillisLong long duration) {
321         mMetadata.mTimeFrames.add(new TimeFrame(monotonicTime, wallClockTime, duration));
322     }
323 
addSection(Section section)324     void addSection(Section section) {
325         mMetadata.addSection(section.getType());
326         mSections.add(section);
327     }
328 
329     @NonNull
getSections()330     public List<Section> getSections() {
331         return mSections;
332     }
333 
isCompatibleXmlFormat(int version)334     private static boolean isCompatibleXmlFormat(int version) {
335         return version == VERSION;
336     }
337 
338     /**
339      * Creates an XML file containing the persistent state of the power stats span.
340      */
341     @VisibleForTesting
writeXml(OutputStream out, TypedXmlSerializer serializer)342     public void writeXml(OutputStream out, TypedXmlSerializer serializer) throws IOException {
343         serializer.setOutput(out, StandardCharsets.UTF_8.name());
344         serializer.startDocument(null, true);
345         mMetadata.write(serializer);
346         for (Section section : mSections) {
347             serializer.startTag(null, XML_TAG_SECTION);
348             serializer.attribute(null, XML_ATTR_SECTION_TYPE, section.mType);
349             section.write(serializer);
350             serializer.endTag(null, XML_TAG_SECTION);
351         }
352         serializer.endDocument();
353     }
354 
355     @Nullable
read(InputStream in, TypedXmlPullParser parser, SectionReader sectionReader, String... sectionTypes)356     static PowerStatsSpan read(InputStream in, TypedXmlPullParser parser,
357             SectionReader sectionReader, String... sectionTypes)
358             throws IOException, XmlPullParserException {
359         Set<String> neededSections = Sets.newArraySet(sectionTypes);
360         boolean selectSections = !neededSections.isEmpty();
361         parser.setInput(in, StandardCharsets.UTF_8.name());
362 
363         Metadata metadata = Metadata.read(parser);
364         if (metadata == null) {
365             return null;
366         }
367 
368         PowerStatsSpan span = new PowerStatsSpan(metadata);
369         boolean skipSection = false;
370         int nestingLevel = 0;
371         int eventType = parser.getEventType();
372         while (eventType != XmlPullParser.END_DOCUMENT) {
373             if (skipSection) {
374                 if (eventType == XmlPullParser.END_TAG
375                         && parser.getName().equals(XML_TAG_SECTION)) {
376                     nestingLevel--;
377                     if (nestingLevel == 0) {
378                         skipSection = false;
379                     }
380                 } else if (eventType == XmlPullParser.START_TAG
381                            && parser.getName().equals(XML_TAG_SECTION)) {
382                     nestingLevel++;
383                 }
384             } else if (eventType == XmlPullParser.START_TAG) {
385                 String tag = parser.getName();
386                 if (tag.equals(XML_TAG_SECTION)) {
387                     String sectionType = parser.getAttributeValue(null, XML_ATTR_SECTION_TYPE);
388                     if (!selectSections || neededSections.contains(sectionType)) {
389                         Section section = sectionReader.read(sectionType, parser);
390                         if (section == null) {
391                             if (selectSections) {
392                                 throw new XmlPullParserException(
393                                         "Unsupported PowerStatsStore section type: " + sectionType);
394                             } else {
395                                 section = new Section(sectionType) {
396                                     @Override
397                                     public void dump(IndentingPrintWriter ipw) {
398                                         ipw.println("Unsupported PowerStatsStore section type: "
399                                                     + sectionType);
400                                     }
401 
402                                     @Override
403                                     void write(TypedXmlSerializer serializer) {
404                                     }
405                                 };
406                             }
407                         }
408                         span.addSection(section);
409                     } else {
410                         skipSection = true;
411                     }
412                 } else if (tag.equals(XML_TAG_METADATA)) {
413                     Metadata.read(parser);
414                 }
415             }
416             eventType = parser.next();
417         }
418         return span;
419     }
420 
421     /**
422      * Prints the contents of this power stats span.
423      */
dump(IndentingPrintWriter ipw)424     public void dump(IndentingPrintWriter ipw) {
425         mMetadata.dump(ipw, /* includeSections */ false);
426         for (Section section : mSections) {
427             ipw.increaseIndent();
428             ipw.println(section.mType);
429             section.dump(ipw);
430             ipw.decreaseIndent();
431         }
432     }
433 }
434