diff --git a/caddy/admin_test.go b/caddy/admin_test.go index 09576d3f8..f5ec8c4c7 100644 --- a/caddy/admin_test.go +++ b/caddy/admin_test.go @@ -82,6 +82,46 @@ func TestShowTheCorrectThreadDebugStatus(t *testing.T) { assert.Len(t, debugState.ThreadDebugStates, 3) } +func TestThreadDebugStateMetricsAfterRequests(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + admin localhost:2999 + http_port `+testPort+` + + frankenphp { + num_threads 2 + worker ../testdata/worker-with-counter.php 1 + } + } + + localhost:`+testPort+` { + route { + root ../testdata + rewrite worker-with-counter.php + php + } + } + `, "caddyfile") + + // make a few requests so counters are populated + tester.AssertGetResponse("http://localhost:"+testPort+"/", http.StatusOK, "requests:1") + tester.AssertGetResponse("http://localhost:"+testPort+"/", http.StatusOK, "requests:2") + tester.AssertGetResponse("http://localhost:"+testPort+"/", http.StatusOK, "requests:3") + + debugState := getDebugState(t, tester) + + hasRequestCount := false + for _, ts := range debugState.ThreadDebugStates { + if ts.RequestCount > 0 { + hasRequestCount = true + assert.Greater(t, ts.MemoryUsage, int64(0), "thread %d (%s) should report memory usage", ts.Index, ts.Name) + } + } + assert.True(t, hasRequestCount, "at least one thread should have RequestCount > 0 after serving requests") +} + func TestAutoScaleWorkerThreads(t *testing.T) { wg := sync.WaitGroup{} maxTries := 10 diff --git a/debugstate.go b/debugstate.go index c18813ec1..62f3f9f7a 100644 --- a/debugstate.go +++ b/debugstate.go @@ -1,5 +1,7 @@ package frankenphp +// #include "frankenphp.h" +import "C" import ( "github.com/dunglas/frankenphp/internal/state" ) @@ -12,6 +14,11 @@ type ThreadDebugState struct { IsWaiting bool IsBusy bool WaitingSinceMilliseconds int64 + CurrentURI string + CurrentMethod string + RequestStartedAt int64 + RequestCount int64 + MemoryUsage int64 } // EXPERIMENTAL: FrankenPHPDebugState prints the state of all PHP threads - debugging purposes only @@ -39,12 +46,51 @@ func DebugState() FrankenPHPDebugState { // threadDebugState creates a small jsonable status message for debugging purposes func threadDebugState(thread *phpThread) ThreadDebugState { - return ThreadDebugState{ + isBusy := !thread.state.IsInWaitingState() + + s := ThreadDebugState{ Index: thread.threadIndex, Name: thread.name(), State: thread.state.Name(), IsWaiting: thread.state.IsInWaitingState(), - IsBusy: !thread.state.IsInWaitingState(), + IsBusy: isBusy, WaitingSinceMilliseconds: thread.state.WaitTime(), } + + s.RequestCount = thread.requestCount.Load() + s.MemoryUsage = int64(C.frankenphp_get_thread_memory_usage(C.uintptr_t(thread.threadIndex))) + + if !isBusy { + return s + } + + thread.handlerMu.RLock() + handler := thread.handler + thread.handlerMu.RUnlock() + + if handler == nil { + return s + } + + thread.contextMu.RLock() + defer thread.contextMu.RUnlock() + + fc := handler.frankenPHPContext() + if fc == nil || fc.request == nil || fc.responseWriter == nil { + return s + } + + if fc.originalRequest == nil { + s.CurrentURI = fc.requestURI + s.CurrentMethod = fc.request.Method + } else { + s.CurrentURI = fc.originalRequest.URL.RequestURI() + s.CurrentMethod = fc.originalRequest.Method + } + + if !fc.startedAt.IsZero() { + s.RequestStartedAt = fc.startedAt.UnixMilli() + } + + return s } diff --git a/frankenphp.c b/frankenphp.c index c25a3505f..787c566d4 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -244,8 +244,13 @@ static void frankenphp_reset_session_state(void) { } #endif +static frankenphp_thread_metrics *thread_metrics = NULL; + /* Adapted from php_request_shutdown */ static void frankenphp_worker_request_shutdown() { + __atomic_store_n(&thread_metrics[thread_index].last_memory_usage, + zend_memory_usage(0), __ATOMIC_RELAXED); + /* Flush all output buffers */ zend_try { php_output_end_all(); } zend_end_try(); @@ -1233,6 +1238,8 @@ int frankenphp_execute_script(char *file_name) { sandboxed_env = NULL; } + __atomic_store_n(&thread_metrics[thread_index].last_memory_usage, + zend_memory_usage(0), __ATOMIC_RELAXED); php_request_shutdown((void *)0); frankenphp_free_request_context(); @@ -1405,6 +1412,20 @@ int frankenphp_reset_opcache(void) { int frankenphp_get_current_memory_limit() { return PG(memory_limit); } +void frankenphp_init_thread_metrics(int max_threads) { + thread_metrics = calloc(max_threads, sizeof(frankenphp_thread_metrics)); +} + +void frankenphp_destroy_thread_metrics(void) { + free(thread_metrics); + thread_metrics = NULL; +} + +size_t frankenphp_get_thread_memory_usage(uintptr_t idx) { + return __atomic_load_n(&thread_metrics[idx].last_memory_usage, + __ATOMIC_RELAXED); +} + static zend_module_entry **modules = NULL; static int modules_len = 0; static int (*original_php_register_internal_extensions_func)(void) = NULL; diff --git a/frankenphp.h b/frankenphp.h index f25cb8512..bdba41d84 100644 --- a/frankenphp.h +++ b/frankenphp.h @@ -186,6 +186,14 @@ zend_string *frankenphp_init_persistent_string(const char *string, size_t len); int frankenphp_reset_opcache(void); int frankenphp_get_current_memory_limit(); +typedef struct { + size_t last_memory_usage; +} frankenphp_thread_metrics; + +void frankenphp_init_thread_metrics(int max_threads); +void frankenphp_destroy_thread_metrics(void); +size_t frankenphp_get_thread_memory_usage(uintptr_t thread_index); + void register_extensions(zend_module_entry **m, int len); #endif diff --git a/phpmainthread.go b/phpmainthread.go index ba3917e84..7f9b8fb94 100644 --- a/phpmainthread.go +++ b/phpmainthread.go @@ -54,6 +54,8 @@ func initPHPThreads(numThreads int, numMaxThreads int, phpIni map[string]string) return nil, err } + C.frankenphp_init_thread_metrics(C.int(mainThread.maxThreads)) + // initialize all other threads phpThreads = make([]*phpThread, mainThread.maxThreads) phpThreads[0] = initialThread @@ -97,6 +99,7 @@ func drainPHPThreads() { doneWG.Wait() mainThread.state.Set(state.Done) mainThread.state.WaitFor(state.Reserved) + C.frankenphp_destroy_thread_metrics() phpThreads = nil } diff --git a/phpthread.go b/phpthread.go index fdf263717..c2660c550 100644 --- a/phpthread.go +++ b/phpthread.go @@ -7,6 +7,7 @@ import ( "context" "runtime" "sync" + "sync/atomic" "unsafe" "github.com/dunglas/frankenphp/internal/state" @@ -16,12 +17,14 @@ import ( // identified by the index in the phpThreads slice type phpThread struct { runtime.Pinner - threadIndex int - requestChan chan contextHolder - drainChan chan struct{} - handlerMu sync.RWMutex - handler threadHandler - state *state.ThreadState + threadIndex int + requestChan chan contextHolder + drainChan chan struct{} + handlerMu sync.RWMutex + handler threadHandler + contextMu sync.RWMutex + state *state.ThreadState + requestCount atomic.Int64 } // threadHandler defines how the callbacks from the C thread should be handled @@ -125,10 +128,13 @@ func (thread *phpThread) context() context.Context { func (thread *phpThread) name() string { thread.handlerMu.RLock() - name := thread.handler.name() - thread.handlerMu.RUnlock() + defer thread.handlerMu.RUnlock() - return name + if thread.handler == nil { + return "unknown" + } + + return thread.handler.name() } // Pin a string that is not null-terminated diff --git a/threadregular.go b/threadregular.go index 938d98350..30a343440 100644 --- a/threadregular.go +++ b/threadregular.go @@ -60,6 +60,7 @@ func (handler *regularThread) beforeScriptExecution() string { } func (handler *regularThread) afterScriptExecution(_ int) { + handler.thread.requestCount.Add(1) handler.afterRequest() } @@ -88,8 +89,10 @@ func (handler *regularThread) waitForRequest() string { case ch = <-handler.thread.requestChan: } + handler.thread.contextMu.Lock() handler.ctx = ch.ctx handler.contextHolder.frankenPHPContext = ch.frankenPHPContext + handler.thread.contextMu.Unlock() handler.state.MarkAsWaiting(false) // set the scriptFilename that should be executed @@ -98,8 +101,10 @@ func (handler *regularThread) waitForRequest() string { func (handler *regularThread) afterRequest() { handler.contextHolder.frankenPHPContext.closeContext() + handler.thread.contextMu.Lock() handler.contextHolder.frankenPHPContext = nil handler.ctx = nil + handler.thread.contextMu.Unlock() } func handleRequestWithRegularPHPThreads(ch contextHolder) error { diff --git a/threadworker.go b/threadworker.go index a0984afab..bc1500240 100644 --- a/threadworker.go +++ b/threadworker.go @@ -131,8 +131,10 @@ func tearDownWorkerScript(handler *workerThread, exitStatus int) { // make sure to close the worker request context if handler.workerFrankenPHPContext != nil { handler.workerFrankenPHPContext.closeContext() + handler.thread.contextMu.Lock() handler.workerFrankenPHPContext = nil handler.workerContext = nil + handler.thread.contextMu.Unlock() } // on exit status 0 we just run the worker script again @@ -235,8 +237,10 @@ func (handler *workerThread) waitForWorkerRequest() (bool, any) { case requestCH = <-handler.worker.requestChan: } + handler.thread.contextMu.Lock() handler.workerContext = requestCH.ctx handler.workerFrankenPHPContext = requestCH.frankenPHPContext + handler.thread.contextMu.Unlock() handler.state.MarkAsWaiting(false) if globalLogger.Enabled(requestCH.ctx, slog.LevelDebug) { @@ -292,9 +296,13 @@ func go_frankenphp_finish_worker_request(threadIndex C.uintptr_t, retval *C.zval fc.handlerReturn = r } + thread.requestCount.Add(1) + fc.closeContext() + thread.contextMu.Lock() thread.handler.(*workerThread).workerFrankenPHPContext = nil thread.handler.(*workerThread).workerContext = nil + thread.contextMu.Unlock() if globalLogger.Enabled(ctx, slog.LevelDebug) { if fc.request == nil {