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
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