mirror of
https://github.com/php/frankenphp.git
synced 2026-03-24 00:52:11 +01:00
Closes #83 #880 #1286. Working patch for Windows support. Supports linking to the [official PHP release (TS version)](https://www.php.net/downloads.php). Includes some work from #1286 (thanks @TenHian!!) This patch allows using Visual Studio to compile the cgo code. To do so, it must be compiled with Go 1.26 (RC) with the following setup: ```powershell winget install -e --id Microsoft.VisualStudio.2022.Community --override "--passive --wait --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Component.VC.Llvm.Clang --includeRecommended" winget install -e --id GoLang.Go $env:PATH += ';C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\bin' cd c:\ gh repo clone microsoft/vcpkg .\vcpkg\bootstrap-vcpkg.bat .\vcpkg\vcpkg install pthreads brotli # build watcher Invoke-WebRequest -Uri "https://github.com/e-dant/watcher/releases/download/0.14.3/x86_64-pc-windows-msvc.tar" -OutFile "$env:TEMP\watcher.tar" tar -xf "$env:TEMP\watcher.tar" -C C:\ Rename-Item -Path "C:\x86_64-pc-windows-msvc" -NewName "watcher-x86_64-pc-windows-msvc" Remove-Item "$env:TEMP\watcher.tar" # download php Invoke-WebRequest -Uri "https://downloads.php.net/~windows/releases/archives/php-8.5.1-Win32-vs17-x64.zip" -OutFile "$env:TEMP\php.zip" Expand-Archive -Path "$env:TEMP\php.zip" -DestinationPath "C:\" Remove-Item "$env:TEMP\php.zip" # download php development package Invoke-WebRequest -Uri "https://downloads.php.net/~windows/releases/archives/php-devel-pack-8.5.1-Win32-vs17-x64.zip" -OutFile "$env:TEMP\php-devel.zip" Expand-Archive -Path "$env:TEMP\php-devel.zip" -DestinationPath "C:\" Remove-Item "$env:TEMP\php-devel.zip" $env:GOTOOLCHAIN = 'go1.26rc1' $env:CC = 'clang' $env:CXX = 'clang++' $env:CGO_CFLAGS = "-I$env:C:\vcpkg\installed\x64-windows\include -IC:\watcher-x86_64-pc-windows-msvc -IC:\php-8.5.1-devel-vs17-x64\include -IC:\php-8.5.1-devel-vs17-x64\include\main -IC:\php-8.5.1-devel-vs17-x64\include\TSRM -IC:\php-8.5.1-devel-vs17-x64\include\Zend -IC:\php-8.5.1-devel-vs17-x64\include\ext" $env:CGO_LDFLAGS = '-LC:\vcpkg\installed\x64-windows\lib -lbrotlienc -LC:\watcher-x86_64-pc-windows-msvc -llibwatcher-c -LC:\php-8.5.1-Win32-vs17-x64 -LC:\php-8.5.1-devel-vs17-x64\lib -lphp8ts -lphp8embed' # clone frankenphp and build git clone -b windows https://github.com/php/frankenphp.git cd frankenphp\caddy\frankenphp go build -ldflags '-extldflags="-fuse-ld=lld"' -tags nowatcher,nobadger,nomysql,nopgx # Tests $env:PATH += ";$env:VCPKG_ROOT\installed\x64-windows\bin;C:\watcher-x86_64-pc-windows-msvc";C:\php-8.5.1-Win32-vs17-x64" "opcache.enable=0`r`nopcache.enable_cli=0" | Out-File -Encoding ascii php.ini $env:PHPRC = Get-Location go test -ldflags '-extldflags="-fuse-ld=lld"' -tags nowatcher,nobadger,nomysql,nopgx . ``` TODO: - [x] Fix remaining skipped tests (scaling and watcher) - [x] Test if the watcher mode works as expected - [x] Automate the build with GitHub Actions --------- Signed-off-by: Marc <m@pyc.ac> Co-authored-by: Kévin Dunglas <kevin@dunglas.dev> Co-authored-by: DubbleClick <m@pyc.ac>
248 lines
7.2 KiB
Go
248 lines
7.2 KiB
Go
package frankenphp
|
|
|
|
import (
|
|
"errors"
|
|
"log/slog"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/dunglas/frankenphp/internal/cpu"
|
|
"github.com/dunglas/frankenphp/internal/state"
|
|
)
|
|
|
|
const (
|
|
// requests have to be stalled for at least this amount of time before scaling
|
|
minStallTime = 5 * time.Millisecond
|
|
// time to check for CPU usage before scaling a single thread
|
|
cpuProbeTime = 120 * time.Millisecond
|
|
// do not scale over this amount of CPU usage
|
|
maxCpuUsageForScaling = 0.8
|
|
// downscale idle threads every x seconds
|
|
downScaleCheckTime = 5 * time.Second
|
|
// max amount of threads stopped in one iteration of downScaleCheckTime
|
|
maxTerminationCount = 10
|
|
// autoscaled threads waiting for longer than this time are downscaled
|
|
maxThreadIdleTime = 5 * time.Second
|
|
)
|
|
|
|
var (
|
|
ErrMaxThreadsReached = errors.New("max amount of overall threads reached")
|
|
|
|
scaleChan chan *frankenPHPContext
|
|
autoScaledThreads = []*phpThread{}
|
|
scalingMu = new(sync.RWMutex)
|
|
)
|
|
|
|
func initAutoScaling(mainThread *phpMainThread) {
|
|
if mainThread.maxThreads <= mainThread.numThreads {
|
|
scaleChan = nil
|
|
return
|
|
}
|
|
|
|
scalingMu.Lock()
|
|
scaleChan = make(chan *frankenPHPContext)
|
|
maxScaledThreads := mainThread.maxThreads - mainThread.numThreads
|
|
autoScaledThreads = make([]*phpThread, 0, maxScaledThreads)
|
|
scalingMu.Unlock()
|
|
|
|
go startUpscalingThreads(maxScaledThreads, scaleChan, mainThread.done)
|
|
go startDownScalingThreads(mainThread.done)
|
|
}
|
|
|
|
func drainAutoScaling() {
|
|
scalingMu.Lock()
|
|
|
|
if globalLogger.Enabled(globalCtx, slog.LevelDebug) {
|
|
globalLogger.LogAttrs(globalCtx, slog.LevelDebug, "shutting down autoscaling", slog.Int("autoScaledThreads", len(autoScaledThreads)))
|
|
}
|
|
|
|
scalingMu.Unlock()
|
|
}
|
|
|
|
func addRegularThread() (*phpThread, error) {
|
|
thread := getInactivePHPThread()
|
|
if thread == nil {
|
|
return nil, ErrMaxThreadsReached
|
|
}
|
|
convertToRegularThread(thread)
|
|
thread.state.WaitFor(state.Ready, state.ShuttingDown, state.Reserved)
|
|
return thread, nil
|
|
}
|
|
|
|
func addWorkerThread(worker *worker) (*phpThread, error) {
|
|
thread := getInactivePHPThread()
|
|
if thread == nil {
|
|
return nil, ErrMaxThreadsReached
|
|
}
|
|
convertToWorkerThread(thread, worker)
|
|
thread.state.WaitFor(state.Ready, state.ShuttingDown, state.Reserved)
|
|
return thread, nil
|
|
}
|
|
|
|
// scaleWorkerThread adds a worker PHP thread automatically
|
|
func scaleWorkerThread(worker *worker) {
|
|
// probe CPU usage before acquiring the lock (avoids holding lock during 120ms sleep)
|
|
if !cpu.ProbeCPUs(cpuProbeTime, maxCpuUsageForScaling, mainThread.done) {
|
|
return
|
|
}
|
|
|
|
scalingMu.Lock()
|
|
defer scalingMu.Unlock()
|
|
|
|
if !mainThread.state.Is(state.Ready) {
|
|
return
|
|
}
|
|
|
|
thread, err := addWorkerThread(worker)
|
|
if err != nil {
|
|
if globalLogger.Enabled(globalCtx, slog.LevelWarn) {
|
|
globalLogger.LogAttrs(globalCtx, slog.LevelWarn, "could not increase max_threads, consider raising this limit", slog.String("worker", worker.name), slog.Any("error", err))
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
autoScaledThreads = append(autoScaledThreads, thread)
|
|
|
|
if globalLogger.Enabled(globalCtx, slog.LevelInfo) {
|
|
globalLogger.LogAttrs(globalCtx, slog.LevelInfo, "upscaling worker thread", slog.String("worker", worker.name), slog.Int("thread", thread.threadIndex), slog.Int("num_threads", len(autoScaledThreads)))
|
|
}
|
|
}
|
|
|
|
// scaleRegularThread adds a regular PHP thread automatically
|
|
func scaleRegularThread() {
|
|
// probe CPU usage before acquiring the lock (avoids holding lock during 120ms sleep)
|
|
if !cpu.ProbeCPUs(cpuProbeTime, maxCpuUsageForScaling, mainThread.done) {
|
|
return
|
|
}
|
|
|
|
scalingMu.Lock()
|
|
defer scalingMu.Unlock()
|
|
|
|
if !mainThread.state.Is(state.Ready) {
|
|
return
|
|
}
|
|
|
|
thread, err := addRegularThread()
|
|
if err != nil {
|
|
if globalLogger.Enabled(globalCtx, slog.LevelWarn) {
|
|
globalLogger.LogAttrs(globalCtx, slog.LevelWarn, "could not increase max_threads, consider raising this limit", slog.Any("error", err))
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
autoScaledThreads = append(autoScaledThreads, thread)
|
|
|
|
if globalLogger.Enabled(globalCtx, slog.LevelInfo) {
|
|
globalLogger.LogAttrs(globalCtx, slog.LevelInfo, "upscaling regular thread", slog.Int("thread", thread.threadIndex), slog.Int("num_threads", len(autoScaledThreads)))
|
|
}
|
|
}
|
|
|
|
func startUpscalingThreads(maxScaledThreads int, scale chan *frankenPHPContext, done chan struct{}) {
|
|
for {
|
|
scalingMu.Lock()
|
|
scaledThreadCount := len(autoScaledThreads)
|
|
scalingMu.Unlock()
|
|
if scaledThreadCount >= maxScaledThreads {
|
|
// we have reached max_threads, check again later
|
|
select {
|
|
case <-done:
|
|
return
|
|
case <-time.After(downScaleCheckTime):
|
|
continue
|
|
}
|
|
}
|
|
|
|
select {
|
|
case fc := <-scale:
|
|
timeSinceStalled := time.Since(fc.startedAt)
|
|
|
|
// if the request has not been stalled long enough, wait and repeat
|
|
if timeSinceStalled < minStallTime {
|
|
select {
|
|
case <-done:
|
|
return
|
|
case <-time.After(minStallTime - timeSinceStalled):
|
|
continue
|
|
}
|
|
}
|
|
|
|
// if the request has been stalled long enough, scale
|
|
if fc.worker == nil {
|
|
scaleRegularThread()
|
|
continue
|
|
}
|
|
|
|
// check for max worker threads here again in case requests overflowed while waiting
|
|
if fc.worker.isAtThreadLimit() {
|
|
if globalLogger.Enabled(globalCtx, slog.LevelInfo) {
|
|
globalLogger.LogAttrs(globalCtx, slog.LevelInfo, "cannot scale worker thread, max threads reached for worker", slog.String("worker", fc.worker.name))
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
scaleWorkerThread(fc.worker)
|
|
case <-done:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func startDownScalingThreads(done chan struct{}) {
|
|
for {
|
|
select {
|
|
case <-done:
|
|
return
|
|
case <-time.After(downScaleCheckTime):
|
|
deactivateThreads()
|
|
}
|
|
}
|
|
}
|
|
|
|
// deactivateThreads checks all threads and removes those that have been inactive for too long
|
|
func deactivateThreads() {
|
|
stoppedThreadCount := 0
|
|
scalingMu.Lock()
|
|
defer scalingMu.Unlock()
|
|
for i := len(autoScaledThreads) - 1; i >= 0; i-- {
|
|
thread := autoScaledThreads[i]
|
|
|
|
// the thread might have been stopped otherwise, remove it
|
|
if thread.state.Is(state.Reserved) {
|
|
autoScaledThreads = append(autoScaledThreads[:i], autoScaledThreads[i+1:]...)
|
|
continue
|
|
}
|
|
|
|
waitTime := thread.state.WaitTime()
|
|
if stoppedThreadCount > maxTerminationCount || waitTime == 0 {
|
|
continue
|
|
}
|
|
|
|
// convert threads to inactive if they have been idle for too long
|
|
if thread.state.Is(state.Ready) && waitTime > maxThreadIdleTime.Milliseconds() {
|
|
convertToInactiveThread(thread)
|
|
stoppedThreadCount++
|
|
autoScaledThreads = append(autoScaledThreads[:i], autoScaledThreads[i+1:]...)
|
|
|
|
if globalLogger.Enabled(globalCtx, slog.LevelInfo) {
|
|
globalLogger.LogAttrs(globalCtx, slog.LevelInfo, "downscaling thread", slog.Int("thread", thread.threadIndex), slog.Int64("wait_time", waitTime), slog.Int("num_threads", len(autoScaledThreads)))
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
// TODO: Completely stopping threads is more memory efficient
|
|
// Some PECL extensions like #1296 will prevent threads from fully stopping (they leak memory)
|
|
// Reactivate this if there is a better solution or workaround
|
|
// if thread.state.Is(state.Inactive) && waitTime > maxThreadIdleTime.Milliseconds() {
|
|
// logger.LogAttrs(nil, slog.LevelDebug, "auto-stopping thread", slog.Int("thread", thread.threadIndex))
|
|
// thread.shutdown()
|
|
// stoppedThreadCount++
|
|
// autoScaledThreads = append(autoScaledThreads[:i], autoScaledThreads[i+1:]...)
|
|
// continue
|
|
// }
|
|
}
|
|
}
|