From 05a7bfe9b3313f4c5d3a2a6f2c77922c97efc171 Mon Sep 17 00:00:00 2001 From: Dzmitry Fomchyn <5161509+DzmitryFomchyn@users.noreply.github.com> Date: Mon, 8 Jul 2024 12:53:42 +0200 Subject: [PATCH] Optimize Memory Usage by Interning Frequently Occurring Strings in JSON (#1585) * Intern IntersectionLanes's indication strings to save memory. * Optimize StepManeuver json parsing --- .../v5/models/IntersectionLanes.java | 2 +- .../models/IntersectionLanesTypeAdapter.java | 91 ++++++++++++++++ .../directions/v5/models/StepManeuver.java | 2 +- .../v5/models/StepManeuverTypeAdapter.java | 102 ++++++++++++++++++ .../api/directions/v5/utils/ParseUtils.java | 17 +++ .../v5/models/IntersectionLanesTest.java | 43 ++++++++ .../v5/models/StepManeuverTest.java | 39 +++++++ 7 files changed, 294 insertions(+), 2 deletions(-) create mode 100644 services-directions-models/src/main/java/com/mapbox/api/directions/v5/models/IntersectionLanesTypeAdapter.java create mode 100644 services-directions-models/src/main/java/com/mapbox/api/directions/v5/models/StepManeuverTypeAdapter.java diff --git a/services-directions-models/src/main/java/com/mapbox/api/directions/v5/models/IntersectionLanes.java b/services-directions-models/src/main/java/com/mapbox/api/directions/v5/models/IntersectionLanes.java index c6b25d192..f61588962 100644 --- a/services-directions-models/src/main/java/com/mapbox/api/directions/v5/models/IntersectionLanes.java +++ b/services-directions-models/src/main/java/com/mapbox/api/directions/v5/models/IntersectionLanes.java @@ -109,7 +109,7 @@ public static Builder builder() { * @since 3.0.0 */ public static TypeAdapter typeAdapter(Gson gson) { - return new AutoValue_IntersectionLanes.GsonTypeAdapter(gson); + return new IntersectionLanesTypeAdapter(new AutoValue_IntersectionLanes.GsonTypeAdapter(gson)); } /** diff --git a/services-directions-models/src/main/java/com/mapbox/api/directions/v5/models/IntersectionLanesTypeAdapter.java b/services-directions-models/src/main/java/com/mapbox/api/directions/v5/models/IntersectionLanesTypeAdapter.java new file mode 100644 index 000000000..5a425a533 --- /dev/null +++ b/services-directions-models/src/main/java/com/mapbox/api/directions/v5/models/IntersectionLanesTypeAdapter.java @@ -0,0 +1,91 @@ +package com.mapbox.api.directions.v5.models; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import com.mapbox.api.directions.v5.utils.ParseUtils; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A TypeAdapter for {@link IntersectionLanes} used to optimize JSON deserialization. + * + *

Strings in {@link IntersectionLanes#indications()} and + * {@link IntersectionLanes#validIndication()} can accept a limited set of possible values + * (e.g., "straight," "right," "left," etc.). + * This adapter invokes {@link String#intern()} on these strings to save memory.

+ */ +class IntersectionLanesTypeAdapter extends TypeAdapter { + + private final TypeAdapter defaultAdapter; + + IntersectionLanesTypeAdapter(TypeAdapter defaultAdapter) { + this.defaultAdapter = defaultAdapter; + } + + @Override + public void write(JsonWriter out, IntersectionLanes value) throws IOException { + defaultAdapter.write(out, value); + } + + @Override + public IntersectionLanes read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + final JsonObject jsonObject = JsonParser.parseReader(in).getAsJsonObject(); + + final IntersectionLanes.Builder builder = IntersectionLanes.builder(); + Map unrecognized = null; + + for (Map.Entry entry : jsonObject.entrySet()) { + final String key = entry.getKey(); + final JsonElement value = entry.getValue(); + + if (value.isJsonNull()) { + continue; + } + + switch (key) { + case "valid": + builder.valid(value.getAsBoolean()); + break; + + case "active": + builder.active(value.getAsBoolean()); + break; + + case "valid_indication": + builder.validIndication(value.getAsString().intern()); + break; + + case "indications": + builder.indications(ParseUtils.parseAndInternJsonStringArray(value.getAsJsonArray())); + break; + + case "payment_methods": + builder.paymentMethods(ParseUtils.parseAndInternJsonStringArray(value.getAsJsonArray())); + break; + + default: + if (unrecognized == null) { + unrecognized = new LinkedHashMap<>(); + } + unrecognized.put(key, value); + break; + } + } + + return builder + .unrecognizedJsonProperties(unrecognized) + .build(); + } +} diff --git a/services-directions-models/src/main/java/com/mapbox/api/directions/v5/models/StepManeuver.java b/services-directions-models/src/main/java/com/mapbox/api/directions/v5/models/StepManeuver.java index 15e9aa3d1..fbdf523d5 100644 --- a/services-directions-models/src/main/java/com/mapbox/api/directions/v5/models/StepManeuver.java +++ b/services-directions-models/src/main/java/com/mapbox/api/directions/v5/models/StepManeuver.java @@ -301,7 +301,7 @@ public Point location() { * @since 3.0.0 */ public static TypeAdapter typeAdapter(Gson gson) { - return new AutoValue_StepManeuver.GsonTypeAdapter(gson); + return new StepManeuverTypeAdapter(new AutoValue_StepManeuver.GsonTypeAdapter(gson)); } /** diff --git a/services-directions-models/src/main/java/com/mapbox/api/directions/v5/models/StepManeuverTypeAdapter.java b/services-directions-models/src/main/java/com/mapbox/api/directions/v5/models/StepManeuverTypeAdapter.java new file mode 100644 index 000000000..14140301c --- /dev/null +++ b/services-directions-models/src/main/java/com/mapbox/api/directions/v5/models/StepManeuverTypeAdapter.java @@ -0,0 +1,102 @@ +package com.mapbox.api.directions.v5.models; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A TypeAdapter for {@link StepManeuver} used to optimize JSON deserialization. + * + *

Strings {@link StepManeuver#type()} and {@link StepManeuver#modifier()} can accept + * a limited set of possible values. + * This adapter invokes {@link String#intern()} on these strings to save memory.

+ */ +class StepManeuverTypeAdapter extends TypeAdapter { + private final TypeAdapter defaultAdapter; + + StepManeuverTypeAdapter(TypeAdapter defaultAdapter) { + this.defaultAdapter = defaultAdapter; + } + + @Override + public void write(JsonWriter out, StepManeuver value) throws IOException { + defaultAdapter.write(out, value); + } + + @Override + public StepManeuver read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + final JsonObject jsonObject = JsonParser.parseReader(in).getAsJsonObject(); + + final StepManeuver.Builder builder = StepManeuver.builder(); + Map unrecognized = null; + + for (Map.Entry entry : jsonObject.entrySet()) { + final String key = entry.getKey(); + final JsonElement value = entry.getValue(); + + if (value.isJsonNull()) { + continue; + } + + switch (key) { + case "location": + final JsonArray jsonArray = value.getAsJsonArray(); + final double[] locations = new double[jsonArray.size()]; + for (int i = 0; i < jsonArray.size(); ++i) { + locations[i] = jsonArray.get(i).getAsDouble(); + } + builder.rawLocation(locations); + break; + + case "bearing_before": + builder.bearingBefore(value.getAsDouble()); + break; + + case "bearing_after": + builder.bearingAfter(value.getAsDouble()); + break; + + case "instruction": + builder.instruction(value.getAsString()); + break; + + case "type": + builder.type(value.getAsString().intern()); + break; + + case "modifier": + builder.modifier(value.getAsString().intern()); + break; + + case "exit": + builder.exit(value.getAsInt()); + break; + + default: + if (unrecognized == null) { + unrecognized = new LinkedHashMap<>(); + } + unrecognized.put(key, value); + break; + } + } + + return builder + .unrecognizedJsonProperties(unrecognized) + .build(); + } +} diff --git a/services-directions-models/src/main/java/com/mapbox/api/directions/v5/utils/ParseUtils.java b/services-directions-models/src/main/java/com/mapbox/api/directions/v5/utils/ParseUtils.java index 7c25ea6aa..7eb4e7cf8 100644 --- a/services-directions-models/src/main/java/com/mapbox/api/directions/v5/utils/ParseUtils.java +++ b/services-directions-models/src/main/java/com/mapbox/api/directions/v5/utils/ParseUtils.java @@ -2,6 +2,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; + +import com.google.gson.JsonArray; import com.mapbox.api.directions.v5.models.Bearing; import com.mapbox.geojson.Point; import java.util.ArrayList; @@ -213,4 +215,19 @@ public static List parseBearings(@Nullable String original) { public static List parseToBooleans(@Nullable String original) { return parseToList(SEMICOLON, original, BOOLEAN_PARSER); } + + /** + * Parses strings from json array and invokes {@link String#intern()} on each item. + * + * @param jsonArray json array with string items + * @return List of interned parsed strings + */ + @NonNull + public static List parseAndInternJsonStringArray(JsonArray jsonArray) { + final List result = new ArrayList<>(jsonArray.size()); + for (int i = 0; i < jsonArray.size(); ++i) { + result.add(jsonArray.get(i).getAsString().intern()); + } + return result; + } } diff --git a/services-directions-models/src/test/java/com/mapbox/api/directions/v5/models/IntersectionLanesTest.java b/services-directions-models/src/test/java/com/mapbox/api/directions/v5/models/IntersectionLanesTest.java index c6df601ce..d59e85994 100644 --- a/services-directions-models/src/test/java/com/mapbox/api/directions/v5/models/IntersectionLanesTest.java +++ b/services-directions-models/src/test/java/com/mapbox/api/directions/v5/models/IntersectionLanesTest.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; import com.mapbox.api.directions.v5.DirectionsCriteria; import com.mapbox.core.TestUtils; @@ -102,4 +103,46 @@ public void testFromJson_etc2Payment() { assertEquals(intersectionLanes, intersectionLanesFromJson); } + + @Test + public void testIndicationsAreInterned() { + IntersectionLanes intersectionLanes = IntersectionLanes.builder() + .validIndication("straight") + .indications(Arrays.asList("straight","straight")) + .build(); + + IntersectionLanes deserialized = IntersectionLanes.fromJson( + intersectionLanes.toJson() + ); + + List indications = deserialized.indications(); + assertEquals(Arrays.asList("straight","straight"), indications); + assertEquals("straight", deserialized.validIndication()); + + assertSame(indications.get(0), indications.get(1)); + assertSame(indications.get(0), deserialized.validIndication()); + } + + @Test + public void testPaymentMethodsAreInterned() { + List paymentMethods = Arrays.asList( + DirectionsCriteria.PAYMENT_METHOD_GENERAL, + DirectionsCriteria.PAYMENT_METHOD_GENERAL + ); + + IntersectionLanes intersectionLanes = IntersectionLanes.builder() + .paymentMethods(paymentMethods) + .build(); + + IntersectionLanes deserialized = IntersectionLanes.fromJson( + intersectionLanes.toJson() + ); + + assertEquals(paymentMethods, deserialized.paymentMethods()); + + assertSame( + deserialized.paymentMethods().get(0), + deserialized.paymentMethods().get(1) + ); + } } diff --git a/services-directions-models/src/test/java/com/mapbox/api/directions/v5/models/StepManeuverTest.java b/services-directions-models/src/test/java/com/mapbox/api/directions/v5/models/StepManeuverTest.java index cc15aba5c..53336d253 100644 --- a/services-directions-models/src/test/java/com/mapbox/api/directions/v5/models/StepManeuverTest.java +++ b/services-directions-models/src/test/java/com/mapbox/api/directions/v5/models/StepManeuverTest.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; import com.mapbox.core.TestUtils; import com.mapbox.geojson.Point; @@ -71,4 +72,42 @@ public void testFromJson() { compareJson(stepManeuverJsonString, jsonStr); } + + @Test + public void testTypesAreInterned() { + StepManeuver stepManeuver1 = StepManeuver.builder() + .rawLocation(new double[] {1.0, 2.0}) + .type(StepManeuver.TURN) + .build(); + + StepManeuver stepManeuver2 = StepManeuver.builder() + .rawLocation(new double[] {1.0, 3.0}) + .type(StepManeuver.TURN) + .build(); + + StepManeuver deserialized1 = StepManeuver.fromJson(stepManeuver1.toJson()); + StepManeuver deserialized2 = StepManeuver.fromJson(stepManeuver2.toJson()); + + assertEquals(deserialized1.type(), deserialized2.type()); + assertSame(deserialized1.type(), deserialized2.type()); + } + + @Test + public void testModifierAreInterned() { + StepManeuver stepManeuver1 = StepManeuver.builder() + .rawLocation(new double[] {1.0, 2.0}) + .modifier("slight right") + .build(); + + StepManeuver stepManeuver2 = StepManeuver.builder() + .rawLocation(new double[] {1.0, 3.0}) + .modifier("slight right") + .build(); + + StepManeuver deserialized1 = StepManeuver.fromJson(stepManeuver1.toJson()); + StepManeuver deserialized2 = StepManeuver.fromJson(stepManeuver2.toJson()); + + assertEquals(deserialized1.modifier(), deserialized2.modifier()); + assertSame(deserialized1.modifier(), deserialized2.modifier()); + } }