1 /* 2 * Copyright (C) 2017 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 package com.google.android.car.kitchensink.storagelifetime; 17 18 import static android.system.OsConstants.O_APPEND; 19 import static android.system.OsConstants.O_RDWR; 20 21 import android.annotation.Nullable; 22 import android.car.Car; 23 import android.car.storagemonitoring.CarStorageMonitoringManager; 24 import android.car.storagemonitoring.CarStorageMonitoringManager.IoStatsListener; 25 import android.car.storagemonitoring.IoStats; 26 import android.car.storagemonitoring.IoStatsEntry; 27 import android.os.Bundle; 28 import android.os.StatFs; 29 import android.system.ErrnoException; 30 import android.system.Os; 31 import android.util.Log; 32 import android.view.LayoutInflater; 33 import android.view.View; 34 import android.view.ViewGroup; 35 import android.widget.ArrayAdapter; 36 import android.widget.ListView; 37 import android.widget.TextView; 38 39 import androidx.fragment.app.Fragment; 40 41 import com.google.android.car.kitchensink.KitchenSinkActivity; 42 import com.google.android.car.kitchensink.R; 43 44 import java.io.File; 45 import java.io.FileDescriptor; 46 import java.io.IOException; 47 import java.nio.ByteBuffer; 48 import java.nio.file.Files; 49 import java.nio.file.Path; 50 import java.nio.file.StandardOpenOption; 51 import java.security.NoSuchAlgorithmException; 52 import java.security.SecureRandom; 53 import java.util.List; 54 55 public class StorageLifetimeFragment extends Fragment { 56 private static final String FILE_NAME = "storage.bin"; 57 private static final String TAG = "CAR.STORAGELIFETIME.KS"; 58 59 private static final int KILOBYTE = 1024; 60 private static final int MEGABYTE = 1024 * 1024; 61 62 private StatFs mStatFs; 63 private KitchenSinkActivity mActivity; 64 private TextView mStorageWearInfo; 65 private ListView mStorageChangesHistory; 66 private TextView mFreeSpaceInfo; 67 private TextView mIoActivity; 68 private CarStorageMonitoringManager mStorageManager; 69 70 private final IoStatsListener mIoListener = new IoStatsListener() { 71 @Override 72 public void onSnapshot(IoStats snapshot) { 73 if (mIoActivity != null) { 74 mIoActivity.setText(""); 75 snapshot.getStats().forEach(uidIoStats -> { 76 final long bytesWrittenToStorage = uidIoStats.foreground.bytesWrittenToStorage + 77 uidIoStats.background.bytesWrittenToStorage; 78 final long fsyncCalls = uidIoStats.foreground.fsyncCalls + 79 uidIoStats.background.fsyncCalls; 80 if (bytesWrittenToStorage > 0 || fsyncCalls > 0) { 81 mIoActivity.append(String.format( 82 "uid = %d, runtime = %d, bytes writen to disk = %d, fsync calls = %d\n", 83 uidIoStats.uid, 84 uidIoStats.runtimeMillis, 85 bytesWrittenToStorage, 86 fsyncCalls)); 87 } 88 }); 89 final List<IoStatsEntry> totals = mStorageManager.getAggregateIoStats(); 90 91 final long totalBytesWrittenToStorage = totals.stream() 92 .mapToLong(stats -> stats.foreground.bytesWrittenToStorage + 93 stats.background.bytesWrittenToStorage) 94 .reduce(0L, (x,y)->x+y); 95 final long totalFsyncCalls = totals.stream() 96 .mapToLong(stats -> stats.foreground.fsyncCalls + 97 stats.background.fsyncCalls) 98 .reduce(0L, (x,y)->x+y); 99 100 mIoActivity.append(String.format( 101 "total bytes written to disk = %d, total fsync calls = %d", 102 totalBytesWrittenToStorage, 103 totalFsyncCalls)); 104 } 105 } 106 }; 107 108 // TODO(egranata): put this somewhere more useful than KitchenSink preEolToString(int preEol)109 private static String preEolToString(int preEol) { 110 switch (preEol) { 111 case 1: return "normal"; 112 case 2: return "warning"; 113 case 3: return "urgent"; 114 default: 115 return "unknown"; 116 } 117 } 118 getFilePath()119 private Path getFilePath() throws IOException { 120 Path filePath = new File(mActivity.getFilesDir(), FILE_NAME).toPath(); 121 if (Files.notExists(filePath)) { 122 Files.createFile(filePath); 123 } 124 return filePath; 125 } 126 writeBytesToFile(int size)127 private void writeBytesToFile(int size) { 128 try { 129 final Path filePath = getFilePath(); 130 byte[] data = new byte[size]; 131 SecureRandom.getInstanceStrong().nextBytes(data); 132 Files.write(filePath, 133 data, 134 StandardOpenOption.APPEND); 135 } catch (NoSuchAlgorithmException | IOException e) { 136 Log.w(TAG, "could not append data", e); 137 } 138 } 139 fsyncFile()140 private void fsyncFile() { 141 try { 142 final Path filePath = getFilePath(); 143 FileDescriptor fd = Os.open(filePath.toString(), O_APPEND | O_RDWR, 0); 144 if (!fd.valid()) { 145 Log.w(TAG, "file descriptor is invalid"); 146 return; 147 } 148 // fill byteBuffer with arbitrary data in order to make an fsync() meaningful 149 ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[] {101, 110, 114, 105, 99, 111}); 150 Os.write(fd, byteBuffer); 151 Os.fsync(fd); 152 Os.close(fd); 153 } catch (ErrnoException | IOException e) { 154 Log.w(TAG, "could not fsync data", e); 155 } 156 } 157 158 @Nullable 159 @Override onCreateView( LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)160 public View onCreateView( 161 LayoutInflater inflater, 162 @Nullable ViewGroup container, 163 @Nullable Bundle savedInstanceState) { 164 View view = inflater.inflate(R.layout.storagewear, container, false); 165 mActivity = (KitchenSinkActivity) getHost(); 166 mStorageWearInfo = view.findViewById(R.id.storage_wear_info); 167 mStorageChangesHistory = view.findViewById(R.id.storage_events_list); 168 mFreeSpaceInfo = view.findViewById(R.id.free_disk_space); 169 mIoActivity = view.findViewById(R.id.last_io_snapshot); 170 171 view.findViewById(R.id.write_one_kilobyte).setOnClickListener( 172 v -> writeBytesToFile(KILOBYTE)); 173 174 view.findViewById(R.id.write_one_megabyte).setOnClickListener( 175 v -> writeBytesToFile(MEGABYTE)); 176 177 view.findViewById(R.id.perform_fsync).setOnClickListener( 178 v -> fsyncFile()); 179 180 return view; 181 } 182 reloadInfo()183 private void reloadInfo() { 184 mStatFs = new StatFs(mActivity.getFilesDir().getAbsolutePath()); 185 186 mStorageManager = 187 (CarStorageMonitoringManager) mActivity.getCar().getCarManager( 188 Car.STORAGE_MONITORING_SERVICE); 189 190 mStorageWearInfo.setText("Wear estimate: " + mStorageManager.getWearEstimate() 191 + "\nPre EOL indicator: " 192 + preEolToString(mStorageManager.getPreEolIndicatorStatus())); 193 194 mStorageChangesHistory.setAdapter(new ArrayAdapter(mActivity, 195 R.layout.wear_estimate_change_textview, 196 mStorageManager.getWearEstimateHistory().toArray())); 197 198 mFreeSpaceInfo.setText("Available blocks: " + mStatFs.getAvailableBlocksLong() 199 + "\nBlock size: " + mStatFs.getBlockSizeLong() + " bytes" 200 + "\nfor a total free space of: " 201 + (mStatFs.getBlockSizeLong() * mStatFs.getAvailableBlocksLong() / MEGABYTE) 202 + "MB"); 203 } 204 registerListener()205 private void registerListener() { 206 mStorageManager.registerListener(mIoListener); 207 } 208 unregisterListener()209 private void unregisterListener() { 210 mStorageManager.unregisterListener(mIoListener); 211 } 212 213 @Override onResume()214 public void onResume() { 215 super.onResume(); 216 if (!mActivity.getCar().isFeatureEnabled(Car.STORAGE_MONITORING_SERVICE)) { 217 Log.w(TAG, "STORAGE_MONITORING_SERVICE not supported"); 218 return; 219 } 220 reloadInfo(); 221 registerListener(); 222 } 223 224 @Override onPause()225 public void onPause() { 226 if (mActivity.getCar().isFeatureEnabled(Car.STORAGE_MONITORING_SERVICE)) { 227 unregisterListener(); 228 } 229 super.onPause(); 230 } 231 } 232