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 /* 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
343 void
foz_destroy(struct foz_db * foz_db)344 foz_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 */
364 void *
foz_read_entry(struct foz_db * foz_db,const uint8_t * cache_key_160bit,size_t * size)365 foz_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
423 fail:
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 */
434 bool
foz_write_entry(struct foz_db * foz_db,const uint8_t * cache_key_160bit,const void * blob,size_t blob_size)435 foz_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
530 fail:
531 simple_mtx_unlock(&foz_db->mtx);
532 fail_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
539 bool
foz_prepare(struct foz_db * foz_db,char * filename)540 foz_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
548 void
foz_destroy(struct foz_db * foz_db)549 foz_destroy(struct foz_db *foz_db)
550 {
551 }
552
553 void *
foz_read_entry(struct foz_db * foz_db,const uint8_t * cache_key_160bit,size_t * size)554 foz_read_entry(struct foz_db *foz_db, const uint8_t *cache_key_160bit,
555 size_t *size)
556 {
557 return false;
558 }
559
560 bool
foz_write_entry(struct foz_db * foz_db,const uint8_t * cache_key_160bit,const void * blob,size_t size)561 foz_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