diff --git a/README.md b/README.md index ecfae09..aa07603 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,8 @@ In this example, `Ksoup.parseGetRequest` fetches and parses HTML content from Wi #### For further documentation, please check here: [Jsoup](https://jsoup.org/) ### Ksoup vs. Jsoup Performance: Parsing & Selecting 448KB HTML File [test.tx](https://github.com/fleeksoft/ksoup/blob/develop/ksoup-test/testResources/test.txt) +![Ksoup vs Jsoup](performance1.png) + ![Ksoup vs Jsoup](performance.png) ## Open source diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e228fc7..3e2c4a9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,9 +8,10 @@ ktor = "3.0.0-rc-1" ktor2 = "2.3.12" coroutines = "1.8.1" kotlinxDatetime = "0.6.1" -kotlinx-io = "0.5.3" +kotlinx-io = "0.5.4" okio = "3.9.0" dokka = "1.9.20" +kotlinx-benchmark = "0.4.12" #korlibs = "999.0.0.999" # 999.0.0.999 is local version korlibs = "6.0.1" @@ -48,6 +49,7 @@ stately-concurrency = { module = "co.touchlab:stately-concurrency", version.ref jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } okio = { module = "com.squareup.okio:okio", version.ref = "okio" } okio-nodefilesystem = { module = "com.squareup.okio:okio-nodefilesystem", version.ref = "okio" } +kotlinx-benchmark-runtime = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime", version.ref = "kotlinx-benchmark" } [plugins] androidLibrary = { id = "com.android.library", version.ref = "agp" } @@ -55,3 +57,5 @@ kmp = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } power-assert = { id = "org.jetbrains.kotlin.plugin.power-assert", version.ref = "kotlin" } mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" } +kotlinx-benchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "kotlinx-benchmark" } +allopen = { id = "org.jetbrains.kotlin.plugin.allopen", version.ref = "kotlin" } diff --git a/ksoup-benchmark/build.gradle.kts b/ksoup-benchmark/build.gradle.kts new file mode 100644 index 0000000..0fdce0d --- /dev/null +++ b/ksoup-benchmark/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(libs.plugins.kotlinx.benchmark) + alias(libs.plugins.allopen) +} + +allOpen { + annotation("org.openjdk.jmh.annotations.State") +} + + +benchmark { + targets { + register("jvm") + } + + configurations { + named("main") { +// exclude("org.jsoup.parser.JsoupBenchmark") +// exclude("com.fleeksoft.ksoup.benchmark.KsoupBenchmark") + } + } + +} diff --git a/ksoup-benchmark/module.yaml b/ksoup-benchmark/module.yaml new file mode 100644 index 0000000..5b16f5b --- /dev/null +++ b/ksoup-benchmark/module.yaml @@ -0,0 +1,24 @@ +product: + type: lib + platforms: [ jvm, js, android, linuxX64, linuxArm64, tvosArm64, tvosX64, tvosSimulatorArm64, macosX64, macosArm64, iosArm64, iosSimulatorArm64, iosX64, mingwX64 ] + +apply: [ ../common.module-template.yaml ] + +aliases: + - jvmAndAndroid: [ jvm, android ] + +repositories: + - mavenLocal + +dependencies: + - $libs.kotlinx.io + - $libs.kotlinx.benchmark.runtime +# - com.fleeksoft.ksoup:ksoup-lite:0.1.8 + - ../ksoup + +dependencies@jvm: + - $libs.jsoup + +settings: + kotlin: + optIns: [ kotlinx.cinterop.BetaInteropApi, kotlinx.cinterop.UnsafeNumber, kotlinx.cinterop.ExperimentalForeignApi, kotlin.experimental.ExperimentalNativeApi ] diff --git a/ksoup-benchmark/src@jvm/com/fleeksoft/ksoup/benchmark/KsoupBenchmark.kt b/ksoup-benchmark/src@jvm/com/fleeksoft/ksoup/benchmark/KsoupBenchmark.kt new file mode 100644 index 0000000..289465f --- /dev/null +++ b/ksoup-benchmark/src@jvm/com/fleeksoft/ksoup/benchmark/KsoupBenchmark.kt @@ -0,0 +1,49 @@ +package com.fleeksoft.ksoup.benchmark + +import com.fleeksoft.ksoup.Ksoup +import com.fleeksoft.ksoup.nodes.Document +import com.fleeksoft.ksoup.nodes.Element +import com.fleeksoft.ksoup.select.Elements +import com.fleeksoft.ksoup.select.Evaluator +import kotlinx.benchmark.* +import kotlinx.io.buffered +import kotlinx.io.files.Path +import kotlinx.io.files.SystemFileSystem +import kotlinx.io.readString + + +@State(Scope.Benchmark) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = BenchmarkTimeUnit.SECONDS) +class KsoupBenchmark { + private lateinit var fileData: String + private lateinit var doc1: Document + + @Setup + fun setUp() { + fileData = + SystemFileSystem.source(Path("/Users/sabeeh/IdeaProjects/ksoup-benchmark/ksoup-test/testResources/test.txt")).buffered().readString() + doc1 = parseHtml() + } + + @Benchmark + fun parse() { + val doc = parseHtml() + } + + @Benchmark + fun select() { + val doc = parseHtml() + doc.getElementsByClass("an-info").mapNotNull { anInfo -> + anInfo.parent()?.let { a -> + val attr = a.attr("href") + if (attr.isEmpty()) return@let null + + attr.substringAfter("/Home/Bangumi/", "") + .takeIf { it.isNotBlank() } + } + } + } + + private fun parseHtml() = Ksoup.parse(fileData) +} \ No newline at end of file diff --git a/ksoup-benchmark/src@jvm/org/jsoup/parser/JsoupBenchmark.kt b/ksoup-benchmark/src@jvm/org/jsoup/parser/JsoupBenchmark.kt new file mode 100644 index 0000000..47f439e --- /dev/null +++ b/ksoup-benchmark/src@jvm/org/jsoup/parser/JsoupBenchmark.kt @@ -0,0 +1,45 @@ +package org.jsoup.parser + +import kotlinx.benchmark.* +import kotlinx.io.buffered +import kotlinx.io.files.Path +import kotlinx.io.files.SystemFileSystem +import kotlinx.io.readString +import org.jsoup.Jsoup +import org.jsoup.nodes.Document + +@State(Scope.Benchmark) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = BenchmarkTimeUnit.SECONDS) +class JsoupBenchmark { + private lateinit var fileData: String + private lateinit var doc1: Document + + @Setup + fun setUp() { + fileData = + SystemFileSystem.source(Path("/Users/sabeeh/IdeaProjects/ksoup-benchmark/ksoup-test/testResources/test.txt")).buffered().readString() + doc1 = parseHtml() + } + + @Benchmark + fun parse() { + val doc = parseHtml() + } + + @Benchmark + fun select() { + val doc = parseHtml() + doc.getElementsByClass("an-info").mapNotNull { anInfo -> + anInfo.parent()?.let { a -> + val attr = a.attr("href") + if (attr.isEmpty()) return@let null + + attr.substringAfter("/Home/Bangumi/", "") + .takeIf { it.isNotBlank() } + } + } + } + + private fun parseHtml() = Jsoup.parse(fileData) +} \ No newline at end of file diff --git a/ksoup-engine-common/src/com/fleeksoft/ksoup/io/Charset.kt b/ksoup-engine-common/src/com/fleeksoft/ksoup/io/Charset.kt index 8b4e0a1..c9b9912 100644 --- a/ksoup-engine-common/src/com/fleeksoft/ksoup/io/Charset.kt +++ b/ksoup-engine-common/src/com/fleeksoft/ksoup/io/Charset.kt @@ -20,7 +20,7 @@ interface Charset { }.isSuccess*/ } - fun decode(stringBuilder: StringBuilder, byteArray: ByteArray, start: Int, end: Int): Int + fun decode(stringBuilder: StringBuilder, byteArray: ByteArray, start: Int, end: Int = byteArray.size): Int fun toByteArray(value: String): ByteArray fun onlyUtf8(): Boolean = false diff --git a/ksoup-test/test@jvmAndAndroid/com/fleeksoft/ksoup/PerformanceComparisonTest.kt b/ksoup-test/test@jvmAndAndroid/com/fleeksoft/ksoup/PerformanceComparisonTest.kt index 5f2f0f7..f5b01fc 100644 --- a/ksoup-test/test@jvmAndAndroid/com/fleeksoft/ksoup/PerformanceComparisonTest.kt +++ b/ksoup-test/test@jvmAndAndroid/com/fleeksoft/ksoup/PerformanceComparisonTest.kt @@ -44,6 +44,17 @@ class PerformanceComparisonTest { val jsoupParseTimes = mutableListOf() val jsoupSelectTimes = mutableListOf() +// warmup + repeat(10) { + ksoupTest(ksoupParseTimes, ksoupSelectTimes) + jsoupTest(jsoupParseTimes, jsoupSelectTimes) + } + + ksoupParseTimes.clear() + ksoupSelectTimes.clear() + jsoupParseTimes.clear() + jsoupSelectTimes.clear() + // Perform multiple tests repeat(30) { ksoupTest(ksoupParseTimes, ksoupSelectTimes) diff --git a/ksoup/src/com/fleeksoft/ksoup/internal/StringUtil.kt b/ksoup/src/com/fleeksoft/ksoup/internal/StringUtil.kt index df29419..266c201 100644 --- a/ksoup/src/com/fleeksoft/ksoup/internal/StringUtil.kt +++ b/ksoup/src/com/fleeksoft/ksoup/internal/StringUtil.kt @@ -199,7 +199,7 @@ public object StringUtil { } public fun inSorted(needle: String, haystack: Array): Boolean { - return haystack.toList().binarySearch(needle) >= 0 + return haystack.binarySearch(needle) >= 0 } /** diff --git a/ksoup/src/com/fleeksoft/ksoup/nodes/Attribute.kt b/ksoup/src/com/fleeksoft/ksoup/nodes/Attribute.kt index 8fde9a1..2c0ff33 100644 --- a/ksoup/src/com/fleeksoft/ksoup/nodes/Attribute.kt +++ b/ksoup/src/com/fleeksoft/ksoup/nodes/Attribute.kt @@ -4,6 +4,7 @@ import com.fleeksoft.ksoup.helper.Validate import com.fleeksoft.ksoup.internal.StringUtil import com.fleeksoft.ksoup.nodes.Document.OutputSettings.Syntax import com.fleeksoft.ksoup.ported.KCloneable +import com.fleeksoft.ksoup.ported.binarySearchBy import com.fleeksoft.ksoup.ported.exception.IOException import com.fleeksoft.ksoup.ported.exception.SerializationException @@ -351,7 +352,7 @@ public open class Attribute : Map.Entry, KCloneable * Checks if this attribute name is defined as a boolean attribute in HTML5 */ public fun isBooleanAttribute(key: String): Boolean { - return booleanAttributes.toList().binarySearch { it.compareTo(key.lowercase()) } >= 0 + return booleanAttributes.binarySearchBy { it.compareTo(key.lowercase()) } >= 0 } } } diff --git a/ksoup/src/com/fleeksoft/ksoup/nodes/Element.kt b/ksoup/src/com/fleeksoft/ksoup/nodes/Element.kt index 06fc9df..d79fa0b 100644 --- a/ksoup/src/com/fleeksoft/ksoup/nodes/Element.kt +++ b/ksoup/src/com/fleeksoft/ksoup/nodes/Element.kt @@ -17,7 +17,6 @@ import com.fleeksoft.ksoup.select.* import kotlin.js.JsName import kotlin.jvm.JvmOverloads import kotlin.reflect.KClass -import kotlin.reflect.cast /** * An HTML Element consists of a tag name, attributes, and child nodes (including text nodes and other elements). @@ -398,8 +397,6 @@ public open class Element : Node { private inline fun filterNodes(clazz: KClass): List { return _childNodes.filterIsInstance() - .map { clazz.cast(it) } - .toList() } /** diff --git a/ksoup/src/com/fleeksoft/ksoup/nodes/Entities.kt b/ksoup/src/com/fleeksoft/ksoup/nodes/Entities.kt index 51bb323..1580794 100644 --- a/ksoup/src/com/fleeksoft/ksoup/nodes/Entities.kt +++ b/ksoup/src/com/fleeksoft/ksoup/nodes/Entities.kt @@ -69,7 +69,7 @@ public object Entities { if (value != null) return value val codepoint = extended.codepointForName(name) return if (codepoint != empty) { - charArrayOf(codepoint.toChar()).concatToString() + codepoint.toChar().toString() } else { emptyName } @@ -420,12 +420,12 @@ public object Entities { } public fun codepointForName(name: String): Int { - val index: Int = nameKeys.toList().binarySearch(name) + val index: Int = nameKeys.binarySearch(name) return if (index >= 0) codeVals[index] else empty } public fun nameForCodepoint(codepoint: Int): String { - val index: Int = codeKeys.toList().binarySearch(codepoint) + val index: Int = codeKeys.binarySearch(codepoint) return if (index >= 0) { // the results are ordered so lower case versions of same codepoint come after uppercase, and we prefer to emit lower // (and binary search for same item with multi results is undefined diff --git a/ksoup/src/com/fleeksoft/ksoup/nodes/FormElement.kt b/ksoup/src/com/fleeksoft/ksoup/nodes/FormElement.kt index 0bbf992..73c48c5 100644 --- a/ksoup/src/com/fleeksoft/ksoup/nodes/FormElement.kt +++ b/ksoup/src/com/fleeksoft/ksoup/nodes/FormElement.kt @@ -19,7 +19,7 @@ public class FormElement(tag: Tag, baseUri: String?, attributes: Attributes?) : private val linkedEls: Elements = Elements() // contains form submittable elements that were linked during the parse (and due to parse rules, may no longer be a child of this form) - private val submittable = QueryParser.parse(StringUtil.join(SharedConstants.FormSubmitTags.toList(), ", ")) + private val submittable = QueryParser.parse(SharedConstants.FormSubmitTags.joinToString(", ")) /** * Get the list of form control elements associated with this form. diff --git a/ksoup/src/com/fleeksoft/ksoup/parser/HtmlTreeBuilder.kt b/ksoup/src/com/fleeksoft/ksoup/parser/HtmlTreeBuilder.kt index 5993484..3d03279 100644 --- a/ksoup/src/com/fleeksoft/ksoup/parser/HtmlTreeBuilder.kt +++ b/ksoup/src/com/fleeksoft/ksoup/parser/HtmlTreeBuilder.kt @@ -26,8 +26,7 @@ public open class HtmlTreeBuilder : TreeBuilder() { // fragment parse root; name only copy of context. could be null even if fragment parsing private var contextElement: Element? = null - private var formattingElements: ArrayList? = - null // active (open) formatting elements + private var formattingElements: ArrayList = ArrayList() // active (open) formatting elements private var tmplInsertMode: ArrayList? = null // stack of Template Insertion modes private var pendingTableCharacters: MutableList? = @@ -60,7 +59,6 @@ public open class HtmlTreeBuilder : TreeBuilder() { headElement = null formElement = null contextElement = null - formattingElements = ArrayList() tmplInsertMode = ArrayList() pendingTableCharacters = ArrayList() emptyEnd = Token.EndTag(this) @@ -785,29 +783,29 @@ public open class HtmlTreeBuilder : TreeBuilder() { } private fun lastFormattingElement(): Element? { - return if ((formattingElements?.size ?: 0) > 0) { - formattingElements!![formattingElements!!.size - 1] + return if (formattingElements.size > 0) { + formattingElements[formattingElements.size - 1] } else { null } } public fun positionOfElement(el: Element?): Int { - for (i in formattingElements!!.indices) { - if (el === formattingElements!!.get(i)) return i + for (i in formattingElements.indices) { + if (el === formattingElements[i]) return i } return -1 } public fun removeLastFormattingElement(): Element? { - val size: Int = formattingElements?.size ?: 0 - return if (size > 0) formattingElements!!.removeAt(size - 1) else null + val size: Int = formattingElements.size + return if (size > 0) formattingElements.removeAt(size - 1) else null } // active formatting elements public fun pushActiveFormattingElements(`in`: Element) { checkActiveFormattingElements(`in`) - formattingElements!!.add(`in`) + formattingElements.add(`in`) } public fun pushWithBookmark( @@ -817,22 +815,22 @@ public open class HtmlTreeBuilder : TreeBuilder() { checkActiveFormattingElements(`in`) // catch any range errors and assume bookmark is incorrect - saves a redundant range check. try { - formattingElements!!.add(bookmark, `in`) + formattingElements.add(bookmark, `in`) } catch (e: IndexOutOfBoundsException) { - formattingElements!!.add(`in`) + formattingElements.add(`in`) } } public fun checkActiveFormattingElements(`in`: Element) { var numSeen = 0 - val size: Int = formattingElements!!.size - 1 + val size: Int = formattingElements.size - 1 var ceil = size - maxUsedFormattingElements if (ceil < 0) ceil = 0 for (pos in size downTo ceil) { - val el: Element = formattingElements?.get(pos) ?: break // marker + val el: Element = formattingElements[pos] ?: break // marker if (isSameFormattingElement(`in`, el)) numSeen++ if (numSeen == 3) { - formattingElements!!.removeAt(pos) + formattingElements.removeAt(pos) break } } @@ -883,16 +881,16 @@ public open class HtmlTreeBuilder : TreeBuilder() { } public fun clearFormattingElementsToLastMarker() { - while (!formattingElements!!.isEmpty()) { + while (!formattingElements.isEmpty()) { removeLastFormattingElement() ?: break } } public fun removeFromActiveFormattingElements(el: Element) { - for (pos in formattingElements!!.indices.reversed()) { + for (pos in formattingElements.indices.reversed()) { val next: Element? = formattingElements?.get(pos) if (next === el) { - formattingElements!!.removeAt(pos) + formattingElements.removeAt(pos) break } } @@ -903,7 +901,7 @@ public open class HtmlTreeBuilder : TreeBuilder() { } public fun getActiveFormattingElement(nodeName: String?): Element? { - for (pos in formattingElements!!.indices.reversed()) { + for (pos in formattingElements.indices.reversed()) { val next: Element? = formattingElements?.get(pos) if (next == null) { // scope marker @@ -919,11 +917,11 @@ public open class HtmlTreeBuilder : TreeBuilder() { out: Element, `in`: Element, ) { - replaceInQueue(formattingElements!!, out, `in`) + replaceInQueue(formattingElements, out, `in`) } public fun insertMarkerToFormattingElements() { - formattingElements?.add(null) + formattingElements.add(null) } public fun insertInFosterParent(inNode: Node) { diff --git a/ksoup/src/com/fleeksoft/ksoup/parser/HtmlTreeBuilderState.kt b/ksoup/src/com/fleeksoft/ksoup/parser/HtmlTreeBuilderState.kt index e639615..2705a0a 100644 --- a/ksoup/src/com/fleeksoft/ksoup/parser/HtmlTreeBuilderState.kt +++ b/ksoup/src/com/fleeksoft/ksoup/parser/HtmlTreeBuilderState.kt @@ -916,7 +916,7 @@ public enum class HtmlTreeBuilderState { ): Boolean { // case insensitive search - goal is to preserve output case, not for the parse to be case sensitive val name: String = t.asEndTag().normalName!! - val stack: ArrayList = arrayListOf(*tb.getStack().mapNotNull { it }.toList().toTypedArray()) + val stack: ArrayList = arrayListOf(*tb.getStack().mapNotNull { it }.toTypedArray()) // deviate from spec slightly to speed when super deeply nested val elFromStack: Element? = tb.getFromStack(name) @@ -1984,8 +1984,7 @@ public enum class HtmlTreeBuilderState { } // Any other end tag - val stack: ArrayList = - arrayListOf(*tb.getStack().mapNotNull { it }.toList().toTypedArray()) + val stack: ArrayList = arrayListOf(*tb.getStack().mapNotNull { it }.toTypedArray()) if (stack.isEmpty()) Validate.wtf("Stack unexpectedly empty") var i: Int = stack.size - 1 var el: Element = stack[i] diff --git a/ksoup/src/com/fleeksoft/ksoup/ported/KsoupExt.kt b/ksoup/src/com/fleeksoft/ksoup/ported/KsoupExt.kt index a06ff45..7cf3a64 100644 --- a/ksoup/src/com/fleeksoft/ksoup/ported/KsoupExt.kt +++ b/ksoup/src/com/fleeksoft/ksoup/ported/KsoupExt.kt @@ -19,4 +19,60 @@ fun SourceReader.toReader(charset: Charset = Charsets.UTF8, chunkSize: Int = Sha fun String.toByteArray(charset: Charset? = null): ByteArray = charset?.toByteArray(this) ?: this.encodeToByteArray() -fun String.toSourceFile(): FileSource = KsoupEngineInstance.ksoupEngine.pathToFileSource(this) \ No newline at end of file +fun String.toSourceFile(): FileSource = KsoupEngineInstance.ksoupEngine.pathToFileSource(this) + + +public fun > Array.binarySearch(element: T): Int { + var low = 0 + var high = this.size - 1 + + while (low <= high) { + val mid = (low + high).ushr(1) // safe from overflows + val midVal = get(mid) + val cmp = compareValues(midVal, element) + + if (cmp < 0) + low = mid + 1 + else if (cmp > 0) + high = mid - 1 + else + return mid // key found + } + return -(low + 1) // key not found +} + +fun IntArray.binarySearch(key: Int): Int { + var low = 0 + var high = this.size - 1 + + while (low <= high) { + val mid = (low + high) ushr 1 + val midVal = this[mid] + + if (midVal < key) low = mid + 1 + else if (midVal > key) high = mid - 1 + else return mid // key found + } + return -(low + 1) // key not found. +} + + +public fun Array.binarySearchBy(comparison: (T) -> Int): Int { + + var low = 0 + var high = size - 1 + + while (low <= high) { + val mid = (low + high).ushr(1) // safe from overflows + val midVal = get(mid) + val cmp = comparison(midVal) + + if (cmp < 0) + low = mid + 1 + else if (cmp > 0) + high = mid - 1 + else + return mid // key found + } + return -(low + 1) // key not found +} \ No newline at end of file diff --git a/ksoup/src/com/fleeksoft/ksoup/ported/io/StreamDecoder.kt b/ksoup/src/com/fleeksoft/ksoup/ported/io/StreamDecoder.kt index f00573b..25fc076 100644 --- a/ksoup/src/com/fleeksoft/ksoup/ported/io/StreamDecoder.kt +++ b/ksoup/src/com/fleeksoft/ksoup/ported/io/StreamDecoder.kt @@ -145,7 +145,9 @@ class StreamDecoder(source: SourceReader, charset: Charset) : Reader() { if (bb.available() <= 0) return -1 val text = bb.readText(cs, min(length, bb.size)) - text.toCharArray().copyInto(cbuf, off) + for (i in text.indices) { + cbuf[off + i] = text[i] + } return text.length } diff --git a/ksoup/src/com/fleeksoft/ksoup/ported/io/StringReader.kt b/ksoup/src/com/fleeksoft/ksoup/ported/io/StringReader.kt index 572e6ba..ab8fee7 100644 --- a/ksoup/src/com/fleeksoft/ksoup/ported/io/StringReader.kt +++ b/ksoup/src/com/fleeksoft/ksoup/ported/io/StringReader.kt @@ -46,9 +46,8 @@ class StringReader(s: String) : Reader() { } if (next >= this.length) return -1 val n: Int = min(this.length - next, length) - val charArray = str!!.toCharArray(startIndex = next, endIndex = next + n) - charArray.indices.forEach { i -> - cbuf[offset + i] = charArray[i] + for (i in 0 until n) { + cbuf[offset + i] = str!![next + i] } next += n return n diff --git a/ksoup/src/com/fleeksoft/ksoup/select/Collector.kt b/ksoup/src/com/fleeksoft/ksoup/select/Collector.kt index 10419e3..ca846cb 100644 --- a/ksoup/src/com/fleeksoft/ksoup/select/Collector.kt +++ b/ksoup/src/com/fleeksoft/ksoup/select/Collector.kt @@ -19,7 +19,9 @@ internal object Collector { root: Element, ): Elements { eval.reset() - return Elements(root.stream().filter(eval.asPredicate(root)).toList()) + return Elements().apply { + addAll(root.stream().filter(eval.asPredicate(root))) + } } /** diff --git a/ksoup/src/com/fleeksoft/ksoup/select/Elements.kt b/ksoup/src/com/fleeksoft/ksoup/select/Elements.kt index 08cbf62..cfa6dc8 100644 --- a/ksoup/src/com/fleeksoft/ksoup/select/Elements.kt +++ b/ksoup/src/com/fleeksoft/ksoup/select/Elements.kt @@ -230,7 +230,7 @@ public class Elements(private val delegateList: MutableList = mutableLi * @see .outerHtml */ public fun html(): String { - return this.map { it.html() }.joinToString("\n") + return this.joinToString("\n") { it.html() } } /** diff --git a/ksoup/src/com/fleeksoft/ksoup/select/QueryParser.kt b/ksoup/src/com/fleeksoft/ksoup/select/QueryParser.kt index eb2fce7..2c3f5e3 100644 --- a/ksoup/src/com/fleeksoft/ksoup/select/QueryParser.kt +++ b/ksoup/src/com/fleeksoft/ksoup/select/QueryParser.kt @@ -81,22 +81,19 @@ public class QueryParser private constructor(query: String) { } ' ' -> - currentEval = - CombiningEvaluator.And(StructuralEvaluator.Parent(currentEval!!), newEval) + currentEval = CombiningEvaluator.And(StructuralEvaluator.Parent(currentEval!!), newEval) '+' -> - currentEval = - CombiningEvaluator.And( - StructuralEvaluator.ImmediatePreviousSibling(currentEval!!), - newEval, - ) + currentEval = CombiningEvaluator.And( + StructuralEvaluator.ImmediatePreviousSibling(currentEval!!), + newEval, + ) '~' -> - currentEval = - CombiningEvaluator.And( - StructuralEvaluator.PreviousSibling(currentEval!!), - newEval, - ) + currentEval = CombiningEvaluator.And( + StructuralEvaluator.PreviousSibling(currentEval!!), + newEval, + ) ',' -> { val or: CombiningEvaluator.Or diff --git a/performance.png b/performance.png index 3340b67..45e9c11 100644 Binary files a/performance.png and b/performance.png differ diff --git a/performance1.png b/performance1.png new file mode 100644 index 0000000..6faddd7 Binary files /dev/null and b/performance1.png differ diff --git a/settings.gradle.kts b/settings.gradle.kts index 361ee90..a38dc06 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -48,6 +48,7 @@ if (libBuildType == "ktor2" || libBuildType == "dev") { if (libBuildType != "common") { include("ksoup") include("ksoup-test") + include("ksoup-benchmark") } //include("sample:shared", "sample:desktop")