From e12e8039483051706320145601bb114d3d8c3da4 Mon Sep 17 00:00:00 2001 From: John Uhlmann Date: Wed, 25 Feb 2026 15:48:34 +0800 Subject: [PATCH 1/3] Optimize parser property lookup with persistent name-to-index map Replace the per-event deque/linear-scan property cache with a two-level scheme: a persistent name-to-index map in schema_locator (built once per event type, shared across all events) and a flat vector in parser indexed by property position. This eliminates per-event hash table allocation and removes string comparisons during the blob walk. Co-Authored-By: Claude Opus 4.6 --- krabs/krabs/parser.hpp | 84 +++++++++++++++++++++------------- krabs/krabs/schema.hpp | 4 ++ krabs/krabs/schema_locator.hpp | 50 +++++++++++++++++++- 3 files changed, 103 insertions(+), 35 deletions(-) diff --git a/krabs/krabs/parser.hpp b/krabs/krabs/parser.hpp index d346486..6ea7864 100644 --- a/krabs/krabs/parser.hpp +++ b/krabs/krabs/parser.hpp @@ -4,9 +4,9 @@ #pragma once #include -#include -#include #include +#include +#include #include @@ -92,16 +92,17 @@ namespace krabs { private: property_info find_property(const std::wstring &name); - void cache_property(const wchar_t *name, property_info info); + void cache_property(ULONG index, property_info info); private: const schema &schema_; const BYTE *pEndBuffer_; BYTE *pBufferIndex_; ULONG lastPropertyIndex_; - - // Maintain a mapping from property name to blob data index. - std::deque> propertyCache_; + // Persistent name to index map shared across all events of the same type. + const property_name_map *pPropertyNames_; + // Maintain a mapping from property index to blob data location. + std::vector propertyCache_; }; // Implementation @@ -112,6 +113,8 @@ namespace krabs { , pEndBuffer_((BYTE*)s.record_.UserData + s.record_.UserDataLength) , pBufferIndex_((BYTE*)s.record_.UserData) , lastPropertyIndex_(0) + , pPropertyNames_(s.pPropertyNames_) + , propertyCache_(s.pSchema_->PropertyCount) {} inline property_iterator parser::properties() const @@ -129,15 +132,40 @@ namespace krabs { // the contents within it. This is janky, so our strategy is to // minimize this as much as possible via caching. - // The first step is to use our cache for the property to see if we've - // discovered it already. - for (auto &item : propertyCache_) { - if (name == item.first) { - return item.second; + const ULONG totalPropCount = schema_.pSchema_->PropertyCount; + + // Resolve property name to index. + ULONG index = totalPropCount; // sentinel = not found + if (pPropertyNames_) { + // Fast path: use the persistent name to index map shared across + // all events of the same type. + auto it = pPropertyNames_->find(std::wstring_view(name)); + if (it != pPropertyNames_->end()) { + index = it->second; + } + } else { + // Fallback: linear scan of property names in the schema. + for (ULONG i = 0; i < totalPropCount; ++i) { + auto &propInfo = schema_.pSchema_->EventPropertyInfoArray[i]; + const wchar_t *pName = reinterpret_cast( + reinterpret_cast(schema_.pSchema_) + + propInfo.NameOffset); + if (name == pName) { + index = i; + break; + } } } - const ULONG totalPropCount = schema_.pSchema_->PropertyCount; + if (index >= totalPropCount) { + return property_info(); + } + + // The first step is to use our cache for the property to see if we've + // discovered it already. + if (index < lastPropertyIndex_) { + return propertyCache_[index]; + } assert((pBufferIndex_ <= pEndBuffer_ && pBufferIndex_ >= schema_.record_.UserData) && "invariant: we should've already thrown for falling off the edge"); @@ -153,16 +181,13 @@ namespace krabs { // to find it. While we're going through the blob to find it, we'll // remember what we've seen to save time later. // - // Question: Why don't we just populate the cache before looking up any - // properties and simplify our code (less state, etc)? - // - // Answer: Doing that introduces overhead in the case that only a - // subset of properties are needed. While this code is a bit - // more complicated, we introduce no additional performance - // overhead at runtime. - for (auto &i = lastPropertyIndex_; i < totalPropCount; ++i) { + // Note: The name-to-index map is built once per schema type (cheap + // metadata scan). But the blob walk below is lazy per-event -- we + // only walk forward to the requested index, avoiding overhead when + // only a subset of properties are needed. + while (lastPropertyIndex_ <= index) { - auto ¤tPropInfo = schema_.pSchema_->EventPropertyInfoArray[i]; + auto ¤tPropInfo = schema_.pSchema_->EventPropertyInfoArray[lastPropertyIndex_]; const wchar_t *pName = reinterpret_cast( reinterpret_cast(schema_.pSchema_) + currentPropInfo.NameOffset); @@ -179,26 +204,19 @@ namespace krabs { } property_info propInfo(pBufferIndex_, currentPropInfo, propertyLength); - cache_property(pName, propInfo); + cache_property(lastPropertyIndex_, propInfo); // advance the buffer index since we've already processed this property pBufferIndex_ += propertyLength; - - // The property was found, return it - if (name == pName) { - // advance the index since we've already processed this property - ++i; - return propInfo; - } + lastPropertyIndex_++; } - // property wasn't found, return an empty propInfo - return property_info(); + return propertyCache_[index]; } - inline void parser::cache_property(const wchar_t *name, property_info propInfo) + inline void parser::cache_property(ULONG index, property_info info) { - propertyCache_.push_front(std::make_pair(name, propInfo)); + propertyCache_[index] = info; } inline void throw_if_property_not_found(const property_info &propInfo) diff --git a/krabs/krabs/schema.hpp b/krabs/krabs/schema.hpp index 9016f7b..5212e8c 100644 --- a/krabs/krabs/schema.hpp +++ b/krabs/krabs/schema.hpp @@ -308,6 +308,8 @@ namespace krabs { private: const EVENT_RECORD &record_; const TRACE_EVENT_INFO *pSchema_; + // Persistent name to index map, owned by schema_locator. May be nullptr. + const property_name_map *pPropertyNames_; private: friend std::wstring event_name(const schema &); @@ -337,11 +339,13 @@ namespace krabs { inline schema::schema(const EVENT_RECORD &record, const krabs::schema_locator &schema_locator) : record_(record) , pSchema_(schema_locator.get_event_schema(record)) + , pPropertyNames_(schema_locator.get_property_names(pSchema_)) { } inline schema::schema(const EVENT_RECORD &record, const PTRACE_EVENT_INFO pSchema) : record_(record) , pSchema_(pSchema) + , pPropertyNames_(nullptr) { } inline bool schema::operator==(const schema &other) const diff --git a/krabs/krabs/schema_locator.hpp b/krabs/krabs/schema_locator.hpp index 2bc43ea..db00d56 100644 --- a/krabs/krabs/schema_locator.hpp +++ b/krabs/krabs/schema_locator.hpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -172,6 +173,14 @@ namespace krabs { */ std::string_view get_trace_logger_event_name(const EVENT_RECORD &); + /** + * + * Maps property names to their index in the schema. + * Keys are wstring_views pointing into stable TRACE_EVENT_INFO memory. + * + */ + using property_name_map = std::unordered_map; + /** * * Fetches and caches schemas from TDH. @@ -206,8 +215,21 @@ namespace krabs { */ bool has_event_schema(const EVENT_RECORD& record) const; + /** + * + * Returns the persistent property name to index map for a schema. + * The map is built when the schema is first cached. + * Returns nullptr if pSchema is null or not in the cache. + * + */ + const property_name_map* get_property_names(const TRACE_EVENT_INFO* pSchema) const; + private: + void build_property_names(const TRACE_EVENT_INFO* pSchema) const; + mutable std::unordered_map, TDHSTATUS>> cache_; + // Persistent property name to index maps, keyed by schema pointer. + mutable std::unordered_map property_name_cache_; }; // Implementation @@ -310,9 +332,10 @@ namespace krabs { // Add the new instance to the cache. // NB: key's 'internalize_name' gets called by the cctor here. - if (status == ERROR_SUCCESS) + if (status == ERROR_SUCCESS) { cache_.emplace(key, std::move(buffer)); - else + build_property_names(returnVal); + } else cache_.emplace(key, status); return returnVal; @@ -325,6 +348,29 @@ namespace krabs { return status == ERROR_SUCCESS; } + inline void schema_locator::build_property_names(const TRACE_EVENT_INFO* pSchema) const + { + property_name_map names; + for (ULONG i = 0; i < pSchema->PropertyCount; ++i) { + const wchar_t* pName = reinterpret_cast( + reinterpret_cast(pSchema) + + pSchema->EventPropertyInfoArray[i].NameOffset); + names.emplace(std::wstring_view(pName), i); + } + property_name_cache_.emplace(pSchema, std::move(names)); + } + + inline const property_name_map* schema_locator::get_property_names(const TRACE_EVENT_INFO* pSchema) const + { + if (!pSchema) return nullptr; + + auto it = property_name_cache_.find(pSchema); + if (it != property_name_cache_.end()) { + return &it->second; + } + return nullptr; + } + inline std::unique_ptr get_event_schema_from_tdh(const EVENT_RECORD &record) { TDHSTATUS status = ERROR_SUCCESS; From 14ac4631b8efa438539ee3cad06fa13ffac23505 Mon Sep 17 00:00:00 2001 From: John Uhlmann Date: Wed, 25 Feb 2026 15:58:32 +0800 Subject: [PATCH 2/3] Use std::wstring_view for parser property name parameters Replace const std::wstring& with std::wstring_view in parser's public API (parse, try_parse, view_of) and internal helpers (find_property, assert_valid_assignment, throw_if_invalid). This eliminates heap-allocating std::wstring temporaries when callers pass string literals (the common case). Property names longer than MSVC's SSO threshold of 7 wchar_t previously caused a heap allocation and free per field access. Not a breaking change - std::wstring_view is implicitly constructible from both std::wstring and const wchar_t*. Co-Authored-By: Claude Opus 4.6 --- krabs/krabs/parser.hpp | 36 ++++++++++++++++++------------------ krabs/krabs/tdh_helpers.hpp | 17 +++++++++-------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/krabs/krabs/parser.hpp b/krabs/krabs/parser.hpp index 6ea7864..2736857 100644 --- a/krabs/krabs/parser.hpp +++ b/krabs/krabs/parser.hpp @@ -76,7 +76,7 @@ namespace krabs { * */ template - bool try_parse(const std::wstring &name, T &out); + bool try_parse(std::wstring_view name, T &out); /** * @@ -85,13 +85,13 @@ namespace krabs { * */ template - T parse(const std::wstring &name); + T parse(std::wstring_view name); template - auto view_of(const std::wstring &name, Adapter &adapter) -> collection_view; + auto view_of(std::wstring_view name, Adapter &adapter) -> collection_view; private: - property_info find_property(const std::wstring &name); + property_info find_property(std::wstring_view name); void cache_property(ULONG index, property_info info); private: @@ -122,7 +122,7 @@ namespace krabs { return property_iterator(schema_); } - inline property_info parser::find_property(const std::wstring &name) + inline property_info parser::find_property(std::wstring_view name) { // A schema contains a collection of properties that are keyed by name. // These properties are stored in a blob of bytes that needs to be @@ -139,7 +139,7 @@ namespace krabs { if (pPropertyNames_) { // Fast path: use the persistent name to index map shared across // all events of the same type. - auto it = pPropertyNames_->find(std::wstring_view(name)); + auto it = pPropertyNames_->find(name); if (it != pPropertyNames_->end()) { index = it->second; } @@ -245,7 +245,7 @@ namespace krabs { // ------------------------------------------------------------------------ template - bool parser::try_parse(const std::wstring &name, T &out) + bool parser::try_parse(std::wstring_view name, T &out) { try { out = parse(name); @@ -270,7 +270,7 @@ namespace krabs { // ------------------------------------------------------------------------ template - T parser::parse(const std::wstring &name) + T parser::parse(std::wstring_view name) { auto propInfo = find_property(name); throw_if_property_not_found(propInfo); @@ -286,7 +286,7 @@ namespace krabs { } template<> - inline bool parser::parse(const std::wstring& name) + inline bool parser::parse(std::wstring_view name) { auto propInfo = find_property(name); throw_if_property_not_found(propInfo); @@ -298,7 +298,7 @@ namespace krabs { } template <> - inline std::wstring parser::parse(const std::wstring &name) + inline std::wstring parser::parse(std::wstring_view name) { auto propInfo = find_property(name); throw_if_property_not_found(propInfo); @@ -312,7 +312,7 @@ namespace krabs { } template <> - inline std::string parser::parse(const std::wstring &name) + inline std::string parser::parse(std::wstring_view name) { auto propInfo = find_property(name); throw_if_property_not_found(propInfo); @@ -326,7 +326,7 @@ namespace krabs { } template<> - inline const counted_string* parser::parse(const std::wstring &name) + inline const counted_string* parser::parse(std::wstring_view name) { auto propInfo = find_property(name); throw_if_property_not_found(propInfo); @@ -337,7 +337,7 @@ namespace krabs { } template<> - inline binary parser::parse(const std::wstring &name) + inline binary parser::parse(std::wstring_view name) { auto propInfo = find_property(name); throw_if_property_not_found(propInfo); @@ -349,7 +349,7 @@ namespace krabs { template<> inline ip_address parser::parse( - const std::wstring &name) + std::wstring_view name) { auto propInfo = find_property(name); throw_if_property_not_found(propInfo); @@ -372,7 +372,7 @@ namespace krabs { template<> inline socket_address parser::parse( - const std::wstring &name) + std::wstring_view name) { auto propInfo = find_property(name); throw_if_property_not_found(propInfo); @@ -384,7 +384,7 @@ namespace krabs { template<> inline sid parser::parse( - const std::wstring& name) + std::wstring_view name) { auto propInfo = find_property(name); throw_if_property_not_found(propInfo); @@ -422,7 +422,7 @@ namespace krabs { } template<> - inline pointer parser::parse(const std::wstring& name) + inline pointer parser::parse(std::wstring_view name) { auto propInfo = find_property(name); throw_if_property_not_found(propInfo); @@ -436,7 +436,7 @@ namespace krabs { // ------------------------------------------------------------------------ template - auto parser::view_of(const std::wstring &name, Adapter &adapter) + auto parser::view_of(std::wstring_view name, Adapter &adapter) -> collection_view { auto propInfo = find_property(name); diff --git a/krabs/krabs/tdh_helpers.hpp b/krabs/krabs/tdh_helpers.hpp index 2c5a80e..2e633f1 100644 --- a/krabs/krabs/tdh_helpers.hpp +++ b/krabs/krabs/tdh_helpers.hpp @@ -7,6 +7,7 @@ #include #include +#include #include #include "compiler_check.hpp" @@ -74,7 +75,7 @@ namespace krabs { // type that does not have any assignment validation. This compiles // to a no-op in release. template - inline void assert_valid_assignment(const std::wstring&, const property_info&) + inline void assert_valid_assignment(std::wstring_view, const property_info&) { #ifndef NDEBUG @@ -93,7 +94,7 @@ namespace krabs { // will fall back to the unspecialized version which is a no-op in release. inline void throw_if_invalid( - const std::wstring& name, + std::wstring_view name, const property_info& info, _TDH_IN_TYPE requested) { @@ -120,7 +121,7 @@ namespace krabs { #define BUILD_ASSERT(type, tdh_type) \ template <> \ inline void assert_valid_assignment( \ - const std::wstring& name, const property_info& info) \ + std::wstring_view name, const property_info& info) \ { \ throw_if_invalid(name, info, tdh_type); \ } @@ -159,7 +160,7 @@ namespace krabs { template <> inline void assert_valid_assignment( - const std::wstring&, const property_info& info) + std::wstring_view, const property_info& info) { auto outType = info.pEventPropertyInfo_->nonStructType.OutType; @@ -172,7 +173,7 @@ namespace krabs { template <> inline void assert_valid_assignment( - const std::wstring&, const property_info& info) + std::wstring_view, const property_info& info) { auto outType = info.pEventPropertyInfo_->nonStructType.OutType; @@ -184,7 +185,7 @@ namespace krabs { template <> inline void assert_valid_assignment( - const std::wstring&, const property_info& info) + std::wstring_view, const property_info& info) { auto inType = info.pEventPropertyInfo_->nonStructType.InType; @@ -196,7 +197,7 @@ namespace krabs { template <> inline void assert_valid_assignment( - const std::wstring&, const property_info& info) + std::wstring_view, const property_info& info) { auto inType = info.pEventPropertyInfo_->nonStructType.InType; @@ -208,7 +209,7 @@ namespace krabs { template <> inline void assert_valid_assignment( - const std::wstring&, const property_info& info) + std::wstring_view, const property_info& info) { auto inType = info.pEventPropertyInfo_->nonStructType.InType; From 3a81f57ddb9310b13a8f5f8f925ae22f64c1f2a5 Mon Sep 17 00:00:00 2001 From: John Uhlmann Date: Wed, 25 Feb 2026 16:12:36 +0800 Subject: [PATCH 3/3] Bump NuGet version to 4.4.8 --- O365.Security.Native.ETW.Debug.nuspec | 7 ++++--- O365.Security.Native.ETW.nuspec | 7 ++++--- krabsetw.nuspec | 7 ++++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/O365.Security.Native.ETW.Debug.nuspec b/O365.Security.Native.ETW.Debug.nuspec index c8c67de..4961ca7 100644 --- a/O365.Security.Native.ETW.Debug.nuspec +++ b/O365.Security.Native.ETW.Debug.nuspec @@ -2,7 +2,7 @@ Microsoft.O365.Security.Native.ETW.Debug - 4.4.7 + 4.4.8 Microsoft.O365.Security.Native.ETW Debug - managed wrappers for krabsetw Microsoft Microsoft @@ -12,8 +12,9 @@ Microsoft.O365.Security.Native.ETW Debug is a managed wrapper around the krabsetw ETW library. This is the Debug build. Microsoft.O365.Security.Native.ETW Debug is a managed wrapper around the krabsetw ETW library. This is the Debug build. - Version 4.4.7: - - Add process_start_key() to read ProcessStartKey from extended data + Version 4.4.8: + - Optimize parser property lookup with persistent name-to-index map + - Use std::wstring_view for parser property name parameters © Microsoft Corporation. All rights reserved. diff --git a/O365.Security.Native.ETW.nuspec b/O365.Security.Native.ETW.nuspec index 52abb38..7d0945a 100644 --- a/O365.Security.Native.ETW.nuspec +++ b/O365.Security.Native.ETW.nuspec @@ -2,7 +2,7 @@ Microsoft.O365.Security.Native.ETW - 4.4.7 + 4.4.8 Microsoft.O365.Security.Native.ETW - managed wrappers for krabsetw Microsoft Microsoft @@ -12,8 +12,9 @@ Microsoft.O365.Security.Native.ETW is a managed wrapper around the krabsetw ETW library. Microsoft.O365.Security.Native.ETW is a managed wrapper around the krabsetw ETW library. - Version 4.4.7: - - Add process_start_key() to read ProcessStartKey from extended data + Version 4.4.8: + - Optimize parser property lookup with persistent name-to-index map + - Use std::wstring_view for parser property name parameters © Microsoft Corporation. All rights reserved. diff --git a/krabsetw.nuspec b/krabsetw.nuspec index 086f45c..f1ce3b5 100644 --- a/krabsetw.nuspec +++ b/krabsetw.nuspec @@ -2,7 +2,7 @@ Microsoft.O365.Security.Krabsetw - 4.4.7 + 4.4.8 Krabs ETW Wrappers Microsoft Microsoft @@ -12,8 +12,9 @@ Krabs ETW provides a modern C++ wrapper around the low-level ETW trace consumption functions Krabs ETW provides a modern C++ wrapper around the low-level ETW trace consumption functions - Version 4.4.7: - - Add process_start_key() to read ProcessStartKey from extended data + Version 4.4.8: + - Optimize parser property lookup with persistent name-to-index map + - Use std::wstring_view for parser property name parameters © Microsoft Corporation. All rights reserved.