From 272d4fc0ad1006e17972491986aab558f83f4476 Mon Sep 17 00:00:00 2001 From: Kasim Necdet Percinel Date: Wed, 13 May 2026 18:19:29 +0000 Subject: [PATCH 1/6] add logging per chunk, and put chunk size to 50 in config --- settings/Config.Example.ini | 5 +++ src/Config.php | 7 ++-- src/Event/Api/EventsApi.php | 23 +++++++++++-- src/Event/Api/EventsApiInterface.php | 4 ++- src/Movie/HelioviewerMovie.php | 34 ++++++++++++++++--- tests/autoload.php | 8 +++++ .../events/api/GetEventsBatchTest.php | 4 +-- 7 files changed, 72 insertions(+), 13 deletions(-) diff --git a/settings/Config.Example.ini b/settings/Config.Example.ini index d9f907838..64fd3ce96 100644 --- a/settings/Config.Example.ini +++ b/settings/Config.Example.ini @@ -91,6 +91,11 @@ events_api_url = "https://events.helioviewer.org" ; Timeout in seconds for Events API requests events_api_timeout = 10 +; Number of timestamps per Events API batch request when building a movie. +; Smaller = faster individual requests, more requests total; larger = fewer +; round-trips but slower per request. 50 is a balanced default. +event_api_events_per_frame_chunksize = 50 + [movie_params] ; FFmpeg location ffmpeg = ffmpeg diff --git a/src/Config.php b/src/Config.php index abc9b6215..d48df1226 100644 --- a/src/Config.php +++ b/src/Config.php @@ -18,7 +18,8 @@ class Config { private $_bools = array('disable_cache', 'enable_statistics_collection', 'db_events','sentry_enabled'); private $_ints = array('build_num', 'ffmpeg_max_threads', - 'max_jpx_frames', 'max_movie_frames'); + 'max_jpx_frames', 'max_movie_frames', + 'event_api_events_per_frame_chunksize'); private $_floats = array('events_api_timeout'); private $config; @@ -86,7 +87,9 @@ private function _fixTypes() { // integers foreach ($this->_ints as $int) { - $this->config[$int] = (int)$this->config[$int]; + if (isset($this->config[$int])) { + $this->config[$int] = (int)$this->config[$int]; + } } // floats diff --git a/src/Event/Api/EventsApi.php b/src/Event/Api/EventsApi.php index ae50b2f4c..fc085c228 100644 --- a/src/Event/Api/EventsApi.php +++ b/src/Event/Api/EventsApi.php @@ -156,7 +156,7 @@ public function getDistributions(string $size, int $fromTimestamp, int $toTimest } /** {@inheritdoc} */ - public function getEventsBatch(array $timestamps, array $sources): array + public function getEventsBatch(array $timestamps, array $sources, int $chunkSize = 50, string $logLabel = ''): array { // Only allow known sources $validSources = self::filterSources($sources); @@ -166,9 +166,12 @@ public function getEventsBatch(array $timestamps, array $sources): array if (empty($timestamps)) { return []; } + if ($chunkSize < 1) { + $chunkSize = 50; + } $sourcesParam = implode('::', $validSources); - $chunks = array_chunk($timestamps, 150); + $chunks = array_chunk($timestamps, $chunkSize); $url = "/helioviewer/events/{$sourcesParam}/observations"; // Closure to fetch a single chunk of timestamps @@ -193,12 +196,26 @@ public function getEventsBatch(array $timestamps, array $sources): array } }; + $logChunk = function (int $i, int $total, int $size, int $elapsedMs) use ($logLabel) { + if ($logLabel === '') { + return; + } + error_log(sprintf( + "[%s] EventsApi chunk %d/%d (%d timestamps) took %dms", + $logLabel, $i + 1, $total, $size, $elapsedMs + )); + }; + // First chunk returns full response (event_types + events + observations) + $start = microtime(true); $merged = $fetchChunk($chunks[0]); + $logChunk(0, count($chunks), count($chunks[0]), (int) round((microtime(true) - $start) * 1000)); // Subsequent chunks only add new observations (event_types and events are the same) for ($i = 1; $i < count($chunks); $i++) { + $start = microtime(true); $chunk = $fetchChunk($chunks[$i]); + $logChunk($i, count($chunks), count($chunks[$i]), (int) round((microtime(true) - $start) * 1000)); $merged['observations'] += $chunk['observations']; } @@ -216,7 +233,7 @@ public function getEventsBatch(array $timestamps, array $sources): array */ private function parseResponse($response): array { - $body = (string)$response->getBody(); + $body = (string)$response->getBody(); $data = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE) { diff --git a/src/Event/Api/EventsApiInterface.php b/src/Event/Api/EventsApiInterface.php index c5d096c3d..a4adcd6fa 100644 --- a/src/Event/Api/EventsApiInterface.php +++ b/src/Event/Api/EventsApiInterface.php @@ -45,8 +45,10 @@ public function getDistributions(string $size, int $fromTimestamp, int $toTimest * * @param string[] $timestamps Array of observation datetime strings * @param string[] $sources Array of source names (e.g. ['HEK', 'CCMC', 'RHESSI']) + * @param int $chunkSize Max timestamps per upstream POST request + * @param string $logLabel Optional label prepended to per-chunk error_log lines (e.g. "Movie:Xp66n") * @return array Keyed by timestamp, each value is legacy-format event categories * @throws EventsApiException on API errors or unexpected responses */ - public function getEventsBatch(array $timestamps, array $sources): array; + public function getEventsBatch(array $timestamps, array $sources, int $chunkSize = 50, string $logLabel = ''): array; } diff --git a/src/Movie/HelioviewerMovie.php b/src/Movie/HelioviewerMovie.php index 557f0ff82..899e8c9e4 100644 --- a/src/Movie/HelioviewerMovie.php +++ b/src/Movie/HelioviewerMovie.php @@ -520,21 +520,45 @@ private function _buildMovieFrames($watermark) { 'switchSources' => $this->switchSources ); - // Preload events for all frames in 1-2 batch requests + // Preload events for all frames. EventsApi handles chunking internally + // using the configured chunk size and labels per-chunk logs with the movie ID. $timestamps = $this->_getTimeStamps(); $eventsApi = new EventsApi(); $batchResponse = []; $sources = $this->_eventsManager->getSources(); + $movieId = $this->publicId; + + error_log(sprintf( + "[Movie:%s] Starting movie build, frames=%d, sources=%s", + $movieId, + count($timestamps), + $sources ? implode(',', $sources) : '(none)' + )); if ($this->_eventsManager->hasEvents()) { + $chunkSize = defined('HV_EVENT_API_EVENTS_PER_FRAME_CHUNKSIZE') + ? HV_EVENT_API_EVENTS_PER_FRAME_CHUNKSIZE + : 50; + + $totalStart = microtime(true); try { - $batchResponse = $eventsApi->getEventsBatch($timestamps, $sources); + $batchResponse = $eventsApi->getEventsBatch( + $timestamps, + $sources, + $chunkSize, + "Movie:{$movieId}" + ); } catch (EventsApiException $e) { - error_log("[Movie:{$this->publicId}] Batch events failed: " . $e->getMessage()); - } catch (\Exception $e) { - error_log("[Movie:{$this->publicId}] Unexpected error fetching events: " . $e->getMessage()); + error_log("[Movie:{$movieId}] Batch events failed: " . $e->getMessage()); + } catch (\Throwable $e) { + error_log("[Movie:{$movieId}] Unexpected error fetching events: " . $e->getMessage()); Sentry::capture($e); } + $totalMs = (int) round((microtime(true) - $totalStart) * 1000); + error_log(sprintf( + "[Movie:%s] all event chunks done in %dms (%d frames)", + $movieId, $totalMs, count($timestamps) + )); } $options['batchEventResponse'] = $batchResponse; diff --git a/tests/autoload.php b/tests/autoload.php index e4d5c0395..32e7130ea 100644 --- a/tests/autoload.php +++ b/tests/autoload.php @@ -9,6 +9,14 @@ * included in every main php source file...). */ +// Redirect PHP's error_log destination off stderr. PHPUnit's +// @runInSeparateProcess tests use stderr as their IPC channel with the child +// process, so any stray error_log() call in production code corrupts the IPC +// and surfaces as a PHPUnit\Framework\Exception. Sending to a temp file keeps +// the messages around for inspection without breaking the tests. +ini_set('log_errors', '1'); +ini_set('error_log', sys_get_temp_dir() . '/helioviewer-test.log'); + // Load Helioviewer Configuration. This defines all the HV_* variables // seen throughout the project require_once __DIR__ . '/../src/Config.php'; diff --git a/tests/unit_tests/events/api/GetEventsBatchTest.php b/tests/unit_tests/events/api/GetEventsBatchTest.php index c28ef9a2e..a871248ae 100644 --- a/tests/unit_tests/events/api/GetEventsBatchTest.php +++ b/tests/unit_tests/events/api/GetEventsBatchTest.php @@ -88,7 +88,7 @@ public function testItShouldPaginateTimestampsAt150(): void $this->mockLegacyEvents->method('convertAll')->willReturn([]); - $this->eventsApi->getEventsBatch($timestamps, ['HEK']); + $this->eventsApi->getEventsBatch($timestamps, ['HEK'], 150); } public function testItShouldThrowAndCaptureSentryOnHttpError(): void @@ -156,6 +156,6 @@ public function testItShouldMergeObservationsAcrossChunksAndPassToConverter(): v })) ->willReturn([]); - $this->eventsApi->getEventsBatch($timestamps, ['HEK']); + $this->eventsApi->getEventsBatch($timestamps, ['HEK'], 150); } } From 3dd18b4efd7776d5a0b363affd445cd0d64a1239 Mon Sep 17 00:00:00 2001 From: Kasim Necdet Percinel Date: Wed, 13 May 2026 18:33:22 +0000 Subject: [PATCH 2/6] fix typo comment --- src/Event/Api/EventsApi.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Event/Api/EventsApi.php b/src/Event/Api/EventsApi.php index fc085c228..04431c549 100644 --- a/src/Event/Api/EventsApi.php +++ b/src/Event/Api/EventsApi.php @@ -233,7 +233,7 @@ public function getEventsBatch(array $timestamps, array $sources, int $chunkSize */ private function parseResponse($response): array { - $body = (string)$response->getBody(); + $body = (string)$response->getBody(); $data = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE) { From 0760d48888e40311589aa7d98766c1a41aafb314 Mon Sep 17 00:00:00 2001 From: Kasim Necdet Percinel Date: Wed, 13 May 2026 19:05:35 +0000 Subject: [PATCH 3/6] fix early status check of movie, craete frames directory early --- src/Movie/HelioviewerMovie.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Movie/HelioviewerMovie.php b/src/Movie/HelioviewerMovie.php index 899e8c9e4..4ffd48d90 100644 --- a/src/Movie/HelioviewerMovie.php +++ b/src/Movie/HelioviewerMovie.php @@ -423,9 +423,11 @@ public function getCurrentFrame() { } } - // Do not call closedir boolean if we can not open directory + // The frames directory may not exist yet when getMovieStatus is polled + // very early in PROCESSING -- before _buildMovieFrames has created it. + // Treat that as "0 frames written so far" instead of throwing. if (false === $handle) { - throw new \Exception("Could not find requested movie frames"); + return 0; } @closedir($handle); @@ -502,6 +504,14 @@ private function _buildMovieFrames($watermark) { $this->_dbSetup(); + // Pre-create the frames directory so getMovieStatus polls that arrive + // while the events prefetch is still running don't see a missing dir + // (status flips to PROCESSING the moment build() is invoked). + $framesDir = $this->directory . 'frames'; + if (!@file_exists($framesDir)) { + @mkdir($framesDir, 0775, true); + } + $frameNum = 0; // Movie frame parameters From bf7f7f9ac3b0219b95499a437079343cc7b2bed6 Mon Sep 17 00:00:00 2001 From: Kasim Necdet Percinel Date: Wed, 13 May 2026 19:12:03 +0000 Subject: [PATCH 4/6] rename new chunksize configuration name, align with old config names --- settings/Config.Example.ini | 2 +- src/Config.php | 2 +- src/Movie/HelioviewerMovie.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/settings/Config.Example.ini b/settings/Config.Example.ini index 64fd3ce96..5cd878c86 100644 --- a/settings/Config.Example.ini +++ b/settings/Config.Example.ini @@ -94,7 +94,7 @@ events_api_timeout = 10 ; Number of timestamps per Events API batch request when building a movie. ; Smaller = faster individual requests, more requests total; larger = fewer ; round-trips but slower per request. 50 is a balanced default. -event_api_events_per_frame_chunksize = 50 +events_api_events_per_frame_chunksize = 50 [movie_params] ; FFmpeg location diff --git a/src/Config.php b/src/Config.php index d48df1226..d7d29fabc 100644 --- a/src/Config.php +++ b/src/Config.php @@ -19,7 +19,7 @@ class Config { private $_bools = array('disable_cache', 'enable_statistics_collection', 'db_events','sentry_enabled'); private $_ints = array('build_num', 'ffmpeg_max_threads', 'max_jpx_frames', 'max_movie_frames', - 'event_api_events_per_frame_chunksize'); + 'events_api_events_per_frame_chunksize'); private $_floats = array('events_api_timeout'); private $config; diff --git a/src/Movie/HelioviewerMovie.php b/src/Movie/HelioviewerMovie.php index 4ffd48d90..9cc7dedbf 100644 --- a/src/Movie/HelioviewerMovie.php +++ b/src/Movie/HelioviewerMovie.php @@ -546,8 +546,8 @@ private function _buildMovieFrames($watermark) { )); if ($this->_eventsManager->hasEvents()) { - $chunkSize = defined('HV_EVENT_API_EVENTS_PER_FRAME_CHUNKSIZE') - ? HV_EVENT_API_EVENTS_PER_FRAME_CHUNKSIZE + $chunkSize = defined('HV_EVENTS_API_EVENTS_PER_FRAME_CHUNKSIZE') + ? HV_EVENTS_API_EVENTS_PER_FRAME_CHUNKSIZE : 50; $totalStart = microtime(true); From 70700a66267bd0039ec0c34adeea7d387ed68150 Mon Sep 17 00:00:00 2001 From: Kasim Necdet Percinel Date: Wed, 13 May 2026 19:24:27 +0000 Subject: [PATCH 5/6] add log if chunk requests failing, movie maker starts to make requests per frame again --- src/Image/Composite/HelioviewerCompositeImage.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Image/Composite/HelioviewerCompositeImage.php b/src/Image/Composite/HelioviewerCompositeImage.php index 2f6379af5..ea1e44866 100644 --- a/src/Image/Composite/HelioviewerCompositeImage.php +++ b/src/Image/Composite/HelioviewerCompositeImage.php @@ -646,6 +646,7 @@ private function _addEventLayer($imagickImage) { // Fetch events via batch (movies have pre-fetched, screenshots fetch for single timestamp) if (empty($this->batchEventResponse)) { + error_log("[date={$this->date}] batchEventResponse empty, fetching single timestamp"); try { $this->batchEventResponse = $this->eventsApi->getEventsBatch( [$this->date], From 46ae577df1e0b85497fa72aa65f0d26762d93db0 Mon Sep 17 00:00:00 2001 From: Kasim Necdet Percinel Date: Wed, 13 May 2026 20:14:20 +0000 Subject: [PATCH 6/6] add more logging for edge cases of event selections --- src/Movie/HelioviewerMovie.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Movie/HelioviewerMovie.php b/src/Movie/HelioviewerMovie.php index 9cc7dedbf..0b7cecf00 100644 --- a/src/Movie/HelioviewerMovie.php +++ b/src/Movie/HelioviewerMovie.php @@ -539,10 +539,11 @@ private function _buildMovieFrames($watermark) { $movieId = $this->publicId; error_log(sprintf( - "[Movie:%s] Starting movie build, frames=%d, sources=%s", + "[Movie:%s] Starting movie build, frames=%d, sources=%s, hasEvents=%s", $movieId, count($timestamps), - $sources ? implode(',', $sources) : '(none)' + $sources ? implode(',', $sources) : '(none)', + $this->_eventsManager->hasEvents() ? 'true' : 'false' )); if ($this->_eventsManager->hasEvents()) { @@ -569,6 +570,8 @@ private function _buildMovieFrames($watermark) { "[Movie:%s] all event chunks done in %dms (%d frames)", $movieId, $totalMs, count($timestamps) )); + } else { + error_log("[Movie:{$movieId}] No event types selected, skipping EventsApi request"); } $options['batchEventResponse'] = $batchResponse;