diff options
| author | William Hergès <william@herges.fr> | 2026-01-17 21:50:54 +0100 |
|---|---|---|
| committer | William Hergès <william@herges.fr> | 2026-01-17 21:50:54 +0100 |
| commit | c661541e45dddd6a082af66fcf7df7ba7dfdc6a6 (patch) | |
| tree | 318b29652c4f59a7f6a16ff7a566b1a9935d069d | |
| parent | ec5cfa632eeb607351f67bad6686ec872291bd61 (diff) | |
perf(command): store data used by top in state
| -rw-r--r-- | commands/rank.go | 7 | ||||
| -rw-r--r-- | commands/top.go | 11 | ||||
| -rw-r--r-- | exp/functions.go | 2 | ||||
| -rw-r--r-- | justfile | 5 | ||||
| -rw-r--r-- | user/level.go | 4 | ||||
| -rw-r--r-- | user/state.go | 63 | ||||
| -rw-r--r-- | user/xp.go | 96 |
7 files changed, 79 insertions, 109 deletions
diff --git a/commands/rank.go b/commands/rank.go index c65c758..3a017f6 100644 --- a/commands/rank.go +++ b/commands/rank.go @@ -38,15 +38,12 @@ func Rank(ctx context.Context) func(s bot.Session, i *event.InteractionCreate, o c = user.GetCopaing(ctx, u.ID, i.GuildID) // current user = member targeted by member who wrote /rank msg = fmt.Sprintf("Le niveau de %s", m.DisplayName()) } - xp := c.XPs + xp := c.XP lvl := exp.Level(xp) nxtLvlXP := exp.LevelXP(lvl + 1) err = resp.SetMessage(fmt.Sprintf( "%s : **%d**\n> XP : %d\n> Prochain niveau dans %d XP", - msg, - lvl, - xp, - nxtLvlXP-xp, + msg, lvl, xp, nxtLvlXP-xp, )).Send() if err != nil { s.Logger().Error("sending rank", "error", err) diff --git a/commands/top.go b/commands/top.go index fa12a66..99c8f16 100644 --- a/commands/top.go +++ b/commands/top.go @@ -16,17 +16,12 @@ import ( func Top(ctx context.Context) func(s bot.Session, i *event.InteractionCreate, _ cmd.OptionMap, resp *cmd.ResponseBuilder) { return func(s bot.Session, i *event.InteractionCreate, _ cmd.OptionMap, resp *cmd.ResponseBuilder) { - err := resp.IsDeferred().Send() - if err != nil { - s.Logger().Error("sending deferred", "error", err) - return - } embeds := make([]*channel.MessageEmbed, 3) wg := sync.WaitGroup{} fn := func(str string, n uint, d int, id int) { defer wg.Done() - tops, err := user.GetBestXP(ctx, s.Logger(), i.GuildID, n, d) + tops, err := user.GetBestXP(ctx, i.GuildID, n, d) if err != nil { s.Logger().Error("fetching best xp", "error", err, "n", n, "d", d, "id", id, "guild", i.GuildID) embeds[id] = &channel.MessageEmbed{ @@ -60,7 +55,7 @@ func Top(ctx context.Context) func(s bot.Session, i *event.InteractionCreate, _ resp.AddEmbed(embeds[1]). AddEmbed(embeds[2]) } - err = resp.Send() + err := resp.Send() if err != nil { s.Logger().Error("sending response top", "error", err) } @@ -71,7 +66,7 @@ func Top(ctx context.Context) func(s bot.Session, i *event.InteractionCreate, _ func genTopsMessage(tops []user.CopaingCached) string { msg := "" for i, c := range tops { - msg += fmt.Sprintf("%d. **<@%s>** - niveau %d", i+1, c.DiscordID, exp.Level(c.XPs)) + msg += fmt.Sprintf("%d. **<@%s>** - niveau %d", i+1, c.DiscordID, exp.Level(c.XP)) if i != len(tops)-1 { msg += "\n" } diff --git a/exp/functions.go b/exp/functions.go index 1b2fe89..681c135 100644 --- a/exp/functions.go +++ b/exp/functions.go @@ -11,7 +11,7 @@ import ( "github.com/anhgelus/gokord" ) -const DebugFactor = 5 +const DebugFactor = 30 func MessageXP(length uint, diversity uint) uint { return uint(math.Floor( @@ -12,10 +12,7 @@ update: go run . stop: - podman stop postgres adminer || (echo "no container"; exit 0) - podman network rm db - -clean-network: + podman stop postgres adminer || (echo "no container") podman network rm db || echo "no network" build: diff --git a/user/level.go b/user/level.go index 07df16f..92c10ab 100644 --- a/user/level.go +++ b/user/level.go @@ -93,13 +93,13 @@ func syncCopaings(ctx context.Context, s *discordgo.Session, ccs []CopaingCached s.Logger().Debug("sleeping...") time.Sleep(15 * time.Second) // prevents spamming the API } - oldXp := cc.XPs + oldXp := cc.XP err := cc.Sync(ctx) if err != nil { s.Logger().Error("syncing copaing", "error", err, "copaing", cc.ID, "guild", cc.GuildID) continue } - xp := cc.XPs + xp := cc.XP if exp.Level(oldXp) != exp.Level(xp) { cc.onNewLevel(s, exp.Level(xp)) } diff --git a/user/state.go b/user/state.go index b977fb6..effdc80 100644 --- a/user/state.go +++ b/user/state.go @@ -6,18 +6,24 @@ import ( "sync" "time" + "github.com/anhgelus/gokord" "github.com/nyttikord/gokord/state" ) var ErrSyncingUnsavedData = errors.New("trying to sync unsaved data") +type XPCached struct { + XP uint + Time time.Duration +} + type CopaingCached struct { ID uint DiscordID string GuildID string - XPs uint + XP uint + XPs []XPCached XPToAdd uint - lastSync time.Time // time.Time of the lastSync } // copaing turns a CopaingCached into a Copaing. @@ -39,9 +45,8 @@ func (cc *CopaingCached) Sync(ctx context.Context) error { return ErrSyncingUnsavedData } synced := FromCopaing(cc.copaing()) - synced.XPs += cc.XPToAdd + synced.XP += cc.XPToAdd synced.XPToAdd = cc.XPToAdd - synced.lastSync = time.Now() err := synced.Save(ctx) if err != nil { return err @@ -89,7 +94,12 @@ func (cc *CopaingCached) mustSave() bool { } func saveStateInDB(ctx context.Context) error { - for _, v := range GetState(ctx).storage { + state := GetState(ctx) + + state.saveInDB.Lock() + defer state.saveInDB.Unlock() + + for _, v := range state.storage { if v.mustSave() { err := v.SaveInDB(ctx) if err != nil { @@ -105,7 +115,8 @@ func FromCopaing(c *Copaing) *CopaingCached { ID: c.ID, DiscordID: c.DiscordID, GuildID: c.GuildID, - XPs: calcXP(c), + XP: calcXP(c), + XPs: generateXPs(c), XPToAdd: 0, } } @@ -121,14 +132,24 @@ func KeyCopaingCachedRaw(guildID, copaingID string) state.Key { } type State struct { - mu sync.RWMutex - storage state.MapStorage[CopaingCached] + mu sync.RWMutex + saveInDB sync.Mutex + storage state.MapStorage[CopaingCached] } func NewState() *State { - return &State{ + state := &State{ storage: state.MapStorage[CopaingCached]{}, } + var cs []*Copaing + err := gokord.DB.Find(&cs).Error + if err != nil { + panic(err) + } + for _, v := range cs { + FromCopaing(v).Save(SetState(context.Background(), state)) + } + return state } const ContextKeyState = "state" @@ -173,3 +194,27 @@ func calcXP(c *Copaing) uint { } return sum } + +func generateXPs(c *Copaing) []XPCached { + data := map[time.Duration]XPCached{} + sixH := 6 * time.Hour + for _, xp := range c.CopaingXPs { + // we add sixH at the end because we want it to be rounded to ceil + since := time.Since(xp.CreatedAt)/sixH + sixH + if v, ok := data[since]; ok { + v.XP += xp.XP + } else { + data[since] = XPCached{ + Time: since, + XP: xp.XP, + } + } + } + ccs := make([]XPCached, len(data)) + i := 0 + for _, v := range data { + ccs[i] = v + i++ + } + return ccs +} @@ -2,13 +2,10 @@ package user import ( "context" - "log/slog" "slices" - "sync" + "time" - "git.anhgelus.world/anhgelus/les-copaings-bot/config" "git.anhgelus.world/anhgelus/les-copaings-bot/exp" - "github.com/anhgelus/gokord" "github.com/nyttikord/gokord/bot" "github.com/nyttikord/gokord/user" ) @@ -27,10 +24,10 @@ func (c *cXP) GetXP() uint { } func (cc *CopaingCached) AddXP(ctx context.Context, s bot.Session, m *user.Member, xp uint, fn func(uint, uint)) { - old := cc.XPs + old := cc.XP pastLevel := exp.Level(old) s.Logger().Debug("adding xp", "user", m.DisplayName(), "old", old, "to add", xp) - cc.XPs += xp + cc.XP += xp cc.XPToAdd += xp if err := cc.Save(ctx); err != nil { s.Logger().Error("saving user in state", "error", err, "user", m.DisplayName(), "xp", xp, "guild", cc.GuildID) @@ -43,88 +40,27 @@ func (cc *CopaingCached) AddXP(ctx context.Context, s bot.Session, m *user.Membe } } -func (c *Copaing) GetXP(logger *slog.Logger) (uint, error) { - cfg := config.GetGuildConfig(c.GuildID) - return c.GetXPForDays(logger, cfg.DaysXPRemains) -} - -func (c *Copaing) GetXPForDays(logger *slog.Logger, n uint) (uint, error) { +func (cc *CopaingCached) GetXPForDays(n uint) uint { xp := uint(0) - rows, err := gokord.DB. - Model(&CopaingXP{}). - Where( - "created_at >= ? and guild_id = ? and copaing_id = ?", - exp.TimeStampNDaysBefore(n), - c.GuildID, - c.ID, - ). - Rows() - if err != nil { - return 0, err - } - defer rows.Close() - for rows.Next() { - var cxp CopaingXP - err = gokord.DB.ScanRows(rows, &cxp) - if err != nil { - logger.Error("scanning rows", "error", err, "copaing", c.ID, "guild", c.GuildID) - continue + for _, v := range cc.XPs { + if v.Time <= time.Duration(n*24)*time.Hour { + xp += v.XP } - xp += cxp.XP } - return xp, nil + return xp + cc.XPToAdd } // GetBestXP returns n Copaing with the best XP within d days (d <= cfg.DaysXPRemain; d < 0 <=> d = cfg.DaysXPRemain) -// -// This function is slow -func GetBestXP(ctx context.Context, logger *slog.Logger, guildId string, n uint, d int) ([]CopaingCached, error) { - if d < 0 { - cfg := config.GetGuildConfig(guildId) - d = int(cfg.DaysXPRemains) - return getBestXPFull(ctx, guildId, n), nil - } - rows, err := gokord.DB.Model(&Copaing{}).Where("guild_id = ?", guildId).Rows() - if err != nil { - return nil, err - } - defer rows.Close() - var l []*cXP - var wg sync.WaitGroup - for rows.Next() { - var c Copaing - err = gokord.DB.ScanRows(rows, &c) - if err != nil { - logger.Error("scanning rows", "error", err, "copaing", c.ID, "guild", c.GuildID) - continue +func GetBestXP(ctx context.Context, guildId string, n uint, d int) ([]CopaingCached, error) { + ccs := GetState(ctx).Copaings(guildId) + if d > 0 { + for _, v := range ccs { + v.XP = v.GetXPForDays(n) } - wg.Go(func() { - xp, err := c.GetXPForDays(logger, uint(d)) - if err != nil { - logger.Error("fetching xp", "error", err, "copaing", c.ID, "guild", c.GuildID) - return - } - l = append(l, &cXP{Cxp: xp, copaing: &c}) - }) } - wg.Wait() - slices.SortFunc(l, func(a, b *cXP) int { - // desc order - return int(b.Cxp) - int(a.Cxp) - }) - m := min(len(l), int(n)) - cs := make([]CopaingCached, m) - for i, c := range l[:m] { - cs[i] = CopaingCached{DiscordID: c.copaing.DiscordID, XPs: c.Cxp} - } - return cs, nil -} - -func getBestXPFull(ctx context.Context, guildId string, n uint) []CopaingCached { - ccs := GetState(ctx).Copaings(guildId) slices.SortFunc(ccs, func(a, b CopaingCached) int { - return int(b.XPs) - int(a.XPs) + // desc order + return int(b.XP) - int(a.XP) }) - m := min(len(ccs), int(n)) - return ccs[:m] + return ccs[:min(len(ccs), int(n))], nil } |
