@@ -28,16 +28,11 @@ import { selectRegions } from './src/regions';
2828import { SessionService } from './src/sessions' ;
2929import { buildSessionAllocationEvent , buildSessionAnalyticsMetadata , normalizeViewerRttSummary } from './src/session-analytics' ;
3030import {
31- getActiveSessionCount ,
32- getSessionsByRegion ,
33- getSessionAllocationStats ,
34- getSessionRttStats ,
31+ getActiveSessionStats ,
32+ getAnalyticsTimeSeries ,
33+ getSessionDimensions ,
34+ getSessionOverviewStats ,
3535 getSessionRttStatsByRegion ,
36- getSessionTimeSeries ,
37- getSessionWindowStats ,
38- getViewerRttTimeSeries ,
39- getStaleActiveSessionCount ,
40- getTopClients ,
4136} from './src/stats' ;
4237import { expiresAtFromTtlSeconds , extendExpiresAt , readOptionalSeconds , validateTtlSeconds } from './src/ttl' ;
4338import { X402PaymentGateway } from './src/x402-payment' ;
@@ -775,7 +770,7 @@ function normalizeWindowHours(raw: string | number | undefined): number {
775770}
776771
777772// Pick a trend-chart bucket count so longer ranges keep useful granularity
778- // without over-crowding the x-axis (getSessionTimeSeries caps at 48).
773+ // without over-crowding the x-axis (getAnalyticsTimeSeries caps at 48).
779774function bucketsForWindow ( windowHours : number ) : number {
780775 if ( windowHours <= 24 ) return 12 ; // 1h→5m, 6h→30m, 24h→2h
781776 if ( windowHours <= 72 ) return 12 ; // 2d→4h, 3d→6h
@@ -784,6 +779,87 @@ function bucketsForWindow(windowHours: number): number {
784779 return 15 ; // 30d→2d
785780}
786781
782+ type HistoricalSessionAnalytics = {
783+ overview : Awaited < ReturnType < typeof getSessionOverviewStats > > ;
784+ rttByRegion : Awaited < ReturnType < typeof getSessionRttStatsByRegion > > ;
785+ timeSeries : Awaited < ReturnType < typeof getAnalyticsTimeSeries > > ;
786+ dimensions : Awaited < ReturnType < typeof getSessionDimensions > > ;
787+ } ;
788+
789+ type HistoricalSessionAnalyticsCacheEntry = {
790+ value ?: HistoricalSessionAnalytics ;
791+ expiresAt : number ;
792+ inFlight ?: Promise < HistoricalSessionAnalytics > ;
793+ } ;
794+
795+ const HISTORICAL_ANALYTICS_CACHE_MAX_ENTRIES = 16 ;
796+ const historicalAnalyticsCache = new Map < number , HistoricalSessionAnalyticsCacheEntry > ( ) ;
797+
798+ function historicalAnalyticsTtlMs ( windowHours : number ) {
799+ if ( windowHours >= 168 ) return 60_000 ;
800+ if ( windowHours >= 24 ) return 30_000 ;
801+ return 15_000 ;
802+ }
803+
804+ async function loadHistoricalSessionAnalytics ( windowHours : number ) : Promise < HistoricalSessionAnalytics > {
805+ // Keep the two most expensive percentile phases separate to bound peak
806+ // memory on PostgreSQL. Queries inside each phase reuse covering indexes.
807+ const [ overview , rttByRegion ] = await Promise . all ( [
808+ getSessionOverviewStats ( windowHours ) ,
809+ getSessionRttStatsByRegion ( windowHours ) ,
810+ ] ) ;
811+ const [ timeSeries , dimensions ] = await Promise . all ( [
812+ getAnalyticsTimeSeries ( windowHours , bucketsForWindow ( windowHours ) ) ,
813+ getSessionDimensions ( windowHours ) ,
814+ ] ) ;
815+ return { overview, rttByRegion, timeSeries, dimensions } ;
816+ }
817+
818+ function startHistoricalAnalyticsRefresh (
819+ windowHours : number ,
820+ entry : HistoricalSessionAnalyticsCacheEntry ,
821+ ) : Promise < HistoricalSessionAnalytics > {
822+ const inFlight = loadHistoricalSessionAnalytics ( windowHours )
823+ . then ( ( value ) => {
824+ historicalAnalyticsCache . delete ( windowHours ) ;
825+ historicalAnalyticsCache . set ( windowHours , {
826+ value,
827+ expiresAt : Date . now ( ) + historicalAnalyticsTtlMs ( windowHours ) ,
828+ } ) ;
829+ while ( historicalAnalyticsCache . size > HISTORICAL_ANALYTICS_CACHE_MAX_ENTRIES ) {
830+ const oldestKey = historicalAnalyticsCache . keys ( ) . next ( ) . value ;
831+ if ( oldestKey === undefined ) break ;
832+ historicalAnalyticsCache . delete ( oldestKey ) ;
833+ }
834+ return value ;
835+ } )
836+ . catch ( ( error ) => {
837+ if ( entry . value ) {
838+ entry . inFlight = undefined ;
839+ historicalAnalyticsCache . set ( windowHours , entry ) ;
840+ } else {
841+ historicalAnalyticsCache . delete ( windowHours ) ;
842+ }
843+ throw error ;
844+ } ) ;
845+ entry . inFlight = inFlight ;
846+ historicalAnalyticsCache . set ( windowHours , entry ) ;
847+ return inFlight ;
848+ }
849+
850+ async function getHistoricalSessionAnalytics ( windowHours : number ) : Promise < HistoricalSessionAnalytics > {
851+ const entry = historicalAnalyticsCache . get ( windowHours ) ?? { expiresAt : 0 } ;
852+ if ( entry . value && entry . expiresAt > Date . now ( ) ) return entry . value ;
853+ if ( entry . value ) {
854+ if ( ! entry . inFlight ) {
855+ void startHistoricalAnalyticsRefresh ( windowHours , entry )
856+ . catch ( ( error ) => console . error ( 'Failed to refresh historical analytics cache:' , error ) ) ;
857+ }
858+ return entry . value ;
859+ }
860+ return entry . inFlight ?? startHistoricalAnalyticsRefresh ( windowHours , entry ) ;
861+ }
862+
787863// Combines live Agones gauges (via pool managers) with cumulative Postgres
788864// session stats into a single payload shared by the API and the admin UI.
789865// USDC and the other supported x402 settlement assets use 6 decimals; the
@@ -803,19 +879,24 @@ async function buildX402AnalyticsPayload(windowHours: number): Promise<X402Analy
803879}
804880
805881async function buildStatsPayload ( windowHours : number ) : Promise < AnalyticsData > {
806- const [ regions , windowStats , allocationStats , rttStats , rttByRegion , rttSeries , activeSessions , staleActiveSessions , series , regionSessions , topClients ] = await Promise . all ( [
882+ // Live fleet state remains uncached. Historical aggregates use a short,
883+ // bounded stale-while-refresh cache so manual refreshes do not repeatedly
884+ // sort the same 30-day percentile data.
885+ const [ regions , historical , activeSessionStats ] = await Promise . all ( [
807886 loadAdminRegions ( ) ,
808- getSessionWindowStats ( windowHours ) ,
809- getSessionAllocationStats ( windowHours ) ,
810- getSessionRttStats ( windowHours ) ,
811- getSessionRttStatsByRegion ( windowHours ) ,
812- getViewerRttTimeSeries ( windowHours , bucketsForWindow ( windowHours ) ) ,
813- getActiveSessionCount ( ) ,
814- getStaleActiveSessionCount ( ) ,
815- getSessionTimeSeries ( windowHours , bucketsForWindow ( windowHours ) ) ,
816- getSessionsByRegion ( windowHours ) ,
817- getTopClients ( windowHours ) ,
887+ getHistoricalSessionAnalytics ( windowHours ) ,
888+ getActiveSessionStats ( ) ,
818889 ] ) ;
890+ const { overview, rttByRegion, timeSeries, dimensions } = historical ;
891+ const windowStats = overview . window ;
892+ const allocationStats = overview . allocation ;
893+ const rttStats = overview . viewerRtt ;
894+ const series = timeSeries . sessions ;
895+ const rttSeries = timeSeries . viewerRtt ;
896+ const activeSessions = activeSessionStats . active ;
897+ const staleActiveSessions = activeSessionStats . stale ;
898+ const regionSessions = dimensions . byRegion ;
899+ const topClients = dimensions . topClients ;
819900
820901 const enabledRegions = regions . filter ( ( region ) => region . enabled ) ;
821902 const servers = enabledRegions . flatMap ( ( region ) => region . servers || [ ] ) ;
0 commit comments