Index: gnome-shell-3.10.4/js/ui/background.js
===================================================================
--- gnome-shell-3.10.4.orig/js/ui/background.js
+++ gnome-shell-3.10.4/js/ui/background.js
@@ -32,6 +32,10 @@ const ANIMATION_MIN_WAKEUP_INTERVAL = 1.
 
 let _backgroundCache = null;
 
+// NOTE: the caching role is for various BMs like the normal desktop background,
+// overview background, screen shield background, thumbnail backgrounds and etc,
+// *NOT* for different backgrounds across selections. This is a major
+// misunderstanding I had.
 const BackgroundCache = new Lang.Class({
     Name: 'BackgroundCache',
 
@@ -40,6 +44,7 @@ const BackgroundCache = new Lang.Class({
        this._images = [];
        this._pendingFileLoads = [];
        this._fileMonitors = {};
+       // NOTE: in recent GS, `_animations` is schema-keyed hash.
        this._animations = [];
     },
 
@@ -53,6 +58,12 @@ const BackgroundCache = new Lang.Class({
         let content = null;
 
         let candidateContent = null;
+        // FIX: add exact match check to avoid unnecessary copy
+        //
+        // NOTE: the use of `candidateIsFullMatch` helps reduce the memory but
+        // the content removing code is dumb and might lead to premature content
+        // removal.
+        let candidateIsFullMatch = false;
         for (let i = 0; i < this._patterns.length; i++) {
             if (this._patterns[i].get_shading() != params.shadingType)
                 continue;
@@ -69,11 +80,17 @@ const BackgroundCache = new Lang.Class({
             if (params.effects != this._patterns[i].effects)
                 continue;
 
+            candidateIsFullMatch = true;
             break;
         }
 
         if (candidateContent) {
-            content = candidateContent.copy(params.monitorIndex, params.effects);
+            if (candidateIsFullMatch) {
+                content = candidateContent;
+            } else {
+                content = candidateContent.copy(params.monitorIndex, params.effects);
+                this._patterns.push(content);
+            }
         } else {
             content = new Meta.Background({ meta_screen: global.screen,
                                             monitor: params.monitorIndex,
@@ -84,9 +101,11 @@ const BackgroundCache = new Lang.Class({
             } else {
                 content.load_gradient(params.shadingType, params.color, params.secondColor);
             }
+
+            // FIX: push the new content, only when it's NEW
+            this._patterns.push(content);
         }
 
-        this._patterns.push(content);
         return content;
     },
 
@@ -112,6 +131,14 @@ const BackgroundCache = new Lang.Class({
                                            monitor.disconnect(signalId);
 
                                            this.emit('file-changed', filename);
+
+                                           // FIX: also remove this file monitor
+                                           // here. It should get removed while
+                                           // the related content is removed
+                                           // from `_images` but as noted in
+                                           // `_removeImageContent` there are
+                                           // some problems in relying on it.
+                                           delete this._fileMonitors[filename];
                                        }));
 
         this._fileMonitors[filename] = monitor;
@@ -124,15 +151,30 @@ const BackgroundCache = new Lang.Class({
             contentList.splice(index, 1);
     },
 
+    // FIX add animation cache management code, when background `_destroy`, it
+    // should call this function if it's an Animation.
+    removeAnimation: function(animation) {
+        delete this._fileMonitors[animation.filename];
+
+        this._animations = this._animations.filter(function(anime, idx){
+            return anime !== animation;
+        });
+    },
+
     removePatternContent: function(content) {
         this._removeContent(this._patterns, content);
     },
 
     removeImageContent: function(content) {
-        let filename = content.get_filename();
-
-        if (filename && this._fileMonitors[filename])
-            delete this._fileMonitors[filename];
+        // TODO: one file can be loaded as different content and thus the
+        // following direct removal is too simplistic. We need to track refs to
+        // file to properly remove them. Here, we do NOT remove file watches (a
+        // leak). Enable this to sacrifice file watching in exchange of less
+        // memory drain see bnc#963664 (until we have proper ref count).
+        //
+        // let filename = content.get_filename();
+        // if (filename && this._fileMonitors[filename])
+        //     delete this._fileMonitors[filename];
 
         this._removeContent(this._images, content);
     },
@@ -143,6 +185,10 @@ const BackgroundCache = new Lang.Class({
         if (!caller.cancellable)
             return;
 
+        // NOTE: connect to a signal `cancelled`, looks weird but seems to be
+        // valid, see https://www.roojs.com/seed/gir-1.2-gtk-3.0/gjs/Gio.Cancellable.html#expand
+        //
+        // This use of `connect` seems to be a special case
         caller.cancellable.connect(Lang.bind(this, function() {
             let idx = fileLoad.callers.indexOf(caller);
             fileLoad.callers.splice(idx, 1);
@@ -183,39 +229,62 @@ const BackgroundCache = new Lang.Class({
                          cancellable: new Gio.Cancellable(),
                          callers: [] };
         this._attachCallerToFileLoad(caller, fileLoad);
+        // FIX: add this fileLoad to `_pendingFileLoads`, o/w we'll not be above
+        // to group callers together
+        this._pendingFileLoads.push(fileLoad);
 
+        // NOTE: do not set `monitorIndex` so later it will get re-copied with
+        // right caller info
         let content = new Meta.Background({ meta_screen: global.screen });
 
-        content.load_file_async(params.filename,
-                                params.style,
-                                params.cancellable,
-                                Lang.bind(this,
-                                          function(object, result) {
-                                              try {
-                                                  content.load_file_finish(result);
-
-                                                  this._monitorFile(params.filename);
-                                              } catch(e) {
-                                                  content = null;
-                                              }
-
-                                              for (let i = 0; i < fileLoad.callers.length; i++) {
-                                                  let caller = fileLoad.callers[i];
-                                                  if (caller.onFinished) {
-                                                      let newContent;
-
-                                                      if (content) {
-                                                          newContent = content.copy(caller.monitorIndex, caller.effects);
-                                                          this._images.push(newContent);
-                                                      }
-
-                                                      caller.onFinished(newContent);
-                                                  }
-                                              }
-
-                                              let idx = this._pendingFileLoads.indexOf(fileLoad);
-                                              this._pendingFileLoads.splice(idx, 1);
-                                          }));
+        content.load_file_async(
+            params.filename,
+            params.style,
+            params.cancellable,
+            Lang.bind(this,
+                      function(object, result) {
+                          try {
+                              content.load_file_finish(result);
+
+                              this._monitorFile(params.filename);
+                          } catch(e) {
+                              content = null;
+                          }
+
+                          // TODO: from my log, there is only one caller for fileLoad
+                          // but this piece of code can run up to 6 times, with some of
+                          // them having effects set to 2(Overview BG). This leads to
+                          // multiple same entries in `cache._images`
+
+                          // FIX: a temporary area to avoid duplicates in cache `_images`.
+                          let newContents = [];
+                          let newContent = content;
+                          for (let i = 0; i < fileLoad.callers.length; i++) {
+                              let caller = fileLoad.callers[i];
+
+                              if (content) {
+                                  newContent = content.copy(caller.monitorIndex, caller.effects);
+
+                                  // only cache this content when other callers have NOT cached it.
+                                  let shouldCache = ! newContents.some(function(ctn) {
+                                      return ( (ctn.monitor === caller.monitorIndex)
+                                               && (ctn.effects === caller.effects) );
+                                  });
+                                  if (shouldCache) {
+                                      newContents.push(newContent);
+                                      this._images.push(newContent);
+                                  }
+                              }
+
+                              // run `onFinished` despite caching condition
+                              if (caller.onFinished) {
+                                  caller.onFinished(newContent);
+                              }
+                          }
+
+                          let idx = this._pendingFileLoads.indexOf(fileLoad);
+                          this._pendingFileLoads.splice(idx, 1);
+                      }));
     },
 
     getImageContent: function(params) {
@@ -229,6 +298,7 @@ const BackgroundCache = new Lang.Class({
         let content = null;
 
         let candidateContent = null;
+        let candidateIsFullMatch = false;
         for (let i = 0; i < this._images.length; i++) {
             if (this._images[i].get_style() != params.style)
                 continue;
@@ -242,19 +312,24 @@ const BackgroundCache = new Lang.Class({
 
             candidateContent = this._images[i];
 
+            // NOTE `effects` field does exist, just not enlisted in the LookingGlass....
             if (params.effects != this._images[i].effects)
                 continue;
 
+            candidateIsFullMatch = true;
             break;
         }
 
         if (candidateContent) {
-            content = candidateContent.copy(params.monitorIndex, params.effects);
+            if (candidateIsFullMatch) {
+                content = candidateContent;
+            } else {
+                content = candidateContent.copy(params.monitorIndex, params.effects);
+                this._images.push(content);
+            }
 
             if (params.cancellable && params.cancellable.is_cancelled())
                 content = null;
-            else
-                this._images.push(content);
 
             if (params.onFinished)
                 params.onFinished(content);
@@ -276,25 +351,22 @@ const BackgroundCache = new Lang.Class({
         let i;
         let animation = null;
         for (i = 0; i < this._animations.length; i++) {
-            if (this._animations[i].filename == params.filename) {
+            if (this._animations[i].filename === params.filename) {
                 animation = this._animations[i];
-                if (params.onLoaded) {
-                    let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, Lang.bind(this, function() {
-                        params.onLoaded(this._animation);
-                        return GLib.SOURCE_REMOVE;
-                    }));
-                    GLib.Source.set_name_by_id(id, '[gnome-shell] params.onLoaded');
-                }
+
+                // FIX: a match has been found
+                break;
             }
         }
 
         if (animation == null) {
             animation = new Animation({ filename: params.filename });
             this._animations.push(animation);
+
+            // FIX: only monitor new animation's file
+            this._monitorFile(params.filename);
         }
         animation.load(Lang.bind(this, function() {
-                           this._monitorFile(params.filename);
-
                            if (params.onLoaded) {
                                let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, Lang.bind(this, function() {
                                    params.onLoaded(animation);
@@ -336,12 +408,18 @@ const Background = new Lang.Class({
         // contains a single image for static backgrounds and
         // two images (from and to) for slide shows
         this._images = {};
+        // FIX: declare `_animation` here to avoid confusion
+        this._animation = null;
+        // FIX: declare here to avoid confusion
+        this._updateAnimationTimeoutId = 0;
 
         this._brightness = 1.0;
         this._vignetteSharpness = 0.2;
         this._cancellable = new Gio.Cancellable();
         this.isLoaded = false;
 
+        // NOTE looks weird to me. shouldn't `changed` signal on settings be attached to BM instead?
+        // However, the end result is the same and should not affect functionality.
         this._settingsChangedSignalId = this._settings.connect('changed', Lang.bind(this, function() {
                                             this.emit('changed');
                                         }));
@@ -364,6 +442,10 @@ const Background = new Lang.Class({
         }
         this._fileWatches = null;
 
+        if (this._animation) {
+            this._cache.removeAnimation(this._animation);
+            this._animation = null;
+        }
         if (this._pattern) {
             if (this._pattern.content)
                 this._cache.removePatternContent(this._pattern.content);
@@ -389,6 +471,9 @@ const Background = new Lang.Class({
         if (this._settingsChangedSignalId != 0)
             this._settings.disconnect(this._settingsChangedSignalId);
         this._settingsChangedSignalId = 0;
+
+        // FIX: reset `isLoaded`
+        this.isLoaded = false;
     },
 
     _setLoaded: function() {
@@ -397,10 +482,14 @@ const Background = new Lang.Class({
 
         this.isLoaded = true;
 
-        GLib.idle_add(GLib.PRIORITY_DEFAULT, Lang.bind(this, function() {
+        // NOTE: the extra waiting seems to avoid blocking the main loop as
+        // there might be multiple heavy handlers attached to `loaded` signal.
+        let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, Lang.bind(this, function() {
             this.emit('loaded');
-            return false;
+            return GLib.SOURCE_REMOVE;
         }));
+
+        GLib.Source.set_name_by_id(id, '[gnome-shell] this.emit');
     },
 
     _loadPattern: function() {
@@ -438,6 +527,7 @@ const Background = new Lang.Class({
         this._fileWatches[filename] = signalId;
     },
 
+    // NOTE maybe more properly, `_ensureImageActor`
     _ensureImage: function(index) {
         if (this._images[index])
             return;
@@ -450,13 +540,14 @@ const Background = new Lang.Class({
         this._images[index] = actor;
     },
 
+    // NOTE maybe more properly, `_updateImageContent`
     _updateImage: function(index, content, filename) {
         content.brightness = this._brightness;
         content.vignette_sharpness = this._vignetteSharpness;
 
         let image = this._images[index];
         if (image.content)
-            this._cache.removeImageContent(content);
+            this._cache.removeImageContent(image.content);
         image.content = content;
         this._watchCacheFile(filename);
     },
@@ -536,29 +627,33 @@ const Background = new Lang.Class({
         if (interval > GLib.MAXUINT32)
             return;
 
-        this._updateAnimationTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
-                                                      interval,
-                                                      Lang.bind(this, function() {
-                                                                    this._updateAnimationTimeoutId = 0;
-                                                                    this._updateAnimation();
-                                                                    return false;
-                                                                }));
+        this._updateAnimationTimeoutId = GLib.timeout_add(
+            GLib.PRIORITY_DEFAULT,
+            interval,
+            Lang.bind(this, function() {
+                this._updateAnimationTimeoutId = 0;
+                this._updateAnimation();
+                return GLib.SOURCE_REMOVE;
+            }));
+
+        GLib.Source.set_name_by_id(this._updateAnimationTimeoutId,
+                                   '[gnome-shell] this._updateAnimation');
     },
 
     _loadAnimation: function(filename) {
         this._cache.getAnimation({ filename: filename,
-                                             onLoaded: Lang.bind(this, function(animation) {
-                                                 this._animation = animation;
+                                   onLoaded: Lang.bind(this, function(animation) {
+                                       this._animation = animation;
 
-                                                 if (!this._animation || this._cancellable.is_cancelled()) {
-                                                     this._setLoaded();
-                                                     return;
-                                                 }
-
-                                                 this._updateAnimation();
-                                                 this._watchCacheFile(filename);
-                                             })
-                                           });
+                                       if (!this._animation || this._cancellable.is_cancelled()) {
+                                           this._setLoaded();
+                                           return;
+                                       }
+
+                                       this._updateAnimation();
+                                       this._watchCacheFile(filename);
+                                   })
+                                 });
     },
 
     _loadImage: function(filename) {
@@ -587,8 +682,9 @@ const Background = new Lang.Class({
     _load: function () {
         this._cache = getBackgroundCache();
 
-        this._loadPattern(this._cache);
+        this._loadPattern();
 
+        // NOTE: the following says: no image will be used so only show the pattern
         this._style = this._settings.get_enum(BACKGROUND_STYLE_KEY);
         if (this._style == GDesktopEnums.BackgroundStyle.NONE) {
             this._setLoaded();
@@ -666,7 +762,11 @@ const SystemBackground = new Lang.Class(
     },
 
     _onDestroy: function() {
-        this._cache.removeImageContent(this.actor.content);
+        // FIX: upstream cfef107114
+        let content = this.actor.content;
+
+        if (content)
+            this._cache.removeImageContent(content);
     },
 });
 Signals.addSignalMethods(SystemBackground.prototype);
@@ -755,30 +855,39 @@ const BackgroundManager = new Lang.Class
         }
     },
 
-    _updateBackground: function(background) {
+    _updateBackground: function() {
         let newBackground = this._createBackground();
-        newBackground.vignetteSharpness = background.vignetteSharpness;
-        newBackground.brightness = background.brightness;
-        newBackground.visible = background.visible;
+        newBackground.vignetteSharpness = this.background.vignetteSharpness;
+        newBackground.brightness = this.background.brightness;
+        newBackground.visible = this.background.visible;
 
         newBackground.loadedSignalId = newBackground.connect('loaded',
             Lang.bind(this, function() {
                 newBackground.disconnect(newBackground.loadedSignalId);
                 newBackground.loadedSignalId = 0;
-                Tweener.addTween(background.actor,
+                // NOTE: new background is loaded *asynchronously*, while
+                // `this._newBackground` holds the *newest* background. The
+                // following check make sure what gets loaded is still relevant.
+                //
+                // Backported from upstream commits: 933f38390ba1512aa and d6197b0904fa.
+                if (this._newBackground != newBackground) {
+                    /* Not interesting, we queued another load */
+                    newBackground.actor.destroy();
+                    return;
+                }
+                Tweener.addTween(this.background.actor,
                                  { opacity: 0,
                                    time: FADE_ANIMATION_TIME,
                                    transition: 'easeOutQuad',
                                    onComplete: Lang.bind(this, function() {
-                                       if (this._newBackground == newBackground) {
-                                           this.background = newBackground;
-                                           this._newBackground = null;
-                                       } else {
-                                           newBackground.actor.destroy();
-                                       }
-
-                                       background.actor.destroy();
-
+                                       this.background.actor.destroy();
+                                       this.background = newBackground;
+                                       this._newBackground = null;
+
+                                       // FIX: "changed" signal is emitted
+                                       // and old background destroyed *only
+                                       // when* changes really happen
+                                       // see c#24 for the discussion.
                                        this.emit('changed');
                                    })
                                  });
@@ -805,13 +914,14 @@ const BackgroundManager = new Lang.Class
         background.changeSignalId = background.connect('changed', Lang.bind(this, function() {
             background.disconnect(background.changeSignalId);
             background.changeSignalId = 0;
-            this._updateBackground(background);
+            this._updateBackground();
         }));
 
         background.actor.connect('destroy', Lang.bind(this, function() {
             if (background.changeSignalId)
                 background.disconnect(background.changeSignalId);
 
+            // NOTE: loadedSignalId is set in _updateBackground
             if (background.loadedSignalId)
                 background.disconnect(background.loadedSignalId);
         }));
Index: gnome-shell-3.10.4/src/shell-global.c
===================================================================
--- gnome-shell-3.10.4.orig/src/shell-global.c
+++ gnome-shell-3.10.4/src/shell-global.c
@@ -1417,6 +1417,13 @@ run_leisure_functions (gpointer data)
   if (global->work_count > 0)
     return FALSE;
 
+  /* MEM-FIX: run gc at tweener's end, this reduces the performance but avoid memory leaks
+   * see http://bazaar.launchpad.net/~ubuntu-branches/ubuntu/wily/gnome-shell/wily/view/head:/debian/patches/revert-disable-periodic-gc.patch
+   *
+   * NOTE: a bit overkill to fix bg memory problem, what I want is simply run gc after every background change not every tweener.
+   */
+  gjs_context_gc (global->js_context);
+
   /* No leisure closures, so we are done */
   if (global->leisure_closures == NULL)
     return FALSE;
