From 4d77a498f6957b7ff8cc0ab3abcb29a5c0d6e27b Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Tue, 4 Jul 2017 22:00:03 +0200
Subject: [PATCH] Add a WIP memory tracker that runs GC reload, and fix config

---
 Configuration/UserConfig.cs                   |  5 +-
 Core/FormBrowser.cs                           | 17 ++++
 .../Settings/TabSettingsAdvanced.Designer.cs  | 68 +++++++-------
 Core/Utils/MemoryUsageTracker.cs              | 91 +++++++++++++++++++
 Resources/Scripts/code.js                     | 34 +++++++
 TweetDuck.csproj                              |  1 +
 6 files changed, 180 insertions(+), 36 deletions(-)
 create mode 100644 Core/Utils/MemoryUsageTracker.cs

diff --git a/Configuration/UserConfig.cs b/Configuration/UserConfig.cs
index f2446c23..4e108ac9 100644
--- a/Configuration/UserConfig.cs
+++ b/Configuration/UserConfig.cs
@@ -19,7 +19,7 @@ public override Type BindToType(string assemblyName, string typeName){
             }
         }
 
-        private const int CurrentFileVersion = 11;
+        private const int CurrentFileVersion = 12;
 
         // START OF CONFIGURATION
 
@@ -202,6 +202,9 @@ private void UpgradeFile(){
 
             if (fileVersion == 10){
                 NotificationSize = TweetNotification.Size.Auto;
+            }
+
+            if (fileVersion == 11){
                 BrowserMemoryThreshold = 350;
                 ++fileVersion;
             }
diff --git a/Core/FormBrowser.cs b/Core/FormBrowser.cs
index baf2ba72..f89e4353 100644
--- a/Core/FormBrowser.cs
+++ b/Core/FormBrowser.cs
@@ -32,6 +32,7 @@ sealed partial class FormBrowser : Form{
         private readonly UpdateHandler updates;
         private readonly FormNotificationTweet notification;
         private readonly ContextMenu contextMenu;
+        private readonly MemoryUsageTracker memoryUsageTracker;
 
         private bool isLoaded;
         private bool isBrowserReady;
@@ -50,6 +51,7 @@ public FormBrowser(PluginManager pluginManager, UpdaterSettings updaterSettings)
             this.plugins.PluginChangedState += plugins_PluginChangedState;
 
             this.contextMenu = ContextMenuBrowser.CreateMenu(this);
+            this.memoryUsageTracker = new MemoryUsageTracker("TDGF_tryRunCleanup");
 
             this.notification = new FormNotificationTweet(this, plugins){
                 #if DEBUG
@@ -87,6 +89,8 @@ public FormBrowser(PluginManager pluginManager, UpdaterSettings updaterSettings)
             Controls.Add(new MenuStrip{ Visible = false }); // fixes Alt freezing the program in Win 10 Anniversary Update
 
             Disposed += (sender, args) => {
+                memoryUsageTracker.Dispose();
+
                 browser.Dispose();
                 contextMenu.Dispose();
 
@@ -165,6 +169,8 @@ private void browser_LoadingStateChanged(object sender, LoadingStateChangedEvent
 
         private void browser_FrameLoadStart(object sender, FrameLoadStartEventArgs e){
             if (e.Frame.IsMain){
+                memoryUsageTracker.Stop();
+
                 if (Config.ZoomLevel != 100){
                     BrowserUtils.SetZoomLevel(browser.GetBrowser(), Config.ZoomLevel);
                 }
@@ -190,6 +196,10 @@ private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
                 }
 
                 TweetDeckBridge.ResetStaticProperties();
+
+                if (Config.EnableBrowserGCReload){
+                    memoryUsageTracker.Start(this, e.Browser, Config.BrowserMemoryThreshold);
+                }
             }
         }
 
@@ -424,6 +434,13 @@ public void OpenSettings(Type startTab){
                     if (!Config.EnableTrayHighlight){
                         trayIcon.HasNotifications = false;
                     }
+
+                    if (Config.EnableBrowserGCReload){
+                        memoryUsageTracker.Start(this, browser.GetBrowser(), Config.BrowserMemoryThreshold);
+                    }
+                    else{
+                        memoryUsageTracker.Stop();
+                    }
                     
                     UpdateProperties(PropertyBridge.Properties.ExpandLinksOnHover | PropertyBridge.Properties.SwitchAccountSelectors | PropertyBridge.Properties.HasCustomNotificationSound);
 
diff --git a/Core/Other/Settings/TabSettingsAdvanced.Designer.cs b/Core/Other/Settings/TabSettingsAdvanced.Designer.cs
index 287d859d..4304bcd9 100644
--- a/Core/Other/Settings/TabSettingsAdvanced.Designer.cs
+++ b/Core/Other/Settings/TabSettingsAdvanced.Designer.cs
@@ -24,7 +24,6 @@ protected override void Dispose(bool disposing) {
         /// </summary>
         private void InitializeComponent() {
             this.components = new System.ComponentModel.Container();
-            System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(TabSettingsAdvanced));
             this.btnClearCache = new System.Windows.Forms.Button();
             this.checkHardwareAcceleration = new System.Windows.Forms.CheckBox();
             this.toolTip = new System.Windows.Forms.ToolTip(this.components);
@@ -35,17 +34,17 @@ private void InitializeComponent() {
             this.btnOpenAppFolder = new System.Windows.Forms.Button();
             this.btnOpenDataFolder = new System.Windows.Forms.Button();
             this.checkBrowserGCReload = new System.Windows.Forms.CheckBox();
+            this.numMemoryThreshold = new TweetDuck.Core.Controls.NumericUpDownEx();
             this.labelApp = new System.Windows.Forms.Label();
             this.panelApp = new System.Windows.Forms.Panel();
             this.labelPerformance = new System.Windows.Forms.Label();
             this.panelPerformance = new System.Windows.Forms.Panel();
-            this.numMemoryThreshold = new TweetDuck.Core.Controls.NumericUpDownEx();
             this.labelMemoryUsage = new System.Windows.Forms.Label();
             this.panelConfiguration = new System.Windows.Forms.Panel();
             this.labelConfiguration = new System.Windows.Forms.Label();
+            ((System.ComponentModel.ISupportInitialize)(this.numMemoryThreshold)).BeginInit();
             this.panelApp.SuspendLayout();
             this.panelPerformance.SuspendLayout();
-            ((System.ComponentModel.ISupportInitialize)(this.numMemoryThreshold)).BeginInit();
             this.panelConfiguration.SuspendLayout();
             this.SuspendLayout();
             // 
@@ -147,9 +146,38 @@ private void InitializeComponent() {
             this.checkBrowserGCReload.Size = new System.Drawing.Size(190, 17);
             this.checkBrowserGCReload.TabIndex = 4;
             this.checkBrowserGCReload.Text = "Enable Browser Memory Threshold";
-            this.toolTip.SetToolTip(this.checkBrowserGCReload, resources.GetString("checkBrowserGCReload.ToolTip"));
             this.checkBrowserGCReload.UseVisualStyleBackColor = true;
             // 
+            // numMemoryThreshold
+            // 
+            this.numMemoryThreshold.Increment = new decimal(new int[] {
+            50,
+            0,
+            0,
+            0});
+            this.numMemoryThreshold.Location = new System.Drawing.Point(202, 82);
+            this.numMemoryThreshold.Maximum = new decimal(new int[] {
+            3000,
+            0,
+            0,
+            0});
+            this.numMemoryThreshold.Minimum = new decimal(new int[] {
+            200,
+            0,
+            0,
+            0});
+            this.numMemoryThreshold.Name = "numMemoryThreshold";
+            this.numMemoryThreshold.Size = new System.Drawing.Size(97, 20);
+            this.numMemoryThreshold.TabIndex = 3;
+            this.numMemoryThreshold.TextSuffix = " MB";
+            this.toolTip.SetToolTip(this.numMemoryThreshold, "Minimum amount of memory usage by the browser process to trigger the cleanup.\r\nTh" +
+        "is is not a limit, the usage is allowed to exceed this value.");
+            this.numMemoryThreshold.Value = new decimal(new int[] {
+            350,
+            0,
+            0,
+            0});
+            // 
             // labelApp
             // 
             this.labelApp.AutoSize = true;
@@ -199,36 +227,6 @@ private void InitializeComponent() {
             this.panelPerformance.Size = new System.Drawing.Size(322, 105);
             this.panelPerformance.TabIndex = 3;
             // 
-            // numMemoryThreshold
-            // 
-            this.numMemoryThreshold.Increment = new decimal(new int[] {
-            50,
-            0,
-            0,
-            0});
-            this.numMemoryThreshold.Location = new System.Drawing.Point(202, 82);
-            this.numMemoryThreshold.Maximum = new decimal(new int[] {
-            3000,
-            0,
-            0,
-            0});
-            this.numMemoryThreshold.Minimum = new decimal(new int[] {
-            200,
-            0,
-            0,
-            0});
-            this.numMemoryThreshold.Name = "numMemoryThreshold";
-            this.numMemoryThreshold.Size = new System.Drawing.Size(97, 20);
-            this.numMemoryThreshold.TabIndex = 3;
-            this.numMemoryThreshold.TextSuffix = " MB";
-            this.toolTip.SetToolTip(this.numMemoryThreshold, "Minimum amount of memory usage by the browser process to trigger the cleanup.\r\nTh" +
-        "is is not a limit, the usage is allowed to exceed this value.");
-            this.numMemoryThreshold.Value = new decimal(new int[] {
-            350,
-            0,
-            0,
-            0});
-            // 
             // labelMemoryUsage
             // 
             this.labelMemoryUsage.AutoSize = true;
@@ -273,10 +271,10 @@ private void InitializeComponent() {
             this.Controls.Add(this.labelApp);
             this.Name = "TabSettingsAdvanced";
             this.Size = new System.Drawing.Size(340, 328);
+            ((System.ComponentModel.ISupportInitialize)(this.numMemoryThreshold)).EndInit();
             this.panelApp.ResumeLayout(false);
             this.panelPerformance.ResumeLayout(false);
             this.panelPerformance.PerformLayout();
-            ((System.ComponentModel.ISupportInitialize)(this.numMemoryThreshold)).EndInit();
             this.panelConfiguration.ResumeLayout(false);
             this.ResumeLayout(false);
             this.PerformLayout();
diff --git a/Core/Utils/MemoryUsageTracker.cs b/Core/Utils/MemoryUsageTracker.cs
new file mode 100644
index 00000000..d31ab7a3
--- /dev/null
+++ b/Core/Utils/MemoryUsageTracker.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Diagnostics;
+using System.Timers;
+using System.Windows.Forms;
+using CefSharp;
+using Timer = System.Timers.Timer;
+
+namespace TweetDuck.Core.Utils{
+    sealed class MemoryUsageTracker : IDisposable{
+        private const int IntervalMemoryCheck = 60000*30; // 30 minutes
+        private const int IntervalCleanupAttempt = 60000*5; // 5 minutes
+
+        private readonly string script;
+        private readonly Timer timer;
+        private Form owner;
+        private IBrowser browser;
+
+        private long threshold;
+        private bool needsCleanup;
+
+        public MemoryUsageTracker(string cleanupFunctionName){
+            this.script = $"window.{cleanupFunctionName} && window.{cleanupFunctionName}()";
+
+            this.timer = new Timer{ Interval = IntervalMemoryCheck };
+            this.timer.Elapsed += timer_Elapsed;
+        }
+
+        public void Start(Form owner, IBrowser browser, int thresholdMB){
+            Stop();
+
+            this.owner = owner;
+            this.browser = browser;
+            this.threshold = thresholdMB*1024L*1024L;
+            this.timer.SynchronizingObject = owner;
+            this.timer.Start();
+        }
+
+        public void Stop(){
+            timer.Stop();
+            timer.SynchronizingObject = null;
+            owner = null;
+            browser = null;
+            SetNeedsCleanup(false);
+        }
+
+        public void Dispose(){
+            timer.SynchronizingObject = null;
+            timer.Dispose();
+            owner = null;
+            browser = null;
+        }
+
+        private void SetNeedsCleanup(bool value){
+            if (needsCleanup != value){
+                needsCleanup = value;
+                timer.Interval = value ? IntervalCleanupAttempt : IntervalMemoryCheck; // restarts timer
+            }
+        }
+
+        private void timer_Elapsed(object sender, ElapsedEventArgs e){
+            if (owner == null || browser == null){
+                return;
+            }
+            
+            if (needsCleanup){
+                if (!owner.ContainsFocus){
+                    using(IFrame frame = browser.MainFrame){
+                        frame.EvaluateScriptAsync(script).ContinueWith(task => {
+                            JavascriptResponse response = task.Result;
+
+                            if (response.Success && (response.Result as bool? ?? false)){
+                                SetNeedsCleanup(false);
+                            }
+                        });
+                    }
+                }
+            }
+            else{
+                try{
+                    using(Process process = BrowserProcesses.FindProcess(browser)){
+                        if (process?.PrivateMemorySize64 > threshold){
+                            SetNeedsCleanup(true);
+                        }
+                    }
+                }catch{
+                    // ignore I guess?
+                }
+            }
+        }
+    }
+}
diff --git a/Resources/Scripts/code.js b/Resources/Scripts/code.js
index 12d2a6f9..6ff4382b 100644
--- a/Resources/Scripts/code.js
+++ b/Resources/Scripts/code.js
@@ -784,6 +784,40 @@
     };
   }
   
+  //
+  // Block: Memory cleanup check and execution.
+  //
+  window.TDGF_tryRunCleanup = function(){
+    // all textareas are empty
+    if ($("textarea").is(function(){
+      return $(this).val().length > 0;
+    })){
+      return false;
+    }
+    
+    // no modals are visible
+    if ($("#open-modal").is(":visible") || !$(".js-modals-container").is(":empty")){
+      return false;
+    }
+    
+    // all columns are in a default state
+    if ($("section.js-column").is(".is-shifted-1,.is-shifted-2")){
+      return false;
+    }
+    
+    // all columns are scrolled to top
+    if ($(".js-column-scroller").is(function(){
+      return $(this).scrollTop() > 0;
+    })){
+      return false;
+    }
+    
+    // cleanup
+    window.gc && window.gc();
+    window.location.reload();
+    return true;
+  };
+  
   //
   // Block: Disable TweetDeck metrics.
   //
diff --git a/TweetDuck.csproj b/TweetDuck.csproj
index 7f5ced4f..c689bcae 100644
--- a/TweetDuck.csproj
+++ b/TweetDuck.csproj
@@ -203,6 +203,7 @@
     </Compile>
     <Compile Include="Core\Notification\Screenshot\TweetScreenshotManager.cs" />
     <Compile Include="Core\Utils\InjectedHTML.cs" />
+    <Compile Include="Core\Utils\MemoryUsageTracker.cs" />
     <Compile Include="Core\Utils\TwoKeyDictionary.cs" />
     <Compile Include="Core\Utils\WindowState.cs" />
     <Compile Include="Core\Utils\WindowsUtils.cs" />