--- /dev/null
+// OpenTween - Client of Twitter
+// Copyright (c) 2019 kim_upsilon (@kim_upsilon) <https://upsilo.net/~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 <http://www.gnu.org/licenses/>, 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<Task> UpdateHome;
+ public Func<Task> UpdateMention;
+ public Func<Task> UpdateDm;
+ public Func<Task> UpdatePublicSearch;
+ public Func<Task> UpdateUser;
+ public Func<Task> UpdateList;
+ public Func<Task> 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<Task>(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;
+ }
+ }
+}
private string[] ColumnText = new string[9];
private bool _DoFavRetweetFlags = false;
- private bool osResumed = false;
//////////////////////////////////////////////////////////////////////////////////////////////////////////
- private System.Timers.Timer TimerTimeline = new System.Timers.Timer();
+ private readonly TimelineScheduler timelineScheduler = new TimelineScheduler();
private ThrottlingTimer RefreshThrottlingTimer;
private ThrottlingTimer selectionDebouncer;
private ThrottlingTimer saveConfigDebouncer;
//タイマー設定
+ this.timelineScheduler.UpdateHome = () => this.InvokeAsync(() => this.RefreshTabAsync<HomeTabModel>());
+ this.timelineScheduler.UpdateMention = () => this.InvokeAsync(() => this.RefreshTabAsync<MentionsTabModel>());
+ this.timelineScheduler.UpdateDm = () => this.InvokeAsync(() => this.RefreshTabAsync<DirectMessagesTabModel>());
+ this.timelineScheduler.UpdatePublicSearch = () => this.InvokeAsync(() => this.RefreshTabAsync<PublicSearchTabModel>());
+ this.timelineScheduler.UpdateUser = () => this.InvokeAsync(() => this.RefreshTabAsync<UserTimelineTabModel>());
+ this.timelineScheduler.UpdateList = () => this.InvokeAsync(() => this.RefreshTabAsync<ListTimelineTabModel>());
+ this.timelineScheduler.UpdateConfig = () => this.InvokeAsync(() => Task.WhenAll(new[]
+ {
+ this.doGetFollowersMenu(),
+ this.RefreshBlockIdsAsync(),
+ this.RefreshMuteUserIdsAsync(),
+ this.RefreshNoRetweetIdsAsync(),
+ this.RefreshTwitterConfigurationAsync(),
+ }));
+ this.RefreshTimelineScheduler();
+
var streamingRefreshInterval = TimeSpan.FromSeconds(SettingManager.Common.UserstreamPeriod);
this.RefreshThrottlingTimer = ThrottlingTimer.Throttle(() => this.InvokeAsync(() => this.RefreshTimeline()), streamingRefreshInterval);
this.selectionDebouncer = ThrottlingTimer.Debounce(() => this.InvokeAsync(() => this.UpdateSelectedPost()), TimeSpan.FromMilliseconds(100), leading: true);
this.saveConfigDebouncer = ThrottlingTimer.Debounce(() => this.InvokeAsync(() => this.SaveConfigsAll(ifModified: true)), TimeSpan.FromSeconds(1));
- TimerTimeline.AutoReset = true;
- TimerTimeline.SynchronizingObject = this;
- //Recent取得間隔
- TimerTimeline.Interval = 1000;
- TimerTimeline.Enabled = true;
//更新中アイコンアニメーション間隔
TimerRefreshIcon.Interval = 200;
TimerRefreshIcon.Enabled = false;
private void TimerInterval_Changed(object sender, IntervalChangedEventArgs e) //Handles SettingDialog.IntervalChanged
{
- if (!TimerTimeline.Enabled) return;
-
if (e.UserStream)
{
var interval = TimeSpan.FromSeconds(SettingManager.Common.UserstreamPeriod);
oldTimer.Dispose();
}
- ResetTimers = e;
+ this.RefreshTimelineScheduler();
}
- private IntervalChangedEventArgs ResetTimers = IntervalChangedEventArgs.ResetAll;
-
- private static int homeCounter = 0;
- private static int mentionCounter = 0;
- private static int dmCounter = 0;
- private static int pubSearchCounter = 0;
- private static int userTimelineCounter = 0;
- private static int listsCounter = 0;
- private static int ResumeWait = 0;
- private static int refreshFollowers = 0;
-
- private async void TimerTimeline_Elapsed(object sender, EventArgs e)
+ private void RefreshTimelineScheduler()
{
- if (homeCounter > 0) Interlocked.Decrement(ref homeCounter);
- if (mentionCounter > 0) Interlocked.Decrement(ref mentionCounter);
- if (dmCounter > 0) Interlocked.Decrement(ref dmCounter);
- if (pubSearchCounter > 0) Interlocked.Decrement(ref pubSearchCounter);
- if (userTimelineCounter > 0) Interlocked.Decrement(ref userTimelineCounter);
- if (listsCounter > 0) Interlocked.Decrement(ref listsCounter);
- Interlocked.Increment(ref refreshFollowers);
-
- var refreshTasks = new List<Task>();
-
- ////タイマー初期化
- if (ResetTimers.Timeline || homeCounter <= 0 && SettingManager.Common.TimelinePeriod > 0)
- {
- Interlocked.Exchange(ref homeCounter, SettingManager.Common.TimelinePeriod);
- if (!tw.IsUserstreamDataReceived && !ResetTimers.Timeline)
- refreshTasks.Add(this.RefreshTabAsync<HomeTabModel>());
- ResetTimers.Timeline = false;
- }
- if (ResetTimers.Reply || mentionCounter <= 0 && SettingManager.Common.ReplyPeriod > 0)
- {
- Interlocked.Exchange(ref mentionCounter, SettingManager.Common.ReplyPeriod);
- if (!tw.IsUserstreamDataReceived && !ResetTimers.Reply)
- refreshTasks.Add(this.RefreshTabAsync<MentionsTabModel>());
- ResetTimers.Reply = false;
- }
- if (ResetTimers.DirectMessage || dmCounter <= 0 && SettingManager.Common.DMPeriod > 0)
- {
- Interlocked.Exchange(ref dmCounter, SettingManager.Common.DMPeriod);
- if (!tw.IsUserstreamDataReceived && !ResetTimers.DirectMessage)
- refreshTasks.Add(this.RefreshTabAsync<DirectMessagesTabModel>());
- ResetTimers.DirectMessage = false;
- }
- if (ResetTimers.PublicSearch || pubSearchCounter <= 0 && SettingManager.Common.PubSearchPeriod > 0)
- {
- Interlocked.Exchange(ref pubSearchCounter, SettingManager.Common.PubSearchPeriod);
- if (!ResetTimers.PublicSearch)
- refreshTasks.Add(this.RefreshTabAsync<PublicSearchTabModel>());
- ResetTimers.PublicSearch = false;
- }
- if (ResetTimers.UserTimeline || userTimelineCounter <= 0 && SettingManager.Common.UserTimelinePeriod > 0)
- {
- Interlocked.Exchange(ref userTimelineCounter, SettingManager.Common.UserTimelinePeriod);
- if (!ResetTimers.UserTimeline)
- refreshTasks.Add(this.RefreshTabAsync<UserTimelineTabModel>());
- ResetTimers.UserTimeline = false;
- }
- if (ResetTimers.Lists || listsCounter <= 0 && SettingManager.Common.ListsPeriod > 0)
- {
- Interlocked.Exchange(ref listsCounter, SettingManager.Common.ListsPeriod);
- if (!ResetTimers.Lists)
- refreshTasks.Add(this.RefreshTabAsync<ListTimelineTabModel>());
- ResetTimers.Lists = false;
- }
- if (refreshFollowers > 6 * 3600)
- {
- Interlocked.Exchange(ref refreshFollowers, 0);
- refreshTasks.AddRange(new[]
- {
- this.doGetFollowersMenu(),
- this.RefreshNoRetweetIdsAsync(),
- this.RefreshTwitterConfigurationAsync(),
- });
- }
- if (osResumed)
- {
- Interlocked.Increment(ref ResumeWait);
- if (ResumeWait > 30)
- {
- osResumed = false;
- Interlocked.Exchange(ref ResumeWait, 0);
- refreshTasks.AddRange(new[]
- {
- this.RefreshTabAsync<HomeTabModel>(),
- this.RefreshTabAsync<MentionsTabModel>(),
- this.RefreshTabAsync<DirectMessagesTabModel>(),
- this.RefreshTabAsync<PublicSearchTabModel>(),
- this.RefreshTabAsync<UserTimelineTabModel>(),
- this.RefreshTabAsync<ListTimelineTabModel>(),
- this.doGetFollowersMenu(),
- this.RefreshTwitterConfigurationAsync(),
- });
- }
- }
+ this.timelineScheduler.UpdateIntervalHome = TimeSpan.FromSeconds(SettingManager.Common.TimelinePeriod);
+ this.timelineScheduler.UpdateIntervalMention = TimeSpan.FromSeconds(SettingManager.Common.ReplyPeriod);
+ this.timelineScheduler.UpdateIntervalDm = TimeSpan.FromSeconds(SettingManager.Common.DMPeriod);
+ this.timelineScheduler.UpdateIntervalPublicSearch = TimeSpan.FromSeconds(SettingManager.Common.PubSearchPeriod);
+ this.timelineScheduler.UpdateIntervalUser = TimeSpan.FromSeconds(SettingManager.Common.UserTimelinePeriod);
+ this.timelineScheduler.UpdateIntervalList = TimeSpan.FromSeconds(SettingManager.Common.ListsPeriod);
+ this.timelineScheduler.UpdateIntervalConfig = TimeSpan.FromHours(6);
+ this.timelineScheduler.UpdateAfterSystemResume = TimeSpan.FromSeconds(30);
- await Task.WhenAll(refreshTasks);
+ this.timelineScheduler.RefreshSchedule();
}
private void MarkSettingCommonModified()
_hookGlobalHotkey.UnregisterAllOriginalHotkey();
_ignoreConfigSave = true;
MyCommon._endingFlag = true;
- TimerTimeline.Enabled = false;
+ this.timelineScheduler.Enabled = false;
TimerRefreshIcon.Enabled = false;
}
}
_initial = false;
- TimerTimeline.Enabled = true;
+ this.timelineScheduler.Enabled = true;
}
private async Task doGetFollowersMenu()
this.tweetDetailsView.Owner = this;
- this.TimerTimeline.Elapsed += this.TimerTimeline_Elapsed;
this._hookGlobalHotkey.HotkeyPressed += _hookGlobalHotkey_HotkeyPressed;
this.gh.NotifyClicked += GrowlHelper_Callback;
private void SystemEvents_PowerModeChanged(object sender, Microsoft.Win32.PowerModeChangedEventArgs e)
{
- if (e.Mode == Microsoft.Win32.PowerModes.Resume) osResumed = true;
+ if (e.Mode == Microsoft.Win32.PowerModes.Resume)
+ this.timelineScheduler.SystemResumed();
}
private void SystemEvents_TimeChanged(object sender, EventArgs e)
{
tw.StopUserStream();
}
- TimerTimeline.Enabled = isEnable;
+ this.timelineScheduler.Enabled = isEnable;
}
private void StopRefreshAllMenuItem_CheckedChanged(object sender, EventArgs e)