OSDN Git Service

Merge branch 'reduce-timer-events'
[opentween/open-tween.git] / OpenTween / TimelineScheduler.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2019 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
4 //
5 // This file is part of OpenTween.
6 //
7 // This program is free software; you can redistribute it and/or modify it
8 // under the terms of the GNU General Public License as published by the Free
9 // Software Foundation; either version 3 of the License, or (at your option)
10 // any later version.
11 //
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15 // for more details.
16 //
17 // You should have received a copy of the GNU General Public License along
18 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
19 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
20 // Boston, MA 02110-1301, USA.
21
22 using System;
23 using System.Collections.Generic;
24 using System.Threading;
25 using System.Threading.Tasks;
26
27 namespace OpenTween
28 {
29     public class TimelineScheduler
30     {
31         private readonly Timer timer;
32
33         private bool enabled = false;
34         private bool systemResumeMode = false;
35         private bool preventTimerUpdate = false;
36
37         public bool Enabled
38         {
39             get => this.enabled;
40             set
41             {
42                 if (this.enabled == value)
43                     return;
44
45                 if (value)
46                 {
47                     var now = DateTimeUtc.Now;
48                     this.LastUpdateHome = now;
49                     this.LastUpdateMention = now;
50                     this.LastUpdateDm = now;
51                     this.LastUpdatePublicSearch = now;
52                     this.LastUpdateUser = now;
53                     this.LastUpdateList = now;
54                     this.LastUpdateConfig = now;
55                 }
56                 this.enabled = value;
57                 this.RefreshSchedule();
58             }
59         }
60
61         public DateTimeUtc LastUpdateHome { get; private set; } = DateTimeUtc.MinValue;
62         public DateTimeUtc LastUpdateMention { get; private set; } = DateTimeUtc.MinValue;
63         public DateTimeUtc LastUpdateDm { get; private set; } = DateTimeUtc.MinValue;
64         public DateTimeUtc LastUpdatePublicSearch { get; private set; } = DateTimeUtc.MinValue;
65         public DateTimeUtc LastUpdateUser { get; private set; } = DateTimeUtc.MinValue;
66         public DateTimeUtc LastUpdateList { get; private set; } = DateTimeUtc.MinValue;
67         public DateTimeUtc LastUpdateConfig { get; private set; } = DateTimeUtc.MinValue;
68         public DateTimeUtc SystemResumedAt { get; private set; } = DateTimeUtc.MinValue;
69
70         public TimeSpan UpdateIntervalHome { get; set; } = Timeout.InfiniteTimeSpan;
71         public TimeSpan UpdateIntervalMention { get; set; } = Timeout.InfiniteTimeSpan;
72         public TimeSpan UpdateIntervalDm { get; set; } = Timeout.InfiniteTimeSpan;
73         public TimeSpan UpdateIntervalPublicSearch { get; set; } = Timeout.InfiniteTimeSpan;
74         public TimeSpan UpdateIntervalUser { get; set; } = Timeout.InfiniteTimeSpan;
75         public TimeSpan UpdateIntervalList { get; set; } = Timeout.InfiniteTimeSpan;
76         public TimeSpan UpdateIntervalConfig { get; set; } = Timeout.InfiniteTimeSpan;
77         public TimeSpan UpdateAfterSystemResume { get; set; } = Timeout.InfiniteTimeSpan;
78
79         public Func<Task> UpdateHome;
80         public Func<Task> UpdateMention;
81         public Func<Task> UpdateDm;
82         public Func<Task> UpdatePublicSearch;
83         public Func<Task> UpdateUser;
84         public Func<Task> UpdateList;
85         public Func<Task> UpdateConfig;
86
87         [Flags]
88         private enum UpdateTask
89         {
90             None = 0,
91             Home = 1,
92             Mention = 1 << 2,
93             Dm = 1 << 3,
94             PublicSearch = 1 << 4,
95             User = 1 << 5,
96             List = 1 << 6,
97             Config = 1 << 7,
98             All = Home | Mention | Dm | PublicSearch | User | List | Config,
99         }
100
101         public TimelineScheduler()
102             => this.timer = new Timer(_ => this.TimerCallback());
103
104         public void RefreshSchedule()
105         {
106             if (this.preventTimerUpdate)
107                 return; // TimerCallback 内で更新されるのでここは単に無視してよい
108
109             if (this.Enabled)
110                 this.timer.Change(this.NextTimerDelay(), Timeout.InfiniteTimeSpan);
111             else
112                 this.timer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
113         }
114
115         public void SystemResumed()
116         {
117             this.SystemResumedAt = DateTimeUtc.Now;
118             this.systemResumeMode = true;
119             this.RefreshSchedule();
120         }
121
122         private async void TimerCallback()
123         {
124             try
125             {
126                 this.preventTimerUpdate = true;
127
128                 if (this.systemResumeMode)
129                     await this.TimerCallback_AfterSystemResume().ConfigureAwait(false);
130                 else
131                     await this.TimerCallback_Normal().ConfigureAwait(false);
132             }
133             finally
134             {
135                 this.preventTimerUpdate = false;
136                 this.RefreshSchedule();
137             }
138         }
139
140         private async Task TimerCallback_Normal()
141         {
142             var now = DateTimeUtc.Now;
143             var round = TimeSpan.FromSeconds(1); // 1秒未満の差異であればまとめて実行する
144             var tasks = UpdateTask.None;
145
146             var nextScheduledHome = this.LastUpdateHome + this.UpdateIntervalHome;
147             if (nextScheduledHome - now < round)
148                 tasks |= UpdateTask.Home;
149
150             var nextScheduledMention = this.LastUpdateMention + this.UpdateIntervalMention;
151             if (nextScheduledMention - now < round)
152                 tasks |= UpdateTask.Mention;
153
154             var nextScheduledDm = this.LastUpdateDm + this.UpdateIntervalDm;
155             if (nextScheduledDm - now < round)
156                 tasks |= UpdateTask.Dm;
157
158             var nextScheduledPublicSearch = this.LastUpdatePublicSearch + this.UpdateIntervalPublicSearch;
159             if (nextScheduledPublicSearch - now < round)
160                 tasks |= UpdateTask.PublicSearch;
161
162             var nextScheduledUser = this.LastUpdateUser + this.UpdateIntervalUser;
163             if (nextScheduledUser - now < round)
164                 tasks |= UpdateTask.User;
165
166             var nextScheduledList = this.LastUpdateList + this.UpdateIntervalList;
167             if (nextScheduledList - now < round)
168                 tasks |= UpdateTask.List;
169
170             var nextScheduledConfig = this.LastUpdateConfig + this.UpdateIntervalConfig;
171             if (nextScheduledConfig - now < round)
172                 tasks |= UpdateTask.Config;
173
174             await this.RunUpdateTasks(tasks, now).ConfigureAwait(false);
175         }
176
177         private async Task TimerCallback_AfterSystemResume()
178         {
179             // systemResumeMode では一定期間経過後に全てのタイムラインを更新する
180             var now = DateTimeUtc.Now;
181
182             var nextScheduledUpdateAll = this.SystemResumedAt + this.UpdateAfterSystemResume;
183             if (nextScheduledUpdateAll - now < TimeSpan.Zero)
184             {
185                 this.systemResumeMode = false;
186                 await this.RunUpdateTasks(UpdateTask.All, now).ConfigureAwait(false);
187             }
188         }
189
190         private async Task RunUpdateTasks(UpdateTask tasks, DateTimeUtc now)
191         {
192             var updateTasks = new List<Task>(capacity: 7);
193
194             // LastUpdate* を次の時刻に更新してから Update* を実行すること
195             // (LastUpdate* が更新されずに Update* が例外を投げると無限ループに陥る)
196
197             if ((tasks & UpdateTask.Home) == UpdateTask.Home)
198             {
199                 this.LastUpdateHome = now;
200                 if (this.UpdateHome != null)
201                     updateTasks.Add(this.UpdateHome());
202             }
203
204             if ((tasks & UpdateTask.Mention) == UpdateTask.Mention)
205             {
206                 this.LastUpdateMention = now;
207                 if (this.UpdateMention != null)
208                     updateTasks.Add(this.UpdateMention());
209             }
210
211             if ((tasks & UpdateTask.Dm) == UpdateTask.Dm)
212             {
213                 this.LastUpdateDm = now;
214                 if (this.UpdateDm != null)
215                     updateTasks.Add(this.UpdateDm());
216             }
217
218             if ((tasks & UpdateTask.PublicSearch) == UpdateTask.PublicSearch)
219             {
220                 this.LastUpdatePublicSearch = now;
221                 if (this.UpdatePublicSearch != null)
222                     updateTasks.Add(this.UpdatePublicSearch());
223             }
224
225             if ((tasks & UpdateTask.User) == UpdateTask.User)
226             {
227                 this.LastUpdateUser = now;
228                 if (this.UpdateUser != null)
229                     updateTasks.Add(this.UpdateUser());
230             }
231
232             if ((tasks & UpdateTask.List) == UpdateTask.List)
233             {
234                 this.LastUpdateList = now;
235                 if (this.UpdateList != null)
236                     updateTasks.Add(this.UpdateList());
237             }
238
239             if ((tasks & UpdateTask.Config) == UpdateTask.Config)
240             {
241                 this.LastUpdateConfig = now;
242                 if (this.UpdateConfig != null)
243                     updateTasks.Add(this.UpdateConfig());
244             }
245
246             await Task.WhenAll(updateTasks).ConfigureAwait(false);
247         }
248
249         private TimeSpan NextTimerDelay()
250         {
251             TimeSpan delay;
252
253             if (this.systemResumeMode)
254             {
255                 // systemResumeMode が有効な間は UpdateAfterSystemResume 以外の設定値を見ない
256                 var nextScheduledUpdateAll = this.SystemResumedAt + this.UpdateAfterSystemResume;
257                 delay = nextScheduledUpdateAll - DateTimeUtc.Now;
258
259                 return delay > TimeSpan.Zero ? delay : TimeSpan.Zero;
260             }
261
262             // 次に更新が予定される時刻を判定する
263             var min = DateTimeUtc.MaxValue;
264
265             var nextScheduledHome = this.LastUpdateHome + this.UpdateIntervalHome;
266             if (nextScheduledHome < min)
267                 min = nextScheduledHome;
268
269             var nextScheduledMention = this.LastUpdateMention + this.UpdateIntervalMention;
270             if (nextScheduledMention < min)
271                 min = nextScheduledMention;
272
273             var nextScheduledDm = this.LastUpdateDm + this.UpdateIntervalDm;
274             if (nextScheduledDm < min)
275                 min = nextScheduledDm;
276
277             var nextScheduledPublicSearch = this.LastUpdatePublicSearch + this.UpdateIntervalPublicSearch;
278             if (nextScheduledPublicSearch < min)
279                 min = nextScheduledPublicSearch;
280
281             var nextScheduledUser = this.LastUpdateUser + this.UpdateIntervalUser;
282             if (nextScheduledUser < min)
283                 min = nextScheduledUser;
284
285             var nextScheduledList = this.LastUpdateList + this.UpdateIntervalList;
286             if (nextScheduledList < min)
287                 min = nextScheduledList;
288
289             var nextScheduledConfig = this.LastUpdateConfig + this.UpdateIntervalConfig;
290             if (nextScheduledConfig < min)
291                 min = nextScheduledConfig;
292
293             delay = min - DateTimeUtc.Now;
294
295             return delay > TimeSpan.Zero ? delay : TimeSpan.Zero;
296         }
297     }
298 }