/* Cache subsystem */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include "elinks.h" #include "bfu/dialog.h" #include "cache/cache.h" #include "cache/dialogs.h" #include "config/options.h" #include "main/main.h" #include "main/object.h" #include "network/connection.h" #include "protocol/protocol.h" #include "protocol/proxy.h" #include "protocol/uri.h" #ifdef CONFIG_SCRIPTING_SPIDERMONKEY # include "scripting/smjs/smjs.h" #endif #include "util/error.h" #include "util/memory.h" #include "util/string.h" #include "util/time.h" /* The list of cache entries */ static INIT_LIST_OF(struct cache_entry, cache_entries); static unsigned longlong cache_size; static int id_counter = 1; static void truncate_entry(struct cache_entry *cached, off_t offset, int final); /* Change 0 to 1 to enable cache debugging features (redirect stderr to a file). */ #if 0 #define DEBUG_CACHE #endif #ifdef DEBUG_CACHE #define dump_frag(frag, count) \ do { \ DBG(" [%d] f=%p offset=%" OFF_PRINT_FORMAT \ " length=%" OFF_PRINT_FORMAT \ " real_length=%" OFF_PRINT_FORMAT, \ count, frag, (off_print_T) frag->offset, \ (off_print_T) frag->length, (off_print_T) frag->real_length); \ } while (0) #define dump_frags(entry, comment) \ do { \ struct fragment *frag; \ int count = 0; \ \ DBG("%s: url=%s, cache_size=%li", comment, struri(entry->uri), cache_size); \ foreach (frag, entry->frag) \ dump_frag(frag, ++count); \ } while (0) #else #define dump_frags(entry, comment) #endif /* DEBUG_CACHE */ unsigned longlong get_cache_size(void) { return cache_size; } int get_cache_entry_count(void) { return list_size(&cache_entries); } int get_cache_entry_used_count(void) { struct cache_entry *cached; int i = 0; foreach (cached, cache_entries) i += is_object_used(cached); return i; } int get_cache_entry_loading_count(void) { struct cache_entry *cached; int i = 0; foreach (cached, cache_entries) i += is_entry_used(cached); return i; } struct cache_entry * find_in_cache(struct uri *uri) { struct cache_entry *cached; int proxy = (uri->protocol == PROTOCOL_PROXY); foreach (cached, cache_entries) { struct uri *c_uri; if (!cached->valid) continue; c_uri = proxy ? cached->proxy_uri : cached->uri; if (!compare_uri(c_uri, uri, URI_BASE)) continue; move_to_top_of_list(cache_entries, cached); return cached; } return NULL; } struct cache_entry * get_cache_entry(struct uri *uri) { struct cache_entry *cached = find_in_cache(uri); assertm(!uri->fragment, "Fragment in URI (%s)", struri(uri)); if (cached) return cached; shrink_memory(0); cached = mem_calloc(1, sizeof(*cached)); if (!cached) return NULL; cached->uri = get_proxied_uri(uri); if (!cached->uri) { mem_free(cached); return NULL; } cached->proxy_uri = get_proxy_uri(uri, NULL); if (!cached->proxy_uri) { done_uri(cached->uri); mem_free(cached); return NULL; } cached->incomplete = 1; cached->valid = 1; init_list(cached->frag); cached->cache_id = id_counter++; object_nolock(cached, "cache_entry"); /* Debugging purpose. */ cached->box_item = add_listbox_leaf(&cache_browser, NULL, cached); add_to_list(cache_entries, cached); return cached; } static int cache_entry_has_expired(struct cache_entry *cached) { timeval_T now; timeval_now(&now); return timeval_cmp(&cached->max_age, &now) <= 0; } struct cache_entry * get_validated_cache_entry(struct uri *uri, enum cache_mode cache_mode) { struct cache_entry *cached; /* We have to check if something should be reloaded */ if (cache_mode > CACHE_MODE_NORMAL) return NULL; /* We only consider complete entries */ cached = find_in_cache(uri); if (!cached || cached->incomplete) return NULL; /* A bit of a gray zone. Delete the entry if the it has the strictest * cache mode and we don't want the most aggressive mode or we have to * remove the redirect or the entry expired. Please enlighten me. * --jonas */ if ((cached->cache_mode == CACHE_MODE_NEVER && cache_mode != CACHE_MODE_ALWAYS) || (cached->redirect && !get_opt_bool("document.cache.cache_redirects", NULL)) || (cached->expire && cache_entry_has_expired(cached))) { if (!is_object_used(cached)) delete_cache_entry(cached); return NULL; } if (cached->cache_mode <= CACHE_MODE_CHECK_IF_MODIFIED && cache_mode <= CACHE_MODE_CHECK_IF_MODIFIED && (cached->last_modified || cached->etag) && get_opt_int("document.cache.revalidation_interval", NULL) >= 0) { if (cached->seconds + get_opt_int("document.cache.revalidation_interval", NULL) < time(NULL)) return NULL; } return cached; } int cache_entry_is_valid(struct cache_entry *cached) { struct cache_entry *valid_cached; foreach (valid_cached, cache_entries) { if (valid_cached == cached) return 1; } return 0; } struct cache_entry * follow_cached_redirects(struct cache_entry *cached) { int redirects = 0; while (cached) { if (!cached->redirect) { /* XXX: This is not quite true, but does that difference * matter here? */ return cached; } if (++redirects > MAX_REDIRECTS) break; cached = find_in_cache(cached->redirect); } return NULL; } struct cache_entry * get_redirected_cache_entry(struct uri *uri) { struct cache_entry *cached = find_in_cache(uri); return cached ? follow_cached_redirects(cached) : NULL; } static inline void enlarge_entry(struct cache_entry *cached, off_t size) { cached->data_size += size; assertm(cached->data_size >= 0, "cache entry data_size underflow: %ld", cached->data_size); if_assert_failed { cached->data_size = 0; } cache_size += size; assertm(cache_size >= 0, "cache_size underflow: %ld", cache_size); if_assert_failed { cache_size = 0; } } #define CACHE_PAD(x) (((x) | 0x3fff) + 1) /* One byte is reserved for data in struct fragment. */ #define FRAGSIZE(x) (sizeof(struct fragment) + (x) - 1) /* We store the fragments themselves in a private vault, safely separated from * the rest of memory structures. If we lived in the main libc memory pool, we * would trigger annoying pathological behaviour like artificially enlarging * the memory pool to 50M, then securing it with some stupid cookie record at * the top and then no matter how you flush the cache the data segment is still * 50M big. * * Cool, but we don't want that, so fragments (where the big data is stored) * live in their little mmap()ed worlds. There is some overhead, but if we * assume single fragment per cache entry and page size (mmap() allocation * granularity) 4096, for a squad of ten 1kb documents this amounts 30kb. * That's not *that* horrible when you realize that the freshmeat front page * takes 300kb in memory and we usually do not deal with documents so small * that max. 4kb overhead would be visible there. * * The alternative would be of course to manage an entire custom memory pool, * but that is feasible only when we are able to resize it efficiently. We * aren't, except on Linux. * * Of course for all this to really completely prevent the pathological cases, * we need to stuff the rendered documents in too, because they seem to amount * the major memory bursts. */ static struct fragment * frag_alloc(size_t size) { struct fragment *f = mem_mmap_alloc(FRAGSIZE(size)); if (!f) return NULL; memset(f, 0, FRAGSIZE(size)); return f; } static struct fragment * frag_realloc(struct fragment *f, size_t size) { return mem_mmap_realloc(f, FRAGSIZE(f->real_length), FRAGSIZE(size)); } static void frag_free(struct fragment *f) { mem_mmap_free(f, FRAGSIZE(f->real_length)); } /* Concatenate overlapping fragments. */ static void remove_overlaps(struct cache_entry *cached, struct fragment *f, int *trunc) { off_t f_end_offset = f->offset + f->length; /* Iterate thru all fragments we still overlap to. */ while (list_has_next(cached->frag, f) && f_end_offset > f->next->offset) { struct fragment *nf; off_t end_offset = f->next->offset + f->next->length; if (f_end_offset < end_offset) { /* We end before end of the following fragment, though. * So try to append overlapping part of that fragment * to us. */ nf = frag_realloc(f, end_offset - f->offset); if (nf) { nf->prev->next = nf; nf->next->prev = nf; f = nf; if (memcmp(f->data + f->next->offset - f->offset, f->next->data, f->offset + f->length - f->next->offset)) *trunc = 1; memcpy(f->data + f->length, f->next->data + f_end_offset - f->next->offset, end_offset - f_end_offset); enlarge_entry(cached, end_offset - f_end_offset); f->length = f->real_length = end_offset - f->offset; } } else { /* We will just discard this, it's complete subset of * our new fragment. */ if (memcmp(f->data + f->next->offset - f->offset, f->next->data, f->next->length)) *trunc = 1; } /* Remove the fragment, it influences our new one! */ nf = f->next; enlarge_entry(cached, -nf->length); del_from_list(nf); frag_free(nf); } } /* Note that this function is maybe overcommented, but I'm certainly not * unhappy from that. */ int add_fragment(struct cache_entry *cached, off_t offset, const char *data, ssize_t length) { struct fragment *f, *nf; int trunc = 0; off_t end_offset; if (!length) return 0; end_offset = offset + length; if (cached->length < end_offset) cached->length = end_offset; /* id marks each entry, and change each time it's modified, * used in HTML renderer. */ cached->cache_id = id_counter++; /* Possibly insert the new data in the middle of existing fragment. */ foreach (f, cached->frag) { int ret = 0; off_t f_end_offset = f->offset + f->length; /* No intersection? */ if (f->offset > offset) break; if (f_end_offset < offset) continue; if (end_offset > f_end_offset) { /* Overlap - we end further than original fragment. */ if (end_offset - f->offset <= f->real_length) { /* We fit here, so let's enlarge it by delta of * old and new end.. */ enlarge_entry(cached, end_offset - f_end_offset); /* ..and length is now total length. */ f->length = end_offset - f->offset; ret = 1; /* It was enlarged. */ } else { /* We will reduce fragment length only to the * starting non-interjecting size and add new * fragment directly after this one. */ f->length = offset - f->offset; f = f->next; break; } } /* else We are subset of original fragment. */ /* Copy the stuff over there. */ memcpy(f->data + offset - f->offset, data, length); remove_overlaps(cached, f, &trunc); /* We truncate the entry even if the data contents is the * same as what we have in the fragment, because that does * not mean that what is going to follow won't differ, This * is a serious problem when rendering HTML frame with onload * snippets - we "guess" the rest of the document here, * interpret the snippet, then it turns out in the real * document the snippet is different and we are in trouble. * * Debugging this took me about 1.5 day (really), the diff with * all the debugging print commands amounted about 20kb (gdb * wasn't much useful since it stalled the download, de facto * eliminating the bad behaviour). */ truncate_entry(cached, end_offset, 0); dump_frags(cached, "add_fragment"); return ret; } /* Make up new fragment. */ nf = frag_alloc(CACHE_PAD(length)); if (!nf) return -1; nf->offset = offset; nf->length = length; nf->real_length = CACHE_PAD(length); memcpy(nf->data, data, length); add_at_pos(f->prev, nf); enlarge_entry(cached, length); remove_overlaps(cached, nf, &trunc); if (trunc) truncate_entry(cached, end_offset, 0); dump_frags(cached, "add_fragment"); return 1; } /* Try to defragment the cache entry. Defragmentation will not be possible * if there is a gap in the fragments; if we have bytes 1-100 in one fragment * and bytes 201-300 in the second, we must leave those two fragments separate * so that the fragment for bytes 101-200 can later be inserted. However, * if we have the fragments for bytes 1-100, 101-200, and 201-300, we will * catenate them into one new fragment and replace the original fragments * with that new fragment. * * If are no fragments, return NULL. If there is no fragment with byte 1, * return NULL. Otherwise, return the first fragment, whether or not it was * possible to fully defragment the entry. */ struct fragment * get_cache_fragment(struct cache_entry *cached) { struct fragment *first_frag, *adj_frag, *frag, *new_frag; int new_frag_len; if (list_empty(cached->frag)) return NULL; first_frag = cached->frag.next; if (first_frag->offset) return NULL; /* Only one fragment so no defragmentation is needed */ if (list_is_singleton(cached->frag)) return first_frag; /* Find the first pair of fragments with a gap in between. Only * fragments up to the first gap can be defragmented. */ for (adj_frag = first_frag->next; adj_frag != (void *) &cached->frag; adj_frag = adj_frag->next) { long gap = adj_frag->offset - (adj_frag->prev->offset + adj_frag->prev->length); if (gap > 0) break; if (gap == 0) continue; INTERNAL("fragments overlap"); return NULL; } /* There is a gap between the first two fragments, so we can't * defragment anything. */ if (adj_frag == first_frag->next) return first_frag; /* Calculate the length of the defragmented fragment. */ for (new_frag_len = 0, frag = first_frag; frag != adj_frag; frag = frag->next) new_frag_len += frag->length; /* XXX: If the defragmentation fails because of allocation failure, * fall back to return the first fragment and pretend all is well. */ /* FIXME: Is this terribly brain-dead? It corresponds to the semantic of * the code this extended version of the old defrag_entry() is supposed * to replace. --jonas */ new_frag = frag_alloc(new_frag_len); if (!new_frag) return first_frag->length ? first_frag : NULL; new_frag->length = new_frag_len; new_frag->real_length = new_frag_len; for (new_frag_len = 0, frag = first_frag; frag != adj_frag; frag = frag->next) { struct fragment *tmp = frag; memcpy(new_frag->data + new_frag_len, frag->data, frag->length); new_frag_len += frag->length; frag = frag->prev; del_from_list(tmp); frag_free(tmp); } add_to_list(cached->frag, new_frag); dump_frags(cached, "get_cache_fragment"); return new_frag; } static void delete_fragment(struct cache_entry *cached, struct fragment *f) { while ((void *) f != &cached->frag) { struct fragment *tmp = f->next; enlarge_entry(cached, -f->length); del_from_list(f); frag_free(f); f = tmp; } } static void truncate_entry(struct cache_entry *cached, off_t offset, int final) { struct fragment *f; if (cached->length > offset) { cached->length = offset; cached->incomplete = 1; } foreach (f, cached->frag) { off_t size = offset - f->offset; /* XXX: is zero length fragment really legal here ? --Zas */ assert(f->length >= 0); if (size >= f->length) continue; if (size > 0) { enlarge_entry(cached, -(f->length - size)); f->length = size; if (final) { struct fragment *nf; nf = frag_realloc(f, f->length); if (nf) { nf->next->prev = nf; nf->prev->next = nf; f = nf; f->real_length = f->length; } } f = f->next; } delete_fragment(cached, f); dump_frags(cached, "truncate_entry"); return; } } void free_entry_to(struct cache_entry *cached, off_t offset) { struct fragment *f; foreach (f, cached->frag) { if (f->offset + f->length <= offset) { struct fragment *tmp = f; enlarge_entry(cached, -f->length); f = f->prev; del_from_list(tmp); frag_free(tmp); } else if (f->offset < offset) { off_t size = offset - f->offset; enlarge_entry(cached, -size); f->length -= size; memmove(f->data, f->data + size, f->length); f->offset = offset; } else break; } } void delete_entry_content(struct cache_entry *cached) { enlarge_entry(cached, -cached->data_size); while (cached->frag.next != (void *) &cached->frag) { struct fragment *f = cached->frag.next; del_from_list(f); frag_free(f); } cached->cache_id = id_counter++; cached->length = 0; cached->incomplete = 1; mem_free_set(&cached->last_modified, NULL); mem_free_set(&cached->etag, NULL); } static void done_cache_entry(struct cache_entry *cached) { assertm(!is_object_used(cached), "deleting locked cache entry"); assertm(!is_entry_used(cached), "deleting loading cache entry"); delete_entry_content(cached); if (cached->box_item) done_listbox_item(&cache_browser, cached->box_item); #ifdef CONFIG_SCRIPTING_SPIDERMONKEY if (cached->jsobject) smjs_detach_cache_entry_object(cached); #endif if (cached->uri) done_uri(cached->uri); if (cached->proxy_uri) done_uri(cached->proxy_uri); if (cached->redirect) done_uri(cached->redirect); mem_free_if(cached->head); mem_free_if(cached->content_type); mem_free_if(cached->last_modified); mem_free_if(cached->ssl_info); mem_free_if(cached->encoding_info); mem_free_if(cached->etag); mem_free(cached); } void delete_cache_entry(struct cache_entry *cached) { del_from_list(cached); done_cache_entry(cached); } void normalize_cache_entry(struct cache_entry *cached, off_t truncate_length) { if (truncate_length < 0) return; truncate_entry(cached, truncate_length, 1); cached->incomplete = 0; cached->preformatted = 0; cached->seconds = time(NULL); } struct uri * redirect_cache(struct cache_entry *cached, char *location, int get, int incomplete) { char *uristring; /* XXX: I am a little puzzled whether we should only use the cache * entry's URI if it is valid. Hopefully always using it won't hurt. * Currently we handle direction redirects where "/" should be appended * special dunno if join_urls() could be made to handle that. * --jonas */ /* XXX: We are assuming here that incomplete will only be zero when * doing these fake redirects which only purpose is to add an ending * slash *cough* dirseparator to the end of the URI. */ if (incomplete == 0 && dir_sep(location[0]) && location[1] == 0) { /* To be sure use get_uri_string() to get rid of post data */ uristring = get_uri_string(cached->uri, URI_ORIGINAL); if (uristring) add_to_strn(&uristring, location); } else { uristring = join_urls(cached->uri, location); } if (!uristring) return NULL; #ifdef CONFIG_SCRIPTING { static int follow_url_event_id = EVENT_NONE; set_event_id(follow_url_event_id, "follow-url"); trigger_event(follow_url_event_id, &uristring, NULL); if (!uristring || !*uristring) { mem_free_if(uristring); return NULL; } /* FIXME: Compare if uristring and struri(uri) are equal */ /* FIXME: When uri->post will no longer be an encoded string (but * hopefully some refcounted object) we will have to assign the post * data object to the translated URI. */ } #endif /* Only add the post data if the redirect should not use GET method. * This is tied to the HTTP handling of the 303 and (if the * protocol.http.bugs.broken_302_redirect is enabled) the 302 status * code handling. */ if (cached->uri->post && !cached->redirect_get && !get) { /* XXX: Add POST_CHAR and post data assuming URI components * belong to one string. */ /* To be certain we don't append post data twice in some * conditions... --Zas */ assert(!strchr((const char *)uristring, POST_CHAR)); add_to_strn(&uristring, cached->uri->post - 1); } if (cached->redirect) done_uri(cached->redirect); cached->redirect = get_uri(uristring, URI_NONE); cached->redirect_get = get; if (incomplete >= 0) cached->incomplete = incomplete; mem_free(uristring); return cached->redirect; } void garbage_collection(int whole) { struct cache_entry *cached; /* We recompute cache_size when scanning cache entries, to ensure * consistency. */ unsigned longlong old_cache_size = 0; /* The maximal cache size tolerated by user. Note that this is only * size of the "just stored" unused cache entries, used cache entries * are not counted to that. */ unsigned longlong opt_cache_size = get_opt_long("document.cache.memory.size", NULL); /* The low-treshold cache size. Basically, when the cache size is * higher than opt_cache_size, we free the cache so that there is no * more than this value in the cache anymore. This is to make sure we * aren't cleaning cache too frequently when working with a lot of * small cache entries but rather free more and then let it grow a * little more as well. */ unsigned longlong gc_cache_size = opt_cache_size * MEMORY_CACHE_GC_PERCENT / 100; /* The cache size we aim to reach. */ unsigned longlong new_cache_size = cache_size; #ifdef DEBUG_CACHE /* Whether we've hit an used (unfreeable) entry when collecting * garbage. */ int obstacle_entry = 0; #endif #ifdef DEBUG_CACHE DBG("gc whole=%d opt_cache_size=%ld gc_cache_size=%ld", whole, opt_cache_size,gc_cache_size); #endif if (!whole && cache_size <= opt_cache_size) return; /* Scanning cache, pass #1: * Weed out the used cache entries from @new_cache_size, so that we * will work only with the unused entries from then on. Also ensure * that @cache_size is in sync. */ foreach (cached, cache_entries) { old_cache_size += cached->data_size; if (!is_object_used(cached) && !is_entry_used(cached)) continue; assertm(new_cache_size >= cached->data_size, "cache_size (%ld) underflow: subtracting %ld from %ld", cache_size, cached->data_size, new_cache_size); new_cache_size -= cached->data_size; if_assert_failed { new_cache_size = 0; } } assertm(old_cache_size == cache_size, "cache_size out of sync: %ld != (actual) %ld", cache_size, old_cache_size); if_assert_failed { cache_size = old_cache_size; } if (!whole && new_cache_size <= opt_cache_size) return; /* Scanning cache, pass #2: * Mark potential targets for destruction, from the oldest to the * newest. */ foreachback (cached, cache_entries) { /* We would have shrinked enough already? */ if (!whole && new_cache_size <= gc_cache_size) goto shrinked_enough; /* Skip used cache entries. */ if (is_object_used(cached) || is_entry_used(cached)) { #ifdef DEBUG_CACHE obstacle_entry = 1; #endif cached->gc_target = 0; continue; } /* FIXME: Optionally take cached->max_age into consideration, * but that will probably complicate things too much. We'd have * to sort entries so prioritize removing the oldest entries. */ assertm(new_cache_size >= cached->data_size, "cache_size (%ld) underflow: subtracting %ld from %ld", cache_size, cached->data_size, new_cache_size); /* Mark me for destruction, sir. */ cached->gc_target = 1; new_cache_size -= cached->data_size; if_assert_failed { new_cache_size = 0; } } /* If we'd free the whole cache... */ assertm(new_cache_size == 0, "cache_size (%ld) overflow: %ld", cache_size, new_cache_size); if_assert_failed { new_cache_size = 0; } shrinked_enough: /* Now turn around and start walking in the opposite direction. */ cached = cached->next; /* Something is strange when we decided all is ok before dropping any * cache entry. */ if ((void *) cached == &cache_entries) return; if (!whole) { struct cache_entry *entry; /* Scanning cache, pass #3: * Walk back in the cache and unmark the cache entries which * could still fit into the cache. */ /* This makes sense when the newest entry is HUGE and after it, * there's just plenty of tiny entries. By this point, all the * tiny entries would be marked for deletion even though it'd * be enough to free the huge entry. This actually fixes that * situation. */ for (entry = cached; (void *) entry != &cache_entries; entry = entry->next) { unsigned longlong newer_cache_size = new_cache_size + entry->data_size; if (newer_cache_size > gc_cache_size) continue; new_cache_size = newer_cache_size; entry->gc_target = 0; } } /* Scanning cache, pass #4: * Destroy the marked entries. So sad, but that's life, bro'. */ for (; (void *) cached != &cache_entries; ) { cached = cached->next; if (cached->prev->gc_target) delete_cache_entry(cached->prev); } #ifdef DEBUG_CACHE if ((whole || !obstacle_entry) && cache_size > gc_cache_size) { DBG("garbage collection doesn't work, cache size %ld > %ld, " "document.cache.memory.size set to: %ld bytes", cache_size, gc_cache_size, get_opt_long("document.cache.memory.size", NULL)); } #endif }