Note
Go to the end to download the full example code.
Replaying a file in a separate process๐
With a PlayerLSL
object, you can replay an MNE-Python readable
file as a valid LSL stream. This is very useful for testing or debugging purposes. The
created LSL stream is managed in the main process, as a separate thread. Thus, if the
code running the main process is hogging all of the CPU resources, then the
PlayerLSL
object may not be able to keep up with the LSL stream
and to push (publish) new data when it should. This is why it is often useful to run the
PlayerLSL
object in a separate process.
Single process execution๐
Letโs start by demonstrating the issue with a single process where the main thread is
hogging all of the CPU resources, leaving almost nothing for the
PlayerLSL
thread to run and mock the LSL stream.
import multiprocessing as mp
import time
from mne.io import read_raw_fif
from mne_lsl.datasets import sample
from mne_lsl.player import PlayerLSL as Player
raw = read_raw_fif(sample.data_path() / "sample-ecg-raw.fif", preload=False)
raw.crop(0, 3).load_data()
player = Player(raw, chunk_size=200, n_repeat=1, name="example-process")
player
<Player: example-process | OFF | /home/runner/mne_data/MNE-LSL-data/sample/sample-ecg-raw.fif>
Note
Note that we use a chunk_size
of 200 samples which does replicate a real-time
scenario where the LSL stream is usually using finer granularity (specific to the
device and LSL stream application). However, this large chunk size provides
stability to the documentation build on our CIs.
The smaller the chunk size, the more resources are needed by the
PlayerLSL
object to keep up with the LSL stream. This is why
with a chunk size of 200 samples, we donโt observe a difference between a main thread
giving time to the player to run (scenario 1, while loop with time.sleep()
calls
to create idle time) and a main thread hogging all of the CPU resources (scenario 2,
while loop without any idle time).
# scenario 1: give time to the player to run
start = time.time()
player.start()
while player.running:
time.sleep(0.5)
stop = time.time()
print(f"Elapsed time (free time): {stop - start:.2f} s")
# scenario 2: hog all of the CPU resources
start = time.time()
player.start()
while player.running:
pass
stop = time.time()
print(f"Elapsed time (hogging): {stop - start:.2f} s")
# clean-up resources
del player
Elapsed time (free time): 3.00 s
Elapsed time (hogging): 2.97 s
If you run the code above with a chunk_size
of 10 samples, or of 1 sample, you
will observe the following difference in elapsed time:
chunk size of 10 samples:
3.5 seconds for scenario 1 (due to one extra sleep call)
7.4 seconds for scenario 2
chunk size of 1 sample:
3.5 seconds for scenario 1 (due to one extra sleep call)
68.8 seconds for scenario 2
Multi-process execution๐
To avoid this problem, we can run the PlayerLSL
object in a
separate process. This way, the main process can hog all of the CPU resources without
affecting the player process.
This can be achieved in the following ways:
use the command-line entry point to run the player in a separate terminal
$ mne_lsl_player sample-ecg-raw.fif --chunk-size 1 --n-repeat 1
use the python API to run the player in a separate python interpreter
use the python API and the
multiprocessing
module to run the player in a separate process
Here, we demonstrate the last option.
Note
On Windows, the process
creation and execurtion must be protected behind an
if __name__ == "__main__":
block. This is not necessary on Unix systems, on
which this documentation is built.
def player_process(raw):
"""Process which runs the player."""
from mne_lsl.player import PlayerLSL
player = PlayerLSL(raw, chunk_size=200, n_repeat=1, name="example-process-2")
player.start()
while player.running:
pass
process = mp.Process(target=player_process, args=(raw,))
process.start()
print("Player started in a separate process!")
process.join()
Player started in a separate process!
This is still a bit too simplistic. The process takes time to actually start, thus when the print is emitted, the player has not yet started. To fix this, we can use a shared variable to signal when the player has started. This shared variable can also be used to interrupt the player process from the main process.
def player_process(raw, status):
"""Process which runs the process."""
from mne_lsl.player import PlayerLSL
player = PlayerLSL(raw, chunk_size=200, name="example-process-3")
player.start()
status.value = 1
while status.value:
pass
player.stop()
manager = mp.Manager()
# integer value, used to store 0 or 1 to turn off the player
status = manager.Value("i", 0)
process = mp.Process(target=player_process, args=(raw, status))
process.start()
while status.value != 1:
pass # wait for the player to actually start
print("Player started in a separate process!")
status.value = 0 # stops the player
process.join()
Player started in a separate process!
Total running time of the script: (0 minutes 9.689 seconds)
Estimated memory usage: 175 MB