From patchwork Thu Aug 18 15:32:17 2016 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: glenvt18 X-Patchwork-Id: 36497 Received: from localhost ([127.0.0.1] helo=www.linuxtv.org) by www.linuxtv.org with esmtp (Exim 4.84_2) (envelope-from ) id 1baPJP-0006ED-P8; Thu, 18 Aug 2016 15:32:55 +0000 Received: from mail.tu-berlin.de ([130.149.7.33]) by www.linuxtv.org with esmtp (Exim 4.84_2) (envelope-from ) id 1baPJK-0006Du-JY for vdr@linuxtv.org; Thu, 18 Aug 2016 15:32:52 +0000 X-tubIT-Incoming-IP: 209.85.215.68 Received: from mail-lf0-f68.google.com ([209.85.215.68]) by mail.tu-berlin.de (exim-4.84_2/mailfrontend-7) with esmtps [TLSv1.2:AES128-GCM-SHA256:128] for id 1baPJI-0006JG-2o; Thu, 18 Aug 2016 17:32:50 +0200 Received: by mail-lf0-f68.google.com with SMTP id l89so1946943lfi.2 for ; Thu, 18 Aug 2016 08:32:48 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20120113; h=from:to:cc:subject:date:message-id:in-reply-to:references; bh=Rlvp4PcYax2UIx95I0Z5tc1kw7PxM/VfDIV3EyJHt48=; b=u+5r70Fejj46k/0zvDEu3V7ZvDjB/Lz8hI9JS+/RMX4E4f5sVn7spV9Byhw6bF5AGK d6mgbhUh96Gff+zvKcC0GdJyKEZa3QDzy0mpsUb9hho+uNUr+CweBB8dtznO/lH635WA kR/r9YKki62dVa+vGtR6QF1qXDWv+CcTLYrmXX22S8qSvK1TBy9xxJoRhfHyXJx1YUym pBnzdRUVNOusCRKUpqP+Jd2gUi/2FffYnI087ghneILc93hrlPJZR7g/Vvb8rgTSZEDc /pvQTX2D4vnFjF7FTU9Y/N+VS77RCi0nT0EG5n5rQeLuLY/DZtzLR4gEeLVlgImUnY/M B51w== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20130820; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references; bh=Rlvp4PcYax2UIx95I0Z5tc1kw7PxM/VfDIV3EyJHt48=; b=gq50hOeTTl1N7hbso1Yc8+kDpA4NvV78hPYD93lO0ogD7rgCjhodGdp0Cu6E8ZXzJv fs04it2YFwPAvkJQ+4oiruBHPm94A4PY5O+AYCtMhbPfIMsRVkSmPIDVxit0cKkSQ0vX HxvxYHIIpc9kuH0B3mMn90pWws4lxHK+Dl5zjVkWfqiX9knH13kMyBp3h5jIpDVTnv18 tJmqjySU+KHaxJF9sgygYSoGPcpjRvwCTDfAQCp71mHbC06P5zB9kDKQYM+VIVUSjyAm wycHHz7X0J3xbqn/Di20ykQc6P4Y4y1qbU6W/3clIgTdLVEr+48zgHN7gI7HdiPJ9RBM ddZA== X-Gm-Message-State: AEkoousOdYWl79yy5Fpkc4iNojzNFLkCAV7Vh3OgDlLihYV9Gq5wsEve/XmQiaqcMsKuVA== X-Received: by 10.25.84.132 with SMTP id i126mr689167lfb.116.1471534367623; Thu, 18 Aug 2016 08:32:47 -0700 (PDT) Received: from localhost.localdomain ([185.135.148.114]) by smtp.gmail.com with ESMTPSA id g74sm404777ljg.24.2016.08.18.08.32.46 (version=TLS1_2 cipher=ECDHE-RSA-AES128-SHA bits=128/128); Thu, 18 Aug 2016 08:32:47 -0700 (PDT) From: glenvt18 To: VDR Mailing List Date: Thu, 18 Aug 2016 18:32:17 +0300 Message-Id: <1471534337-23889-1-git-send-email-glenvt18@gmail.com> X-Mailer: git-send-email 2.7.4 In-Reply-To: References: X-PMX-Version: 6.0.0.2142326, Antispam-Engine: 2.7.2.2107409, Antispam-Data: 2016.8.18.152417 X-PMX-Spam: Gauge=IIIIIIII, Probability=8%, Report=' FROM_NAME_ONE_WORD 0.05, HTML_00_01 0.05, HTML_00_10 0.05, BODY_SIZE_10000_PLUS 0, DKIM_SIGNATURE 0, IN_REP_TO 0, LEGITIMATE_NEGATE 0, LEGITIMATE_SIGNS 0, MSG_THREAD 0, NO_URI_HTTPS 0, REFERENCES 0, WEBMAIL_SOURCE 0, __ANY_URI 0, __CC_NAME 0, __CP_URI_IN_BODY 0, __DATE_TZ_RU 0, __DQ_NEG_HEUR 0, __DQ_NEG_IP 0, __FRAUD_WEBMAIL 0, __FRAUD_WEBMAIL_FROM 0, __FROM_DOMAIN_IN_ANY_CC2 0, __FROM_DOMAIN_IN_RCPT 0, __FROM_GMAIL 0, __HAS_CC_HDR 0, __HAS_FROM 0, __HAS_MSGID 0, __HAS_X_MAILER 0, __IN_REP_TO 0, __MAL_TELEKOM_URI 0, __MIME_TEXT_ONLY 0, __MULTIPLE_URI_TEXT 0, __PHISH_SPEAR_STRUCTURE_1 0, __RDNS_GMAIL 0, __REFERENCES 0, __SANE_MSGID 0, __SUBJ_ALPHA_END 0, __TO_MALFORMED_2 0, __TO_NAME 0, __TO_NAME_DIFF_FROM_ACC 0, __TO_REAL_NAMES 0, __URI_IN_BODY 0, __URI_NO_MAILTO 0, __URI_NO_WWW 0, __URI_NS , __URI_WITH_PATH 0, __YOUTUBE_RCVD 0' X-LSpam-Score: 0.8 (/) X-LSpam-Report: No, score=0.8 required=5.0 tests=FREEMAIL_ENVFROM_END_DIGIT=0.25, FREEMAIL_FROM=0.001, RCVD_IN_DNSWL_LOW=-0.7, RCVD_IN_MSPIKE_H3=-0.01, RCVD_IN_MSPIKE_WL=-0.01, RDNS_NONE=1.274, T_DKIM_INVALID=0.01 autolearn=no autolearn_force=no Subject: [vdr] [PATCH v2] Device power saving feature X-BeenThere: vdr@linuxtv.org X-Mailman-Version: 2.1.18 Precedence: list List-Id: VDR Mailing List List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: VDR Mailing List MIME-Version: 1.0 Errors-To: vdr-bounces@linuxtv.org Sender: "vdr" I've renamed PowerDown() to PowerDownMode() as Lars suggested and added a comment on IsTunedToTransponder(). Klaus, could you take a look at this please. A copy of this patch: http://pastebin.com/skxw80hN VDR 2.2.0 version: http://pastebin.com/JQLjMKvu Add a newline to the end of a patch file downloaded from pastebin.com. This patch introduces a feature which allows an idle device (a device which is not currently recording or streaming) to enter a power-down mode after some period of time. Given two timeout values, PowerdownTimeoutM and PowerdownWakeupH, it works like this: when a device becomes idle, it is kept powered up for PowerdownTimeoutM minutes doing, for instance, an EPG scan before it is powered down. If the device is still idle and has been powered down for PowerdownWakeupH hours it is powered up for PowerdownTimeoutM minutes and so on. When recording, streaming or a forced EPG scan starts, the device is powered up and it's idle timer is disabled. This implies that PowerdownTimeoutM should be enough for a full round of EPG scanning (20 seconds * number_of_transponders). Another option is to run EPG scans from cron (at night) and use SVDRP SCAN command. Actual implementation of power saving facilities is left to a derived device class. In the case of a DVB device it is implemented by closing it's frontend device. For a DVB-S/S2 tuner this usually means powering the LNB off. My measurements show 3-4W power consumption drops per tuner for various DVB-S/S2 tuners. So, this feature (together with HDD spin-down) is especially valuable while running a headless 24/7 VDR server and/or using several tuners. A SATIP device can also implement power saving if it is supported by a server. --- config.c | 9 ++++++ config.h | 3 ++ device.c | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- device.h | 31 ++++++++++++++++++++ dvbdevice.c | 39 +++++++++++++++++++++++++ dvbdevice.h | 7 +++++ eitscan.c | 7 ++++- menu.c | 9 +++++- vdr.c | 6 ++++ 9 files changed, 203 insertions(+), 4 deletions(-) diff --git a/config.c b/config.c index e5f5463..794c9f8 100644 --- a/config.c +++ b/config.c @@ -395,6 +395,9 @@ cSetup::cSetup(void) PositionerSpeed = 15; PositionerSwing = 650; PositionerLastLon = 0; + PowerdownEnabled = 0; + PowerdownTimeoutM = 15; + PowerdownWakeupH = 4; SetSystemTime = 0; TimeSource = 0; TimeTransponder = 0; @@ -622,6 +625,9 @@ bool cSetup::Parse(const char *Name, const char *Value) else if (!strcasecmp(Name, "PositionerSpeed")) PositionerSpeed = atoi(Value); else if (!strcasecmp(Name, "PositionerSwing")) PositionerSwing = atoi(Value); else if (!strcasecmp(Name, "PositionerLastLon")) PositionerLastLon = atoi(Value); + else if (!strcasecmp(Name, "PowerdownEnabled")) PowerdownEnabled = atoi(Value); + else if (!strcasecmp(Name, "PowerdownTimeoutM")) PowerdownTimeoutM = atoi(Value); + else if (!strcasecmp(Name, "PowerdownWakeupH")) PowerdownWakeupH = atoi(Value); else if (!strcasecmp(Name, "SetSystemTime")) SetSystemTime = atoi(Value); else if (!strcasecmp(Name, "TimeSource")) TimeSource = cSource::FromString(Value); else if (!strcasecmp(Name, "TimeTransponder")) TimeTransponder = atoi(Value); @@ -753,6 +759,9 @@ bool cSetup::Save(void) Store("PositionerSpeed", PositionerSpeed); Store("PositionerSwing", PositionerSwing); Store("PositionerLastLon", PositionerLastLon); + Store("PowerdownEnabled", PowerdownEnabled); + Store("PowerdownTimeoutM", PowerdownTimeoutM); + Store("PowerdownWakeupH", PowerdownWakeupH); Store("SetSystemTime", SetSystemTime); Store("TimeSource", cSource::ToString(TimeSource)); Store("TimeTransponder", TimeTransponder); diff --git a/config.h b/config.h index e5565da..7a73d9d 100644 --- a/config.h +++ b/config.h @@ -273,6 +273,9 @@ public: int PositionerSpeed; int PositionerSwing; int PositionerLastLon; + int PowerdownEnabled; + int PowerdownTimeoutM; + int PowerdownWakeupH; int SetSystemTime; int TimeSource; int TimeTransponder; diff --git a/device.c b/device.c index 542d120..adbe973 100644 --- a/device.c +++ b/device.c @@ -104,6 +104,9 @@ cDevice::cDevice(void) dvbSubtitleConverter = NULL; autoSelectPreferredSubtitleLanguage = true; + idleTimerExpires = time(NULL) + Setup.PowerdownTimeoutM * 60; + wakeupTimerExpires = 0; + for (int i = 0; i < MAXRECEIVERS; i++) receiver[i] = NULL; @@ -745,6 +748,11 @@ bool cDevice::SwitchChannel(int Direction) return result; } +// While switching to a channel, the device will be kept powered up +// for at least this number of seconds before a receiver is attached. +// Must be less than cEITScanner::ScanTimeout. +#define CHANNEL_SWITCH_POWERUP_TIMEOUT 10 + eSetChannelResult cDevice::SetChannel(const cChannel *Channel, bool LiveView) { cStatus::MsgChannelSwitch(this, 0, LiveView); @@ -778,6 +786,8 @@ eSetChannelResult cDevice::SetChannel(const cChannel *Channel, bool LiveView) Result = scrNotAvailable; } else { + // Power up the device + PowerUp(CHANNEL_SWITCH_POWERUP_TIMEOUT); // Stop section handling: if (sectionHandler) { sectionHandler->SetStatus(false); @@ -843,8 +853,11 @@ int cDevice::Occupied(void) const void cDevice::SetOccupied(int Seconds) { - if (Seconds >= 0) + if (Seconds >= 0) { occupiedTimeout = time(NULL) + min(Seconds, MAXOCCUPIEDTIMEOUT); + // avoid short power-down/power-up cycles + SetIdleTimer(true, Seconds + 30); + } } bool cDevice::SetChannelDevice(const cChannel *Channel, bool LiveView) @@ -1675,6 +1688,7 @@ bool cDevice::AttachReceiver(cReceiver *Receiver) startScrambleDetection = time(NULL); } Start(); + SetIdleTimer(false); return true; } } @@ -1708,8 +1722,10 @@ void cDevice::Detach(cReceiver *Receiver) camSlot->Assign(NULL); } } - if (!receiversLeft) + if (!receiversLeft) { Cancel(-1); + SetIdleTimer(true); + } } void cDevice::DetachAll(int Pid) @@ -1731,6 +1747,82 @@ void cDevice::DetachAllReceivers(void) Detach(receiver[i]); } +void cDevice::CheckIdle(void) +{ + if (!SupportsPowerDown() || !Setup.PowerdownEnabled) + return; + cMutexLock MutexLock(&mutexPowerSaving); + if (idleTimerExpires != 0 && time(NULL) > idleTimerExpires) { + // idle, powered up + dsyslog("power saving: device %d idle timer expired", CardIndex() + 1); + SetIdleTimer(false); + if (Setup.PowerdownWakeupH != 0) + wakeupTimerExpires = time(NULL) + Setup.PowerdownWakeupH * 3600; + else + dsyslog("power saving: waking up is disabled"); + if (!IsPoweredDown()) { + dsyslog("power saving: powering device %d down", CardIndex() + 1); + if (sectionHandler) { + sectionHandler->SetStatus(false); + sectionHandler->SetChannel(NULL); + } + PowerDownMode(true); + } + } + if (wakeupTimerExpires != 0 && time(NULL) > wakeupTimerExpires) { + // idle, powered down + dsyslog("power saving: device %d wakeup timer expired", CardIndex() + 1); + SetIdleTimer(true); + if (IsPoweredDown()) { + dsyslog("power saving: waking up device %d", CardIndex() + 1); + PowerDownMode(false); + } + } +} + +void cDevice::SetIdleTimer(bool On, int ExtraTimeoutS) +{ + if (!SupportsPowerDown()) + return; + cMutexLock MutexLock(&mutexPowerSaving); + if (On) { + int Tout = Setup.PowerdownTimeoutM * 60; + time_t Now = time(NULL); + if (ExtraTimeoutS > 0) { + if (idleTimerExpires >= Now + ExtraTimeoutS) + return; + Tout = ExtraTimeoutS; + } + idleTimerExpires = Now + Tout; + if (Setup.PowerdownEnabled) + dsyslog("power saving: set device %d idle timer to %d sec", CardIndex() + 1, Tout); + } + else { + idleTimerExpires = 0; + if (Setup.PowerdownEnabled) + dsyslog("power saving: disable device %d idle timer", CardIndex() + 1); + } + wakeupTimerExpires = 0; +} + +bool cDevice::PoweredDown(void) +{ + if (SupportsPowerDown() && Setup.PowerdownEnabled) { + cMutexLock MutexLock(&mutexPowerSaving); + return IsPoweredDown(); + } + else + return false; +} + +void cDevice::PowerUp(int ExtraTimeoutS) +{ + cMutexLock MutexLock(&mutexPowerSaving); + SetIdleTimer(true, ExtraTimeoutS); + if (SupportsPowerDown() && IsPoweredDown()) + PowerDownMode(false); +} + // --- cTSBuffer ------------------------------------------------------------- cTSBuffer::cTSBuffer(int File, int Size, int CardIndex) diff --git a/device.h b/device.h index 31ee303..cc40bb7 100644 --- a/device.h +++ b/device.h @@ -821,6 +821,37 @@ public: ///< Detaches all receivers from this device for this pid. virtual void DetachAllReceivers(void); ///< Detaches all receivers from this device. + +// Power saving facilities + +private: + cMutex mutexPowerSaving; + time_t idleTimerExpires, wakeupTimerExpires; + void PowerUp(int ExtraTimeoutS); + ///< If the device is powered down, powers it up and keeps it + ///< powered up for at least ExtraTimeoutS seconds (see + ///< cDevice::SetIdleTimer()). +public: + void CheckIdle(void); + ///< Should be called periodically in the main loop. + bool PoweredDown(void); + ///< Returns true if the device is powered down "logically", that is, + ///< idle tasks like EPG scanning are disabled. + void SetIdleTimer(bool On, int ExtraTimeoutS = 0); + ///< Starts/disables the idle timer. This timer must be started when + ///< a device gets idle and must be disabled when it is receiving. + ///< If ExtraTimeoutS is greater than zero and On is true, a new timer + ///< won't be set, but the device will be kept powered up for at least + ///< ExtraTimeoutS seconds. +protected: + ///< NOTE: IsTunedToTransponder() should return false if the + ///< device is powered down. + virtual bool IsPoweredDown(void) {return false;} + ///< Returns true if the device is powered down "physically". + virtual void PowerDownMode(bool On) {}; + ///< Actually powers the device down/up. + virtual bool SupportsPowerDown() {return false;} + ///< Returns true if a derived device supports power saving. }; /// Derived cDevice classes that can receive channels will have to provide diff --git a/dvbdevice.c b/dvbdevice.c index 63af52e..87555b7 100644 --- a/dvbdevice.c +++ b/dvbdevice.c @@ -348,6 +348,8 @@ public: const cPositioner *Positioner(void) const { return positioner; } int GetSignalStrength(void) const; int GetSignalQuality(void) const; + bool IsPoweredDown(void) {return fd_frontend < 0;} + void PowerDownMode(bool On); }; cMutex cDvbTuner::bondMutex; @@ -544,6 +546,8 @@ void cDvbTuner::ClearEventQueue(void) const bool cDvbTuner::GetFrontendStatus(fe_status_t &Status) const { + if (fd_frontend < 0) + return false; ClearEventQueue(); while (1) { if (ioctl(fd_frontend, FE_READ_STATUS, &Status) != -1) @@ -559,6 +563,8 @@ bool cDvbTuner::GetFrontendStatus(fe_status_t &Status) const int cDvbTuner::GetSignalStrength(void) const { + if (fd_frontend < 0) + return -1; ClearEventQueue(); uint16_t Signal; while (1) { @@ -1001,6 +1007,26 @@ void cDvbTuner::Action(void) } } +void cDvbTuner::PowerDownMode(bool On) +{ + cMutexLock MutexLock(&mutex); + if (On && fd_frontend >= 0) { + isyslog("dvb tuner: power-down - closing frontend %d/%d", adapter, frontend); + tunerStatus = tsIdle; + close(fd_frontend); + fd_frontend = -1; + } + if (!On && fd_frontend < 0) { + cString Filename = cString::sprintf("%s/%s%d/%s%d", + DEV_DVB_BASE, DEV_DVB_ADAPTER, adapter, DEV_DVB_FRONTEND, frontend); + isyslog("dvb tuner: power-up - opening frontend %d/%d", adapter, frontend); + fd_frontend = open(Filename, O_RDWR | O_NONBLOCK); + if (fd_frontend < 0) + esyslog("ERROR: can't open DVB device frontend %d/%d", adapter, frontend); + tunerStatus = tsIdle; + } +} + // --- cDvbSourceParam ------------------------------------------------------- class cDvbSourceParam : public cSourceParam { @@ -1712,6 +1738,19 @@ void cDvbDevice::DetachAllReceivers(void) needsDetachBondedReceivers = false; } +bool cDvbDevice::IsPoweredDown(void) +{ + if (dvbTuner) + return dvbTuner->IsPoweredDown(); + return false; +} + +void cDvbDevice::PowerDownMode(bool On) +{ + if (dvbTuner) + dvbTuner->PowerDownMode(On); +} + // --- cDvbDeviceProbe ------------------------------------------------------- cList DvbDeviceProbes; diff --git a/dvbdevice.h b/dvbdevice.h index 5ae4952..e21c652 100644 --- a/dvbdevice.h +++ b/dvbdevice.h @@ -290,6 +290,13 @@ protected: virtual void CloseDvr(void); virtual bool GetTSPacket(uchar *&Data); virtual void DetachAllReceivers(void); + +// Power saving facilities + +protected: + virtual bool IsPoweredDown(void); + virtual void PowerDownMode(bool On); + virtual bool SupportsPowerDown() {return true;} }; // A plugin that implements a DVB device derived from cDvbDevice needs to create diff --git a/eitscan.c b/eitscan.c index 41ac25e..765055c 100644 --- a/eitscan.c +++ b/eitscan.c @@ -144,7 +144,8 @@ void cEITScanner::Process(void) bool AnyDeviceSwitched = false; for (int i = 0; i < cDevice::NumDevices(); i++) { cDevice *Device = cDevice::GetDevice(i); - if (Device && Device->ProvidesEIT()) { + if (Device && Device->ProvidesEIT() + && (!Device->PoweredDown() || lastActivity == 0)) { // powered up or forced scan for (cScanData *ScanData = scanList->First(); ScanData; ScanData = scanList->Next(ScanData)) { const cChannel *Channel = ScanData->GetChannel(); if (Channel) { @@ -165,6 +166,10 @@ void cEITScanner::Process(void) } } //dsyslog("EIT scan: device %d source %-8s tp %5d", Device->DeviceNumber() + 1, *cSource::ToString(Channel->Source()), Channel->Transponder()); + if (lastActivity == 0) + // forced scan - set idle timer for each channel switch; + // this prevents powering down while scanning a transponder + Device->SetIdleTimer(true, ScanTimeout + 5); Device->SwitchChannel(Channel, false); scanList->Del(ScanData); AnyDeviceSwitched = true; diff --git a/menu.c b/menu.c index 569900c..5a89771 100644 --- a/menu.c +++ b/menu.c @@ -3715,6 +3715,12 @@ void cMenuSetupLNB::Setup(void) Add(new cMenuEditIntxItem(tr("Setup.LNB$Positioner speed (degrees/s)"), &data.PositionerSpeed, 1, 1800, 10)); } + Add(new cMenuEditBoolItem(tr("Setup.LNB$Enable power saving"), &data.PowerdownEnabled)); + if (data.PowerdownEnabled) { + Add(new cMenuEditIntItem(tr("Setup.LNB$Power down an idle device after (min)"), &data.PowerdownTimeoutM)); + Add(new cMenuEditIntItem(tr("Setup.LNB$Wake up from power-down after (h)"), &data.PowerdownWakeupH)); + } + SetCurrent(Get(current)); Display(); } @@ -3723,6 +3729,7 @@ eOSState cMenuSetupLNB::ProcessKey(eKeys Key) { int oldDiSEqC = data.DiSEqC; int oldUsePositioner = data.UsePositioner; + int oldPowerdownEnabled = data.PowerdownEnabled; bool DeviceBondingsChanged = false; if (Key == kOk) { cString NewDeviceBondings = satCableNumbers.ToString(); @@ -3731,7 +3738,7 @@ eOSState cMenuSetupLNB::ProcessKey(eKeys Key) } eOSState state = cMenuSetupBase::ProcessKey(Key); - if (Key != kNone && (data.DiSEqC != oldDiSEqC || data.UsePositioner != oldUsePositioner)) + if (Key != kNone && (data.DiSEqC != oldDiSEqC || data.UsePositioner != oldUsePositioner || data.PowerdownEnabled != oldPowerdownEnabled)) Setup(); else if (DeviceBondingsChanged) cDvbDevice::BondDevices(data.DeviceBondings); diff --git a/vdr.c b/vdr.c index 6b0bf2b..c8de702 100644 --- a/vdr.c +++ b/vdr.c @@ -1515,6 +1515,12 @@ int main(int argc, char *argv[]) ReportEpgBugFixStats(); + for (int i = 0; i < cDevice::NumDevices(); i++) { + cDevice *d = cDevice::GetDevice(i); + if (d) + d->CheckIdle(); + } + // Main thread hooks of plugins: PluginManager.MainThreadHook(); }