- Timestamp:
- 2022-06-08T17:48:47+02:00 (2 years ago)
- Location:
- trunk/src/org/openstreetmap/josm/data
- Files:
-
- 13 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java
r18431 r18473 2 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 3 4 import java.io.ByteArrayOutputStream; 4 5 import java.io.IOException; 5 6 import java.text.NumberFormat; … … 27 28 /** 28 29 * The number format instance to use (using a static instance gets rid of quite o few allocations) 29 * Doing this reduced the allocations of {@link #parseTagValue(String, Layer, Number )} from 22.79% of parent to30 * Doing this reduced the allocations of {@link #parseTagValue(String, Layer, Number, List)} from 22.79% of parent to 30 31 * 12.2% of parent. 31 32 */ 32 33 private static final NumberFormat NUMBER_FORMAT = NumberFormat.getNumberInstance(Locale.ROOT); 34 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 33 35 /** 34 36 * The geometry of the feature. Required. … … 48 50 * The tags of the feature. Optional. 49 51 */ 50 private TagMap tags;52 private final TagMap tags; 51 53 private Geometry geometryObject; 52 54 … … 62 64 GeometryTypes geometryTypeTemp = GeometryTypes.UNKNOWN; 63 65 String key = null; 66 // Use a list where we can grow capacity easily (TagMap will do an array copy every time a tag is added) 67 // This lets us avoid most array copies (i.e., this should only happen if some software decided it would be 68 // a good idea to have multiple tag fields). 69 // By avoiding array copies in TagMap, Feature#init goes from 339 MB to 188 MB. 70 ArrayList<String> tagList = null; 64 71 try (ProtobufParser parser = new ProtobufParser(record.getBytes())) { 72 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(4); 65 73 while (parser.hasNext()) { 66 try (ProtobufRecord next = new ProtobufRecord( parser)) {74 try (ProtobufRecord next = new ProtobufRecord(byteArrayOutputStream, parser)) { 67 75 if (next.getField() == TAG_FIELD) { 68 if (tags == null) { 69 tags = new TagMap(); 76 // This is packed in v1 and v2 77 ProtobufPacked packed = new ProtobufPacked(byteArrayOutputStream, next.getBytes()); 78 if (tagList == null) { 79 tagList = new ArrayList<>(packed.getArray().length); 80 } else { 81 tagList.ensureCapacity(tagList.size() + packed.getArray().length); 70 82 } 71 // This is packed in v1 and v272 ProtobufPacked packed = new ProtobufPacked(next.getBytes());73 83 for (Number number : packed.getArray()) { 74 key = parseTagValue(key, layer, number );84 key = parseTagValue(key, layer, number, tagList); 75 85 } 76 86 } else if (next.getField() == GEOMETRY_FIELD) { 77 87 // This is packed in v1 and v2 78 ProtobufPacked packed = new ProtobufPacked( next.getBytes());88 ProtobufPacked packed = new ProtobufPacked(byteArrayOutputStream, next.getBytes()); 79 89 CommandInteger currentCommand = null; 80 90 for (Number number : packed.getArray()) { … … 91 101 // TODO fallback to non-packed 92 102 } else if (next.getField() == GEOMETRY_TYPE_FIELD) { 93 geometryTypeTemp = GeometryTypes.values()[next.asUnsignedVarInt().intValue()]; 103 // by using getAllValues, we avoid 12.4 MB allocations 104 geometryTypeTemp = GeometryTypes.getAllValues()[next.asUnsignedVarInt().intValue()]; 94 105 } else if (next.getField() == ID_FIELD) { 95 106 tId = next.asUnsignedVarInt().longValue(); … … 101 112 this.geometryType = geometryTypeTemp; 102 113 record.close(); 114 if (tagList != null && !tagList.isEmpty()) { 115 this.tags = new TagMap(tagList.toArray(EMPTY_STRING_ARRAY)); 116 } else { 117 this.tags = null; 118 } 103 119 } 104 120 … … 109 125 * @param layer The layer with key/value information 110 126 * @param number The number to get the value from 127 * @param tagList The list to add the new value to 111 128 * @return The new key (if {@code null}, then a value was parsed and added to tags) 112 129 */ 113 private String parseTagValue(String key, Layer layer, Number number ) {130 private String parseTagValue(String key, Layer layer, Number number, List<String> tagList) { 114 131 if (key == null) { 115 132 key = layer.getKey(number.intValue()); 116 133 } else { 134 tagList.add(key); 117 135 Object value = layer.getValue(number.intValue()); 118 136 if (value instanceof Double || value instanceof Float) { … … 122 140 try { 123 141 NUMBER_FORMAT.setGroupingUsed(false); 124 t his.tags.put(key, NUMBER_FORMAT.format(value));142 tagList.add(Utils.intern(NUMBER_FORMAT.format(value))); 125 143 } finally { 126 144 NUMBER_FORMAT.setGroupingUsed(grouping); 127 145 } 128 146 } else { 129 t his.tags.put(key,Utils.intern(value.toString()));147 tagList.add(Utils.intern(value.toString())); 130 148 } 131 149 key = null; -
trunk/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java
r18185 r18473 19 19 */ 20 20 public class Geometry { 21 final Collection<Shape> shapes = new ArrayList<>();21 final Collection<Shape> shapes; 22 22 23 23 /** … … 29 29 public Geometry(GeometryTypes geometryType, List<CommandInteger> commands) { 30 30 if (geometryType == GeometryTypes.POINT) { 31 for (CommandInteger command : commands) { 32 final short[] operations = command.getOperations(); 33 // Each MoveTo command is a new point 34 if (command.getType() == Command.MoveTo && operations.length % 2 == 0 && operations.length > 0) { 35 for (int i = 0; i < operations.length / 2; i++) { 36 // Just using Ellipse2D since it extends Shape 37 shapes.add(new Ellipse2D.Float(operations[2 * i], operations[2 * i + 1], 0, 0)); 38 } 39 } else { 40 throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length)); 31 // This gets rid of most of the expensive array copies from ArrayList#grow 32 shapes = new ArrayList<>(commands.size()); 33 initializePoints(geometryType, commands); 34 } else if (geometryType == GeometryTypes.LINESTRING || geometryType == GeometryTypes.POLYGON) { 35 // This gets rid of most of the expensive array copies from ArrayList#grow 36 shapes = new ArrayList<>(1); 37 initializeWayGeometry(geometryType, commands); 38 } else { 39 shapes = Collections.emptyList(); 40 } 41 } 42 43 /** 44 * Initialize point geometry 45 * @param geometryType The geometry type (used for logging) 46 * @param commands The commands to use to create the geometry 47 */ 48 private void initializePoints(GeometryTypes geometryType, List<CommandInteger> commands) { 49 for (CommandInteger command : commands) { 50 final short[] operations = command.getOperations(); 51 // Each MoveTo command is a new point 52 if (command.getType() == Command.MoveTo && operations.length % 2 == 0 && operations.length > 0) { 53 for (int i = 0; i < operations.length / 2; i++) { 54 // Just using Ellipse2D since it extends Shape 55 shapes.add(new Ellipse2D.Float(operations[2 * i], operations[2 * i + 1], 0, 0)); 41 56 } 57 } else { 58 throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length)); 42 59 } 43 } else if (geometryType == GeometryTypes.LINESTRING || geometryType == GeometryTypes.POLYGON) { 44 Path2D.Float line = null; 45 Area area = null; 46 // MVT uses delta encoding. Each feature starts at (0, 0). 47 int x = 0; 48 int y = 0; 49 // Area is used to determine the inner/outer of a polygon 50 final int maxArraySize = commands.stream().filter(command -> command.getType() != Command.ClosePath) 51 .mapToInt(command -> command.getOperations().length).sum(); 52 final List<Integer> xArray = new ArrayList<>(maxArraySize); 53 final List<Integer> yArray = new ArrayList<>(maxArraySize); 54 for (CommandInteger command : commands) { 55 final short[] operations = command.getOperations(); 56 // Technically, there is no reason why there can be multiple MoveTo operations in one command, but that is undefined behavior 57 if (command.getType() == Command.MoveTo && operations.length == 2) { 58 x += operations[0]; 59 y += operations[1]; 60 line = new Path2D.Float(); 61 line.moveTo(x, y); 60 } 61 } 62 63 /** 64 * Initialize way geometry 65 * @param geometryType The geometry type 66 * @param commands The commands to use to create the geometry 67 */ 68 private void initializeWayGeometry(GeometryTypes geometryType, List<CommandInteger> commands) { 69 Path2D.Float line = null; 70 Area area = null; 71 // MVT uses delta encoding. Each feature starts at (0, 0). 72 int x = 0; 73 int y = 0; 74 // Area is used to determine the inner/outer of a polygon 75 final int maxArraySize = commands.stream().filter(command -> command.getType() != Command.ClosePath) 76 .mapToInt(command -> command.getOperations().length).sum(); 77 final List<Integer> xArray = new ArrayList<>(maxArraySize); 78 final List<Integer> yArray = new ArrayList<>(maxArraySize); 79 for (CommandInteger command : commands) { 80 final short[] operations = command.getOperations(); 81 // Technically, there is no reason why there can be multiple MoveTo operations in one command, but that is undefined behavior 82 if (command.getType() == Command.MoveTo && operations.length == 2) { 83 x += operations[0]; 84 y += operations[1]; 85 // Avoid fairly expensive Arrays.copyOf calls 86 line = new Path2D.Float(Path2D.WIND_NON_ZERO, commands.size()); 87 line.moveTo(x, y); 88 xArray.add(x); 89 yArray.add(y); 90 shapes.add(line); 91 } else if (command.getType() == Command.LineTo && operations.length % 2 == 0 && line != null) { 92 for (int i = 0; i < operations.length / 2; i++) { 93 x += operations[2 * i]; 94 y += operations[2 * i + 1]; 62 95 xArray.add(x); 63 96 yArray.add(y); 64 shapes.add(line); 65 } else if (command.getType() == Command.LineTo && operations.length % 2 == 0 && line != null) { 66 for (int i = 0; i < operations.length / 2; i++) { 67 x += operations[2 * i]; 68 y += operations[2 * i + 1]; 69 xArray.add(x); 70 yArray.add(y); 71 line.lineTo(x, y); 72 } 97 line.lineTo(x, y); 98 } 73 99 // ClosePath should only be used with Polygon geometry 74 75 76 77 78 79 80 100 } else if (geometryType == GeometryTypes.POLYGON && command.getType() == Command.ClosePath && line != null) { 101 shapes.remove(line); 102 // new Area() closes the line if it isn't already closed 103 if (area == null) { 104 area = new Area(); 105 shapes.add(area); 106 } 81 107 82 final double areaAreaSq = calculateSurveyorsArea(xArray.stream().mapToInt(i -> i).toArray(), 83 yArray.stream().mapToInt(i -> i).toArray()); 84 Area nArea = new Area(line); 85 // SonarLint thinks that this is never > 0. It can be. 86 if (areaAreaSq > 0) { 87 area.add(nArea); 88 } else if (areaAreaSq < 0) { 89 area.exclusiveOr(nArea); 90 } else { 91 throw new IllegalArgumentException(tr("{0} cannot have zero area", geometryType)); 92 } 93 xArray.clear(); 94 yArray.clear(); 108 final double areaAreaSq = calculateSurveyorsArea(xArray.stream().mapToInt(i -> i).toArray(), 109 yArray.stream().mapToInt(i -> i).toArray()); 110 Area nArea = new Area(line); 111 // SonarLint thinks that this is never > 0. It can be. 112 if (areaAreaSq > 0) { 113 area.add(nArea); 114 } else if (areaAreaSq < 0) { 115 area.exclusiveOr(nArea); 95 116 } else { 96 throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length));117 throw new IllegalArgumentException(tr("{0} cannot have zero area", geometryType)); 97 118 } 119 xArray.clear(); 120 yArray.clear(); 121 } else { 122 throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length)); 98 123 } 99 124 } -
trunk/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java
r17867 r18473 19 19 POLYGON; 20 20 21 private static final GeometryTypes[] CACHED_VALUES = values(); 21 22 /** 22 23 * Rings used by {@link GeometryTypes#POLYGON} … … 29 30 InteriorRing 30 31 } 32 33 /** 34 * A replacement for {@link #values()} which can be used when there are no changes to the underlying array. 35 * This is useful for avoiding unnecessary allocations. 36 * @return A cached array from {@link #values()}. Do not modify. 37 */ 38 static GeometryTypes[] getAllValues() { 39 return CACHED_VALUES; 40 } 31 41 } -
trunk/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
r17867 r18473 1 1 // License: GPL. For details, see LICENSE file. 2 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 4 5 6 import java.io.ByteArrayOutputStream; 5 7 import java.io.IOException; 6 8 import java.util.ArrayList; … … 8 10 import java.util.Collection; 9 11 import java.util.Collections; 10 import java.util.Hash Set;12 import java.util.HashMap; 11 13 import java.util.List; 12 14 import java.util.Map; … … 18 20 import org.openstreetmap.josm.data.protobuf.ProtobufRecord; 19 21 import org.openstreetmap.josm.tools.Destroyable; 20 import org.openstreetmap.josm.tools.Logging;21 22 22 23 /** … … 100 101 public Layer(Collection<ProtobufRecord> records) throws IOException { 101 102 // Do the unique required fields first 102 Map<Integer, List<ProtobufRecord>> sorted = records.stream().collect(Collectors.groupingBy(ProtobufRecord::getField)); 103 this.version = sorted.getOrDefault((int) VERSION_FIELD, Collections.emptyList()).parallelStream() 104 .map(ProtobufRecord::asUnsignedVarInt).map(Number::byteValue).findFirst().orElse(DEFAULT_VERSION); 105 // Per spec, we cannot continue past this until we have checked the version number 106 if (this.version != 1 && this.version != 2) { 107 throw new IllegalArgumentException(tr("We do not understand version {0} of the vector tile specification", this.version)); 108 } 109 this.name = sorted.getOrDefault((int) NAME_FIELD, Collections.emptyList()).parallelStream().map(ProtobufRecord::asString).findFirst() 110 .orElseThrow(() -> new IllegalArgumentException(tr("Vector tile layers must have a layer name"))); 111 this.extent = sorted.getOrDefault((int) EXTENT_FIELD, Collections.emptyList()).parallelStream().map(ProtobufRecord::asUnsignedVarInt) 112 .map(Number::intValue).findAny().orElse(DEFAULT_EXTENT); 113 114 sorted.getOrDefault((int) KEY_FIELD, Collections.emptyList()).parallelStream().map(ProtobufRecord::asString) 115 .forEachOrdered(this.keyList::add); 116 sorted.getOrDefault((int) VALUE_FIELD, Collections.emptyList()).parallelStream().map(ProtobufRecord::getBytes) 117 .map(ProtobufParser::new).map(parser1 -> { 118 try { 119 return new ProtobufRecord(parser1); 120 } catch (IOException e) { 121 Logging.error(e); 122 return null; 123 } 124 }) 125 .filter(Objects::nonNull) 126 .map(value -> ValueFields.MAPPERS.parallelStream() 127 .filter(v -> v.getField() == value.getField()) 128 .map(v -> v.convertValue(value)).findFirst() 129 .orElseThrow(() -> new IllegalArgumentException(tr("Unknown field in vector tile layer value ({0})", value.getField())))) 130 .forEachOrdered(this.valueList::add); 131 Collection<IOException> exceptions = new HashSet<>(0); 132 this.featureCollection = sorted.getOrDefault((int) FEATURE_FIELD, Collections.emptyList()).parallelStream().map(feature -> { 133 try { 134 return new Feature(this, feature); 135 } catch (IOException e) { 136 exceptions.add(e); 103 Map<Integer, List<ProtobufRecord>> sorted = new HashMap<>(records.size()); 104 byte tVersion = DEFAULT_VERSION; 105 String tName = null; 106 int tExtent = DEFAULT_EXTENT; 107 final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(4); 108 for (ProtobufRecord protobufRecord : records) { 109 if (protobufRecord.getField() == VERSION_FIELD) { 110 tVersion = protobufRecord.asUnsignedVarInt().byteValue(); 111 // Per spec, we cannot continue past this until we have checked the version number 112 if (tVersion != 1 && tVersion != 2) { 113 throw new IllegalArgumentException(tr("We do not understand version {0} of the vector tile specification", tVersion)); 114 } 115 } else if (protobufRecord.getField() == NAME_FIELD) { 116 tName = protobufRecord.asString(); 117 } else if (protobufRecord.getField() == EXTENT_FIELD) { 118 tExtent = protobufRecord.asUnsignedVarInt().intValue(); 119 } else if (protobufRecord.getField() == KEY_FIELD) { 120 this.keyList.add(protobufRecord.asString()); 121 } else if (protobufRecord.getField() == VALUE_FIELD) { 122 parseValueRecord(byteArrayOutputStream, protobufRecord); 123 } else { 124 sorted.computeIfAbsent(protobufRecord.getField(), i -> new ArrayList<>(records.size())).add(protobufRecord); 137 125 } 138 return null; 139 }).collect(Collectors.toList()); 140 if (!exceptions.isEmpty()) { 141 throw exceptions.iterator().next(); 126 } 127 this.version = tVersion; 128 if (tName == null) { 129 throw new IllegalArgumentException(tr("Vector tile layers must have a layer name")); 130 } 131 this.name = tName; 132 this.extent = tExtent; 133 134 this.featureCollection = new ArrayList<>(sorted.getOrDefault((int) FEATURE_FIELD, Collections.emptyList()).size()); 135 for (ProtobufRecord protobufRecord : sorted.getOrDefault((int) FEATURE_FIELD, Collections.emptyList())) { 136 this.featureCollection.add(new Feature(this, protobufRecord)); 142 137 } 143 138 // Cleanup bytes (for memory) 144 for (ProtobufRecord record : records) { 145 record.close(); 139 for (ProtobufRecord protobufRecord : records) { 140 protobufRecord.close(); 141 } 142 } 143 144 private void parseValueRecord(ByteArrayOutputStream byteArrayOutputStream, ProtobufRecord protobufRecord) 145 throws IOException { 146 try (ProtobufParser parser = new ProtobufParser(protobufRecord.getBytes())) { 147 ProtobufRecord protobufRecord2 = new ProtobufRecord(byteArrayOutputStream, parser); 148 int field = protobufRecord2.getField(); 149 int valueListSize = this.valueList.size(); 150 for (Layer.ValueFields<?> mapper : ValueFields.MAPPERS) { 151 if (mapper.getField() == field) { 152 this.valueList.add(mapper.convertValue(protobufRecord2)); 153 break; 154 } 155 } 156 if (valueListSize == this.valueList.size()) { 157 throw new IllegalArgumentException(tr("Unknown field in vector tile layer value ({0})", field)); 158 } 146 159 } 147 160 } -
trunk/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java
r18156 r18473 9 9 import java.util.List; 10 10 import java.util.Objects; 11 import java.util.stream.Collectors;12 11 13 12 import org.openstreetmap.gui.jmapviewer.Tile; … … 54 53 ProtobufParser parser = new ProtobufParser(inputStream); 55 54 Collection<ProtobufRecord> protobufRecords = parser.allRecords(); 56 this.layers = new HashSet<>(); 57 this.layers = protobufRecords.stream().map(protoBufRecord -> { 58 Layer mvtLayer = null; 55 this.layers = new HashSet<>(protobufRecords.size()); 56 for (ProtobufRecord protoBufRecord : protobufRecords) { 59 57 if (protoBufRecord.getField() == Layer.LAYER_FIELD) { 60 58 try (ProtobufParser tParser = new ProtobufParser(protoBufRecord.getBytes())) { 61 mvtLayer = new Layer(tParser.allRecords());59 this.layers.add(new Layer(tParser.allRecords())); 62 60 } catch (IOException e) { 63 61 Logging.error(e); … … 67 65 } 68 66 } 69 return mvtLayer; 70 }).collect(Collectors.toCollection(HashSet::new)); 67 } 68 this.layers = new HashSet<>(this.layers); 69 71 70 this.extent = layers.stream().filter(Objects::nonNull).mapToInt(Layer::getExtent).max().orElse(Layer.DEFAULT_EXTENT); 72 71 if (this.getData() != null) { -
trunk/src/org/openstreetmap/josm/data/osm/AbstractPrimitive.java
r18208 r18473 6 6 import java.text.MessageFormat; 7 7 import java.time.Instant; 8 import java.util.ArrayList; 8 9 import java.util.Arrays; 9 10 import java.util.Collection; … … 610 611 keysChangedImpl(originalKeys); 611 612 } 613 } 614 615 @Override 616 public void putAll(Map<String, String> tags) { 617 if (tags == null || tags.isEmpty()) { 618 return; 619 } 620 // Defensive copy of keys 621 String[] newKeys = keys; 622 Map<String, String> originalKeys = getKeys(); 623 List<Map.Entry<String, String>> tagsToAdd = new ArrayList<>(tags.size()); 624 for (Map.Entry<String, String> tag : tags.entrySet()) { 625 if (!Utils.isBlank(tag.getKey())) { 626 int keyIndex = indexOfKey(newKeys, tag.getKey()); 627 // Realistically, we will not hit the newKeys == null branch. If it is null, keyIndex is always < 1 628 if (keyIndex < 0 || newKeys == null) { 629 tagsToAdd.add(tag); 630 } else { 631 newKeys[keyIndex + 1] = tag.getValue(); 632 } 633 } 634 } 635 if (!tagsToAdd.isEmpty()) { 636 int index = newKeys != null ? newKeys.length : 0; 637 newKeys = newKeys != null ? Arrays.copyOf(newKeys, newKeys.length + 2 * tagsToAdd.size()) : new String[2 * tagsToAdd.size()]; 638 for (Map.Entry<String, String> tag : tagsToAdd) { 639 newKeys[index++] = tag.getKey(); 640 newKeys[index++] = tag.getValue(); 641 } 642 keys = newKeys; 643 } 644 keysChangedImpl(originalKeys); 612 645 } 613 646 -
trunk/src/org/openstreetmap/josm/data/osm/Tagged.java
r18211 r18473 70 70 71 71 /** 72 * Add all key/value pairs. This <i>may</i> be more performant than {@link #put}, depending upon the implementation. 73 * By default, this calls {@link #put} for each map entry. 74 * @param tags The tag map to add 75 * @since xxx 76 */ 77 default void putAll(Map<String, String> tags) { 78 for (Map.Entry<String, String> entry : tags.entrySet()) { 79 put(entry.getKey(), entry.getValue()); 80 } 81 } 82 83 /** 72 84 * Replies the value of the given key; null, if there is no value for this key 73 85 * -
trunk/src/org/openstreetmap/josm/data/protobuf/ProtobufPacked.java
r18431 r18473 13 13 */ 14 14 public class ProtobufPacked { 15 private static final Number[] NO_NUMBERS = new Number[0]; 15 16 private final byte[] bytes; 16 17 private final Number[] numbers; … … 20 21 * Create a new ProtobufPacked object 21 22 * 23 * @param byteArrayOutputStream A reusable ByteArrayOutputStream (helps to reduce memory allocations) 22 24 * @param bytes The packed bytes 23 25 */ 24 public ProtobufPacked( byte[] bytes) {26 public ProtobufPacked(ByteArrayOutputStream byteArrayOutputStream, byte[] bytes) { 25 27 this.location = 0; 26 28 this.bytes = bytes; 27 List<Number> numbersT = new ArrayList<>(); 29 30 // By creating a list of size bytes.length, we avoid 36 MB of allocations from list growth. This initialization 31 // only adds 3.7 MB to the ArrayList#init calls. Note that the real-world test case (Mapillary vector tiles) 32 // primarily created Shorts. 33 List<Number> numbersT = new ArrayList<>(bytes.length); 28 34 // By reusing a ByteArrayOutputStream, we can reduce allocations in nextVarInt from 230 MB to 74 MB. 29 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(4);30 35 while (this.location < bytes.length) { 31 36 numbersT.add(ProtobufParser.convertByteArray(this.nextVarInt(byteArrayOutputStream), ProtobufParser.VAR_INT_BYTE_SIZE)); 32 byteArrayOutputStream.reset();33 37 } 34 38 35 this.numbers = new Number[numbersT.size()]; 36 for (int i = 0; i < numbersT.size(); i++) { 37 this.numbers[i] = numbersT.get(i); 38 } 39 this.numbers = numbersT.toArray(NO_NUMBERS); 39 40 } 40 41 … … 51 52 // In a real world test, the largest List<Byte> seen had 3 elements. Use 4 to avoid most new array allocations. 52 53 // Memory allocations went from 368 MB to 280 MB by using an initial array allocation. When using a 53 // ByteArrayOutputStream, it went down to 230 MB. 54 // ByteArrayOutputStream, it went down to 230 MB. By further reusing the ByteArrayOutputStream between method 55 // calls, it went down further to 73 MB. 54 56 while ((this.bytes[this.location] & ProtobufParser.MOST_SIGNIFICANT_BYTE) 55 57 == ProtobufParser.MOST_SIGNIFICANT_BYTE) { … … 59 61 // The last byte doesn't drop the most significant bit 60 62 byteArrayOutputStream.write(this.bytes[this.location++]); 61 return byteArrayOutputStream.toByteArray(); 63 try { 64 return byteArrayOutputStream.toByteArray(); 65 } finally { 66 byteArrayOutputStream.reset(); 67 } 62 68 } 63 69 } -
trunk/src/org/openstreetmap/josm/data/protobuf/ProtobufParser.java
r18431 r18473 122 122 public Collection<ProtobufRecord> allRecords() throws IOException { 123 123 Collection<ProtobufRecord> records = new ArrayList<>(); 124 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(4); 124 125 while (this.hasNext()) { 125 records.add(new ProtobufRecord( this));126 records.add(new ProtobufRecord(byteArrayOutputStream, this)); 126 127 } 127 128 return records; … … 197 198 * Get the next delimited message ({@link WireType#LENGTH_DELIMITED}) 198 199 * 200 * @param byteArrayOutputStream A reusable stream to write bytes to. This can significantly reduce the allocations 201 * (150 MB to 95 MB in a test area). 199 202 * @return The next length delimited message 200 203 * @throws IOException - if an IO error occurs 201 204 */ 202 public byte[] nextLengthDelimited( ) throws IOException {203 int length = convertByteArray(this.nextVarInt( ), VAR_INT_BYTE_SIZE).intValue();205 public byte[] nextLengthDelimited(ByteArrayOutputStream byteArrayOutputStream) throws IOException { 206 int length = convertByteArray(this.nextVarInt(byteArrayOutputStream), VAR_INT_BYTE_SIZE).intValue(); 204 207 return readNextBytes(length); 205 208 } … … 208 211 * Get the next var int ({@code WireType#VARINT}) 209 212 * 213 * @param byteArrayOutputStream A reusable stream to write bytes to. This can significantly reduce the allocations 214 * (150 MB to 95 MB in a test area). 210 215 * @return The next var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum}) 211 216 * @throws IOException - if an IO error occurs 212 217 */ 213 public byte[] nextVarInt( ) throws IOException {218 public byte[] nextVarInt(ByteArrayOutputStream byteArrayOutputStream) throws IOException { 214 219 // Using this reduces the allocations from 150 MB to 95 MB. 215 final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(4);216 220 int currentByte = this.nextByte(); 217 221 while ((byte) (currentByte & MOST_SIGNIFICANT_BYTE) == MOST_SIGNIFICANT_BYTE && currentByte > 0) { … … 222 226 // The last byte doesn't drop the most significant bit 223 227 byteArrayOutputStream.write(currentByte); 224 return byteArrayOutputStream.toByteArray(); 228 try { 229 return byteArrayOutputStream.toByteArray(); 230 } finally { 231 byteArrayOutputStream.reset(); 232 } 225 233 } 226 234 -
trunk/src/org/openstreetmap/josm/data/protobuf/ProtobufRecord.java
r18432 r18473 2 2 package org.openstreetmap.josm.data.protobuf; 3 3 4 import java.io.ByteArrayOutputStream; 4 5 import java.io.IOException; 5 6 import java.nio.charset.StandardCharsets; … … 22 23 * Create a new Protobuf record 23 24 * 25 * @param byteArrayOutputStream A reusable ByteArrayOutputStream to avoid unnecessary allocations 24 26 * @param parser The parser to use to create the record 25 27 * @throws IOException - if an IO error occurs 26 28 */ 27 public ProtobufRecord( ProtobufParser parser) throws IOException {28 Number number = ProtobufParser.convertByteArray(parser.nextVarInt( ), ProtobufParser.VAR_INT_BYTE_SIZE);29 public ProtobufRecord(ByteArrayOutputStream byteArrayOutputStream, ProtobufParser parser) throws IOException { 30 Number number = ProtobufParser.convertByteArray(parser.nextVarInt(byteArrayOutputStream), ProtobufParser.VAR_INT_BYTE_SIZE); 29 31 // I don't foresee having field numbers > {@code Integer#MAX_VALUE >> 3} 30 32 this.field = (int) number.longValue() >> 3; … … 43 45 44 46 if (this.type == WireType.VARINT) { 45 this.bytes = parser.nextVarInt( );47 this.bytes = parser.nextVarInt(byteArrayOutputStream); 46 48 } else if (this.type == WireType.SIXTY_FOUR_BIT) { 47 49 this.bytes = parser.nextFixed64(); … … 49 51 this.bytes = parser.nextFixed32(); 50 52 } else if (this.type == WireType.LENGTH_DELIMITED) { 51 this.bytes = parser.nextLengthDelimited( );53 this.bytes = parser.nextLengthDelimited(byteArrayOutputStream); 52 54 } else { 53 55 this.bytes = EMPTY_BYTES; -
trunk/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
r17994 r18473 13 13 import java.util.Collection; 14 14 import java.util.Collections; 15 import java.util.HashMap; 15 16 import java.util.List; 17 import java.util.Map; 16 18 import java.util.Objects; 17 19 18 import org.openstreetmap.gui.jmapviewer.Coordinate;19 20 import org.openstreetmap.gui.jmapviewer.Tile; 20 21 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 21 22 import org.openstreetmap.josm.data.IQuadBucketType; 23 import org.openstreetmap.josm.data.coor.ILatLon; 24 import org.openstreetmap.josm.data.coor.LatLon; 22 25 import org.openstreetmap.josm.data.imagery.vectortile.VectorTile; 23 26 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Feature; … … 36 39 import org.openstreetmap.josm.tools.JosmRuntimeException; 37 40 import org.openstreetmap.josm.tools.Logging; 41 import org.openstreetmap.josm.tools.Utils; 38 42 39 43 /** … … 157 161 158 162 private synchronized <T extends Tile & VectorTile> VectorNode pointToNode(T tile, Layer layer, 159 Collection<VectorPrimitive> featureObjects, int x, int y ) {163 Collection<VectorPrimitive> featureObjects, int x, int y, final Map<ILatLon, VectorNode> nodeMap) { 160 164 final BBox tileBbox; 161 165 if (tile instanceof IQuadBucketType) { … … 169 173 } 170 174 final int layerExtent = layer.getExtent(); 171 final ICoordinate coords = new Coordinate(175 final LatLon coords = new LatLon( 172 176 tileBbox.getMaxLat() - (tileBbox.getMaxLat() - tileBbox.getMinLat()) * y / layerExtent, 173 177 tileBbox.getMinLon() + (tileBbox.getMaxLon() - tileBbox.getMinLon()) * x / layerExtent 174 178 ); 179 if (nodeMap.containsKey(coords)) { 180 return nodeMap.get(coords); 181 } 175 182 final Collection<VectorNode> nodes = this.store 176 .searchNodes(new BBox(coords. getLon(), coords.getLat(), VectorDataSet.DUPE_NODE_DISTANCE));183 .searchNodes(new BBox(coords.lon(), coords.lat(), VectorDataSet.DUPE_NODE_DISTANCE)); 177 184 final VectorNode node; 178 185 if (!nodes.isEmpty()) { … … 203 210 node.setCoor(coords); 204 211 featureObjects.add(node); 212 nodeMap.put(node.getCoor(), node); 205 213 return node; 206 214 } 207 215 208 216 private <T extends Tile & VectorTile> List<VectorWay> pathToWay(T tile, Layer layer, 209 Collection<VectorPrimitive> featureObjects, Path2D shape ) {217 Collection<VectorPrimitive> featureObjects, Path2D shape, Map<ILatLon, VectorNode> nodeMap) { 210 218 final PathIterator pathIterator = shape.getPathIterator(null); 211 final List<VectorWay> ways = pathIteratorToObjects(tile, layer, featureObjects, pathIterator).stream()212 .filter(VectorWay.class::isInstance).map(VectorWay.class::cast).collect(toList());219 final List<VectorWay> ways = new ArrayList<>( 220 Utils.filteredCollection(pathIteratorToObjects(tile, layer, featureObjects, pathIterator, nodeMap), VectorWay.class)); 213 221 // These nodes technically do not exist, so we shouldn't show them 214 ways.stream().flatMap(way -> way.getNodes().stream()) 215 .filter(prim -> !prim.isTagged() && prim.getReferrers(true).size() == 1 && prim.getId() <= 0) 216 .forEach(prim -> { 217 prim.setDisabled(true); 218 prim.setVisible(false); 219 }); 222 for (VectorWay way : ways) { 223 for (VectorNode node : way.getNodes()) { 224 if (!node.hasKeys() && node.getReferrers(true).size() == 1 && node.getId() <= 0) { 225 node.setDisabled(true); 226 node.setVisible(false); 227 } 228 } 229 } 220 230 return ways; 221 231 } 222 232 223 233 private <T extends Tile & VectorTile> List<VectorPrimitive> pathIteratorToObjects(T tile, Layer layer, 224 Collection<VectorPrimitive> featureObjects, PathIterator pathIterator ) {234 Collection<VectorPrimitive> featureObjects, PathIterator pathIterator, Map<ILatLon, VectorNode> nodeMap) { 225 235 final List<VectorNode> nodes = new ArrayList<>(); 226 236 final double[] coords = new double[6]; … … 243 253 } 244 254 if (PathIterator.SEG_MOVETO == type || PathIterator.SEG_LINETO == type) { 245 final VectorNode node = pointToNode(tile, layer, featureObjects, (int) coords[0], (int) coords[1] );255 final VectorNode node = pointToNode(tile, layer, featureObjects, (int) coords[0], (int) coords[1], nodeMap); 246 256 nodes.add(node); 247 257 } else if (PathIterator.SEG_CLOSE != type) { … … 260 270 261 271 private <T extends Tile & VectorTile> VectorRelation areaToRelation(T tile, Layer layer, 262 Collection<VectorPrimitive> featureObjects, Area area ) {272 Collection<VectorPrimitive> featureObjects, Area area, Map<ILatLon, VectorNode> nodeMap) { 263 273 VectorRelation vectorRelation = new VectorRelation(layer.getName()); 264 for (VectorPrimitive member : pathIteratorToObjects(tile, layer, featureObjects, area.getPathIterator(null) )) {274 for (VectorPrimitive member : pathIteratorToObjects(tile, layer, featureObjects, area.getPathIterator(null), nodeMap)) { 265 275 final String role; 266 276 if (member instanceof VectorWay && ((VectorWay) member).isClosed()) { … … 280 290 */ 281 291 public <T extends Tile & VectorTile> void addDataTile(T tile) { 292 // Using a map reduces the cost of addFeatureData from 2,715,158,632 bytes to 235,042,184 bytes (-91.3%) 293 // This was somewhat variant, with some runs being closer to ~560 MB (still -80%). 294 final Map<ILatLon, VectorNode> nodeMap = new HashMap<>(); 282 295 for (Layer layer : tile.getLayers()) { 283 296 for (Feature feature : layer.getFeatures()) { 284 297 try { 285 addFeatureData(tile, layer, feature );298 addFeatureData(tile, layer, feature, nodeMap); 286 299 } catch (IllegalArgumentException e) { 287 300 Logging.error("Cannot add vector data for feature {0} of tile {1}: {2}", feature, tile, e.getMessage()); … … 299 312 } 300 313 301 private <T extends Tile & VectorTile> void addFeatureData(T tile, Layer layer, Feature feature) { 302 List<VectorPrimitive> featureObjects = new ArrayList<>(); 303 List<VectorPrimitive> primaryFeatureObjects = feature.getGeometryObject().getShapes().stream() 304 .map(shape -> shapeToPrimaryFeatureObject(tile, layer, shape, featureObjects)).collect(toList()); 314 private <T extends Tile & VectorTile> void addFeatureData(T tile, Layer layer, Feature feature, Map<ILatLon, VectorNode> nodeMap) { 315 // This will typically be larger than primaryFeatureObjects, but this at least avoids quite a few ArrayList#grow calls 316 List<VectorPrimitive> featureObjects = new ArrayList<>(feature.getGeometryObject().getShapes().size()); 317 List<VectorPrimitive> primaryFeatureObjects = new ArrayList<>(featureObjects.size()); 318 for (Shape shape : feature.getGeometryObject().getShapes()) { 319 primaryFeatureObjects.add(shapeToPrimaryFeatureObject(tile, layer, shape, featureObjects, nodeMap)); 320 } 305 321 final VectorPrimitive primitive; 306 322 if (primaryFeatureObjects.size() == 1) { … … 311 327 } else if (!primaryFeatureObjects.isEmpty()) { 312 328 VectorRelation relation = new VectorRelation(layer.getName()); 313 primaryFeatureObjects.stream().map(prim -> new VectorRelationMember("", prim)) 314 .forEach(relation::addRelationMember); 329 List<VectorRelationMember> members = new ArrayList<>(primaryFeatureObjects.size()); 330 for (VectorPrimitive prim : primaryFeatureObjects) { 331 members.add(new VectorRelationMember("", prim)); 332 } 333 relation.setMembers(members); 315 334 primitive = relation; 316 335 } else { … … 326 345 } 327 346 if (feature.getTags() != null) { 328 feature.getTags().forEach(primitive::put); 329 } 330 featureObjects.forEach(this::addPrimitive); 331 primaryFeatureObjects.forEach(this::addPrimitive); 347 primitive.putAll(feature.getTags()); 348 } 349 for (VectorPrimitive prim : featureObjects) { 350 this.addPrimitive(prim); 351 } 352 for (VectorPrimitive prim : primaryFeatureObjects) { 353 this.addPrimitive(prim); 354 } 332 355 try { 333 356 this.addPrimitive(primitive); … … 339 362 340 363 private <T extends Tile & VectorTile> VectorPrimitive shapeToPrimaryFeatureObject( 341 T tile, Layer layer, Shape shape, List<VectorPrimitive> featureObjects ) {364 T tile, Layer layer, Shape shape, List<VectorPrimitive> featureObjects, Map<ILatLon, VectorNode> nodeMap) { 342 365 final VectorPrimitive primitive; 343 366 if (shape instanceof Ellipse2D) { 344 367 primitive = pointToNode(tile, layer, featureObjects, 345 (int) ((Ellipse2D) shape).getCenterX(), (int) ((Ellipse2D) shape).getCenterY() );368 (int) ((Ellipse2D) shape).getCenterX(), (int) ((Ellipse2D) shape).getCenterY(), nodeMap); 346 369 } else if (shape instanceof Path2D) { 347 primitive = pathToWay(tile, layer, featureObjects, (Path2D) shape ).stream().findFirst().orElse(null);370 primitive = pathToWay(tile, layer, featureObjects, (Path2D) shape, nodeMap).stream().findFirst().orElse(null); 348 371 } else if (shape instanceof Area) { 349 primitive = areaToRelation(tile, layer, featureObjects, (Area) shape );372 primitive = areaToRelation(tile, layer, featureObjects, (Area) shape, nodeMap); 350 373 primitive.put(RELATION_TYPE, MULTIPOLYGON_TYPE); 351 374 } else { -
trunk/src/org/openstreetmap/josm/data/vector/VectorPrimitive.java
r17867 r18473 3 3 4 4 import java.util.Arrays; 5 import java.util.Collections; 5 6 import java.util.List; 6 7 import java.util.Map; … … 118 119 @Override 119 120 public final List<VectorPrimitive> getReferrers(boolean allowWithoutDataset) { 121 if (this.referrers == null) { 122 return Collections.emptyList(); 123 } else if (this.referrers instanceof VectorPrimitive) { 124 return Collections.singletonList((VectorPrimitive) this.referrers); 125 } 120 126 return referrers(allowWithoutDataset, VectorPrimitive.class) 121 127 .collect(Collectors.toList()); -
trunk/src/org/openstreetmap/josm/data/vector/VectorRelation.java
r17867 r18473 91 91 this.members.clear(); 92 92 this.members.addAll(members); 93 for (VectorRelationMember member : members) { 94 member.getMember().addReferrer(this); 95 } 96 cachedBBox = null; 93 97 } 94 98
Note:
See TracChangeset
for help on using the changeset viewer.