From 73b1503476a715bd99ebfce4f42f473b12d78f70 Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Wed, 8 Jul 2026 15:22:48 +0300 Subject: [PATCH] perf: memoize derived path/query projections on Uri and Url Uri and Url are immutable value objects, yet several derived projections were recomputed on every access while their siblings (canonicalUri / canonicalHref, decodedPath, decodedPathSegments) were already memoized with `by lazy`: - Uri.queryParameters() re-parsed the raw query on each call. - Uri.encodedPath re-joined every path segment on each access. - Url.pathSegments percent-decoded every segment on each access, so fileName() / fileExtension() decoded the path twice. - Url.encodedPath and Url.queryParameters recomputed likewise. Back each with a private `by lazy` field, mirroring the existing canonical/decoded caches. Public signatures are unchanged and every cached value is immutable (a String, a read-only segment list, or the QueryParameters snapshot whose entries are copied), so the change is behavior-preserving and observable only as fewer allocations on repeated reads of the same value. --- kuri/src/commonMain/kotlin/org/dexpace/kuri/Uri.kt | 8 ++++++-- kuri/src/commonMain/kotlin/org/dexpace/kuri/Url.kt | 13 ++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/kuri/src/commonMain/kotlin/org/dexpace/kuri/Uri.kt b/kuri/src/commonMain/kotlin/org/dexpace/kuri/Uri.kt index cf33f6a..0248e7a 100644 --- a/kuri/src/commonMain/kotlin/org/dexpace/kuri/Uri.kt +++ b/kuri/src/commonMain/kotlin/org/dexpace/kuri/Uri.kt @@ -158,7 +158,7 @@ public class Uri internal constructor( * * @return the decoded, ordered, duplicate-preserving snapshot; empty when there is no query. */ - public fun queryParameters(): QueryParameters = QueryParameters.parseOrEmpty(query) + public fun queryParameters(): QueryParameters = queryParameterSnapshot /** * The last non-empty decoded path segment — the "file name" — or `""` when the path has none. @@ -213,7 +213,7 @@ public class Uri internal constructor( */ @get:JvmName("encodedPath") public val encodedPath: String - get() = components.path.toUriPathString() + get() = encodedPathText /** * Returns a [Builder] pre-filled with this URI's components, for producing a modified copy. @@ -524,6 +524,10 @@ public class Uri internal constructor( private val decodedPath: String by lazy { computeDecodedPath() } private val decodedPathSegments: List by lazy { computeDecodedPathSegments() } + /** Encoded path and query snapshot, computed once each; both are immutable, mirroring [canonicalUri]. */ + private val encodedPathText: String by lazy { components.path.toUriPathString() } + private val queryParameterSnapshot: QueryParameters by lazy { QueryParameters.parseOrEmpty(query) } + /** * Percent-decodes the stored path — an opaque path whole, else each segment — backing [path]. * diff --git a/kuri/src/commonMain/kotlin/org/dexpace/kuri/Url.kt b/kuri/src/commonMain/kotlin/org/dexpace/kuri/Url.kt index f0fea3b..f851832 100644 --- a/kuri/src/commonMain/kotlin/org/dexpace/kuri/Url.kt +++ b/kuri/src/commonMain/kotlin/org/dexpace/kuri/Url.kt @@ -137,12 +137,12 @@ public class Url internal constructor( /** The decoded path segments in order (read-only); an opaque path yields its single decoded value. */ @get:JvmName("pathSegments") public val pathSegments: List - get() = decodedSegments(components.path) { PercentCodec.decode(it) } + get() = decodedPathSegments /** The canonical encoded path string (e.g. `/a/b`, or the opaque path verbatim). */ @get:JvmName("encodedPath") public val encodedPath: String - get() = serializeUrlPath(components) + get() = encodedPathText /** * The raw encoded query without its leading `?`, or `null` when no `?` was present. @@ -157,7 +157,7 @@ public class Url internal constructor( /** A decoded, immutable snapshot of the query's `name=value` pairs; never live. */ @get:JvmName("queryParameters") public val queryParameters: QueryParameters - get() = QueryParameters.parseOrEmpty(components.query) + get() = queryParameterSnapshot /** * The raw encoded fragment without its leading `#`, or `null` when no `#` was present. @@ -215,6 +215,13 @@ public class Url internal constructor( /** Cached canonical serialization, computed once (permits caching an immutable value). */ private val canonicalHref: String by lazy { Serializer.serialize(components, ParseProfile.URL) } + /** Path/query projections, each computed once; every value is immutable, mirroring [canonicalHref]. */ + private val decodedPathSegments: List by lazy { + decodedSegments(components.path) { PercentCodec.decode(it) } + } + private val encodedPathText: String by lazy { serializeUrlPath(components) } + private val queryParameterSnapshot: QueryParameters by lazy { QueryParameters.parseOrEmpty(components.query) } + /** * The canonical serialized URL; the basis of [toString], [equals], and [hashCode]. *