OSDN Git Service

タイムラインの定期更新に使用するタイマーの間隔を動的に制御する
authorKimura Youichi <kim.upsilon@bucyou.net>
Tue, 16 Jul 2019 06:55:35 +0000 (15:55 +0900)
committerKimura Youichi <kim.upsilon@bucyou.net>
Thu, 15 Aug 2019 20:29:30 +0000 (05:29 +0900)
変更前は常に1秒おきに更新が必要なタブをチェックしていたのに対して、
変更後は次に更新が必要になる時刻まで待機するようにタイマーの実行間隔を動的に設定する

OpenTween/OpenTween.csproj
OpenTween/TimelineScheduler.cs [new file with mode: 0644]
OpenTween/Tween.cs

index 6b25a31..42fd0cf 100644 (file)
     <Compile Include="ShortcutCommand.cs" />
     <Compile Include="ThrottlingTimer.cs" />
     <Compile Include="Thumbnail\Services\PbsTwimgCom.cs" />
+    <Compile Include="TimelineScheduler.cs" />
     <Compile Include="TweetDetailsView.cs">
       <SubType>UserControl</SubType>
     </Compile>
diff --git a/OpenTween/TimelineScheduler.cs b/OpenTween/TimelineScheduler.cs
new file mode 100644 (file)
index 0000000..421dae1
--- /dev/null
@@ -0,0 +1,298 @@
+// 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;
+        }
+    }
+}
index 3ab2f3b..7b96145 100644 (file)
@@ -264,11 +264,10 @@ namespace OpenTween
         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;
@@ -1114,16 +1113,27 @@ namespace OpenTween
 
             //タイマー設定
 
+            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;
@@ -1279,8 +1289,6 @@ namespace OpenTween
 
         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);
@@ -1289,107 +1297,21 @@ namespace OpenTween
                 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()
@@ -2280,7 +2202,7 @@ namespace OpenTween
                 _hookGlobalHotkey.UnregisterAllOriginalHotkey();
                 _ignoreConfigSave = true;
                 MyCommon._endingFlag = true;
-                TimerTimeline.Enabled = false;
+                this.timelineScheduler.Enabled = false;
                 TimerRefreshIcon.Enabled = false;
             }
         }
@@ -9898,7 +9820,7 @@ namespace OpenTween
 
             _initial = false;
 
-            TimerTimeline.Enabled = true;
+            this.timelineScheduler.Enabled = true;
         }
 
         private async Task doGetFollowersMenu()
@@ -11032,7 +10954,6 @@ namespace OpenTween
 
             this.tweetDetailsView.Owner = this;
 
-            this.TimerTimeline.Elapsed += this.TimerTimeline_Elapsed;
             this._hookGlobalHotkey.HotkeyPressed += _hookGlobalHotkey_HotkeyPressed;
             this.gh.NotifyClicked += GrowlHelper_Callback;
 
@@ -11684,7 +11605,8 @@ namespace OpenTween
 
         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)
@@ -11715,7 +11637,7 @@ namespace OpenTween
             {
                 tw.StopUserStream();
             }
-            TimerTimeline.Enabled = isEnable;
+            this.timelineScheduler.Enabled = isEnable;
         }
 
         private void StopRefreshAllMenuItem_CheckedChanged(object sender, EventArgs e)