package com.onthegomap.planetiler.benchmarks; import com.google.common.base.Stopwatch; import com.onthegomap.planetiler.config.Arguments; import com.onthegomap.planetiler.geo.TileCoord; import com.onthegomap.planetiler.mbtiles.Mbtiles; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class BenchmarkMbtilesRead { private static final Logger LOGGER = LoggerFactory.getLogger(BenchmarkMbtilesRead.class); private static final String SELECT_RANDOM_COORDS = "select tile_column, tile_row, zoom_level from tiles order by random() limit ?"; public static void main(String[] args) throws Exception { Arguments arguments = Arguments.fromArgs(args); int repetitions = arguments.getInteger("bench_repetitions", "number of repetitions", 10); int nrTileReads = arguments.getInteger("bench_nr_tile_reads", "number of tiles to read", 500_000); int preWarms = arguments.getInteger("bench_pre_warms", "number of pre warm runs", 3); List mbtilesPaths = arguments.getList("bench_mbtiles", "the mbtiles file to read from", List.of()).stream() .map(Paths::get).toList(); if (mbtilesPaths.isEmpty()) { throw new IllegalArgumentException("pass one or many paths to the same mbtiles file"); } mbtilesPaths.stream().forEach(p -> { if (!Files.exists(p) || !Files.isRegularFile(p)) { throw new IllegalArgumentException("%s does not exists".formatted(p)); } }); List randomCoordsToFetchPerRepetition = new LinkedList<>(); do { try (var db = Mbtiles.newReadOnlyDatabase(mbtilesPaths.getFirst())) { try (var statement = db.connection().prepareStatement(SELECT_RANDOM_COORDS)) { statement.setInt(1, nrTileReads - randomCoordsToFetchPerRepetition.size()); var rs = statement.executeQuery(); while (rs.next()) { int x = rs.getInt("tile_column"); int y = rs.getInt("tile_row"); int z = rs.getInt("zoom_level"); randomCoordsToFetchPerRepetition.add(TileCoord.ofXYZ(x, (1 << z) - 1 - y, z)); } } } } while (randomCoordsToFetchPerRepetition.size() < nrTileReads); Map avgReadOperationsPerSecondPerDb = new HashMap<>(); for (Path dbPath : mbtilesPaths) { List results = new LinkedList<>(); LOGGER.info("working on {}", dbPath); for (int preWarm = 0; preWarm < preWarms; preWarm++) { readEachTile(randomCoordsToFetchPerRepetition, dbPath); } for (int rep = 0; rep < repetitions; rep++) { results.add(readEachTile(randomCoordsToFetchPerRepetition, dbPath)); } var readOperationsPerSecondStats = results.stream().mapToDouble(ReadResult::readOperationsPerSecond).summaryStatistics(); LOGGER.info("readOperationsPerSecondStats: {}", readOperationsPerSecondStats); avgReadOperationsPerSecondPerDb.put(dbPath, readOperationsPerSecondStats.getAverage()); } List keysSorted = avgReadOperationsPerSecondPerDb.entrySet().stream() .sorted((o1, o2) -> o1.getValue().compareTo(o2.getValue())) .map(Map.Entry::getKey) .toList(); LOGGER.info("diffs"); for (int i = 0; i < keysSorted.size() - 1; i++) { for (int j = i + 1; j < keysSorted.size(); j++) { Path db0 = keysSorted.get(i); double avg0 = avgReadOperationsPerSecondPerDb.get(db0); Path db1 = keysSorted.get(j); double avg1 = avgReadOperationsPerSecondPerDb.get(db1); double diff = avg1 * 100 / avg0 - 100; LOGGER.info("\"{}\" to \"{}\": avg read operations per second improved by {}%", db0, db1, diff); } } } private static ReadResult readEachTile(List coordsToFetch, Path dbPath) throws IOException { try (var db = Mbtiles.newReadOnlyDatabase(dbPath)) { db.getTile(0, 0, 0); // trigger prepared statement creation var totalSw = Stopwatch.createStarted(); for (var coordToFetch : coordsToFetch) { if (db.getTile(coordToFetch) == null) { throw new IllegalStateException("%s should exist in %s".formatted(coordToFetch, dbPath)); } } totalSw.stop(); return new ReadResult(totalSw.elapsed(), coordsToFetch.size()); } } private record ReadResult(Duration duration, int coordsFetchedCount) { double readOperationsPerSecond() { double secondsFractional = duration.toNanos() / 1E9; return coordsFetchedCount / secondsFractional; } } }