Skip to content

Commit db2d6bf

Browse files
committed
perf(gdrive): optimize trip log exports with columnar JSON
- Refactored `TripLogTransformer` to generate a Columnar Series-based JSON format instead of a flat row-based array. This eliminates redundant keys and string repetitions, further shrinking the payload. - Introduced a `signal_dictionary` to the JSON schema. Time-series data is now keyed by raw numeric PIDs to guarantee consistency across app language changes, while the dictionary provides the translated names. - Updated `TripLogTransformerTest` assertions to validate the new schema and dictionary mapping.
1 parent f2d7e78 commit db2d6bf

File tree

3 files changed

+107
-79
lines changed

3 files changed

+107
-79
lines changed

app/build.gradle

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,23 +81,23 @@ android {
8181
resValue "string", "DEFAULT_PROFILE", "profile_8"
8282
resValue "string", "applicationId", "org.obd.graphs.my.giulia.aa"
8383
applicationId "org.obd.graphs.my.giulia.aa"
84-
versionCode 220
84+
versionCode 221
8585
}
8686

8787
giuliaPerformanceMonitor {
8888
dimension "version"
8989
resValue "string", "DEFAULT_PROFILE", "profile_8"
9090
resValue "string", "applicationId", "org.obd.graphs.my.giulia.performance_monitor"
9191
applicationId "org.obd.graphs.my.giulia.performance_monitor"
92-
versionCode 101
92+
versionCode 102
9393
}
9494

9595
giulia {
9696
dimension "version"
9797
resValue "string", "DEFAULT_PROFILE", "profile_3"
9898
resValue "string", "applicationId", "org.obd.graphs.my.giulia"
9999
applicationId "org.obd.graphs.my.giulia"
100-
versionCode 74
100+
versionCode 75
101101
}
102102
}
103103

integrations/src/main/java/org/obd/graphs/integrations/log/TripLogTransformer.kt

Lines changed: 87 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -48,29 +48,81 @@ private class DefaultJSONOutput(
4848
private val valueMapper: (signal: Int, value: Any) -> Any
4949
) : TripLogTransformer {
5050

51+
private class SeriesData {
52+
val timestamps = mutableListOf<Long>()
53+
val values = mutableListOf<Any?>()
54+
}
55+
5156
override fun transform(file: File, metadata: Map<String, String>): File =
5257
file.inputStream().use { input ->
5358
process(JsonReader(InputStreamReader(input)), metadata)
5459
}
5560

56-
override fun transform(log: String, metadata: Map<String, String>): File = process(JsonReader(StringReader(log)), metadata)
61+
override fun transform(log: String, metadata: Map<String, String>): File =
62+
process(JsonReader(StringReader(log)), metadata)
5763

5864
private fun process(reader: JsonReader, metadata: Map<String, String>): File {
5965
Log.d("DefaultJSONOutput", "Received $metadata")
6066
val tempFile =
6167
File.createTempFile("json_buffer_", ".tmp").apply {
62-
// Ensures the file is cleaned up if the JVM shuts down
6368
deleteOnExit()
6469
}
6570

71+
val seriesMap = mutableMapOf<String, SeriesData>()
72+
6673
try {
67-
// Nested .use calls ensure all streams are closed even if an exception occurs
74+
reader.isLenient = true
75+
parseRootToMemory(reader, seriesMap)
76+
6877
tempFile.outputStream().bufferedWriter().use { fileWriter ->
6978
JsonWriter(fileWriter).use { writer ->
70-
reader.isLenient = true
71-
writer.beginArray()
72-
parseRoot(reader, writer, metadata)
73-
writer.endArray()
79+
writer.beginObject() // Root object
80+
81+
// Write Metadata
82+
if (metadata.isNotEmpty()) {
83+
writer.name("metadata")
84+
writer.beginObject()
85+
metadata.forEach { (key, value) ->
86+
writer.name(key).value(value)
87+
}
88+
writer.endObject()
89+
}
90+
91+
// Write Signal Dictionary
92+
writer.name("signal_dictionary")
93+
writer.beginObject()
94+
seriesMap.keys.forEach { signalKey ->
95+
val idAsInt = signalKey.toIntOrNull()
96+
val translatedName = if (idAsInt != null) {
97+
signalMapper[idAsInt] ?: signalKey
98+
} else {
99+
signalKey
100+
}
101+
writer.name(signalKey).value(translatedName.toString())
102+
}
103+
writer.endObject()
104+
105+
// Write Series Data
106+
writer.name("series")
107+
writer.beginObject()
108+
seriesMap.forEach { (signalId, seriesData) ->
109+
writer.name(signalId)
110+
writer.beginObject()
111+
112+
writer.name("t")
113+
writer.beginArray()
114+
seriesData.timestamps.forEach { writer.value(it) }
115+
writer.endArray()
116+
117+
writer.name("v")
118+
writer.beginArray()
119+
seriesData.values.forEach { writer.writeDynamicValue(it) }
120+
writer.endArray()
121+
122+
writer.endObject()
123+
}
124+
writer.endObject() // end series
125+
writer.endObject() // end root
74126
}
75127
}
76128
return tempFile
@@ -85,85 +137,73 @@ private class DefaultJSONOutput(
85137
}
86138
}
87139

88-
private fun parseRoot(
140+
private fun parseRootToMemory(
89141
reader: JsonReader,
90-
writer: JsonWriter,
91-
metadata: Map<String, String>
142+
seriesMap: MutableMap<String, SeriesData>
92143
) {
93-
if (metadata.isNotEmpty()) {
94-
writer.beginObject()
95-
writer.name("metadata")
96-
writer.beginObject()
97-
metadata.forEach { (key, value) ->
98-
writer.name(key).value(value)
99-
}
100-
writer.endObject()
101-
writer.endObject()
102-
}
103-
104144
reader.beginObject()
105145
while (reader.hasNext()) {
106146
if (reader.nextName() == "entries") {
107-
parseEntries(reader, writer)
147+
parseEntriesToMemory(reader, seriesMap)
108148
} else {
109149
reader.skipValue()
110150
}
111151
}
112152
reader.endObject()
113153
}
114154

115-
private fun parseEntries(
155+
private fun parseEntriesToMemory(
116156
reader: JsonReader,
117-
writer: JsonWriter
157+
seriesMap: MutableMap<String, SeriesData>
118158
) {
119-
reader.beginObject() // Start "entries" map
159+
reader.beginObject()
120160
while (reader.hasNext()) {
121161
reader.nextName() // Skip the dynamic key ("12", "99")
122-
parseEntryGroup(reader, writer)
162+
parseEntryGroupToMemory(reader, seriesMap)
123163
}
124164
reader.endObject()
125165
}
126166

127-
private fun parseEntryGroup(
167+
private fun parseEntryGroupToMemory(
128168
reader: JsonReader,
129-
writer: JsonWriter
169+
seriesMap: MutableMap<String, SeriesData>
130170
) {
131-
reader.beginObject() // Inside "12": {
171+
reader.beginObject()
132172
while (reader.hasNext()) {
133173
if (reader.nextName() == "metrics") {
134-
parseMetricsArray(reader, writer)
174+
parseMetricsArrayToMemory(reader, seriesMap)
135175
} else {
136-
reader.skipValue() // Skip "id", "mean", etc.
176+
reader.skipValue()
137177
}
138178
}
139179
reader.endObject()
140180
}
141181

142-
private fun parseMetricsArray(
182+
private fun parseMetricsArrayToMemory(
143183
reader: JsonReader,
144-
writer: JsonWriter
184+
seriesMap: MutableMap<String, SeriesData>
145185
) {
146-
reader.beginArray() // [
186+
reader.beginArray()
147187
while (reader.hasNext()) {
148-
parseSingleMetric(reader, writer)
188+
parseSingleMetricToMemory(reader, seriesMap)
149189
}
150190
reader.endArray()
151191
}
152192

153-
private fun parseSingleMetric(
193+
private fun parseSingleMetricToMemory(
154194
reader: JsonReader,
155-
writer: JsonWriter
195+
seriesMap: MutableMap<String, SeriesData>
156196
) {
157197
var ts: Long = 0
158198
var signal = 0
159199
var value: Any = 0.0
160200

161-
reader.beginObject() // Metric object {
201+
reader.beginObject()
162202
while (reader.hasNext()) {
163203
when (reader.nextName()) {
164204
"ts" -> ts = reader.nextLong()
165205
"entry" -> {
166-
reader.beginObject() // Nested "entry": {
206+
reader.beginObject()
167207
while (reader.hasNext()) {
168208
when (reader.nextName()) {
169209
"data" -> signal = reader.nextInt()
@@ -179,36 +219,28 @@ private class DefaultJSONOutput(
179219
}
180220
reader.endObject()
181221
}
182-
183222
else -> reader.skipValue()
184223
}
185224
}
186-
187225
reader.endObject()
188-
writer.beginObject()
189-
writer.name("t").value(ts)
190-
writer.name("s").value((signalMapper[signal] ?: signal).toString())
191-
val mappedResult: Any = valueMapper(signal, value)
192226

193-
writer.name("v")
194-
writer.writeDynamicValue(mappedResult)
195-
writer.endObject()
227+
val signalKey = signal.toString() // Group purely by ID
228+
val mappedResult = valueMapper(signal, value)
229+
230+
val series = seriesMap.getOrPut(signalKey) { SeriesData() }
231+
series.timestamps.add(ts)
232+
series.values.add(mappedResult)
196233
}
197234

198-
/**
199-
* Recursively reads a JSON object from the reader and returns it as a Map.
200-
*/
201235
private fun JsonReader.readMap(): Map<String, Any?> {
202236
val map = mutableMapOf<String, Any?>()
203237

204238
this.beginObject()
205239
while (this.hasNext()) {
206240
val key = this.nextName()
207241
val value: Any? = when (this.peek()) {
208-
JsonToken.BEGIN_OBJECT -> readMap() // Recursive call for nested maps
242+
JsonToken.BEGIN_OBJECT -> readMap()
209243
JsonToken.BEGIN_ARRAY -> {
210-
// Optional: Handle arrays if your source map has lists
211-
// For now we just skip or you can implement readList() similarly
212244
this.skipValue()
213245
null
214246
}
@@ -227,36 +259,27 @@ private class DefaultJSONOutput(
227259
return map
228260
}
229261

230-
/**
231-
* Extension to write mixed types (Number, String, Map, List) to JsonWriter.
232-
*/
233262
private fun JsonWriter.writeDynamicValue(value: Any?) {
234263
when (value) {
235264
null -> this.nullValue()
236265
is Number -> this.value(value)
237266
is String -> this.value(value)
238267
is Boolean -> this.value(value)
239-
240-
// Handle Map -> JSON Object
241268
is Map<*, *> -> {
242269
this.beginObject()
243270
for ((k, v) in value) {
244271
this.name(k.toString())
245-
writeDynamicValue(v) // Recursive call for nested values
272+
writeDynamicValue(v)
246273
}
247274
this.endObject()
248275
}
249-
250-
// Handle List/Array -> JSON Array (Optional, but good for safety)
251276
is Collection<*> -> {
252277
this.beginArray()
253278
for (item in value) {
254279
writeDynamicValue(item)
255280
}
256281
this.endArray()
257282
}
258-
259-
// Fallback for unknown objects
260283
else -> this.value(value.toString())
261284
}
262285
}

integrations/src/test/java/org/obd/graphs/integrations/gcp/gdrive/TripLogTransformerTest.kt

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ class TripLogTransformerTest {
7070
val transformer: TripLogTransformer = TripLog.transformer { s, v -> v }
7171
val result = transformer.transform(file).readText()
7272

73-
Assertions.assertThat(result).startsWith("[{\"t\":1765481896083,\"s\":\"12\",\"v\":3298.0767},{\"t\":1765481896267,\"s\":\"12\",\"v\":3298.0767},{\"t\":1765481896463,\"s\":\"12\",\"v\":3298.0767},{\"t\":1765481896666,\"s\":\"12\"")
73+
Assertions.assertThat(result).contains("\"signal_dictionary\":{")
74+
Assertions.assertThat(result).contains("\"series\":{")
75+
Assertions.assertThat(result).contains("\"12\":{\"t\":[1765481896083,1765481896267")
76+
Assertions.assertThat(result).contains("\"v\":[3298.0767,3298.0767")
7477
}
7578

7679
@Test
@@ -111,10 +114,11 @@ class TripLogTransformerTest {
111114

112115
val result = transformer.transform(rawJson, meta).readText()
113116

117+
// Notice 12 is mapped to "Boost" in dictionary, but 13 defaults to "13" since it's unmapped.
114118
val expectedJson =
115-
"""[{"metadata":{"key1":"value1","key2":"value2"}},{"t":1000,"s":"Boost","v":101.0},{"t":2000,"s":"13","v":121.0}]"""
119+
"""{"metadata":{"key1":"value1","key2":"value2"},"signal_dictionary":{"12":"Boost","13":"13"},"series":{"12":{"t":[1000],"v":[101.0]},"13":{"t":[2000],"v":[121.0]}}}"""
116120

117-
Assertions.assertThat(expectedJson).isEqualTo(result)
121+
Assertions.assertThat(result).isEqualTo(expectedJson)
118122
}
119123

120124
@Test
@@ -148,7 +152,7 @@ class TripLogTransformerTest {
148152
val transformer: TripLogTransformer = TripLog.transformer { s, v -> v }
149153
val result = transformer.transform(rawJson).readText()
150154

151-
val expectedJson = """[{"t":1500,"s":"99","v":{"GPS altitude":57.10662841796875,"GPS Location":{"altitude":57.10662841796875,"accuracy":46.843723,"latitude":54.16406183,"longitude":16.29066863}}}]"""
155+
val expectedJson = """{"signal_dictionary":{"99":"99"},"series":{"99":{"t":[1500],"v":[{"GPS altitude":57.10662841796875,"GPS Location":{"altitude":57.10662841796875,"accuracy":46.843723,"latitude":54.16406183,"longitude":16.29066863}}]}}}"""
152156

153157
Assertions.assertThat(result).isEqualTo(expectedJson)
154158
}
@@ -186,13 +190,13 @@ class TripLogTransformerTest {
186190
val result = transformer.transform(rawJson).readText()
187191

188192
val expectedJson =
189-
"""[{"t":1000,"s":"Boost","v":101.0},{"t":2000,"s":"13","v":121.0}]"""
193+
"""{"signal_dictionary":{"12":"Boost","13":"13"},"series":{"12":{"t":[1000],"v":[101.0]},"13":{"t":[2000],"v":[121.0]}}}"""
190194

191-
Assertions.assertThat(expectedJson).isEqualTo(result)
195+
Assertions.assertThat(result).isEqualTo(expectedJson)
192196
}
193197

194198
@Test
195-
fun `optimize should convert complex json to optimized flat format`() {
199+
fun `optimize should convert complex json to optimized columnar format`() {
196200
val rawJson =
197201
"""
198202
{
@@ -224,9 +228,9 @@ class TripLogTransformerTest {
224228
val result = transformer.transform(rawJson).readText()
225229

226230
val expectedJson =
227-
"""[{"t":1000,"s":"12","v":50.5},{"t":2000,"s":"12","v":60.5}]"""
231+
"""{"signal_dictionary":{"12":"12"},"series":{"12":{"t":[1000,2000],"v":[50.5,60.5]}}}"""
228232

229-
Assertions.assertThat(expectedJson).isEqualTo(result)
233+
Assertions.assertThat(result).isEqualTo(expectedJson)
230234
}
231235

232236
@Test
@@ -253,9 +257,10 @@ class TripLogTransformerTest {
253257
assertFalse(result.contains("\"rawAnswer\""))
254258
assertFalse(result.contains("\"entry\""))
255259

256-
assertTrue(result.contains("\"s\""))
260+
assertTrue(result.contains("\"signal_dictionary\""))
261+
assertTrue(result.contains("\"series\""))
257262
assertTrue(result.contains("\"t\""))
258-
assertTrue(result.contains("\"v\":2.0"))
263+
assertTrue(result.contains("\"v\":[2.0]"))
259264
}
260265

261266
@Test
@@ -271,7 +276,7 @@ class TripLogTransformerTest {
271276
val transformer: TripLogTransformer = TripLog.transformer { s, v -> v }
272277
val result = transformer.transform(rawJson).readText()
273278

274-
val expected = """[]"""
279+
val expected = """{"signal_dictionary":{},"series":{}}"""
275280
assertEquals(expected, result)
276281
}
277282
}

0 commit comments

Comments
 (0)