Skip to content
This repository was archived by the owner on Nov 14, 2023. It is now read-only.

Commit 1fec188

Browse files
committed
publish
0 parents  commit 1fec188

File tree

5 files changed

+353
-0
lines changed

5 files changed

+353
-0
lines changed

BUILDOZER_README.txt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# The following changes to buildozer.spec are required:
2+
3+
# (list) Application requirements
4+
# comma separated e.g. requirements = sqlite3,kivy
5+
requirements = python3, kivy==master, oscpy
6+
7+
# (list) List of service to declare
8+
#services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY
9+
services = Worker_0:service.py:foreground,
10+
Worker_1:service.py:foreground,
11+
Worker_2:service.py:foreground,
12+
Worker_3:service.py:foreground,
13+
Worker_4:service.py:foreground,
14+
Worker_5:service.py:foreground,
15+
Worker_6:service.py:foreground,
16+
Worker_7:service.py:foreground
17+
18+
# (list) Permissions
19+
android.permissions = INTERNET, FOREGROUND_SERVICE
20+
21+
# This one is not reqired, but is a really good idea:
22+
23+
# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86
24+
android.arch = arm64-v8a

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Multi-Service Example
2+
3+
*Schedule tasks on multiple services.*
4+
5+
Background reading [Android Service](https://github.com/Android-for-Python/Android-for-Python-Users#android-service).
6+
7+
Start the services, start the tasks (repeat?), later stop the services. Perhaps repeat this sequence.
8+
9+
The number of services and the number of tasks are configured in the `build()` method. By default the app starts 6 services, and 20 tasks. The default number of services will be decreased if the device has less than 6 cores.
10+
11+
In this example the task (implemented in `service.py`) is to generate a bounded psudo random prime number in a somewhat inefficient way. Just busy work. When the tasks are complete the app reports a normalized measure of time taken.
12+
13+
Foreground services are used, these can be replaced with backdround services in `buildozer.spec`. The service has AutoRestartService(True) and the app attempts to connect to a restarted service. Reconnection may not be reliable in all cases in which case the restarted service will be unavailable.
14+
15+
Performace for fine grained tasks depends on the number of services started. Starting more services than the device has cores is always counter productive. For the plot below, the example was modified to always generate the same prime number and report latency in seconds. The UI activity was minimal.
16+
17+
![Performance](performance.jpg)
18+
19+
Clearly there is some overhead somewhere, as it takes adding between 4 and 5 cores to half the time taken by the tasks on 1 core.

main.py

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# Multi Service example
2+
#
3+
# 1) starts/stops some number of services.
4+
# 2) starts/gets result from a task defined in the service.
5+
# In this case, the task is to generate psudo random primes inefficiently.
6+
# 3) Given some number of tasks, schedules them on the available services.
7+
# 4) Displays average execution time for all the tasks in
8+
# 'micro seconds per task unit'. Normalized because task time is 'random'.
9+
#
10+
# See the build() method for configuring the number of services or tasks.
11+
#
12+
# DEPENDS ON:
13+
#
14+
# A) kivy==master
15+
# kivy==2.0.0 has an issue,
16+
# see https://github.com/Android-for-Python/Android-for-Python-Users#service-lifetime
17+
# B) oscpy ( minimum 0.6.0 )
18+
#
19+
# Based, in part, on https://github.com/tshirtman/kivy_service_osc
20+
#
21+
# Source https://github.com/Android-for-Python/Multi-Service-Example
22+
23+
24+
from kivy.app import App
25+
from kivy.lang import Builder
26+
from kivy.utils import platform
27+
from kivy.clock import Clock
28+
from functools import partial
29+
from oscpy.client import OSCClient
30+
from oscpy.server import OSCThreadServer
31+
from time import time, sleep
32+
from os import cpu_count
33+
34+
if platform == 'android':
35+
from jnius import autoclass
36+
elif platform in ('linux', 'linux2', 'macos', 'win'):
37+
from runpy import run_path
38+
from threading import Thread
39+
else:
40+
raise NotImplementedError("service start not implemented on this platform")
41+
42+
KV = '''
43+
BoxLayout:
44+
orientation: 'vertical'
45+
BoxLayout:
46+
size_hint_y: None
47+
height: '30sp'
48+
Button:
49+
text: 'start services'
50+
on_press: app.start_services()
51+
Button:
52+
text: 'start tasks'
53+
on_press: app.start_tasks()
54+
Button:
55+
text: 'stop services'
56+
on_press: app.stop_services()
57+
58+
ScrollView:
59+
Label:
60+
id: label
61+
size_hint_y: None
62+
height: self.texture_size[1]
63+
text_size: self.size[0], None
64+
65+
'''
66+
67+
class MultiService(App):
68+
### Build
69+
###################
70+
def build(self):
71+
self.server = server = OSCThreadServer()
72+
server.listen(
73+
address=b'localhost',
74+
port=3002, #### Also hardcoded 3002 in service.py
75+
default=True,
76+
)
77+
server.bind(b'/result', self.task_finished)
78+
server.bind(b'/tcip_port', self.save_tcip_port)
79+
server.bind(b'/echo', self.recieve_echo)
80+
self.service = None
81+
self.num_services_ready = 0
82+
self.clients = []
83+
self.tcpip_ports = []
84+
################# Configure here #################
85+
# num services MUST be <= number in buildozer.spec
86+
# num services approx upper bound is os.cpu_count
87+
self.num_buildozer_spec_services = 8
88+
self.number_of_services = min(6, cpu_count())
89+
self.number_of_tasks = 20
90+
##################################################
91+
self.root = Builder.load_string(KV)
92+
return self.root
93+
94+
### Manage Services
95+
###################
96+
def start_services(self):
97+
if not self.service:
98+
for i in range(self.number_of_services):
99+
self.start_service(i)
100+
101+
def start_service(self,id):
102+
if platform == 'android':
103+
from android import mActivity
104+
context = mActivity.getApplicationContext()
105+
SERVICE_NAME = str(context.getPackageName()) +\
106+
'.Service' + 'Worker_' + str(id)
107+
self.service = autoclass(SERVICE_NAME)
108+
self.service.start(mActivity,'')
109+
110+
elif platform in ('linux', 'linux2', 'macos', 'win'):
111+
# Usually 'import multiprocessing'
112+
# This is for debug of service.py behavior (not performance)
113+
self.service = Thread(
114+
target=run_path,
115+
args=['service.py'],
116+
kwargs={'run_name': '__main__'},
117+
daemon=True
118+
)
119+
self.service.start()
120+
121+
def stop_services(self):
122+
for client in self.clients:
123+
client.send_message(b'/stop_service', [])
124+
self.service = None
125+
self.clients = []
126+
self.tcpip_ports = []
127+
self.num_services_ready = 0
128+
129+
def save_tcip_port(self,message):
130+
msg = message.decode('utf8')
131+
if len(self.clients) == self.number_of_services:
132+
# a service has restarted and reported a tcpip port
133+
# if it is the same port there is nothing to do
134+
# else we look for an unresponsive service and replace it.
135+
if msg not in self.tcpip_ports:
136+
self.echoes = []
137+
for p,c in zip(self.tcpip_ports,self.clients):
138+
c.send_message(b'/echo',[p.encode('utf8'),])
139+
# We dont know how long all the responses will take.
140+
# Guess 2 sec, this is OK because we wont get any new
141+
# results from the killed service id
142+
Clock.schedule_once(partial(self.replace_service,msg),2)
143+
else:
144+
self.tcpip_ports.append(msg)
145+
# Each service listens on its own tcpip port,
146+
# Make a Client to talk to that service
147+
self.clients.append(OSCClient(b'localhost',int(msg)))
148+
# When we get them all
149+
if len(self.clients) == self.number_of_services:
150+
self.num_services_ready = self.number_of_services
151+
self.root.ids.label.text +=\
152+
'Started ' + str(self.number_of_services) + ' services\n'
153+
154+
def recieve_echo(self,message):
155+
self.echoes.append(message.decode('utf8'))
156+
157+
### Replace a killed service
158+
############################
159+
def replace_service(self,msg,dt):
160+
for p in self.tcpip_ports:
161+
if p not in self.echoes:
162+
# replace the port
163+
id = self.tcpip_ports.index(p)
164+
self.tcpip_ports[id] = msg
165+
self.clients[id] = OSCClient(b'localhost',int(msg))
166+
# reuse the restarted service
167+
# the lost result is replaced with a new result
168+
if self.last_task_number < self.number_of_tasks:
169+
self.start_task(int(id))
170+
return
171+
172+
### Manage Tasks
173+
###################
174+
def start_tasks(self):
175+
if self.num_services_ready:
176+
self.root.ids.label.text +=\
177+
'Started '+str(self.number_of_tasks)+' tasks, wait.'
178+
self.result_magnitude = 0
179+
self.num_results = 0
180+
self.last_task_number = 0
181+
self.start_time = time()
182+
for i in range(min(self.number_of_tasks,
183+
self.num_services_ready,
184+
self.num_buildozer_spec_services)):
185+
self.num_services_ready -= 1
186+
self.last_task_number += 1
187+
self.start_task(i)
188+
else:
189+
self.root.ids.label.text += 'No services available\n'
190+
191+
def start_task(self, id):
192+
self.clients[id].send_message(b'/start_task',
193+
[str(id).encode('utf8'),])
194+
195+
def task_finished(self,message):
196+
id, res = message.decode('utf8').split(',')
197+
# service available
198+
self.num_services_ready +=1
199+
# collect result
200+
self.result_magnitude += int(res)
201+
self.num_results += 1
202+
# new task ?
203+
if self.last_task_number < self.number_of_tasks:
204+
self.num_services_ready -= 1
205+
self.last_task_number += 1
206+
self.start_task(int(id))
207+
self.display_result(id, res)
208+
209+
### Display results
210+
###################
211+
def display_result(self, id, res):
212+
if self.root:
213+
#self.root.ids.label.text += ' ' + id + ' ' + res + '\n'
214+
self.root.ids.label.text += '.'
215+
if self.number_of_tasks == self.num_results:
216+
self.root.ids.label.text += '\n'
217+
# the tasks have different execution times
218+
# a task unit is 'execution time'/'prime value'
219+
msg = str(round((time() - self.start_time)*1000000/\
220+
self.result_magnitude))
221+
msg += ' micro seconds per normalized prime\n'
222+
self.root.ids.label.text += msg
223+
224+
if __name__ == '__main__':
225+
MultiService().run()

performance.jpg

26.1 KB
Loading

service.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# A service example
2+
#
3+
# responds to '\start_task` message (Generates a pseudo random number)
4+
# responds to `\stop_service` message
5+
# responds to `\echo` message
6+
# sends `\result` message
7+
# sends `\tcip_port` message
8+
#
9+
# Source https://github.com/Android-for-Python/Multi-Service-Example
10+
11+
from time import sleep
12+
from threading import Thread
13+
from kivy.utils import platform
14+
from oscpy.server import OSCThreadServer
15+
from oscpy.client import OSCClient
16+
if platform == 'android':
17+
from jnius import autoclass
18+
19+
from random import random
20+
21+
### Also hardcoded 3002 in main.py
22+
CLIENT = OSCClient('localhost', 3002)
23+
24+
if platform == 'android':
25+
PythonService = autoclass('org.kivy.android.PythonService')
26+
PythonService.mService.setAutoRestartService(True)
27+
28+
#### The task, a synthetic example.
29+
def generate_pseudo_random_prime():
30+
# A task that is slow-ish, make it slower by increasing MAX
31+
MAX = 40000
32+
last = 0
33+
for num in range(1, round(MAX*random())):
34+
for i in range(2, num):
35+
if (num % i) == 0:
36+
break
37+
else:
38+
last = num
39+
return last
40+
41+
def do_task(name):
42+
result = generate_pseudo_random_prime()
43+
send_message(b'/result', name + ',' + str(result))
44+
45+
#### messages to app
46+
def send_message(type,message):
47+
CLIENT.send_message(type, [message.encode('utf8'), ])
48+
49+
#### messages from app
50+
stopped = False
51+
52+
def stop_service():
53+
if platform == 'android':
54+
PythonService.mService.setAutoRestartService(False)
55+
global stopped
56+
stopped = True
57+
58+
def start_task(message):
59+
msg = message.decode('utf8')
60+
# task must be a Thread so that the messages are responsive
61+
Thread(target=do_task, args=(msg,),daemon=True).start()
62+
63+
def echo(message):
64+
send_message(b'/echo',message.decode('utf8'))
65+
66+
#### main loop
67+
def message_loop():
68+
SERVER = OSCThreadServer()
69+
SERVER.listen('localhost', default=True)
70+
SERVER.bind(b'/stop_service', stop_service)
71+
SERVER.bind(b'/start_task', start_task)
72+
SERVER.bind(b'/echo', echo)
73+
send_message(b'/tcip_port', str(SERVER.getaddress()[1]))
74+
75+
while True:
76+
sleep(1)
77+
if stopped:
78+
break
79+
SERVER.terminate_server()
80+
sleep(0.1)
81+
SERVER.close()
82+
83+
if __name__ == '__main__':
84+
message_loop()
85+

0 commit comments

Comments
 (0)