From ae04cc9897d7fc385001038e0714013b6702b189 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Tue, 30 Aug 2022 21:38:48 +0200 Subject: [PATCH 1/7] [module/pulseaudio] somewhat experimental immediate update try to immediately update pulseaudio, if pactl subscribe exists & allows us to monitor update. see #913 --- bumblebee_status/modules/core/pulseaudio.py | 46 +++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/bumblebee_status/modules/core/pulseaudio.py b/bumblebee_status/modules/core/pulseaudio.py index 70acb01..22d5b07 100644 --- a/bumblebee_status/modules/core/pulseaudio.py +++ b/bumblebee_status/modules/core/pulseaudio.py @@ -33,12 +33,17 @@ Requires the following executable: """ import re +import os import logging import functools +import threading +import subprocess +import select import core.module import core.widget import core.input +import core.event import util.cli import util.graph @@ -103,6 +108,42 @@ class Module(core.module.Module): for event in events: core.input.register(self, button=event["button"], cmd=event["action"]) + self.__monitor = threading.Thread(target=self.__subscribe, args=()) + self.__monitor.start() + + def __subscribe(self): + self.update2() + core.event.trigger("update", [self.id], redraw_only=True) + core.event.trigger("draw") + try: + proc = subprocess.Popen("pactl subscribe", + stdout = subprocess.PIPE, + stderr = subprocess.PIPE, + shell = True + ) + except: + return + os.set_blocking(proc.stdout.fileno(), False) + while threading.main_thread().is_alive(): + r, w, e = select.select([proc.stdout], [], [], 1) + + if not (r or w or e): + continue # timeout + + line = 'test' + update = False + while line: + line = proc.stdout.readline().decode("ascii", errors="ignore") + if "client" in line: + update = True + + if update: + self.update2() + core.event.trigger("update", [self.id], redraw_only=True) + core.event.trigger("draw") + while proc.stdout.readline().decode("ascii", errors="ignore"): + pass + def set_volume(self, amount): util.cli.execute( "pactl set-{}-{} @DEFAULT_{}@ {}".format( @@ -200,6 +241,11 @@ class Module(core.module.Module): return output def update(self): + if self.__monitor.is_alive(): + return + self.update2() + + def update2(self): try: self._failed = False channel = "sinks" if self._channel == "sink" else "sources" From 8d2cdebbafdb01f7ffc912555413fbc8bb9ee6e0 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Wed, 31 Aug 2022 19:05:08 +0200 Subject: [PATCH 2/7] [modules/pulseaudio] fix pactl subscribe thread calling "draw" too early causes status line to be messed up. see #917 --- bumblebee_status/modules/core/pulseaudio.py | 27 +++++++-------------- 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/bumblebee_status/modules/core/pulseaudio.py b/bumblebee_status/modules/core/pulseaudio.py index 22d5b07..795751e 100644 --- a/bumblebee_status/modules/core/pulseaudio.py +++ b/bumblebee_status/modules/core/pulseaudio.py @@ -113,36 +113,27 @@ class Module(core.module.Module): def __subscribe(self): self.update2() - core.event.trigger("update", [self.id], redraw_only=True) - core.event.trigger("draw") + core.event.trigger("update", [self.id], redraw_only=False) try: proc = subprocess.Popen("pactl subscribe", stdout = subprocess.PIPE, - stderr = subprocess.PIPE, + stderr = subprocess.STDOUT, shell = True ) except: return - os.set_blocking(proc.stdout.fileno(), False) while threading.main_thread().is_alive(): r, w, e = select.select([proc.stdout], [], [], 1) if not (r or w or e): continue # timeout - - line = 'test' - update = False - while line: - line = proc.stdout.readline().decode("ascii", errors="ignore") - if "client" in line: - update = True - - if update: - self.update2() - core.event.trigger("update", [self.id], redraw_only=True) - core.event.trigger("draw") - while proc.stdout.readline().decode("ascii", errors="ignore"): - pass + # whateve we got, use it + self.update2() + core.event.trigger("update", [self.id], redraw_only=True) + core.event.trigger("draw") + os.set_blocking(proc.stdout.fileno(), False) + proc.stdout.read() + os.set_blocking(proc.stdout.fileno(), True) def set_volume(self, amount): util.cli.execute( From 978519e130709ec8a0c3f186f7eb6d8444cf9dee Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Thu, 1 Sep 2022 19:00:06 +0200 Subject: [PATCH 3/7] [modules/pulseaudio] redraw only make sure that we only ever redraw see #917 --- bumblebee_status/modules/core/pulseaudio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee_status/modules/core/pulseaudio.py b/bumblebee_status/modules/core/pulseaudio.py index 795751e..c21c577 100644 --- a/bumblebee_status/modules/core/pulseaudio.py +++ b/bumblebee_status/modules/core/pulseaudio.py @@ -113,7 +113,7 @@ class Module(core.module.Module): def __subscribe(self): self.update2() - core.event.trigger("update", [self.id], redraw_only=False) + core.event.trigger("update", [self.id], redraw_only=True) try: proc = subprocess.Popen("pactl subscribe", stdout = subprocess.PIPE, From b90346424b0615e58ac9d166d87a17cad39c4bb0 Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Thu, 1 Sep 2022 21:12:21 +0200 Subject: [PATCH 4/7] [core/output] rate-limit output (see #917) according to research (Jakob Nielsen '93), roughly 0.1s is what is required for the user to feel "instantaneous". based on this, rate-limit updates to only once per ~0.03s (0.1 felt really laggy for me, so let's be conservative) --- bumblebee_status/core/output.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/bumblebee_status/core/output.py b/bumblebee_status/core/output.py index cee579f..21f74ae 100644 --- a/bumblebee_status/core/output.py +++ b/bumblebee_status/core/output.py @@ -1,6 +1,7 @@ import sys import json import time +import datetime import core.theme import core.event @@ -145,6 +146,7 @@ class i3(object): self.__content = {} self.__theme = theme self.__config = config + self.__previous_draw = datetime.datetime.min core.event.register("update", self.update) core.event.register("start", self.draw, "start") core.event.register("draw", self.draw, "statusline") @@ -176,6 +178,14 @@ class i3(object): self.__content[widget_id]["minimized"] = not self.__content[widget_id]["minimized"] def draw(self, what, args=None): + + if what == "statusline": + now = datetime.datetime.now() + prev = self.__previous_draw + self.__previous_draw = now + if (now - prev).total_seconds() < 0.03: + return + cb = getattr(self, what) data = cb(args) if args else cb() if "blocks" in data: From 40041d60802a856b8ac575fb7ebd1ba70f758e1f Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 4 Sep 2022 09:41:20 +0200 Subject: [PATCH 5/7] Revert "[core/output] rate-limit output (see #917)" (did not fix the issue) This reverts commit b90346424b0615e58ac9d166d87a17cad39c4bb0. --- bumblebee_status/core/output.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/bumblebee_status/core/output.py b/bumblebee_status/core/output.py index 21f74ae..cee579f 100644 --- a/bumblebee_status/core/output.py +++ b/bumblebee_status/core/output.py @@ -1,7 +1,6 @@ import sys import json import time -import datetime import core.theme import core.event @@ -146,7 +145,6 @@ class i3(object): self.__content = {} self.__theme = theme self.__config = config - self.__previous_draw = datetime.datetime.min core.event.register("update", self.update) core.event.register("start", self.draw, "start") core.event.register("draw", self.draw, "statusline") @@ -178,14 +176,6 @@ class i3(object): self.__content[widget_id]["minimized"] = not self.__content[widget_id]["minimized"] def draw(self, what, args=None): - - if what == "statusline": - now = datetime.datetime.now() - prev = self.__previous_draw - self.__previous_draw = now - if (now - prev).total_seconds() < 0.03: - return - cb = getattr(self, what) data = cb(args) if args else cb() if "blocks" in data: From ee81f6198e8a0aacf4176161a1477420c00d3b1e Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 4 Sep 2022 09:55:33 +0200 Subject: [PATCH 6/7] [modules/pulseaudio] rate-limit pulseaudio events attempting a different tack: Reduce the amount of draws that are being emitted by the pulseaudio module to max. 2/s. That hopefully increases reactivity and at the same time keeps flickering to a minimum. see #917 --- bumblebee_status/modules/core/pulseaudio.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bumblebee_status/modules/core/pulseaudio.py b/bumblebee_status/modules/core/pulseaudio.py index c21c577..aa9eb3b 100644 --- a/bumblebee_status/modules/core/pulseaudio.py +++ b/bumblebee_status/modules/core/pulseaudio.py @@ -34,6 +34,7 @@ Requires the following executable: import re import os +import time import logging import functools import threading @@ -126,12 +127,16 @@ class Module(core.module.Module): r, w, e = select.select([proc.stdout], [], [], 1) if not (r or w or e): + self.update2() + core.event.trigger("update", [self.id], redraw_only=True) + core.event.trigger("draw") continue # timeout # whateve we got, use it self.update2() core.event.trigger("update", [self.id], redraw_only=True) core.event.trigger("draw") os.set_blocking(proc.stdout.fileno(), False) + time.sleep(0.5) proc.stdout.read() os.set_blocking(proc.stdout.fileno(), True) From ee9885a6010c9384d276b031e9adb23d347280be Mon Sep 17 00:00:00 2001 From: tobi-wan-kenobi Date: Sun, 4 Sep 2022 16:22:29 +0200 Subject: [PATCH 7/7] [core] fix concurrency issues * initialize first line of output earlier (before modules are initialized, so that module/thread output cannot interfere) * make sure that update and draw are protected against concurrent access --- bumblebee-status | 4 +++- bumblebee_status/core/output.py | 23 ++++++++++++++------- bumblebee_status/modules/core/pulseaudio.py | 1 - 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/bumblebee-status b/bumblebee-status index 698bcf8..e7b3ea3 100755 --- a/bumblebee-status +++ b/bumblebee-status @@ -75,6 +75,7 @@ def handle_events(config, update_lock): def main(): + global started config = core.config.Config(sys.argv[1:]) level = logging.DEBUG if config.debug() else logging.ERROR if config.logfile(): @@ -97,6 +98,8 @@ def main(): core.input.register(None, core.input.WHEEL_UP, "i3-msg workspace prev_on_output") core.input.register(None, core.input.WHEEL_DOWN, "i3-msg workspace next_on_output") + core.event.trigger("start") + update_lock = threading.Lock() event_thread = threading.Thread(target=handle_events, args=(config, update_lock, )) event_thread.daemon = True @@ -127,7 +130,6 @@ def main(): if util.format.asbool(config.get("engine.collapsible", True)) == True: core.input.register(None, core.input.MIDDLE_MOUSE, output.toggle_minimize) - core.event.trigger("start") started = True signal.signal(10, sig_USR1_handler) while True: diff --git a/bumblebee_status/core/output.py b/bumblebee_status/core/output.py index cee579f..1bd5038 100644 --- a/bumblebee_status/core/output.py +++ b/bumblebee_status/core/output.py @@ -1,6 +1,7 @@ import sys import json import time +import threading import core.theme import core.event @@ -145,6 +146,7 @@ class i3(object): self.__content = {} self.__theme = theme self.__config = config + self.__lock = threading.Lock() core.event.register("update", self.update) core.event.register("start", self.draw, "start") core.event.register("draw", self.draw, "statusline") @@ -176,14 +178,15 @@ class i3(object): self.__content[widget_id]["minimized"] = not self.__content[widget_id]["minimized"] def draw(self, what, args=None): - cb = getattr(self, what) - data = cb(args) if args else cb() - if "blocks" in data: - sys.stdout.write(json.dumps(data["blocks"], default=dump_json)) - if "suffix" in data: - sys.stdout.write(data["suffix"]) - sys.stdout.write("\n") - sys.stdout.flush() + with self.__lock: + cb = getattr(self, what) + data = cb(args) if args else cb() + if "blocks" in data: + sys.stdout.write(json.dumps(data["blocks"], default=dump_json)) + if "suffix" in data: + sys.stdout.write(data["suffix"]) + sys.stdout.write("\n") + sys.stdout.flush() def start(self): return { @@ -244,6 +247,10 @@ class i3(object): return blocks def update(self, affected_modules=None, redraw_only=False, force=False): + with self.__lock: + self.update2(affected_modules, redraw_only, force) + + def update2(self, affected_modules=None, redraw_only=False, force=False): now = time.time() for module in self.__modules: if affected_modules and not module.id in affected_modules: diff --git a/bumblebee_status/modules/core/pulseaudio.py b/bumblebee_status/modules/core/pulseaudio.py index aa9eb3b..9e7150b 100644 --- a/bumblebee_status/modules/core/pulseaudio.py +++ b/bumblebee_status/modules/core/pulseaudio.py @@ -136,7 +136,6 @@ class Module(core.module.Module): core.event.trigger("update", [self.id], redraw_only=True) core.event.trigger("draw") os.set_blocking(proc.stdout.fileno(), False) - time.sleep(0.5) proc.stdout.read() os.set_blocking(proc.stdout.fileno(), True)