Skip to content

Commit d29d3f8

Browse files
committed
chore(android-sqlite): Repair start times of spans generated by SentrySQLiteDriver (JAVA-275)
Repairs the nanoTimetamp of the SentryNanotimeDates used as start times for the spans generated by SentrySQLiteDriver. Without those repairs, all spans within a given wall clock millisecond are displayed by Sentry UI as starting at that same millisecond and are re-ordered arbitrarily. Often that's quite confusing as actual BEGIN -> EXECUTE STATEMENT -> END sequences can appear as EXECUTE STATEMENT -> END -> BEGIN (etc.). For more details, see the discussion [here](#5504 (comment)).
1 parent 007c537 commit d29d3f8

3 files changed

Lines changed: 325 additions & 18 deletions

File tree

sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SQLiteSpanManager.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ internal class SQLiteSpanManager(
4141
if (result is CrossProcessCursor) {
4242
return SentryCrossProcessCursor(result, this, sql) as T
4343
}
44-
spans.recordSpan(sql, startTimestamp, SpanStatus.OK)
44+
spans.recordCoarseSpan(sql, startTimestamp, SpanStatus.OK)
4545
result
4646
} catch (e: Throwable) {
47-
spans.recordSpan(sql, startTimestamp, SpanStatus.INTERNAL_ERROR, e)
47+
spans.recordCoarseSpan(sql, startTimestamp, SpanStatus.INTERNAL_ERROR, e)
4848
throw e
4949
}
5050
}

sentry-android-sqlite/src/main/java/io/sentry/sqlite/SQLiteSpanInstrumentation.kt

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package io.sentry.sqlite
22

33
import io.sentry.IScopes
4+
import io.sentry.ISpan
45
import io.sentry.Instrumenter
56
import io.sentry.ScopesAdapter
67
import io.sentry.SentryDate
78
import io.sentry.SentryLongDate
9+
import io.sentry.SentryNanotimeDate
810
import io.sentry.SentryStackTraceFactory
911
import io.sentry.SpanDataConvention
1012
import io.sentry.SpanStatus
@@ -28,36 +30,44 @@ internal class SQLiteSpanInstrumentation(
2830
*/
2931
fun startTimestamp(): SentryDate = scopes.options.dateProvider.now()
3032

31-
/** Records a `db.sql.query` span from [startTimestamp] to the moment of invocation. */
33+
/** Records a `db.sql.query` span from [startTimestamp] to [startTimestamp] + [durationNanos]. */
3234
fun recordSpan(
3335
sql: String,
3436
startTimestamp: SentryDate,
37+
durationNanos: Long,
3538
status: SpanStatus,
3639
throwable: Throwable? = null,
3740
) {
38-
recordSpan(sql, startTimestamp, endTimestamp = null, status, throwable)
41+
val parent = scopes.span ?: return
42+
val nanoPrecisionStart = startTimestamp.repairPrecision(baseline = parent.startDate)
43+
val endTimestamp = SentryLongDate(nanoPrecisionStart.nanoTimestamp() + durationNanos)
44+
parent.recordChild(sql, nanoPrecisionStart, endTimestamp, status, throwable)
3945
}
4046

41-
/** Records a `db.sql.query` span from [startTimestamp] to [startTimestamp] + [durationNanos]. */
42-
fun recordSpan(
47+
/**
48+
* Records a `db.sql.query` span from [startTimestamp] to the moment of invocation.
49+
*
50+
* "Coarse" in that it doesn't ensure nanosecond precision for [SentryNanotimeDate]
51+
* [startTimestamp]s.
52+
*/
53+
fun recordCoarseSpan(
4354
sql: String,
4455
startTimestamp: SentryDate,
45-
durationNanos: Long,
4656
status: SpanStatus,
4757
throwable: Throwable? = null,
4858
) {
49-
val endTimestamp = SentryLongDate(startTimestamp.nanoTimestamp() + durationNanos)
50-
recordSpan(sql, startTimestamp, endTimestamp, status, throwable)
59+
val parent = scopes.span ?: return
60+
parent.recordChild(sql, startTimestamp, endTimestamp = null, status, throwable)
5161
}
5262

53-
private fun recordSpan(
63+
private fun ISpan.recordChild(
5464
sql: String,
5565
startTimestamp: SentryDate,
5666
endTimestamp: SentryDate?,
5767
status: SpanStatus,
5868
throwable: Throwable?,
5969
) {
60-
scopes.span?.startChild("db.sql.query", sql, startTimestamp, Instrumenter.SENTRY)?.apply {
70+
startChild("db.sql.query", sql, startTimestamp, Instrumenter.SENTRY).apply {
6171
spanContext.origin = SQLITE_TRACE_ORIGIN
6272
throwable?.let { this.throwable = it }
6373

@@ -74,6 +84,38 @@ internal class SQLiteSpanInstrumentation(
7484
}
7585
}
7686

87+
/**
88+
* Repairs the receiver's [nanoTimestamp][SentryDate.nanoTimestamp] if needed so that it actually
89+
* has nanosecond precision.
90+
*
91+
* Designed for use with spans whose start timestamps are [SentryNanotimeDate]s. Without repair,
92+
* those timestamps will be aligned to the same millisecond at transport, and the Sentry UI will
93+
* arbitrarily reorder them:
94+
* ```
95+
* Parent span ├█████████████┤
96+
* END TRANSACTION ├███┤ 0.18 ms ← (Wrong order)
97+
* BEGIN IMMEDIATE TRANSACTION ├████┤ 0.25 ms
98+
* INSERT INTO `my_db` … ├██┤ 0.10 ms
99+
* ↑
100+
* (All spans share the same ms baseline
101+
* even though their execution was staggered)
102+
* ```
103+
*
104+
* Repair ensures proper ordering and lets the spans stagger:
105+
* ```
106+
* Parent span ├█████████████┤
107+
* BEGIN IMMEDIATE TRANSACTION ├████┤ 0.25 ms
108+
* INSERT INTO `my_db` … ├██┤ 0.10 ms
109+
* END TRANSACTION ├███┤ 0.18 ms
110+
* ```
111+
*/
112+
private fun SentryDate.repairPrecision(baseline: SentryDate?): SentryDate =
113+
if (baseline is SentryNanotimeDate) {
114+
SentryLongDate(baseline.laterDateNanosTimestampByDiff(this))
115+
} else {
116+
this
117+
}
118+
77119
companion object {
78120

79121
fun fromDatabaseName(databaseName: String?, scopes: IScopes = ScopesAdapter.getInstance()) =

0 commit comments

Comments
 (0)