1 /*
2  * Copyright 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.helpers;
18 
19 import android.os.SystemClock;
20 import android.system.Os;
21 import android.system.OsConstants;
22 
23 import androidx.annotation.VisibleForTesting;
24 
25 import java.io.BufferedReader;
26 import java.io.FileReader;
27 import java.io.IOException;
28 import java.util.ArrayList;
29 import java.util.Collections;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Set;
33 import java.util.TreeMap;
34 import java.util.concurrent.Executors;
35 import java.util.concurrent.ScheduledExecutorService;
36 import java.util.concurrent.ScheduledFuture;
37 import java.util.concurrent.TimeUnit;
38 
39 /**
40  * SlabinfoHelper parses /proc/slabinfo and reports the rate of increase or decrease in allocation
41  * sizes for each slab over the collection period. The rate is determined from the slope of a line
42  * fit to all samples collected between startCollecting and getMetrics. These metrics are only
43  * useful over long (hours) test durations. Samples are taken once per minute. getMetrics returns
44  * null for tests shorter than one minute.
45  */
46 public class SlabinfoHelper implements ICollectorHelper<Double> {
47     private static final String SLABINFO_PATH = "/proc/slabinfo";
48     private static final long PAGE_SIZE = Os.sysconf(OsConstants._SC_PAGESIZE);
49 
50     private final ScheduledExecutorService mScheduler = Executors.newScheduledThreadPool(1);
51 
52     @VisibleForTesting
53     public static class SlabinfoSample {
54         // seconds, monotonically increasing
55         public long time;
56 
57         // Key: Slab name
58         // Value: size in bytes
59         public Map<String, Long> slabs;
60     }
61 
62     private final List<SlabinfoSample> mSamples =
63             Collections.synchronizedList(new ArrayList((int) TimeUnit.HOURS.toMinutes(8)));
64     private ScheduledFuture<?> mReaderHandle;
65 
66     @Override
startCollecting()67     public boolean startCollecting() {
68         if (!slabinfoVersionIsSupported()) return false;
69 
70         mReaderHandle = mScheduler.scheduleAtFixedRate(mReader, 0, 1, TimeUnit.MINUTES);
71         return true;
72     }
73 
74     @Override
stopCollecting()75     public boolean stopCollecting() {
76         if (mReaderHandle != null) mReaderHandle.cancel(false);
77 
78         mScheduler.shutdownNow();
79 
80         try {
81             mScheduler.awaitTermination(1, TimeUnit.SECONDS);
82         } catch (InterruptedException ex) {
83             Thread.currentThread().interrupt();
84         }
85         mSamples.clear();
86         return true;
87     }
88 
89     @Override
getMetrics()90     public Map<String, Double> getMetrics() {
91         synchronized (mSamples) {
92             if (mSamples.size() < 2) return null;
93 
94             return fitLinesToSamples(mSamples);
95         }
96     }
97 
98     private final Runnable mReader =
99             new Runnable() {
100                 @Override
101                 public void run() {
102                     SlabinfoSample sample = new SlabinfoSample();
103                     try {
104                         sample.time = getMonotonicSeconds();
105                         sample.slabs = readSlabinfo();
106                         synchronized (mSamples) {
107                             mSamples.add(sample);
108                         }
109                     } catch (IOException ex) {
110                         ex.printStackTrace();
111                     }
112                 }
113             };
114 
getMonotonicSeconds()115     private static long getMonotonicSeconds() throws IOException {
116         // NOTE: This is a truncating conversion without rounding
117         return TimeUnit.SECONDS.convert(SystemClock.elapsedRealtime(), TimeUnit.MILLISECONDS);
118     }
119 
slabinfoVersionIsSupported()120     private static boolean slabinfoVersionIsSupported() {
121         try {
122             BufferedReader reader = new BufferedReader(new FileReader(SLABINFO_PATH));
123             String line = reader.readLine();
124             reader.close();
125             return line.equals("slabinfo - version: 2.1");
126         } catch (IOException ex) {
127             ex.printStackTrace();
128             return false;
129         }
130     }
131 
readSlabinfo()132     private static Map<String, Long> readSlabinfo() throws IOException {
133         Map<String, Long> slabinfo = new TreeMap<>();
134 
135         BufferedReader reader = new BufferedReader(new FileReader(SLABINFO_PATH));
136 
137         // Discard the first two header lines
138         reader.readLine();
139         reader.readLine();
140 
141         for (String line = reader.readLine(); line != null; line = reader.readLine()) {
142             // Convert multiple adjacent spaces into a single space for tokenization
143             String tokens[] = line.replaceAll(" +", " ").split(" ");
144             String name = tokens[0];
145             long pagesPerSlab = Long.parseLong(tokens[5]), numSlabs = Long.parseLong(tokens[14]);
146             long bytes = PAGE_SIZE * pagesPerSlab * numSlabs;
147 
148             // Nobody duplicates slab names except for device mapper. Keep the maximum if we
149             // encounter a duplicate slab name.
150             Long val = slabinfo.get(name);
151             if (val != null) val = Math.max(val, bytes);
152             else val = bytes;
153 
154             slabinfo.put(name, val);
155         }
156         reader.close();
157         return slabinfo;
158     }
159 
160     // Returns the slope (bytes/5 minutes) of a line fit to the samples for each slab using the
161     // least squares method. Prefixes slab names with "slabinfo." for metric reporting. Adds an
162     // entry: "slabinfo.duration_seconds" to record the duration of the collection period.
163     @VisibleForTesting
fitLinesToSamples(List<SlabinfoSample> samples)164     public static Map<String, Double> fitLinesToSamples(List<SlabinfoSample> samples) {
165         // Grab slab names from the first entry
166         Set<String> names = samples.get(0).slabs.keySet();
167 
168         // Compute averages for each dimension
169         double xbar = 0;
170         Map<String, Double> ybars = new TreeMap<>();
171         for (String name : names) ybars.put(name, 0.0);
172 
173         for (SlabinfoSample sample : samples) {
174             xbar += sample.time;
175             for (String name : names) ybars.put(name, ybars.get(name) + sample.slabs.get(name));
176         }
177         xbar /= samples.size();
178         for (String name : names) ybars.put(name, ybars.get(name) / samples.size());
179 
180         // Compute slopes
181         Map<String, Double> slopes = new TreeMap<>();
182         for (String name : names) {
183             double num = 0, denom = 0;
184             for (SlabinfoSample sample : samples) {
185                 double delta_x = sample.time - xbar;
186                 double delta_y = sample.slabs.get(name) - ybars.get(name);
187                 num += delta_x * delta_y;
188                 denom += delta_x * delta_x;
189             }
190             slopes.put("slabinfo." + name, num * TimeUnit.MINUTES.toSeconds(5) / denom);
191         }
192 
193         slopes.put(
194                 "slabinfo.duration_seconds",
195                 (double) samples.get(samples.size() - 1).time - samples.get(0).time);
196 
197         return slopes;
198     }
199 }
200