Skip to content

Commit

Permalink
Add comments & cosmetic changes
Browse files Browse the repository at this point in the history
  • Loading branch information
timholy committed Oct 19, 2022
1 parent cd8d7b6 commit 4d56e46
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 109 deletions.
185 changes: 76 additions & 109 deletions src/staticdata.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,12 @@
/*
saving and restoring system images
This performs serialization and deserialization of in-memory data. The dump.c file is similar, but has less complete coverage:
dump.c has no knowledge of native code (and simply discards it), whereas this supports native code caching in .o files.
Duplication is avoided by elevating the .o-serialized versions of global variables and native-compiled functions to become
the authoritative source for such entities in the system image, with references to these objects appropriately inserted into
the (de)serialized version of Julia's internal data.
Another difference is that while dump.c defines a serialization format that it writes item-by-item, this serializer creates and
saves a compact binary blob. This makes deserialization simple and fast: we only need to deal with pointer relocation,
registering with the garbage collector, and making note of special internal types. During serialization, we also need to
pay special attention to things like builtin functions, C-implemented types (those in jltypes.c), the metadata for documentation,
optimal layouts, integration with native system image generation, and preparing other preprocessing directives.
dump.c has capabilities missing from this serializer, most notably the ability to manage (and merge) method tables.
This is not needed for system images as they are self-contained. However, it would be needed to support incremental
compilation of packages.
This performs serialization and deserialization of system and package images. It creates and saves a compact binary
blob, making deserialization "simple" and fast: we "only" need to deal with uniquing, pointer relocation,
method root insertion, registering with the garbage collector, making note of special internal types, and
backedges/invalidation. Special objects include things like builtin functions, C-implemented types (those in jltypes.c),
the metadata for documentation, optimal layouts, integration with native system image generation, and preparing other
preprocessing directives.
During serialization, the flow has several steps:
Expand Down Expand Up @@ -47,20 +38,24 @@
details of this encoding can be found in the pair of functions `get_reloc_for_item` and `get_item_for_reloc`.
`uniquing_list` also holds the serialized location of external DataTypes, MethodInstances, and singletons
in the serialized blob (i.e., new-at-the-time-of-serialization specializations). The target item must
be checked against the running system to see whether such an object already exists (i.e., whether some other
previously-loaded package or workload has created such types/MethodInstances previously). If so,
then the pointer at `location` must be updated to the one in the running system.
`uniquing_target` is a hash table for which `uniquing_target[targetpos] -> chosen_target`.
in the serialized blob (i.e., new-at-the-time-of-serialization specializations).
Most of step 2 is handled by `jl_write_values`, followed by special handling of the dedicated parallel streams.
- step 3 combines the different sections (fields of `jl_serializer_state`) into one
- step 4 writes the values of the hard-coded tagged items and `ccallable_list`
The tables written to the serializer stream make deserialization fairly straightforward. Much of the "real work" is
done by `get_item_for_reloc`.
Much of the "real work" during deserialization is done by `get_item_for_reloc`. But a few items require specific
attention:
- uniquing: during deserialization, the target item (an "external" type or MethodInstance) must be checked against
the running system to see whether such an object already exists (i.e., whether some other previously-loaded package
or workload has created such types/MethodInstances previously) or whether it needs to be created de-novo.
In either case, all references at `location` must be updated to the one in the running system.
`uniquing_target` is a hash table for which `uniquing_target[targetpos] -> chosen_target`.
- method root insertion: when new specializations generate new roots, these roots must be inserted into
method root tables
- backedges & invalidation: external edges have to be checked against the running system and any invalidations executed.
Encoding of a pointer:
- in the location of the pointer, we initially write zero padding
Expand Down Expand Up @@ -583,6 +578,7 @@ static uintptr_t jl_fptr_id(void *fptr)
return *(uintptr_t*)pbp;
}

// `jl_queue_for_serialization` adds items to `serialization_order`
#define jl_queue_for_serialization(s, v) jl_queue_for_serialization_((s), (jl_value_t*)(v), 1, 0)
static void jl_queue_for_serialization_(jl_serializer_state *s, jl_value_t *v, int recursive, int immediate);

Expand Down Expand Up @@ -616,21 +612,22 @@ static void jl_queue_module_for_serialization(jl_serializer_state *s, jl_module_
}
}

// Anything that requires uniquing or fixing during deserialization needs to be "toplevel"
// in serialization (i.e., have its own entry in `serialization_order`). Consequently,
// objects that act as containers for other potentially-"problematic" objects must add such "children"
// to the queue.
// Most objects use preorder traversal. But things that need uniquing require postorder:
// you want to handle uniquing of `Dict{String,Float64}` before you tackle `Vector{Dict{String,Float64}}`.
// Uniquing is done in `serialization_order`, so the very first mention of such an object must
// be the "source" rather than merely a cross-reference.
static void jl_insert_into_serialization_queue(jl_serializer_state *s, jl_value_t *v, int recursive)
{
assert(!jl_is_symbol(v));

// some values have special representations
jl_datatype_t *t = (jl_datatype_t*)jl_typeof(v);
jl_queue_for_serialization(s, t);

if (!recursive)
goto done_fields;

// TODO: make some of this code conditional on s->incremental flag?

// Because of recaching, we visit types in a depth-first postorder, so that the dependencies
// are recached before the objects that wrap them.
if (jl_is_datatype(v)) {
jl_datatype_t *dt = (jl_datatype_t*)v;
jl_svec_t *tt = dt->parameters;
Expand All @@ -646,7 +643,6 @@ static void jl_insert_into_serialization_queue(jl_serializer_state *s, jl_value_
}
}
else if (jl_is_typename(v)) {
// XXX: typename might require really complicating uniquing to handle kwfunc
jl_typename_t *tn = (jl_typename_t*)v;
if (s->incremental) {
assert(!jl_object_in_image((jl_value_t*)tn->module));
Expand Down Expand Up @@ -733,8 +729,8 @@ static void jl_queue_for_serialization_(jl_serializer_state *s, jl_value_t *v, i
return;
*bp = (void*)(uintptr_t)-1;

// visit some child field before the use of this value
// TODO: make some of this code conditional on s->incremental flag?
// Items that require postorder traversal must visit their children prior to insertion into
// the worklist/serialization_order
if (!immediate) {
if (jl_is_uniontype(v))
immediate = 1;
Expand Down Expand Up @@ -1033,6 +1029,8 @@ static void record_gvars(jl_serializer_state *s, arraylist_t *globals) JL_NOTSAF

jl_value_t *jl_find_ptr = NULL;
// The main function for serializing all the items queued in `serialization_order`
// (They are also stored in `serialization_queue` which is order-preserving, unlike the hash table used
// for `serialization_order`).
static void jl_write_values(jl_serializer_state *s) JL_GC_DISABLED
{
size_t l = serialization_queue.len;
Expand All @@ -1041,15 +1039,6 @@ static void jl_write_values(jl_serializer_state *s) JL_GC_DISABLED
arraylist_grow(&layout_table, l * 2);
memset(layout_table.items, 0, l * 2 * sizeof(void*));

// if (serializer_worklist)
// for (i = 0; i < objects_list.len; i+= 2) {
// size_t id = (uintptr_t)objects_list.items[i+1];
// jl_value_t *v = objects_list.items[i];
// char *linkcode = externally_linked(v) ? "*" : "";
// jl_printf(JL_STDOUT, "%ld%s: ", id, linkcode);
// jl_(v);
// }

// Serialize all entries
for (size_t item = 0; item < l; item++) {
jl_value_t *v = (jl_value_t*)serialization_queue.items[item]; // the object
Expand Down Expand Up @@ -1584,17 +1573,14 @@ static inline uintptr_t get_item_for_reloc(jl_serializer_state *s, uintptr_t bas
assert(0 <= *link_index && *link_index < jl_array_len(link_ids));
uint64_t build_id = link_id_data[*link_index];
*link_index += 1;
// jl_printf(JL_STDOUT, "Relocating external link %d to buildid %ld with offset 0x%lx\n", link_index, build_id, offset);
size_t i = 0, nids = jl_array_len(jl_build_ids);
while (i < nids) {
if (build_id == build_id_data[i])
break;
i++;
}
// jl_printf(JL_STDOUT, "i = %ld\n", i);
assert(i < nids);
assert(2*i < jl_linkage_blobs.len);
// jl_printf(JL_STDOUT, "Restoring link %ld with offset 0x%lx and base position %p\n", link_index, offset, jl_linkage_blobs.items[2*i]);
return (uintptr_t)jl_linkage_blobs.items[2*i] + offset*sizeof(void*);
}
abort();
Expand Down Expand Up @@ -1725,6 +1711,9 @@ void gc_sweep_sysimg(void)
}
}

// jl_write_value and jl_read_value are used for storing Julia objects that are adjuncts to
// the image proper. For example, new methods added to external callables require
// insertion into the appropriate method table.
#define jl_write_value(s, v) _jl_write_value((s), (jl_value_t*)(v))
static void _jl_write_value(jl_serializer_state *s, jl_value_t *v)
{
Expand All @@ -1737,7 +1726,25 @@ static void _jl_write_value(jl_serializer_state *s, jl_value_t *v)
write_reloc_t(s->s, reloc);
}

static jl_value_t *jl_read_value(jl_serializer_state *s)
{
uintptr_t base = (uintptr_t)&s->s->buf[0];
size_t size = s->s->size;
uintptr_t offset = *(reloc_t*)(base + (uintptr_t)s->s->bpos);
s->s->bpos += sizeof(reloc_t);
if (offset == 0)
return NULL;
return (jl_value_t*)get_item_for_reloc(s, base, size, offset, NULL, NULL);
}

// The next two, `jl_read_offset` and `jl_delayed_reloc`, are essentially a split version
// of `jl_read_value` that allows usage of the relocation data rather than passing NULL
// to `get_item_for_reloc`.
// This works around what would otherwise be an order-dependency conundrum: objects
// that may require relocation data have to be inserted into `serialization_order`,
// and that may include some of the adjunct data that gets serialized via
// `jl_write_value`. But we can't interpret them properly until we read the relocation
// data, and that happens after we pull items out of the serialization stream.
static uintptr_t jl_read_offset(jl_serializer_state *s)
{
uintptr_t base = (uintptr_t)&s->s->buf[0];
Expand All @@ -1758,16 +1765,6 @@ static jl_value_t *jl_delayed_reloc(jl_serializer_state *s, uintptr_t offset)
return ret;
}

static jl_value_t *jl_read_value(jl_serializer_state *s)
{
uintptr_t base = (uintptr_t)&s->s->buf[0];
size_t size = s->s->size;
uintptr_t offset = *(reloc_t*)(base + (uintptr_t)s->s->bpos);
s->s->bpos += sizeof(reloc_t);
if (offset == 0)
return NULL;
return (jl_value_t*)get_item_for_reloc(s, base, size, offset, NULL, NULL);
}

static void jl_update_all_fptrs(jl_serializer_state *s)
{
Expand Down Expand Up @@ -1863,55 +1860,6 @@ static void jl_update_all_gvars(jl_serializer_state *s)
assert(!s->link_ids_gvars || link_index == jl_array_len(s->link_ids_gvars));
}

// New roots for external methods
static void jl_collect_methods(htable_t *mset, jl_array_t *new_specializations)
{
size_t i, l = new_specializations ? jl_array_len(new_specializations) : 0;
jl_value_t *v;
jl_method_t *m;
for (i = 0; i < l; i++) {
v = jl_array_ptr_ref(new_specializations, i);
assert(jl_is_code_instance(v));
m = ((jl_code_instance_t*)v)->def->def.method;
assert(jl_is_method(m));
ptrhash_put(mset, (void*)m, (void*)m);
}
}

static void jl_collect_new_roots(jl_array_t *roots, htable_t *mset, uint64_t key)
{
size_t i, sz = mset->size;
int nwithkey;
jl_method_t *m;
void **table = mset->table;
jl_array_t *newroots = NULL;
JL_GC_PUSH1(&newroots);
for (i = 0; i < sz; i += 2) {
if (table[i+1] != HT_NOTFOUND) {
m = (jl_method_t*)table[i];
assert(jl_is_method(m));
nwithkey = nroots_with_key(m, key);
if (nwithkey) {
jl_array_ptr_1d_push(roots, (jl_value_t*)m);
newroots = jl_alloc_vec_any(nwithkey);
jl_array_ptr_1d_push(roots, (jl_value_t*)newroots);
rle_iter_state rootiter = rle_iter_init(0);
uint64_t *rletable = NULL;
size_t nblocks2 = 0, nroots = jl_array_len(m->roots), k = 0;
if (m->root_blocks) {
rletable = (uint64_t*)jl_array_data(m->root_blocks);
nblocks2 = jl_array_len(m->root_blocks);
}
while (rle_iter_increment(&rootiter, nroots, rletable, nblocks2))
if (rootiter.key == key)
jl_array_ptr_set(newroots, k++, jl_array_ptr_ref(m->roots, rootiter.i));
assert(k == nwithkey);
}
}
}
JL_GC_POP();
}


static void jl_compile_extern(jl_method_t *m, void *sysimg_handle) JL_GC_DISABLED
{
Expand Down Expand Up @@ -2092,6 +2040,7 @@ static void jl_strip_all_codeinfos(void)
// triggering non-relocatability of compressed CodeInfos.
// Set the number of such roots in each method when the sysimg is
// serialized.
// TODO: move this to `jl_write_values`
static int set_nroots_sysimg__(jl_typemap_entry_t *def, void *_env)
{
jl_method_t *m = def->func.method;
Expand Down Expand Up @@ -2473,7 +2422,7 @@ static void jl_save_system_image_to_stream(ios_t *f,
jl_gc_enable(en);
}

static int64_t jl_incremental_header_stuff(ios_t *f, jl_array_t *worklist, jl_array_t **mod_array, jl_array_t **udeps)
static int64_t jl_write_header_for_incremental(ios_t *f, jl_array_t *worklist, jl_array_t **mod_array, jl_array_t **udeps)
{
*mod_array = jl_get_loaded_modules(); // __toplevel__ modules loaded in this session (from Base.loaded_modules_array)
assert(jl_precompile_toplevel_module == NULL);
Expand Down Expand Up @@ -2508,7 +2457,7 @@ JL_DLLEXPORT ios_t *jl_create_system_image(void *_native_data, jl_array_t *workl
JL_GC_PUSH7(&mod_array, &udeps, &extext_methods, &new_specializations, &method_roots_list, &ext_targets, &edges);
int64_t srctextpos = 0;
if (worklist) {
srctextpos = jl_incremental_header_stuff(f, worklist, &mod_array, &udeps);
srctextpos = jl_write_header_for_incremental(f, worklist, &mod_array, &udeps);
jl_gc_enable_finalizers(ct, 0); // make sure we don't run any Julia code concurrently after this point
jl_prepare_serialization_data(mod_array, newly_inferred, jl_worklist_key(worklist), &extext_methods, &new_specializations, &method_roots_list, &ext_targets, &edges);
write_padding(f, LLT_ALIGN(ios_pos(f), JL_CACHE_BYTE_ALIGNMENT) - ios_pos(f));
Expand Down Expand Up @@ -2758,6 +2707,16 @@ static void jl_restore_system_image_from_stream_(ios_t *f, jl_array_t *depmods,
jl_read_relocations(&s, s.link_ids_relocs, 0);
jl_read_arraylist(s.relocs, &s.uniquing_list);
jl_read_arraylist(s.relocs, &s.fixup_list);
// Perform the uniquing of objects that we don't "own" and consequently can't promise
// weren't created by some other package before this one got loaded:
// - iterate through all objects that need to be uniqued. The first encounter has to be the
// "reconstructable blob". We either look up the object (if something has created it previously)
// or construct it for the first time, crucially outside the pointer range of any pkgimage.
// This ensures it stays unique-worthy.
// - after we've stored the address of the "real" object (which for convenience we do among the data
// written to allow lookup/reconstruction), then we have to update references to that "reconstructable blob":
// instead of performing the relocation within the package image, we instead (re)direct all references
// to the external object.
for (size_t i = 0; i < s.uniquing_list.len; i++) {
uintptr_t item = (uintptr_t)s.uniquing_list.items[i];
int tag = (item & 1) == 1;
Expand Down Expand Up @@ -2823,6 +2782,8 @@ static void jl_restore_system_image_from_stream_(ios_t *f, jl_array_t *depmods,
assert(!(image_base < (char*)newobj && (char*)newobj <= image_base + sizeof_sysimg + sizeof(uintptr_t)));
assert(jl_typeis(obj, otyp));
}
// Write junk in place of the source data we used during uniquing, to catch inadvertent references to
// it from elsewhere.
for (size_t i = 0; i < s.uniquing_list.len; i++) {
void *item = s.uniquing_list.items[i];
jl_taggedvalue_t *o = jl_astaggedvalue(item);
Expand All @@ -2833,6 +2794,7 @@ static void jl_restore_system_image_from_stream_(ios_t *f, jl_array_t *depmods,
else
memset(o, 0xba, sizeof(jl_value_t*));
}
// Perform fixups: things like updating world ages, inserting methods & specializations, etc.
size_t world = jl_atomic_load_acquire(&jl_world_counter);
for (size_t i = 0; i < s.fixup_list.len; i++) {
uintptr_t item = (uintptr_t)s.fixup_list.items[i];
Expand Down Expand Up @@ -2970,7 +2932,7 @@ static void jl_restore_system_image_from_stream_(ios_t *f, jl_array_t *depmods,
jl_gc_enable(en);
}

static jl_value_t *jl_restore_package_image_from_stream(ios_t *f, jl_array_t *depmods)
static jl_value_t *jl_validate_cache_file(ios_t *f, jl_array_t *depmods)
{
if (ios_eof(f) || !jl_read_verify_header(f)) {
return jl_get_exceptionf(jl_errorexception_type,
Expand All @@ -2987,10 +2949,15 @@ static jl_value_t *jl_restore_package_image_from_stream(ios_t *f, jl_array_t *de
}

// verify that the system state is valid
jl_value_t *verify_fail = read_verify_mod_list(f, depmods);
if (verify_fail) {
return read_verify_mod_list(f, depmods);
}

// TODO?: refactor to make it easier to create the "package inspector"
static jl_value_t *jl_restore_package_image_from_stream(ios_t *f, jl_array_t *depmods)
{
jl_value_t *verify_fail = jl_validate_cache_file(f, depmods);
if (verify_fail)
return verify_fail;
}

jl_value_t *restored = NULL;
jl_array_t *init_order = NULL, *extext_methods = NULL, *new_specializations = NULL, *method_roots_list = NULL, *ext_targets = NULL, *edges = NULL;
Expand Down
Loading

0 comments on commit 4d56e46

Please sign in to comment.