V2 : optimisation API (cache, index, vue materialisee)
- BDD (migrations 1.6.0 + 1.7.0, idempotentes) :
* 4 nouveaux index composites sur reactevents, triggerevents, sessions
* vue sessiondebriefs reecrite (sous-requetes correlees -> JOINs)
puis materialisee dans table session_debriefs_cache via procedure
stockee rebuild_debrief_cache, recalculee uniquement aux changements
* fix bug double JOIN participates : TargetRole = "IA" pour les hits
NPC/objets (au lieu d'un role aleatoire de participant)
- API PHP :
* connexion PDO en singleton + ATTR_PERSISTENT
* cache disque par session (cache/sess_<id>/) pour debrief, get, userhistory
avec invalidation ciblee sur events et lifecycle
* pagination opt-in (limit/offset) sur lists/sessions_for_user
* helpers ensure_/rebuild_/invalidate_session_debrief_cache_db
* debug/benchmark.php pour mesurer la latence des 6 endpoints cles
- Endpoints HTTP : signatures inchangees, compat Unreal preservee
- Gain mesure : -58% a -83% sur les 5 endpoints cles
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
109
ProserveAPI/htdocs/proserve/config/cache.php
Normal file
109
ProserveAPI/htdocs/proserve/config/cache.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
// Cache disque pour resultats lourds et immuables (debrief, userhistory, get session).
|
||||
// Chaque session a son propre sous-dossier cache/sess_<id>/ : invalider une session
|
||||
// se reduit a supprimer le dossier, sans risque de match partiel sur d'autres ids.
|
||||
// Toutes les fonctions sont silencieuses en cas d'echec (best-effort, jamais bloquant).
|
||||
|
||||
if (!defined('PROSERVE_CACHE_DIR'))
|
||||
define('PROSERVE_CACHE_DIR', __DIR__ . '/../cache');
|
||||
|
||||
function cache_session_dir($sessionId)
|
||||
{
|
||||
return PROSERVE_CACHE_DIR . '/sess_' . intval($sessionId);
|
||||
}
|
||||
|
||||
function cache_path($sessionId, $key)
|
||||
{
|
||||
$safe = preg_replace('/[^a-zA-Z0-9_-]/', '_', $key);
|
||||
return cache_session_dir($sessionId) . '/' . $safe . '.json';
|
||||
}
|
||||
|
||||
function cache_get($sessionId, $key)
|
||||
{
|
||||
if (intval($sessionId) <= 0) return null;
|
||||
$path = cache_path($sessionId, $key);
|
||||
if (!file_exists($path)) return null;
|
||||
$data = @file_get_contents($path);
|
||||
if ($data === false || $data === '') return null;
|
||||
return $data;
|
||||
}
|
||||
|
||||
function cache_put($sessionId, $key, $jsonString)
|
||||
{
|
||||
if (intval($sessionId) <= 0) return;
|
||||
$dir = cache_session_dir($sessionId);
|
||||
if (!is_dir($dir)) @mkdir($dir, 0755, true);
|
||||
@file_put_contents(cache_path($sessionId, $key), $jsonString, LOCK_EX);
|
||||
}
|
||||
|
||||
function cache_invalidate_session($sessionId)
|
||||
{
|
||||
$sid = intval($sessionId);
|
||||
if ($sid <= 0) return;
|
||||
$dir = cache_session_dir($sid);
|
||||
if (!is_dir($dir)) return;
|
||||
$files = glob($dir . '/*');
|
||||
if (is_array($files)) {
|
||||
foreach ($files as $f) @unlink($f);
|
||||
}
|
||||
@rmdir($dir);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DB-level cache for the materialized sessiondebriefs view (table session_debriefs_cache)
|
||||
// Populated by stored procedure rebuild_debrief_cache(p_session_id) created in migration 1.7.0.
|
||||
// =============================================================================
|
||||
|
||||
// Marque le cache DB d'une session comme stale (delete des lignes).
|
||||
// Appele depuis les endpoints d'evenements (storetriggerevent, storereactevent)
|
||||
// pour amortir le cout : la reconstruction se fait paresseusement au prochain
|
||||
// read via ensure_session_debriefs_cached_db, ou explicitement via rebuild...
|
||||
function invalidate_session_debrief_cache_db($db, $sessionId)
|
||||
{
|
||||
$sid = intval($sessionId);
|
||||
if ($sid <= 0 || $db === null) return;
|
||||
try {
|
||||
$stmt = $db->prepare("DELETE FROM session_debriefs_cache WHERE SessionId = ?");
|
||||
$stmt->execute([$sid]);
|
||||
} catch (Exception $e) {
|
||||
// best-effort, ne jamais bloquer la requete metier
|
||||
}
|
||||
}
|
||||
|
||||
// Reconstruit immediatement le cache DB d'une session (DELETE + INSERT via la proc).
|
||||
// Appele depuis session/stop, session/userleave, session/updateobjectives.
|
||||
function rebuild_session_debrief_cache_db($db, $sessionId)
|
||||
{
|
||||
$sid = intval($sessionId);
|
||||
if ($sid <= 0 || $db === null) return;
|
||||
try {
|
||||
$stmt = $db->prepare("CALL rebuild_debrief_cache(?)");
|
||||
$stmt->execute([$sid]);
|
||||
$stmt->closeCursor();
|
||||
} catch (Exception $e) {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
|
||||
// Garantit que le cache DB contient au moins une ligne pour la session.
|
||||
// Appele avant tout SELECT sur sessiondebriefs cote stats_object.php.
|
||||
// Si le cache a ete invalide par un evenement et n'a pas encore ete reconstruit,
|
||||
// cette fonction le reconstruit a la demande.
|
||||
function ensure_session_debriefs_cached_db($db, $sessionId)
|
||||
{
|
||||
$sid = intval($sessionId);
|
||||
if ($sid <= 0 || $db === null) return;
|
||||
try {
|
||||
$stmt = $db->prepare("SELECT 1 FROM session_debriefs_cache WHERE SessionId = ? LIMIT 1");
|
||||
$stmt->execute([$sid]);
|
||||
$exists = $stmt->fetchColumn();
|
||||
$stmt->closeCursor();
|
||||
if ($exists === false) {
|
||||
rebuild_session_debrief_cache_db($db, $sid);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// si la table n'existe pas (migration 1.7.0 pas encore appliquee),
|
||||
// on ne fait rien : le code legacy continue de marcher via la vue
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -21,28 +21,30 @@ define("SESSIONDEBRIEFS_VIEW_NAME", "sessiondebriefs");
|
||||
|
||||
//////////////////////////////////////// query strings for SESSIONDEBRIEFS_VIEW ////////////////////////////////////////
|
||||
// SELECT clause
|
||||
define("SESSIONDEBRIEFS_VIEW_QUERY_SELECT",
|
||||
define("SESSIONDEBRIEFS_VIEW_QUERY_SELECT",
|
||||
"SELECT S.id AS SessionId, S.sessionType AS SessionTypeId, ST.displayName AS SessionType, S.sessionName AS SessionName, S.sessionDate AS SessionDate, " .
|
||||
"S.mapName AS MapName, S.scenarioName AS ScenarioName, S.success AS SessionSuccessful, S.timeToFinish AS SessionDuration, " .
|
||||
"COALESCE(T.type, -1) AS TriggerTypeId, COALESCE(TT.displayName, '') AS TriggerType, (SELECT IFNULL(T.srcUserId, PS.userId)) AS ShooterId, US.username AS ShooterName, " .
|
||||
"(SELECT IFNULL(PS.role, 3)) AS ShooterRoleId, (SELECT R.displayName FROM userroles R WHERE R.id = ShooterRoleId) AS ShooterRole, " .
|
||||
"COALESCE(T.type, -1) AS TriggerTypeId, COALESCE(TT.displayName, '') AS TriggerType, IFNULL(T.srcUserId, PS.userId) AS ShooterId, US.username AS ShooterName, " .
|
||||
"IFNULL(PS.role, 3) AS ShooterRoleId, URS.displayName AS ShooterRole, " .
|
||||
"COALESCE(T.indexCount, -1) AS ShotIndex, COALESCE(R.id, -1) AS ReactId, COALESCE(R.reactMode, 2) AS ReactModeId, COALESCE(RM.displayName, '') AS ReactMode, " .
|
||||
"COALESCE(R.reactType, -1) AS ReactTypeId, COALESCE(RT.displayName, '') AS ReactType, COALESCE(R.hitUserId, -1) AS TargetUserId, " .
|
||||
"COALESCE(UH.username, '') AS TargetUserName, (SELECT IFNULL(PH.role, 3)) AS TargetRoleId, (SELECT R.displayName FROM userroles R WHERE R.id = TargetRoleId) AS TargetRole, " .
|
||||
"COALESCE(UH.username, '') AS TargetUserName, IFNULL(PH.role, 3) AS TargetRoleId, URT.displayName AS TargetRole, " .
|
||||
"COALESCE(R.hitTargetName, '') AS TargetName, COALESCE(R.hitBoneName, '') AS TargetBoneName, COALESCE(R.targetKilled, 0) AS TargetKilled, " .
|
||||
"COALESCE(R.objectHitLocationX, 0) AS HitLocationX, COALESCE(R.objectHitLocationY, 0) AS HitLocationY, COALESCE(R.objectHitTagLocation, '') AS HitLocationTag, " .
|
||||
"COALESCE(R.hitPrecision, 0) AS HitPrecision, COALESCE(R.distance, 0) AS HitTargetDistance, COALESCE(R.reactTime, 0) AS ReactionTime, " .
|
||||
"COALESCE(R.timeStamp, 0) AS TimeStamp, COUNT(DISTINCT R.id) AS NbHit, COUNT(DISTINCT RK.srcEventIndex,RK.hitTargetName) AS NbKilled");
|
||||
|
||||
// FROM clause
|
||||
define ("SESSIONDEBRIEFS_VIEW_QUERY_FROM",
|
||||
define ("SESSIONDEBRIEFS_VIEW_QUERY_FROM",
|
||||
" FROM " . SESSIONS_TABLE_NAME . " S LEFT JOIN ". PARTICIPATES_TABLE_NAME . " PS ON (S.id = PS.sessionId) " .
|
||||
"LEFT JOIN ". PARTICIPATES_TABLE_NAME . " PH ON (S.id = PH.sessionId) LEFT JOIN " . TRIGGEREVENTS_TABLE_NAME . " T ON (S.id = T.sessionId) " .
|
||||
"LEFT JOIN " . TRIGGEREVENTTYPES_TABLE_NAME . " TT ON (TT.id = T.type) LEFT JOIN " . SESSIONTYPES_TABLE_NAME . " ST ON (ST.id = S.sessionType) " .
|
||||
"LEFT JOIN " . REACTEVENTS_TABLE_NAME . " R ON ( T.indexCount = R.srcEventIndex AND T.sessionId = R.srcEventSessionId ) " .
|
||||
"LEFT JOIN " . USERS_TABLE_NAME . " UH ON (UH.id = R.hitUserId) LEFT JOIN " . USERS_TABLE_NAME . " US ON (US.id = T.srcUserId OR US.id = PS.userId ) " .
|
||||
"LEFT JOIN " . REACTEVENTTYPES_TABLE_NAME . " RT ON (RT.id = R.reactType) LEFT JOIN " . REACTEVENTMODES_TABLE_NAME . " RM ON (RM.id = COALESCE(R.reactMode, 2)) " .
|
||||
"LEFT JOIN " . REACTEVENTS_TABLE_NAME . " RK ON (R.id = RK.id AND RK.targetKilled = 1)");
|
||||
"LEFT JOIN " . REACTEVENTS_TABLE_NAME . " RK ON (R.id = RK.id AND RK.targetKilled = 1) " .
|
||||
"LEFT JOIN " . USERROLES_TABLE_NAME . " URS ON (URS.id = IFNULL(PS.role, 3)) " .
|
||||
"LEFT JOIN " . USERROLES_TABLE_NAME . " URT ON (URT.id = IFNULL(PH.role, 3))");
|
||||
|
||||
// GROUP BY clause
|
||||
define("SESSIONDEBRIEFS_VIEW_QUERY_GROUPBY", " GROUP BY SessionId,ShooterId,ShotIndex,TargetName");
|
||||
|
||||
@@ -7,22 +7,34 @@ class Database
|
||||
private $username = "root"; //UserName of Phpmyadmin
|
||||
private $password = ""; //Password associated with username
|
||||
public $conn;
|
||||
|
||||
|
||||
// shared connection re-used across endpoint includes within a single request,
|
||||
// and (with PDO::ATTR_PERSISTENT) across requests within an Apache child process.
|
||||
private static $sharedConn = null;
|
||||
|
||||
// get the database connection
|
||||
public function getConnection()
|
||||
{
|
||||
$this->conn = null;
|
||||
try
|
||||
{
|
||||
$this->conn = new PDO("mysql:host=" . $this->host . ";dbname=" . $this->db_name, $this->username, $this->password);
|
||||
$this->conn->exec("set names utf8");
|
||||
{
|
||||
if (self::$sharedConn === null)
|
||||
{
|
||||
try
|
||||
{
|
||||
self::$sharedConn = new PDO(
|
||||
"mysql:host=" . $this->host . ";dbname=" . $this->db_name,
|
||||
$this->username,
|
||||
$this->password,
|
||||
[PDO::ATTR_PERSISTENT => true]
|
||||
);
|
||||
self::$sharedConn->exec("set names utf8");
|
||||
}
|
||||
catch(PDOException $exception)
|
||||
{
|
||||
echo "Connection error: " . $exception->getMessage();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch(PDOException $exception)
|
||||
{
|
||||
echo "Connection error: " . $exception->getMessage();
|
||||
}
|
||||
|
||||
return $this->conn;
|
||||
$this->conn = self::$sharedConn;
|
||||
return self::$sharedConn;
|
||||
}
|
||||
}
|
||||
?>
|
||||
?>
|
||||
|
||||
@@ -13,6 +13,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
|
||||
// include database and object files
|
||||
include_once '../config/database.php';
|
||||
include_once '../config/cache.php';
|
||||
|
||||
// get database connection
|
||||
$database = new Database();
|
||||
|
||||
Reference in New Issue
Block a user