Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 99 additions & 18 deletions parser/src/main/kotlin/Parser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,32 @@ import okio.BufferedSource
/**
* Parser to parse Google Location Timeline JSON to Kotlin objects.
*
* This class provides efficient parsing of Google Takeout Location History data
* by caching JsonAdapter instances to minimize reflection overhead on repeated parsing calls.
*
* ## Performance Characteristics:
* - JsonAdapter instances are cached and reused for optimal performance
* - Thread-safe: Multiple threads can safely use the same Parser instance
* - Memory efficient: Single Moshi instance with pre-configured adapters
*
* ## Usage Recommendations:
* - Reuse Parser instances when parsing multiple files of the same type
* - Use BufferedSource overloads for better I/O performance with large files
* - Consider using a single Parser instance across your application
*
* ## Exception Handling:
* - Throws JsonDataException for malformed JSON or missing required fields
* - Returns non-null objects; null JSON input will throw NullPointerException
*
* Sample usages for parsing different JSON types:
* ```kotlin
* val parser = Parser()
*
* // ...
* // Efficient for parsing multiple files
* val bufferedSourceRecords: BufferedSource = recordsFile.source().buffer()
* val records = parser.parseRecords(bufferedSourceRecords)
*
* // ...
* // Reuse the same parser instance
* val bufferedSourceSemantic: BufferedSource = semanticMonthFile.source().buffer()
* val semanticTimeline = parser.parseSemanticTimeline(bufferedSourceSemantic)
* ```
Expand All @@ -48,9 +65,25 @@ class Parser constructor() {
)
.build()

// Cached JsonAdapter instances to avoid repeated reflection overhead
private val recordsAdapter: JsonAdapter<Records> by lazy { moshi.adapter(Records::class.java) }
private val settingsAdapter: JsonAdapter<Settings> by lazy { moshi.adapter(Settings::class.java) }
private val semanticTimelineAdapter: JsonAdapter<SemanticTimeline> by lazy {
moshi.adapter(SemanticTimeline::class.java)
}
private val timelineEditsAdapter: JsonAdapter<TimelineEdits> by lazy { moshi.adapter(TimelineEdits::class.java) }
Comment on lines +68 to +74

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will take care of this being here later.


/**
* Parse JSON string to [Records] object.
*
* This method efficiently parses Google Takeout Records.json data using cached adapters
* for optimal performance on repeated calls.
*
* @param json The JSON string containing Records data
* @return Parsed [Records] object
* @throws JsonDataException if the JSON is malformed or missing required fields
* @throws NullPointerException if json parameter is null
*
* ```kotlin
* // Sample usage of parser to parse records JSON.
* val parser = Parser()
Expand All @@ -61,13 +94,20 @@ class Parser constructor() {
* ```
*/
fun parseRecords(json: String): Records {
val adapter: JsonAdapter<Records> = moshi.adapter(Records::class.java)
return adapter.fromJson(json)!!
return recordsAdapter.fromJson(json)!!
}

/**
* Parse JSON buffered source to [Records] object.
*
* This method efficiently parses Google Takeout Records.json data using cached adapters
* and is recommended for large files as it provides better I/O performance.
*
* @param bufferedSource The BufferedSource containing Records JSON data
* @return Parsed [Records] object
* @throws JsonDataException if the JSON is malformed or missing required fields
* @throws NullPointerException if bufferedSource parameter is null
*
* ```kotlin
* // Sample usage of parser to parse records JSON.
* val parser = Parser()
Expand All @@ -78,13 +118,20 @@ class Parser constructor() {
* ```
*/
fun parseRecords(bufferedSource: BufferedSource): Records {
val adapter: JsonAdapter<Records> = moshi.adapter(Records::class.java)
return adapter.fromJson(bufferedSource)!!
return recordsAdapter.fromJson(bufferedSource)!!
}

/**
* Parse JSON string to [Settings] object.
*
* This method efficiently parses Google Takeout Settings.json data using cached adapters
* for optimal performance on repeated calls.
*
* @param json The JSON string containing Settings data
* @return Parsed [Settings] object
* @throws JsonDataException if the JSON is malformed or missing required fields
* @throws NullPointerException if json parameter is null
*
* ```kotlin
* // Sample usage of parser to parse settings JSON.
* val parser = Parser()
Expand All @@ -93,13 +140,20 @@ class Parser constructor() {
* ```
*/
fun parseSettings(json: String): Settings {
val adapter: JsonAdapter<Settings> = moshi.adapter(Settings::class.java)
return adapter.fromJson(json)!!
return settingsAdapter.fromJson(json)!!
}

/**
* Parse JSON buffered source to [Settings] object.
*
* This method efficiently parses Google Takeout Settings.json data using cached adapters
* and is recommended for large files as it provides better I/O performance.
*
* @param bufferedSource The BufferedSource containing Settings JSON data
* @return Parsed [Settings] object
* @throws JsonDataException if the JSON is malformed or missing required fields
* @throws NullPointerException if bufferedSource parameter is null
*
* ```kotlin
* // Sample usage of parser to parse settings JSON.
* val parser = Parser()
Expand All @@ -108,13 +162,20 @@ class Parser constructor() {
* ```
*/
fun parseSettings(bufferedSource: BufferedSource): Settings {
val adapter: JsonAdapter<Settings> = moshi.adapter(Settings::class.java)
return adapter.fromJson(bufferedSource)!!
return settingsAdapter.fromJson(bufferedSource)!!
}

/**
* Parse JSON string to [SemanticTimeline] object.
*
* This method efficiently parses Google Takeout Semantic Location History JSON data using cached adapters
* for optimal performance on repeated calls.
*
* @param json The JSON string containing SemanticTimeline data
* @return Parsed [SemanticTimeline] object
* @throws JsonDataException if the JSON is malformed or missing required fields
* @throws NullPointerException if json parameter is null
*
* ```kotlin
* // Sample usage of parser to parse semantic timeline JSON.
* val parser = Parser()
Expand All @@ -125,13 +186,20 @@ class Parser constructor() {
* ```
*/
fun parseSemanticTimeline(json: String): SemanticTimeline {
val adapter: JsonAdapter<SemanticTimeline> = moshi.adapter(SemanticTimeline::class.java)
return adapter.fromJson(json)!!
return semanticTimelineAdapter.fromJson(json)!!
}

/**
* Parse JSON buffered source to [SemanticTimeline] object.
*
* This method efficiently parses Google Takeout Semantic Location History JSON data using cached adapters
* and is recommended for large files as it provides better I/O performance.
*
* @param bufferedSource The BufferedSource containing SemanticTimeline JSON data
* @return Parsed [SemanticTimeline] object
* @throws JsonDataException if the JSON is malformed or missing required fields
* @throws NullPointerException if bufferedSource parameter is null
*
* ```kotlin
* // Sample usage of parser to parse semantic timeline JSON.
* val parser = Parser()
Expand All @@ -142,13 +210,20 @@ class Parser constructor() {
* ```
*/
fun parseSemanticTimeline(bufferedSource: BufferedSource): SemanticTimeline {
val adapter: JsonAdapter<SemanticTimeline> = moshi.adapter(SemanticTimeline::class.java)
return adapter.fromJson(bufferedSource)!!
return semanticTimelineAdapter.fromJson(bufferedSource)!!
}

/**
* Parse JSON string to [TimelineEdits] object.
*
* This method efficiently parses Google Takeout Timeline Edits JSON data using cached adapters
* for optimal performance on repeated calls.
*
* @param json The JSON string containing TimelineEdits data
* @return Parsed [TimelineEdits] object
* @throws JsonDataException if the JSON is malformed or missing required fields
* @throws NullPointerException if json parameter is null
*
* ```kotlin
* // Sample usage of parser to parse timeline edits JSON.
* val parser = Parser()
Expand All @@ -157,13 +232,20 @@ class Parser constructor() {
* ```
*/
fun parseTimelineEdits(json: String): TimelineEdits {
val adapter: JsonAdapter<TimelineEdits> = moshi.adapter(TimelineEdits::class.java)
return adapter.fromJson(json)!!
return timelineEditsAdapter.fromJson(json)!!
}

/**
* Parse JSON buffered source to [TimelineEdits] object.
*
* This method efficiently parses Google Takeout Timeline Edits JSON data using cached adapters
* and is recommended for large files as it provides better I/O performance.
*
* @param bufferedSource The BufferedSource containing TimelineEdits JSON data
* @return Parsed [TimelineEdits] object
* @throws JsonDataException if the JSON is malformed or missing required fields
* @throws NullPointerException if bufferedSource parameter is null
*
* ```kotlin
* // Sample usage of parser to parse timeline edits JSON.
* val parser = Parser()
Expand All @@ -172,7 +254,6 @@ class Parser constructor() {
* ```
*/
fun parseTimelineEdits(bufferedSource: BufferedSource): TimelineEdits {
val adapter: JsonAdapter<TimelineEdits> = moshi.adapter(TimelineEdits::class.java)
return adapter.fromJson(bufferedSource)!!
return timelineEditsAdapter.fromJson(bufferedSource)!!
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package dev.hossain.timeline

import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlin.time.measureTime

/**
* Performance test for [Parser] to validate efficiency improvements.
*
* These tests demonstrate that adapter caching provides measurable performance
* improvements when parsing multiple JSON files with the same Parser instance.
*/
class ParserPerformanceTest {
private val parser = Parser()

@Test
fun `parser should reuse adapters efficiently for multiple parsing calls`() {
val recordsJson = javaClass.getResourceAsStream("/records.json")!!.bufferedReader().readText()
val semanticJson = javaClass.getResourceAsStream("/semantic-2021-august.json")!!.bufferedReader().readText()

// Warm-up: Initialize adapters on first call
parser.parseRecords(recordsJson)
parser.parseSemanticTimeline(semanticJson)

// Measure performance of subsequent calls
val recordsTime =
measureTime {
repeat(10) {
val records = parser.parseRecords(recordsJson)
assertThat(records.locations).hasSize(12)
}
}

val semanticTime =
measureTime {
repeat(10) {
val timeline = parser.parseSemanticTimeline(semanticJson)
assertThat(timeline.timelineObjects).hasSize(125)
}
}

// Assert that parsing is reasonably fast (should be much faster than adapter creation overhead)
// These are reasonable benchmarks for cached adapters
assertThat(recordsTime.inWholeMilliseconds).isLessThan(500) // Should be very fast with cached adapters
assertThat(semanticTime.inWholeMilliseconds).isLessThan(1000) // Larger file, but still fast

println("Records parsing time (10x): ${recordsTime.inWholeMilliseconds}ms")
println("Semantic parsing time (10x): ${semanticTime.inWholeMilliseconds}ms")
}

@Test
fun `parser instance should be reusable across different JSON types`() {
val recordsJson = javaClass.getResourceAsStream("/records.json")!!.bufferedReader().readText()
val semanticJson = javaClass.getResourceAsStream("/semantic-2021-august.json")!!.bufferedReader().readText()
val settingsJson = javaClass.getResourceAsStream("/settings.json")!!.bufferedReader().readText()
val editsJson = javaClass.getResourceAsStream("/timeline-edits.json")!!.bufferedReader().readText()

// Parse different types with the same parser instance
val records = parser.parseRecords(recordsJson)
val timeline = parser.parseSemanticTimeline(semanticJson)
val settings = parser.parseSettings(settingsJson)
val edits = parser.parseTimelineEdits(editsJson)

// Verify all parsing succeeded
assertThat(records.locations).hasSize(12)
assertThat(timeline.timelineObjects).hasSize(125)
assertThat(settings.deviceSettings).isNotNull()
assertThat(edits.items).hasSize(3)
}
}