From def19b13c91b0010017ec150112656ea1e4d789c Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Sun, 13 Sep 2026 09:50:02 -0700 Subject: [PATCH] The module-cache record is a third of its size and writes twice as fast: strings, types, files, modules and expression classes stream as per-record first-mention numbers, a LineInfo is coded against the previous one, 32-bit integers are adaptive sizes, and the writer appends through an inline path. A record of the JIT module graph (hello_world under -jit, 30 modules) goes from 9.2 MB to 2.4 MB, its write from 37 ms to 16 ms and its decode from 26 ms to 19 ms; the CI ser/deser sweep's stream from 265 MB to 133 MB. The string table holds pointers to strings that live in process memory for the record - the AST's own fields, which nothing changes while a record is written or read - so neither side copies them and neither side looks into the storage; a string with no home past the call (a computed mangled name, a lookup key, a map element, the policies local, the record header's locals) goes through serializeTemp, which parks one copy in the record's arena. The type table keys the live TypeDecl and hashes and compares its content recursively; a type carrying expressions streams inline; a type takes its number after its payload, because the payload can carry a same-module structure inline whose field holds an equal type, and the writer counts fresh entries apart from the map, because such an equal type registered from inside the payload would otherwise skip a number the reader pushes; the reader keeps the first decoded instance and deep-copies it per reference, so no two nodes share a type. The two findAlias cache bits are neither in the key nor in the stream. A FileInfo reference is a per-record number rather than an 8-byte stream offset, so a record never points into another and the skipped-record back-reference check is gone. A LineInfo is a shape byte (same file, one line, a six-bit line delta with an escape), the column and a zigzag span. The adaptive size is seven bits a byte. A Module and an expression class are per-record numbers; every hand-written module hash site goes through the one Module operator. The vector storage grows by doubling behind a write cursor and flush() trims it - consumers call it - and reading marks the finalizeModule and program-setup times apart from the decode: ast_serializer_finalize_usec / ast_serializer_setup_usec on the das handle, and -log-compile-time's cache line prints decode against finalize. DAS_SERIALIZE_PROFILE, off by default, is the size and time breakdown rail: every compound serialize opens a frame, and ModuleFileCache::finish prints self and inclusive bytes per frame, the frame tree, the string and type histograms and per-record sizes. Version 211. The module-cache eviction test's fixture requires strings_boost, fio and ast so a record sizes into the band its 1 MB limit needs. Co-Authored-By: Claude Fable 5.1 --- doc/reflections/das2rst.das | 2 +- ...lizer_finalize_usec-0x4aa223d6ebfe0792.rst | 2 + ...rializer_setup_usec-0xb312e3fc052a000e.rst | 2 + include/daScript/ast/REVIEW.md | 40 +- include/daScript/ast/ast_serializer.h | 185 ++- skills/daslang/references/everything.md | 2 + skills/internal/build_and_debug.md | 1 + src/ast/ARCHITECTURE.md | 7 + src/ast/REVIEW.md | 49 +- src/ast/ast_parse.cpp | 30 +- src/builtin/ARCHITECTURE.md | 78 ++ src/builtin/REVIEW.md | 67 +- src/builtin/module_builtin_ast_serialize.cpp | 1063 +++++++++++++---- src/builtin/module_builtin_rtti.cpp | 6 + src/misc/REVIEW.md | 12 +- src/misc/daScriptC.cpp | 1 + tests-cpp/small/test_env_serializer.cpp | 41 + tests/REVIEW.md | 4 +- tests/module_cache/REVIEW.md | 22 +- .../module_cache/test_default_cache_path.das | 5 +- tests/module_cache/test_rtti_serializer.das | 53 + 21 files changed, 1324 insertions(+), 348 deletions(-) create mode 100644 doc/source/stdlib/handmade/function-rtti-ast_serializer_finalize_usec-0x4aa223d6ebfe0792.rst create mode 100644 doc/source/stdlib/handmade/function-rtti-ast_serializer_setup_usec-0xb312e3fc052a000e.rst create mode 100644 tests/module_cache/test_rtti_serializer.das diff --git a/doc/reflections/das2rst.das b/doc/reflections/das2rst.das index 702945eb08..60f447fad3 100644 --- a/doc/reflections/das2rst.das +++ b/doc/reflections/das2rst.das @@ -453,7 +453,7 @@ def document_module_rtti(_root : string) { group_by_regex("Program access", mod, %regex~(program_for_each_module|program_for_each_registered_module|get_this_module|get_module|has_module|module_group_for_each_member)$%%), group_by_regex("Module access", mod, %regex~(module_for_each_structure|module_for_each_enumeration|module_for_each_function|module_for_each_generic|module_for_each_global|module_for_each_annotation|module_for_each_dependency)$%%), group_by_regex("Annotation access", mod, %regex~(get_annotation_argument_value|add_annotation_argument|get_annotation|get_annotation_argument|resolve_annotation|each_annotation|each_annotation_argument)$%%), - group_by_regex("Compilation and simulation", mod, %regex~(compile|compile_file|for_each_expected_error|for_each_require_declaration|simulate|create_ast_serializer|create_ast_deserializer|delete_ast_serializer|serialize_program|deserialize_program|ast_serializer_get_data)$%%), + group_by_regex("Compilation and simulation", mod, %regex~(compile|compile_file|for_each_expected_error|for_each_require_declaration|simulate|create_ast_serializer|create_ast_deserializer|delete_ast_serializer|serialize_program|deserialize_program|ast_serializer_get_data|ast_serializer_finalize_usec|ast_serializer_setup_usec)$%%), group_by_regex("File access", mod, %regex~(make_file_access|set_file_source|add_file_access_root|add_extra_module)$%%), group_by_regex("Structure access", mod, %regex~(rtti_builtin_structure_for_each_annotation|basic_struct_for_each_field|structure_for_each_annotation|basic_struct_for_each_parent)$%%), group_by_regex("Data walking and printing", mod, %regex~(sprint_data|sprint_json_at|sscan_json_at|describe|get_mangled_name)$%%), diff --git a/doc/source/stdlib/handmade/function-rtti-ast_serializer_finalize_usec-0x4aa223d6ebfe0792.rst b/doc/source/stdlib/handmade/function-rtti-ast_serializer_finalize_usec-0x4aa223d6ebfe0792.rst new file mode 100644 index 0000000000..f70e226491 --- /dev/null +++ b/doc/source/stdlib/handmade/function-rtti-ast_serializer_finalize_usec-0x4aa223d6ebfe0792.rst @@ -0,0 +1,2 @@ +Microseconds the reads through `deserialize_program` on this deserializer spent inside finalizeModule - the annotations, the macro-module re-simulation and the gc_collect of every program it restored; the decode is the read minus this and `ast_serializer_setup_usec`. +def ast_serializer_finalize_usec (serializer: AstSerializer?) : int64 diff --git a/doc/source/stdlib/handmade/function-rtti-ast_serializer_setup_usec-0xb312e3fc052a000e.rst b/doc/source/stdlib/handmade/function-rtti-ast_serializer_setup_usec-0xb312e3fc052a000e.rst new file mode 100644 index 0000000000..54277f65e4 --- /dev/null +++ b/doc/source/stdlib/handmade/function-rtti-ast_serializer_setup_usec-0xb312e3fc052a000e.rst @@ -0,0 +1,2 @@ +Microseconds the reads through `deserialize_program` on this deserializer spent in the program setup that follows the stream - symbol marking and stack allocation - over every program it restored. +def ast_serializer_setup_usec (serializer: AstSerializer?) : int64 diff --git a/include/daScript/ast/REVIEW.md b/include/daScript/ast/REVIEW.md index 576f7035c5..9eb8e7112b 100644 --- a/include/daScript/ast/REVIEW.md +++ b/include/daScript/ast/REVIEW.md @@ -1,27 +1,27 @@ # AST Headers Code Review Checklist **Read `REVIEW_COMMON.md` (repo root) first - its contract binds this checklist.** Architecture doc: -`src/ast/ARCHITECTURE.md` (repo root). A diff that adds, reorders or retypes a data member of a -C++ type that das binds through an annotation applies -`src/builtin/REVIEW.md` too; checklist discovery walks changed paths only, so a header edit -never opens that checklist on its own. +`src/ast/ARCHITECTURE.md` (repo root). Apply `src/builtin/REVIEW.md` too when a diff changes the +offset or type of a data member of a C++ type some `Managed*Annotation` is instantiated over - +adding, removing, reordering or retyping a member all count, and the das-visible name can differ +from the C++ type's, so check the annotation's template argument, not the string. Checklist +discovery walks changed paths only, so a header edit never opens that checklist on its own. -- **A diff that changes what a cached JIT DLL binds - the module a bind registers into or the - name it registers under (`vectorHomeModule`, `typeFactory>::make`, - `registerVectorFunctions`, the name a `ManagedVectorAnnotation` or - `ManagedStructureAnnotation` takes - all in `ast_handle.h`), or the field layout of a C++ type - das code reads through a binding - bumps `LLVM_JIT_CODEGEN_VERSION` in - `modules/dasLLVM/daslib/llvm_jit_plan.das` (repo root), in the same change.** The key is built - from the codegen version and each function's AST hash, never from the module an extern lives - in nor a bound type's offsets, so a cached DLL binds the old name or the old offset and - crashes on the hit. +- **A diff that changes the module an existing bind registers into or the name it registers + under (`vectorHomeModule`, `typeFactory>::make`, `registerVectorFunctions`, the name + a `ManagedVectorAnnotation` or `ManagedStructureAnnotation` takes - all in `ast_handle.h`), or + the offset or type of a data member of a C++ type some `Managed*Annotation` is instantiated + over, bumps `LLVM_JIT_CODEGEN_VERSION` in `modules/dasLLVM/daslib/llvm_jit_plan.das` (repo + root), in the same change.** The key is built from the codegen version and each function's AST + hash, never from the module an extern lives in nor a bound type's offsets, so a cached DLL + binds the old name or the old offset and crashes on the hit. - **A diff that adds a field to `Function` or `Variable` (`ast.h`) holding something one program's compile decides - whether the program uses it, the slot it holds in that program's - context, whether its JIT selected it - is a defect; the value goes into `Program`'s symbol - tables (`usedFunctions`, `functionIndices` and their variable twins, `jitSelected`) - instead.** A shared module's objects are one - instance for every program in the process, and a compile that runs inside another - a macro's - `compile`, a late `require`, the folding program - would otherwise overwrite the outer - program's answer mid-simulate. The per-compile fields that remain, and why each is tolerated, - are `src/ast/ARCHITECTURE.md` sec.4's. \ No newline at end of file + context, whether this program's JIT selected it - is a defect; the value goes into `Program`'s + symbol tables (`usedFunctions`, `functionIndices`, `usedVariables`, `variableIndices`, + `jitSelected`) instead.** A shared module's objects are one instance for every program in the + process, and a compile that runs inside another - a macro's `compile`, a late `require`, the + folding program - would otherwise overwrite the outer program's answer mid-simulate. + `src/ast/ARCHITECTURE.md` sec.4 lists the per-compile fields that remain and why each is + tolerated. diff --git a/include/daScript/ast/ast_serializer.h b/include/daScript/ast/ast_serializer.h index 085687a7f3..81fc6ef896 100644 --- a/include/daScript/ast/ast_serializer.h +++ b/include/daScript/ast/ast_serializer.h @@ -12,6 +12,10 @@ #define DAS_SERIALIZE_DTAG 0 #endif +#ifndef DAS_SERIALIZE_PROFILE +#define DAS_SERIALIZE_PROFILE 0 +#endif + namespace das { struct SerializationStorage { vector buffer; @@ -53,12 +57,17 @@ namespace das { // seek keeps the default false — record lengths then stay 0 and the reader falls // back to the legacy stop-at-first-failure cutoff instead of skip-and-resume. virtual bool patch ( size_t /*at*/, const void * /*data*/, size_t /*size*/ ) { return false; } + //! called at the end of every program's bytes: a storage that buffers ahead settles what it holds here; a storage that never wrote is left alone + virtual void flush () {} + virtual struct SerializationStorageVector * asVector () { return nullptr; } virtual ~SerializationStorage() {} }; struct SerializationStorageVector : SerializationStorage { + size_t writePos = 0; + bool wrote = false; virtual size_t writingSize() const override { - return buffer.size(); + return writePos; } virtual bool readOverflow ( void * data, size_t size ) override { if ( bufferPos + size > buffer.size() ) return false; @@ -66,13 +75,30 @@ namespace das { bufferPos += size; return true; } + void grow ( size_t need ) { + size_t cap = buffer.size(); + size_t next = cap ? cap : 4096; + while ( next < need ) { + if ( next > (size_t(-1) >> 1) ) { next = need; break; } + next *= 2; + } + buffer.resize(next); + } + __forceinline void append ( const void * data, size_t size ) { + if ( writePos + size > buffer.size() ) grow(writePos + size); + memcpy(buffer.data() + writePos, data, size); + writePos += size; + wrote = true; + } virtual void write ( const void * data, size_t size ) override { - auto at = buffer.size(); - buffer.resize(at + size); - memcpy(buffer.data() + at, data, size); + append(data, size); } + virtual void flush () override { + if ( wrote ) buffer.resize(writePos); + } + virtual SerializationStorageVector * asVector () override { return this; } virtual bool patch ( size_t at, const void * data, size_t size ) override { - if ( at + size > buffer.size() ) return false; + if ( at + size > writePos ) return false; memcpy(buffer.data() + at, data, size); return true; } @@ -107,8 +133,8 @@ namespace das { Module * astModule = nullptr; bool writing = false; bool failed = false; - size_t readOffset = 0; SerializationStorage * buffer = nullptr; + SerializationStorageVector * bufferAsVector = nullptr; bool seenNewModule = false; // module-cache resume state (trySerializeProgramModule) bool checkedStreamHeader = false; @@ -128,8 +154,48 @@ namespace das { // file info clean up vector deleteUponFinish; // these pointers are for builtins (which we don't serialize) and need to be cleaned manually das_hash_set doNotDelete; + // per-record tables, cleared in clearNodeIds (src/builtin/ARCHITECTURE.md sec.6) + das_hash_map writeFileInfos; + vector readFileInfos; + FileInfo * lastWriteFileInfo = nullptr; + uint32_t lastWriteFileInfoNumber = 0; + FileInfo * lineBaseFile = nullptr; + uint32_t lineBaseLine = 0; + struct TypeHash { size_t operator () ( const TypeDecl * t ) const noexcept; }; + struct TypeEqual { bool operator () ( const TypeDecl * a, const TypeDecl * b ) const noexcept; }; + das_hash_map writeTypes; + uint32_t writeFreshTypeCount = 0; + vector readTypes; + das_hash_map writeExprClasses; + vector readExprClasses; + das_hash_map writeModules; + vector> readModules; + das_hash_map writeMangledNames; + struct StringView { + const char * data = nullptr; + uint32_t length = 0; + }; + struct StringViewHash { + size_t operator () ( const StringView & v ) const noexcept { return size_t(hash_block64((const uint8_t *) v.data, v.length)); } + }; + struct StringViewEqual { + bool operator () ( const StringView & a, const StringView & b ) const noexcept { + return a.length == b.length && memcmp(a.data, b.data, a.length) == 0; + } + }; + das_hash_map writeStrings; + int32_t emptyStringNumber = -1; + vector readStrings; + vector> stringArena; + AstSerializer & serializeString ( string & str, bool temp ); + //! a string with no home past the call: the table gets a copy of it; any other value streams as usual + AstSerializer & serializeTemp ( string & str ) { return serializeString(str, true); } + template + void serializeTemp ( T & value ) { *this << value; } // profile data uint64_t totMacroTime = 0; + uint64_t totFinalizeTime = 0; + uint64_t totSetupTime = 0; // node identity (SerializeNodeId): the writer numbers a node at its first mention and // remembers which numbers have had their payload written; the reader keeps the node // each number resolved to, null until its payload is read (a forward reference is @@ -138,9 +204,6 @@ namespace das { vector writtenIds; // indexed by SerializeNodeId::index vector readNodes; // indexed by SerializeNodeId::index vector> pendingRefs; // storage of a TT * slot, and the number it waits for - using DataOffset = uint64_t; - das_hash_map writingFileInfoMap; - das_hash_map readingFileInfoMap; // fieldRefs tuple contains: fieldptr, module, structname, fieldname vector> fieldRefs; // parsedModules record: fileName, source content hash, source size, program, thisModule, the collector's require names @@ -150,6 +213,51 @@ namespace das { das_hash_set writingReadyModules; bool ignoreEmptyExternal = false; void tag ( const char * name, uint32_t hash ); +#if DAS_SERIALIZE_PROFILE + struct ProfAgg { + uint64_t selfBytes = 0; + uint64_t outermostInclBytes = 0; + uint64_t count = 0; + int64_t outermostInclTicks = 0; + }; + struct ProfNode { + uint32_t nameId = 0; + uint32_t parent = 0; + uint64_t inclBytes = 0; + uint64_t count = 0; + int64_t inclTicks = 0; + vector children; + }; + struct ProfFrame { + uint32_t nameId; + uint32_t node; + uint64_t startBytes; + uint64_t childBytes; + int64_t startTicks; + }; + struct ProfRecord { + string file; + uint64_t lengthWordBytes = 0; + uint64_t payloadBytes = 0; + int64_t usec = 0; + }; + vector> profNameIds; + vector profNames; + vector profAgg; + vector profNodes; + vector profStack; + das_hash_map profStrings; + das_hash_map profTypes; + uint64_t profTypeBytes = 0; + vector profRecords; + int64_t profStartTicks = 0; + uint64_t profPosition () const { return writing ? buffer->writingSize() : buffer->bufferPos; } + void profBegin ( const char * name ); + void profEnd (); + void profString ( const string & str ); + void profType ( const TypeDecl * type, uint64_t bytes ); + void profReport ( TextWriter & tw ) const; +#endif #if DAS_SERIALIZE_DTAG __forceinline void dtag ( const char * name, uint32_t hash ) { tag(name,hash); } #else @@ -164,7 +272,10 @@ namespace das { } void read ( void * data, size_t size ); [[noreturn]] void onReadFailure (); // throws dasException ("read overflow") - void write ( const void * data, size_t size ); + __forceinline void write ( const void * data, size_t size ) { + if ( bufferAsVector ) bufferAsVector->append(data, size); + else buffer->write(data, size); + } template void serialize ( T & data ) { if ( writing ) { @@ -176,23 +287,45 @@ namespace das { void serialize ( void * data, size_t size ); void serializeAdaptiveSize64 ( uint64_t & size ); void serializeAdaptiveSize32 ( uint32_t & size ); + //! encodes into a caller's buffer of at least 5 bytes and answers the bytes used + static __forceinline uint32_t zigzag32 ( int32_t v ) { return (uint32_t(v) << 1) ^ uint32_t(v >> 31); } + static __forceinline int32_t unzigzag32 ( uint32_t v ) { return int32_t(v >> 1) ^ -int32_t(v & 1); } + static __forceinline size_t encodeAdaptiveSize32 ( uint8_t * out, uint32_t size ) { + size_t n = 0; + while ( size >= 0x80 ) { + out[n++] = uint8_t(size) | 0x80; + size >>= 7; + } + out[n++] = uint8_t(size); + return n; + } // reject a deserialized element count that exceeds the bytes left in the stream // BEFORE it gates an allocation (throws; reading only) void verifyLength ( uint64_t size ); void collectFileInfo ( vector & orphanedFileInfos ); void getCompiledModules ( ); void patch (); - AstSerializer & operator << ( string & str ); + AstSerializer & operator << ( string & str ) { return serializeString(str, false); } AstSerializer & operator << ( const char * & value ); AstSerializer & operator << ( bool & value ) { serialize(value); return *this; } AstSerializer & operator << ( vec4f & value ) { serialize(value); return *this; } AstSerializer & operator << ( float & value ) { serialize(value); return *this; } AstSerializer & operator << ( void * & value ) { serialize(value); return *this; } AstSerializer & operator << ( uint8_t & value ) { serialize(value); return *this; } - AstSerializer & operator << ( int32_t & value ) { serialize(value); return *this; } + AstSerializer & operator << ( int32_t & value ) { + if ( writing ) { + uint32_t z = zigzag32(value); + serializeAdaptiveSize32(z); + } else { + uint32_t z = 0; + serializeAdaptiveSize32(z); + value = unzigzag32(z); + } + return *this; + } AstSerializer & operator << ( int64_t & value ) { serialize(value); return *this; } AstSerializer & operator << ( uint16_t & value ) { serialize(value); return *this; } - AstSerializer & operator << ( uint32_t & value ) { serialize(value); return *this; } + AstSerializer & operator << ( uint32_t & value ) { serializeAdaptiveSize32(value); return *this; } AstSerializer & operator << ( uint64_t & value ) { serialize(value); return *this; } AstSerializer & operator << ( pair & value ) { serialize(value); return *this; } AstSerializer & operator << ( pair & value ) { serialize(value); return *this; } @@ -232,7 +365,7 @@ namespace das { AstSerializer & serializeModule ( Module & module, bool already_exists ); static constexpr uint32_t getVersion () { - return 210; // 210: module_cache joins the policy stream; `options no_optimizations` is read (209: a record written by a recompile that served a dasbind registrar could carry a dependent's calls to the extern stubs unrewritten - the format is unchanged, the bump discards those records (208: a node reference is the writer's first-mention number as a varint, not a pointer-and-epoch word; 207: neither Function nor Variable flags carry a used bit, and neither streams an index; 206: the record header carries the requires the parse took; 205: a vector of a handled element streams under the element's module; 204: the record header stamps the source by content hash; the policy stream carries every CodeOfPolicies field) + return 211; // 211: strings stream through a per-record first-mention table, adaptive sizes are seven bits a byte, a FileInfo is a per-record number, a LineInfo is coded against the previous one, a TypeDecl is a per-record content number, 32-bit integers are adaptive sizes, an expression class and a module are per-record numbers (210: module_cache joins the policy stream; `options no_optimizations` is read (209: a record written by a recompile that served a dasbind registrar could carry a dependent's calls to the extern stubs unrewritten - the format is unchanged, the bump discards those records (208: a node reference is the writer's first-mention number as a varint, not a pointer-and-epoch word; 207: neither Function nor Variable flags carry a used bit, and neither streams an index; 206: the record header carries the requires the parse took; 205: a vector of a handled element streams under the element's module; 204: the record header stamps the source by content hash; the policy stream carries every CodeOfPolicies field) } void serializeProgram ( ProgramPtr program, ModuleGroup & libGroup ) noexcept; @@ -249,6 +382,9 @@ namespace das { template AstSerializer & operator << ( vector & value ) { dtag("Vector",hash_tag("Vector")); +#if DAS_SERIALIZE_PROFILE + profBegin("Vector"); +#endif if ( writing ) { uint64_t size = value.size(); serializeAdaptiveSize64(size); @@ -261,6 +397,9 @@ namespace das { for ( TT & v : value ) { *this << v; } +#if DAS_SERIALIZE_PROFILE + profEnd(); +#endif return *this; } @@ -366,6 +505,19 @@ namespace das { } }; +#if DAS_SERIALIZE_PROFILE + struct SerProfileScope { + AstSerializer & ser; + SerProfileScope ( AstSerializer & s, const char * name ) : ser(s) { ser.profBegin(name); } + ~SerProfileScope () { ser.profEnd(); } + }; + #define DAS_SER_PROFILE_CAT2(a,b) a##b + #define DAS_SER_PROFILE_CAT(a,b) DAS_SER_PROFILE_CAT2(a,b) + #define DAS_SER_PROFILE(ser, name) das::SerProfileScope DAS_SER_PROFILE_CAT(_serProfScope_, __LINE__)((ser), (name)) +#else + #define DAS_SER_PROFILE(ser, name) +#endif + // File-backed driver for the env module-cache rail (daScriptEnvironment::serializer_read // / serializer_write): install() binds a reader (when the file exists and is non-empty) // and/or a writer around a compile; finish() unbinds, classifies what the reader saw, @@ -446,4 +598,9 @@ namespace das { AstSerializerState * state, const TBlock const>> & block, Context * context, LineInfoArg * at ); + + //! microseconds this state's reads spent inside finalizeModule, over every program it read + int64_t rtti_ast_serializer_finalize_usec ( AstSerializerState * state ); + //! microseconds this state's reads spent in the setup Program::serialize runs after the stream + int64_t rtti_ast_serializer_setup_usec ( AstSerializerState * state ); } diff --git a/skills/daslang/references/everything.md b/skills/daslang/references/everything.md index 94c4f69b21..a8ff0da3b6 100644 --- a/skills/daslang/references/everything.md +++ b/skills/daslang/references/everything.md @@ -3419,7 +3419,9 @@ The RTTI module exposes runtime type information and program introspection facil ### Compilation and simulation +- `ast_serializer_finalize_usec` - Microseconds the reads through `deserialize_program` on this deserializer spent inside finalizeModule - the annotations, the macro-module re-simulation and the gc_collect of every program it restored; the decode is the read minus this and `ast_serializer_setup_usec`. - `ast_serializer_get_data` - Returns content of serializer. +- `ast_serializer_setup_usec` - Microseconds the reads through `deserialize_program` on this deserializer spent in the program setup that follows the stream - symbol marking and stack allocation - over every program it restored. - `create_ast_deserializer` - Creates deserializer. - `create_ast_serializer` - Creates serializer object. - `delete_ast_serializer` - Frees memory for ast_serializer. diff --git a/skills/internal/build_and_debug.md b/skills/internal/build_and_debug.md index 57de06915e..18df40e5f8 100644 --- a/skills/internal/build_and_debug.md +++ b/skills/internal/build_and_debug.md @@ -110,6 +110,7 @@ Dev-tier rails that cut the edit-compile-run loop. Never benchmark through `--ji - **`deser: partial`** - a record that cannot deserialize in a cold process (`modules/dasLLVM/bindings/llvm_func.das`, whose `[dasbind]` externs register into the dasbind builtin module during compile) reparses in place (~0.03s) without cutting the stream; so does a record written by a compile with different policies (above). - **`-ser ` / `-deser `** are the explicit write and read halves - the round-trip test instrument. - **Composes with the split obj cache:** a warm `-jit` edit deserializes the unchanged prefix and re-emits only the tail partitions. The cache is one file you own - delete it when in doubt. +- **Size and time breakdown of a record:** set `DAS_SERIALIZE_PROFILE` to 1 in `include/daScript/ast/ast_serializer.h` (compile-time, off by default; rebuild `daslang` - ten TUs) and run with an explicit `-module-cache `: `ModuleFileCache::finish` prints, for the writer and the reader, self and inclusive bytes per serializer frame, a frame tree, the string and TypeDecl histograms with their table-dedup estimates, and per-record sizes. The frames cost ~30% of serialization time, so profile speed with the rail off - a native sampler over a loop of `serialize_program`/`deserialize_program` (the `rtti` das API) sees the real distribution. ## Build configurations (module flags) diff --git a/src/ast/ARCHITECTURE.md b/src/ast/ARCHITECTURE.md index 0c9c0487b9..4037eadffd 100644 --- a/src/ast/ARCHITECTURE.md +++ b/src/ast/ARCHITECTURE.md @@ -49,6 +49,13 @@ on unasked for an ordinary run; ungated, each of those lines would be output eve an ordinary edit. An explicit `-module-cache` leaves the flag off, so those runs get the lines together with the host's verdict. +A served record's time splits three ways: the decode, the finalize (`finalizeModule` - +annotations, macro-module re-simulation, `gc_collect`) and the setup `Program::serialize` runs +after the stream (symbol marking, stack allocation). The serializer accumulates the last two over +every program it read, das reading them back through `ast_serializer_finalize_usec` and +`ast_serializer_setup_usec`, and the decode is the read minus both; `-log-compile-time`'s cache +line prints decode against finalize, with the macro simulate named inside finalize. + ## 2. The module scan and the descriptor manifest (`dyn_modules.cpp`) `require_dynamic_modules` walks `/modules/`, then the project root's, then each diff --git a/src/ast/REVIEW.md b/src/ast/REVIEW.md index a3ba0f2193..c5ea42e821 100644 --- a/src/ast/REVIEW.md +++ b/src/ast/REVIEW.md @@ -9,23 +9,26 @@ What the gate enforces is read from the gate itself. - **A diff that adds a module-cache read diagnostic outside `trySerializeProgramModule` - (`ast_parse.cpp`), or moves one out of it - into a helper it calls, or another function - - extends `REVIEW.das`'s scan to the new home in the same change, and prints the line only when - the serializer's `quietCache` is off or `log_module_compile_time` is set.** A module-cache read + (`ast_parse.cpp`), or moves one out of it - into a helper it calls, or another function in this + folder - extends `REVIEW.das`'s scan to the new home in the same change.** A module-cache read diagnostic is a line reporting one record read of the module cache, the kind `trySerializeProgramModule` prints; the module scan's `[module]` trace is not one. A print the - gate does not scan is a print nobody checks, and an ungated line there is output every user of - the default cache sees. + gate does not scan is a print nobody checks. + +- **A `string` local that `trySerializeProgramModule` (`ast_parse.cpp`) reads from the + module-cache record header - the bytes read before the record payload - goes through the + serializer's `serializeTemp`, never plain `operator<<`.** The serializer's string table keeps a + pointer to the string it read into, so a header local read plainly is a dead pointer at the + next mention. - **A diff that adds a `[module]` line to the module scan's trace (`dyn_modules.cpp`) prints it only behind `trace_scan()`, the switch `DAS_TRACE_MODULE_LOAD` sets.** The scan runs on every start, so a line outside the switch is output every user sees. -- **A diff that changes what a manifest written by an earlier binary replays to - a field added, - removed, reordered or re-typed in `read_manifest` or `write_manifest` (`dyn_modules.cpp`), a - line added beside the key lines `stamp`, `dll`, `root`, `dasroot`, `target` and `dep`, or a - change to what an existing row registers - bumps the version in `MANIFEST_HEADER` in the same - change.** A reader accepts a manifest whose first line equals `MANIFEST_HEADER`, so without the +- **A diff that changes what a manifest written by an earlier binary replays to - what + `read_manifest` and `write_manifest` (`dyn_modules.cpp`) carry, in what order, in what encoding, + a key line the manifest did not carry before, or a change to what an existing row registers - + bumps the version in `MANIFEST_HEADER` in the same change.** A reader accepts a manifest whose first line equals `MANIFEST_HEADER`, so without the bump an older manifest decodes the changed bytes as a wrong registration with no diagnostic. - **A diff that gives `read_manifest` a new kind of row - one it pushes into `rows` - gives the @@ -37,8 +40,8 @@ whitespace a form allows - or changes when a guarded require or a guarded group member is skipped, teaches every site that decides it the same spelling and the same skip-or-take decision, in the same change: the text collector `getAllRequireReq` (`ast_parse.cpp`), the - parser's `ast_requireGuardAvailable` (`parser_impl.cpp`), and the member walk - `moduleGroupMemberAvailable` (`module_builtin_rtti.cpp`).** The prerequisite walk collects + parser's `ast_requireGuardAvailable` (`src/parser/parser_impl.cpp`), and the member walk + `moduleGroupMemberAvailable` (`src/builtin/module_builtin_rtti.cpp`).** The prerequisite walk collects requires from the source text before any parse, so a spelling only the parser reads is a module the walk never compiles, a decision the two make differently is a require the parse takes with no module behind it, and a member the calls take but the require did not is a call @@ -49,20 +52,16 @@ late walk runs mid-parse of another module, and a field restored by a plain statement after the walk is not restored by an unwind through it. -- **A diff that changes the module-cache record header `writebackModules` writes and - `trySerializeProgramModule` reads (`ast_parse.cpp`) - a field added, removed, reordered or - re-typed - bumps `AstSerializer::getVersion()` (`include/daScript/ast/ast_serializer.h`, - repo root) in the same change.** A reader accepts a stream whose stored version equals - `getVersion()`, so without the bump an older cache decodes the changed header as the old one - with no diagnostic. - -- **Removing the `setDeferredModuleLoader` call from `require_dynamic_modules` - (`dyn_modules.cpp`) is a defect.** A descriptor compiled during the scan can require a module - an earlier replay deferred, and with no loader installed that require fails. +- **A diff that changes the bytes the module-cache record header `writebackModules` writes and + `trySerializeProgramModule` reads (`ast_parse.cpp`) - their content, order or encoding - bumps + `AstSerializer::getVersion()` (`include/daScript/ast/ast_serializer.h`, repo root) in the same + change.** A reader accepts a stream whose stored version equals `getVersion()`, so without the + bump an older cache decodes the changed header as the old one with no diagnostic. -- **A diff that moves the `setDeferredModuleLoader` call in `require_dynamic_modules` keeps it - above the first `init_modules_for_folder` call, in the same change.** That call compiles the - descriptors, and a descriptor can require a module an earlier replay deferred. +- **The `setDeferredModuleLoader` call in `require_dynamic_modules` (`dyn_modules.cpp`) stays, + and stays above the first `init_modules_for_folder` call - removing it or moving it below is a + defect.** That call compiles the descriptors, and a descriptor can require a module an earlier + replay deferred, which fails with no loader installed. - **A diff that changes how `Function::getMangledName` (`ast.cpp`) forms a name, or which module `Module::addFunction` (`ast_module.cpp`) files a builtin function under, bumps diff --git a/src/ast/ast_parse.cpp b/src/ast/ast_parse.cpp index 1e5e35a97a..15e983cdc1 100644 --- a/src/ast/ast_parse.cpp +++ b/src/ast/ast_parse.cpp @@ -620,6 +620,7 @@ namespace das { static DAS_THREAD_LOCAL(int64_t) totCacheRead; static DAS_THREAD_LOCAL(int64_t) cntCacheRead; static DAS_THREAD_LOCAL(int64_t) totCacheMacroSim; + static DAS_THREAD_LOCAL(int64_t) totCacheFinalize; // deserialization may have left the active gc root pointing at (or the old program // owning) a module root that dies with the old program — repoint around the swap so @@ -766,14 +767,14 @@ namespace das { if ( !serializer_read->trySerialize([&](AstSerializer & serializer) { serializer << saved_hash; serializer << saved_size; - serializer << saved_filename; + serializer.serializeTemp(saved_filename); // macro file dependencies (Program::moduleCacheDependencies) ride the record // header, not the payload: they must be validated BEFORE the payload is trusted serializer << depCount; if ( depCount <= SER_MAX_MACRO_DEPS ) { savedDeps.resize(depCount); for ( auto & dep : savedDeps ) { - serializer << get<0>(dep); + serializer.serializeTemp(get<0>(dep)); serializer << get<1>(dep); serializer << get<2>(dep); } @@ -782,7 +783,7 @@ namespace das { if ( depCount <= SER_MAX_MACRO_DEPS && reqCount <= SER_MAX_MACRO_DEPS ) { savedReq.resize(reqCount); for ( auto & req : savedReq ) { - serializer << req; + serializer.serializeTemp(req); } } serializer << payload_size; @@ -849,11 +850,17 @@ namespace das { // failure branches below carry everything a human needs deriveSourcePolicies(access, fileName, program->policies); serializer_read->readingRecord ++; +#if DAS_SERIALIZE_PROFILE + auto profT0 = ref_time_ticks(); +#endif bool read_ok = serializer_read->trySerialize([&](AstSerializer & serializer) { serializer.thisModuleGroup = &libGroup; serializer.fileAccess = access.get(); serializer.serializeProgram(program, libGroup); }); +#if DAS_SERIALIZE_PROFILE + serializer_read->profRecords.push_back({fileName, 0, payload_size, get_time_usec(profT0)}); +#endif serializer_read->readingRecord --; if ( read_ok && !program->failed() && !serializer_read->failed ) { @@ -1149,12 +1156,15 @@ namespace das { program->inferPassesUsed = 0; // reset once per module; inferTypesDirty accumulates across all inferTypes legs (incl. restartInfer) program->policies = policies; // before the cache read: the reader compares the record's policies against this compile's + // ARCHITECTURE.md sec.1 auto & serializer_read = daScriptEnvironment::getBound()->serializer_read; uint64_t macroSim0 = serializer_read ? serializer_read->totMacroTime : 0; + uint64_t finalize0 = serializer_read ? serializer_read->totFinalizeTime : 0; if ( trySerializeProgramModule(program, access, fileName, libGroup, logs) ) { program->access = access; // the read replaced the program object auto readT = get_time_usec(time0); auto macroSimT = int64_t(serializer_read->totMacroTime - macroSim0); + *totCacheFinalize += int64_t(serializer_read->totFinalizeTime - finalize0); *totCacheRead += readT; *cntCacheRead += 1; *totCacheMacroSim += macroSimT; @@ -1628,6 +1638,11 @@ namespace das { size_t len_at = serializer_write->buffer->writingSize(); *serializer_write << payload_size; size_t payload_start = serializer_write->buffer->writingSize(); +#if DAS_SERIALIZE_PROFILE + AstSerializer::ProfRecord profRecord; + profRecord.file = fileName; + auto profT0 = ref_time_ticks(); +#endif if ( program->thisModule ) { // the program owns its module (entry script) - write as is, never mutate it serializer_write->serializeProgram(program, libGroup); @@ -1639,6 +1654,12 @@ namespace das { } payload_size = uint64_t(serializer_write->buffer->writingSize() - payload_start); serializer_write->buffer->patch(len_at, &payload_size, sizeof(payload_size)); +#if DAS_SERIALIZE_PROFILE + profRecord.usec = get_time_usec(profT0); + profRecord.payloadBytes = payload_size; + profRecord.lengthWordBytes = uint64_t(payload_start - len_at); + serializer_write->profRecords.push_back(profRecord); +#endif } serializer_write->writtenModules = serializer_write->parsedModules.size(); } @@ -2049,6 +2070,7 @@ namespace das { *totCacheRead = 0; *cntCacheRead = 0; *totCacheMacroSim = 0; + *totCacheFinalize = 0; daScriptEnvironment::getBound()->macroTimeTicks = 0; vector req; vector missing; @@ -2182,7 +2204,7 @@ namespace das { auto totT = get_time_usec(time0); logs << "total compile took " << (totT / 1000000.) << ", " << fileName << " -- " << res->totalFunctions << " functions\n" << "\trequire " << (preqT / 1000000.) << "\n" - << "\tcache read " << (*totCacheRead / 1000000.) << " (" << *cntCacheRead << " modules, macro simulate " << (*totCacheMacroSim / 1000000.) << ")\n" + << "\tcache read " << (*totCacheRead / 1000000.) << " (" << *cntCacheRead << " modules, decode " << ((*totCacheRead - *totCacheFinalize) / 1000000.) << ", finalize " << (*totCacheFinalize / 1000000.) << " of which macro simulate " << (*totCacheMacroSim / 1000000.) << ")\n" << "\tparse " << (*totParse / 1000000.) << "\n" << "\tinfer " << (*totInfer / 1000000.) << "\n" << "\toptimize " << (*totOpt / 1000000.) << "\n" diff --git a/src/builtin/ARCHITECTURE.md b/src/builtin/ARCHITECTURE.md index 1cd6e9ff6e..4233b5a852 100644 --- a/src/builtin/ARCHITECTURE.md +++ b/src/builtin/ARCHITECTURE.md @@ -173,3 +173,81 @@ collects. A POSIX pipe carries 64 KB by default, and Windows sizes an anonymous when asked for the default - a chatty child under the watchdog's tick moves at most 16 KB a second through one of those, the pipe's 4 KB four drains a second - so the Windows pipe is created at the POSIX capacity, and every platform drains the same bursts. + +## 6. What a module-cache record's bytes are + +`module_builtin_ast_serialize.cpp` writes and reads one record per module. Every table below is +per record: it clears in `AstSerializer::clearNodeIds` at the end of each program, so no record +points into another and a reader that skipped one can still read the next. The file, module and +expression-class tables number from 1 and keep 0 for null, a fresh entry taking its number from +the table's size after its own insert; the string table numbers from 0, since a string is never +null; the type table's entries start at 3, after its three tags. + +**Sizes.** A 32-bit value streams as an adaptive size - seven bits a byte, low bits first, the +high bit saying another byte follows, so one byte below 128, two below 16384, at most five +(`encodeAdaptiveSize32`, `serializeAdaptiveSize32`). Flags, offsets, indices, counts and table +numbers are all small, so a `uint32_t` and a zigzagged `int32_t` both stream that way; a 32-bit +hash keeps a raw four-byte write, since a hash has no small end - the `tag()` field and an +expression class's first mention are the two. Reading takes the bytes straight off the vector +storage while five are in reach and falls back to a checked byte-at-a-time read at the end of the +buffer. A field group encodes into one local buffer and writes once. + +**Strings.** A string streams as its number in a per-record table, numbered from 0 in +first-mention order; a first mention is that number followed by the bytes. The table holds pointers to strings +that live in process memory for the record - the AST's own fields, which nothing changes while a +record is written or read - so neither side copies them and neither side looks into the storage. +A string with no home past the call goes through `serializeTemp`, which parks one copy in the +record's arena for the table to point at; the same name on any other type is the ordinary +operator, so a container element or a field of a local record streams through `serializeTemp` +whatever its type. The empty string is most of the occurrences, every unaliased `TypeDecl` +carrying one, so it answers from its own slot without hashing. + +**Types.** A `TypeDecl` reference streams as a number (`TypeRef`): 0 null, 1 an inline payload, +2 a fresh table entry whose payload follows, 3 and up an entry already in the table followed by +its own `at`. A type carrying expressions - a fixed-dim expression, a type macro - is not tableable and +streams inline, and so does any type with such a child: the table dedups by content, and an +expression is not content the key covers. The writer keys the live type and hashes and compares +its content recursively, the two `findAlias` cache bits masked out of both the key and the stream +since they are a cache and not part of the type; the reader keeps the first decoded instance and +deep-copies it for every later reference, so no two nodes share a type. A type takes its number +AFTER its payload on both sides: the payload can carry a same-module structure inline whose field +holds a type equal to this one, and with the number taken first the reader would copy a prototype +it has not finished decoding. The writer counts fresh entries apart from the map, since an equal +type registered from inside the payload keeps the map's number while the reader pushes every +fresh entry. + +**Files and lines.** A `FileInfo` streams as its per-record number, the first mention carrying +the payload after the number; a record names its own few files, so the number is one byte. A +one-entry cache sits in front of the writer's map, because `LineInfo`s come in runs from one +file. A `LineInfo` is coded against the previous one of the record: a shape byte (`LineShape`) - +bit 7 the file is the previous one's, else its number follows; bit 6 `last_line` equals `line`, +else the difference follows; the low six bits the line's delta from the previous line, -31..31, +or 63 when a zigzag delta follows - then the column and the zigzag span to `last_column`. + +**Modules, expression classes, functions.** A `Module` and an expression class each stream as a +per-record number, the first mention carrying the module's name hash or the class's rtti hash and +every later one a byte; the reader keeps the hash beside the module it resolved, so a later +mention of a module the library lacks names it. Under `ignoreEmptyExternal` a module the library lacks reads as null; +outside that context a module the library cannot resolve fails the record. A cross-module +function's mangled name is computed once per record and kept in a table keyed by the function, +since the same function is named at every call site of a module. + +**The storage.** The vector storage (`SerializationStorageVector`, `ast_serializer.h`) backs +every in-process stream. Writing grows the vector by doubling and counts the bytes in `writePos`, +so a field write is a bounds check and a store rather than a resize, and the serializer writes +and reads through it directly when the storage is that one. `flush()` trims the vector to the +bytes written and `serializeProgram` calls it at the end of every program, so `buffer` is exactly +the stream at a quiescent point; a storage that buffers ahead settles what it holds in its own +`flush()`. + +## 7. The serialization profile rail + +`DAS_SERIALIZE_PROFILE` (`ast_serializer.h`, 0 unless the build defines it) compiles in the +stream's size and time breakdown; off, every `DAS_SER_PROFILE` expands to nothing and the +serializer carries no profile state. On, every compound serialize opens a named frame: bytes and +ticks accrue to a frame tree keyed by parent and name, and an aggregate per name carries self +bytes beside the bytes and ticks of that name's outermost frames only, so a recursive shape +counts once. Two histograms - string payloads by text, `TypeDecl` payloads by mangled name - +carry the dedup estimate, and the record layer (`ast_parse.cpp`) appends one row per record with +its file, payload size and time. `ModuleFileCache::finish` prints the report through +`AstSerializer::profReport`, for the reader and the writer alike. diff --git a/src/builtin/REVIEW.md b/src/builtin/REVIEW.md index 096af89c02..a1b9e0c09a 100644 --- a/src/builtin/REVIEW.md +++ b/src/builtin/REVIEW.md @@ -20,20 +20,18 @@ modules the scan covers. - **A diff that changes the bytes a module-cache record carries or what they resolve to - a - field added, removed, reordered, re-typed or given a new meaning, wherever the edit lives, or - a change anywhere to which module owns a streamed annotation, function or type - bumps the - version `getVersion()` returns in `include/daScript/ast/ast_serializer.h`, in the same - change** - a reader accepts a stream only when its stored version equals `getVersion()`, so - without the bump an older cache passes that check and decodes the changed bytes as something - else. A C++ layout change to a handled type or an AST class is not a record byte: functions - and annotations stream by module hash and name and re-resolve against the running binary. - -- **A diff that makes a record written before it wrong, wherever the edit lives - the bytes - still decode, but what they encode is no longer what this build would write - bumps the - version `getVersion()` returns in `include/daScript/ast/ast_serializer.h`, in the same - change** - the version is the only - thing that discards a cache a user already holds, so without the bump every later launch is - served the stale record. + field added, removed, reordered or re-typed in `module_builtin_ast_serialize.cpp`, an encoding + changed there, or a change to which module owns a streamed annotation, function or type - + bumps the version `getVersion()` returns in `include/daScript/ast/ast_serializer.h`, in the + same change** - a reader accepts a stream only when its stored version equals `getVersion()`, + so without the bump an older cache passes that check and decodes the changed bytes as + something else. A C++ layout change to a handled type or an AST class is not a record byte. + +- **A diff that makes a record written before it wrong - the bytes still decode, but what they + encode is no longer what this build would write - bumps the version `getVersion()` returns in + `include/daScript/ast/ast_serializer.h`, in the same change** - the version is the only thing + that discards a cache a user already holds, so without the bump every later launch is served + the stale record. - **A diff that streams or compares a `CodeOfPolicies` field in `module_builtin_ast_serialize.cpp` outside `DAS_MODULE_CACHE_POLICY_FIELDS` is a defect - put the field on the list instead** - the @@ -41,11 +39,11 @@ other policies, so a field handled outside it is written without being compared, or compared without being written. -- **A diff that adds a diagnostic in `AstSerializer::serializeProgram`, - `AstSerializer::serializeProgramImpl` or `ModuleFileCache` (`module_builtin_ast_serialize.cpp`) - without the serializer's `quietCache` gate, or drops that gate from one already there, is a - defect** - the default cache is on unasked for an ordinary run, - so an ungated line becomes output every user sees. +- **A diff that adds to `AstSerializer::serializeProgram`, `AstSerializer::serializeProgramImpl` + or `ModuleFileCache` (`module_builtin_ast_serialize.cpp`) a diagnostic the default build + compiles, without the serializer's `quietCache` gate - or drops that gate from one already + there - is a defect** - the module cache is on by default, so an ungated line prints on an + ordinary run. - **A diff that adds a builtin a `.das_module` descriptor can call whose effect outlives the descriptor's own program - a row in a process-wide registration table a warm start must @@ -70,8 +68,8 @@ - **A diff that changes what `ModuleFileCache::defaultPath` folds into the module-cache key - the binary, the command line, the environment names, or which script arguments count - updates - the cache-key paragraph of `ARCHITECTURE.md` in the same change.** The key is what stops a - native-compiled module serving a cross compile, so a wrong description of it gets trusted. + section 2 of `ARCHITECTURE.md` in the same change.** The key is what stops a native-compiled + module serving a cross compile, so a wrong description of it gets trusted. - **A diff that adds a `std::filesystem` call in `module_builtin_fio.cpp` passes every path into it through `das_to_path` and every path out of it through `path_to_das`.** On Windows a path @@ -84,9 +82,8 @@ change.** C++ carries no `[arch]` annotation, so nothing but this rule keeps a cited section true. -- **In a C++ type das binds through an annotation with `addField` (declared in this folder's - `module_builtin_rtti.cpp`, `module_builtin_fio.cpp`, `module_builtin_ast_annotations*.cpp`), - a member whose size differs between the standard libraries the repo's targets use +- **In a C++ type das binds through an annotation with `addField` in this folder, a member + whose size differs between the standard libraries the repo's targets use (`std::mutex`, `std::function`, `condition_variable`; not `std::string`) or a platform struct embedded by value (`struct stat`) is declared after the last das-visible field.** A cross-compiled exe bakes the host's field offsets into the code it generates, and the @@ -103,3 +100,25 @@ through an annotation states its `--jit-check-abi` result for a cross target in its own PR description.** The check reports a mismatch at the bundle's first launch, for the types the bundle links; nothing native can observe one. + +- **A `string` streamed in `module_builtin_ast_serialize.cpp` whose storage does not outlive the + module-cache record being written or read - a computed mangled name, a lookup key, a container + element, a field of a local - goes through `serializeTemp`, never plain `operator<<`.** The + string table holds pointers into the caller's own bytes, so an entry left pointing at a dead + local is what every later mention of that string writes or reads. + +- **A diff that adds a per-record table or vector the serializer numbers into + (`module_builtin_ast_serialize.cpp`) clears it in `AstSerializer::clearNodeIds`, in the same + change.** A later compile reuses a freed address, so a table that outlives its record answers a + new node with the old node's number or name. + +- **Weakening `tests-cpp/small/test_env_serializer.cpp`'s position census - the cold-to-warm + comparison of every `line`, `column` and `last_column` - is a defect.** The GC reads a local's + visibility range to decide the local is dead, and inlining can put two locals on one line + where they differ only in column, so a program restored with rounded columns has its GC free + a local still in use. + +- **A diff that reads a writing `SerializationStorageVector`'s `buffer` outside + `AstSerializer::serializeProgram` (`module_builtin_ast_serialize.cpp`) - to hand the bytes out, + hash them, or write them - calls `flush()` first.** The writer grows the vector by doubling and + counts the bytes in `writePos`, so before a flush the vector is longer than the stream. diff --git a/src/builtin/module_builtin_ast_serialize.cpp b/src/builtin/module_builtin_ast_serialize.cpp index 4c87f731bf..bc080a2073 100644 --- a/src/builtin/module_builtin_ast_serialize.cpp +++ b/src/builtin/module_builtin_ast_serialize.cpp @@ -45,6 +45,7 @@ namespace das { } writing = isWriting; buffer = storage; + bufferAsVector = storage->asVector(); } void AstSerializer::collectFileInfo ( vector & orphanedFileInfos ) { @@ -112,6 +113,23 @@ namespace das { writtenIds.clear(); readNodes.clear(); pendingRefs.clear(); + writeMangledNames.clear(); + writeStrings.clear(); + emptyStringNumber = -1; + readStrings.clear(); + stringArena.clear(); + writeFileInfos.clear(); + readFileInfos.clear(); + lastWriteFileInfo = nullptr; + lineBaseFile = nullptr; + lineBaseLine = 0; + writeTypes.clear(); + writeFreshTypeCount = 0; + readTypes.clear(); + writeExprClasses.clear(); + readExprClasses.clear(); + writeModules.clear(); + readModules.clear(); } void AstSerializer::patch () { @@ -136,10 +154,6 @@ namespace das { fieldRefs.clear(); } - void AstSerializer::write ( const void * data, size_t size ) { - buffer->write(data, size); - } - void AstSerializer::read ( void * data, size_t size ) { if ( !buffer->read(data, size) ) { throw_formatted_error("ast serializer read overflow"); @@ -171,48 +185,231 @@ namespace das { #define HASH_TAG(tag) tag,hash_tag(tag) void AstSerializer::tag ( const char * name, uint32_t hash ) { + DAS_SER_PROFILE(*this, "tag"); if ( writing ) { - *this << hash; + serialize(hash); } else { uint32_t hash2 = 0; - *this << hash2; + serialize(hash2); if ( hash != hash2 ) { throw_formatted_error("ast serializer tag '%s' mismatch", name); } } } +#if DAS_SERIALIZE_PROFILE + void AstSerializer::profBegin ( const char * name ) { + if ( profNodes.empty() ) { + profNodes.push_back({}); + profStartTicks = ref_time_ticks(); + } + uint32_t nameId = ~0u; + for ( auto & [ptr, id] : profNameIds ) { + if ( ptr == name ) { nameId = id; break; } + } + if ( nameId == ~0u ) { + nameId = uint32_t(profNames.size()); + profNames.push_back(name); + profAgg.push_back({}); + profNameIds.push_back({name, nameId}); + } + uint32_t parent = profStack.empty() ? 0 : profStack.back().node; + uint32_t node = ~0u; + for ( auto child : profNodes[parent].children ) { + if ( profNodes[child].nameId == nameId ) { node = child; break; } + } + if ( node == ~0u ) { + node = uint32_t(profNodes.size()); + profNodes.push_back({nameId, parent, 0, 0, 0, {}}); + profNodes[parent].children.push_back(node); + } + profStack.push_back({nameId, node, profPosition(), 0, ref_time_ticks()}); + } + + void AstSerializer::profEnd () { + auto frame = profStack.back(); + profStack.pop_back(); + uint64_t bytes = profPosition() - frame.startBytes; + int64_t ticks = get_time_nsec(frame.startTicks); + auto & node = profNodes[frame.node]; + node.inclBytes += bytes; + node.count += 1; + node.inclTicks += ticks; + auto & agg = profAgg[frame.nameId]; + agg.count += 1; + agg.selfBytes += bytes - frame.childBytes; + bool outermost = true; + for ( auto & f : profStack ) { + if ( f.nameId == frame.nameId ) { outermost = false; break; } + } + if ( outermost ) { + agg.outermostInclBytes += bytes; + agg.outermostInclTicks += ticks; + } + if ( !profStack.empty() ) profStack.back().childBytes += bytes; + } + + void AstSerializer::profString ( const string & str ) { + profStrings[str] += 1; + } + + void AstSerializer::profType ( const TypeDecl * type, uint64_t bytes ) { + profTypes[type->getMangledName(true)] += 1; + profTypeBytes += bytes; + } + + static string profFmtBytes ( uint64_t b ) { + char buf[64]; + if ( b >= 10*1024*1024 ) snprintf(buf, sizeof(buf), "%.1f MB", double(b) / (1024.0*1024.0)); + else if ( b >= 10*1024 ) snprintf(buf, sizeof(buf), "%.1f KB", double(b) / 1024.0); + else snprintf(buf, sizeof(buf), "%llu B", (unsigned long long) b); + return buf; + } + + // src/builtin/ARCHITECTURE.md sec.7 + void AstSerializer::profReport ( TextWriter & tw ) const { + if ( profNodes.empty() ) return; + uint64_t total = 0; + int64_t totalTicks = 0; + for ( auto & n : profNodes ) { + if ( n.parent == 0 && &n != &profNodes[0] ) { total += n.inclBytes; totalTicks += n.inclTicks; } + } + if ( total == 0 ) total = 1; + auto pct = [&](uint64_t b) { return 100.0 * double(b) / double(total); }; + tw << "=== serialization profile (" << (writing ? "writing" : "reading") << "): " + << profFmtBytes(total) << " (" << total << " bytes), " << (totalTicks / 1000000.0) << " ms inside frames ===\n"; + das_hash_map byText; + for ( size_t i = 0; i != profNames.size(); ++i ) { + auto & a = byText[profNames[i]]; + a.selfBytes += profAgg[i].selfBytes; + a.outermostInclBytes += profAgg[i].outermostInclBytes; + a.count += profAgg[i].count; + a.outermostInclTicks += profAgg[i].outermostInclTicks; + } + vector> rows(byText.begin(), byText.end()); + auto printRows = [&](const char * title, uint64_t ProfAgg::* field, bool showTicks) { + sort(rows.begin(), rows.end(), [&](auto & a, auto & b) { return a.second.*field > b.second.*field; }); + tw << "-- " << title << " --\n"; + char line[256]; + int shown = 0; + for ( auto & r : rows ) { + uint64_t v = r.second.*field; + if ( v == 0 || shown++ >= 48 ) break; + if ( showTicks ) { + snprintf(line, sizeof(line), "%-28s %12llu %6.2f%% %10llu x %8.1f %8.2f ms\n", r.first.c_str(), + (unsigned long long) v, pct(v), (unsigned long long) r.second.count, + double(v) / double(r.second.count ? r.second.count : 1), r.second.outermostInclTicks / 1000000.0); + } else { + snprintf(line, sizeof(line), "%-28s %12llu %6.2f%% %10llu x %8.1f\n", r.first.c_str(), + (unsigned long long) v, pct(v), (unsigned long long) r.second.count, + double(v) / double(r.second.count ? r.second.count : 1)); + } + tw << line; + } + }; + printRows("self bytes by frame (name, bytes, %, count, avg)", &ProfAgg::selfBytes, false); + printRows("inclusive bytes by frame, outermost only (name, bytes, %, count, avg, ms)", &ProfAgg::outermostInclBytes, true); + tw << "-- tree (depth<=6, >=0.5%) --\n"; + vector> children(profNodes.size()); + for ( uint32_t i = 1; i < uint32_t(profNodes.size()); ++i ) children[profNodes[i].parent].push_back(i); + for ( auto & c : children ) sort(c.begin(), c.end(), [&](uint32_t a, uint32_t b) { return profNodes[a].inclBytes > profNodes[b].inclBytes; }); + vector> walk; + for ( auto it = children[0].rbegin(); it != children[0].rend(); ++it ) walk.push_back({*it, 0}); + char line[512]; + while ( !walk.empty() ) { + auto [ni, depth] = walk.back(); walk.pop_back(); + auto & n = profNodes[ni]; + if ( pct(n.inclBytes) < 0.5 ) continue; + snprintf(line, sizeof(line), "%*s%-*s %12llu %6.2f%% %9llu x %8.1f %8.2f ms\n", depth*2, "", 30 - depth*2, + profNames[n.nameId].c_str(), (unsigned long long) n.inclBytes, pct(n.inclBytes), (unsigned long long) n.count, + double(n.inclBytes) / double(n.count ? n.count : 1), n.inclTicks / 1000000.0); + tw << line; + if ( depth < 6 ) { + for ( auto it = children[ni].rbegin(); it != children[ni].rend(); ++it ) walk.push_back({*it, depth + 1}); + } + } + { + uint64_t occ = 0, bytes = 0, distinct = profStrings.size(), distinctBytes = 0; + for ( auto & [s, c] : profStrings ) { occ += c; bytes += uint64_t(s.size()) * c; distinctBytes += s.size(); } + tw << "-- strings: " << occ << " occurrences, " << profFmtBytes(bytes) << " payload, " + << distinct << " distinct (" << profFmtBytes(distinctBytes) << "); a per-record string table would take ~" + << profFmtBytes(distinctBytes + distinct + occ * 2) << "\n"; + vector> top(profStrings.begin(), profStrings.end()); + sort(top.begin(), top.end(), [](auto & a, auto & b) { return uint64_t(a.first.size()) * a.second > uint64_t(b.first.size()) * b.second; }); + for ( size_t i = 0; i < top.size() && i < 24; ++i ) { + snprintf(line, sizeof(line), " %8u x %5u B %.60s\n", top[i].second, unsigned(top[i].first.size()), top[i].first.c_str()); + tw << line; + } + } + { + uint64_t occ = 0; + for ( auto & [s, c] : profTypes ) occ += c; + tw << "-- TypeDecl payloads: " << occ << " written, " << profFmtBytes(profTypeBytes) << ", " + << profTypes.size() << " distinct mangled types; a per-record type table would take ~" + << profFmtBytes(profTypes.size() ? profTypeBytes * profTypes.size() / (occ ? occ : 1) + occ * 2 : 0) << "\n"; + vector> top(profTypes.begin(), profTypes.end()); + sort(top.begin(), top.end(), [](auto & a, auto & b) { return a.second > b.second; }); + for ( size_t i = 0; i < top.size() && i < 16; ++i ) { + snprintf(line, sizeof(line), " %8u x %.80s\n", top[i].second, top[i].first.c_str()); + tw << line; + } + } + if ( !profRecords.empty() ) { + auto recs = profRecords; + sort(recs.begin(), recs.end(), [](auto & a, auto & b) { return a.payloadBytes > b.payloadBytes; }); + uint64_t sum = 0; + for ( auto & r : recs ) sum += r.lengthWordBytes + r.payloadBytes; + tw << "-- records: " << recs.size() << ", " << profFmtBytes(sum) << " (payload, length word, ms, file) --\n"; + for ( auto & r : recs ) { + snprintf(line, sizeof(line), "%12llu %6llu %8.2f %s\n", (unsigned long long) r.payloadBytes, + (unsigned long long) r.lengthWordBytes, r.usec / 1000.0, r.file.c_str()); + tw << line; + } + } + } +#endif + //////////////////////////////////////////////////////////////////////////// - // Encode numbers by their size: - // 0...254 (just value) => 1 byte - // 254 (tag) + 2 bytes value => 3 bytes - // 255 (tag) + 4 bytes value => 5 bytes + // src/builtin/ARCHITECTURE.md sec.6 void AstSerializer::serializeAdaptiveSize32 ( uint32_t & size ) { if ( writing ) { - if ( size < 254 ) { - uint8_t sz = static_cast(size); - *this << sz; - } else if ( size <= 65535 ) { - uint8_t tag = 254; - uint16_t sz = static_cast(size); - *this << tag << sz; - } else { - uint8_t tag = 255; - uint32_t sz = static_cast(size); - *this << tag << sz; - } + uint8_t enc[5]; + write(enc, encodeAdaptiveSize32(enc, size)); } else { - uint8_t tag = 0; *this << tag; - if ( tag < 254 ) { - size = tag; - } else if ( tag == 254 ) { - uint16_t sz = 0; *this << sz; - size = sz; - } else { - uint32_t sz = 0; *this << sz; - size = sz; + if ( bufferAsVector ) { + auto & pos = bufferAsVector->bufferPos; + auto & buf = bufferAsVector->buffer; + if ( buf.size() - pos >= 5 ) { + const uint8_t * p = buf.data() + pos; + uint32_t b = p[0]; + if ( b < 0x80 ) { + size = b; + pos += 1; + return; + } + uint32_t value = b & 0x7f; + size_t n = 1; + for ( ;; ) { + b = p[n]; + value |= (b & 0x7f) << (7 * n); + n ++; + if ( b < 0x80 ) break; + SERIALIZER_VERIFYF(n < 5, "corrupt stream: adaptive size runs past five bytes"); + } + size = value; + pos += n; + return; + } + } + uint32_t value = 0; + for ( size_t n = 0; ; ++n ) { + SERIALIZER_VERIFYF(n < 5, "corrupt stream: adaptive size runs past five bytes"); + uint8_t b = 0; *this << b; + value |= uint32_t(b & 0x7f) << (7 * n); + if ( b < 0x80 ) break; } + size = value; } } @@ -241,6 +438,7 @@ namespace das { AstSerializer & AstSerializer::operator << ( SerializeNodeId & value ) { dtag(HASH_TAG("SerializeNodeId")); + DAS_SER_PROFILE(*this, "NodeId"); serializeAdaptiveSize32(value.index); if ( !writing ) { // numbers arrive in first-mention order, so a first sight is exactly the next @@ -256,24 +454,64 @@ namespace das { return *this; } - AstSerializer & AstSerializer::operator << ( string & str ) { + // src/builtin/ARCHITECTURE.md sec.6 + AstSerializer & AstSerializer::serializeString ( string & str, bool temp ) { dtag(HASH_TAG("string")); + DAS_SER_PROFILE(*this, "string"); if ( writing ) { - uint64_t size = str.size(); - serializeAdaptiveSize64(size); - write((void *)str.data(), size); +#if DAS_SERIALIZE_PROFILE + profString(str); +#endif + SERIALIZER_VERIFYF(uint64_t(str.size()) < (uint64_t(1) << 32), "string too long"); + uint32_t length = uint32_t(str.size()); + if ( length == 0 && emptyStringNumber >= 0 ) { + uint32_t known = uint32_t(emptyStringNumber); + serializeAdaptiveSize32(known); + return *this; + } + StringView view { str.data(), length }; + if ( auto it = writeStrings.find(view); it != writeStrings.end() ) { + serializeAdaptiveSize32(it->second); + return *this; + } + if ( temp ) { + stringArena.push_back(make_unique(str)); + view.data = stringArena.back()->data(); + } + uint32_t index = uint32_t(writeStrings.size()); + writeStrings.emplace(view, index); + if ( length == 0 ) emptyStringNumber = int32_t(index); + serializeAdaptiveSize32(index); + serializeAdaptiveSize32(length); + write(str.data(), length); } else { - uint64_t size = 0; - serializeAdaptiveSize64(size); - verifyLength(size); - str.resize(size); - read(&str[0], size); + uint32_t index = 0; + serializeAdaptiveSize32(index); + if ( index < readStrings.size() ) { + auto & view = readStrings[index]; + str.assign(view.data, view.length); + return *this; + } + SERIALIZER_VERIFYF(index == readStrings.size(), "corrupt stream: string #%u past the %u numbered so far", + index, unsigned(readStrings.size())); + uint32_t length = 0; + serializeAdaptiveSize32(length); + verifyLength(length); + str.resize(length); + if ( length ) read(&str[0], length); + const char * data = str.data(); + if ( temp ) { + stringArena.push_back(make_unique(str)); + data = stringArena.back()->data(); + } + readStrings.push_back({data, length}); } return *this; } AstSerializer & AstSerializer::operator << ( const char * & value ) { dtag(HASH_TAG("const char *")); + DAS_SER_PROFILE(*this, "cstring"); bool is_null = value == nullptr; *this << is_null; if ( is_null ) { @@ -299,6 +537,7 @@ namespace das { template AstSerializer & AstSerializer::operator << ( safebox & box ) { dtag(HASH_TAG("Safebox")); + DAS_SER_PROFILE(*this, "Safebox"); if ( writing ) { uint64_t size = box.unlocked_size(); *this << size; box.foreach_with_hash ([&](VT obj, uint64_t hash) { @@ -320,10 +559,11 @@ namespace das { template void AstSerializer::serialize_hash_map ( das_hash_map & value ) { dtag(HASH_TAG("DasHashmap")); + DAS_SER_PROFILE(*this, "HashMap"); if ( writing ) { uint64_t size = value.size(); *this << size; for ( auto & item : value ) { - *this << item.first << item.second; + serializeTemp(item.first); serializeTemp(item.second); } return; } @@ -332,7 +572,7 @@ namespace das { das_hash_map deser; deser.reserve(size); for ( uint64_t i = 0; i < size; i++ ) { - K k; V v; *this << k << v; + K k; V v; serializeTemp(k); serializeTemp(v); deser.emplace(das::move(k),das::move(v)); } value = das::move(deser); @@ -350,10 +590,11 @@ namespace das { template void AstSerializer::serialize_hash_map ( das_insert_only_hash_map & value ) { dtag(HASH_TAG("DasHashmap")); + DAS_SER_PROFILE(*this, "HashMap"); if ( writing ) { uint64_t size = value.size(); *this << size; for ( auto & item : value ) { - *this << item.first << item.second; + serializeTemp(item.first); serializeTemp(item.second); } return; } @@ -362,7 +603,7 @@ namespace das { das_insert_only_hash_map deser; deser.reserve(size); for ( uint64_t i = 0; i < size; i++ ) { - K k; V v; *this << k << v; + K k; V v; serializeTemp(k); serializeTemp(v); deser.emplace(das::move(k),das::move(v)); } value = das::move(deser); @@ -414,47 +655,51 @@ namespace das { return ptr->module == thisModule; } + // src/builtin/ARCHITECTURE.md sec.6 void AstSerializer::writeIdentifications ( Function * & func ) { - string mangeldName = func->getMangledName(); - uint64_t moduleName = func->module->nameHash; - *this << moduleName << mangeldName; + auto & name = writeMangledNames[func]; + if ( name.empty() ) name = func->getMangledName(); + Module * module = func->module; + *this << module; + serializeTemp(name); } void AstSerializer::writeIdentifications ( Enumeration * & ptr ) { - *this << ptr->module->nameHash; + Module * module = ptr->module; + *this << module; uint64_t nameHash = hash64z(ptr->name.c_str()); *this << nameHash; } void AstSerializer::writeIdentifications ( Structure * & ptr ) { - *this << ptr->module->nameHash; + Module * module = ptr->module; + *this << module; uint64_t nameHash = hash64z(ptr->name.c_str()); *this << nameHash; } void AstSerializer::writeIdentifications ( Variable * & ptr ) { - *this << ptr->module->nameHash << ptr->name; + Module * module = ptr->module; + *this << module << ptr->name; } void AstSerializer::writeIdentifications ( TypeInfoMacro * & ptr ) { - *this << ptr->module->nameHash << ptr->name; + Module * module = ptr->module; + *this << module << ptr->name; } auto AstSerializer::readModuleAndNameHash () -> pair { - uint64_t moduleNameHash = 0; + Module * funcModule = nullptr; uint64_t mangledNameHash = 0; - *this << moduleNameHash << mangledNameHash; - auto funcModule = moduleLibrary->findModuleByMangledNameHash(moduleNameHash); - SERIALIZER_VERIFYF(ignoreEmptyExternal || funcModule, "module '%llu' is not found", moduleNameHash); + *this << funcModule << mangledNameHash; return {funcModule, mangledNameHash}; } auto AstSerializer::readModuleAndName () -> pair { - uint64_t moduleNameHash = 0; + Module * funcModule = nullptr; string mangledName; - *this << moduleNameHash << mangledName; - auto funcModule = moduleLibrary->findModuleByMangledNameHash(moduleNameHash); - SERIALIZER_VERIFYF(ignoreEmptyExternal || funcModule, "module '%llu' is not found", moduleNameHash); + *this << funcModule; + serializeTemp(mangledName); return {funcModule, mangledName}; } @@ -517,8 +762,16 @@ namespace das { SERIALIZER_VERIFYF(ptr!=nullptr, "type info macro '%s' is not found", mangledName.c_str()); } +#if DAS_SERIALIZE_PROFILE + static const char * serializePointerName ( Function * ) { return "Pointer"; } + static const char * serializePointerName ( Enumeration * ) { return "Pointer"; } + static const char * serializePointerName ( Structure * ) { return "Pointer"; } + static const char * serializePointerName ( Variable * ) { return "Pointer"; } +#endif + template AstSerializer & AstSerializer::serializePointer ( TT * & ptr ) { + DAS_SER_PROFILE(*this, serializePointerName(ptr)); auto fid = getSerializeId(ptr); *this << fid; if ( !fid.index ) { @@ -544,6 +797,7 @@ namespace das { AstSerializer & AstSerializer::operator << ( FunctionPtr & func ) { dtag(HASH_TAG("FunctionPtr")); + DAS_SER_PROFILE(*this, "FunctionPtr"); if ( writing && func ) { SERIALIZER_VERIFYF(!func->builtIn, "cannot serialize built-in function"); } @@ -568,11 +822,11 @@ namespace das { } } if ( func ) { + DAS_SER_PROFILE(*this, "FunctionPtr.nameCheck"); if ( writing ) { - string name = func->name; - *this << name; + *this << func->name; } else { - string name; *this << name; + string name; serializeTemp(name); string expect = func->name; SERIALIZER_VERIFYF(name == expect, "expected different function %s %s", name.c_str(), expect.c_str()); } @@ -582,6 +836,7 @@ namespace das { AstSerializer & AstSerializer::operator << ( TypeInfoMacro * & ptr ) { dtag(HASH_TAG("TypeInfoMacroPtr")); + DAS_SER_PROFILE(*this, "TypeInfoMacro*"); // TypeInfoMacro is not gc_node and is always external (lives in another // module). It is identified by name+module hash, so the wire form only // needs a presence bit — no pointer/id leaks into the stream. @@ -605,41 +860,142 @@ namespace das { return *this; } + static uint32_t aliasCacheFlagsMask () { + static const uint32_t mask = [] { + TypeDecl probe; + probe.flags = 0; + probe.aliasCacheValid = true; + probe.aliasCacheHasAlias = true; + return probe.flags; + }(); + return mask; + } + + static bool typeTableable ( const TypeDecl * t ) { + if ( t->fixedDimExpr || !t->typeMacroExpr.empty() ) return false; + if ( t->firstType && !typeTableable(t->firstType) ) return false; + if ( t->secondType && !typeTableable(t->secondType) ) return false; + for ( auto & a : t->argTypes ) { + if ( a && !typeTableable(a) ) return false; + } + return true; + } + + static uint64_t hashTypeContent ( const TypeDecl * t ) { + // FNV-1a over the words: hash_combine64 runs wyhash per word and costs 4 ms of a 16 ms write + uint64_t h = 14695981039346656037ull; + auto mix = [&h]( uint64_t v ) { h = (h ^ v) * 1099511628211ull; }; + mix(uint32_t(t->baseType)); + mix(t->flags & ~aliasCacheFlagsMask()); + mix(uint32_t(t->fixedDim)); + mix(uintptr_t(t->structType) >> 4); + mix(uintptr_t(t->enumType) >> 4); + mix(uintptr_t(t->annotation) >> 4); + mix(uintptr_t(t->module) >> 4); + if ( !t->alias.empty() ) mix(hash_block64((const uint8_t *) t->alias.data(), t->alias.size())); + mix(t->firstType ? hashTypeContent(t->firstType) : 7); + mix(t->secondType ? hashTypeContent(t->secondType) : 11); + mix(t->argTypes.size()); + for ( auto & a : t->argTypes ) mix(a ? hashTypeContent(a) : 13); + mix(t->argNames.size()); + for ( auto & n : t->argNames ) mix(hash_block64((const uint8_t *) n.data(), n.size())); + return h; + } + + static bool sameTypeContent ( const TypeDecl * a, const TypeDecl * b ) { + if ( a == b ) return true; + if ( a->baseType != b->baseType ) return false; + if ( ((a->flags ^ b->flags) & ~aliasCacheFlagsMask()) != 0 ) return false; + if ( a->fixedDim != b->fixedDim ) return false; + if ( a->structType != b->structType || a->enumType != b->enumType || a->annotation != b->annotation || a->module != b->module ) return false; + if ( a->alias != b->alias ) return false; + if ( (a->firstType == nullptr) != (b->firstType == nullptr) ) return false; + if ( a->firstType && !sameTypeContent(a->firstType, b->firstType) ) return false; + if ( (a->secondType == nullptr) != (b->secondType == nullptr) ) return false; + if ( a->secondType && !sameTypeContent(a->secondType, b->secondType) ) return false; + if ( a->argTypes.size() != b->argTypes.size() ) return false; + for ( size_t i = 0; i != a->argTypes.size(); ++i ) { + if ( (a->argTypes[i] == nullptr) != (b->argTypes[i] == nullptr) ) return false; + if ( a->argTypes[i] && !sameTypeContent(a->argTypes[i], b->argTypes[i]) ) return false; + } + if ( a->argNames != b->argNames ) return false; + return true; + } + + size_t AstSerializer::TypeHash::operator () ( const TypeDecl * t ) const noexcept { + return size_t(hashTypeContent(t)); + } + + bool AstSerializer::TypeEqual::operator () ( const TypeDecl * a, const TypeDecl * b ) const noexcept { + return sameTypeContent(a, b); + } + + enum TypeRef : uint32_t { TypeRefNull = 0, TypeRefInline = 1, TypeRefFresh = 2, TypeRefFirstEntry = 3 }; + + // src/builtin/ARCHITECTURE.md sec.6 AstSerializer & AstSerializer::operator << ( TypeDeclPtr & type ) { dtag(HASH_TAG("TypeDeclPtr")); - bool is_null = type == nullptr; - *this << is_null; - if ( is_null ) { - if ( !writing ) type = nullptr; - return *this; - } - auto id = getSerializeId(type); - *this << id; + DAS_SER_PROFILE(*this, "TypeDeclPtr"); if ( writing ) { - if ( !isWritten(id) ) { - markWritten(id); + if ( !type ) { + uint32_t ref = TypeRefNull; + serializeAdaptiveSize32(ref); + return *this; + } + if ( !typeTableable(type) ) { + uint32_t ref = TypeRefInline; + serializeAdaptiveSize32(ref); type->serialize(*this); + return *this; } + if ( auto it = writeTypes.find(type); it != writeTypes.end() ) { + uint32_t known = it->second; + serializeAdaptiveSize32(known); + *this << type->at; + return *this; + } + uint32_t ref = TypeRefFresh; + serializeAdaptiveSize32(ref); + type->serialize(*this); + writeFreshTypeCount ++; + writeTypes.emplace(type, writeFreshTypeCount - 1 + TypeRefFirstEntry); } else { - if ( auto node = readNode(id) ) { - type = node; - } else { + uint32_t number = 0; + serializeAdaptiveSize32(number); + if ( number == TypeRefNull ) { + type = nullptr; + return *this; + } + if ( number == TypeRefInline ) { + type = new TypeDecl(); + type->serialize(*this); + return *this; + } + if ( number == TypeRefFresh ) { type = new TypeDecl(); - setReadNode(id, type); type->serialize(*this); + readTypes.push_back(type); + return *this; } + uint32_t index = number - TypeRefFirstEntry; + SERIALIZER_VERIFYF(index < readTypes.size(), "corrupt stream: type #%u past the %u numbered so far", + number, unsigned(readTypes.size())); + type = new TypeDecl(*readTypes[index]); + *this << type->at; } return *this; } AstSerializer & AstSerializer::operator << ( AnnotationArgument & arg ) { dtag(HASH_TAG("AnnotationArgument")); + DAS_SER_PROFILE(*this, "AnnotationArgument"); arg.serialize(*this); return *this; } AstSerializer & AstSerializer::operator << ( AnnotationDeclarationPtr & annotation_decl ) { dtag(HASH_TAG("AnnotationDeclarationPtr")); + DAS_SER_PROFILE(*this, "AnnotationDecl"); if ( !writing ) annotation_decl = new AnnotationDeclaration(); annotation_decl->serialize(*this); return *this; @@ -677,7 +1033,8 @@ namespace das { ser.serialize_enum(op); anno->serialize(ser); } else { - ser << anno->module->nameHash; + Module * module = anno->module; + ser << module; } } else { // das-declared distinct-type entities round-trip with the module itself @@ -700,16 +1057,15 @@ namespace das { ser << inThisModule; if ( !inThisModule ) { string name; - ser << name; + ser.serializeTemp(name); if ( isLogicAnnotation(name) ) { LogicAnnotationOp op; ser.serialize_enum(op); anno = newLogicAnnotation(op); anno->serialize(ser); } else { - uint64_t moduleNameHash = 0; - ser << moduleNameHash; - auto mod = ser.moduleLibrary->findModuleByMangledNameHash(moduleNameHash); - SERIALIZER_VERIFYF(mod!=nullptr, "module '%llu' is not found", moduleNameHash); + Module * mod = nullptr; + ser << mod; + SERIALIZER_VERIFYF(mod!=nullptr, "module of annotation '%s' is not found", name.c_str()); anno = mod->findAnnotation(name); SERIALIZER_VERIFYF(anno!=nullptr, "annotation '%s' is not found", name.c_str()); } @@ -718,7 +1074,7 @@ namespace das { ser << isDistinct; if ( isDistinct ) { string name; - ser << name; + ser.serializeTemp(name); anno = ser.thisModule->findAnnotation(name); SERIALIZER_VERIFYF(anno!=nullptr && anno->rtti_isDistinctTypeAnnotation(), "distinct type '%s' is not found in module '%s'", name.c_str(), ser.thisModule->name.c_str()); @@ -729,6 +1085,7 @@ namespace das { AstSerializer & AstSerializer::operator << ( AnnotationPtr & anno ) { dtag(HASH_TAG("AnnotationPtr")); + DAS_SER_PROFILE(*this, "AnnotationPtr"); serializeAnnotationPointer(*this, anno); return *this; } @@ -738,75 +1095,112 @@ namespace das { return *this; } - AstSerializer & AstSerializer::operator << ( LineInfo & at ) { - dtag(HASH_TAG("LineInfo")); - *this << at.fileInfo; - serializeAdaptiveSize32(at.line); + enum LineShape : uint8_t { LineSameFile = 0x80, LineOneLine = 0x40, LineDeltaMask = 0x3f, LineDeltaBias = 31, LineDeltaEscape = 63 }; + // src/builtin/ARCHITECTURE.md sec.6 + AstSerializer & AstSerializer::operator << ( LineInfo & at ) { + dtag(HASH_TAG("LineInfo")); + DAS_SER_PROFILE(*this, "LineInfo"); if ( writing ) { - uint32_t diff = at.last_line - at.line; - serializeAdaptiveSize32(diff); + int32_t lineDelta = int32_t(at.line) - int32_t(lineBaseLine); + bool sameFile = at.fileInfo == lineBaseFile; + bool oneLine = at.last_line == at.line; + bool smallDelta = lineDelta >= -LineDeltaBias && lineDelta <= LineDeltaBias; + uint8_t shape = uint8_t(smallDelta ? lineDelta + LineDeltaBias : LineDeltaEscape); + if ( oneLine ) shape |= LineOneLine; + if ( sameFile ) shape |= LineSameFile; + write(&shape, 1); + if ( !sameFile ) *this << at.fileInfo; + uint8_t enc[20]; + size_t n = 0; + if ( !smallDelta ) n += encodeAdaptiveSize32(enc + n, AstSerializer::zigzag32(lineDelta)); + if ( !oneLine ) n += encodeAdaptiveSize32(enc + n, at.last_line - at.line); + n += encodeAdaptiveSize32(enc + n, at.column); + n += encodeAdaptiveSize32(enc + n, AstSerializer::zigzag32(int32_t(at.last_column) - int32_t(at.column))); + write(enc, n); } else { - uint32_t diff; serializeAdaptiveSize32(diff); - at.last_line = at.line + diff; + uint8_t shape = 0; + read(&shape, 1); + if ( shape & LineSameFile ) { + at.fileInfo = lineBaseFile; + } else { + *this << at.fileInfo; + } + int32_t lineDelta; + if ( (shape & LineDeltaMask) == LineDeltaEscape ) { + uint32_t z; serializeAdaptiveSize32(z); + lineDelta = AstSerializer::unzigzag32(z); + } else { + lineDelta = int32_t(shape & LineDeltaMask) - LineDeltaBias; + } + at.line = uint32_t(int32_t(lineBaseLine) + lineDelta); + if ( shape & LineOneLine ) { + at.last_line = at.line; + } else { + uint32_t diff; serializeAdaptiveSize32(diff); + at.last_line = at.line + diff; + } + serializeAdaptiveSize32(at.column); + uint32_t span; serializeAdaptiveSize32(span); + at.last_column = uint32_t(int32_t(at.column) + AstSerializer::unzigzag32(span)); } - - // columns must round-trip: local VISIBILITY ranges gate the GC walk, and inline - // splices distinguish locals on one line by column alone (the SpliceAtStamp - // ladder) - dropping columns here made a deserialized program collect unsoundly - serializeAdaptiveSize32(at.column); - serializeAdaptiveSize32(at.last_column); - + lineBaseFile = at.fileInfo; + lineBaseLine = at.line; return *this; } + // src/builtin/ARCHITECTURE.md sec.6 AstSerializer & AstSerializer::operator << ( FileInfo * & info ) { dtag(HASH_TAG("FileInfo *")); - bool is_null = info == nullptr; - *this << is_null; - if ( is_null ) { - if ( !writing ) { info = nullptr; } - return *this; - } + DAS_SER_PROFILE(*this, "FileInfo*"); if ( writing ) { - if ( writingFileInfoMap[info] == 0 ) { - uint64_t curOffset = buffer->writingSize() + sizeof(curOffset); - *this << curOffset; - writingFileInfoMap[info] = curOffset; + if ( !info ) { + uint32_t null = 0; + serializeAdaptiveSize32(null); + return *this; + } + if ( info == lastWriteFileInfo ) { + serializeAdaptiveSize32(lastWriteFileInfoNumber); + return *this; + } + auto & number = writeFileInfos[info]; + if ( number == 0 ) { + DAS_SER_PROFILE(*this, "FileInfo.payload"); + number = uint32_t(writeFileInfos.size()); + serializeAdaptiveSize32(number); info->serialize(*this); } else { - *this << writingFileInfoMap[info]; + serializeAdaptiveSize32(number); } + lastWriteFileInfo = info; + lastWriteFileInfoNumber = number; } else { - uint64_t curOffset = 0; *this << curOffset; - if ( readingFileInfoMap[curOffset] == nullptr ) { - // a genuine first occurrence reads its payload INLINE, so the recorded - // offset must equal the current position; a mismatch is a back-reference - // into a SKIPPED record - fail this record (recoverable: resume reparses - // it) instead of misparsing a FileInfo from the wrong bytes - SERIALIZER_VERIFYF(curOffset == uint64_t(buffer->bufferPos), - "file info back-reference into a skipped record"); - uint64_t savedOffset = readOffset; - readOffset = curOffset; - uint8_t tag = 0; *this << tag; - switch ( tag ) { - case 0: info = new FileInfo; break; - case 1: info = new TextFileInfo; break; - default: SERIALIZER_VERIFYF(false, "Unreachable"); - } - info->serialize(*this); - if ( fileAccess && !info->name.empty() ) { - if ( FileInfo * live = fileAccess->getFileInfo(info->name) ) { - info = live; - } + uint32_t number = 0; + serializeAdaptiveSize32(number); + if ( number == 0 ) { + info = nullptr; + return *this; + } + if ( number <= readFileInfos.size() ) { + info = readFileInfos[number - 1]; + return *this; + } + SERIALIZER_VERIFYF(number == readFileInfos.size() + 1, "corrupt stream: file info #%u past the %u numbered so far", + number, unsigned(readFileInfos.size())); + uint8_t tag = 0; *this << tag; + switch ( tag ) { + case 0: info = new FileInfo; break; + case 1: info = new TextFileInfo; break; + default: SERIALIZER_VERIFYF(false, "Unreachable"); + } + info->serialize(*this); + if ( fileAccess && !info->name.empty() ) { + if ( FileInfo * live = fileAccess->getFileInfo(info->name) ) { + info = live; } - readingFileInfoMap[curOffset] = info; - if ( curOffset != savedOffset ) - readOffset = savedOffset; - } else { - info = readingFileInfoMap[curOffset]; } + readFileInfos.push_back(info); } return *this; } @@ -831,6 +1225,7 @@ namespace das { } AstSerializer & AstSerializer::operator << ( StructurePtr & struct_ ) { + DAS_SER_PROFILE(*this, "StructurePtr"); auto id = getSerializeId(struct_); *this << id; if ( id.index == 0 ) { @@ -906,13 +1301,13 @@ namespace das { } AstSerializer & AstSerializer::operator << ( EnumerationPtr & enum_type ) { + DAS_SER_PROFILE(*this, "EnumerationPtr"); if ( writing ) { bool builtin = enum_type->module->builtIn && !enum_type->module->promoted; *this << builtin; if ( builtin ) { - uint64_t module = enum_type->module->nameHash; - string name = enum_type->name; - *this << module << name; + Module * module = enum_type->module; + *this << module << enum_type->name; } else { auto id = getSerializeId(enum_type); *this << id; @@ -925,13 +1320,13 @@ namespace das { bool builtin = false; *this << builtin; if ( builtin ) { - uint64_t module = 0; + Module * pModule = nullptr; string name; - *this << module << name; - auto pModule = this->moduleLibrary->findModuleByMangledNameHash(module); - SERIALIZER_VERIFYF(pModule, "expected to find module '%llu'", module); + *this << pModule; + serializeTemp(name); + SERIALIZER_VERIFYF(pModule, "expected to find the module of enumeration '%s'", name.c_str()); enum_type = pModule->findEnum(name); - SERIALIZER_VERIFYF(enum_type, "expected to find enumeration '%llu'::'%s'", module, name.c_str()); + SERIALIZER_VERIFYF(enum_type, "expected to find enumeration '%s'::'%s'", pModule->name.c_str(), name.c_str()); } else { SerializeNodeId id; *this << id; @@ -964,6 +1359,7 @@ namespace das { } AstSerializer & AstSerializer::operator << ( VariablePtr & var ) { + DAS_SER_PROFILE(*this, "VariablePtr"); auto id = getSerializeId(var); *this << id; if ( id.index == 0 ) { @@ -996,21 +1392,42 @@ namespace das { return m; } + // src/builtin/ARCHITECTURE.md sec.6 AstSerializer & AstSerializer::operator << ( Module * & module ) { - bool is_null = module == nullptr; - *this << is_null; + DAS_SER_PROFILE(*this, "Module*"); if ( writing ) { - if ( !is_null ) { + if ( !module ) { + uint32_t null = 0; + serializeAdaptiveSize32(null); + return *this; + } + auto & number = writeModules[module]; + if ( number == 0 ) { + number = uint32_t(writeModules.size()); + serializeAdaptiveSize32(number); *this << module->nameHash; + } else { + serializeAdaptiveSize32(number); } } else { - if ( !is_null ) { - uint64_t nameHash; *this << nameHash; - module = moduleLibrary->findModuleByMangledNameHash(nameHash); - SERIALIZER_VERIFYF(module, "expected to fetch module %llu from library", nameHash); - } else { + uint32_t number = 0; + serializeAdaptiveSize32(number); + if ( number == 0 ) { module = nullptr; + return *this; } + if ( number <= readModules.size() ) { + module = readModules[number - 1].first; + SERIALIZER_VERIFYF(module || ignoreEmptyExternal, "expected to fetch module %llu from library", + (unsigned long long) readModules[number - 1].second); + return *this; + } + SERIALIZER_VERIFYF(number == readModules.size() + 1, "corrupt stream: module #%u past the %u numbered so far", + number, unsigned(readModules.size())); + uint64_t nameHash = 0; *this << nameHash; + module = moduleLibrary->findModuleByMangledNameHash(nameHash); + SERIALIZER_VERIFYF(module || ignoreEmptyExternal, "expected to fetch module %llu from library", nameHash); + readModules.push_back({module, nameHash}); } return *this; } @@ -1022,17 +1439,19 @@ namespace das { AstSerializer & AstSerializer::operator << ( ReaderMacroPtr & ptr ) { dtag(HASH_TAG("ReaderMacroPtr")); + DAS_SER_PROFILE(*this, "ReaderMacroPtr"); if ( writing ) { - SERIALIZER_VERIFYF(ptr, "did not expext to see null ReaderMacroPtr"); + SERIALIZER_VERIFYF(ptr, "did not expect to see null ReaderMacroPtr"); SERIALIZER_VERIFYF(!(ptr->module == thisModule), "did not expect to find macro from the current module"); - *this << ptr->module->nameHash; + Module * module = ptr->module; + *this << module; *this << ptr->name; } else { - uint64_t moduleNameHash = 0; + Module * mod = nullptr; string name; - *this << moduleNameHash << name; - auto mod = moduleLibrary->findModuleByMangledNameHash(moduleNameHash); - SERIALIZER_VERIFYF(mod!=nullptr, "module '%llu' not found", moduleNameHash); + *this << mod; + serializeTemp(name); + SERIALIZER_VERIFYF(mod!=nullptr, "module of reader macro '%s' not found", name.c_str()); ptr = mod->findReaderMacro(name); SERIALIZER_VERIFYF(ptr, "Reader macro '%s' not found in the module '%s'", name.c_str(), mod->name.c_str() @@ -1043,6 +1462,7 @@ namespace das { AstSerializer & AstSerializer::operator << ( ExprBlock * & block ) { dtag(HASH_TAG("ExprBlock*")); + DAS_SER_PROFILE(*this, "ExprBlock*"); auto id = getSerializeId(block); *this << id; if ( !writing ) { @@ -1054,11 +1474,13 @@ namespace das { AstSerializer & AstSerializer::operator << ( InferHistory & history ) { dtag(HASH_TAG("InferHistory")); + DAS_SER_PROFILE(*this, "InferHistory"); history.serialize(*this); return *this; } AstSerializer & AstSerializer::operator << ( CaptureEntry & entry ) { + DAS_SER_PROFILE(*this, "CaptureEntry"); *this << entry.name; serialize_enum(entry.mode); return *this; @@ -1066,6 +1488,7 @@ namespace das { AstSerializer & AstSerializer::operator << ( MakeFieldDeclPtr & ptr ) { dtag(HASH_TAG("MakeFieldDeclPtr")); + DAS_SER_PROFILE(*this, "MakeFieldDecl"); bool is_null = ptr == nullptr; *this << is_null; if ( is_null ) { @@ -1084,6 +1507,7 @@ namespace das { AstSerializer & AstSerializer::operator << ( MakeStructPtr & ptr ) { dtag(HASH_TAG("MakeStructPtr")); + DAS_SER_PROFILE(*this, "MakeStruct"); bool is_null = ptr == nullptr; *this << is_null; if ( is_null ) { @@ -1136,6 +1560,14 @@ namespace das { void TypeDecl::serialize ( AstSerializer & ser ) { ser.dtag(HASH_TAG("TypeDecl")); + DAS_SER_PROFILE(ser, "TypeDecl"); +#if DAS_SERIALIZE_PROFILE + uint64_t profStart = ser.profPosition(); + struct ProfTypeEnd { + AstSerializer & ser; const TypeDecl * type; uint64_t start; + ~ProfTypeEnd () { if ( ser.writing ) ser.profType(type, ser.profPosition() - start); } + } profTypeEnd { ser, this, profStart }; +#endif ser << baseType; switch ( baseType ) { case Type::typeMacro: @@ -1308,7 +1740,13 @@ namespace das { // autoinfer firstType (FIXED_ARRAY_REWORK.md, 1b) ser << typeMacroExpr; - ser << flags << at << module; + if ( ser.writing ) { + uint32_t streamed = flags & ~aliasCacheFlagsMask(); + ser << streamed; + } else { + ser << flags; + } + ser << at << module; } void AnnotationArgument::serialize ( AstSerializer & ser ) { @@ -1323,7 +1761,12 @@ namespace das { void AnnotationDeclaration::serialize ( AstSerializer & ser ) { ser.dtag(HASH_TAG("AnnotationDeclaration")); - ser << annotation << arguments << at << flags; + ser << annotation; + { + DAS_SER_PROFILE(ser, "AnnotationDecl.arguments"); + ser << arguments; + } + ser << at << flags; } void ptr_ref_count::serialize ( AstSerializer & ser ) { @@ -1333,6 +1776,7 @@ namespace das { void Structure::FieldDeclaration::serialize ( AstSerializer & ser ) { ser.dtag(HASH_TAG("FieldDeclaration")); + DAS_SER_PROFILE(ser, "FieldDecl"); ser << name << at; ser << type; ser.ignoreEmptyExternal = true; @@ -1343,10 +1787,12 @@ namespace das { void Enumeration::EnumEntry::serialize( AstSerializer & ser ) { ser.dtag(HASH_TAG("EnumEntry")); + DAS_SER_PROFILE(ser, "EnumEntry"); ser << name << cppName << at << value; } void serializeAnnotationList ( AstSerializer & ser, AnnotationList & list ) { + DAS_SER_PROFILE(ser, "AnnotationList"); if ( ser.writing ) { uint64_t size = 0; // count the real size without generated annotations @@ -1370,6 +1816,7 @@ namespace das { } void Enumeration::serialize ( AstSerializer & ser ) { + DAS_SER_PROFILE(ser, "Enumeration"); ser.tag(HASH_TAG("Enumeration")); ser << name << cppName << at << list << module << external << baseType << isPrivate; @@ -1377,10 +1824,18 @@ namespace das { } void Structure::serialize ( AstSerializer & ser ) { + DAS_SER_PROFILE(ser, "Structure"); ser.tag(HASH_TAG("Structure")); ser << name; ser << at << module; - ser << fields << fieldLookup; + { + DAS_SER_PROFILE(ser, "Structure.fields"); + ser << fields; + } + { + DAS_SER_PROFILE(ser, "Structure.fieldLookup"); + ser << fieldLookup; + } ser << aliases; // A cross-module parent may live in another module whose content is NOT in this // stream; serializePointer binds it by name to the real library structure, avoiding @@ -1398,6 +1853,7 @@ namespace das { } void Variable::serialize ( AstSerializer & ser ) { + DAS_SER_PROFILE(ser, "Variable"); ser.tag(HASH_TAG("Variable")); ser << name << aka << type << init << source << at << stackTop << extraLocalOffset << module @@ -1405,6 +1861,7 @@ namespace das { } void Function::AliasInfo::serialize ( AstSerializer & ser ) { + DAS_SER_PROFILE(ser, "AliasInfo"); ser.tag(HASH_TAG("AliasInfo")); // var is often a global owned by ANOTHER module (deriveAliasing pulls // useGlobalVariables transitively across modules). The owning module's content is @@ -1443,15 +1900,26 @@ namespace das { } void Function::serialize ( AstSerializer & ser ) { + DAS_SER_PROFILE(ser, "Function"); ser.tag(HASH_TAG("Function")); ser << name; // Note: important fields are placed separately for easier debugging serializeAnnotationList(ser, annotations); - ser.ignoreEmptyExternal = true; - ser << arguments; - ser.ignoreEmptyExternal = false; - ser << result; - ser << body; + { + DAS_SER_PROFILE(ser, "Function.arguments"); + ser.ignoreEmptyExternal = true; + ser << arguments; + ser.ignoreEmptyExternal = false; + } + { + DAS_SER_PROFILE(ser, "Function.result"); + ser << result; + } + { + DAS_SER_PROFILE(ser, "Function.body"); + ser << body; + } + DAS_SER_PROFILE(ser, "Function.tail"); // cross-module by name, else inline smart-map — see Structure::serialize (parent) bool classParentCross = ser.writing && classParent && classParent->module && classParent->module != ser.thisModule; ser << classParentCross; @@ -1468,7 +1936,10 @@ namespace das { ser << totalStackSize << totalGenLabel; ser << at << atDecl << module; ser << hash << aotHash; // do not serialize inferStack - ser << resultAliases << argumentAliases << resultAliasesGlobals; + { + DAS_SER_PROFILE(ser, "Function.aliases"); + ser << resultAliases << argumentAliases << resultAliasesGlobals; + } ser << flags << moreFlags << moreFlags2 << sideEffectFlags; } @@ -1640,7 +2111,7 @@ namespace das { mangledName = expr->value->type->structType->getMangledName(); ser << expr->value->type->structType->module; } - ser << mangledName; + ser.serializeTemp(mangledName); if ( expr->annotation != nullptr && expr->annotation->getFieldOffset(expr->name) == static_cast(-1) ) { LOG(LogLevel::warning) << "das: serialize: Field '" << expr->name << "' not found in '" << expr->annotation->name << "'"; } @@ -1651,7 +2122,7 @@ namespace das { bool has_field = false; ser << has_field; if ( !has_field ) return; Module * module = nullptr; ser << module; - string mangledName; ser << mangledName; + string mangledName; ser.serializeTemp(mangledName); ser.fieldRefs.emplace_back(&expr->fieldRef, module, das::move(mangledName), expr->name); } } @@ -1773,7 +2244,8 @@ namespace das { ser << expr->variable; // serialize as smart pointer } else { ser << expr->variable->name; - ser << expr->variable->module->nameHash; + Module * module = expr->variable->module; + ser << module; } } else { @@ -1782,10 +2254,10 @@ namespace das { ser << expr->variable; } else { string varname; - uint64_t modname = 0; - ser << varname << modname; - auto mod = ser.moduleLibrary->findModuleByMangledNameHash(modname); - SERIALIZER_VERIFYF(mod, "expected to find module '%llu'", modname); + Module * mod = nullptr; + ser.serializeTemp(varname); + ser << mod; + SERIALIZER_VERIFYF(mod, "expected to find the module of variable '%s'", varname.c_str()); expr->variable = mod->findVariable(varname); } @@ -2046,26 +2518,50 @@ namespace das { ser << expr->typeexpr; } + // src/builtin/ARCHITECTURE.md sec.6 AstSerializer & AstSerializer::operator << ( ExpressionPtr & expr ) { dtag(HASH_TAG("ExpressionPtr")); - bool is_null = expr == nullptr; - *this << is_null; - if ( is_null ) { - if ( !writing ) expr = nullptr; - return *this; - } + DAS_SER_PROFILE(*this, "ExpressionPtr"); SerializeVisitor sv(*this); if ( writing ) { + if ( !expr ) { + uint32_t null = 0; + serializeAdaptiveSize32(null); + return *this; + } uint32_t rtti = hash_tag(expr->__rtti); DAS_ASSERT(rtti); - *this << rtti; + auto & number = writeExprClasses[rtti]; + if ( number == 0 ) { + number = uint32_t(writeExprClasses.size()); + serializeAdaptiveSize32(number); + serialize(rtti); + } else { + serializeAdaptiveSize32(number); + } + DAS_SER_PROFILE(*this, expr->__rtti); expr->dispatch(sv); } else { - uint32_t rtti = 0; *this << rtti; - auto itA = rttiHash2Annotation.find(rtti); - SERIALIZER_VERIFYF(itA != rttiHash2Annotation.end(), "annotation '%u' is not found", rtti); - auto annotation = itA->second; + uint32_t number = 0; + serializeAdaptiveSize32(number); + if ( number == 0 ) { + expr = nullptr; + return *this; + } + Annotation * annotation; + if ( number <= readExprClasses.size() ) { + annotation = readExprClasses[number - 1]; + } else { + SERIALIZER_VERIFYF(number == readExprClasses.size() + 1, "corrupt stream: expression class #%u past the %u numbered so far", + number, unsigned(readExprClasses.size())); + uint32_t rtti = 0; serialize(rtti); + auto itA = rttiHash2Annotation.find(rtti); + SERIALIZER_VERIFYF(itA != rttiHash2Annotation.end(), "annotation '%u' is not found", rtti); + annotation = itA->second; + readExprClasses.push_back(annotation); + } expr = (Expression *) static_cast(annotation)->factory(); + DAS_SER_PROFILE(*this, annotation->name.c_str()); expr->dispatch(sv); } dtag(HASH_TAG("/ExpressionPtr")); @@ -2113,17 +2609,19 @@ namespace das { AstSerializer & AstSerializer::operator << ( CallMacro * & ptr ) { dtag(HASH_TAG("CallMacro *")); + DAS_SER_PROFILE(*this, "CallMacro*"); if ( writing ) { SERIALIZER_VERIFYF ( ptr, "did not expect to see a nullptr CallMacro *" ); SERIALIZER_VERIFYF ( !(ptr->module == thisModule), "did not expect to find macro from the current module" ); - *this << ptr->module->nameHash; + Module * module = ptr->module; + *this << module; *this << ptr->name; } else { - uint64_t moduleName = 0; + Module * mod = nullptr; string name; - *this << moduleName << name; - auto mod = moduleLibrary->findModuleByMangledNameHash(moduleName); - SERIALIZER_VERIFYF(mod!=nullptr, "module '%llu' not found", moduleName); + *this << mod; + serializeTemp(name); + SERIALIZER_VERIFYF(mod!=nullptr, "module of call macro '%s' not found", name.c_str()); // perform a litte dance to access the internal macro; // for details see: src/builtin/module_builtin_ast_adapters.cpp // 1564: void addModuleCallMacro ( .... CallMacroPtr & .... ) @@ -2211,9 +2709,10 @@ namespace das { } void serializeUseFunctions ( AstSerializer & ser, const FunctionPtr & f ) { + DAS_SER_PROFILE(ser, "UseFunctions"); ser.tag(HASH_TAG("serializeUseFunctions")); if ( ser.writing ) { - string fname = f->name; ser << fname; + ser << f->name; uint64_t sz = f->useFunctions.size(); ser << sz; for ( auto & usedFun : f->useFunctions ) { @@ -2221,7 +2720,7 @@ namespace das { bool builtin = usedFun->module->builtIn || usedFun->module != f->module; ser << builtin; if ( builtin ) { - uint64_t module = usedFun->module->nameHash; + Module * module = usedFun->module; uint64_t mnh = usedFun->getMangledNameHash(); ser << module << mnh; } else { @@ -2233,7 +2732,7 @@ namespace das { } } } else { - string fname; ser << fname; + string fname; ser.serializeTemp(fname); SERIALIZER_VERIFYF(fname == f->name, "expected to serialize in the same order: %s != %s", fname.c_str(), f->name.c_str()); uint64_t size = 0; ser << size; f->useFunctions.reserve(size); @@ -2241,12 +2740,11 @@ namespace das { bool builtin = false; ser << builtin; if ( builtin ) { - uint64_t module = 0; + Module * pModule = nullptr; uint64_t mnh = 0; - ser << module << mnh; - auto pModule = ser.moduleLibrary->findModuleByMangledNameHash(module); - SERIALIZER_VERIFYF(pModule, "expected to find module '%llu' (useFunctions[%llu/%llu] of function '%s')", - (unsigned long long) module, (unsigned long long) i, (unsigned long long) size, f->name.c_str()); + ser << pModule << mnh; + SERIALIZER_VERIFYF(pModule, "expected to find the module (useFunctions[%llu/%llu] of function '%s')", + (unsigned long long) i, (unsigned long long) size, f->name.c_str()); auto fun = pModule->findFunctionByMangledNameHash(mnh); SERIALIZER_VERIFYF(fun, "expected to find function (mnh %llu in module '%s', useFunctions[%llu/%llu] of function '%s')", (unsigned long long) mnh, pModule->name.c_str(), (unsigned long long) i, (unsigned long long) size, f->name.c_str()); @@ -2263,9 +2761,10 @@ namespace das { } void serializeUseFunctions ( AstSerializer & ser, const VariablePtr & f ) { + DAS_SER_PROFILE(ser, "UseFunctions"); ser.tag(HASH_TAG("serializeUseFunctions")); if ( ser.writing ) { - string name = f->name; ser << name; + ser << f->name; uint64_t sz = f->useFunctions.size(); ser << sz; for ( auto & usedFun : f->useFunctions ) { @@ -2273,7 +2772,7 @@ namespace das { bool builtin = usedFun->module->builtIn || usedFun->module != f->module; ser << builtin; if ( builtin ) { - uint64_t module = usedFun->module->nameHash; + Module * module = usedFun->module; uint64_t mnh = usedFun->getMangledNameHash(); ser << module << mnh; } else { @@ -2285,7 +2784,7 @@ namespace das { } } } else { - string name; ser << name; + string name; ser.serializeTemp(name); SERIALIZER_VERIFYF(name == f->name, "expected to serialize in the same order: %s != %s", name.c_str(), f->name.c_str()); uint64_t size = 0; ser << size; f->useFunctions.reserve(size); @@ -2293,12 +2792,11 @@ namespace das { bool builtin = false; ser << builtin; if ( builtin ) { - uint64_t module = 0; + Module * pModule = nullptr; uint64_t mnh = 0; - ser << module << mnh; - auto pModule = ser.moduleLibrary->findModuleByMangledNameHash(module); - SERIALIZER_VERIFYF(pModule, "expected to find module '%llu' (useFunctions[%llu/%llu] of global '%s')", - (unsigned long long) module, (unsigned long long) i, (unsigned long long) size, f->name.c_str()); + ser << pModule << mnh; + SERIALIZER_VERIFYF(pModule, "expected to find the module (useFunctions[%llu/%llu] of global '%s')", + (unsigned long long) i, (unsigned long long) size, f->name.c_str()); auto fun = pModule->findFunctionByMangledNameHash(mnh); SERIALIZER_VERIFYF(fun, "expected to find function (mnh %llu in module '%s', useFunctions[%llu/%llu] of global '%s')", (unsigned long long) mnh, pModule->name.c_str(), (unsigned long long) i, (unsigned long long) size, f->name.c_str()); @@ -2315,9 +2813,10 @@ namespace das { } void serializeUseVariables ( AstSerializer & ser, const FunctionPtr & f ) { + DAS_SER_PROFILE(ser, "UseVariables"); ser.tag(HASH_TAG("serializeUseVariables")); if ( ser.writing ) { - string name = f->name; ser << name; + ser << f->name; uint64_t sz = f->useGlobalVariables.size(); ser << sz; for ( auto & use : f->useGlobalVariables ) { @@ -2328,9 +2827,8 @@ namespace das { bool builtin = use->module->builtIn || use->module != f->module; ser << builtin; if ( builtin ) { - uint64_t module = use->module->nameHash; - string varname = use->name; - ser << module << varname; + Module * module = use->module; + ser << module << use->name; } else { auto vid = ser.getSerializeId(use); if ( !ser.isWritten(vid) ) @@ -2340,7 +2838,7 @@ namespace das { } } } else { - string name; ser << name; + string name; ser.serializeTemp(name); SERIALIZER_VERIFYF(name == f->name, "expected to serialize in the same order: %s %s", name.c_str(), f->name.c_str()); uint64_t size = 0; ser << size; f->useGlobalVariables.reserve(size); @@ -2348,12 +2846,12 @@ namespace das { bool builtin = false; ser << builtin; if ( builtin ) { - uint64_t module = 0; + Module * pModule = nullptr; string varname; - ser << module << varname; - auto pModule = ser.moduleLibrary->findModuleByMangledNameHash(module); - SERIALIZER_VERIFYF(pModule, "expected to find module '%llu' (useGlobalVariables[%llu/%llu] of function '%s')", - (unsigned long long) module, (unsigned long long) i, (unsigned long long) size, f->name.c_str()); + ser << pModule; + ser.serializeTemp(varname); + SERIALIZER_VERIFYF(pModule, "expected to find the module (useGlobalVariables[%llu/%llu] of function '%s')", + (unsigned long long) i, (unsigned long long) size, f->name.c_str()); auto var = pModule->findVariable(varname); SERIALIZER_VERIFYF(var, "expected to find variable '%s::%s' (useGlobalVariables[%llu/%llu] of function '%s')", pModule->name.c_str(), varname.c_str(), (unsigned long long) i, (unsigned long long) size, f->name.c_str()); @@ -2370,9 +2868,10 @@ namespace das { } void serializeUseVariables ( AstSerializer & ser, const VariablePtr & f ) { + DAS_SER_PROFILE(ser, "UseVariables"); ser.tag(HASH_TAG("serializeUseVariables")); if ( ser.writing ) { - string name = f->name; ser << name; + ser << f->name; uint64_t sz = f->useGlobalVariables.size(); ser << sz; for ( auto & use : f->useGlobalVariables ) { @@ -2380,9 +2879,8 @@ namespace das { bool builtin = use->module->builtIn || use->module != f->module; ser << builtin; if ( builtin ) { - uint64_t module = use->module->nameHash; - string varname = use->name; - ser << module << varname; + Module * module = use->module; + ser << module << use->name; } else { auto vid = ser.getSerializeId(use); if ( !ser.isWritten(vid) ) @@ -2392,7 +2890,7 @@ namespace das { } } } else { - string name; ser << name; + string name; ser.serializeTemp(name); SERIALIZER_VERIFYF(name == f->name, "expected to serialize in the same order: %s != %s", name.c_str(), f->name.c_str()); uint64_t size = 0; ser << size; f->useGlobalVariables.reserve(size); @@ -2400,12 +2898,12 @@ namespace das { bool builtin = false; ser << builtin; if ( builtin ) { - uint64_t module = 0; + Module * pModule = nullptr; string varname; - ser << module << varname; - auto pModule = ser.moduleLibrary->findModuleByMangledNameHash(module); - SERIALIZER_VERIFYF(pModule, "expected to find module '%llu' (useGlobalVariables[%llu/%llu] of global '%s')", - (unsigned long long) module, (unsigned long long) i, (unsigned long long) size, f->name.c_str()); + ser << pModule; + ser.serializeTemp(varname); + SERIALIZER_VERIFYF(pModule, "expected to find the module (useGlobalVariables[%llu/%llu] of global '%s')", + (unsigned long long) i, (unsigned long long) size, f->name.c_str()); auto var = pModule->findVariable(varname); SERIALIZER_VERIFYF(var, "expected to find variable '%s::%s' (useGlobalVariables[%llu/%llu] of global '%s')", pModule->name.c_str(), varname.c_str(), (unsigned long long) i, (unsigned long long) size, f->name.c_str()); @@ -2460,12 +2958,16 @@ namespace das { uint64_t size = functions.unlocked_size(); ser << size; functions.foreach ( [&] ( FunctionPtr g ) { string name = g->getMangledName(); - ser << name << g; + { + DAS_SER_PROFILE(ser, "Functions.mangledName"); + ser.serializeTemp(name); + } + ser << g; }); } else { uint64_t size = 0; ser << size; for ( uint64_t i = 0; i < size; i++ ) { - string name; ser << name; + string name; ser.serializeTemp(name); FunctionPtr g = nullptr; ser << g; SERIALIZER_VERIFYF(g!=nullptr, "expected to find function"); functions.insert(name, g); @@ -2489,6 +2991,7 @@ namespace das { } void serializeFunctionsByName ( AstSerializer & ser, fragile_hash> & functionsByName ) { + DAS_SER_PROFILE(ser, "FunctionsByName"); if ( ser.writing ) { uint32_t capacity = functionsByName.capacity(); uint32_t size = functionsByName.size(); @@ -2517,6 +3020,7 @@ namespace das { } void Module::serialize ( AstSerializer & ser, bool already_exists ) { + DAS_SER_PROFILE(ser, "Module"); ser.tag(HASH_TAG("Module")); // builtIn / promoted say whether this module is linked into daScriptEnvironment::modules, // which is a fact about the running process, not about the stream. Restoring them hands @@ -2557,14 +3061,23 @@ namespace das { LineInfo dat; bool dpriv = false; TypeDeclPtr dunder = nullptr; - ser << dname << dcppName << dat << dpriv << dunder; + ser.serializeTemp(dname); + ser.serializeTemp(dcppName); + ser << dat << dpriv << dunder; auto dann = new DistinctTypeAnnotation(dname, dunder, dcppName); dann->at = dat; dann->isPrivate = dpriv; addAnnotation(dann, true); } } - ser << aliasTypes << enumerations; + { + DAS_SER_PROFILE(ser, "Module.aliasTypes"); + ser << aliasTypes; + } + { + DAS_SER_PROFILE(ser, "Module.enumerations"); + ser << enumerations; + } /* // serialize handleTypes (annotation lookup table) if ( ser.writing ) { @@ -2589,23 +3102,48 @@ namespace das { } } */ - ser << keywords; - ser << typeFunctions; - serializeGlobals(ser, globals); // globals require insertion in the same order - serializeStructures(ser, structures); - serializeFunctions(ser, functions); + { + DAS_SER_PROFILE(ser, "Module.keywords"); + ser << keywords; + } + { + DAS_SER_PROFILE(ser, "Module.typeFunctions"); + ser << typeFunctions; + } + { + DAS_SER_PROFILE(ser, "Module.globals"); + serializeGlobals(ser, globals); // globals require insertion in the same order + } + { + DAS_SER_PROFILE(ser, "Module.structures"); + serializeStructures(ser, structures); + } + { + DAS_SER_PROFILE(ser, "Module.functions"); + serializeFunctions(ser, functions); + } if ( ser.failed ) return; - serializeFunctions(ser, generics); + { + DAS_SER_PROFILE(ser, "Module.generics"); + serializeFunctions(ser, generics); + } if ( ser.failed ) return; - serializeFunctionsByName(ser, functionsByName); - serializeFunctionsByName(ser, genericsByName); + { + DAS_SER_PROFILE(ser, "Module.functionsByName"); + serializeFunctionsByName(ser, functionsByName); + } + { + DAS_SER_PROFILE(ser, "Module.genericsByName"); + serializeFunctionsByName(ser, genericsByName); + } ser << ownFileInfo; //<< promotedAccess; + DAS_SER_PROFILE(ser, "Module.useSets"); functions.foreach ([&] ( FunctionPtr f ) { if ( ser.writing ) { ser << f->name; } else { - string fname; ser << fname; + string fname; ser.serializeTemp(fname); SERIALIZER_VERIFYF(fname == f->name, "expected to walk in the same order: %s != %s", fname.c_str(), f->name.c_str()); } serializeUseVariables(ser, f); @@ -2616,7 +3154,7 @@ namespace das { if ( ser.writing ) { ser << f->name; } else { - string fname; ser << fname; + string fname; ser.serializeTemp(fname); SERIALIZER_VERIFYF(fname == f->name, "expected to walk in the same order: %s != %s", fname.c_str(), f->name.c_str()); } serializeUseVariables(ser, f); @@ -2640,7 +3178,9 @@ namespace das { // Now we need to restore the internal state in case this has been a macro module + auto finalize0 = ref_time_ticks(); finalizeModule(ser, *ser.moduleLibrary, this, already_exists); + if ( !ser.writing ) ser.totFinalizeTime += get_time_usec(finalize0); } class TopSort { @@ -2731,7 +3271,7 @@ namespace das { } AstSerializer & AstSerializer::operator << ( CodeOfPolicies & value ) { - #define DAS_POLICY_FIELD_STREAM(f) *this << value.f; + #define DAS_POLICY_FIELD_STREAM(f) serializeTemp(value.f); DAS_MODULE_CACHE_POLICY_FIELDS(DAS_POLICY_FIELD_STREAM) #undef DAS_POLICY_FIELD_STREAM return *this; @@ -2757,6 +3297,7 @@ namespace das { // embedders are built without exception handling, so nothing may escape this // rail: contain dasException here (truncated/corrupt cache streams throw from // the stream readers) and report failure through the failed/failToCompile flags + // src/builtin/ARCHITECTURE.md sec.6 void AstSerializer::serializeProgram ( ProgramPtr program, ModuleGroup & libGroup ) noexcept { try { serializeProgramImpl(program, libGroup); @@ -2783,6 +3324,7 @@ namespace das { // already and patch() cleared the refs - re-clearing is free. fieldRefs.clear(); clearNodeIds(); + if ( writing ) buffer->flush(); } void AstSerializer::serializeProgramImpl ( ProgramPtr program, ModuleGroup & libGroup ) { @@ -2802,6 +3344,10 @@ namespace das { ser.builtinHashDrift = false; // per-record flavor bit, read by the resume path ser.clearNodeIds(); // numbering restarts with every program, on both sides + DAS_SER_PROFILE(ser, "Program"); +#if DAS_SERIALIZE_PROFILE + ser.profBegin("Program.header"); +#endif ser << program->thisNamespace << program->thisModuleName; ser << program->totalFunctions << program->totalVariables << program->newLambdaIndex; @@ -2817,11 +3363,17 @@ namespace das { if ( !cachedPoliciesMatch(stored, program->policies) ) { ser.policyMismatch = true; ser.failed = true; +#if DAS_SERIALIZE_PROFILE + ser.profEnd(); +#endif return; } ser.readJitEnabled = program->policies.jit_enabled; ser.readOptions = program->options; } +#if DAS_SERIALIZE_PROFILE + ser.profEnd(); +#endif if ( writing ) { moduleLibrary = &program->library; // Module::serialize binds *moduleLibrary (finalizeModule) @@ -2861,6 +3413,7 @@ namespace das { *this << thisAt; for ( auto & m : modules ) { + DAS_SER_PROFILE(ser, "Program.moduleRef"); bool builtin = m->builtIn, promoted = m->promoted; *this << builtin << promoted; *this << m->name; @@ -2898,9 +3451,10 @@ namespace das { moduleLibrary = &program->library; for ( uint64_t i = 0; i < size; i++ ) { + DAS_SER_PROFILE(ser, "Program.moduleRef"); bool builtin = false, promoted = false; ser << builtin << promoted; - string name; ser << name; + string name; ser.serializeTemp(name); if ( builtin && !promoted ) { auto m = requireBuiltinModule(name); @@ -3088,6 +3642,7 @@ namespace das { } ser << allRequireDecl; + ser.buffer->flush(); return; } @@ -3108,7 +3663,7 @@ namespace das { uint64_t size_builtin = 0; ser << size_builtin; for ( uint64_t i = 0; i < size_builtin; i++ ) { - string name; ser << name; + string name; ser.serializeTemp(name); Module * m = requireBuiltinModule(name); library.addModule(m); } @@ -3117,7 +3672,10 @@ namespace das { for ( uint64_t i = 0; i < size; i++ ) { bool builtin = false, promoted = false; string name, fileName, promotedRequire; - ser << builtin << promoted << name << fileName << promotedRequire; + ser << builtin << promoted; + ser.serializeTemp(name); + ser.serializeTemp(fileName); + ser.serializeTemp(promotedRequire); if ( builtin && !promoted ) { // pass } else if ( builtin && promoted ) { @@ -3180,10 +3738,12 @@ namespace das { ser << allRequireDecl; // for the last module, mark symbols manually + auto setup0 = ref_time_ticks(); markExecutableSymbolUse(); removeUnusedSymbols(); TextWriter logs; allocateStack(logs,true,false); + ser.totSetupTime += get_time_usec(setup0); } #if !DAS_NO_FILEIO @@ -3332,6 +3892,7 @@ namespace das { AstSerializer ser(&storage, true); CodeOfPolicies streamed = policies; ser << streamed; + storage.flush(); string jitTarget = commandLineArgumentOccurrences("--jit-target"); // NUL-separated; the key hashes as a C string for ( auto & ch : jitTarget ) { if ( ch == 0 ) ch = '\n'; @@ -3396,6 +3957,9 @@ namespace das { env.serializer_read = nullptr; env.serializer_write = nullptr; if ( reader ) { +#if DAS_SERIALIZE_PROFILE + if ( !reader->quietCache ) { LOG tw(LogLevel::info); reader->profReport(tw); } +#endif if ( !reader->checkedStreamHeader ) { res.verdict = ReadVerdict::none; // nothing was ever read (rail disabled mid-compile) } else if ( reader->failed || reader->seenNewModule ) { @@ -3417,8 +3981,12 @@ namespace das { reader.reset(); } if ( writer ) { +#if DAS_SERIALIZE_PROFILE + if ( !writer->quietCache ) { LOG tw(LogLevel::info); writer->profReport(tw); } +#endif writer->moduleLibrary = nullptr; writer.reset(); // releases the parsedModules program refs before the program runs + writeStorage.flush(); if ( !writeStorage.buffer.empty() ) { // write-to-temp + rename: a concurrent reader on the same path sees a // complete old or a complete new stream, never a torn one. The CRT rename @@ -3534,10 +4102,19 @@ namespace das { rtti_ast_serializer_deserialize_program_ex(state, nullptr, block, context, at); } + int64_t rtti_ast_serializer_finalize_usec ( AstSerializerState * state ) { + return state ? int64_t(state->serializer->totFinalizeTime) : 0; + } + + int64_t rtti_ast_serializer_setup_usec ( AstSerializerState * state ) { + return state ? int64_t(state->serializer->totSetupTime) : 0; + } + void rtti_ast_serializer_get_data ( AstSerializerState * state, const TBlock const>> & block, Context * context, LineInfoArg * at ) { + state->storage->flush(); Array arr; array_mark_locked(arr, state->storage->buffer.data(), uint64_t(state->storage->buffer.size()), uint64_t(state->storage->buffer.size())); diff --git a/src/builtin/module_builtin_rtti.cpp b/src/builtin/module_builtin_rtti.cpp index c0f402a367..de1a655593 100644 --- a/src/builtin/module_builtin_rtti.cpp +++ b/src/builtin/module_builtin_rtti.cpp @@ -2046,6 +2046,12 @@ namespace das { addExtern(*this, lib, "ast_serializer_get_data", SideEffects::modifyExternal, "rtti_ast_serializer_get_data") ->args({"serializer","block","context","line"}); + addExtern(*this, lib, "ast_serializer_finalize_usec", + SideEffects::none, "rtti_ast_serializer_finalize_usec") + ->args({"serializer"}); + addExtern(*this, lib, "ast_serializer_setup_usec", + SideEffects::none, "rtti_ast_serializer_setup_usec") + ->args({"serializer"}); addExtern(*this, lib, "set_file_source", SideEffects::modifyExternal, "introduceFile") ->args({"access","fileName","text","context","line"}); diff --git a/src/misc/REVIEW.md b/src/misc/REVIEW.md index ceaf3e5b8f..fc8e9e9d61 100644 --- a/src/misc/REVIEW.md +++ b/src/misc/REVIEW.md @@ -18,9 +18,15 @@ switches either to `size` is a defect - the constant length is what the compiler `DAS_F2S_BUFFER_SIZE` (`include/daScript/misc/float2string.h`), which sizes every caller's buffer.** -**Never call `isfinite`, `isnan`, or `signbit` in `luau_float2string.cpp` - classify special -values from the IEEE bits instead.** A build with `-ffinite-math-only` folds those calls to -constants. +**Never classify a float in `luau_float2string.cpp` through libc - `isfinite`, `isnan`, +`isinf`, `fpclassify`, `signbit` - read the IEEE bits instead: the exponent bits for NaN and +infinity, the sign bit for the sign.** A build with `-ffinite-math-only` folds a NaN or infinity +test to a constant. + +**A diff in `daScriptC.cpp` that hands a writing `SerializationStorageVector`'s `buffer` out to +a caller calls `flush()` on the storage first.** The writer grows the vector by doubling and +counts the written bytes in `writePos`, so before a flush the vector is longer than the stream +and the caller gets trailing garbage. **Weakening `REVIEW.das` (beside this file) is a defect: dropping a check, narrowing the files or lines a check scans, or rewriting a finding text so it no longer names what failed.** diff --git a/src/misc/daScriptC.cpp b/src/misc/daScriptC.cpp index 2e83f59844..80c2f33811 100644 --- a/src/misc/daScriptC.cpp +++ b/src/misc/daScriptC.cpp @@ -1045,6 +1045,7 @@ das_serialized_data * das_program_serialize_n ( das_program * program, const voi ((Program *)program)->serialize(ser); ser.moduleLibrary = nullptr; } + storage->flush(); *out_data = storage->buffer.data(); *out_size = storage->buffer.size(); return (das_serialized_data *) storage; diff --git a/tests-cpp/small/test_env_serializer.cpp b/tests-cpp/small/test_env_serializer.cpp index 3b76f192b0..ef467f7c5d 100644 --- a/tests-cpp/small/test_env_serializer.cpp +++ b/tests-cpp/small/test_env_serializer.cpp @@ -46,6 +46,42 @@ void warmUp ( const char * relPath ) { REQUIRE_FALSE(program->failed()); } +// every expression's position and its type's position, in visit order - what the packed +// LineInfo, the type table's per-reference `at` and the deep copy must all preserve +struct PositionCensus : Visitor { + vector positions; + void record ( const LineInfo & at ) { + positions.push_back(at.line); + positions.push_back(at.last_line); + positions.push_back(at.column); + positions.push_back(at.last_column); + positions.push_back(uint64_t(hash_blockz64((const uint8_t *) (at.fileInfo ? at.fileInfo->name.c_str() : "")))); + } + void preVisitExpression ( Expression * expr ) override { + record(expr->at); + if ( expr->type ) record(expr->type->at); + } + void preVisit ( Function * fn ) override { + record(fn->at); + for ( auto & arg : fn->arguments ) { + record(arg->at); + if ( arg->type ) record(arg->type->at); + } + } +}; + +// the program's own module and the modules the record carries for it (the fixture's +// required module among them); the builtins are not in the stream +vector positionCensus ( const ProgramPtr & program ) { + PositionCensus census; + for ( auto m : program->library.getModules() ) { + if ( m->builtIn && !m->promoted ) continue; + if ( m->name != program->thisModule->name && m->name != "test_env_serializer_module" ) continue; + m->functions.foreach([&](FunctionPtr fn) { fn->visit(census); }); + } + return census.positions; +} + } // namespace TEST_CASE("env serializer: module cache round trip") { @@ -55,6 +91,7 @@ TEST_CASE("env serializer: module cache round trip") { REQUIRE(env.serializer_write == nullptr); warmUp("/tests-cpp/small/test_env_serializer.das"); SerializationStorageVector storage; + vector coldPositions; // cold pass — modules written into the cache { TextWriter logs; @@ -66,8 +103,10 @@ TEST_CASE("env serializer: module cache round trip") { writer.moduleLibrary = nullptr; REQUIRE(program != nullptr); REQUIRE_FALSE(program->failed()); + coldPositions = positionCensus(program); } CHECK(storage.buffer.size() > 0); + CHECK(storage.buffer.size() == storage.writingSize()); // flushed: the vector is exactly the stream, not its growth // warm pass — modules read back instead of parsed { TextWriter logs; @@ -80,6 +119,8 @@ TEST_CASE("env serializer: module cache round trip") { REQUIRE_FALSE(program->failed()); CHECK_FALSE(reader.failed); // no fallback to reparse CHECK(logs.str().find("ser:") == string::npos); // no cache-miss diagnostics + CHECK(coldPositions.size() > 8); + CHECK(positionCensus(program) == coldPositions); // every line, column and file survives the record reader.moduleLibrary = nullptr; // the cache-fed program must actually run Context ctx(program->getContextStackSize()); diff --git a/tests/REVIEW.md b/tests/REVIEW.md index ff968301a8..6ba2f6e893 100644 --- a/tests/REVIEW.md +++ b/tests/REVIEW.md @@ -2,8 +2,8 @@ **Read `REVIEW_COMMON.md` (repo root) first - its contract binds this checklist.** -**A test answers to its kind's checklist wherever the diff puts it:** a module-scan, -descriptor-manifest, module-cache or deferred-load test to `module_cache/REVIEW.md`; a test that +**A test answers to its kind's checklist wherever the diff puts it:** a test whose subject is +module resolution, module loading or the module cache to `module_cache/REVIEW.md`; a test that runs a dasMetal kernel class or creates any Metal object to `metal/REVIEW.md`; an MSL emitter fixture or census test to `msl/REVIEW.md`; a SPIR-V emitter `*_words` fixture or census test to `spirv/REVIEW.md`; a test that calls `tick_debug_agent` to `debug_agent/REVIEW.md`. diff --git a/tests/module_cache/REVIEW.md b/tests/module_cache/REVIEW.md index 844bf94fbf..8dc8fadd11 100644 --- a/tests/module_cache/REVIEW.md +++ b/tests/module_cache/REVIEW.md @@ -3,18 +3,20 @@ **Read `REVIEW_COMMON.md` (repo root) first - its contract binds this checklist.** Architecture doc: `ARCHITECTURE.md`. -- **A diff that adds or changes an assertion literal in this folder's tests makes it match only - text the spawned child emits, in its output or in a file it wrote.** A child's output is the - only instrument a human has for what the cache and the module scan served. +- **A diff that adds or changes an assertion literal in this folder's tests - a marker or a + field a test compares against, not the message an assertion reports - makes it match only text + the spawned child emits, in its output or in a file it wrote.** A child's output is the only + instrument a human has for what the cache and the module scan served. -- **A diff that adds or changes a helper that produces the text a test in this folder compares - against a spawned child's output keeps every field of the child's line but its elapsed - times.** A dropped field is a change the test can no longer see. +- **A diff that adds or changes a helper that decides whether a child's line is the one a test in + this folder wants makes that decision compare every field of the line except its elapsed + times.** A field the helper does not compare is a change the test can no longer see. - **A test in this folder writes only under a directory it created for this process - its own files and its children's - and removes that directory.** -- **Silencing LINT030 for a test in this folder - a `nolint:LINT030` marker or a `.lint_config` - exclusion - is a defect.** The order a call's arguments are evaluated in is not defined, so a - child run nested beside the `out` it writes reads that output before the child wrote it on - some tiers and after on others; the rule is what keeps every run a statement of its own. +- **Silencing LINT030 for a test in this folder - a `nolint:LINT030` marker, or an entry in the + repo-root `.lint_config` that turns LINT030 off or excludes this folder's paths - is a defect; + give the child run a statement of its own and read `out` in the next one.** The order a call's + arguments are evaluated in is not defined, so a child run nested beside the `out` it writes + reads that output before the child wrote it on some tiers and after on others. diff --git a/tests/module_cache/test_default_cache_path.das b/tests/module_cache/test_default_cache_path.das index b98384e92f..cd225c3f90 100644 --- a/tests/module_cache/test_default_cache_path.das +++ b/tests/module_cache/test_default_cache_path.das @@ -434,13 +434,14 @@ def test_default_cache_evicts_to_the_limit(t : T?) { let enter = win ? "cd /d {tmp} && set \"DAS_MODULE_CACHE_LIMIT=1\"&&" : "cd {tmp} && DAS_MODULE_CACHE_LIMIT=1" let base = "{enter} {get_full_file_name(das_exe())} -dasroot {get_das_root()}" let cacheDir = path_join(tmp, ".jitted_scripts/module_cache") + let sizeBallast = "require daslib/strings_boost\nrequire daslib/fio\nrequire daslib/ast\n" for (stem in ["ev_a", "ev_b", "ev_c", "ev_d"]) { - fwrite(path_join(tmp, "{stem}.das"), "options gen2\n[export]\ndef main \{\n print(\"MARK_{stem}\\n\")\n\}\n") + fwrite(path_join(tmp, "{stem}.das"), "options gen2\n{sizeBallast}[export]\ndef main \{\n print(\"MARK_{stem}\\n\")\n\}\n") } var out : string t |> success(run_reported(t, "evict a", "{base} {path_join(tmp, "ev_a.das")}", out, "MARK_ev_a"), "the first root writes its record") let one = record_size_in(cacheDir, "ev_a") - t |> success(one > 0ul && 2ul * one <= 1048576ul && 3ul * one > 1048576ul, "a hello record ({one} bytes) sizes so that two fit the 1 MB limit and three do not - retune the limit if daslib grew") + t |> success(one > 0ul && 2ul * one <= 1048576ul && 3ul * one > 1048576ul, "a record ({one} bytes) sizes so that two fit the 1 MB limit and three do not - retune the fixture's ballast requires if daslib or the stream format moved") sleep(1100u) //! past the mtime's one-second grain, so the records order by age t |> success(run_reported(t, "evict b", "{base} {path_join(tmp, "ev_b.das")}", out, "MARK_ev_b"), "the second root writes its record") sleep(1100u) diff --git a/tests/module_cache/test_rtti_serializer.das b/tests/module_cache/test_rtti_serializer.das new file mode 100644 index 0000000000..4e3e76eba6 --- /dev/null +++ b/tests/module_cache/test_rtti_serializer.das @@ -0,0 +1,53 @@ +options gen2 +options indenting = 4 +options no_aot = true + +require dastest/testing_boost public + +require daslib/rtti +require daslib/ast +require daslib/fio +require _mc_common + +// the rtti serializer API round-trips a whole program in process, and the reader's phase timers +// account for the read: finalize and setup are the read's parts, not more than the read +[test] +def test_rtti_serializer_round_trip(t : T?) { + var inscope access <- make_file_access("") + using() $(var mg : ModuleGroup) { + using() $(var cop : CodeOfPolicies) { + cop.ignore_shared_modules = true + cop.version_2_syntax = true + compile_file(path_join(get_das_root(), "examples/hello_world.das"), access, unsafe(addr(mg)), cop) $(ok, program, issues) { + if (!ok) { + t |> failure("the fixture does not compile: {issues}") + return + } + var bytes : array + var ser = create_ast_serializer() + t |> success(serialize_program(ser, program), "the program serializes") + ast_serializer_get_data(ser) $(data) { + bytes := data + } + delete_ast_serializer(ser) + t |> success(length(bytes) > 1000, "the stream carries the program's modules ({length(bytes)} bytes)") + var restored = false + let t0 = ref_time_ticks() + var deser = create_ast_deserializer(bytes) + deserialize_program(deser) $(dok, _prog, err) { + restored = dok + if (!dok) { + t |> failure("deserialize_program: {err}") + } + } + let readUsec = int64(get_time_usec(t0)) + let finalizeUsec = ast_serializer_finalize_usec(deser) + let setupUsec = ast_serializer_setup_usec(deser) + delete_ast_serializer(deser) + t |> success(restored, "the stream deserializes") + t |> success(finalizeUsec >= 0l && setupUsec >= 0l, "the phase timers are non-negative ({finalizeUsec}, {setupUsec})") + t |> success(finalizeUsec + setupUsec <= readUsec, "finalize {finalizeUsec} + setup {setupUsec} fit inside the read {readUsec} us") + } + } + } +}