// 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. #nullable enable using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace OpenTween { public class TimelineScheduler : IDisposable { private static readonly TimelineSchedulerTaskType[] AllTaskTypes = { TimelineSchedulerTaskType.Home, TimelineSchedulerTaskType.Mention, TimelineSchedulerTaskType.Dm, TimelineSchedulerTaskType.PublicSearch, TimelineSchedulerTaskType.User, TimelineSchedulerTaskType.List, TimelineSchedulerTaskType.Config, }; private readonly ITimer timer; private bool enabled = false; private bool systemResumeMode = false; private bool preventTimerUpdate = false; public bool IsDisposed { get; private set; } = false; public bool Enabled { get => this.enabled; set { if (this.enabled == value) return; this.enabled = value; this.Reset(); } } public DateTimeUtc SystemResumedAt { get; private set; } = DateTimeUtc.MinValue; public TimeSpan UpdateAfterSystemResume { get; set; } = Timeout.InfiniteTimeSpan; public bool EnableUpdateSystemResume => this.UpdateAfterSystemResume != Timeout.InfiniteTimeSpan; public Dictionary LastUpdatedAt { get; } = new Dictionary(); public Dictionary UpdateInterval { get; } = new Dictionary(); public Dictionary> UpdateFunc { get; } = new Dictionary>(); public IEnumerable EnabledTaskTypes => TimelineScheduler.AllTaskTypes.Where(x => this.IsEnabledType(x)); public TimelineScheduler() { this.timer = this.CreateTimer(this.TimerCallback); foreach (var taskType in TimelineScheduler.AllTaskTypes) { this.LastUpdatedAt[taskType] = DateTimeUtc.MinValue; this.UpdateInterval[taskType] = Timeout.InfiniteTimeSpan; } } protected virtual ITimer CreateTimer(Func callback) => new AsyncTimer(callback); public bool IsEnabledType(TimelineSchedulerTaskType task) => this.UpdateInterval[task] != Timeout.InfiniteTimeSpan; 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() { if (!this.EnableUpdateSystemResume) return; this.SystemResumedAt = DateTimeUtc.Now; this.systemResumeMode = true; this.RefreshSchedule(); } public void Reset() { foreach (var taskType in TimelineScheduler.AllTaskTypes) this.LastUpdatedAt[taskType] = DateTimeUtc.MinValue; this.systemResumeMode = false; this.RefreshSchedule(); } private async Task 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 = new List(capacity: TimelineScheduler.AllTaskTypes.Length); foreach (var taskType in this.EnabledTaskTypes) { var nextScheduledAt = this.LastUpdatedAt[taskType] + this.UpdateInterval[taskType]; if (nextScheduledAt - now < round) tasks.Add(taskType); } await this.RunUpdateTasks(tasks, now).ConfigureAwait(false); } private async Task TimerCallback_AfterSystemResume() { // systemResumeMode では一定期間経過後に全てのタイムラインを更新する var now = DateTimeUtc.Now; this.systemResumeMode = false; await this.RunUpdateTasks(TimelineScheduler.AllTaskTypes, now).ConfigureAwait(false); } private async Task RunUpdateTasks(IEnumerable taskTypes, DateTimeUtc now) { var updateTasks = new List>(capacity: TimelineScheduler.AllTaskTypes.Length); foreach (var taskType in taskTypes) { this.LastUpdatedAt[taskType] = now; if (this.UpdateFunc.TryGetValue(taskType, out var func)) updateTasks.Add(func); } await Task.WhenAll(updateTasks.Select(x => Task.Run(x))) .ConfigureAwait(false); } private TimeSpan NextTimerDelay() { TimeSpan delay; if (this.systemResumeMode) { // systemResumeMode が有効な間は UpdateAfterSystemResume 以外の設定値を見ない var nextScheduledUpdateAll = this.SystemResumedAt + this.UpdateAfterSystemResume; delay = nextScheduledUpdateAll - DateTimeUtc.Now; } else { // 次に更新が予定される時刻を判定する var min = DateTimeUtc.MaxValue; foreach (var taskType in this.EnabledTaskTypes) { var nextScheduledAt = this.LastUpdatedAt[taskType] + this.UpdateInterval[taskType]; if (nextScheduledAt < min) min = nextScheduledAt; } if (min == DateTimeUtc.MaxValue) return Timeout.InfiniteTimeSpan; delay = min - DateTimeUtc.Now; } return delay > TimeSpan.Zero ? delay : TimeSpan.Zero; } protected virtual void Dispose(bool disposing) { if (this.IsDisposed) return; if (disposing) this.timer.Dispose(); this.IsDisposed = true; } public void Dispose() { this.Dispose(disposing: true); GC.SuppressFinalize(this); } } public enum TimelineSchedulerTaskType { Home, Mention, Dm, PublicSearch, User, List, Config, } }