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
5 changes: 5 additions & 0 deletions settings/Config.Example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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.
events_api_events_per_frame_chunksize = 50

[movie_params]
; FFmpeg location
ffmpeg = ffmpeg
Expand Down
7 changes: 5 additions & 2 deletions src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
'events_api_events_per_frame_chunksize');
private $_floats = array('events_api_timeout');
private $config;

Expand Down Expand Up @@ -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
Expand Down
21 changes: 19 additions & 2 deletions src/Event/Api/EventsApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand All @@ -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'];
}

Expand Down
4 changes: 3 additions & 1 deletion src/Event/Api/EventsApiInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
1 change: 1 addition & 0 deletions src/Image/Composite/HelioviewerCompositeImage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
51 changes: 44 additions & 7 deletions src/Movie/HelioviewerMovie.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -520,21 +530,48 @@ 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, hasEvents=%s",
$movieId,
count($timestamps),
$sources ? implode(',', $sources) : '(none)',
$this->_eventsManager->hasEvents() ? 'true' : 'false'
));

if ($this->_eventsManager->hasEvents()) {
$chunkSize = defined('HV_EVENTS_API_EVENTS_PER_FRAME_CHUNKSIZE')
? HV_EVENTS_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)
));
} else {
error_log("[Movie:{$movieId}] No event types selected, skipping EventsApi request");
}

$options['batchEventResponse'] = $batchResponse;
Expand Down
8 changes: 8 additions & 0 deletions tests/autoload.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
4 changes: 2 additions & 2 deletions tests/unit_tests/events/api/GetEventsBatchTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -156,6 +156,6 @@ public function testItShouldMergeObservationsAcrossChunksAndPassToConverter(): v
}))
->willReturn([]);

$this->eventsApi->getEventsBatch($timestamps, ['HEK']);
$this->eventsApi->getEventsBatch($timestamps, ['HEK'], 150);
}
}
Loading