/* File caching.
Copyright (C) 2023-2025 Free Software Foundation, Inc.
This file is part of GCC.
GCC is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free
Software Foundation; either version 3, or (at your option) any later
version.
GCC is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
along with GCC; see the file COPYING3. If not see
. */
#define INCLUDE_ALGORITHM
#define INCLUDE_STRING
#define INCLUDE_ARRAY
#define INCLUDE_MAP
#define INCLUDE_VECTOR
#include "config.h"
#include "system.h"
#include "sha1.h"
#include "lto-ltrans-cache.h"
static const checksum_t NULL_CHECKSUM = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
/* Computes checksum for given file, returns NULL_CHECKSUM if not
possible. */
static checksum_t
file_checksum (char const *filename)
{
FILE *file = fopen (filename, "rb");
if (!file)
return NULL_CHECKSUM;
checksum_t result = NULL_CHECKSUM;
int ret = sha1_stream (file, &result);
if (ret)
result = NULL_CHECKSUM;
fclose (file);
return result;
}
/* Checks identity of two files. */
static bool
files_identical (char const *first_filename, char const *second_filename)
{
bool ret = true;
#if HAVE_MMAP_FILE
struct stat st;
if (stat (first_filename, &st) < 0 || !S_ISREG (st.st_mode))
return false;
size_t len = st.st_size;
if (stat (second_filename, &st) < 0 || !S_ISREG (st.st_mode))
return false;
if (len != (size_t) st.st_size)
return false;
int fd;
void *map[2] = { NULL, NULL };
fd = open (first_filename, O_RDONLY);
if (fd < 0)
goto fail_mmap;
map[0] = mmap (NULL, len, PROT_READ, MAP_PRIVATE, fd, 0);
close (fd);
if (map[0] == MAP_FAILED)
goto fail_mmap;
fd = open (second_filename, O_RDONLY);
if (fd < 0)
goto fail_mmap2;
map[1] = mmap (NULL, len, PROT_READ, MAP_PRIVATE, fd, 0);
close (fd);
if (map[1] == MAP_FAILED)
goto fail_mmap2;
ret = memcmp (map[0], map[1], len) == 0;
munmap (map[1], len);
munmap (map[0], len);
return ret;
fail_mmap2:
munmap (map[0], len);
fail_mmap:
#endif
/* Fallback. */
FILE *f_first = fopen (first_filename, "rb");
if (!f_first)
return false;
FILE *f_second = fopen (second_filename, "rb");
if (!f_second)
{
fclose (f_first);
return false;
}
const size_t buffer_len = 64 * 1024;
uint8_t *buffer1 = (uint8_t*)xmalloc (buffer_len);
uint8_t *buffer2 = (uint8_t*)xmalloc (buffer_len);
size_t len1, len2;
do
{
len1 = fread (buffer1, 1, buffer_len, f_first);
len2 = fread (buffer2, 1, buffer_len, f_second);
if (len1 != len2 || memcmp (buffer1, buffer2, len1) != 0)
{
ret = false;
break;
}
}
while (len1 != 0);
free (buffer2);
free (buffer1);
fclose (f_first);
fclose (f_second);
return ret;
}
/* Contructor of cache item. */
ltrans_file_cache::item::item (std::string input, std::string output,
checksum_t input_checksum,
uint32_t last_used):
input (std::move (input)), output (std::move (output)),
input_checksum (input_checksum), last_used (last_used)
{
lock = lockfile (this->input + ".lock");
}
/* Destructor of cache item. */
ltrans_file_cache::item::~item ()
{
lock.unlock ();
}
/* Reads next cache item from cachedata file.
Adds `dir/` prefix to filenames. */
static ltrans_file_cache::item*
read_cache_item (FILE* f, const char* dir)
{
checksum_t checksum;
uint32_t last_used;
if (fread (&checksum, 1, checksum.size (), f) != checksum.size ())
return NULL;
if (fread (&last_used, sizeof (last_used), 1, f) != 1)
return NULL;
std::string input (dir);
input.push_back ('/');
std::string output = input; /* Copy. */
int c;
while ((c = getc (f)))
{
if (c == EOF)
return NULL;
input.push_back (c);
}
while ((c = getc (f)))
{
if (c == EOF)
return NULL;
output.push_back (c);
}
return new ltrans_file_cache::item (&input[0], &output[0], checksum,
last_used);
}
/* Writes cache item to cachedata file.
Removes `dir/` prefix from filenames. */
static void
write_cache_item (FILE* f, ltrans_file_cache::item *item, const char* dir)
{
fwrite (&item->input_checksum, 1, item->input_checksum.size (), f);
fwrite (&item->last_used, sizeof (item->last_used), 1, f);
gcc_assert (item->input.size () > strlen (dir));
fputs (item->input.c_str () + strlen (dir) + 1, f);
fputc (0, f);
gcc_assert (item->output.size () > strlen (dir));
fputs (item->output.c_str () + strlen (dir) + 1, f);
fputc (0, f);
}
/* Constructor. Resulting cache item filenames will be
in format `prefix%d[.ltrans]suffix`. */
ltrans_file_cache::ltrans_file_cache (const char* dir, const char* prefix,
const char* suffix,
size_t soft_cache_size):
dir (dir), prefix (prefix), suffix (suffix),
soft_cache_size (soft_cache_size)
{
if (!dir) return;
creation_lock = lockfile (std::string (dir) + "/lockfile_creation");
deletion_lock = lockfile (std::string (dir) + "/lockfile_deletion");
cache_prefix = std::string (dir) + "/" + prefix;
cache_free_idx = 0;
str_buffer = (char *)xmalloc (cache_prefix.size ()
+ sizeof ("4294967295.ltrans")
+ strlen (suffix));
}
/* Destructor. */
ltrans_file_cache::~ltrans_file_cache ()
{
if (!*this)
return;
cleanup ();
free (str_buffer);
}
/* Adds given cache item to all relevant datastructures. */
void
ltrans_file_cache::add_item (ltrans_file_cache::item* item)
{
items.push_back (item);
map_checksum[item->input_checksum] = item;
map_input[item->input] = item;
usage_counter = std::max (usage_counter, item->last_used);
}
/* Creates cachedata filename for save/load. */
std::string
ltrans_file_cache::filename_cachedata ()
{
return std::string (dir) + "/cachedata";
}
/* Loads data about previously cached items from cachedata file.
Sorts items by last_used and remaps last_used to small integers.
Must be called with creation_lock or deletion_lock held to
prevent data race. */
void
ltrans_file_cache::load_cache ()
{
cleanup ();
std::string filename = filename_cachedata ();
FILE *file = fopen (filename.c_str (), "rb");
if (!file)
return;
ltrans_file_cache::item* _item;
while ((_item = read_cache_item (file, dir)))
add_item (_item);
fclose (file);
std::sort (items.begin (), items.end (),
[](item* a, item* b)
{return a->last_used < b->last_used;});
for (size_t i = 0; i < items.size (); ++i)
items[i]->last_used = (uint32_t) i;
usage_counter = (uint32_t) items.size ();
}
/* Rewrites data about cache items into cachedata file.
Must be only called when creation_lock or deletion_lock was held since last
call to load_cache. */
void
ltrans_file_cache::save_cache ()
{
std::string filename = filename_cachedata ();
FILE *file = fopen (filename.c_str (), "wb");
if (!file)
return;
for (item* _item: items)
write_cache_item (file, _item, dir);
fclose (file);
}
/* Creates new cache item with given checksum.
New input/output files are chosen to not collide with other items.
Must be called with creation_lock held to prevent data race. */
ltrans_file_cache::item*
ltrans_file_cache::create_item (const checksum_t& checksum)
{
size_t prefix_len = cache_prefix.size ();
strcpy (str_buffer, cache_prefix.c_str ());
for (;;)
{
sprintf (str_buffer + prefix_len, "%04u%s", cache_free_idx, suffix);
if (map_input.find (str_buffer) == map_input.end ())
break;
cache_free_idx++;
}
std::string input = str_buffer;
sprintf (str_buffer + prefix_len, "%04u.ltrans%s", cache_free_idx, suffix);
return new item (std::move (input), str_buffer, checksum, usage_counter++);
}
/* Adds input file into cache. Cache item with input file identical to
added input file will be returned as _item.
If the file was already cached, `true` is returned, `false` otherwise.
The added input file is deleted (or moved).
Must be called with creation_lock held to prevent data race. */
bool
ltrans_file_cache::add_to_cache (const char* filename, item*& _item)
{
checksum_t checksum = file_checksum (filename);
auto it = map_checksum.find (checksum);
if (it != map_checksum.end ()
&& files_identical (filename, it->second->input.c_str ()))
{
unlink (filename);
_item = it->second;
_item->last_used = usage_counter++;
return true;
}
else
{
/* Cache miss. Move into cache dir. */
_item = create_item (checksum);
add_item (_item);
rename (filename, _item->input.c_str ());
return false;
}
}
/* If exists, returns cache item corresponding to cached input file. */
ltrans_file_cache::item*
ltrans_file_cache::get_item (const char* input)
{
auto it = map_input.find (input);
if (it == map_input.end ())
return NULL;
return it->second;
}
/* If no other process holds the deletion_lock, prunes oldest unused cache
items over limit. */
void
ltrans_file_cache::try_prune ()
{
if (deletion_lock.try_lock_write () == 0)
{
prune ();
deletion_lock.unlock ();
}
}
/* Returns true if file exists. */
static int
file_exists (char const *name)
{
return access (name, R_OK) == 0;
}
/* Prunes oldest unused cache items over limit.
Must be called with deletion_lock held to prevent data race. */
void
ltrans_file_cache::prune ()
{
load_cache ();
if (items.size () > soft_cache_size)
{
std::vector- sorted_items = std::move (items);
cleanup ();
for (size_t i = 0; i < sorted_items.size (); ++i)
{
ltrans_file_cache::item* item = sorted_items[i];
if ((i < sorted_items.size () - soft_cache_size)
|| !file_exists (item->input.c_str ())
|| !file_exists (item->output.c_str ()))
{
unlink (item->input.c_str ());
unlink (item->output.c_str ());
delete item;
}
else
add_item (item);
}
}
save_cache ();
}
/* Clears cache class, as if only constructor was called. */
void
ltrans_file_cache::cleanup ()
{
map_checksum.clear ();
map_input.clear ();
for (ltrans_file_cache::item* item: items)
delete item;
items.clear ();
usage_counter = 0;
}