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