Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions caddy/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 48 additions & 2 deletions debugstate.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package frankenphp

// #include "frankenphp.h"
import "C"
import (
"github.com/dunglas/frankenphp/internal/state"
)
Expand All @@ -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
Expand Down Expand Up @@ -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
}
21 changes: 21 additions & 0 deletions frankenphp.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions frankenphp.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions phpmainthread.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
24 changes: 15 additions & 9 deletions phpthread.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"runtime"
"sync"
"sync/atomic"
"unsafe"

"github.com/dunglas/frankenphp/internal/state"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions threadregular.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func (handler *regularThread) beforeScriptExecution() string {
}

func (handler *regularThread) afterScriptExecution(_ int) {
handler.thread.requestCount.Add(1)
handler.afterRequest()
}

Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions threadworker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down
Loading