Browse Source

Merge pull request #1399 from cryptomator/feature/io-stats

closes #1070
Armin Schrenk 4 years ago
parent
commit
ad44af338d

+ 13 - 0
main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java

@@ -32,7 +32,9 @@ import javafx.beans.Observable;
 import javafx.beans.binding.Bindings;
 import javafx.beans.binding.BooleanBinding;
 import javafx.beans.binding.StringBinding;
+import javafx.beans.property.BooleanProperty;
 import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
 import java.io.IOException;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
@@ -68,6 +70,7 @@ public class Vault {
 	private final BooleanBinding unknownError;
 	private final StringBinding accessPoint;
 	private final BooleanBinding accessPointPresent;
+	private final BooleanProperty showingStats;
 
 	private volatile Volume volume;
 
@@ -90,6 +93,7 @@ public class Vault {
 		this.unknownError = Bindings.createBooleanBinding(this::isUnknownError, state);
 		this.accessPoint = Bindings.createStringBinding(this::getAccessPoint, state);
 		this.accessPointPresent = this.accessPoint.isNotEmpty();
+		this.showingStats = new SimpleBooleanProperty(false);
 	}
 
 	// ******************************************************************************
@@ -268,6 +272,15 @@ public class Vault {
 		}
 	}
 
+	public BooleanProperty showingStatsProperty() {
+		return showingStats;
+	}
+
+	public boolean isShowingStats() {
+		return accessPointPresent.get();
+	}
+
+
 	// ******************************************************************************
 	// Getter/Setter
 	// *******************************************************************************/

+ 79 - 2
main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java

@@ -8,8 +8,10 @@ import org.slf4j.LoggerFactory;
 import javax.inject.Inject;
 import javafx.application.Platform;
 import javafx.beans.Observable;
+import javafx.beans.property.DoubleProperty;
 import javafx.beans.property.LongProperty;
 import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleDoubleProperty;
 import javafx.beans.property.SimpleLongProperty;
 import javafx.concurrent.ScheduledService;
 import javafx.concurrent.Task;
@@ -28,6 +30,15 @@ public class VaultStats {
 	private final ScheduledService<Optional<CryptoFileSystemStats>> updateService;
 	private final LongProperty bytesPerSecondRead = new SimpleLongProperty();
 	private final LongProperty bytesPerSecondWritten = new SimpleLongProperty();
+	private final LongProperty bytesPerSecondEncrypted = new SimpleLongProperty();
+	private final LongProperty bytesPerSecondDecrypted = new SimpleLongProperty();
+	private final DoubleProperty cacheHitRate = new SimpleDoubleProperty();
+	private final LongProperty toalBytesRead = new SimpleLongProperty();
+	private final LongProperty toalBytesWritten = new SimpleLongProperty();
+	private final LongProperty totalBytesEncrypted = new SimpleLongProperty();
+	private final LongProperty totalBytesDecrypted = new SimpleLongProperty();
+	private final LongProperty filesRead = new SimpleLongProperty();
+	private final LongProperty filesWritten = new SimpleLongProperty();
 
 	@Inject
 	VaultStats(AtomicReference<CryptoFileSystem> fs, ObjectProperty<VaultState> state, ExecutorService executor) {
@@ -53,8 +64,28 @@ public class VaultStats {
 
 	private void updateStats(Optional<CryptoFileSystemStats> stats) {
 		assert Platform.isFxApplicationThread();
-		bytesPerSecondRead.set(stats.map(CryptoFileSystemStats::pollBytesRead).orElse(0l));
-		bytesPerSecondWritten.set(stats.map(CryptoFileSystemStats::pollBytesWritten).orElse(0l));
+		bytesPerSecondRead.set(stats.map(CryptoFileSystemStats::pollBytesRead).orElse(0L));
+		bytesPerSecondWritten.set(stats.map(CryptoFileSystemStats::pollBytesWritten).orElse(0L));
+		cacheHitRate.set(stats.map(this::getCacheHitRate).orElse(0.0));
+		bytesPerSecondDecrypted.set(stats.map(CryptoFileSystemStats::pollBytesDecrypted).orElse(0L));
+		bytesPerSecondEncrypted.set(stats.map(CryptoFileSystemStats::pollBytesEncrypted).orElse(0L));
+		toalBytesRead.set(stats.map(CryptoFileSystemStats::pollTotalBytesRead).orElse(0L));
+		toalBytesWritten.set(stats.map(CryptoFileSystemStats::pollTotalBytesWritten).orElse(0L));
+		totalBytesEncrypted.set(stats.map(CryptoFileSystemStats::pollTotalBytesEncrypted).orElse(0L));
+		totalBytesDecrypted.set(stats.map(CryptoFileSystemStats::pollTotalBytesDecrypted).orElse(0L));
+		filesRead.set(stats.map(CryptoFileSystemStats::pollAmountOfAccessesRead).orElse(0L));
+		filesWritten.set(stats.map(CryptoFileSystemStats::pollAmountOfAccessesWritten).orElse(0L));
+
+	}
+
+	private double getCacheHitRate(CryptoFileSystemStats stats) {
+		long accesses = stats.pollChunkCacheAccesses();
+		long hits = stats.pollChunkCacheHits();
+		if (accesses == 0) {
+			return 0.0;
+		} else {
+			return hits / (double) accesses;
+		}
 	}
 
 	private class UpdateStatsService extends ScheduledService<Optional<CryptoFileSystemStats>> {
@@ -98,4 +129,50 @@ public class VaultStats {
 	public long getBytesPerSecondWritten() {
 		return bytesPerSecondWritten.get();
 	}
+
+	public LongProperty bytesPerSecondEncryptedProperty() {
+		return bytesPerSecondEncrypted;
+	}
+
+	public long getBytesPerSecondEnrypted() {
+		return bytesPerSecondEncrypted.get();
+	}
+
+	public LongProperty bytesPerSecondDecryptedProperty() {
+		return bytesPerSecondDecrypted;
+	}
+
+	public long getBytesPerSecondDecrypted() {
+		return bytesPerSecondDecrypted.get();
+	}
+
+	public DoubleProperty cacheHitRateProperty() { return cacheHitRate; }
+
+	public double getCacheHitRate() {
+		return cacheHitRate.get();
+	}
+
+	public LongProperty toalBytesReadProperty() {return toalBytesRead;}
+
+	public long getTotalBytesRead() { return toalBytesRead.get();}
+
+	public LongProperty toalBytesWrittenProperty() {return toalBytesWritten;}
+
+	public long getTotalBytesWritten() { return toalBytesWritten.get();}
+
+	public LongProperty totalBytesEncryptedProperty() {return totalBytesEncrypted;}
+
+	public long getTotalBytesEncrypted() { return totalBytesEncrypted.get();}
+
+	public LongProperty totalBytesDecryptedProperty() {return totalBytesDecrypted;}
+
+	public long getTotalBytesDecrypted() { return totalBytesDecrypted.get();}
+
+	public LongProperty filesRead() { return filesRead;}
+
+	public long getFilesRead() { return filesRead.get();}
+
+	public LongProperty filesWritten() {return filesWritten;}
+
+	public long getFilesWritten() {return filesWritten.get();}
 }

+ 1 - 0
main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java

@@ -28,6 +28,7 @@ public enum FxmlFile {
 	UNLOCK_INVALID_MOUNT_POINT("/fxml/unlock_invalid_mount_point.fxml"), //
 	UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), //
 	VAULT_OPTIONS("/fxml/vault_options.fxml"), //
+	VAULT_STATISTICS("/fxml/stats.fxml"), //
 	WRONGFILEALERT("/fxml/wrongfilealert.fxml");
 
 	private final String ressourcePathString;

+ 61 - 0
main/ui/src/main/java/org/cryptomator/ui/common/WeakBindings.java

@@ -1,7 +1,11 @@
 package org.cryptomator.ui.common;
 
+import javafx.beans.binding.DoubleBinding;
+import javafx.beans.binding.IntegerBinding;
+import javafx.beans.binding.LongBinding;
 import javafx.beans.binding.StringBinding;
 import javafx.beans.value.ObservableObjectValue;
+import javafx.beans.value.ObservableValue;
 
 
 /**
@@ -29,4 +33,61 @@ public final class WeakBindings {
 		};
 	}
 
+	/**
+	 * Create a new LongBinding that listens to changes from the given observable without being strongly referenced by it.
+	 *
+	 * @param observable The observable
+	 * @return a LongBinding weakly referenced from the given observable
+	 */
+	public static LongBinding bindLong(ObservableValue<Number> observable) {
+		return new LongBinding() {
+			{
+				bind(observable);
+			}
+
+			@Override
+			protected long computeValue() {
+				return observable.getValue().longValue();
+			}
+		};
+	}
+
+	/**
+	 * Create a new DoubleBinding that listens to changes from the given observable without being strongly referenced by it.
+	 *
+	 * @param observable The observable
+	 * @return a DoubleBinding weakly referenced from the given observable
+	 */
+	public static DoubleBinding bindDouble(ObservableValue<Number> observable) {
+		return new DoubleBinding() {
+			{
+				bind(observable);
+			}
+
+			@Override
+			protected double computeValue() {
+				return observable.getValue().doubleValue();
+			}
+		};
+	}
+
+	/**
+	 * Create a new IntegerBinding that listens to changes from the given observable without being strongly referenced by it.
+	 *
+	 * @param observable The observable
+	 * @return a IntegerBinding weakly referenced from the given observable
+	 */
+	public static IntegerBinding bindInterger(ObservableValue<Number> observable) {
+		return new IntegerBinding() {
+			{
+				bind(observable);
+			}
+
+			@Override
+			protected int computeValue() {
+				return observable.getValue().intValue();
+			}
+		};
+	}
+
 }

+ 88 - 0
main/ui/src/main/java/org/cryptomator/ui/controls/DataLabel.java

@@ -0,0 +1,88 @@
+package org.cryptomator.ui.controls;
+
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.StringBinding;
+import javafx.beans.property.LongProperty;
+import javafx.beans.property.SimpleLongProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.scene.control.Label;
+
+public class DataLabel extends Label {
+
+	private static final long KIB_THRESHOLD = 1l << 7; // 0.128 kiB
+	private static final long MIB_THRESHOLD = 1l << 19; // 0.512 MiB
+	private static final long GIB_THRESHOLD = 1l << 29; // 0.512 GiB
+
+	private final StringProperty byteFormat = new SimpleStringProperty("-");
+	private final StringProperty kibFormat = new SimpleStringProperty("%.3f");
+	private final StringProperty mibFormat = new SimpleStringProperty("%.3f");
+	private final StringProperty gibFormat = new SimpleStringProperty("%.3f");
+	private final LongProperty dataInBytes = new SimpleLongProperty();
+
+	public DataLabel() {
+		textProperty().bind(createStringBinding());
+	}
+
+	protected StringBinding createStringBinding() {
+		return Bindings.createStringBinding(this::updateText, kibFormat, mibFormat, gibFormat, dataInBytes);
+	}
+
+	private String updateText() {
+		long data = dataInBytes.get();
+		if (data > GIB_THRESHOLD) {
+			double giB = ((double) data) / 1024.0 / 1024.0 / 1024.0;
+			return String.format(gibFormat.get(), giB);
+		} else if (data > MIB_THRESHOLD) {
+			double miB = ((double) data) / 1024.0 / 1024.0;
+			return String.format(mibFormat.get(), miB);
+		} else if (data > KIB_THRESHOLD) {
+			double kiB = ((double) data) / 1024.0;
+			return String.format(kibFormat.get(), kiB);
+		} else {
+			return String.format(byteFormat.get(), data);
+		}
+	}
+
+	public StringProperty byteFormatProperty() { return byteFormat; }
+
+	public String getByteFormat() { return byteFormat.get(); }
+
+	public void setByteFormat(String byteFormat) {
+		this.byteFormat.set(byteFormat);
+	}
+
+	public StringProperty kibFormatProperty() { return kibFormat; }
+
+	public String getKibFormat() { return kibFormat.get(); }
+
+	public void setKibFormat(String kibFormat) {
+		this.kibFormat.set(kibFormat);
+	}
+
+	public StringProperty mibFormatProperty() { return mibFormat; }
+
+	public String getMibFormat() { return mibFormat.get(); }
+
+	public void setMibFormat(String mibFormat) {
+		this.mibFormat.set(mibFormat);
+	}
+
+	public StringProperty gibFormatProperty() { return gibFormat; }
+
+	public String getGibFormat() { return gibFormat.get(); }
+
+	public void setGibFormat(String gibFormat) {
+		this.gibFormat.set(gibFormat);
+	}
+
+	public LongProperty dataInBytesProperty() { return dataInBytes; }
+
+	public long getDataInBytes() {
+		return dataInBytes.get();
+	}
+
+	public void setDataInBytes(long dataInBytes) { this.dataInBytes.set(dataInBytes); }
+
+
+}

+ 6 - 7
main/ui/src/main/java/org/cryptomator/ui/controls/ThrougputLabel.java

@@ -10,8 +10,8 @@ import javafx.scene.control.Label;
 
 public class ThrougputLabel extends Label {
 
-	private static final long kibsThreshold = 1l << 7; // 0.128 kiB/s
-	private static final long mibsThreshold = 1l << 19; // 0.512 MiB/s
+	private static final long KIBS_THRESHOLD = 1l << 7; // 0.128 kiB/s
+	private static final long MIBS_THRESHOLD = 1l << 19; // 0.512 MiB/s
 
 	private final StringProperty idleFormat = new SimpleStringProperty("-");
 	private final StringProperty kibsFormat = new SimpleStringProperty("%.3f");
@@ -22,18 +22,17 @@ public class ThrougputLabel extends Label {
 		textProperty().bind(createStringBinding());
 	}
 
-
 	protected StringBinding createStringBinding() {
 		return Bindings.createStringBinding(this::updateText, kibsFormat, mibsFormat, bytesPerSecond);
 	}
 
 	private String updateText() {
 		long bps = bytesPerSecond.get();
-		if (bps > mibsThreshold) {
-			double mibs = ((double) bytesPerSecond.get()) / 1024.0 / 1024.0;
+		if (bps > MIBS_THRESHOLD) {
+			double mibs = ((double) bps) / 1024.0 / 1024.0;
 			return String.format(mibsFormat.get(), mibs);
-		} else if (bps > kibsThreshold) {
-			double kibs = ((double) bytesPerSecond.get()) / 1024.0;
+		} else if (bps > KIBS_THRESHOLD) {
+			double kibs = ((double) bps) / 1024.0;
 			return String.format(kibsFormat.get(), kibs);
 		} else {
 			return String.format(idleFormat.get(), bps);

+ 2 - 1
main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java

@@ -15,6 +15,7 @@ import org.cryptomator.ui.common.StageFactory;
 import org.cryptomator.ui.migration.MigrationComponent;
 import org.cryptomator.ui.removevault.RemoveVaultComponent;
 import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
+import org.cryptomator.ui.stats.VaultStatisticsComponent;
 import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
 
 import javax.inject.Provider;
@@ -26,7 +27,7 @@ import javafx.stage.StageStyle;
 import java.util.Map;
 import java.util.ResourceBundle;
 
-@Module(subcomponents = {AddVaultWizardComponent.class, MigrationComponent.class, RemoveVaultComponent.class, VaultOptionsComponent.class, WrongFileAlertComponent.class})
+@Module(subcomponents = {AddVaultWizardComponent.class, MigrationComponent.class, RemoveVaultComponent.class, VaultOptionsComponent.class, VaultStatisticsComponent.class, WrongFileAlertComponent.class})
 abstract class MainWindowModule {
 
 	@Provides

+ 18 - 1
main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java

@@ -1,8 +1,12 @@
 package org.cryptomator.ui.mainwindow;
 
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.common.VaultService;
+import org.cryptomator.ui.stats.VaultStatisticsComponent;
 
 import javax.inject.Inject;
 import javafx.beans.property.ObjectProperty;
@@ -14,11 +18,19 @@ public class VaultDetailUnlockedController implements FxController {
 
 	private final ReadOnlyObjectProperty<Vault> vault;
 	private final VaultService vaultService;
+	private final LoadingCache<Vault, VaultStatisticsComponent> vaultStats;
+	private final VaultStatisticsComponent.Builder vaultStatsBuilder;
 
 	@Inject
-	public VaultDetailUnlockedController(ObjectProperty<Vault> vault, VaultService vaultService) {
+	public VaultDetailUnlockedController(ObjectProperty<Vault> vault, VaultService vaultService, VaultStatisticsComponent.Builder vaultStatsBuilder) {
 		this.vault = vault;
 		this.vaultService = vaultService;
+		this.vaultStats = CacheBuilder.newBuilder().weakValues().build(CacheLoader.from(this::buildVaultStats));
+		this.vaultStatsBuilder = vaultStatsBuilder;
+	}
+
+	private VaultStatisticsComponent buildVaultStats(Vault vault) {
+		return vaultStatsBuilder.vault(vault).build();
 	}
 
 	@FXML
@@ -32,6 +44,11 @@ public class VaultDetailUnlockedController implements FxController {
 		// TODO count lock attempts, and allow forced lock
 	}
 
+	@FXML
+	public void showVaultStatistics() {
+		vaultStats.getUnchecked(vault.get()).showVaultStatisticsWindow();
+	}
+
 	/* Getter/Setter */
 
 	public ReadOnlyObjectProperty<Vault> vaultProperty() {

+ 49 - 0
main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsComponent.java

@@ -0,0 +1,49 @@
+package org.cryptomator.ui.stats;
+
+import dagger.BindsInstance;
+import dagger.Lazy;
+import dagger.Subcomponent;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.ui.common.FxmlFile;
+import org.cryptomator.ui.common.FxmlScene;
+
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+
+/**
+ * For each vault there can be up to one statistics component.
+ * <p>
+ * <b>Important:</b> Outside of {@link org.cryptomator.ui.stats}, this component should be weakly referenced,
+ * as it include memory-intensive UI nodes.
+ * <p>
+ * While the stats window is visible, this component is strongly referenced by the window's main controller.
+ * As soon as the window is closed, the full objectgraph becomes eligible for GC.
+ */
+@VaultStatisticsScoped
+@Subcomponent(modules = {VaultStatisticsModule.class})
+public interface VaultStatisticsComponent {
+
+	@VaultStatisticsWindow
+	Stage window();
+
+	@FxmlScene(FxmlFile.VAULT_STATISTICS)
+	Lazy<Scene> scene();
+
+	default void showVaultStatisticsWindow() {
+		Stage stage = window();
+		stage.setScene(scene().get());
+		stage.sizeToScene();
+		stage.show();
+		stage.requestFocus();
+	}
+
+	@Subcomponent.Builder
+	interface Builder {
+
+		@BindsInstance
+		Builder vault(@VaultStatisticsWindow Vault vault);
+
+		VaultStatisticsComponent build();
+	}
+
+}

+ 210 - 0
main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsController.java

@@ -0,0 +1,210 @@
+package org.cryptomator.ui.stats;
+
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.common.vaults.VaultStats;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.common.WeakBindings;
+
+import javax.inject.Inject;
+import javafx.animation.Animation;
+import javafx.animation.KeyFrame;
+import javafx.animation.Timeline;
+import javafx.beans.binding.DoubleBinding;
+import javafx.beans.binding.LongBinding;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.fxml.FXML;
+import javafx.scene.chart.AreaChart;
+import javafx.scene.chart.NumberAxis;
+import javafx.scene.chart.XYChart.Data;
+import javafx.scene.chart.XYChart.Series;
+import javafx.stage.Stage;
+import javafx.util.Duration;
+import java.util.Arrays;
+
+@VaultStatisticsScoped
+public class VaultStatisticsController implements FxController {
+
+	private static final int IO_SAMPLING_STEPS = 30;
+	private static final double IO_SAMPLING_INTERVAL = 1;
+
+	private final VaultStatisticsComponent component; // keep a strong reference to the component (see component's javadoc)
+	private final VaultStats stats;
+	private final Series<Number, Number> readData;
+	private final Series<Number, Number> writeData;
+	private final Timeline ioAnimation;
+	private final LongBinding bpsRead;
+	private final LongBinding bpsWritten;
+	private final DoubleBinding cacheHitRate;
+	private final DoubleBinding cacheHitDegrees;
+	private final DoubleBinding cacheHitPercentage;
+	private final LongBinding totalBytesRead;
+	private final LongBinding totalBytesWritten;
+	private final LongBinding totalBytesEncrypted;
+	private final LongBinding totalBytesDecrypted;
+	private final LongBinding filesRead;
+	private final LongBinding filesWritten;
+	private final LongBinding bpsEncrypted;
+	private final LongBinding bpsDecrypted;
+
+	public AreaChart<Number, Number> readChart;
+	public AreaChart<Number, Number> writeChart;
+	public NumberAxis readChartXAxis;
+	public NumberAxis readChartYAxis;
+	public NumberAxis writeChartXAxis;
+	public NumberAxis writeChartYAxis;
+
+	@Inject
+	public VaultStatisticsController(VaultStatisticsComponent component, @VaultStatisticsWindow Stage window, @VaultStatisticsWindow Vault vault) {
+		this.component = component;
+		this.stats = vault.getStats();
+		this.readData = new Series<>();
+		this.writeData = new Series<>();
+		this.bpsRead = WeakBindings.bindLong(stats.bytesPerSecondReadProperty());
+		this.bpsWritten = WeakBindings.bindLong(stats.bytesPerSecondWrittenProperty());
+		this.cacheHitRate = WeakBindings.bindDouble(stats.cacheHitRateProperty());
+		this.cacheHitDegrees = cacheHitRate.multiply(-270);
+		this.cacheHitPercentage = cacheHitRate.multiply(100);
+		this.totalBytesRead = WeakBindings.bindLong(stats.toalBytesReadProperty());
+		this.totalBytesWritten = WeakBindings.bindLong(stats.toalBytesWrittenProperty());
+		this.totalBytesDecrypted = WeakBindings.bindLong(stats.totalBytesDecryptedProperty());
+		this.totalBytesEncrypted = WeakBindings.bindLong(stats.totalBytesEncryptedProperty());
+		this.filesRead = WeakBindings.bindLong(stats.filesRead());
+		this.filesWritten = WeakBindings.bindLong(stats.filesWritten());
+		this.bpsEncrypted = WeakBindings.bindLong(stats.bytesPerSecondEncryptedProperty());
+		this.bpsDecrypted = WeakBindings.bindLong(stats.bytesPerSecondDecryptedProperty());
+
+		this.ioAnimation = new Timeline(); //TODO Research better timer
+		ioAnimation.getKeyFrames().add(new KeyFrame(Duration.seconds(IO_SAMPLING_INTERVAL), new IoSamplingAnimationHandler(readData, writeData)));
+		ioAnimation.setCycleCount(Animation.INDEFINITE);
+		ioAnimation.play();
+
+		// make sure to stop animating while window is closed
+		// otherwise a global timer (GC root) will keep a strong reference to animation
+		window.setOnHiding(evt -> ioAnimation.stop());
+		window.setOnShowing(evt -> ioAnimation.play());
+	}
+
+	@FXML
+	public void initialize() {
+		readChart.getData().addAll(readData);
+		writeChart.getData().addAll(writeData);
+	}
+
+	private class IoSamplingAnimationHandler implements EventHandler<ActionEvent> {
+
+		private long step = IO_SAMPLING_STEPS;
+		private final Series<Number, Number> decryptedBytesRead;
+		private final Series<Number, Number> encryptedBytesWrite;
+		private final long[] maxBuf = new long[IO_SAMPLING_STEPS];
+
+		public IoSamplingAnimationHandler(Series<Number, Number> readData, Series<Number, Number> writeData) {
+			this.decryptedBytesRead = readData;
+			this.encryptedBytesWrite = writeData;
+
+			// initialize data once and change value of datapoints later:
+			for (int i = 0; i < IO_SAMPLING_STEPS; i++) {
+				decryptedBytesRead.getData().add(new Data<>(i, 0));
+				encryptedBytesWrite.getData().add(new Data<>(i, 0));
+			}
+		}
+
+		@Override
+		public void handle(ActionEvent event) {
+			final long currentStep = step++;
+			final long decBytes = stats.bytesPerSecondReadProperty().get();
+			final long encBytes = stats.bytesPerSecondWrittenProperty().get();
+
+			maxBuf[(int) currentStep % IO_SAMPLING_STEPS] = Math.max(decBytes, encBytes);
+			long allTimeMax = Arrays.stream(maxBuf).max().orElse(0l);
+
+			// remove oldest value:
+			decryptedBytesRead.getData().remove(0);
+			encryptedBytesWrite.getData().remove(0);
+
+			// add latest value:
+			decryptedBytesRead.getData().add(new Data<>(currentStep, decBytes));
+			encryptedBytesWrite.getData().add(new Data<>(currentStep, encBytes));
+
+			// adjust ranges:
+			readChartXAxis.setLowerBound(currentStep - IO_SAMPLING_STEPS);
+			readChartXAxis.setUpperBound(currentStep);
+			readChartYAxis.setUpperBound(allTimeMax);
+			writeChartXAxis.setLowerBound(currentStep - IO_SAMPLING_STEPS);
+			writeChartXAxis.setUpperBound(currentStep);
+			writeChartYAxis.setUpperBound(allTimeMax);
+		}
+	}
+
+	/* Getter/Setter */
+
+	public LongBinding bpsReadProperty() {
+		return bpsRead;
+	}
+
+	public long getBpsRead() {
+		return bpsRead.get();
+	}
+
+	public LongBinding bpsWrittenProperty() {
+		return bpsWritten;
+	}
+
+	public long getBpsWritten() {
+		return bpsWritten.get();
+	}
+
+	public DoubleBinding cacheHitPercentageProperty() {
+		return cacheHitPercentage;
+	}
+
+	public double getCacheHitPercentage() { return cacheHitPercentage.get(); }
+
+	public DoubleBinding cacheHitDegreesProperty() {
+		return cacheHitDegrees;
+	}
+
+	public double getCacheHitDegrees() {
+		return cacheHitDegrees.get();
+	}
+
+	public LongBinding totalBytesReadProperty() { return totalBytesRead;}
+
+	public long getTotalBytesRead() { return totalBytesRead.get();}
+
+	public LongBinding totalBytesWrittenProperty() { return totalBytesWritten;}
+
+	public long getTotalBytesWritten() { return totalBytesWritten.get();}
+
+	public LongBinding totalBytesEncryptedProperty() {return totalBytesEncrypted;}
+
+	public long getTotalBytesEncrypted() { return totalBytesEncrypted.get();}
+
+	public LongBinding totalBytesDecryptedProperty() {return totalBytesDecrypted;}
+
+	public long getTotalBytesDecrypted() { return totalBytesDecrypted.get();}
+
+	public LongBinding bpsEncryptedProperty() {
+		return bpsEncrypted;
+	}
+
+	public long getBpsEncrypted() {
+		return bpsEncrypted.get();
+	}
+
+	public LongBinding bpsDecryptedProperty() {
+		return bpsDecrypted;
+	}
+
+	public long getBpsDecrypted() {
+		return bpsDecrypted.get();
+	}
+
+	public LongBinding filesReadProperty() { return filesRead;}
+
+	public long getFilesRead() { return filesRead.get();}
+
+	public LongBinding filesWrittenProperty() {return filesWritten;}
+
+	public long getFilesWritten() {return filesWritten.get();}
+}

+ 73 - 0
main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsModule.java

@@ -0,0 +1,73 @@
+package org.cryptomator.ui.stats;
+
+import dagger.Binds;
+import dagger.Module;
+import dagger.Provides;
+import dagger.multibindings.IntoMap;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.common.vaults.VaultState;
+import org.cryptomator.ui.common.DefaultSceneFactory;
+import org.cryptomator.ui.common.FXMLLoaderFactory;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.common.FxControllerKey;
+import org.cryptomator.ui.common.FxmlFile;
+import org.cryptomator.ui.common.FxmlScene;
+import org.cryptomator.ui.common.StageFactory;
+
+import javax.inject.Provider;
+import java.lang.ref.WeakReference;
+import java.util.Map;
+import java.util.ResourceBundle;
+
+@Module
+abstract class VaultStatisticsModule {
+
+	@Provides
+	@VaultStatisticsWindow
+	@VaultStatisticsScoped
+	static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
+		return new FXMLLoaderFactory(factories, sceneFactory, resourceBundle);
+	}
+
+	@Provides
+	@VaultStatisticsWindow
+	@VaultStatisticsScoped
+	static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle, @VaultStatisticsWindow Vault vault) {
+		Stage stage = factory.create();
+		stage.setTitle(String.format(resourceBundle.getString("stats.title"), vault.getDisplayName()));
+		stage.setResizable(false);
+		var weakStage = new WeakReference<>(stage);
+		vault.stateProperty().addListener(new ChangeListener<>() {
+			@Override
+			public void changed(ObservableValue<? extends VaultState> observable, VaultState oldValue, VaultState newValue) {
+				if (newValue != VaultState.UNLOCKED) {
+					Stage stage = weakStage.get();
+					if (stage != null) {
+						stage.hide();
+					}
+					observable.removeListener(this);
+				}
+			}
+		});
+		stage.setOnCloseRequest(windowEvent -> vault.showingStatsProperty().setValue(false));
+		return stage;
+	}
+
+	@Provides
+	@FxmlScene(FxmlFile.VAULT_STATISTICS)
+	@VaultStatisticsScoped
+	static Scene provideVaultStatisticsScene(@VaultStatisticsWindow FXMLLoaderFactory fxmlLoaders) {
+		return fxmlLoaders.createScene("/fxml/stats.fxml");
+	}
+
+	// ------------------
+
+	@Binds
+	@IntoMap
+	@FxControllerKey(VaultStatisticsController.class)
+	abstract FxController bindVaultStatisticsController(VaultStatisticsController controller);
+}

+ 13 - 0
main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsScoped.java

@@ -0,0 +1,13 @@
+package org.cryptomator.ui.stats;
+
+import javax.inject.Scope;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Scope
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+public @interface VaultStatisticsScoped {
+
+}

+ 14 - 0
main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsWindow.java

@@ -0,0 +1,14 @@
+package org.cryptomator.ui.stats;
+
+import javax.inject.Qualifier;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+@Qualifier
+@Documented
+@Retention(RUNTIME)
+@interface VaultStatisticsWindow {
+
+}

+ 54 - 0
main/ui/src/main/resources/css/dark_theme.css

@@ -884,3 +884,57 @@
 	-fx-background-color: PROGRESS_BAR_BG;
 	-fx-background-radius: 4px;
 }
+/*******************************************************************************
+ *                                                                             *
+ * I/O Statistics                                                              *
+ *                                                                             *
+ ******************************************************************************/
+
+.cache-arc-background {
+	-fx-fill: transparent;
+	-fx-stroke: MUTED_BG;
+	-fx-stroke-type: centered;
+	-fx-stroke-width: 12;
+	-fx-stroke-line-cap: butt;
+}
+
+.cache-arc-foreground {
+	-fx-fill: transparent;
+	-fx-stroke: PRIMARY;
+	-fx-stroke-type: centered;
+	-fx-stroke-width: 12;
+	-fx-stroke-line-cap: butt;
+}
+
+.chart.io-stats {
+	-fx-padding: 10px;
+	-fx-horizontal-grid-lines-visible: false;
+	-fx-horizontal-zero-line-visible: false;
+	-fx-vertical-grid-lines-visible: false;
+	-fx-vertical-zero-line-visible: false;
+}
+
+.axis.io-stats {
+	-fx-tick-mark-visible: false;
+	-fx-minor-tick-visible: false;
+	-fx-tick-labels-visible: false;
+}
+
+.chart-plot-background {
+	-fx-background-color: transparent;
+}
+
+.chart-vertical-zero-line,
+.chart-horizontal-zero-line,
+.chart-alternative-row-fill {
+	-fx-stroke: transparent;
+	-fx-stroke-width: 0;
+}
+
+.default-color0.chart-series-area-line {
+	-fx-stroke: PRIMARY;
+}
+.default-color0.chart-series-area-fill {
+	-fx-fill: linear-gradient(to bottom, PRIMARY, transparent);
+	-fx-stroke: transparent;
+}

+ 65 - 0
main/ui/src/main/resources/css/light_theme.css

@@ -882,3 +882,68 @@
 	-fx-background-color: PROGRESS_BAR_BG;
 	-fx-background-radius: 4px;
 }
+/*******************************************************************************
+ *                                                                             *
+ * I/O Statistics                                                              *
+ *                                                                             *
+ ******************************************************************************/
+.chart {
+	-fx-padding: 10px;
+}
+
+.chart-plot-background {
+	-fx-background-color: MAIN_BG;
+	-fx-padding: 20px;
+}
+
+/* content */
+
+
+.cache-arc-background {
+	-fx-fill: transparent;
+	-fx-stroke: MUTED_BG;
+	-fx-stroke-type: centered;
+	-fx-stroke-width: 12;
+	-fx-stroke-line-cap: butt;
+}
+
+.cache-arc-foreground {
+	-fx-fill: transparent;
+	-fx-stroke: PRIMARY;
+	-fx-stroke-type: centered;
+	-fx-stroke-width: 12;
+	-fx-stroke-line-cap: butt;
+}
+
+.chart.io-stats {
+	-fx-padding: 10px;
+	-fx-horizontal-grid-lines-visible: false;
+	-fx-horizontal-zero-line-visible: false;
+	-fx-vertical-grid-lines-visible: false;
+	-fx-vertical-zero-line-visible: false;
+}
+
+.axis.io-stats {
+	-fx-tick-mark-visible: false;
+	-fx-minor-tick-visible: false;
+	-fx-tick-labels-visible: false;
+}
+
+.chart-plot-background {
+	-fx-background-color: transparent;
+}
+
+.chart-vertical-zero-line,
+.chart-horizontal-zero-line,
+.chart-alternative-row-fill {
+	-fx-stroke: transparent;
+	-fx-stroke-width: 0;
+}
+
+.default-color0.chart-series-area-line {
+	-fx-stroke: PRIMARY;
+}
+.default-color0.chart-series-area-fill {
+	-fx-fill: linear-gradient(to bottom, PRIMARY, transparent);
+	-fx-stroke: transparent;
+}

+ 107 - 0
main/ui/src/main/resources/fxml/stats.fxml

@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.chart.AreaChart?>
+<?import javafx.scene.chart.NumberAxis?>
+<?import javafx.scene.control.Label?>
+<?import javafx.scene.Cursor?>
+<?import javafx.scene.layout.HBox?>
+<?import javafx.scene.layout.StackPane?>
+<?import javafx.scene.layout.VBox?>
+<?import javafx.scene.shape.Arc?>
+<?import org.cryptomator.ui.controls.FormattedLabel?>
+<?import org.cryptomator.ui.controls.ThrougputLabel?>
+<?import javafx.scene.Group?>
+<?import org.cryptomator.ui.controls.DataLabel?>
+<HBox xmlns="http://javafx.com/javafx"
+	  xmlns:fx="http://javafx.com/fxml"
+	  fx:controller="org.cryptomator.ui.stats.VaultStatisticsController"
+	  prefWidth="800.0" spacing="12">
+	<padding>
+		<Insets topRightBottomLeft="12"/>
+	</padding>
+
+	<!-- Caching -->
+	<VBox prefWidth="200" prefHeight="200">
+		<StackPane>
+			<Group>
+				<Arc styleClass="cache-arc-background" centerX="100" centerY="100" radiusX="100" radiusY="100" startAngle="225" length="-270"/>
+				<Arc styleClass="cache-arc-foreground" centerX="100" centerY="100" radiusX="100" radiusY="100" startAngle="225" length="${controller.cacheHitDegrees}"/>
+			</Group>
+			<VBox StackPane.alignment="CENTER" alignment="CENTER">
+				<FormattedLabel styleClass="label-large" format="\%1.0f %%" arg1="${controller.cacheHitPercentage}"/>
+				<Label text="%stats.cacheHitRate"/>
+			</VBox>
+		</StackPane>
+	</VBox>
+
+	<!-- Read -->
+	<VBox prefWidth="300" prefHeight="300">
+		<HBox spacing="12" alignment="CENTER">
+			<Label styleClass="label-large" text="%stats.readDataLabel"/>
+			<ThrougputLabel styleClass="label-large" alignment="CENTER_RIGHT" minWidth="60" idleFormat="%main.vaultDetail.throughput.idle" kibsFormat="%main.vaultDetail.throughput.kbps"
+							mibsFormat="%main.vaultDetail.throughput.mbps" bytesPerSecond="${controller.bpsRead}"/>
+		</HBox>
+		<AreaChart fx:id="readChart" styleClass="io-stats" createSymbols="false" animated="false">
+			<xAxis>
+				<NumberAxis fx:id="readChartXAxis" styleClass="io-stats" autoRanging="false" forceZeroInRange="false" side="BOTTOM"/>
+			</xAxis>
+			<yAxis>
+				<NumberAxis fx:id="readChartYAxis" styleClass="io-stats" autoRanging="false" forceZeroInRange="true" side="LEFT" tickUnit="Infinity"/>
+			</yAxis>
+			<cursor>
+				<Cursor fx:constant="DEFAULT"/>
+			</cursor>
+		</AreaChart>
+		<VBox spacing="12" alignment="CENTER">
+			<HBox alignment="CENTER">
+				<Label styleClass="label-large" text="%stats.totalMiBRead"/>
+				<DataLabel styleClass="label-large" alignment="CENTER_RIGHT" minWidth="60" byteFormat="%stats.data.bytes" kibFormat="%stats.data.kiB" mibFormat="%stats.data.miB" gibFormat="%stats.data.giB"
+						   dataInBytes="${controller.totalBytesRead}"/>
+			</HBox>
+			<HBox alignment="CENTER">
+				<Label styleClass="label-large" text="%stats.totalMiBDecrypted"/>
+				<DataLabel styleClass="label-large" alignment="CENTER_RIGHT" minWidth="60" byteFormat="%stats.data.bytes" kibFormat="%stats.data.kiB" mibFormat="%stats.data.miB" gibFormat="%stats.data.giB"
+						   dataInBytes="${controller.totalBytesDecrypted}"/>
+			</HBox>
+			<FormattedLabel styleClass="label-large" alignment="CENTER_RIGHT" minWidth="60" format="%stats.totalReads" arg1="${controller.filesRead}"/>
+
+		</VBox>
+
+	</VBox>
+
+	<!-- Write -->
+	<VBox prefWidth="300" prefHeight="300">
+		<HBox alignment="CENTER">
+			<Label styleClass="label-large" text="%stats.writtenDataLabel"/>
+			<ThrougputLabel styleClass="label-large" alignment="CENTER_RIGHT" minWidth="60" idleFormat="%main.vaultDetail.throughput.idle" kibsFormat="%main.vaultDetail.throughput.kbps"
+							mibsFormat="%main.vaultDetail.throughput.mbps" bytesPerSecond="${controller.bpsWritten}"/>
+		</HBox>
+		<AreaChart fx:id="writeChart" styleClass="io-stats" createSymbols="false" animated="false">
+			<xAxis>
+				<NumberAxis fx:id="writeChartXAxis" styleClass="io-stats" autoRanging="false" forceZeroInRange="false" side="BOTTOM"/>
+			</xAxis>
+			<yAxis>
+				<NumberAxis fx:id="writeChartYAxis" styleClass="io-stats" autoRanging="false" forceZeroInRange="true" side="LEFT" tickUnit="Infinity"/>
+			</yAxis>
+			<cursor>
+				<Cursor fx:constant="DEFAULT"/>
+			</cursor>
+		</AreaChart>
+		<VBox spacing="12" alignment="CENTER">
+			<HBox alignment="CENTER">
+				<Label styleClass="label-large" text="%stats.totalMiBWritten"/>
+				<DataLabel styleClass="label-large" alignment="CENTER_RIGHT" minWidth="60" byteFormat="%stats.data.bytes" kibFormat="%stats.data.kiB" mibFormat="%stats.data.miB" gibFormat="%stats.data.giB"
+						   dataInBytes="${controller.totalBytesWritten}"/>
+			</HBox>
+			<HBox alignment="CENTER">
+				<Label styleClass="label-large" text="%stats.totalMiBEncrypted"/>
+				<DataLabel styleClass="label-large" alignment="CENTER_RIGHT" minWidth="60" byteFormat="%stats.data.bytes" kibFormat="%stats.data.kiB" mibFormat="%stats.data.miB" gibFormat="%stats.data.giB"
+						   dataInBytes="${controller.totalBytesEncrypted}"/>
+			</HBox>
+			<FormattedLabel styleClass="label-large" alignment="CENTER_RIGHT" minWidth="60" format="%stats.totalWrites" arg1="${controller.filesWritten}"/>
+
+		</VBox>
+
+	</VBox>
+</HBox>

+ 1 - 11
main/ui/src/main/resources/fxml/vault_detail_unlocked.fxml

@@ -34,15 +34,5 @@
 	</Button>
 
 	<Region VBox.vgrow="ALWAYS"/>
-
-	<HBox alignment="CENTER_RIGHT" spacing="6">
-		<Label styleClass="label-small,label-muted" text="%main.vaultDetail.bytesPerSecondRead"/>
-		<ThrougputLabel styleClass="label-small,label-muted" alignment="CENTER_RIGHT" minWidth="60" idleFormat="%main.vaultDetail.throughput.idle" kibsFormat="%main.vaultDetail.throughput.kbps"
-						mibsFormat="%main.vaultDetail.throughput.mbps" bytesPerSecond="${controller.vault.stats.bytesPerSecondRead}"/>
-	</HBox>
-	<HBox alignment="CENTER_RIGHT" spacing="6">
-		<Label styleClass="label-small,label-muted" text="%main.vaultDetail.bytesPerSecondWritten"/>
-		<ThrougputLabel styleClass="label-small,label-muted" alignment="CENTER_RIGHT" minWidth="60" idleFormat="%main.vaultDetail.throughput.idle" kibsFormat="%main.vaultDetail.throughput.kbps"
-						mibsFormat="%main.vaultDetail.throughput.mbps" bytesPerSecond="${controller.vault.stats.bytesPerSecondWritten}"/>
-	</HBox>
+	<Button styleClass="button-large" text="%main.vaultDetail.stats" minWidth="120" onAction="#showVaultStatistics"/>
 </VBox>

+ 18 - 0
main/ui/src/main/resources/i18n/strings.properties

@@ -170,6 +170,23 @@ preferences.donationKey.getDonationKey=Get a donation key
 ## About
 preferences.about=About
 
+# Vault Statistics
+stats.title=Statistics for %s
+stats.readDataLabel=Read Data
+stats.writtenDataLabel=Written Data
+stats.cacheHitRate=Cache Hit Rate
+stats.totalMiBRead=Total Read: 
+stats.totalMiBWritten=Total Written: 
+stats.totalMiBEncrypted=Total Encypted: 
+stats.totalMiBDecrypted=Total Decrypted: 
+stats.totalReads=%s Number of Reads
+stats.totalWrites=%s Number of Writes
+stats.data.bytes=%s Bytes
+stats.data.kiB=%.1f KiB
+stats.data.miB=%.1f MiB
+stats.data.giB=%.1f GiB
+
+
 # Main Window
 main.closeBtn.tooltip=Close
 main.minimizeBtn.tooltip=Minimize
@@ -202,6 +219,7 @@ main.vaultDetail.bytesPerSecondWritten=written:
 main.vaultDetail.throughput.idle=idle
 main.vaultDetail.throughput.kbps=%.1f kiB/s
 main.vaultDetail.throughput.mbps=%.1f MiB/s
+main.vaultDetail.stats=Show Statistics
 ### Missing
 main.vaultDetail.missing.info=Cryptomator could not find a vault at this path.
 main.vaultDetail.missing.recheck=Recheck