Skip to content

Commit

Permalink
Merge pull request #1678 from DataDog/nogorodnikov/rum-321/reduce-vie…
Browse files Browse the repository at this point in the history
…w-events-in-upload-pipeline

RUM-321: Introduce view event filtering in upload pipeline, remove view event throttling in write pipeline
  • Loading branch information
0xnm authored Oct 26, 2023
2 parents 4744e9d + 4a3f24c commit ad4d00a
Show file tree
Hide file tree
Showing 28 changed files with 759 additions and 555 deletions.
1 change: 1 addition & 0 deletions detekt_custom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,7 @@ datadog:
- "kotlin.collections.MutableMap.clear()"
- "kotlin.collections.MutableMap.containsKey(android.view.Window)"
- "kotlin.collections.MutableMap.containsKey(com.datadog.android.api.SdkCore)"
- "kotlin.collections.MutableMap.containsKey(com.datadog.android.api.storage.RawBatchEvent)"
- "kotlin.collections.MutableMap.containsKey(kotlin.Long)"
- "kotlin.collections.MutableMap.containsKey(kotlin.String)"
- "kotlin.collections.MutableMap.filterKeys(kotlin.Function1)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ import com.datadog.android.rum.internal.anr.ANRDetectorRunnable
import com.datadog.android.rum.internal.debug.UiRumDebugListener
import com.datadog.android.rum.internal.domain.RumDataWriter
import com.datadog.android.rum.internal.domain.event.RumEventMapper
import com.datadog.android.rum.internal.domain.event.RumEventMetaDeserializer
import com.datadog.android.rum.internal.domain.event.RumEventMetaSerializer
import com.datadog.android.rum.internal.domain.event.RumEventSerializer
import com.datadog.android.rum.internal.domain.event.RumViewEventFilter
import com.datadog.android.rum.internal.instrumentation.MainLooperLongTaskStrategy
import com.datadog.android.rum.internal.instrumentation.UserActionTrackingStrategyApi29
import com.datadog.android.rum.internal.instrumentation.UserActionTrackingStrategyLegacy
Expand Down Expand Up @@ -182,7 +185,13 @@ internal class RumFeature constructor(
}

override val requestFactory: RequestFactory by lazy {
RumRequestFactory(configuration.customEndpointUrl, sdkCore.internalLogger)
RumRequestFactory(
customEndpointUrl = configuration.customEndpointUrl,
viewEventFilter = RumViewEventFilter(
eventMetaDeserializer = RumEventMetaDeserializer(sdkCore.internalLogger)
),
internalLogger = sdkCore.internalLogger
)
}

override val storageConfiguration: FeatureStorageConfiguration =
Expand Down Expand Up @@ -217,7 +226,7 @@ internal class RumFeature constructor(
sdkCore: InternalSdkCore
): DataWriter<Any> {
return RumDataWriter(
serializer = MapperSerializer(
eventSerializer = MapperSerializer(
RumEventMapper(
sdkCore,
viewEventMapper = configuration.viewEventMapper,
Expand All @@ -230,6 +239,7 @@ internal class RumFeature constructor(
),
RumEventSerializer(sdkCore.internalLogger)
),
eventMetaSerializer = RumEventMetaSerializer(),
sdkCore = sdkCore
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.datadog.android.core.InternalSdkCore
import com.datadog.android.core.persistence.Serializer
import com.datadog.android.core.persistence.serializeToByteArray
import com.datadog.android.rum.GlobalRumMonitor
import com.datadog.android.rum.internal.domain.event.RumEventMeta
import com.datadog.android.rum.internal.monitor.AdvancedRumMonitor
import com.datadog.android.rum.internal.monitor.StorageEvent
import com.datadog.android.rum.model.ActionEvent
Expand All @@ -23,21 +24,38 @@ import com.datadog.android.rum.model.ResourceEvent
import com.datadog.android.rum.model.ViewEvent

internal class RumDataWriter(
internal val serializer: Serializer<Any>,
internal val eventSerializer: Serializer<Any>,
private val eventMetaSerializer: Serializer<RumEventMeta>,
private val sdkCore: InternalSdkCore
) : DataWriter<Any> {

// region DataWriter

@WorkerThread
override fun write(writer: EventBatchWriter, element: Any): Boolean {
val byteArray = serializer.serializeToByteArray(
val byteArray = eventSerializer.serializeToByteArray(
element,
sdkCore.internalLogger
) ?: return false

val batchEvent = if (element is ViewEvent) {
val eventMeta = RumEventMeta.View(
viewId = element.view.id,
documentVersion = element.dd.documentVersion
)
val serializedEventMeta =
eventMetaSerializer.serializeToByteArray(eventMeta, sdkCore.internalLogger)
?: EMPTY_BYTE_ARRAY
RawBatchEvent(
data = byteArray,
metadata = serializedEventMeta
)
} else {
RawBatchEvent(data = byteArray)
}

synchronized(this) {
val result = writer.write(RawBatchEvent(data = byteArray), null)
val result = writer.write(batchEvent, null)
if (result) {
onDataWritten(element, byteArray)
}
Expand Down Expand Up @@ -86,4 +104,8 @@ internal class RumDataWriter(
}

// endregion

companion object {
val EMPTY_BYTE_ARRAY = ByteArray(0)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.rum.internal.domain.event

import com.datadog.android.api.InternalLogger
import com.google.gson.JsonObject
import com.google.gson.JsonParseException
import com.google.gson.JsonParser
import java.util.Locale
import kotlin.jvm.Throws

internal sealed class RumEventMeta {

abstract val type: String

open fun toJson(): JsonObject {
val model = JsonObject()

model.addProperty(TYPE_KEY, type)

return model
}

data class View(
val viewId: String,
val documentVersion: Long
) : RumEventMeta() {

override val type: String = VIEW_TYPE_VALUE

override fun toJson(): JsonObject {
val model = super.toJson()

model.addProperty(VIEW_ID_KEY, viewId)
model.addProperty(DOCUMENT_VERSION_KEY, documentVersion)

return model
}
}

companion object {

private const val UNKNOWN_RUM_EVENT_META_TYPE_ERROR = "Unknown RUM event meta type value [%s]"
private const val UNABLE_TO_PARSE_JSON_INTO_META = "Unable to parse json into RUM event meta"

const val TYPE_KEY = "type"
const val VIEW_TYPE_VALUE = "view"
const val VIEW_ID_KEY = "viewId"
const val DOCUMENT_VERSION_KEY = "documentVersion"

@Suppress("ThrowsCount", "ThrowingInternalException")
@Throws(JsonParseException::class)
fun fromJson(jsonString: String, internalLogger: InternalLogger): RumEventMeta? {
return try {
@Suppress("UnsafeThirdPartyFunctionCall") // JsonParseException is handled by the caller
val model = JsonParser.parseString(jsonString).asJsonObject
when (val type = model.get(TYPE_KEY).asString) {
VIEW_TYPE_VALUE -> {
val viewId = model.get(VIEW_ID_KEY).asString
val docVersion = model.get(DOCUMENT_VERSION_KEY).asLong

View(viewId, docVersion)
}

else -> {
internalLogger.log(
InternalLogger.Level.ERROR,
InternalLogger.Target.USER,
{ UNKNOWN_RUM_EVENT_META_TYPE_ERROR.format(Locale.US, type) }
)
null
}
}
} catch (@Suppress("TooGenericExceptionCaught") e: NullPointerException) {
throw JsonParseException(UNABLE_TO_PARSE_JSON_INTO_META, e)
} catch (e: ClassCastException) {
throw JsonParseException(UNABLE_TO_PARSE_JSON_INTO_META, e)
} catch (e: IllegalStateException) {
throw JsonParseException(UNABLE_TO_PARSE_JSON_INTO_META, e)
} catch (e: NumberFormatException) {
throw JsonParseException(UNABLE_TO_PARSE_JSON_INTO_META, e)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.rum.internal.domain.event

import com.datadog.android.api.InternalLogger
import com.datadog.android.core.internal.persistence.Deserializer
import com.google.gson.JsonParseException

internal class RumEventMetaDeserializer(
private val internalLogger: InternalLogger
) : Deserializer<ByteArray, RumEventMeta> {
override fun deserialize(model: ByteArray): RumEventMeta? {
if (model.isEmpty()) return null

return try {
RumEventMeta.fromJson(String(model, Charsets.UTF_8), internalLogger)
} catch (e: JsonParseException) {
internalLogger.log(
InternalLogger.Level.ERROR,
InternalLogger.Target.USER,
{ DESERIALIZATION_ERROR },
e
)
null
}
}

companion object {
const val DESERIALIZATION_ERROR = "Failed to deserialize RUM event meta"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.rum.internal.domain.event

import com.datadog.android.core.persistence.Serializer

internal class RumEventMetaSerializer : Serializer<RumEventMeta> {
override fun serialize(model: RumEventMeta): String {
return when (model) {
is RumEventMeta.View -> model.toJson().toString()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.rum.internal.domain.event

import com.datadog.android.api.storage.RawBatchEvent
import com.datadog.android.core.internal.persistence.Deserializer
import kotlin.math.max

internal class RumViewEventFilter(
private val eventMetaDeserializer: Deserializer<ByteArray, RumEventMeta>
) {

fun filterOutRedundantViewEvents(batch: List<RawBatchEvent>): List<RawBatchEvent> {
val maxDocVersionByViewId = mutableMapOf<String, Long>()
val viewMetaByEvent = mutableMapOf<RawBatchEvent, RumEventMeta.View>()

batch.forEach {
val eventMeta = eventMetaDeserializer.deserialize(it.metadata)
if (eventMeta is RumEventMeta.View) {
viewMetaByEvent += it to eventMeta
val viewId = eventMeta.viewId
val documentVersion = eventMeta.documentVersion
val maxDocVersionSeen = maxDocVersionByViewId[viewId]
if (maxDocVersionSeen == null) {
maxDocVersionByViewId[viewId] = documentVersion
} else {
maxDocVersionByViewId[viewId] = max(documentVersion, maxDocVersionSeen)
}
}
}

return batch.filter {
if (viewMetaByEvent.containsKey(it)) {
@Suppress("UnsafeThirdPartyFunctionCall") // we checked the key before
val viewMeta = viewMetaByEvent.getValue(it)
// we need to leave only view event with a max doc version for a give viewId in the
// batch, because backend will do the same during the reduce process
@Suppress("UnsafeThirdPartyFunctionCall") // if there is a meta, there is a max doc version
viewMeta.documentVersion == maxDocVersionByViewId.getValue(viewMeta.viewId)
} else {
true
}
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ internal open class RumViewScope(
internal val cpuVitalMonitor: VitalMonitor,
internal val memoryVitalMonitor: VitalMonitor,
internal val frameRateVitalMonitor: VitalMonitor,
private val viewUpdatePredicate: ViewUpdatePredicate = DefaultViewUpdatePredicate(),
private val featuresContextResolver: FeaturesContextResolver = FeaturesContextResolver(),
internal val type: RumViewType = RumViewType.FOREGROUND,
private val trackFrustrations: Boolean,
Expand Down Expand Up @@ -653,9 +652,6 @@ internal open class RumViewScope(
@Suppress("LongMethod", "ComplexMethod")
private fun sendViewUpdate(event: RumRawEvent, writer: DataWriter<Any>) {
val viewComplete = isViewComplete()
if (!viewUpdatePredicate.canUpdateView(viewComplete, event)) {
return
}
attributes.putAll(GlobalRumMonitor.get(sdkCore).getAttributes())
version++

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ import com.datadog.android.api.net.RequestFactory
import com.datadog.android.api.storage.RawBatchEvent
import com.datadog.android.core.internal.utils.join
import com.datadog.android.rum.RumAttributes
import com.datadog.android.rum.internal.domain.event.RumViewEventFilter
import java.util.Locale
import java.util.UUID

internal class RumRequestFactory(
internal val customEndpointUrl: String?,
private val viewEventFilter: RumViewEventFilter,
private val internalLogger: InternalLogger
) : RequestFactory {

Expand All @@ -38,7 +40,8 @@ internal class RumRequestFactory(
context.source,
context.sdkVersion
),
body = batchData.map { it.data }
body = viewEventFilter.filterOutRedundantViewEvents(batchData)
.map { it.data }
.join(
separator = PAYLOAD_SEPARATOR,
internalLogger = internalLogger
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ internal class RumFeatureTest {

// Then
assertThat(testedFeature.dataWriter).isInstanceOf(RumDataWriter::class.java)
val serializer = (testedFeature.dataWriter as RumDataWriter).serializer
val serializer = (testedFeature.dataWriter as RumDataWriter).eventSerializer
assertThat(serializer).isInstanceOf(MapperSerializer::class.java)
val eventMapper = (serializer as MapperSerializer)
.getFieldValue<EventMapper<*>, MapperSerializer<*>>("eventMapper")
Expand Down
Loading

0 comments on commit ad4d00a

Please sign in to comment.