diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java b/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java index aa9a021a9..f2ab973f6 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/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 // *******************************************************************************/ diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java index 46ffe28be..46cf31991 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java +++ b/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> 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 fs, ObjectProperty state, ExecutorService executor) { @@ -53,8 +64,28 @@ public class VaultStats { private void updateStats(Optional 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> { @@ -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();} } diff --git a/main/pom.xml b/main/pom.xml index ae8070a29..5989c3276 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -24,31 +24,29 @@ UTF-8 - 1.9.12 + 1.9.13 0.1.6 0.1.0-beta1 0.1.0-beta3 - 0.1.0-beta1 + 0.1.0-beta2 1.2.5 - 1.1.15 - 1.0.12 + 1.2.0 + 1.0.13 - 14 + 15 3.11 - 1.1.0 - 1.1.1 - 3.10.3 + 3.11.0 2.1.0 30.0-jre - 2.22 + 2.29.1 2.8.6 1.7.30 1.2.3 - 5.6.2 - 3.3.3 + 5.7.0 + 3.6.0 2.2 @@ -79,11 +77,6 @@ - - org.cryptomator - siv-mode - 1.4.0 - org.cryptomator cryptofs @@ -242,6 +235,7 @@ org.junit.jupiter junit-jupiter + test org.hamcrest @@ -382,7 +376,7 @@ org.jacoco jacoco-maven-plugin - 0.8.5 + 0.8.6 prepare-agent diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/ErrorComponent.java b/main/ui/src/main/java/org/cryptomator/ui/common/ErrorComponent.java index 6a50ee44a..285270b4c 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/ErrorComponent.java +++ b/main/ui/src/main/java/org/cryptomator/ui/common/ErrorComponent.java @@ -4,6 +4,7 @@ import dagger.BindsInstance; import dagger.Subcomponent; import javax.annotation.Nullable; +import javafx.application.Platform; import javafx.scene.Scene; import javafx.stage.Stage; @@ -16,6 +17,14 @@ public interface ErrorComponent { Scene scene(); default void showErrorScene() { + if (Platform.isFxApplicationThread()) { + show(); + } else { + Platform.runLater(this::show); + } + } + + private void show() { Stage stage = window(); stage.setScene(scene()); stage.show(); diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java index f89c0dd95..43074a605 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/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; diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/WeakBindings.java b/main/ui/src/main/java/org/cryptomator/ui/common/WeakBindings.java index c02eb1d86..e6071df1d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/WeakBindings.java +++ b/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 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 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 observable) { + return new IntegerBinding() { + { + bind(observable); + } + + @Override + protected int computeValue() { + return observable.getValue().intValue(); + } + }; + } + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/DataLabel.java b/main/ui/src/main/java/org/cryptomator/ui/controls/DataLabel.java new file mode 100644 index 000000000..a5834cce7 --- /dev/null +++ b/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); } + + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/ThrougputLabel.java b/main/ui/src/main/java/org/cryptomator/ui/controls/ThrougputLabel.java index b07cf9b6f..a21e6d916 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controls/ThrougputLabel.java +++ b/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); diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java index 255f560a6..e39a9d991 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java +++ b/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 diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java index 8eb4d7f74..1806d9e5e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailUnlockedController.java +++ b/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; private final VaultService vaultService; + private final LoadingCache vaultStats; + private final VaultStatisticsComponent.Builder vaultStatsBuilder; @Inject - public VaultDetailUnlockedController(ObjectProperty vault, VaultService vaultService) { + public VaultDetailUnlockedController(ObjectProperty 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 vaultProperty() { diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java index 208605f51..496ada2ce 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java @@ -6,6 +6,7 @@ import org.cryptomator.common.settings.KeychainBackend; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.UiTheme; import org.cryptomator.integrations.keychain.KeychainAccessProvider; +import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.FxController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,6 +25,7 @@ import javafx.scene.control.ChoiceBox; import javafx.scene.control.RadioButton; import javafx.scene.control.Toggle; import javafx.scene.control.ToggleGroup; +import javafx.stage.Stage; import javafx.util.StringConverter; import java.util.Arrays; import java.util.Optional; @@ -37,6 +39,7 @@ public class GeneralPreferencesController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(GeneralPreferencesController.class); + private final Stage window; private final Settings settings; private final boolean trayMenuSupported; private final Optional autoStartStrategy; @@ -47,6 +50,7 @@ public class GeneralPreferencesController implements FxController { private final Application application; private final Environment environment; private final Set keychainAccessProviders; + private final ErrorComponent.Builder errorComponent; public ChoiceBox themeChoiceBox; public ChoiceBox keychainBackendChoiceBox; public CheckBox startHiddenCheckbox; @@ -57,7 +61,8 @@ public class GeneralPreferencesController implements FxController { public RadioButton nodeOrientationRtl; @Inject - GeneralPreferencesController(Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional autoStartStrategy, Set keychainAccessProviders, ObjectProperty selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor, ResourceBundle resourceBundle, Application application, Environment environment) { + GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional autoStartStrategy, Set keychainAccessProviders, ObjectProperty selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor, ResourceBundle resourceBundle, Application application, Environment environment, ErrorComponent.Builder errorComponent) { + this.window = window; this.settings = settings; this.trayMenuSupported = trayMenuSupported; this.autoStartStrategy = autoStartStrategy; @@ -68,6 +73,7 @@ public class GeneralPreferencesController implements FxController { this.resourceBundle = resourceBundle; this.application = application; this.environment = environment; + this.errorComponent = errorComponent; } @FXML @@ -129,6 +135,7 @@ public class GeneralPreferencesController implements FxController { toggleTask.setOnFailed(event -> { autoStartCheckbox.setSelected(!enableAutoStart); // restore previous state LOG.error("Failed to toggle autostart.", event.getSource().getException()); + errorComponent.cause(event.getSource().getException()).window(window).returnToScene(window.getScene()).build().showErrorScene(); }); executor.execute(toggleTask); }); diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java index de756d899..902f2cd50 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java @@ -4,6 +4,7 @@ import dagger.Lazy; import org.cryptomator.common.vaults.Vault; import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.ui.common.Animations; +import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; @@ -31,16 +32,18 @@ public class RecoveryKeyCreationController implements FxController { private final ExecutorService executor; private final RecoveryKeyFactory recoveryKeyFactory; private final StringProperty recoveryKeyProperty; + private final ErrorComponent.Builder errorComponent; public NiceSecurePasswordField passwordField; @Inject - public RecoveryKeyCreationController(@RecoveryKeyWindow Stage window, @FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS) Lazy successScene, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey) { + public RecoveryKeyCreationController(@RecoveryKeyWindow Stage window, @FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS) Lazy successScene, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, ErrorComponent.Builder errorComponent) { this.window = window; this.successScene = successScene; this.vault = vault; this.executor = executor; this.recoveryKeyFactory = recoveryKeyFactory; this.recoveryKeyProperty = recoveryKey; + this.errorComponent = errorComponent; } @FXML @@ -59,6 +62,7 @@ public class RecoveryKeyCreationController implements FxController { Animations.createShakeWindowAnimation(window).play(); } else { LOG.error("Creation of recovery key failed.", task.getException()); + errorComponent.cause(task.getException()).window(window).returnToScene(window.getScene()).build().showErrorScene(); } }); executor.submit(task); diff --git a/main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsComponent.java b/main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsComponent.java new file mode 100644 index 000000000..aa7b410c2 --- /dev/null +++ b/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. + *

+ * Important: Outside of {@link org.cryptomator.ui.stats}, this component should be weakly referenced, + * as it include memory-intensive UI nodes. + *

+ * 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(); + + 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(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsController.java b/main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsController.java new file mode 100644 index 000000000..f8937e90f --- /dev/null +++ b/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 readData; + private final Series 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 readChart; + public AreaChart 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 { + + private long step = IO_SAMPLING_STEPS; + private final Series decryptedBytesRead; + private final Series encryptedBytesWrite; + private final long[] maxBuf = new long[IO_SAMPLING_STEPS]; + + public IoSamplingAnimationHandler(Series readData, Series 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();} +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsModule.java b/main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsModule.java new file mode 100644 index 000000000..14395af24 --- /dev/null +++ b/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, Provider> 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 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); +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsScoped.java b/main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsScoped.java new file mode 100644 index 000000000..43de85b54 --- /dev/null +++ b/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 { + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsWindow.java b/main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsWindow.java new file mode 100644 index 000000000..2cd3ca37f --- /dev/null +++ b/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 { + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java index 9b95103ad..8cb7e0752 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java @@ -209,9 +209,7 @@ public class UnlockWorkflow extends Task { private void handleGenericError(Throwable e) { LOG.error("Unlock failed for technical reasons.", e); - Platform.runLater(() -> { - errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene(); - }); + errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene(); } private void wipePassword(char[] pw) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java index 5d65107d0..b638ad889 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java @@ -162,7 +162,9 @@ public class MountOptionsController implements FxController { @Override public String toString(String driveLetter) { - if (occupiedDriveLetters.contains(driveLetter)) { + if (Strings.isNullOrEmpty(driveLetter)) { + return ""; + } else if (occupiedDriveLetters.contains(driveLetter)) { return driveLetter + ": (" + resourceBundle.getString("vaultOptions.mount.winDriveLetterOccupied") + ")"; } else { return driveLetter + ":"; diff --git a/main/ui/src/main/resources/css/dark_theme.css b/main/ui/src/main/resources/css/dark_theme.css index 997cc233a..8d20ad9be 100644 --- a/main/ui/src/main/resources/css/dark_theme.css +++ b/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; +} diff --git a/main/ui/src/main/resources/css/light_theme.css b/main/ui/src/main/resources/css/light_theme.css index 606198cbc..b0ba8ac8c 100644 --- a/main/ui/src/main/resources/css/light_theme.css +++ b/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; +} diff --git a/main/ui/src/main/resources/fxml/stats.fxml b/main/ui/src/main/resources/fxml/stats.fxml new file mode 100644 index 000000000..a2ef920b9 --- /dev/null +++ b/main/ui/src/main/resources/fxml/stats.fxml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/main/ui/src/main/resources/fxml/vault_detail_unlocked.fxml b/main/ui/src/main/resources/fxml/vault_detail_unlocked.fxml index 655bae1bf..7a4d62952 100644 --- a/main/ui/src/main/resources/fxml/vault_detail_unlocked.fxml +++ b/main/ui/src/main/resources/fxml/vault_detail_unlocked.fxml @@ -35,14 +35,20 @@ - - - -