Skip to content

Commit 484a4b3

Browse files
committed
Reduce allocations in outputc collectors. Cache hashcode to avoid deep recurcive calculation
1 parent daa4552 commit 484a4b3

File tree

3 files changed

+125
-27
lines changed

3 files changed

+125
-27
lines changed

src/commonMain/kotlin/io/github/optimumcode/json/pointer/JsonPointer.kt

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,19 +66,29 @@ public sealed class JsonPointer(
6666
if (this !is SegmentPointer) {
6767
return last
6868
}
69-
var parent: PointerParent? = null
70-
var node: JsonPointer = this
71-
while (node is SegmentPointer) {
72-
parent =
73-
PointerParent(
74-
parent,
75-
node.propertyName,
76-
node.index,
69+
return insertLastDeepCopy(this, last)
70+
}
71+
72+
// there might be an issue with stack in case this function is called deep on the stack
73+
private fun insertLastDeepCopy(
74+
pointer: SegmentPointer,
75+
last: SegmentPointer,
76+
): JsonPointer =
77+
with(pointer) {
78+
if (next is SegmentPointer) {
79+
SegmentPointer(
80+
propertyName = propertyName,
81+
index = index,
82+
next = insertLastDeepCopy(next, last),
7783
)
78-
node = node.next
84+
} else {
85+
SegmentPointer(
86+
propertyName = propertyName,
87+
index = index,
88+
next = last,
89+
)
90+
}
7991
}
80-
return buildPath(last, parent)
81-
}
8292

8393
private fun escapeJsonPointer(propertyName: String): String {
8494
if (propertyName.contains(SEPARATOR) || propertyName.contains(QUOTATION)) {

src/commonMain/kotlin/io/github/optimumcode/json/schema/OutputCollector.kt

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -263,12 +263,23 @@ public sealed class OutputCollector<T> private constructor(
263263
private val collapse: Boolean = true,
264264
transformer: OutputErrorTransformer<ValidationOutput.Detailed> = NO_TRANSFORMATION,
265265
) : OutputCollector<ValidationOutput.Detailed>(parent, transformer) {
266-
private val errors: MutableList<ValidationOutput.Detailed> = mutableListOf()
266+
private lateinit var results: MutableSet<ValidationOutput.Detailed>
267+
268+
private fun addResult(result: ValidationOutput.Detailed) {
269+
if (result.valid) {
270+
// do not add valid
271+
return
272+
}
273+
if (!::results.isInitialized) {
274+
results = linkedSetOf()
275+
}
276+
results.add(result)
277+
}
267278

268279
override val output: ValidationOutput.Detailed
269280
get() {
270-
val valid = errors.none { !it.valid }
271-
if (valid) {
281+
if (!::results.isInitialized) {
282+
// variable is uninitialized only if all results are valid
272283
return Detailed(
273284
valid = true,
274285
keywordLocation = keywordLocation,
@@ -277,7 +288,7 @@ public sealed class OutputCollector<T> private constructor(
277288
errors = emptySet(),
278289
)
279290
}
280-
val failed = errors.filterTo(hashSetOf()) { it.error != null || it.errors.isNotEmpty() }
291+
val failed = results
281292
return if (failed.size == 1 && collapse) {
282293
failed.single()
283294
} else {
@@ -331,12 +342,12 @@ public sealed class OutputCollector<T> private constructor(
331342
Detailed(location, keywordLocation, parent, absoluteLocation, collapse, transformer = transformer)
332343

333344
override fun reportErrors() {
334-
parent?.errors?.add(output)
345+
parent?.addResult(output)
335346
}
336347

337348
override fun onError(error: ValidationError) {
338349
val err = transformError(error) ?: return
339-
errors.add(
350+
addResult(
340351
Detailed(
341352
valid = false,
342353
instanceLocation = err.objectPath,
@@ -355,14 +366,20 @@ public sealed class OutputCollector<T> private constructor(
355366
private val absoluteLocation: AbsoluteLocation? = null,
356367
transformer: OutputErrorTransformer<ValidationOutput.Verbose> = NO_TRANSFORMATION,
357368
) : OutputCollector<ValidationOutput.Verbose>(parent, transformer) {
358-
private val errors: MutableList<ValidationOutput.Verbose> = mutableListOf()
369+
private val results: MutableList<ValidationOutput.Verbose> = ArrayList(1)
370+
371+
private fun addResult(result: ValidationOutput.Verbose) {
372+
// init hashCode to reduce overhead in future
373+
result.hashCode()
374+
results.add(result)
375+
}
359376

360377
override val output: ValidationOutput.Verbose
361378
get() {
362-
if (errors.size == 1) {
379+
if (results.size == 1) {
363380
// when this is a leaf we should return the reported error
364381
// instead of creating a new node
365-
val childError = errors.single()
382+
val childError = results.single()
366383
if (
367384
childError.errors.isEmpty() &&
368385
childError.let {
@@ -373,11 +390,11 @@ public sealed class OutputCollector<T> private constructor(
373390
}
374391
}
375392
return Verbose(
376-
valid = errors.none { !it.valid },
393+
valid = results.none { !it.valid },
377394
keywordLocation = keywordLocation,
378395
absoluteKeywordLocation = absoluteLocation,
379396
instanceLocation = location,
380-
errors = errors.toSet(),
397+
errors = results.toSet(),
381398
)
382399
}
383400

@@ -422,12 +439,12 @@ public sealed class OutputCollector<T> private constructor(
422439
Verbose(location, keywordLocation, parent, absoluteLocation, transformer)
423440

424441
override fun reportErrors() {
425-
parent?.errors?.add(output)
442+
parent?.addResult(output)
426443
}
427444

428445
override fun onError(error: ValidationError) {
429446
val err = transformError(error) ?: return
430-
errors.add(
447+
addResult(
431448
Verbose(
432449
valid = false,
433450
instanceLocation = err.objectPath,

src/commonMain/kotlin/io/github/optimumcode/json/schema/ValidationOutput.kt

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,43 @@ public sealed class ValidationOutput private constructor() {
3535
public val absoluteKeywordLocation: AbsoluteLocation? = null,
3636
public val error: String? = null,
3737
public val errors: Set<Detailed> = emptySet(),
38-
) : ValidationOutput()
38+
) : ValidationOutput() {
39+
// hashcode is stored to avoid recursive recalculation for each error in `errors` property
40+
private var hash = 0
41+
42+
override fun equals(other: Any?): Boolean {
43+
if (this === other) return true
44+
if (other == null || this::class != other::class) return false
45+
46+
other as Detailed
47+
48+
if (valid != other.valid) return false
49+
if (keywordLocation != other.keywordLocation) return false
50+
if (instanceLocation != other.instanceLocation) return false
51+
if (absoluteKeywordLocation != other.absoluteKeywordLocation) return false
52+
if (error != other.error) return false
53+
if (errors != other.errors) return false
54+
55+
return true
56+
}
57+
58+
override fun hashCode(): Int {
59+
if (hash != 0) {
60+
return hash
61+
}
62+
var result = valid.hashCode()
63+
result = 31 * result + keywordLocation.hashCode()
64+
result = 31 * result + instanceLocation.hashCode()
65+
result = 31 * result + (absoluteKeywordLocation?.hashCode() ?: 0)
66+
result = 31 * result + (error?.hashCode() ?: 0)
67+
result = 31 * result + errors.hashCode()
68+
if (result == 0) {
69+
result = 31
70+
}
71+
hash = result
72+
return result
73+
}
74+
}
3975

4076
public data class Verbose(
4177
override val valid: Boolean,
@@ -44,6 +80,41 @@ public sealed class ValidationOutput private constructor() {
4480
public val absoluteKeywordLocation: AbsoluteLocation? = null,
4581
public val error: String? = null,
4682
public val errors: Set<Verbose> = emptySet(),
47-
public val annotations: Set<Verbose> = emptySet(),
48-
) : ValidationOutput()
83+
) : ValidationOutput() {
84+
// hashcode is stored to avoid recursive recalculation for each error in `errors` property
85+
private var hash = 0
86+
87+
override fun equals(other: Any?): Boolean {
88+
if (this === other) return true
89+
if (other == null || this::class != other::class) return false
90+
91+
other as Verbose
92+
93+
if (valid != other.valid) return false
94+
if (keywordLocation != other.keywordLocation) return false
95+
if (instanceLocation != other.instanceLocation) return false
96+
if (absoluteKeywordLocation != other.absoluteKeywordLocation) return false
97+
if (error != other.error) return false
98+
if (errors != other.errors) return false
99+
100+
return true
101+
}
102+
103+
override fun hashCode(): Int {
104+
if (hash != 0) {
105+
return hash
106+
}
107+
var result = valid.hashCode()
108+
result = 31 * result + keywordLocation.hashCode()
109+
result = 31 * result + instanceLocation.hashCode()
110+
result = 31 * result + (absoluteKeywordLocation?.hashCode() ?: 0)
111+
result = 31 * result + (error?.hashCode() ?: 0)
112+
result = 31 * result + errors.hashCode()
113+
if (result == 0) {
114+
result = 31
115+
}
116+
hash = result
117+
return result
118+
}
119+
}
49120
}

0 commit comments

Comments
 (0)