Added new service ProfileActivityService

Maintains a timestamp of when each profile was last active on the server.
Timestamp is refreshed on two events: `game/start and `game/keepalive`

Timestamp is used to determine if a profiles hideout should be processed. If a profile has a timestamp older than 90 minutes, it is not processed. (could be set lower but its a conservative value)
This has the side effects of not adjusting the `sptUpdateLastRunTimestamp` property, resulting in `saveProfile()` not running for every profile.

My testing shows a 20x perf increase for every profile in SPT not in use.

Service could likely be used in other scenarios to avoid doing unnecessary work
This commit is contained in:
Dev 2024-04-13 12:46:43 +01:00
parent 73a1dd9511
commit a9d89695f0
6 changed files with 59 additions and 2 deletions

View File

@ -7,5 +7,6 @@
}, },
"expCraftAmount": 10, "expCraftAmount": 10,
"overrideCraftTimeSeconds": -1, "overrideCraftTimeSeconds": -1,
"overrideBuildTimeSeconds": -1 "overrideBuildTimeSeconds": -1,
"updateProfileHideoutWhenActiveWithinMinutes": 90
} }

View File

@ -39,6 +39,7 @@ import { GiftService } from "@spt-aki/services/GiftService";
import { ItemBaseClassService } from "@spt-aki/services/ItemBaseClassService"; import { ItemBaseClassService } from "@spt-aki/services/ItemBaseClassService";
import { LocalisationService } from "@spt-aki/services/LocalisationService"; import { LocalisationService } from "@spt-aki/services/LocalisationService";
import { OpenZoneService } from "@spt-aki/services/OpenZoneService"; import { OpenZoneService } from "@spt-aki/services/OpenZoneService";
import { ProfileActivityService } from "@spt-aki/services/ProfileActivityService";
import { ProfileFixerService } from "@spt-aki/services/ProfileFixerService"; import { ProfileFixerService } from "@spt-aki/services/ProfileFixerService";
import { RaidTimeAdjustmentService } from "@spt-aki/services/RaidTimeAdjustmentService"; import { RaidTimeAdjustmentService } from "@spt-aki/services/RaidTimeAdjustmentService";
import { SeasonalEventService } from "@spt-aki/services/SeasonalEventService"; import { SeasonalEventService } from "@spt-aki/services/SeasonalEventService";
@ -78,6 +79,7 @@ export class GameController
@inject("ItemBaseClassService") protected itemBaseClassService: ItemBaseClassService, @inject("ItemBaseClassService") protected itemBaseClassService: ItemBaseClassService,
@inject("GiftService") protected giftService: GiftService, @inject("GiftService") protected giftService: GiftService,
@inject("RaidTimeAdjustmentService") protected raidTimeAdjustmentService: RaidTimeAdjustmentService, @inject("RaidTimeAdjustmentService") protected raidTimeAdjustmentService: RaidTimeAdjustmentService,
@inject("ProfileActivityService") protected profileActivityService: ProfileActivityService,
@inject("ApplicationContext") protected applicationContext: ApplicationContext, @inject("ApplicationContext") protected applicationContext: ApplicationContext,
@inject("ConfigServer") protected configServer: ConfigServer, @inject("ConfigServer") protected configServer: ConfigServer,
) )
@ -109,6 +111,8 @@ export class GameController
// Store client start time in app context // Store client start time in app context
this.applicationContext.addValue(ContextVariableType.CLIENT_START_TIMESTAMP, startTimeStampMS); this.applicationContext.addValue(ContextVariableType.CLIENT_START_TIMESTAMP, startTimeStampMS);
this.profileActivityService.setActivityTimestamp(sessionID);
if (this.coreConfig.fixes.fixShotgunDispersion) if (this.coreConfig.fixes.fixShotgunDispersion)
{ {
this.fixShotgunDispersions(); this.fixShotgunDispersions();
@ -504,6 +508,7 @@ export class GameController
*/ */
public getKeepAlive(sessionId: string): IGameKeepAliveResponse public getKeepAlive(sessionId: string): IGameKeepAliveResponse
{ {
this.profileActivityService.setActivityTimestamp(sessionId);
return { msg: "OK", utc_time: new Date().getTime() / 1000 }; return { msg: "OK", utc_time: new Date().getTime() / 1000 };
} }

View File

@ -48,6 +48,7 @@ import { SaveServer } from "@spt-aki/servers/SaveServer";
import { FenceService } from "@spt-aki/services/FenceService"; import { FenceService } from "@spt-aki/services/FenceService";
import { LocalisationService } from "@spt-aki/services/LocalisationService"; import { LocalisationService } from "@spt-aki/services/LocalisationService";
import { PlayerService } from "@spt-aki/services/PlayerService"; import { PlayerService } from "@spt-aki/services/PlayerService";
import { ProfileActivityService } from "@spt-aki/services/ProfileActivityService";
import { HashUtil } from "@spt-aki/utils/HashUtil"; import { HashUtil } from "@spt-aki/utils/HashUtil";
import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil"; import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil";
import { JsonUtil } from "@spt-aki/utils/JsonUtil"; import { JsonUtil } from "@spt-aki/utils/JsonUtil";
@ -79,6 +80,7 @@ export class HideoutController
@inject("HideoutHelper") protected hideoutHelper: HideoutHelper, @inject("HideoutHelper") protected hideoutHelper: HideoutHelper,
@inject("ScavCaseRewardGenerator") protected scavCaseRewardGenerator: ScavCaseRewardGenerator, @inject("ScavCaseRewardGenerator") protected scavCaseRewardGenerator: ScavCaseRewardGenerator,
@inject("LocalisationService") protected localisationService: LocalisationService, @inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ProfileActivityService") protected profileActivityService: ProfileActivityService,
@inject("ConfigServer") protected configServer: ConfigServer, @inject("ConfigServer") protected configServer: ConfigServer,
@inject("JsonUtil") protected jsonUtil: JsonUtil, @inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("FenceService") protected fenceService: FenceService, @inject("FenceService") protected fenceService: FenceService,
@ -1323,7 +1325,13 @@ export class HideoutController
{ {
for (const sessionID in this.saveServer.getProfiles()) for (const sessionID in this.saveServer.getProfiles())
{ {
if ("Hideout" in this.saveServer.getProfile(sessionID).characters.pmc) if (
"Hideout" in this.saveServer.getProfile(sessionID).characters.pmc
&& this.profileActivityService.activeWithinLastMinutes(
sessionID,
this.hideoutConfig.updateProfileHideoutWhenActiveWithinMinutes,
)
)
{ {
this.hideoutHelper.updatePlayerHideout(sessionID); this.hideoutHelper.updatePlayerHideout(sessionID);
} }

View File

@ -213,6 +213,7 @@ import { OpenZoneService } from "@spt-aki/services/OpenZoneService";
import { PaymentService } from "@spt-aki/services/PaymentService"; import { PaymentService } from "@spt-aki/services/PaymentService";
import { PlayerService } from "@spt-aki/services/PlayerService"; import { PlayerService } from "@spt-aki/services/PlayerService";
import { PmcChatResponseService } from "@spt-aki/services/PmcChatResponseService"; import { PmcChatResponseService } from "@spt-aki/services/PmcChatResponseService";
import { ProfileActivityService } from "@spt-aki/services/ProfileActivityService";
import { ProfileFixerService } from "@spt-aki/services/ProfileFixerService"; import { ProfileFixerService } from "@spt-aki/services/ProfileFixerService";
import { ProfileSnapshotService } from "@spt-aki/services/ProfileSnapshotService"; import { ProfileSnapshotService } from "@spt-aki/services/ProfileSnapshotService";
import { RagfairCategoriesService } from "@spt-aki/services/RagfairCategoriesService"; import { RagfairCategoriesService } from "@spt-aki/services/RagfairCategoriesService";
@ -747,6 +748,10 @@ export class Container
depContainer.register<GiftService>("GiftService", GiftService); depContainer.register<GiftService>("GiftService", GiftService);
depContainer.register<MailSendService>("MailSendService", MailSendService); depContainer.register<MailSendService>("MailSendService", MailSendService);
depContainer.register<RaidTimeAdjustmentService>("RaidTimeAdjustmentService", RaidTimeAdjustmentService); depContainer.register<RaidTimeAdjustmentService>("RaidTimeAdjustmentService", RaidTimeAdjustmentService);
depContainer.register<ProfileActivityService>("ProfileActivityService", ProfileActivityService, {
lifecycle: Lifecycle.Singleton,
});
} }
private static registerServers(depContainer: DependencyContainer): void private static registerServers(depContainer: DependencyContainer): void

View File

@ -11,4 +11,6 @@ export interface IHideoutConfig extends IBaseConfig
expCraftAmount: number; expCraftAmount: number;
overrideCraftTimeSeconds: number; overrideCraftTimeSeconds: number;
overrideBuildTimeSeconds: number; overrideBuildTimeSeconds: number;
/** Only process a profiles hideout crafts when it has been active in the last x minutes */
updateProfileHideoutWhenActiveWithinMinutes: number;
} }

View File

@ -0,0 +1,36 @@
import { injectable } from "tsyringe";
@injectable()
export class ProfileActivityService
{
protected profileActivityTimestamps: Record<string, number> = {};
/**
* Was the requested profile active in the last requested minutes
* @param sessionId Profile to check
* @param minutes Minutes to check for activity in
* @returns True when profile was active within past x minutes
*/
public activeWithinLastMinutes(sessionId: string, minutes: number): boolean
{
const currentTimestamp = new Date().getTime() / 1000;
const storedActivityTimestamp = this.profileActivityTimestamps[sessionId];
if (!storedActivityTimestamp)
{
// No value, no assumed activity (server offline?)
return false;
}
// True if difference since last timestamp to now is below desired amount
return (currentTimestamp - storedActivityTimestamp) < (minutes * 60); // convert minutes to seconds to compare
}
/**
* Update the timestamp a profile was last observed active
* @param sessionId Profile to update
*/
public setActivityTimestamp(sessionId: string): void
{
this.profileActivityTimestamps[sessionId] = new Date().getTime() / 1000;
}
}