diff --git a/layerstats/README.md b/layerstats/README.md index 8baf345d10..9fcad3d8db 100644 --- a/layerstats/README.md +++ b/layerstats/README.md @@ -5,14 +5,14 @@ This page describes how to generate and analyze layer stats data to find ways to ### Generating Layer Stats -Run planetiler with `--output-layerstats` to generate an extra `.layerstats.tsv.gz` file with a row for each +Run planetiler with `--output-layerstats` to generate an extra `.layerstats.parquet` file with a row for each layer in each tile that can be used to analyze tile sizes. You can also get stats for an existing archive by running: ```bash -java -jar planetiler.jar stats --input= --output=layerstats.tsv.gz +java -jar planetiler.jar stats --input= --output=layerstats.parquet ``` -The output is a gzipped tsv with a row per layer on each tile and the following columns: +The output is a Parquet file with a row per layer on each tile and the following columns: | column | description | |---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------| @@ -34,7 +34,7 @@ The output is a gzipped tsv with a row per layer on each tile and the following Load a layer stats file in [duckdb](https://duckdb.org/): ```sql -CREATE TABLE layerstats AS SELECT * FROM 'output.pmtiles.layerstats.tsv.gz'; +CREATE TABLE layerstats AS SELECT * FROM 'output.pmtiles.layerstats.parquet'; ``` Then get the biggest layers: diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/Planetiler.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/Planetiler.java index 56ee3dcb97..80eb174d8b 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/Planetiler.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/Planetiler.java @@ -835,7 +835,7 @@ public void run() { } Path layerStatsPath = arguments.file("layer_stats", "layer stats output path", - // default to .layerstats.tsv.gz + // default to .layerstats.parquet TileSizeStats.getDefaultLayerstatsPath(Optional.ofNullable(output.getLocalPath()).orElse(Path.of("output")))); if (config.tileWriteThreads() < 1) { diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/archive/TileArchiveWriter.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/archive/TileArchiveWriter.java index 0c929b6a32..3cb046b494 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/archive/TileArchiveWriter.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/archive/TileArchiveWriter.java @@ -176,7 +176,8 @@ public static void writeOutput(FeatureGroup features, WriteableTileArchive outpu if (config.outputLayerStats()) { layerStatsBranch = pipeline.readFromQueue(layerStatsQueue) - .sinkTo("stats", 1, tileStatsWriter(layerStatsPath)); + // instance method: needs config to resolve layerstats format + .sinkTo("stats", 1, writer.tileStatsWriter(layerStatsPath)); } var loggers = ProgressLoggers.create() @@ -210,14 +211,17 @@ public static void writeOutput(FeatureGroup features, WriteableTileArchive outpu timer.stop(); } - private static WorkerPipeline.SinkStep tileStatsWriter(Path layerStatsPath) { + private WorkerPipeline.SinkStep tileStatsWriter(Path layerStatsPath) { return prev -> { - try (var statsWriter = TileSizeStats.newWriter(layerStatsPath)) { - statsWriter.write(TileSizeStats.headerRow()); + try (var statsWriter = TileSizeStats.createWriter(layerStatsPath)) { for (var batch : prev) { for (var encodedTile : batch.out().get()) { - for (var line : encodedTile.layerStats()) { - statsWriter.write(line); + if (encodedTile.layerStatsData() != null) { + statsWriter.write( + encodedTile.layerStatsData().coord(), + encodedTile.layerStatsData().archivedBytes(), + encodedTile.layerStatsData().layerStats() + ); } } } @@ -280,7 +284,6 @@ private void tileEncoderSink(Iterable prev) throws IOException { boolean lastIsFill = false; List lastLayerStats = null; boolean skipFilled = config.skipFilledTiles(); - var layerStatsSerializer = TileSizeStats.newThreadLocalSerializer(); boolean includeIds = !config.excludeIds(); var tileStatsUpdater = tileStats.threadLocalUpdater(); @@ -373,16 +376,17 @@ private void tileEncoderSink(Iterable prev) throws IOException { } if (!(skipFilled && lastIsFill) && bytes != null) { tileStatsUpdater.recordTile(tileFeatures.tileCoord(), bytes.length, layerStats); - List layerStatsRows = config.outputLayerStats() ? - layerStatsSerializer.formatOutputRows(tileFeatures.tileCoord(), bytes.length, layerStats) : - List.of(); + // store raw stats; writer decides format (tsv or parquet) + LayerStatsData layerStatsData = config.outputLayerStats() ? + new LayerStatsData(tileFeatures.tileCoord(), bytes.length, layerStats) : + null; result.add( new TileEncodingResult( tileFeatures.tileCoord(), bytes, encoded.length, tileDataHash == null ? OptionalLong.empty() : OptionalLong.of(tileDataHash), - layerStatsRows + layerStatsData ) ); } @@ -528,9 +532,5 @@ private record TileBatch( public int size() { return in.size(); } - - public boolean isEmpty() { - return in.isEmpty(); - } } } diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/archive/TileEncodingResult.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/archive/TileEncodingResult.java index ecbd8fb3e2..d3d650bce0 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/archive/TileEncodingResult.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/archive/TileEncodingResult.java @@ -1,25 +1,33 @@ package com.onthegomap.planetiler.archive; import com.onthegomap.planetiler.geo.TileCoord; +import com.onthegomap.planetiler.util.TileSizeStats; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.OptionalLong; +/** Layer statistics data for a tile. */ +record LayerStatsData( + TileCoord coord, + int archivedBytes, + List layerStats +) {} + public record TileEncodingResult( TileCoord coord, byte[] tileData, int rawTileSize, /* will always be empty in non-compact mode and might also be empty in compact mode */ OptionalLong tileDataHash, - List layerStats + LayerStatsData layerStatsData ) { public TileEncodingResult( TileCoord coord, byte[] tileData, OptionalLong tileDataHash ) { - this(coord, tileData, tileData.length, tileDataHash, List.of()); + this(coord, tileData, tileData.length, tileDataHash, null); } @Override diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/config/PlanetilerConfig.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/config/PlanetilerConfig.java index 3a826153c9..a0fc6e80e4 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/config/PlanetilerConfig.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/config/PlanetilerConfig.java @@ -236,7 +236,7 @@ public static PlanetilerConfig from(Arguments arguments) { arguments.getBoolean("mlt_reorder_features", "Allow re-ordering output features within each layer when tile format=MLT to reduce tile sizes", false), arguments.getBoolean("mlt_shared_dict", "Share dictionaries between string fields when tile format=MLT", false), - arguments.getBoolean("output_layerstats", "output a tsv.gz file for each tile/layer size", false), + arguments.getBoolean("output_layerstats", "output a parquet file for each tile/layer size", false), arguments.getString("debug_url", "debug url to use for displaying tiles with {z} {lat} {lon} placeholders", "https://onthegomap.github.io/planetiler-demo/#{z}/{lat}/{lon}"), tmpDir, diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/TileSizeStats.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/TileSizeStats.java index 2f61522070..dfa352e754 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/util/TileSizeStats.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/util/TileSizeStats.java @@ -39,6 +39,14 @@ import java.util.concurrent.atomic.AtomicLong; import me.lemire.integercompression.IntWrapper; import org.apache.commons.lang3.tuple.Pair; +import org.apache.parquet.example.data.Group; +import org.apache.parquet.example.data.simple.SimpleGroupFactory; +import org.apache.parquet.hadoop.ParquetWriter; +import org.apache.parquet.hadoop.example.ExampleParquetWriter; +import org.apache.parquet.hadoop.metadata.CompressionCodecName; +import org.apache.parquet.schema.MessageType; +import org.apache.parquet.schema.PrimitiveType; +import org.apache.parquet.schema.Types; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryCollection; import org.maplibre.mlt.converter.encodings.MltTypeMap; @@ -57,14 +65,14 @@ * Utilities for extracting tile and layer size summaries from encoded vector tiles. *

* {@link #computeTileStats(VectorTileProto.Tile)} extracts statistics about each layer in a tile and - * {@link TsvSerializer} formats them as row of a TSV file to write. + * writes them to a Parquet file. *

- * To generate a tsv.gz file with stats for each tile, you can add {@code --output-layerstats} option when generating an - * archive, or run the following an existing archive: + * To generate a Parquet file with stats for each tile, you can add {@code --output-layerstats} option when generating an + * archive, or run the following on an existing archive: * *

  * {@code
- * java -jar planetiler.jar stats --input= --output=layerstats.tsv.gz
+ * java -jar planetiler.jar stats --input= --output=layerstats.parquet
  * }
  * 
*/ @@ -79,7 +87,12 @@ public class TileSizeStats { /** Returns the default path that a layerstats file should go relative to an existing archive. */ public static Path getDefaultLayerstatsPath(Path archive) { - return archive.resolveSibling(archive.getFileName() + ".layerstats.tsv.gz"); + return archive.resolveSibling(archive.getFileName() + ".layerstats.parquet"); + } + + /** Creates a Parquet layerstats writer. */ + public static LayerStatsWriter createWriter(Path output) throws IOException { + return new ParquetLayerStatsWriter(output); } public static void main(String... args) { @@ -99,7 +112,8 @@ public static void main(String... args) { arguments.file("output", "output file", getDefaultLayerstatsPath(localPath)); var counter = new AtomicLong(0); var timer = stats.startStage("tilestats"); - record Batch(List tiles, CompletableFuture> stats) {} + record BatchData(TileCoord coord, int archivedBytes, List layerStats) {} + record Batch(List tiles, CompletableFuture> stats) {} WorkQueue writerQueue = new WorkQueue<>("tilestats_write_queue", 1_000, 1, stats); var pipeline = WorkerPipeline.start("tilestats", stats); var readBranch = pipeline @@ -137,9 +151,9 @@ record Batch(List tiles, CompletableFuture> stats) {} List layerStats = null; var updater = tileStats.threadLocalUpdater(); - var layerStatsSerializer = TileSizeStats.newThreadLocalSerializer(); for (var batch : prev) { - List lines = new ArrayList<>(batch.tiles.size()); + // collect raw stats per tile; serialization happens in write stage + List batchData = new ArrayList<>(batch.tiles.size()); for (var tile : batch.tiles) { if (!Arrays.equals(zipped, tile.bytes())) { zipped = tile.bytes(); @@ -148,19 +162,18 @@ record Batch(List tiles, CompletableFuture> stats) {} layerStats = computeTileStats(decoded); } updater.recordTile(tile.coord(), zipped.length, layerStats); - lines.addAll(layerStatsSerializer.formatOutputRows(tile.coord(), zipped.length, layerStats)); + batchData.add(new BatchData(tile.coord(), zipped.length, layerStats)); } - batch.stats.complete(lines); + batch.stats.complete(batchData); } }); var writeBranch = pipeline.readFromQueue(writerQueue) .sinkTo("write", 1, prev -> { - try (var writer = newWriter(output)) { - writer.write(headerRow()); + try (var writer = createWriter(output)) { for (var batch : prev) { - for (var line : batch.stats.get()) { - writer.write(line); + for (var data : batch.stats.get()) { + writer.write(data.coord, data.archivedBytes, data.layerStats); } } } @@ -419,4 +432,76 @@ public int compareTo(LayerStats o) { return layer.compareTo(o.layer); } } + + /** + * Writer abstraction for layerstats output. + */ + public interface LayerStatsWriter extends AutoCloseable { + + /** Write layerstats for a tile. */ + void write(TileCoord tileCoord, int archivedBytes, List layerStats) throws IOException; + + @Override + void close() throws IOException; + } + + /** + * Parquet writer for layerstats. + */ + private static class ParquetLayerStatsWriter implements LayerStatsWriter { + + private static final MessageType SCHEMA = Types.buildMessage() + .required(PrimitiveType.PrimitiveTypeName.INT32).named("z") + .required(PrimitiveType.PrimitiveTypeName.INT32).named("x") + .required(PrimitiveType.PrimitiveTypeName.INT32).named("y") + .required(PrimitiveType.PrimitiveTypeName.INT64).named("hilbert") + .required(PrimitiveType.PrimitiveTypeName.INT32).named("archived_tile_bytes") + .required(PrimitiveType.PrimitiveTypeName.BINARY).as(org.apache.parquet.schema.LogicalTypeAnnotation.stringType()) + .named("layer") + .required(PrimitiveType.PrimitiveTypeName.INT32).named("layer_bytes") + .required(PrimitiveType.PrimitiveTypeName.INT32).named("layer_features") + .required(PrimitiveType.PrimitiveTypeName.INT32).named("layer_geometries") + .required(PrimitiveType.PrimitiveTypeName.INT32).named("layer_attr_bytes") + .required(PrimitiveType.PrimitiveTypeName.INT32).named("layer_attr_keys") + .required(PrimitiveType.PrimitiveTypeName.INT32).named("layer_attr_values") + .named("layerstats"); + + private final ParquetWriter writer; + private final SimpleGroupFactory groupFactory; + + ParquetLayerStatsWriter(Path output) throws IOException { + this.groupFactory = new SimpleGroupFactory(SCHEMA); + org.apache.hadoop.fs.Path hadoopPath = new org.apache.hadoop.fs.Path(output.toString()); + this.writer = ExampleParquetWriter.builder(hadoopPath) + .withType(SCHEMA) + .withCompressionCodec(CompressionCodecName.SNAPPY) + .build(); + } + + @Override + public void write(TileCoord tileCoord, int archivedBytes, List layerStats) throws IOException { + long hilbert = tileCoord.hilbertEncoded(); + for (var layer : layerStats) { + Group group = groupFactory.newGroup() + .append("z", tileCoord.z()) + .append("x", tileCoord.x()) + .append("y", tileCoord.y()) + .append("hilbert", hilbert) + .append("archived_tile_bytes", archivedBytes) + .append("layer", layer.layer) + .append("layer_bytes", layer.layerBytes) + .append("layer_features", layer.layerFeatures) + .append("layer_geometries", layer.layerGeometries) + .append("layer_attr_bytes", layer.layerAttrBytes) + .append("layer_attr_keys", layer.layerAttrKeys) + .append("layer_attr_values", layer.layerAttrValues); + writer.write(group); + } + } + + @Override + public void close() throws IOException { + writer.close(); + } + } } diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java index 1f402eb4e3..e11abaa474 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java @@ -3,8 +3,6 @@ import static com.onthegomap.planetiler.TestUtils.*; import static org.junit.jupiter.api.Assertions.*; -import com.fasterxml.jackson.dataformat.csv.CsvMapper; -import com.fasterxml.jackson.dataformat.csv.CsvSchema; import com.onthegomap.planetiler.archive.ReadableTileArchive; import com.onthegomap.planetiler.archive.TileArchiveConfig; import com.onthegomap.planetiler.archive.TileArchiveMetadata; @@ -37,10 +35,8 @@ import com.onthegomap.planetiler.util.BuildInfo; import com.onthegomap.planetiler.util.FileUtils; import com.onthegomap.planetiler.util.FunctionThatThrows; -import com.onthegomap.planetiler.util.Gzip; import com.onthegomap.planetiler.util.TileSizeStats; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -2827,54 +2823,55 @@ public void processFeature(SourceFeature source, FeatureCollector features) { } } - final Path layerstats = outputPath.resolveSibling(outputPath.getFileName().toString() + ".layerstats.tsv.gz"); + final Path layerstats = outputPath.resolveSibling(outputPath.getFileName().toString() + ".layerstats.parquet"); if (args.contains("--output-layerstats")) { assertTrue(Files.exists(layerstats)); - byte[] data = Files.readAllBytes(layerstats); - byte[] uncompressed = Gzip.gunzip(data); - String[] lines = new String(uncompressed, StandardCharsets.UTF_8).split("\n"); - assertEquals(33, lines.length); - - assertEquals(List.of( - "z", - "x", - "y", - "hilbert", - "archived_tile_bytes", - "layer", - "layer_bytes", - "layer_features", - "layer_geometries", - "layer_attr_bytes", - "layer_attr_keys", - "layer_attr_values" - ), List.of(lines[0].split("\t")), lines[0]); - - var mapper = new CsvMapper(); - var reader = mapper - .readerFor(Map.class) - .with(CsvSchema.emptySchema().withColumnSeparator('\t').withLineSeparator("\n").withHeader()); - try (var items = reader.readValues(uncompressed)) { - while (items.hasNext()) { - @SuppressWarnings("unchecked") Map next = (Map) items.next(); - int z = Integer.parseInt(next.get("z")); - int x = Integer.parseInt(next.get("x")); - int y = Integer.parseInt(next.get("y")); - long hilbert = Long.parseLong(next.get("hilbert")); - assertEquals(hilbert, TileCoord.ofXYZ(x, y, z).hilbertEncoded()); - assertTrue(Integer.parseInt(next.get("z")) <= 14, "bad z: " + next); - } + + blue.strategic.parquet.Hydrator, Map> hydrator = + new blue.strategic.parquet.Hydrator<>() { + @Override + public Map start() { + return new HashMap<>(); + } + + @Override + public Map add(Map target, String heading, Object value) { + target.put(heading, value); + return target; + } + + @Override + public Map finish(Map target) { + return target; + } + }; + + List> rows; + try (var stream = blue.strategic.parquet.ParquetReader.streamContent( + layerstats.toFile(), blue.strategic.parquet.HydratorSupplier.constantly(hydrator))) { + rows = stream.toList(); + } + + assertEquals(32, rows.size()); + for (var row : rows) { + int z = ((Number) row.get("z")).intValue(); + int x = ((Number) row.get("x")).intValue(); + int y = ((Number) row.get("y")).intValue(); + long hilbert = ((Number) row.get("hilbert")).longValue(); + assertEquals(hilbert, TileCoord.ofXYZ(x, y, z).hilbertEncoded(), "bad hilbert: " + row); + assertTrue(z <= 14, "bad z: " + row); } // ensure tilestats standalone executable produces same output - var standaloneLayerstatsOutput = tempDir.resolve("layerstats2.tsv.gz"); + var standaloneLayerstatsOutput = tempDir.resolve("layerstats2.parquet"); TileSizeStats.main("--input=" + outputPath, "--output=" + standaloneLayerstatsOutput); - byte[] standaloneData = Files.readAllBytes(standaloneLayerstatsOutput); - byte[] standaloneUncompressed = Gzip.gunzip(standaloneData); - assertEquals( - new String(uncompressed, StandardCharsets.UTF_8), - new String(standaloneUncompressed, StandardCharsets.UTF_8) - ); + List> standaloneRows; + try (var stream = blue.strategic.parquet.ParquetReader.streamContent( + standaloneLayerstatsOutput.toFile(), blue.strategic.parquet.HydratorSupplier.constantly(hydrator))) { + standaloneRows = stream.toList(); + } + assertEquals(rows.size(), standaloneRows.size()); + assertEquals(rows, standaloneRows); } else { assertFalse(Files.exists(layerstats)); } diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/util/TileSizeStatsTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/TileSizeStatsTest.java index 5ab6ef360b..51e9699b24 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/util/TileSizeStatsTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/util/TileSizeStatsTest.java @@ -2,15 +2,20 @@ import static com.onthegomap.planetiler.TestUtils.newPoint; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.onthegomap.planetiler.VectorTile; import com.onthegomap.planetiler.geo.TileCoord; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.regex.Pattern; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.maplibre.mlt.converter.ConversionConfig; import org.maplibre.mlt.converter.FeatureTableOptimizations; import org.maplibre.mlt.converter.MltConverter; @@ -221,4 +226,69 @@ void computeStats2FeaturesNested() throws IOException { .trim(), (TileSizeStats.headerRow() + String.join("", formatted)).trim()); } + + @Test + void testParquetOutput(@TempDir Path tempDir) throws IOException { + Path parquetFile = tempDir.resolve("layerstats.parquet"); + var tileCoord = TileCoord.ofXYZ(1, 2, 3); + var stats = List.of( + new TileSizeStats.LayerStats("layer1", 100, 5, 5, 20, 2, 3), + new TileSizeStats.LayerStats("layer2", 150, 10, 10, 30, 3, 4) + ); + + try (var writer = TileSizeStats.createWriter(parquetFile)) { + writer.write(tileCoord, 999, stats); + } + + assertTrue(Files.exists(parquetFile)); + assertTrue(Files.size(parquetFile) > 0); + + blue.strategic.parquet.Hydrator, Map> hydrator = + new blue.strategic.parquet.Hydrator<>() { + @Override + public Map start() { + return new HashMap<>(); + } + + @Override + public Map add(Map target, String heading, Object value) { + target.put(heading, value); + return target; + } + + @Override + public Map finish(Map target) { + return target; + } + }; + + List> rows; + try (var stream = blue.strategic.parquet.ParquetReader.streamContent( + parquetFile.toFile(), blue.strategic.parquet.HydratorSupplier.constantly(hydrator))) { + rows = stream.toList(); + } + + assertEquals(2, rows.size()); + + var row0 = rows.get(0); + assertEquals(tileCoord.z(), ((Number) row0.get("z")).intValue()); + assertEquals(tileCoord.x(), ((Number) row0.get("x")).intValue()); + assertEquals(tileCoord.y(), ((Number) row0.get("y")).intValue()); + assertEquals(tileCoord.hilbertEncoded(), ((Number) row0.get("hilbert")).longValue()); + assertEquals(999, ((Number) row0.get("archived_tile_bytes")).intValue()); + assertEquals("layer1", row0.get("layer")); + assertEquals(100, ((Number) row0.get("layer_bytes")).intValue()); + assertEquals(5, ((Number) row0.get("layer_features")).intValue()); + assertEquals(5, ((Number) row0.get("layer_geometries")).intValue()); + assertEquals(20, ((Number) row0.get("layer_attr_bytes")).intValue()); + assertEquals(2, ((Number) row0.get("layer_attr_keys")).intValue()); + assertEquals(3, ((Number) row0.get("layer_attr_values")).intValue()); + + var row1 = rows.get(1); + assertEquals("layer2", row1.get("layer")); + assertEquals(150, ((Number) row1.get("layer_bytes")).intValue()); + assertEquals(10, ((Number) row1.get("layer_features")).intValue()); + assertEquals(30, ((Number) row1.get("layer_attr_bytes")).intValue()); + } + }