diff options
author | Christian Hergert <chergert@redhat.com> | 2019-07-26 10:40:43 -0700 |
---|---|---|
committer | Christian Hergert <chergert@redhat.com> | 2019-07-28 10:34:15 -0700 |
commit | 9926e6ebde28956ae65e5127966fd740552fbe6a (patch) | |
tree | 39a8c56dd5b059b2b1da5b8097e2160344617150 /gtk/gtktextlinedisplaycache.c | |
parent | b930c5a8a2545a8534585ae92f2d25a9581d345d (diff) | |
download | gtk+-9926e6ebde28956ae65e5127966fd740552fbe6a.tar.gz |
textlayout: introduce caching for GtkTextLineDisplay
This adds a GtkTextLineDisplayCache which can be used to cache a number
of GtkTextLineDisplay (and thus, PangoLayout) while displaying a view.
It uses a GSequence to track the position of the GtkTextLineDisplay
relative to each other, a MRU to cull the least recently used display,
and and a direct hashtable to lookup display by GtkTextLine.
We only cache lines that are to be displayed (!size_only). We may want to
either create a second collection of "size_only" lines to speed that up,
or determine that it is unnecessary (which is likely the case).
Diffstat (limited to 'gtk/gtktextlinedisplaycache.c')
-rw-r--r-- | gtk/gtktextlinedisplaycache.c | 716 |
1 files changed, 716 insertions, 0 deletions
diff --git a/gtk/gtktextlinedisplaycache.c b/gtk/gtktextlinedisplaycache.c new file mode 100644 index 0000000000..6129d787fc --- /dev/null +++ b/gtk/gtktextlinedisplaycache.c @@ -0,0 +1,716 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* GTK - The GIMP Toolkit + * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald + * Copyright (C) 2019 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include "gtktextbtree.h" +#include "gtktextbufferprivate.h" +#include "gtktextiterprivate.h" +#include "gtktextlinedisplaycacheprivate.h" + +#define MRU_MAX_SIZE 250 +#define BLOW_CACHE_TIMEOUT_SEC 20 +#define DEBUG_LINE_DISPLAY_CACHE 0 + +struct _GtkTextLineDisplayCache +{ + GSequence *sorted_by_line; + GHashTable *line_to_display; + GtkTextLine *cursor_line; + GQueue mru; + GSource *evict_source; + +#if DEBUG_LINE_DISPLAY_CACHE + guint log_source; + gint hits; + gint misses; + gint inval; + gint inval_cursors; + gint inval_by_line; + gint inval_by_range; + gint inval_by_y_range; +#endif +}; + +#if DEBUG_LINE_DISPLAY_CACHE +# define STAT_ADD(val,n) ((val) += n) +# define STAT_INC(val) STAT_ADD(val,1) +static gboolean +dump_stats (gpointer data) +{ + GtkTextLineDisplayCache *cache = data; + g_printerr ("%p: size=%u hits=%d misses=%d inval_total=%d " + "inval_cursors=%d inval_by_line=%d " + "inval_by_range=%d inval_by_y_range=%d\n", + cache, g_hash_table_size (cache->line_to_display), + cache->hits, cache->misses, + cache->inval, cache->inval_cursors, + cache->inval_by_line, cache->inval_by_range, + cache->inval_by_y_range); + return G_SOURCE_CONTINUE; +} +#else +# define STAT_ADD(val,n) +# define STAT_INC(val) +#endif + +GtkTextLineDisplayCache * +gtk_text_line_display_cache_new (void) +{ + GtkTextLineDisplayCache *ret; + + ret = g_slice_new0 (GtkTextLineDisplayCache); + ret->sorted_by_line = g_sequence_new ((GDestroyNotify)gtk_text_line_display_unref); + ret->line_to_display = g_hash_table_new (NULL, NULL); + +#if DEBUG_LINE_DISPLAY_CACHE + ret->log_source = g_timeout_add_seconds (1, dump_stats, ret); +#endif + + return g_steal_pointer (&ret); +} + +void +gtk_text_line_display_cache_free (GtkTextLineDisplayCache *cache) +{ +#if DEBUG_LINE_DISPLAY_CACHE + g_clear_handle_id (&cache->log_source, g_source_remove); +#endif + + g_clear_pointer (&cache->evict_source, g_source_destroy); + g_clear_pointer (&cache->sorted_by_line, g_sequence_free); + g_clear_pointer (&cache->line_to_display, g_hash_table_unref); + g_slice_free (GtkTextLineDisplayCache, cache); +} + +static gboolean +gtk_text_line_display_cache_blow_cb (gpointer data) +{ + GtkTextLineDisplayCache *cache = data; + + g_assert (cache != NULL); + +#if DEBUG_LINE_DISPLAY_CACHE + g_printerr ("Evicting GtkTextLineDisplayCache\n"); +#endif + + cache->evict_source = NULL; + + gtk_text_line_display_cache_invalidate (cache); + + return G_SOURCE_REMOVE; +} + +void +gtk_text_line_display_cache_delay_eviction (GtkTextLineDisplayCache *cache) +{ + g_assert (cache != NULL); + + if (cache->evict_source != NULL) + { + gint64 deadline; + + deadline = g_get_monotonic_time () + (BLOW_CACHE_TIMEOUT_SEC * G_USEC_PER_SEC); + g_source_set_ready_time (cache->evict_source, deadline); + } + else + { + guint tag; + + tag = g_timeout_add_seconds (BLOW_CACHE_TIMEOUT_SEC, + gtk_text_line_display_cache_blow_cb, + cache); + cache->evict_source = g_main_context_find_source_by_id (NULL, tag); + g_source_set_name (cache->evict_source, "[gtk+] gtk_text_line_display_cache_blow_cb"); + } +} + +#if DEBUG_LINE_DISPLAY_CACHE +static void +check_disposition (GtkTextLineDisplayCache *cache, + GtkTextLayout *layout) +{ + GSequenceIter *iter; + gint last = G_MAXUINT; + + g_assert (cache != NULL); + g_assert (cache->sorted_by_line != NULL); + g_assert (cache->line_to_display != NULL); + + for (iter = g_sequence_get_begin_iter (cache->sorted_by_line); + !g_sequence_iter_is_end (iter); + iter = g_sequence_iter_next (iter)) + { + GtkTextLineDisplay *display = g_sequence_get (iter); + GtkTextIter text_iter; + guint line; + + gtk_text_layout_get_iter_at_line (layout, &text_iter, display->line, 0); + line = gtk_text_iter_get_line (&text_iter); + + g_assert_cmpint (line, >, last); + + last = line; + } +} +#endif + +static void +gtk_text_line_display_cache_take_display (GtkTextLineDisplayCache *cache, + GtkTextLineDisplay *display, + GtkTextLayout *layout) +{ + g_assert (cache != NULL); + g_assert (display != NULL); + g_assert (display->line != NULL); + g_assert (display->cache_iter == NULL); + g_assert (display->mru_link.data == display); + g_assert (display->mru_link.prev == NULL); + g_assert (display->mru_link.next == NULL); + g_assert (g_hash_table_lookup (cache->line_to_display, display->line) == NULL); + +#if DEBUG_LINE_DISPLAY_CACHE + check_disposition (cache, layout); +#endif + + display->cache_iter = + g_sequence_insert_sorted (cache->sorted_by_line, + display, + (GCompareDataFunc) gtk_text_line_display_compare, + layout); + g_hash_table_insert (cache->line_to_display, display->line, display); + g_queue_push_head_link (&cache->mru, &display->mru_link); + + /* Cull the cache if we're at capacity */ + while (cache->mru.length > MRU_MAX_SIZE) + { + display = g_queue_peek_tail (&cache->mru); + + gtk_text_line_display_cache_invalidate_display (cache, display, FALSE); + } +} + +/* + * gtk_text_line_display_cache_invalidate_display: + * @cache: a GtkTextLineDisplayCache + * @display: a GtkTextLineDisplay + * @cursors_only: if only the cursor positions should be invalidated + * + * If @cursors_only is TRUE, then only the cursors are invalidated. Otherwise, + * @display is removed from the cache. + * + * Use this function when you already have access to a display as it reduces + * some overhead. + */ +void +gtk_text_line_display_cache_invalidate_display (GtkTextLineDisplayCache *cache, + GtkTextLineDisplay *display, + gboolean cursors_only) +{ + g_assert (cache != NULL); + g_assert (display != NULL); + g_assert (display->line != NULL); + + if (cursors_only) + { + g_clear_pointer (&display->cursors, g_array_unref); + display->cursors_invalid = TRUE; + display->has_block_cursor = FALSE; + } + else + { + GSequenceIter *iter = g_steal_pointer (&display->cache_iter); + + if (cache->cursor_line == display->line) + cache->cursor_line = NULL; + + g_hash_table_remove (cache->line_to_display, display->line); + g_queue_unlink (&cache->mru, &display->mru_link); + + if (iter != NULL) + g_sequence_remove (iter); + } + + STAT_INC (cache->inval); +} + +/* + * gtk_text_line_display_cache_get: + * @cache: a #GtkTextLineDisplayCache + * @layout: a GtkTextLayout + * @line: a GtkTextLine + * @size_only: if only line sizing is needed + * + * Gets a GtkTextLineDisplay for @line. + * + * If no cached display exists, a new display will be created. + * + * It's possible that calling this function will cause some existing + * cached displays to be released and destroyed. + * + * Returns: (transfer full) (not nullable): a #GtkTextLineDisplay + */ +GtkTextLineDisplay * +gtk_text_line_display_cache_get (GtkTextLineDisplayCache *cache, + GtkTextLayout *layout, + GtkTextLine *line, + gboolean size_only) +{ + GtkTextLineDisplay *display; + + g_assert (cache != NULL); + g_assert (layout != NULL); + g_assert (line != NULL); + + display = g_hash_table_lookup (cache->line_to_display, line); + + if (display != NULL) + { + if (size_only || !display->size_only) + { + STAT_INC (cache->hits); + + if (!size_only && display->line == cache->cursor_line) + gtk_text_layout_update_display_cursors (layout, display->line, display); + + /* Move to front of MRU */ + g_queue_unlink (&cache->mru, &display->mru_link); + g_queue_push_head_link (&cache->mru, &display->mru_link); + + return gtk_text_line_display_ref (display); + } + + /* We need an updated display that includes more than just + * sizing, so we need to drop this entry and force the layout + * to create a new one. + */ + gtk_text_line_display_cache_invalidate_display (cache, display, FALSE); + } + + STAT_INC (cache->misses); + + g_assert (!g_hash_table_lookup (cache->line_to_display, line)); + + display = gtk_text_layout_create_display (layout, line, size_only); + + g_assert (display != NULL); + g_assert (display->line == line); + + if (!size_only) + { + if (line == cache->cursor_line) + gtk_text_layout_update_display_cursors (layout, line, display); + + gtk_text_line_display_cache_take_display (cache, + gtk_text_line_display_ref (display), + layout); + } + + return g_steal_pointer (&display); +} + +void +gtk_text_line_display_cache_invalidate (GtkTextLineDisplayCache *cache) +{ + g_assert (cache != NULL); + g_assert (cache->sorted_by_line != NULL); + g_assert (cache->line_to_display != NULL); + + STAT_ADD (cache->inval, g_hash_table_size (cache->line_to_display)); + + cache->cursor_line = NULL; + + while (cache->mru.head != NULL) + { + GtkTextLineDisplay *display = g_queue_peek_head (&cache->mru); + + gtk_text_line_display_cache_invalidate_display (cache, display, FALSE); + } + + g_assert (g_hash_table_size (cache->line_to_display) == 0); + g_assert (g_sequence_get_length (cache->sorted_by_line) == 0); + g_assert (cache->mru.length == 0); +} + +void +gtk_text_line_display_cache_invalidate_cursors (GtkTextLineDisplayCache *cache, + GtkTextLine *line) +{ + GtkTextLineDisplay *display; + + g_assert (cache != NULL); + g_assert (line != NULL); + + STAT_INC (cache->inval_cursors); + + display = g_hash_table_lookup (cache->line_to_display, line); + + if (display != NULL) + gtk_text_line_display_cache_invalidate_display (cache, display, TRUE); +} + +/* + * gtk_text_line_display_cache_invalidate_line: + * @self: a GtkTextLineDisplayCache + * @line: a GtkTextLine + * + * Removes a cached display for @line. + * + * Compare to gtk_text_line_display_cache_invalidate_cursors() which + * only invalidates the cursors for this row. + */ +void +gtk_text_line_display_cache_invalidate_line (GtkTextLineDisplayCache *cache, + GtkTextLine *line) +{ + GtkTextLineDisplay *display; + + g_assert (cache != NULL); + g_assert (line != NULL); + + display = g_hash_table_lookup (cache->line_to_display, line); + + if (display != NULL) + gtk_text_line_display_cache_invalidate_display (cache, display, FALSE); + + STAT_INC (cache->inval_by_line); +} + +static GSequenceIter * +find_iter_at_text_iter (GtkTextLineDisplayCache *cache, + GtkTextLayout *layout, + const GtkTextIter *iter) +{ + GSequenceIter *left; + GSequenceIter *right; + GSequenceIter *mid; + GSequenceIter *end; + GtkTextLine *target; + guint target_lineno; + + g_assert (cache != NULL); + g_assert (iter != NULL); + + if (g_sequence_is_empty (cache->sorted_by_line)) + return NULL; + + /* gtk_text_iter_get_line() will have cached value */ + target_lineno = gtk_text_iter_get_line (iter); + target = _gtk_text_iter_get_text_line (iter); + + /* Get some iters so we can work with pointer compare */ + end = g_sequence_get_end_iter (cache->sorted_by_line); + left = g_sequence_get_begin_iter (cache->sorted_by_line); + right = g_sequence_iter_prev (end); + + /* We already checked for empty above */ + g_assert (!g_sequence_iter_is_end (left)); + g_assert (!g_sequence_iter_is_end (right)); + + for (;;) + { + GtkTextLineDisplay *display; + guint lineno; + + if (left == right) + mid = left; + else + mid = g_sequence_range_get_midpoint (left, right); + + g_assert (mid != NULL); + g_assert (!g_sequence_iter_is_end (mid)); + + if (mid == end) + break; + + display = g_sequence_get (mid); + + g_assert (display != NULL); + g_assert (display->line != NULL); + g_assert (display->cache_iter != NULL); + + if (target == display->line) + return mid; + + if (right == left) + break; + + lineno = _gtk_text_line_get_number (display->line); + + if (target_lineno < lineno) + right = mid; + else if (target_lineno > lineno) + left = g_sequence_iter_next (mid); + else + g_assert_not_reached (); + } + + return NULL; +} + + +/* + * gtk_text_line_display_cache_invalidate_range: + * @cache: a GtkTextLineDisplayCache + * @begin: the starting text iter + * @end: the ending text iter + * + * Removes all GtkTextLineDisplay that fall between or including + * @begin and @end. + */ +void +gtk_text_line_display_cache_invalidate_range (GtkTextLineDisplayCache *cache, + GtkTextLayout *layout, + const GtkTextIter *begin, + const GtkTextIter *end, + gboolean cursors_only) +{ + GSequenceIter *begin_iter; + GSequenceIter *end_iter; + GSequenceIter *iter; + + g_assert (cache != NULL); + g_assert (layout != NULL); + g_assert (begin != NULL); + g_assert (end != NULL); + + STAT_INC (cache->inval_by_range); + + /* Short-circuit, is_empty() is O(1) */ + if (g_sequence_is_empty (cache->sorted_by_line)) + return; + + /* gtk_text_iter_order() preserving const */ + if (gtk_text_iter_compare (begin, end) > 0) + { + const GtkTextIter *tmp = begin; + end = begin; + begin = tmp; + } + + /* Common case, begin/end on same line. Just try to find the line by + * line number and invalidate it alone. + */ + if G_LIKELY (_gtk_text_iter_same_line (begin, end)) + { + begin_iter = find_iter_at_text_iter (cache, layout, begin); + + if (begin_iter != NULL) + { + GtkTextLineDisplay *display = g_sequence_get (begin_iter); + + g_assert (display != NULL); + g_assert (display->line != NULL); + + gtk_text_line_display_cache_invalidate_display (cache, display, cursors_only); + } + + return; + } + + /* Find GSequenceIter containing GtkTextLineDisplay that correspond + * to each of the text positions. + */ + begin_iter = find_iter_at_text_iter (cache, layout, begin); + end_iter = find_iter_at_text_iter (cache, layout, end); + + /* Short-circuit if we found nothing */ + if (begin_iter == NULL && end_iter == NULL) + return; + + /* If nothing matches the end, we need to walk to the end of our + * cached displays. We know there is a non-zero number of items + * in the sequence at this point, so we can iter_prev() safely. + */ + if (end_iter == NULL) + end_iter = g_sequence_iter_prev (g_sequence_get_end_iter (cache->sorted_by_line)); + + /* If nothing matched the begin, we need to walk starting from + * the first display we have cached. + */ + if (begin_iter == NULL) + begin_iter = g_sequence_get_begin_iter (cache->sorted_by_line); + + iter = begin_iter; + + for (;;) + { + GtkTextLineDisplay *display = g_sequence_get (iter); + GSequenceIter *next = g_sequence_iter_next (iter); + + gtk_text_line_display_cache_invalidate_display (cache, display, cursors_only); + + if (iter == end_iter) + break; + + iter = next; + } +} + +static GSequenceIter * +find_iter_at_at_y (GtkTextLineDisplayCache *cache, + GtkTextLayout *layout, + gint y) +{ + GtkTextBTree *btree; + GSequenceIter *left; + GSequenceIter *right; + GSequenceIter *mid; + GSequenceIter *end; + + g_assert (cache != NULL); + g_assert (layout != NULL); + + if (g_sequence_is_empty (cache->sorted_by_line)) + return NULL; + + btree = _gtk_text_buffer_get_btree (layout->buffer); + + /* Get some iters so we can work with pointer compare */ + end = g_sequence_get_end_iter (cache->sorted_by_line); + left = g_sequence_get_begin_iter (cache->sorted_by_line); + right = g_sequence_iter_prev (end); + + /* We already checked for empty above */ + g_assert (!g_sequence_iter_is_end (left)); + g_assert (!g_sequence_iter_is_end (right)); + + for (;;) + { + GtkTextLineDisplay *display; + gint cache_y; + gint cache_height; + + if (left == right) + mid = left; + else + mid = g_sequence_range_get_midpoint (left, right); + + g_assert (mid != NULL); + g_assert (!g_sequence_iter_is_end (mid)); + + if (mid == end) + break; + + display = g_sequence_get (mid); + + g_assert (display != NULL); + g_assert (display->line != NULL); + + cache_y = _gtk_text_btree_find_line_top (btree, display->line, layout); + cache_height = display->height; + + if (y >= cache_y && y <= (cache_y + cache_height)) + return mid; + + if (left == right) + break; + + if (y < cache_y) + right = mid; + else if (y > (cache_y + cache_height)) + left = g_sequence_iter_next (mid); + else + g_assert_not_reached (); + } + + return NULL; +} + +/* + * gtk_text_line_display_cache_invalidate_y_range: + * @cache: a GtkTextLineDisplayCache + * @y: the starting Y position + * @old_height: the height to invalidate + * @cursors_only: if only cursors should be invalidated + * + * Remove all GtkTextLineDisplay that fall into the range starting + * from the Y position to Y+Height. + */ +void +gtk_text_line_display_cache_invalidate_y_range (GtkTextLineDisplayCache *cache, + GtkTextLayout *layout, + gint y, + gint old_height, + gboolean cursors_only) +{ + GSequenceIter *iter; + GtkTextBTree *btree; + + g_assert (cache != NULL); + g_assert (layout != NULL); + + STAT_INC (cache->inval_by_y_range); + + btree = _gtk_text_buffer_get_btree (layout->buffer); + iter = find_iter_at_at_y (cache, layout, y); + + if (iter == NULL) + return; + + while (!g_sequence_iter_is_end (iter)) + { + GtkTextLineDisplay *display; + gint cache_y; + gint cache_height; + + display = g_sequence_get (iter); + iter = g_sequence_iter_next (iter); + + cache_y = _gtk_text_btree_find_line_top (btree, display->line, layout); + cache_height = display->height; + + if (cache_y + cache_height > y && cache_y < y + old_height) + { + gtk_text_line_display_cache_invalidate_display (cache, display, cursors_only); + + y += cache_height; + old_height -= cache_height; + + if (old_height > 0) + continue; + } + + break; + } +} + +void +gtk_text_line_display_cache_set_cursor_line (GtkTextLineDisplayCache *cache, + GtkTextLine *cursor_line) +{ + GtkTextLineDisplay *display; + + g_assert (cache != NULL); + + if (cursor_line == cache->cursor_line) + return; + + display = g_hash_table_lookup (cache->line_to_display, cache->cursor_line); + + if (display != NULL) + gtk_text_line_display_cache_invalidate_display (cache, display, FALSE); + + cache->cursor_line = cursor_line; + + display = g_hash_table_lookup (cache->line_to_display, cache->cursor_line); + + if (display != NULL) + gtk_text_line_display_cache_invalidate_display (cache, display, FALSE); +} |