Skip to content

Commit c284275

Browse files
author
Adriano Santos
committed
feat: added android specific scheduler and SensorActor
1 parent 7b2343b commit c284275

File tree

15 files changed

+335
-40
lines changed

15 files changed

+335
-40
lines changed

synapsys-android-extensions/build.gradle.kts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree.Companion.instrumentedTest
2+
import org.jetbrains.kotlin.gradle.plugin.KotlinTargetHierarchy.SourceSetTree.Companion.instrumentedTest
3+
14
plugins {
25
id("com.android.library")
36
id("org.jetbrains.kotlin.multiplatform")
@@ -9,6 +12,7 @@ android {
912

1013
defaultConfig {
1114
minSdk = 21
15+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
1216
}
1317

1418
compileOptions {
@@ -28,6 +32,13 @@ kotlin {
2832
jvmTarget = "17"
2933
}
3034
}
35+
36+
/*instrumentedTest {
37+
dependencies {
38+
implementation("androidx.test:core:1.5.0")
39+
implementation("org.robolectric:robolectric:4.9")
40+
}
41+
}*/
3142
}
3243

3344
jvmToolchain(17)
@@ -45,6 +56,24 @@ kotlin {
4556
implementation("androidx.room:room-runtime")
4657
implementation("androidx.room:room-ktx")
4758
implementation("androidx.sqlite:sqlite-ktx")
59+
implementation("org.tinylog:slf4j-tinylog:2.6.2")
60+
implementation("org.tinylog:tinylog-impl:2.6.2")
61+
}
62+
}
63+
64+
val androidUnitTest by getting {
65+
dependencies {
66+
implementation(kotlin("test"))
67+
implementation("junit:junit:4.13.2")
68+
implementation("org.robolectric:robolectric:4.11.1")
69+
implementation("androidx.test:core:1.6.1")
70+
implementation("androidx.test.ext:junit:1.2.1")
71+
implementation("androidx.test:runner:1.6.2")
72+
implementation("androidx.test:rules:1.6.1")
73+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
74+
implementation("org.slf4j:slf4j-api:2.0.16")
75+
implementation("org.tinylog:slf4j-tinylog:2.6.2")
76+
implementation("org.tinylog:tinylog-impl:2.6.2")
4877
}
4978
}
5079
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package io.eigr.synapsys.extensions.android.sensors
2+
3+
import io.eigr.synapsys.core.actor.ActorSystem
4+
import io.eigr.synapsys.core.actor.Config
5+
import io.eigr.synapsys.core.actor.Context
6+
import io.eigr.synapsys.core.internals.scheduler.ActorExecutor
7+
import io.eigr.synapsys.core.internals.scheduler.Scheduler
8+
import io.eigr.synapsys.core.internals.scheduler.WorkingStealingScheduler
9+
import io.eigr.synapsys.extensions.android.sensors.actor.SensorActor
10+
import io.eigr.synapsys.extensions.android.sensors.events.SensorData
11+
12+
import kotlinx.coroutines.CoroutineScope
13+
import kotlinx.coroutines.Dispatchers
14+
import kotlinx.coroutines.launch
15+
16+
class AndroidScheduler(config: Config) : Scheduler {
17+
private val sensorScope = CoroutineScope(Dispatchers.IO)
18+
private val workingStealingScheduler = WorkingStealingScheduler(config.maxReductions)
19+
private val sensorActors = mutableMapOf<String, ActorExecutor<*>>()
20+
private lateinit var _system: ActorSystem
21+
22+
override fun enqueue(actorExecutor: ActorExecutor<*>) {
23+
// Enqueue the actor executor on the sensor scope if instance of actor is SensorActor
24+
when (val actor = actorExecutor.actor.getActor<Any, Any, Any>()) {
25+
is SensorActor<*, *> -> {
26+
sensorActors[actorExecutor.actor.id] = actorExecutor
27+
handleSensorActor(actor)
28+
}
29+
else -> workingStealingScheduler.enqueue(actorExecutor)
30+
}
31+
}
32+
33+
override fun removeActor(actorId: String): Boolean {
34+
if (sensorActors.containsKey(actorId)) {
35+
sensorActors.remove(actorId)
36+
return true
37+
}
38+
39+
return workingStealingScheduler.removeActor(actorId)
40+
}
41+
42+
override fun cleanAllWorkerQueues() {
43+
sensorActors.clear()
44+
workingStealingScheduler.cleanAllWorkerQueues()
45+
}
46+
47+
override fun setSystem(actorSystem: ActorSystem) {
48+
this._system = actorSystem
49+
}
50+
51+
@Suppress("UNCHECKED_CAST")
52+
private fun <S : Any> handleSensorActor(sensorActor: SensorActor<S, *>) {
53+
val ctx = Context(
54+
internalState = sensorActor.initialState,
55+
actorSystem = _system
56+
)
57+
58+
sensorScope.launch {
59+
(sensorActor as SensorActor<S, SensorData>).onStart(ctx)
60+
}
61+
}
62+
}

synapsys-android-extensions/src/main/kotlin/io/eigr/synapsys/extensions/android/sensors/actor/SensorActor.kt

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import io.eigr.synapsys.core.actor.ActorPointer
99
import io.eigr.synapsys.extensions.android.sensors.events.SensorData
1010
import io.eigr.synapsys.extensions.android.sensors.internals.ActorHandler
1111
import kotlinx.coroutines.runBlocking
12+
import org.slf4j.LoggerFactory
1213
import android.content.Context as AndroidContext
1314
import io.eigr.synapsys.core.actor.Context as ActorContext
1415

@@ -24,16 +25,20 @@ open class SensorActor<S : Any, M : SensorData>(
2425
),
2526
SensorEventListener {
2627

28+
private val log = LoggerFactory.getLogger(SensorActor::class.java)
29+
2730
private val sensorManager by lazy {
2831
androidContext.getSystemService(AndroidContext.SENSOR_SERVICE) as SensorManager
2932
}
3033

31-
private lateinit var targetActor: ActorPointer<*>
34+
private var targetActor: ActorPointer<*>? = null
3235

3336
override fun onStart(ctx: ActorContext<S>): ActorContext<S> {
37+
log.info("Registering sensor {}", sensorType)
3438
targetActor = ctx.system.actorOf(
3539
id = "processor-$id",
36-
initialState = initialState!!) {id, state -> ActorHandler(id, state) }
40+
initialState = initialState!!
41+
) { id, state -> ActorHandler<S, M>(id, state).apply { parentActor = this@SensorActor } }
3742

3843
val sensor = sensorManager.getDefaultSensor(sensorType)
3944
sensor?.let {
@@ -50,7 +55,7 @@ open class SensorActor<S : Any, M : SensorData>(
5055
}
5156

5257
override fun onReceive(message: M, ctx: ActorContext<S>): Pair<ActorContext<S>, Unit> {
53-
return ctx to Unit
58+
return this.onReceive(message, ctx)
5459
}
5560

5661
@Suppress("UNCHECKED_CAST")
@@ -82,7 +87,9 @@ open class SensorActor<S : Any, M : SensorData>(
8287
)
8388
}
8489

85-
runBlocking { targetActor.send(message as M) }
90+
runBlocking {
91+
targetActor?.send(message as M)
92+
}
8693
}
8794

8895
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}

synapsys-android-extensions/src/main/kotlin/io/eigr/synapsys/extensions/android/sensors/internals/ActorHandler.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,7 @@ import io.eigr.synapsys.extensions.android.sensors.events.SensorData
77

88
class ActorHandler<S : Any, M : SensorData>(id: String?, initialState: S) : Actor<S, M, Unit>(id, initialState) {
99

10-
private lateinit var parentActor: SensorActor<S,M>
11-
12-
internal fun setParentActor(parent: SensorActor<S,M>) {
13-
this.parentActor = parent
14-
}
10+
internal lateinit var parentActor: SensorActor<S,M>
1511

1612
override fun onReceive(message: M, ctx: Context<S>): Pair<Context<S>, Unit> {
1713
return parentActor.onReceive(message, ctx)

synapsys-android-extensions/src/main/resources/logback.xml

Lines changed: 0 additions & 13 deletions
This file was deleted.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
tinylog.writer = logcat
2+
tinylog.level = info
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package io.eigr.synapsys.extensions.android.sensors
2+
3+
import android.content.Context
4+
import android.hardware.Sensor
5+
import android.hardware.Sensor.TYPE_ACCELEROMETER
6+
import android.hardware.SensorEvent
7+
import android.hardware.SensorManager
8+
import androidx.test.core.app.ApplicationProvider
9+
import io.eigr.synapsys.core.actor.Actor
10+
import io.eigr.synapsys.core.actor.ActorSystem
11+
import io.eigr.synapsys.core.actor.Config
12+
import io.eigr.synapsys.core.actor.RestartStrategy
13+
import io.eigr.synapsys.core.actor.Supervisor
14+
import io.eigr.synapsys.core.actor.SupervisorStrategy
15+
import io.eigr.synapsys.core.internals.BaseActorAdapter
16+
import io.eigr.synapsys.core.internals.mailbox.Mailbox
17+
import io.eigr.synapsys.core.internals.mailbox.MailboxAbstractQueue
18+
import io.eigr.synapsys.core.internals.mailbox.transport.ChannelMailbox
19+
import io.eigr.synapsys.core.internals.scheduler.ActorExecutor
20+
import io.eigr.synapsys.extensions.android.sensors.actor.SensorActor
21+
import io.eigr.synapsys.extensions.android.sensors.events.SensorData
22+
import junit.framework.TestCase.assertFalse
23+
import junit.framework.TestCase.assertTrue
24+
import kotlinx.coroutines.delay
25+
import kotlinx.coroutines.runBlocking
26+
import kotlinx.coroutines.test.runTest
27+
import org.junit.After
28+
import org.junit.Before
29+
import org.junit.Test
30+
import org.junit.runner.RunWith
31+
import org.robolectric.RobolectricTestRunner
32+
import org.robolectric.Shadows
33+
import org.robolectric.shadows.ShadowSensor
34+
import org.robolectric.shadows.ShadowSensorManager
35+
import org.slf4j.LoggerFactory
36+
import io.eigr.synapsys.core.actor.Context as ActorContext
37+
import org.robolectric.annotation.Config as RoboConfig
38+
39+
@RunWith(RobolectricTestRunner::class)
40+
@RoboConfig(manifest = RoboConfig.NONE)
41+
class AndroidSchedulerIntegrationTest {
42+
private lateinit var system: ActorSystem
43+
private val config = Config(maxReductions = 100)
44+
private val androidContext = ApplicationProvider.getApplicationContext<Context>()
45+
private val scheduler = AndroidScheduler(config)
46+
private lateinit var shadowSensorManager: ShadowSensorManager
47+
48+
@Before
49+
fun setup() {
50+
system = ActorSystem.create(config, scheduler)
51+
shadowSensorManager = Shadows.shadowOf(
52+
androidContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager
53+
)
54+
}
55+
56+
@After
57+
fun tearDown() {
58+
scheduler.cleanAllWorkerQueues()
59+
}
60+
61+
@Test
62+
fun `enqueue should add SensorActor to sensorActors map`() {
63+
val sensorActor = TestSensorActor(
64+
id = "sensor-1",
65+
initialState = State(0),
66+
context = androidContext
67+
)
68+
69+
scheduler.enqueue(createExecutor(sensorActor))
70+
71+
sensorActor.id?.let { scheduler.removeActor(it) }?.let { assertTrue(it) }
72+
}
73+
74+
@Test
75+
fun `enqueue should delegate non-sensor actors to working stealing scheduler`() {
76+
val regularActor = RegularActor("regular-1", 0)
77+
val executor = createExecutor(regularActor)
78+
79+
scheduler.enqueue(executor)
80+
81+
regularActor.id?.let { scheduler.removeActor(it) }?.let { assertTrue(it) }
82+
}
83+
84+
@Test
85+
fun `cleanAllWorkerQueues should remove all actors`() {
86+
val sensorExecutor = createExecutor(
87+
TestSensorActor(id = "sensor-1", initialState = State(0), context = androidContext)
88+
)
89+
90+
val regularExecutor = createExecutor(RegularActor(id = "regular-1", initialState = 0))
91+
92+
scheduler.enqueue(sensorExecutor)
93+
scheduler.enqueue(regularExecutor)
94+
scheduler.cleanAllWorkerQueues()
95+
96+
assertFalse(scheduler.removeActor("sensor-1"))
97+
assertFalse(scheduler.removeActor("regular-1"))
98+
}
99+
100+
@Test
101+
fun `should deliver sensor events to actor`() = runTest {
102+
103+
val testSensor = ShadowSensor.newInstance(TYPE_ACCELEROMETER)
104+
shadowSensorManager.addSensor(testSensor)
105+
106+
val testActor = TestSensorActor(
107+
id = "sensor-1",
108+
initialState = State(0),
109+
context = androidContext
110+
)
111+
112+
//system.actorOf(id = "sensor-1", initialState = Unit) { id, state -> TestSensorActor(id, state, androidContext)}
113+
scheduler.enqueue(createExecutor(testActor))
114+
115+
for (i in 1..1000) {
116+
shadowSensorManager.sendSensorEventToListeners(
117+
createSensorEvent(
118+
testSensor,
119+
floatArrayOf(1.0f, 2.0f, 3.0f)
120+
)
121+
)
122+
}
123+
124+
runBlocking {
125+
delay(5000)
126+
}
127+
}
128+
129+
private fun createSensorEvent(sensor: Sensor, values: FloatArray): SensorEvent {
130+
return ShadowSensorManager.createSensorEvent(values.size, TYPE_ACCELEROMETER)
131+
}
132+
133+
private fun <S : Any, M : Any, R : Any> createExecutor(actor: Actor<S, M, R>): ActorExecutor<*> {
134+
actor.system = system
135+
136+
val adapter = BaseActorAdapter(actor, actor.system)
137+
val mailbox = Mailbox(queue = ChannelMailbox<M>() as MailboxAbstractQueue<M>)
138+
val supervisor = Supervisor(
139+
id = "root-supervisor",
140+
strategy = SupervisorStrategy(RestartStrategy.OneForOne, 5)
141+
)
142+
143+
return ActorExecutor(adapter, mailbox, supervisor.getMessageChannel())
144+
}
145+
146+
data class State(var count: Int = 0)
147+
148+
class TestSensorActor(
149+
id: String,
150+
initialState: State,
151+
context: Context,
152+
sensorType: Int = TYPE_ACCELEROMETER,
153+
samplingPeriod: Int = SensorManager.SENSOR_DELAY_NORMAL
154+
) : SensorActor<State, SensorData>(
155+
id = id,
156+
initialState = initialState,
157+
androidContext = context,
158+
sensorType = sensorType,
159+
samplingPeriod = samplingPeriod
160+
) {
161+
private val log = LoggerFactory.getLogger(TestSensorActor::class.java)
162+
163+
override fun onReceive(
164+
message: SensorData,
165+
ctx: ActorContext<State>
166+
): Pair<ActorContext<State>, Unit> {
167+
log.info("Message data: {}", message)
168+
val state: Int = ctx.state?.count?.plus(1) ?: 0
169+
log.info("New State: {}", state)
170+
return ctx.withState(newState = State(count = state)) to Unit
171+
}
172+
}
173+
174+
class RegularActor<S : Any>(
175+
id: String,
176+
initialState: S?
177+
) : Actor<S, String, String>(id, initialState) {
178+
override fun onReceive(
179+
message: String,
180+
ctx: ActorContext<S>
181+
): Pair<ActorContext<S>, String> {
182+
println("Received message: $message")
183+
return ctx to "processed: $message"
184+
}
185+
}
186+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
tinylog.writer = logcat
2+
tinylog.level = info

0 commit comments

Comments
 (0)