Skip to content

Commit 05a7bfe

Browse files
Optimize Memory Usage by Interning Frequently Occurring Strings in JSON (#1585)
* Intern IntersectionLanes's indication strings to save memory. * Optimize StepManeuver json parsing
1 parent be2ba14 commit 05a7bfe

File tree

7 files changed

+294
-2
lines changed

7 files changed

+294
-2
lines changed

services-directions-models/src/main/java/com/mapbox/api/directions/v5/models/IntersectionLanes.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public static Builder builder() {
109109
* @since 3.0.0
110110
*/
111111
public static TypeAdapter<IntersectionLanes> typeAdapter(Gson gson) {
112-
return new AutoValue_IntersectionLanes.GsonTypeAdapter(gson);
112+
return new IntersectionLanesTypeAdapter(new AutoValue_IntersectionLanes.GsonTypeAdapter(gson));
113113
}
114114

115115
/**
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package com.mapbox.api.directions.v5.models;
2+
3+
import com.google.gson.JsonElement;
4+
import com.google.gson.JsonObject;
5+
import com.google.gson.JsonParser;
6+
import com.google.gson.TypeAdapter;
7+
import com.google.gson.stream.JsonReader;
8+
import com.google.gson.stream.JsonToken;
9+
import com.google.gson.stream.JsonWriter;
10+
import com.mapbox.api.directions.v5.utils.ParseUtils;
11+
12+
import java.io.IOException;
13+
import java.util.LinkedHashMap;
14+
import java.util.Map;
15+
16+
/**
17+
* A TypeAdapter for {@link IntersectionLanes} used to optimize JSON deserialization.
18+
*
19+
* <p>Strings in {@link IntersectionLanes#indications()} and
20+
* {@link IntersectionLanes#validIndication()} can accept a limited set of possible values
21+
* (e.g., "straight," "right," "left," etc.).
22+
* This adapter invokes {@link String#intern()} on these strings to save memory.</p>
23+
*/
24+
class IntersectionLanesTypeAdapter extends TypeAdapter<IntersectionLanes> {
25+
26+
private final TypeAdapter<IntersectionLanes> defaultAdapter;
27+
28+
IntersectionLanesTypeAdapter(TypeAdapter<IntersectionLanes> defaultAdapter) {
29+
this.defaultAdapter = defaultAdapter;
30+
}
31+
32+
@Override
33+
public void write(JsonWriter out, IntersectionLanes value) throws IOException {
34+
defaultAdapter.write(out, value);
35+
}
36+
37+
@Override
38+
public IntersectionLanes read(JsonReader in) throws IOException {
39+
if (in.peek() == JsonToken.NULL) {
40+
in.nextNull();
41+
return null;
42+
}
43+
44+
final JsonObject jsonObject = JsonParser.parseReader(in).getAsJsonObject();
45+
46+
final IntersectionLanes.Builder builder = IntersectionLanes.builder();
47+
Map<String, JsonElement> unrecognized = null;
48+
49+
for (Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) {
50+
final String key = entry.getKey();
51+
final JsonElement value = entry.getValue();
52+
53+
if (value.isJsonNull()) {
54+
continue;
55+
}
56+
57+
switch (key) {
58+
case "valid":
59+
builder.valid(value.getAsBoolean());
60+
break;
61+
62+
case "active":
63+
builder.active(value.getAsBoolean());
64+
break;
65+
66+
case "valid_indication":
67+
builder.validIndication(value.getAsString().intern());
68+
break;
69+
70+
case "indications":
71+
builder.indications(ParseUtils.parseAndInternJsonStringArray(value.getAsJsonArray()));
72+
break;
73+
74+
case "payment_methods":
75+
builder.paymentMethods(ParseUtils.parseAndInternJsonStringArray(value.getAsJsonArray()));
76+
break;
77+
78+
default:
79+
if (unrecognized == null) {
80+
unrecognized = new LinkedHashMap<>();
81+
}
82+
unrecognized.put(key, value);
83+
break;
84+
}
85+
}
86+
87+
return builder
88+
.unrecognizedJsonProperties(unrecognized)
89+
.build();
90+
}
91+
}

services-directions-models/src/main/java/com/mapbox/api/directions/v5/models/StepManeuver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ public Point location() {
301301
* @since 3.0.0
302302
*/
303303
public static TypeAdapter<StepManeuver> typeAdapter(Gson gson) {
304-
return new AutoValue_StepManeuver.GsonTypeAdapter(gson);
304+
return new StepManeuverTypeAdapter(new AutoValue_StepManeuver.GsonTypeAdapter(gson));
305305
}
306306

307307
/**
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package com.mapbox.api.directions.v5.models;
2+
3+
import com.google.gson.JsonArray;
4+
import com.google.gson.JsonElement;
5+
import com.google.gson.JsonObject;
6+
import com.google.gson.JsonParser;
7+
import com.google.gson.TypeAdapter;
8+
import com.google.gson.stream.JsonReader;
9+
import com.google.gson.stream.JsonToken;
10+
import com.google.gson.stream.JsonWriter;
11+
12+
import java.io.IOException;
13+
import java.util.LinkedHashMap;
14+
import java.util.Map;
15+
16+
/**
17+
* A TypeAdapter for {@link StepManeuver} used to optimize JSON deserialization.
18+
*
19+
* <p>Strings {@link StepManeuver#type()} and {@link StepManeuver#modifier()} can accept
20+
* a limited set of possible values.
21+
* This adapter invokes {@link String#intern()} on these strings to save memory.</p>
22+
*/
23+
class StepManeuverTypeAdapter extends TypeAdapter<StepManeuver> {
24+
private final TypeAdapter<StepManeuver> defaultAdapter;
25+
26+
StepManeuverTypeAdapter(TypeAdapter<StepManeuver> defaultAdapter) {
27+
this.defaultAdapter = defaultAdapter;
28+
}
29+
30+
@Override
31+
public void write(JsonWriter out, StepManeuver value) throws IOException {
32+
defaultAdapter.write(out, value);
33+
}
34+
35+
@Override
36+
public StepManeuver read(JsonReader in) throws IOException {
37+
if (in.peek() == JsonToken.NULL) {
38+
in.nextNull();
39+
return null;
40+
}
41+
42+
final JsonObject jsonObject = JsonParser.parseReader(in).getAsJsonObject();
43+
44+
final StepManeuver.Builder builder = StepManeuver.builder();
45+
Map<String, JsonElement> unrecognized = null;
46+
47+
for (Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) {
48+
final String key = entry.getKey();
49+
final JsonElement value = entry.getValue();
50+
51+
if (value.isJsonNull()) {
52+
continue;
53+
}
54+
55+
switch (key) {
56+
case "location":
57+
final JsonArray jsonArray = value.getAsJsonArray();
58+
final double[] locations = new double[jsonArray.size()];
59+
for (int i = 0; i < jsonArray.size(); ++i) {
60+
locations[i] = jsonArray.get(i).getAsDouble();
61+
}
62+
builder.rawLocation(locations);
63+
break;
64+
65+
case "bearing_before":
66+
builder.bearingBefore(value.getAsDouble());
67+
break;
68+
69+
case "bearing_after":
70+
builder.bearingAfter(value.getAsDouble());
71+
break;
72+
73+
case "instruction":
74+
builder.instruction(value.getAsString());
75+
break;
76+
77+
case "type":
78+
builder.type(value.getAsString().intern());
79+
break;
80+
81+
case "modifier":
82+
builder.modifier(value.getAsString().intern());
83+
break;
84+
85+
case "exit":
86+
builder.exit(value.getAsInt());
87+
break;
88+
89+
default:
90+
if (unrecognized == null) {
91+
unrecognized = new LinkedHashMap<>();
92+
}
93+
unrecognized.put(key, value);
94+
break;
95+
}
96+
}
97+
98+
return builder
99+
.unrecognizedJsonProperties(unrecognized)
100+
.build();
101+
}
102+
}

services-directions-models/src/main/java/com/mapbox/api/directions/v5/utils/ParseUtils.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import androidx.annotation.NonNull;
44
import androidx.annotation.Nullable;
5+
6+
import com.google.gson.JsonArray;
57
import com.mapbox.api.directions.v5.models.Bearing;
68
import com.mapbox.geojson.Point;
79
import java.util.ArrayList;
@@ -213,4 +215,19 @@ public static List<Bearing> parseBearings(@Nullable String original) {
213215
public static List<Boolean> parseToBooleans(@Nullable String original) {
214216
return parseToList(SEMICOLON, original, BOOLEAN_PARSER);
215217
}
218+
219+
/**
220+
* Parses strings from json array and invokes {@link String#intern()} on each item.
221+
*
222+
* @param jsonArray json array with string items
223+
* @return List of interned parsed strings
224+
*/
225+
@NonNull
226+
public static List<String> parseAndInternJsonStringArray(JsonArray jsonArray) {
227+
final List<String> result = new ArrayList<>(jsonArray.size());
228+
for (int i = 0; i < jsonArray.size(); ++i) {
229+
result.add(jsonArray.get(i).getAsString().intern());
230+
}
231+
return result;
232+
}
216233
}

services-directions-models/src/test/java/com/mapbox/api/directions/v5/models/IntersectionLanesTest.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static org.junit.Assert.assertEquals;
44
import static org.junit.Assert.assertNotNull;
5+
import static org.junit.Assert.assertSame;
56

67
import com.mapbox.api.directions.v5.DirectionsCriteria;
78
import com.mapbox.core.TestUtils;
@@ -102,4 +103,46 @@ public void testFromJson_etc2Payment() {
102103

103104
assertEquals(intersectionLanes, intersectionLanesFromJson);
104105
}
106+
107+
@Test
108+
public void testIndicationsAreInterned() {
109+
IntersectionLanes intersectionLanes = IntersectionLanes.builder()
110+
.validIndication("straight")
111+
.indications(Arrays.asList("straight","straight"))
112+
.build();
113+
114+
IntersectionLanes deserialized = IntersectionLanes.fromJson(
115+
intersectionLanes.toJson()
116+
);
117+
118+
List<String> indications = deserialized.indications();
119+
assertEquals(Arrays.asList("straight","straight"), indications);
120+
assertEquals("straight", deserialized.validIndication());
121+
122+
assertSame(indications.get(0), indications.get(1));
123+
assertSame(indications.get(0), deserialized.validIndication());
124+
}
125+
126+
@Test
127+
public void testPaymentMethodsAreInterned() {
128+
List<String> paymentMethods = Arrays.asList(
129+
DirectionsCriteria.PAYMENT_METHOD_GENERAL,
130+
DirectionsCriteria.PAYMENT_METHOD_GENERAL
131+
);
132+
133+
IntersectionLanes intersectionLanes = IntersectionLanes.builder()
134+
.paymentMethods(paymentMethods)
135+
.build();
136+
137+
IntersectionLanes deserialized = IntersectionLanes.fromJson(
138+
intersectionLanes.toJson()
139+
);
140+
141+
assertEquals(paymentMethods, deserialized.paymentMethods());
142+
143+
assertSame(
144+
deserialized.paymentMethods().get(0),
145+
deserialized.paymentMethods().get(1)
146+
);
147+
}
105148
}

services-directions-models/src/test/java/com/mapbox/api/directions/v5/models/StepManeuverTest.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static org.junit.Assert.assertEquals;
44
import static org.junit.Assert.assertNotNull;
5+
import static org.junit.Assert.assertSame;
56

67
import com.mapbox.core.TestUtils;
78
import com.mapbox.geojson.Point;
@@ -71,4 +72,42 @@ public void testFromJson() {
7172

7273
compareJson(stepManeuverJsonString, jsonStr);
7374
}
75+
76+
@Test
77+
public void testTypesAreInterned() {
78+
StepManeuver stepManeuver1 = StepManeuver.builder()
79+
.rawLocation(new double[] {1.0, 2.0})
80+
.type(StepManeuver.TURN)
81+
.build();
82+
83+
StepManeuver stepManeuver2 = StepManeuver.builder()
84+
.rawLocation(new double[] {1.0, 3.0})
85+
.type(StepManeuver.TURN)
86+
.build();
87+
88+
StepManeuver deserialized1 = StepManeuver.fromJson(stepManeuver1.toJson());
89+
StepManeuver deserialized2 = StepManeuver.fromJson(stepManeuver2.toJson());
90+
91+
assertEquals(deserialized1.type(), deserialized2.type());
92+
assertSame(deserialized1.type(), deserialized2.type());
93+
}
94+
95+
@Test
96+
public void testModifierAreInterned() {
97+
StepManeuver stepManeuver1 = StepManeuver.builder()
98+
.rawLocation(new double[] {1.0, 2.0})
99+
.modifier("slight right")
100+
.build();
101+
102+
StepManeuver stepManeuver2 = StepManeuver.builder()
103+
.rawLocation(new double[] {1.0, 3.0})
104+
.modifier("slight right")
105+
.build();
106+
107+
StepManeuver deserialized1 = StepManeuver.fromJson(stepManeuver1.toJson());
108+
StepManeuver deserialized2 = StepManeuver.fromJson(stepManeuver2.toJson());
109+
110+
assertEquals(deserialized1.modifier(), deserialized2.modifier());
111+
assertSame(deserialized1.modifier(), deserialized2.modifier());
112+
}
74113
}

0 commit comments

Comments
 (0)