/* Directory name lookup caching

   Copyright (C) 1996 Free Software Foundation, Inc.
   Written by Michael I. Bushnell, p/BSG, & Miles Bader.

   This file is part of the GNU Hurd.

   The GNU Hurd is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License as
   published by the Free Software Foundation; either version 2, or (at
   your option) any later version.

   The GNU Hurd is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111, USA. */

#include "priv.h"
#include <string.h>
#include <cacheq.h>

/* Maximum number of names to cache at once */
#define MAXCACHE 256

/* Maximum length of file name we bother caching */
#define CACHE_NAME_LEN 100

/* Cache entry */
struct lookup_cache
{
  struct cacheq_hdr hdr;

  /* Used to indentify nodes to the fs dependent code.  0 for NODE_CACHE_ID
     means a `negative' entry -- recording that there's definitely no node with
     this name.  */
  int dir_cache_id, node_cache_id;

  /* Name of the node NODE_CACHE_ID in the directory DIR_CACHE_ID.  Entries
     with names too long to fit in this buffer aren't cached at all.  */
  char name[CACHE_NAME_LEN];

  /* Strlen of NAME.  If this is zero, it's an unused entry. */
  size_t name_len;		
};

/* The contents of the cache in no particular order */
static struct cacheq lookup_cache = { sizeof (struct lookup_cache) };

static spin_lock_t cache_lock = SPIN_LOCK_INITIALIZER;

/* Buffer to hold statistics */
static struct
{
  long pos_hits;
  long neg_hits;
  long miss;
  long fetch_errors;
} statistics;

/* If there's an entry for NAME, of length NAME_LEN, in directory DIR in the
   cache, return it's entry, otherwise 0.  CACHE_LOCK must be held.  */
static struct lookup_cache *
find_cache (struct node *dir, const char *name, size_t name_len)
{
  struct lookup_cache *c;

  /* Search the list.  All unused entries are contiguous at the end of the
     list, so we can stop searching when we see the first one.  */
  for (c = lookup_cache.mru; c && c->name_len; c = c->hdr.next)
    if (c->name_len == name_len
	&& c->dir_cache_id == dir->cache_id
	&& c->name[0] == name[0] && strcmp (c->name, name) == 0)
      return c;

  return 0;
}

/* Node NP has just been found in DIR with NAME.  If NP is null, that
   means that this name has been confirmed as absent in the directory. */
void
diskfs_enter_lookup_cache (struct node *dir, struct node *np, char *name)
{
  struct lookup_cache *c;
  size_t name_len = strlen (name);
  
  if (name_len > CACHE_NAME_LEN - 1)
    return;

  /* Never cache . or ..; it's too much trouble to get the locking
     order right.  */
  if (name[0] == '.' 
      && (name[1] == '\0'
	  || (name[1] == '.' && name[2] == '\0')))
    return;

  spin_lock (&cache_lock);

  if (lookup_cache.length == 0)
    /* There should always be an lru_cache; this being zero means that the
       cache hasn't been initialized yet.  Do so.  */
    cacheq_set_length (&lookup_cache, MAXCACHE);

  /* See if there's an old entry for NAME in DIR.  If not, replace the least
     recently used entry.  */
  c = find_cache (dir, name, name_len) ?: lookup_cache.lru;

  /* Fill C with the new entry.  */
  c->dir_cache_id = dir->cache_id;
  c->node_cache_id = np ? np->cache_id : 0;
  strcpy (c->name, name);
  c->name_len = name_len;

  /* Now C becomes the MRU entry!  */
  cacheq_make_mru (&lookup_cache, c);

  spin_unlock (&cache_lock);
}

/* Purge all references in the cache to NP as a node inside 
   directory DP. */
void
diskfs_purge_lookup_cache (struct node *dp, struct node *np)
{
  struct lookup_cache *c, *next;
  
  spin_lock (&cache_lock);
  for (c = lookup_cache.mru; c; c = next)
    {
      /* Save C->hdr.next, since we may move C from this position. */
      next = c->hdr.next;

      if (c->name_len
	  && c->dir_cache_id == dp->cache_id
	  && c->node_cache_id == np->cache_id)
	{
	  c->name_len = 0;
	  cacheq_make_lru (&lookup_cache, c); /* Use C as the next free
						 entry. */
	}
    }
  spin_unlock (&cache_lock);
}

/* Scan the cache looking for NAME inside DIR.  If we don't know
   anything entry at all, then return 0.  If the entry is confirmed to
   not exist, then return -1.  Otherwise, return NP for the entry, with
   a newly allocated reference. */
struct node *
diskfs_check_lookup_cache (struct node *dir, char *name)
{
  struct lookup_cache *c;
  
  spin_lock (&cache_lock);

  c = find_cache (dir, name, strlen (name));
  if (c)
    {
      int id = c->node_cache_id;

      cacheq_make_mru (&lookup_cache, c); /* Record C as recently used.  */

      statistics.pos_hits++;
      spin_unlock (&cache_lock);

      if (id == 0)
	/* A negative cache entry.  */
	return (struct node *)-1;
      else if (id == dir->cache_id)
	/* The cached node is the same as DIR.  */
	{
	  diskfs_nref (dir);
	  return dir;
	}
      else
	/* Just a normal entry in DIR; get the actual node.  */
	{
	  struct node *np;
	  error_t err = diskfs_cached_lookup (id, &np);
	  return err ? 0 : np;
	}
    }
  
  statistics.miss++;
  spin_unlock (&cache_lock);

  return 0;
}