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
9 changes: 7 additions & 2 deletions ProcessMaker/Http/Middleware/ServerTimingMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@
use Closure;
use Illuminate\Http\Request;
use ProcessMaker\Providers\ProcessMakerServiceProvider;
use ProcessMaker\Services\WorkerBootTimingService;
use Symfony\Component\HttpFoundation\Response;

class ServerTimingMiddleware
{
public function __construct(private WorkerBootTimingService $workerBootTimingService)
{
}

/**
* Handle an incoming request.
*
Expand All @@ -31,7 +36,7 @@ public function handle(Request $request, Closure $next): Response
// Calculate execution times
$controllerTime = (microtime(true) - $startController) * 1000; // Convert to ms
// Fetch service provider boot time
$serviceProviderTime = ProcessMakerServiceProvider::getBootTime() ?? 0;
$serviceProviderTime = $this->workerBootTimingService->getProviderBootTime() ?? 0;
// Fetch query time
$queryTime = ProcessMakerServiceProvider::getQueryTime() ?? 0;

Expand All @@ -47,7 +52,7 @@ public function handle(Request $request, Closure $next): Response
array_unshift($serverTiming, "boot;dur={$bootTiming}");
}

$packageTimes = ProcessMakerServiceProvider::getPackageBootTiming();
$packageTimes = $this->workerBootTimingService->getPackageBootTiming();
$minPackageTime = config('app.server_timing.min_package_time');

foreach ($packageTimes as $package => $timing) {
Expand Down
75 changes: 7 additions & 68 deletions ProcessMaker/Providers/ProcessMakerServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
use ProcessMaker\Services\ConditionalRedirectService;
use ProcessMaker\Services\RedirectToEventService;
use ProcessMaker\Services\SmartExtractConfiguration;
use ProcessMaker\Services\WorkerBootTimingService;
use RuntimeException;
use Spatie\Multitenancy\Events\MadeTenantCurrentEvent;
use Spatie\Multitenancy\Events\TenantNotFoundForRequestEvent;
Expand All @@ -68,22 +69,13 @@
*/
class ProcessMakerServiceProvider extends ServiceProvider
{
// Track the start time for service providers boot
private static $bootStart;

// Track the boot time for service providers
private static $bootTime;

// Track the boot time for each package
private static $packageBootTiming = [];

// Track the query time for each request
private static $queryTime = 0;

public function boot(): void
{
// Track the start time for service providers boot
self::$bootStart = microtime(true);
$bootStart = microtime(true);

// Set the current tenant
$this->setCurrentTenantForConsoleCommands();
Expand Down Expand Up @@ -112,11 +104,15 @@ public function boot(): void
$this->registerOctaneListeners();

// Hook after service providers boot
self::$bootTime = (microtime(true) - self::$bootStart) * 1000; // Convert to milliseconds
$this->app->make(WorkerBootTimingService::class)
->setProviderBootTime((microtime(true) - $bootStart) * 1000);
}

public function register(): void
{
// Boot metrics live for the lifetime of the application worker.
$this->app->singleton(WorkerBootTimingService::class);

if (config('app.server_timing.enabled')) {
// Listen to query events and accumulate query execution time
DB::listen(function ($query) {
Expand Down Expand Up @@ -512,16 +508,6 @@ public static function forceHttps(): void
}
}

/**
* Get the boot time for service providers.
*
* @return float|null
*/
public static function getBootTime(): ?float
{
return self::$bootTime;
}

/**
* Reset per-request query timing metrics.
*/
Expand All @@ -540,53 +526,6 @@ public static function getQueryTime(): float
return self::$queryTime;
}

/**
* Set the boot time for service providers.
*
* @param string $package
* @param float $time
*/
public static function setPackageBootStart(string $package, float $time): void
{
if ($time < 0) {
Log::info("Server Timing: Invalid boot time for package: {$package}, time: {$time}");

$time = 0;
}

self::$packageBootTiming[$package] = [
'start' => $time,
'end' => null,
];
}

/**
* Set the boot time for service providers.
*
*
* @param float $time
*/
public static function setPackageBootedTime(string $package, $time): void
{
if (!isset(self::$packageBootTiming[$package]) || $time < 0) {
Log::info("Server Timing: Invalid booted time for package: {$package}, time: {$time}");

return;
}

self::$packageBootTiming[$package]['end'] = $time;
}

/**
* Get the boot time for service providers.
*
* @return array
*/
public static function getPackageBootTiming(): array
{
return self::$packageBootTiming;
}

/**
* Reset per-request static state between Octane requests.
*
Expand Down
95 changes: 95 additions & 0 deletions ProcessMaker/Services/WorkerBootTimingService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

namespace ProcessMaker\Services;

use Illuminate\Support\Facades\Log;

/**
* Stores application and package boot timing for the lifetime of an Octane worker.
*
* This service is written only while the application is booting. Requests read the
* captured values when building the Server-Timing header, so the service must not
* be scoped to or flushed after an individual request.
*/
final class WorkerBootTimingService
{
private ?float $providerBootTime = null;

/**
* @var array<string, array{start: float, end: float|null}>
*/
private array $packageBootTiming = [];

/**
* Store the ProcessMaker service provider boot duration for this worker.
*
* @param float $time Boot duration in milliseconds
*/
public function setProviderBootTime(float $time): void
{
$this->providerBootTime = $time;
}

/**
* Get the ProcessMaker service provider boot duration for this worker.
*
* @return float|null Boot duration in milliseconds, or null before it is recorded
*/
public function getProviderBootTime(): ?float
{
return $this->providerBootTime;
}

/**
* Record when a package service provider starts booting.
*
* Invalid negative timestamps are logged and stored as zero.
* Calling this method again for the same package replaces its prior timing.
*
* @param string $package Package name used in the Server-Timing header
* @param float $time Start timestamp in seconds, as returned by microtime(true)
*/
public function setPackageBootStart(string $package, float $time): void
{
if ($time < 0) {
Log::info("Server Timing: Invalid boot time for package: {$package}, time: {$time}");

$time = 0.0;
}

$this->packageBootTiming[$package] = [
'start' => $time,
'end' => null,
];
}

/**
* Record when a package service provider finishes booting.
*
* Invalid negative timestamps and packages without a recorded start are
* logged and ignored.
*
* @param string $package Package name used in the Server-Timing header
* @param float $time End timestamp in seconds, as returned by microtime(true)
*/
public function setPackageBootedTime(string $package, float $time): void
{
if (!isset($this->packageBootTiming[$package]) || $time < 0) {
Log::info("Server Timing: Invalid booted time for package: {$package}, time: {$time}");

return;
}

$this->packageBootTiming[$package]['end'] = $time;
}

/**
* Get all package boot timestamps recorded for this worker.
*
* @return array<string, array{start: float, end: float|null}>
*/
public function getPackageBootTiming(): array
{
return $this->packageBootTiming;
}
}
16 changes: 5 additions & 11 deletions ProcessMaker/Traits/PluginServiceProviderTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
use ProcessMaker\Managers\IndexManager;
use ProcessMaker\Managers\LoginManager;
use ProcessMaker\Managers\PackageManager;
use ProcessMaker\Providers\ProcessMakerServiceProvider;
use ProcessMaker\Services\WorkerBootTimingService;

/**
* Add functionality to control a PM plug-in
Expand All @@ -22,10 +22,6 @@ trait PluginServiceProviderTrait

private $scriptBuilderScripts = [];

private static $bootStart = null;

private static $bootTime;

public function __construct($app)
{
parent::__construct($app);
Expand All @@ -48,15 +44,13 @@ protected function bootServerTiming(): void
$package = $this->getPackageName();

$this->booting(function () use ($package) {
self::$bootStart = microtime(true);

ProcessMakerServiceProvider::setPackageBootStart($package, self::$bootStart);
$this->app->make(WorkerBootTimingService::class)
->setPackageBootStart($package, microtime(true));
});

$this->booted(function () use ($package) {
self::$bootTime = microtime(true);

ProcessMakerServiceProvider::setPackageBootedTime($package, self::$bootTime);
$this->app->make(WorkerBootTimingService::class)
->setPackageBootedTime($package, microtime(true));
});
}

Expand Down
58 changes: 52 additions & 6 deletions tests/Feature/ServerTimingMiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use ProcessMaker\Http\Middleware\ServerTimingMiddleware;
use ProcessMaker\Models\User;
use ProcessMaker\Providers\ProcessMakerServiceProvider;
use ProcessMaker\Services\WorkerBootTimingService;
use ReflectionClass;
use Tests\Feature\Shared\RequestHelper;
use Tests\TestCase;
Expand Down Expand Up @@ -177,6 +178,49 @@ public function testServiceProviderTimeIsMeasured()
$this->assertGreaterThanOrEqual(0, (float) $providersTime);
}

public function testOctaneGatewayPreservesWorkerBootTimingAcrossConsecutiveRequests()
{
config(['app.server_timing.min_package_time' => 0]);

$workerTiming = app(WorkerBootTimingService::class);
$workerTiming->setProviderBootTime(12.5);
$workerTiming->setPackageBootStart('four32501-worker-package', 10.0);
$workerTiming->setPackageBootedTime('four32501-worker-package', 10.025);
$expectedPackageTiming = $workerTiming->getPackageBootTiming();

Route::middleware(ServerTimingMiddleware::class)->get('/octane-worker-timing', function () {
return response()->json(['message' => 'Octane worker timing test']);
});

$firstRequest = Request::create('/octane-worker-timing');
$firstGateway = new ApplicationGateway($this->app, clone $this->app);
$firstResponse = $firstGateway->handle($firstRequest);

$this->assertSame(12.5, $this->getMetricDuration($firstResponse, 'provider'));
$this->assertEqualsWithDelta(
25.0,
$this->getMetricDuration($firstResponse, 'four32501-worker-package'),
0.001
);

$firstGateway->terminate($firstRequest, $firstResponse);

$secondRequest = Request::create('/octane-worker-timing');
$secondGateway = new ApplicationGateway($this->app, clone $this->app);
$secondResponse = $secondGateway->handle($secondRequest);

$this->assertSame(12.5, $this->getMetricDuration($secondResponse, 'provider'));
$this->assertEqualsWithDelta(
25.0,
$this->getMetricDuration($secondResponse, 'four32501-worker-package'),
0.001
);

$secondGateway->terminate($secondRequest, $secondResponse);

$this->assertSame($expectedPackageTiming, $workerTiming->getPackageBootTiming());
}

public function testControllerTimingIsMeasuredCorrectly()
{
// Mock a route
Expand Down Expand Up @@ -241,11 +285,12 @@ public function testPackageTimingRespectsMinPackageTimeThreshold()
'app.server_timing.min_package_time' => 5,
]);

ProcessMakerServiceProvider::setPackageBootStart('foour32507-fast-package', 0.0);
ProcessMakerServiceProvider::setPackageBootedTime('foour32507-fast-package', 0.002);
$workerTiming = app(WorkerBootTimingService::class);
$workerTiming->setPackageBootStart('foour32507-fast-package', 0.0);
$workerTiming->setPackageBootedTime('foour32507-fast-package', 0.002);

ProcessMakerServiceProvider::setPackageBootStart('foour32507-slow-package', 0.0);
ProcessMakerServiceProvider::setPackageBootedTime('foour32507-slow-package', 0.010);
$workerTiming->setPackageBootStart('foour32507-slow-package', 0.0);
$workerTiming->setPackageBootedTime('foour32507-slow-package', 0.010);

Route::middleware(ServerTimingMiddleware::class)->get('/package-threshold-test', function () {
return response()->json(['message' => 'Package threshold test']);
Expand All @@ -264,8 +309,9 @@ public function testMinPackageTimeReadsConfigPerRequest()
{
config(['app.server_timing.enabled' => true]);

ProcessMakerServiceProvider::setPackageBootStart('foour32507-octane-package', 0.0);
ProcessMakerServiceProvider::setPackageBootedTime('foour32507-octane-package', 0.008);
$workerTiming = app(WorkerBootTimingService::class);
$workerTiming->setPackageBootStart('foour32507-octane-package', 0.0);
$workerTiming->setPackageBootedTime('foour32507-octane-package', 0.008);

Route::middleware(ServerTimingMiddleware::class)->get('/octane-min-package-test', function () {
return response()->json(['message' => 'Octane min package test']);
Expand Down
Loading
Loading