• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
50 static 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  */
62 static uint64_t
truncate_hash_to_64bits(const uint8_t * cache_key)63 truncate_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 
74 static bool
check_files_opened_successfully(FILE * file,FILE * db_idx)75 check_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 
92 static bool
create_foz_db_filenames(char * cache_path,char * name,char ** filename,char ** idx_filename)93 create_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 */
110 static void
update_foz_index(struct foz_db * foz_db,FILE * db_idx,unsigned file_idx)111 update_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 */
lock_file_with_timeout(FILE * f,int64_t timeout)181 static 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 
198 static bool
load_foz_dbs(struct foz_db * foz_db,FILE * db_idx,uint8_t file_idx,bool read_only)199 load_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 
258 fail:
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  */
268 bool
foz_prepare(struct foz_db * foz_db,char * cache_path)269 foz_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          continue; /* Ignore invalid user provided filename and continue */
323 
324       if (!load_foz_dbs(foz_db, db_idx, file_idx, true)) {
325          fclose(db_idx);
326          return false;
327       }
328 
329       fclose(db_idx);
330       file_idx++;
331 
332       if (file_idx >= FOZ_MAX_DBS)
333          break;
334    }
335 
336    return true;
337 }
338 
339 void
foz_destroy(struct foz_db * foz_db)340 foz_destroy(struct foz_db *foz_db)
341 {
342    if (foz_db->db_idx)
343       fclose(foz_db->db_idx);
344    for (unsigned i = 0; i < FOZ_MAX_DBS; i++) {
345       if (foz_db->file[i])
346          fclose(foz_db->file[i]);
347    }
348 
349    if (foz_db->mem_ctx) {
350       _mesa_hash_table_u64_destroy(foz_db->index_db);
351       ralloc_free(foz_db->mem_ctx);
352       simple_mtx_destroy(&foz_db->flock_mtx);
353       simple_mtx_destroy(&foz_db->mtx);
354    }
355 }
356 
357 /* Here we lookup a cache entry in the index hash table. If an entry is found
358  * we use the retrieved offset to read the cache entry from disk.
359  */
360 void *
foz_read_entry(struct foz_db * foz_db,const uint8_t * cache_key_160bit,size_t * size)361 foz_read_entry(struct foz_db *foz_db, const uint8_t *cache_key_160bit,
362                size_t *size)
363 {
364    uint64_t hash = truncate_hash_to_64bits(cache_key_160bit);
365 
366    void *data = NULL;
367 
368    if (!foz_db->alive)
369       return NULL;
370 
371    simple_mtx_lock(&foz_db->mtx);
372 
373    struct foz_db_entry *entry =
374       _mesa_hash_table_u64_search(foz_db->index_db, hash);
375    if (!entry) {
376       update_foz_index(foz_db, foz_db->db_idx, 0);
377       entry = _mesa_hash_table_u64_search(foz_db->index_db, hash);
378    }
379    if (!entry) {
380       simple_mtx_unlock(&foz_db->mtx);
381       return NULL;
382    }
383 
384    uint8_t file_idx = entry->file_idx;
385    if (fseek(foz_db->file[file_idx], entry->offset, SEEK_SET) < 0)
386       goto fail;
387 
388    uint32_t header_size = sizeof(struct foz_payload_header);
389    if (fread(&entry->header, 1, header_size, foz_db->file[file_idx]) !=
390        header_size)
391       goto fail;
392 
393    /* Check for collision using full 160bit hash for increased assurance
394     * against potential collisions.
395     */
396    for (int i = 0; i < 20; i++) {
397       if (cache_key_160bit[i] != entry->key[i])
398          goto fail;
399    }
400 
401    uint32_t data_sz = entry->header.payload_size;
402    data = malloc(data_sz);
403    if (fread(data, 1, data_sz, foz_db->file[file_idx]) != data_sz)
404       goto fail;
405 
406    /* verify checksum */
407    if (entry->header.crc != 0) {
408       if (util_hash_crc32(data, data_sz) != entry->header.crc)
409          goto fail;
410    }
411 
412    simple_mtx_unlock(&foz_db->mtx);
413 
414    if (size)
415       *size = data_sz;
416 
417    return data;
418 
419 fail:
420    free(data);
421 
422    /* reading db entry failed. reset the file offset */
423    simple_mtx_unlock(&foz_db->mtx);
424 
425    return NULL;
426 }
427 
428 /* Here we write the cache entry to disk and store its offset in the index db.
429  */
430 bool
foz_write_entry(struct foz_db * foz_db,const uint8_t * cache_key_160bit,const void * blob,size_t blob_size)431 foz_write_entry(struct foz_db *foz_db, const uint8_t *cache_key_160bit,
432                 const void *blob, size_t blob_size)
433 {
434    uint64_t hash = truncate_hash_to_64bits(cache_key_160bit);
435 
436    if (!foz_db->alive)
437       return false;
438 
439    /* The flock is per-fd, not per thread, we do it outside of the main mutex to avoid having to
440     * wait in the mutex potentially blocking reads. We use the secondary flock_mtx to stop race
441     * conditions between the write threads sharing the same file descriptor. */
442    simple_mtx_lock(&foz_db->flock_mtx);
443 
444    /* Wait for 1 second. This is done outside of the main mutex as I believe there is more potential
445     * for file contention than mtx contention of significant length. */
446    int err = lock_file_with_timeout(foz_db->file[0], 1000000000);
447    if (err == -1)
448       goto fail_file;
449 
450    simple_mtx_lock(&foz_db->mtx);
451 
452    update_foz_index(foz_db, foz_db->db_idx, 0);
453 
454    struct foz_db_entry *entry =
455       _mesa_hash_table_u64_search(foz_db->index_db, hash);
456    if (entry) {
457       simple_mtx_unlock(&foz_db->mtx);
458       flock(fileno(foz_db->file[0]), LOCK_UN);
459       simple_mtx_unlock(&foz_db->flock_mtx);
460       return NULL;
461    }
462 
463    /* Prepare db entry header and blob ready for writing */
464    struct foz_payload_header header;
465    header.uncompressed_size = blob_size;
466    header.format = FOSSILIZE_COMPRESSION_NONE;
467    header.payload_size = blob_size;
468    header.crc = util_hash_crc32(blob, blob_size);
469 
470    fseek(foz_db->file[0], 0, SEEK_END);
471 
472    /* Write hash header to db */
473    char hash_str[FOSSILIZE_BLOB_HASH_LENGTH + 1]; /* 40 digits + null */
474    _mesa_sha1_format(hash_str, cache_key_160bit);
475    if (fwrite(hash_str, 1, FOSSILIZE_BLOB_HASH_LENGTH, foz_db->file[0]) !=
476        FOSSILIZE_BLOB_HASH_LENGTH)
477       goto fail;
478 
479    off_t offset = ftell(foz_db->file[0]);
480 
481    /* Write db entry header */
482    if (fwrite(&header, 1, sizeof(header), foz_db->file[0]) != sizeof(header))
483       goto fail;
484 
485    /* Now write the db entry blob */
486    if (fwrite(blob, 1, blob_size, foz_db->file[0]) != blob_size)
487       goto fail;
488 
489    /* Flush everything to file to reduce chance of cache corruption */
490    fflush(foz_db->file[0]);
491 
492    /* Write hash header to index db */
493    if (fwrite(hash_str, 1, FOSSILIZE_BLOB_HASH_LENGTH, foz_db->db_idx) !=
494        FOSSILIZE_BLOB_HASH_LENGTH)
495       goto fail;
496 
497    header.uncompressed_size = sizeof(uint64_t);
498    header.format = FOSSILIZE_COMPRESSION_NONE;
499    header.payload_size = sizeof(uint64_t);
500    header.crc = 0;
501 
502    if (fwrite(&header, 1, sizeof(header), foz_db->db_idx) !=
503        sizeof(header))
504       goto fail;
505 
506    if (fwrite(&offset, 1, sizeof(uint64_t), foz_db->db_idx) !=
507        sizeof(uint64_t))
508       goto fail;
509 
510    /* Flush everything to file to reduce chance of cache corruption */
511    fflush(foz_db->db_idx);
512 
513    entry = ralloc(foz_db->mem_ctx, struct foz_db_entry);
514    entry->header = header;
515    entry->offset = offset;
516    entry->file_idx = 0;
517    _mesa_sha1_hex_to_sha1(entry->key, hash_str);
518    _mesa_hash_table_u64_insert(foz_db->index_db, hash, entry);
519 
520    simple_mtx_unlock(&foz_db->mtx);
521    flock(fileno(foz_db->file[0]), LOCK_UN);
522    simple_mtx_unlock(&foz_db->flock_mtx);
523 
524    return true;
525 
526 fail:
527    simple_mtx_unlock(&foz_db->mtx);
528 fail_file:
529    flock(fileno(foz_db->file[0]), LOCK_UN);
530    simple_mtx_unlock(&foz_db->flock_mtx);
531    return false;
532 }
533 #else
534 
535 bool
foz_prepare(struct foz_db * foz_db,char * filename)536 foz_prepare(struct foz_db *foz_db, char *filename)
537 {
538    fprintf(stderr, "Warning: Mesa single file cache selected but Mesa wasn't "
539            "built with single cache file support. Shader cache will be disabled"
540            "!\n");
541    return false;
542 }
543 
544 void
foz_destroy(struct foz_db * foz_db)545 foz_destroy(struct foz_db *foz_db)
546 {
547 }
548 
549 void *
foz_read_entry(struct foz_db * foz_db,const uint8_t * cache_key_160bit,size_t * size)550 foz_read_entry(struct foz_db *foz_db, const uint8_t *cache_key_160bit,
551                size_t *size)
552 {
553    return false;
554 }
555 
556 bool
foz_write_entry(struct foz_db * foz_db,const uint8_t * cache_key_160bit,const void * blob,size_t size)557 foz_write_entry(struct foz_db *foz_db, const uint8_t *cache_key_160bit,
558                 const void *blob, size_t size)
559 {
560    return false;
561 }
562 
563 #endif
564