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