835c6c2001-06-17Martin Nilsson // This file is part of Roxen WebServer.
f41b982009-05-07Martin Stjernholm // Copyright © 1996 - 2009, Roxen IS.
e451b02009-11-18Martin Stjernholm // $Id: cache.pike,v 1.109 2009/11/18 17:43:43 mast Exp $
a565891999-12-27Martin Nilsson 
b796b51998-11-18Per Hedbor #include <roxen.h>
b1fca01996-11-12Per Hedbor #include <config.h>
1353182009-11-12Martin Stjernholm #ifdef MORE_CACHE_DEBUG # define MORE_CACHE_WERR(X...) report_debug("CACHE: "+X) # undef CACHE_DEBUG # define CACHE_DEBUG #else # define MORE_CACHE_WERR(X...) 0 #endif #ifdef CACHE_DEBUG # define CACHE_WERR(X...) report_debug("CACHE: "+X) #else # define CACHE_WERR(X...) 0 #endif #ifdef NEW_RAM_CACHE // FIXME: Statistics from the gc for invalid cache entry ratio.
1f6e162009-11-18Martin Stjernholm constant startup_cache_size = 1024 * 1024; // Cache size per manager to use before we've read the config setting.
1353182009-11-12Martin Stjernholm  class CacheEntry (mixed key, mixed data) //! Base class for cache entries. { // FIXME: Consider unifying this with CacheKey. But in that case we // need to ensure "interpreter lock" atomicity below. int size; //! The size of this cache entry, as measured by @[Pike.count_memory].
7f93b62009-11-17Martin Stjernholm #ifdef DEBUG_CACHE_SIZES int cmp_size; // Size without counting strings. Used to compare the size between // cache_set and cache_clean. Strings are excluded since they might // get or lose unrelated refs in the time between which would make // the comparison unreliable. This might make us miss significant // strings though, but it's hard to get around it. #endif
1353182009-11-12Martin Stjernholm  int timeout; //! Unix time when the entry times out, or zero if there's no //! timeout. //! @decl int|float cost; //! //! The creation cost for the entry, according to the metric used by //! the cache manager (provided it implements cost).
7f93b62009-11-17Martin Stjernholm  protected string format_key() { if (stringp (key)) {
4787532009-11-17Martin Stjernholm  if (sizeof (key) > 40) return sprintf ("%q...", key[..39 - sizeof ("...")]);
7f93b62009-11-17Martin Stjernholm  else return sprintf ("%q", key); } else if (objectp (key)) return sprintf ("%O", key); else return sprintf ("%t", key); }
1353182009-11-12Martin Stjernholm  protected string _sprintf (int flag) {
7f93b62009-11-17Martin Stjernholm  return flag == 'O' && sprintf ("CacheEntry(%s, %db)", format_key(), size);
1353182009-11-12Martin Stjernholm  } } class CacheStats //! Holds statistics for each named cache. { int count; //! The number of entries in the cache. int size; //! The sum of @[CacheEntry.size] for all cache entries in the cache. int hits, misses; //! Plain counts of cache hits and misses. #ifdef RAMCACHE_STATS int byte_hits, byte_misses; //! Byte hit and miss count. Note that @[byte_misses] is determined //! when a new entry is added - it will not include when no new //! entry was created after a cache miss. int|float cost_hits, cost_misses; //! Hit and miss count according to the cache manager cost metric. //! Note that @[cost_misses] is determined when a new entry is added //! - it will not include when no new entry was created after a //! cache miss. #endif protected string _sprintf (int flag) { return flag == 'O' && sprintf ("CacheStats(%d, %dk)", count, size / 1024); } } class CacheManager //! A cache manager handles one or more caches, applying the same //! eviction policy and the same size limit on all of them. I.e. it's //! practically one cache, and the named caches inside only act as //! separate name spaces. { //! @decl constant string name; //! //! A unique name to identify the manager. It is also used as //! display name. //! @decl constant string doc; //! //! A description of the manager and its eviction policy in html. int total_size_limit; //! Maximum allowed size including the cache manager overhead. int size; //! The sum of @[CacheStats.size] for all named caches. int size_limit; //! Maximum allowed size for cache entries - @[size] should never //! greater than this. This is a cached value calculated from //! @[total_size_limit] on regular intervals. mapping(string:mapping(mixed:CacheEntry)) lookup = ([]); //! Lookup mapping on the form @expr{(["cache_name": ([key: data])])@}. //! //! For functions in this class, a cache submapping does not exist //! only due to race, so the cache should just be ignored. mapping(string:CacheStats) stats = ([]); //! Statistics for the named caches managed by this object. //! //! For functions in this class, a @[CacheStats] object does not //! exist only due to race, so the cache should just be ignored. //! @decl program CacheEntry; //! //! The manager-specific class to use to create @[CacheEntry] objects. void got_miss (string cache_name, mixed key, mapping cache_context); //! Called when @[cache_lookup] records a cache miss. protected void account_miss (string cache_name) { if (CacheStats cs = stats[cache_name]) cs->misses++; }
c872b92009-11-16Martin Stjernholm  void got_hit (string cache_name, CacheEntry entry, mapping cache_context);
1353182009-11-12Martin Stjernholm  //! Called when @[cache_lookup] records a cache hit. protected void account_hit (string cache_name, CacheEntry entry) { if (CacheStats cs = stats[cache_name]) { cs->hits++; #ifdef RAMCACHE_STATS cs->byte_hits += entry->size; cs->cost_hits += entry->cost; #endif } } int add_entry (string cache_name, CacheEntry entry, int old_entry, mapping cache_context); //! Called to add an entry to the cache. Should also evict entries //! as necessary to keep @expr{@[size] <= @[size_limit]@}. //! //! If @[old_entry] is set then the entry hasn't been created from //! scratch, e.g. there is no prior @[got_miss] call. Returns 1 if //! the entry got added to the cache. Returns 0 if the function //! chose to evict it immediately or if the cache has disappeared.
4787532009-11-17Martin Stjernholm  private void account_remove_entry (string cache_name, CacheEntry entry) { if (CacheStats cs = stats[cache_name]) { cs->count--; cs->size -= entry->size; ASSERT_IF_DEBUG (cs->size /*%O*/ >= 0, cs->size); ASSERT_IF_DEBUG (cs->count /*%O*/ >= 0, cs->count); } size -= entry->size; ASSERT_IF_DEBUG (size /*%O*/ >= 0, size); }
1353182009-11-12Martin Stjernholm  protected int low_add_entry (string cache_name, CacheEntry entry) { ASSERT_IF_DEBUG (entry->size /*%O*/, entry->size); if (CacheStats cs = stats[cache_name]) { #ifdef RAMCACHE_STATS // Assume that the addition of the new entry came about due to a // cache miss. cs->byte_misses += entry->size; cs->cost_misses += entry->cost; #endif if (mapping(mixed:CacheEntry) lm = lookup[cache_name]) { // vvv Relying on the interpreter lock from here. CacheEntry old_entry = lm[entry->key]; lm[entry->key] = entry; // ^^^ Relying on the interpreter lock to here.
4787532009-11-17Martin Stjernholm  if (old_entry) { account_remove_entry (cache_name, old_entry);
1353182009-11-12Martin Stjernholm  remove_entry (cache_name, old_entry);
4787532009-11-17Martin Stjernholm  }
1353182009-11-12Martin Stjernholm  cs->count++; cs->size += entry->size; size += entry->size; if (!(cs->misses & 0x3fff)) // = 16383 // Approximate the number of misses as the number of new entries // added to the cache. That should be a suitable unit to use for the // update interval since the manager overhead should be linear to // the number of cached entries. update_size_limit(); } return 1; } return 0; } int remove_entry (string cache_name, CacheEntry entry);
4787532009-11-17Martin Stjernholm  //! Called to delete an entry from the cache. Should use //! @[low_remove_entry] to do the atomic removal. Must ensure the //! entry is removed from any extra data structures, regardless //! whether it's already gone from the @[lookup] mapping or not. //! Returns the return value from @[low_remove_entry].
1353182009-11-12Martin Stjernholm  protected int low_remove_entry (string cache_name, CacheEntry entry)
4787532009-11-17Martin Stjernholm  //! Returns 1 if the entry got removed from the cache, or 0 if it //! wasn't found in the cache or if the cache has disappeared.
1353182009-11-12Martin Stjernholm  { if (mapping(mixed:CacheEntry) lm = lookup[cache_name])
4787532009-11-17Martin Stjernholm  if (lm[entry->key] == entry) { // Relying on the interpreter here. m_delete (lm, entry->key); account_remove_entry (cache_name, entry);
1353182009-11-12Martin Stjernholm  return 1; } return 0; } void evict (int max_size); //! Called to evict entries until @expr{@[size] <= @[max_size]@}. void after_gc() {} //! Called from the periodic GC, after stale and invalid entries //! have been removed from the cache. int manager_size_overhead() //! Returns the size consumed by the manager itself, excluding the //! cache entries. { return (Pike.count_memory (-1, this) + Pike.count_memory (0, stats) + Pike.count_memory ((["block_objects": 1]), lookup)); } void update_size_limit() { MORE_CACHE_WERR ("%O: update_size_limit\n", this); int mgr_oh = manager_size_overhead(); size_limit = max (0, total_size_limit - mgr_oh); if (size > size_limit) { CACHE_WERR ("%O: Evicting %db " "(entry size limit %db, manager overhead %db, total %db)\n", this, size - size_limit, size_limit, mgr_oh, total_size_limit); evict (size_limit); } } string format_cost (int|float cost) {return "-";} //! Function to format a cost measurement for display in the status //! page. protected void create (int total_size_limit) { this_program::total_size_limit = total_size_limit; update_size_limit(); } protected string _sprintf (int flag) { return flag == 'O' && sprintf ("CacheManager(%s: %dk/%dk)", this->name || "-", size / 1024, size_limit / 1024); } }
68dfb92009-11-12Martin Stjernholm #if 0
1353182009-11-12Martin Stjernholm class CM_Random { inherit CacheManager; constant name = "Random"; constant doc = #"\ This is a very simple cache manager that just evicts entries from the cache at random. The only upside with it is that the cache management overhead is minimal."; // Workaround since "constant CacheEntry = global::CacheEntry;" // currently causes segfault in 7.8. constant CacheEntry = global::CacheEntry; void got_miss (string cache_name, mixed key, mapping cache_context) { account_miss (cache_name); }
c872b92009-11-16Martin Stjernholm  void got_hit (string cache_name, CacheEntry entry, mapping cache_context)
1353182009-11-12Martin Stjernholm  { account_hit (cache_name, entry); } int add_entry (string cache_name, CacheEntry entry, int old_entry, mapping cache_context) { int res = low_add_entry (cache_name, entry); if (size > size_limit) evict (size_limit); return res; } int remove_entry (string cache_name, CacheEntry entry) { return low_remove_entry (cache_name, entry); } void evict (int max_size) { while (size > max_size) { if (!sizeof (lookup)) break; // Relying on the interpreter lock here. string cache_name = random (lookup)[0]; if (mapping(mixed:CacheEntry) lm = lookup[cache_name]) { if (sizeof (lm)) { // Relying on the interpreter lock here. CacheEntry entry = random (lm)[1]; MORE_CACHE_WERR ("%s: Size is %d - evicting %O.\n", cache_name, size, entry); low_remove_entry (cache_name, entry); } else m_delete (lookup, cache_name); } } } }
1f6e162009-11-18Martin Stjernholm protected CM_Random cm_random = CM_Random (startup_cache_size);
68dfb92009-11-12Martin Stjernholm #endif
1353182009-11-12Martin Stjernholm  class CM_GreedyDual //! Base class for cache managers that works with some variant of the //! GreedyDual algorithm (see e.g. Cao and Irani, "Cost-Aware WWW //! Proxy Caching Algorithms" in "Proceedings of the 1997 USENIX //! Symposium on Internet Technology and Systems"): //! //! A priority queue is maintained, which contains all entries ordered //! according to a priority value. The entry with the lowest priority //! is always chosen for eviction. When a new entry p is added, and //! each time it is hit afterwards, the priority is set to v(p) + L, //! where v(p) is p's value according to some algorithm-specific //! definition, and L is the lowest priority value in the cache. This //! means that the priority values constantly increases, so that old //! entries without hits eventually gets evicted regardless of their //! initial v(p). { inherit CacheManager; class CacheEntry { 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]. string cache_name; //! Need the cache name to find the entry, since @[priority_list] //! is global. protected int `< (CacheEntry other) { return pval < other->pval; } protected string _sprintf (int flag) {
7f93b62009-11-17Martin Stjernholm  return flag == 'O' && sprintf ("CacheEntry(%s, %db, %O)", format_key(), size, value);
1353182009-11-12Martin Stjernholm  } } multiset(CacheEntry) priority_list = (<>); //! A list of all entries in priority order, by using the multiset //! builtin sorting through @[CacheEntry.`<].
397d522009-11-17Martin Stjernholm  protected int max_used_pval; // Used to detect when the entry pval's get too big to warrant a // reset. // // For integers, this is the maximum used pval and we reset at the // next gc when it gets over Int.NATIVE_MAX/2 (to have some spare // room for the gc delay). // // For floats, we reset whenever L is so big that less than 8 // significant bits remains when v(p) is added to it. In that case // max_used_pval only works as a flag, and we set it to // Int.NATIVE_MAX when that state is reached.
1353182009-11-12Martin Stjernholm  int|float calc_value (string cache_name, CacheEntry entry, int old_entry, mapping cache_context); //! Called to calculate the value for @[entry], which gets assigned //! to the @expr{value@} variable. Arguments are the same as to //! @[add_entry]. void got_miss (string cache_name, mixed key, mapping cache_context) { account_miss (cache_name); }
397d522009-11-17Martin Stjernholm  local protected int|float calc_pval (CacheEntry entry)
1353182009-11-12Martin Stjernholm  {
397d522009-11-17Martin Stjernholm  int|float pval;
1353182009-11-12Martin Stjernholm  if (CacheEntry lowest = get_iterator (priority_list)->index()) {
397d522009-11-17Martin Stjernholm  int|float l = lowest->pval, v = entry->value; pval = l + v;
1353182009-11-12Martin Stjernholm 
397d522009-11-17Martin Stjernholm  if (floatp (v)) { if (v != 0.0 && v < l * (Float.EPSILON * 0x10)) {
1353182009-11-12Martin Stjernholm #ifdef DEBUG
397d522009-11-17Martin Stjernholm  if (max_used_pval != Int.NATIVE_MAX) werror ("%O: Ran out of significant digits for cache entry %O - " "got min priority %O and entry value %O.\n", this, entry, l, v);
1353182009-11-12Martin Stjernholm #endif
397d522009-11-17Martin Stjernholm  // Force a reset of the pvals in the next gc. max_used_pval = Int.NATIVE_MAX; }
1353182009-11-12Martin Stjernholm  }
397d522009-11-17Martin Stjernholm  else if (pval > max_used_pval) max_used_pval = pval;
1353182009-11-12Martin Stjernholm  } else
397d522009-11-17Martin Stjernholm  // Assume entry->value isn't greater than Int.NATIVE_MAX/2 right away. pval = entry->value; return pval; }
1353182009-11-12Martin Stjernholm 
397d522009-11-17Martin Stjernholm  void got_hit (string cache_name, CacheEntry entry, mapping cache_context) { account_hit (cache_name, entry); int|float pval = calc_pval (entry);
1353182009-11-12Martin Stjernholm  // vvv Relying on the interpreter lock from here. priority_list[entry] = 0;
397d522009-11-17Martin Stjernholm  entry->pval = pval;
1353182009-11-12Martin Stjernholm  priority_list[entry] = 1; // ^^^ Relying on the interpreter lock to here. } int add_entry (string cache_name, CacheEntry entry, int old_entry, mapping cache_context) { entry->cache_name = cache_name; int|float v = entry->value = calc_value (cache_name, entry, old_entry, cache_context); if (!low_add_entry (cache_name, entry)) return 0;
397d522009-11-17Martin Stjernholm  entry->pval = calc_pval (entry);
1353182009-11-12Martin Stjernholm  priority_list[entry] = 1; if (size > size_limit) evict (size_limit); return 1; } int remove_entry (string cache_name, CacheEntry entry) { priority_list[entry] = 0;
4787532009-11-17Martin Stjernholm  return low_remove_entry (cache_name, entry);
1353182009-11-12Martin Stjernholm  } void evict (int max_size) { while (size > max_size) { CacheEntry entry = get_iterator (priority_list)->index(); if (!entry) break; MORE_CACHE_WERR ("%s: Size is %d - evicting %O.\n", entry->cache_name, size, entry); priority_list[entry] = 0; low_remove_entry (entry->cache_name, entry); } } void after_gc() {
397d522009-11-17Martin Stjernholm  if (max_used_pval > Int.NATIVE_MAX / 2) {
1353182009-11-12Martin Stjernholm  // The neat thing to do here is to lower all priority values, // but it has to be done atomically. Since this presumably // happens so seldom we take the easy way and just empty the // caches instead.
397d522009-11-17Martin Stjernholm  CACHE_WERR ("%O: Max priority value too large - resetting.\n", this);
1353182009-11-12Martin Stjernholm  if (Configuration admin_config = roxenp()->get_admin_configuration()) // Log an event, in case it doesn't happen that seldom afterall. admin_config->log_event ("roxen", "reset-ram-cache", this->name); while (sizeof (priority_list)) evict (0);
397d522009-11-17Martin Stjernholm  max_used_pval = 0;
1353182009-11-12Martin Stjernholm  } } } class CM_GDS_1 { inherit CM_GreedyDual; constant name = "GDS(1)"; constant doc = #"\ This cache manager implements <a href='http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.30.7285'>GreedyDual-Size</a> with the cost of each entry fixed at 1, which makes it optimize the cache hit ratio."; float calc_value (string cache_name, CacheEntry entry, int old_entry, mapping cache_context) { ASSERT_IF_DEBUG (entry->size /*%O*/ > 10, entry->size); return 1.0 / entry->size; } }
1f6e162009-11-18Martin Stjernholm protected CM_GDS_1 cm_gds_1 = CM_GDS_1 (startup_cache_size);
1353182009-11-12Martin Stjernholm  protected Thread.Local cache_contexts = Thread.Local(); // A thread local mapping to store the timestamp from got_miss so it // can be read from the (presumably) following add_entry. // // In an entry with index 0 in the mapping, the time spent creating // cache entries is accumulated. It is used to deduct the time for // creating entries in subcaches.
68dfb92009-11-12Martin Stjernholm class CM_GDS_Time
c872b92009-11-16Martin Stjernholm //! Like @[CM_GDS_1] but adds support for calculating entry cost based //! on passed time.
1353182009-11-12Martin Stjernholm { inherit CM_GreedyDual; #ifdef RAMCACHE_STATS class CacheEntry { inherit CM_GreedyDual::CacheEntry; int|float cost; protected string _sprintf (int flag) {
7f93b62009-11-17Martin Stjernholm  return flag == 'O' && sprintf ("CacheEntry(%s, %db, %O, %O)", format_key(), size, value, cost);
1353182009-11-12Martin Stjernholm  } } #endif protected int gettime_func();
68dfb92009-11-12Martin Stjernholm  //! Returns the current time for cost calculation. (@[format_cost] //! assumes this is in microseconds.)
1353182009-11-12Martin Stjernholm 
c872b92009-11-16Martin Stjernholm  protected void save_start_hrtime (string cache_name, mixed key, mapping cache_context)
1353182009-11-12Martin Stjernholm  { if (mapping all_ctx = cache_context || cache_contexts->get()) { int start = gettime_func() - all_ctx[0]; if (mapping(mixed:int) ctx = all_ctx[cache_name]) { #if 0 if (!zero_type (ctx[key])) // This warning is useful since strictly speaking we don't // know which cache_lookup calls to use as start for the // time measurement, so the time cost might be bogus. If it // isn't the last one then you should probably replace some // calls with cache_peek. werror ("Warning: Detected repeated missed lookup calls.\n%s\n", describe_backtrace (backtrace())); #endif ctx[key] = start; } else all_ctx[cache_name] = ([key: start]); } else { #ifdef DEBUG werror ("Warning: Got call from %O without cache context mapping.\n%s\n", Thread.this_thread(), describe_backtrace (backtrace())); #endif } }
c872b92009-11-16Martin Stjernholm  void got_miss (string cache_name, mixed key, mapping cache_context) { //werror ("Miss.\n%s\n", describe_backtrace (backtrace())); account_miss (cache_name); save_start_hrtime (cache_name, key, cache_context); } void got_hit (string cache_name, CacheEntry entry, mapping 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. account_hit (cache_name, entry); save_start_hrtime (cache_name, entry->key, cache_context); }
68dfb92009-11-12Martin Stjernholm  protected int entry_create_hrtime (string cache_name, mixed key, mapping cache_context)
1353182009-11-12Martin Stjernholm  { if (mapping all_ctx = cache_context || cache_contexts->get()) if (mapping(mixed:int) ctx = all_ctx[cache_name]) { int start = m_delete (ctx, key); if (!zero_type (start)) { int duration = (gettime_func() - all_ctx[0]) - start; ASSERT_IF_DEBUG (duration >= 0); all_ctx[0] += duration; return duration; } } #ifdef DEBUG werror ("Warning: No preceding missed lookup for this key - " "cannot determine entry creation time.\n%s\n", describe_backtrace (backtrace())); #endif return 0; } protected float mean_cost; protected int mean_count = 0; // This is not a real mean value since we (normally) don't keep // track of the cost of each entry. Instead it's a decaying average. float calc_value (string cache_name, CacheEntry entry, int old_entry, mapping cache_context) { if (int hrtime = !old_entry && entry_create_hrtime (cache_name, entry->key, cache_context)) { float cost = (float) hrtime; #ifdef RAMCACHE_STATS entry->cost = cost; #endif if (!mean_count) { mean_cost = cost; mean_count = 1; } else { mean_cost = (mean_count * mean_cost + cost) / (mean_count + 1); if (mean_count < 1000) mean_count++; } return cost / entry->size; } // Awkward situation: We don't have any cost for this entry. Just // use the mean cost of all entries in the cache, so it at least // isn't way off in either direction. return mean_cost / entry->size; } void evict (int max_size) { ::evict (max_size); if (!max_size) mean_count = 0; } string format_cost (float cost) { return Roxen.format_hrtime ((int) cost); } } class CM_GDS_CPUTime {
68dfb92009-11-12Martin Stjernholm  inherit CM_GDS_Time;
1353182009-11-12Martin Stjernholm  constant name = "GDS(cpu time)";
7f93b62009-11-17Martin Stjernholm  string doc = #"\
1353182009-11-12Martin Stjernholm This cache manager implements <a href='http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.30.7285'>GreedyDual-Size</a> with the cost of each entry determined by the CPU time it took to
7f93b62009-11-17Martin Stjernholm create it. The CPU time implementation is " + Roxen.html_encode_string (System.CPU_TIME_IMPLEMENTATION) + " which is " + (System.CPU_TIME_IS_THREAD_LOCAL ? "thread local" : "not thread local") + " and has a resolution of " + (System.CPU_TIME_RESOLUTION / 1e6) + " ms.";
1353182009-11-12Martin Stjernholm  protected int gettime_func() { return gethrvtime(); } }
1f6e162009-11-18Martin Stjernholm protected CM_GDS_CPUTime cm_gds_cputime = CM_GDS_CPUTime (startup_cache_size);
1353182009-11-12Martin Stjernholm 
68dfb92009-11-12Martin Stjernholm class CM_GDS_RealTime { inherit CM_GDS_Time; constant name = "GDS(real time)";
7f93b62009-11-17Martin Stjernholm  string doc = #"\
68dfb92009-11-12Martin Stjernholm This cache manager implements <a href='http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.30.7285'>GreedyDual-Size</a> with the cost of each entry determined by the real (wall) time it took
7f93b62009-11-17Martin Stjernholm to create it. The real time implementation is " + Roxen.html_encode_string (System.REAL_TIME_IMPLEMENTATION) + " which is " + (System.REAL_TIME_IS_MONOTONIC ? "monotonic" : "not monotonic") + " and has a resolution of " + (System.REAL_TIME_RESOLUTION / 1e6) + " ms.";
68dfb92009-11-12Martin Stjernholm  protected int gettime_func() { // The real time includes a lot of noise that isn't appropriate // for cache entry cost measurement. Let's compensate for the time // spent in the pike gc, at least. return gethrtime() - Pike.implicit_gc_real_time(); } } protected CM_GDS_RealTime cm_gds_realtime =
1f6e162009-11-18Martin Stjernholm  CM_GDS_RealTime (startup_cache_size);
68dfb92009-11-12Martin Stjernholm 
7f93b62009-11-17Martin Stjernholm #ifdef DEBUG_CACHE_SIZES
0819a32009-11-17Martin Stjernholm protected int cmp_sizeof_cache_entry (string cache_name, CacheEntry entry)
7f93b62009-11-17Martin Stjernholm { int res; mixed data = entry->data; mapping opts = (["block_strings": 1, #if DEBUG_CACHE_SIZES > 1 "collect_internals": 1, #endif ]); if (function(int|mapping:int) cm_cb = objectp (data) && data->cache_count_memory) res = cm_cb (opts) + Pike.count_memory (-1, entry, entry->key); else res = Pike.count_memory (opts, entry, entry->key, data); #if DEBUG_CACHE_SIZES > 1
0819a32009-11-17Martin Stjernholm  werror ("Internals counted for %O / %O: ({\n%{%s,\n%}})\n", cache_name, entry, sort (map (opts->collect_internals, lambda (mixed m) {return sprintf ("%O", m);})));
7f93b62009-11-17Martin Stjernholm #endif return res; } #endif
88be2a2009-11-17Martin Stjernholm //! The preferred managers according to various caching requirements. //! When several apply for a cache, choose the first one in this list.
1353182009-11-12Martin Stjernholm //! //! @dl //! @item "default" //! The default manager for caches that do not specify any //! requirements. //!
88be2a2009-11-17Martin Stjernholm //! @item "no_cpu_timings" //! The manager to use for caches where a cache entry is created //! synchronously by one thread, but that thread spends most of its //! time waiting for a result from an external party, meaning that //! consumed cpu time is not an accurate measurement of the cost. //!
1353182009-11-12Martin Stjernholm //! @item "no_thread_timings" //! The manager to use for caches where a cache entry isn't created //! synchronously by one thread in the span between the //! @[cache_lookup] miss and the following @[cache_set]. //! //! @item "no_timings" //! The manager to use for caches that do not have a usage pattern //! where it is meaningful to calculate the creation cost from the //! time between a @[cache_lookup] miss to the following //! @[cache_set]. //! @enddl mapping(string:CacheManager) cache_manager_prefs = ([
7f93b62009-11-17Martin Stjernholm  "default": (System.CPU_TIME_IS_THREAD_LOCAL != "yes" || System.CPU_TIME_RESOLUTION > 10000 ? // Don't use cpu time if it's too bad. Buglet: We just // assume the real time is better. cm_gds_realtime : cm_gds_cputime),
88be2a2009-11-17Martin Stjernholm  "no_cpu_timings": cm_gds_realtime,
1353182009-11-12Martin Stjernholm  "no_thread_timings": cm_gds_realtime, "no_timings": cm_gds_1, ]); //! All available cache managers. array(CacheManager) cache_managers = Array.uniq (({cache_manager_prefs->default,
1f6e162009-11-18Martin Stjernholm  cache_manager_prefs->no_cpu_timings,
1353182009-11-12Martin Stjernholm  cache_manager_prefs->no_thread_timings, cache_manager_prefs->no_timings,
68dfb92009-11-12Martin Stjernholm #if 0
1353182009-11-12Martin Stjernholm  cm_random,
68dfb92009-11-12Martin Stjernholm #endif cm_gds_1, cm_gds_realtime, cm_gds_cputime, }));
1353182009-11-12Martin Stjernholm  protected mapping(string:CacheManager) caches = ([]); // Maps the named caches to the cache managers that handle them. protected Thread.Mutex cache_mgmt_mutex = Thread.Mutex(); // Locks operations that manipulate named caches, i.e. changes in the // caches, CacheManager.stats and CacheManager.lookup mappings.
1f6e162009-11-18Martin Stjernholm void set_total_size_limit (int size) //! Sets the total size limit available to all caches. { // FIXME: Currently this is per-cache. foreach (cache_managers, CacheManager mgr) { mgr->total_size_limit = size; mgr->update_size_limit(); } }
1353182009-11-12Martin Stjernholm mapping(string:CacheManager) cache_list() //! Returns a list of all currently registered caches and their //! managers. { return caches + ([]); } CacheManager cache_register (string cache_name, void|string|CacheManager manager) //! Registers a new cache. Returns its @[CacheManager] instance. //! //! @[manager] can be a specific @[CacheManager] instance to use, a //! string that specifies a type of manager (see //! @[cache_manager_prefs]), or zero to select the default manager. //! //! If the cache already exists, its current manager is simply //! returned, and @[manager] has no effect. //! //! Registering a cache is not mandatory before it is used - one will //! be created automatically with the default manager otherwise. //! Still, it's a good idea so that the cache list in the admin //! interface gets populated timely. { Thread.Mutex lock = cache_mgmt_mutex->lock (2); // Called from cache_change_manager too. if (CacheManager mgr = caches[cache_name]) return mgr; if (!manager) manager = cache_manager_prefs->default; else if (stringp (manager)) { string cache_type = manager; manager = cache_manager_prefs[cache_type]; if (!manager) error ("Unknown cache manager type %O requested.\n", cache_type); } caches[cache_name] = manager; manager->stats[cache_name] = CacheStats(); manager->lookup[cache_name] = ([]); return manager; } void cache_unregister (string cache_name) //! Unregisters the specified cache. This empties the cache and also //! removes it from the cache overview in the admin interface. { Thread.Mutex lock = cache_mgmt_mutex->lock(); // vvv Relying on the interpreter lock from here. if (CacheManager mgr = m_delete (caches, cache_name)) { mapping(mixed:CacheEntry) lm = m_delete (mgr->lookup, cache_name); CacheStats cs = m_delete (mgr->stats, cache_name); // ^^^ Relying on the interpreter lock to here. mgr->size -= cs->size; destruct (lock); foreach (lm;; CacheEntry entry) mgr->remove_entry (cache_name, entry); } } void cache_change_manager (string cache_name, CacheManager manager) //! Changes the manager for a cache. All the cache entries are moved //! to the new manager, but it might not have adequate information to //! give them an accurate cost (typically applies to cost derived from //! the creation time). { Thread.Mutex lock = cache_mgmt_mutex->lock(); // vvv Relying on the interpreter lock from here. CacheManager old_mgr = m_delete (caches, cache_name); if (old_mgr == manager) caches[cache_name] = manager; // ^^^ Relying on the interpreter lock to here. else { mapping(mixed:CacheEntry) old_lm = m_delete (old_mgr->lookup, cache_name); CacheStats old_cs = m_delete (old_mgr->stats, cache_name); // ^^^ Relying on the interpreter lock to here. old_mgr->size -= old_cs->size; cache_register (cache_name, manager); // Move over the entries. destruct (lock); int entry_size_diff = (Pike.count_memory (0, manager->CacheEntry (0, 0)) - Pike.count_memory (0, old_mgr->CacheEntry (0, 0))); foreach (old_lm; mixed key; CacheEntry old_ent) { old_mgr->remove_entry (cache_name, old_ent); CacheEntry new_ent = manager->CacheEntry (key, old_ent->data); new_ent->size = old_ent->size + entry_size_diff; manager->add_entry (cache_name, new_ent, 1, 0); } manager->update_size_limit(); // Evicts superfluous entries if necessary. } } void cache_expire (void|string cache_name) //! Expires (i.e. removes) all entries in a named cache, or in all //! caches if @[cache_name] is left out. { // Currently not very efficiently implemented, but this function // doesn't have to be quick.
aee4a62009-11-16Martin Jonsson  foreach (cache_name ? ({cache_name}) : indices (caches), string cn) {
1353182009-11-12Martin Stjernholm  CACHE_WERR ("Emptying cache %O.\n", cn); if (CacheManager mgr = caches[cn]) { mgr->evict (0); mgr->update_size_limit(); } } } void flush_memory_cache (void|string cache_name) {cache_expire (cache_name);} void cache_clear_deltas() { cache_contexts->set (([])); } mixed cache_lookup (string cache_name, mixed key, void|mapping cache_context) //! Looks up an entry in a cache. Returns @[UNDEFINED] if not found. //! //! @[cache_context] is an optional mapping used to pass info between //! @[cache_lookup] and @[cache_set], which some cache managers need //! to determine the cost of the created entry (the work done between //! a failed @[cache_lookup] and the following @[cache_set] with the //! same key is assumed to be the creation of the cache entry). //! //! If @[cache_context] is not specified, a thread local mapping is //! used. @[cache_context] is necessary when @[cache_lookup] and //! @[cache_set] are called from different threads, or in different //! callbacks from a backend. It should not be specified otherwise. //! //! If you need to use @[cache_context], create an empty mapping and //! give it to @[cache_lookup]. Then give the same mapping to the //! corresponding @[cache_set] when the entry has been created. { CacheManager mgr = caches[cache_name] || cache_register (cache_name); if (mapping(mixed:CacheEntry) lm = mgr->lookup[cache_name]) if (CacheEntry entry = lm[key]) { if (entry->timeout && entry->timeout <= time (1)) { mgr->remove_entry (cache_name, entry); mgr->got_miss (cache_name, key, cache_context); MORE_CACHE_WERR ("cache_lookup (%O, %s): Timed out\n", cache_name, RXML.utils.format_short (key)); return 0; }
c872b92009-11-16Martin Stjernholm  mgr->got_hit (cache_name, entry, cache_context);
1353182009-11-12Martin Stjernholm  MORE_CACHE_WERR ("cache_lookup (%O, %s): Hit\n", cache_name, RXML.utils.format_short (key)); return entry->data; } mgr->got_miss (cache_name, key, cache_context); MORE_CACHE_WERR ("cache_lookup (%O, %s): Miss\n", cache_name, RXML.utils.format_short (key)); return 0; } mixed cache_peek (string cache_name, mixed key) //! Checks if the cache contains an entry. Same as @[cache_lookup] //! except that it doesn't affect the hit/miss statistics or the time //! accounting used to estimate entry creation cost. { if (CacheManager mgr = caches[cache_name]) if (mapping(mixed:CacheEntry) lm = mgr->lookup[cache_name]) if (CacheEntry entry = lm[key]) { if (entry->timeout && entry->timeout <= time (1)) { mgr->remove_entry (cache_name, entry); MORE_CACHE_WERR ("cache_peek (%O, %s): Timed out\n", cache_name, RXML.utils.format_short (key)); return 0; } MORE_CACHE_WERR ("cache_peek (%O, %s): Entry found\n", cache_name, RXML.utils.format_short (key)); return entry->data; } MORE_CACHE_WERR ("cache_peek (%O, %s): Entry not found\n", cache_name, RXML.utils.format_short (key)); return 0; } mixed cache_set (string cache_name, mixed key, mixed data, void|int timeout, void|mapping cache_context) //! Adds an entry to a cache. //! //! @param cache_name //! The name of the cache. The cache has preferably been created with //! @[cache_register], but otherwise it is created on-demand using the //! default cache manager. //! //! @param key //! The key for the cache entry. Normally a string, but can be //! anything that works as an index in a mapping. //! //! @param data //! The payload data. This cannot be a zero, since the cache garb will //! consider that a destructed object and evict it from the cache. //! //! @param timeout //! If nonzero, sets the maximum time in seconds that the entry is //! valid. //! //! @param cache_context //! The cache context mapping given to the earlier @[cache_lookup] //! which failed to find the entry that this call adds to the cache. //! See @[cache_lookup] for more details. //! //! @returns //! Returns @[data]. { ASSERT_IF_DEBUG (data); CacheManager mgr = caches[cache_name] || cache_register (cache_name); CacheEntry new_entry = mgr->CacheEntry (key, data); #ifdef DEBUG_COUNT_MEM mapping opts = (["lookahead": DEBUG_COUNT_MEM - 1, "collect_stats": 1, "collect_direct_externals": 1, ]); float t = gauge {
68dfb92009-11-12Martin Stjernholm #else #define opts 0 #endif if (function(int|mapping:int) cm_cb = objectp (data) && data->cache_count_memory)
7f93b62009-11-17Martin Stjernholm  new_entry->size = cm_cb (opts) + Pike.count_memory (-1, new_entry, key);
68dfb92009-11-12Martin Stjernholm  else new_entry->size = Pike.count_memory (opts, new_entry, key, data); #ifdef DEBUG_COUNT_MEM
1353182009-11-12Martin Stjernholm  };
7f93b62009-11-17Martin Stjernholm  werror ("%O: la %d size %d time %g int %d cyc %d ext %d vis %d revis %d " "rnd %d wqa %d\n", entry, opts->lookahead, opts->size, t, opts->internal, opts->cyclic,
1353182009-11-12Martin Stjernholm  opts->external, opts->visits, opts->revisits, opts->rounds, opts->work_queue_alloc);
7f93b62009-11-17Martin Stjernholm 
1353182009-11-12Martin Stjernholm #if 0 if (opts->external) { opts->collect_direct_externals = 1; // Raise the lookahead to 1 to recurse the closest externals. if (opts->lookahead < 1) opts->lookahead = 1; if (function(int|mapping:int) cm_cb =
7f93b62009-11-17Martin Stjernholm  objectp (data) && data->cache_count_memory) res = cm_cb (opts) + Pike.count_memory (-1, entry, key);
1353182009-11-12Martin Stjernholm  else
7f93b62009-11-17Martin Stjernholm  res = Pike.count_memory (opts, entry, key, data);
1353182009-11-12Martin Stjernholm  array exts = opts->collect_direct_externals; werror ("Externals found using lookahead %d: %O\n", opts->lookahead, exts); #if 0 foreach (exts, mixed ext) if (objectp (ext) && ext->locate_my_ext_refs) { werror ("Refs to %O:\n", ext); _locate_references (ext); } #endif } #endif
7f93b62009-11-17Martin Stjernholm 
68dfb92009-11-12Martin Stjernholm #endif // DEBUG_COUNT_MEM
7f93b62009-11-17Martin Stjernholm #undef opts #ifdef DEBUG_CACHE_SIZES
0819a32009-11-17Martin Stjernholm  new_entry->cmp_size = cmp_sizeof_cache_entry (cache_name, new_entry);
7f93b62009-11-17Martin Stjernholm #endif
1353182009-11-12Martin Stjernholm  if (timeout) new_entry->timeout = time (1) + timeout; mgr->add_entry (cache_name, new_entry, 0, cache_context); MORE_CACHE_WERR ("cache_set (%O, %s, %s, %O): %O\n", cache_name, RXML.utils.format_short (key), sprintf (objectp (data) ? "%O" : "%t", data), timeout, new_entry); return data; } void cache_remove (string cache_name, mixed key) //! Removes an entry from the cache. //! //! @note //! If @[key] was zero, this function used to remove the whole cache. //! Use @[cache_expire] for that instead. { MORE_CACHE_WERR ("cache_remove (%O, %O)\n", cache_name, key); if (CacheManager mgr = caches[cache_name]) if (mapping(mixed:CacheEntry) lm = mgr->lookup[cache_name])
4787532009-11-17Martin Stjernholm  if (CacheEntry entry = lm[key])
1353182009-11-12Martin Stjernholm  mgr->remove_entry (cache_name, entry); } mapping(mixed:CacheEntry) cache_entries (string cache_name) //! Returns the lookup mapping for the given named cache. Don't be //! destructive on the returned mapping or anything inside it. { if (CacheManager mgr = caches[cache_name]) if (mapping(mixed:CacheEntry) lm = mgr->lookup[cache_name]) return lm; return ([]); } array cache_indices(string|void cache_name) // Deprecated compat function. { if (!cache_name) return indices (caches); else return indices (cache_entries (cache_name)); } mapping(CacheManager:mapping(string:CacheStats)) cache_stats() //! Returns the complete cache statistics. For each cache manager, a //! mapping with the named caches it handles is returned, with their //! respective @[CacheStat] objects. Don't be destructive on any part //! of the returned value. { mapping(CacheManager:mapping(string:CacheStats)) res = ([]); foreach (cache_managers, CacheManager mgr) res[mgr] = mgr->stats; return res; }
e451b02009-11-18Martin Stjernholm // GC statistics. These are decaying sums over the last
015b5f2009-11-18Martin Stjernholm // gc_stats_period seconds. constant gc_stats_period = 60 * 60; float sum_gc_runs = 0.0, sum_gc_time = 0.0;
e451b02009-11-18Martin Stjernholm float sum_destruct_garbage_size = 0.0; float sum_timeout_garbage_size = 0.0;
015b5f2009-11-18Martin Stjernholm  protected int cache_start_time = time(); int last_gc_run;
1353182009-11-12Martin Stjernholm protected void cache_clean() // Periodic gc, to clean up timed out and destructed entries. { int now = time (1); int vt = gethrvtime(), t = gethrtime();
015b5f2009-11-18Martin Stjernholm  int destr_garb_size, timeout_garb_size;
1353182009-11-12Martin Stjernholm  CACHE_WERR ("Starting RAM cache cleanup.\n");
0819a32009-11-17Martin Stjernholm  // Note: Might be necessary to always recheck the sizes here, since // entries can change in size for a number of reasons. Most of the // time it doesn't matter much, but the risk is that the size limit // gets unacceptably off after a while. foreach (caches; string cache_name; CacheManager mgr) { if (mapping(mixed:CacheEntry) lm = mgr->lookup[cache_name])
7f93b62009-11-17Martin Stjernholm  foreach (lm;; CacheEntry entry) {
015b5f2009-11-18Martin Stjernholm  if (!entry->data) { MORE_CACHE_WERR ("%s: Removing destructed entry %O\n", cache_name, entry); destr_garb_size += entry->size; mgr->remove_entry (cache_name, entry); } else if (entry->timeout && entry->timeout <= now) { MORE_CACHE_WERR ("%s: Removing timed out entry %O\n", cache_name, entry); timeout_garb_size += entry->size;
1353182009-11-12Martin Stjernholm  mgr->remove_entry (cache_name, entry); }
7f93b62009-11-17Martin Stjernholm  else { #ifdef DEBUG_CACHE_SIZES
0819a32009-11-17Martin Stjernholm  int size = cmp_sizeof_cache_entry (cache_name, entry);
7f93b62009-11-17Martin Stjernholm  if (size != entry->cmp_size) {
0819a32009-11-17Martin Stjernholm  werror ("Size difference for %O / %O: " "Is %d, was %d in cache_set() - diff %d.\n", cache_name, entry, size, entry->cmp_size, size - entry->cmp_size);
7f93b62009-11-17Martin Stjernholm  // Update to avoid repeated messages. entry->cmp_size = size; } #endif } }
1353182009-11-12Martin Stjernholm  }
1f6e162009-11-18Martin Stjernholm  foreach (cache_managers, CacheManager mgr)
c081db2009-11-17Martin Stjernholm  mgr->after_gc();
1353182009-11-12Martin Stjernholm  vt = gethrvtime() - vt; // -1 - -1 if cpu time isn't working.
66c5012009-11-18Martin Stjernholm  t = gethrtime() - t;
1353182009-11-12Martin Stjernholm  CACHE_WERR ("Finished RAM cache cleanup - took %s.\n",
66c5012009-11-18Martin Stjernholm  Roxen.format_hrtime (vt || t));
015b5f2009-11-18Martin Stjernholm  int stat_last_period = now - last_gc_run; int stat_tot_period = now - cache_start_time; int startup = stat_tot_period < gc_stats_period; if (!startup) stat_tot_period = gc_stats_period; if (stat_last_period > stat_tot_period) { // GC intervals are larger than the statistics interval, so just // set the values. Note that stat_last_period is very large on the // first call since last_gc_run is zero, so we always get here then.
66c5012009-11-18Martin Stjernholm  sum_gc_time = (float) (vt || t);
015b5f2009-11-18Martin Stjernholm  sum_gc_runs = 1.0;
e451b02009-11-18Martin Stjernholm  sum_destruct_garbage_size = (float) destr_garb_size; sum_timeout_garbage_size = (float) timeout_garb_size; } else if (startup) { sum_gc_time += (float) (vt || t); sum_gc_runs += 1.0; sum_destruct_garbage_size += (float) destr_garb_size; sum_timeout_garbage_size += (float) timeout_garb_size;
015b5f2009-11-18Martin Stjernholm  } else {
e451b02009-11-18Martin Stjernholm  float weight = 1.0 - (float) stat_last_period / stat_tot_period; sum_gc_runs = weight * sum_gc_runs + 1.0; sum_gc_time = weight * sum_gc_time + (float) (vt || t); sum_destruct_garbage_size = (weight * sum_destruct_garbage_size + (float) destr_garb_size); sum_timeout_garbage_size = (weight * sum_timeout_garbage_size + (float) timeout_garb_size);
015b5f2009-11-18Martin Stjernholm  } last_gc_run = now;
1353182009-11-12Martin Stjernholm  if (Configuration admin_config = roxenp()->get_admin_configuration()) admin_config->log_event ("roxen", "ram-gc", 0, ([ "handle-cputime": vt, "handle-time": t, ])); // Fall back to 60 secs just in case the config is messed up somehow. roxenp()->background_run (roxenp()->query ("mem_cache_gc_2") || 60, cache_clean); } #else // !NEW_RAM_CACHE
a72f372007-09-06Henrik Grubbström (Grubba) // Base the cache retention time on the time it took to // generate the entry. /* #define TIME_BASED_CACHE */ #ifdef TIME_BASED_CACHE // A cache entry is an array with six elements #define ENTRY_SIZE 6 #else /* !TIME_BASED_CACHE */
cfb98d2000-04-30Martin Nilsson // A cache entry is an array with four elements #define ENTRY_SIZE 4
e85d6f2007-09-06Henrik Grubbström (Grubba) #endif /* TIME_BASED_CACHE */
cfb98d2000-04-30Martin Nilsson // The elements are as follows: // A timestamp when the entry was last used
b1fca01996-11-12Per Hedbor #define TIMESTAMP 0
cfb98d2000-04-30Martin Nilsson // The actual data
b1fca01996-11-12Per Hedbor #define DATA 1
cfb98d2000-04-30Martin Nilsson // A timeout telling when the data is no longer valid.
33e3251998-05-07Johan Schön #define TIMEOUT 2
f7136a2004-09-20Martin Stjernholm // The size of the entry, in bytes.
b1be432000-02-15Martin Nilsson #define SIZE 3
a72f372007-09-06Henrik Grubbström (Grubba) #ifdef TIME_BASED_CACHE // The approximate time in µs it took to generate the data for the entry. #define HRTIME 4 // The number of hits for this entry. #define HITS 5 #endif /* TIME_BASED_CACHE */
b1fca01996-11-12Per Hedbor 
cfb98d2000-04-30Martin Nilsson // The actual cache along with some statistics mappings.
fc40392008-08-15Martin Stjernholm protected mapping(string:mapping(string:array)) cache; protected mapping(string:int) hits=([]), all=([]);
b1fca01996-11-12Per Hedbor 
a72f372007-09-06Henrik Grubbström (Grubba) #ifdef TIME_BASED_CACHE
fc40392008-08-15Martin Stjernholm protected Thread.Local deltas = Thread.Local();
a72f372007-09-06Henrik Grubbström (Grubba) #endif /* TIME_BASED_CACHE */
f7136a2004-09-20Martin Stjernholm #ifdef CACHE_DEBUG
fc40392008-08-15Martin Stjernholm protected array(int) memory_usage_summary()
f7136a2004-09-20Martin Stjernholm { int count, bytes; foreach (_memory_usage(); string descr; int amount) if (has_prefix (descr, "num_")) count += amount; else if (has_suffix (descr, "_bytes")) bytes += amount; return ({count, bytes}); } #endif
daebe52008-10-14Martin Stjernholm protected int sizeof_cache_entry (array entry)
d62fc02008-10-04Martin Stjernholm {
daebe52008-10-14Martin Stjernholm  int res; #ifdef DEBUG_COUNT_MEM mapping opts = (["lookahead": DEBUG_COUNT_MEM - 1, "collect_stats": 1, "collect_direct_externals": 1, ]); float t = gauge { #else #define opts 0 #endif if (function(int|mapping:int) cm_cb = objectp (entry[DATA]) && entry[DATA]->cache_count_memory) res = cm_cb (opts) + Pike.count_memory (-1, entry); else res = Pike.count_memory (opts, entry); #ifdef DEBUG_COUNT_MEM }; werror ("%s: la %d size %d time %g int %d cyc %d ext %d vis %d revis %d " "rnd %d wqa %d\n", (objectp (entry[DATA]) ? sprintf ("%O", entry[DATA]) : sprintf ("%t", entry[DATA])), opts->lookahead, opts->size, t, opts->internal, opts->cyclic, opts->external, opts->visits, opts->revisits, opts->rounds, opts->work_queue_alloc);
d62fc02008-10-04Martin Stjernholm #if 0
daebe52008-10-14Martin Stjernholm  if (opts->external) { opts->collect_direct_externals = 1; // Raise the lookahead to 1 to recurse the closest externals. if (opts->lookahead < 1) opts->lookahead = 1; if (function(int|mapping:int) cm_cb = objectp (entry[DATA]) && entry[DATA]->cache_count_memory) res = cm_cb (opts) + Pike.count_memory (-1, entry); else res = Pike.count_memory (opts, entry); array exts = opts->collect_direct_externals; werror ("Externals found using lookahead %d: %O\n", opts->lookahead, exts); #if 0 foreach (exts, mixed ext) if (objectp (ext) && ext->locate_my_ext_refs) { werror ("Refs to %O:\n", ext); _locate_references (ext); }
d62fc02008-10-04Martin Stjernholm #endif
daebe52008-10-14Martin Stjernholm  }
d62fc02008-10-04Martin Stjernholm #endif
daebe52008-10-14Martin Stjernholm #endif // DEBUG_COUNT_MEM #undef opts return res; }
d62fc02008-10-04Martin Stjernholm 
f7136a2004-09-20Martin Stjernholm void flush_memory_cache (void|string in) { CACHE_WERR ("flush_memory_cache(%O)\n", in);
bfb3d62001-07-20Martin Stjernholm  if (in) { m_delete (cache, in); m_delete (hits, in); m_delete (all, in); }
f7136a2004-09-20Martin Stjernholm 
bfb3d62001-07-20Martin Stjernholm  else {
f7136a2004-09-20Martin Stjernholm #ifdef CACHE_DEBUG
fa7c032007-06-05Martin Stjernholm  //gc();
f7136a2004-09-20Martin Stjernholm  [int before_count, int before_bytes] = memory_usage_summary(); #endif foreach (cache; string cache_class; mapping(string:array) subcache) { #ifdef CACHE_DEBUG int num_entries_before= sizeof (subcache); #endif m_delete (cache, cache_class); m_delete (hits, cache_class); m_delete (all, cache_class); #ifdef CACHE_DEBUG
fa7c032007-06-05Martin Stjernholm  //gc();
f7136a2004-09-20Martin Stjernholm  [int after_count, int after_bytes] = memory_usage_summary(); CACHE_WERR (" Flushed %O that had %d entries: " "Freed %d things and %d bytes\n", cache_class, num_entries_before, before_count - after_count, before_bytes - after_bytes); before_count = after_count; before_bytes = after_bytes; #endif }
bfb3d62001-07-20Martin Stjernholm  }
f7136a2004-09-20Martin Stjernholm  CACHE_WERR ("flush_memory_cache() done\n");
49f9242000-05-15Martin Nilsson }
a72f372007-09-06Henrik Grubbström (Grubba) void cache_clear_deltas() { #ifdef TIME_BASED_CACHE deltas->set(([])); #endif /* TIME_BASED_CACHE */ }
fc7d5a2001-07-03Martin Nilsson constant svalsize = 4*4;
0281ea2000-03-07Martin Nilsson 
1353182009-11-12Martin Stjernholm object cache_register (string cache_name, void|string|object manager) // Forward compat dummy. { return 0; }
cfb98d2000-04-30Martin Nilsson // Expire a whole cache
f6d62d1997-03-26Per Hedbor void cache_expire(string in) {
f7136a2004-09-20Martin Stjernholm  CACHE_WERR("cache_expire(%O)\n", in);
f6d62d1997-03-26Per Hedbor  m_delete(cache, in); }
cfb98d2000-04-30Martin Nilsson // Lookup an entry in a cache
1353182009-11-12Martin Stjernholm mixed cache_lookup(string in, mixed what, void|mapping ignored)
b1fca01996-11-12Per Hedbor { all[in]++;
9a002e2000-04-19Martin Nilsson  int t=time(1);
a72f372007-09-06Henrik Grubbström (Grubba) #ifdef TIME_BASED_CACHE mapping deltas = this_program::deltas->get() || ([]); if (deltas[in]) { deltas[in][what] = gethrtime(); } else { deltas[in] = ([ what : gethrtime() ]); } #endif /* TIME_BASED_CACHE */
cfb98d2000-04-30Martin Nilsson  // Does the entry exist at all?
1b41df2000-04-18Martin Nilsson  if(array entry = (cache[in] && cache[in][what]) )
cfb98d2000-04-30Martin Nilsson  // Is it time outed?
1b41df2000-04-18Martin Nilsson  if (entry[TIMEOUT] && entry[TIMEOUT] < t) {
249ed12000-01-06Martin Stjernholm  m_delete (cache[in], what);
f7136a2004-09-20Martin Stjernholm  MORE_CACHE_WERR("cache_lookup(%O, %O) -> Timed out\n", in, what);
249ed12000-01-06Martin Stjernholm  } else {
cfb98d2000-04-30Martin Nilsson  // Update the timestamp and hits counter and return the value.
1b41df2000-04-18Martin Nilsson  cache[in][what][TIMESTAMP]=t;
f7136a2004-09-20Martin Stjernholm  MORE_CACHE_WERR("cache_lookup(%O, %O) -> Hit\n", in, what);
249ed12000-01-06Martin Stjernholm  hits[in]++;
a72f372007-09-06Henrik Grubbström (Grubba) #ifdef TIME_BASED_CACHE entry[HITS]++; #endif /* TIME_BASED_CACHE */
249ed12000-01-06Martin Stjernholm  return entry[DATA]; }
f7136a2004-09-20Martin Stjernholm  else MORE_CACHE_WERR("cache_lookup(%O, %O) -> Miss\n", in, what);
249ed12000-01-06Martin Stjernholm  return ([])[0];
b1fca01996-11-12Per Hedbor }
1353182009-11-12Martin Stjernholm mixed cache_peek (string cache_name, mixed key) // Forward compat alias. { return cache_lookup (cache_name, key); }
471c292000-09-04Jonas Wallden // Return all indices used by a given cache or indices of available caches array(string) cache_indices(string|void in) { if (in) return (cache[in] && indices(cache[in])) || ({ }); else return indices(cache); }
cfb98d2000-04-30Martin Nilsson // Return some fancy cache statistics.
1e857b2000-08-01Martin Nilsson mapping(string:array(int)) status()
b1fca01996-11-12Per Hedbor {
a8228b2000-08-14Jonas Wallden  mapping(string:array(int)) ret = ([ ]);
daebe52008-10-14Martin Stjernholm  foreach (cache; string name; mapping(string:array) cache_class) { #ifdef DEBUG_COUNT_MEM werror ("\nCache: %s\n", name); #endif
a8228b2000-08-14Jonas Wallden  // We only show names up to the first ":" if present. This lets us // group entries together in the status table. string show_name = (name / ":")[0];
daebe52008-10-14Martin Stjernholm  int size = 0; foreach (cache_class; string idx; array entry) { if (!entry[SIZE]) entry[SIZE] = Pike.count_memory (0, idx) + sizeof_cache_entry (entry); size += entry[SIZE]; }
b7d8902000-08-14Jonas Wallden  array(int) entry = ({ sizeof(cache[name]), hits[name], all[name],
33fdf52003-04-07Martin Stjernholm  size });
a8228b2000-08-14Jonas Wallden  if (!zero_type(ret[show_name]))
8ea5662009-11-12Martin Jonsson  for (int idx = 0; idx <= 3; idx++)
a8228b2000-08-14Jonas Wallden  ret[show_name][idx] += entry[idx]; else ret[show_name] = entry;
b1fca01996-11-12Per Hedbor  }
1e857b2000-08-01Martin Nilsson  return ret;
b1fca01996-11-12Per Hedbor }
cfb98d2000-04-30Martin Nilsson // Remove an entry from the cache. Removes the entire cache if no // entry key is given.
852b472002-02-12Jonas Wallden void cache_remove(string in, mixed what)
b1fca01996-11-12Per Hedbor {
f7136a2004-09-20Martin Stjernholm  MORE_CACHE_WERR("cache_remove(%O, %O)\n", in, what);
b1fca01996-11-12Per Hedbor  if(!what) m_delete(cache, in); else
a565891999-12-27Martin Nilsson  if(cache[in])
b1fca01996-11-12Per Hedbor  m_delete(cache[in], what); }
cfb98d2000-04-30Martin Nilsson // Add an entry to a cache
1353182009-11-12Martin Stjernholm mixed cache_set(string in, mixed what, mixed to, int|void tm, void|mapping ignored)
b1fca01996-11-12Per Hedbor {
f7136a2004-09-20Martin Stjernholm  MORE_CACHE_WERR("cache_set(%O, %O, %O)\n", in, what, /* to */ _typeof(to));
9a002e2000-04-19Martin Nilsson  int t=time(1);
b1fca01996-11-12Per Hedbor  if(!cache[in]) cache[in]=([ ]); cache[in][what] = allocate(ENTRY_SIZE); cache[in][what][DATA] = to;
1b41df2000-04-18Martin Nilsson  if(tm) cache[in][what][TIMEOUT] = t + tm; cache[in][what][TIMESTAMP] = t;
a72f372007-09-06Henrik Grubbström (Grubba) #ifdef TIME_BASED_CACHE mapping deltas = this_program::deltas->get() || ([]); cache[in][what][HRTIME] = gethrtime() - (deltas[in] && deltas[in][what]); cache[in][what][HITS] = 1; CACHE_WERR("[%O] HRTIME: %d\n", in, cache[in][what][HRTIME]); #endif /* TIME_BASED_CACHE */
f7a34a1998-06-24Johan Schön  return to;
b1fca01996-11-12Per Hedbor }
cfb98d2000-04-30Martin Nilsson // Clean the cache.
b1fca01996-11-12Per Hedbor void cache_clean() {
ea9a4b2001-01-10Per Hedbor  int gc_time=[int](([function(string:mixed)]roxenp()->query)("mem_cache_gc"));
f7136a2004-09-20Martin Stjernholm  int now=time(1); #ifdef CACHE_DEBUG [int mem_count, int mem_bytes] = memory_usage_summary(); CACHE_WERR("cache_clean() [memory usage: %d things, %d bytes]\n", mem_count, mem_bytes); #endif foreach(cache; string cache_class_name; mapping(string:array) cache_class)
b1fca01996-11-12Per Hedbor  {
f7136a2004-09-20Martin Stjernholm #ifdef CACHE_DEBUG int num_entries_before = sizeof (cache_class); #endif MORE_CACHE_WERR(" Class %O\n", cache_class_name);
daebe52008-10-14Martin Stjernholm #ifdef DEBUG_COUNT_MEM werror ("\nCache: %s\n", cache_class_name); #endif
f7136a2004-09-20Martin Stjernholm  foreach(cache_class; string idx; array entry)
b1fca01996-11-12Per Hedbor  {
ba73a21996-12-10Per Hedbor #ifdef DEBUG
f7136a2004-09-20Martin Stjernholm  if(!intp(entry[TIMESTAMP])) error("Illegal timestamp in cache ("+cache_class_name+":"+idx+")\n");
b1fca01996-11-12Per Hedbor #endif
f7136a2004-09-20Martin Stjernholm  if(entry[TIMEOUT] && entry[TIMEOUT] < now) { MORE_CACHE_WERR(" %O: Deleted (explicit timeout)\n", idx); m_delete(cache_class, idx);
b1fca01996-11-12Per Hedbor  }
b1be432000-02-15Martin Nilsson  else {
a72f372007-09-06Henrik Grubbström (Grubba) #ifdef TIME_BASED_CACHE if (entry[HRTIME] < 10*60*1000000) { // 10 minutes. // Valid HRTIME entry. // Let an entry live for 5000 times longer than // it takes to create it times the 2-logarithm of // the number of hits. // Minimum one second. // 5000/1000000 = 1/200 // FIXME: Adjust the factor dynamically? int t = [int](entry[HRTIME]*(entry[HITS]->size(2)))/200 + 1; if ((entry[TIMESTAMP] + t) < now) { m_delete(cache_class, idx); MORE_CACHE_WERR(" %O with lifetime %d seconds (%d hits): Deleted\n", idx, t, entry[HITS]); } else { MORE_CACHE_WERR(" %O with lifetime %d seconds (%d hits): Ok\n", idx, t, entry[HITS]); } continue;
4282db2003-03-04Henrik Grubbström (Grubba)  }
a72f372007-09-06Henrik Grubbström (Grubba) #endif /* TIME_BASED_CACHE */
daebe52008-10-14Martin Stjernholm  if(!entry[SIZE]) entry[SIZE] = Pike.count_memory (0, idx) + sizeof_cache_entry (entry);
f7136a2004-09-20Martin Stjernholm  if(entry[TIMESTAMP]+1 < now &&
daebe52008-10-14Martin Stjernholm  entry[TIMESTAMP] + gc_time - entry[SIZE] / 100 < now)
f7136a2004-09-20Martin Stjernholm  { m_delete(cache_class, idx);
daebe52008-10-14Martin Stjernholm  MORE_CACHE_WERR(" %O with size %d bytes: Deleted\n", idx, [int] entry[SIZE]);
f7136a2004-09-20Martin Stjernholm  }
b1be432000-02-15Martin Nilsson  else
daebe52008-10-14Martin Stjernholm  MORE_CACHE_WERR(" %O with size %d bytes: Ok\n", idx, [int] entry[SIZE]);
b1fca01996-11-12Per Hedbor  } }
f7136a2004-09-20Martin Stjernholm  if(!sizeof(cache_class)) m_delete(cache, cache_class_name); #ifdef CACHE_DEBUG [int new_mem_count, int new_mem_bytes] = memory_usage_summary(); CACHE_WERR(" Class %O: Cleaned up %d of %d entries " "[freed %d things and %d bytes]\n", cache_class_name, num_entries_before - sizeof (cache_class), num_entries_before, mem_count - new_mem_count, mem_bytes - new_mem_bytes); mem_count = new_mem_count; mem_bytes = new_mem_bytes; #endif
b1fca01996-11-12Per Hedbor  }
f7136a2004-09-20Martin Stjernholm  CACHE_WERR("cache_clean() done\n");
4d1c5f2001-07-26Martin Stjernholm  roxenp()->background_run (gc_time, cache_clean);
b1fca01996-11-12Per Hedbor }
1353182009-11-12Martin Stjernholm #endif // !NEW_RAM_CACHE
b3a87b2001-01-21Martin Nilsson 
69c0412001-03-19Martin Nilsson // --- Non-garbing "cache" ----------- private mapping(string:mapping(string:mixed)) nongc_cache; //! Associates a @[value] to a @[key] in a cache identified with //! the @[cache_id]. This cache does not garb, hence it should be //! used for storing data where its size is well controled. void nongarbing_cache_set(string cache_id, string key, mixed value) {
38a2562001-04-19Jonas Wallden  if(nongc_cache[cache_id]) nongc_cache[cache_id][key] = value; else nongc_cache[cache_id] = ([ key:value ]);
69c0412001-03-19Martin Nilsson } //! Returns the value associated to the @[key] in the cache //! identified by @[cache_id] in the non-garbing cache. mixed nongarbing_cache_lookup(string cache_id, string key) { return nongc_cache[cache_id]?nongc_cache[cache_id][key]:([])[0]; } //! Remove a value from the non-garbing cache. void nongarbing_cache_remove(string cache_id, string key) { if(nongc_cache[cache_id]) m_delete(nongc_cache[cache_id], key); } //! Flush a cache in the non-garbing cache. void nongarbing_cache_flush(string cache_id) { m_delete(nongc_cache, cache_id); }
fc7d5a2001-07-03Martin Nilsson mapping(string:array(int)) ngc_status() { mapping(string:array(int)) res = ([]);
daebe52008-10-14Martin Stjernholm  foreach(nongc_cache; string cache; mapping(string:mixed) cachemap) { int size = Pike.count_memory (0, cachemap); res[cache] = ({ sizeof(cachemap), size});
33fdf52003-04-07Martin Stjernholm  }
fc7d5a2001-07-03Martin Nilsson  return res; }
69c0412001-03-19Martin Nilsson 
b3a87b2001-01-21Martin Nilsson // --- Session cache ----------------- #ifndef SESSION_BUCKETS # define SESSION_BUCKETS 4 #endif #ifndef SESSION_SHIFT_TIME
d82ce42001-04-23Martin Nilsson # define SESSION_SHIFT_TIME 15*60
b3a87b2001-01-21Martin Nilsson #endif
9b44022001-03-11Martin Nilsson // The minimum time until which the session should be stored.
296af92001-02-04Martin Nilsson private mapping(string:int) session_persistence;
9b44022001-03-11Martin Nilsson // The sessions, divided into several buckets.
b3a87b2001-01-21Martin Nilsson private array(mapping(string:mixed)) session_buckets;
9b44022001-03-11Martin Nilsson // The database for storage of the sessions.
316c9f2001-08-13Per Hedbor private function(string:Sql.Sql) db;
9b44022001-03-11Martin Nilsson // The biggest value in session_persistence private int max_persistence;
b3a87b2001-01-21Martin Nilsson 
9b44022001-03-11Martin Nilsson // The low level call for storing a session in the database private void store_session(string id, mixed data, int t) { data = encode_value(data);
e4d9462003-11-11Dan Nelson  db("local")->query("REPLACE INTO session_cache VALUES (%s," + t + ",%s)", id, data);
9b44022001-03-11Martin Nilsson } // GC that, depending on the sessions session_persistence either // throw the session away or store it in a database.
b3a87b2001-01-21Martin Nilsson private void session_cache_handler() {
296af92001-02-04Martin Nilsson  int t=time(1);
9b44022001-03-11Martin Nilsson  if(max_persistence>t) { clean: foreach(indices(session_buckets[-1]), string id) { if(session_persistence[id]<t) { m_delete(session_buckets[-1], id); m_delete(session_persistence, id); continue; } for(int i; i<SESSION_BUCKETS-2; i++) if(session_buckets[i][id]) { continue clean; } if(objectp(session_buckets[-1][id])) { m_delete(session_buckets[-1], id); m_delete(session_persistence, id); continue; } store_session(id, session_buckets[-1][id], session_persistence[id]);
296af92001-02-04Martin Nilsson  m_delete(session_buckets[-1], id); m_delete(session_persistence, id); } }
9b44022001-03-11Martin Nilsson 
296af92001-02-04Martin Nilsson  session_buckets = ({ ([]) }) + session_buckets[..SESSION_BUCKETS-2];
4d1c5f2001-07-26Martin Stjernholm  roxenp()->background_run(SESSION_SHIFT_TIME, session_cache_handler);
b3a87b2001-01-21Martin Nilsson }
9b44022001-03-11Martin Nilsson // Stores all sessions that should be persistent in the database. // This function is called upon exit. private void session_cache_destruct() { int t=time(1); if(max_persistence>t) { report_notice("Synchronizing session cache"); foreach(session_buckets, mapping(string:mixed) session_bucket) foreach(indices(session_bucket), string id) if(session_persistence[id]>t) { store_session(id, session_bucket[id], session_persistence[id]); m_delete(session_persistence, id); } } report_notice("Session cache synchronized\n"); }
f14da62001-09-20Martin Nilsson //! Removes the session data assiciate with @[id] from the //! session cache and session database. //! //! @seealso //! set_session_data
14151c2001-04-21Martin Nilsson void clear_session(string id) { m_delete(session_persistence, id); foreach(session_buckets, mapping bucket) m_delete(bucket, id);
316c9f2001-08-13Per Hedbor  db("local")->query("DELETE FROM session_cache WHERE id=%s", id);
14151c2001-04-21Martin Nilsson }
9b44022001-03-11Martin Nilsson //! Returns the data associated with the session @[id]. //! Returns a zero type upon failure.
f14da62001-09-20Martin Nilsson //! //! @seealso //! set_session_data
b3a87b2001-01-21Martin Nilsson mixed get_session_data(string id) { mixed data; foreach(session_buckets, mapping bucket) if(data=bucket[id]) { session_buckets[0][id] = data; return data; }
316c9f2001-08-13Per Hedbor  data = db("local")->query("SELECT data FROM session_cache WHERE id=%s", id);
9b44022001-03-11Martin Nilsson  if(sizeof([array]data) && !catch(data=decode_value( ([array(mapping(string:string))]data)[0]->data ))) return data;
b3a87b2001-01-21Martin Nilsson  return ([])[0]; }
9b44022001-03-11Martin Nilsson //! Assiciates the session @[id] to the @[data]. If no @[id] is provided //! a unique id will be generated. The session id is returned from the //! function. The minimum guaranteed storage time may be set with the
f14da62001-09-20Martin Nilsson //! @[persistence] argument. Note that this is a time stamp, not a time out.
9b44022001-03-11Martin Nilsson //! If @[store] is set, the @[data] will be stored in a database directly, //! and not when the garbage collect tries to delete the data. This
f14da62001-09-20Martin Nilsson //! will ensure that the data is kept safe in case the server restarts
9b44022001-03-11Martin Nilsson //! before the next GC.
f14da62001-09-20Martin Nilsson //! //! @note //! The @[data] must not contain any object, programs or functions, or the //! storage in database will throw an error. //! //! @seealso //! get_session_data, clear_session
9b44022001-03-11Martin Nilsson string set_session_data(mixed data, void|string id, void|int persistence, void|int(0..1) store) {
b3a87b2001-01-21Martin Nilsson  if(!id) id = ([function(void:string)]roxenp()->create_unique_id)();
296af92001-02-04Martin Nilsson  session_persistence[id] = persistence;
b3a87b2001-01-21Martin Nilsson  session_buckets[0][id] = data;
9b44022001-03-11Martin Nilsson  max_persistence = max(max_persistence, persistence); if(store && persistence) store_session(id, data, persistence);
b3a87b2001-01-21Martin Nilsson  return id; }
9b44022001-03-11Martin Nilsson // Sets up the session database tables. private void setup_tables() {
316c9f2001-08-13Per Hedbor  db("local")->query("CREATE TABLE IF NOT EXISTS session_cache (" "id CHAR(32) NOT NULL PRIMARY KEY, " "persistence INT UNSIGNED NOT NULL DEFAULT 0, " "data BLOB NOT NULL)"); master()->resolv("DBManager.is_module_table") ( 0, "local", "session_cache", "Used by the session manager" );
9b44022001-03-11Martin Nilsson } //! Initializes the session handler. void init_session_cache() {
3909932001-04-09Per Hedbor  db = (([function(string:function(string:object(Sql.Sql)))]master()->resolv)
316c9f2001-08-13Per Hedbor  ("DBManager.cached_get")); setup_tables();
9b44022001-03-11Martin Nilsson }
4d1c5f2001-07-26Martin Stjernholm void init_call_outs() { roxenp()->background_run(60, cache_clean); roxenp()->background_run(SESSION_SHIFT_TIME, session_cache_handler); CACHE_WERR("Cache garb call outs installed.\n"); }
b1fca01996-11-12Per Hedbor void create() {
1847a21999-09-06Per Hedbor  add_constant( "cache", this_object() );
1353182009-11-12Martin Stjernholm #ifndef NEW_RAM_CACHE
5b4a542001-02-08Martin Nilsson  cache = ([ ]);
1353182009-11-12Martin Stjernholm #endif
5b4a542001-02-08Martin Nilsson 
69c0412001-03-19Martin Nilsson  nongc_cache = ([ ]);
5b4a542001-02-08Martin Nilsson  session_buckets = ({ ([]) }) * SESSION_BUCKETS; session_persistence = ([]);
f7136a2004-09-20Martin Stjernholm  CACHE_WERR("Now online.\n");
b1fca01996-11-12Per Hedbor }
9b44022001-03-11Martin Nilsson  void destroy() { session_cache_destruct(); return; }