summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJan-Marek Glogowski <glogow@fbihome.de>2019-07-07 23:09:15 +0200
committerJan-Marek Glogowski <glogow@fbihome.de>2019-08-13 16:53:07 +0200
commit6e13585508ca3c9b66c6571ad1eb42bfcb66ef0b (patch)
treedf44551b7d3182036117c10ff694d1ae5905264f
parent536ab2f3ba1e71badaaf98db9419ca7c7ddd7ac4 (diff)
Add a TaskStopwatch to interrupt idle loops
If we have multiple pending Idles, they will interrupt / starve each other, because there will be an instant pending timeout for the next Idle. This patch introduces a time slice to tasks, so long running events can use a TaskStopwatch to do the real interrupt after running out of their time slice. Apart from the time, this breaks when AnyInput is available, except for the timer event. This class just helps to track the time, as the scheduler is coop, not preemptive. Change-Id: I9d0b4a5aa388ebdf496b355d100152d890224524 Reviewed-on: https://gerrit.libreoffice.org/75568 Tested-by: Jenkins Reviewed-by: Jan-Marek Glogowski <glogow@fbihome.de>
-rw-r--r--include/vcl/TaskStopwatch.hxx123
-rw-r--r--vcl/CppunitTest_vcl_timer.mk1
-rw-r--r--vcl/README.scheduler59
-rw-r--r--vcl/qa/cppunit/timer.cxx78
-rw-r--r--vcl/source/app/scheduler.cxx3
5 files changed, 259 insertions, 5 deletions
diff --git a/include/vcl/TaskStopwatch.hxx b/include/vcl/TaskStopwatch.hxx
new file mode 100644
index 000000000000..c98dcc6f5527
--- /dev/null
+++ b/include/vcl/TaskStopwatch.hxx
@@ -0,0 +1,123 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef INCLUDED_VCL_TASK_STOPWATCH_HXX
+#define INCLUDED_VCL_TASK_STOPWATCH_HXX
+
+#include <tools/time.hxx>
+#include <vcl/dllapi.h>
+#include <vcl/inputtypes.hxx>
+#include <vcl/svapp.hxx>
+
+/**
+ * Helper class primary used to track time of long running iterating tasks.
+ *
+ * Normally it should be sufficiant to instanciate the watch object before
+ * starting the iteration and query continueIter() at the end of each.
+ *
+ * Called Stopwatch, because there is already a Timer class in the Scheduler.
+ *
+ * TODO: merge into the general Scheduler, so this can also be used to track
+ * Task runtimes in a more general way.
+ * TODO: handle fast iterations, where continueIter is called multiple times
+ * per tick, by counting the iterations per tick and use that for appoximation.
+ **/
+class VCL_DLLPUBLIC TaskStopwatch
+{
+ static constexpr VclInputFlags eDefaultInputStop = VCL_INPUT_ANY & ~VclInputFlags::TIMER;
+ static constexpr unsigned int nDefaultTimeSlice = 50;
+ static unsigned int m_nTimeSlice;
+
+ sal_uInt64 m_nStartTicks;
+ sal_uInt64 m_nIterStartTicks;
+ bool m_bConsiderLastIterTime;
+ VclInputFlags m_eInputStop;
+
+ bool nextIter(bool bQueryOnly)
+ {
+ sal_uInt64 nCurTicks = tools::Time::GetSystemTicks();
+ // handle system ticks wrap as exceeded time slice
+ if (nCurTicks < m_nStartTicks)
+ return false;
+
+ if (!bQueryOnly && m_bConsiderLastIterTime)
+ {
+ // based on the last iter runtime, we don't expect to finish in time
+ // m_nTimeSlice < (nCurTicks - m_nStartTicks) + (nCurTicks - m_nIterStartTicks)
+ if (m_nTimeSlice < 2 * nCurTicks - m_nIterStartTicks - m_nStartTicks)
+ return false;
+ }
+ // time slice exceeded
+ else if (m_nTimeSlice < nCurTicks - m_nStartTicks)
+ return false;
+
+ if (!bQueryOnly)
+ m_nIterStartTicks = nCurTicks;
+
+ return !Application::AnyInput(m_eInputStop);
+ }
+
+public:
+ /**
+ * Per default the watch consideres the last iter time when asking for an
+ * other iteration, so considers Scheduler::acceptableTaskTime as a
+ * maximum value.
+ *
+ * If you already know your iter time vary in a large range, consider
+ * setting bConciderLastIterTime to false, so Scheduler::acceptableTaskTime
+ * will be used as a mimimum time slot.
+ **/
+ TaskStopwatch(bool bConciderLastIterTime = true)
+ : m_nStartTicks(tools::Time::GetSystemTicks())
+ , m_nIterStartTicks(m_nStartTicks)
+ , m_bConsiderLastIterTime(bConciderLastIterTime)
+ , m_eInputStop(eDefaultInputStop)
+ {
+ }
+
+ /**
+ * Returns true, if the time slot is already exceeded
+ **/
+ bool exceededRuntime() { return !nextIter(true); }
+
+ /**
+ * Returns true, if an other iteration will probably pass in the time slot
+ **/
+ bool continueIter() { return nextIter(false); }
+
+ /**
+ * Reset the stopwatch
+ **/
+ void reset()
+ {
+ m_nStartTicks = tools::Time::GetSystemTicks();
+ m_nIterStartTicks = m_nStartTicks;
+ }
+
+ /**
+ * Sets the input events, which should also "exceed" the stopwatch.
+ *
+ * Per default this ignores the VclInputFlags::TIMER.
+ */
+ void setInputStop(VclInputFlags eInputStop = eDefaultInputStop) { m_eInputStop = eInputStop; }
+ VclInputFlags inputStop() const { return m_eInputStop; }
+
+ /**
+ * Sets the time considered the acceptable maximum for a task to run
+ *
+ * This is an orientation for long time background jobs to yield to
+ * the scheduler, so Idle task don't starve each other too much.
+ **/
+ static unsigned int timeSlice() { return m_nTimeSlice; }
+ static void setTimeSlice(unsigned int nTimeSlice) { m_nTimeSlice = nTimeSlice; }
+};
+
+#endif // INCLUDED_VCL_TASK_STOPWATCH_HXX
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/CppunitTest_vcl_timer.mk b/vcl/CppunitTest_vcl_timer.mk
index b17bfc3a27f6..a89e4e070ec8 100644
--- a/vcl/CppunitTest_vcl_timer.mk
+++ b/vcl/CppunitTest_vcl_timer.mk
@@ -27,6 +27,7 @@ $(eval $(call gb_CppunitTest_use_libraries,vcl_timer, \
sal \
salhelper \
test \
+ tl \
unotest \
vcl \
))
diff --git a/vcl/README.scheduler b/vcl/README.scheduler
index 56be76913781..a05a291bc1c3 100644
--- a/vcl/README.scheduler
+++ b/vcl/README.scheduler
@@ -26,8 +26,7 @@ C.2. Tasks should be split into sensible blocks
C.3. This is not an OS scheduler
There is no real way to "fix" B.2. and B.3.
If you need to do a preemptive task, use a thread!
- Otherwise make your task suspendable and check SalInstance::AnyInput
- or call Application::Reschedule regularly.
+ Otherwise make your task suspendable.
= Driving the scheduler AKA the system timer =
@@ -43,8 +42,7 @@ Every time a task is started, the scheduler timer is adjusted. When the timer
fires, it posts an event to the system message queue. If the next most
important task is an Idle (AKA instant, 0ms timeout), the event is pushed to
the back of the queue, so we don't starve system messages, otherwise to the
-front. This is especially important to get a correct SalInstance::AnyInput
-handling, as this is used to suspend long background Idle tasks.
+front.
Every time the scheduler is invoked it searches for the next task to process,
restarts the timer with the timeout for the next event and then invokes the
@@ -120,7 +118,7 @@ bool DoYield( bool bWait, bool bAllCurrent )
== General: main thread deferral ==
-Currently for Mac and Windows, we run main thread deferrals by disabling the
+In almost all VCL backends, we run main thread deferrals by disabling the
SolarMutex using a boolean. In the case of the redirect, this makes
tryToAcquire and doAcquire return true or 1, while a release is ignored.
Also the IsCurrentThread() mutex check function will act accordingly, so all
@@ -176,6 +174,52 @@ To prevent these problems, we don't even try to remove these events, but
invalidate them by versioning the timer events. Timer events with invalid
versions are processed but simply don't run the scheduler.
+== General: track time of long running tasks ==
+
+There is TaskStopwatch class. It'll track the time and report a timout either
+when the tasks time slice is finished or some system event did occur.
+
+Eventually it will be merged into the main scheduler, so each invoked task can
+easily track it's runtime and eventually this can be used to "blame" / find
+other long running tasks, so interactivity can be improved.
+
+There were some questions coming up when implementing it:
+
+=== Why does the scheduler not detect that we only have idle tasks pending,
+and skip the instant timeout? ===
+
+You never know how long a task will run. Currently the scheduler simply asks
+each task when it'll be ready to run, until two runable tasks are found.
+Normally this is very quick, as LO has a lot of one-shot instant tasks / Idles
+and just a very few long term pending Timers.
+
+Especially UNO calls add a lot of Idles to the task list, which just need to
+be processed in order.
+
+=== Why not use things like Linux timer wheels? ===
+
+LO has relatively few timers and a lot one-shot Idles. 99% of time the search
+for the next task is quick, because there are just ~5 long term timers per
+document (cache invalidation, cursor blinking etc.).
+
+This might become a problem, if you have a lot of open documents, so the long
+term timer list increases AKA for highly loaded LOOL instances.
+
+But the Linux timer wheel mainly relies on the facts that the OS timers are
+expected to not expire, as they are use to catch "error" timeouts, which rarely
+happen, so this definitly not matches LO's usage.
+
+=== Not really usable to find misbehaving tasks ===
+
+The TaskStopwatch class is just a little time keeper + detecting of input
+events. This is not about misbehaving Tasks, but long running tasks, which
+have to yield to the Scheduler, so other Tasks and System events can be
+processed.
+
+There is the TODO to merge the functionality into the Scheduler itself, at
+which point we can think about profiling individual Tasks to improve
+interactivity.
+
== macOS implementation details ==
Generally the Scheduler is handled as expected, except on resize, which is
@@ -343,3 +387,8 @@ A few layers of indirection make this code hard to follow. The SalXLib::Yield
and SalX11Display::Yield architecture makes it impossible to process just the
current events. This really needs a refactoring and rearchitecture step, which
will also affect the Gtk+ and KDE backend for the user event handling.
+
+== Merge TaskStopwatch functionality into the Scheduler ==
+
+This way it can be easier used to profile Tasks, eventually to improve LO's
+interactivity.
diff --git a/vcl/qa/cppunit/timer.cxx b/vcl/qa/cppunit/timer.cxx
index 90705d26ec19..d0cfca5c1088 100644
--- a/vcl/qa/cppunit/timer.cxx
+++ b/vcl/qa/cppunit/timer.cxx
@@ -15,6 +15,7 @@
#include <osl/thread.hxx>
#include <chrono>
+#include <vcl/TaskStopwatch.hxx>
#include <vcl/timer.hxx>
#include <vcl/idle.hxx>
#include <vcl/svapp.hxx>
@@ -72,6 +73,8 @@ public:
void testPriority();
void testRoundRobin();
+ void testStopwatch();
+
CPPUNIT_TEST_SUITE(TimerTest);
CPPUNIT_TEST(testIdle);
CPPUNIT_TEST(testIdleMainloop);
@@ -91,6 +94,8 @@ public:
CPPUNIT_TEST(testPriority);
CPPUNIT_TEST(testRoundRobin);
+ CPPUNIT_TEST(testStopwatch);
+
CPPUNIT_TEST_SUITE_END();
};
@@ -527,6 +532,79 @@ void TimerTest::testRoundRobin()
CPPUNIT_ASSERT( 3 == nCount1 && 3 == nCount2 );
}
+class StopwatchIdle : public AutoIdle
+{
+ sal_uInt32 m_nIters;
+ sal_uInt64 m_nBusyTicks;
+
+public:
+ StopwatchIdle(sal_uInt64 nBusyTicks)
+ : AutoIdle("StopwatchIdle")
+ , m_nIters(0)
+ , m_nBusyTicks(nBusyTicks)
+ {
+ if (m_nBusyTicks > 0)
+ Start();
+ }
+
+ virtual void Invoke() override
+ {
+ TaskStopwatch aWatch;
+ // ignore all system events
+ aWatch.setInputStop(VclInputFlags::NONE);
+
+ sal_uInt64 nStartTicks = tools::Time::GetSystemTicks();
+ sal_uInt64 nCurTicks = nStartTicks;
+
+ while (!aWatch.exceededRuntime())
+ {
+ nCurTicks = tools::Time::GetSystemTicks();
+ if (nCurTicks - nStartTicks >= m_nBusyTicks)
+ {
+ nCurTicks = nStartTicks + m_nBusyTicks;
+ break;
+ }
+ }
+
+ assert(m_nBusyTicks >= (nCurTicks - nStartTicks));
+ m_nBusyTicks -= (nCurTicks - nStartTicks);
+ m_nIters++;
+
+ if (m_nBusyTicks == 0)
+ Stop();
+ }
+
+ bool isDone(sal_uInt32 &nIters)
+ {
+ nIters = m_nIters;
+ return (m_nBusyTicks == 0);
+ }
+};
+
+void TimerTest::testStopwatch()
+{
+ TaskStopwatch::setTimeSlice(10);
+
+ StopwatchIdle a1Idle(100);
+ StopwatchIdle a2Idle(100);
+
+ sal_uInt32 n1Iter, n2Iter;
+ while (true)
+ {
+ Application::Reschedule();
+
+ bool b1Done = a1Idle.isDone(n1Iter);
+ bool b2Done = a2Idle.isDone(n2Iter);
+ CPPUNIT_ASSERT(n1Iter >= n2Iter);
+
+ if (b1Done && b2Done)
+ break;
+ }
+
+ CPPUNIT_ASSERT_EQUAL(sal_uInt32(10), n1Iter);
+ CPPUNIT_ASSERT_EQUAL(sal_uInt32(10), n2Iter);
+}
+
CPPUNIT_TEST_SUITE_REGISTRATION(TimerTest);
CPPUNIT_PLUGIN_IMPLEMENT();
diff --git a/vcl/source/app/scheduler.cxx b/vcl/source/app/scheduler.cxx
index 23bba5d7a25e..09480a6a05e3 100644
--- a/vcl/source/app/scheduler.cxx
+++ b/vcl/source/app/scheduler.cxx
@@ -33,6 +33,7 @@
#include <tools/debug.hxx>
#include <tools/diagnose_ex.h>
#include <unotools/configmgr.hxx>
+#include <vcl/TaskStopwatch.hxx>
#include <vcl/scheduler.hxx>
#include <vcl/idle.hxx>
#include <saltimer.hxx>
@@ -96,6 +97,8 @@ std::basic_ostream<charT, traits> & operator <<(
} // end anonymous namespace
+unsigned int TaskStopwatch::m_nTimeSlice = TaskStopwatch::nDefaultTimeSlice;
+
void Scheduler::ImplDeInitScheduler()
{
ImplSVData* pSVData = ImplGetSVData();