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