├── .codecov.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── academic_join_calculus_2.png ├── benchmark ├── lib │ ├── JiansenJoin-0.3.6-JoinRun-0.1.0.jar │ └── LICENSE.JiansenHe-join.txt └── src │ ├── main │ └── scala │ │ └── io │ │ └── chymyst │ │ └── benchmark │ │ ├── Benchmarks1.scala │ │ ├── Benchmarks11.scala │ │ ├── Benchmarks4.scala │ │ ├── Benchmarks7.scala │ │ ├── Benchmarks9.scala │ │ ├── Common.scala │ │ ├── MainApp.scala │ │ └── MergeSort.scala │ └── test │ └── scala │ └── io │ └── chymyst │ └── benchmark │ ├── ConcurrentCounterSpec.scala │ ├── EightQueensSpec.scala │ ├── JiansenFairnessSpec.scala │ ├── LtqExecutor.scala │ ├── MapReduceSpec.scala │ ├── MergesortSpec.scala │ ├── MultithreadSpec.scala │ ├── ReactionDelaySpec.scala │ ├── RepeatedInputSpec.scala │ └── SingleThreadSpec.scala ├── build.sbt ├── core ├── lib │ ├── javolution-core-java-6.0.0-javadoc.jar │ ├── javolution-core-java-6.0.0-sources.jar │ ├── javolution-core-java-6.0.0.jar │ ├── javolution.LICENSE.txt │ ├── scalaxy-streams.LICENSE.txt │ ├── scalaxy-streams_2.12-0.4-SNAPSHOT-javadoc.jar │ ├── scalaxy-streams_2.12-0.4-SNAPSHOT-sources.jar │ └── scalaxy-streams_2.12-0.4-SNAPSHOT.jar └── src │ ├── main │ ├── scala │ │ └── io │ │ │ └── chymyst │ │ │ ├── jc │ │ │ ├── BlockingPool.scala │ │ │ ├── ChymystThread.scala │ │ │ ├── Core.scala │ │ │ ├── CrossMoleculeSorting.scala │ │ │ ├── EventReporting.scala │ │ │ ├── FixedPool.scala │ │ │ ├── Macros.scala │ │ │ ├── Molecules.scala │ │ │ ├── MutableBag.scala │ │ │ ├── Pool.scala │ │ │ ├── Reaction.scala │ │ │ ├── ReactionMacros.scala │ │ │ ├── ReactionSite.scala │ │ │ ├── StaticAnalysis.scala │ │ │ └── package.scala │ │ │ └── util │ │ │ ├── Budu.scala │ │ │ ├── ConjunctiveNormalForm.scala │ │ │ └── LabeledTypes.scala │ └── tut │ │ └── chymyst-quick.md │ └── test │ ├── resources │ └── fork-join-test │ │ ├── fork-join-test-1.txt │ │ ├── fork-join-test-2.txt │ │ ├── fork-join-test-3.txt │ │ └── non-empty-dir │ │ └── fork-join-test-1.txt │ └── scala │ └── io │ └── chymyst │ ├── jc │ ├── CoreSpec.scala │ ├── CrossMoleculeSortingUtest.scala │ ├── GuardsErrorsUtest.scala │ ├── GuardsSpec.scala │ ├── MacroCompileErrorsUtest.scala │ ├── MacroErrorSpec.scala │ ├── MacrosSpec.scala │ ├── MoleculesSpec.scala │ ├── MutableBagSpec.scala │ ├── PoolSpec.scala │ ├── ReactionSiteSpec.scala │ └── Sha1Props.scala │ ├── test │ ├── BlockingMoleculesSpec.scala │ ├── Common.scala │ ├── DiningPhilosophersNSpec.scala │ ├── DiningPhilosophersSpec.scala │ ├── EventHooksSpec.scala │ ├── FairnessSpec.scala │ ├── GameOfLifeSpec.scala │ ├── LogSpec.scala │ ├── MoreBlockingSpec.scala │ ├── ParallelOrSpec.scala │ ├── Patterns01Spec.scala │ ├── Patterns02Spec.scala │ ├── Patterns03Spec.scala │ ├── PaxosSpec.scala │ ├── ShutdownSpec.scala │ ├── StaticAnalysisSpec.scala │ └── StaticMoleculesSpec.scala │ └── util │ ├── BuduSpec.scala │ ├── FinalTagless.scala │ ├── FinalTaglessSpec.scala │ └── LabeledTypesSpec.scala ├── docs ├── An_illustration_of_the_dining_philosophers_problem.png ├── Boyle_Self-Flowing_Flask.png ├── CNAME ├── README.md ├── SUMMARY.md ├── academic_join_calculus_2.png ├── chymyst-actor.md ├── chymyst-core.md ├── chymyst-preface.md ├── chymyst-quick.md ├── chymyst00.md ├── chymyst01.md ├── chymyst02.md ├── chymyst03.md ├── chymyst04.md ├── chymyst05.md ├── chymyst07.md ├── chymyst08.md ├── chymyst09.md ├── chymyst_features.md ├── chymyst_game_of_life.md ├── chymyst_vs_jc.md ├── concurrency-in-reactions-declarative-multicore-in-Scala.epub ├── concurrency-in-reactions-declarative-multicore-in-Scala.pdf ├── concurrency-nontechnical.md ├── concurrency.md ├── counter-incr-decr.svg ├── counter-multiple-molecules-after-reaction.svg ├── counter-multiple-molecules.svg ├── counter-multiple-reactions.svg ├── fork-join-weights.svg ├── other_work.md ├── reactions1.svg ├── reactions2.svg ├── roadmap.md └── tables.css ├── project ├── assembly.sbt ├── build.properties └── plugins.sbt └── sonatype.sbt /.codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "benchmark/src" # ignore benchmarks for coverage analysis 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache 6 | .history 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # Scala-IDE specific 16 | .scala_dependencies 17 | .worksheet 18 | *.iml 19 | project/*.iml 20 | project/project 21 | project/target 22 | .idea* 23 | .iml* 24 | *.ipr 25 | *.iws 26 | 27 | benchmark*.txt 28 | local.* 29 | 30 | logs/ 31 | *.log.txt.xz 32 | 33 | *.jfr 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | 3 | # This is only necessary for trying out oraclejdk9, which doesn't yet work on their side 4 | # sudo: false 5 | # dist: trusty 6 | 7 | scala: 8 | - 2.11.8 9 | - 2.11.12 10 | - 2.12.2 11 | - 2.12.6 12 | jdk: 13 | - oraclejdk8 14 | 15 | addons: 16 | apt: 17 | packages: 18 | - oracle-java8-installer 19 | - oracle-java8-set-default 20 | 21 | script: 22 | - sbt clean coverage test coverageReport package package-doc 23 | 24 | after_success: 25 | - bash <(curl -s https://codecov.io/bash) 26 | 27 | # This part of the script doesn't seem to work, actually! I never see any binary releases uploaded to github. 28 | deploy: 29 | file_glob: true 30 | provider: releases 31 | api_key: 32 | secure: "QtQK+MK4yylLKyLV6wDF4uuT9R9i/8OhK7OGC8pKAIzu9wylWGMfFmje3JwD69Ek0yd06h+3xG8OU8rRAfQ+hfCRubXGHKi76zeouvxe8BGpbQ6PSsrnUZV6mmOKFWG2EMhVds+r8vZkV/LcT8+RJ6hZoQWkxrsQ7teiV/Bxm5FYiN3hQJHjbJ5H7jRKBRDnDTI1LbTejpVaNCYq7habYhjSYwHpzfuLrtoE0RZvArAz06ltG8iAgLUZiYGPzv3eVWwhhtsj+dTX+B4p5hvdj5ULeRUdU5+XbBwu6+ZneYI1nDLZCxQiN2LWmqhfBvYSeYsmAOySexueOMIwL/+92mD3EHTNoXs39ZX0DSa8sulLQdq+SCeTc7F2SlpsZVesmDm3Y2cfGC5uhIdj7TeP0XD7ex5sL7Wt7gYrrEjhghxaTbyMZy9KtGHw0YEfSM3n9QbDXPun9mk7YPJs+m1jvTa5cL5kxFWI+Oz3eoFV/tSpmVNfxjTIQLlWcasriotMjIM1JoO/qcAe8eoLefpJnfsZZK3IdBRtPm2Gvv+b4I4ET/td/N5k4cFJqp6++vk6ByE+MnctYsOmeftpNPlLRAfh4Ma3OTg9xbaTXUWV62E39XYb6RRTEj0KfI3wvOounWcYIZdhUnLr8hBkczXO8AsPlOHviYU7fpldut6tCUs=" 33 | file: "core/target/scala-2.12/*core*.jar" 34 | skip_cleanup: true 35 | on: 36 | tags 37 | -------------------------------------------------------------------------------- /academic_join_calculus_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chymyst/chymyst-core/8355442b8effdf2fe8bc9c66fe02cb73eb05e71d/academic_join_calculus_2.png -------------------------------------------------------------------------------- /benchmark/lib/JiansenJoin-0.3.6-JoinRun-0.1.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chymyst/chymyst-core/8355442b8effdf2fe8bc9c66fe02cb73eb05e71d/benchmark/lib/JiansenJoin-0.3.6-JoinRun-0.1.0.jar -------------------------------------------------------------------------------- /benchmark/src/main/scala/io/chymyst/benchmark/Benchmarks1.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.benchmark 2 | 3 | import java.time.LocalDateTime 4 | 5 | import io.chymyst.benchmark.Common._ 6 | import io.chymyst.jc._ 7 | 8 | import code.jiansen.scalajoin._ // Use precompiled classes from Jiansen's Join.scala, which are in that package. 9 | 10 | object Benchmarks1 { 11 | 12 | def benchmark1(count: Int, tp: Pool): Long = { 13 | 14 | val c = m[Int] 15 | val g = b[Unit,Int] 16 | val i = m[Unit] 17 | val d = m[Unit] 18 | val f = b[LocalDateTime,Long] 19 | 20 | withPool(FixedPool(8)) { tp1 => 21 | site(tp)( // Right now we don't use tp1. If we use `site(tp, tp1)`, the run time is increased by factor 2. 22 | go { case c(0) + f(tInit, r) => 23 | r(elapsed(tInit)) 24 | }, 25 | go { case g(_, reply) + c(n) => c(n); reply(n) }, 26 | go { case c(n) + i(_) => c(n + 1) }, 27 | go { case c(n) + d(_) if n > 0 => c(n - 1) } 28 | ) 29 | 30 | c(count) 31 | val initialTime = LocalDateTime.now 32 | (1 to count).foreach { _ => d() } 33 | f(initialTime) 34 | }.get 35 | } 36 | 37 | def make_counterJoinScala(init: Int): (AsyName[Unit],AsyName[Unit],SynName[LocalDateTime, Long],SynName[Unit,Int]) = { 38 | object j2 extends Join { 39 | object c extends AsyName[Int] 40 | object g extends SynName[Unit, Int] 41 | object i extends AsyName[Unit] 42 | object d extends AsyName[Unit] 43 | object f extends SynName[LocalDateTime, Long] 44 | 45 | join { 46 | case c(0) and f(tInit) => 47 | f.reply(elapsed(tInit)) 48 | case c(n) and d(_) if n > 0 => c(n-1) 49 | case c(n) and i(_) => c(n+1) 50 | case c(n) and g(_) => c(n); g.reply(n) 51 | } 52 | 53 | } 54 | 55 | j2.c(init) 56 | (j2.d,j2.i,j2.f,j2.g) 57 | } 58 | 59 | def make_counterChymyst(init: Int, tp: Pool) = { 60 | val c = m[Int] 61 | val g = b[Unit,Int] 62 | val i = m[Unit] 63 | val d = m[Unit] 64 | val f = b[LocalDateTime,Long] 65 | 66 | site(tp)( 67 | go { case c(0) + f(tInit, r) => r(elapsed(tInit)) }, 68 | go { case g(_, reply) + c(n) => c(n); reply(n) }, 69 | go { case c(n) + i(_) => c(n + 1) }, 70 | go { case c(n) + d(_) if n > 0 => c(n - 1) } 71 | ) 72 | 73 | c(init) 74 | (d,i,f,g) 75 | } 76 | 77 | def benchmark2(count: Int, tp: Pool): Long = { 78 | 79 | object j2 extends Join { 80 | object c extends AsyName[Int] 81 | object g extends SynName[Unit, Int] 82 | object i extends AsyName[Unit] 83 | object d extends AsyName[Unit] 84 | object f extends SynName[LocalDateTime, Long] 85 | 86 | join { 87 | case c(0) and f(tInit) => 88 | f.reply(elapsed(tInit)) 89 | case c(n) and d(_) if n > 0 => c(n-1) 90 | case c(n) and i(_) => c(n+1) 91 | case c(n) and g(_) => c(n); g.reply(n) 92 | } 93 | 94 | } 95 | j2.c(count) 96 | 97 | val initialTime = LocalDateTime.now 98 | (1 to count).foreach{ _ => j2.d(()) } 99 | j2.f(initialTime) 100 | } 101 | 102 | def benchmark2a(count: Int, tp: Pool): Long = { 103 | 104 | val initialTime = LocalDateTime.now 105 | 106 | val (d,_,f,_) = make_counterJoinScala(count) 107 | (1 to count).foreach{ _ => d(()) } 108 | f(initialTime) 109 | } 110 | 111 | def benchmark3(count: Int, tp: Pool): Long = { 112 | 113 | val initialTime = LocalDateTime.now 114 | 115 | val (d,_,f,_) = make_counterChymyst(count, tp) 116 | (1 to count).foreach{ _ => d() } 117 | 118 | f(initialTime) 119 | } 120 | 121 | } -------------------------------------------------------------------------------- /benchmark/src/main/scala/io/chymyst/benchmark/Benchmarks11.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.benchmark 2 | 3 | import io.chymyst.jc._ 4 | import Common._ 5 | import MergeSort._ 6 | 7 | object Benchmarks11 { 8 | val counterMultiplier = 5 9 | 10 | def benchmark11(count: Int, tp: Pool): Long = timeThis { 11 | (1 to counterMultiplier).foreach { _ ⇒ 12 | val c = m[(Int, Int)] 13 | val done = m[Int] 14 | val f = b[Unit, Int] 15 | 16 | val total = count 17 | 18 | site(tp)( 19 | go { case f(_, r) + done(x) => r(x) }, 20 | go { case c((n, x)) + c((m, y)) if x <= y => 21 | val p = n + m 22 | val z = x + y 23 | if (p == total) 24 | done(z) 25 | else 26 | c((n + m, x + y)) 27 | } 28 | ) 29 | 30 | (1 to total).foreach(i => c((1, i * i))) 31 | f() 32 | } 33 | } 34 | 35 | val mergeSortSize = 1000 36 | val mergeSortIterations = 20 37 | 38 | def benchmark12(count: Int, tp: Pool): Long = timeThis { 39 | 40 | (1 to mergeSortIterations).foreach { _ ⇒ 41 | val arr = Array.fill[Int](mergeSortSize)(scala.util.Random.nextInt(mergeSortSize)) 42 | performMergeSort(arr, 8) 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /benchmark/src/main/scala/io/chymyst/benchmark/Benchmarks7.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.benchmark 2 | 3 | import java.time.LocalDateTime 4 | import io.chymyst.benchmark.Common._ 5 | import io.chymyst.jc._ 6 | 7 | import code.jiansen.scalajoin._ // Use precompiled classes from Jiansen's Join.scala, which are in that package. 8 | 9 | object Benchmarks7 { 10 | 11 | /// Concurrent decrement of `n` counters, each going from `count` to 0 concurrently. 12 | 13 | /// create `n` asynchronous counters, initialize each to `count`, then decrement `count*n` times, until all counters are zero. 14 | /// collect the zero-counter events, make sure there are `n` of them, then fire an `all_done` event that yields the benchmark time. 15 | val numberOfCounters = 5 16 | 17 | def benchmark7(count: Int, tp: Pool): Long = { 18 | 19 | val done = m[Unit] 20 | val all_done = m[Int] 21 | val f = b[LocalDateTime,Long] 22 | 23 | site(tp)( 24 | go { case all_done(0) + f(tInit, r) => r(elapsed(tInit)) }, 25 | go { case all_done(x) + done(_) if x > 0 => all_done(x-1) } 26 | ) 27 | val initialTime = LocalDateTime.now 28 | all_done(numberOfCounters) 29 | 30 | val d = make_counters(done, numberOfCounters, count, tp) 31 | (1 to (count*numberOfCounters)).foreach{ _ => d() } 32 | 33 | f(initialTime) 34 | } 35 | 36 | // this deadlocks whenever `count` * `counters` becomes large. 37 | def benchmark8(count: Int, tp: Pool): Long = { 38 | 39 | println(s"Creating $numberOfCounters concurrent counters, each going from $count to 0") 40 | 41 | object j8 extends Join { 42 | object done extends AsyName[Unit] 43 | object all_done extends AsyName[Int] 44 | object f extends SynName[LocalDateTime, Long] 45 | 46 | join { 47 | case all_done(0) and f(tInit) => 48 | f.reply(elapsed(tInit)) 49 | case all_done(x) and done(_) if x > 0 => all_done(x-1) 50 | } 51 | 52 | } 53 | 54 | val initialTime = LocalDateTime.now 55 | j8.all_done(count) 56 | val d = make_counters8a(j8.done, numberOfCounters, count) 57 | (1 to (count*numberOfCounters)).foreach{ _ => d(()) } 58 | j8.f(initialTime) 59 | } 60 | 61 | private def make_counters(done: M[Unit], counters: Int, init: Int, tp: Pool) = { 62 | val c = m[Int] 63 | val d = m[Unit] 64 | 65 | site(tp)( 66 | go { case c(0) => done() }, 67 | go { case c(n) + d(_) if n > 0 => c(n - 1) } 68 | ) 69 | (1 to counters).foreach(_ => c(init)) 70 | // We return just one molecule. 71 | d 72 | } 73 | 74 | private def make_counters8a(done: AsyName[Unit], counters: Int, init: Int): AsyName[Unit] = { 75 | object j8a extends Join { 76 | object c extends AsyName[Int] 77 | object d extends AsyName[Unit] 78 | 79 | join { 80 | case c(0) => done(()) 81 | case c(n) and d(_) if n > 0 => c(n-1) 82 | } 83 | 84 | } 85 | (1 to counters).foreach(_ => j8a.c(init)) 86 | // We return just one molecule. 87 | j8a.d 88 | } 89 | 90 | } -------------------------------------------------------------------------------- /benchmark/src/main/scala/io/chymyst/benchmark/Benchmarks9.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.benchmark 2 | 3 | import java.time.LocalDateTime 4 | import io.chymyst.benchmark.Common._ 5 | import io.chymyst.jc._ 6 | import scala.concurrent.duration._ 7 | 8 | import code.jiansen.scalajoin._ // Use precompiled classes from Jiansen's Join.scala, which are in that package. 9 | 10 | object Benchmarks9 { 11 | 12 | val numberOfCounters = 5 13 | 14 | def make_counter_1(done: M[Unit], counters: Int, init: Int, tp: Pool): B[Unit, Unit] = { 15 | val c = m[Int] 16 | val d = b[Unit, Unit] 17 | 18 | site(tp)( 19 | go { case c(0) => done(()) }, 20 | go { case c(n) + d(_, r) if n > 0 => c(n - 1); r() } 21 | ) 22 | (1 to counters).foreach(_ => c(init)) 23 | // We return just one molecule. 24 | d 25 | } 26 | 27 | // emit a blocking molecule many times 28 | def benchmark9_1(count: Int, tp: Pool): Long = { 29 | 30 | val done = m[Unit] 31 | val all_done = m[Int] 32 | val f = b[LocalDateTime,Long] 33 | 34 | site(tp)( 35 | go { case all_done(0) + f(tInit, r) => r(elapsed(tInit)) }, 36 | go { case all_done(x) + done(_) if x > 0 => all_done(x-1) } 37 | ) 38 | val initialTime = LocalDateTime.now 39 | all_done(numberOfCounters) 40 | 41 | val d = make_counter_1(done, numberOfCounters, count, tp) 42 | (1 to (count*numberOfCounters)).foreach{ _ => d() } 43 | 44 | f(initialTime) 45 | } 46 | 47 | 48 | def make_ping_pong_stack(done: M[Unit], tp: Pool): B[Int,Int] = { 49 | val c = m[Unit] 50 | val d = b[Int, Int] 51 | val e = b[Int, Int] 52 | 53 | site(tp)( 54 | go { case c(_) + d(n, reply) => if (n > 0) { 55 | c() 56 | e(n-1) 57 | } 58 | else { 59 | done() 60 | } 61 | reply(n) 62 | }, 63 | go { case c(_) + e(n, reply) => if (n > 0) { 64 | c() 65 | d(n-1) 66 | } 67 | else { 68 | done() 69 | } 70 | reply(n-1) 71 | } 72 | ) 73 | c() 74 | // We return just one molecule emitter. 75 | d 76 | } 77 | 78 | val pingPongCalls = 1000 79 | 80 | // ping-pong-stack with blocking molecules 81 | def benchmark9_2(count: Int, tp: Pool): Long = { 82 | 83 | val done = m[Unit] 84 | val all_done = m[Int] 85 | val f = b[LocalDateTime,Long] 86 | 87 | val tp = BlockingPool(MainAppConfig.threads) // this benchmark will not work with a fixed pool 88 | 89 | site(tp)( 90 | go { case all_done(0) + f(tInit, r) => r(elapsed(tInit)) }, 91 | go { case all_done(x) + done(_) if x > 0 => all_done(x-1) } 92 | ) 93 | val initialTime = LocalDateTime.now 94 | all_done(1) 95 | 96 | val d = make_ping_pong_stack(done, tp) 97 | d(pingPongCalls) 98 | 99 | val result = f(initialTime) 100 | tp.shutdownNow() 101 | result 102 | } 103 | 104 | val counterMultiplier = 10 105 | 106 | def benchmark10(count: Int, tp: Pool): Long = { 107 | 108 | val initialTime = LocalDateTime.now 109 | 110 | val a = m[Boolean] 111 | val collect = m[Int] 112 | val f = b[Unit, Int] 113 | val get = b[Unit, Int] 114 | 115 | site(tp)( 116 | go { case f(_, reply) => a(reply(123)) }, 117 | go { case a(x) + collect(n) => collect(n + (if (x) 0 else 1)) }, 118 | go { case collect(n) + get(_, reply) => reply(n) } 119 | ) 120 | collect(0) 121 | 122 | val numberOfFailures = (1 to count*counterMultiplier).map { _ => 123 | if (f.timeout()(200.millis).isEmpty) 1 else 0 124 | }.sum 125 | 126 | // In this benchmark, we used to have about 4% numberOfFailures and about 2 numberOfFalseReplies in 100,000 127 | val numberOfFalseReplies = get() 128 | 129 | if (numberOfFailures != 0 || numberOfFalseReplies != 0) 130 | println(s"failures=$numberOfFailures, false replies=$numberOfFalseReplies (if these numbers are not equal, there is a race condition)") 131 | 132 | elapsed(initialTime) 133 | } 134 | 135 | def make_counter_1_Jiansen(done: AsyName[Unit], counters: Int, init: Int): SynName[Unit,Unit] = { 136 | object b9c1 extends Join { 137 | object c extends AsyName[Int] 138 | object d extends SynName[Unit, Unit] 139 | 140 | join { 141 | case c(0) => done(()) 142 | case c(n) and d(_) if n > 0 => c(n-1); d.reply(()) 143 | } 144 | } 145 | import b9c1._ 146 | (1 to counters).foreach(_ => c(init)) 147 | // We return just one molecule. 148 | d 149 | } 150 | 151 | // emit a blocking molecule many times 152 | def benchmark9_1_Jiansen(count: Int, tp: Pool): Long = { 153 | 154 | object b9c1b extends Join { 155 | object done extends AsyName[Unit] 156 | object all_done extends AsyName[Int] 157 | object f extends SynName[LocalDateTime, Long] 158 | 159 | join { 160 | case all_done(0) and f(tInit) => f.reply(elapsed(tInit)) 161 | case all_done(x) and done(_) if x > 0 => all_done(x-1) 162 | } 163 | } 164 | 165 | import b9c1b._ 166 | 167 | val initialTime = LocalDateTime.now 168 | all_done(numberOfCounters) 169 | 170 | val d = make_counter_1_Jiansen(done, numberOfCounters, count) 171 | (1 to (count*numberOfCounters)).foreach{ _ => d(()) } 172 | 173 | val result = f(initialTime) 174 | result 175 | } 176 | 177 | 178 | } -------------------------------------------------------------------------------- /benchmark/src/main/scala/io/chymyst/benchmark/Common.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.benchmark 2 | 3 | import java.time.LocalDateTime 4 | import java.time.temporal.ChronoUnit 5 | 6 | object Common { 7 | val warmupTimeMs = 50L 8 | 9 | def elapsed(initTime: LocalDateTime): Long = initTime.until(LocalDateTime.now, ChronoUnit.MILLIS) 10 | 11 | def elapsed(initTime: Long): Long = System.currentTimeMillis() - initTime 12 | 13 | def timeThis(task: => Unit): Long = { 14 | val initTime = LocalDateTime.now 15 | task 16 | elapsed(initTime) 17 | } 18 | 19 | def timeWithPriming(task: => Unit): Long = { 20 | task // this is just priming, no measurement 21 | 22 | val result1 = timeThis { 23 | task 24 | } 25 | val result2 = timeThis { 26 | task 27 | } 28 | (result1 + result2 + 1) / 2 29 | } 30 | 31 | def waitSome(): Unit = Thread.sleep(warmupTimeMs) 32 | 33 | } 34 | -------------------------------------------------------------------------------- /benchmark/src/main/scala/io/chymyst/benchmark/MainApp.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.benchmark 2 | 3 | import Benchmarks1._ 4 | import Benchmarks4._ 5 | import Benchmarks7._ 6 | import Benchmarks9._ 7 | import Benchmarks11._ 8 | import io.chymyst.jc._ 9 | 10 | object MainAppConfig { 11 | 12 | val n = 50000 13 | 14 | val threads = 8 15 | } 16 | 17 | object MainApp extends App { 18 | 19 | import MainAppConfig._ 20 | 21 | def run3times(task: => Long): Long = { 22 | task 23 | // just priming, no measurement 24 | val result1 = { 25 | task 26 | } 27 | val result2 = { 28 | task 29 | } 30 | println(s"debug: run3times got $result1, $result2") 31 | (result1 + result2 + 1) / 2 32 | } 33 | 34 | println(s"Benchmark parameters: count to $n, threads = $threads") 35 | 36 | Seq[(String, (Int, Pool) => Long)]( 37 | // "(this deadlocks) 50 different reactions chained together, using Jiansen's Join.scala" -> benchmark5_100 _, 38 | // "(StackOverflowError) same but with only 6 reactions, using Jiansen's Join.scala" -> benchmark5_6 _, 39 | // "(this deadlocks) many concurrent counters with non-blocking access, using Jiansen's Join.scala" -> benchmark8 _, 40 | // List the benchmarks that we should run. 41 | s"count using Chymyst" -> benchmark1 _ 42 | , s"count using Jiansen's Join.scala" -> benchmark2 _ 43 | , "counter in a closure, using Chymyst" -> benchmark3 _ 44 | , "counter in a closure, using Jiansen's Join.scala" -> benchmark2a _ 45 | , s"${Benchmarks4.differentReactions} different reactions chained together, 2000 times" -> benchmark4_100 _ 46 | , s"${Benchmarks7.numberOfCounters} concurrent counters with non-blocking access" -> benchmark7 _ 47 | , s"${Benchmarks9.numberOfCounters} concurrent counters with blocking access, using Chymyst" -> benchmark9_1 _ 48 | , s"${Benchmarks9.numberOfCounters} concurrent counters with blocking access, using Jiansen's Join.scala" -> benchmark9_1_Jiansen _ 49 | , s"${Benchmarks9.pingPongCalls} blocked threads with ping-pong calls" -> benchmark9_2 _ 50 | , s"count to ${Benchmarks9.counterMultiplier * n} using blocking access with checking reply status" -> benchmark10 _ 51 | , s"sum an array of size $n using repeated molecules, ${Benchmarks11.counterMultiplier} times" -> benchmark11 _ 52 | , s"perform merge-sort of size ${Benchmarks11.mergeSortSize}, ${Benchmarks11.mergeSortIterations} times" → benchmark12 _ 53 | ).zipWithIndex.foreach { 54 | case ((message, benchmark), i) => println(s"Benchmark ${i + 1} took ${ 55 | run3times { 56 | val tp = FixedPool(threads) 57 | val result = benchmark(n, tp) 58 | tp.shutdownNow() 59 | result 60 | } 61 | } ms ($message)") 62 | } 63 | 64 | defaultPool.shutdownNow() 65 | } 66 | -------------------------------------------------------------------------------- /benchmark/src/main/scala/io/chymyst/benchmark/MergeSort.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.benchmark 2 | 3 | // Make all imports explicit, just to see what is the entire set of required imports. 4 | // Do not optimize imports in this file! 5 | import io.chymyst.jc.{+, FixedPool, M, m, B, b, go, Reaction, ReactionInfo, InputMoleculeInfo, AllMatchersAreTrivial, OutputMoleculeInfo, site, EmitMultiple} 6 | import io.chymyst.jc.ConsoleErrorsAndWarningsReporter 7 | 8 | import scala.annotation.tailrec 9 | import scala.collection.mutable 10 | 11 | object MergeSort { 12 | type Coll[T] = IndexedSeq[T] 13 | 14 | def arrayMerge[T: Ordering](arr1: Coll[T], arr2: Coll[T]): Coll[T] = { 15 | val result = new mutable.ArraySeq[T](arr1.length + arr2.length) // just to allocate space 16 | 17 | def isLess(x: T, y: T) = implicitly[Ordering[T]].compare(x, y) < 0 18 | 19 | // Will now modify the `result` array in place. 20 | @tailrec 21 | def mergeRec(i1: Int, i2: Int, i: Int): Unit = { 22 | if (i1 == arr1.length && i2 == arr2.length) () 23 | else { 24 | val (x, newI1, newI2) = if (i1 < arr1.length && (i2 == arr2.length || isLess(arr1(i1), arr2(i2)))) 25 | (arr1(i1), i1 + 1, i2) else (arr2(i2), i1, i2 + 1) 26 | result(i) = x 27 | mergeRec(newI1, newI2, i + 1) 28 | } 29 | } 30 | 31 | mergeRec(0, 0, 0) 32 | result.toIndexedSeq 33 | } 34 | 35 | def performMergeSort[T: Ordering](array: Coll[T], threads: Int = 8): Coll[T] = { 36 | 37 | val finalResult = m[Coll[T]] 38 | val getFinalResult = b[Unit, Coll[T]] 39 | val reactionPool = FixedPool(threads) 40 | 41 | val pool2 = FixedPool(threads) 42 | 43 | site(pool2)( 44 | go { case finalResult(arr) + getFinalResult(_, r) => r(arr) } 45 | ) 46 | 47 | // The `mergesort()` molecule will start the chain reactions at one level lower. 48 | 49 | val mergesort = m[(Coll[T], M[Coll[T]])] 50 | 51 | site(reactionPool)( 52 | go { case mergesort((arr, resultToYield)) => 53 | if (arr.length <= 1) resultToYield(arr) 54 | else { 55 | val (part1, part2) = arr.splitAt(arr.length / 2) 56 | // The `sorted1()` and `sorted2()` molecules will carry the sorted results from the lower level. 57 | val sorted1 = m[Coll[T]] 58 | val sorted2 = m[Coll[T]] 59 | site(reactionPool)( 60 | go { case sorted1(x) + sorted2(y) => 61 | resultToYield(arrayMerge(x, y)) 62 | } 63 | ) 64 | // emit `mergesort` with the lower-level `sorted` result molecules 65 | mergesort((part1, sorted1)) + mergesort((part2, sorted2)) 66 | } 67 | } 68 | ) 69 | // Sort our array: emit `mergesort()` at top level. 70 | mergesort((array, finalResult)) 71 | 72 | val result = getFinalResult() 73 | reactionPool.shutdownNow() 74 | pool2.shutdownNow() 75 | result 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /benchmark/src/test/scala/io/chymyst/benchmark/ConcurrentCounterSpec.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.benchmark 2 | 3 | import org.scalatest.{FlatSpec, Matchers} 4 | import io.chymyst.jc._ 5 | import io.chymyst.test.Common._ 6 | import ammonite.ops._ 7 | import io.chymyst.test.LogSpec 8 | 9 | class ConcurrentCounterSpec extends LogSpec { 10 | 11 | val count = 50000 12 | 13 | def runBenchmark(count: Int, threads: Int, verbose: Boolean, writeFile: Boolean = false): Unit = { 14 | val c = m[Int] 15 | val g = b[Unit, Int] 16 | val i = m[Unit] 17 | val d = m[Unit] 18 | val f = b[Long, Long] 19 | 20 | val memoryLogger = new MemoryLogger 21 | val elapsed = withPool(FixedPool(threads).withReporter(if (verbose) new DebugAllReporter(memoryLogger) else ConsoleEmptyReporter)) { tp ⇒ 22 | 23 | site(tp)( 24 | go { case c(0) + f(tInit, r) ⇒ 25 | val x = System.nanoTime() 26 | r(x - tInit) 27 | }, 28 | go { case g(_, reply) + c(n) ⇒ c(n); reply(n) }, 29 | go { case c(n) + i(_) ⇒ c(n + 1) }, 30 | go { case c(n) + d(x) if n > 0 ⇒ c(n - 1) } 31 | ) 32 | c(count) 33 | val initialTime = System.nanoTime() 34 | (1 to count).foreach { _ => d() } 35 | f(initialTime) 36 | }.get 37 | println(s"benchmark with $count reactions on $threads threads, with${if(verbose) " " + memoryLogger.messages.size else " no"} logs took ${formatNanosToMs(elapsed)}") 38 | if (writeFile) write.over(pwd / 'logs / s"benchmark_$count-on-$threads-threads_${if(verbose) "" else "no-"}logging.txt", memoryLogger.messages.map(_ + "\n")) 39 | } 40 | 41 | def runPipelinedBenchmark(count: Int, threads: Int, pipelined: Boolean, verbose: Boolean, writeFile: Boolean = false): Unit = { 42 | val c = m[Int] 43 | val d = m[Int] 44 | val i = m[Unit] 45 | val done = m[Unit] 46 | val f = b[Long, Long] 47 | 48 | val memoryLogger = new MemoryLogger 49 | val elapsed = withPool(FixedPool(threads).withReporter(if (verbose) new DebugAllReporter(memoryLogger) else ConsoleEmptyReporter)) { tp ⇒ 50 | site(tp)( 51 | go { case done(_) + f(tInit, r) ⇒ 52 | val x = System.nanoTime() 53 | r(x - tInit) 54 | }, 55 | go { case c(n) + i(_) ⇒ c(n + 1) }, 56 | if (pipelined) go { case c(n) + d(x) ⇒ if (n > 1) c(n - 1) else done() } else go { case c(n) + d(x) if n + x > -1 ⇒ if (n > 1) c(n - 1) else done() } 57 | ) 58 | val initialTime = System.nanoTime() 59 | c(count) 60 | (1 to count).foreach(d) 61 | f(initialTime) 62 | }.get 63 | 64 | println(s"${if(pipelined) "" else "non-"}pipelined benchmark with $count reactions on $threads threads, with${if(verbose) " " + memoryLogger.messages.size else " no"} logs took ${formatNanosToMs(elapsed)}") 65 | if (writeFile) write.over(pwd / 'logs / s"${if(pipelined) "" else "non-"}pipelined benchmark_$count-on-$threads-threads_${if(verbose) "" else "no-"}logging.txt", memoryLogger.messages.map(_ + "\n")) 66 | } 67 | 68 | behavior of "concurrent counter" 69 | 70 | it should s"produce no-logging benchmark1 data on $count counter runs with 8 threads" in { 71 | runBenchmark(count, 8, verbose = false) 72 | } 73 | 74 | it should s"produce logging benchmark1 data on $count counter runs with 8 threads" in { 75 | runBenchmark(count, 8, verbose = true) 76 | } 77 | 78 | it should s"produce no-logging pipelined benchmark1 data on $count counter runs with 8 threads" in { 79 | runPipelinedBenchmark(count, 8, pipelined = true, verbose = false) 80 | } 81 | 82 | // This is extremely slow due to quadratic time while printing log messages: the pipelined queue needs to be traversed each time! 83 | // it should s"produce logging pipelined benchmark1 data on $count counter runs with 8 threads" in { 84 | // runPipelinedBenchmark(count, 8, pipelined = true, verbose = true) 85 | // } 86 | 87 | it should s"produce no-logging non-pipelined benchmark1 data on $count counter runs with 8 threads" in { 88 | runPipelinedBenchmark(count, 8, pipelined = false, verbose = false) 89 | } 90 | 91 | it should s"produce no-logging benchmark1 data on $count counter runs with 1 threads" in { 92 | runBenchmark(count, 1, verbose = false) 93 | } 94 | 95 | it should s"produce no-logging pipelined benchmark1 data on $count counter runs with 1 threads" in { 96 | runPipelinedBenchmark(count, 1, pipelined = true, verbose = false) 97 | } 98 | 99 | it should s"produce no-logging non-pipelined benchmark1 data on $count counter runs with 1 threads" in { 100 | runPipelinedBenchmark(count, 1, pipelined = false, verbose = false) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /benchmark/src/test/scala/io/chymyst/benchmark/JiansenFairnessSpec.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.benchmark 2 | 3 | import code.jiansen.scalajoin._ 4 | import io.chymyst.test.LogSpec // Use precompiled classes from Jiansen's Join.scala, which are in that package. 5 | 6 | class JiansenFairnessSpec extends LogSpec { 7 | 8 | // fairness over reactions: 9 | // We have n molecules A:JA[Unit], which can all interact with a single molecule C:JA[(Int,Array[Int])]. 10 | // We first emit all A's and then a single C. 11 | // Each molecule A_i will increment C's counter at index i upon reaction. 12 | // We repeat this for N iterations, then we read the array and check that its values are distributed more or less randomly. 13 | 14 | it should "fail to implement fairness across reactions in Jiansen's Join" in { 15 | 16 | val reactions = 4 17 | val N = 100 // with 1000 we get a stack overflow 18 | 19 | object j3 extends Join { 20 | object c extends AsyName[(Int, Array[Int])] 21 | object done extends AsyName[Array[Int]] 22 | object getC extends SynName[Unit, Array[Int]] 23 | object a0 extends AsyName[Unit] 24 | object a1 extends AsyName[Unit] 25 | object a2 extends AsyName[Unit] 26 | object a3 extends AsyName[Unit] 27 | 28 | join { 29 | case getC(_) and done(arr) => getC.reply(arr) 30 | case a0(_) and c((n, arr)) => if (n > 0) { arr(0) += 1; c((n-1,arr)); a0(()) } else done(arr) 31 | case a1(_) and c((n, arr)) => if (n > 0) { arr(1) += 1; c((n-1,arr)); a1(()) } else done(arr) 32 | case a2(_) and c((n, arr)) => if (n > 0) { arr(2) += 1; c((n-1,arr)); a2(()) } else done(arr) 33 | case a3(_) and c((n, arr)) => if (n > 0) { arr(3) += 1; c((n-1,arr)); a3(()) } else done(arr) 34 | } 35 | 36 | } 37 | j3.a0(()) 38 | j3.a1(()) 39 | j3.a2(()) 40 | j3.a3(()) 41 | j3.c((N, Array.fill[Int](reactions)(0))) 42 | 43 | val result = j3.getC(()) 44 | 45 | // println(result.mkString(", ")) 46 | 47 | result.min should be (0) 48 | result.max should be (N) 49 | 50 | } 51 | 52 | // fairness across molecules: 53 | // Emit n molecules A[Int] that can all interact with C[Int]. Each time they interact, their counter is incremented. 54 | // Then emit a single C molecule, which will react until its counter goes to 0. 55 | // At this point, gather all results from A[Int] into an array and return that array. 56 | 57 | it should "fail to implement fairness across molecules in Jiansen's Join" in { 58 | 59 | val counters = 10 60 | 61 | val cycles = 40 // again, stack overflow with 1000 counters 62 | 63 | object j4 extends Join { 64 | object c extends AsyName[Int] 65 | object done extends AsyName[List[Int]] 66 | object getC extends SynName[Unit, List[Int]] 67 | object gather extends AsyName[List[Int]] 68 | object a extends AsyName[Int] 69 | 70 | join { 71 | case getC(_) and done(arr) => getC.reply(arr) 72 | case c(n) and a(m) => if (n > 0) { c(n-1); a(m+1) } else { a(m); gather(List()) } 73 | case gather(arr) and a(i) => 74 | val newArr = i :: arr 75 | if (newArr.size < counters) gather(newArr) else done(newArr) 76 | } 77 | } 78 | 79 | (1 to counters).foreach(_ => j4.a(0)) 80 | Thread.sleep(200) 81 | j4.c(cycles) 82 | 83 | val result = j4.getC(()) 84 | 85 | // println(result.mkString(", ")) 86 | 87 | result.max should be (0) 88 | 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /benchmark/src/test/scala/io/chymyst/benchmark/LtqExecutor.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.benchmark 2 | 3 | import java.util 4 | import java.util.concurrent.locks.LockSupport 5 | import java.util.concurrent.{Callable, ExecutorService, Future, LinkedTransferQueue, TimeUnit} 6 | 7 | /* See http://fasterjava.blogspot.co.uk/2014/09/writing-non-blocking-executor.html */ 8 | 9 | final class LtqExecutor[T](val threadCount: Int) extends ExecutorService { 10 | val queue = new LinkedTransferQueue[Runnable]() 11 | val threads = Array.tabulate[Thread](threadCount){ _ => 12 | val thread = new Thread(new Worker) 13 | thread.start() 14 | thread 15 | } 16 | private var running: Boolean = true 17 | private var stopped: Boolean = false 18 | 19 | private val MAX_QUEUE_SIZE: Int = 1 << 18 20 | 21 | def execute(runnable: Runnable) { 22 | while (queue.size > MAX_QUEUE_SIZE) { 23 | { 24 | // cap the size 25 | LockSupport.parkNanos(1) 26 | } 27 | } 28 | /* while (!queue.offer(runnable)) { 29 | { 30 | LockSupport.parkNanos(1) 31 | /* try { 32 | Thread.sleep(1); 33 | } catch (InterruptedException ie) { 34 | throw new RuntimeException(ie); 35 | } */ 36 | } 37 | }*/ 38 | try queue.put(runnable) 39 | catch { 40 | case e: InterruptedException ⇒ throw new RuntimeException(e) 41 | } 42 | /* try { 43 | queue.put(runnable); 44 | } catch (InterruptedException ie) { 45 | throw new RuntimeException(ie); 46 | } */ 47 | } 48 | /* 49 | private class Worker extends Runnable { 50 | def run() { 51 | while (running) { 52 | { 53 | var runnable: Runnable = null 54 | while (runnable == null) { 55 | { 56 | if (Thread.interrupted) return 57 | runnable = queue.poll() 58 | if (runnable == null) 59 | LockSupport.parkNanos(1) 60 | } 61 | } 62 | /* try { 63 | runnable = queue.take(); 64 | } catch (InterruptedException ie) { 65 | // was interrupted - just go round the loop again, 66 | // if running = false will cause it to exit 67 | } */ try { 68 | if (runnable != null) { 69 | runnable.run() 70 | } 71 | } 72 | catch { 73 | case e: Exception => { 74 | System.out.println("failed because of: " + e.toString) 75 | e.printStackTrace() 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | */ 83 | def shutdown() { 84 | running = false 85 | var i: Int = 0 86 | while (i < threadCount) { 87 | { 88 | threads(i).interrupt() 89 | threads(i) = null 90 | } 91 | { 92 | i += 1; i - 1 93 | } 94 | } 95 | stopped = true 96 | } 97 | 98 | def isShutdown: Boolean = { 99 | running 100 | } 101 | 102 | def isTerminated: Boolean = { 103 | stopped 104 | } 105 | 106 | // *************************************************** 107 | def awaitTermination(timeout: Long, unit: Nothing): Boolean = { 108 | throw new UnsupportedOperationException("oops!") 109 | } 110 | 111 | def invokeAll[T](tasks: Nothing): Nothing = { 112 | throw new UnsupportedOperationException("oops!") 113 | } 114 | 115 | def invokeAll[T](tasks: Nothing, timeout: Long, unit: Nothing): Nothing = { 116 | throw new UnsupportedOperationException("oops!") 117 | } 118 | 119 | def invokeAny[T](tasks: Nothing): T = { 120 | throw new UnsupportedOperationException("oops!") 121 | } 122 | 123 | def invokeAny[T](tasks: Nothing, timeout: Long, unit: Nothing): T = { 124 | throw new UnsupportedOperationException("oops!") 125 | } 126 | 127 | def shutdownNow: Nothing = { 128 | throw new UnsupportedOperationException("oops!") 129 | } 130 | 131 | def submit[T](task: Nothing): Nothing = { 132 | throw new UnsupportedOperationException("oops!") 133 | } 134 | 135 | def submit(task: Runnable): Nothing = { 136 | throw new UnsupportedOperationException("oops!") 137 | } 138 | 139 | def submit[T](task: Runnable, result: T): Nothing = { 140 | throw new UnsupportedOperationException("oops!") 141 | } 142 | 143 | override def toString: String = { 144 | throw new UnsupportedOperationException("oops!") 145 | } 146 | 147 | override def awaitTermination(timeout: Long, unit: TimeUnit): Boolean = ??? 148 | 149 | override def invokeAll[T](tasks: util.Collection[_ <: Callable[T]]): util.List[Future[T]] = ??? 150 | 151 | override def invokeAll[T](tasks: util.Collection[_ <: Callable[T]], timeout: Long, unit: TimeUnit): util.List[Future[T]] = ??? 152 | 153 | override def invokeAny[T](tasks: util.Collection[_ <: Callable[T]]): T = ??? 154 | 155 | override def invokeAny[T](tasks: util.Collection[_ <: Callable[T]], timeout: Long, unit: TimeUnit): T = ??? 156 | 157 | override def submit[T](task: Callable[T]): Future[T] = ??? 158 | 159 | 160 | private class Worker extends Runnable { 161 | def run(): Unit = { 162 | while (running) { 163 | var runnable: Runnable = null 164 | try { 165 | runnable = queue.take() 166 | } catch { 167 | case e: InterruptedException ⇒ 168 | // was interrupted - just go round the loop again, 169 | // if running = false will cause it to exit 170 | } 171 | try { 172 | if (runnable != null) { 173 | runnable.run() 174 | } 175 | } catch { 176 | case e: Exception ⇒ 177 | System.out.println("failed because of: " + e.toString()) 178 | e.printStackTrace() 179 | } 180 | } 181 | } 182 | } 183 | 184 | 185 | } 186 | -------------------------------------------------------------------------------- /benchmark/src/test/scala/io/chymyst/benchmark/MergesortSpec.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.benchmark 2 | 3 | import io.chymyst.benchmark.Common._ 4 | import io.chymyst.benchmark.MergeSort._ 5 | import io.chymyst.test.LogSpec 6 | 7 | class MergesortSpec extends LogSpec { 8 | 9 | // auxiliary functions for merge-sort tests 10 | 11 | behavior of "merge sort" 12 | 13 | it should "merge arrays correctly" in { 14 | arrayMerge(IndexedSeq(1, 2, 5), IndexedSeq(3, 6)) shouldEqual IndexedSeq(1, 2, 3, 5, 6) 15 | } 16 | 17 | it should "sort an array using concurrent merge-sort correctly with one thread" in { 18 | 19 | val count = 10 20 | val threads = 1 21 | 22 | val arr = Array.fill[Int](count)(scala.util.Random.nextInt(count)) 23 | val expectedResult = arr.sorted 24 | 25 | performMergeSort(arr, threads) shouldEqual expectedResult 26 | } 27 | 28 | it should "sort an array using concurrent merge-sort correctly with many threads" in { 29 | 30 | val count = 10 31 | val threads = 8 32 | 33 | val arr = Array.fill[Int](count)(scala.util.Random.nextInt(count)) 34 | val expectedResult = arr.sorted 35 | 36 | performMergeSort(arr, threads) shouldEqual expectedResult 37 | } 38 | 39 | it should "perform concurrent merge-sort without deadlock" in { // This has been a race condition in a MessageDigest global object. 40 | val count = 5 41 | (1 to 2000).foreach { i ⇒ 42 | val arr = Array.fill[Int](count)(scala.util.Random.nextInt(count)) 43 | performMergeSort(arr, 4) 44 | } 45 | } 46 | 47 | it should "sort an array using concurrent merge-sort more quickly with many threads than with one thread" in { 48 | 49 | val count = 10000 50 | // 1000000 51 | val threads = 8 // typical thread utilization at 600% 52 | 53 | val arr = Array.fill[Int](count)(scala.util.Random.nextInt(count)) 54 | 55 | val result = timeWithPriming { 56 | performMergeSort(arr, threads) 57 | () 58 | } 59 | println(s"concurrent merge-sort test with count=$count and $threads threads took $result ms") 60 | 61 | val result1 = timeWithPriming { 62 | performMergeSort(arr, 1) 63 | () 64 | } 65 | println(s"concurrent merge-sort test with count=$count and 1 threads took $result1 ms") 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /benchmark/src/test/scala/io/chymyst/benchmark/MultithreadSpec.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.benchmark 2 | 3 | import io.chymyst.benchmark.Common._ 4 | import io.chymyst.jc._ 5 | import io.chymyst.test.LogSpec 6 | 7 | class MultithreadSpec extends LogSpec { 8 | 9 | it should "run time-consuming tasks on many threads faster than on one thread" in { 10 | 11 | def runWork(threads: Int) = { 12 | 13 | def performWork(): Unit = { 14 | // Simulate CPU load. 15 | Thread.sleep(1000) 16 | } 17 | 18 | val work = m[Unit] 19 | val finished = m[Unit] 20 | val counter = m[Int] 21 | val allFinished = b[Unit, Unit] 22 | val tp = FixedPool(threads) 23 | site(tp)( 24 | go { case work(_) => performWork(); finished() }, 25 | go { case counter(n) + finished(_) => counter(n-1) }, 26 | go { case allFinished(_, r) + counter(0) => r() } 27 | ) 28 | val total = 8 29 | (1 to total).foreach(_ => work()) 30 | counter(total) 31 | allFinished() 32 | tp.shutdownNow() 33 | } 34 | 35 | val result1 = timeWithPriming{runWork(1)} 36 | val result8 = timeWithPriming{runWork(8)} 37 | 38 | println(s"with 1 thread $result1 ms, with 8 threads $result8 ms") 39 | 40 | withClue("Running Thread.sleep() on 8 threads should be at least 7 times faster than on 1 thread") { 41 | (result1 / 7) should be > result8 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /benchmark/src/test/scala/io/chymyst/benchmark/RepeatedInputSpec.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.benchmark 2 | 3 | import io.chymyst.jc._ 4 | import io.chymyst.test.Common.{elapsedTimeMs, litmus} 5 | import io.chymyst.test.LogSpec 6 | 7 | class RepeatedInputSpec extends LogSpec { 8 | 9 | behavior of "reactions with repeated input" 10 | 11 | it should "handle cross-molecule guard with constant values" in { 12 | val k = 5 13 | val total = 1000 14 | val repetitions = 20 15 | 16 | val (_, elapsed) = elapsedTimeMs( 17 | (1 to repetitions).foreach { i => 18 | withPool(FixedPool(8)) { tp => 19 | val a = m[Option[Int]] 20 | val done = m[Int] 21 | val (all_done, f) = litmus[Boolean](tp) 22 | site(tp)( 23 | go { case a(Some(1)) + a(Some(2)) + a(Some(3)) + a(Some(4)) + a(Some(5)) + a(Some(x)) if x > k => done(1) }, 24 | go { case done(x) + done(y) => if (x + y < total) done(x + y) else all_done(true) } 25 | ) 26 | 27 | (1 to total).foreach(_ => a(Some(1)) + a(Some(2)) + a(Some(3)) + a(Some(4)) + a(Some(5)) + a(Some(k * 2))) 28 | f() 29 | }.get shouldEqual true 30 | } 31 | ) 32 | println(s"Repeated input 1: total = $total, repetitions = $repetitions, elapsed = $elapsed ms") 33 | } 34 | 35 | it should "handle cross-molecule guard with effectively constant values" in { 36 | val k = 5 37 | val total = 1000 38 | val repetitions = 20 39 | 40 | val (_, elapsed) = elapsedTimeMs( 41 | (1 to repetitions).foreach { i => 42 | withPool(FixedPool(8)) { tp => 43 | val a = m[Option[Int]] 44 | val done = m[Int] 45 | val (all_done, f) = litmus[Boolean](tp) 46 | site(tp)( 47 | go { 48 | case a(Some(x1)) + a(Some(x2)) + a(Some(x3)) + a(Some(x4)) + a(Some(x5)) + a(Some(x)) 49 | if x > k && x1 == 1 && x2 == 2 && x3 == 3 && x4 == 4 && x5 == 5 50 | => done(1) 51 | }, 52 | go { case done(x) + done(y) => if (x + y < total) done(x + y) else all_done(true) } 53 | ) 54 | 55 | (1 to total).foreach(_ => a(Some(1)) + a(Some(2)) + a(Some(3)) + a(Some(4)) + a(Some(5)) + a(Some(k * 2))) 56 | f() 57 | }.get shouldEqual true 58 | } 59 | ) 60 | println(s"Repeated input 2: total = $total, repetitions = $repetitions, elapsed = $elapsed ms") 61 | } 62 | 63 | it should "handle cross-molecule guard with simple effectively constant values" in { 64 | val k = 5 65 | val total = 1000 66 | val repetitions = 20 67 | 68 | val (_, elapsed) = elapsedTimeMs( 69 | (1 to repetitions).foreach { i => 70 | withPool(FixedPool(8)) { tp => 71 | val a = m[Int] 72 | val done = m[Int] 73 | val (all_done, f) = litmus[Boolean](tp) 74 | site(tp)( 75 | go { 76 | case a(x1) + a(x2) + a(x3) + a(x4) + a(x5) + a(x) 77 | if x > k && x1 == 1 && x2 == 2 && x3 == 3 && x4 == 4 && x5 == 5 78 | => done(1) 79 | }, 80 | go { case done(x) + done(y) => if (x + y < total) done(x + y) else all_done(true) } 81 | ) 82 | 83 | (1 to total).foreach(_ => a(1) + a(2) + a(3) + a(4) + a(5) + a(k * 2)) 84 | f() 85 | }.get shouldEqual true 86 | } 87 | ) 88 | println(s"Repeated input 3: total = $total, repetitions = $repetitions, elapsed = $elapsed ms") 89 | } 90 | 91 | it should "handle cross-molecule guard with simple constant values" in { 92 | val k = 5 93 | val total = 1000 94 | val repetitions = 20 95 | 96 | val (_, elapsed) = elapsedTimeMs( 97 | (1 to repetitions).foreach { i => 98 | withPool(FixedPool(8)) { tp => 99 | val a = m[Int] 100 | val done = m[Int] 101 | val (all_done, f) = litmus[Boolean](tp) 102 | site(tp)( 103 | go { 104 | case a(1) + a(2) + a(3) + a(4) + a(5) + a(x) 105 | if x > k 106 | => done(1) 107 | }, 108 | go { case done(x) + done(y) => if (x + y < total) done(x + y) else all_done(true) } 109 | ) 110 | 111 | (1 to total).foreach(_ => a(1) + a(2) + a(3) + a(4) + a(5) + a(k * 2)) 112 | f() 113 | }.get shouldEqual true 114 | } 115 | ) 116 | println(s"Repeated input 4: total = $total, repetitions = $repetitions, elapsed = $elapsed ms") 117 | } 118 | 119 | it should "handle cross-molecule guard with simple constant values and inert values" in { 120 | val k = 5 121 | val total = 200 122 | val repetitions = 20 123 | 124 | val (_, elapsed) = elapsedTimeMs( 125 | (1 to repetitions).foreach { i => 126 | withPool(FixedPool(8)) { tp => 127 | val a = m[Int] 128 | val done = m[Int] 129 | val (all_done, f) = litmus[Boolean](tp) 130 | site(tp)( 131 | go { 132 | case a(1) + a(2) + a(3) + a(4) + a(5) + a(x) 133 | if x > k 134 | => done(1) 135 | }, 136 | go { case done(x) + done(y) => if (x + y < total) done(x + y) else all_done(true) } 137 | ) 138 | 139 | (1 to total).foreach(_ => a(-100) + a(1) + a(2) + a(3) + a(4) + a(5) + a(k * 2)) 140 | f() 141 | }.get shouldEqual true 142 | } 143 | ) 144 | println(s"Repeated input 5: total = $total, repetitions = $repetitions, elapsed = $elapsed ms") 145 | } 146 | 147 | it should "handle cross-molecule guard with nonconstant values" in { 148 | val k = 4 149 | val total = 5 150 | val repetitions = 10 151 | 152 | // Use a simple deterministic generator so that timings are more consistent 153 | var seed = 654321L 154 | def getRandomNumber: Long = { 155 | seed = (seed * 67867979L + 982451653L) % 472882049L 156 | seed 157 | } 158 | 159 | def getHash(xs: Seq[Long]): Long = { 160 | xs.fold(0L)(_ * k + _) 161 | } 162 | 163 | getHash(Seq(1, 2, 3)) shouldEqual 3 + k * (2 + k * 1) 164 | 165 | val (_, elapsed) = elapsedTimeMs( 166 | (1 to repetitions).foreach { i => 167 | // println(s"iteration $i") 168 | withPool(FixedPool(8)) { tp => 169 | val a = m[Long] 170 | val done = m[Int] 171 | val (all_done, f) = litmus[Boolean](tp) 172 | site(tp)( 173 | go { case a(x1) + a(x2) + a(x3) + a(x4) + a(y) if getHash(Seq(x1, x2, x3, x4)) == y => done(1) }, 174 | go { case done(x) + done(y) => if (x + y < total) done(x + y) else all_done(true) } 175 | ) 176 | (1 to total).foreach { i => 177 | val data = (1 to k).map(i => getRandomNumber) 178 | val y = getHash(data) 179 | a(y) 180 | data.foreach(a) 181 | } 182 | f() 183 | }.get shouldEqual true 184 | } 185 | ) 186 | println(s"Repeated input 6: total = $total, repetitions = $repetitions, elapsed = $elapsed ms") 187 | } 188 | 189 | } 190 | -------------------------------------------------------------------------------- /benchmark/src/test/scala/io/chymyst/benchmark/SingleThreadSpec.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.benchmark 2 | 3 | import java.util.concurrent.atomic.AtomicInteger 4 | import java.util.concurrent.{LinkedBlockingQueue, ThreadPoolExecutor, TimeUnit} 5 | import java.util.{Timer, TimerTask} 6 | 7 | import io.chymyst.jc.{FixedPool, withPool} 8 | import io.chymyst.test.LogSpec 9 | import io.chymyst.test.Common._ 10 | 11 | import scala.concurrent.duration.Duration 12 | import scala.concurrent.{Await, Promise} 13 | 14 | class SingleThreadSpec extends LogSpec { 15 | 16 | def incrementRunnable: Runnable = { () ⇒ increment() } 17 | 18 | behavior of "thread executor with 2 threads" 19 | 20 | it should "schedule tasks" in { 21 | val queue = new LinkedBlockingQueue[Runnable] 22 | val secondsToRecycleThread = 1L 23 | val executor = new ThreadPoolExecutor(2, 2, secondsToRecycleThread, TimeUnit.SECONDS, queue) 24 | executor.allowCoreThreadTimeOut(true) 25 | 26 | counter.set(0) 27 | counter.get() shouldEqual 0 28 | val result = elapsedTimesNs(executor.execute(incrementRunnable), n) 29 | showStd("lbq execute(), 2 threads", result) 30 | val done = Promise[Unit]() 31 | executor.execute({ () ⇒ 32 | done.success(()) 33 | () 34 | }) 35 | Await.result(done.future, Duration.Inf) 36 | counter.get() shouldEqual n 37 | } 38 | 39 | behavior of "single-thread pool" 40 | 41 | val counter = new AtomicInteger() 42 | 43 | def increment(): Unit = { 44 | counter.incrementAndGet() 45 | () 46 | } 47 | 48 | val n = 1000000 49 | 50 | it should "schedule tasks" in { 51 | 52 | counter.set(0) 53 | withPool(FixedPool(1)) { tp ⇒ 54 | val result = elapsedTimesNs(tp.runReaction(s"task $n", incrementTask.run()), n) 55 | showStd("FixedPool(1).runReaction, 2 threads", result) 56 | val done = Promise[Unit]() 57 | tp.runReaction("done", doneTask(done).run()) 58 | Await.result(done.future, Duration.Inf) 59 | counter.get() shouldEqual n 60 | }.get 61 | } 62 | 63 | def incrementTask = new TimerTask { 64 | override def run(): Unit = increment() 65 | } 66 | 67 | def doneTask(done: Promise[Unit]) = new TimerTask { 68 | override def run(): Unit = { 69 | done.success(()) 70 | () 71 | } 72 | } 73 | 74 | behavior of "timer" 75 | 76 | it should "schedule tasks" in { 77 | counter.set(0) 78 | val timer = new Timer() 79 | counter.get() shouldEqual 0 80 | val result = elapsedTimesNs(timer.schedule(incrementTask, 0), n) 81 | showStd("java.util.timer.schedule(), 2 threads", result) 82 | 83 | val done = Promise[Unit]() 84 | timer.schedule(doneTask(done), 10) 85 | Await.result(done.future, Duration.Inf) 86 | counter.get() shouldEqual n 87 | } 88 | 89 | behavior of "thread executor" 90 | 91 | it should "schedule tasks" in { 92 | val queue = new LinkedBlockingQueue[Runnable] 93 | val secondsToRecycleThread = 1L 94 | val executor = new ThreadPoolExecutor(1, 1, secondsToRecycleThread, TimeUnit.SECONDS, queue) 95 | executor.allowCoreThreadTimeOut(true) 96 | 97 | counter.set(0) 98 | counter.get() shouldEqual 0 99 | val result = elapsedTimesNs(executor.execute(incrementRunnable), n) 100 | showStd("lbq execute(), 1 thread", result) 101 | val done = Promise[Unit]() 102 | executor.execute({ () ⇒ 103 | done.success(()) 104 | () 105 | }) 106 | Await.result(done.future, Duration.Inf) 107 | counter.get() shouldEqual n 108 | } 109 | 110 | behavior of "ltq executor" 111 | 112 | it should "schedule tasks with 2 threads" in { 113 | val executor = new LtqExecutor(2) 114 | counter.set(0) 115 | counter.get() shouldEqual 0 116 | val result = elapsedTimesNs(executor.execute(incrementRunnable), n) 117 | showStd("ltq execute(), 2 thread", result) 118 | val done = Promise[Unit]() 119 | executor.execute({ () ⇒ 120 | done.success(()) 121 | () 122 | }) 123 | Await.result(done.future, Duration.Inf) 124 | counter.get() shouldEqual n 125 | } 126 | 127 | it should "schedule tasks" in { 128 | val executor = new LtqExecutor(1) 129 | counter.set(0) 130 | counter.get() shouldEqual 0 131 | val result = elapsedTimesNs(executor.execute(incrementRunnable), n) 132 | showStd("ltq execute(), 1 thread", result) 133 | val done = Promise[Unit]() 134 | executor.execute({ () ⇒ 135 | done.success(()) 136 | () 137 | }) 138 | Await.result(done.future, Duration.Inf) 139 | counter.get() shouldEqual n 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /core/lib/javolution-core-java-6.0.0-javadoc.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chymyst/chymyst-core/8355442b8effdf2fe8bc9c66fe02cb73eb05e71d/core/lib/javolution-core-java-6.0.0-javadoc.jar -------------------------------------------------------------------------------- /core/lib/javolution-core-java-6.0.0-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chymyst/chymyst-core/8355442b8effdf2fe8bc9c66fe02cb73eb05e71d/core/lib/javolution-core-java-6.0.0-sources.jar -------------------------------------------------------------------------------- /core/lib/javolution-core-java-6.0.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chymyst/chymyst-core/8355442b8effdf2fe8bc9c66fe02cb73eb05e71d/core/lib/javolution-core-java-6.0.0.jar -------------------------------------------------------------------------------- /core/lib/javolution.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | * Javolution - Java(tm) Solution for Real-Time and Embedded Systems 3 | * Copyright (c) 2012, Javolution (http://javolution.org/) 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 20 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 21 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 22 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 23 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 25 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | */ -------------------------------------------------------------------------------- /core/lib/scalaxy-streams.LICENSE.txt: -------------------------------------------------------------------------------- 1 | SCALAXY LICENSE 2 | 3 | Copyright (c) 2012-2015 Olivier Chafik, unless otherwise specified. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | 3. Neither the name of Scalaxy nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 15 | -------------------------------------------------------------------------------- /core/lib/scalaxy-streams_2.12-0.4-SNAPSHOT-javadoc.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chymyst/chymyst-core/8355442b8effdf2fe8bc9c66fe02cb73eb05e71d/core/lib/scalaxy-streams_2.12-0.4-SNAPSHOT-javadoc.jar -------------------------------------------------------------------------------- /core/lib/scalaxy-streams_2.12-0.4-SNAPSHOT-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chymyst/chymyst-core/8355442b8effdf2fe8bc9c66fe02cb73eb05e71d/core/lib/scalaxy-streams_2.12-0.4-SNAPSHOT-sources.jar -------------------------------------------------------------------------------- /core/lib/scalaxy-streams_2.12-0.4-SNAPSHOT.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chymyst/chymyst-core/8355442b8effdf2fe8bc9c66fe02cb73eb05e71d/core/lib/scalaxy-streams_2.12-0.4-SNAPSHOT.jar -------------------------------------------------------------------------------- /core/src/main/scala/io/chymyst/jc/BlockingPool.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.jc 2 | 3 | import scala.language.experimental.macros 4 | 5 | /** This is similar to scala.concurrent.blocking and is used to annotate expressions that should lead to a possible increase of thread count. 6 | * Multiple nested calls to `BlockingIdle` are equivalent to one call. 7 | */ 8 | object BlockingIdle { 9 | def apply[T](expr: => T): T = apply(selfBlocking = false)(expr) 10 | 11 | private[jc] def apply[T](selfBlocking: Boolean)(expr: => T): T = 12 | Thread.currentThread() match { 13 | case t: ChymystThread => t.blockingCall(expr, selfBlocking) 14 | case _ => expr // BlockingIdle{...} has no effect if we are not running on a ChymystThread 15 | } 16 | } 17 | 18 | /** A cached pool that increases its thread count whenever a blocking molecule is emitted, and decreases afterwards. 19 | * The `BlockingIdle` function, similar to `scala.concurrent.blocking`, is used to annotate expressions that should lead to an increase of thread count, and to a decrease of thread count once the idle blocking call returns. 20 | * @param parallelism Initial number of threads. 21 | */ 22 | final class BlockingPool( 23 | name: String, 24 | override val parallelism: Int = cpuCores, 25 | priority: Int = Thread.NORM_PRIORITY, 26 | reporter: EventReporting = ConsoleErrorReporter 27 | ) extends Pool(name, priority, reporter) { 28 | 29 | // Looks like we will die hard at about 2021 threads... 30 | val poolSizeLimit: Int = math.min(2000, 1000 + 2 * parallelism) 31 | 32 | def currentPoolSize: Int = workerExecutor.getCorePoolSize 33 | 34 | private[jc] def startedBlockingCall(selfBlocking: Boolean) = synchronized { // Need a lock to modify the pool sizes. 35 | val newPoolSize = math.min(currentPoolSize + 1, poolSizeLimit) 36 | if (newPoolSize > currentPoolSize) { 37 | workerExecutor.setMaximumPoolSize(newPoolSize) 38 | workerExecutor.setCorePoolSize(newPoolSize) 39 | } else { 40 | reporter.warnTooManyThreads(toString, currentPoolSize) 41 | } 42 | } 43 | 44 | private[jc] def finishedBlockingCall(selfBlocking: Boolean) = synchronized { // Need a lock to modify the pool sizes. 45 | val newPoolSize = math.max(parallelism, currentPoolSize - 1) 46 | workerExecutor.setCorePoolSize(newPoolSize) // Must set them in this order, so that the core pool size is never larger than the maximum pool size. 47 | workerExecutor.setMaximumPoolSize(newPoolSize) 48 | } 49 | 50 | def withReporter(r: EventReporting): BlockingPool = new BlockingPool(name, parallelism, priority, r) 51 | } 52 | 53 | object BlockingPool { 54 | def apply(): BlockingPool = macro PoolMacros.newBlockingPoolImpl0 // IntelliJ cannot resolve the symbol PoolMacros, but compilation works. 55 | def apply(parallelism: Int): BlockingPool = macro PoolMacros.newBlockingPoolImpl1 // IntelliJ cannot resolve the symbol PoolMacros, but compilation works. 56 | } 57 | -------------------------------------------------------------------------------- /core/src/main/scala/io/chymyst/jc/ChymystThread.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.jc 2 | 3 | import io.chymyst.jc.Core.ReactionString 4 | 5 | /** Thread that knows which Chymyst reaction is running on it, and which pool it belongs to. 6 | * This is used for debugging and for implementing [[BlockingIdle]] functionality. 7 | * 8 | * @param runnable The initial task given to the thread. (Required by the [[Thread]] interface.) 9 | */ 10 | private[jc] final class ChymystThread(runnable: Runnable, val pool: Pool) extends Thread(pool.threadGroup, runnable, pool.nextThreadName) { 11 | private var inBlockingCall: Boolean = false 12 | 13 | private[jc] var reactionInfoString: ReactionString = Core.NO_REACTION_INFO_STRING 14 | 15 | def reactionInfo: ReactionString = reactionInfoString 16 | 17 | /** Given that the expression `expr` is "idle blocking", the thread pool will increase the parallelism. 18 | * This method always runs on `this` thread, so no need to synchronize the mutation of `var inBlockingCall`. 19 | * 20 | * @param expr Expression that will be idle blocking. 21 | * @tparam T Type of value of this expression. 22 | * @return The same result as the expression would return. 23 | */ 24 | private[jc] def blockingCall[T](expr: => T, selfBlocking: Boolean = false): T = if (inBlockingCall) expr else { 25 | inBlockingCall = true 26 | pool.startedBlockingCall(selfBlocking) 27 | val result = expr 28 | pool.finishedBlockingCall(selfBlocking) 29 | inBlockingCall = false 30 | result 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /core/src/main/scala/io/chymyst/jc/CrossMoleculeSorting.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.jc 2 | 3 | import scala.annotation.tailrec 4 | import scalaxy.streams.optimize 5 | import scalaxy.streams.strategy.aggressive 6 | 7 | /** Utility functions for various calculations related to cross-molecule guards and conditions. 8 | * Molecules are represented by their zero-based indices in the reaction input list. (These are not the site-wide molecule indices.) 9 | */ 10 | private[jc] object CrossMoleculeSorting { 11 | 12 | private[jc] type Coll[T] = Array[T] // This type can be easily changed to another ordered collection such as `IndexedSeq` if that proves to be better. 13 | // There are only a few usages of Array() and Array(x) below. 14 | 15 | @tailrec 16 | private def sortConnectedSets(connectedSets: Coll[Set[Int]], result: Coll[Set[Int]] = Array()): Coll[Set[Int]] = { 17 | connectedSets.lastOption match { 18 | case None ⇒ 19 | result 20 | case Some(largest) ⇒ 21 | val (connected, nonConnected) = connectedSets.partition(_.exists(largest.contains)) 22 | sortConnectedSets(nonConnected, optimize { result ++ connected.sortBy(g ⇒ (-(largest intersect g).size, g.min, g.max)) }) 23 | } 24 | } 25 | 26 | private[jc] def sortedConnectedSets(groupedSets: Coll[(Set[Int], Coll[Set[Int]])]): Coll[(Set[Int], Coll[Set[Int]])] = optimize { 27 | groupedSets 28 | .sortBy(_._1.size) 29 | .map { case (s, a) ⇒ 30 | (s, sortConnectedSets(a.sortBy { g ⇒ a.map(_ intersect g).map(_.size).sum })) 31 | } 32 | } 33 | 34 | @tailrec 35 | private[jc] def groupConnectedSets( 36 | allGroups: Coll[Set[Int]], 37 | result: Coll[(Set[Int], Coll[Set[Int]])] = Array() 38 | ): Coll[(Set[Int], Coll[Set[Int]])] = { 39 | val (currentSet, currentResult, remaining) = findFirstConnectedGroupSet(allGroups) 40 | if (currentSet.isEmpty) 41 | result 42 | else { 43 | val newResult = result :+ ((currentSet, currentResult)) 44 | if (remaining.isEmpty) 45 | newResult 46 | else 47 | groupConnectedSets(remaining, newResult) 48 | } 49 | } 50 | 51 | @tailrec 52 | private[jc] def findFirstConnectedGroupSet( 53 | allGroups: Coll[Set[Int]], 54 | currentSet: Set[Int] = Set(), 55 | result: Coll[Set[Int]] = Array() 56 | ): (Set[Int], Coll[Set[Int]], Coll[Set[Int]]) = { 57 | allGroups.headOption match { 58 | case None ⇒ 59 | (currentSet, result, allGroups) 60 | case Some(firstGroup) ⇒ 61 | // `allGroups` is non-empty 62 | val effectiveCurrentSet = if (currentSet.isEmpty) 63 | firstGroup 64 | else 65 | currentSet 66 | val (intersecting, nonIntersecting) = allGroups.partition(_.exists(effectiveCurrentSet.contains)) 67 | if (intersecting.isEmpty) 68 | (currentSet, result, nonIntersecting) 69 | else 70 | findFirstConnectedGroupSet(nonIntersecting, effectiveCurrentSet ++ intersecting.flatten, result ++ intersecting) 71 | } 72 | } 73 | 74 | // Insert ConstrainGuard(i) commands whenever the already chosen molecules contain the set constrained by the guard. 75 | private def insertConstrainGuardCommands(crossGroups: Coll[Set[Int]], program: Coll[SearchDSL]): Coll[SearchDSL] = { // can't use scalaxy optimize here 76 | val groupIndexed = crossGroups.zipWithIndex 77 | // Accumulator is a 4-tuple: (all guards seen so far, current guards, all molecules seen so far, current command) 78 | program.scanLeft((Set[Int](), Set[Int](), Set[Int](), CloseGroup: SearchDSL)) { 79 | // If the current command is ChooseMol() then we need to update the sets of molecules and guards. 80 | case ((oldAllGuards, _, oldAvailableMolecules, _), c@ChooseMol(i)) ⇒ 81 | val newAvailableMolecules = oldAvailableMolecules + i 82 | val newAllGuards = groupIndexed.filter(_._1.subsetOf(newAvailableMolecules)).map(_._2).toSet 83 | val currentGuards = newAllGuards diff oldAllGuards 84 | (newAllGuards, currentGuards, newAvailableMolecules, c) 85 | // If the current command is any other `c`, we need to simply insert it into the tuple, with empty set of guards. 86 | case ((a, _, s, _), c) ⇒ 87 | (a, Set[Int](), s, c) 88 | } 89 | .drop(1) // scanLeft produces an initial element, which we don't need. 90 | // Retain only the sets of currentGuards; transform into ConstrainGuard(i). 91 | .flatMap { case (_, g, _, c) ⇒ Array(c) ++ g.toArray.map(ConstrainGuard) } 92 | } 93 | 94 | private[jc] def getDSLProgram( 95 | crossGroups: Coll[Set[Int]], 96 | repeatedMols: Coll[Set[Int]], 97 | moleculeWeights: Coll[(Int, Boolean)] 98 | ): Coll[SearchDSL] = { 99 | val allGroups: Coll[Set[Int]] = crossGroups ++ repeatedMols 100 | 101 | val sortedCrossGroups: Coll[Coll[Coll[Int]]] = optimize { 102 | sortedConnectedSets(groupConnectedSets(allGroups)) 103 | .map(_._2.map(_.toArray.sortBy(moleculeWeights.apply))) // each cross-guard set needs to be sorted by molecule weight 104 | } 105 | 106 | val programWithoutConstrainGuardCommands: Coll[SearchDSL] = { // can't use scalaxy optimize on this! 107 | sortedCrossGroups.flatMap( 108 | // We reverse the order so that smaller groups go first, which may help optimize the guard positions. 109 | _.reverse 110 | .flatMap(_.map(ChooseMol)).distinct :+ CloseGroup 111 | ) 112 | } 113 | 114 | insertConstrainGuardCommands(crossGroups, programWithoutConstrainGuardCommands) 115 | } 116 | 117 | } 118 | 119 | /** Commands used while searching for molecule values among groups of input molecules that are constrained by cross-molecule guards or conditionals. 120 | * A sequence of these commands (the "SearchDSL program") is computed for each reaction by the reaction site. 121 | * SearchDSL programs are interpreted at run time by [[ReactionSite.findInputMolecules]]. 122 | */ 123 | private[jc] sealed trait SearchDSL 124 | 125 | /** Choose a molecule value among the molecules available at the reaction site. 126 | * 127 | * @param i Index of the molecule within the reaction input list (the "input index"). 128 | */ 129 | private[jc] final case class ChooseMol(i: Int) extends SearchDSL 130 | 131 | /** Impose a guard condition on the molecule values found so far. 132 | * 133 | * @param i Index of the guard in `crossGuards` array 134 | */ 135 | private[jc] final case class ConstrainGuard(i: Int) extends SearchDSL 136 | 137 | /** A group of cross-dependent molecules has been closed. 138 | * At this point, we can select one set of molecule values and stop searching for other molecule values within this group. 139 | * If no molecule values are found that satisfy all constraints for this group, the search for the molecule values can be abandoned (the current reaction cannot run). 140 | */ 141 | private[jc] case object CloseGroup extends SearchDSL 142 | -------------------------------------------------------------------------------- /core/src/main/scala/io/chymyst/jc/FixedPool.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.jc 2 | 3 | import java.util.concurrent.atomic.AtomicInteger 4 | 5 | import scala.language.experimental.macros 6 | 7 | /** The fixed-thread implementation of a `Chymyst` thread pool. 8 | * 9 | * @param parallelism Total number of threads. 10 | */ 11 | final class FixedPool( 12 | name: String, 13 | override val parallelism: Int = cpuCores, 14 | priority: Int = Thread.NORM_PRIORITY, 15 | reporter: EventReporting = ConsoleErrorReporter 16 | ) extends Pool(name, priority, reporter) { 17 | private[jc] val blockingCalls = new AtomicInteger(0) 18 | 19 | private def deadlockCheck(): Unit = 20 | if (blockingCalls.get >= workerExecutor.getMaximumPoolSize) 21 | reporter.reportDeadlock(toString, workerExecutor.getMaximumPoolSize, blockingCalls.get, Core.getReactionInfo) 22 | 23 | override private[chymyst] def runReaction(name: String, closure: ⇒ Unit): Unit = { 24 | deadlockCheck() 25 | super.runReaction(name, closure) 26 | } 27 | 28 | private[jc] def startedBlockingCall(selfBlocking: Boolean) = if (selfBlocking) { 29 | blockingCalls.getAndIncrement() 30 | deadlockCheck() 31 | } 32 | 33 | private[jc] def finishedBlockingCall(selfBlocking: Boolean) = if (selfBlocking) { 34 | blockingCalls.getAndDecrement() 35 | deadlockCheck() 36 | } 37 | 38 | def withReporter(r: EventReporting): FixedPool = new FixedPool(name, parallelism, priority, r) 39 | } 40 | 41 | object FixedPool { 42 | def apply(): FixedPool = macro PoolMacros.newFixedPoolImpl0 // IntelliJ cannot resolve the symbol PoolMacros, but compilation works. 43 | def apply(parallelism: Int): FixedPool = macro PoolMacros.newFixedPoolImpl1 44 | } 45 | -------------------------------------------------------------------------------- /core/src/main/scala/io/chymyst/jc/Pool.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.jc 2 | 3 | 4 | import java.util.concurrent.atomic.AtomicInteger 5 | import java.util.concurrent._ 6 | 7 | import scala.concurrent.ExecutionContext 8 | import Core._ 9 | 10 | /** A pool of execution threads, or another way of running tasks (could use actors or whatever else). 11 | * Tasks submitted for execution can have Chymyst-specific info (useful for debugging) when scheduled using `runReaction`. 12 | * The pool can be shut down, in which case all further tasks will be refused. 13 | * 14 | * @param name Name assigned to the thread pool, used for debugging purposes. 15 | * @param priority Thread group priority for this pool, such as [[Thread.NORM_PRIORITY]]. 16 | * @param _reporter An instance of [[EmptyReporter]] that will be used to gather performance metrics for each reaction site using this thread pool. 17 | * By default, a [[ConsoleErrorReporter]] is assigned, which only logs run-time errors to the console. 18 | */ 19 | abstract class Pool(val name: String, val priority: Int, private[this] var _reporter: EventReporting) extends AutoCloseable { 20 | override val toString: String = s"${this.getClass.getSimpleName}:$name" 21 | 22 | private[jc] def startedBlockingCall(selfBlocking: Boolean): Unit 23 | 24 | private[jc] def finishedBlockingCall(selfBlocking: Boolean): Unit 25 | 26 | def parallelism: Int 27 | 28 | /** Create a new task queue. This is used to create the worker task queue and also to create the scheduler task queue. 29 | * 30 | * Possible implementations include [[LinkedBlockingQueue]] and [[LinkedTransferQueue]]. 31 | * @return A new instance of a [[BlockingQueue]]. 32 | */ 33 | def createQueue: BlockingQueue[Runnable] = new LinkedTransferQueue[Runnable]() 34 | 35 | /** Run a reaction closure on the thread pool. 36 | * The reaction closure will be created by [[ReactionSite.reactionClosure]]. 37 | * 38 | * @param closure A reaction closure to run. 39 | */ 40 | private[chymyst] def runReaction(name: String, closure: ⇒ Unit): Unit = workerExecutor.execute(new Runnable { 41 | override def toString: String = name 42 | 43 | override def run(): Unit = closure 44 | }) 45 | 46 | def isInactive: Boolean = workerExecutor.isShutdown || workerExecutor.isTerminated 47 | 48 | override def close(): Unit = shutdownNow() 49 | 50 | def recycleThreadTimeMs: Long = 1000L 51 | 52 | def shutdownWaitTimeMs: Long = 200L 53 | 54 | private val threadGroupName = toString + ",thread_group" 55 | 56 | private val threadNameBase = toString + ",worker_thread:" 57 | 58 | val threadGroup: ThreadGroup = { 59 | val tg = new ThreadGroup(threadGroupName) 60 | tg.setMaxPriority(priority) 61 | tg 62 | } 63 | 64 | private val schedulerQueue: BlockingQueue[Runnable] = createQueue 65 | 66 | private val schedulerThreadFactory: ThreadFactory = { (r: Runnable) ⇒ new Thread(threadGroup, r, toString + ",scheduler_thread") } 67 | 68 | private[jc] val schedulerExecutor: ThreadPoolExecutor = { 69 | val executor = new ThreadPoolExecutor(1, 1, recycleThreadTimeMs, TimeUnit.MILLISECONDS, schedulerQueue, schedulerThreadFactory) 70 | executor.allowCoreThreadTimeOut(true) 71 | executor 72 | } 73 | 74 | private[jc] def runScheduler(runnable: Runnable): Unit = schedulerExecutor.execute(runnable) 75 | 76 | private val workerQueue: BlockingQueue[Runnable] = createQueue 77 | 78 | private val workerThreadFactory: ThreadFactory = { (r: Runnable) ⇒ new ChymystThread(r, Pool.this) } 79 | 80 | protected val workerExecutor: ThreadPoolExecutor = { 81 | val executor = new ThreadPoolExecutor(parallelism, parallelism, recycleThreadTimeMs, TimeUnit.MILLISECONDS, workerQueue, workerThreadFactory) 82 | executor.allowCoreThreadTimeOut(true) 83 | executor 84 | } 85 | 86 | val executionContext: ExecutionContext = ExecutionContext.fromExecutor(workerExecutor) 87 | 88 | private val currentThreadId: AtomicInteger = new AtomicInteger(0) 89 | 90 | private[jc] def nextThreadName: String = threadNameBase + currentThreadId.getAndIncrement().toString 91 | 92 | /** Shut down the thread pool when required. This will interrupt all threads and clear the worker and the scheduler queues. 93 | * 94 | * Usually this is not needed in application code. Call this method in a situation when work has to be stopped immediately. 95 | */ 96 | def shutdownNow(): Unit = new Thread { 97 | try { 98 | schedulerExecutor.getQueue.clear() 99 | schedulerExecutor.shutdown() 100 | schedulerExecutor.awaitTermination(shutdownWaitTimeMs, TimeUnit.MILLISECONDS) 101 | workerExecutor.getQueue.clear() 102 | workerExecutor.shutdown() 103 | workerExecutor.awaitTermination(shutdownWaitTimeMs, TimeUnit.MILLISECONDS) 104 | } finally { 105 | schedulerExecutor.shutdown() 106 | workerExecutor.shutdownNow() 107 | workerExecutor.awaitTermination(shutdownWaitTimeMs, TimeUnit.MILLISECONDS) 108 | workerExecutor.shutdownNow() 109 | () 110 | } 111 | }.start() 112 | 113 | @inline def reporter: EventReporting = _reporter 114 | 115 | def reporter_=(r: EventReporting): Unit = { 116 | val reporterChanged = _reporter.asInstanceOf[EventReporting] =!= r 117 | if (reporterChanged) { 118 | reporter.reporterUnassigned(this, r) 119 | _reporter = r 120 | r.reporterAssigned(this) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /core/src/main/scala/io/chymyst/jc/package.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst 2 | 3 | import scala.language.experimental.macros 4 | import scala.util.{Failure, Success, Try} 5 | 6 | /** This object contains code that should be visible to users of `Chymyst Core`. 7 | * It also serves as an interface to macros. 8 | * This allows users to import just one package and use all functionality of `Chymyst Core`. 9 | */ 10 | package object jc { 11 | 12 | /** A convenience method that fetches the number of CPU cores of the current machine. 13 | * 14 | * @return The number of available CPU cores. 15 | */ 16 | def cpuCores: Int = Runtime.getRuntime.availableProcessors() 17 | 18 | /** Create a reaction site with one or more reactions. 19 | * All input and output molecules in reactions used in this site should have been 20 | * already defined, and input molecules should not be already bound to another site. 21 | * 22 | * @param reactions One or more reactions of type [[Reaction]] 23 | * @param reactionPool Thread pool for running new reactions. 24 | * @return List of warning messages. 25 | */ 26 | def site(reactionPool: Pool)(reactions: Reaction*): WarningsAndErrors = { 27 | 28 | // Create a reaction site object holding the given local chemistry. 29 | // The constructor of ReactionSite will perform static analysis of all given reactions. 30 | val reactionSite = new ReactionSite(reactions, reactionPool) 31 | 32 | reactionSite.checkWarningsAndErrors() 33 | } 34 | 35 | /** `site()` call with a default reaction pool. */ 36 | def site(reactions: Reaction*): WarningsAndErrors = site(defaultPool)(reactions: _*) 37 | 38 | /** 39 | * This is the main method for defining reactions. 40 | * Examples: {{{ go { a(_) => ... } }}} 41 | * {{{ go { a (_) => ...}.withRetry onThreads threadPool }}} 42 | * 43 | * The macro also obtains statically checkable information about input and output molecules in the reaction. 44 | * 45 | * @param reactionBody The body of the reaction. This must be a partial function with pattern-matching on molecules. 46 | * @return A [[Reaction]] value, containing the reaction body as well as static information about input and output molecules. 47 | */ 48 | def go(reactionBody: Core.ReactionBody): Reaction = macro BlackboxMacros.buildReactionImpl // IntelliJ cannot resolve symbol `BlackboxMacros`, but compilation works. 49 | 50 | /** 51 | * Convenience syntax: users can write `a(x) + b(y)` to emit several molecules at once. 52 | * However, the molecules are still emitted one by one in the present implementation. 53 | * So, `a(x) + b(y) + c(z)` is equivalent to `a(x); b(y); c(z)`. 54 | * 55 | * @param x the first emitted molecule 56 | * @return An auxiliary class with a `+` operation. 57 | */ 58 | // Making this `extend AnyVal` crashes JVM in tests! 59 | implicit final class EmitMultiple(x: Unit) { 60 | def +(n: Unit): Unit = () // parameter n is not used 61 | } 62 | 63 | implicit final class SiteWithPool(val pool: Pool) extends AnyVal { 64 | def apply(reactions: Reaction*): WarningsAndErrors = site(pool)(reactions: _*) 65 | } 66 | 67 | /** Declare a new non-blocking molecule emitter. 68 | * The name of the molecule will be automatically assigned (via macro) to the name of the enclosing variable. 69 | * 70 | * @tparam T Type of the value carried by the molecule. 71 | * @return A new instance of class [[io.chymyst.jc.M]]`[T]`. 72 | */ 73 | def m[T]: M[T] = macro MoleculeMacros.mImpl[T] // IntelliJ cannot resolve symbol `MoleculeMacros`, but compilation works. 74 | 75 | /** Declare a new blocking molecule emitter. 76 | * The name of the molecule will be automatically assigned (via macro) to the name of the enclosing variable. 77 | * 78 | * @tparam T Type of the value carried by the molecule. 79 | * @tparam R Type of the reply value. 80 | * @return A new instance of class [[io.chymyst.jc.B]]`[T,R]`. 81 | */ 82 | def b[T, R]: B[T, R] = macro MoleculeMacros.bImpl[T, R] // IntelliJ cannot resolve symbol `MoleculeMacros`, but compilation works. 83 | 84 | /** This pool is used for sites that do not specify a thread pool. */ 85 | lazy val defaultPool = new BlockingPool("defaultPool") 86 | 87 | /** A helper method to run a closure that uses a thread pool, safely closing the pool after use. 88 | * 89 | * @param pool A thread pool value, evaluated lazily - typically `BlockingPool(...)`. 90 | * @param doWork A closure, typically containing a `site(pool)(...)` call. 91 | * @tparam T Type of the value returned by the closure. 92 | * @return The value returned by the closure, wrapped in a `Try`. 93 | */ 94 | def withPool[T, P <: Pool](pool: => P)(doWork: P => T): Try[T] = cleanup(pool)(_.shutdownNow())(doWork) 95 | 96 | /** Run a closure with a resource that is allocated and safely cleaned up after use. 97 | * Resource will be cleaned up even if the closure throws an exception. 98 | * 99 | * @param resource A value of type `T` that needs to be created for use by `doWork`. 100 | * @param cleanup A closure that will perform the necessary cleanup on the resource. 101 | * @param doWork A closure that will perform useful work, using the resource. 102 | * @tparam T Type of the resource value. 103 | * @tparam R Type of the result of `doWork`. 104 | * @return The value returned by `doWork`, wrapped in a `Try`. 105 | */ 106 | def cleanup[T, R](resource: => T)(cleanup: T => Unit)(doWork: T => R): Try[R] = { 107 | try { 108 | Success(doWork(resource)) 109 | } catch { 110 | case e: Exception => Failure(e) 111 | } 112 | finally { 113 | try { 114 | if (Option(resource).isDefined) { 115 | cleanup(resource) 116 | } 117 | } catch { 118 | case e: Exception => e.printStackTrace() 119 | } 120 | } 121 | } 122 | 123 | /** We need to have a single implicit instance of [[TypeMustBeUnit]]`[Unit]`. */ 124 | implicit val UnitArgImplicit: TypeMustBeUnit[Unit] = UnitTypeMustBeUnit 125 | 126 | /** Check whether the current thread is a Chymyst thread. 127 | * 128 | * @return `true` if the current thread belongs to a Chymyst reaction pool, `false` otherwise. 129 | */ 130 | def isChymystThread: Boolean = Thread.currentThread() match { 131 | case t: ChymystThread ⇒ true 132 | case _ ⇒ false 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /core/src/main/scala/io/chymyst/util/Budu.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.util 2 | 3 | import io.chymyst.jc.Core.AnyOpsEquals 4 | import io.chymyst.util.Budu._ 5 | 6 | import scala.annotation.tailrec 7 | import scala.concurrent.duration.Duration 8 | import scala.concurrent.{Future, Promise} 9 | 10 | // There are only three states: 2 is empty, no time-out yet. 3 is empty, have time-out. 1 not empty, no time-out. 11 | 12 | final class Budu[X](useFuture: Boolean) { 13 | private val resultPromise: Promise[X] = 14 | if (useFuture) 15 | Promise[X]() 16 | else null.asInstanceOf[Promise[X]] 17 | 18 | @volatile private var result: X = _ 19 | // @volatile var haveNoReply: Boolean = true 20 | // @volatile var notTimedOutYet: Boolean = true 21 | @volatile private var state: Int = EmptyNoTimeout 22 | 23 | @inline def isEmpty: Boolean = haveNoReply 24 | 25 | @inline def isTimedOut: Boolean = !notTimedOutYet 26 | 27 | @inline private def haveNoReply: Boolean = state > 1 28 | 29 | @inline private def notTimedOutYet: Boolean = state < 3 30 | 31 | /** Wait until the thread is notified and we get a reply value. 32 | * Do not wait any longer than until the given target time. 33 | * This function needs to be called inside a `synchronized` block. 34 | * 35 | * @param targetTime The absolute time (in milliseconds) until which we need to wait. 36 | * @param newDuration The first duration of waiting, needs to be precomputed and supplied as argument. 37 | */ 38 | @tailrec 39 | private def waitUntil(targetTime: Long, newDuration: Long): Unit = { 40 | if (newDuration > 0) { 41 | wait(newDuration) 42 | if (haveNoReply) { 43 | waitUntil(targetTime, targetTime - System.currentTimeMillis()) 44 | } 45 | } 46 | } 47 | 48 | def await(duration: Duration): Option[X] = 49 | if (state === EmptyNoTimeout) { 50 | val newDuration = duration.toMillis 51 | val targetTime = newDuration + System.currentTimeMillis() 52 | synchronized { 53 | if (haveNoReply) 54 | waitUntil(targetTime, newDuration) // This is an additional check that we have no reply. It's hard to trigger a race condition when `haveNoReply` would be false here. So, coverage cannot be 100%. 55 | // At this point, we have been notified. 56 | // If we are here, it means that we are holding a `synchronized` monitor, and thus the notifying thread is not holding it any more. 57 | // Therefore, the notifying thread has either finished its work and supplied us with a reply value, or it did not yet start replying. 58 | // Checking `haveNoReply` at this point will reveal which of these two possibilities is the case. 59 | if (haveNoReply) { 60 | state = EmptyAfterTimeout 61 | None 62 | } else 63 | Some(result) 64 | } // End of `synchronized` 65 | } else 66 | Option(result) 67 | 68 | 69 | def await: X = { 70 | if (state === EmptyNoTimeout) { 71 | synchronized { 72 | while (haveNoReply) { 73 | wait() 74 | } 75 | } // End of `synchronized` 76 | } 77 | result 78 | } 79 | 80 | def getFuture: Future[X] = if (useFuture) 81 | resultPromise.future 82 | else throw new Exception("getFuture() is disabled, initialize as Budu(useFuture = true) to enable") 83 | 84 | def is(x: X): Boolean = 85 | if (state === EmptyNoTimeout) { 86 | synchronized { 87 | if (notTimedOutYet) { 88 | result = x 89 | state = NonEmptyNoTimeout 90 | // If we are here, we are holding the `synchronized` monitor, which means that the waiting thread is suspended. 91 | notify() 92 | // At this point, we are still holding the `synchronized` monitor, and the waiting thread is still suspended. 93 | // Therefore, it is safe to read or modify the state. 94 | if (useFuture) 95 | resultPromise.success(x) 96 | } 97 | notTimedOutYet 98 | } // End of `synchronized` 99 | // It is only here, after we release the `synchronized` monitor, that the waiting thread is woken up and resumes computation within its `synchronized` block after `wait()`. 100 | } else notTimedOutYet 101 | } 102 | 103 | object Budu { 104 | private final val NonEmptyNoTimeout: Int = 1 105 | private final val EmptyNoTimeout: Int = 2 106 | private final val EmptyAfterTimeout: Int = 3 107 | 108 | def apply[X](useFuture: Boolean): Budu[X] = new Budu[X](useFuture) 109 | 110 | def apply[X]: Budu[X] = new Budu[X](useFuture = false) 111 | } 112 | -------------------------------------------------------------------------------- /core/src/main/scala/io/chymyst/util/ConjunctiveNormalForm.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.util 2 | 3 | /** Helper functions that perform computations with Boolean formulas while keeping them in the conjunctive normal form. 4 | * 5 | * A Boolean formula in CNF is represented by a list of lists of an arbitrary type `T`. 6 | * For instance, the Boolean formula (a || b) && (c || d || e) is represented as 7 | * {{{ List( List(a, b), List(c, d, e) ) }}} 8 | * 9 | * These helper methods will compute disjunction, conjunction, and negation of Boolean formulas in CNF, outputting the results also in CNF. 10 | * Simplifications are performed only in so far as to remove exact duplicate terms or clauses such as `a || a` or `(a || b) && (a || b)`. 11 | * 12 | * The type `T` represents primitive Boolean terms that cannot be further simplified or factored to CNF. 13 | * These terms could be represented by expression trees or in another way; the CNF computations do not depend on the representation of terms. 14 | * 15 | * Note that negation such as `! a` is considered to be a primitive term. 16 | * Negation of a conjunction or disjunction, such as `! (a || b)`, can be simplified to CNF. 17 | */ 18 | object ConjunctiveNormalForm { 19 | 20 | type CNF[T] = List[List[T]] 21 | 22 | /** Compute `a || b` where `a` is a single Boolean term and `b` is a Boolean formula in CNF. 23 | * 24 | * @param a Primitive Boolean term that cannot be simplified; does not contain disjunctions or conjunctions. 25 | * @param b A Boolean formula in CNF. 26 | * @tparam T Type of primitive Boolean terms. 27 | * @return The resulting Boolean formula in CNF. 28 | */ 29 | def disjunctionOneTerm[T](a: T, b: CNF[T]): CNF[T] = b.map(y => (a :: y).distinct).distinct 30 | 31 | /** Compute `a || b` where `a` is a single "clause", i.e. a disjunction of primitive Boolean terms, and `b` is a Boolean formula in CNF. 32 | * 33 | * @param a A list of primitive Boolean terms. This list represents a single disjunction clause, e.g. `List(a, b, c)` represents `a || b || c`. 34 | * @param b A Boolean formula in CNF. 35 | * @tparam T Type of primitive Boolean terms. 36 | * @return The resulting Boolean formula in CNF. 37 | */ 38 | def disjunctionOneClause[T](a: List[T], b: CNF[T]): CNF[T] = b.map(y => (a ++ y).distinct).distinct 39 | 40 | /** Compute `a || b` where `a` and `b` are Boolean formulas in CNF. 41 | * 42 | * @param a A Boolean formula in CNF. 43 | * @param b A Boolean formula in CNF. 44 | * @tparam T Type of primitive Boolean terms. 45 | * @return The resulting Boolean formula in CNF. 46 | */ 47 | def disjunction[T](a: CNF[T], b: CNF[T]): CNF[T] = a.flatMap(x => disjunctionOneClause(x, b)).distinct 48 | 49 | /** Compute `a && b` where `a` and `b` are Boolean formulas in CNF. 50 | * 51 | * @param a A Boolean formula in CNF. 52 | * @param b A Boolean formula in CNF. 53 | * @tparam T Type of primitive Boolean terms. 54 | * @return The resulting Boolean formula in CNF. 55 | */ 56 | def conjunction[T](a: CNF[T], b: CNF[T]): CNF[T] = (a ++ b).distinct 57 | 58 | /** Compute `! a` where `a` is a Boolean formula in CNF. 59 | * 60 | * @param a A Boolean formula in CNF. 61 | * @param negateOneTerm A function that describes the transformation of a primitive term under negation. 62 | * For instance, `negateOneTerm(a)` should return `! a` in the term's appropriate representation. 63 | * @tparam T Type of primitive Boolean terms. 64 | * @return The resulting Boolean formula in CNF. 65 | */ 66 | def negation[T](negateOneTerm: T => T)(a: CNF[T]): CNF[T] = a match { 67 | case x :: xs => 68 | val nxs = negation(negateOneTerm)(xs) 69 | x.flatMap(t => disjunctionOneTerm(negateOneTerm(t), nxs)) 70 | case Nil => List(List()) // negation of true is false 71 | } 72 | 73 | /** Represents the constant `true` value in CNF. */ 74 | def trueConstant[T]: CNF[T] = List() 75 | 76 | /** Represents the constant `false` value in CNF. */ 77 | def falseConstant[T]: CNF[T] = List(List()) 78 | 79 | /** Injects a single primitive term into a CNF. */ 80 | def oneTerm[T](a: T): CNF[T] = List(List(a)) 81 | } -------------------------------------------------------------------------------- /core/src/main/scala/io/chymyst/util/LabeledTypes.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.util 2 | 3 | import scala.language.higherKinds 4 | 5 | trait LabeledType[X] { 6 | type T 7 | 8 | /** Convert a collection of raw type `X` into a collection of labeled type. 9 | * This does not iterate over the collection and does not allocate memory. 10 | * 11 | * @param xs A collection of raw type. 12 | * @tparam F The collection's type constructor. 13 | * @return Collection of labeled type. 14 | */ 15 | def maptype[F[_]](xs: F[X]): F[T] 16 | 17 | /** Convert a collection of labeled type into a collection of raw type `X`. 18 | * This does not iterate over the collection and does not allocate memory. 19 | * 20 | * @param ts A collection of labeled type. 21 | * @tparam F The collection's type constructor. 22 | * @return Collection of raw type. 23 | */ 24 | def unmaptype[F[_]](ts: F[T]): F[X] 25 | 26 | /** Convert a value of raw type `X` into a labeled type. 27 | * No memory allocation is performed during this conversion. 28 | * 29 | * @param x A value of raw type `X`. 30 | * @return A value of labeled type. 31 | */ 32 | def apply(x: X): T 33 | 34 | /** Convert a value of labeled type into a value of the raw type `X`. 35 | * No memory allocation is performed during this conversion. 36 | * 37 | * @param t A value of labeled type. 38 | * @return A value of corresponding raw type. 39 | */ 40 | def get(t: T): X 41 | } 42 | 43 | trait LabeledSubtype[X] { 44 | type T <: X 45 | 46 | /** Convert a value of raw type `X` into a labeled type. 47 | * This does not iterate over the collection and does not allocate memory. 48 | * 49 | * @param x A value of raw type `X`. 50 | * @return A value of labeled type. 51 | */ 52 | def apply(x: X): T 53 | 54 | /** Convert a collection of raw type `X` into a collection of labeled type. 55 | * 56 | * @param xs A collection of raw type. 57 | * @tparam F The collection's type constructor. 58 | * @return Collection of labeled type. 59 | */ 60 | def maptype[F[_]](xs: F[X]): F[T] 61 | } 62 | 63 | /** Light-weight type aliases. 64 | * Unlike type classes, these type aliases never perform any memory allocation at run time. 65 | */ 66 | object LabeledTypes { 67 | 68 | /** Define a type alias for an existing type `X`. 69 | * Example usage: 70 | * {{{ 71 | * val UserName = Newtype[String] 72 | * type UserName = UserName.T // optional, for convenience 73 | * 74 | * val x = UserName("user 1") // create a UserName 75 | * val length = UserName.get(x).length // need conversion to recover the underlying String 76 | * }}} 77 | * The new type `UserName.T` will not be the same as `X` and will not be a subtype of `X`. 78 | * 79 | * @tparam X An existing type. 80 | * @return A constructor for a new type alias. 81 | */ 82 | def Newtype[X]: LabeledType[X] = new LabeledType[X] { 83 | type T = X 84 | 85 | def maptype[F[_]](fs: F[X]): F[T] = fs 86 | 87 | def unmaptype[F[_]](fs: F[T]): F[X] = fs 88 | 89 | def apply(x: X): T = x 90 | 91 | def get(lbl: T): X = lbl 92 | } 93 | 94 | /** Define a subtype alias for an existing type `X`. 95 | * Example usage: 96 | * {{{ 97 | * val UserName = Subtype[String] 98 | * type UserName = UserName.T // optional, for convenience 99 | * 100 | * val x = UserName("user 1") // create a UserName 101 | * val length = x.length // recover and use the underlying String without conversion 102 | * }}} 103 | * The new type `UserName.T` will not be the same as `String` but will be a subtype of `String`. 104 | * Due to subtyping, conversion from `UserName` back to `String` is immediate and requires no code. 105 | * Also, conversion of `List[UserName]` back to `List[String]` is immediate. 106 | * 107 | * @tparam X An existing type. 108 | * @return A constructor for a new type alias. 109 | */ 110 | def Subtype[X]: LabeledSubtype[X] = new LabeledSubtype[X] { 111 | type T = X 112 | 113 | def maptype[F[_]](fs: F[X]): F[T] = fs 114 | 115 | def apply(x: X): T = x 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /core/src/test/resources/fork-join-test/fork-join-test-1.txt: -------------------------------------------------------------------------------- 1 | This file should have length 140. 2 | The three files fork-join-test-1.txt, fork-join-test-2.txt, fork-join-test-3.txt should have equal length. -------------------------------------------------------------------------------- /core/src/test/resources/fork-join-test/fork-join-test-2.txt: -------------------------------------------------------------------------------- 1 | This file should have length 140. 2 | The three files fork-join-test-1.txt, fork-join-test-2.txt, fork-join-test-3.txt should have equal length. -------------------------------------------------------------------------------- /core/src/test/resources/fork-join-test/fork-join-test-3.txt: -------------------------------------------------------------------------------- 1 | This file should have length 140. 2 | The three files fork-join-test-1.txt, fork-join-test-2.txt, fork-join-test-3.txt should have equal length. -------------------------------------------------------------------------------- /core/src/test/resources/fork-join-test/non-empty-dir/fork-join-test-1.txt: -------------------------------------------------------------------------------- 1 | This file should have length 140. 2 | The three files fork-join-test-1.txt, fork-join-test-2.txt, fork-join-test-3.txt should have equal length. -------------------------------------------------------------------------------- /core/src/test/scala/io/chymyst/jc/GuardsErrorsUtest.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.jc 2 | 3 | import utest._ 4 | 5 | object GuardsErrorsUtest extends TestSuite { 6 | val tests = this { 7 | "recognize an identically false guard condition" - { 8 | val a = m[Int] 9 | val n = 10 10 | assert(a.isInstanceOf[M[Int]], n == 10) 11 | * - { 12 | compileError( 13 | "val result = go { case a(x) if false => }" 14 | ).check( 15 | """ 16 | | "val result = go { case a(x) if false => }" 17 | | ^ 18 | |""".stripMargin, "Reaction must not have an identically false guard condition") 19 | } 20 | * - { 21 | compileError( 22 | "val result = go { case a(x) if false ^ false => }" 23 | ).check( 24 | """ 25 | | "val result = go { case a(x) if false ^ false => }" 26 | | ^ 27 | |""".stripMargin, "Reaction must not have an identically false guard condition") 28 | } 29 | * - { 30 | compileError( 31 | "val result = go { case a(x) if !(false ^ !false) => }" 32 | ).check( 33 | """ 34 | | "val result = go { case a(x) if !(false ^ !false) => }" 35 | | ^ 36 | |""".stripMargin, "Reaction must not have an identically false guard condition") 37 | } 38 | * - { 39 | compileError( 40 | "val result = go { case a(x) if false || (true && false) || !true && n > 0 => }" 41 | ).check( 42 | """ 43 | | "val result = go { case a(x) if false || (true && false) || !true && n > 0 => }" 44 | | ^ 45 | |""".stripMargin, "Reaction must not have an identically false guard condition") 46 | } 47 | * - { 48 | compileError( 49 | "val result = go { case a(x) if false ^ (true && false) || !true && n > 0 => }" 50 | ).check( 51 | """ 52 | | "val result = go { case a(x) if false ^ (true && false) || !true && n > 0 => }" 53 | | ^ 54 | |""".stripMargin, "Reaction must not have an identically false guard condition") 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /core/src/test/scala/io/chymyst/jc/MacroErrorSpec.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.jc 2 | 3 | import io.chymyst.test.LogSpec 4 | import org.scalatest.Matchers 5 | 6 | // Note: Compilation of this test suite will generate warnings such as "crushing into 2-tuple". This is expected and cannot be avoided. 7 | 8 | class MacroErrorSpec extends LogSpec with Matchers { 9 | 10 | it should "support concise syntax for Unit-typed molecules" in { 11 | val a = new M[Unit]("a") 12 | val f = new B[Unit, Unit]("f") 13 | val h = b[Unit, Boolean] 14 | val g = b[Unit, Unit] 15 | // This should compile without any argument adaptation warnings: 16 | go { case a(_) + f(_, r) + g(_, s) + h(_, q) => a() + f(); s(); val status = r(); q(status) } 17 | } 18 | 19 | it should "compile a reaction within scalatest scope" in { 20 | val x = m[Int] 21 | site(go { case x(_) => }) shouldEqual WarningsAndErrors(List(), List(), "Site{x → ...}") 22 | } 23 | 24 | it should "compile a reaction with scoped pattern variables" in { 25 | val a = m[(Int, Int)] 26 | site(go { case a(y@(_, q@_)) => }) shouldEqual WarningsAndErrors(List(), List(), "Site{a → ...}") 27 | 28 | val c = m[(Int, (Int, Int))] 29 | c.name shouldEqual "c" 30 | // TODO: make this compile in actual code, not just inside scalatest macro. This is github issue #109 31 | "val r1 = go { case c(y@(_, z@(_, _))) => }" should compile // But this actually fails to compile when used in the code! 32 | 33 | val d = m[(Int, Option[Int])] 34 | "val r2 = go { case d((x, z@Some(_))) => }" should compile // But this actually fails to compile when used in the code! 35 | 36 | site( 37 | go { case d((x, z)) if z.nonEmpty => } // ignore warning about "non-variable type argument Int" 38 | ) shouldEqual WarningsAndErrors(List(), List(), "Site{d → ...}") 39 | } 40 | 41 | behavior of "output environments" 42 | 43 | it should "refuse emitting blocking molecules" in { 44 | val c = m[Unit] 45 | val f = b[Unit, Unit] 46 | val g: Any => Any = x => x 47 | 48 | assert(c.name == "c") 49 | assert(f.name == "f") 50 | assert(g(()) == (())) 51 | 52 | // For some reason, utest cannot get the compiler error message for this case. 53 | // So we have to move this test back to scalatest, even though here we can't check the error message. 54 | "val r = go { case c(_) => (0 until 10).foreach{_ => g(f); () } }" shouldNot compile 55 | } 56 | 57 | it should "recognize emitter under finally{}" in { 58 | val a = m[Unit] 59 | val c = m[Unit] 60 | val reaction = go { case a(_) ⇒ 61 | try 62 | throw new Exception("") 63 | catch { 64 | case _: Exception ⇒ 65 | } finally c() 66 | } 67 | reaction.info.outputs.length shouldEqual 1 68 | reaction.info.outputs(0).environments.toList should matchPattern { case List() ⇒ } 69 | } 70 | 71 | behavior of "compile-time errors due to chemistry" 72 | 73 | it should "inspect a pattern with a compound constant" in { 74 | val a = m[(Int, Int)] 75 | val c = m[Unit] 76 | val reaction = go { case a((1, _)) + c(_) => a((1, 1)) } 77 | reaction.info.inputs.head.flag should matchPattern { case OtherInputPattern(_, List(), false) => } 78 | (reaction.info.inputs.head.flag match { 79 | case OtherInputPattern(matcher, List(), false) => 80 | matcher.isDefinedAt((1, 1)) shouldEqual true 81 | matcher.isDefinedAt((1, 2)) shouldEqual true 82 | matcher.isDefinedAt((0, 1)) shouldEqual false 83 | true 84 | case _ => false 85 | }) shouldEqual true 86 | } 87 | 88 | it should "inspect a pattern with a crushed tuple" in { 89 | 90 | val bb = m[(Int, Option[Int])] 91 | 92 | val result = go { // ignore warning about "non-variable type argument Int" 93 | 94 | // This generates a compiler warning "class M expects 2 patterns to hold (Int, Option[Int]) but crushing into 2-tuple to fit single pattern (SI-6675)". 95 | // However, this "crushing" is precisely what this test focuses on, and we cannot tell scalac to ignore this warning. 96 | case bb(_) + bb(z) if (z match // ignore warning about "class M expects 2 patterns to hold" 97 | { 98 | case (1, Some(x)) if x > 0 => true; 99 | case _ => false 100 | }) => 101 | } 102 | 103 | (result.info.inputs.toList match { 104 | case List( 105 | InputMoleculeInfo(`bb`, 0, WildcardInput, _, Symbol("(Int, Option[Int])")), 106 | InputMoleculeInfo(`bb`, 1, SimpleVarInput('z, Some(cond)), _, _) 107 | ) => 108 | cond.isDefinedAt((1, Some(2))) shouldEqual true 109 | cond.isDefinedAt((1, None)) shouldEqual false 110 | cond.isDefinedAt((1, Some(0))) shouldEqual false 111 | cond.isDefinedAt((0, Some(2))) shouldEqual false 112 | true 113 | case _ => false 114 | }) shouldEqual true 115 | 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /core/src/test/scala/io/chymyst/jc/PoolSpec.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.jc 2 | 3 | import io.chymyst.test.{Common, LogSpec} 4 | import org.scalactic.source.Position 5 | import org.scalatest.concurrent.Waiters.{PatienceConfig, Waiter} 6 | import org.scalatest.time.{Millis, Span} 7 | 8 | import scala.concurrent.ExecutionContext 9 | 10 | class PoolSpec extends LogSpec { 11 | 12 | val patienceConfig = PatienceConfig(timeout = Span(500, Millis)) 13 | 14 | behavior of "BlockingPool" 15 | 16 | it should "refuse to increase the thread pool beyond its limit" in { 17 | val n = 1065 18 | 19 | val memoryLogger = new MemoryLogger 20 | val tp = BlockingPool(2).withReporter(new ErrorsAndWarningsReporter(memoryLogger)) 21 | 22 | tp.poolSizeLimit should be < n 23 | tp.currentPoolSize shouldEqual 2 24 | 25 | (1 to tp.poolSizeLimit + 2).foreach (_ => tp.startedBlockingCall(false)) 26 | 27 | tp.currentPoolSize shouldEqual tp.poolSizeLimit 28 | 29 | Common.globalLogHas(memoryLogger, "dangerous", "Warning: In BlockingPool:tp: It is dangerous to further increase the pool size, which is now 1004") 30 | tp.shutdownNow() 31 | } 32 | 33 | it should "report as warning that it refused to increase the thread pool beyond its limit" in { 34 | val n = 1065 35 | 36 | val memoryLogger = new MemoryLogger 37 | val tp = BlockingPool(2).withReporter(new ErrorReporter(memoryLogger)) 38 | 39 | tp.poolSizeLimit should be < n 40 | tp.currentPoolSize shouldEqual 2 41 | 42 | (1 to tp.poolSizeLimit + 2).foreach (_ => tp.startedBlockingCall(false)) 43 | 44 | tp.currentPoolSize shouldEqual tp.poolSizeLimit 45 | 46 | memoryLogger.messages.size shouldEqual 0 47 | tp.shutdownNow() 48 | } 49 | 50 | behavior of "fixed thread pool" 51 | 52 | it should "run a task on a separate thread" in { 53 | val waiter = new Waiter 54 | 55 | val tp = FixedPool(2) 56 | 57 | tp.executionContext.isInstanceOf[ExecutionContext] shouldEqual true 58 | 59 | tp.runReaction("", { 60 | waiter.dismiss() 61 | 62 | try { 63 | Thread.sleep(10000000) // this should not time out 64 | } catch { 65 | case _: InterruptedException => () 66 | } 67 | }) 68 | 69 | waiter.await()(patienceConfig, implicitly[Position]) 70 | 71 | tp.shutdownNow() 72 | } 73 | 74 | it should "interrupt a thread when shutting down" in { 75 | val waiter = new Waiter 76 | 77 | val tp = FixedPool(2) 78 | 79 | tp.runReaction("", { 80 | try { 81 | Thread.sleep(10000000) // this should not time out 82 | 83 | } catch { 84 | case _: InterruptedException => waiter.dismiss() 85 | case other: Exception => 86 | other.printStackTrace() 87 | waiter { 88 | false shouldEqual true; 89 | () 90 | } 91 | } 92 | }) 93 | Thread.sleep(20) 94 | 95 | tp.shutdownNow() 96 | 97 | waiter.await()(patienceConfig, implicitly[Position]) 98 | } 99 | 100 | behavior of "Chymyst thread" 101 | 102 | it should "return empty info by default" in { 103 | val tp = BlockingPool() 104 | val thread = new ChymystThread(() ⇒ (), tp) 105 | thread.reactionInfo shouldEqual Core.NO_REACTION_INFO_STRING 106 | 107 | thread.getName shouldEqual "BlockingPool:tp,worker_thread:0" 108 | } 109 | 110 | behavior of "fixed pool" 111 | 112 | it should "initialize with default settings" in { 113 | val tp = FixedPool() 114 | tp.name shouldEqual "tp" 115 | tp.toString shouldEqual "FixedPool:tp" 116 | tp.parallelism shouldEqual cpuCores 117 | tp.executionContext.isInstanceOf[ExecutionContext] shouldEqual true 118 | tp.close() 119 | } 120 | 121 | it should "initialize with specified settings" in { 122 | val tp = FixedPool(313) 123 | tp.name shouldEqual "tp" 124 | tp.toString shouldEqual "FixedPool:tp" 125 | tp.parallelism shouldEqual 313 126 | tp.executionContext.isInstanceOf[ExecutionContext] shouldEqual true 127 | tp.close() 128 | } 129 | 130 | behavior of "blocking pool" 131 | 132 | it should "initialize with default CPU core parallelism" in { 133 | val tp = BlockingPool() 134 | tp.name shouldEqual "tp" 135 | tp.toString shouldEqual "BlockingPool:tp" 136 | tp.currentPoolSize shouldEqual cpuCores 137 | tp.poolSizeLimit should be > 1000 138 | tp.executionContext.isInstanceOf[ExecutionContext] shouldEqual true 139 | tp.close() 140 | } 141 | 142 | it should "initialize with specified parallelism" in { 143 | val tp = BlockingPool(313) 144 | tp.name shouldEqual "tp" 145 | tp.toString shouldEqual "BlockingPool:tp" 146 | tp.currentPoolSize shouldEqual 313 147 | tp.poolSizeLimit should be > 1000 148 | tp.executionContext.isInstanceOf[ExecutionContext] shouldEqual true 149 | tp.close() 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /core/src/test/scala/io/chymyst/jc/Sha1Props.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.jc 2 | 3 | 4 | import Core._ 5 | 6 | import org.scalacheck.Properties 7 | import org.scalacheck.Prop.forAll 8 | 9 | object Sha1Props extends Properties("Sha1") { 10 | 11 | private val md = getMessageDigest 12 | 13 | property("noHashCollisionLongs") = forAll { (a: Long, b: Long) => 14 | a == b || getSha1(a.toString, md) != getSha1(b.toString, md) 15 | } 16 | } -------------------------------------------------------------------------------- /core/src/test/scala/io/chymyst/test/Common.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.test 2 | 3 | import java.io.File 4 | 5 | import io.chymyst.jc._ 6 | import org.sameersingh.scalaplot.jfreegraph.JFGraphPlotter 7 | import org.scalatest.Assertion 8 | import org.scalatest.Matchers._ 9 | 10 | object Common { 11 | 12 | def globalLogHas(reporter: MemoryLogger, part: String, message: String): Assertion = { 13 | reporter.messages.find(_.contains(part)) match { 14 | case Some(str) ⇒ str should endWith(message) 15 | case None ⇒ 16 | reporter.messages.foreach(println) shouldEqual "Test failed, see log messages above" // this fails and alerts the user 17 | } 18 | } 19 | 20 | // Note: log messages have a timestamp prepended to them, so we use `endsWith` when matching a log message. 21 | def logShouldHave(reporter: MemoryLogger, message: String): Assertion = { 22 | val status = reporter.messages.exists(_ endsWith message) 23 | if (!status) reporter.messages.foreach(println) shouldEqual "Test failed, see log messages above" // this fails and alerts the user 24 | else status shouldEqual true 25 | } 26 | 27 | def repeat[A](n: Int)(x: => A): Unit = (1 to n).foreach(_ => x) 28 | 29 | def repeat[A](n: Int, f: Int => A): Unit = (1 to n).foreach(f) 30 | 31 | def litmus[T](tp: Pool): (M[T], B[Unit, T]) = { 32 | val signal = m[T] 33 | val fetch = b[Unit, T] 34 | site(tp)( 35 | go { case signal(x) + fetch(_, r) ⇒ r(x) } 36 | ) 37 | (signal, fetch) 38 | } 39 | 40 | def checkExpectedPipelined(expectedMap: Map[MolEmitter, Boolean]): String = { 41 | val transformed = expectedMap.toList.map { case (t, r) => (t, t.isPipelined, r) } 42 | // Print detailed message. 43 | val difference = transformed.filterNot { case (_, x, y) => x == y }.map { case (m, actual, expected) => s"$m.isPipelined is $actual instead of $expected" } 44 | if (difference.nonEmpty) s"Test fails: ${difference.mkString("; ")}" else "" 45 | } 46 | 47 | def elapsedTimeMs[T](x: ⇒ T): (T, Long) = { 48 | val initTime = System.currentTimeMillis() 49 | val result = x 50 | val y = System.currentTimeMillis() 51 | val elapsedTime = y - initTime 52 | (result, elapsedTime) 53 | } 54 | 55 | def elapsedTimeNs[T](x: ⇒ T): (T, Long) = { 56 | val initTime = System.nanoTime() 57 | val result = x 58 | val y = System.nanoTime() 59 | val elapsedTime = y - initTime 60 | (result, elapsedTime) 61 | } 62 | 63 | def elapsedTimesNs[T](x: ⇒ Any, total: Int): Seq[Double] = { 64 | (1 to total).map { _ ⇒ 65 | val initTime = System.nanoTime() 66 | x 67 | val y = System.nanoTime() 68 | (y - initTime).toDouble 69 | } 70 | } 71 | 72 | def meanAndStdev(d: Seq[Double]): (Double, Double) = { 73 | val size = safeSize(d.size) 74 | val mean = d.sum / size 75 | val std = math.sqrt(d.map(x => x - mean).map(x => x * x).sum / size) 76 | (mean, std) 77 | } 78 | 79 | def formatNanosToMs(x: Double): String = f"${x / 1000000.0}%1.3f ms" 80 | 81 | def formatNanosToMicros(x: Double): String = f"${x / 1000.0}%1.3f µs" 82 | 83 | def formatNanosToMicrosWithMeanStd(mean: Double, std: Double): String = s"${formatNanosToMicros(mean)} ± ${formatNanosToMicros(std)}" 84 | 85 | val safeSize: Int => Double = x => if (x == 0) 1.0f else x.toDouble 86 | 87 | private def det(a00: Double, a01: Double, a10: Double, a11: Double): Double = a00 * a11 - a01 * a10 88 | 89 | private def regressLSQ(xs: Seq[Double], ys: Seq[Double], funcX: Double ⇒ Double, funcY: Double ⇒ Double): (Double, Double, Double) = { 90 | val n = xs.length 91 | val sumX = xs.map(funcX).sum 92 | val sumXX = xs.map(funcX).map(x ⇒ x * x).sum 93 | val sumY = ys.map(funcY).sum 94 | val sumXY = xs.zip(ys).map { case (x, y) ⇒ funcX(x) * funcY(y) }.sum 95 | val detS = det(n.toDouble, sumX, sumX, sumXX) 96 | val a0 = det(sumY, sumX, sumXY, sumXX) / detS 97 | val a1 = det(n.toDouble, sumY, sumX, sumXY) / detS 98 | val eps = math.sqrt(xs.zip(ys).map { case (x, y) ⇒ math.pow(a0 + a1 * funcX(x) - funcY(y), 2) }.sum) / n 99 | (a0, a1, eps) 100 | } 101 | 102 | def showRegression(message: String, resultsRaw: Seq[Double]): Unit = { 103 | // Perform regression to determine the effect of JVM warm-up. 104 | // Assume that the warm-up works as a0 + a1*x^(-c). Try linear regression with different values of c. 105 | val total = resultsRaw.length 106 | val take = (total * 0.02).toInt // omit the first few % of data due to extreme variability before JVM warm-up 107 | val results = resultsRaw.drop(take) 108 | val dataX = results.indices.map(_.toDouble) 109 | val dataY = results // smoothing pass with a min window 110 | .zipAll(results.drop(1), Double.PositiveInfinity, Double.PositiveInfinity) 111 | .zipAll(results.drop(2), (Double.PositiveInfinity, Double.PositiveInfinity), Double.PositiveInfinity) 112 | .map { case ((x, y), z) ⇒ math.min(x, math.min(y, z)) } 113 | val (shift, (a0, a1, a0stdev)) = (0 to 100).map { i ⇒ 114 | val shift = 0.1 + 2.5 * i 115 | (shift, regressLSQ(dataX, dataY, x ⇒ math.pow(x + shift, -1.0), identity)) 116 | }.minBy(_._2._3) 117 | val funcX = (x: Double) ⇒ math.pow(x + shift, -1.0) 118 | val earlyValue = dataY.take(take).sum / take 119 | val lateValue = dataY.takeRight(take).sum / take 120 | val speedup = f"${earlyValue / lateValue}%1.2f" 121 | println(s"Regression (total=$total) for $message: constant = ${formatNanosToMicros(a0)} ± ${formatNanosToMicros(a0stdev)}, gain = ${formatNanosToMicros(a1)}*iteration, max. speedup = $speedup, shift = $shift") 122 | 123 | import org.sameersingh.scalaplot.Implicits._ 124 | 125 | val dataXplotting = dataX.drop(take) 126 | val dataTheoryY = dataXplotting.map(i ⇒ a0 + a1 * funcX(i)) 127 | val chart = xyChart(dataXplotting → ((dataTheoryY, dataY.drop(take)))) 128 | val plotter = new JFGraphPlotter(chart) 129 | val plotdir = "logs/" 130 | new File(plotdir).mkdir() 131 | val plotfile = "benchmark." + message.replaceAll(" ", "_") 132 | plotter.pdf(plotdir, plotfile) 133 | println(s"Plot file produced in $plotdir$plotfile.pdf") 134 | } 135 | 136 | def showStd(message: String, results: Seq[Double], factor: Double = 20.0): Unit = { 137 | val total = results.length 138 | val take = (total / factor).toInt 139 | val (mean, std) = meanAndStdev(results.sortBy(-_).takeRight(take)) 140 | val headPortion = results.take(take) 141 | println(s"$message: best result overall: ${formatNanosToMicros(results.min)}; best portion: ${formatNanosToMicrosWithMeanStd(mean, std)}; first portion ($take) ranges from ${formatNanosToMicros(headPortion.max)} to ${formatNanosToMicros(headPortion.min)}") 142 | 143 | } 144 | 145 | def showFullStatistics(message: String, results: Seq[Double], factor: Double = 20.0): Unit = { 146 | showStd(message, results, factor) 147 | showRegression(message, results) 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /core/src/test/scala/io/chymyst/test/DiningPhilosophersNSpec.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.test 2 | 3 | import io.chymyst.jc._ 4 | 5 | class DiningPhilosophersNSpec extends LogSpec { 6 | 7 | behavior of "n dining philosophers" 8 | 9 | val cycles = 50 10 | 11 | val philosophers = 6 12 | 13 | it should s"run $cycles cycles for $philosophers philosophers without deadlock" in { 14 | diningPhilosophersN(cycles) 15 | } 16 | 17 | def randomWait(message: String): Unit = { 18 | Thread.sleep(math.floor(scala.util.Random.nextDouble * 20.0 + 2.0).toLong) 19 | } 20 | 21 | def eat(philosopher: Int): Unit = { 22 | randomWait(s"philosopher $philosopher is eating") 23 | } 24 | 25 | def think(philosopher: Int): Unit = { 26 | randomWait(s"philosopher $philosopher is thinking") 27 | } 28 | 29 | def diningPhilosophersN(cycles: Int): Unit = { 30 | val tp = FixedPool(8) 31 | 32 | val hungryPhilosophers = Seq.tabulate(philosophers)(i ⇒ new M[Int](s"hungry $i")) 33 | val thinkingPhilosophers = Seq.tabulate(philosophers)(i ⇒ new M[Int](s"thinking $i")) 34 | val forks = Seq.tabulate(philosophers)(i ⇒ new M[Unit](s"right fork of $i")) 35 | 36 | val done = m[Unit] 37 | val check = b[Unit, Unit] 38 | 39 | val reactions: Seq[Reaction] = Seq.tabulate(philosophers) { i ⇒ 40 | val thinking = thinkingPhilosophers(i) 41 | val hungry = hungryPhilosophers(i) 42 | val leftFork = forks((i + 1) % philosophers) 43 | val rightFork = forks(i) 44 | Seq( 45 | go { case thinking(n) ⇒ think(i); hungry(n - 1) }, 46 | go { case hungry(n) + leftFork(()) + rightFork(()) ⇒ 47 | eat(i) + thinking(n) + leftFork() + rightFork() 48 | if (n == 0) done() 49 | } 50 | ) 51 | }.flatten 52 | 53 | site(tp)( 54 | go { case done(()) + check((), reply) => reply() } 55 | ) 56 | 57 | site(tp)(reactions: _*) 58 | 59 | Seq.tabulate(philosophers) { i ⇒ 60 | thinkingPhilosophers(i)(cycles) 61 | forks(i)() 62 | } 63 | 64 | check() shouldEqual (()) 65 | 66 | tp.shutdownNow() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /core/src/test/scala/io/chymyst/test/DiningPhilosophersSpec.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.test 2 | 3 | import io.chymyst.jc._ 4 | 5 | class DiningPhilosophersSpec extends LogSpec { 6 | 7 | def randomWait(message: String): Unit = { 8 | Thread.sleep(math.floor(scala.util.Random.nextDouble*20.0 + 2.0).toLong) 9 | } 10 | 11 | def eat(philosopher: Philosopher): Unit = { 12 | randomWait(s"$philosopher is eating") 13 | } 14 | 15 | def think(philosopher: Philosopher): Unit = { 16 | randomWait(s"$philosopher is thinking") 17 | } 18 | 19 | val cycles: Int = 50 20 | it should s"run 5 dining philosophers for $cycles cycles without deadlock" in { 21 | diningPhilosophers(cycles) 22 | } 23 | 24 | sealed trait Philosopher 25 | case object Socrates extends Philosopher 26 | case object Confucius extends Philosopher 27 | case object Plato extends Philosopher 28 | case object Descartes extends Philosopher 29 | case object Voltaire extends Philosopher 30 | 31 | private def diningPhilosophers(cycles: Int) = { 32 | 33 | val tp = FixedPool(8) 34 | 35 | val hungry1 = m[Int] // The `Int` value represents how many cycles of eating/thinking still remain. 36 | val hungry2 = m[Int] 37 | val hungry3 = m[Int] 38 | val hungry4 = m[Int] 39 | val hungry5 = m[Int] 40 | val thinking1 = m[Int] 41 | val thinking2 = m[Int] 42 | val thinking3 = m[Int] 43 | val thinking4 = m[Int] 44 | val thinking5 = m[Int] 45 | val fork12 = m[Unit] 46 | val fork23 = m[Unit] 47 | val fork34 = m[Unit] 48 | val fork45 = m[Unit] 49 | val fork51 = m[Unit] 50 | 51 | val done = m[Unit] 52 | val check = b[Unit, Unit] 53 | 54 | site(tp) ( 55 | go { case thinking1(n) => think(Socrates); hungry1(n - 1) }, 56 | go { case thinking2(n) => think(Confucius); hungry2(n - 1) }, 57 | go { case thinking3(n) => think(Plato); hungry3(n - 1) }, 58 | go { case thinking4(n) => think(Descartes); hungry4(n - 1) }, 59 | go { case thinking5(n) => think(Voltaire); hungry5(n - 1) }, 60 | 61 | go { case done(()) + check((), reply) => reply() }, 62 | 63 | go { case hungry1(n) + fork12(()) + fork51(()) => eat(Socrates); thinking1(n) + fork12() + fork51(); if (n == 0) done() }, 64 | go { case hungry2(n) + fork23(()) + fork12(()) => eat(Confucius); thinking2(n) + fork23() + fork12() }, 65 | go { case hungry3(n) + fork34(()) + fork23(()) => eat(Plato); thinking3(n) + fork34() + fork23() }, 66 | go { case hungry4(n) + fork45(()) + fork34(()) => eat(Descartes); thinking4(n) + fork45() + fork34() }, 67 | go { case hungry5(n) + fork51(()) + fork45(()) => eat(Voltaire); thinking5(n) + fork51() + fork45() } 68 | ) 69 | 70 | thinking1(cycles) + thinking2(cycles) + thinking3(cycles) + thinking4(cycles) + thinking5(cycles) 71 | fork12() + fork23() + fork34() + fork45() + fork51() 72 | 73 | check() shouldEqual (()) 74 | tp.shutdownNow() 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /core/src/test/scala/io/chymyst/test/EventHooksSpec.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.test 2 | 3 | import io.chymyst.jc._ 4 | import org.scalatest.{FlatSpec, Matchers} 5 | 6 | import scala.concurrent.duration.DurationInt 7 | import scala.concurrent.{Await, Future} 8 | import scala.util.{Failure, Success, Try} 9 | 10 | class EventHooksSpec extends FlatSpec with Matchers { 11 | 12 | behavior of "whenEmitted" 13 | 14 | it should "resolve future when some molecule is emitted" in { 15 | val a = m[Int] 16 | val c = m[Int] 17 | withPool(FixedPool(2)) { tp ⇒ 18 | site(tp)( 19 | go { case a(x) + c(_) ⇒ c(x) } 20 | ) 21 | c(0) 22 | val fut = c.whenEmitted 23 | a(123) 24 | val res = Await.result(fut, 10.seconds) 25 | res shouldEqual 123 26 | }.get 27 | } 28 | 29 | it should "resolve future when some blocking molecule is emitted" in { 30 | val a = m[Int] 31 | val c = b[Int, Int] 32 | withPool(FixedPool(2)) { tp ⇒ 33 | site(tp)( 34 | go { case a(x) ⇒ c(x); () } 35 | , go { case c(_, r) ⇒ r(0) } 36 | ) 37 | val fut = c.whenEmitted 38 | a(123) 39 | 40 | val res = Await.result(fut, 2.seconds) 41 | res shouldEqual 123 42 | }.get 43 | } 44 | 45 | behavior of "whenScheduled" 46 | 47 | it should "resolve when some reaction is scheduled" in { 48 | val res = (1 to 20).map { _ => 49 | val a = m[Int] 50 | val c = m[Int] 51 | withPool(FixedPool(2)) { tp ⇒ 52 | site(tp)( 53 | go { case a(x) + c(_) ⇒ c(x) } 54 | ) 55 | val fut = a.whenScheduled 56 | c(0) 57 | a(1) 58 | Try(Await.result(fut, 2.seconds)) match { 59 | case Failure(exception) ⇒ "none" 60 | case Success(value) ⇒ value 61 | } 62 | }.get 63 | }.toSet 64 | println(res) 65 | res should contain("c") 66 | } 67 | 68 | it should "sometimes fail to resolve when reaction scheduler finishes too early" in { 69 | val res = (1 to 20).map { _ => 70 | val a = m[Int] 71 | val c = m[Int] 72 | withPool(FixedPool(2)) { tp ⇒ 73 | site(tp)( 74 | go { case a(x) + c(_) ⇒ c(x) } 75 | ) 76 | val fut = c.whenScheduled 77 | c(0) 78 | a(1) 79 | Try(Await.result(fut, 2.seconds)) match { 80 | case Failure(exception) ⇒ "none" 81 | case Success(value) ⇒ value 82 | } 83 | }.get 84 | }.toSet 85 | println(res) 86 | res should contain("c") 87 | } 88 | 89 | it should "resolve when another reaction is scheduled" in { 90 | val res = (1 to 20).map { _ => 91 | val a = m[Int] 92 | val c = m[Int] 93 | withPool(FixedPool(2)) { tp ⇒ 94 | site(tp)( 95 | go { case a(x) + c(_) ⇒ c(x) } 96 | ) 97 | val fut = c.whenScheduled 98 | a(1) 99 | c(0) 100 | Try(Await.result(fut, 2.seconds)) match { 101 | case Failure(_) ⇒ "none" 102 | case Success(v) ⇒ v 103 | } 104 | }.get 105 | }.toSet 106 | println(res) 107 | res should contain("a") 108 | } 109 | 110 | it should "fail to resolve when no reaction can be scheduled" in { 111 | val a = m[Int] 112 | val c = m[Int] 113 | withPool(FixedPool(2)) { tp ⇒ 114 | site(tp)( 115 | go { case a(x) + c(_) ⇒ c(x) } 116 | ) 117 | val fut = a.whenScheduled 118 | a(1) 119 | the[Exception] thrownBy Await.result(fut, 2.seconds) should have message "a.whenScheduled() failed because no reaction could be scheduled (this is not an error)" 120 | }.get 121 | 122 | } 123 | 124 | it should "be an automatic failure when used on reaction threads" in { 125 | val a = m[Int] 126 | val c = b[Unit, Future[String]] 127 | withPool(FixedPool(2)) { tp ⇒ 128 | site(tp)( 129 | go { case a(x) + c(_, r) ⇒ r(a.whenScheduled) } 130 | ) 131 | a(1) 132 | val fut = c() 133 | fut.isCompleted shouldEqual true 134 | the[Exception] thrownBy Await.result(fut, 2.seconds) should have message "whenScheduled() is disallowed on reaction threads (molecule: a)" 135 | }.get 136 | } 137 | 138 | behavior of "whenConsumed" 139 | 140 | it should "resolve when a molecule is consumed by reaction" in { 141 | val a = m[Unit] 142 | withPool(FixedPool(2)) { tp ⇒ 143 | site(tp)( 144 | go { case a(x) ⇒ } 145 | ) 146 | val fut = a.emitUntilConsumed() 147 | Await.result(fut, 2.seconds) shouldEqual (()) 148 | }.get 149 | } 150 | 151 | it should "time out when another copy of molecule is consumed" in { 152 | val a = m[Int] 153 | val f = b[Unit, Unit] 154 | withPool(FixedPool(2)) { tp ⇒ 155 | site(tp)( 156 | go { case a(x) + f(_, r) ⇒ r() } 157 | ) 158 | a.isPipelined shouldEqual true 159 | val fut0 = a.emitUntilConsumed(10) 160 | val fut = a.emitUntilConsumed(123) 161 | f() 162 | fut0.isCompleted shouldEqual true 163 | the[Exception] thrownBy Await.result(fut, 500.millis) should have message "Futures timed out after [500 milliseconds]" 164 | Await.result(fut0, 500.millis) shouldEqual 10 165 | }.get 166 | 167 | } 168 | 169 | it should "be an automatic failure when used on reaction threads" in { 170 | val a = m[Int] 171 | val c = b[Unit, Future[Int]] 172 | withPool(FixedPool(2)) { tp ⇒ 173 | site(tp)( 174 | go { case a(x) + c(_, r) ⇒ r(a.emitUntilConsumed(123)) } 175 | ) 176 | a(1) 177 | val fut = c() 178 | fut.isCompleted shouldEqual true 179 | the[Exception] thrownBy Await.result(fut, 2.seconds) should have message "emitUntilConsumed() is disallowed on reaction threads (molecule: a)" 180 | }.get 181 | } 182 | 183 | it should "signal error on static molecules" in { 184 | val a = m[Unit] 185 | val c = m[Unit] 186 | withPool(FixedPool(2)) { tp ⇒ 187 | site(tp)( 188 | go { case _ ⇒ a() } 189 | , go { case a(_) + c(_) ⇒ a() } 190 | ) 191 | a.isStatic shouldEqual true 192 | c.isStatic shouldEqual false 193 | the[ExceptionEmittingStaticMol] thrownBy a.emitUntilConsumed() should have message "Error: static molecule a(()) cannot be emitted non-statically" 194 | }.get 195 | } 196 | 197 | behavior of "user code utilizing test hooks" 198 | 199 | def makeCounter(initValue: Int, tp: Pool): (M[Unit], B[Unit, Int]) = { 200 | val c = m[Int] 201 | val d = m[Unit] 202 | val f = b[Unit, Int] 203 | 204 | site(tp)( 205 | go { case c(x) + f(_, r) ⇒ c(x) + r(x) }, 206 | go { case c(x) + d(_) ⇒ c(x - 1) }, 207 | go { case _ ⇒ c(initValue) } 208 | ) 209 | 210 | (d, f) 211 | } 212 | 213 | it should "verify the operation of the concurrent counter" in { 214 | val (d, f) = makeCounter(10, FixedPool(2)) 215 | 216 | val x = f() // current value 217 | val fut = d.emitUntilConsumed() 218 | // give a timeout just to be safe; actually, this will be quick 219 | Await.result(fut, 5.seconds) 220 | f() shouldEqual x - 1 221 | } 222 | 223 | } 224 | -------------------------------------------------------------------------------- /core/src/test/scala/io/chymyst/test/FairnessSpec.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.test 2 | 3 | import io.chymyst.jc._ 4 | import io.chymyst.test.Common._ 5 | 6 | class FairnessSpec extends LogSpec { 7 | 8 | behavior of "reaction site" 9 | 10 | // fairness over reactions: 11 | // We have n molecules A:M[Unit], which can all interact with a single molecule C:M[(Int,Array[Int])]. 12 | // We first emit all A's and then a single C. 13 | // Each molecule A_i will increment C's counter at index i upon reaction. 14 | // We repeat this for N iterations, then we read the array and check that its values are distributed more or less randomly. 15 | 16 | it should "implement fairness across reactions" in { 17 | (1 to 10).map { _ => 18 | val reactions = 4 19 | val N = 200 20 | 21 | val c = m[(Int, Array[Int])] 22 | val done = m[Array[Int]] 23 | val getC = b[Unit, Array[Int]] 24 | val a0 = m[Unit] 25 | val a1 = m[Unit] 26 | val a2 = m[Unit] 27 | val a3 = m[Unit] 28 | //n = 4 29 | 30 | val tp = FixedPool(4) 31 | 32 | site(tp)( 33 | go { case getC(_, r) + done(arr) => r(arr) }, 34 | go { 35 | case a0(_) + c((n, arr)) => if (n > 0) { 36 | arr(0) += 1 37 | c((n - 1, arr)) 38 | Thread.sleep(10) 39 | a0() 40 | } else done(arr) 41 | }, 42 | go { 43 | case a1(_) + c((n, arr)) => if (n > 0) { 44 | arr(1) += 1 45 | c((n - 1, arr)) 46 | Thread.sleep(10) 47 | a1() 48 | } else done(arr) 49 | }, 50 | go { 51 | case a2(_) + c((n, arr)) => if (n > 0) { 52 | arr(2) += 1 53 | c((n - 1, arr)) 54 | Thread.sleep(10) 55 | a2() 56 | } else done(arr) 57 | }, 58 | go { 59 | case a3(_) + c((n, arr)) => if (n > 0) { 60 | arr(3) += 1 61 | c((n - 1, arr)) 62 | Thread.sleep(10) 63 | a3() 64 | } else done(arr) 65 | } 66 | ) 67 | 68 | a0() + a1() + a2() + a3() 69 | Thread.sleep(10) 70 | c((N, Array.fill[Int](reactions)(0))) 71 | 72 | val result = getC() 73 | tp.shutdownNow() 74 | 75 | val average = N / reactions 76 | val max_deviation = math.max(math.abs(result.min - average).toDouble, math.abs(result.max - average).toDouble) / average 77 | println(s"Fairness across 4 reactions: ${result.mkString(", ")}. Average = $average. Max relative deviation = $max_deviation") 78 | max_deviation 79 | }.min should be < 0.2 80 | } 81 | 82 | // fairness across molecules: will be automatic here since all molecules are pipelined. 83 | // Emit n molecules A[Int] that can all interact with C[Int]. Each time they interact, their counter is incremented. 84 | // Then emit a single C molecule, which will react until its counter goes to 0. 85 | // At this point, gather all results from A[Int] into an array and return that array. 86 | 87 | it should "implement fairness across molecules" in { 88 | 89 | val counters = 20 90 | 91 | val cycles = 10000 92 | 93 | val c = m[Int] 94 | val done = m[List[Int]] 95 | val getC = b[Unit, List[Int]] 96 | val gather = m[List[Int]] 97 | val a = m[Int] 98 | 99 | val tp = FixedPool(8) 100 | 101 | site(tp)( 102 | go { case done(arr) + getC(_, r) => r(arr) }, 103 | go { case c(n) + a(i) if n > 0 => a(i + 1) + c(n - 1) }, 104 | go { case c(0) + a(i) => a(i) + gather(List()) }, 105 | go { case gather(arr) + a(i) => 106 | val newArr = i :: arr 107 | if (newArr.size < counters) gather(newArr) else done(newArr) 108 | } 109 | ) 110 | 111 | (1 to counters).foreach(_ => a(0)) 112 | Thread.sleep(100) 113 | c(cycles) 114 | 115 | val result = getC() 116 | println(result.mkString(", ")) 117 | 118 | tp.shutdownNow() 119 | 120 | result.min.toDouble should be > (cycles / counters * 0.8) 121 | result.max.toDouble should be < (cycles / counters * 1.2) 122 | } 123 | 124 | behavior of "multiple emission" 125 | 126 | /** Emit an equal number of a, bb, c molecules. One reaction consumes a + bb and the other consumes bb + c. 127 | * Verify that both reactions proceed with probability roughly 1/2. 128 | */ 129 | it should "schedule reactions fairly after multiple emission" in { 130 | val a = m[Unit] 131 | val bb = m[Unit] 132 | val c = m[Unit] 133 | val d = m[Unit] 134 | val e = m[Unit] 135 | val f = m[(Int, Int, Int)] 136 | val g = b[Unit, (Int, Int)] 137 | 138 | val tp = FixedPool(8) 139 | 140 | site(tp)( 141 | go { case a(x) + bb(y) if x == y => d() }, 142 | go { case bb(x) + c(y) if x == y => e() }, 143 | go { case d(_) + f((x, y, t)) => f((x + 1, y, t - 1)) }, 144 | go { case e(_) + f((x, y, t)) => f((x, y + 1, t - 1)) }, 145 | go { case g(_, r) + f((x, y, 0)) => r((x, y)) } 146 | ) 147 | val total = 200 148 | val results = (1 to total).map { _ ⇒ 149 | val n = 50 150 | 151 | f((0, 0, n)) 152 | (1 to n).foreach { _ => 153 | if (scala.util.Random.nextInt(2) == 0) 154 | a() + c() + bb() 155 | else c() + a() + bb() 156 | } 157 | 158 | val (ab, bc) = g() 159 | ab + bc shouldEqual n 160 | val discrepancy = math.abs(ab - bc + 0.0) / n 161 | // println(s"Reaction a + bb occurred $ab times. Reaction bb + c occurred $bc times. Discrepancy $discrepancy") 162 | (ab.toDouble / n, discrepancy) 163 | } 164 | val averageAB = results.map(_._1).sum / total 165 | val minDiscrepancy = results.map(_._2).min 166 | println(s"Average occurrence of a + bb is $averageAB. Min. discrepancy = $minDiscrepancy") 167 | averageAB should be > 0.3 168 | averageAB should be < 0.7 169 | 170 | tp.shutdownNow() 171 | } 172 | 173 | // This test failed to complete in 500ms on Travis CI with Scala 2.10, but succeeds with 2.11. However, this could have been a fluctuation. 174 | it should "fail to schedule reactions fairly after multiple emission into separate RSs" in { 175 | 176 | val tp = FixedPool(8) 177 | 178 | def makeRS(d1: M[Unit], d2: M[Unit]): (M[Unit], M[Unit], M[Unit]) = { 179 | val a = m[Unit] 180 | val b = m[Unit] 181 | val c = m[Unit] 182 | 183 | site(tp)( 184 | go { case a(_) + b(_) => d1() }, 185 | go { case b(_) + c(_) => d2() } 186 | ) 187 | (a, b, c) 188 | } 189 | 190 | val d = m[Unit] 191 | val e = m[Unit] 192 | val f = m[(Int, Int, Int)] 193 | val g = b[Unit, (Int, Int)] 194 | 195 | site(tp)( 196 | go { case d(_) + f((x, y, t)) => f((x + 1, y, t - 1)) }, 197 | go { case e(_) + f((x, y, t)) => f((x, y + 1, t - 1)) }, 198 | go { case g(_, r) + f((x, y, 0)) => r((x, y)) } 199 | ) 200 | 201 | val n = 400 202 | 203 | f((0, 0, n)) 204 | 205 | repeat(n) { 206 | val (a, b, c) = makeRS(d, e) 207 | a() + b() + c() // at the moment, this is equivalent to a(); b(); c. 208 | // this test will need to be changed when true multiple emission is implemented. 209 | } 210 | 211 | val (ab, bc) = g() 212 | ab + bc shouldEqual n 213 | val discrepancy = math.abs(ab - bc + 0.0) / n 214 | discrepancy should be > 0.4 215 | 216 | tp.shutdownNow() 217 | } 218 | 219 | } 220 | -------------------------------------------------------------------------------- /core/src/test/scala/io/chymyst/test/LogSpec.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.test 2 | 3 | import org.scalatest.{Args, FlatSpec, Matchers, Status} 4 | 5 | class LogSpec extends FlatSpec with Matchers { 6 | 7 | protected override def runTest(testName: String, args: Args): Status = { 8 | val initTime = System.currentTimeMillis() 9 | println(s"*** Starting test ($initTime): $testName") 10 | val result = super.runTest(testName, args) 11 | println(s"*** Finished test ($initTime): $testName in ${System.currentTimeMillis() - initTime} ms") 12 | result 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/src/test/scala/io/chymyst/test/ParallelOrSpec.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.test 2 | 3 | import io.chymyst.jc._ 4 | 5 | import scala.annotation.tailrec 6 | import scala.concurrent.duration._ 7 | import scala.language.postfixOps 8 | 9 | class ParallelOrSpec extends LogSpec { 10 | 11 | behavior of "test" 12 | 13 | /** Given two blocking molecules f and g returning Boolean, construct a new blocking molecule `parallelOr` 14 | * that returns the Boolean Or value of whichever of f and g unblocks first. 15 | * If one of `f` and `g` returns `true` then `parallelOr` also unblocks and returns `true`. 16 | * If both `f` and `g` return `false` then `parallelOr` also returns `false`. 17 | * Otherwise (if both `f` and `g` remain blocked or one of them returns `false` while the other remains blocked), 18 | * `parallelOr` will continue to be blocked. 19 | * 20 | * @param f First blocking molecule emitter. 21 | * @param g Second blocking molecule emitter. 22 | * @param tp Thread pool on which to run this. 23 | * @return New blocking molecule emitter that will return the desired result or block. 24 | */ 25 | def parallelOr(f: B[Unit, Boolean], g: B[Unit, Boolean], tp: Pool): B[Unit, Boolean] = { 26 | val c = m[Unit] 27 | val d = m[Unit] 28 | val done = m[Boolean] 29 | val result = m[Int] 30 | val finalResult = m[Boolean] 31 | 32 | val parallelOr = b[Unit, Boolean] 33 | 34 | site(tp)( 35 | go { case parallelOr(_, r) + finalResult(x) => r(x) } 36 | ) 37 | 38 | site(tp)( 39 | go { case result(_) + done(true) => finalResult(true) }, 40 | go { case result(1) + done(false) => finalResult(false) }, 41 | go { case result(0) + done(false) => result(1) } 42 | ) 43 | 44 | site(tp)( 45 | go { case c(_) => val x = f(); done(x) }, 46 | go { case d(_) => val y = g(); done(y) } 47 | ) 48 | 49 | c() + d() + result(0) 50 | 51 | parallelOr 52 | } 53 | 54 | it should "implement the Parallel Or operation correctly" in { 55 | 56 | val never = b[Unit, Boolean] 57 | val fastTrue = b[Unit, Boolean] 58 | val slowTrue = b[Unit, Boolean] 59 | val fastFalse = b[Unit, Boolean] 60 | val slowFalse = b[Unit, Boolean] 61 | 62 | val tp = FixedPool(30) 63 | 64 | site(tp)( 65 | go { case never(_, r) => r(neverReturn[Boolean]) }, 66 | go { case fastTrue(_, r) => Thread.sleep(10); r(true) }, 67 | go { case slowTrue(_, r) => Thread.sleep(200); r(true) }, 68 | go { case fastFalse(_, r) => Thread.sleep(10); r(false) }, 69 | go { case slowFalse(_, r) => Thread.sleep(200); r(false) } 70 | ) 71 | 72 | parallelOr(fastTrue, fastFalse, tp)() shouldEqual true 73 | parallelOr(fastTrue, slowFalse, tp)() shouldEqual true 74 | parallelOr(slowTrue, fastFalse, tp)() shouldEqual true 75 | parallelOr(slowTrue, slowFalse, tp)() shouldEqual true 76 | parallelOr(fastTrue, never, tp)() shouldEqual true 77 | parallelOr(never, slowTrue, tp)() shouldEqual true 78 | 79 | parallelOr(slowFalse, fastFalse, tp)() shouldEqual false 80 | parallelOr(slowFalse, fastFalse, tp)() shouldEqual false 81 | parallelOr(fastFalse, slowTrue, tp)() shouldEqual true 82 | 83 | parallelOr(never, fastFalse, tp).timeout()(200 millis) shouldEqual None 84 | parallelOr(never, slowFalse, tp).timeout()(200 millis) shouldEqual None 85 | 86 | tp.shutdownNow() 87 | } 88 | 89 | /** Given two blocking molecules b1 and b2, construct a new blocking molecule emitter that returns 90 | * the result of whichever of b1 and b2 unblocks first. 91 | * 92 | * @param b1 First blocking molecule emitter. 93 | * @param b2 Second blocking molecule emitter. 94 | * @param tp Thread pool on which to run this. 95 | * @tparam T Type of the return value. 96 | * @return New blocking molecule emitter that will return the desired result. 97 | */ 98 | def firstResult[T](b1: B[Unit, T], b2: B[Unit, T], tp: Pool): B[Unit, T] = { 99 | val get = b[Unit, T] 100 | val res = b[Unit, T] 101 | val res1 = m[Unit] 102 | val res2 = m[Unit] 103 | val done = m[T] 104 | 105 | site(tp)( 106 | go { case res1(_) => val x = b1(); done(x) }, // IntelliJ 2016.3 CE insists on `b1(())` here, but scalac is fine with `b1()`. 107 | go { case res2(_) => val x = b2(); done(x) } 108 | ) 109 | 110 | site(tp)(go { case get(_, r) + done(x) => r(x) }) 111 | 112 | site(tp)(go { case res(_, r) => res1() + res2(); val x = get(); r(x) }) 113 | 114 | res 115 | } 116 | 117 | @tailrec 118 | final def neverReturn[T]: T = { 119 | Thread.sleep(1000000) 120 | neverReturn[T] 121 | } 122 | 123 | it should "implement the First Result operation correctly" in { 124 | 125 | val never = b[Unit, String] 126 | val fast = b[Unit, String] 127 | val slow = b[Unit, String] 128 | 129 | val tp = FixedPool(30) 130 | 131 | site(tp)( 132 | go { case never(_, r) => r(neverReturn[String]) }, 133 | go { case fast(_, r) => Thread.sleep(10); r("fast") }, 134 | go { case slow(_, r) => Thread.sleep(200); r("slow") } 135 | ) 136 | 137 | firstResult(fast, fast, tp)() shouldEqual "fast" 138 | firstResult(fast, slow, tp)() shouldEqual "fast" 139 | firstResult(slow, fast, tp)() shouldEqual "fast" 140 | firstResult(slow, slow, tp)() shouldEqual "slow" 141 | firstResult(fast, never, tp)() shouldEqual "fast" 142 | firstResult(never, slow, tp)() shouldEqual "slow" 143 | 144 | tp.shutdownNow() 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /core/src/test/scala/io/chymyst/test/PaxosSpec.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.test 2 | 3 | import io.chymyst.jc._ 4 | 5 | // See https://understandingpaxos.wordpress.com/ 6 | 7 | class PaxosSpec extends LogSpec { 8 | 9 | behavior of "Paxos algorithm" 10 | 11 | it should "run single-decree Paxos with three nodes" in { 12 | val tp = FixedPool(4) 13 | 14 | val response = m[String] 15 | val fetch = b[Unit, String] 16 | 17 | val nodes = 3 18 | lazy val (request, propose0: M[PermissionRequest[String]], accept0) = createNode(0, tp, Some(response)) 19 | lazy val (_, propose1, accept1) = createNode(1, tp) 20 | lazy val (_, propose2, accept2) = createNode(2, tp) 21 | 22 | lazy val proposeMolecules = Seq(propose0, propose1, propose2) 23 | lazy val acceptMolecules = Seq(accept0, accept1, accept2) 24 | 25 | def createNode(index: Int, tp: Pool, response: Option[M[String]] = None): (M[String], M[PermissionRequest[String]], M[Suggestion[String]]) = { 26 | 27 | def proposalNumber(): ProposalNumber = ProposalNumber(timestamp, index) 28 | 29 | def computeAcceptorResponse[T]( 30 | state: AcceptorNodeState[T], 31 | requestedProposalNumber: ProposalNumber 32 | ): (ProposalNumber, PermissionResponse[T]) = { 33 | val lastSeenProposalNumber = state.rejectBelow match { 34 | case Some(p) if p > requestedProposalNumber ⇒ p 35 | case _ ⇒ requestedProposalNumber 36 | } 37 | val responseToProposer: PermissionResponse[T] = 38 | if (lastSeenProposalNumber > requestedProposalNumber) 39 | PermissionDenied 40 | else PermissionGranted(requestedProposalNumber, state.lastAccepted) 41 | 42 | (lastSeenProposalNumber, responseToProposer) 43 | } 44 | 45 | def computeNewProposal[T](p: Option[Proposal[T]], lastProposal: Option[Proposal[T]]): Option[Proposal[T]] = { 46 | p match { 47 | case Some(proposal) ⇒ lastProposal match { 48 | case Some(lastP) if proposal.number <= lastP.number ⇒ Some(lastP) 49 | case _ ⇒ Some(proposal) 50 | } 51 | case None ⇒ lastProposal 52 | } 53 | } 54 | 55 | val acceptorNode = m[AcceptorNodeState[String]] 56 | val proposerNode = m[ProposerNodeState[String]] 57 | 58 | val clientRequest = m[String] 59 | val propose = m[PermissionRequest[String]] 60 | val promise = m[PermissionResponse[String]] 61 | val accept = m[Suggestion[String]] 62 | val learn = m[PermissionResponse[String]] 63 | 64 | // Each node plays all three roles. 65 | // Reactions for one node: 66 | site(tp)( 67 | go { case proposerNode(_) + clientRequest(v) ⇒ 68 | // Send a proposal to all acceptor nodes. 69 | val newNumber = proposalNumber() 70 | val proposal = PermissionRequest(newNumber, promise) 71 | proposeMolecules.foreach(_.apply(proposal)) 72 | val newState = ProposerNodeState(Some(Proposal(newNumber, v))) 73 | proposerNode(newState) 74 | }, 75 | go { case acceptorNode(state) + propose(PermissionRequest(proposalNumber, proposerResponse)) ⇒ 76 | val (lastProposalNumber, newResponse) = computeAcceptorResponse(state, proposalNumber) 77 | proposerResponse(newResponse) 78 | acceptorNode(state.copy(rejectBelow = Some(lastProposalNumber))) 79 | }, 80 | go { case acceptorNode(state) + accept(Suggestion(proposal, proposer)) ⇒ 81 | val (lastProposalNumber, newResponse) = computeAcceptorResponse(state, proposal.number) 82 | proposer(newResponse) 83 | acceptorNode(state.copy(rejectBelow = Some(lastProposalNumber))) 84 | }, 85 | go { case proposerNode(state) + promise(permissionResponse) ⇒ 86 | permissionResponse match { 87 | case PermissionGranted(_, p) ⇒ 88 | val newProposal: Option[Proposal[String]] = computeNewProposal(p, state.lastProposal) 89 | val newPromisesReceived = state.promisesReceived + 1 90 | proposerNode(state.copy(lastProposal = newProposal, promisesReceived = newPromisesReceived)) 91 | if (newPromisesReceived > nodes / 2) { 92 | // Stage 1 consensus achieved. 93 | acceptMolecules.foreach(_.apply(Suggestion(newProposal.get, learn))) 94 | } 95 | 96 | case PermissionDenied ⇒ 97 | // Reset state. 98 | proposerNode(state.copy(promisesReceived = 0, acksReceived = 0)) 99 | state.lastProposal.foreach(p ⇒ clientRequest(p.value)) // Make another attempt to achieve consensus for this value. 100 | } 101 | }, 102 | go { case proposerNode(state) + learn(permissionResponse) ⇒ 103 | permissionResponse match { 104 | case PermissionGranted(_, p) ⇒ 105 | val newProposal: Option[Proposal[String]] = computeNewProposal(p, state.lastProposal) 106 | val newAcksReceived = state.acksReceived + 1 107 | proposerNode(state.copy(lastProposal = newProposal, acksReceived = newAcksReceived)) 108 | if (newAcksReceived > nodes / 2) { 109 | // Stage 2 consensus achieved. Send response. 110 | response.foreach(_.apply(newProposal.get.value)) 111 | } 112 | 113 | case PermissionDenied ⇒ 114 | // Reset state. 115 | proposerNode(state.copy(promisesReceived = 0, acksReceived = 0)) 116 | state.lastProposal.foreach(p ⇒ clientRequest(p.value)) // Make another attempt to achieve consensus for this value. 117 | } 118 | } 119 | ) 120 | 121 | acceptorNode(AcceptorNodeState()) 122 | proposerNode(ProposerNodeState()) 123 | 124 | (clientRequest, propose, accept) 125 | } 126 | 127 | site(tp)( 128 | go { case response(v) + fetch(_, r) ⇒ r(v) } 129 | ) 130 | 131 | request("abc") 132 | request("xyz") 133 | val result = fetch() 134 | result shouldEqual "xyz" 135 | tp.shutdownNow() 136 | } 137 | 138 | def timestamp: Long = System.nanoTime() 139 | 140 | final case class AcceptorNodeState[T](rejectBelow: Option[ProposalNumber] = None, lastAccepted: Option[Proposal[T]] = None) 141 | 142 | final case class ProposerNodeState[T](lastProposal: Option[Proposal[T]] = None, promisesReceived: Int = 0, acksReceived: Int = 0) 143 | 144 | sealed trait PermissionResponse[+T] 145 | 146 | final case class PermissionGranted[T](initialProposalNumber: ProposalNumber, lastAccepted: Option[Proposal[T]] = None) extends PermissionResponse[T] 147 | 148 | case object PermissionDenied extends PermissionResponse[Nothing] 149 | 150 | final case class PermissionRequest[T](proposalNumber: ProposalNumber, proposerResponse: M[PermissionResponse[T]]) 151 | 152 | final case class Suggestion[T](proposal: Proposal[T], proposer: M[PermissionResponse[T]]) 153 | 154 | type Accepted[T] = ProposalNumber 155 | 156 | final case class Proposal[T](number: ProposalNumber, value: T) 157 | 158 | final case class ProposalNumber(n: Long, i: Int) extends Ordered[ProposalNumber] { 159 | 160 | import scala.math.Ordered.orderingToOrdered 161 | 162 | override def compare(that: ProposalNumber): Int = (n, i) compare((that.n, that.i)) 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /core/src/test/scala/io/chymyst/test/ShutdownSpec.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.test 2 | 3 | import io.chymyst.jc._ 4 | 5 | /** This test will shutdown the default thread pools and check that no reactions can occur afterwards. 6 | * 7 | * This test suite is run last, after all other tests, and hopefully will be able to shutdown the test suites in CI. 8 | */ 9 | class ShutdownSpec extends LogSpec { 10 | 11 | behavior of "pool threads" 12 | 13 | it should "not fail to schedule reactions after a timeout when fixed pool may free threads" in { 14 | 15 | val pool = FixedPool() 16 | 17 | val x = m[Unit] 18 | site(pool)(go { case x(()) => }) 19 | x() 20 | Thread.sleep(5000) 21 | x() 22 | pool.shutdownNow() 23 | } 24 | 25 | it should "not fail to schedule reactions after a timeout when BlockingPool may free threads" in { 26 | 27 | val pool = BlockingPool() 28 | 29 | pool.currentPoolSize shouldEqual cpuCores 30 | 31 | val x = m[Unit] 32 | site(pool)(go { case x(()) => }) 33 | x() 34 | pool.currentPoolSize shouldEqual cpuCores 35 | Thread.sleep(5000) 36 | pool.currentPoolSize shouldEqual cpuCores 37 | x() 38 | pool.shutdownNow() 39 | } 40 | 41 | it should "not fail to schedule reactions after shutdown of custom reaction pool" in { 42 | 43 | val pool = FixedPool(2) 44 | pool.shutdownNow() 45 | 46 | val x = m[Unit] 47 | site(pool)(go { case x(()) => }) 48 | the[Exception] thrownBy { 49 | x() 50 | } should have message "In Site{x → ...}: Cannot emit molecule x() because reaction pool FixedPool:pool is not active" 51 | } 52 | 53 | it should "fail to schedule reactions after shutdown of default thread pools" in { 54 | 55 | defaultPool.shutdownNow() 56 | 57 | val x = m[Unit] 58 | site(go { case x(_) => }) 59 | 60 | the[Exception] thrownBy { 61 | x() 62 | } should have message "In Site{x → ...}: Cannot emit molecule x() because reaction pool BlockingPool:defaultPool is not active" 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /core/src/test/scala/io/chymyst/util/BuduSpec.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.util 2 | 3 | import io.chymyst.test.Common._ 4 | import io.chymyst.test.LogSpec 5 | 6 | import scala.concurrent.ExecutionContext.Implicits.global 7 | import scala.concurrent.duration.{Duration, DurationInt} 8 | import scala.concurrent.{Await, Future, Promise} 9 | 10 | class BuduSpec extends LogSpec { 11 | behavior of "Budu()" 12 | 13 | it should "report status = true if no waiting occurred" in { 14 | val x = Budu[Int] 15 | x.isEmpty shouldEqual true 16 | x.isTimedOut shouldEqual false 17 | x.is(123) shouldEqual true 18 | x.isEmpty shouldEqual false 19 | x.isTimedOut shouldEqual false 20 | } 21 | 22 | it should "wait for reply" in { 23 | val x = Budu[Int] 24 | Future { 25 | x.is(123) 26 | } 27 | x.await shouldEqual 123 28 | } 29 | 30 | it should "return old result when already have reply" in { 31 | val x = Budu[Int] 32 | Future { 33 | x.is(123) 34 | x.is(200) 35 | } 36 | x.await shouldEqual 123 37 | x.await shouldEqual 123 38 | x.is(300) 39 | x.await shouldEqual 123 40 | } 41 | 42 | it should "wait for reply using Future" in { 43 | val x = Budu[Int](useFuture = true) 44 | Future { 45 | x.is(123) 46 | } 47 | Await.result(x.getFuture, Duration.Inf) shouldEqual 123 48 | } 49 | 50 | it should "produce error when using getFuture without correct initialization" in { 51 | val x = Budu[Int] 52 | the[Exception] thrownBy x.getFuture should have message "getFuture() is disabled, initialize as Budu(useFuture = true) to enable" 53 | } 54 | 55 | it should "wait for reply and report status" in { 56 | val x = Budu[Int] 57 | val y = Budu[Boolean] 58 | x.isEmpty shouldEqual true 59 | Future { 60 | y.is(x.is(123)) 61 | } 62 | x.isTimedOut shouldEqual false 63 | x.await shouldEqual 123 64 | x.isEmpty shouldEqual false 65 | x.isTimedOut shouldEqual false 66 | y.await shouldEqual true 67 | x.is(200) shouldEqual true // repeated reply is silently ignored 68 | x.isEmpty shouldEqual false 69 | x.isTimedOut shouldEqual false 70 | } 71 | 72 | it should "wait for reply with time-out not reached" in { 73 | val x = Budu[Int] 74 | Future { 75 | Thread.sleep(100) 76 | x.is(123) 77 | } 78 | x.await(500.millis) shouldEqual Some(123) 79 | } 80 | 81 | it should "wait for reply with time-out reached" in { 82 | val x = Budu[Int] 83 | Future { 84 | Thread.sleep(500) 85 | x.is(123) 86 | } 87 | x.await(100.millis) shouldEqual None 88 | x.is(200) shouldEqual false 89 | x.await(100.millis) shouldEqual None 90 | } 91 | 92 | it should "wait for reply with time-out not reached and report status" in { 93 | val x = Budu[Int] 94 | val y = Budu[Boolean] 95 | Future { 96 | Thread.sleep(100) 97 | y.is(x.is(123)) 98 | } 99 | x.await(500.millis) shouldEqual Some(123) 100 | y.await shouldEqual true 101 | } 102 | 103 | it should "wait for reply with time-out reached and report status" in { 104 | val x = Budu[Int] 105 | val y = Budu[Boolean] 106 | Future { 107 | Thread.sleep(500) 108 | y.is(x.is(123)) 109 | } 110 | x.await(100.millis) shouldEqual None 111 | y.await shouldEqual false 112 | } 113 | 114 | behavior of "performance benchmark" 115 | 116 | val total = 20000 117 | val best = 100 118 | 119 | def getBest(res: Seq[Double]): Seq[Double] = res.sortBy(- _).dropRight(best) 120 | 121 | it should "measure reply speed for Budu" in { 122 | val results = (1 to total).map { _ ⇒ 123 | val x = Budu[Long] 124 | Future { 125 | x.is(System.nanoTime()) 126 | } 127 | val now = x.await 128 | System.nanoTime - now 129 | }.map(_.toDouble) 130 | val (average, stdev) = meanAndStdev(getBest(results)) 131 | println(s"Best amortized reply speed for Budu, based on $best best samples: ${formatNanosToMicros(average)} ± ${formatNanosToMicros(stdev)}") 132 | showFullStatistics("reply speed for Budu", results) 133 | } 134 | 135 | it should "measure reply speed for Promise" in { 136 | val results = (1 to total).map { _ ⇒ 137 | val x = Promise[Long] 138 | Future { 139 | x.success(System.nanoTime()) 140 | } 141 | val now = Await.result(x.future, Duration.Inf) 142 | System.nanoTime - now 143 | }.map(_.toDouble) 144 | val (average, stdev) = meanAndStdev(getBest(results)) 145 | println(s"Best amortized reply speed for Promise, based on $best best samples: ${formatNanosToMicros(average)} ± ${formatNanosToMicros(stdev)}") 146 | showFullStatistics("reply speed for Promise", results) 147 | } 148 | 149 | it should "measure time-out reply speed for Budu" in { 150 | val results = (1 to total).map { _ ⇒ 151 | val x = Budu[Long] 152 | Future { 153 | x.is(System.nanoTime()) 154 | } 155 | val now = x.await(10.seconds).get 156 | System.nanoTime - now 157 | }.map(_.toDouble) 158 | val (average, stdev) = meanAndStdev(getBest(results)) 159 | println(s"Best amortized time-out reply speed for Budu, based on $best best samples: ${formatNanosToMicros(average)} ± ${formatNanosToMicros(stdev)}") 160 | showFullStatistics("time-out reply speed for Budu", results) 161 | } 162 | 163 | it should "measure time-out reply speed for Promise" in { 164 | val results = (1 to total).map { _ ⇒ 165 | val x = Promise[Long] 166 | Future { 167 | x.success(System.nanoTime()) 168 | } 169 | val now = Await.result(x.future, 10.seconds) 170 | System.nanoTime - now 171 | }.map(_.toDouble) 172 | val (average, stdev) = meanAndStdev(getBest(results)) 173 | println(s"Best amortized time-out reply speed for Promise, based on $best best samples: ${formatNanosToMicros(average)} ± ${formatNanosToMicros(stdev)}") 174 | showFullStatistics("time-out reply speed for Promise", results) 175 | } 176 | 177 | } 178 | -------------------------------------------------------------------------------- /core/src/test/scala/io/chymyst/util/FinalTaglessSpec.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.util 2 | 3 | import io.chymyst.test.{Common, LogSpec} 4 | 5 | class FinalTaglessSpec extends LogSpec { 6 | 7 | behavior of "final tagless Option" 8 | 9 | def benchmark(message: String, x: => Any): Unit = { 10 | val total = 50000 11 | 12 | val result = Common.elapsedTimesNs(x, total) 13 | val (mean0, std0) = Common.meanAndStdev(result.slice(50, 150)) 14 | val (mean1, std1) = Common.meanAndStdev(result.takeRight(200)) 15 | println(s"$message (total=$total) takes before warmup ${Common.formatNanosToMicrosWithMeanStd(mean0, std0)}, after warmup ${Common.formatNanosToMicrosWithMeanStd(mean1, std1)}") 16 | Common.showFullStatistics(message, result) 17 | } 18 | 19 | it should "benchmark creating an Option[Int] value" in { 20 | benchmark("standard Option Some(3).getOrElse(2)", Some(3).getOrElse(2)) 21 | } 22 | 23 | // see https://oleksandrmanzyuk.wordpress.com/2014/06/18/from-object-algebras-to-finally-tagless-interpreters-2/ 24 | it should "create some() and none() values secundo Oleksandr Manzyuk" in { 25 | 26 | import FinalTagless._ 27 | 28 | // testing with explicit ftIsEmpty 29 | val some3: FTOptionIsEmpty = some(3)(ie) 30 | val some3IsEmpty: Boolean = some3.isEmpty 31 | some3IsEmpty shouldEqual false 32 | val noneInt: FTOptionIsEmpty = none(ie) 33 | val noneIntIsEmpty: Boolean = noneInt.isEmpty 34 | noneIntIsEmpty shouldEqual true 35 | val some3GOE = some(3)(goe) // [Int] is inferred here 36 | val noneGOE = none(goe[Int]) // necessary to say goe[Int] here 37 | val res1 = some3GOE.getOrElse(100) // this is 3 38 | res1 shouldEqual 3 39 | val res2 = noneGOE.getOrElse(100) // this is 100 40 | res2 shouldEqual 100 41 | benchmark("FTOption some(3).getOrElse(2)", some(3)(goe).getOrElse(2)) 42 | } 43 | 44 | it should "use less boilerplate with FT2" in { 45 | import FT2._ 46 | 47 | def some3[X] = some[Int, X](3) 48 | 49 | some3[Boolean].isEmpty shouldEqual false 50 | benchmark("FT2Option some(3).getOrElse(2)", some[Int, Int ⇒ Int](3).getOrElse(2)) 51 | } 52 | 53 | it should "use less boilerplate with FT3" in { 54 | import FT3._ 55 | 56 | def some3[X] = some[Int, X](3) 57 | 58 | some3[Boolean].isEmpty shouldEqual false 59 | benchmark("FT3Option some(3).getOrElse(2)", some[Int, Int ⇒ Int](3).getOrElse(2)) 60 | } 61 | 62 | it should "use less boilerplate with FT4" in { 63 | import FT4._ 64 | 65 | val some3 = some(3) // some3 has type FT4Option[Int] with type inference. 66 | some3.isEmpty shouldEqual false 67 | benchmark("FT4Option some(3).getOrElse(2)", some(3).getOrElse(2)) 68 | val none3 = none[Int] // otherwise none3 has type FT4Option[Nothing] and that fails... 69 | none3.isEmpty shouldEqual true 70 | some3.getOrElse(2) shouldEqual 3 71 | none3.getOrElse(2) shouldEqual 2 72 | } 73 | 74 | it should "use less boilerplate with FT5" in { 75 | import FT5._ 76 | 77 | val some3 = some(3) // some3 has type FT4Option[Int] with type inference. 78 | some3.isEmpty shouldEqual false 79 | benchmark("FT5Option some(3).getOrElse(2)", some(3).getOrElse(2)) 80 | val none3 = none[Int] // otherwise none3 has type FT4Option[Nothing] and that fails... 81 | none3.isEmpty shouldEqual true 82 | some3.getOrElse(2) shouldEqual 3 83 | none3.getOrElse(2) shouldEqual 2 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /core/src/test/scala/io/chymyst/util/LabeledTypesSpec.scala: -------------------------------------------------------------------------------- 1 | package io.chymyst.util 2 | 3 | import io.chymyst.test.LogSpec 4 | import io.chymyst.util.LabeledTypes.{Newtype, Subtype} 5 | 6 | class LabeledTypesSpec extends LogSpec { 7 | 8 | // Example 1: labeled String type 9 | 10 | val UserName = Newtype[String] 11 | // the new type UserName is not the same as String and not a subtype of String 12 | type UserName = UserName.T // optional boilerplate to define a shorter type alias 13 | 14 | val UserNameS = Subtype[String] 15 | // the new type UserNameS is a subtype of String 16 | type UserNameS = UserNameS.T // optional boilerplate to define a shorter type alias 17 | 18 | // Example 2: labeled Int type 19 | 20 | val UserId = Newtype[Int] 21 | // the new type UserId is not the same as Int and not a subtype of Int 22 | type UserId = UserId.T // optional boilerplate to define a shorter type alias 23 | 24 | val UserIdS = Subtype[Int] 25 | // the new type UserIdS is a subtype of Int 26 | type UserIdS = UserIdS.T // optional boilerplate to define a shorter type alias 27 | 28 | behavior of "labeled string" 29 | 30 | it should "convert a string and then unwrap using labeled subtype" in { 31 | val x = UserNameS("user name") // x is now of type UserNameS 32 | 33 | x shouldEqual "user name" 34 | 35 | def f(x: UserNameS): Int = x.length // automatic upcast of x to String 36 | def g(x: String): Int = x.length 37 | 38 | f(x) shouldEqual 9 39 | """ f("abc") """ shouldNot compile 40 | 41 | // automatic upcast 42 | g(x) shouldEqual 9 43 | } 44 | 45 | it should "distinguish different newtype instances with the same underlying type" in { 46 | val EnvName = Newtype[String] 47 | type EnvName = EnvName.T 48 | val TenantName = Newtype[String] 49 | type TenantName = TenantName.T 50 | 51 | def f(env: EnvName, tenant: TenantName): Unit = () 52 | 53 | val env = EnvName("env1") 54 | val tenant = TenantName("tenant1") 55 | 56 | f(env, tenant) shouldEqual (()) 57 | 58 | " f(tenant, env) " shouldNot compile 59 | """ f("tenant1", "env1") """ shouldNot compile 60 | } 61 | 62 | it should "distinguish different subtype instances with the same underlying type" in { 63 | val EnvName = Subtype[String] 64 | type EnvName = EnvName.T 65 | val TenantName = Subtype[String] 66 | type TenantName = TenantName.T 67 | 68 | def f(env: EnvName, tenant: TenantName): Unit = () 69 | 70 | val env = EnvName("env1") 71 | val tenant = TenantName("tenant1") 72 | 73 | f(env, tenant) shouldEqual (()) 74 | 75 | " f(tenant, env) " shouldNot compile 76 | """ f("tenant1", "env1") """ shouldNot compile 77 | } 78 | 79 | it should "convert a string and then unwrap using labeled newtype" in { 80 | val x = UserName("user name") // x is now of type UserName 81 | 82 | x shouldEqual "user name" 83 | 84 | def f(x: UserName): Int = UserName.get(x).length // no automatic upcast of x to String 85 | def g(x: String): Int = x.length 86 | 87 | g("user name") shouldEqual 9 88 | 89 | f(x) shouldEqual 9 90 | """ f("abc") """ shouldNot compile 91 | 92 | " g(x) " shouldNot compile // no automatic upcast of x to String 93 | } 94 | 95 | it should "convert a list of labeled newtypes" in { 96 | val rawList = List("a", "b", "c") 97 | val usernameList = UserName.maptype(rawList) // usernameList is now of type List[UserName] 98 | 99 | def f(x: List[UserName]): Int = x.length 100 | 101 | def g(x: List[String]): Int = x.length 102 | 103 | " f(rawList) " shouldNot compile 104 | f(usernameList) shouldEqual 3 // conversion from List[String] to List[UserName] 105 | 106 | " g(usernameList) " shouldNot compile // No automatic converstion back to List[String] 107 | 108 | // Manual conversion back to List[String]. This does not iterate over the list. 109 | val unwrappedList = UserName.unmaptype(usernameList) 110 | g(unwrappedList) shouldEqual 3 111 | } 112 | 113 | it should "convert a list of labeled subtypes" in { 114 | val rawList = List("a", "b", "c") 115 | val usernameList = UserNameS.maptype(rawList) // usernameList is now of type List[UserNameS] 116 | 117 | def f(x: List[UserNameS]): Int = x.length 118 | 119 | def g(x: List[String]): Int = x.length 120 | 121 | f(usernameList) shouldEqual 3 122 | g(usernameList) shouldEqual 3 // automatic conversion from List[UserNameS] back to List[String] 123 | 124 | " f(rawList) " shouldNot compile 125 | f(usernameList) shouldEqual 3 // OK 126 | } 127 | 128 | behavior of "labeled integer" 129 | 130 | it should "convert an integer and then unwrap using labeled subtype" in { 131 | val x = UserIdS(123) // x is now of type UserIdS 132 | 133 | def f(x: UserIdS): Int = x * 2 134 | 135 | f(x) shouldEqual 246 136 | """ f(123) """ shouldNot compile 137 | } 138 | 139 | it should "convert an integer and then unwrap using labeled type" in { 140 | val x = UserId(123) // x is now of type UserId 141 | 142 | def f(x: UserId): Int = UserId.get(x) * 2 143 | 144 | f(x) shouldEqual 246 145 | """ f(123) """ shouldNot compile 146 | } 147 | 148 | it should "convert a list of labeled integers" in { 149 | val rawList = List(1, 2, 3) 150 | val useridList = UserId.maptype(rawList) // useridList is now of type List[UserId] 151 | 152 | def f(x: List[UserId]): Int = x.length 153 | 154 | def g(x: List[Int]): Int = x.length 155 | 156 | f(useridList) shouldEqual 3 157 | " g(useridList) " shouldNot compile 158 | 159 | val unwrappedList = UserId.unmaptype(useridList) // convert back to List[Int] 160 | g(unwrappedList) shouldEqual 3 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /docs/An_illustration_of_the_dining_philosophers_problem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chymyst/chymyst-core/8355442b8effdf2fe8bc9c66fe02cb73eb05e71d/docs/An_illustration_of_the_dining_philosophers_problem.png -------------------------------------------------------------------------------- /docs/Boyle_Self-Flowing_Flask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chymyst/chymyst-core/8355442b8effdf2fe8bc9c66fe02cb73eb05e71d/docs/Boyle_Self-Flowing_Flask.png -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | chemist.io -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # `Chymyst`: declarative concurrency in Scala 4 | 5 | `Chymyst` is a framework for declarative concurrency in functional programming 6 | implementing the **Chemical Machine** paradigm, also known in the academic world as [Join Calculus](https://en.wikipedia.org/wiki/Join-calculus). 7 | This concurrency paradigm has the same expressive power as CSP ([Communicating Sequential Processes](https://en.wikipedia.org/wiki/Communicating_sequential_processes)), 8 | the [Pi calculus](https://en.wikipedia.org/wiki/%CE%A0-calculus), and [the Actor model](https://en.wikipedia.org/wiki/Actor_model), 9 | but is easier to use and reason about, more high-level, and more declarative. 10 | 11 | [`Chymyst Core`](https://github.com/Chymyst/chymyst-core) is a library that implements the high-level concurrency primitives as a domain-specific language in Scala. 12 | [`Chymyst`](https://github.com/Chymyst/Chymyst) is a framework-in-planning that will build upon `Chymyst Core` and bring declarative concurrency to practical applications. 13 | 14 | The code of `Chymyst Core` is a clean-room implementation and is not based on previous Join Calculus implementations, such as [`ScalaJoin` by He Jiansen](https://github.com/Jiansen/ScalaJoin) (2011) and [`ScalaJoins` by Philipp Haller](http://lampwww.epfl.ch/~phaller/joins/index.html) (2008). 15 | The algorithm is similar to that used in my earlier Join Calculus prototypes, [Objective-C/iOS](https://github.com/winitzki/CocoaJoin) and [Java/Android](https://github.com/winitzki/AndroJoin). 16 | 17 | # [The _Concurrency in Reactions_ tutorial book: table of contents](chymyst00.md) 18 | 19 | ## Overview of `Chymyst` and the Chemical Machine paradigm 20 | 21 | ### [Concurrency in Reactions: Get started with this extensive tutorial book](https://winitzki.gitbooks.io/concurrency-in-reactions-declarative-multicore-in/content/) 22 | 23 | #### [From actors to reactions: a guide for those familiar with the Actor model](https://chymyst.github.io/chymyst-core/chymyst-actor.html) 24 | 25 | #### [A "Hello, world" project](https://github.com/Chymyst/helloworld) 26 | 27 | #### Presentations on `Chymyst` and on the Chemical Machine programming paradigm 28 | 29 | See [this YouTube channel](https://www.youtube.com/playlist?list=PLcoadSpY7rHWh23clCzL0SuJ4fAtzVzTb) for tutorials and presentations. 30 | 31 | Oct. 16, 2017: Talk given at the [Scala Bay meetup](https://www.meetup.com/Scala-Bay/events/243931229): 32 | 33 | - [Talk slides with audio](https://youtu.be/Iu2KBYNF-6M) 34 | - See also the [talk slides (PDF)](https://github.com/winitzki/talks/blob/master/join_calculus/join_calculus_2017_Scala_Bay.pdf) and the [code examples for the talk](https://github.com/Chymyst/jc-talk-2017-examples). 35 | 36 | July 2017: [Draft of an academic paper](https://github.com/winitzki/talks/blob/master/join-calculus-paper/join-calculus-paper.pdf) describing Chymyst and its approach to join calculus 37 | 38 | Nov. 11, 2016: Talk given at [Scalæ by the Bay 2016](https://scalaebythebay2016.sched.org/event/7iU2/concurrent-join-calculus-in-scala): 39 | 40 | - [Video presentation of early version of `Chymyst Core`, then called `JoinRun`](https://www.youtube.com/watch?v=jawyHGjUfBU) 41 | - See also the [talk slides revised for the current syntax](https://github.com/winitzki/talks/raw/master/join_calculus/join_calculus_2016_revised.pdf). 42 | 43 | ### [Main features of the chemical machine](chymyst_features.md) 44 | 45 | #### [Comparison of the chemical machine vs. academic Join Calculus](chymyst_vs_jc.md#comparison-chemical-machine-vs-academic-join-calculus) 46 | 47 | #### [Comparison of the chemical machine vs. the Actor model](chymyst_vs_jc.md#comparison-chemical-machine-vs-actor-model) 48 | 49 | #### [Comparison of the chemical machine vs. the coroutines / channels approach (CSP)](chymyst_vs_jc.md#comparison-chemical-machine-vs-csp) 50 | 51 | #### [Technical documentation for `Chymyst Core`](chymyst-core.md) 52 | 53 | #### [Source code repository for `Chymyst Core`](https://github.com/Chymyst/chymyst-core) 54 | 55 | 56 | ### [Version history and roadmap](roadmap.md) 57 | 58 | 59 | ## Status of the project 60 | 61 | The `Chymyst Core` library is in alpha pre-release, with very few API changes envisioned for the future. 62 | 63 | The semantics of the chemical machine (restricted to single-host, multicore computations) is fully implemented and tested on many nontrivial examples. 64 | 65 | The library JAR is [published to Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cchymyst-core). 66 | 67 | Extensive tutorial and usage documentation is available. 68 | 69 | Unit tests (more than 500 at the moment) exercise all aspects of the DSL provided by `Chymyst`. 70 | Test coverage is [100% according to codecov.io](https://codecov.io/gh/Chymyst/chymyst-core?branch=master). 71 | 72 | Test suites also complement the tutorial book and include examples such as barriers, asynchronous and synchronous rendezvous, local critical sections, parallel “or”, parallel map/reduce, parallel merge-sort, “dining philosophers”, as well as many other concurrency algorithms. 73 | 74 | Performance benchmarks indicate that `Chymyst Core` can schedule about 100,000 reactions per second per CPU core, and the performance bottleneck is in submitting jobs to threads (a distant second bottleneck is pattern-matching in the internals of the library). 75 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [Preface](chymyst-preface.md) 4 | * [Chapter 0: Quick start in a REPL](chymyst-quick.md) 5 | * [Chapter 1: The chemical machine paradigm](chymyst01.md) 6 | * [Chapter 2: Readers/Writers, Map/Reduce, and Merge-Sort](chymyst02.md) 7 | * [Chapter 3: Blocking and non-blocking molecules](chymyst03.md) 8 | * [Chapter 4: Molecules and emitters, in depth](chymyst04.md) 9 | * [Chapter 5: Reaction constructors](chymyst05.md) 10 | * [Chapter 6: Conceptual overview of concurrency](concurrency.md) 11 | * [Nontechnical version](concurrency-nontechnical.md) 12 | * [Chapter 7: Concurrency patterns](chymyst07.md) 13 | * [Chapter 8: Advanced examples](chymyst08.md) 14 | * [Chapter 9: Game of Life](chymyst_game_of_life.md) 15 | 16 | * [Appendix A: From actors to reactions: The chemical machine explained through the Actor model](chymyst-actor.md) 17 | * [Appendix B: Other work on Join Calculus](other_work.md) 18 | -------------------------------------------------------------------------------- /docs/academic_join_calculus_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chymyst/chymyst-core/8355442b8effdf2fe8bc9c66fe02cb73eb05e71d/docs/academic_join_calculus_2.png -------------------------------------------------------------------------------- /docs/chymyst-preface.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Preface 4 | 5 | `Chymyst` is a library and a framework for declarative concurrent programming. 6 | 7 | It follows the **chemical machine** paradigm (known in the academic world as “Join Calculus”) and is implemented as an embedded DSL (domain-specific language) in Scala. 8 | 9 | The goal of this book is to explain the chemical machine paradigm and to show examples of implementing concurrent programs in `Chymyst`. 10 | To follow the examples, the reader needs a basic familiarity with the `Scala` programming language. 11 | 12 | ## Source code 13 | 14 | The source code repository for `Chymyst Core` is at [https://github.com/Chymyst/chymyst-core](https://github.com/Chymyst/chymyst-core). 15 | 16 | Although this book focuses on using `Chymyst` in the Scala programming language, 17 | one can straightforwardly embed the chemical machine as a library on top of any programming language that has threads and semaphores. 18 | The main concepts and techniques of the chemical machine paradigm are independent of the chosen programming language. 19 | However, a purely functional language is a better fit for the chemical machine. 20 | 21 | ## Dedication to Robert Boyle (1626-1691) 22 | 23 | [![Robert Boyle's self-flowing flask](Boyle_Self-Flowing_Flask.png)](https://en.wikipedia.org/wiki/Robert_Boyle#/media/File:Boyle%27sSelfFlowingFlask.png) 24 | 25 | This drawing is by [Robert Boyle](https://en.wikipedia.org/wiki/Robert_Boyle), who was one of the founders of the science of chemistry. 26 | In 1661 he published a treatise titled [_“The Sceptical Chymist”_](https://upload.wikimedia.org/wikipedia/commons/thumb/d/db/Sceptical_chymist_1661_Boyle_Title_page_AQ18_%283%29.jpg/220px-Sceptical_chymist_1661_Boyle_Title_page_AQ18_%283%29.jpg), from which the `Chymyst` framework derives its name. 27 | -------------------------------------------------------------------------------- /docs/chymyst00.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Concurrency in Reactions: Declarative Scala multiprocessing with `Chymyst` 4 | 5 | (c) by Sergei Winitzki, 2016-2018 6 | 7 | [Preface](chymyst-preface.md) 8 | 9 | [Chapter 0: Quick start in a REPL](chymyst-quick.md) 10 | 11 | [Chapter 1: The chemical machine paradigm](chymyst01.md) 12 | 13 | [Chapter 2: Readers/Writers, Map/Reduce, and Merge-Sort](chymyst02.md) 14 | 15 | [Chapter 3: Blocking and non-blocking molecules](chymyst03.md) 16 | 17 | [Chapter 4: Molecules and emitters, in depth](chymyst04.md) 18 | 19 | [Chapter 5: Advanced features: reaction constructors and pipelining](chymyst05.md) 20 | 21 | [Chapter 6: Conceptual overview of concurrency](concurrency.md) / [Nontechnical version](concurrency-nontechnical.md) 22 | 23 | [Chapter 7: Concurrency patterns](chymyst07.md) 24 | 25 | [Chapter 8: Advanced examples](chymyst08.md) 26 | 27 | [Chapter 9: Game of Life](chymyst_game_of_life.md) 28 | 29 | [Appendix A: From actors to reactions: The chemical machine explained through the Actor model](chymyst-actor.md) 30 | 31 | [Appendix B: Other work on Join Calculus](other_work.md) 32 | -------------------------------------------------------------------------------- /docs/chymyst09.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Generators and coroutines 4 | 5 | TODO 6 | 7 | # Timers and timeouts 8 | 9 | ## Timers in the chemical machine 10 | 11 | The basic function of a **timer** is to schedule some computation at a fixed time in the future. 12 | In the chemical machine, any computation is a reaction, and reactions can be started in only one way — by emitting some molecules. 13 | 14 | However, the chemical machine does not provide a means of delaying a reaction when all input molecules are present. 15 | Doing so would be inconsistent with the semantics of chemical reactions, 16 | because it is impossible to know or to predict when the required molecules are going to be emitted and arrive at a reaction site. 17 | 18 | So, a "chemical timer" would be a facility for emitting a molecule after a fixed time delay. 19 | 20 | How can we implement this in the chemical machine? 21 | For example, what code needs to be written if a reaction needs to emit its output molecules after a 10 second delay? 22 | 23 | This functionality can be of course implemented in a simple way if we are willing to block a thread, for example: 24 | 25 | ```scala 26 | go { case a(x) + ... ⇒ Thread.sleep(1000); b(x) } 27 | 28 | ``` 29 | 30 | In this way, we add a `delaying` wrapper to a molecule emitter, transforming it automatically into an emitter that works with a given time delay. 31 | This wrapper can be made universal and encapsulated into a function, 32 | 33 | ```scala 34 | def delaying[A](delayMs: Long): M[A] = { 35 | val delayed = m[(A, M[A])] 36 | site(go { case delayed((x, mx)) ⇒ 37 | Thread.sleep(delayMs) 38 | mx(x) 39 | }) 40 | delayed 41 | } 42 | 43 | ``` 44 | 45 | Instead of emitting a non-blocking molecule `a(123)`, we can now emit `delaying[Int](1000)((a, 123))`. 46 | 47 | Alternatively, the wrapping can be performed by creating a new `Reaction` value, and using it while constructing reaction sites like this: 48 | 49 | ```scala 50 | def delayingReaction[A](emitter: M[A], delayMs: Long): (M[A], Reaction) = { 51 | val delayed = m[A] 52 | val reaction = go { case delayed(x) ⇒ 53 | BlockingIdle(Thread.sleep(delayMs)) 54 | emitter(x) 55 | } 56 | (delayed, reaction) 57 | } 58 | 59 | // User code: 60 | val a = m[Int] 61 | val b = m[Int] 62 | val (aDelayed, aDelayedR) = delayingReaction(a, 1000) 63 | site( 64 | go { case a(x) + b(y) ⇒ ... aDelayed(x+y) }, 65 | aDelayedR // Add the reaction to the reaction site. 66 | ) 67 | 68 | ``` 69 | 70 | However, in many cases we would like to avoid blocking a thread. 71 | 72 | A more efficient approach is to allocate statically a Java timer that will provide delayed emissions for a given molecule (or for a set of molecules). 73 | However, this timer now needs to be accessible to any reaction (at any reaction site!) that might need to schedule a delayed emission of those molecules. 74 | 75 | Another solution is to allocate a timer automatically within each reaction site, and to make that timer accessible through the molecule emitters. 76 | This requires special support for timers in the `Chymyst` implementation; it would not be hard to add this support if there is a compelling use case. 77 | 78 | Without such support, we must maintain Java timers in the application code. 79 | Constructing a delayed emitter is then no different than implementing a delay on any side effect: 80 | 81 | ```scala 82 | import java.util.{Timer, TimerTask} 83 | val delayTimer = new Timer() // This timer is shared by all calls to `delay()`. 84 | def delay[A](delayMs: Long)(effect: ⇒ Unit): Unit = 85 | delayTimer.schedule( 86 | new TimerTask { override def run(): Unit = effect }, 87 | delayMs 88 | ) 89 | } 90 | 91 | // User code: 92 | val a = m[Int] 93 | val b = m[Int] 94 | site( 95 | go { case a(x) + b(y) ⇒ delay(1000) { b(x + y) } } 96 | ) 97 | 98 | ``` 99 | 100 | ## Timeouts and cancellation 101 | 102 | In the context of the chemical machine, a timeout means that some action is taken when a given molecule is not consumed (or a given reaction does not start) within a specified time. 103 | When a timeout happens, we would typically need to cancel the reaction that would otherwise have started. 104 | 105 | We can use a time-delayed emitter to signal a timeout. 106 | It remains to implement the cancellation of a reaction. 107 | 108 | To derive the implementation for this functionality in `Chymyst`, we need to reason about what molecules are present when we need to cancel a pending reaction. 109 | Suppose a given reaction such as 110 | 111 | ```scala 112 | go { case a(x) + b(y) ⇒ ... } 113 | 114 | ``` 115 | 116 | consumes molecules `a()` and `b()`, and we are to cancel that reaction when some molecule `c()` is emitted. 117 | At that time, molecules `a()` and `b()` may or may not be present. 118 | The only way to prevent the reaction from starting without changing the reaction's code is to remove `a()` or `b()` from the reaction site. 119 | To do that, we could define an additional reaction that consumes, say, `a()` and `c()`, or `b()` and `c()`, and emits nothing: 120 | 121 | ```scala 122 | go { case a(_) + c(_) ⇒ } 123 | go { case b(_) + c(_) ⇒ } 124 | 125 | ``` 126 | 127 | These two reactions will need to be defined at the same reaction site as the original reaction. 128 | 129 | As we emit `c()`, one of these two reactions would start if `a()` is already present but `b()` is not yet present, or vice versa. 130 | If, however, both `a()` and `b()` are already emitted before `c()`, we are not guaranteed that we can cancel the reaction between `a()` and `b()`. 131 | 132 | There are two other ways of cancelling a reaction, but both require modifying the reaction's code. 133 | One way is to add a guard condition involving a mutable flag: 134 | 135 | ```scala 136 | @volatile var enabled: Boolean = true 137 | 138 | site( 139 | go { case a(x) + b(y) if enabled ⇒ ... } 140 | ) 141 | 142 | ``` 143 | 144 | In this way, we can cancel a reaction without actually removing its input molecules, by setting `enabled = false`. 145 | We can also reset it back to true if we want to enable the reaction again. 146 | Of course, we have no control over _when_ the re-enabled reaction will start. 147 | 148 | Another way to implement cancellation is to put the flag on the value of, say, the molecule `a()`, and include a condition into the reaction's body: 149 | 150 | ```scala 151 | site( 152 | go { case a(x) + b(y) ⇒ 153 | if (x) do_something() else stop() 154 | } 155 | ) 156 | 157 | ``` 158 | 159 | This implementation would be suitable if we need to run this reaction many times with different input molecules, but stop it at some point. 160 | To stop the reaction, we emit `a(false)`. 161 | The function we called `stop()` would perform some cleanup or, at any rate, would not call `do_something()` as this reaction normally does. 162 | 163 | Since the molecule `a()` is pipelined in this program, each emitted copy of `a()` will be kept in a linear queue and consumed in the order emitted. 164 | So, we can be reasonably certain that the reaction will stop as soon as the emitted copy `a(false)` is consumed. 165 | -------------------------------------------------------------------------------- /docs/chymyst_vs_jc.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Comparison: chemical machine vs. academic Join Calculus 4 | 5 | In talking about `Chymyst`, I follow the chemical machine metaphor and terminology, which differs from the terminology usually found in academic papers on JC. 6 | Here is a dictionary: 7 | 8 | | Chemical machine | Academic Join Calculus | `Chymyst` code | 9 | |---|---|---| 10 | | input molecule | message on a channel | `case a(123) => ...` _// pattern-matching_ | 11 | | molecule emitter | channel name | `val a : M[Int]` | 12 | | blocking emitter | synchronous channel | `val q : B[Unit, Int]` | 13 | | reaction | process | `val r1 = go { case a(x) + ... => ... }` | 14 | | emitting a molecule | sending a message | `a(123)` _// side effect_ | 15 | | emitting a blocking molecule | sending a synchronous message | `q()` _// returns_ `Int` | 16 | | reaction site | join definition | `site(r1, r2, ...)` | 17 | 18 | As another comparison, here is some code in academic Join Calculus notation, taken from [this tutorial](http://research.microsoft.com/en-us/um/people/fournet/papers/join-tutorial.pdf): 19 | 20 | def newVar(v0) def put(w) etc. 21 | 22 | This code creates a shared value container `val` with synchronized single access. 23 | 24 | The equivalent `Chymyst` code looks like this: 25 | 26 | ```scala 27 | def newVar[T](v0: T): (B[T, Unit], B[Unit, T]) = { 28 | val put = b[T, Unit] 29 | val get = b[Unit, T] 30 | val vl = m[T] // Will use the name `vl` since `val` is a Scala keyword. 31 | 32 | site( 33 | go { case put(w, ret) + vl(v) => vl(w); ret() }, 34 | go { case get(_, ret) + vl(v) => vl(v); ret(v) } 35 | ) 36 | vl(v0) 37 | 38 | (put, get) 39 | } 40 | 41 | ``` 42 | 43 | ### Extensions to Join Calculus 44 | 45 | `Chymyst` implements significantly fewer restrictions than other versions of Join Calculus: 46 | 47 | - reactions may have arbitrary guard conditions on molecule values 48 | - reactions may consume several molecules of the same sort (“nonlinear input patterns”) 49 | - reactions may consume an arbitrary number of blocking input molecules, and each blocking input molecule can receive its own reply (“nonlinear reply patterns”) 50 | - reactions are values — user's code can construct and define chemical laws incrementally at run time 51 | 52 | `Chymyst` also implements some additional features that are important for practical applications but not supported by other versions of Join Calculus: 53 | 54 | - timeouts on blocking calls 55 | - being able to terminate a reaction site, in order to make the program stop 56 | - explicit thread pools for controlling latency and throughput of concurrent computations 57 | 58 | ## Comparison: chemical machine vs. Actor model 59 | 60 | Chemical machine programming is similar in some aspects to the well-known Actor model (e.g. as implemented by the [Akka library](https://github.com/akka/akka)). 61 | 62 | ### Similarities 63 | 64 | | Chemical machine | Actor model | 65 | |---|---| 66 | | molecules carry values | messages carry values | 67 | | reactions wait to consume certain molecules | actors wait to receive certain messages | 68 | | synchronization is implicit in molecule emission | synchronization is implicit in message-passing | 69 | | reaction starts when input molecules are available | actor starts running when a message is received | 70 | | reactions can define new reactions and emit new input molecules for them | actors can create new actors and send messages to them | 71 | 72 | ### Differences 73 | 74 | | Chemical machine | Actor model | 75 | |---|---| 76 | | several concurrent reactions start automatically whenever several input molecules are available | a desired number of concurrent actors must be created and managed manually | 77 | | the user's code only manipulates molecules | the user's code must manipulate explicit references to actors as well as messages | 78 | | reactions may wait for (and consume) several input molecules at once | actors wait for (and consume) only one input message at a time | 79 | | reactions are immutable and stateless; all data is stored on molecules | actors can mutate (“become another actor”); actors may carry mutable state | 80 | | molecules are held in an unordered bag and may be processed in random order | messages are held in an ordered queue (mailbox) and are processed in the order received | 81 | | molecule data is statically typed | message data is untyped (but not if using [Akka Typed](https://doc.akka.io/docs/akka/2.5/typed/index.html)) | 82 | 83 | ## Comparison: chemical machine vs. CSP 84 | 85 | CSP (Communicating Sequential Processes) is another approach to declarative concurrency, used today in the [Go programming language](https://golang.org/). 86 | 87 | Similarities: 88 | 89 | The channels of CSP are similar to blocking molecules: sending a message will block until a process can be started that consumes the message and replies with a value. 90 | 91 | Differences: 92 | 93 | The chemical machine admits only one reply to a blocking channel; CSP can open a channel and send many messages to it. 94 | 95 | The chemical machine will start processes automatically and concurrently whenever input molecules are available. 96 | In CSP, the user needs to create and manage new threads manually. 97 | 98 | JC has non-blocking channels as a primitive construct. 99 | In CSP, non-blocking channels need to be simulated by [additional user code](https://gobyexample.com/non-blocking-channel-operations). 100 | -------------------------------------------------------------------------------- /docs/concurrency-in-reactions-declarative-multicore-in-Scala.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chymyst/chymyst-core/8355442b8effdf2fe8bc9c66fe02cb73eb05e71d/docs/concurrency-in-reactions-declarative-multicore-in-Scala.epub -------------------------------------------------------------------------------- /docs/concurrency-in-reactions-declarative-multicore-in-Scala.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chymyst/chymyst-core/8355442b8effdf2fe8bc9c66fe02cb73eb05e71d/docs/concurrency-in-reactions-declarative-multicore-in-Scala.pdf -------------------------------------------------------------------------------- /docs/counter-incr-decr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | background 5 | 6 | 7 | 8 | 9 | 10 | 11 | Layer 1 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | n 40 | n+1 41 | n 42 | counter 43 | incr 44 | counter 45 | 46 | counter 47 | decr 48 | n–1 49 | counter 50 | 51 | -------------------------------------------------------------------------------- /docs/counter-multiple-molecules-after-reaction.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | background 6 | 7 | 8 | 9 | 10 | 11 | 12 | Layer 1 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | n+2 28 | incr 29 | counter 30 | counter 31 | 32 | 33 | incr 34 | incr 35 | 36 | 37 | 38 | 39 | n+2 40 | counter 41 | 42 | 43 | 44 | 45 | 46 | 47 | n+1 48 | 49 | -------------------------------------------------------------------------------- /docs/counter-multiple-reactions.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | background 6 | 7 | 8 | 9 | 10 | 11 | 12 | Layer 1 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | n+1 40 | n 41 | incr 42 | counter 43 | 44 | counter 45 | decr 46 | n–1 47 | counter 48 | 49 | -------------------------------------------------------------------------------- /docs/fork-join-weights.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | background 5 | 6 | 7 | 8 | Layer 1 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 1 18 | 1/3 19 | 1/3 20 | 1/3 21 | 1/6 22 | 1/6 23 | 1/9 24 | 1/9 25 | 1/9 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | partial result 34 | 35 | -------------------------------------------------------------------------------- /docs/reactions1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | background 6 | 7 | 8 | 9 | 10 | 11 | 12 | Layer 1 13 | 14 | a 15 | 16 | a 17 | 18 | b 19 | 20 | a 21 | 22 | c 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/tables.css: -------------------------------------------------------------------------------- 1 | /* markdown tables */ 2 | table { 3 | border:#ccc 1px solid; 4 | border-radius:3px; 5 | border-collapse: separate; 6 | margin: 1em; 7 | } 8 | table th { 9 | padding:21px 25px 22px 25px; 10 | border:1px solid #d0d0d0; 11 | border-bottom:1px solid #808080; 12 | } 13 | table tr { 14 | padding-left:20px; 15 | } 16 | table td { 17 | padding:18px; 18 | /* border-top: 1px solid #ffffff; */ 19 | border:1px solid #d0d0d0; 20 | } 21 | 22 | /* markdown code */ 23 | 24 | code { 25 | font-size: inherit; 26 | } 27 | 28 | .highlighter-rouge { 29 | background-color: #f8f8f8; 30 | } 31 | 32 | h1, h2, h3, h4, h5, h6 { 33 | margin-top: 1em; 34 | margin-bottom: 0.6em; 35 | } 36 | 37 | .container { 38 | width: 80%; 39 | } 40 | 41 | ul, ol { 42 | padding-left: 40px; 43 | margin-bottom: 12px; 44 | } 45 | 46 | .highlight { 47 | margin-bottom: 12px; 48 | padding-top: 12px; 49 | padding-left: 12px; 50 | } 51 | 52 | body blockquote { 53 | padding: 0 1em; 54 | color: #777; 55 | border-left: 0.25em solid #ddd; 56 | margin: 0; 57 | } 58 | -------------------------------------------------------------------------------- /project/assembly.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.4") 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 0.13.16 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | logLevel := Level.Warn 2 | addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.5.2") 3 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.0") 4 | addSbtPlugin("org.wartremover" % "sbt-wartremover" % "2.0.3") 5 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "1.1") 6 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") 7 | -------------------------------------------------------------------------------- /sonatype.sbt: -------------------------------------------------------------------------------- 1 | // Following instructions from https://github.com/xerial/sbt-sonatype 2 | // see https://issues.sonatype.org/browse/OSSRH-27720 3 | pomExtra in Global := 4 | 2016 5 | 6 | git@github.com:Chymyst/chymyst-core.git 7 | scm:git:git@github.com:Chymyst/chymyst-core.git 8 | 9 | 10 | 11 | winitzki 12 | Sergei Winitzki 13 | https://sites.google.com/site/winitzki 14 | 15 | 16 | phderome 17 | Philippe Derome 18 | https://ca.linkedin.com/in/philderome 19 | 20 | 21 | 22 | sonatypeProfileName := "winitzki" 23 | --------------------------------------------------------------------------------