/* -*- 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 . */ #include "config.h" #include "gtktextbtreeprivate.h" #include "gtktextbufferprivate.h" #include "gtktextiterprivate.h" #include "gtktextlinedisplaycacheprivate.h" #include "gtkprivate.h" #define DEFAULT_MRU_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; guint mru_size; #if DEBUG_LINE_DISPLAY_CACHE guint log_source; int hits; int misses; int inval; int inval_cursors; int inval_by_line; int inval_by_range; int 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); ret->mru_size = DEFAULT_MRU_SIZE; #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 gtk_text_line_display_cache_invalidate (cache); 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_static_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; int 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 > cache->mru_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); g_clear_pointer (&display->node, gsk_render_node_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); if (!size_only && display->has_children) gtk_text_layout_update_children (layout, 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); if (display->has_children) gtk_text_layout_update_children (layout, 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, int 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; int cache_y; int 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 old height of the range * @new_height: the new height of the range * @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, int y, int old_height, int new_height, gboolean cursors_only) { GSequenceIter *iter; GtkTextBTree *btree; g_assert (cache != NULL); g_assert (layout != NULL); STAT_INC (cache->inval_by_y_range); /* A common pattern is to invalidate the whole buffer using y==0 and * old_height==new_height. So special case that instead of walking through * each display item one at a time. */ if (y < 0 || (y == 0 && old_height == new_height)) { gtk_text_line_display_cache_invalidate (cache); return; } 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; int cache_y; int 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); } void gtk_text_line_display_cache_set_mru_size (GtkTextLineDisplayCache *cache, guint mru_size) { GtkTextLineDisplay *display; g_assert (cache != NULL); if (mru_size == 0) mru_size = DEFAULT_MRU_SIZE; if (mru_size != cache->mru_size) { cache->mru_size = mru_size; while (cache->mru.length > cache->mru_size) { display = g_queue_peek_tail (&cache->mru); gtk_text_line_display_cache_invalidate_display (cache, display, FALSE); } } }