diff --git a/src/Runtime/ColdStartTracker.php b/src/Runtime/ColdStartTracker.php index 49b7dd25a..93d266fb0 100644 --- a/src/Runtime/ColdStartTracker.php +++ b/src/Runtime/ColdStartTracker.php @@ -10,6 +10,7 @@ class ColdStartTracker { private const FILE = '/tmp/.bref-cold-start'; + private const PROACTIVE_INITIALIZATION_IDLE_THRESHOLD_SECONDS = 0.05; private static bool $currentInvocationIsColdStart = false; private static ?float $coldStartBeginningTime = null; @@ -47,6 +48,7 @@ public static function invocationStarted(): void // so we are no longer in the cold start invocation anymore if (self::$hasFirstInvocationStarted) { self::$currentInvocationIsColdStart = false; + self::$wasProactiveInitialization = false; return; } @@ -55,11 +57,11 @@ public static function invocationStarted(): void if (self::$currentInvocationIsColdStart) { // There was a cold start, let's figure out if it was a proactive initialization $timeElapsedSinceColdStartEnded = microtime(true) - self::$coldStartEndedTime; - // If more than 100ms have passed since the cold start ended, we can assume the + // If more than 50ms have passed since the cold start ended, we can assume the // Lambda sandbox was paused/frozen between the cold start and the first invocation - // (100ms is an arbitrary value, we could use a lower value but I want to be conservative) + // (50ms is an arbitrary value, we could use a lower value but I want to be conservative) // That means the Lambda sandbox was initialized proactively - self::$wasProactiveInitialization = $timeElapsedSinceColdStartEnded > 0.1; + self::$wasProactiveInitialization = $timeElapsedSinceColdStartEnded > self::PROACTIVE_INITIALIZATION_IDLE_THRESHOLD_SECONDS; } else { // There was no cold start, we are in a warm start self::$wasProactiveInitialization = false; diff --git a/tests/Runtime/ColdStartTrackerTest.php b/tests/Runtime/ColdStartTrackerTest.php new file mode 100644 index 000000000..9d84646de --- /dev/null +++ b/tests/Runtime/ColdStartTrackerTest.php @@ -0,0 +1,138 @@ +resetColdStartTracker(); + } + + protected function tearDown(): void + { + $this->resetColdStartTracker(); + } + + public function test_first_invocation_after_initialization_is_a_user_facing_cold_start(): void + { + ColdStartTracker::init(); + ColdStartTracker::coldStartFinished(); + ColdStartTracker::invocationStarted(); + + $this->assertTrue(ColdStartTracker::currentInvocationIsColdStart()); + $this->assertTrue(ColdStartTracker::currentInvocationIsUserFacingColdStart()); + $this->assertFalse(ColdStartTracker::wasProactiveInitialization()); + } + + public function test_short_idle_time_after_initialization_is_not_a_proactive_initialization(): void + { + ColdStartTracker::init(); + ColdStartTracker::coldStartFinished(); + + ColdStartTracker::invocationStarted(); + + $this->assertTrue(ColdStartTracker::currentInvocationIsUserFacingColdStart()); + $this->assertFalse(ColdStartTracker::wasProactiveInitialization()); + } + + public function test_first_invocation_more_than_fifty_milliseconds_after_initialization_is_proactive(): void + { + ColdStartTracker::init(); + ColdStartTracker::coldStartFinished(); + + usleep(75_000); + + ColdStartTracker::invocationStarted(); + + $this->assertTrue(ColdStartTracker::currentInvocationIsColdStart()); + $this->assertFalse(ColdStartTracker::currentInvocationIsUserFacingColdStart()); + $this->assertTrue(ColdStartTracker::wasProactiveInitialization()); + } + + public function test_second_invocation_after_user_facing_cold_start_is_warm(): void + { + ColdStartTracker::init(); + ColdStartTracker::coldStartFinished(); + + ColdStartTracker::invocationStarted(); + ColdStartTracker::invocationStarted(); + + $this->assertFalse(ColdStartTracker::currentInvocationIsColdStart()); + $this->assertFalse(ColdStartTracker::currentInvocationIsUserFacingColdStart()); + $this->assertFalse(ColdStartTracker::wasProactiveInitialization()); + } + + public function test_proactive_initialization_annotation_does_not_leak_to_warm_invocations(): void + { + ColdStartTracker::init(); + ColdStartTracker::coldStartFinished(); + $this->setColdStartTrackerProperty('coldStartEndedTime', microtime(true) - 0.075); + + ColdStartTracker::invocationStarted(); + + $this->assertTrue(ColdStartTracker::currentInvocationIsColdStart()); + $this->assertFalse(ColdStartTracker::currentInvocationIsUserFacingColdStart()); + $this->assertTrue(ColdStartTracker::wasProactiveInitialization()); + + ColdStartTracker::invocationStarted(); + + $this->assertFalse(ColdStartTracker::currentInvocationIsColdStart()); + $this->assertFalse(ColdStartTracker::currentInvocationIsUserFacingColdStart()); + $this->assertFalse(ColdStartTracker::wasProactiveInitialization()); + } + + public function test_process_restart_inside_same_sandbox_is_a_warm_start(): void + { + touch(self::COLD_START_FILE); + + ColdStartTracker::init(); + ColdStartTracker::coldStartFinished(); + ColdStartTracker::invocationStarted(); + + $this->assertFalse(ColdStartTracker::currentInvocationIsColdStart()); + $this->assertFalse(ColdStartTracker::currentInvocationIsUserFacingColdStart()); + $this->assertFalse(ColdStartTracker::wasProactiveInitialization()); + } + + public function test_records_cold_start_timestamps(): void + { + $beforeInit = microtime(true); + ColdStartTracker::init(); + $afterInit = microtime(true); + + $beforeFinished = microtime(true); + ColdStartTracker::coldStartFinished(); + $afterFinished = microtime(true); + + $this->assertGreaterThanOrEqual($beforeInit, ColdStartTracker::getColdStartBeginningTime()); + $this->assertLessThanOrEqual($afterInit, ColdStartTracker::getColdStartBeginningTime()); + $this->assertGreaterThanOrEqual($beforeFinished, ColdStartTracker::getColdStartEndedTime()); + $this->assertLessThanOrEqual($afterFinished, ColdStartTracker::getColdStartEndedTime()); + } + + private function resetColdStartTracker(): void + { + if (file_exists(self::COLD_START_FILE)) { + unlink(self::COLD_START_FILE); + } + + $this->setColdStartTrackerProperty('currentInvocationIsColdStart', false); + $this->setColdStartTrackerProperty('coldStartBeginningTime', null); + $this->setColdStartTrackerProperty('coldStartEndedTime', null); + $this->setColdStartTrackerProperty('hasFirstInvocationStarted', false); + $this->setColdStartTrackerProperty('wasProactiveInitialization', false); + } + + private function setColdStartTrackerProperty(string $property, mixed $value): void + { + $reflection = new ReflectionProperty(ColdStartTracker::class, $property); + $reflection->setValue(null, $value); + } +}