Skip to content

Commit b3a3802

Browse files
committed
perf(rendering): make rendering parallel
1 parent 7836622 commit b3a3802

7 files changed

Lines changed: 106 additions & 4 deletions

File tree

.run/CLI_ Docs.run.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<component name="ProjectRunConfigurationManager">
2+
<configuration default="false" name="CLI: Docs" type="JetRunConfigurationType">
3+
<option name="MAIN_CLASS_NAME" value="com.quarkdown.cli.QuarkdownCliKt" />
4+
<module name="quarkdown.quarkdown-cli.main" />
5+
<option name="PROGRAM_PARAMETERS" value="c main.qd --strict --allow all --clean --libs ../quarkdown-libs/src/main/resources --out-name wiki --allow all" />
6+
<shortenClasspath name="NONE" />
7+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/docs" />
8+
<method v="2">
9+
<option name="Make" enabled="true" />
10+
</method>
11+
</configuration>
12+
</component>

quarkdown-core/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ dependencies {
2323
cslStyles("org.citationstyles:styles:26.2")
2424
implementation("org.citationstyles:locales:26.2")
2525
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0")
26+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
2627
}
2728

2829
// Extracts only the CSL style files listed in csl-styles.txt from the full styles collection, to reduce the bundle size.

quarkdown-core/src/main/kotlin/com/quarkdown/core/ast/Node.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.quarkdown.core.ast
22

3+
import com.quarkdown.core.util.mapParallel
34
import com.quarkdown.core.visitor.node.NodeVisitor
45

56
/**
@@ -37,3 +38,19 @@ interface SingleChildNestableNode<T : Node> : NestableNode {
3738
override val children: List<Node>
3839
get() = listOf(child)
3940
}
41+
42+
/**
43+
* Accepts a visitor for each node sequentially.
44+
* @param visitor the visitor to accept
45+
* @return the list of results from each visit, preserving order
46+
*/
47+
fun <T> List<Node>.acceptAll(visitor: NodeVisitor<T>): List<T> = map { it.accept(visitor) }
48+
49+
/**
50+
* Accepts a visitor for each node, executing visits in parallel when beneficial.
51+
* Falls back to sequential execution for small lists.
52+
* @param visitor the visitor to accept
53+
* @return the list of results from each visit, preserving order
54+
* @see mapParallel
55+
*/
56+
fun <T> List<Node>.parallelAcceptAll(visitor: NodeVisitor<T>): List<T> = mapParallel { it.accept(visitor) }

quarkdown-core/src/main/kotlin/com/quarkdown/core/property/AssociatedProperties.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package com.quarkdown.core.property
22

3+
import java.util.concurrent.ConcurrentHashMap
4+
import java.util.concurrent.ConcurrentMap
5+
36
/**
47
* Associations between a key of type [T] and a [PropertyContainer].
58
*
@@ -30,7 +33,7 @@ interface AssociatedProperties<T, V> {
3033
* Mutable implementation of [AssociatedProperties].
3134
*/
3235
class MutableAssociatedProperties<T, V> : AssociatedProperties<T, V> {
33-
private val properties: MutableMap<T, MutablePropertyContainer<V>> = mutableMapOf()
36+
private val properties: ConcurrentMap<T, MutablePropertyContainer<V>> = ConcurrentHashMap()
3437

35-
override fun of(key: T): MutablePropertyContainer<V> = properties.getOrPut(key) { MutablePropertyContainer() }
38+
override fun of(key: T): MutablePropertyContainer<V> = properties.computeIfAbsent(key) { MutablePropertyContainer() }
3639
}

quarkdown-core/src/main/kotlin/com/quarkdown/core/rendering/tag/TagBuilder.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.quarkdown.core.rendering.tag
22

33
import com.quarkdown.core.ast.Node
4+
import com.quarkdown.core.ast.parallelAcceptAll
45

56
/**
67
* A builder of a generic output code wrapped within tags (of any kind) which can be unlimitedly nested.
@@ -68,7 +69,7 @@ abstract class TagBuilder(
6869
* Usage: `+someNode.children`
6970
*/
7071
operator fun List<Node>.unaryPlus() {
71-
forEach { +it }
72+
parallelAcceptAll(renderer).forEach { +it }
7273
}
7374
}
7475

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.quarkdown.core.util
2+
3+
import kotlinx.coroutines.Dispatchers
4+
import kotlinx.coroutines.async
5+
import kotlinx.coroutines.awaitAll
6+
import kotlinx.coroutines.coroutineScope
7+
import kotlinx.coroutines.runBlocking
8+
9+
/**
10+
* Minimum number of items required for parallel execution to be worthwhile.
11+
* Below this threshold, the overhead of coroutine scheduling exceeds the benefit.
12+
*/
13+
private const val MIN_ITEMS_FOR_PARALLELISM = 4
14+
15+
/**
16+
* Tracks whether the current thread is already inside a [mapParallel] invocation,
17+
* preventing nested [runBlocking] calls that would risk thread pool exhaustion.
18+
*/
19+
private val insideParallelBlock = ThreadLocal.withInitial { false }
20+
21+
/**
22+
* Maps each element of this list using [transform], executing transformations in parallel
23+
* via coroutines when the list is large enough to benefit from concurrency.
24+
* Falls back to sequential mapping for small lists where coroutine overhead exceeds benefit.
25+
*
26+
* Handles nested invocations safely: if already executing inside a parallel block,
27+
* the inner call uses [coroutineScope] instead of [runBlocking] to avoid thread pool exhaustion.
28+
*
29+
* Results are returned in the same order as the input list.
30+
* @param transform the transformation to apply to each element
31+
* @return the list of transformed results, preserving input order
32+
*/
33+
fun <T, R> List<T>.mapParallel(transform: (T) -> R): List<R> {
34+
if (size < MIN_ITEMS_FOR_PARALLELISM) {
35+
return map(transform)
36+
}
37+
38+
// If already inside a parallel block, delegate to a suspending coroutineScope
39+
// to participate in the existing dispatcher without blocking a thread.
40+
if (insideParallelBlock.get()) {
41+
return runBlocking {
42+
mapParallelAsync(transform)
43+
}
44+
}
45+
46+
return runBlocking(Dispatchers.Default) {
47+
mapParallelAsync(transform)
48+
}
49+
}
50+
51+
/**
52+
* Suspending implementation of parallel mapping.
53+
* Launches an [async] coroutine per element and awaits all results in order.
54+
*/
55+
private suspend fun <T, R> List<T>.mapParallelAsync(transform: (T) -> R): List<R> =
56+
coroutineScope {
57+
map { element ->
58+
async {
59+
insideParallelBlock.set(true)
60+
try {
61+
transform(element)
62+
} finally {
63+
insideParallelBlock.set(false)
64+
}
65+
}
66+
}.awaitAll()
67+
}

quarkdown-plaintext/src/main/kotlin/com/quarkdown/rendering/plaintext/node/PlainTextNodeRenderer.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import com.quarkdown.core.ast.base.inline.StrongEmphasis
3939
import com.quarkdown.core.ast.base.inline.SubdocumentLink
4040
import com.quarkdown.core.ast.base.inline.Text
4141
import com.quarkdown.core.ast.dsl.buildBlock
42+
import com.quarkdown.core.ast.parallelAcceptAll
4243
import com.quarkdown.core.ast.quarkdown.CaptionableNode
4344
import com.quarkdown.core.ast.quarkdown.FunctionCallNode
4445
import com.quarkdown.core.ast.quarkdown.bibliography.BibliographyCitation
@@ -91,7 +92,7 @@ class PlainTextNodeRenderer(
9192
) : NodeRenderer {
9293
private fun NestableNode.visitChildren() = children.visitAll()
9394

94-
private fun InlineContent.visitAll() = joinToString(separator = "") { it.accept(this@PlainTextNodeRenderer) }
95+
private fun InlineContent.visitAll() = parallelAcceptAll(this@PlainTextNodeRenderer).joinToString(separator = "")
9596

9697
private val String.blockNode: String
9798
get() =

0 commit comments

Comments
 (0)