diff --git a/Jint.Benchmark/InteropWrapperChurnBenchmark.cs b/Jint.Benchmark/InteropWrapperChurnBenchmark.cs index e475a0be7..13273afca 100644 --- a/Jint.Benchmark/InteropWrapperChurnBenchmark.cs +++ b/Jint.Benchmark/InteropWrapperChurnBenchmark.cs @@ -4,9 +4,10 @@ namespace Jint.Benchmark; /// /// Measures ObjectWrapper churn: a CLR member returning the SAME object reference repeatedly. -/// With the default TrackObjectWrapperIdentity=false every crossing allocates a fresh -/// ObjectWrapper; with the opt-in identity flag the ConditionalWeakTable returns the cached one. -/// The two benchmarks bracket the design space for cheaper wrapper reuse. +/// Since 4.14 the default engine keeps a small bounded cache of recently wrapped objects +/// (CacheRecentObjectWrappers), so repeated crossings reuse the wrapper; the NoCache rows opt out +/// to measure the fresh-wrapper-per-crossing path, and the identity-flag rows measure the +/// ConditionalWeakTable variant. Together they bracket the design space for wrapper reuse. /// [MemoryDiagnoser] public class InteropWrapperChurnBenchmark @@ -31,6 +32,7 @@ public sealed class Payload } private Engine _engineDefault = null!; + private Engine _engineNoCache = null!; private Engine _engineIdentity = null!; private Engine _engineRecentCache = null!; private Engine _engineCopy = null!; @@ -40,18 +42,26 @@ public void GlobalSetup() { var holder = new Holder(new Payload { Value = 42 }); - // the out-of-the-box engine: ArrayConversion defaults to LiveView since 4.14 + // the out-of-the-box engine: since 4.14 ArrayConversion defaults to LiveView and + // CacheRecentObjectWrappers defaults to true (repeated crossings reuse the wrapper) _engineDefault = new Engine(); _engineDefault.SetValue("h", holder); _engineDefault.Execute("h.Payload.Value"); + // opt out of the recent-wrapper cache: every crossing builds a fresh wrapper + _engineNoCache = new Engine(cfg => cfg.Interop.CacheRecentObjectWrappers = false); + _engineNoCache.SetValue("h", holder); + _engineNoCache.Execute("h.Payload.Value"); + // the identity flags are most interesting on top of Copy, where they let the otherwise // per-read deep-copied JsArray snapshot be reused across crossings (pin Copy so the array - // traversal rows below measure exactly that; the object-churn rows are array-mode agnostic) + // traversal rows below measure exactly that; the object-churn rows are array-mode agnostic). + // The recent cache is disabled in the CWT lane so each row measures a single mechanism. _engineIdentity = new Engine(cfg => { cfg.Interop.ArrayConversion = ArrayConversionMode.Copy; cfg.Interop.TrackObjectWrapperIdentity = true; + cfg.Interop.CacheRecentObjectWrappers = false; }); _engineIdentity.SetValue("h", holder); _engineIdentity.Execute("h.Payload.Value"); @@ -65,7 +75,12 @@ public void GlobalSetup() _engineRecentCache.Execute("h.Payload.Value"); // the pre-4.14 default: every array crossing deep-copies into a fresh JsArray snapshot - _engineCopy = new Engine(cfg => cfg.Interop.ArrayConversion = ArrayConversionMode.Copy); + // (the cache opt-out keeps this row measuring the copy itself) + _engineCopy = new Engine(cfg => + { + cfg.Interop.ArrayConversion = ArrayConversionMode.Copy; + cfg.Interop.CacheRecentObjectWrappers = false; + }); _engineCopy.SetValue("h", holder); _engineCopy.Execute("h.Payload.Value"); } @@ -76,6 +91,12 @@ public void SameObjectReturnedInLoop_DefaultFlag() for (var i = 0; i < OperationsPerInvoke; i++) _engineDefault.Execute("h.Payload.Value"); } + [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] + public void SameObjectReturnedInLoop_NoCache() + { + for (var i = 0; i < OperationsPerInvoke; i++) _engineNoCache.Execute("h.Payload.Value"); + } + [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] public void SameObjectReturnedInLoop_IdentityFlag() { @@ -100,6 +121,12 @@ public void ArrayMemberTraversal_DefaultLiveView() _engineDefault.Execute(_arrayTraversal); } + [Benchmark] + public void ArrayMemberTraversal_LiveViewNoCache() + { + _engineNoCache.Execute(_arrayTraversal); + } + // The README-recommended pattern: hoist the collection into a local so the wrapper is created // once and the loop measures pure element-read cost (index dispatch + item conversion). private static readonly Prepared