0

I have been debugging some code which seems to be locking the main thread when the GUI is not visible. I have stripped it right back to the following code snippet and I have identified that a problem exists around my implementation of QTimer::singleShot.

To replicate the issue I set the timer to 1 (millisecond) start the application looping and send the app window to to the background. Eventually the app will grind to a halt until the UI is brought into the foreground.

If I set the timer to 0, it then the runs flawlessly regardless of where the main app window is located, I have increased the loop size as far as 4 million without issue.

I guess what I am stuck with is, what is the difference between

QTimer::singleShot(1, this, SLOT(emptyListOfAnotherObjects()));

and

QTimer::singleShot(0, this, SLOT(emptyListOfAnotherObjects()));?

Why would one block the main thread over the other?

For what it is worth running this loop in a different thread, keeping the GUI separate, results in the same issue.

class AnotherObject : public QObject
{
    Q_OBJECT

public:
    AnotherObject();

    void process();

signals:
    void done();
};

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

public slots:
    void start(bool);

private slots:
    void emptyListOfAnotherObjects();

    void delayEmptyOfAnotherObject();

private:
    QList<AnotherObject*> m_listOfAnotherObject;
    Ui::MainWindow *ui;
};

==

AnotherObject::AnotherObject()
{
    qDebug() << "CTOR AnotherObject" << this;
}

void AnotherObject::process()
{
    emit done();
    deleteLater();
}

//==

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    connect(ui->start, SIGNAL(clicked(bool)), this, SLOT(start(bool)));
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::start(bool)
{
    for(long i=0; i<40000; i++){
        m_listOfAnotherObject.append(new AnotherObject());
    }

    emptyListOfAnotherObjects();
}

void MainWindow::emptyListOfAnotherObjects()
{
    if(m_listOfAnotherObject.isEmpty()) {
        qDebug() << "List empty, done.";
        return;
    }

    AnotherObject* i = m_listOfAnotherObject.takeFirst();
    connect(i, SIGNAL(done()), this, SLOT(delayEmptyOfAnotherObject()));
    i->process();

    qDebug() << m_listOfAnotherObject.count();
}

void MainWindow::delayEmptyOfAnotherObject()
{
    QTimer::singleShot(1, this, SLOT(emptyListOfAnotherObjects()));
}

Thanks

Update:

Sorry, should have added that this is Qt 5.2 on OSX.

TheDarkKnight
  • 27,181
  • 6
  • 55
  • 85
  • I tried your code and I cannot reproduce your problem. Everything works well on 0 and 1ms. What do you mean by "sending the app to background"? I tried a lot of stuff, but it seems to run fluently. – Paraboloid87 Apr 04 '16 at 12:18
  • 3
    @aManFromOuterSpace - What platform are you testing this on? If you're using OS X it may be due to [app nap](https://developer.apple.com/library/mac/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/AppNap.html) – TheDarkKnight Apr 04 '16 at 12:22
  • Once main window comes to the foreground I click a button to start the loop then click on another open window, QtCreator in my case, and bring that app to the foreground and my app goes to the back. – aManFromOuterSpace Apr 04 '16 at 12:23
  • I did the same things. On Win10 with Qt 5.5.1, 64bit it works! – Paraboloid87 Apr 04 '16 at 12:26
  • @TheDarkKnight Qt 5.2 on OSX. App nap, never heard of that. Sounds suspicious, will take a closer look. – aManFromOuterSpace Apr 04 '16 at 12:27
  • 2
    As the [documentation shows](https://developer.apple.com/library/mac/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/AppNap.html), you can simply test this by checking in Activity Monitor, enabling the "App Nap" column, to show which processes are in this state. – TheDarkKnight Apr 04 '16 at 12:30
  • @TheDarkKnight App Nap seems to be the culprit. I see it switching on in activity monitor the the same moment the output in the debugger stops. Now for a workaround. I did find if I replace The QTimer::singleShot with a QMetaObject::invokeMethod, it can get through 4 millions loops without issue...weird. – aManFromOuterSpace Apr 04 '16 at 12:42
  • Side note: Using a 1ms single shot is very wasteful, you shouldn't ever be using it unless you literally need a 1ms delay for some purpose, like pacing MIDI output maybe. If you want to relinquish control to the event loop until no more events are available, use a **zero** timeout instead. You also don't need a `.ui` file etc. to replicate this, you really didn't minimize this test case as far as possible. – Kuba hasn't forgotten Monica Apr 04 '16 at 18:02

1 Answers1

2

App Nap is likely the cause of this problem.

I suspect that using invokeMethod places a message on the thread's event queue, which prevents the app from napping. In contrast, using QTimer waits and checks the timeout, so an empty event queue will allow it to enter the nap state.

Anyhow, it is possible to disable app nap from within your application, as described here.

As you're using Qt, you'll want to use a .mm file to encapsulate the objective-c code.

Community
  • 1
  • 1
TheDarkKnight
  • 27,181
  • 6
  • 55
  • 85