Commit c7469c91 authored by Jim Nelson's avatar Jim Nelson

#741: Now performing readaheads in full-window and fullscreen view with a...

#741: Now performing readaheads in full-window and fullscreen view with a cache filled by background threads.  
Also fixed some issues exposed by these readaheads in direct edit mode and fullscreen mode.
parent 5d3b898f
......@@ -60,7 +60,8 @@ SRC_FILES = \
Config.vala \
Event.vala \
International.vala \
Workers.vala
Workers.vala \
PixbufCache.vala
VAPI_FILES = \
libexif.vapi \
......
......@@ -19,6 +19,7 @@ public class FullscreenWindow : PageWindow {
private bool is_toolbar_shown = false;
private bool waiting_for_invoke = false;
private time_t left_toolbar_time = 0;
private bool switched_to = false;
public FullscreenWindow(Page page) {
current_page = page;
......@@ -82,11 +83,16 @@ public class FullscreenWindow : PageWindow {
// start off with toolbar invoked, as a clue for the user
invoke_toolbar();
}
private override void realize() {
base.realize();
current_page.switched_to();
private override bool configure_event(Gdk.EventConfigure event) {
bool result = base.configure_event(event);
if (!switched_to) {
current_page.switched_to();
switched_to = true;
}
return result;
}
private Gtk.ActionEntry[] create_actions() {
......@@ -119,6 +125,7 @@ public class FullscreenWindow : PageWindow {
toolbar_window = null;
current_page.switching_from();
current_page = null;
AppWindow.get_instance().end_fullscreen();
}
......
......@@ -974,7 +974,7 @@ public class ImportQueuePage : SinglePhotoPage {
private uint64 total_bytes = 0;
public ImportQueuePage() {
base(_("Importing ..."));
base(_("Importing..."));
init_ui("import_queue.ui", "/ImportQueueMenuBar", "ImportQueueActionGroup",
create_actions());
......
......@@ -1037,6 +1037,7 @@ public abstract class SinglePhotoPage : Page {
protected Gtk.Viewport viewport = new Gtk.Viewport(null, null);
protected Gdk.GC text_gc = null;
private Gtk.Window container = null;
private Gdk.Pixmap pixmap = null;
private Dimensions pixmap_dim = Dimensions();
private Gdk.Pixbuf unscaled = null;
......@@ -1070,6 +1071,17 @@ public abstract class SinglePhotoPage : Page {
set_event_source(canvas);
}
public Gtk.Window? get_container() {
return container;
}
public virtual void set_container(Gtk.Window container) {
// this should only be called once
assert(this.container == null);
this.container = container;
}
public Gdk.InterpType set_default_interp(Gdk.InterpType default_interp) {
Gdk.InterpType old = this.default_interp;
this.default_interp = default_interp;
......@@ -1108,7 +1120,7 @@ public abstract class SinglePhotoPage : Page {
}
public Scaling get_canvas_scaling() {
return Scaling.for_widget(viewport);
return (container is FullscreenWindow) ? Scaling.for_screen() : Scaling.for_widget(viewport);
}
public Gdk.Pixbuf? get_unscaled_pixbuf() {
......
This diff is collapsed.
This diff is collapsed.
/* Copyright 2009 Yorba Foundation
*
* This software is licensed under the GNU LGPL (version 2.1 or later).
* See the COPYING file in this distribution.
*/
public class PixbufCache {
public enum PhotoType {
REGULAR,
ORIGINAL
}
private abstract class FetchJob : BackgroundJob {
public PixbufCache owner;
public BackgroundJob.JobPriority priority;
public TransformablePhoto photo;
public Scaling scaling;
public Gdk.Pixbuf pixbuf = null;
public Error err = null;
public FetchJob(PixbufCache owner, BackgroundJob.JobPriority priority, TransformablePhoto photo,
Scaling scaling, CompletionCallback callback, Cancellable cancellable) {
base(callback, cancellable);
// maintain the owner ref because Workers do not, and if the PixbufCache is derefed
// before a job completes, an assertion fires
this.owner = owner;
this.priority = priority;
this.photo = photo;
this.scaling = scaling;
}
public override BackgroundJob.JobPriority get_priority() {
return priority;
}
}
private class RegularFetchJob : FetchJob {
public RegularFetchJob(PixbufCache owner, BackgroundJob.JobPriority priority, TransformablePhoto photo,
Scaling scaling, CompletionCallback callback, Cancellable cancellable) {
base(owner, priority, photo, scaling, callback, cancellable);
}
public override void execute() {
try {
pixbuf = photo.get_pixbuf(scaling);
} catch (Error err) {
this.err = err;
}
}
}
private class OriginalFetchJob : FetchJob {
public OriginalFetchJob(PixbufCache owner, BackgroundJob.JobPriority priority, TransformablePhoto photo,
Scaling scaling, CompletionCallback callback, Cancellable cancellable) {
base(owner, priority, photo, scaling, callback, cancellable);
}
public override void execute() {
try {
pixbuf = photo.get_original_pixbuf(scaling);
} 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;
private Gee.HashMap<TransformablePhoto, Gdk.Pixbuf> cache = new Gee.HashMap<TransformablePhoto,
Gdk.Pixbuf>();
private Gee.ArrayList<TransformablePhoto> lru = new Gee.ArrayList<TransformablePhoto>();
private Gee.HashMap<TransformablePhoto, Cancellable> in_progress = new Gee.HashMap<TransformablePhoto,
Cancellable>();
public signal void fetched(TransformablePhoto photo, Gdk.Pixbuf? pixbuf, Error? err);
public PixbufCache(SourceCollection sources, PhotoType type, Scaling scaling, int max_count) {
this.sources = sources;
this.type = type;
this.scaling = scaling;
this.max_count = max_count;
assert(max_count > 0);
if (background_workers == null)
background_workers = new Workers(Workers.THREAD_PER_CPU, false);
// monitor changes in the photos to discard from cache
sources.item_altered += on_source_altered;
sources.items_removed += on_sources_removed;
}
~PixbufCache() {
debug("Freeing %d pixbufs and cancelling %d jobs", cache.size, in_progress.size);
sources.item_altered -= on_source_altered;
sources.items_removed -= on_sources_removed;
foreach (Cancellable cancellable in in_progress.values)
cancellable.cancel();
}
public Scaling get_scaling() {
return scaling;
}
// This call never blocks. Returns null if the pixbuf is not present.
public Gdk.Pixbuf? get_ready_pixbuf(TransformablePhoto photo) {
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.
public Gdk.Pixbuf? fetch(TransformablePhoto photo) throws Error {
Gdk.Pixbuf pixbuf = get_cached(photo);
if (pixbuf != null)
return pixbuf;
pixbuf = photo.get_pixbuf(scaling);
encache(photo, pixbuf);
return pixbuf;
}
// This call signals the cache to pre-load the pixbuf for the photo. When loaded the fetched
// signal is fired.
public void prefetch(TransformablePhoto photo,
BackgroundJob.JobPriority priority = BackgroundJob.JobPriority.NORMAL, bool force = false) {
if (!force && cache.contains(photo))
return;
if (in_progress.contains(photo))
return;
Cancellable cancellable = new Cancellable();
in_progress.set(photo, cancellable);
FetchJob job = null;
switch (type) {
case PhotoType.REGULAR:
job = new RegularFetchJob(this, priority, photo, scaling, on_fetched, cancellable);
break;
case PhotoType.ORIGINAL:
job = new OriginalFetchJob(this, priority, photo, scaling, on_fetched, cancellable);
break;
default:
error("Unknown photo type: %d", (int) type);
break;
}
background_workers.enqueue(job);
}
public bool cancel_prefetch(TransformablePhoto photo) {
Cancellable cancellable = in_progress.get(photo);
if (cancellable == null)
return false;
// remove here because if fully cancelled the callback is never called
bool removed = in_progress.unset(photo);
assert(removed);
cancellable.cancel();
return true;
}
public void cancel_all() {
foreach (Cancellable cancellable in in_progress.values)
cancellable.cancel();
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;
}
encache(job.photo, job.pixbuf);
// fire signal
fetched(job.photo, job.pixbuf, null);
}
private void on_source_altered(DataObject object) {
TransformablePhoto photo = object as TransformablePhoto;
assert(photo != null);
debug("Removing altered pixbuf from cache: %s", photo.to_string());
decache(photo);
}
private void on_sources_removed(Gee.Iterable<DataObject> removed) {
foreach (DataObject object in removed) {
TransformablePhoto photo = object as TransformablePhoto;
assert(photo != null);
debug("Removing destroyed photo from cache: %s", photo.to_string());
decache(photo);
}
}
private Gdk.Pixbuf? get_cached(TransformablePhoto photo) {
Gdk.Pixbuf pixbuf = cache.get(photo);
if (pixbuf == null)
return null;
// move up in the LRU
int index = lru.index_of(photo);
assert(index >= 0);
lru.remove_at(index);
lru.insert(0, photo);
return pixbuf;
}
private void encache(TransformablePhoto photo, Gdk.Pixbuf pixbuf) {
// 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) {
TransformablePhoto cached_photo = lru.remove_at(lru.size - 1);
assert(cached_photo != null);
bool removed = cache.unset(cached_photo);
assert(removed);
}
assert(lru.size == cache.size);
}
private void decache(TransformablePhoto photo) {
if (!cache.remove(photo)) {
assert(!lru.contains(photo));
return;
}
bool removed = lru.remove(photo);
assert(removed);
}
}
......@@ -277,14 +277,8 @@ public class ThumbnailCache : Object {
// situation. This may change in the future, and the caching situation will need to be
// handled.
AsyncFetchJob job = new AsyncFetchJob(this, photo_id, pixbuf, dim, interp, callback,
cancellable);
try {
fetch_workers.enqueue(job);
} catch (Error err) {
error("Unable to enqueue async thumbnail fetch: %s", err.message);
}
fetch_workers.enqueue(new AsyncFetchJob(this, photo_id, pixbuf, dim, interp, callback,
cancellable));
}
// Called within Gtk.main's thread context
......
......@@ -13,6 +13,19 @@ public delegate void CompletionCallback(BackgroundJob job);
// worker thread prior to calling execute()). The BackgroundJob may also specify a
// CompletionCallback to be executed within Gtk's event loop.
public abstract class BackgroundJob {
public enum JobPriority {
HIGHEST = 100,
HIGH = 75,
NORMAL = 50,
LOW = 25,
LOWEST = 0;
// Returns negative if this is higher, zero if equal, positive if this is lower
public int compare(JobPriority other) {
return (int) other - (int) this;
}
}
private CompletionCallback callback;
private Cancellable cancellable;
private BackgroundJob self = null;
......@@ -24,6 +37,10 @@ public abstract class BackgroundJob {
public abstract void execute();
public virtual JobPriority get_priority() {
return JobPriority.NORMAL;
}
public bool is_cancelled() {
return (cancellable != null) ? cancellable.is_cancelled() : false;
}
......@@ -89,7 +106,7 @@ public class Workers {
max_threads = THREAD_PER_CPU;
if (max_threads == THREAD_PER_CPU) {
max_threads = (int) ExtendedPosix.sysconf(ExtendedPosix.ConfName._SC_NPROCESSORS_ONLN) - 1;
max_threads = (int) ExtendedPosix.sysconf(ExtendedPosix.ConfName._SC_NPROCESSORS_ONLN);
if (max_threads <= 0)
max_threads = 1;
}
......@@ -109,17 +126,20 @@ public class Workers {
// Enqueues a BackgroundJob for work in a thread context. BackgroundJob.execute() is called
// within the thread's context, while its CompletionCallback is called within the Gtk event loop.
public void enqueue(BackgroundJob background_job) throws Error {
queue.push(background_job);
public void enqueue(BackgroundJob background_job) {
queue.push_sorted(background_job, compare_jobs);
}
private static int compare_jobs(void *a, void *b) {
BackgroundJob.JobPriority a_priority = ((BackgroundJob *) a)->get_priority();
BackgroundJob.JobPriority b_priority = ((BackgroundJob *) b)->get_priority();
return a_priority.compare(b_priority);
}
public void die() {
try {
for (int ctr = 0; ctr < thread_count; ctr++)
enqueue(die_job);
} catch (Error err) {
warning("Unable to kill worker threads: %s", err.message);
}
for (int ctr = 0; ctr < thread_count; ctr++)
enqueue(die_job);
}
private void *thread_start() {
......
......@@ -63,7 +63,8 @@ void library_exec(string[] mounts) {
if (!verify_databases(out app_version)) {
string errormsg = _("The database for your photo library is not compatible with this version of Shotwell. It appears it was created by Shotwell %s. Please clear your library by deleting %s and re-import your photos.");
Gtk.MessageDialog dialog = new Gtk.MessageDialog(null, Gtk.DialogFlags.MODAL,
Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, errormsg, app_version, AppWindow.get_data_dir());
Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, errormsg, app_version,
AppWindow.get_data_dir().get_path());
dialog.title = Resources.APP_TITLE;
dialog.run();
dialog.destroy();
......
......@@ -89,6 +89,14 @@ public class KeyValueMap {
this.group = group;
}
public KeyValueMap copy() {
KeyValueMap clone = new KeyValueMap(group);
foreach (string key in map.keys)
clone.map.set(key, map.get(key));
return clone;
}
public string get_group() {
return group;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment