1

I'm writing a function that does some work to provide a result to the caller; but once this work is done there exists some followup work that is not required by the caller but is known is going to be useful in the future. I'd like this followup work to be executed when the MATLAB process is no longer busy doing what is immediately required of it so it doesn't slow down, or worse block execution of, other priority tasks.

On the front end interface, MATLAB has a clear sense of whether code is currently executing. When there is, the status bar reads "Busy" and the Editor ribbon strip updates to show a "Pause" button to interrupt execution. You can enter new commands to the command window, but they aren't read and executed until the currently executing code completes. Once execution is completed, the status bar is cleared of text, the ribbon "Pause" button is replaced with a "Run" button, and the command window displays a >> indicating that new commands entered there will be executed immediately.

Effectively I'd like code running in the middle of a function to be able to have the equivalent effect of entering new commands into the command window, to be run after currently-executing code is done.

For example, consider the function:

function testfun(x)

    disp("Starting main execution with input " + x);

    pause(1)

    disp("Completed main execution with input " + x);
    
    disp("Starting followup execution with input " + x);

    pause(1)

    disp("Completed followup execution with input " + x);

end

A function or script that invokes this a couple of times (and where the extra disp calls are, there could be other unspecified time-consuming activities):

testfun(1)
disp("Done testfun(1).")
testfun(2)
disp("Done testfun(2).")

would result in the output:

Starting main execution with input 1
Completed main execution with input 1
Starting followup execution with input 1
Completed followup execution with input 1
Done testfun(1).
Starting main execution with input 2
Completed main execution with input 2
Starting followup execution with input 2
Completed followup execution with input 2
Done testfun(2).

and take four seconds in total. An alternative solution would result in the output:

Starting main execution with input 1
Completed main execution with input 1
Done testfun(1).
Starting main execution with input 2
Completed main execution with input 2
Done testfun(2).
Starting followup execution with input 1
Completed followup execution with input 1
Starting followup execution with input 2
Completed followup execution with input 2

The output Done testfun(2), which is the last of the higher-priority output results to appear, would only have taken 2 seconds to appear, rather than the original 4.

Is this possible? Alternatively, if I could allow execution to clear the current execution stack (as I could if interrupting the code with the debugger, then using dbstep out all the way up to the Base workspace, then calling new code from the command window) this would be a useful compromise even if it leaves open the possibility there are more function calls queued in the Base workspace. (The actual choice of workspace the code executes in doesn't matter too much, but stepping out of deeper nested workspaces would at least allow the remaining code queued within those workspaces to be completed before the workspace is destroyed.)

Will
  • 1,835
  • 10
  • 21
  • To run code in the base workspace, as if typed at the command window, use `evalin`. Note it is inefficient and potentially dangerous to use `eval` or any of its relatives, but it’s the only way to “run code” in a different workspace. – Cris Luengo Nov 11 '21 at 14:36
  • 1
    @CrisLuengo although `evalin` has the effect of evaluating as if typed in the command window, it executes immediately. So rather than behaving as if typed *while other code is executing* it's behaving as if I hit Pause to engage the debugger, *then* typed the code in the command window, then hit continue. So the execution still interrupts other upcoming tasks, which fails the aim of my question. – Will Nov 11 '21 at 15:07
  • I know, that’s why that is a comment and not an answer. On your last paragraph it seems as if you want code executed in the base workspace, I thought `evalin` would be a piece of that puzzle. – Cris Luengo Nov 11 '21 at 15:16
  • @Will So why don't you just put the followup code at the end of the function? That way it will execute when the (rest of the) function code has been run. I'm not sure I understand your use case – Luis Mendo Nov 11 '21 at 15:35
  • @CrisLuengo no, afraid not, though I see why I gave that impression. I've added a little to the question to try to clarify intent – Will Nov 11 '21 at 15:38
  • @LuisMendo the function represents a small chunk of work that will be invoked as part of different sequences within a larger application. I want the followup code to run after as much of this work is complete as possible. Engineering every function that calls this one to be aware of a followup payload, and every function that calls those functions to do the same, all the way up to the top so any top-level caller can execute it at the end, is technically possible but completely unmaintainable. – Will Nov 11 '21 at 15:48
  • I don't think there's something related to "code queued within a workspace" actually happening in MATLAB. There is no queue. When you type at the command prompt while MATLAB is busy, the keystrokes are stored in the message queue, the UI gets to process them when MATLAB is idle. It's exactly as if you typed after MATLAB finished doing what it was doing. – Cris Luengo Nov 11 '21 at 15:50
  • 1
    I think it would be really good if you gave a practical example for what you want to accomplish. I think you're looking in the wrong corner. – Cris Luengo Nov 11 '21 at 15:51
  • @CrisLuengo `the message queue` sounds like a queue to me! Even if it doesn't exist as a programmatic concept in the MATLAB interpreter, only at a UI level then that's regrettable, but a direct way to add to this queue may still be a better workaround than other options. Still better to add unprocessed UI events to the queue than to poll a status indicator in the UI I reckon. I'll have a go at a practical example to make it clearer what I'm aiming for. – Will Nov 11 '21 at 16:11
  • 1
    @Will In case it helps, you can add keystroke events in Matlab using the `java.awt.Robot` class. See [here](https://stackoverflow.com/questions/27933270/programmatically-press-an-enter-key-after-starting-exe-file-in-matlab/27933690#27933690) or [here](https://stackoverflow.com/q/27933270/2586922). You would need to write a function to convert text (code) to keystroke events – Luis Mendo Nov 11 '21 at 16:18
  • @LuisMendo that might be the start of something workable! I notice that graphics clicks are queued in the same manner; if `java.awt.Robot` can generate those clicks then I don't need to worry about typing out whole blocks of code as a single UI event can potentially be bound to a conventional event callback. – Will Nov 11 '21 at 16:22
  • 1
    @Will But you are aware this is an ugly and fragile approach, are you? :-) Your need (which I'm afraid I fail to fully understand) can probably be solved in a cleaner way – Luis Mendo Nov 11 '21 at 16:24
  • @LuisMendo - I hope so! But is it uglier and more fragile than reading from the status bar, which is the cleanest solution (by being the only way of producing the behaviour I can think of) I'm otherwise aware of? If I can create a target for the Robot that is otherwise invisible and unobtrusive to the user and the rest of the application, I think it might be just slightly less (clearing a low bar, I know) – Will Nov 11 '21 at 16:36
  • @Will Yes, Robot is probably better than using the status bar, especially because with the latter you would need to periodically poll it (I couldn't find a way to add a listener to the status var, because only the first syntax [here](https://www.mathworks.com/help/matlab/ref/handle.addlistener.html) seems to be supported by Java objects, and I don't know the name of the event to listen to, if any). My main concern with using Robot is that the target window might lose focus, if someone is using the computer – Luis Mendo Nov 11 '21 at 16:39
  • @LuisMendo hmm, I didn't realise Robot works at literally the human input level rather than sending virtual inputs to nominated windows. That's definitely not workable in my implementation; it has to be able to operate in the background while the user continues to work with their mouse and keyboard. I'm still wondering if there is something else that can programmatically interact with a window in a way that triggers a callback but not until MATLAB returns control to the UI in the way it does with human inputs. – Will Nov 12 '21 at 15:29

2 Answers2

1

The best part-solution to this I'm aware of, with thanks to Jan Simon at MathWorks, is to observe the actual status bar of the MATLAB window. This is accessible with:

statusbar = com.mathworks.mde.desk.MLDesktop.getInstance.getMainFrame.getStatusBar;

The text contained in the status bar is then retrievable with statusbar.getText. When code is executing, the text contains the word "Busy", and when nothing is executing it doesn't, so I can use logic from this to decide MATLAB is currently busy (with careful attention to the possibility that other things, such as the profiler, will also modify the contents of this text).

A timer can poll this text so a callback will fire a short time (though not instantly, and dependent on how aggressively I'm willing to poll the interface) after MATLAB's Busy status ends.

This is not ideal because it only indirectly infers the busy status from a UI element that isn't designed to be involved in application control, and may be unreliable, and because it depends on repeated polling, but in some circumstances it will be better than nothing.

Applying this to the simplified example, we can break the task into two functions, with the main one taking an extra argument t to receive a timer object to interact with:

function testmain(x,t)

    disp("Starting main execution with input " + x);

    pause(1)

    disp("Completed main execution with input " + x);

    t.UserData.followups(end+1) = x;

end

function testfollowup(x)

    disp("Starting followup execution with input " + x);

    pause(1)

    disp("Completed followup execution with input " + x);

end

A callback for the timer could then be:

function followupcallback(t,~)

    if isempty(t.UserData.followups)
        return
    end

    status = t.UserData.statusbar.getText;

    if ~isempty(status) && contains(string(status),"Busy")
        return
    end

    arrayfun(@testfollowup,t.UserData.followups);
    t.UserData.followups = [];
    t.stop

end

and the polling timer can then be set up:

t = timer( ...
    "TimerFcn",@followupcallback, ...
    "Period",0.25, ...
    "ExecutionMode","fixedRate", ...
    "UserData",struct( ...
        "statusbar",com.mathworks.mde.desk.MLDesktop.getInstance.getMainFrame.getStatusBar, ...
        "followups",[]));
t.start;

With the example script modified to pass the timer through:

testfun(1,t)
disp("Done testfun(1).")
testfun(2,t)
disp("Done testfun(2).")

The resulting output is reorded as intended.

Will
  • 1,835
  • 10
  • 21
0

I think you could build an actual work queue. I'm using a global variable here for that, so that multiple different functions can add to the work queue. I'm also using nested functions to encapsulate arbitrary code and variables into a function handle that can be stored and run later on.

I'm still hazy as to why this would be useful, but it does process things in the order given by the example in the OP.

initialize_work_queue
testfun(1)
disp("Done testfun(1).")
testfun(2)
disp("Done testfun(2).")
process_work_queue
disp("Done process_work_queue.")

function testfun(x)
    disp("Starting main execution with input " + x);
    pause(1)
    disp("Completed main execution with input " + x);
    function follow_up_work
        disp("Starting followup execution with input " + x);
        pause(1)
        disp("Completed followup execution with input " + x);
    end
    global my_work_queue
    my_work_queue{end+1} = @follow_up_work;
end

function initialize_work_queue
    global my_work_queue
    my_work_queue = {};
end

function process_work_queue
    global my_work_queue
    while ~isempty(my_work_queue)
        func = my_work_queue{1};
        my_work_queue(1) = [];
        func();
    end
end
Cris Luengo
  • 55,762
  • 10
  • 62
  • 120
  • While this achieves the required example behaviour, my aim is for the function that generates the work to take responsibility for getting the work executed in the satisfactory sequence. Otherwise every top level caller needs to be engineered to invoke an equivalent of `process_work_queue`, which could quickly become a great case study in the need for the dependency inversion principle. – Will Nov 12 '21 at 13:56
  • @Will Explicitly running code is the most robust way. Of course you could combine this with an external tool that controls your UI, or with the other solution’s timer, to run `process_work_queue` when idling, but what if your user starts MATLAB with `-nodesktop` or runs your program with `-batch`? Another way to use this would be to run all your code through the work queue. The user would call your code by `enqueue(@()testfun(1)); enqueue(@()testfun(2)); process_work_queue`. Anyway, I don’t think you will find a better solution, and I still don’t think your whole approach makes sense. – Cris Luengo Nov 12 '21 at 14:19
  • Robustness of execution order is not a hard and fast requirement for a performance optimisation exercise like this. The minimum criteria for success here is an increased *probability* that execution occurs in a more favourable order, while guaranteeing that required execution robustly occurs *eventually*. A UI-based solution is certainly not satisfactory but still easily capable of failing by executing in the original, less optimal, sequence if the UI required for that trick isn't present. – Will Nov 12 '21 at 15:23
  • This solution relies on high level control systems being aware of and managing this low level implementation detail correctly to be sure of the required tasks completing *at all* which is a much greater threat to robustness. – Will Nov 12 '21 at 15:23