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         FileOutputStream out = new FileOutputStream(file);
147         byte[] data = generateRandomData(BUFFER_SIZE);
148         long written = 0;
149         while (written < length) {
150             int toWrite = (int) Math.min(BUFFER_SIZE, length - written);
151             out.write(data, 0, toWrite);
152             written += toWrite;
153         }
154         out.flush();
155         out.close();
156         return file;
157     }
158 
159     /**
160      * remove given file or directory under the current app's files dir.
161      * @param context
162      * @param name
163      */
removeFileOrDir(Context context, String name)164     public static void removeFileOrDir(Context context, String name) {
165         File entry = new File(context.getFilesDir(), name);
166         if (entry.exists()) {
167             removeEntry(entry);
168         }
169     }
170 
removeEntry(File entry)171     private static void removeEntry(File entry) {
172         if (entry.isDirectory()) {
173             String[] children = entry.list();
174             for (String child : children) {
175                 removeEntry(new File(entry, child));
176             }
177         }
178         Log.i(TAG, "delete file " + entry.getAbsolutePath());
179         entry.delete();
180     }
181 
182     /**
183      * measure time taken for each IO run with amount R/W
184      * @param count
185      * @param run
186      * @param readAmount returns amount of read in bytes for each interval.
187      *        Value will not be written if /proc/self/io does not exist.
188      * @param writeAmount returns amount of write in bytes for each interval.
189      * @return time per each interval
190      * @throws IOException
191      */
measureIO(int count, double[] readAmount, double[] writeAmount, MeasureRun run)192     public static double[] measureIO(int count, double[] readAmount, double[] writeAmount,
193             MeasureRun run)  throws Exception {
194         double[] result = new double[count];
195         File procIo = new File("/proc/self/io");
196         boolean measureIo = procIo.exists() && procIo.canRead();
197         long prev = System.currentTimeMillis();
198         RWAmount prevAmount = new RWAmount();
199         if (measureIo) {
200             prevAmount = getRWAmount(procIo);
201         }
202         for (int i = 0; i < count; i++) {
203             run.run(i);
204             long current =  System.currentTimeMillis();
205             result[i] = current - prev;
206             prev = current;
207             if (measureIo) {
208                 RWAmount currentAmount = getRWAmount(procIo);
209                 readAmount[i] = currentAmount.mRd - prevAmount.mRd;
210                 writeAmount[i] = currentAmount.mWr - prevAmount.mWr;
211                 prevAmount = currentAmount;
212             }
213         }
214         return result;
215     }
216 
217     private static class RWAmount {
218         public double mRd = 0.0;
219         public double mWr = 0.0;
220     };
221 
getRWAmount(File file)222     private static RWAmount getRWAmount(File file) throws IOException {
223         RWAmount amount = new RWAmount();
224 
225         BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
226         String line;
227         while((line = br.readLine())!= null) {
228             if (line.startsWith("read_bytes")) {
229                 amount.mRd = Double.parseDouble(line.split(" ")[1]);
230             } else if (line.startsWith("write_bytes")) {
231                 amount.mWr = Double.parseDouble(line.split(" ")[1]);
232             }
233         }
234         br.close();
235         return amount;
236     }
237 
238     /**
239      * get file size exceeding total memory size ( 2x total memory).
240      * The size is rounded in bufferSize. And the size will be bigger than 400MB.
241      * @param context
242      * @param bufferSize
243      * @return file size or 0 if there is not enough space.
244      */
getFileSizeExceedingMemory(Context context, int bufferSize)245     public static long getFileSizeExceedingMemory(Context context, int bufferSize) {
246         long freeDisk = SystemUtil.getFreeDiskSize(context);
247         long memSize = SystemUtil.getTotalMemory(context);
248         long diskSizeTarget = (2 * memSize / bufferSize) * bufferSize;
249         final long minimumDiskSize = (512L * 1024L * 1024L / bufferSize) * bufferSize;
250         final long reservedDiskSize = (50L * 1024L * 1024L / bufferSize) * bufferSize;
251         if ( diskSizeTarget < minimumDiskSize ) {
252             diskSizeTarget = minimumDiskSize;
253         }
254         if (diskSizeTarget > freeDisk) {
255             Log.i(TAG, "Free disk size " + freeDisk + " too small");
256             return 0;
257         }
258         if ((freeDisk - diskSizeTarget) < reservedDiskSize) {
259             diskSizeTarget -= reservedDiskSize;
260         }
261         return diskSizeTarget;
262     }
263 
264     /**
265      *
266      * @param context
267      * @param dirName
268      * @param report
269      * @param fileSize
270      * @param bufferSize should be power of two
271      * @throws IOException
272      */
doRandomReadTest(Context context, String dirName, ReportLog report, long fileSize, int bufferSize)273     public static void 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     }
317 
318     /**
319      *
320      * @param context
321      * @param dirName
322      * @param report
323      * @param fileSize
324      * @param bufferSize should be power of two
325      * @throws IOException
326      */
doRandomWriteTest(Context context, String dirName, ReportLog report, long fileSize, int bufferSize)327     public static void doRandomWriteTest(Context context, String dirName, ReportLog report,
328             long fileSize, int bufferSize) throws Exception {
329         File file = FileUtil.createNewFilledFile(context,
330                 dirName, fileSize);
331         final byte[] data = FileUtil.generateRandomData(bufferSize);
332         Random random = new Random(0);
333         final int totalWriteCount = (int)(fileSize / bufferSize);
334         final int[] writeOffsets = new int[totalWriteCount];
335         for (int i = 0; i < totalWriteCount; i++) {
336             writeOffsets[i] = (int)(random.nextFloat() * (fileSize - bufferSize)) &
337                     ~(bufferSize - 1);
338         }
339         final int runsInOneGo = 16;
340         final int writesInOneMeasure = totalWriteCount / runsInOneGo;
341 
342         final RandomAccessFile randomFile = new RandomAccessFile(file, "rwd"); // force O_SYNC
343         double[] rdAmount = new double[runsInOneGo];
344         double[] wrAmount = new double[runsInOneGo];
345         double[] times = FileUtil.measureIO(runsInOneGo, rdAmount, wrAmount, new MeasureRun() {
346 
347             @Override
348             public void run(int i) throws IOException {
349                 Log.i(TAG, "starting " + i + " -th round");
350                 int start = i * writesInOneMeasure;
351                 int end = (i + 1) * writesInOneMeasure;
352                 for (int j = start; j < end; j++) {
353                     randomFile.seek(writeOffsets[j]);
354                     randomFile.write(data);
355                 }
356             }
357         });
358         randomFile.close();
359         double[] mbps = Stat.calcRatePerSecArray((double)fileSize / runsInOneGo / 1024 / 1024,
360                 times);
361         report.addValues("write_throughput", mbps, ResultType.HIGHER_BETTER, ResultUnit.MBPS);
362         report.addValues("write_amount", wrAmount, ResultType.NEUTRAL, ResultUnit.BYTE);
363         Stat.StatResult stat = Stat.getStat(mbps);
364 
365         report.setSummary("write_throughput_average", 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, long fileSize, int bufferSize, int numberRepetition, String reportName, String streamName)379     public static void doSequentialUpdateTest(Context context, String dirName, long fileSize,
380             int bufferSize, int numberRepetition, String reportName, String streamName)
381             throws Exception {
382         File file = FileUtil.createNewFilledFile(context,
383                 dirName, fileSize);
384         final byte[] data = FileUtil.generateRandomData(bufferSize);
385         int numberRepeatInOneRun = (int)(fileSize / bufferSize);
386         double[] mbpsAll = new double[numberRepetition * numberRepeatInOneRun];
387         for (int i = 0; i < numberRepetition; i++) {
388             Log.i(TAG, "starting " + i + " -th round");
389             DeviceReportLog report = new DeviceReportLog(reportName, streamName);
390             report.addValue("round", i,  ResultType.NEUTRAL, ResultUnit.NONE);
391             final RandomAccessFile randomFile = new RandomAccessFile(file, "rwd");  // force O_SYNC
392             randomFile.seek(0L);
393             double[] times = MeasureTime.measure(numberRepeatInOneRun, new MeasureRun() {
394 
395                 @Override
396                 public void run(int i) throws IOException {
397                     randomFile.write(data);
398                 }
399             });
400             randomFile.close();
401             double[] mbps = Stat.calcRatePerSecArray((double)bufferSize / 1024 / 1024,
402                     times);
403             report.addValues("throughput", mbps, ResultType.HIGHER_BETTER, ResultUnit.MBPS);
404             int offset = i * numberRepeatInOneRun;
405             for (int j = 0; j < mbps.length; j++) {
406                 mbpsAll[offset + j] = mbps[j];
407             }
408             report.submit();
409         }
410         Stat.StatResult stat = Stat.getStat(mbpsAll);
411         DeviceReportLog report = new DeviceReportLog(reportName, String.format("%s_average",
412                 streamName));
413         report.addValue("update_throughput", stat.mAverage, ResultType.HIGHER_BETTER,
414                 ResultUnit.MBPS);
415         report.submit();
416     }
417 }
418