├── .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 | [](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 |
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 |
--------------------------------------------------------------------------------
/docs/counter-multiple-molecules-after-reaction.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/counter-multiple-reactions.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/fork-join-weights.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/reactions1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------