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