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