1 /*
2  * Copyright (C) 2012 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.cts.filesystemperf;
18 
19 import java.io.BufferedReader;
20 import java.io.File;
21 import java.io.FileInputStream;
22 import java.io.FileOutputStream;
23 import java.io.IOException;
24 import java.io.InputStreamReader;
25 import java.io.RandomAccessFile;
26 import java.util.Random;
27 
28 import com.android.cts.util.MeasureRun;
29 import com.android.cts.util.MeasureTime;
30 import com.android.cts.util.ResultType;
31 import com.android.cts.util.ResultUnit;
32 import com.android.cts.util.ReportLog;
33 import com.android.cts.util.Stat;
34 import android.cts.util.SystemUtil;
35 
36 import android.content.Context;
37 import android.util.Log;
38 
39 public class FileUtil {
40     private static final String TAG = "FileUtil";
41     private static final Random mRandom = new Random(0);
42     private static long mFileId = 0;
43     /**
44      * create array with different data per each call
45      *
46      * @param length
47      * @param randomSeed
48      * @return
49      */
generateRandomData(int length)50     public static byte[] generateRandomData(int length) {
51         byte[] buffer = new byte[length];
52         int val = mRandom.nextInt();
53         for (int i = 0; i < length / 4; i++) {
54             // in little-endian
55             buffer[i * 4] = (byte)(val & 0x000000ff);
56             buffer[i * 4 + 1] = (byte)((val & 0x0000ff00) >> 8);
57             buffer[i * 4 + 2] = (byte)((val & 0x00ff0000) >> 16);
58             buffer[i * 4 + 3] = (byte)((val & 0xff000000) >> 24);
59             val++;
60         }
61         for (int i = (length / 4) * 4; i < length; i++) {
62             buffer[i] = 0;
63         }
64         return buffer;
65     }
66 
67     /**
68      * create a new file under the given dirName.
69      * Existing files will not be affected.
70      * @param context
71      * @param dirName
72      * @return
73      */
createNewFile(Context context, String dirName)74     public static File createNewFile(Context context, String dirName) {
75         File topDir = new File(context.getFilesDir(), dirName);
76         topDir.mkdir();
77         String[] list = topDir.list();
78 
79         String newFileName;
80         while (true) {
81             newFileName = Long.toString(mFileId);
82             boolean fileExist = false;
83             for (String child : list) {
84                 if (child.equals(newFileName)) {
85                     fileExist = true;
86                     break;
87                 }
88             }
89             if (!fileExist) {
90                 break;
91             }
92             mFileId++;
93         }
94         mFileId++;
95         //Log.i(TAG, "filename" + Long.toString(mFileId));
96         return new File(topDir, newFileName);
97     }
98 
99     /**
100      * create multiple new files
101      * @param context
102      * @param dirName
103      * @param count number of files to create
104      * @return
105      */
createNewFiles(Context context, String dirName, int count)106     public static File[] createNewFiles(Context context, String dirName, int count) {
107         File[] files = new File[count];
108         for (int i = 0; i < count; i++) {
109             files[i] = createNewFile(context, dirName);
110         }
111         return files;
112     }
113 
114     /**
115      * write file with given byte array
116      * @param file
117      * @param data
118      * @param append will append if set true. Otherwise, write from beginning
119      * @throws IOException
120      */
writeFile(File file, byte[] data, boolean append)121     public static void writeFile(File file, byte[] data, boolean append) throws IOException {
122         final RandomAccessFile randomFile = new RandomAccessFile(file, "rwd"); // force O_SYNC
123         if (append) {
124             randomFile.seek(randomFile.length());
125         } else {
126             randomFile.seek(0L);
127         }
128         randomFile.write(data);
129         randomFile.close();
130     }
131 
132     /**
133      * create a new file with given length.
134      * @param context
135      * @param dirName
136      * @param length
137      * @return
138      * @throws IOException
139      */
createNewFilledFile(Context context, String dirName, long length)140     public static File createNewFilledFile(Context context, String dirName, long length)
141             throws IOException {
142         final int BUFFER_SIZE = 10 * 1024 * 1024;
143         File file = createNewFile(context, dirName);
144         FileOutputStream out = new FileOutputStream(file);
145         byte[] data = generateRandomData(BUFFER_SIZE);
146         long written = 0;
147         while (written < length) {
148             out.write(data);
149             written += BUFFER_SIZE;
150         }
151         out.flush();
152         out.close();
153         return file;
154     }
155 
156     /**
157      * remove given file or directory under the current app's files dir.
158      * @param context
159      * @param name
160      */
removeFileOrDir(Context context, String name)161     public static void removeFileOrDir(Context context, String name) {
162         File entry = new File(context.getFilesDir(), name);
163         if (entry.exists()) {
164             removeEntry(entry);
165         }
166     }
167 
removeEntry(File entry)168     private static void removeEntry(File entry) {
169         if (entry.isDirectory()) {
170             String[] children = entry.list();
171             for (String child : children) {
172                 removeEntry(new File(entry, child));
173             }
174         }
175         Log.i(TAG, "delete file " + entry.getAbsolutePath());
176         entry.delete();
177     }
178 
179     /**
180      * measure time taken for each IO run with amount R/W
181      * @param count
182      * @param run
183      * @param readAmount returns amount of read in bytes for each interval.
184      *        Value will not be written if /proc/self/io does not exist.
185      * @param writeAmount returns amount of write in bytes for each interval.
186      * @return time per each interval
187      * @throws IOException
188      */
measureIO(int count, double[] readAmount, double[] writeAmount, MeasureRun run)189     public static double[] measureIO(int count, double[] readAmount, double[] writeAmount,
190             MeasureRun run)  throws Exception {
191         double[] result = new double[count];
192         File procIo = new File("/proc/self/io");
193         boolean measureIo = procIo.exists() && procIo.canRead();
194         long prev = System.currentTimeMillis();
195         RWAmount prevAmount = new RWAmount();
196         if (measureIo) {
197             prevAmount = getRWAmount(procIo);
198         }
199         for (int i = 0; i < count; i++) {
200             run.run(i);
201             long current =  System.currentTimeMillis();
202             result[i] = current - prev;
203             prev = current;
204             if (measureIo) {
205                 RWAmount currentAmount = getRWAmount(procIo);
206                 readAmount[i] = currentAmount.mRd - prevAmount.mRd;
207                 writeAmount[i] = currentAmount.mWr - prevAmount.mWr;
208                 prevAmount = currentAmount;
209             }
210         }
211         return result;
212     }
213 
214     private static class RWAmount {
215         public double mRd = 0.0;
216         public double mWr = 0.0;
217     };
218 
getRWAmount(File file)219     private static RWAmount getRWAmount(File file) throws IOException {
220         RWAmount amount = new RWAmount();
221 
222         BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
223         String line;
224         while((line = br.readLine())!= null) {
225             if (line.startsWith("read_bytes")) {
226                 amount.mRd = Double.parseDouble(line.split(" ")[1]);
227             } else if (line.startsWith("write_bytes")) {
228                 amount.mWr = Double.parseDouble(line.split(" ")[1]);
229             }
230         }
231         br.close();
232         return amount;
233     }
234 
235     /**
236      * get file size exceeding total memory size ( 2x total memory).
237      * The size is rounded in bufferSize. And the size will be bigger than 400MB.
238      * @param context
239      * @param bufferSize
240      * @return file size or 0 if there is not enough space.
241      */
getFileSizeExceedingMemory(Context context, int bufferSize)242     public static long getFileSizeExceedingMemory(Context context, int bufferSize) {
243         long freeDisk = SystemUtil.getFreeDiskSize(context);
244         long memSize = SystemUtil.getTotalMemory(context);
245         long diskSizeTarget = (2 * memSize / bufferSize) * bufferSize;
246         final long minimumDiskSize = (512L * 1024L * 1024L / bufferSize) * bufferSize;
247         final long reservedDiskSize = (50L * 1024L * 1024L / bufferSize) * bufferSize;
248         if ( diskSizeTarget < minimumDiskSize ) {
249             diskSizeTarget = minimumDiskSize;
250         }
251         if (diskSizeTarget > freeDisk) {
252             Log.i(TAG, "Free disk size " + freeDisk + " too small");
253             return 0;
254         }
255         if ((freeDisk - diskSizeTarget) < reservedDiskSize) {
256             diskSizeTarget -= reservedDiskSize;
257         }
258         return diskSizeTarget;
259     }
260 
261     /**
262      *
263      * @param context
264      * @param dirName
265      * @param report
266      * @param fileSize
267      * @param bufferSize should be power of two
268      * @throws IOException
269      */
doRandomReadTest(Context context, String dirName, ReportLog report, long fileSize, int bufferSize)270     public static void doRandomReadTest(Context context, String dirName, ReportLog report,
271             long fileSize, int bufferSize) throws Exception {
272         File file = FileUtil.createNewFilledFile(context,
273                 dirName, fileSize);
274 
275         final byte[] data = FileUtil.generateRandomData(bufferSize);
276         Random random = new Random(0);
277         final int totalReadCount = (int)(fileSize / bufferSize);
278         final int[] readOffsets = new int[totalReadCount];
279         for (int i = 0; i < totalReadCount; i++) {
280             // align in buffer size
281             readOffsets[i] = (int)(random.nextFloat() * (fileSize - bufferSize)) &
282                     ~(bufferSize - 1);
283         }
284         final int runsInOneGo = 16;
285         final int readsInOneMeasure = totalReadCount / runsInOneGo;
286 
287         final RandomAccessFile randomFile = new RandomAccessFile(file, "rw"); // do not need O_SYNC
288         double[] rdAmount = new double[runsInOneGo];
289         double[] wrAmount = new double[runsInOneGo];
290         double[] times = FileUtil.measureIO(runsInOneGo, rdAmount, wrAmount, new MeasureRun() {
291 
292             @Override
293             public void run(int i) throws IOException {
294                 Log.i(TAG, "starting " + i + " -th round");
295                 int start = i * readsInOneMeasure;
296                 int end = (i + 1) * readsInOneMeasure;
297                 for (int j = start; j < end; j++) {
298                     randomFile.seek(readOffsets[j]);
299                     randomFile.read(data);
300                 }
301             }
302         });
303         randomFile.close();
304         double[] mbps = ReportLog.calcRatePerSecArray((double)fileSize / runsInOneGo / 1024 / 1024,
305                 times);
306         report.printArray("read throughput",
307                 mbps, ResultType.HIGHER_BETTER, ResultUnit.MBPS);
308         // This is just the amount of IO returned from kernel. So this is performance neutral.
309         report.printArray("read amount", rdAmount, ResultType.NEUTRAL, ResultUnit.BYTE);
310         Stat.StatResult stat = Stat.getStat(mbps);
311 
312         report.printSummary("read throughput", stat.mAverage, ResultType.HIGHER_BETTER,
313                 ResultUnit.MBPS);
314     }
315 
316     /**
317      *
318      * @param context
319      * @param dirName
320      * @param report
321      * @param fileSize
322      * @param bufferSize should be power of two
323      * @throws IOException
324      */
doRandomWriteTest(Context context, String dirName, ReportLog report, long fileSize, int bufferSize)325     public static void doRandomWriteTest(Context context, String dirName, ReportLog report,
326             long fileSize, int bufferSize) throws Exception {
327         File file = FileUtil.createNewFilledFile(context,
328                 dirName, fileSize);
329         final byte[] data = FileUtil.generateRandomData(bufferSize);
330         Random random = new Random(0);
331         final int totalWriteCount = (int)(fileSize / bufferSize);
332         final int[] writeOffsets = new int[totalWriteCount];
333         for (int i = 0; i < totalWriteCount; i++) {
334             writeOffsets[i] = (int)(random.nextFloat() * (fileSize - bufferSize)) &
335                     ~(bufferSize - 1);
336         }
337         final int runsInOneGo = 16;
338         final int writesInOneMeasure = totalWriteCount / runsInOneGo;
339 
340         final RandomAccessFile randomFile = new RandomAccessFile(file, "rwd"); // force O_SYNC
341         double[] rdAmount = new double[runsInOneGo];
342         double[] wrAmount = new double[runsInOneGo];
343         double[] times = FileUtil.measureIO(runsInOneGo, rdAmount, wrAmount, new MeasureRun() {
344 
345             @Override
346             public void run(int i) throws IOException {
347                 Log.i(TAG, "starting " + i + " -th round");
348                 int start = i * writesInOneMeasure;
349                 int end = (i + 1) * writesInOneMeasure;
350                 for (int j = start; j < end; j++) {
351                     randomFile.seek(writeOffsets[j]);
352                     randomFile.write(data);
353                 }
354             }
355         });
356         randomFile.close();
357         double[] mbps = ReportLog.calcRatePerSecArray((double)fileSize / runsInOneGo / 1024 / 1024,
358                 times);
359         report.printArray("write throughput",
360                 mbps, ResultType.HIGHER_BETTER, ResultUnit.MBPS);
361         report.printArray("write amount", wrAmount, ResultType.NEUTRAL,
362                 ResultUnit.BYTE);
363         Stat.StatResult stat = Stat.getStat(mbps);
364 
365         report.printSummary("write throughput", stat.mAverage, ResultType.HIGHER_BETTER,
366                 ResultUnit.MBPS);
367     }
368 
369     /**
370      *
371      * @param context
372      * @param dirName
373      * @param report
374      * @param fileSize fileSize should be multiple of bufferSize.
375      * @param bufferSize
376      * @param numberRepetition
377      * @throws IOException
378      */
doSequentialUpdateTest(Context context, String dirName, ReportLog report, long fileSize, int bufferSize, int numberRepetition)379     public static void doSequentialUpdateTest(Context context, String dirName, ReportLog report,
380             long fileSize, int bufferSize, int numberRepetition) throws Exception {
381         File file = FileUtil.createNewFilledFile(context,
382                 dirName, fileSize);
383         final byte[] data = FileUtil.generateRandomData(bufferSize);
384         int numberRepeatInOneRun = (int)(fileSize / bufferSize);
385         double[] mbpsAll = new double[numberRepetition * numberRepeatInOneRun];
386         for (int i = 0; i < numberRepetition; i++) {
387             Log.i(TAG, "starting " + i + " -th round");
388             final RandomAccessFile randomFile = new RandomAccessFile(file, "rwd");  // force O_SYNC
389             randomFile.seek(0L);
390             double[] times = MeasureTime.measure(numberRepeatInOneRun, new MeasureRun() {
391 
392                 @Override
393                 public void run(int i) throws IOException {
394                     randomFile.write(data);
395                 }
396             });
397             randomFile.close();
398             double[] mbps = ReportLog.calcRatePerSecArray((double)bufferSize / 1024 / 1024,
399                     times);
400             report.printArray(i + "-th round throughput",
401                     mbps, ResultType.HIGHER_BETTER, ResultUnit.MBPS);
402             ReportLog.copyArray(mbps, mbpsAll, i * numberRepeatInOneRun);
403         }
404         Stat.StatResult stat = Stat.getStat(mbpsAll);
405         report.printSummary("update throughput", stat.mAverage, ResultType.HIGHER_BETTER,
406                 ResultUnit.MBPS);
407     }
408 }
409