A brief description for JarvisEngine
Let's take a look inside the project created by python -m JarvisEngine create -d MyProject
earlier in README.md
The project structure should look like this.
MyProject
├── app.py
└── config.json
- app.py
from JarvisEngine.apps import BaseApp
class App(BaseApp):
def Start(self):
self.logger.info("Started!")
frame_rate = 10.0
def Update(self, delta_time: float) -> None:
self.logger.info(f"Updating in {delta_time:.2f} secs.")
- config.json
{
"MyApp": {
"path": "app.App",
"thread": true,
"apps":{}
}
}
In a template project, There are 2 files: application file and structure description file.
These files are essential to run JarvisEngine. Let's explain one by one.
A file where an Application functions and process is written. (python source code)
An application use threads and process to run in parallel.
An application work by BaseApp
(Overridable Methods) inheritance.
-
Start(self)
Application's main operation. A method is called when the Parallel process is started.class App(BaseApp): def Start(self): ...
-
frame_rate
A frame_rate to call theUpdate
method. frame_rate value will determine how theUpdate
method is called- Positive value
Update
call with frame_rate value - Zero
Update
call once. This is because the loop is divergence. - Negative value
Immediately execute the
Update
method. This is because the cycle will be negative, and the frame will be in the past. Hence, the wait time will always be zero.
class App(BaseApp): frame_rate = 1.0 ...
- Positive value
-
Update(self, delta_time)
The function called by frame_rate value (in 1 second) The argumentdelta_time
is the elapsed time since the previous frame. Hence,delta_time
will only be 0.0 (or close to 0.0) only at the beginningclass App(BaseApp): def Update(self, delta_time): ...
In addition to the methods listed here, there are several other methods that can be overridden.
-
Init(self)
A function called at last in Application Constructor. -
RegisterProcessSharedValues(self, sync_manager)
Please see section for more information Sharing values between applications. -
RegisterThreadSharedValues(self)
Please see section for more information Sharing values between applications. -
Awake(self)
A function called immediately after the start of a process or thread. Note that shared values between processes/threads cannot be used after this function is called. -
End(self)
A function called at the end of the process/thread. -
Terminate(self)
A function called just before the end of the process/thread. Note that this method will not be called if the child application is not terminated.
This is a json file that describes the application startup structure. It is described by specifying the module path of the application. It also describes whether to start parallel processing in threads or processes.
Running multiple application with the same format at the topmost field of this file or after apps
is also possible. More information regarding startup will be written at Launch multiple applications
- Example
{
"App0": {
"path": "App0.app.App0",
"thread": true
// `apps` is not necessary.
},
"App1": {
"path": "App1.app.App1",
"thread": false,
"apps": {
"App1_1": {
"path": "App1.App1_1.app.App1_1",
"thread": true,
"apps": {}
},
"App1_2": {
"path": "App1.App1_2.app.App1_2",
"thread": false
}
}
}
}
- About
path
This is the module path to the application class to start. Write in a form readable by python import - About
thread
In casetrue
, The application will use thread (threading
module) and begin parallel processing. In casefalse
, The process(multiprocessing
module) will use other intepreter instead.
The main reason for using json because it can add comments with ease. JSON files with other names can also be read if explicitly specified at startup. Details are explained in section JarvisEngine startup commands.
Structure description file (config.json
) in example of application launch structure have the following tree structure.
The application on top is Launcher
application. The application after that will be launch following the tree structure.
graph TD
L("Launcher")
A0("App0")
A1("App1")
A1_1("App1_1")
A1_2("App1_2")
L --"thread"-->A0
L --"process"-->A1
A1 --"thread"-->A1_1
A1 --"process"-->A1_2
There is a clear difference between starting an application with process and with thread. In process, interpreter and memory are completely seperated. While thread is an execution inside process while sharing memory.
In the case of threads, memory is shared and resources can be handed over very easily and startup is fast, but there are performance limitations due to GIL.
In the case of a process, the interpreter is completely separated, so performance is not limited by the GIL, but there are limitations on the resources that can be shared.
There are two typical ways to start a process: spawn
and fork
. (fork
is available only on UNIX-like systems.)
Please note that JarvisEngine is using spawn
on default.
In later at Engine Settings, We will explain how to change the start_method
of JarvisEngine.
Note that start a Multi-process threads using fork
are dangerous and may encounter unexpected bugs such as freezing! If you use are using fork
, please design your application startup configuration carefully.
Sharing values between applications is essential for parallel processing. However, this is also where we encounter most of the bugs. JarvisEngine explicitly registers values to be shared among threads and processes and manages them as a single object.
The basic when sharing a file is to refer to File system, and use the paths which are consistent with the python module system. Let's say your application is configured as follows. We will use this as an example.
graph TD
L("Launcher")
A0("App0")
A1("App1")
A1_1("App1_1")
A1_2("App1_2")
L --"thread"-->A0
L --"process"-->A1
A1 --"thread"-->A1_1
A1 --"process"-->A1_2
All shared object is manage as single class named FolderDict_withLock
Please see FolderDict repository for a detailed specification of FolderDict
You can register objects by path. Seperaotr in JarvisEngine is .
. This cannot be changed.
- Example
fd = FolderDict(sep=".")
fd["path.to.object"] = "instance"
> fd["path.to"]["object"]
--> "instance"
Paths are in dot .
delimited format, and there are two forms: absolute paths, where there is no dot at the beginning of the path string, and relative paths, where there are several dots at the beginning and the referencing is relative.
Absolute paths can be used well in any application, but if there is a change in application startup configuration, the described path must be changed as well. Relative paths are based on the location of the application startup configuration file and can be easily import to other JarvisEngine projects (group of several applications as a component).
Note: Relative paths are referenced backward in the parent directory by the number of dots at the beginning of the path string - 1.
Note: Absolute paths are always prefixed at Launcher
. This is because the top-level application in the startup configuration file is launched by JarvisEngine.apps.Launcher
.
-
Example
Suppose thatApp1
is sharing value namedint_value
between processes. You can share by do the following.import multiprocessing as mp class App1(BaseApp): def RegisterProcessSharedValues(self, sync_manager): super().RegisterProcessSharedValues(sync_manager) # must call. self.addProcessSharedValues("int_value",mp.Value("i"))
By doing this, Inside of
FolderDict
, The value will be registered and manage asLauncher.App1.int_value
. By referencing startup position, all of the application can access toint_values
To access
int_value
, you use a method name<BaseApp>.getProcessSharedValues(name)
. Of course, both absolute and relative paths can be accessed. And even ifApp0
shares a value with the namebool_value
, it can be accessed either absolutely or relatively.... # in App1 class. def Start(self): # Absolute int_value = self.getProcessSharedValue("Launcher.App1.int_value") bool_value = self.getProcessSharedvalue("Launcher.App0.bool_value") # Relative int_value = self.getProcessSharedValue(".int_value") bool_value = self.getProcessSharedValue("..App0.bool_value") ...
- Registration
To share values among multiple processes, Please override
RegisterProcessSharedValues
method, and use internaladdProcessSharedValue
method to register instead. Note: Please don't forget to use super classRegisterProcessSharedValues
Note: Argument value ofsync_manager
is return value ofmultiprocessing.Manager
import multiprocessing as mp
class App(BaseApp):
def RegisterProcessSharedValues(self, sync_manager):
super().RegisterProcessSharedValues(sync_manager)
v = mp.Queue()
self.addProcessSharedValue("queue",v)
- Reference
Use
getProcessSharedValue
to refer
... # in App class
def Start(self):
v = self.getProcessSharedValue("Launcher.path.to.queue")
...
Note: Attribute process_shared_values
manages all objects shared between processes. You can access to FolderDict
class too.
Note: The only objects whose state is synchronized even between processes are ones provided by the multiprocessing
module only.
Even for memory of object used between thread, It is not synchronized.
- Registration
To share values among multiple processes, Please overrideRegisterThreadSharedValues
method, and use internaladdThreadSharedValue
method to register instead.
class App(BaseApp):
def RegisterThreadSharedValues(self):
super().RegisterThreadSharedValues(sync_manager)
v = {"age": 19}
self.addThreadSharedValue("personal_data",v)
Note: Please don't forget to use super class RegisterThreadSharedValues
Note: addThreadSharedValues
can also be called in the Start
and Update
methods. As explained in Override Methods.
- Reference
UsegetThreadSharedValue
to refer
... # in App class
def Start(self):
v = self.getThreadSharedValue("Launcher.path.to.queue")
...
Note: Attribute thread_shared_values
manages all objects shared between threads. You can access to FolderDict
class too.
Note: Memory is shared between threads, so any object can be use betwenn threads.
Note: Shared only within the same process in the startup configuration.
There are several customizable configuration. All configurable items and their default values are described in JarvisEngine/default_engine_config.toml
.
You can change any of the settings by override the value and specifying the file at startup.
python -m JarvisEngine run -ec engine_config.toml
The log will be written in the [logging]
table.
-
host A host where you set up the
LoggingServer
.Logger
also sends logs to this host. -
port
A port where you set up theLoggingServer
.Logger
also sends logs to this port. -
message_format
A message format of log output. (using the official logging format) -
date_format
The format for displaying the timestamps of log messages.
The data will be written in the [multiprocessing]
table.
- start_method
The method to start multiprocessing. Default value isspawn
.
create
command to create a project
run
command to start project.
python -m JarvisEngine command --args
-ll
,--log_level
Output level of the log. Default value isDEBUG
.
You can choose fromDEBUG
,INFO
,WARNING
,ERROR
,CRITICAL
Create a template project that can be use by JarvisEngine.
python -m JarvisEngine create --args
Arguments
-d
,--creating_dir
The directory where the project will be created. Default is. /
.
You can give a project name here.
Run the following JarvisEngine project.
python -m JarvisEngine run --args
Arguments
-
-d
,--project_dir
The directory of project you want to run. Default value is `. /``. -
-c
,--config_file
Path of the startup configuration file of the application. The default value isconfig.json
. -
-ec
,--engine_config_file
The engine configuration file, The default isJarvisEngine/default_engine_config.toml
.