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") + } + } + } +}