PixbufCache.vala 11.6 KB
Newer Older
1
/* Copyright 2016 Software Freedom Conservancy Inc.
2 3
 *
 * This software is licensed under the GNU LGPL (version 2.1 or later).
4
 * See the COPYING file in this distribution.
5 6
 */

7
public class PixbufCache : Object {
8 9
    public delegate bool CacheFilter(Photo photo);
    
10
    public enum PhotoType {
11
        BASELINE,
12
        MASTER
13 14
    }
    
15 16
    public class PixbufCacheBatch : Gee.TreeMultiMap<BackgroundJob.JobPriority, Photo> {
        public PixbufCacheBatch() {
17
            base ((GLib.CompareDataFunc<BackgroundJob.JobPriority>)BackgroundJob.JobPriority.compare_func);
18 19 20
        }
    }
    
21 22
    private abstract class FetchJob : BackgroundJob {
        public BackgroundJob.JobPriority priority;
23
        public Photo photo;
24 25 26 27
        public Scaling scaling;
        public Gdk.Pixbuf pixbuf = null;
        public Error err = null;
        
28
        public FetchJob(PixbufCache owner, BackgroundJob.JobPriority priority, Photo photo, 
29
            Scaling scaling, CompletionCallback callback) {
30
            base(owner, callback, new Cancellable(), null, new Semaphore());
31 32 33 34 35 36 37 38 39 40 41
            
            this.priority = priority;
            this.photo = photo;
            this.scaling = scaling;
        }
        
        public override BackgroundJob.JobPriority get_priority() {
            return priority;
        }
    }
    
42 43
    private class BaselineFetchJob : FetchJob {
        public BaselineFetchJob(PixbufCache owner, BackgroundJob.JobPriority priority, Photo photo, 
44 45
            Scaling scaling, CompletionCallback callback) {
            base(owner, priority, photo, scaling, callback);
46
        }
47
        
48 49 50 51 52 53 54 55 56
        public override void execute() {
            try {
                pixbuf = photo.get_pixbuf(scaling);
            } catch (Error err) {
                this.err = err;
            }
        }
    }
    
57 58
    private class MasterFetchJob : FetchJob {
        public MasterFetchJob(PixbufCache owner, BackgroundJob.JobPriority priority, Photo photo, 
59 60
            Scaling scaling, CompletionCallback callback) {
            base(owner, priority, photo, scaling, callback);
61
        }
62
        
63 64
        public override void execute() {
            try {
65
                pixbuf = photo.get_master_pixbuf(scaling);
66 67 68 69 70 71 72 73 74 75 76 77
            } catch (Error err) {
                this.err = err;
            }
        }
    }
    
    private static Workers background_workers = null;
    
    private SourceCollection sources;
    private PhotoType type;
    private int max_count;
    private Scaling scaling;
78
    private unowned CacheFilter? filter;
79 80 81
    private Gee.HashMap<Photo, Gdk.Pixbuf> cache = new Gee.HashMap<Photo, Gdk.Pixbuf>();
    private Gee.ArrayList<Photo> lru = new Gee.ArrayList<Photo>();
    private Gee.HashMap<Photo, FetchJob> in_progress = new Gee.HashMap<Photo, FetchJob>();
82
    
83
    public signal void fetched(Photo photo, Gdk.Pixbuf? pixbuf, Error? err);
84
    
85 86
    public PixbufCache(SourceCollection sources, PhotoType type, Scaling scaling, int max_count,
        CacheFilter? filter = null) {
87 88 89 90
        this.sources = sources;
        this.type = type;
        this.scaling = scaling;
        this.max_count = max_count;
91
        this.filter = filter;
92 93 94 95
        
        assert(max_count > 0);
        
        if (background_workers == null)
96
            background_workers = new Workers(Workers.thread_per_cpu_minus_one(), false);
97
        
98 99 100 101
        // monitor changes in the photos to discard from cache ... only interested in changes if
        // not master files
        if (type != PhotoType.MASTER)
            sources.items_altered.connect(on_sources_altered);
102
        sources.items_removed.connect(on_sources_removed);
103 104 105
    }
    
    ~PixbufCache() {
106
#if TRACE_PIXBUF_CACHE
107
        debug("Freeing %d pixbufs and cancelling %d jobs", cache.size, in_progress.size);
108
#endif
109
        
110 111
        if (type != PhotoType.MASTER)
            sources.items_altered.disconnect(on_sources_altered);
112
        sources.items_removed.disconnect(on_sources_removed);
113
        
114 115
        foreach (FetchJob job in in_progress.values)
            job.cancel();
116 117 118 119 120 121 122
    }
    
    public Scaling get_scaling() {
        return scaling;
    }
    
    // This call never blocks.  Returns null if the pixbuf is not present.
123
    public Gdk.Pixbuf? get_ready_pixbuf(Photo photo) {
124 125 126 127 128
        return get_cached(photo);
    }
    
    // This call can potentially block if the pixbuf is not in the cache.  Once loaded, it will
    // be cached.  No signal is fired.
129
    public Gdk.Pixbuf? fetch(Photo photo) throws Error {
130 131 132
        if (!photo.get_actual_file().query_exists(null))
            decache(photo);
        
133
        Gdk.Pixbuf pixbuf = get_cached(photo);
134 135 136 137 138
        if (pixbuf != null) {
#if TRACE_PIXBUF_CACHE
            debug("Fetched in-memory pixbuf for %s @ %s", photo.to_string(), scaling.to_string());
#endif
            
139
            return pixbuf;
140 141
        }
        
142 143 144 145 146 147 148 149 150
        FetchJob? job = in_progress.get(photo);
        if (job != null) {
            job.wait_for_completion();
            if (job.err != null)
                throw job.err;
            
            return job.pixbuf;
        }
        
151 152 153
#if TRACE_PIXBUF_CACHE
        debug("Forced to make a blocking fetch of %s @ %s", photo.to_string(), scaling.to_string());
#endif
154 155 156 157 158 159 160 161
        
        pixbuf = photo.get_pixbuf(scaling);
        
        encache(photo, pixbuf);
        
        return pixbuf;
    }
    
162 163 164
    // This can be used to clear specific pixbufs from the cache, allowing finer control over what
    // pixbufs remain and avoid being dropped when other fetches follow.  It implicitly cancels
    // any outstanding prefetches for the photo.
165
    public void drop(Photo photo) {
166 167 168 169
        cancel_prefetch(photo);
        decache(photo);
    }
    
170 171
    // This call signals the cache to pre-load the pixbuf for the photo.  When loaded the fetched
    // signal is fired.
172
    public void prefetch(Photo photo, 
173
        BackgroundJob.JobPriority priority = BackgroundJob.JobPriority.NORMAL, bool force = false) {
174 175 176
        if (!photo.get_actual_file().query_exists(null))
            decache(photo);
        
177 178 179
        if (!force && cache.has_key(photo)) {
            prioritize(photo);
            
180
            return;
181
        }
182
        
183
        if (in_progress.has_key(photo))
184 185
            return;
        
186 187 188
        if (filter != null && !filter(photo))
            return;
        
189 190
        FetchJob job = null;
        switch (type) {
191 192
            case PhotoType.BASELINE:
                job = new BaselineFetchJob(this, priority, photo, scaling, on_fetched);
193 194
            break;
            
195 196
            case PhotoType.MASTER:
                job = new MasterFetchJob(this, priority, photo, scaling, on_fetched);
197 198 199 200 201 202
            break;
            
            default:
                error("Unknown photo type: %d", (int) type);
        }
        
203 204
        in_progress.set(photo, job);
        
205 206 207
        background_workers.enqueue(job);
    }
    
208 209
    // This call signals the cache to pre-load the pixbufs for all supplied photos.  Each fires
    // the fetch signal as they arrive.
210
    public void prefetch_many(Gee.Collection<Photo> photos,
211
        BackgroundJob.JobPriority priority = BackgroundJob.JobPriority.NORMAL, bool force = false) {
212
        foreach (Photo photo in photos)
213 214 215
            prefetch(photo, priority, force);
    }
    
216 217 218 219 220 221 222 223
    // Like prefetch_many, but allows for priorities to be set for each photo
    public void prefetch_batch(PixbufCacheBatch batch, bool force = false) {
        foreach (BackgroundJob.JobPriority priority in batch.get_keys()) {
            foreach (Photo photo in batch.get(priority))
                prefetch(photo, priority, force);
        }
    }
    
224
    public bool cancel_prefetch(Photo photo) {
225 226
        FetchJob job = in_progress.get(photo);
        if (job == null)
227 228 229 230 231 232
            return false;
        
        // remove here because if fully cancelled the callback is never called
        bool removed = in_progress.unset(photo);
        assert(removed);
        
233
        job.cancel();
234
        
235 236 237 238
#if TRACE_PIXBUF_CACHE
        debug("Cancelled prefetch of %s @ %s", photo.to_string(), scaling.to_string());
#endif
        
239 240 241 242
        return true;
    }
    
    public void cancel_all() {
243 244 245
#if TRACE_PIXBUF_CACHE
        debug("Cancelling prefetch of %d photos at %s", in_progress.values.size, scaling.to_string());
#endif
246 247
        foreach (FetchJob job in in_progress.values)
            job.cancel();
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
        
        in_progress.clear();
    }
    
    private void on_fetched(BackgroundJob j) {
        FetchJob job = (FetchJob) j;
        
        // remove Cancellable from in_progress list, but don't assert on it because it's possible
        // the cancel was called after the task completed
        in_progress.unset(job.photo);
        
        if (job.err != null) {
            assert(job.pixbuf == null);
            
            critical("Unable to readahead %s: %s", job.photo.to_string(), job.err.message);
            fetched(job.photo, null, job.err);
            
            return;
        }
        
268 269 270 271
#if TRACE_PIXBUF_CACHE
        debug("%s %s fetched into pixbuf cache", type.to_string(), job.photo.to_string());
#endif
        
272 273 274 275 276 277
        encache(job.photo, job.pixbuf);
        
        // fire signal
        fetched(job.photo, job.pixbuf, null);
    }
    
278 279 280 281 282
    private void on_sources_altered(Gee.Map<DataObject, Alteration> map) {
        foreach (DataObject object in map.keys) {
            if (!map.get(object).has_subject("image"))
                continue;
            
283
            Photo photo = (Photo) object;
284
            
285
            if (in_progress.has_key(photo)) {
286 287
                // Load is in progress, must cancel, but consider in-cache (since it was decached
                // before being put into progress)
288 289
                in_progress.get(photo).cancel();
                in_progress.unset(photo);
290
            } else if (!cache.has_key(photo)) {
291 292 293
                continue;
            }
            
294 295
            decache(photo);
            
296
#if TRACE_PIXBUF_CACHE
297 298
            debug("Re-fetching altered pixbuf from cache: %s @ %s", photo.to_string(),
                scaling.to_string());
299
#endif
300 301 302
            
            prefetch(photo, BackgroundJob.JobPriority.HIGH);
        }
303 304 305 306
    }
    
    private void on_sources_removed(Gee.Iterable<DataObject> removed) {
        foreach (DataObject object in removed) {
307
            Photo photo = object as Photo;
308 309 310 311 312 313
            assert(photo != null);
            
            decache(photo);
        }
    }
    
314
    private Gdk.Pixbuf? get_cached(Photo photo) {
315
        Gdk.Pixbuf pixbuf = cache.get(photo);
316 317
        if (pixbuf != null)
            prioritize(photo);
318
        
319 320 321 322 323
        return pixbuf;
    }
    
    // Moves the photo up in the cache LRU.  Assumes photo is actually in cache.
    private void prioritize(Photo photo) {
324 325
        int index = lru.index_of(photo);
        assert(index >= 0);
326 327 328 329 330
        
        if (index > 0) {
            lru.remove_at(index);
            lru.insert(0, photo);
        }
331 332
    }
    
333
    private void encache(Photo photo, Gdk.Pixbuf pixbuf) {
334 335 336 337 338 339 340
        // if already in cache, remove (means it was re-fetched, probably due to modification)
        decache(photo);
        
        cache.set(photo, pixbuf);
        lru.insert(0, photo);
        
        while (lru.size > max_count) {
341
            Photo cached_photo = lru.remove_at(lru.size - 1);
342 343 344 345 346 347 348 349 350
            assert(cached_photo != null);
            
            bool removed = cache.unset(cached_photo);
            assert(removed);
        }
        
        assert(lru.size == cache.size);
    }
    
351
    private void decache(Photo photo) {
352
        if (!cache.unset(photo)) {
353 354 355 356 357 358 359 360 361 362
            assert(!lru.contains(photo));
            
            return;
        }
        
        bool removed = lru.remove(photo);
        assert(removed);
    }
}