diff --git a/bramble-api/src/test/java/org/briarproject/bramble/test/TestUtils.java b/bramble-api/src/test/java/org/briarproject/bramble/test/TestUtils.java
index 3d1aa37d117c9f3987d5eb181d63a53b0d128b9b..248552ba098163fb288cf25ac7ccbda16aed96d3 100644
--- a/bramble-api/src/test/java/org/briarproject/bramble/test/TestUtils.java
+++ b/bramble-api/src/test/java/org/briarproject/bramble/test/TestUtils.java
@@ -8,6 +8,8 @@ import org.briarproject.bramble.api.identity.LocalAuthor;
 import org.briarproject.bramble.api.sync.ClientId;
 import org.briarproject.bramble.api.sync.Group;
 import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.util.IoUtils;
 
 import java.io.File;
@@ -21,6 +23,8 @@ import java.util.concurrent.atomic.AtomicInteger;
 import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
 import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
 import static org.briarproject.bramble.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
+import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
+import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
 import static org.briarproject.bramble.util.StringUtils.getRandomString;
 
 public class TestUtils {
@@ -45,6 +49,13 @@ public class TestUtils {
 		return b;
 	}
 
+	public static byte[] getRandomBytes(int minLength, int maxLength) {
+		int length = minLength + random.nextInt(maxLength - minLength + 1);
+		byte[] b = new byte[length];
+		random.nextBytes(b);
+		return b;
+	}
+
 	public static byte[] getRandomId() {
 		return getRandomBytes(UniqueId.LENGTH);
 	}
@@ -53,9 +64,17 @@ public class TestUtils {
 		return new SecretKey(getRandomBytes(SecretKey.LENGTH));
 	}
 
+	public static int getRandomLength(int min, int max) {
+		return min + random.nextInt(max - min + 1);
+	}
+
 	public static LocalAuthor getLocalAuthor() {
+		return getLocalAuthor(getRandomLength(1, MAX_AUTHOR_NAME_LENGTH));
+	}
+
+	public static LocalAuthor getLocalAuthor(int nameLength) {
 		AuthorId id = new AuthorId(getRandomId());
-		String name = getRandomString(MAX_AUTHOR_NAME_LENGTH);
+		String name = getRandomString(nameLength);
 		byte[] publicKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
 		byte[] privateKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
 		long created = System.currentTimeMillis();
@@ -63,18 +82,39 @@ public class TestUtils {
 	}
 
 	public static Author getAuthor() {
+		return getAuthor(getRandomLength(1, MAX_AUTHOR_NAME_LENGTH));
+	}
+
+	public static Author getAuthor(int nameLength) {
 		AuthorId id = new AuthorId(getRandomId());
-		String name = getRandomString(MAX_AUTHOR_NAME_LENGTH);
+		String name = getRandomString(nameLength);
 		byte[] publicKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
 		return new Author(id, name, publicKey);
 	}
 
 	public static Group getGroup(ClientId clientId) {
+		int descriptorLength = getRandomLength(1, MAX_GROUP_DESCRIPTOR_LENGTH);
+		return getGroup(clientId, descriptorLength);
+	}
+
+	public static Group getGroup(ClientId clientId, int descriptorLength) {
 		GroupId groupId = new GroupId(getRandomId());
-		byte[] descriptor = getRandomBytes(MAX_GROUP_DESCRIPTOR_LENGTH);
+		byte[] descriptor = getRandomBytes(descriptorLength);
 		return new Group(groupId, clientId, descriptor);
 	}
 
+	public static Message getMessage(GroupId groupId) {
+		int bodyLength = getRandomLength(1, MAX_MESSAGE_BODY_LENGTH);
+		return getMessage(groupId, MESSAGE_HEADER_LENGTH + bodyLength);
+	}
+
+	public static Message getMessage(GroupId groupId, int rawLength) {
+		MessageId id = new MessageId(getRandomId());
+		byte[] raw = getRandomBytes(rawLength);
+		long timestamp = System.currentTimeMillis();
+		return new Message(id, groupId, timestamp, raw);
+	}
+
 	public static double getMedian(Collection<? extends Number> samples) {
 		int size = samples.size();
 		if (size == 0) throw new IllegalArgumentException();
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/BenchmarkTask.java b/bramble-core/src/test/java/org/briarproject/bramble/db/BenchmarkTask.java
index a00a6c28c5113fbd1c40540f4b258c729712bfa0..90317ce44ea0dc4d80ef8c3bcdeb9b5a132e7676 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/BenchmarkTask.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/BenchmarkTask.java
@@ -1,10 +1,6 @@
 package org.briarproject.bramble.db;
 
-import org.briarproject.bramble.api.db.DbException;
-
 interface BenchmarkTask<T> {
 
-	void prepareBenchmark(Database<T> db) throws DbException;
-
-	void runBenchmark(Database<T> db) throws DbException;
+	void run(T context) throws Exception;
 }
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabasePerformanceTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabasePerformanceTest.java
index 88172b4b301b2d6c34d3e1e5afb810abab5faa1d..e28e228816418a72ef3570560f9c2a5b07dc4bcd 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabasePerformanceTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabasePerformanceTest.java
@@ -1,14 +1,15 @@
 package org.briarproject.bramble.db;
 
+import org.briarproject.bramble.api.contact.ContactId;
 import org.briarproject.bramble.api.db.DatabaseConfig;
 import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.db.Metadata;
+import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.identity.LocalAuthor;
 import org.briarproject.bramble.api.sync.ClientId;
 import org.briarproject.bramble.api.sync.Group;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.Message;
-import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.sync.ValidationManager.State;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.bramble.system.SystemClock;
@@ -23,24 +24,20 @@ import java.io.FileOutputStream;
 import java.io.PrintWriter;
 import java.sql.Connection;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.LinkedHashMap;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
 
-import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_LENGTH;
 import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED;
-import static org.briarproject.bramble.api.sync.ValidationManager.State.PENDING;
-import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN;
 import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
 import static org.briarproject.bramble.test.TestUtils.getAuthor;
 import static org.briarproject.bramble.test.TestUtils.getGroup;
 import static org.briarproject.bramble.test.TestUtils.getLocalAuthor;
 import static org.briarproject.bramble.test.TestUtils.getMean;
 import static org.briarproject.bramble.test.TestUtils.getMedian;
+import static org.briarproject.bramble.test.TestUtils.getMessage;
 import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
-import static org.briarproject.bramble.test.TestUtils.getRandomId;
 import static org.briarproject.bramble.test.TestUtils.getStandardDeviation;
 import static org.briarproject.bramble.test.TestUtils.getTestDirectory;
 import static org.briarproject.bramble.util.StringUtils.getRandomString;
@@ -50,33 +47,58 @@ public abstract class JdbcDatabasePerformanceTest extends BrambleTestCase {
 
 	private static final int ONE_MEGABYTE = 1024 * 1024;
 	private static final int MAX_SIZE = 100 * ONE_MEGABYTE;
-	private static final int CLIENT_ID_LENGTH = 100;
-	private static final int METADATA_KEY_LENGTH = 100;
-	private static final int METADATA_VALUE_LENGTH = 100;
 
 	/**
-	 * Skip test cases that create more than this many rows.
+	 * How many contacts to simulate.
 	 */
-	private static final int MAX_ROWS = 100 * 1000;
+	private static final int CONTACTS = 20;
 
 	/**
-	 * How many times to run the benchmark before measuring, to warm up the JIT.
+	 * How many clients to simulate. Briar has nine: transport properties,
+	 * introductions, messaging, forums, forum sharing, blogs,
+	 * blog sharing, private groups, and private group sharing.
 	 */
-	private static final int WARMUP_ITERATIONS = 100;
+	private static final int CLIENTS = 10;
+	private static final int CLIENT_ID_LENGTH = 50;
 
 	/**
-	 * How many times to run the benchmark while measuring.
+	 * How many groups to simulate for each contact. Briar has seven:
+	 * transport properties, introductions, messaging, forum sharing, blog
+	 * sharing, private group sharing, and the contact's blog.
 	 */
-	private static final int MEASUREMENT_ITERATIONS = 100;
+	private static final int GROUPS_PER_CONTACT = 10;
+
+	/**
+	 * How many local groups to simulate. Briar has three: transport
+	 * properties, introductions and RSS feeds.
+	 */
+	private static final int LOCAL_GROUPS = 5;
+
+	private static final int MESSAGES_PER_GROUP = 20;
+	private static final int METADATA_KEYS_PER_GROUP = 5;
+	private static final int METADATA_KEYS_PER_MESSAGE = 5;
+	private static final int METADATA_KEY_LENGTH = 10;
+	private static final int METADATA_VALUE_LENGTH = 100;
 
 	/**
-	 * How much time to allow for background operations to complete after
-	 * preparing the benchmark.
+	 * How many times to run each benchmark before measuring, to warm up the
+	 * JIT and DB indices.
 	 */
-	private static final int SLEEP_BEFORE_MEASUREMENT_MS = 500;
+	private static final int WARMUP_ITERATIONS = 1000;
+
+	/**
+	 * How many times to run each benchmark while measuring.
+	 */
+	private static final int MEASUREMENT_ITERATIONS = 100;
 
 	private final File testDir = getTestDirectory();
 	private final File resultsFile = getResultsFile();
+	private final Random random = new Random();
+
+	private List<ClientId> clientIds;
+	private List<Group> groups;
+	private List<Message> messages;
+	private Map<GroupId, List<Metadata>> messageMeta;
 
 	protected abstract String getTestName();
 
@@ -98,626 +120,140 @@ public abstract class JdbcDatabasePerformanceTest extends BrambleTestCase {
 		return new File(getTestName() + "-" + timestamp + ".tsv");
 	}
 
-	@Test
-	public void testAddContact() throws Exception {
-		for (int contacts : new int[] {0, 1, 10, 100, 1000}) {
-			testAddContact(contacts);
-		}
-	}
-
-	private void testAddContact(final int contacts) throws Exception {
-		String name = "addContact(T, Author, AuthorId, boolean, boolean)";
-		Map<String, Object> args =
-				Collections.<String, Object>singletonMap("contacts", contacts);
-
-		benchmark(name, args, new BenchmarkTask<Connection>() {
-
-			private final LocalAuthor localAuthor = getLocalAuthor();
-
-			@Override
-			public void prepareBenchmark(Database<Connection> db)
-					throws DbException {
-				Connection txn = db.startTransaction();
-				db.addLocalAuthor(txn, localAuthor);
-				for (int i = 0; i < contacts; i++) {
-					db.addContact(txn, getAuthor(), localAuthor.getId(), true,
-							true);
-				}
-				db.commitTransaction(txn);
-			}
-
-			@Override
-			public void runBenchmark(Database<Connection> db)
-					throws DbException {
-				Connection txn = db.startTransaction();
-				db.addContact(txn, getAuthor(), localAuthor.getId(), true,
-						true);
-				db.commitTransaction(txn);
-			}
-		});
-	}
-
-	@Test
-	public void testAddGroup() throws Exception {
-		for (int groups : new int[] {0, 1, 10, 100, 1000}) {
-			testAddGroup(groups);
-		}
-	}
-
-	private void testAddGroup(final int groups) throws Exception {
-		String name = "addGroup(T, group)";
-		Map<String, Object> args =
-				Collections.<String, Object>singletonMap("groups", groups);
-
-		benchmark(name, args, new BenchmarkTask<Connection>() {
-
-			private final ClientId clientId = getClientId();
-
-			@Override
-			public void prepareBenchmark(Database<Connection> db)
-					throws DbException {
-				Connection txn = db.startTransaction();
-				for (int i = 0; i < groups; i++)
-					db.addGroup(txn, getGroup(clientId));
-				db.commitTransaction(txn);
-			}
-
-			@Override
-			public void runBenchmark(Database<Connection> db)
-					throws DbException {
-				Connection txn = db.startTransaction();
-				db.addGroup(txn, getGroup(clientId));
-				db.commitTransaction(txn);
-			}
-		});
-	}
-
 	@Test
 	public void testGetContacts() throws Exception {
-		for (int contacts : new int[] {1, 10, 100, 1000}) {
-			testGetContacts(contacts);
-		}
-	}
-
-	private void testGetContacts(final int contacts) throws Exception {
 		String name = "getContacts(T)";
-		Map<String, Object> args =
-				Collections.<String, Object>singletonMap("contacts", contacts);
-
-		benchmark(name, args, new BenchmarkTask<Connection>() {
-
-			@Override
-			public void prepareBenchmark(Database<Connection> db)
-					throws DbException {
-				LocalAuthor localAuthor = getLocalAuthor();
-				Connection txn = db.startTransaction();
-				db.addLocalAuthor(txn, localAuthor);
-				for (int i = 0; i < contacts; i++) {
-					db.addContact(txn, getAuthor(), localAuthor.getId(), true,
-							true);
-				}
-				db.commitTransaction(txn);
-			}
-
-			@Override
-			public void runBenchmark(Database<Connection> db)
-					throws DbException {
-				Connection txn = db.startTransaction();
-				db.getContacts(txn);
-				db.commitTransaction(txn);
-			}
+		benchmark(name, db -> {
+			Connection txn = db.startTransaction();
+			db.getContacts(txn);
+			db.commitTransaction(txn);
 		});
 	}
 
 	@Test
 	public void testGetRawMessage() throws Exception {
-		for (int messages : new int[] {1, 100, 10000}) {
-			testGetRawMessage(messages);
-		}
-	}
-
-	private void testGetRawMessage(final int messages) throws Exception {
 		String name = "getRawMessage(T, MessageId)";
-		Map<String, Object> args =
-				Collections.<String, Object>singletonMap("messages", messages);
-
-		benchmark(name, args, new BenchmarkTask<Connection>() {
-
-			private MessageId messageId;
-
-			@Override
-			public void prepareBenchmark(Database<Connection> db)
-					throws DbException {
-				Group group = getGroup(getClientId());
-				Connection txn = db.startTransaction();
-				db.addGroup(txn, group);
-				for (int i = 0; i < messages; i++) {
-					Message m = getMessage(group.getId());
-					if (i == 0) messageId = m.getId();
-					db.addMessage(txn, m, DELIVERED, false);
-				}
-				db.commitTransaction(txn);
-			}
-
-			@Override
-			public void runBenchmark(Database<Connection> db)
-					throws DbException {
-				Connection txn = db.startTransaction();
-				db.getRawMessage(txn, messageId);
-				db.commitTransaction(txn);
-			}
+		benchmark(name, db -> {
+			Connection txn = db.startTransaction();
+			db.getRawMessage(txn, pickRandom(messages).getId());
+			db.commitTransaction(txn);
 		});
 	}
 
 	@Test
 	public void testGetMessageIds() throws Exception {
-		for (int groups : new int[] {1, 10, 100}) {
-			for (int messagesPerGroup : new int[] {1, 10, 100}) {
-				int rows = groups * messagesPerGroup;
-				if (rows > MAX_ROWS) continue;
-				testGetMessageIds(groups, messagesPerGroup);
-			}
-		}
-	}
-
-	private void testGetMessageIds(final int groups, final int messagesPerGroup)
-			throws Exception {
 		String name = "getMessageIds(T, GroupId)";
-		Map<String, Object> args = new LinkedHashMap<String, Object>();
-		args.put("groups", groups);
-		args.put("messagesPerGroup", messagesPerGroup);
-
-		benchmark(name, args, new BenchmarkTask<Connection>() {
-
-			private GroupId groupId;
-
-			@Override
-			public void prepareBenchmark(Database<Connection> db)
-					throws DbException {
-				ClientId clientId = getClientId();
-				Connection txn = db.startTransaction();
-				for (int i = 0; i < groups; i++) {
-					Group g = getGroup(clientId);
-					if (i == 0) groupId = g.getId();
-					db.addGroup(txn, g);
-					for (int j = 0; j < messagesPerGroup; j++) {
-						Message m = getMessage(g.getId());
-						db.addMessage(txn, m, DELIVERED, false);
-					}
-				}
-				db.commitTransaction(txn);
-			}
-
-			@Override
-			public void runBenchmark(Database<Connection> db)
-					throws DbException {
-				Connection txn = db.startTransaction();
-				db.getMessageIds(txn, groupId);
-				db.commitTransaction(txn);
-			}
+		benchmark(name, db -> {
+			Connection txn = db.startTransaction();
+			db.getMessageIds(txn, pickRandom(groups).getId());
+			db.commitTransaction(txn);
 		});
 	}
 
 	@Test
-	public void testGetMessageIdsWithQuery() throws Exception {
-		for (int groups : new int[] {1, 10, 100}) {
-			for (int messagesPerGroup : new int[] {1, 10, 100}) {
-				for (int keysPerMessage : new int[] {1, 10, 100}) {
-					int rows = groups * messagesPerGroup * keysPerMessage;
-					if (rows > MAX_ROWS) continue;
-					for (int keysPerQuery : new int[] {1, 10}) {
-						testGetMessageIdsWithQuery(groups, messagesPerGroup,
-								keysPerMessage, keysPerQuery);
-					}
-				}
-			}
-		}
+	public void testGetMessageIdsWithMatchingQuery() throws Exception {
+		String name = "getMessageIds(T, GroupId, Metadata)";
+		benchmark(name, db -> {
+			Connection txn = db.startTransaction();
+			GroupId g = pickRandom(groups).getId();
+			db.getMessageIds(txn, g, pickRandom(messageMeta.get(g)));
+			db.commitTransaction(txn);
+		});
 	}
 
-	private void testGetMessageIdsWithQuery(final int groups,
-			final int messagesPerGroup, final int keysPerMessage,
-			final int keysPerQuery) throws Exception {
+	@Test
+	public void testGetMessageIdsWithNonMatchingQuery() throws Exception {
 		String name = "getMessageIds(T, GroupId, Metadata)";
-		Map<String, Object> args = new LinkedHashMap<String, Object>();
-		args.put("groups", groups);
-		args.put("messagesPerGroup", messagesPerGroup);
-		args.put("keysPerMessage", keysPerMessage);
-		args.put("keysPerQuery", keysPerQuery);
-
-		benchmark(name, args, new BenchmarkTask<Connection>() {
-
-			private final Metadata query = getMetadata(keysPerQuery);
-			private GroupId groupId;
-
-			@Override
-			public void prepareBenchmark(Database<Connection> db)
-					throws DbException {
-				ClientId clientId = getClientId();
-				Connection txn = db.startTransaction();
-				for (int i = 0; i < groups; i++) {
-					Group g = getGroup(clientId);
-					if (i == 0) groupId = g.getId();
-					db.addGroup(txn, g);
-					for (int j = 0; j < messagesPerGroup; j++) {
-						Message m = getMessage(g.getId());
-						db.addMessage(txn, m, DELIVERED, false);
-						Metadata meta = getMetadata(keysPerMessage);
-						db.mergeMessageMetadata(txn, m.getId(), meta);
-					}
-				}
-				db.commitTransaction(txn);
-			}
-
-			@Override
-			public void runBenchmark(Database<Connection> db)
-					throws DbException {
-				Connection txn = db.startTransaction();
-				db.getMessageIds(txn, groupId, query);
-				db.commitTransaction(txn);
-			}
+		benchmark(name, db -> {
+			Connection txn = db.startTransaction();
+			Metadata query = getMetadata(METADATA_KEYS_PER_MESSAGE);
+			db.getMessageIds(txn, pickRandom(groups).getId(), query);
+			db.commitTransaction(txn);
 		});
 	}
 
 	@Test
 	public void testGetGroupMetadata() throws Exception {
-		for (int groups : new int[] {1, 10, 100, 1000}) {
-			for (int keysPerGroup : new int[] {1, 10, 100}) {
-				int rows = groups * keysPerGroup;
-				if (rows > MAX_ROWS) continue;
-				testGetGroupMetadata(groups, keysPerGroup);
-			}
-		}
-	}
-
-	private void testGetGroupMetadata(final int groups, final int keysPerGroup)
-			throws Exception {
 		String name = "getGroupMetadata(T, GroupId)";
-		Map<String, Object> args = new LinkedHashMap<String, Object>();
-		args.put("groups", groups);
-		args.put("keysPerGroup", keysPerGroup);
-
-		benchmark(name, args, new BenchmarkTask<Connection>() {
-
-			private GroupId groupId;
-
-			@Override
-			public void prepareBenchmark(Database<Connection> db)
-					throws DbException {
-				ClientId clientId = getClientId();
-				Connection txn = db.startTransaction();
-				for (int i = 0; i < groups; i++) {
-					Group g = getGroup(clientId);
-					if (i == 0) groupId = g.getId();
-					db.addGroup(txn, g);
-					Metadata meta = getMetadata(keysPerGroup);
-					db.mergeGroupMetadata(txn, g.getId(), meta);
-				}
-				db.commitTransaction(txn);
-			}
-
-			@Override
-			public void runBenchmark(Database<Connection> db)
-					throws DbException {
-				Connection txn = db.startTransaction();
-				db.getGroupMetadata(txn, groupId);
-				db.commitTransaction(txn);
-			}
+		benchmark(name, db -> {
+			Connection txn = db.startTransaction();
+			db.getGroupMetadata(txn, pickRandom(groups).getId());
+			db.commitTransaction(txn);
 		});
 	}
 
 	@Test
 	public void testGetMessageMetadataForGroup() throws Exception {
-		for (int groups : new int[] {1, 10, 100}) {
-			for (int messagesPerGroup : new int[] {1, 10, 100}) {
-				for (int keysPerMessage : new int[] {1, 10, 100}) {
-					int rows = groups * messagesPerGroup * keysPerMessage;
-					if (rows > MAX_ROWS) continue;
-					testGetMessageMetadataForGroup(groups, messagesPerGroup,
-							keysPerMessage);
-				}
-			}
-		}
-	}
-
-	private void testGetMessageMetadataForGroup(final int groups,
-			final int messagesPerGroup, final int keysPerMessage)
-			throws Exception {
 		String name = "getMessageMetadata(T, GroupId)";
-		Map<String, Object> args = new LinkedHashMap<String, Object>();
-		args.put("groups", groups);
-		args.put("messagesPerGroup", messagesPerGroup);
-		args.put("keysPerMessage", keysPerMessage);
-
-		benchmark(name, args, new BenchmarkTask<Connection>() {
-
-			private GroupId groupId;
-
-			@Override
-			public void prepareBenchmark(Database<Connection> db)
-					throws DbException {
-				ClientId clientId = getClientId();
-				Connection txn = db.startTransaction();
-				for (int i = 0; i < groups; i++) {
-					Group g = getGroup(clientId);
-					if (i == 0) groupId = g.getId();
-					db.addGroup(txn, g);
-					for (int j = 0; j < messagesPerGroup; j++) {
-						Message m = getMessage(g.getId());
-						db.addMessage(txn, m, DELIVERED, false);
-						Metadata meta = getMetadata(keysPerMessage);
-						db.mergeMessageMetadata(txn, m.getId(), meta);
-					}
-				}
-				db.commitTransaction(txn);
-			}
-
-			@Override
-			public void runBenchmark(Database<Connection> db)
-					throws DbException {
-				Connection txn = db.startTransaction();
-				db.getMessageMetadata(txn, groupId);
-				db.commitTransaction(txn);
-			}
+		benchmark(name, db -> {
+			Connection txn = db.startTransaction();
+			db.getMessageMetadata(txn, pickRandom(groups).getId());
+			db.commitTransaction(txn);
 		});
 	}
 
 	@Test
 	public void testGetMessageMetadataForMessage() throws Exception {
-		for (int messages : new int[] {1, 100, 10000}) {
-			for (int keysPerMessage : new int[] {1, 10, 100}) {
-				int rows = messages * keysPerMessage;
-				if (rows > MAX_ROWS) continue;
-				testGetMessageMetadataForMessage(messages, keysPerMessage);
-			}
-		}
-	}
-
-	private void testGetMessageMetadataForMessage(final int messages,
-			final int keysPerMessage) throws Exception {
 		String name = "getMessageMetadata(T, MessageId)";
-		Map<String, Object> args = new LinkedHashMap<String, Object>();
-		args.put("messages", messages);
-		args.put("keysPerMessage", keysPerMessage);
-
-		benchmark(name, args, new BenchmarkTask<Connection>() {
-
-			private MessageId messageId;
-
-			@Override
-			public void prepareBenchmark(Database<Connection> db)
-					throws DbException {
-				Group g = getGroup(getClientId());
-				Connection txn = db.startTransaction();
-				db.addGroup(txn, g);
-				for (int i = 0; i < messages; i++) {
-					Message m = getMessage(g.getId());
-					if (i == 0) messageId = m.getId();
-					db.addMessage(txn, m, DELIVERED, false);
-					Metadata meta = getMetadata(keysPerMessage);
-					db.mergeMessageMetadata(txn, m.getId(), meta);
-				}
-				db.commitTransaction(txn);
-			}
-
-			@Override
-			public void runBenchmark(Database<Connection> db)
-					throws DbException {
-				Connection txn = db.startTransaction();
-				db.getMessageMetadata(txn, messageId);
-				db.commitTransaction(txn);
-			}
+		benchmark(name, db -> {
+			Connection txn = db.startTransaction();
+			db.getMessageMetadata(txn, pickRandom(messages).getId());
+			db.commitTransaction(txn);
 		});
 	}
 
 	@Test
 	public void testGetMessagesToShare() throws Exception {
-		for (int clients : new int[] {1, 10, 100}) {
-			for (int groupsPerClient : new int[] {1, 10, 100}) {
-				for (int messagesPerGroup : new int[] {1, 10, 100}) {
-					int rows = clients * groupsPerClient * messagesPerGroup;
-					if (rows > MAX_ROWS) continue;
-					testGetMessagesToShare(clients, groupsPerClient,
-							messagesPerGroup);
-				}
-			}
-		}
-	}
-
-	private void testGetMessagesToShare(final int clients,
-			final int groupsPerClient, final int messagesPerGroup)
-			throws Exception {
 		String name = "getMessagesToShare(T, ClientId)";
-		Map<String, Object> args = new LinkedHashMap<String, Object>();
-		args.put("clients", clients);
-		args.put("groupsPerClient", groupsPerClient);
-		args.put("messagesPerGroup", messagesPerGroup);
-
-		benchmark(name, args, new BenchmarkTask<Connection>() {
-
-			private ClientId clientId;
-
-			@Override
-			public void prepareBenchmark(Database<Connection> db)
-					throws DbException {
-				Random random = new Random();
-				Connection txn = db.startTransaction();
-				for (int i = 0; i < clients; i++) {
-					ClientId c = getClientId();
-					if (i == 0) clientId = c;
-					for (int j = 0; j < groupsPerClient; j++) {
-						Group g = getGroup(c);
-						db.addGroup(txn, g);
-						MessageId lastMessageId = null;
-						for (int k = 0; k < messagesPerGroup; k++) {
-							Message m = getMessage(g.getId());
-							boolean shared = random.nextBoolean();
-							db.addMessage(txn, m, DELIVERED, shared);
-							if (lastMessageId != null) {
-								db.addMessageDependency(txn, g.getId(),
-										m.getId(), lastMessageId);
-							}
-							lastMessageId = m.getId();
-						}
-					}
-				}
-				db.commitTransaction(txn);
-			}
-
-			@Override
-			public void runBenchmark(Database<Connection> db)
-					throws DbException {
-				Connection txn = db.startTransaction();
-				db.getMessagesToShare(txn, clientId);
-				db.commitTransaction(txn);
-			}
+		benchmark(name, db -> {
+			Connection txn = db.startTransaction();
+			db.getMessagesToShare(txn, pickRandom(clientIds));
+			db.commitTransaction(txn);
 		});
 	}
 
 	@Test
 	public void testGetMessagesToValidate() throws Exception {
-		for (int clients : new int[] {1, 10, 100}) {
-			for (int groupsPerClient : new int[] {1, 10, 100}) {
-				for (int messagesPerGroup : new int[] {1, 10, 100}) {
-					int rows = clients * groupsPerClient * messagesPerGroup;
-					if (rows > MAX_ROWS) continue;
-					testGetMessagesToValidate(clients, groupsPerClient,
-							messagesPerGroup);
-				}
-			}
-		}
-	}
-
-	private void testGetMessagesToValidate(final int clients,
-			final int groupsPerClient, final int messagesPerGroup)
-			throws Exception {
 		String name = "getMessagesToValidate(T, ClientId)";
-		Map<String, Object> args = new LinkedHashMap<String, Object>();
-		args.put("clients", clients);
-		args.put("groupsPerClient", groupsPerClient);
-		args.put("messagesPerGroup", messagesPerGroup);
-
-		benchmark(name, args, new BenchmarkTask<Connection>() {
-
-			private ClientId clientId;
-
-			@Override
-			public void prepareBenchmark(Database<Connection> db)
-					throws DbException {
-				Random random = new Random();
-				Connection txn = db.startTransaction();
-				for (int i = 0; i < clients; i++) {
-					ClientId c = getClientId();
-					if (i == 0) clientId = c;
-					for (int j = 0; j < groupsPerClient; j++) {
-						Group g = getGroup(c);
-						db.addGroup(txn, g);
-						for (int k = 0; k < messagesPerGroup; k++) {
-							Message m = getMessage(g.getId());
-							State s = random.nextBoolean() ? UNKNOWN : PENDING;
-							db.addMessage(txn, m, s, false);
-						}
-					}
-				}
-				db.commitTransaction(txn);
-			}
-
-			@Override
-			public void runBenchmark(Database<Connection> db)
-					throws DbException {
-				Connection txn = db.startTransaction();
-				db.getMessagesToValidate(txn, clientId);
-				db.commitTransaction(txn);
-			}
+		benchmark(name, db -> {
+			Connection txn = db.startTransaction();
+			db.getMessagesToValidate(txn, pickRandom(clientIds));
+			db.commitTransaction(txn);
 		});
 	}
 
 	@Test
 	public void testGetPendingMessages() throws Exception {
-		for (int clients : new int[] {1, 10, 100}) {
-			for (int groupsPerClient : new int[] {1, 10, 100}) {
-				for (int messagesPerGroup : new int[] {1, 10, 100}) {
-					int rows = clients * groupsPerClient * messagesPerGroup;
-					if (rows > MAX_ROWS) continue;
-					testGetPendingMessages(clients, groupsPerClient,
-							messagesPerGroup);
-				}
-			}
-		}
-	}
-
-	private void testGetPendingMessages(final int clients,
-			final int groupsPerClient, final int messagesPerGroup)
-			throws Exception {
 		String name = "getPendingMessages(T, ClientId)";
-		Map<String, Object> args = new LinkedHashMap<String, Object>();
-		args.put("clients", clients);
-		args.put("groupsPerClient", groupsPerClient);
-		args.put("messagesPerGroup", messagesPerGroup);
-
-		benchmark(name, args, new BenchmarkTask<Connection>() {
-
-			private ClientId clientId;
-
-			@Override
-			public void prepareBenchmark(Database<Connection> db)
-					throws DbException {
-				Random random = new Random();
-				Connection txn = db.startTransaction();
-				for (int i = 0; i < clients; i++) {
-					ClientId c = getClientId();
-					if (i == 0) clientId = c;
-					for (int j = 0; j < groupsPerClient; j++) {
-						Group g = getGroup(c);
-						db.addGroup(txn, g);
-						for (int k = 0; k < messagesPerGroup; k++) {
-							Message m = getMessage(g.getId());
-							State s = random.nextBoolean() ? UNKNOWN : PENDING;
-							db.addMessage(txn, m, s, false);
-						}
-					}
-				}
-				db.commitTransaction(txn);
-			}
-
-			@Override
-			public void runBenchmark(Database<Connection> db)
-					throws DbException {
-				Connection txn = db.startTransaction();
-				db.getPendingMessages(txn, clientId);
-				db.commitTransaction(txn);
-			}
+		benchmark(name, db -> {
+			Connection txn = db.startTransaction();
+			db.getPendingMessages(txn, pickRandom(clientIds));
+			db.commitTransaction(txn);
 		});
 	}
 
-	private void benchmark(String name, Map<String, Object> args,
-			BenchmarkTask<Connection> task) throws Exception {
-		for (int i = 0; i < WARMUP_ITERATIONS; i++) {
-			Database<Connection> db = open();
-			try {
-				task.prepareBenchmark(db);
-				task.runBenchmark(db);
-			} finally {
-				db.close();
-			}
-		}
-		List<Long> durations = new ArrayList<Long>(MEASUREMENT_ITERATIONS);
+	private <T> T pickRandom(List<T> list) {
+		return list.get(random.nextInt(list.size()));
+	}
+
+	private void benchmark(String name,
+			BenchmarkTask<Database<Connection>> task) throws Exception {
+		deleteTestDirectory(testDir);
+		Database<Connection> db = openDatabase();
+		populateDatabase(db);
+		db.close();
+		db = openDatabase();
+		List<Long> durations = new ArrayList<>(MEASUREMENT_ITERATIONS);
+		for (int i = 0; i < WARMUP_ITERATIONS; i++) task.run(db);
 		for (int i = 0; i < MEASUREMENT_ITERATIONS; i++) {
-			Database<Connection> db = open();
-			try {
-				task.prepareBenchmark(db);
-				Thread.sleep(SLEEP_BEFORE_MEASUREMENT_MS);
-				long start = System.nanoTime();
-				task.runBenchmark(db);
-				durations.add(System.nanoTime() - start);
-			} finally {
-				db.close();
-			}
+			long start = System.nanoTime();
+			task.run(db);
+			durations.add(System.nanoTime() - start);
 		}
-		double meanMillis = getMean(durations) / 1000 / 1000;
-		double medianMillis = getMedian(durations) / 1000 / 1000;
-		double stdDevMillis = getStandardDeviation(durations) / 1000 / 1000;
-		String result = name + '\t' + args + '\t' + meanMillis
-				+ '\t' + medianMillis + '\t' + stdDevMillis;
+		db.close();
+		String result = String.format("%s\t%,d\t%,d\t%,d", name,
+				(long) getMean(durations), (long) getMedian(durations),
+				(long) getStandardDeviation(durations));
 		System.out.println(result);
 		PrintWriter out =
 				new PrintWriter(new FileOutputStream(resultsFile, true), true);
@@ -725,23 +261,68 @@ public abstract class JdbcDatabasePerformanceTest extends BrambleTestCase {
 		out.close();
 	}
 
-	private Database<Connection> open() throws DbException {
-		deleteTestDirectory(testDir);
+	private Database<Connection> openDatabase() throws DbException {
 		Database<Connection> db = createDatabase(
 				new TestDatabaseConfig(testDir, MAX_SIZE), new SystemClock());
 		db.open();
 		return db;
 	}
 
-	private ClientId getClientId() {
-		return new ClientId(getRandomString(CLIENT_ID_LENGTH));
+	private void populateDatabase(Database<Connection> db) throws DbException {
+		clientIds = new ArrayList<>();
+		groups = new ArrayList<>();
+		messages = new ArrayList<>();
+		messageMeta = new HashMap<>();
+
+		for (int i = 0; i < CLIENTS; i++) clientIds.add(getClientId());
+
+		Connection txn = db.startTransaction();
+		LocalAuthor localAuthor = getLocalAuthor();
+		db.addLocalAuthor(txn, localAuthor);
+		for (int i = 0; i < CONTACTS; i++) {
+			Author a = getAuthor();
+			ContactId contactId = db.addContact(txn, a, localAuthor.getId(),
+					random.nextBoolean(), true);
+			for (int j = 0; j < GROUPS_PER_CONTACT; j++) {
+				Group g = getGroup(clientIds.get(j % CLIENTS));
+				groups.add(g);
+				messageMeta.put(g.getId(), new ArrayList<>());
+				db.addGroup(txn, g);
+				db.addGroupVisibility(txn, contactId, g.getId(), true);
+				Metadata gm = getMetadata(METADATA_KEYS_PER_GROUP);
+				db.mergeGroupMetadata(txn, g.getId(), gm);
+				for (int k = 0; k < MESSAGES_PER_GROUP; k++) {
+					Message m = getMessage(g.getId());
+					messages.add(m);
+					State state = State.fromValue(random.nextInt(4));
+					db.addMessage(txn, m, state, random.nextBoolean());
+					Metadata mm = getMetadata(METADATA_KEYS_PER_MESSAGE);
+					messageMeta.get(g.getId()).add(mm);
+					db.mergeMessageMetadata(txn, m.getId(), mm);
+				}
+			}
+		}
+		for (int i = 0; i < LOCAL_GROUPS; i++) {
+			Group g = getGroup(clientIds.get(i % CLIENTS));
+			groups.add(g);
+			messageMeta.put(g.getId(), new ArrayList<>());
+			db.addGroup(txn, g);
+			Metadata gm = getMetadata(METADATA_KEYS_PER_GROUP);
+			db.mergeGroupMetadata(txn, g.getId(), gm);
+			for (int j = 0; j < MESSAGES_PER_GROUP; j++) {
+				Message m = getMessage(g.getId());
+				messages.add(m);
+				db.addMessage(txn, m, DELIVERED, false);
+				Metadata mm = getMetadata(METADATA_KEYS_PER_MESSAGE);
+				messageMeta.get(g.getId()).add(mm);
+				db.mergeMessageMetadata(txn, m.getId(), mm);
+			}
+		}
+		db.commitTransaction(txn);
 	}
 
-	private Message getMessage(GroupId groupId) {
-		MessageId id = new MessageId(getRandomId());
-		byte[] raw = getRandomBytes(MAX_MESSAGE_LENGTH);
-		long timestamp = System.currentTimeMillis();
-		return new Message(id, groupId, timestamp, raw);
+	private ClientId getClientId() {
+		return new ClientId(getRandomString(CLIENT_ID_LENGTH));
 	}
 
 	private Metadata getMetadata(int keys) {