• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
21 import androidx.annotation.VisibleForTesting;
22 import androidx.test.platform.app.InstrumentationRegistry;
23 import androidx.test.uiautomator.UiDevice;
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;
49 
50     static {
51         try {
52             UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
53             String pageSize = uiDevice.executeShellCommand("getconf PAGE_SIZE").trim();
54             PAGE_SIZE = Long.parseLong(pageSize);
55         } catch (IOException ex) {
56             throw new ExceptionInInitializerError(ex);
57         }
58     }
59 
60     private final ScheduledExecutorService mScheduler = Executors.newScheduledThreadPool(1);
61 
62     @VisibleForTesting
63     public static class SlabinfoSample {
64         // seconds, monotonically increasing
65         public long time;
66 
67         // Key: Slab name
68         // Value: size in bytes
69         public Map<String, Long> slabs;
70     }
71 
72     private final List<SlabinfoSample> mSamples =
73             Collections.synchronizedList(new ArrayList((int) TimeUnit.HOURS.toMinutes(8)));
74     private ScheduledFuture<?> mReaderHandle;
75 
76     @Override
startCollecting()77     public boolean startCollecting() {
78         if (!slabinfoVersionIsSupported()) return false;
79 
80         mReaderHandle = mScheduler.scheduleAtFixedRate(mReader, 0, 1, TimeUnit.MINUTES);
81         return true;
82     }
83 
84     @Override
stopCollecting()85     public boolean stopCollecting() {
86         if (mReaderHandle != null) mReaderHandle.cancel(false);
87 
88         mScheduler.shutdownNow();
89 
90         try {
91             mScheduler.awaitTermination(1, TimeUnit.SECONDS);
92         } catch (InterruptedException ex) {
93             Thread.currentThread().interrupt();
94         }
95         mSamples.clear();
96         return true;
97     }
98 
99     @Override
getMetrics()100     public Map<String, Double> getMetrics() {
101         synchronized (mSamples) {
102             if (mSamples.size() < 2) return null;
103 
104             return fitLinesToSamples(mSamples);
105         }
106     }
107 
108     private final Runnable mReader =
109             new Runnable() {
110                 @Override
111                 public void run() {
112                     SlabinfoSample sample = new SlabinfoSample();
113                     try {
114                         sample.time = getMonotonicSeconds();
115                         sample.slabs = readSlabinfo();
116                         synchronized (mSamples) {
117                             mSamples.add(sample);
118                         }
119                     } catch (IOException ex) {
120                         ex.printStackTrace();
121                     }
122                 }
123             };
124 
getMonotonicSeconds()125     private static long getMonotonicSeconds() throws IOException {
126         // NOTE: This is a truncating conversion without rounding
127         return TimeUnit.SECONDS.convert(SystemClock.elapsedRealtime(), TimeUnit.MILLISECONDS);
128     }
129 
slabinfoVersionIsSupported()130     private static boolean slabinfoVersionIsSupported() {
131         try {
132             BufferedReader reader = new BufferedReader(new FileReader(SLABINFO_PATH));
133             String line = reader.readLine();
134             reader.close();
135             return line.equals("slabinfo - version: 2.1");
136         } catch (IOException ex) {
137             ex.printStackTrace();
138             return false;
139         }
140     }
141 
readSlabinfo()142     private static Map<String, Long> readSlabinfo() throws IOException {
143         Map<String, Long> slabinfo = new TreeMap<>();
144 
145         BufferedReader reader = new BufferedReader(new FileReader(SLABINFO_PATH));
146 
147         // Discard the first two header lines
148         reader.readLine();
149         reader.readLine();
150 
151         for (String line = reader.readLine(); line != null; line = reader.readLine()) {
152             // Convert multiple adjacent spaces into a single space for tokenization
153             String tokens[] = line.replaceAll(" +", " ").split(" ");
154             String name = tokens[0];
155             long pagesPerSlab = Long.parseLong(tokens[5]), numSlabs = Long.parseLong(tokens[14]);
156             long bytes = PAGE_SIZE * pagesPerSlab * numSlabs;
157 
158             // Nobody duplicates slab names except for device mapper. Keep the maximum if we
159             // encounter a duplicate slab name.
160             Long val = slabinfo.get(name);
161             if (val != null) val = Math.max(val, bytes);
162             else val = bytes;
163 
164             slabinfo.put(name, val);
165         }
166         reader.close();
167         return slabinfo;
168     }
169 
170     // Returns the slope (bytes/5 minutes) of a line fit to the samples for each slab using the
171     // least squares method. Prefixes slab names with "slabinfo." for metric reporting. Adds an
172     // entry: "slabinfo.duration_seconds" to record the duration of the collection period.
173     @VisibleForTesting
fitLinesToSamples(List<SlabinfoSample> samples)174     public static Map<String, Double> fitLinesToSamples(List<SlabinfoSample> samples) {
175         // Grab slab names from the first entry
176         Set<String> names = samples.get(0).slabs.keySet();
177 
178         // Compute averages for each dimension
179         double xbar = 0;
180         Map<String, Double> ybars = new TreeMap<>();
181         for (String name : names) ybars.put(name, 0.0);
182 
183         for (SlabinfoSample sample : samples) {
184             xbar += sample.time;
185             for (String name : names) ybars.put(name, ybars.get(name) + sample.slabs.get(name));
186         }
187         xbar /= samples.size();
188         for (String name : names) ybars.put(name, ybars.get(name) / samples.size());
189 
190         // Compute slopes
191         Map<String, Double> slopes = new TreeMap<>();
192         for (String name : names) {
193             double num = 0, denom = 0;
194             for (SlabinfoSample sample : samples) {
195                 double delta_x = sample.time - xbar;
196                 double delta_y = sample.slabs.get(name) - ybars.get(name);
197                 num += delta_x * delta_y;
198                 denom += delta_x * delta_x;
199             }
200             slopes.put("slabinfo." + name, num * TimeUnit.MINUTES.toSeconds(5) / denom);
201         }
202 
203         slopes.put(
204                 "slabinfo.duration_seconds",
205                 (double) samples.get(samples.size() - 1).time - samples.get(0).time);
206 
207         return slopes;
208     }
209 }
210