gpg: New caching functions.
authorWerner Koch <wk@gnupg.org>
Sat, 13 Apr 2019 09:48:58 +0000 (11:48 +0200)
committerWerner Koch <wk@gnupg.org>
Sat, 13 Apr 2019 09:48:58 +0000 (11:48 +0200)
* g10/objcache.c: New.
* g10/objcache.h: New.
* g10/Makefile.am (common_source): Add them.
* g10/gpg.c: Include objcache.h.
(g10_exit): Call objcache_dump_stats.
* g10/getkey.c: Include objcache.h.
(get_primary_uid, release_keyid_list): Remove.
(cache_user_id): Remove.
(finish_lookup): Call the new cache_put_keyblock instead of
cache_user_id.
(get_user_id_string): Remove code for mode 2.
(get_user_id): Implement using cache_get_uid_bykid.
--

This generic caching module is better than the ad-hoc code we used in
getkey.c.  More cleanup in getkey is still required but it is a
start.  There is also a small performance increase with the new cache:

With a large keyring and --list-sigs I get these numbers:

|      | before     | after      |
|------+------------+------------|
| real | 14m1.028s  | 12m16.186s |
| user |  2m18.484s |  1m36.040s |
| sys  | 11m42.420s | 10m40.044s |

Note the speedup in the user time which is due to the improved cache
algorithm.  This is obvious, because the old cache was just a long
linked list; the new cache are two hash tables.

Signed-off-by: Werner Koch <wk@gnupg.org>
g10/Makefile.am
g10/getkey.c
g10/gpg.c
g10/objcache.c [new file with mode: 0644]
g10/objcache.h [new file with mode: 0644]

index 3b44643..884b474 100644 (file)
@@ -121,6 +121,7 @@ common_source =  \
              sig-check.c       \
              keylist.c         \
              pkglue.c pkglue.h \
+             objcache.c objcache.h \
              ecdh.c
 
 gpg_sources = server.c          \
index 49e237a..34bc4bf 100644 (file)
@@ -36,6 +36,7 @@
 #include "../common/i18n.h"
 #include "keyserver-internal.h"
 #include "call-agent.h"
+#include "objcache.h"
 #include "../common/host2net.h"
 #include "../common/mbox-util.h"
 #include "../common/status.h"
@@ -141,7 +142,6 @@ typedef struct user_id_db
   char name[1];
 } *user_id_db_t;
 static user_id_db_t user_id_db;
-static int uid_cache_entries;  /* Number of entries in uid cache. */
 
 static void merge_selfsigs (ctrl_t ctrl, kbnode_t keyblock);
 static int lookup (ctrl_t ctrl, getkey_ctx_t ctx, int want_secret,
@@ -263,115 +263,6 @@ user_id_not_found_utf8 (void)
 
 
 
-/* Return the user ID from the given keyblock.
- * We use the primary uid flag which has been set by the merge_selfsigs
- * function.  The returned value is only valid as long as the given
- * keyblock is not changed.  */
-static const char *
-get_primary_uid (KBNODE keyblock, size_t * uidlen)
-{
-  KBNODE k;
-  const char *s;
-
-  for (k = keyblock; k; k = k->next)
-    {
-      if (k->pkt->pkttype == PKT_USER_ID
-         && !k->pkt->pkt.user_id->attrib_data
-         && k->pkt->pkt.user_id->flags.primary)
-       {
-         *uidlen = k->pkt->pkt.user_id->len;
-         return k->pkt->pkt.user_id->name;
-       }
-    }
-  s = user_id_not_found_utf8 ();
-  *uidlen = strlen (s);
-  return s;
-}
-
-
-static void
-release_keyid_list (keyid_list_t k)
-{
-  while (k)
-    {
-      keyid_list_t k2 = k->next;
-      xfree (k);
-      k = k2;
-    }
-}
-
-/****************
- * Store the association of keyid and userid
- * Feed only public keys to this function.
- */
-static void
-cache_user_id (KBNODE keyblock)
-{
-  user_id_db_t r;
-  const char *uid;
-  size_t uidlen;
-  keyid_list_t keyids = NULL;
-  KBNODE k;
-  size_t n;
-
-  for (k = keyblock; k; k = k->next)
-    {
-      if (k->pkt->pkttype == PKT_PUBLIC_KEY
-         || k->pkt->pkttype == PKT_PUBLIC_SUBKEY)
-       {
-         keyid_list_t a = xmalloc_clear (sizeof *a);
-         /* Hmmm: For a long list of keyids it might be an advantage
-          * to append the keys.  */
-          fingerprint_from_pk (k->pkt->pkt.public_key, a->fpr, &n);
-          a->fprlen = n;
-         keyid_from_pk (k->pkt->pkt.public_key, a->keyid);
-         /* First check for duplicates.  */
-         for (r = user_id_db; r; r = r->next)
-           {
-             keyid_list_t b;
-
-             for (b = r->keyids; b; b = b->next)
-               {
-                 if (b->fprlen == a->fprlen
-                      && !memcmp (b->fpr, a->fpr, a->fprlen))
-                   {
-                     if (DBG_CACHE)
-                       log_debug ("cache_user_id: already in cache\n");
-                     release_keyid_list (keyids);
-                     xfree (a);
-                     return;
-                   }
-               }
-           }
-         /* Now put it into the cache.  */
-         a->next = keyids;
-         keyids = a;
-       }
-    }
-  if (!keyids)
-    BUG (); /* No key no fun.  */
-
-
-  uid = get_primary_uid (keyblock, &uidlen);
-
-  if (uid_cache_entries >= MAX_UID_CACHE_ENTRIES)
-    {
-      /* fixme: use another algorithm to free some cache slots */
-      r = user_id_db;
-      user_id_db = r->next;
-      release_keyid_list (r->keyids);
-      xfree (r);
-      uid_cache_entries--;
-    }
-  r = xmalloc (sizeof *r + uidlen - 1);
-  r->keyids = keyids;
-  r->len = uidlen;
-  memcpy (r->name, uid, r->len);
-  r->next = user_id_db;
-  user_id_db = r;
-  uid_cache_entries++;
-}
-
 
 /* Disable and drop the public key cache (which is filled by
    cache_public_key and get_pubkey).  Note: there is currently no way
@@ -3627,7 +3518,7 @@ finish_lookup (kbnode_t keyblock, unsigned int req_usage, int want_exact,
       xfree (tempkeystr);
     }
 
-  cache_user_id (keyblock);
+  cache_put_keyblock (keyblock);
 
   return latest_key ? latest_key : keyblock; /* Found.  */
 }
@@ -3848,6 +3739,7 @@ get_user_id_string (ctrl_t ctrl, u32 * keyid, int mode, size_t *r_len,
   int pass = 0;
   char *p;
 
+  log_assert (mode != 2);
   if (r_nouid)
     *r_nouid = 0;
 
@@ -3862,13 +3754,7 @@ get_user_id_string (ctrl_t ctrl, u32 * keyid, int mode, size_t *r_len,
                {
                   if (mode == 2)
                     {
-                      /* An empty string as user id is possible.  Make
-                         sure that the malloc allocates one byte and
-                         does not bail out.  */
-                      p = xmalloc (r->len? r->len : 1);
-                      memcpy (p, r->name, r->len);
-                      if (r_len)
-                        *r_len = r->len;
+                      BUG ();
                     }
                   else
                     {
@@ -3926,7 +3812,31 @@ get_long_user_id_string (ctrl_t ctrl, u32 * keyid)
 char *
 get_user_id (ctrl_t ctrl, u32 *keyid, size_t *rn, int *r_nouid)
 {
-  return get_user_id_string (ctrl, keyid, 2, rn, r_nouid);
+  char *name;
+  unsigned int namelen;
+
+  if (r_nouid)
+    *r_nouid = 0;
+
+  name = cache_get_uid_bykid (keyid, &namelen);
+  if (!name)
+    {
+      /* Get it so that the cache will be filled.  */
+      if (!get_pubkey (ctrl, NULL, keyid))
+        name = cache_get_uid_bykid (keyid, &namelen);
+    }
+
+  if (!name)
+    {
+      name = xstrdup (user_id_not_found_utf8 ());
+      namelen = strlen (name);
+      if (r_nouid)
+        *r_nouid = 1;
+    }
+
+  if (rn && name)
+    *rn = namelen;
+  return name;
 }
 
 
index 1ab7b04..b46d226 100644 (file)
--- a/g10/gpg.c
+++ b/g10/gpg.c
@@ -59,6 +59,7 @@
 #include "../common/asshelp.h"
 #include "call-dirmngr.h"
 #include "tofu.h"
+#include "objcache.h"
 #include "../common/init.h"
 #include "../common/mbox-util.h"
 #include "../common/shareddefs.h"
@@ -5223,6 +5224,7 @@ g10_exit( int rc )
     {
       keydb_dump_stats ();
       sig_check_dump_stats ();
+      objcache_dump_stats ();
       gcry_control (GCRYCTL_DUMP_MEMORY_STATS);
       gcry_control (GCRYCTL_DUMP_RANDOM_STATS);
     }
diff --git a/g10/objcache.c b/g10/objcache.c
new file mode 100644 (file)
index 0000000..7eb92cd
--- /dev/null
@@ -0,0 +1,642 @@
+/* objcache.c - Caching functions for keys and user ids.
+ * Copyright (C) 2019 g10 Code GmbH
+ *
+ * This file is part of GnuPG.
+ *
+ * GnuPG 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GnuPG 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, see <https://www.gnu.org/licenses/>.
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include <config.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "gpg.h"
+#include "../common/util.h"
+#include "packet.h"
+#include "keydb.h"
+#include "options.h"
+#include "objcache.h"
+
+/* Note that max value for uid_items is actually a the threshold when
+ * we start to look for ietms which can be removed.  */
+#define NO_OF_UID_ITEM_BUCKETS    107
+#define MAX_UID_ITEMS_PER_BUCKET  20
+
+#define NO_OF_KEY_ITEM_BUCKETS    383
+#define MAX_KEY_ITEMS_PER_BUCKET  20
+
+
+/* An object to store a user id.  This describes an item in the linked
+ * lists of a bucket in hash table.  The reference count will
+ * eventually be used to remove items from the table.  */
+typedef struct uid_item_s
+{
+  struct uid_item_s *next;
+  unsigned int refcount;  /* The reference count for this item.   */
+  unsigned int namelen;   /* The length of the UID sans the nul.  */
+  char name[1];
+} *uid_item_t;
+
+static uid_item_t *uid_table; /* Hash table for with user ids.  */
+static size_t uid_table_size; /* Number of allocated buckets.   */
+static unsigned int uid_table_max;    /* Max. # of items in a bucket.  */
+static unsigned int uid_table_added;  /* # of items added.   */
+static unsigned int uid_table_dropped;/* # of items dropped.  */
+
+
+/* An object to store properties of a key.  Note that this can be used
+ * for a primary or a subkey.  The key is linked to a user if that
+ * exists.  */
+typedef struct key_item_s
+{
+  struct key_item_s *next;
+  unsigned int usecount;
+  byte fprlen;
+  char fpr[MAX_FINGERPRINT_LEN];
+  u32 keyid[2];
+  uid_item_t ui;          /* NULL of a ref'ed user id item.      */
+} *key_item_t;
+
+static key_item_t *key_table; /* Hash table with the keys.      */
+static size_t key_table_size; /* Number of allocated buckents.  */
+static unsigned int key_table_max;    /* Max. # of items in a bucket.  */
+static unsigned int key_table_added;  /* # of items added.   */
+static unsigned int key_table_dropped;/* # of items dropped.  */
+static key_item_t key_item_attic;     /* List of freed items.  */
+
+
+\f
+/* Dump stats.  */
+void
+objcache_dump_stats (void)
+{
+  unsigned int idx;
+  int len, minlen, maxlen;
+  unsigned int count, attic, empty;
+  key_item_t ki;
+  uid_item_t ui;
+
+  count = empty = 0;
+  minlen = -1;
+  maxlen = 0;
+  for (idx = 0; idx < key_table_size; idx++)
+    {
+      len = 0;
+      for (ki = key_table[idx]; ki; ki = ki->next)
+        {
+          count++;
+          len++;
+          /* log_debug ("key bucket %u: kid=%08lX used=%u ui=%p\n", */
+          /*            idx, (ulong)ki->keyid[0], ki->usecount, ki->ui); */
+        }
+      if (len > maxlen)
+        maxlen = len;
+
+      if (!len)
+        empty++;
+      else if (minlen == -1 || len < minlen)
+        minlen = len;
+    }
+  for (attic=0, ki = key_item_attic; ki; ki = ki->next)
+    attic++;
+  log_info ("objcache: keys=%u/%u/%u chains=%u,%d..%d buckets=%zu/%u"
+            " attic=%u\n",
+            count, key_table_added, key_table_dropped,
+            empty, minlen > 0? minlen : 0, maxlen,
+            key_table_size, key_table_max, attic);
+
+  count = empty = 0;
+  minlen = -1;
+  maxlen = 0;
+  for (idx = 0; idx < uid_table_size; idx++)
+    {
+      len = 0;
+      for (ui = uid_table[idx]; ui; ui = ui->next)
+        {
+          count++;
+          len++;
+          /* log_debug ("uid bucket %u: %p ref=%u l=%u (%.20s)\n", */
+          /*            idx, ui, ui->refcount, ui->namelen, ui->name); */
+        }
+      if (len > maxlen)
+        maxlen = len;
+
+      if (!len)
+        empty++;
+      else if (minlen == -1 || len < minlen)
+        minlen = len;
+    }
+  log_info ("objcache: uids=%u/%u/%u chains=%u,%d..%d buckets=%zu/%u\n",
+            count, uid_table_added, uid_table_dropped,
+            empty, minlen > 0? minlen : 0, maxlen,
+            uid_table_size, uid_table_max);
+}
+
+
+\f
+/* The hash function we use for the uid_table.  Must not call a system
+ * function.  */
+static inline unsigned int
+uid_table_hasher (const char *name, unsigned namelen)
+{
+  const unsigned char *s = (const unsigned char*)name;
+  unsigned int hashval = 0;
+  unsigned int carry;
+
+  for (; namelen; namelen--, s++)
+    {
+      hashval = (hashval << 4) + *s;
+      if ((carry = (hashval & 0xf0000000)))
+        {
+          hashval ^= (carry >> 24);
+          hashval ^= carry;
+        }
+    }
+
+  return hashval % uid_table_size;
+}
+
+
+/* Run time allocation of the uid table.  This allows us to eventually
+ * add an option to gpg to control the size.  */
+static void
+uid_table_init (void)
+{
+  if (uid_table)
+    return;
+  uid_table_size = NO_OF_UID_ITEM_BUCKETS;
+  uid_table_max = MAX_UID_ITEMS_PER_BUCKET;
+  uid_table = xcalloc (uid_table_size, sizeof *uid_table);
+}
+
+
+static uid_item_t
+uid_item_ref (uid_item_t ui)
+{
+  if (ui)
+    ui->refcount++;
+  return ui;
+}
+
+static void
+uid_item_unref (uid_item_t uid)
+{
+  if (!uid)
+    return;
+  if (!uid->refcount)
+    log_fatal ("too many unrefs for uid_item\n");
+
+  uid->refcount--;
+  /* We do not release the item here because that would require that
+   * we locate the head of the list which has this item.  This will
+   * take too long and thus the item is removed when we need to purge
+   * some items for the list during uid_item_put.  */
+}
+
+
+/* Put (NAME,NAMELEN) into the UID_TABLE and return the item.  The
+ * reference count for that item is incremented.  NULL is return on an
+ * allocation error.  The caller should release the returned item
+ * using uid_item_unref.  */
+static uid_item_t
+uid_table_put (const char *name, unsigned int namelen)
+{
+  unsigned int hash;
+  uid_item_t ui;
+  unsigned int count;
+
+  if (!uid_table)
+    uid_table_init ();
+
+  hash = uid_table_hasher (name, namelen);
+  for (ui = uid_table[hash], count = 0; ui; ui = ui->next, count++)
+    if (ui->namelen == namelen && !memcmp (ui->name, name, namelen))
+      return uid_item_ref (ui);  /* Found.  */
+
+  /* If the bucket is full remove all unrefed items.  */
+  if (count >= uid_table_max)
+    {
+      uid_item_t ui_next, ui_prev, list_head, drop_head;
+
+      /* No syscalls from here .. */
+      list_head = uid_table[hash];
+      drop_head = NULL;
+      while (list_head && !list_head->refcount)
+        {
+          ui = list_head;
+          list_head = ui->next;
+          ui->next = drop_head;
+          drop_head = ui;
+        }
+      if ((ui_prev = list_head))
+        for (ui = ui_prev->next; ui; ui = ui_next)
+          {
+            ui_next = ui->next;
+            if (!ui->refcount)
+              {
+                ui->next = drop_head;
+                drop_head = ui;
+                ui_prev->next = ui_next;
+              }
+            else
+              ui_prev = ui;
+          }
+      uid_table[hash] = list_head;
+      /* ... to here */
+
+      for (ui = drop_head; ui; ui = ui_next)
+        {
+          ui_next = ui->next;
+          xfree (ui);
+          uid_table_dropped++;
+        }
+    }
+
+  count = uid_table_added + uid_table_dropped;
+  ui = xtrycalloc (1, sizeof *ui + namelen);
+  if (!ui)
+    return NULL;  /* Out of core.  */
+  if (count != uid_table_added + uid_table_dropped)
+    {
+      /* During the malloc another thread added an item.  Thus we need
+       * to check again.  */
+      uid_item_t ui_new = ui;
+      for (ui = uid_table[hash]; ui; ui = ui->next)
+        if (ui->namelen == namelen && !memcmp (ui->name, name, namelen))
+          {
+            /* Found.  */
+            xfree (ui_new);
+            return uid_item_ref (ui);
+          }
+      ui = ui_new;
+    }
+
+  memcpy (ui->name, name, namelen);
+  ui->name[namelen] = 0; /* Extra Nul so we can use it as a string.  */
+  ui->namelen = namelen;
+  ui->refcount = 1;
+  ui->next = uid_table[hash];
+  uid_table[hash] = ui;
+  uid_table_added++;
+  return ui;
+}
+
+
+\f
+/* The hash function we use for the key_table.  Must not call a system
+ * function.  */
+static inline unsigned int
+key_table_hasher (u32 *keyid)
+{
+  /* A fingerprint could be used directly as a hash value.  However,
+   * we use the keyid here because it is used in encrypted packets and
+   * older signatures to identify a key.  Since v4 keys the keyid is
+   * anyway a part of the fingerprint so it quickly extracted from a
+   * fingerprint.  Note that v3 keys are not supported by gpg.  */
+  return keyid[0] % key_table_size;
+}
+
+
+/* Run time allocation of the key table.  This allows us to eventually
+ * add an option to gpg to control the size.  */
+static void
+key_table_init (void)
+{
+  if (key_table)
+    return;
+  key_table_size = NO_OF_KEY_ITEM_BUCKETS;
+  key_table_max  = MAX_KEY_ITEMS_PER_BUCKET;
+  key_table = xcalloc (key_table_size, sizeof *key_table);
+}
+
+
+static void
+key_item_free (key_item_t ki)
+{
+  if (!ki)
+    return;
+  uid_item_unref (ki->ui);
+  ki->ui = NULL;
+  ki->next = key_item_attic;
+  key_item_attic = ki;
+}
+
+
+/* Get a key item from PK or if that is NULL from KEYID.  The
+ * reference count for that item is incremented.  NULL is return if it
+ * was not found.  */
+static key_item_t
+key_table_get (PKT_public_key *pk, u32 *keyid)
+{
+  unsigned int hash;
+  key_item_t ki, ki2;
+
+  if (!key_table)
+    key_table_init ();
+
+  if (pk)
+    {
+      byte fpr[MAX_FINGERPRINT_LEN];
+      size_t fprlen;
+      u32 tmpkeyid[2];
+
+      fingerprint_from_pk (pk, fpr, &fprlen);
+      keyid_from_pk (pk, tmpkeyid);
+      hash = key_table_hasher (tmpkeyid);
+      for (ki = key_table[hash]; ki; ki = ki->next)
+        if (ki->fprlen == fprlen && !memcmp (ki->fpr, fpr, fprlen))
+          return ki; /* Found */
+    }
+  else if (keyid)
+    {
+      hash = key_table_hasher (keyid);
+      for (ki = key_table[hash]; ki; ki = ki->next)
+        if (ki->keyid[0] == keyid[0] && ki->keyid[1] == keyid[1])
+          {
+            /* Found.  We need to check for dups.  */
+            for (ki2 = ki->next; ki2; ki2 = ki2->next)
+              if (ki2->keyid[0] == keyid[0] && ki2->keyid[1] == keyid[1])
+                return NULL;  /* Duplicated keyid - retrun NULL.  */
+
+            /* This is the only one - return it.  */
+            return ki;
+          }
+    }
+  return NULL;
+}
+
+
+/* Helper for the qsort in key_table_put.  */
+static int
+compare_key_items (const void *arg_a, const void *arg_b)
+{
+  const key_item_t a = *(const key_item_t *)arg_a;
+  const key_item_t b = *(const key_item_t *)arg_b;
+
+  /* Reverse sort on the usecount.  */
+  if (a->usecount > b->usecount)
+    return -1;
+  else if (a->usecount == b->usecount)
+    return 0;
+  else
+    return 1;
+}
+
+
+/* Put PK into the KEY_TABLE and return a key item.  The reference
+ * count for that item is incremented.  If UI is given it is put into
+ * the entry.  NULL is return on an allocation error.  */
+static key_item_t
+key_table_put (PKT_public_key *pk, uid_item_t ui)
+{
+  unsigned int hash;
+  key_item_t ki;
+  u32 keyid[2];
+  byte fpr[MAX_FINGERPRINT_LEN];
+  size_t fprlen;
+  unsigned int count, n;
+
+  if (!key_table)
+    key_table_init ();
+
+  fingerprint_from_pk (pk, fpr, &fprlen);
+  keyid_from_pk (pk, keyid);
+  hash = key_table_hasher (keyid);
+  for (ki = key_table[hash], count=0; ki; ki = ki->next, count++)
+    if (ki->fprlen == fprlen && !memcmp (ki->fpr, fpr, fprlen))
+      return ki;  /* Found  */
+
+  /* If the bucket is full remove a couple of items. */
+  if (count >= key_table_max)
+    {
+      key_item_t list_head, *list_tailp, ki_next;
+      key_item_t *array;
+      int narray, idx;
+
+      /* Unlink from the global list so that other threads don't
+       * disturb us.  If another thread adds or removes something only
+       * one will be the winner.  Bad luck for the drooped cache items
+       * but after all it is just a cache.  */
+      list_head = key_table[hash];
+      key_table[hash] = NULL;
+
+      /* Put all items into an array for sorting.  */
+      array = xtrycalloc (count, sizeof *array);
+      if (!array)
+        {
+          /* That's bad; give up all  items of the bucket.  */
+          log_info ("Note: malloc failed while purging from the key_tabe: %s\n",
+                    gpg_strerror (gpg_error_from_syserror ()));
+          goto leave_drop;
+        }
+      narray = 0;
+      for (ki = list_head; ki; ki = ki_next)
+        {
+          ki_next = ki->next;
+          array[narray++] = ki;
+          ki->next = NULL;
+        }
+      log_assert (narray == count);
+
+      /* Sort the array and put half of it onto a new list.  */
+      qsort (array, narray, sizeof *array, compare_key_items);
+      list_head = NULL;
+      list_tailp = &list_head;
+      for (idx=0; idx < narray/2; idx++)
+        {
+          *list_tailp = array[idx];
+          list_tailp = &array[idx]->next;
+        }
+
+      /* Put the new list into the bucket.  */
+      ki = key_table[hash];
+      key_table[hash] = list_head;
+      list_head = ki;
+
+      /* Free the remaining items and the array.  */
+      for (; idx < narray; idx++)
+        {
+          key_item_free (array[idx]);
+          key_table_dropped++;
+        }
+      xfree (array);
+
+    leave_drop:
+      /* Free any items added in the meantime by other threads.  This
+       * is also used in case of a malloc problem (which won't update
+       * the counters, though). */
+      for ( ; list_head; list_head = ki_next)
+        {
+          ki_next = list_head->next;
+          key_item_free (list_head);
+        }
+    }
+
+  /* Add an item to the bucket.  We allocate a whole block of items
+   * for cache performace reasons.  */
+  if (!key_item_attic)
+    {
+      key_item_t kiblock;
+      int kiblocksize = 256;
+
+      kiblock = xtrymalloc (kiblocksize * sizeof *kiblock);
+      if (!kiblock)
+        return NULL;  /* Out of core.  */
+      for (n = 0; n < kiblocksize; n++)
+        {
+          ki = kiblock + n;
+          ki->next = key_item_attic;
+          key_item_attic = ki;
+        }
+
+      /* During the malloc another thread may have changed the bucket.
+       * Thus we need to check again.  */
+      for (ki = key_table[hash]; ki; ki = ki->next)
+        if (ki->fprlen == fprlen && !memcmp (ki->fpr, fpr, fprlen))
+          return ki;  /* Found  */
+    }
+
+  /* We now know that there is an item in the attic.  */
+  ki = key_item_attic;
+  key_item_attic = ki->next;
+  ki->next = NULL;
+
+  memcpy (ki->fpr, fpr, fprlen);
+  ki->fprlen = fprlen;
+  ki->keyid[0] = keyid[0];
+  ki->keyid[1] = keyid[1];
+  ki->ui = uid_item_ref (ui);
+  ki->usecount = 0;
+  ki->next = key_table[hash];
+  key_table[hash] = ki;
+  key_table_added++;
+  return ki;
+}
+
+
+\f
+/* Return the user ID from the given keyblock.  We use the primary uid
+ * flag which should have already been set.  The returned value is
+ * only valid as long as the given keyblock is not changed. */
+static const char *
+primary_uid_from_keyblock (kbnode_t keyblock, size_t *uidlen)
+{
+  kbnode_t k;
+
+  for (k = keyblock; k; k = k->next)
+    {
+      if (k->pkt->pkttype == PKT_USER_ID
+         && !k->pkt->pkt.user_id->attrib_data
+         && k->pkt->pkt.user_id->flags.primary)
+       {
+         *uidlen = k->pkt->pkt.user_id->len;
+         return k->pkt->pkt.user_id->name;
+       }
+    }
+  return NULL;
+}
+
+
+/* Store the associations of keyid/fingerprint and userid.  Only
+ * public keys should be fed to this function.  */
+void
+cache_put_keyblock (kbnode_t keyblock)
+{
+  uid_item_t ui = NULL;
+  kbnode_t k;
+
+ restart:
+  for (k = keyblock; k; k = k->next)
+    {
+      if (k->pkt->pkttype == PKT_PUBLIC_KEY
+         || k->pkt->pkttype == PKT_PUBLIC_SUBKEY)
+       {
+          if (!ui)
+            {
+              /* Initially we just test for an entry to avoid the need
+               * to create a user id item for a put.  Only if we miss
+               * key in the cache we create a user id and restart.  */
+              if (!key_table_get (k->pkt->pkt.public_key, NULL))
+                {
+                  const char *uid;
+                  size_t uidlen;
+
+                  uid = primary_uid_from_keyblock (keyblock, &uidlen);
+                  if (uid)
+                    {
+                      ui = uid_table_put (uid, uidlen);
+                      if (!ui)
+                        {
+                          log_info ("Note: failed to cache a user id: %s\n",
+                                    gpg_strerror (gpg_error_from_syserror ()));
+                          goto leave;
+                        }
+                      goto restart;
+                    }
+                }
+            }
+          else /* With a UID we use the update cache mode.  */
+            {
+              if (!key_table_put (k->pkt->pkt.public_key, ui))
+                {
+                  log_info ("Note: failed to cache a key: %s\n",
+                            gpg_strerror (gpg_error_from_syserror ()));
+                  goto leave;
+                }
+            }
+        }
+    }
+
+ leave:
+  uid_item_unref (ui);
+}
+
+
+/* Return the user id string for KEYID.  If a user id is not found (or
+ * on malloc error) NULL is returned.  If R_LENGTH is not NULL the
+ * length of the user id is stored there; this does not included the
+ * always appended nul.  Note that a user id may include an internal
+ * nul which can be detected by the caller by comparing to the
+ * returned length.  */
+char *
+cache_get_uid_bykid (u32 *keyid, unsigned int *r_length)
+{
+  key_item_t ki;
+  char *p;
+
+  if (r_length)
+    *r_length = 0;
+
+  ki = key_table_get (NULL, keyid);
+  if (!ki)
+    return NULL; /* Not found or duplicate keyid.  */
+
+  if (!ki->ui)
+    p = NULL;  /* No user id known for key.  */
+  else
+    {
+      p = xtrymalloc (ki->ui->namelen + 1);
+      if (p)
+        {
+          memcpy (p, ki->ui->name, ki->ui->namelen + 1);
+          if (r_length)
+            *r_length = ki->ui->namelen;
+          ki->usecount++;
+        }
+    }
+
+  return p;
+}
diff --git a/g10/objcache.h b/g10/objcache.h
new file mode 100644 (file)
index 0000000..e926791
--- /dev/null
@@ -0,0 +1,28 @@
+/* objcache.h - Caching functions for keys and user ids.
+ * Copyright (C) 2019 g10 Code GmbH
+ *
+ * This file is part of GnuPG.
+ *
+ * GnuPG 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GnuPG 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, see <https://www.gnu.org/licenses/>.
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#ifndef GNUPG_G10_OBJCACHE_H
+#define GNUPG_G10_OBJCACHE_H
+
+void objcache_dump_stats (void);
+void cache_put_keyblock (kbnode_t keyblock);
+char *cache_get_uid_bykid (u32 *keyid, unsigned int *r_length);
+
+#endif /*GNUPG_G10_OBJCACHE_H*/