.gitreview | 2 include/vcl/TaskStopwatch.hxx | 123 +++++++++++++++++++++++++++++++++++++++ sw/source/core/inc/layact.hxx | 27 ++------ sw/source/core/layout/layact.cxx | 59 ++++++++---------- sw/source/core/view/viewsh.cxx | 1 vcl/CppunitTest_vcl_timer.mk | 1 vcl/README.scheduler | 59 +++++++++++++++++- vcl/qa/cppunit/timer.cxx | 77 ++++++++++++++++++++++++ vcl/source/app/scheduler.cxx | 3 9 files changed, 295 insertions(+), 57 deletions(-)
New commits: commit 73b9318dd60b191984ee6e9fa12afb25773ad11b Author: Samuel Mehrbrodt <samuel.mehrbr...@allotropia.de> AuthorDate: Tue Jan 18 10:08:24 2022 +0100 Commit: Samuel Mehrbrodt <samuel.mehrbr...@allotropia.de> CommitDate: Tue Jan 18 10:08:24 2022 +0100 Update .gitreview Change-Id: I360c95365dbe553dc589a0a7da99189f1148a754 diff --git a/.gitreview b/.gitreview index c4d492adf26d..79f1e7ad3fbf 100644 --- a/.gitreview +++ b/.gitreview @@ -3,5 +3,5 @@ host=gerrit.libreoffice.org port=29418 project=core defaultremote=logerrit -defaultbranch=feature/cib_contract57d +defaultbranch=feature/cib_contract57l commit de28100fe351ded5e0724b98e2ebb398ee21825c Author: Jan-Marek Glogowski <jan-marek.glogow...@extern.cib.de> AuthorDate: Tue Aug 6 17:09:51 2019 +0200 Commit: Samuel Mehrbrodt <samuel.mehrbr...@allotropia.de> CommitDate: Tue Jan 18 09:58:02 2022 +0100 tdf#123583 use TaskStopwatch for Writer Idle loop I don't see much of a point in the extra CheckIdleEnd() function. We already check IsInterrupt() almost everywhere, so move that check in there. An other strange thing is the Idle job, which should just be interrupted by keyboard events (using SetInputType(, which this patch removes). Unlucky for me this code was there in the initial import. I can just say that othing obvious breaks... Change-Id: Ia5955d1eaf2ab612f2c4b63b0e458ed92507b75c Reviewed-on: https://gerrit.libreoffice.org/77040 Tested-by: Jenkins Reviewed-by: Jan-Marek Glogowski <glo...@fbihome.de> (cherry picked from commit 383032c50a3e3354f04200ce984a47ab9d2c5c67) diff --git a/sw/source/core/inc/layact.hxx b/sw/source/core/inc/layact.hxx index 50335077694c..1707383b62d7 100644 --- a/sw/source/core/inc/layact.hxx +++ b/sw/source/core/inc/layact.hxx @@ -22,6 +22,7 @@ #include <sal/config.h> #include <vcl/inputtypes.hxx> +#include <vcl/TaskStopwatch.hxx> #include <tools/color.hxx> #include <ctime> @@ -56,6 +57,7 @@ class SwLayAction { SwRootFrame *m_pRoot; SwViewShellImp *m_pImp; // here the action logs in and off + TaskStopwatch* m_pWatch; // For the sake of optimization, so that the tables stick a bit better to // the Cursor when hitting return/backspace in front of one. @@ -74,7 +76,6 @@ class SwLayAction std::clock_t m_nStartTicks; // The Action's starting time; if too much time passes the // WaitCursor can be enabled via CheckWaitCursor() - VclInputFlags m_nInputType; // Which input should terminate processing sal_uInt16 m_nEndPage; // StatBar control sal_uInt16 m_nCheckPageNum; // CheckPageDesc() was delayed if != USHRT_MAX // check from this page onwards @@ -84,9 +85,8 @@ class SwLayAction bool m_bCalcLayout; // Complete reformatting? bool m_bAgain; // For the automatically repeated Action if Pages are deleted bool m_bNextCycle; // Reset on the first invalid Page - bool m_bInput; // For terminating processing on input - bool m_bIdle; // True if the LayAction was triggered by the Idler bool m_bReschedule; // Call Reschedule depending on Progress? + bool m_bInterrupt; // For termination the layouting bool m_bCheckPages; // Run CheckPageDescs() or delay it bool m_bUpdateExpFields; // Is set if, after Formatting, we need to do another round for ExpField bool m_bBrowseActionStop; // Terminate Action early (as per bInput) and leave the rest to the Idler @@ -119,33 +119,26 @@ class SwLayAction bool RemoveEmptyBrowserPages(); - inline void CheckIdleEnd(); - public: - SwLayAction( SwRootFrame *pRt, SwViewShellImp *pImp ); + SwLayAction(SwRootFrame *pRt, SwViewShellImp *pImp, TaskStopwatch* pWatch = nullptr); ~SwLayAction(); - void SetIdle ( bool bNew ) { m_bIdle = bNew; } void SetCheckPages ( bool bNew ) { m_bCheckPages = bNew; } void SetBrowseActionStop( bool bNew ) { m_bBrowseActionStop = bNew; } void SetNextCycle ( bool bNew ) { m_bNextCycle = bNew; } bool IsWaitAllowed() const { return m_bWaitAllowed; } bool IsNextCycle() const { return m_bNextCycle; } - bool IsInput() const { return m_bInput; } bool IsPaint() const { return m_bPaint; } - bool IsIdle() const { return m_bIdle; } bool IsReschedule() const { return m_bReschedule; } - bool IsPaintExtraData() const { return m_bPaintExtraData;} - bool IsInterrupt() const { return IsInput(); } - - VclInputFlags GetInputType() const { return m_nInputType; } + bool IsIdle() const { return m_pWatch != nullptr; } + bool IsPaintExtraData() const { return m_bPaintExtraData; } + bool IsInterrupt(); // adjusting Action to the wanted behaviour void SetPaint ( bool bNew ) { m_bPaint = bNew; } void SetComplete ( bool bNew ) { m_bComplete = bNew; } void SetStatBar ( bool bNew ); - void SetInputType ( VclInputFlags nNew ) { m_nInputType = nNew; } void SetCalcLayout ( bool bNew ) { m_bCalcLayout = bNew; } void SetReschedule ( bool bNew ) { m_bReschedule = bNew; } void SetWaitAllowed ( bool bNew ) { m_bWaitAllowed = bNew; } @@ -182,21 +175,19 @@ public: class SwLayIdle { - + TaskStopwatch m_aWatch; SwRootFrame *pRoot; SwViewShellImp *pImp; // The Idler registers and deregisters here SwContentNode *pContentNode; // The current cursor position is saved here sal_Int32 nTextPos; bool bPageValid; // Were we able to evaluate everything on the whole page? - #ifdef DBG_UTIL bool m_bIndicator; -#endif -#ifdef DBG_UTIL void ShowIdle( Color eName ); #endif + bool IsInterrupt(); enum IdleJobType{ ONLINE_SPELLING, AUTOCOMPLETE_WORDS, WORD_COUNT, SMART_TAGS }; bool DoIdleJob_( const SwContentFrame*, IdleJobType ); bool DoIdleJob( IdleJobType, bool bVisAreaOnly ); diff --git a/sw/source/core/layout/layact.cxx b/sw/source/core/layout/layact.cxx index 96796cca4662..abac3364efd2 100644 --- a/sw/source/core/layout/layact.cxx +++ b/sw/source/core/layout/layact.cxx @@ -89,13 +89,6 @@ void SwLayAction::CheckWaitCursor() } } -// Time over already? -inline void SwLayAction::CheckIdleEnd() -{ - if ( !IsInput() ) - m_bInput = bool(GetInputType()) && Application::AnyInput( GetInputType() ); -} - void SwLayAction::SetStatBar( bool bNew ) { if ( bNew ) @@ -253,19 +246,19 @@ void SwLayAction::PaintContent( const SwContentFrame *pCnt, } } -SwLayAction::SwLayAction( SwRootFrame *pRt, SwViewShellImp *pI ) : - m_pRoot( pRt ), +SwLayAction::SwLayAction(SwRootFrame *pRt, SwViewShellImp *pI, TaskStopwatch* pWatch) + : m_pRoot(pRt), m_pImp( pI ), + m_pWatch(pWatch), m_pOptTab( nullptr ), m_nPreInvaPage( USHRT_MAX ), m_nStartTicks( std::clock() ), - m_nInputType( VclInputFlags::NONE ), m_nEndPage( USHRT_MAX ), m_nCheckPageNum( USHRT_MAX ) { m_bPaintExtraData = ::IsExtraData( m_pImp->GetShell()->GetDoc() ); m_bPaint = m_bComplete = m_bWaitAllowed = m_bCheckPages = true; - m_bInput = m_bAgain = m_bNextCycle = m_bCalcLayout = m_bIdle = m_bReschedule = + m_bInterrupt = m_bAgain = m_bNextCycle = m_bCalcLayout = m_bReschedule = m_bUpdateExpFields = m_bBrowseActionStop = m_bActionInProgress = false; // init new flag <mbFormatContentOnInterrupt>. mbFormatContentOnInterrupt = false; @@ -280,14 +273,18 @@ SwLayAction::~SwLayAction() m_pImp->m_pLayAction = nullptr; // unregister } +bool SwLayAction::IsInterrupt() +{ + return m_bInterrupt || (m_pWatch && m_pWatch->exceededRuntime()); +} + void SwLayAction::Reset() { m_pOptTab = nullptr; m_nStartTicks = std::clock(); - m_nInputType = VclInputFlags::NONE; m_nEndPage = m_nPreInvaPage = m_nCheckPageNum = USHRT_MAX; m_bPaint = m_bComplete = m_bWaitAllowed = m_bCheckPages = true; - m_bInput = m_bAgain = m_bNextCycle = m_bCalcLayout = m_bIdle = m_bReschedule = + m_bInterrupt = m_bAgain = m_bNextCycle = m_bCalcLayout = m_bReschedule = m_bUpdateExpFields = m_bBrowseActionStop = false; } @@ -446,7 +443,7 @@ void SwLayAction::InternalAction(OutputDevice* pRenderContext) IDocumentLayoutAccess& rLayoutAccess = m_pRoot->GetFormat()->getIDocumentLayoutAccess(); bool bNoLoop = pPage && SwLayouter::StartLoopControl( m_pRoot->GetFormat()->GetDoc(), pPage ); sal_uInt16 nPercentPageNum = 0; - while ( (pPage && !IsInterrupt()) || m_nCheckPageNum != USHRT_MAX ) + while ((!IsInterrupt() && pPage) || (m_nCheckPageNum != USHRT_MAX)) { // note: this is the only place that consumes and resets m_nCheckPageNum if ((IsInterrupt() || !pPage) && m_nCheckPageNum != USHRT_MAX) @@ -570,7 +567,7 @@ void SwLayAction::InternalAction(OutputDevice* pRenderContext) pPage->InvalidateFlyLayout(); pPage->InvalidateFlyContent(); if ( IsBrowseActionStop() ) - m_bInput = true; + m_bInterrupt = true; } } if( bNoLoop ) @@ -588,7 +585,8 @@ void SwLayAction::InternalAction(OutputDevice* pRenderContext) pPage->ValidateFlyLayout(); pPage->ValidateFlyContent(); } - if ( !IsInterrupt() ) + + if (!IsInterrupt()) { SetNextCycle( false ); @@ -629,8 +627,8 @@ void SwLayAction::InternalAction(OutputDevice* pRenderContext) if( bNoLoop ) rLayoutAccess.GetLayouter()->LoopControl( pPage ); } - CheckIdleEnd(); } + if ( !pPage && !IsInterrupt() && (m_pRoot->IsSuperfluous() || m_pRoot->IsAssertFlyPages()) ) { @@ -656,6 +654,7 @@ void SwLayAction::InternalAction(OutputDevice* pRenderContext) pPage = static_cast<SwPageFrame*>(pPage->GetNext()); } } + if ( IsInterrupt() && pPage ) { // If we have input, we don't want to format content anymore, but @@ -684,7 +683,7 @@ void SwLayAction::InternalAction(OutputDevice* pRenderContext) pPg = pPg ? static_cast<SwPageFrame*>(pPg->GetPrev()) : pPage; // set flag for interrupt content formatting - mbFormatContentOnInterrupt = IsInput(); + mbFormatContentOnInterrupt = IsInterrupt(); long nBottom = rVis.Bottom(); // #i42586# - format current page, if idle action is active // This is an optimization for the case that the interrupt is created by @@ -778,7 +777,6 @@ void SwLayAction::InternalAction(OutputDevice* pRenderContext) bool SwLayAction::TurboAction_( const SwContentFrame *pCnt ) { - const SwPageFrame *pPage = nullptr; if ( !pCnt->isFrameAreaDefinitionValid() || pCnt->IsCompletePaint() || pCnt->IsRetouche() ) { @@ -839,10 +837,7 @@ bool SwLayAction::TurboAction() if ( m_pRoot->GetTurbo() ) { if ( !TurboAction_( m_pRoot->GetTurbo() ) ) - { - CheckIdleEnd(); bRet = false; - } m_pRoot->ResetTurbo(); } else @@ -1688,7 +1683,6 @@ bool SwLayAction::FormatContent(SwPageFrame *const pPage) // paragraph has been processed. if (!pTab || !bInValid) { - CheckIdleEnd(); // consider interrupt formatting. if ( ( IsInterrupt() && !mbFormatContentOnInterrupt ) || ( !bBrowse && pPage->IsInvalidLayout() ) || @@ -1781,7 +1775,6 @@ bool SwLayAction::FormatContent(SwPageFrame *const pPage) PaintContent( pContent, pPage, pContent->getFrameArea(), pContent->getFrameArea().Bottom()); if ( IsIdle() ) { - CheckIdleEnd(); // consider interrupt formatting. if ( IsInterrupt() && !mbFormatContentOnInterrupt ) return false; @@ -1877,7 +1870,6 @@ void SwLayAction::FormatFlyContent( const SwFlyFrame *pFly ) // If there's input, we interrupt processing. if ( !pFly->IsFlyInContentFrame() ) { - CheckIdleEnd(); // consider interrupt formatting. if ( IsInterrupt() && !mbFormatContentOnInterrupt ) return; @@ -1887,6 +1879,11 @@ void SwLayAction::FormatFlyContent( const SwFlyFrame *pFly ) CheckWaitCursor(); } +bool SwLayIdle::IsInterrupt() +{ + return m_aWatch.exceededRuntime(); +} + bool SwLayIdle::DoIdleJob_( const SwContentFrame *pCnt, IdleJobType eJob ) { OSL_ENSURE( pCnt->IsTextFrame(), "NoText neighbour of Text" ); @@ -1970,7 +1967,7 @@ bool SwLayIdle::DoIdleJob_( const SwContentFrame *pCnt, IdleJobType eJob ) bPageValid = bPageValid && (SwTextNode::WrongState::TODO != pTextNode->GetWrongDirty()); if ( aRepaint.HasArea() ) pImp->GetShell()->InvalidateWindows( aRepaint ); - if (Application::AnyInput(VCL_INPUT_ANY & VclInputFlags(~VclInputFlags::TIMER))) + if (IsInterrupt()) return true; break; } @@ -1978,7 +1975,7 @@ bool SwLayIdle::DoIdleJob_( const SwContentFrame *pCnt, IdleJobType eJob ) const_cast<SwTextFrame*>(pTextFrame)->CollectAutoCmplWrds(*pTextNode, nPos); // note: bPageValid remains true here even if the cursor // position is skipped, so no PENDING state needed currently - if (Application::AnyInput(VCL_INPUT_ANY & VclInputFlags(~VclInputFlags::TIMER))) + if (IsInterrupt()) return true; break; case WORD_COUNT : @@ -1986,7 +1983,7 @@ bool SwLayIdle::DoIdleJob_( const SwContentFrame *pCnt, IdleJobType eJob ) const sal_Int32 nEnd = pTextNode->GetText().getLength(); SwDocStat aStat; pTextNode->CountWords( aStat, 0, nEnd ); - if ( Application::AnyInput() ) + if (IsInterrupt()) return true; break; } @@ -2001,7 +1998,7 @@ bool SwLayIdle::DoIdleJob_( const SwContentFrame *pCnt, IdleJobType eJob ) // handle smarttag problems gracefully and provide diagnostics SAL_WARN( "sw.core", "SMART_TAGS: " << e); } - if (Application::AnyInput(VCL_INPUT_ANY & VclInputFlags(~VclInputFlags::TIMER))) + if (IsInterrupt()) return true; break; } @@ -2193,9 +2190,7 @@ SwLayIdle::SwLayIdle( SwRootFrame *pRt, SwViewShellImp *pI ) : bool bInterrupt(false); { - SwLayAction aAction( pRoot, pImp ); - aAction.SetInputType( VCL_INPUT_ANY ); - aAction.SetIdle( true ); + SwLayAction aAction(pRoot, pImp, &m_aWatch); aAction.SetWaitAllowed( false ); aAction.Action(pImp->GetShell()->GetOut()); bInterrupt = aAction.IsInterrupt(); diff --git a/sw/source/core/view/viewsh.cxx b/sw/source/core/view/viewsh.cxx index 66775b933a8d..0095d134ba6d 100644 --- a/sw/source/core/view/viewsh.cxx +++ b/sw/source/core/view/viewsh.cxx @@ -291,7 +291,6 @@ void SwViewShell::ImplEndAction( const bool bIdleEnd ) aAction.SetComplete( false ); if ( mnLockPaint ) aAction.SetPaint( false ); - aAction.SetInputType( VclInputFlags::KEYBOARD ); aAction.Action(GetWin()); } commit 95a422aa7105a94f6b0b78246d83df3180a74ecc Author: Stephan Bergmann <sberg...@redhat.com> AuthorDate: Fri Aug 16 09:56:29 2019 +0200 Commit: Samuel Mehrbrodt <samuel.mehrbr...@allotropia.de> CommitDate: Tue Jan 18 09:57:59 2022 +0100 Drop bogus check from TimerTest::testStopwatch (that had been added with 6e13585508ca3c9b66c6571ad1eb42bfcb66ef0b "Add a TaskStopwatch to interrupt idle loops"). For each StopwatchIdle, m_nIters counts the calls to Invoke before it calls Stop (which it calls based on tools::Time::GetSystemTicks calculations). But the number of such GetSystemTicks() spent in each Invoke is nondeterministic (it can e.g. be affected by the overall system load), so a2Idle may Stop prior to a1Idle and thus have a lower nIter2 than nIter1. Change-Id: I416eee9774c3605be25e9832b24dec7d9dcb00c2 Reviewed-on: https://gerrit.libreoffice.org/77561 Tested-by: Jenkins Reviewed-by: Stephan Bergmann <sberg...@redhat.com> (cherry picked from commit 92e42a0fde32e3f2dbe2c786a0e41547e4912b4b) diff --git a/vcl/qa/cppunit/timer.cxx b/vcl/qa/cppunit/timer.cxx index 0531c0c7f99c..5154c1947a75 100644 --- a/vcl/qa/cppunit/timer.cxx +++ b/vcl/qa/cppunit/timer.cxx @@ -603,7 +603,6 @@ void TimerTest::testStopwatch() bool b1Done = a1Idle.isDone(n1Iter); bool b2Done = a2Idle.isDone(n2Iter); - CPPUNIT_ASSERT(n1Iter >= n2Iter); if (b1Done && b2Done) break; commit 7ab267ef5e2d86a2bdb0d67f2ab6294e0ee6fc95 Author: Jan-Marek Glogowski <glo...@fbihome.de> AuthorDate: Sun Jul 7 23:09:15 2019 +0200 Commit: Samuel Mehrbrodt <samuel.mehrbr...@allotropia.de> CommitDate: Tue Jan 18 09:57:47 2022 +0100 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 <glo...@fbihome.de> (cherry picked from commit 6e13585508ca3c9b66c6571ad1eb42bfcb66ef0b) 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 23decf3b7ec2..ebbd7c3682d1 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 01eef078fae8..0531c0c7f99c 100644 --- a/vcl/qa/cppunit/timer.cxx +++ b/vcl/qa/cppunit/timer.cxx @@ -16,6 +16,7 @@ #include <salhelper/thread.hxx> #include <chrono> +#include <vcl/TaskStopwatch.hxx> #include <vcl/timer.hxx> #include <vcl/idle.hxx> #include <vcl/svapp.hxx> @@ -75,6 +76,8 @@ public: void testPriority(); void testRoundRobin(); + void testStopwatch(); + CPPUNIT_TEST_SUITE(TimerTest); CPPUNIT_TEST(testIdle); #ifndef _WIN32 @@ -96,6 +99,8 @@ public: CPPUNIT_TEST(testPriority); CPPUNIT_TEST(testRoundRobin); + CPPUNIT_TEST(testStopwatch); + CPPUNIT_TEST_SUITE_END(); }; @@ -535,6 +540,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();