Skip to content

Commit

Permalink
Optimize Memory Usage by Interning Frequently Occurring Strings in JS…
Browse files Browse the repository at this point in the history
…ON (#1585)

* Intern IntersectionLanes's indication strings to save memory.

* Optimize StepManeuver json parsing
  • Loading branch information
DzmitryFomchyn authored Jul 8, 2024
1 parent be2ba14 commit 05a7bfe
Show file tree
Hide file tree
Showing 7 changed files with 294 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public static Builder builder() {
* @since 3.0.0
*/
public static TypeAdapter<IntersectionLanes> typeAdapter(Gson gson) {
return new AutoValue_IntersectionLanes.GsonTypeAdapter(gson);
return new IntersectionLanesTypeAdapter(new AutoValue_IntersectionLanes.GsonTypeAdapter(gson));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.</p>
*/
class IntersectionLanesTypeAdapter extends TypeAdapter<IntersectionLanes> {

private final TypeAdapter<IntersectionLanes> defaultAdapter;

IntersectionLanesTypeAdapter(TypeAdapter<IntersectionLanes> 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<String, JsonElement> unrecognized = null;

for (Map.Entry<String, JsonElement> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ public Point location() {
* @since 3.0.0
*/
public static TypeAdapter<StepManeuver> typeAdapter(Gson gson) {
return new AutoValue_StepManeuver.GsonTypeAdapter(gson);
return new StepManeuverTypeAdapter(new AutoValue_StepManeuver.GsonTypeAdapter(gson));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.</p>
*/
class StepManeuverTypeAdapter extends TypeAdapter<StepManeuver> {
private final TypeAdapter<StepManeuver> defaultAdapter;

StepManeuverTypeAdapter(TypeAdapter<StepManeuver> 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<String, JsonElement> unrecognized = null;

for (Map.Entry<String, JsonElement> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -213,4 +215,19 @@ public static List<Bearing> parseBearings(@Nullable String original) {
public static List<Boolean> 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<String> parseAndInternJsonStringArray(JsonArray jsonArray) {
final List<String> result = new ArrayList<>(jsonArray.size());
for (int i = 0; i < jsonArray.size(); ++i) {
result.add(jsonArray.get(i).getAsString().intern());
}
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> 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<String> 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)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}

0 comments on commit 05a7bfe

Please sign in to comment.