Skip to content

Commit 3eb3a3f

Browse files
committed
Make repair method internal + add unit tests
1 parent 8d18e1c commit 3eb3a3f

3 files changed

Lines changed: 138 additions & 114 deletions

File tree

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

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,17 @@ internal class SQLiteSpanInstrumentation(
3939
throwable: Throwable? = null,
4040
) {
4141
val parent = scopes.span ?: return
42-
val nanoPrecisionStart = startTimestamp.repairPrecision(baseline = parent.startDate)
42+
val nanoPrecisionStart = startTimestamp.repairPrecision(anchor = parent.startDate)
4343
val endTimestamp = SentryLongDate(nanoPrecisionStart.nanoTimestamp() + durationNanos)
4444
parent.recordChild(sql, nanoPrecisionStart, endTimestamp, status, throwable)
4545
}
4646

4747
/**
4848
* Records a `db.sql.query` span from [startTimestamp] to the moment of invocation.
4949
*
50-
* "Coarse" in that it doesn't ensure nanosecond precision for [SentryNanotimeDate]
51-
* [startTimestamp]s.
50+
* "Coarse" in that it doesn't try to restore nanosecond precision for the start timestamp. Spans
51+
* that start within the same wall clock millisecond will share the same start time and may be
52+
* arbitrarily re-ordered by the Sentry UI.
5253
*/
5354
fun recordCoarseSpan(
5455
sql: String,
@@ -84,38 +85,6 @@ internal class SQLiteSpanInstrumentation(
8485
}
8586
}
8687

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-
11988
companion object {
12089

12190
/**
@@ -139,3 +108,39 @@ internal class SQLiteSpanInstrumentation(
139108
SQLiteSpanInstrumentation(scopes, dbMetadataFromDatabaseName(databaseName))
140109
}
141110
}
111+
112+
/**
113+
* Repairs the receiver's [nanoTimestamp][SentryDate.nanoTimestamp] if needed so that it actually
114+
* has nanosecond precision.
115+
*
116+
* Designed for use with spans whose start timestamps are [SentryNanotimeDate]s. Without repair,
117+
* those timestamps will be aligned to the same millisecond at transport, and the Sentry UI will
118+
* arbitrarily reorder them:
119+
* ```
120+
* (Relative start times out of order)
121+
* ↓
122+
* Parent span ├█████████████┤
123+
* END TRANSACTION ├███┤ 0.33 ms
124+
* BEGIN IMMEDIATE TRANSACTION ├████┤ 0.02 ms
125+
* INSERT INTO `my_db` … ├██┤ 0.30 ms
126+
* ↑
127+
* (All spans share the same ms baseline
128+
* even though their execution was staggered)
129+
* ```
130+
*
131+
* Repair ensures proper ordering and lets the spans stagger:
132+
* ```
133+
* Parent span ├█████████████┤
134+
* BEGIN IMMEDIATE TRANSACTION ├████┤ 0.02 ms
135+
* INSERT INTO `my_db` … ├██┤ 0.30 ms
136+
* END TRANSACTION ├███┤ 0.33 ms
137+
* ```
138+
*/
139+
internal fun SentryDate.repairPrecision(anchor: SentryDate?): SentryDate =
140+
if (anchor is SentryNanotimeDate) {
141+
// Compute a new timestamp with nanosecond precision by using the anchor as the epoch instant
142+
// and adding to it the diff of this.nanos - anchor.nanos.
143+
SentryLongDate(anchor.laterDateNanosTimestampByDiff(this))
144+
} else {
145+
this
146+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package io.sentry.sqlite
2+
3+
import io.sentry.DateUtils
4+
import io.sentry.SentryLongDate
5+
import io.sentry.SentryNanotimeDate
6+
import java.util.Date
7+
import kotlin.test.Test
8+
import kotlin.test.assertEquals
9+
import kotlin.test.assertIs
10+
import kotlin.test.assertSame
11+
12+
class RepairPrecisionTest {
13+
14+
@Test
15+
fun `repairs timestamp precision using nanotime diff from anchor`() {
16+
val anchor = SentryNanotimeDate(Date(1_000_000L), 100_000_000L)
17+
val date = SentryNanotimeDate(Date(1_000_000L), 100_500_000L)
18+
19+
val repaired = date.repairPrecision(anchor)
20+
21+
assertIs<SentryLongDate>(repaired)
22+
val anchorNanoTimestamp = DateUtils.millisToNanos(1_000_000L)
23+
assertEquals(anchorNanoTimestamp + 500_000L, repaired.nanoTimestamp())
24+
}
25+
26+
@Test
27+
fun `two dates in the same millisecond produce distinct ordered timestamps`() {
28+
val anchor = SentryNanotimeDate(Date(1_000_000L), 100_000_000L)
29+
val earlier = SentryNanotimeDate(Date(1_000_000L), 100_200_000L)
30+
val later = SentryNanotimeDate(Date(1_000_000L), 100_800_000L)
31+
32+
assertEquals(
33+
earlier.nanoTimestamp(),
34+
later.nanoTimestamp(),
35+
"Raw timestamps share the same ms-quantized value",
36+
)
37+
38+
val anchorNanoTimestamp = DateUtils.millisToNanos(1_000_000L)
39+
val repairedEarlier = earlier.repairPrecision(anchor)
40+
val repairedLater = later.repairPrecision(anchor)
41+
assertEquals(anchorNanoTimestamp + 200_000L, repairedEarlier.nanoTimestamp())
42+
assertEquals(anchorNanoTimestamp + 800_000L, repairedLater.nanoTimestamp())
43+
}
44+
45+
@Test
46+
fun `returns anchor nano timestamp when date matches anchor nanos`() {
47+
val anchor = SentryNanotimeDate(Date(1_000_000L), 100_000_000L)
48+
val date = SentryNanotimeDate(Date(1_000_000L), 100_000_000L)
49+
50+
val repaired = date.repairPrecision(anchor)
51+
52+
assertIs<SentryLongDate>(repaired)
53+
assertEquals(anchor.nanoTimestamp(), repaired.nanoTimestamp())
54+
}
55+
56+
@Test
57+
fun `repairs SentryLongDate receiver to anchor millisecond baseline`() {
58+
val anchor = SentryNanotimeDate(Date(1_000_000L), 100_000_000L)
59+
val date = SentryLongDate(DateUtils.millisToNanos(1_000_000L))
60+
61+
val repaired = date.repairPrecision(anchor)
62+
63+
assertIs<SentryLongDate>(repaired)
64+
assertEquals(anchor.nanoTimestamp(), repaired.nanoTimestamp())
65+
}
66+
67+
@Test
68+
fun `works when date and anchor span have different wall clock times`() {
69+
val anchor = SentryNanotimeDate(Date(1_000_000L), 100_000_000L)
70+
val date = SentryNanotimeDate(Date(1_000_001L), 101_500_000L)
71+
72+
val repaired = date.repairPrecision(anchor)
73+
74+
val anchorNanoTimestamp = DateUtils.millisToNanos(1_000_000L)
75+
assertEquals(anchorNanoTimestamp + 1_500_000L, repaired.nanoTimestamp())
76+
}
77+
78+
@Test
79+
fun `returns self when anchor is not SentryNanotimeDate`() {
80+
val date = SentryNanotimeDate(Date(1_000_000L), 100_000_000L)
81+
val anchor = SentryLongDate(DateUtils.millisToNanos(1_000_000L))
82+
assertSame(date, date.repairPrecision(anchor = anchor))
83+
}
84+
85+
@Test
86+
fun `returns self when anchor is null`() {
87+
val date = SentryNanotimeDate(Date(1_000_000L), 100_000_000L)
88+
assertSame(date, date.repairPrecision(anchor = null))
89+
}
90+
}

sentry-android-sqlite/src/test/java/io/sentry/sqlite/SQLiteSpanInstrumentationTest.kt

Lines changed: 8 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package io.sentry.sqlite
22

33
import io.sentry.IScopes
44
import io.sentry.SentryDateProvider
5+
import io.sentry.SentryLongDate
56
import io.sentry.SentryNanotimeDate
67
import io.sentry.SentryOptions
78
import io.sentry.SentryTracer
@@ -13,6 +14,7 @@ import java.util.Date
1314
import kotlin.test.Test
1415
import kotlin.test.assertEquals
1516
import kotlin.test.assertFalse
17+
import kotlin.test.assertIs
1618
import kotlin.test.assertNotNull
1719
import kotlin.test.assertNull
1820
import kotlin.test.assertTrue
@@ -149,7 +151,7 @@ class SQLiteSpanInstrumentationTest {
149151
}
150152

151153
@Test
152-
fun `recordSpan repairs start precision when parent uses SentryNanotimeDate`() {
154+
fun `recordSpan repairs start precision`() {
153155
val sameMillis = Date(1_000_000L)
154156
val parentNanos = 100_000_000L
155157
val childNanos = 100_500_000L
@@ -168,75 +170,14 @@ class SQLiteSpanInstrumentationTest {
168170

169171
val parentStart = fixture.sentryTracer.startDate
170172
val expectedStart = parentStart.laterDateNanosTimestampByDiff(start)
173+
// Child and parent share the same ms-quantized baseline; repair adds nanosecond offset.
174+
assertEquals(parentStart.nanoTimestamp(), start.nanoTimestamp())
175+
assertTrue(start.nanoTimestamp() != expectedStart)
176+
assertIs<SentryLongDate>(span.startDate)
171177
assertEquals(expectedStart, span.startDate.nanoTimestamp())
172178
assertEquals(expectedStart + durationNanos, span.finishDate!!.nanoTimestamp())
173179
}
174180

175-
@Test
176-
fun `recordSpan gives distinct ordered starts within the same millisecond`() {
177-
val sameMillis = Date(1_000_000L)
178-
val parentNanos = 100_000_000L
179-
val child1Nanos = 100_200_000L
180-
val child2Nanos = 100_800_000L
181-
182-
val sut =
183-
setUpWithNanotimeDates(
184-
SentryNanotimeDate(sameMillis, parentNanos),
185-
SentryNanotimeDate(sameMillis, child1Nanos),
186-
SentryNanotimeDate(sameMillis, child2Nanos),
187-
)
188-
val start1 = sut.startTimestamp()
189-
val start2 = sut.startTimestamp()
190-
191-
assertEquals(
192-
start1.nanoTimestamp(),
193-
start2.nanoTimestamp(),
194-
"Raw starts share the same ms-quantized timestamp",
195-
)
196-
197-
sut.recordSpan("SELECT 1", start1, 1_000_000, SpanStatus.OK)
198-
sut.recordSpan("SELECT 2", start2, 1_000_000, SpanStatus.OK)
199-
200-
val span1 = fixture.sentryTracer.children[0]
201-
val span2 = fixture.sentryTracer.children[1]
202-
203-
assertTrue(
204-
span1.startDate.nanoTimestamp() < span2.startDate.nanoTimestamp(),
205-
"Repaired starts should be distinct and ordered",
206-
)
207-
}
208-
209-
@Test
210-
fun `recordSpan preserves exact duration after precision repair`() {
211-
val sameMillis = Date(1_000_000L)
212-
val sut =
213-
setUpWithNanotimeDates(
214-
SentryNanotimeDate(sameMillis, 100_000_000L),
215-
SentryNanotimeDate(sameMillis, 100_750_000L),
216-
)
217-
val start = sut.startTimestamp()
218-
val durationNanos = 123_456L
219-
220-
sut.recordSpan("SELECT 1", start, durationNanos, SpanStatus.OK)
221-
222-
val span = fixture.sentryTracer.children.first()
223-
val actualDuration = span.finishDate!!.nanoTimestamp() - span.startDate.nanoTimestamp()
224-
assertEquals(durationNanos, actualDuration)
225-
}
226-
227-
@Test
228-
fun `recordSpan does not repair start when parent is not SentryNanotimeDate`() {
229-
val sut = fixture.getSut(isTransactionActive = true)
230-
val start = sut.startTimestamp()
231-
val durationNanos = 1_000_000L
232-
233-
sut.recordSpan("SELECT 1", start, durationNanos, SpanStatus.OK)
234-
235-
val span = fixture.sentryTracer.children.first()
236-
assertEquals(start.nanoTimestamp(), span.startDate.nanoTimestamp())
237-
assertEquals(start.nanoTimestamp() + durationNanos, span.finishDate!!.nanoTimestamp())
238-
}
239-
240181
@Test
241182
fun `recordCoarseSpan records a span if a transaction is active`() {
242183
val sut = fixture.getSut(isTransactionActive = true)
@@ -346,7 +287,7 @@ class SQLiteSpanInstrumentationTest {
346287
}
347288

348289
@Test
349-
fun `recordCoarseSpan does not repair start precision when parent uses SentryNanotimeDate`() {
290+
fun `recordCoarseSpan does not repair start precision`() {
350291
val sameMillis = Date(1_000_000L)
351292
val parentNanos = 100_000_000L
352293
val childNanos = 100_500_000L
@@ -367,18 +308,6 @@ class SQLiteSpanInstrumentationTest {
367308
assertTrue(span.finishDate!!.nanoTimestamp() >= span.startDate.nanoTimestamp())
368309
}
369310

370-
@Test
371-
fun `recordCoarseSpan does not repair start when parent is not SentryNanotimeDate`() {
372-
val sut = fixture.getSut(isTransactionActive = true)
373-
val start = sut.startTimestamp()
374-
375-
sut.recordCoarseSpan("SELECT 1", start, SpanStatus.OK)
376-
377-
val span = fixture.sentryTracer.children.first()
378-
assertEquals(start.nanoTimestamp(), span.startDate.nanoTimestamp())
379-
assertTrue(span.finishDate!!.nanoTimestamp() >= start.nanoTimestamp())
380-
}
381-
382311
@Test
383312
fun `fromFileName sets db name from fileName when using recordSpan`() {
384313
val options = SentryOptions().apply { dsn = "https://key@sentry.io/proj" }

0 commit comments

Comments
 (0)