11package io.sentry.sqlite
22
33import io.sentry.IScopes
4+ import io.sentry.ISpan
45import io.sentry.Instrumenter
56import io.sentry.ScopesAdapter
67import io.sentry.SentryDate
78import io.sentry.SentryLongDate
9+ import io.sentry.SentryNanotimeDate
810import io.sentry.SentryStackTraceFactory
911import io.sentry.SpanDataConvention
1012import io.sentry.SpanStatus
13+ import java.util.Date
1114
1215private const val SQLITE_TRACE_ORIGIN = " auto.db.sqlite"
1316
14- /* * Shared span instrumentation for SQLite. */
17+ /* *
18+ * Sentinel for extracting a [SentryNanotimeDate]'s underlying [System.nanoTime] value via
19+ * [SentryDate.diff].
20+ */
21+ private val EMPTY_NANO_TIME = SentryNanotimeDate (Date (0 ), 0L )
22+
23+ /* * Span instrumentation for [SentrySQLiteDriver]. */
1524internal class SQLiteSpanInstrumentation (
1625 private val scopes : IScopes ,
1726 private val dbMetadata : DbMetadata ,
@@ -20,44 +29,32 @@ internal class SQLiteSpanInstrumentation(
2029 private val stackTraceFactory = SentryStackTraceFactory (scopes.options)
2130
2231 /* *
23- * Returns a start timestamp for a `db.sql.query` span.
32+ * Returns a timestamp in nanoseconds for use with [recordSpan]. Timestamp is ns-precise if the
33+ * active parent span uses a [SentryNanotimeDate] (the ordinary case); otherwise it's ms-precise.
2434 *
25- * Exposed so callers can capture a wall-clock start before accumulating database time.
26- * Internalizing the start time in [recordSpan] would shift spans to end-of-work on the trace
27- * timeline, which is less desirable .
35+ * Note: Internalizing the start time in [recordSpan] would shift spans to end-of-work on the
36+ * trace timeline, which is less desirable; callers capture the start before doing database work
37+ * and pass it back to [recordSpan] .
2838 */
29- fun startTimestamp (): SentryDate = scopes.options.dateProvider.now()
30-
31- /* * Records a `db.sql.query` span from [startTimestamp] to the moment of invocation. */
32- fun recordSpan (
33- sql : String ,
34- startTimestamp : SentryDate ,
35- status : SpanStatus ,
36- throwable : Throwable ? = null,
37- ) {
38- recordSpan(sql, startTimestamp, endTimestamp = null , status, throwable)
39- }
39+ fun startTimestamp (): Long =
40+ // Try to retain nanosecond precision + avoid SentryDate allocation...
41+ scopes.span?.childStartTimestampOrNull()
42+ // ...otherwise fall back to millisecond precision + allocate.
43+ ? : scopes.options.dateProvider.now().nanoTimestamp()
4044
41- /* * Records a `db.sql.query` span from [startTimestamp] to [startTimestamp] + [durationNanos] . */
45+ /* * Records a `db.sql.query` span. */
4246 fun recordSpan (
4347 sql : String ,
44- startTimestamp : SentryDate ,
48+ startTimestampNanos : Long ,
4549 durationNanos : Long ,
4650 status : SpanStatus ,
4751 throwable : Throwable ? = null,
4852 ) {
49- val endTimestamp = SentryLongDate (startTimestamp.nanoTimestamp() + durationNanos)
50- recordSpan(sql, startTimestamp, endTimestamp, status, throwable )
51- }
53+ val parent = scopes.span ? : return
54+ val startTimestamp = SentryLongDate (startTimestampNanos )
55+ val endTimestamp = SentryLongDate (startTimestampNanos + durationNanos)
5256
53- private fun recordSpan (
54- sql : String ,
55- startTimestamp : SentryDate ,
56- endTimestamp : SentryDate ? ,
57- status : SpanStatus ,
58- throwable : Throwable ? ,
59- ) {
60- scopes.span?.startChild(" db.sql.query" , sql, startTimestamp, Instrumenter .SENTRY )?.apply {
57+ parent.startChild(" db.sql.query" , sql, startTimestamp, Instrumenter .SENTRY ).apply {
6158 spanContext.origin = SQLITE_TRACE_ORIGIN
6259 throwable?.let { this .throwable = it }
6360
@@ -85,15 +82,43 @@ internal class SQLiteSpanInstrumentation(
8582 scopes : IScopes = ScopesAdapter .getInstance(),
8683 ): SQLiteSpanInstrumentation =
8784 SQLiteSpanInstrumentation (scopes, dbMetadataFromFileName(fileName))
85+ }
86+ }
8887
89- /* *
90- * Returns [SQLiteSpanInstrumentation] based on
91- * [SupportSQLiteOpenHelper.databaseName][androidx.sqlite.db.SupportSQLiteOpenHelper.databaseName].
92- */
93- fun fromDatabaseName (
94- databaseName : String? ,
95- scopes : IScopes = ScopesAdapter .getInstance(),
96- ): SQLiteSpanInstrumentation =
97- SQLiteSpanInstrumentation (scopes, dbMetadataFromDatabaseName(databaseName))
88+ /* *
89+ * Computes a start timestamp with nanosecond precision for the child of the receiver span. Returns
90+ * null if nanosecond precision isn't possible.
91+ *
92+ * Lets us improve the display of spans in the Sentry UI. If timestamps are only ms-precise, the
93+ * Sentry UI will left-align and arbitrarily reorder spans that share the same wall clock ms:
94+ * ```
95+ * (Relative start times out of order)
96+ * ↓
97+ * Parent span ├█████████████┤
98+ * END TRANSACTION ├███┤ 0.33 ms
99+ * BEGIN IMMEDIATE TRANSACTION ├████┤ 0.02 ms
100+ * INSERT INTO `my_db` … ├██┤ 0.30 ms
101+ * ↑
102+ * (All spans share the same ms baseline
103+ * even though their execution was staggered)
104+ * ```
105+ *
106+ * Nanosecond precision ensures proper ordering and lets the spans stagger:
107+ * ```
108+ * Parent span ├█████████████┤
109+ * BEGIN IMMEDIATE TRANSACTION ├████┤ 0.02 ms
110+ * INSERT INTO `my_db` … ├██┤ 0.30 ms
111+ * END TRANSACTION ├███┤ 0.33 ms
112+ * ```
113+ */
114+ internal fun ISpan.childStartTimestampOrNull (): Long? {
115+ if (startDate !is SentryNanotimeDate ) {
116+ return null
98117 }
118+
119+ val parentWallClockNanos = startDate.nanoTimestamp()
120+ val parentMonotonicNanos = startDate.diff(EMPTY_NANO_TIME )
121+ val elapsedSinceParentStart = System .nanoTime() - parentMonotonicNanos
122+ // Return the child's absolute start time.
123+ return parentWallClockNanos + elapsedSinceParentStart
99124}
0 commit comments