Skip to content

Commit 5d56150

Browse files
committed
Remove unneeded values from state when expected types are specified.
Optimize reduce graph algorithm. Add benchmarks for core functions Small fixes
1 parent d5b5d54 commit 5d56150

14 files changed

Lines changed: 293 additions & 96 deletions

File tree

tool/execution/parallel/src/main/kotlin/flank/exection/parallel/Parallel.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ object Parallel {
6161
*/
6262
data class Task<R : Any>(
6363
val signature: Signature<R>,
64-
val execute: ExecuteTask<R>
64+
val execute: ExecuteTask<R>,
65+
val expected: Boolean = true,
6566
) {
6667
/**
6768
* The task signature.
@@ -78,7 +79,7 @@ object Parallel {
7879
/**
7980
* Parameterized factory for creating task functions in scope of [X].
8081
*/
81-
class Function<X : Context>(override val context: () -> X) : ContextProvider<X>
82+
class Function<X : Context>(override val context: () -> X) : ContextProvider<X>()
8283

8384
data class Event internal constructor(
8485
val type: Type<*>,
@@ -115,6 +116,11 @@ typealias Output = Any.() -> Unit
115116
*/
116117
typealias ParallelState = Map<Parallel.Type<*>, Any>
117118

119+
/**
120+
* Immutable state for parallel execution.
121+
*/
122+
typealias Property = Pair<Parallel.Type<*>, Any>
123+
118124
/**
119125
* Type for group of parallel tasks. Each task must be unique in group.
120126
*/
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
package flank.exection.parallel
22

3-
import flank.exection.parallel.internal.initialValidators
3+
import flank.exection.parallel.internal.contextValidators
44
import flank.exection.parallel.internal.reduceTo
55

66
/**
7-
* Reduce given [Tasks] by [select] types to remove unneeded tasks from the graph.
7+
* Reduce given [Tasks] by [expected] types to remove unneeded tasks from the graph.
88
* The returned graph will hold only tasks that are returning selected types, their dependencies and derived dependencies.
99
* Additionally this is keeping also the validators for initial state.
1010
*
1111
* @return Reduced [Tasks]
1212
*/
1313
operator fun Tasks.invoke(
14-
select: Set<Parallel.Type<*>>
14+
expected: Set<Parallel.Type<*>>
1515
): Tasks =
16-
reduceTo(select + initialValidators)
16+
reduceTo(expected + contextValidators())
1717

1818
/**
1919
* Shortcut for tasks reducing.
2020
*/
2121
operator fun Tasks.invoke(
22-
vararg returns: Parallel.Type<*>
23-
): Tasks = invoke(returns.toSet())
22+
vararg expected: Parallel.Type<*>
23+
): Tasks = invoke(expected.toSet())

tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/ContextProvider.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ package flank.exection.parallel.internal
22

33
import flank.exection.parallel.ExecuteTask
44
import flank.exection.parallel.Parallel
5+
import flank.exection.parallel.validator
56

67
/**
78
* Abstract factory for creating task function.
89
*/
9-
internal interface ContextProvider<X : Parallel.Context> {
10-
val context: () -> X
10+
abstract class ContextProvider<X : Parallel.Context> {
11+
protected abstract val context: () -> X
12+
1113
operator fun <R> invoke(body: suspend X.() -> R): ExecuteTask<R> =
1214
{ context().also { it.state = this }.body() }
15+
16+
val validator by lazy { validator(context) }
1317
}

tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Execution.kt

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import flank.exection.parallel.Parallel.Logger
66
import flank.exection.parallel.Parallel.Task
77
import flank.exection.parallel.Parallel.Type
88
import flank.exection.parallel.ParallelState
9+
import flank.exection.parallel.Property
910
import flank.exection.parallel.Tasks
1011
import kotlinx.coroutines.CoroutineScope
1112
import kotlinx.coroutines.Dispatchers
@@ -19,6 +20,7 @@ import kotlinx.coroutines.flow.onCompletion
1920
import kotlinx.coroutines.flow.onEach
2021
import kotlinx.coroutines.flow.scan
2122
import kotlinx.coroutines.launch
23+
import java.util.concurrent.atomic.AtomicInteger
2224

2325
/**
2426
* Invoke the given [Execution] in parallel.
@@ -30,7 +32,7 @@ internal operator fun Execution.invoke(): Flow<ParallelState> =
3032
.onEach { (type, value) -> if (value is Throwable && isNotClosed()) abortBy(type, value) }
3133

3234
// Accumulate each received value in state.
33-
.scan(initial) { state, value -> state + value }
35+
.scan(initial, updateState())
3436

3537
// Handle state changes.
3638
.onEach { state: ParallelState ->
@@ -90,13 +92,21 @@ internal class Execution(
9092
/**
9193
* The set of value types required to complete the execution.
9294
*/
93-
val required = tasks.map(Task<*>::type).toSet()
95+
val required = tasks.filter(Task<*>::expected).map(Task<*>::type).toSet()
9496

9597
/**
9698
* Map of remaining tasks for run grouped by arguments.
9799
*/
98100
val remaining = tasks.groupBy(Task<*>::args).toMutableMap()
99101

102+
/**
103+
* Reference counter for state types marked as not expected.
104+
* Values of types that are not expected but required as dependencies,
105+
* can be removed when there are no remaining tasks depending on them.
106+
*/
107+
val references = tasks.flatMap(Task<*>::args).groupBy { it }
108+
.minus(required).mapValues { (_, refs) -> AtomicInteger(refs.size) }
109+
100110
/**
101111
* Reference to optional output for structural logs.
102112
*/
@@ -130,6 +140,17 @@ private suspend fun Execution.abortBy(type: Type<*>, cause: Throwable) {
130140
channel.close()
131141
}
132142

143+
/**
144+
* Create function for updating state depending on reference counter state.
145+
*/
146+
private fun Execution.updateState(): suspend (ParallelState, Property) -> ParallelState =
147+
if (references.isEmpty()) ParallelState::plus
148+
else { state, property ->
149+
references.filterValues { counter -> counter.compareAndSet(0, 0) }
150+
.map { (type, _) -> type }
151+
.let { junks -> state + property - junks }
152+
}
153+
133154
/**
134155
* The execution is complete when all required types was accumulated to state.
135156
*/
@@ -157,7 +178,7 @@ private fun Execution.filterTasksFor(state: ParallelState): Map<Set<Type<*>>, Li
157178
channel.isEmpty.not() || // some values are waiting in the channel queue.
158179
throw DeadlockError(state, jobs, remaining)
159180

160-
// Remove from queue the tasks for current iteration.
181+
// Remove from queue tasks for current iteration.
161182
remaining -= keys
162183
}
163184

@@ -185,6 +206,11 @@ private fun Execution.execute(
185206
task: Task<*>,
186207
args: Map<Type<*>, Any>,
187208
) {
209+
// Decrement references to arguments types
210+
if (references.isNotEmpty()) task.args.forEach { type ->
211+
references[type]?.getAndUpdate { count -> count - 1 }
212+
}
213+
188214
// Obtain type of current task.
189215
val type: Type<*> = task.type
190216

tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Prepare.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ import flank.exection.parallel.Tasks
77
* Get initial state validators.
88
* This is necessary to perform validations of initial state before the execution.
99
*/
10-
internal val Tasks.initialValidators: List<Parallel.Context>
11-
get() = mapNotNull { task -> task.type as? Parallel.Context }
10+
internal fun Tasks.contextValidators(): List<Parallel.Context> =
11+
mapNotNull { task -> task.type as? Parallel.Context }

tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/Property.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import flank.exection.parallel.ParallelState
77
* Factory function for lazy property delegate.
88
*/
99
internal fun <T : Any> Parallel.Context.lazyProperty(type: Parallel.Type<T>) = lazy {
10+
@Suppress("UNCHECKED_CAST")
1011
state[type] as T
1112
}
1213

@@ -16,19 +17,19 @@ internal fun <T : Any> Parallel.Context.lazyProperty(type: Parallel.Type<T>) = l
1617
class EagerProperties(
1718
private val state: () -> ParallelState
1819
) {
19-
private val set = mutableSetOf<Lazy<*>>()
20+
private val props = mutableSetOf<Lazy<*>>()
2021

2122
/**
2223
* Initialize eager properties, this performs also validation.
2324
*/
24-
operator fun invoke(): Unit = set.forEach { prop -> println(prop.value) }
25+
operator fun invoke(): Unit = props.forEach { prop -> prop.value }
2526

2627
/**
2728
* Register new parallel type. Inline modifier is necessary to perform real type check
2829
*/
2930
inline operator fun <reified T : Any> invoke(type: Parallel.Type<T>): Lazy<T> = lazy { type.value() as T }.append()
3031

3132
// local helpers need to be public because of inlined invoke
32-
fun <T> Lazy<T>.append(): Lazy<T> = apply { set.plusAssign(this) }
33+
fun <T> Lazy<T>.append(): Lazy<T> = apply { props += this }
3334
fun <T : Any> Parallel.Type<T>.value(): Any? = state()[this]
3435
}
Lines changed: 16 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,22 @@
11
package flank.exection.parallel.internal
22

3-
import flank.exection.parallel.Parallel
3+
import flank.exection.parallel.Parallel.Task
4+
import flank.exection.parallel.Parallel.Type
45
import flank.exection.parallel.Tasks
6+
import flank.exection.parallel.internal.graph.findDependenciesIn
57

68
internal infix fun Tasks.reduceTo(
7-
selectedTypes: Set<Parallel.Type<*>>
8-
): Tasks =
9-
filter { task -> task.type in selectedTypes }
10-
.toSet()
11-
.apply {
12-
val notFound = selectedTypes - map(Parallel.Task<*>::type)
13-
if (notFound.isNotEmpty()) throw Exception("Cannot reduce find tasks for the following types: $notFound")
9+
expected: Set<Type<*>>
10+
): Tasks {
11+
val notFound = expected - map(Task<*>::type)
12+
if (notFound.isNotEmpty()) throw Exception("Cannot find tasks for the following types: $notFound")
13+
val graph: Map<Type<*>, Set<Type<*>>> = associate { task -> task.type to task.args }
14+
val dependencies = expected.findDependenciesIn(graph)
15+
return mapNotNull { task ->
16+
when (task.type) {
17+
in expected -> task
18+
in dependencies -> task.copy(expected = false)
19+
else -> null
1420
}
15-
.reduce(this)
16-
17-
/**
18-
* Reduce [all] steps to given receiver steps and their dependencies.
19-
*
20-
* @receiver The task selector for current reducing step.
21-
* @param all The list of all tasks that are under reducing.
22-
* @param acc Currently accumulated tasks.
23-
* @return Accumulated tasks if selector is empty.
24-
*/
25-
private tailrec fun Tasks.reduce(
26-
all: Tasks,
27-
acc: Tasks =
28-
if (isEmpty()) all
29-
else emptySet(),
30-
): Tasks =
31-
when {
32-
isEmpty() -> acc
33-
else -> flatMap(Parallel.Task<*>::args)
34-
.mapNotNull(all::findByType)
35-
.toSet()
36-
.reduce(all, acc + this)
37-
}
38-
39-
private fun Tasks.findByType(
40-
type: Parallel.Type<*>
41-
): Parallel.Task<*>? =
42-
find { task -> task.type == type }
21+
}.toSet()
22+
}

tool/execution/parallel/src/main/kotlin/flank/exection/parallel/internal/graph/FindCycles.kt

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
package flank.exection.parallel.internal.graph
22

3-
tailrec fun <T : Any> Map<T, Set<T>>.findCycles(
4-
5-
remaining: List<T> = toList()
6-
.sortedByDescending { (_, edges) -> edges.size }
7-
.map { (vertices, _) -> vertices }
8-
.toMutableList(),
3+
/**
4+
* Find cycles in given graph.
5+
*
6+
* @receiver Graph to search.
7+
* @return List of cycles. Each cycle is a list of nodes.
8+
*/
9+
internal tailrec fun <T : Any> Map<T, Set<T>>.findCycles(
10+
11+
remaining: Set<T> = toList()
12+
.sortedByDescending { (_, children) -> children.size }
13+
.map { (parent, _) -> parent }
14+
.toSet(),
915

1016
queue: List<T> = emptyList(),
1117

@@ -28,7 +34,6 @@ tailrec fun <T : Any> Map<T, Set<T>>.findCycles(
2834
val cycle = current in path + next
2935

3036
// println("$cycle R:$remaining Q:$queue N:$next C:$current P:$path V:$visited")
31-
// println()
3237

3338
return findCycles(
3439
remaining =
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package flank.exection.parallel.internal.graph
2+
3+
/**
4+
* Find dependencies for given nodes in [graph].
5+
*
6+
* @receiver Expected elements for current iteration.
7+
* @param graph Graph to search.
8+
* @param acc Currently accumulated elements.
9+
* @return Set expected elements along with dependencies.
10+
*/
11+
internal tailrec fun <T> Set<T>.findDependenciesIn(
12+
graph: Map<T, Set<T>>,
13+
acc: Set<T> =
14+
if (isEmpty()) graph.keys
15+
else emptySet(),
16+
): Set<T> =
17+
when {
18+
isEmpty() -> acc // No more elements, so return all accumulated.
19+
else -> flatMap(graph::getValue).toSet() // Map each expected element to its dependencies.
20+
.minus(acc) // Remove already accumulated elements to optimize calculations.
21+
.findDependenciesIn(graph, acc + this) // Accumulate current dependencies and run next iteration.
22+
}

0 commit comments

Comments
 (0)