// OpenTween - Client of Twitter // Copyright (c) 2019 kim_upsilon (@kim_upsilon) // All rights reserved. // // This file is part of OpenTween. // // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License // for more details. // // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, // Boston, MA 02110-1301, USA. using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace OpenTween { public class TimelineScheduler { private readonly Timer timer; private bool enabled = false; private bool systemResumeMode = false; private bool preventTimerUpdate = false; public bool Enabled { get => this.enabled; set { if (this.enabled == value) return; if (value) { var now = DateTimeUtc.Now; this.LastUpdateHome = now; this.LastUpdateMention = now; this.LastUpdateDm = now; this.LastUpdatePublicSearch = now; this.LastUpdateUser = now; this.LastUpdateList = now; this.LastUpdateConfig = now; } this.enabled = value; this.RefreshSchedule(); } } public DateTimeUtc LastUpdateHome { get; private set; } = DateTimeUtc.MinValue; public DateTimeUtc LastUpdateMention { get; private set; } = DateTimeUtc.MinValue; public DateTimeUtc LastUpdateDm { get; private set; } = DateTimeUtc.MinValue; public DateTimeUtc LastUpdatePublicSearch { get; private set; } = DateTimeUtc.MinValue; public DateTimeUtc LastUpdateUser { get; private set; } = DateTimeUtc.MinValue; public DateTimeUtc LastUpdateList { get; private set; } = DateTimeUtc.MinValue; public DateTimeUtc LastUpdateConfig { get; private set; } = DateTimeUtc.MinValue; public DateTimeUtc SystemResumedAt { get; private set; } = DateTimeUtc.MinValue; public TimeSpan UpdateIntervalHome { get; set; } = Timeout.InfiniteTimeSpan; public TimeSpan UpdateIntervalMention { get; set; } = Timeout.InfiniteTimeSpan; public TimeSpan UpdateIntervalDm { get; set; } = Timeout.InfiniteTimeSpan; public TimeSpan UpdateIntervalPublicSearch { get; set; } = Timeout.InfiniteTimeSpan; public TimeSpan UpdateIntervalUser { get; set; } = Timeout.InfiniteTimeSpan; public TimeSpan UpdateIntervalList { get; set; } = Timeout.InfiniteTimeSpan; public TimeSpan UpdateIntervalConfig { get; set; } = Timeout.InfiniteTimeSpan; public TimeSpan UpdateAfterSystemResume { get; set; } = Timeout.InfiniteTimeSpan; public Func UpdateHome; public Func UpdateMention; public Func UpdateDm; public Func UpdatePublicSearch; public Func UpdateUser; public Func UpdateList; public Func UpdateConfig; [Flags] private enum UpdateTask { None = 0, Home = 1, Mention = 1 << 2, Dm = 1 << 3, PublicSearch = 1 << 4, User = 1 << 5, List = 1 << 6, Config = 1 << 7, All = Home | Mention | Dm | PublicSearch | User | List | Config, } public TimelineScheduler() => this.timer = new Timer(_ => this.TimerCallback()); public void RefreshSchedule() { if (this.preventTimerUpdate) return; // TimerCallback 内で更新されるのでここは単に無視してよい if (this.Enabled) this.timer.Change(this.NextTimerDelay(), Timeout.InfiniteTimeSpan); else this.timer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); } public void SystemResumed() { this.SystemResumedAt = DateTimeUtc.Now; this.systemResumeMode = true; this.RefreshSchedule(); } private async void TimerCallback() { try { this.preventTimerUpdate = true; if (this.systemResumeMode) await this.TimerCallback_AfterSystemResume().ConfigureAwait(false); else await this.TimerCallback_Normal().ConfigureAwait(false); } finally { this.preventTimerUpdate = false; this.RefreshSchedule(); } } private async Task TimerCallback_Normal() { var now = DateTimeUtc.Now; var round = TimeSpan.FromSeconds(1); // 1秒未満の差異であればまとめて実行する var tasks = UpdateTask.None; var nextScheduledHome = this.LastUpdateHome + this.UpdateIntervalHome; if (nextScheduledHome - now < round) tasks |= UpdateTask.Home; var nextScheduledMention = this.LastUpdateMention + this.UpdateIntervalMention; if (nextScheduledMention - now < round) tasks |= UpdateTask.Mention; var nextScheduledDm = this.LastUpdateDm + this.UpdateIntervalDm; if (nextScheduledDm - now < round) tasks |= UpdateTask.Dm; var nextScheduledPublicSearch = this.LastUpdatePublicSearch + this.UpdateIntervalPublicSearch; if (nextScheduledPublicSearch - now < round) tasks |= UpdateTask.PublicSearch; var nextScheduledUser = this.LastUpdateUser + this.UpdateIntervalUser; if (nextScheduledUser - now < round) tasks |= UpdateTask.User; var nextScheduledList = this.LastUpdateList + this.UpdateIntervalList; if (nextScheduledList - now < round) tasks |= UpdateTask.List; var nextScheduledConfig = this.LastUpdateConfig + this.UpdateIntervalConfig; if (nextScheduledConfig - now < round) tasks |= UpdateTask.Config; await this.RunUpdateTasks(tasks, now).ConfigureAwait(false); } private async Task TimerCallback_AfterSystemResume() { // systemResumeMode では一定期間経過後に全てのタイムラインを更新する var now = DateTimeUtc.Now; var nextScheduledUpdateAll = this.SystemResumedAt + this.UpdateAfterSystemResume; if (nextScheduledUpdateAll - now < TimeSpan.Zero) { this.systemResumeMode = false; await this.RunUpdateTasks(UpdateTask.All, now).ConfigureAwait(false); } } private async Task RunUpdateTasks(UpdateTask tasks, DateTimeUtc now) { var updateTasks = new List(capacity: 7); // LastUpdate* を次の時刻に更新してから Update* を実行すること // (LastUpdate* が更新されずに Update* が例外を投げると無限ループに陥る) if ((tasks & UpdateTask.Home) == UpdateTask.Home) { this.LastUpdateHome = now; if (this.UpdateHome != null) updateTasks.Add(this.UpdateHome()); } if ((tasks & UpdateTask.Mention) == UpdateTask.Mention) { this.LastUpdateMention = now; if (this.UpdateMention != null) updateTasks.Add(this.UpdateMention()); } if ((tasks & UpdateTask.Dm) == UpdateTask.Dm) { this.LastUpdateDm = now; if (this.UpdateDm != null) updateTasks.Add(this.UpdateDm()); } if ((tasks & UpdateTask.PublicSearch) == UpdateTask.PublicSearch) { this.LastUpdatePublicSearch = now; if (this.UpdatePublicSearch != null) updateTasks.Add(this.UpdatePublicSearch()); } if ((tasks & UpdateTask.User) == UpdateTask.User) { this.LastUpdateUser = now; if (this.UpdateUser != null) updateTasks.Add(this.UpdateUser()); } if ((tasks & UpdateTask.List) == UpdateTask.List) { this.LastUpdateList = now; if (this.UpdateList != null) updateTasks.Add(this.UpdateList()); } if ((tasks & UpdateTask.Config) == UpdateTask.Config) { this.LastUpdateConfig = now; if (this.UpdateConfig != null) updateTasks.Add(this.UpdateConfig()); } await Task.WhenAll(updateTasks).ConfigureAwait(false); } private TimeSpan NextTimerDelay() { TimeSpan delay; if (this.systemResumeMode) { // systemResumeMode が有効な間は UpdateAfterSystemResume 以外の設定値を見ない var nextScheduledUpdateAll = this.SystemResumedAt + this.UpdateAfterSystemResume; delay = nextScheduledUpdateAll - DateTimeUtc.Now; return delay > TimeSpan.Zero ? delay : TimeSpan.Zero; } // 次に更新が予定される時刻を判定する var min = DateTimeUtc.MaxValue; var nextScheduledHome = this.LastUpdateHome + this.UpdateIntervalHome; if (nextScheduledHome < min) min = nextScheduledHome; var nextScheduledMention = this.LastUpdateMention + this.UpdateIntervalMention; if (nextScheduledMention < min) min = nextScheduledMention; var nextScheduledDm = this.LastUpdateDm + this.UpdateIntervalDm; if (nextScheduledDm < min) min = nextScheduledDm; var nextScheduledPublicSearch = this.LastUpdatePublicSearch + this.UpdateIntervalPublicSearch; if (nextScheduledPublicSearch < min) min = nextScheduledPublicSearch; var nextScheduledUser = this.LastUpdateUser + this.UpdateIntervalUser; if (nextScheduledUser < min) min = nextScheduledUser; var nextScheduledList = this.LastUpdateList + this.UpdateIntervalList; if (nextScheduledList < min) min = nextScheduledList; var nextScheduledConfig = this.LastUpdateConfig + this.UpdateIntervalConfig; if (nextScheduledConfig < min) min = nextScheduledConfig; delay = min - DateTimeUtc.Now; return delay > TimeSpan.Zero ? delay : TimeSpan.Zero; } } }