What is ValueTimeMark, why is it a value class, and what subtle pitfalls exist with monotonic marks (wraparound, no calendar meaning, cross-platform reading)?
answer
- ValueTimeMark = @JvmInline value class (no alloc)
- Boxing if stored as TimeMark/Any/in a List
- No epoch, no calendar conversion
- nanoTime is per-process; don't persist/compare across runs
- Duration saturates to INFINITE, not overflow-wrap
basics
~20 sValueTimeMark is the inline value-class TimeMark returned by Monotonic, so creating marks allocates nothing. Marks have no calendar meaning, are only comparable within one source, and rely on the platform's monotonic timer, which differs per platform.
solid answer
~40 sTimeSource.Monotonic.markNow() returns TimeSource.Monotonic.ValueTimeMark, an @JvmInline value class wrapping a single Long reading. Being a value class means a mark is essentially a primitive at runtime — markNow() and arithmetic avoid heap allocation in hot loops. Pitfalls: (1) a monotonic mark has no relationship to wall-clock/calendar time — you cannot format it as a date. (2) Marks/arithmetic are only meaningful within the same TimeSource; mixing sources is undefined. (3) The underlying reading (System.nanoTime on JVM, platform timers elsewhere) is platform-specific and unspecified in absolute terms; only differences matter. (4) Resolution and behavior differ by platform; on JVM nanoTime is monotonic per JVM but you should not compare nanoTime values across JVMs/processes. Duration math saturates at Duration.INFINITE rather than overflowing.
code
kotlin · 12 linesimport kotlin.time.TimeSource
import kotlin.time.Duration
fun main() {
val m: TimeSource.Monotonic.ValueTimeMark = TimeSource.Monotonic.markNow()
// saturation, not overflow:
val farFuture = m + Duration.INFINITE
println(farFuture.hasPassedNow()) // false, always
// boxing happens here (stored as the TimeMark interface):
val boxed: List<kotlin.time.TimeMark> = listOf(m)
println(boxed.size)
}go deeper
Knows a monotonic mark is not a date and only measures elapsed time.
States the same-source rule and that the absolute reading is meaningless.
Explains ValueTimeMark as an inline value class (alloc-free, but boxes when erased) and the platform-specific backing.
Reasons about saturation vs overflow, cross-process invalidity, and sets guidance for when to use marks vs Clock/Instant in a system.
## ValueTimeMark: the zero-allocation mark When you call `TimeSource.Monotonic.markNow()`, the concrete return type is **`TimeSource.Monotonic.ValueTimeMark`**. It is declared as an **`@JvmInline value class`** wrapping a single `Long` reading. A **value class** (inline class) holds one underlying value and, where the compiler can, is represented at runtime as that raw value — no wrapper object on the heap. So in a tight loop: ```kotlin for (i in 0 until 1_000_000) { val m = TimeSource.Monotonic.markNow() // no heap allocation val e = m.elapsedNow() } ``` the marks don't generate GC pressure. (Boxing can still occur if you store a `ValueTimeMark` as a generic `TimeMark`/`Any`, or in a nullable/`List<TimeMark>`.) ## Pitfall 1 — no calendar meaning A monotonic mark is a reading of a steady counter with **no defined epoch**. There is no method to convert it to a date/`Instant`. If you need 'what time did this happen,' capture a wall-clock `Instant` (via `Clock`) separately. ## Pitfall 2 — same-source only Comparison and subtraction (`a - b`, `a < b`) are only valid for marks from the **same** `TimeSource`. A `Monotonic` mark and a `TestTimeSource` mark are not comparable in any meaningful way. ## Pitfall 3 — platform-specific backing `Monotonic` maps to the platform's monotonic timer: `System.nanoTime()` on the JVM, `mach_absolute_time`/`clock_gettime` family on native, `performance.now()`/`process.hrtime` on JS depending on target. The absolute value is meaningless; only differences between two marks are. Never persist a raw mark and compare it after a restart or across processes — `nanoTime` has no cross-process meaning. ## Pitfall 4 — saturation, not overflow `Duration` is internally a packed `Long` (nanos or millis). Arithmetic that would overflow **saturates to `Duration.INFINITE`** (or `-INFINITE`) instead of wrapping to a negative value. So `mark + hugeDuration` yields a far-future mark whose `hasPassedNow()` is always false rather than a corrupted past mark. This is safer than raw `nanoTime` subtraction, which *can* wrap if a process runs for centuries. ## Practical guidance - Use marks only for short-to-medium elapsed measurement and deadlines within a single process run. - For audit timestamps / persisted times, use `Clock`/`Instant`. - Treat the absolute reading as opaque; only `elapsedNow()` and mark differences are contractually meaningful.
- When does a ValueTimeMark get boxed despite being a value class?When it is used where a reference type is required: stored as the TimeMark/Any interface, made nullable (TimeMark?), or put in a generic collection like List<TimeMark>.
- Why can't you safely persist a monotonic mark and compare it after a restart?The backing reading (e.g. System.nanoTime) has no defined epoch and no cross-process meaning; after a restart the counter origin differs, so the comparison is meaningless.
A lap counter on a treadmill: the raw number on the display is meaningless on its own; only the difference between two readings tells you how far you ran, and you can't compare it to someone else's treadmill.
saying these in an interview costs you the question
- Trying to format a Monotonic mark as a calendar date
- Persisting a nanoTime-based mark and comparing it across runs
- Assuming Duration arithmetic overflow-wraps instead of saturating
- Claiming ValueTimeMark never boxes under any circumstance
- Comparing marks across different platforms/sources as if equivalent