wx.Yield (all flavors) runs longer when StyledTextCtrl scrolled down

Environment: Windows10, wxPython v.4.2.0, Python 3.10

There’s editor-like GUI app: Frame->Panel->MultiSplitterWindow->Panel->STC.
I’m implementing Search function as LongRunning, simplified excerpt:

   while self.SearchInTarget(stoken) > 0:
        highlighter.FillRange((self.GetTargetStart(), self.GetTargetEnd()))

        self.SetTargetStart(self.GetTargetEnd())
        self.SetTargetEnd(endpos)
        total_found += 1

        start = time.time()
        wx.YieldIfNeeded()
        end = time.time()
        res += end-start

  print('Yield took avg time:', res/total_found)

In case the Doc contains large text data and many found tokens, the search runs much longer, but especially when the Doc is scrolled down even for few hundreds lines (of tens thousands).
By the measurement I observed that wx.Yield actually runs much longer.
Note, I collect search word characters in Queue, then using Thread to read each and send a message to MainThread by QueueEvent (CallAfter/Later were tried as well) with custom event to launch the search loop.

Could someone describe the reason of the scrolling impact and can I work it around?
Thanks.

well, your yielding & scrolling both run on one and the same queue and therefore the more you scroll the less yielding will be done in case the queue is full

the standard remedy is to assign those two tasks to different resources if there are any (best, of course is, to different cores) :wink:

Thanks for reply, however I’ve taken it to account. I have not start search before scrolling and even waited some time upon file loaded and scrolling (“goto line” as well) completed. Of course, many additional searches have been performed as well to empty the events queue, but it changed nothing. BTW, if I scroll up to top, it works fast like w/out scrolling, as there should be delay by your logic.

FYI: the time measurement results:

  1. Scrolling down, delay, Search:
    Start finding: select
    Yield took avg time: 0.0045045388092826225
    Found end: select select

  2. Scrolling on top, Search immediately:
    Start finding: select
    Yield took avg time: 4.241222976356424e-06
    Found end: select select

In the docs for Yield() it says:

Use extreme caution when calling this function as, just as EvtLoopBase.Yield(), it can result in unexpected reentrances.

It’s difficult to say without having a runnable app, but perhaps the combination of YieldIfNeeded() and the scrolling is causing some code to be called recursively?

certainly looks like

Does using wx.SafeYield(onlyIfNeeded=True) instead of wx.YieldIfNeeded() make any difference?

I cannot freeze STC events loop, it should show indicators on found positions ranges

I don’t know if this would work…

app = wx.GetApp()
loop = app.GetMainLoop()
if not loop.IsYielding():
    wx.YieldIfNeeded()
        if not wx.EventLoopBase.GetActive().IsYielding():
            wx.YieldIfNeeded()

the condition is True during running - no improvements

I’d love to have a look/print out inside events queue same time, but don’t know how to

Perhaps you could use wx.EventFilter?

See the example in: wx.EventFilter — wxPython Phoenix 4.2.1 documentation

Also: How to work with wx.EventFilter

1 Like

You can also use the InspectionTool to examine the workings of your app.

Simple example:

import wx

class MyFrame(wx.Frame):
    def __init__(self, parent):
        super().__init__(parent, pos=(900, 0))
        self.timer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer)
        self.timer.Start(1000)


    def OnTimer(self, event):
        loop = wx.EventLoopBase.GetActive()
        print(f"OnTimer(): {loop.Pending()}")


class MyApp(wx.App):
    def OnInit(self):
        self.frame = MyFrame(None)
        self.SetTopWindow(self.frame)
        self.frame.Show()
        from wx.lib.inspection import InspectionTool
        it = InspectionTool()
        it.Show()
        return True


if __name__ == '__main__':
    app = MyApp(0)
    app.MainLoop()

When the InspectionTool window appears, click on the Events button on the toolbar. An ‘Events Watcher’ window should then appear which will display the events occurring in the Frame (try moving the mouse over Frame or resizing it).

1 Like

Thanks Richard. EventFilter did the job - I observed and counted EVT_PAINT that has sent during the search loop. The problem is in massive EVT_PAINT sending when the doc is scrolled down, then searched.
However, when the doc is not scrolled or hidden (Frame collapsed or Splitter sash moved to hide the view) no massive repainting occurs. As you can see in the code, each found token has been highlighted by indicator, that sends repainting (does not if no highlighting).
I cannot realize why the scrolling causes to the doc repainting, obviously from line 0 to the last visible line (last one if fully scrolled). Search runs through the whole doc anyway, as only a screen of ~40 lines is visible. It is fast when it is the first screen, but looks like wxPython repaints the whole doc since 0 till last visible line, which takes larger time for Yield to end.
Now, the question is how to config this repainting.

1 Like

In your original post you said you were using wxPython 4.2.0. Would it be possible for you to upgrade to 4.2.1 and see if it still happens?

Current version: 4.2.1 msw (phoenix) wxWidgets 3.2.2.1
still happens

In the wxWidgets source code for src/stc/ScintillaWX.cpp the following methods might be relevant:

void ScintillaWX::DoPaint(wxDC* dc, wxRect rect) {

    paintState = painting;
    AutoSurface surfaceWindow(dc, this);
    if (surfaceWindow) {
        rcPaint = PRectangleFromwxRect(rect);
        PRectangle rcClient = GetClientRectangle();
        paintingAllText = rcPaint.Contains(rcClient);

        ClipChildren(*dc, rcPaint);
        Paint(surfaceWindow, rcPaint);
        surfaceWindow->Release();
    }

    if (paintState == paintAbandoned) {
        // Painting area was insufficient to cover new styling or brace
        // highlight positions.  So trigger a new paint event that will
        // repaint the whole window.
        stc->Refresh(false);

#if wxALWAYS_NATIVE_DOUBLE_BUFFER
        // On systems using double buffering, we also need to finish the
        // current paint to make sure that everything is on the screen that
        // needs to be there between now and when the next paint event arrives.
        FullPaintDC(dc);
#endif
    }
    paintState = notPainting;
}


// Force the whole window to be repainted
void ScintillaWX::FullPaint() {
    stc->Refresh(false);
    stc->Update();
}


void ScintillaWX::FullPaintDC(wxDC* dc) {
    paintState = painting;
    rcPaint = GetClientRectangle();
    paintingAllText = true;
    AutoSurface surfaceWindow(dc, this);
    if (surfaceWindow) {
        Paint(surfaceWindow, rcPaint);
        surfaceWindow->Release();
    }
    paintState = notPainting;
}

Sorry, I do not know what to do with that knowledge. Mass repainting occurs due to indicating of found tokens and, if STC is not scrolled, the screen is repainted, but if STC scrolled WHOLE doc area has been repainted. I’m not a “software crafter” to fix that bug or work it around by overriding, cause I don’t know which one and what’s the problem.
Meanwhile, I freezed STC window while searching loop running to prevent mass EVT_PAINT to be piled for Yield.
Thanks.

well, what you are doing there are infant steps of separating the resources of your machine many, many years ago
Scintilla is a powerful Text Editing control and if that runs in Python in a dedicated process satisfactorily that’s great luck
everything else must be offloaded to different processes and the results communicated to that main process for display etc
I think a fitting concept in this situation is a Process Pool Executor

I’ll tell you more, when STC works with small files it is a real mustang, just like that. Well, separating my beast to work in few processes instead of break a large file for smaller parts is much more mature for sure. Actually I just wanted to realize is there some problem with WX, as I’ve specified previously in the thread.