Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,9 @@ public function getAttendee($summit_id, $attendee_id)
parameters: [
new OA\Parameter(name: 'id', in: 'path', required: true, description: 'Summit ID', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'attendee_id', in: 'path', required: true, description: 'Attendee ID', schema: new OA\Schema(type: 'integer')),
new OA\Parameter(name: 'expand', description: 'Expand relations', in: 'query', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'fields', description: 'Fields to return', in: 'query', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'relations', description: 'Relations to include', in: 'query', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
Expand All @@ -310,10 +313,20 @@ public function getAttendeeSchedule($summit_id, $attendee_id)
$attendee = CheckAttendeeStrategyFactory::build(CheckAttendeeStrategyFactory::Own, $this->resource_server_context)->check($attendee_id, $summit);
if (is_null($attendee)) return $this->error404();

$attendee_schedule_events = $attendee->getSchedule();
$this->event_repository->preloadEventRelations
(
array_map(fn($attendee_schedule) => $attendee_schedule->getEvent(), $attendee_schedule_events)
);

$schedule = [];
foreach ($attendee->getSchedule() as $attendee_schedule) {
if (!$summit->isEventOnSchedule($attendee_schedule->getEvent()->getId())) continue;
$schedule[] = SerializerRegistry::getInstance()->getSerializer($attendee_schedule)->serialize();
foreach ($attendee_schedule_events as $attendee_schedule) {
$schedule[] = SerializerRegistry::getInstance()->getSerializer($attendee_schedule)->serialize
(
SerializerUtils::getExpand(),
SerializerUtils::getFields(),
SerializerUtils::getRelations()
);
}

return $this->ok($schedule);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use models\exceptions\ValidationException;
use models\main\IMemberRepository;
use models\oauth2\IResourceServerContext;
use models\summit\ISummitEventRepository;
use models\summit\ISummitRepository;
use ModelSerializers\SerializerRegistry;
use services\model\ISummitService;
Expand Down Expand Up @@ -60,12 +61,18 @@ final class OAuth2SummitMembersApiController extends OAuth2ProtectedController
*/
private $rsvp_service;

/**
* @var ISummitEventRepository
*/
private $event_repository;

/**
* OAuth2SummitMembersApiController constructor.
* @param IMemberRepository $member_repository
* @param ISummitRepository $summit_repository
* @param ISummitService $summit_service
* @param ISummitRSVPService $rsvp_service
* @param ISummitEventRepository $event_repository
* @param IResourceServerContext $resource_server_context
*/
public function __construct
Expand All @@ -74,6 +81,7 @@ public function __construct
ISummitRepository $summit_repository,
ISummitService $summit_service,
ISummitRSVPService $rsvp_service,
ISummitEventRepository $event_repository,
IResourceServerContext $resource_server_context
)
{
Expand All @@ -82,6 +90,7 @@ public function __construct
$this->repository = $member_repository;
$this->summit_service = $summit_service;
$this->rsvp_service = $rsvp_service;
$this->event_repository = $event_repository;
}

use RequestProcessor;
Expand Down Expand Up @@ -317,9 +326,14 @@ public function getMemberScheduleSummitEvents($summit_id, $member_id)
$current_member = $this->resource_server_context->getCurrentUser();
if (is_null($current_member)) return $this->error403();

$schedule_events = $current_member->getScheduleBySummit($summit);
$this->event_repository->preloadEventRelations
(
array_map(fn($schedule_event) => $schedule_event->getEvent(), $schedule_events)
);

$schedule = [];
foreach ($current_member->getScheduleBySummit($summit) as $schedule_event) {
if (!$summit->isEventOnSchedule($schedule_event->getEvent()->getId())) continue;
foreach ($schedule_events as $schedule_event) {
$schedule[] = SerializerRegistry::getInstance()->getSerializer($schedule_event)->serialize
(
SerializerUtils::getExpand(),
Expand Down
11 changes: 10 additions & 1 deletion app/Http/Middleware/Doctrine/QueryTimingCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,24 @@ class QueryTimingCollector
public static float $totalMs = 0.0;
public static int $count = 0;

public static function record(float $startedAt): void
/**
* SQL text of every statement executed since reset(), or null while not capturing. Off by
* default: it exists so a test can tell which statements a request repeats, not just how many.
* @var string[]|null
*/
public static ?array $statements = null;

public static function record(float $startedAt, ?string $sql = null): void
{
self::$totalMs += (microtime(true) - $startedAt) * 1000.0;
self::$count++;
if (self::$statements !== null && $sql !== null) self::$statements[] = $sql;
}

public static function reset(): void
{
self::$totalMs = 0.0;
self::$count = 0;
if (self::$statements !== null) self::$statements = [];
}
}
13 changes: 9 additions & 4 deletions app/Http/Middleware/Doctrine/QueryTimingMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public function query(string $sql): DBALResult
try {
return parent::query($sql);
} finally {
QueryTimingCollector::record($start);
QueryTimingCollector::record($start, $sql);
}
}

Expand All @@ -53,25 +53,30 @@ public function exec(string $sql): int
try {
return parent::exec($sql);
} finally {
QueryTimingCollector::record($start);
QueryTimingCollector::record($start, $sql);
}
}

public function prepare(string $sql): DBALStatement
{
return new QueryTimingStatement(parent::prepare($sql));
return new QueryTimingStatement(parent::prepare($sql), $sql);
}
}

class QueryTimingStatement extends AbstractStatementMiddleware
{
public function __construct(DBALStatement $statement, private string $sql)
{
parent::__construct($statement);
}

public function execute($params = null): DBALResult
{
$start = microtime(true);
try {
return parent::execute($params);
} finally {
QueryTimingCollector::record($start);
QueryTimingCollector::record($start, $this->sql);
}
}
}
4 changes: 3 additions & 1 deletion app/Models/Foundation/Main/Member.php
Original file line number Diff line number Diff line change
Expand Up @@ -1451,7 +1451,9 @@ public function getRSVPInvitations(Summit $summit)
public function getScheduleBySummit(Summit $summit)
{

$query = $this->createQuery("SELECT s from models\main\SummitMemberSchedule s
// select the event too: it is read for every row when the schedule is serialized,
// and would otherwise be lazy-loaded one query per row
$query = $this->createQuery("SELECT s, e from models\main\SummitMemberSchedule s
JOIN s.member m
JOIN s.event e
JOIN e.summit su WHERE su.id = :summit_id and m.id = :member_id and e.published = 1 ");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ public function getPublishedOnSameTimeFrame(IPublishableEvent $event): array;
*/
public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Order $order = null);

/**
* Batch-preloads the relations the event serializers read per event, so serializing a list
* of events does not lazy-load them once per event.
* @param SummitEvent[] $events
*/
public function preloadEventRelations(array $events): void;

/**
* @param PagingInfo $paging_info
* @param Filter|null $filter
Expand Down
65 changes: 39 additions & 26 deletions app/Repositories/Summit/DoctrineSummitEventRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,35 @@ public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Ord
if (isset($byId[$id])) $data[] = $byId[$id];
}

// Batch-preload Tag rows for every event on the page so the serializer's
$this->preloadEventRelations($data);

if ($shuffleResults) shuffle($data);

$end = time() - $start;
Log::debug("DoctrineSummitEventRepository::getAllByPage", ['seconds'=>$end]);

return new PagingResponse
(
$total,
$paging_info->getPerPage(),
$paging_info->getCurrentPage(),
$paging_info->getLastPage($total),
$data
);

}

/**
* Batch-preloads the relations the event serializers read per event (tags, sponsors, and for
* presentations the selection status, speaker assignment order and materials), so serializing
* N events does not lazy-load them N times.
* @param SummitEvent[] $events
*/
public function preloadEventRelations(array $events): void
{
$ids = array_map(fn(SummitEvent $event) => $event->getId(), $events);

// Batch-preload Tag rows for every given event so the serializer's
// foreach (\$event->getTags()) does not lazy-load the collection per event.
// SELECT e, t fetch-joins tags onto each SummitEvent.
if (!empty($ids)) {
Expand All @@ -762,7 +790,7 @@ public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Ord
'WHERE e.id IN (:ids)'
)->setParameter('ids', $ids)->getResult();
} catch (\Exception $ex) {
Log::warning('DoctrineSummitEventRepository::getAllByPage tags preload failed', [
Log::warning('DoctrineSummitEventRepository::preloadEventRelations tags preload failed', [
'error' => $ex->getMessage(),
]);
}
Expand All @@ -775,29 +803,29 @@ public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Ord
'WHERE e.id IN (:ids)'
)->setParameter('ids', $ids)->getResult();
} catch (\Exception $ex) {
Log::warning('DoctrineSummitEventRepository::getAllByPage sponsors preload failed', [
Log::warning('DoctrineSummitEventRepository::preloadEventRelations sponsors preload failed', [
'error' => $ex->getMessage(),
]);
}
}

// Targeted N+1 preload for Presentation rows on this page:
// Targeted N+1 preload for Presentation rows in the given set:
// one DQL query that loads PresentationSpeakerAssignment + PresentationSpeaker + Member.
// After this, identity-map lookups by the serializer no longer trigger:
// - per-speaker Member SELECT (was 56 queries on a typical /events page)
// - per-speaker Presentation_Speakers composite-key SELECT (was 19)
// Net: ~75 queries collapsed into 1, ~216ms DB time saved per request.
// Only runs when there is at least one Presentation in the page.
// Only runs when there is at least one Presentation in the given set.
$presentationIds = [];
foreach ($data as $event) {
foreach ($events as $event) {
if ($event instanceof Presentation) $presentationIds[] = $event->getId();
}
if (!empty($presentationIds)) {
try {
$em = $this->getEntityManager();

// Batch-preload SummitSelectedPresentation rows for all presentations
// on this page. getSelectionStatus() on each Presentation then uses the
// in the given set. getSelectionStatus() on each Presentation then uses the
// preloaded data instead of firing its own DQL per presentation.
// Filters match getSelectionStatus()'s constants exactly.
try {
Expand All @@ -822,13 +850,13 @@ public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Ord
$pid = $sp->getPresentation()->getId();
$byPresentation[$pid][] = $sp;
}
foreach ($data as $event) {
foreach ($events as $event) {
if ($event instanceof Presentation && method_exists($event, 'setPreloadedSessionSelections')) {
$event->setPreloadedSessionSelections($byPresentation[$event->getId()] ?? []);
}
}
} catch (\Exception $ex) {
Log::warning('DoctrineSummitEventRepository::getAllByPage selection-status preload failed', [
Log::warning('DoctrineSummitEventRepository::preloadEventRelations selection-status preload failed', [
'error' => $ex->getMessage(),
]);
}
Expand Down Expand Up @@ -863,32 +891,17 @@ public function getAllByPage(PagingInfo $paging_info, Filter $filter = null, Ord
'WHERE p.id IN (:ids)'
)->setParameter('ids', $presentationIds)->getResult();
} catch (\Exception $ex) {
Log::warning('DoctrineSummitEventRepository::getAllByPage materials preload failed', [
Log::warning('DoctrineSummitEventRepository::preloadEventRelations materials preload failed', [
'error' => $ex->getMessage(),
]);
}

} catch (\Exception $ex) {
Log::warning('DoctrineSummitEventRepository::getAllByPage speaker+member preload failed', [
Log::warning('DoctrineSummitEventRepository::preloadEventRelations speaker+member preload failed', [
'error' => $ex->getMessage(),
]);
}
}

if ($shuffleResults) shuffle($data);

$end = time() - $start;
Log::debug("DoctrineSummitEventRepository::getAllByPage", ['seconds'=>$end]);

return new PagingResponse
(
$total,
$paging_info->getPerPage(),
$paging_info->getCurrentPage(),
$paging_info->getLastPage($total),
$data
);

}

/**
Expand Down
Loading
Loading