Branch: Tag:

2016-10-18

2016-10-18 12:26:57 by Henrik Grubbström (Grubba) <grubba@grubba.org>

RAM Cache: Use an ADT.Heap instead of a multiset.

This reduces the performance penalty when the priority of a
CacheEntry is changed in a server with a huge amount of entries.

Also fixes the missing cost update on hit for the CM_GDS_Time cache.

Note that there may still be some thread races lurking.

Fixes [bug 7727 (#7727)].

1:   // This file is part of Roxen WebServer.   // Copyright © 1996 - 2009, Roxen IS. - // $Id: cache.pike,v 1.150 2012/03/05 13:55:10 grubba Exp $ + // $Id$      // FIXME: Add argcache, imagecache & protcache   
501:   {    inherit CacheManager;    +  //! A heap of all [CacheEntry]s in priority order sorted by @[CacheEntry.`<]. +  ADT.Heap priority_queue = ADT.Heap(); +  +  //! Wrapper so that we can get back to @[CacheEntry] from +  //! @[ADT.Heap.Element] easily. +  protected class HeapElement +  { +  inherit ADT.Heap.Element; +  +  //! Return the @[CacheEntry] that contains this @[HeapElement]. +  object(CacheEntry) cache_entry() { return [object(CacheEntry)](mixed)this; } +  } +     class CacheEntry    //!    { -  +  private local inherit HeapElement; +     inherit global::CacheEntry;       int|float value;    //! The value of the entry, i.e. v(p) in the class description.    -  int|float pval; -  //! The priority value for the entry, defining its position in -  //! @[priority_list]. Must not change for an entry that is -  //! currently a member of @[priority_list]. +  //! @decl int|float pval; +  //! The priority value for the entry, effecting its position in +  //! @[priority_queue]. +  //! +  //! If the element is a member of @[priority_queue] it will +  //! automatically adjust its position when this value is changed.    -  string cache_name; -  //! Need the cache name to find the entry, since @[priority_list] -  //! is global. -  -  protected int `< (CacheEntry other) +  int|float `pval()    { -  return pval < other->pval; +  return HeapElement::value;    } -  +  void `pval=(int|float val) +  { +  Element::value = val; +  if (HeapElement::pos != -1) { +  priority_queue->adjust(HeapElement::this); +  } +  }    -  +  //! Return the @[HeapElement] corresponding to this @[CacheEntry]. +  HeapElement element() { return HeapElement::this; } +  +  string cache_name; +  //! Need the cache name to find the entry, since @[priority_queue] +  //! is global. +     protected string _sprintf (int flag)    {    return flag == 'O' &&
531:    }    }    -  multiset(CacheEntry) priority_list = (<>); -  //! A list of all entries in priority order, by using the multiset -  //! builtin sorting through @[CacheEntry.`<]. -  +     protected int max_used_pval;    // Used to detect when the entry pval's get too big to warrant a    // reset.
549:    // Int.NATIVE_MAX when that state is reached.      #ifdef CACHE_DEBUG -  protected void debug_check_priority_list() +  protected void debug_check_priority_queue()    // Assumes no concurrent access - run inside _disable_threads.    { -  werror ("Checking priority_list with %d entries.\n", -  sizeof (priority_list)); -  CacheEntry prev; -  foreach (priority_list; CacheEntry entry;) { -  if (!prev) -  prev = entry; -  else if (prev >= entry) -  error ("Found cache entries in wrong order: %O vs %O in %O\n", -  prev, entry, priority_list); -  if (intp (entry->pval) && entry->pval > max_used_pval) -  error ("Found %O with pval higher than max %O.\n", -  entry, max_used_pval); +  werror ("Checking priority_queue with %d entries.\n", +  sizeof(priority_queue)); +  if (priority_queue->verify_heap) { +  priority_queue->verify_heap();    }    }   #endif
582:    local protected int|float calc_pval (CacheEntry entry)    {    int|float pval; -  if (CacheEntry lowest = get_iterator (priority_list)->index()) { -  int|float l = lowest->pval, v = entry->value; +  if (HeapElement lowest = priority_queue->low_peek()) { +  int|float l = lowest->value, v = entry->value;    pval = l + v;       if (floatp (v)) {
612:    account_hit (cache_name, entry);    int|float pval = calc_pval (entry);    -  // Note: Won't have the interpreter lock during the operation below. The -  // tricky situation occur if the same entry is processed by another -  // got_hit, where we might get it inserted in more than one place and one -  // of them will be inconsistent with the pval value. evict() has a -  // consistency check that should detect and correct this eventually. The -  // worst thing that happens is that the eviction order is more or less -  // wrong until then. -  priority_list[entry] = 0; +  // NB: The priority queue is automatically adjusted on +  // change of pval.    entry->pval = pval; -  priority_list[entry] = 1; +     }       int add_entry (string cache_name, CacheEntry entry,
634:    if (!low_add_entry (cache_name, entry)) return 0;       entry->pval = calc_pval (entry); -  priority_list[entry] = 1; +  priority_queue->push(entry->element());       if (size > size_limit) evict (size_limit);    return 1;
642:       int remove_entry (string cache_name, CacheEntry entry)    { -  priority_list[entry] = 0; +  priority_queue->remove(entry->element());    return low_remove_entry (cache_name, entry);    }       void evict (int max_size)    { -  object threads_disabled; +  // FIXME: Use a proper mutes instead. +  object threads_disabled = _disable_threads(); +  while ((size > max_size) && sizeof(priority_queue)) { +  // NB: Use low_peek() + remove() since low_pop() doesn't exist. +  CacheEntry entry = priority_queue->low_peek()->cache_entry(); +  priority_queue->remove(entry->element());    -  while (size > max_size) { -  CacheEntry entry = get_iterator (priority_list)->index(); -  if (!entry) break; -  -  if (!priority_list[entry]) { -  // The order in the list has become inconsistent with the pval values. -  // It might happen due to the race in got_hit(), or it might just be -  // interference from a concurrent evict() call. -  if (!threads_disabled) -  // Take a hefty lock and retry, to rule out the second case. -  threads_disabled = _disable_threads(); -  else { -  // Got the lock so it can't be a race, i.e. the priority_list order -  // is funky. Have to rebuild it without interventions. - #ifdef CACHE_DEBUG -  debug_check_priority_list(); - #endif -  report_warning ("Warning: Recovering from race inconsistency " -  "in %O->priority_list (on %O).\n", this, entry); -  priority_list = (<>); -  foreach (lookup; string cache_name; mapping(mixed:CacheEntry) lm) -  foreach (lm;; CacheEntry entry) -  priority_list[entry] = 1; -  } -  continue; -  } -  -  priority_list[entry] = 0; -  +     MORE_CACHE_WERR ("evict: Size %db > %db - evicting %O / %O.\n",    size, max_size, entry->cache_name, entry);       low_remove_entry (entry->cache_name, entry);    } -  -  threads_disabled = 0; +     }       int manager_size_overhead()    { -  return Pike.count_memory (-1, priority_list) + ::manager_size_overhead(); +  return Pike.count_memory (-1, priority_queue) + ::manager_size_overhead();    }       void after_gc()    {    if (max_used_pval > Int.NATIVE_MAX / 2) {    int|float pval_base; -  if (CacheEntry lowest = get_iterator (priority_list)->index()) -  pval_base = lowest->pval; +  if (HeapElement lowest = priority_queue->low_peek()) +  pval_base = lowest->value;    else    return;   
716:    cs->size = 0;    }    } -  priority_list = (<>); +  priority_queue = ADT.Heap();    size = 0;    max_used_pval = 0;    threads_disabled = 0;
727:    if (CacheStats cs = stats[cache_name])    if (mapping(mixed:CacheEntry) new_lm = lookup[cache_name])    foreach (old_lm; mixed key; CacheEntry entry) { +  entry->element()->pos = -1;    int|float pval = entry->pval -= pval_base;    if (!new_lm[key]) {    // Relying on the interpreter lock here.    new_lm[key] = entry;    -  priority_list[entry] = 1; +  priority_queue->push(entry->element());    if (pval > max_pval) max_pval = pval;       cs->count++;
742:    }      #ifdef CACHE_DEBUG -  debug_check_priority_list(); +  debug_check_priority_queue();   #endif       if (intp (max_pval))
861:       void got_hit (string cache_name, CacheEntry entry, mapping cache_context)    { -  account_hit (cache_name, entry); +  ::got_hit(cache_name, entry, cache_context);    // It shouldn't be necessary to record the start time for cache    // hits, but do it anyway for now since there are caches that on    // cache hits extend the entries with more data.