diff --git a/main/filesystem-stats/.gitignore b/main/filesystem-stats/.gitignore
new file mode 100644
index 000000000..b83d22266
--- /dev/null
+++ b/main/filesystem-stats/.gitignore
@@ -0,0 +1 @@
+/target/
diff --git a/main/filesystem-stats/pom.xml b/main/filesystem-stats/pom.xml
new file mode 100644
index 000000000..8cf146c7d
--- /dev/null
+++ b/main/filesystem-stats/pom.xml
@@ -0,0 +1,49 @@
+
+
+
+ 4.0.0
+
+ org.cryptomator
+ main
+ 0.11.0-SNAPSHOT
+
+ filesystem-stats
+ Cryptomator filesystem: Throughput statistics
+
+
+
+ org.cryptomator
+ filesystem-api
+
+
+ org.cryptomator
+ commons
+
+
+
+
+ org.cryptomator
+ commons-test
+
+
+ org.cryptomator
+ filesystem-inmemory
+
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+
+
\ No newline at end of file
diff --git a/main/filesystem-stats/src/main/java/org/cryptomator/filesystem/stats/StatsFile.java b/main/filesystem-stats/src/main/java/org/cryptomator/filesystem/stats/StatsFile.java
new file mode 100644
index 000000000..4f334ea5c
--- /dev/null
+++ b/main/filesystem-stats/src/main/java/org/cryptomator/filesystem/stats/StatsFile.java
@@ -0,0 +1,49 @@
+package org.cryptomator.filesystem.stats;
+
+import java.io.UncheckedIOException;
+import java.nio.ByteBuffer;
+import java.util.function.Consumer;
+
+import org.cryptomator.filesystem.File;
+import org.cryptomator.filesystem.ReadableFile;
+import org.cryptomator.filesystem.WritableFile;
+import org.cryptomator.filesystem.delegating.DelegatingFile;
+import org.cryptomator.filesystem.delegating.DelegatingReadableFile;
+import org.cryptomator.filesystem.delegating.DelegatingWritableFile;
+
+public class StatsFile extends DelegatingFile {
+
+ private final Consumer readCounter;
+ private final Consumer writeCounter;
+
+ public StatsFile(StatsFolder parent, File delegate, Consumer readCounter, Consumer writeCounter) {
+ super(parent, delegate);
+ this.readCounter = readCounter;
+ this.writeCounter = writeCounter;
+ }
+
+ @Override
+ public ReadableFile openReadable() throws UncheckedIOException {
+ return new DelegatingReadableFile(delegate.openReadable()) {
+ @Override
+ public int read(ByteBuffer target) throws UncheckedIOException {
+ int num = super.read(target);
+ readCounter.accept((long) num);
+ return num;
+ }
+ };
+ }
+
+ @Override
+ public WritableFile openWritable() throws UncheckedIOException {
+ return new DelegatingWritableFile(delegate.openWritable()) {
+ @Override
+ public int write(ByteBuffer source) throws UncheckedIOException {
+ int num = super.write(source);
+ writeCounter.accept((long) num);
+ return num;
+ }
+ };
+ }
+
+}
diff --git a/main/filesystem-stats/src/main/java/org/cryptomator/filesystem/stats/StatsFileSystem.java b/main/filesystem-stats/src/main/java/org/cryptomator/filesystem/stats/StatsFileSystem.java
new file mode 100644
index 000000000..71b445192
--- /dev/null
+++ b/main/filesystem-stats/src/main/java/org/cryptomator/filesystem/stats/StatsFileSystem.java
@@ -0,0 +1,39 @@
+package org.cryptomator.filesystem.stats;
+
+import java.util.concurrent.atomic.LongAdder;
+
+import org.cryptomator.filesystem.FileSystem;
+import org.cryptomator.filesystem.Folder;
+
+public class StatsFileSystem extends StatsFolder implements FileSystem {
+
+ private final LongAdder read;
+ private final LongAdder written;
+
+ public StatsFileSystem(Folder root) {
+ this(root, new LongAdder(), new LongAdder());
+ }
+
+ private StatsFileSystem(Folder root, LongAdder read, LongAdder written) {
+ super(null, root, read::add, written::add);
+ this.read = read;
+ this.written = written;
+ }
+
+ public long getBytesRead() {
+ return read.sum();
+ }
+
+ public void resetBytesRead() {
+ read.reset();
+ }
+
+ public long getBytesWritten() {
+ return written.sum();
+ }
+
+ public void resetBytesWritten() {
+ written.reset();
+ }
+
+}
diff --git a/main/filesystem-stats/src/main/java/org/cryptomator/filesystem/stats/StatsFolder.java b/main/filesystem-stats/src/main/java/org/cryptomator/filesystem/stats/StatsFolder.java
new file mode 100644
index 000000000..f04de3322
--- /dev/null
+++ b/main/filesystem-stats/src/main/java/org/cryptomator/filesystem/stats/StatsFolder.java
@@ -0,0 +1,30 @@
+package org.cryptomator.filesystem.stats;
+
+import java.util.function.Consumer;
+
+import org.cryptomator.filesystem.File;
+import org.cryptomator.filesystem.Folder;
+import org.cryptomator.filesystem.delegating.DelegatingFolder;
+
+public class StatsFolder extends DelegatingFolder {
+
+ private final Consumer readCounter;
+ private final Consumer writeCounter;
+
+ public StatsFolder(StatsFolder parent, Folder delegate, Consumer readCounter, Consumer writeCounter) {
+ super(parent, delegate);
+ this.readCounter = readCounter;
+ this.writeCounter = writeCounter;
+ }
+
+ @Override
+ protected StatsFile newFile(File delegate) {
+ return new StatsFile(this, delegate, readCounter, writeCounter);
+ }
+
+ @Override
+ protected StatsFolder newFolder(Folder delegate) {
+ return new StatsFolder(this, delegate, readCounter, writeCounter);
+ }
+
+}
diff --git a/main/filesystem-stats/src/test/java/org/cryptomator/filesystem/stats/StatsFileSystemTest.java b/main/filesystem-stats/src/test/java/org/cryptomator/filesystem/stats/StatsFileSystemTest.java
new file mode 100644
index 000000000..7952cdcd2
--- /dev/null
+++ b/main/filesystem-stats/src/test/java/org/cryptomator/filesystem/stats/StatsFileSystemTest.java
@@ -0,0 +1,50 @@
+package org.cryptomator.filesystem.stats;
+
+import java.nio.ByteBuffer;
+
+import org.cryptomator.filesystem.File;
+import org.cryptomator.filesystem.FileSystem;
+import org.cryptomator.filesystem.ReadableFile;
+import org.cryptomator.filesystem.WritableFile;
+import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class StatsFileSystemTest {
+
+ @Test
+ public void testReadAndWriteCounters() {
+ FileSystem underlyingFs = new InMemoryFileSystem();
+ StatsFileSystem statsFs = new StatsFileSystem(underlyingFs);
+ statsFs.folder("foo").create();
+ File testFile = statsFs.folder("foo").file("bar");
+
+ Assert.assertEquals(0l, statsFs.getBytesRead());
+ Assert.assertEquals(0l, statsFs.getBytesWritten());
+
+ try (WritableFile w = testFile.openWritable()) {
+ w.write(ByteBuffer.allocate(15));
+ }
+
+ Assert.assertEquals(0l, statsFs.getBytesRead());
+ Assert.assertEquals(15l, statsFs.getBytesWritten());
+
+ statsFs.resetBytesWritten();
+
+ Assert.assertEquals(0l, statsFs.getBytesRead());
+ Assert.assertEquals(0l, statsFs.getBytesWritten());
+
+ try (ReadableFile r = testFile.openReadable()) {
+ r.read(ByteBuffer.allocate(3));
+ }
+
+ Assert.assertEquals(3l, statsFs.getBytesRead());
+ Assert.assertEquals(0l, statsFs.getBytesWritten());
+
+ statsFs.resetBytesRead();
+
+ Assert.assertEquals(0l, statsFs.getBytesRead());
+ Assert.assertEquals(0l, statsFs.getBytesWritten());
+ }
+
+}
diff --git a/main/filesystem-stats/src/test/java/org/cryptomator/filesystem/stats/StatsFileTest.java b/main/filesystem-stats/src/test/java/org/cryptomator/filesystem/stats/StatsFileTest.java
new file mode 100644
index 000000000..df3ee5573
--- /dev/null
+++ b/main/filesystem-stats/src/test/java/org/cryptomator/filesystem/stats/StatsFileTest.java
@@ -0,0 +1,50 @@
+package org.cryptomator.filesystem.stats;
+
+import java.nio.ByteBuffer;
+import java.util.function.Consumer;
+
+import org.cryptomator.filesystem.File;
+import org.cryptomator.filesystem.ReadableFile;
+import org.cryptomator.filesystem.WritableFile;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class StatsFileTest {
+
+ @Test
+ public void testStatsDuringRead() {
+ ReadableFile readable = Mockito.mock(ReadableFile.class);
+ File file = Mockito.mock(File.class);
+ Mockito.when(file.openReadable()).thenReturn(readable);
+
+ @SuppressWarnings("unchecked")
+ Consumer readCounter = Mockito.mock(Consumer.class);
+ File statsFile = new StatsFile(null, file, readCounter, null);
+
+ Mockito.when(readable.read(Mockito.any())).thenReturn(123);
+ try (ReadableFile r = statsFile.openReadable()) {
+ r.read(ByteBuffer.allocate(0));
+ }
+
+ Mockito.verify(readCounter).accept(123l);
+ }
+
+ @Test
+ public void testStatsDuringWrite() {
+ WritableFile writable = Mockito.mock(WritableFile.class);
+ File file = Mockito.mock(File.class);
+ Mockito.when(file.openWritable()).thenReturn(writable);
+
+ @SuppressWarnings("unchecked")
+ Consumer writeCounter = Mockito.mock(Consumer.class);
+ File statsFile = new StatsFile(null, file, null, writeCounter);
+
+ Mockito.when(writable.write(Mockito.any())).thenReturn(123);
+ try (WritableFile w = statsFile.openWritable()) {
+ w.write(ByteBuffer.allocate(0));
+ }
+
+ Mockito.verify(writeCounter).accept(123l);
+ }
+
+}
diff --git a/main/pom.xml b/main/pom.xml
index 217b825b5..db5ecf041 100644
--- a/main/pom.xml
+++ b/main/pom.xml
@@ -253,6 +253,7 @@
filesystem-nio
filesystem-nameshortening
filesystem-crypto
+ filesystem-stats
filesystem-invariants-tests
frontend-api
frontend-webdav