diff --git a/src/common/activitystate.py b/src/common/activitystate.py index 9b90ae0c..f8194cfa 100644 --- a/src/common/activitystate.py +++ b/src/common/activitystate.py @@ -8,6 +8,7 @@ * Miguel Guthridge [hdsq@outlook.com, HDSQ#2154] """ +from common.profiler import profilerDecoration from common.logger import log, verbosity from common.util.apifixes import ( PluginIndex, @@ -16,7 +17,8 @@ EffectIndex, WindowIndex, ) -from common.util.apifixes import getFocusedPluginIndex, getFocusedWindowIndex +from common.util.apifixes import getFocusedPluginIndex, getFocusedWindowIndex,\ + reset_generator_active class ActivityState: @@ -71,10 +73,13 @@ def _forcePlugUpdate(self) -> None: else: self._effect = plugin # type: ignore + @profilerDecoration("activity.tick") def tick(self) -> None: """ Called frequently when we need to update the current window """ + # HACK: Fix FL Studio bugs + reset_generator_active() self._changed = False if self._doUpdate: # Manually update plugin using selection diff --git a/src/common/contextmanager.py b/src/common/contextmanager.py index 2287546c..e157c8af 100644 --- a/src/common/contextmanager.py +++ b/src/common/contextmanager.py @@ -15,6 +15,7 @@ 'unsafeResetContext' ] +from .profiler import profilerDecoration from . import logger from typing import NoReturn, Optional, Callable, TYPE_CHECKING from time import time_ns @@ -55,7 +56,8 @@ def __init__(self) -> None: # Set the state of the script to wait for the device to be recognised self.state: Optional[IScriptState] = None if self.settings.get("debug.profiling"): - self.profiler: Optional[ProfilerManager] = ProfilerManager() + trace = self.settings.get("debug.exec_tracing") + self.profiler: Optional[ProfilerManager] = ProfilerManager(trace) else: self.profiler = None # Time the device last ticked at @@ -65,6 +67,7 @@ def __init__(self) -> None: self._device: Optional['Device'] = None @catchStateChangeException + @profilerDecoration("initialise") def initialise(self, state: IScriptState) -> None: """Initialise the controller associated with this context manager. @@ -75,6 +78,7 @@ def initialise(self, state: IScriptState) -> None: state.initialise() @catchStateChangeException + @profilerDecoration("deinitialise") def deinitialise(self) -> None: """Deinitialise the controller when FL Studio closes or begins a render """ @@ -87,6 +91,7 @@ def deinitialise(self) -> None: @catchUnsafeOperation @catchStateChangeException + @profilerDecoration("processEvent") def processEvent(self, event: EventData) -> None: """Process a MIDI event @@ -103,6 +108,7 @@ def processEvent(self, event: EventData) -> None: @catchUnsafeOperation @catchStateChangeException + @profilerDecoration("tick") def tick(self) -> None: """ Called frequently to allow any required updates to the controller diff --git a/src/common/defaultconfig.py b/src/common/defaultconfig.py index 81d201f4..bafb20e6 100644 --- a/src/common/defaultconfig.py +++ b/src/common/defaultconfig.py @@ -32,7 +32,11 @@ # Settings used for debugging "debug": { # Whether performance profiling should be enabled - "profiling": False + "profiling": False, + # Whether profiling should print the tracing of profiler contexts + # within the script. Useful for troubleshooting crashes in FL Studio's + # MIDI API. Requires profiling to be enabled. + "exec_tracing": False }, # Settings used during script initialisation "bootstrap": { diff --git a/src/common/profiler/manager.py b/src/common/profiler/manager.py index a1e7ca63..1ebe7475 100644 --- a/src/common/profiler/manager.py +++ b/src/common/profiler/manager.py @@ -78,8 +78,10 @@ def _getProfileName(n: ProfileNode) -> str: else: return ProfilerManager._getProfileName(n.parent) + "." + n.name - def __init__(self) -> None: + def __init__(self, print_traces: bool) -> None: + self._print = print_traces self._current: Optional[ProfileNode] = None + self._depth = 0 self._max_name = 0 self._totals: dict[str, float] = {} self._number: dict[str, float] = {} @@ -99,6 +101,9 @@ def openProfile(self, name: str): ### Args: * `name` (`str`): name of profile to open """ + self._depth += 1 + if self._print: + print("+"*self._depth + name) n = ProfileNode(self._current, name) if self._current is not None: self._current.addChild(n) @@ -111,6 +116,9 @@ def closeProfile(self): ### Raises: * `ValueError`: no profile to close """ + if self._print: + print("-"*self._depth + self._current.name) + self._depth -= 1 self._current.close() if self._current is None: raise ValueError("No profile to close") diff --git a/src/common/states/mainstate.py b/src/common/states/mainstate.py index 9ca28b3a..d96a3135 100644 --- a/src/common/states/mainstate.py +++ b/src/common/states/mainstate.py @@ -45,7 +45,7 @@ def initialise(self) -> None: def deinitialise(self) -> None: pass - @profilerDecoration("tick") + @profilerDecoration("main.tick") def tick(self) -> None: with ProfilerContext("Device tick"): self._device.doTick() @@ -95,7 +95,7 @@ def tick(self) -> None: with ProfilerContext(f"Apply {type(p)}"): p.apply(thorough=True) - @profilerDecoration("processEvent") + @profilerDecoration("main.processEvent") def processEvent(self, event: EventData) -> None: with ProfilerContext("Match event"): mapping = self._device.matchEvent(event) diff --git a/src/common/util/apifixes.py b/src/common/util/apifixes.py index 8b4e5b19..7b2630ed 100644 --- a/src/common/util/apifixes.py +++ b/src/common/util/apifixes.py @@ -12,6 +12,7 @@ import playlist from typing import Union, Optional +from common.profiler import profilerDecoration, ProfilerContext from common.consts import PARAM_CC_START GeneratorIndex = tuple[int] @@ -29,11 +30,25 @@ UnsafeIndex = Union[UnsafePluginIndex, UnsafeWindowIndex] +# HACK: A terrible horrible no good really bad global variable to make sure +# that we hopefully avoid crashes in getFocusedPluginIndex +generator_previously_active = 0 + + +def reset_generator_active(): + """Horrible hacky function to hopefully work around a bug in FL Studio""" + global generator_previously_active + if generator_previously_active != 0: + generator_previously_active -= 1 + + +@profilerDecoration("getFocusedPluginIndex") def getFocusedPluginIndex(force: bool = False) -> UnsafePluginIndex: """ Fixes the horrible ui.getFocusedFormIndex() function - Values are returned as tuples so that they can be unwrapped when + Values are returned as tuples so that they can be unwrapped when being + passed to other API functions Args: * `force` (`bool`, optional): whether to return the selected plugin on the @@ -44,18 +59,31 @@ def getFocusedPluginIndex(force: bool = False) -> UnsafePluginIndex: * `int`: grouped index of a channel rack plugin if one is focused * `int, int`: index of a mixer plugin if one is focused """ - # Check if a channel rack plugin is focused - # if ui.getFocused(7): - form_id = ui.getFocusedFormID() - + # HACK: Move this elsewhere + global generator_previously_active + with ProfilerContext("getFocused"): + # for i in range(8): + # print(f" {ui.getFocused(i)=}, {i=}") + ui_6 = ui.getFocused(6) + ui_7 = ui.getFocused(7) # If a mixer plugin is focused - if ui.getFocused(6): + if ui_6: + # HACK: Error checking to hopefully avoid a crash due to bugs in FL + # Studio + if generator_previously_active: + print("getFocusedPluginIndex() crash prevention") + return None + with ProfilerContext("getFocusedFormID @ mixer"): + form_id = ui.getFocusedFormID() track = form_id // 4194304 slot = (form_id - 4194304 * track) // 65536 return track, slot # Otherwise, assume that a channel is selected # Use the channel rack index so that we always have one - elif ui.getFocused(7): + elif ui_7: + generator_previously_active = 3 + with ProfilerContext("getFocusedFormID @ cr"): + form_id = ui.getFocusedFormID() # NOTE: When using groups, ui.getFocusedFormID() returns the index # respecting groups, instead of the global index, yuck if form_id == -1: @@ -63,30 +91,29 @@ def getFocusedPluginIndex(force: bool = False) -> UnsafePluginIndex: return None return (form_id,) else: + generator_previously_active = 3 if force: - return (channels.selectedChannel(),) + with ProfilerContext("selectedChannel"): + ret = (channels.selectedChannel(),) + return ret else: return None +@profilerDecoration("getFocusedWindowIndex") def getFocusedWindowIndex() -> Optional[int]: """ - Fixes the horrible ui.getFocusedFormIndex() function - - Values are returned as tuples so that they can be unwrapped when + Fixes the horrible ui.getFocused() function Returns: * `None`: if no window is focused * `int`: index of window """ - # Check if a channel rack plugin is focused - if getFocusedPluginIndex() is not None: - return None - else: - ret = ui.getFocusedFormID() - if ret == -1: - return None - return ret + for i in range(5): + if ui.getFocused(i): + return i + return None + # def getPluginName(index: UnsafeIndex) -> str: # """