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:
3
ProserveAPI/htdocs/proserve/cache/.gitignore
vendored
Normal file
3
ProserveAPI/htdocs/proserve/cache/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*
|
||||
!.gitignore
|
||||
!.htaccess
|
||||
4
ProserveAPI/htdocs/proserve/cache/.htaccess
vendored
Normal file
4
ProserveAPI/htdocs/proserve/cache/.htaccess
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
Options -Indexes
|
||||
<FilesMatch "\.json$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
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
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -24,11 +24,11 @@ define("SESSIONDEBRIEFS_VIEW_NAME", "sessiondebriefs");
|
||||
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, " .
|
||||
@@ -42,7 +42,9 @@ define ("SESSIONDEBRIEFS_VIEW_QUERY_FROM",
|
||||
"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");
|
||||
|
||||
@@ -8,21 +8,33 @@ class Database
|
||||
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;
|
||||
if (self::$sharedConn === null)
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->conn = new PDO("mysql:host=" . $this->host . ";dbname=" . $this->db_name, $this->username, $this->password);
|
||||
$this->conn->exec("set names utf8");
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
106
ProserveAPI/htdocs/proserve/debug/benchmark.php
Normal file
106
ProserveAPI/htdocs/proserve/debug/benchmark.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
// Mesure de latence des endpoints lourds de l'API ProserveAPI.
|
||||
// A lancer AVANT et APRES chaque palier d'optimisation pour avoir des chiffres comparatifs.
|
||||
// Usage : POST sur ce fichier avec optionnellement userId et sessionId.
|
||||
// curl -X POST -d "userId=1&sessionId=42" http://localhost/proserve/debug/benchmark.php
|
||||
// ou ouvrir directement dans le navigateur (lance avec userId=-1 et sessionId=-1, certains endpoints retourneront vide mais la mesure du round-trip reste valide).
|
||||
|
||||
include_once '../config/init.php';
|
||||
|
||||
$userId = isset($_POST['userId']) ? intval($_POST['userId']) : (isset($_GET['userId']) ? intval($_GET['userId']) : -1);
|
||||
$sessionId = isset($_POST['sessionId']) ? intval($_POST['sessionId']) : (isset($_GET['sessionId']) ? intval($_GET['sessionId']) : -1);
|
||||
$iterations = isset($_POST['iterations']) ? max(1, intval($_POST['iterations'])) : (isset($_GET['iterations']) ? max(1, intval($_GET['iterations'])) : 5);
|
||||
|
||||
// si pas de sessionId fourni, prendre la plus recente pour avoir des donnees realistes
|
||||
if ($sessionId <= 0) {
|
||||
$stmt = $db->query("SELECT id FROM sessions ORDER BY id DESC LIMIT 1");
|
||||
$row = $stmt ? $stmt->fetch(PDO::FETCH_ASSOC) : null;
|
||||
if ($row) $sessionId = intval($row['id']);
|
||||
}
|
||||
if ($userId <= 0) {
|
||||
$stmt = $db->query("SELECT userId FROM participates WHERE sessionId=" . intval($sessionId) . " LIMIT 1");
|
||||
$row = $stmt ? $stmt->fetch(PDO::FETCH_ASSOC) : null;
|
||||
if ($row) $userId = intval($row['userId']);
|
||||
}
|
||||
|
||||
// resolution de l'URL de base (meme host et meme path)
|
||||
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||
$base = $scheme . '://' . $host . rtrim(dirname(dirname($_SERVER['SCRIPT_NAME'])), '/\\');
|
||||
|
||||
$endpoints = [
|
||||
'session/debrief' => ['sessionId' => $sessionId, 'userId' => $userId],
|
||||
'session/get' => ['sessionId' => $sessionId],
|
||||
'stats/userhistory' => ['userId' => $userId, 'sessionId' => $sessionId, 'quickMode' => 'false'],
|
||||
'stats/get' => ['userId' => $userId, 'sessionId' => $sessionId],
|
||||
'lists/sessions_for_user' => ['userId' => $userId],
|
||||
'lists/users_in_session' => ['sessionId' => $sessionId],
|
||||
];
|
||||
|
||||
function call_endpoint($url, $params)
|
||||
{
|
||||
// ext-curl est dispo dans XAMPP par defaut et bcp plus rapide que
|
||||
// file_get_contents+stream_context_create sur Windows (qui ajoute ~10ms par appel).
|
||||
static $ch = null;
|
||||
if ($ch === null) {
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_TIMEOUT => 60,
|
||||
CURLOPT_FORBID_REUSE => false,
|
||||
CURLOPT_FRESH_CONNECT => false,
|
||||
]);
|
||||
}
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
|
||||
|
||||
$start = microtime(true);
|
||||
$resp = curl_exec($ch);
|
||||
$elapsed_ms = (microtime(true) - $start) * 1000;
|
||||
$err = curl_errno($ch);
|
||||
|
||||
return [
|
||||
'ms' => round($elapsed_ms, 2),
|
||||
'bytes' => $resp === false ? 0 : strlen($resp),
|
||||
'ok' => $err === 0 && $resp !== false,
|
||||
];
|
||||
}
|
||||
|
||||
function summarize($samples)
|
||||
{
|
||||
$times = array_column($samples, 'ms');
|
||||
sort($times);
|
||||
$n = count($times);
|
||||
return [
|
||||
'min' => $times[0],
|
||||
'max' => $times[$n - 1],
|
||||
'median' => $times[intval($n / 2)],
|
||||
'avg' => round(array_sum($times) / $n, 2),
|
||||
'bytes' => $samples[0]['bytes'],
|
||||
'ok' => count(array_filter(array_column($samples, 'ok'))) === $n,
|
||||
];
|
||||
}
|
||||
|
||||
$results = [
|
||||
'context' => [
|
||||
'userId' => $userId,
|
||||
'sessionId' => $sessionId,
|
||||
'iterations' => $iterations,
|
||||
'base_url' => $base,
|
||||
'timestamp' => date('c'),
|
||||
],
|
||||
'endpoints' => [],
|
||||
];
|
||||
|
||||
foreach ($endpoints as $path => $params) {
|
||||
$url = $base . '/' . $path . '.php';
|
||||
$samples = [];
|
||||
for ($i = 0; $i < $iterations; $i++) {
|
||||
$samples[] = call_endpoint($url, $params);
|
||||
}
|
||||
$results['endpoints'][$path] = summarize($samples);
|
||||
}
|
||||
|
||||
print_r(json_encode($results, JSON_PRETTY_PRINT));
|
||||
?>
|
||||
@@ -40,6 +40,8 @@ $event->timeStamp = $_POST['timestamp'];
|
||||
// create reactevent
|
||||
if ($event->record())
|
||||
{
|
||||
cache_invalidate_session($event->srcEventSessionId);
|
||||
invalidate_session_debrief_cache_db($db, $event->srcEventSessionId);
|
||||
$evt_arr = $event->getResultArray(true, "Event_Stored_OK");
|
||||
print_r(json_encode($evt_arr)); // OK
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ $event->successful = $_POST['success'];
|
||||
// create trigger event
|
||||
if ($event->record())
|
||||
{
|
||||
cache_invalidate_session($event->sessionId);
|
||||
invalidate_session_debrief_cache_db($db, $event->sessionId);
|
||||
$evt_arr = $event->getResultArray(true, "Event_Stored_OK");
|
||||
print_r(json_encode($evt_arr)); // OK
|
||||
}
|
||||
|
||||
@@ -6,9 +6,13 @@ include_once '../objects/stats_object.php';
|
||||
|
||||
$userId = isset($_POST['userId']) ? $_POST['userId'] : -1;
|
||||
$typeId = isset($_POST['typeId']) ? $_POST['typeId'] : -1;
|
||||
// Optional pagination : Unreal n'envoie pas ces champs (comportement legacy preserve),
|
||||
// le futur web UI peut les passer pour ne pas charger toutes les sessions d'un coup.
|
||||
$limit = isset($_POST['limit']) ? intval($_POST['limit']) : -1;
|
||||
$offset = isset($_POST['offset']) ? intval($_POST['offset']) : 0;
|
||||
|
||||
$stats = new StatsObject($db);
|
||||
$stats->getSessionsForUser($userId, $typeId);
|
||||
$stats->getSessionsForUser($userId, $typeId, $limit, $offset);
|
||||
|
||||
$stats_arr = $stats->getResultArray(true, "User_Sessions_List_OK");
|
||||
print_r(json_encode($stats_arr)); // OK
|
||||
|
||||
@@ -118,7 +118,8 @@ class StatsObject extends DBTableObject
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// get sessions of given type that given user has taken part in
|
||||
public function getRawSessions ($userId=-1, $typeId=-1)
|
||||
// $limit and $offset are optional : when $limit <= 0 the full list is returned (legacy behavior)
|
||||
public function getRawSessions ($userId=-1, $typeId=-1, $limit=-1, $offset=0)
|
||||
{
|
||||
// Check if we are looking for a specific user and/or type of session
|
||||
$userConstraint = $userId > 0 ? " AND P.userId=" . $userId : "";
|
||||
@@ -127,9 +128,15 @@ class StatsObject extends DBTableObject
|
||||
// Order results from newest to oldest session
|
||||
$orderConstraint = " ORDER BY SD.id DESC";
|
||||
|
||||
// Optional pagination : SQL-level LIMIT/OFFSET when caller provided a positive limit
|
||||
$limitConstraint = "";
|
||||
if (intval($limit) > 0) {
|
||||
$limitConstraint = " LIMIT " . intval($limit) . " OFFSET " . max(0, intval($offset));
|
||||
}
|
||||
|
||||
//$query = "SELECT DISTINCT S.* FROM " . SESSIONS_TABLE_NAME . " S LEFT JOIN " . PARTICIPATES_TABLE_NAME . " P ON S.id = P.sessionId WHERE 1 " .
|
||||
$query = "SELECT DISTINCT SD.* FROM " . SESSIONS_TABLE_NAME . " SD, " . PARTICIPATES_TABLE_NAME . " P WHERE SD.id = P.sessionId " .
|
||||
$this->getCalibrationSessionsConstraint("SD") . $this->getSessionDurationConstraint("SD") . $userConstraint . $typeConstraint . $orderConstraint;
|
||||
$this->getCalibrationSessionsConstraint("SD") . $this->getSessionDurationConstraint("SD") . $userConstraint . $typeConstraint . $orderConstraint . $limitConstraint;
|
||||
|
||||
// prepare and execute query
|
||||
$stmt = $this->conn->prepare($query);
|
||||
@@ -141,10 +148,10 @@ class StatsObject extends DBTableObject
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// get sessions that given user has taken part in
|
||||
public function getSessionsForUser ($userId, $typeId=-1)
|
||||
public function getSessionsForUser ($userId, $typeId=-1, $limit=-1, $offset=0)
|
||||
{
|
||||
// Check if we are looking for a specific user and/or session
|
||||
$stmt = $this->getRawSessions($userId, $typeId);
|
||||
$stmt = $this->getRawSessions($userId, $typeId, $limit, $offset);
|
||||
|
||||
while ($row = $stmt->fetch(PDO::FETCH_ASSOC))
|
||||
{
|
||||
@@ -469,6 +476,11 @@ class StatsObject extends DBTableObject
|
||||
}
|
||||
else
|
||||
{
|
||||
// Lazy rebuild du cache materialise pour cette session avant le SELECT FROM sessiondebriefs
|
||||
if ($sessionId > 0 && function_exists('ensure_session_debriefs_cached_db')) {
|
||||
ensure_session_debriefs_cached_db($this->conn, $sessionId);
|
||||
}
|
||||
|
||||
// create SQL constraints for query
|
||||
$sessionConstraint = $sessionId > 0 ? " AND SD.SessionId = " . $sessionId : "";
|
||||
$userConstraint = $userId >= 0 ? " AND SD.ShooterId = " . $userId : "";
|
||||
@@ -692,6 +704,12 @@ class StatsObject extends DBTableObject
|
||||
// debrief mission : get given users results in given session
|
||||
public function getResultsForSession ($sessionId, $userId, $fromUserId=-1)
|
||||
{
|
||||
// Lazy rebuild du cache materialise (table session_debriefs_cache)
|
||||
// si la session a ete invalidee par un evenement et pas encore reconstruite
|
||||
if ($sessionId > 0 && function_exists('ensure_session_debriefs_cached_db')) {
|
||||
ensure_session_debriefs_cached_db($this->conn, $sessionId);
|
||||
}
|
||||
|
||||
$userConstraint = $userId > 0 ? " AND SD.ShooterId=" . $userId : "";
|
||||
$sessionConstraint = $sessionId > 0 ? " AND SD.SessionId=" . $sessionId : "";
|
||||
|
||||
|
||||
@@ -325,7 +325,61 @@ INSERT INTO `userstatus` (`id`, `displayName`, `description`) VALUES
|
||||
--
|
||||
DROP TABLE IF EXISTS `sessiondebriefs`;
|
||||
|
||||
CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `sessiondebriefs` AS 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`.`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(`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 (((((((((((`sessions` `s` left join `participates` `ps` on(`s`.`id` = `ps`.`sessionId`)) left join `participates` `ph` on(`s`.`id` = `ph`.`sessionId`)) left join `triggerevents` `t` on(`s`.`id` = `t`.`sessionId`)) left join `triggereventtypes` `tt` on(`tt`.`id` = `t`.`type`)) left join `sessiontypes` `st` on(`st`.`id` = `s`.`sessionType`)) left join `reactevents` `r` on(`t`.`indexCount` = `r`.`srcEventIndex` and `t`.`sessionId` = `r`.`srcEventSessionId`)) left join `users` `uh` on(`uh`.`id` = `r`.`hitUserId`)) left join `users` `us` on(`us`.`id` = `t`.`srcUserId` or `us`.`id` = `ps`.`userId`)) left join `reacteventtypes` `rt` on(`rt`.`id` = `r`.`reactType`)) left join `reacteventmodes` `rm` on(`rm`.`id` = coalesce(`r`.`reactMode`,2))) left join `reactevents` `rk` on(`r`.`id` = `rk`.`id` and `rk`.`targetKilled` = 1)) GROUP BY `s`.`id`, (select ifnull(`t`.`srcUserId`,`ps`.`userId`)), `t`.`indexCount`, `r`.`hitTargetName` ORDER BY `s`.`id` ASC, (select ifnull(`t`.`srcUserId`,`ps`.`userId`)) ASC, `t`.`indexCount` ASC, `r`.`id` ASC ;
|
||||
CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `sessiondebriefs` AS
|
||||
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`,
|
||||
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`,
|
||||
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 `sessions` `s`
|
||||
LEFT JOIN `participates` `ps` ON (`s`.`id` = `ps`.`sessionId`)
|
||||
LEFT JOIN `participates` `ph` ON (`s`.`id` = `ph`.`sessionId`)
|
||||
LEFT JOIN `triggerevents` `t` ON (`s`.`id` = `t`.`sessionId`)
|
||||
LEFT JOIN `triggereventtypes` `tt` ON (`tt`.`id` = `t`.`type`)
|
||||
LEFT JOIN `sessiontypes` `st` ON (`st`.`id` = `s`.`sessionType`)
|
||||
LEFT JOIN `reactevents` `r` ON (`t`.`indexCount` = `r`.`srcEventIndex` AND `t`.`sessionId` = `r`.`srcEventSessionId`)
|
||||
LEFT JOIN `users` `uh` ON (`uh`.`id` = `r`.`hitUserId`)
|
||||
LEFT JOIN `users` `us` ON (`us`.`id` = `t`.`srcUserId` OR `us`.`id` = `ps`.`userId`)
|
||||
LEFT JOIN `reacteventtypes` `rt` ON (`rt`.`id` = `r`.`reactType`)
|
||||
LEFT JOIN `reacteventmodes` `rm` ON (`rm`.`id` = COALESCE(`r`.`reactMode`, 2))
|
||||
LEFT JOIN `reactevents` `rk` ON (`r`.`id` = `rk`.`id` AND `rk`.`targetKilled` = 1)
|
||||
LEFT JOIN `userroles` `urs` ON (`urs`.`id` = IFNULL(`ps`.`role`, 3))
|
||||
LEFT JOIN `userroles` `urt` ON (`urt`.`id` = IFNULL(`ph`.`role`, 3))
|
||||
GROUP BY `SessionId`, `ShooterId`, `ShotIndex`, `TargetName`
|
||||
ORDER BY `SessionId`, `ShooterId`, `ShotIndex`, `ReactId`;
|
||||
|
||||
--
|
||||
-- Index pour les tables déchargées
|
||||
|
||||
@@ -16,12 +16,22 @@ $userId = isset($_POST['userId']) ? $_POST['userId'] : -1;
|
||||
// check sessionId is > 0
|
||||
//if ($sessionId > 0)
|
||||
//{
|
||||
// prepare user object
|
||||
$cacheKey = "debrief_" . intval($userId);
|
||||
$cached = cache_get($sessionId, $cacheKey);
|
||||
if ($cached !== null)
|
||||
{
|
||||
print_r($cached);
|
||||
}
|
||||
else
|
||||
{
|
||||
$stats = new StatsObject($db);
|
||||
$stats->getStatsForSession($sessionId, $userId);
|
||||
|
||||
$stats_arr = $stats->getResultArray(true, "Stats_Collected_OK");
|
||||
print_r(json_encode($stats_arr)); // OK
|
||||
$json = json_encode($stats_arr);
|
||||
cache_put($sessionId, $cacheKey, $json);
|
||||
print_r($json); // OK
|
||||
}
|
||||
//}
|
||||
//else trigger_error("Invalid session id");
|
||||
?>
|
||||
@@ -12,11 +12,17 @@ if (isset($_POST['sessionId']))
|
||||
$gameSession->id = $_POST['sessionId'];
|
||||
else trigger_error( missing_parameter_error("sessionId") );
|
||||
|
||||
// create the game session
|
||||
if ($gameSession->load())
|
||||
$cached = cache_get($gameSession->id, "session");
|
||||
if ($cached !== null)
|
||||
{
|
||||
print_r($cached);
|
||||
}
|
||||
else if ($gameSession->load())
|
||||
{
|
||||
$session_arr = $gameSession->getResultArray(true, "Session_Found");
|
||||
print_r(json_encode($session_arr)); // OK
|
||||
$json = json_encode($session_arr);
|
||||
cache_put($gameSession->id, "session", $json);
|
||||
print_r($json); // OK
|
||||
}
|
||||
else trigger_error("Error_Occured");
|
||||
?>
|
||||
@@ -20,6 +20,8 @@ $gameSession->score = $_POST['score'];
|
||||
// close the game session
|
||||
if ($gameSession->stop())
|
||||
{
|
||||
cache_invalidate_session($gameSession->id);
|
||||
rebuild_session_debrief_cache_db($db, $gameSession->id);
|
||||
$session_arr = $gameSession->getResultArray(true, "Session_Stop_OK");
|
||||
print_r(json_encode($session_arr)); // OK
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ if ($participation->load())
|
||||
// update objectives of user in the game session
|
||||
if ($participation->updateObjectives())
|
||||
{
|
||||
cache_invalidate_session($participation->sessionId);
|
||||
rebuild_session_debrief_cache_db($db, $participation->sessionId);
|
||||
$participation_arr = $participation->getResultArray(true, "Results_Saved_OK");
|
||||
print_r(json_encode($participation_arr)); // OK
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ if ($participation->userLeaves())
|
||||
// update score and averages
|
||||
if ($participation->updateUserScores())
|
||||
{
|
||||
cache_invalidate_session($participation->sessionId);
|
||||
rebuild_session_debrief_cache_db($db, $participation->sessionId);
|
||||
$participation_arr = $participation->getResultArray(true, "User_Left_OK");
|
||||
print_r(json_encode($participation_arr)); // OK
|
||||
}
|
||||
|
||||
@@ -20,11 +20,22 @@ if (isset($_POST['quickMode']))
|
||||
$quickMode = strtolower($_POST['quickMode']) == "true" ? 1 : 0;
|
||||
|
||||
//if ($userId > 0 && $sessionId > 0)
|
||||
{
|
||||
$cacheKey = "userhistory_" . intval($userId) . "_" . intval($quickMode);
|
||||
$cached = cache_get($sessionId, $cacheKey);
|
||||
if ($cached !== null)
|
||||
{
|
||||
print_r($cached);
|
||||
}
|
||||
else
|
||||
{
|
||||
$stats = new StatsObject($db);
|
||||
$stats->getUserHistory($userId, $sessionId, $quickMode);
|
||||
|
||||
$stats_arr = $stats->getResultArray(true, "Results_Collected_OK");
|
||||
print_r(json_encode($stats_arr)); // OK
|
||||
$json = json_encode($stats_arr);
|
||||
cache_put($sessionId, $cacheKey, $json);
|
||||
print_r($json); // OK
|
||||
}
|
||||
}
|
||||
?>
|
||||
126
ProserveAPI/htdocs/proserve/update_proserve_db 1.6.0.sql
Normal file
126
ProserveAPI/htdocs/proserve/update_proserve_db 1.6.0.sql
Normal file
@@ -0,0 +1,126 @@
|
||||
-- ============================================================
|
||||
-- Migration 1.6.0 - Optimisation des temps de reponse
|
||||
-- ============================================================
|
||||
-- Cible : MariaDB 10.4+ (XAMPP local).
|
||||
-- Idempotent : ce fichier peut etre rejoue sans erreur sur n'importe quel poste.
|
||||
-- Aucun changement de structure : seuls des index sont ajoutes
|
||||
-- (via une procedure stockee qui teste leur presence) et la vue
|
||||
-- sessiondebriefs est redefinie via CREATE OR REPLACE VIEW.
|
||||
-- ============================================================
|
||||
|
||||
USE proserveapi;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Procedure helper : ajoute un index seulement s'il n'existe pas
|
||||
-- (CREATE INDEX IF NOT EXISTS n'est dispo qu'a partir de MariaDB 10.5.4)
|
||||
-- ------------------------------------------------------------
|
||||
DROP PROCEDURE IF EXISTS create_index_if_missing;
|
||||
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE create_index_if_missing(
|
||||
IN p_table VARCHAR(64),
|
||||
IN p_index VARCHAR(64),
|
||||
IN p_cols VARCHAR(255)
|
||||
)
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = p_table
|
||||
AND INDEX_NAME = p_index
|
||||
) THEN
|
||||
SET @sql = CONCAT('ALTER TABLE `', p_table, '` ADD INDEX `', p_index, '` (', p_cols, ')');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Index manquants identifies a l'analyse des requetes lourdes
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
-- JOIN reactevents <-> triggerevents : la cle composite la plus utilisee
|
||||
CALL create_index_if_missing('reactevents', 'idx_react_session_event', '`srcEventSessionId`, `srcEventIndex`');
|
||||
|
||||
-- Filtres "tirs d'un user dans une session" (stats_object getShotsFiredByUser, getTargetKilledInSession)
|
||||
CALL create_index_if_missing('triggerevents', 'idx_trigger_session_user', '`sessionId`, `srcUserId`');
|
||||
|
||||
-- Agregation NbKilled (auto-jointure RK dans la vue sessiondebriefs)
|
||||
CALL create_index_if_missing('reactevents', 'idx_react_killed', '`targetKilled`, `srcEventSessionId`');
|
||||
|
||||
-- Tri/filtrage des sessions par date (listes recentes)
|
||||
CALL create_index_if_missing('sessions', 'idx_sessions_date', '`sessionDate` DESC');
|
||||
|
||||
-- Nettoyage de la procedure helper
|
||||
DROP PROCEDURE create_index_if_missing;
|
||||
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Redefinition de la vue sessiondebriefs (idempotent via CREATE OR REPLACE)
|
||||
-- ------------------------------------------------------------
|
||||
-- Modifications par rapport a la definition d'origine :
|
||||
-- - Suppression des wraps "(SELECT IFNULL(...))" qui forcaient une evaluation par row
|
||||
-- -> remplaces par IFNULL(...) directement (semantique identique).
|
||||
-- - Suppression des sous-requetes correlees (SELECT R.displayName FROM userroles R WHERE R.id = ...)
|
||||
-- -> remplacees par deux LEFT JOIN explicites sur userroles (URS et URT).
|
||||
-- - Tous les autres JOIN, le GROUP BY et l'ORDER BY restent strictement identiques
|
||||
-- pour preserver le contrat de sortie (memes colonnes, memes lignes, memes valeurs).
|
||||
-- ------------------------------------------------------------
|
||||
|
||||
CREATE OR REPLACE VIEW sessiondebriefs AS
|
||||
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,
|
||||
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,
|
||||
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 sessions S
|
||||
LEFT JOIN participates PS ON (S.id = PS.sessionId)
|
||||
LEFT JOIN participates PH ON (S.id = PH.sessionId)
|
||||
LEFT JOIN triggerevents T ON (S.id = T.sessionId)
|
||||
LEFT JOIN triggereventtypes TT ON (TT.id = T.type)
|
||||
LEFT JOIN sessiontypes ST ON (ST.id = S.sessionType)
|
||||
LEFT JOIN reactevents R ON (T.indexCount = R.srcEventIndex AND T.sessionId = R.srcEventSessionId)
|
||||
LEFT JOIN users UH ON (UH.id = R.hitUserId)
|
||||
LEFT JOIN users US ON (US.id = T.srcUserId OR US.id = PS.userId)
|
||||
LEFT JOIN reacteventtypes RT ON (RT.id = R.reactType)
|
||||
LEFT JOIN reacteventmodes RM ON (RM.id = COALESCE(R.reactMode, 2))
|
||||
LEFT JOIN reactevents RK ON (R.id = RK.id AND RK.targetKilled = 1)
|
||||
LEFT JOIN userroles URS ON (URS.id = IFNULL(PS.role, 3))
|
||||
LEFT JOIN userroles URT ON (URT.id = IFNULL(PH.role, 3))
|
||||
GROUP BY SessionId, ShooterId, ShotIndex, TargetName
|
||||
ORDER BY SessionId, ShooterId, ShotIndex, ReactId;
|
||||
236
ProserveAPI/htdocs/proserve/update_proserve_db 1.7.0.sql
Normal file
236
ProserveAPI/htdocs/proserve/update_proserve_db 1.7.0.sql
Normal file
@@ -0,0 +1,236 @@
|
||||
-- ============================================================
|
||||
-- Migration 1.7.0 - Materialisation de la vue sessiondebriefs
|
||||
-- ============================================================
|
||||
-- Cible : MariaDB 10.4+.
|
||||
-- Idempotent : peut etre rejoue sans erreur. Le cache est entierement
|
||||
-- reconstruit a chaque execution (TRUNCATE + backfill).
|
||||
--
|
||||
-- Changement principal : la vue sessiondebriefs etait recalculee a chaque
|
||||
-- requete (12 LEFT JOIN + GROUP BY + sous-requetes correlees). Elle est
|
||||
-- desormais materialisee dans une vraie table session_debriefs_cache,
|
||||
-- peuplee :
|
||||
-- - en bulk au moment de cette migration (backfill toutes sessions),
|
||||
-- - puis par une procedure stockee rebuild_debrief_cache(p_session_id)
|
||||
-- appelee depuis le code PHP a chaque modification de session
|
||||
-- (session/stop, session/userleave, session/updateobjectives,
|
||||
-- events/storetriggerevent, events/storereactevent).
|
||||
--
|
||||
-- La vue sessiondebriefs garde son nom et ses colonnes : le code consommateur
|
||||
-- (stats_object.php : getResultsForSession, getUserHistory) ne change pas.
|
||||
-- Elle devient un simple passe-plat sur la table cache.
|
||||
--
|
||||
-- Action 2 (fix partiel du double JOIN participates) : la jointure PH
|
||||
-- (qui sert a determiner TargetRoleId/TargetRole) est desormais contrainte
|
||||
-- sur PH.userId = R.hitUserId au lieu de matcher tous les participants.
|
||||
-- Effet :
|
||||
-- * Pour les hits user-on-user : meme resultat (correct par construction).
|
||||
-- * Pour les hits NPC/objets : TargetRoleId vaut 3 (IA) au lieu d'un
|
||||
-- role aleatoire d'un autre participant. C'est un fix de bug.
|
||||
-- La jointure PS reste inchangee (preserve le fallback ShooterId quand T est NULL).
|
||||
-- ============================================================
|
||||
|
||||
USE proserveapi;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 1) Table de cache (creation idempotente)
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS `session_debriefs_cache` (
|
||||
`SessionId` int(11) NOT NULL,
|
||||
`SessionTypeId` int(11) DEFAULT NULL,
|
||||
`SessionType` varchar(255) DEFAULT NULL,
|
||||
`SessionName` varchar(255) DEFAULT NULL,
|
||||
`SessionDate` datetime DEFAULT NULL,
|
||||
`MapName` varchar(255) DEFAULT NULL,
|
||||
`ScenarioName` varchar(255) DEFAULT NULL,
|
||||
`SessionSuccessful` tinyint(1) DEFAULT NULL,
|
||||
`SessionDuration` float DEFAULT NULL,
|
||||
`TriggerTypeId` int(11) DEFAULT NULL,
|
||||
`TriggerType` varchar(255) DEFAULT NULL,
|
||||
`ShooterId` int(11) DEFAULT NULL,
|
||||
`ShooterName` varchar(255) DEFAULT NULL,
|
||||
`ShooterRoleId` int(11) DEFAULT NULL,
|
||||
`ShooterRole` varchar(255) DEFAULT NULL,
|
||||
`ShotIndex` int(11) DEFAULT NULL,
|
||||
`ReactId` int(11) DEFAULT NULL,
|
||||
`ReactModeId` int(11) DEFAULT NULL,
|
||||
`ReactMode` varchar(255) DEFAULT NULL,
|
||||
`ReactTypeId` int(11) DEFAULT NULL,
|
||||
`ReactType` varchar(255) DEFAULT NULL,
|
||||
`TargetUserId` int(11) DEFAULT NULL,
|
||||
`TargetUserName` varchar(255) DEFAULT NULL,
|
||||
`TargetRoleId` int(11) DEFAULT NULL,
|
||||
`TargetRole` varchar(255) DEFAULT NULL,
|
||||
`TargetName` varchar(255) DEFAULT NULL,
|
||||
`TargetBoneName` varchar(255) DEFAULT NULL,
|
||||
`TargetKilled` int(4) DEFAULT NULL,
|
||||
`HitLocationX` double DEFAULT NULL,
|
||||
`HitLocationY` double DEFAULT NULL,
|
||||
`HitLocationTag` varchar(255) DEFAULT NULL,
|
||||
`HitPrecision` double DEFAULT NULL,
|
||||
`HitTargetDistance` double DEFAULT NULL,
|
||||
`ReactionTime` double DEFAULT NULL,
|
||||
`TimeStamp` double DEFAULT NULL,
|
||||
`NbHit` bigint(21) DEFAULT NULL,
|
||||
`NbKilled` bigint(21) DEFAULT NULL,
|
||||
KEY `idx_sdc_session` (`SessionId`),
|
||||
KEY `idx_sdc_session_shooter` (`SessionId`, `ShooterId`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 2) Procedure stockee pour reconstruire le cache d'une session
|
||||
-- ------------------------------------------------------------
|
||||
DROP PROCEDURE IF EXISTS rebuild_debrief_cache;
|
||||
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE rebuild_debrief_cache(IN p_session_id INT)
|
||||
BEGIN
|
||||
DELETE FROM session_debriefs_cache WHERE SessionId = p_session_id;
|
||||
|
||||
INSERT INTO session_debriefs_cache (
|
||||
SessionId, SessionTypeId, SessionType, SessionName, SessionDate,
|
||||
MapName, ScenarioName, SessionSuccessful, SessionDuration,
|
||||
TriggerTypeId, TriggerType, ShooterId, ShooterName,
|
||||
ShooterRoleId, ShooterRole, ShotIndex, ReactId, ReactModeId,
|
||||
ReactMode, ReactTypeId, ReactType, TargetUserId, TargetUserName,
|
||||
TargetRoleId, TargetRole, TargetName, TargetBoneName, TargetKilled,
|
||||
HitLocationX, HitLocationY, HitLocationTag, HitPrecision,
|
||||
HitTargetDistance, ReactionTime, TimeStamp, NbHit, NbKilled
|
||||
)
|
||||
SELECT
|
||||
S.id,
|
||||
S.sessionType,
|
||||
ST.displayName,
|
||||
S.sessionName,
|
||||
S.sessionDate,
|
||||
S.mapName,
|
||||
S.scenarioName,
|
||||
S.success,
|
||||
S.timeToFinish,
|
||||
COALESCE(T.type, -1),
|
||||
COALESCE(TT.displayName, ''),
|
||||
IFNULL(T.srcUserId, PS.userId),
|
||||
US.username,
|
||||
IFNULL(PS.role, 3),
|
||||
URS.displayName,
|
||||
COALESCE(T.indexCount, -1),
|
||||
COALESCE(R.id, -1),
|
||||
COALESCE(R.reactMode, 2),
|
||||
COALESCE(RM.displayName, ''),
|
||||
COALESCE(R.reactType, -1),
|
||||
COALESCE(RT.displayName, ''),
|
||||
COALESCE(R.hitUserId, -1),
|
||||
COALESCE(UH.username, ''),
|
||||
IFNULL(PH.role, 3),
|
||||
URT.displayName,
|
||||
COALESCE(R.hitTargetName, ''),
|
||||
COALESCE(R.hitBoneName, ''),
|
||||
COALESCE(R.targetKilled, 0),
|
||||
COALESCE(R.objectHitLocationX, 0),
|
||||
COALESCE(R.objectHitLocationY, 0),
|
||||
COALESCE(R.objectHitTagLocation, ''),
|
||||
COALESCE(R.hitPrecision, 0),
|
||||
COALESCE(R.distance, 0),
|
||||
COALESCE(R.reactTime, 0),
|
||||
COALESCE(R.timeStamp, 0),
|
||||
COUNT(DISTINCT R.id),
|
||||
COUNT(DISTINCT RK.srcEventIndex, RK.hitTargetName)
|
||||
FROM sessions S
|
||||
LEFT JOIN participates PS ON (S.id = PS.sessionId)
|
||||
LEFT JOIN triggerevents T ON (S.id = T.sessionId)
|
||||
LEFT JOIN triggereventtypes TT ON (TT.id = T.type)
|
||||
LEFT JOIN sessiontypes ST ON (ST.id = S.sessionType)
|
||||
LEFT JOIN reactevents R ON (T.indexCount = R.srcEventIndex AND T.sessionId = R.srcEventSessionId)
|
||||
LEFT JOIN users UH ON (UH.id = R.hitUserId)
|
||||
LEFT JOIN users US ON (US.id = T.srcUserId OR US.id = PS.userId)
|
||||
LEFT JOIN reacteventtypes RT ON (RT.id = R.reactType)
|
||||
LEFT JOIN reacteventmodes RM ON (RM.id = COALESCE(R.reactMode, 2))
|
||||
LEFT JOIN reactevents RK ON (R.id = RK.id AND RK.targetKilled = 1)
|
||||
-- PH : Action 2 du fix - PH represente la participation de l'utilisateur cible
|
||||
-- (au lieu de "n'importe quel participant") pour que TargetRole soit coherent.
|
||||
LEFT JOIN participates PH ON (S.id = PH.sessionId AND PH.userId = R.hitUserId)
|
||||
LEFT JOIN userroles URS ON (URS.id = IFNULL(PS.role, 3))
|
||||
LEFT JOIN userroles URT ON (URT.id = IFNULL(PH.role, 3))
|
||||
WHERE S.id = p_session_id
|
||||
GROUP BY S.id, IFNULL(T.srcUserId, PS.userId), COALESCE(T.indexCount, -1), COALESCE(R.hitTargetName, '')
|
||||
ORDER BY S.id, IFNULL(T.srcUserId, PS.userId), COALESCE(T.indexCount, -1), COALESCE(R.id, -1);
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 3) Backfill complet : recalcule le cache pour toutes les sessions existantes
|
||||
-- ------------------------------------------------------------
|
||||
TRUNCATE TABLE session_debriefs_cache;
|
||||
|
||||
INSERT INTO session_debriefs_cache (
|
||||
SessionId, SessionTypeId, SessionType, SessionName, SessionDate,
|
||||
MapName, ScenarioName, SessionSuccessful, SessionDuration,
|
||||
TriggerTypeId, TriggerType, ShooterId, ShooterName,
|
||||
ShooterRoleId, ShooterRole, ShotIndex, ReactId, ReactModeId,
|
||||
ReactMode, ReactTypeId, ReactType, TargetUserId, TargetUserName,
|
||||
TargetRoleId, TargetRole, TargetName, TargetBoneName, TargetKilled,
|
||||
HitLocationX, HitLocationY, HitLocationTag, HitPrecision,
|
||||
HitTargetDistance, ReactionTime, TimeStamp, NbHit, NbKilled
|
||||
)
|
||||
SELECT
|
||||
S.id,
|
||||
S.sessionType,
|
||||
ST.displayName,
|
||||
S.sessionName,
|
||||
S.sessionDate,
|
||||
S.mapName,
|
||||
S.scenarioName,
|
||||
S.success,
|
||||
S.timeToFinish,
|
||||
COALESCE(T.type, -1),
|
||||
COALESCE(TT.displayName, ''),
|
||||
IFNULL(T.srcUserId, PS.userId),
|
||||
US.username,
|
||||
IFNULL(PS.role, 3),
|
||||
URS.displayName,
|
||||
COALESCE(T.indexCount, -1),
|
||||
COALESCE(R.id, -1),
|
||||
COALESCE(R.reactMode, 2),
|
||||
COALESCE(RM.displayName, ''),
|
||||
COALESCE(R.reactType, -1),
|
||||
COALESCE(RT.displayName, ''),
|
||||
COALESCE(R.hitUserId, -1),
|
||||
COALESCE(UH.username, ''),
|
||||
IFNULL(PH.role, 3),
|
||||
URT.displayName,
|
||||
COALESCE(R.hitTargetName, ''),
|
||||
COALESCE(R.hitBoneName, ''),
|
||||
COALESCE(R.targetKilled, 0),
|
||||
COALESCE(R.objectHitLocationX, 0),
|
||||
COALESCE(R.objectHitLocationY, 0),
|
||||
COALESCE(R.objectHitTagLocation, ''),
|
||||
COALESCE(R.hitPrecision, 0),
|
||||
COALESCE(R.distance, 0),
|
||||
COALESCE(R.reactTime, 0),
|
||||
COALESCE(R.timeStamp, 0),
|
||||
COUNT(DISTINCT R.id),
|
||||
COUNT(DISTINCT RK.srcEventIndex, RK.hitTargetName)
|
||||
FROM sessions S
|
||||
LEFT JOIN participates PS ON (S.id = PS.sessionId)
|
||||
LEFT JOIN triggerevents T ON (S.id = T.sessionId)
|
||||
LEFT JOIN triggereventtypes TT ON (TT.id = T.type)
|
||||
LEFT JOIN sessiontypes ST ON (ST.id = S.sessionType)
|
||||
LEFT JOIN reactevents R ON (T.indexCount = R.srcEventIndex AND T.sessionId = R.srcEventSessionId)
|
||||
LEFT JOIN users UH ON (UH.id = R.hitUserId)
|
||||
LEFT JOIN users US ON (US.id = T.srcUserId OR US.id = PS.userId)
|
||||
LEFT JOIN reacteventtypes RT ON (RT.id = R.reactType)
|
||||
LEFT JOIN reacteventmodes RM ON (RM.id = COALESCE(R.reactMode, 2))
|
||||
LEFT JOIN reactevents RK ON (R.id = RK.id AND RK.targetKilled = 1)
|
||||
LEFT JOIN participates PH ON (S.id = PH.sessionId AND PH.userId = R.hitUserId)
|
||||
LEFT JOIN userroles URS ON (URS.id = IFNULL(PS.role, 3))
|
||||
LEFT JOIN userroles URT ON (URT.id = IFNULL(PH.role, 3))
|
||||
GROUP BY S.id, IFNULL(T.srcUserId, PS.userId), COALESCE(T.indexCount, -1), COALESCE(R.hitTargetName, '')
|
||||
ORDER BY S.id, IFNULL(T.srcUserId, PS.userId), COALESCE(T.indexCount, -1), COALESCE(R.id, -1);
|
||||
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 4) La vue devient un passe-plat sur la table cache
|
||||
-- ------------------------------------------------------------
|
||||
CREATE OR REPLACE VIEW sessiondebriefs AS
|
||||
SELECT * FROM session_debriefs_cache;
|
||||
Reference in New Issue
Block a user