diff --git a/src/config/iisnode_dev_x64.xml b/src/config/iisnode_dev_x64.xml index 71f1d133..cab240cf 100644 --- a/src/config/iisnode_dev_x64.xml +++ b/src/config/iisnode_dev_x64.xml @@ -59,5 +59,6 @@ Details at http://learn.iis.net/page.aspx/241/configuration-extensibility/ + diff --git a/src/config/iisnode_dev_x86_on_x64.xml b/src/config/iisnode_dev_x86_on_x64.xml index 0516dbdc..aaa0bbc9 100644 --- a/src/config/iisnode_dev_x86_on_x64.xml +++ b/src/config/iisnode_dev_x86_on_x64.xml @@ -59,5 +59,6 @@ Details at http://learn.iis.net/page.aspx/241/configuration-extensibility/ + diff --git a/src/config/iisnode_dev_x86_on_x86.xml b/src/config/iisnode_dev_x86_on_x86.xml index 77804a6e..9606a25e 100644 --- a/src/config/iisnode_dev_x86_on_x86.xml +++ b/src/config/iisnode_dev_x86_on_x86.xml @@ -59,5 +59,6 @@ Details at http://learn.iis.net/page.aspx/241/configuration-extensibility/ + diff --git a/src/config/iisnode_express_schema.xml b/src/config/iisnode_express_schema.xml index e6786b7b..2ad9eddc 100644 --- a/src/config/iisnode_express_schema.xml +++ b/src/config/iisnode_express_schema.xml @@ -59,5 +59,6 @@ Details at http://learn.iis.net/page.aspx/241/configuration-extensibility/ + diff --git a/src/config/iisnode_express_schema_x64.xml b/src/config/iisnode_express_schema_x64.xml index 084df609..ddf6368a 100644 --- a/src/config/iisnode_express_schema_x64.xml +++ b/src/config/iisnode_express_schema_x64.xml @@ -59,5 +59,6 @@ Details at http://learn.iis.net/page.aspx/241/configuration-extensibility/ + diff --git a/src/config/iisnode_schema.xml b/src/config/iisnode_schema.xml index 59f4e0d4..4088c009 100644 --- a/src/config/iisnode_schema.xml +++ b/src/config/iisnode_schema.xml @@ -59,5 +59,6 @@ Details at http://learn.iis.net/page.aspx/241/configuration-extensibility/ + diff --git a/src/config/iisnode_schema_x64.xml b/src/config/iisnode_schema_x64.xml index 873ba749..8d454eae 100644 --- a/src/config/iisnode_schema_x64.xml +++ b/src/config/iisnode_schema_x64.xml @@ -59,5 +59,6 @@ Details at http://learn.iis.net/page.aspx/241/configuration-extensibility/ + diff --git a/src/iisnode/cmoduleconfiguration.cpp b/src/iisnode/cmoduleconfiguration.cpp index 5526cd92..c74d935e 100644 --- a/src/iisnode/cmoduleconfiguration.cpp +++ b/src/iisnode/cmoduleconfiguration.cpp @@ -32,7 +32,8 @@ CModuleConfiguration::CModuleConfiguration() debuggerVirtualDirLength(0), debuggerVirtualDirPhysicalPath(NULL), recycleSignalEnabled(FALSE), - debuggerExtensionDll(NULL) + debuggerExtensionDll(NULL), + idlePageOutTimePeriod(0) { InitializeSRWLock(&this->srwlock); } @@ -865,6 +866,10 @@ HRESULT CModuleConfiguration::ApplyConfigOverrideKeyValue(IHttpContext* context, { CheckError(GetString(valueStart, &config->interceptor, TRUE)); } + else if(0 == stricmp(keyStart, "idlePageOutTimePeriod")) + { + CheckError(GetDWORD(valueStart, &config->idlePageOutTimePeriod)); + } return S_OK; Error: @@ -1223,6 +1228,7 @@ HRESULT CModuleConfiguration::GetConfig(IHttpContext* context, CModuleConfigurat CheckError(GetString(section, L"configOverrides", &c->configOverrides)); CheckError(GetString(section, L"nodeProcessCommandLine", &c->nodeProcessCommandLine)); CheckError(GetString(section, L"interceptor", &c->interceptor)); + CheckError(GetDWORD(section, L"idlePageOutTimePeriod", &c->idlePageOutTimePeriod)); // debuggerPathSegment @@ -1299,6 +1305,11 @@ LPWSTR CModuleConfiguration::GetNodeProcessCommandLine(IHttpContext* ctx) GETCONFIG(nodeProcessCommandLine) } +DWORD CModuleConfiguration::GetIdlePageOutTimePeriod(IHttpContext* ctx) +{ + GETCONFIG(idlePageOutTimePeriod) +} + LPWSTR CModuleConfiguration::GetInterceptor(IHttpContext* ctx) { GETCONFIG(interceptor) diff --git a/src/iisnode/cmoduleconfiguration.h b/src/iisnode/cmoduleconfiguration.h index 70ea22e7..c951fca3 100644 --- a/src/iisnode/cmoduleconfiguration.h +++ b/src/iisnode/cmoduleconfiguration.h @@ -28,6 +28,7 @@ class CModuleConfiguration : public IHttpStoredContext DWORD maxLogFiles; BOOL loggingEnabled; BOOL debuggingEnabled; + DWORD idlePageOutTimePeriod; PWSTR debuggerExtensionDll; BOOL debugHeaderEnabled; BOOL recycleSignalEnabled; @@ -90,6 +91,7 @@ class CModuleConfiguration : public IHttpStoredContext static HRESULT GetConfig(IHttpContext* context, CModuleConfiguration** config); + static DWORD GetIdlePageOutTimePeriod(IHttpContext* ctx); static DWORD GetAsyncCompletionThreadCount(IHttpContext* ctx); static DWORD GetNodeProcessCountPerApplication(IHttpContext* ctx); static LPWSTR GetNodeProcessCommandLine(IHttpContext* ctx); diff --git a/src/iisnode/cnodeapplication.cpp b/src/iisnode/cnodeapplication.cpp index 5a4e079b..d5dfc573 100644 --- a/src/iisnode/cnodeapplication.cpp +++ b/src/iisnode/cnodeapplication.cpp @@ -1,10 +1,50 @@ #include "precomp.h" +volatile DWORD CNodeApplication::dwInternalAppId = 0; + CNodeApplication::CNodeApplication(CNodeApplicationManager* applicationManager, BOOL isDebugger, NodeDebugCommand debugCommand, DWORD debugPort) : applicationManager(applicationManager), scriptName(NULL), processManager(NULL), isDebugger(isDebugger), peerApplication(NULL), debugCommand(debugCommand), - debugPort(debugPort), needsRecycling(FALSE), configPath(NULL) + debugPort(debugPort), needsRecycling(FALSE), configPath(NULL), pIdleTimer(NULL), + fIdleCallbackInProgress(FALSE), fRequestsProcessedInLastIdleTimeoutPeriod(FALSE), + fEmptyWorkingSetAtStart(FALSE), dwIdlePageOutTimePeriod(0) +{ +} + +VOID +CALLBACK +CNodeApplication::IdleTimerCallback( + IN PTP_CALLBACK_INSTANCE Instance, + IN PVOID Context, + IN PTP_TIMER Timer + ) { + CNodeApplication* pApplication = (CNodeApplication*) Context; + if(pApplication != NULL) + { + if(!pApplication->fIdleCallbackInProgress) + { + pApplication->fIdleCallbackInProgress = TRUE; + + if(!pApplication->fRequestsProcessedInLastIdleTimeoutPeriod || !pApplication->fEmptyWorkingSetAtStart) + { + pApplication->fEmptyWorkingSetAtStart = TRUE; + pApplication->EmptyWorkingSet(); + if( pApplication->fEmptyW3WPWorkingSet ) + { + SetProcessWorkingSetSize(GetCurrentProcess(), -1, -1); + } + } + + pApplication->fRequestsProcessedInLastIdleTimeoutPeriod = FALSE; + + if(pApplication->pIdleTimer != NULL) + { + pApplication->pIdleTimer->SetTimer(pApplication->dwIdlePageOutTimePeriod, 0); + } + } + pApplication->fIdleCallbackInProgress = FALSE; + } } CNodeApplication::~CNodeApplication() @@ -14,6 +54,13 @@ CNodeApplication::~CNodeApplication() void CNodeApplication::Cleanup() { + if(this->pIdleTimer != NULL) + { + this->pIdleTimer->CancelTimer(); + delete this->pIdleTimer; + this->pIdleTimer = NULL; + } + this->GetApplicationManager()->GetFileWatcher()->RemoveWatch(this); if (NULL != this->scriptName) @@ -41,6 +88,26 @@ HRESULT CNodeApplication::Initialize(PCWSTR scriptName, IHttpContext* context) CheckNull(scriptName); + if(CModuleConfiguration::GetIdlePageOutTimePeriod(context) > 0) + { + dwIdlePageOutTimePeriod = CModuleConfiguration::GetIdlePageOutTimePeriod(context); + if(dwInternalAppId == 0) + { + dwInternalAppId ++; + this->fEmptyW3WPWorkingSet = TRUE; + } + if(pIdleTimer == NULL) + { + pIdleTimer = new STTIMER; + ErrorIf(pIdleTimer == NULL, E_OUTOFMEMORY); + CheckError(pIdleTimer->InitializeTimer( CNodeApplication::IdleTimerCallback, this, 10000, 0)); + } + else + { + pIdleTimer->SetTimer(10000, 0); + } + } + DWORD len = wcslen(scriptName) + 1; ErrorIf(NULL == (this->scriptName = new WCHAR[len]), ERROR_NOT_ENOUGH_MEMORY); wcscpy(this->scriptName, scriptName); @@ -74,6 +141,11 @@ PCWSTR CNodeApplication::GetConfigPath() return this->configPath; } +HRESULT CNodeApplication::EmptyWorkingSet() +{ + return this->processManager->EmptyWorkingSet(); +} + HRESULT CNodeApplication::SetConfigPath(IHttpContext * context) { HRESULT hr = S_OK; @@ -109,6 +181,8 @@ HRESULT CNodeApplication::Dispatch(IHttpContext* context, IHttpEventProvider* pP CheckNull(context); CheckNull(pProvider); + fRequestsProcessedInLastIdleTimeoutPeriod = TRUE; + ErrorIf(NULL == (*ctx = new CNodeHttpStoredContext(this, this->GetApplicationManager()->GetEventProvider(), context)), ERROR_NOT_ENOUGH_MEMORY); IHttpModuleContextContainer* moduleContextContainer = context->GetModuleContextContainer(); moduleContextContainer->SetModuleContext(*ctx, this->GetApplicationManager()->GetModuleId()); diff --git a/src/iisnode/cnodeapplication.h b/src/iisnode/cnodeapplication.h index 8b3190bd..035fcaba 100644 --- a/src/iisnode/cnodeapplication.h +++ b/src/iisnode/cnodeapplication.h @@ -10,6 +10,7 @@ class CNodeApplication { private: + volatile static DWORD dwInternalAppId; PWSTR scriptName; PWSTR configPath; CNodeApplicationManager* applicationManager; @@ -19,6 +20,12 @@ class CNodeApplication NodeDebugCommand debugCommand; DWORD debugPort; BOOL needsRecycling; + STTIMER *pIdleTimer; + BOOL fIdleCallbackInProgress; + BOOL fRequestsProcessedInLastIdleTimeoutPeriod; + BOOL fEmptyW3WPWorkingSet; // flag to indicate that this application is responsible for emptying w3wp working set (only one app should be responsible). + BOOL fEmptyWorkingSetAtStart; + volatile DWORD dwIdlePageOutTimePeriod; void Cleanup(); @@ -27,6 +34,16 @@ class CNodeApplication CNodeApplication(CNodeApplicationManager* applicationManager, BOOL isDebugger, NodeDebugCommand debugCommand, DWORD debugPort); ~CNodeApplication(); + static + VOID + CALLBACK + IdleTimerCallback( + IN PTP_CALLBACK_INSTANCE Instance, + IN PVOID Context, + IN PTP_TIMER Timer + ); + + HRESULT EmptyWorkingSet(); HRESULT Initialize(PCWSTR scriptName, IHttpContext* context); PCWSTR GetScriptName(); PCWSTR GetConfigPath(); diff --git a/src/iisnode/cnodeapplicationmanager.h b/src/iisnode/cnodeapplicationmanager.h index a308d469..b99305d8 100644 --- a/src/iisnode/cnodeapplicationmanager.h +++ b/src/iisnode/cnodeapplicationmanager.h @@ -73,7 +73,6 @@ class CNodeApplicationManager CNodeApplicationManager(IHttpServer* server, HTTP_MODULE_ID moduleId); ~CNodeApplicationManager(); - IHttpServer* GetHttpServer(); HTTP_MODULE_ID GetModuleId(); CAsyncManager* GetAsyncManager(); diff --git a/src/iisnode/cnodehttpmodulefactory.h b/src/iisnode/cnodehttpmodulefactory.h index 2a00554e..8fe84e2b 100644 --- a/src/iisnode/cnodehttpmodulefactory.h +++ b/src/iisnode/cnodehttpmodulefactory.h @@ -5,7 +5,6 @@ class CNodeHttpModuleFactory : public IHttpModuleFactory { CNodeApplicationManager* applicationManager; - public: CNodeHttpModuleFactory(); diff --git a/src/iisnode/cnodeprocessmanager.cpp b/src/iisnode/cnodeprocessmanager.cpp index 3bf18f14..cb6df5d6 100644 --- a/src/iisnode/cnodeprocessmanager.cpp +++ b/src/iisnode/cnodeprocessmanager.cpp @@ -51,6 +51,23 @@ CNodeApplication* CNodeProcessManager::GetApplication() return this->application; } +HRESULT CNodeProcessManager::EmptyWorkingSet() +{ + ENTER_SRW_EXCLUSIVE(this->srwlock) + + for (int i = 0; i < this->processCount; i++) + { + if(this->processes[i] != NULL && !this->processes[i]->HasProcessExited()) + { + SetProcessWorkingSetSize(this->processes[i]->GetProcess(), -1, -1); + } + } + + LEAVE_SRW_EXCLUSIVE(this->srwlock) + + return S_OK; +} + HRESULT CNodeProcessManager::Initialize(IHttpContext* context) { HRESULT hr; diff --git a/src/iisnode/cnodeprocessmanager.h b/src/iisnode/cnodeprocessmanager.h index e3c46cd0..0c41737e 100644 --- a/src/iisnode/cnodeprocessmanager.h +++ b/src/iisnode/cnodeprocessmanager.h @@ -40,6 +40,7 @@ class CNodeProcessManager CNodeProcessManager(CNodeApplication* application, IHttpContext* context); ~CNodeProcessManager(); + HRESULT EmptyWorkingSet(); CNodeApplication* GetApplication(); HRESULT Initialize(IHttpContext* context); HRESULT RecycleProcess(CNodeProcess* process); diff --git a/src/iisnode/cprotocolbridge.cpp b/src/iisnode/cprotocolbridge.cpp index 9071a639..17d16533 100644 --- a/src/iisnode/cprotocolbridge.cpp +++ b/src/iisnode/cprotocolbridge.cpp @@ -719,7 +719,11 @@ void CProtocolBridge::SendHttpRequestHeaders(CNodeHttpStoredContext* context) // to enable named pipe connection pooling request = context->GetHttpContext()->GetRequest(); - CheckError(request->SetHeader(HttpHeaderConnection, "keep-alive", 10, TRUE)); + + if(stricmp(request->GetHeader(HttpHeaderConnection), "upgrade") != 0) + { + CheckError(request->SetHeader(HttpHeaderConnection, "keep-alive", 10, TRUE)); + } // Expect: 100-continue has been processed by IIS - do not propagate it up to node.js since node will // attempt to process it again diff --git a/src/iisnode/precomp.h b/src/iisnode/precomp.h index 19f3e810..a2b6dd7b 100644 --- a/src/iisnode/precomp.h +++ b/src/iisnode/precomp.h @@ -25,6 +25,7 @@ // Project header files #include "errors.h" #include "utils.h" +#include "sttimer.h" #include "cconnectionpool.h" #include "cnodeeventprovider.h" #include "cnodeconstants.h" diff --git a/src/iisnode/sttimer.h b/src/iisnode/sttimer.h new file mode 100644 index 00000000..7e12d957 --- /dev/null +++ b/src/iisnode/sttimer.h @@ -0,0 +1,229 @@ +#ifndef _STTIMER_H +#define _STTIMER_H + +class STTIMER +{ +public: + + STTIMER() + : _pTimer( NULL ) + { + } + + virtual + ~STTIMER() + { + if ( _pTimer ) + { + CancelTimer(); + + CloseThreadpoolTimer( _pTimer ); + + _pTimer = NULL; + } + } + + HRESULT + InitializeTimer( + PTP_TIMER_CALLBACK pfnCallback, + VOID * pContext, + DWORD dwInitialWait = 0, + DWORD dwPeriod = 0 + ) + { + _pTimer = CreateThreadpoolTimer( pfnCallback, + pContext, + NULL ); + + if ( !_pTimer ) + { + return HRESULT_FROM_WIN32( GetLastError() ); + } + + if ( dwInitialWait ) + { + SetTimer( dwInitialWait, + dwPeriod ); + } + + return S_OK; + } + + VOID + SetTimer( + DWORD dwInitialWait, + DWORD dwPeriod = 0 + ) + { + FILETIME ftInitialWait; + + if ( dwInitialWait == 0 && dwPeriod == 0 ) + { + // + // Special case. We are preventing new callbacks + // from being queued. Any existing callbacks in the + // queue will still run. + // + // This effectively disables the timer. It can be + // re-enabled by setting non-zero initial wait or + // period values. + // + + SetThreadpoolTimer( _pTimer, NULL, 0, 0 ); + return; + } + + InitializeRelativeFileTime( &ftInitialWait, dwInitialWait ); + + SetThreadpoolTimer( _pTimer, + &ftInitialWait, + dwPeriod, + 0 ); + } + + VOID + CancelTimer() + { + // + // Disable the timer + // + + SetTimer( 0 ); + + // + // Wait until any callbacks queued prior to disabling + // have completed. + // + + WaitForThreadpoolTimerCallbacks( _pTimer, TRUE ); + } + +private: + + VOID + InitializeRelativeFileTime( + FILETIME * pft, + DWORD dwMilliseconds + ) + { + LARGE_INTEGER li; + + // + // The pftDueTime parameter expects the time to be + // expressed as the number of 100 nanosecond intervals + // times -1. + // + // To convert from milliseconds, we'll multiply by + // -10000 + // + + li.QuadPart = (LONGLONG)dwMilliseconds * -10000; + + pft->dwHighDateTime = li.HighPart; + pft->dwLowDateTime = li.LowPart; + }; + + TP_TIMER * _pTimer; +}; + +class STELAPSED +{ +public: + + STELAPSED() + : _dwInitTime( 0 ), + _dwInitTickCount( 0 ), + _dwPerfCountsPerMillisecond( 0 ), + _fUsingHighResolution( FALSE ) + { + LARGE_INTEGER li; + BOOL fResult; + + _dwInitTickCount = GetTickCount64(); + + fResult = QueryPerformanceFrequency( &li ); + + if ( !fResult ) + { + goto Finished; + } + + _dwPerfCountsPerMillisecond = li.QuadPart / 1000; + + fResult = QueryPerformanceCounter( &li ); + + if ( !fResult ) + { + goto Finished; + } + + _dwInitTime = li.QuadPart / _dwPerfCountsPerMillisecond; + + _fUsingHighResolution = TRUE; + +Finished: + + return; + } + + virtual + ~STELAPSED() + { + } + + LONGLONG + QueryElapsedTime() + { + LARGE_INTEGER li; + + if ( _fUsingHighResolution && QueryPerformanceCounter( &li ) ) + { + DWORD64 dwCurrentTime = li.QuadPart / _dwPerfCountsPerMillisecond; + + if ( dwCurrentTime < _dwInitTime ) + { + // + // It's theoretically possible that QueryPerformanceCounter + // may return slightly different values on different CPUs. + // In this case, we don't want to return an unexpected value + // so we'll return zero. This is acceptable because + // presumably such a case would only happen for a very short + // time window. + // + // It would be possible to prevent this by ensuring processor + // affinity for all calls to QueryPerformanceCounter, but that + // would be undesirable in the general case because it could + // introduce unnecessary context switches and potentially a + // CPU bottleneck. + // + // Note that this issue also applies to callers doing rapid + // calls to this function. If a caller wants to mitigate + // that, they could enforce the affinitization, or they + // could implement a similar sanity check when comparing + // returned values from this function. + // + + return 0; + } + + return dwCurrentTime - _dwInitTime; + } + + return GetTickCount64() - _dwInitTickCount; + } + + BOOL + QueryUsingHighResolution() + { + return _fUsingHighResolution; + } + +private: + + DWORD64 _dwInitTime; + DWORD64 _dwInitTickCount; + DWORD64 _dwPerfCountsPerMillisecond; + BOOL _fUsingHighResolution; +}; + +#endif // _STTIMER_H diff --git a/src/version.txt b/src/version.txt index ad8696b4..4bf518a9 100644 --- a/src/version.txt +++ b/src/version.txt @@ -1 +1 @@ -0.2.15 +0.2.16