mirror of
https://github.com/dunglas/frankenphp.git
synced 2026-04-22 16:27:12 +08:00
feat: add more metrics to the Prometheus endpoint
This commit is contained in:
committed by
GitHub
parent
c12841bf89
commit
5ebf119b2b
@@ -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
|
||||
|
||||
+48
-2
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+15
-9
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user