perf(tor): stop wiping Arti's consensus cache on every start

TorService.start() called clearArtiCache() on every cold start (and every
reset, since reset flips initialized back to false), deleting the
consensus/microdescriptor cache and forcing a full re-download on the next
bootstrap. Measured on device, that turned a ~7.7s warm bootstrap into ~24.2s
— a ~3.2x slowdown paid on every launch.

The wipe was an early attempt at what we later understood to be the wedged-
guard problem (now handled by noUsableGuards()/clearAllArtiData()). It never
actually helped: guards live in state/, not cache/, so wiping the cache can't
fix a stale guard sample; and Arti already validates consensus freshness and
refetches whatever has expired, so there is no stale-consensus risk to guard
against here. The reset/clean-state self-heal paths still call
clearAllArtiData() for genuine corruption recovery.

Drop clearArtiCache() entirely and preserve the cache for warm bootstraps.
Also add bootstrap-timing instrumentation: the "SOCKS proxy active" log now
reports elapsed bootstrap ms, and a cache-size log line correlates cache
state with bootstrap time.

Verified on device: warm bootstrap 7,670ms vs 24,200ms cold, with no
regression — a .onion relay and 200 clearnet relays connected over Tor, 0
pre-ready doomed dials, guards healthy (59/60 usable).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-06-16 11:57:50 -04:00
co-authored by Claude Opus 4.8
parent 40921b80d2
commit 4f668f6175
@@ -54,6 +54,13 @@ class TorService(
private val initialized = AtomicBoolean(false)
private val proxyRunning = AtomicBoolean(false)
/**
* Wall-clock at which the current `initialize()` started, so the bootstrap log can
* report the elapsed time to "Sufficiently bootstrapped". -1 when no init is in flight.
* Diagnostic only.
*/
@Volatile private var bootstrapStartedAtMs: Long = -1L
/**
* Serializes every native lifecycle transition ([start], [stop], [reset],
* [resetWithCleanState]). [ArtiNative] is a process-global singleton over a
@@ -72,18 +79,11 @@ class TorService(
private fun artiDataDir() = File(context.filesDir, "arti")
/**
* Clears the Arti cache directory (consensus, relay descriptors) while
* preserving the state directory (guard selection). This forces a fresh
* consensus download on the next bootstrap, preventing stale cached data
* from causing circuit failures.
*/
private fun clearArtiCache() {
/** Diagnostic: total bytes of the consensus/descriptor cache, to correlate with bootstrap time. */
private fun cacheSizeBytes(): Long {
val cacheDir = File(artiDataDir(), "cache")
if (cacheDir.exists()) {
cacheDir.deleteRecursively()
Log.d("TorService") { "Cleared Arti cache directory" }
}
if (!cacheDir.exists()) return 0
return cacheDir.walkBottomUp().filter { it.isFile }.sumOf { it.length() }
}
/**
@@ -196,15 +196,23 @@ class TorService(
// not resurrect a stale Active status.
if (proxyRunning.get()) {
_status.value = TorServiceStatus.Active(socksPort)
Log.d("TorService") { "Arti SOCKS proxy active on port $socksPort" }
val startedAt = bootstrapStartedAtMs
val elapsed = if (startedAt > 0) System.currentTimeMillis() - startedAt else -1
Log.d("TorService") { "Arti SOCKS proxy active on port $socksPort (bootstrap took ${elapsed}ms)" }
}
}
}
}
// Clear cached consensus/descriptors so Arti bootstraps with
// fresh network data, preventing stale guards/circuits.
clearArtiCache()
// Preserve the consensus/descriptor cache across cold starts. We used to
// wipe it on every launch, which forced a full microdescriptor consensus
// re-download and turned a ~7s warm bootstrap into ~24s. That wipe was an
// early attempt at what turned out to be the wedged-guard problem (now
// handled by [noUsableGuards]); it never actually helped, since guards live
// in state/ — not cache/ — and Arti already validates consensus freshness
// and refetches whatever has expired. The reset/clean-state paths still call
// clearAllArtiData() for genuine corruption recovery.
Log.d("TorService") { "Preserving Arti cache for warm bootstrap (cache size: ${cacheSizeBytes()} bytes)" }
// Self-heal the wedged guard sample (see [noUsableGuards]): if
// the persisted sample has no usable guard left, Arti can
@@ -219,6 +227,7 @@ class TorService(
val dataDir = artiDataDir().absolutePath
Log.d("TorService") { "Initializing Arti with data dir: $dataDir" }
bootstrapStartedAtMs = System.currentTimeMillis()
var initResult = ArtiNative.initialize(dataDir)
if (initResult != 0) {
Log.e("TorService") { "Failed to initialize Arti: error $initResult, clearing data and retrying" }