xref: /third_party/mesa3d/src/util/fossilize_db.c (revision bf215546)
1/*
2 * Copyright © 2020 Valve Corporation
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a
5 * copy of this software and associated documentation files (the "Software"),
6 * to deal in the Software without restriction, including without limitation
7 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
8 * and/or sell copies of the Software, and to permit persons to whom the
9 * Software is furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice (including the next
12 * paragraph) shall be included in all copies or substantial portions of the
13 * Software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
18 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21 * IN THE SOFTWARE.
22 */
23
24/* This is a basic c implementation of a fossilize db like format intended for
25 * use with the Mesa shader cache.
26 *
27 * The format is compatible enough to allow the fossilize db tools to be used
28 * to do things like merge db collections.
29 */
30
31#include "fossilize_db.h"
32
33#ifdef FOZ_DB_UTIL
34
35#include <assert.h>
36#include <stddef.h>
37#include <stdlib.h>
38#include <string.h>
39#include <sys/file.h>
40#include <sys/types.h>
41#include <unistd.h>
42
43#include "crc32.h"
44#include "hash_table.h"
45#include "mesa-sha1.h"
46#include "ralloc.h"
47
48#define FOZ_REF_MAGIC_SIZE 16
49
50static const uint8_t stream_reference_magic_and_version[FOZ_REF_MAGIC_SIZE] = {
51   0x81, 'F', 'O', 'S',
52   'S', 'I', 'L', 'I',
53   'Z', 'E', 'D', 'B',
54   0, 0, 0, FOSSILIZE_FORMAT_VERSION, /* 4 bytes to use for versioning. */
55};
56
57/* Mesa uses 160bit hashes to identify cache entries, a hash of this size
58 * makes collisions virtually impossible for our use case. However the foz db
59 * format uses a 64bit hash table to lookup file offsets for reading cache
60 * entries so we must shorten our hash.
61 */
62static uint64_t
63truncate_hash_to_64bits(const uint8_t *cache_key)
64{
65   uint64_t hash = 0;
66   unsigned shift = 7;
67   for (unsigned i = 0; i < 8; i++) {
68      hash |= ((uint64_t)cache_key[i]) << shift * 8;
69      shift--;
70   }
71   return hash;
72}
73
74static bool
75check_files_opened_successfully(FILE *file, FILE *db_idx)
76{
77   if (!file) {
78      if (db_idx)
79         fclose(db_idx);
80      return false;
81   }
82
83   if (!db_idx) {
84      if (file)
85         fclose(file);
86      return false;
87   }
88
89   return true;
90}
91
92static bool
93create_foz_db_filenames(char *cache_path, char *name, char **filename,
94                        char **idx_filename)
95{
96   if (asprintf(filename, "%s/%s.foz", cache_path, name) == -1)
97      return false;
98
99   if (asprintf(idx_filename, "%s/%s_idx.foz", cache_path, name) == -1) {
100      free(*filename);
101      return false;
102   }
103
104   return true;
105}
106
107
108/* This looks at stuff that was added to the index since the last time we looked at it. This is safe
109 * to do without locking the file as we assume the file is append only */
110static void
111update_foz_index(struct foz_db *foz_db, FILE *db_idx, unsigned file_idx)
112{
113   uint64_t offset = ftell(db_idx);
114   fseek(db_idx, 0, SEEK_END);
115   uint64_t len = ftell(db_idx);
116   uint64_t parsed_offset = offset;
117
118   if (offset == len)
119      return;
120
121   fseek(db_idx, offset, SEEK_SET);
122   while (offset < len) {
123      char bytes_to_read[FOSSILIZE_BLOB_HASH_LENGTH + sizeof(struct foz_payload_header)];
124      struct foz_payload_header *header;
125
126      /* Corrupt entry. Our process might have been killed before we
127       * could write all data.
128       */
129      if (offset + sizeof(bytes_to_read) > len)
130         break;
131
132      /* NAME + HEADER in one read */
133      if (fread(bytes_to_read, 1, sizeof(bytes_to_read), db_idx) !=
134          sizeof(bytes_to_read))
135         break;
136
137      offset += sizeof(bytes_to_read);
138      header = (struct foz_payload_header*)&bytes_to_read[FOSSILIZE_BLOB_HASH_LENGTH];
139
140      /* Corrupt entry. Our process might have been killed before we
141       * could write all data.
142       */
143      if (offset + header->payload_size > len ||
144          header->payload_size != sizeof(uint64_t))
145         break;
146
147      char hash_str[FOSSILIZE_BLOB_HASH_LENGTH + 1] = {0};
148      memcpy(hash_str, bytes_to_read, FOSSILIZE_BLOB_HASH_LENGTH);
149
150      /* read cache item offset from index file */
151      uint64_t cache_offset;
152      if (fread(&cache_offset, 1, sizeof(cache_offset), db_idx) !=
153          sizeof(cache_offset))
154         break;
155
156      offset += header->payload_size;
157      parsed_offset = offset;
158
159      struct foz_db_entry *entry = ralloc(foz_db->mem_ctx,
160                                          struct foz_db_entry);
161      entry->header = *header;
162      entry->file_idx = file_idx;
163      _mesa_sha1_hex_to_sha1(entry->key, hash_str);
164
165      /* Truncate the entry's hash string to a 64bit hash for use with a
166       * 64bit hash table for looking up file offsets.
167       */
168      hash_str[16] = '\0';
169      uint64_t key = strtoull(hash_str, NULL, 16);
170
171      entry->offset = cache_offset;
172
173      _mesa_hash_table_u64_insert(foz_db->index_db, key, entry);
174   }
175
176
177   fseek(db_idx, parsed_offset, SEEK_SET);
178}
179
180/* exclusive flock with timeout. timeout is in nanoseconds */
181static int lock_file_with_timeout(FILE *f, int64_t timeout)
182{
183   int err;
184   int fd = fileno(f);
185   int64_t iterations = MAX2(DIV_ROUND_UP(timeout, 1000000), 1);
186
187   /* Since there is no blocking flock with timeout and we don't want to totally spin on getting the
188    * lock, use a nonblocking method and retry every millisecond. */
189   for (int64_t iter = 0; iter < iterations; ++iter) {
190      err = flock(fd, LOCK_EX | LOCK_NB);
191      if (err == 0 || errno != EAGAIN)
192         break;
193      usleep(1000);
194   }
195   return err;
196}
197
198static bool
199load_foz_dbs(struct foz_db *foz_db, FILE *db_idx, uint8_t file_idx,
200             bool read_only)
201{
202   /* Scan through the archive and get the list of cache entries. */
203   fseek(db_idx, 0, SEEK_END);
204   size_t len = ftell(db_idx);
205   rewind(db_idx);
206
207   /* Try not to take the lock if len >= the size of the header, but if it is smaller we take the
208    * lock to potentially initialize the files. */
209   if (len < sizeof(stream_reference_magic_and_version)) {
210      /* Wait for 100 ms in case of contention, after that we prioritize getting the app started. */
211      int err = lock_file_with_timeout(foz_db->file[file_idx], 100000000);
212      if (err == -1)
213         goto fail;
214
215      /* Compute length again so we know nobody else did it in the meantime */
216      fseek(db_idx, 0, SEEK_END);
217      len = ftell(db_idx);
218      rewind(db_idx);
219   }
220
221   if (len != 0) {
222      uint8_t magic[FOZ_REF_MAGIC_SIZE];
223      if (fread(magic, 1, FOZ_REF_MAGIC_SIZE, db_idx) != FOZ_REF_MAGIC_SIZE)
224         goto fail;
225
226      if (memcmp(magic, stream_reference_magic_and_version,
227                 FOZ_REF_MAGIC_SIZE - 1))
228         goto fail;
229
230      int version = magic[FOZ_REF_MAGIC_SIZE - 1];
231      if (version > FOSSILIZE_FORMAT_VERSION ||
232          version < FOSSILIZE_FORMAT_MIN_COMPAT_VERSION)
233         goto fail;
234
235   } else {
236      /* Appending to a fresh file. Make sure we have the magic. */
237      if (fwrite(stream_reference_magic_and_version, 1,
238                 sizeof(stream_reference_magic_and_version), foz_db->file[file_idx]) !=
239          sizeof(stream_reference_magic_and_version))
240         goto fail;
241
242      if (fwrite(stream_reference_magic_and_version, 1,
243                 sizeof(stream_reference_magic_and_version), db_idx) !=
244          sizeof(stream_reference_magic_and_version))
245         goto fail;
246
247      fflush(foz_db->file[file_idx]);
248      fflush(db_idx);
249   }
250
251   flock(fileno(foz_db->file[file_idx]), LOCK_UN);
252
253   update_foz_index(foz_db, db_idx, file_idx);
254
255   foz_db->alive = true;
256   return true;
257
258fail:
259   flock(fileno(foz_db->file[file_idx]), LOCK_UN);
260   foz_destroy(foz_db);
261   return false;
262}
263
264/* Here we open mesa cache foz dbs files. If the files exist we load the index
265 * db into a hash table. The index db contains the offsets needed to later
266 * read cache entries from the foz db containing the actual cache entries.
267 */
268bool
269foz_prepare(struct foz_db *foz_db, char *cache_path)
270{
271   char *filename = NULL;
272   char *idx_filename = NULL;
273   if (!create_foz_db_filenames(cache_path, "foz_cache", &filename, &idx_filename))
274      return false;
275
276   /* Open the default foz dbs for read/write. If the files didn't already exist
277    * create them.
278    */
279   foz_db->file[0] = fopen(filename, "a+b");
280   foz_db->db_idx = fopen(idx_filename, "a+b");
281
282   free(filename);
283   free(idx_filename);
284
285   if (!check_files_opened_successfully(foz_db->file[0], foz_db->db_idx))
286      return false;
287
288   simple_mtx_init(&foz_db->mtx, mtx_plain);
289   simple_mtx_init(&foz_db->flock_mtx, mtx_plain);
290   foz_db->mem_ctx = ralloc_context(NULL);
291   foz_db->index_db = _mesa_hash_table_u64_create(NULL);
292
293   if (!load_foz_dbs(foz_db, foz_db->db_idx, 0, false))
294      return false;
295
296   uint8_t file_idx = 1;
297   char *foz_dbs = getenv("MESA_DISK_CACHE_READ_ONLY_FOZ_DBS");
298   if (!foz_dbs)
299      return true;
300
301   for (unsigned n; n = strcspn(foz_dbs, ","), *foz_dbs;
302        foz_dbs += MAX2(1, n)) {
303      char *foz_db_filename = strndup(foz_dbs, n);
304
305      filename = NULL;
306      idx_filename = NULL;
307      if (!create_foz_db_filenames(cache_path, foz_db_filename, &filename,
308                                   &idx_filename)) {
309         free(foz_db_filename);
310         continue; /* Ignore invalid user provided filename and continue */
311      }
312      free(foz_db_filename);
313
314      /* Open files as read only */
315      foz_db->file[file_idx] = fopen(filename, "rb");
316      FILE *db_idx = fopen(idx_filename, "rb");
317
318      free(filename);
319      free(idx_filename);
320
321      if (!check_files_opened_successfully(foz_db->file[file_idx], db_idx)) {
322         /* Prevent foz_destroy from destroying it a second time. */
323         foz_db->file[file_idx] = NULL;
324
325         continue; /* Ignore invalid user provided filename and continue */
326      }
327
328      if (!load_foz_dbs(foz_db, db_idx, file_idx, true)) {
329         fclose(db_idx);
330         return false;
331      }
332
333      fclose(db_idx);
334      file_idx++;
335
336      if (file_idx >= FOZ_MAX_DBS)
337         break;
338   }
339
340   return true;
341}
342
343void
344foz_destroy(struct foz_db *foz_db)
345{
346   if (foz_db->db_idx)
347      fclose(foz_db->db_idx);
348   for (unsigned i = 0; i < FOZ_MAX_DBS; i++) {
349      if (foz_db->file[i])
350         fclose(foz_db->file[i]);
351   }
352
353   if (foz_db->mem_ctx) {
354      _mesa_hash_table_u64_destroy(foz_db->index_db);
355      ralloc_free(foz_db->mem_ctx);
356      simple_mtx_destroy(&foz_db->flock_mtx);
357      simple_mtx_destroy(&foz_db->mtx);
358   }
359}
360
361/* Here we lookup a cache entry in the index hash table. If an entry is found
362 * we use the retrieved offset to read the cache entry from disk.
363 */
364void *
365foz_read_entry(struct foz_db *foz_db, const uint8_t *cache_key_160bit,
366               size_t *size)
367{
368   uint64_t hash = truncate_hash_to_64bits(cache_key_160bit);
369
370   void *data = NULL;
371
372   if (!foz_db->alive)
373      return NULL;
374
375   simple_mtx_lock(&foz_db->mtx);
376
377   struct foz_db_entry *entry =
378      _mesa_hash_table_u64_search(foz_db->index_db, hash);
379   if (!entry) {
380      update_foz_index(foz_db, foz_db->db_idx, 0);
381      entry = _mesa_hash_table_u64_search(foz_db->index_db, hash);
382   }
383   if (!entry) {
384      simple_mtx_unlock(&foz_db->mtx);
385      return NULL;
386   }
387
388   uint8_t file_idx = entry->file_idx;
389   if (fseek(foz_db->file[file_idx], entry->offset, SEEK_SET) < 0)
390      goto fail;
391
392   uint32_t header_size = sizeof(struct foz_payload_header);
393   if (fread(&entry->header, 1, header_size, foz_db->file[file_idx]) !=
394       header_size)
395      goto fail;
396
397   /* Check for collision using full 160bit hash for increased assurance
398    * against potential collisions.
399    */
400   for (int i = 0; i < 20; i++) {
401      if (cache_key_160bit[i] != entry->key[i])
402         goto fail;
403   }
404
405   uint32_t data_sz = entry->header.payload_size;
406   data = malloc(data_sz);
407   if (fread(data, 1, data_sz, foz_db->file[file_idx]) != data_sz)
408      goto fail;
409
410   /* verify checksum */
411   if (entry->header.crc != 0) {
412      if (util_hash_crc32(data, data_sz) != entry->header.crc)
413         goto fail;
414   }
415
416   simple_mtx_unlock(&foz_db->mtx);
417
418   if (size)
419      *size = data_sz;
420
421   return data;
422
423fail:
424   free(data);
425
426   /* reading db entry failed. reset the file offset */
427   simple_mtx_unlock(&foz_db->mtx);
428
429   return NULL;
430}
431
432/* Here we write the cache entry to disk and store its offset in the index db.
433 */
434bool
435foz_write_entry(struct foz_db *foz_db, const uint8_t *cache_key_160bit,
436                const void *blob, size_t blob_size)
437{
438   uint64_t hash = truncate_hash_to_64bits(cache_key_160bit);
439
440   if (!foz_db->alive)
441      return false;
442
443   /* The flock is per-fd, not per thread, we do it outside of the main mutex to avoid having to
444    * wait in the mutex potentially blocking reads. We use the secondary flock_mtx to stop race
445    * conditions between the write threads sharing the same file descriptor. */
446   simple_mtx_lock(&foz_db->flock_mtx);
447
448   /* Wait for 1 second. This is done outside of the main mutex as I believe there is more potential
449    * for file contention than mtx contention of significant length. */
450   int err = lock_file_with_timeout(foz_db->file[0], 1000000000);
451   if (err == -1)
452      goto fail_file;
453
454   simple_mtx_lock(&foz_db->mtx);
455
456   update_foz_index(foz_db, foz_db->db_idx, 0);
457
458   struct foz_db_entry *entry =
459      _mesa_hash_table_u64_search(foz_db->index_db, hash);
460   if (entry) {
461      simple_mtx_unlock(&foz_db->mtx);
462      flock(fileno(foz_db->file[0]), LOCK_UN);
463      simple_mtx_unlock(&foz_db->flock_mtx);
464      return NULL;
465   }
466
467   /* Prepare db entry header and blob ready for writing */
468   struct foz_payload_header header;
469   header.uncompressed_size = blob_size;
470   header.format = FOSSILIZE_COMPRESSION_NONE;
471   header.payload_size = blob_size;
472   header.crc = util_hash_crc32(blob, blob_size);
473
474   fseek(foz_db->file[0], 0, SEEK_END);
475
476   /* Write hash header to db */
477   char hash_str[FOSSILIZE_BLOB_HASH_LENGTH + 1]; /* 40 digits + null */
478   _mesa_sha1_format(hash_str, cache_key_160bit);
479   if (fwrite(hash_str, 1, FOSSILIZE_BLOB_HASH_LENGTH, foz_db->file[0]) !=
480       FOSSILIZE_BLOB_HASH_LENGTH)
481      goto fail;
482
483   off_t offset = ftell(foz_db->file[0]);
484
485   /* Write db entry header */
486   if (fwrite(&header, 1, sizeof(header), foz_db->file[0]) != sizeof(header))
487      goto fail;
488
489   /* Now write the db entry blob */
490   if (fwrite(blob, 1, blob_size, foz_db->file[0]) != blob_size)
491      goto fail;
492
493   /* Flush everything to file to reduce chance of cache corruption */
494   fflush(foz_db->file[0]);
495
496   /* Write hash header to index db */
497   if (fwrite(hash_str, 1, FOSSILIZE_BLOB_HASH_LENGTH, foz_db->db_idx) !=
498       FOSSILIZE_BLOB_HASH_LENGTH)
499      goto fail;
500
501   header.uncompressed_size = sizeof(uint64_t);
502   header.format = FOSSILIZE_COMPRESSION_NONE;
503   header.payload_size = sizeof(uint64_t);
504   header.crc = 0;
505
506   if (fwrite(&header, 1, sizeof(header), foz_db->db_idx) !=
507       sizeof(header))
508      goto fail;
509
510   if (fwrite(&offset, 1, sizeof(uint64_t), foz_db->db_idx) !=
511       sizeof(uint64_t))
512      goto fail;
513
514   /* Flush everything to file to reduce chance of cache corruption */
515   fflush(foz_db->db_idx);
516
517   entry = ralloc(foz_db->mem_ctx, struct foz_db_entry);
518   entry->header = header;
519   entry->offset = offset;
520   entry->file_idx = 0;
521   _mesa_sha1_hex_to_sha1(entry->key, hash_str);
522   _mesa_hash_table_u64_insert(foz_db->index_db, hash, entry);
523
524   simple_mtx_unlock(&foz_db->mtx);
525   flock(fileno(foz_db->file[0]), LOCK_UN);
526   simple_mtx_unlock(&foz_db->flock_mtx);
527
528   return true;
529
530fail:
531   simple_mtx_unlock(&foz_db->mtx);
532fail_file:
533   flock(fileno(foz_db->file[0]), LOCK_UN);
534   simple_mtx_unlock(&foz_db->flock_mtx);
535   return false;
536}
537#else
538
539bool
540foz_prepare(struct foz_db *foz_db, char *filename)
541{
542   fprintf(stderr, "Warning: Mesa single file cache selected but Mesa wasn't "
543           "built with single cache file support. Shader cache will be disabled"
544           "!\n");
545   return false;
546}
547
548void
549foz_destroy(struct foz_db *foz_db)
550{
551}
552
553void *
554foz_read_entry(struct foz_db *foz_db, const uint8_t *cache_key_160bit,
555               size_t *size)
556{
557   return false;
558}
559
560bool
561foz_write_entry(struct foz_db *foz_db, const uint8_t *cache_key_160bit,
562                const void *blob, size_t size)
563{
564   return false;
565}
566
567#endif
568