Repository navigation
feat(native): object-level change version (object-local, minimal bookkeeping) - #51
Merged
Merged
Conversation
…checks * feat(native): monotonic per-object change stamp with eager upward propagation Every NativeObject now owns a monotonically increasing version_ taken from a global saturated counter. Any write (Set*, Del/Clear, SetNil, SetId/GroupId, finalizer/method registration, MarkModified, container mutation, child link) bumps it and propagates the same stamp to all ancestors, so a parent's version is always >= any descendant's. Reads never touch it. Design choice: eager propagation of a global timestamp + per-object max, instead of lazy recursive comparison (which is not O(1)) or a parent-side sum of child versions (not monotonic under child removal/sharing). Cost: query is O(1), write is O(number of ancestors); ~8B version, 8B propagation epoch per object plus lazily allocated parent/child edge sets. Track child edges (children ref counts + parents set) so detaching, re-parenting and object destruction can bump former ancestors and avoid dangling pointers. Destroy() detaches first, then frees Impl; version_ lives outside Impl and keeps increasing after Destroy. Public API (include/fakelua.h): uint64_t GetVersion() const; // current subtree stamp bool IsChangedSince(uint64_t v) const; // O(1) dirty test void MarkModified(); // external state linked to an object static void SetVersionDebugEnabled(bool); // FNV-1a content hash self-check static bool SelfCheck(const NativeObject*, bool recursive = true); Callers remember the last seen version per object and compare later; no payload or serialization change, versions are never persisted. * fix(native): guard propagation walk against roots without parent sets Only push parents that actually own a parent edge table; a root reached during the upward walk previously dereferenced a null unique_ptr in the lazy edge bookkeeping. * test(native): cover writes, reads, nesting, saturation, cycles and self-check Add 8 GTest cases plus Lua fixtures covering every write entry point, read-only access paths (direct, snapshot, pairs, containers), nested propagation and sibling isolation, child create/destroy/clear, monotonicity and uint64 saturation, debug hash detection of a deliberately unmarked raw write, cycle guarding, and Lua/container paths. * perf(native): add NativeObject micro-benchmarks for cross-revision comparison SetInt/GetInt/SetString/CreateDestroy/SetObject/DeepLeafWrite use only APIs that exist both before and after this change so numbers stay comparable.
…pagation * refactor(native): remove upward version propagation TouchVersion now only raises the object's own stamp (O(1), independent of tree depth); LinkChild/UnlinkChild no longer push stamps to ancestors and the propagation epoch bookkeeping is gone. Direct child attach/detach still bumps the owning object because that is its own structural change, and child deletion bumps only the direct parent (DetachAll). Semantics is now object-local: a write inside a child never surfaces on its parent or ancestors. Callers that need subtree granularity traverse the graph themselves and aggregate the per-object versions they read (traversal is pointer-only and cheap; field serialization is the expensive part). * test(native): assert child writes do not bump ancestors and SelfCheck is cycle-safe Replace the propagation test with nested_writes_do_not_bump_ancestors, add destroy_bumps_direct_parent_only, and add self_check_is_cycle_safe (A<->B) proving the visited-set walk terminates on cyclic graphs and still detects a write that bypassed the object API. Object graphs may contain cycles: LinkChild does not reject them, so SelfCheck must stay cycle-safe. * docs(native): document object-local scope and cycle safety on the public API
* refactor(native): keep only a forward + friend declaration for test access NativeObjectVersionTestAccess was fully declared in include/fakelua.h, the installed public header. Move its declarations to the internal src/native/object/native_object_test_access.h (included only by the unit tests and native_object.cpp) and leave a single forward declaration plus the friend line in the public header, so installed headers expose no test-only surface. Public API is unchanged: GetVersion/IsChangedSince/MarkModified and the debug SetVersionDebugEnabled/SelfCheck stay in fakelua.h because embedders need them for incremental sync and for diagnosing missed markings in their own builds; they cost nothing while debug is off. * test(native): include the internal test-access header
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #51 +/- ##
==========================================
+ Coverage 89.75% 89.77% +0.01%
==========================================
Files 125 125
Lines 27231 27317 +86
==========================================
+ Hits 24441 24523 +82
- Misses 2790 2794 +4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…surface * test(native): remove the white-box accessor header and rewrite tests Delete src/native/object/native_object_test_access.h together with the forward and friend declarations that made it reachable from the installed header include/fakelua.h. The public header now carries no test-only traces. Tests now use public API only: - drop the SetFromCVar bump case (same path is reached by Lua-side writes); - drop direct GetAsCVar reading (covered by the Lua read-only case); - drop uint64 saturation (unreachable via public API, would need 2^64 writes); - drop the "bypass the API, then detect with SelfCheck" negative case, since without a backdoor a missed marking cannot be simulated; - keep write paths, read-only paths, nesting isolation, direct child create/destroy, destroy-marks-only-direct-parent and cycle-safe SelfCheck.
Follow-up to the previous commit: remove the (now unread) header file itself so the tree contains no test-only sources.
…ompile time * refactor(native): keep only version_ plus the per-write hook A change version is a "may have changed" signal, not a reference ledger. Delete the children refcount map and the parents set: SetObject/Del/SetNil/ Clear/SetFromCVar now only bump the owning object, and Destroy no longer notifies any parent. This removes two per-object unique_ptr fields and one heap allocation per link. Contract (documented in the public header): detach an object (Del/SetNil/ Clear or overwrite) from every parent field before Destroy. Group destruction is unaffected since all objects in the group die together. * perf(native): compile-time gate for the content-hash self-check New CMake option FAKELUA_NATIVE_OBJECT_DEBUG_HASH (default OFF) enables debug_hash/debug_hash_epoch and their epoch maintenance. Default builds carry no debug fields: SetVersionDebugEnabled is a no-op and SelfCheck returns true. SelfCheck no longer walks children because no child links are tracked. * test(native): pin the detach-first contract and shared-child behaviour Replace destroy-bump expectations, add same_child_in_multiple_fields and destroy_requires_detach_first, and keep the cycle-safe SelfCheck smoke test.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
背景
上层(增量存档、C++ 镜像增量同步、UI 缓存失效)需要低成本判断"某 NativeObject 自上次记录以来有没有变过"。
本 PR 为每个 NativeObject 增加单调递增的变更版本号。语义是"对象自身"粒度:只反映这个对象自己的写入,不包含它的子树。
语义与理由
每个对象只维护自己的单调计数;任何写入本对象都会让它自增,不做任何向父对象/祖先的传播,也不维护父子边索引。
SetObject/Del/SetNil/Clear等处一律 bump 自己即可;多 bump 一次无害。其他性质:全局饱和自增时间戳,单调不回绕;只与同一对象上次记录的值比较;不写入存档载荷、不改变既有数据格式;
Destroy()后版本号保留最后一次值。对象图与销毁约定
核实结论(用代码与用例证明):
SetObject("a", c)与SetObject("b", c)同时指向c;解绑其中一处后另一处仍有效(用例same_child_in_multiple_fields)。p1->SetObject("c", leaf)与p2->SetObject("c", leaf)(用例nested_writes_do_not_bump_ancestors)。因此本 PR 明确约定并在公开头注明:
Del/SetNil/Clear或覆盖字段)。移除动作本身会 bump 父对象,版本信号不丢。DestroyGroup/Clear整组销毁不受此限:组内对象同时销毁,不会在存活对象里留下悬空引用。destroy_requires_detach_first钉住该约定。字段收敛(本 PR 最后一轮)
uint64_t version_(写就自增,读不变)、TouchVersion()统一写入钩子。children(子对象引用计数 map)、parents(父对象集合)、LinkChild/UnlinkChild/DetachAll及全部调用点。删除后每个对象不再有额外的unique_ptr字段,每条父子关系也不再分配 unordered_map/unordered_set。debug_hash/debug_hash_epoch改为编译期开关FAKELUA_NATIVE_OBJECT_DEBUG_HASH(CMake option,默认 OFF):默认构建不含这两个字段、不维护 epoch;SetVersionDebugEnabled为空操作,SelfCheck恒返回 true。调试构建(-DFAKELUA_NATIVE_OBJECT_DEBUG_HASH=ON)才有内容哈希自检。SelfCheck不再做子树递归(已无子边索引),recursive参数为兼容保留并忽略。内存变化(Release,实测)
sizeof(NativeObject)impl_8 +version_8,未变)Impl每对象固定字段嵌套场景收益主要来自每条父子关系省掉的 map/set 堆分配。
基准(Release,5 次中位数)
读路径零回退;写路径比基线多约 0.6–0.8 ns(版本号自增 + 统一钩子),深树写与浅层一致。
对外 API(include/fakelua.h)
调用方配合:保存"对象指针 + 上次看到的版本号",
obj->IsChangedSince(saved)为 true 即重新采集/同步,更新后存回GetVersion()。写入点清单(逐一覆盖)
src/native/object/native_object.cppSetInt/SetFloat/SetBool/SetString/SetObject/SetNil/Del/ClearSetFromCVar、NativeSpecSet(快照未建立 / 增量 / 置 nil 三个分支)SetId/SetGroupId/SetFinalizerRegisterMethod/UnregisterMethodMarkModified(容器/外部状态)TouchVersion(唯一钩子;只提升本对象自身)src/native/container/native_container.cpp(内容存在对象之外)ClearOf、Deque/Map/Set/Vector/SmallVector/List 的 Push/Pop/Set/Erase(共 22 处MarkModified())测试(9 个
test_native_version)all_write_paths_bump、read_paths_do_not_bump、nested_writes_do_not_bump_ancestors(含多父共享)、child_create_and_clear_bump_parent、destroy_requires_detach_first、same_child_in_multiple_fields、monotonic_versions、self_check_is_cycle_safe(A↔B 环图不崩不挂起)、lua_paths_and_container(Lua 侧读写与容器)。已知风险 / 未覆盖路径
Kind::Table内部变更不 bump 所属对象;外部资源状态(io/socket/DB)需MarkModified();绕过对象 API 的直接内存写无法感知。SelfCheck恒 true;白盒访问器已删除,"人为制造漏标 → 检出"的负向用例不存在,实际检出能力缺少直接测试,仅靠写入点枚举 + 统一TouchVersion()钩子降低风险。仓库状态说明
远端
master曾被 force-update(本地旧63f823f与远端a6fe459tree 相同,仅历史重写),无法 fast-forward。按约定未对master做 reset/rebase/force-push,而是从origin/master新建feat/native-object-version分支提交;本地master保持原样,变更前快照留存于/Users/mingming/git-backup-2026-11-06/。