diff --git a/docs/roadmaps/game-area-portals.md b/docs/roadmaps/game-area-portals.md index 73767216..96684c94 100644 --- a/docs/roadmaps/game-area-portals.md +++ b/docs/roadmaps/game-area-portals.md @@ -258,6 +258,63 @@ product installation or retained adoption. P3 remains open for performance and fresh/retained product adoption. Original resume04/resume05 effects, caches and custody are unchanged and were not reused. +### P3 current presentation performance smoke source — 2026-10-10 + +An explicit `--presentation-performance` mode requires a declared exact source revision +and emits raw stock counters plus its actual assembly and workload digests. It creates +and validates the existing five-tick scene once, disposes its worlds, warms up five +cycles, and then measures twenty samples of one hundred twelve-view cycles: 24,000 +actual interpolation and scene-projection views over immutable copied snapshots. +Each returned frame is retained and checked outside timing against qualified bindings, +identity/point coverage and structural bounds. Default and presentation modes remain. + +The mode reports elapsed timestamps, process CPU ticks and GC collection deltas, +and allocations on the synchronous measurement thread, with per-view normalization +and sample ranges/medians. Loop/storage/counter overhead is included; setup, validation +and JSON serialization are excluded. Process CPU includes runtime threads and has +coarse resolution. No subtraction, timing threshold or speedup verdict is invented. + +Direct optimized F# compilation against the already qualified public assemblies passed +with warnings as errors. Six compiled refusal controls passed before world setup: +missing, short, uppercase and nonhex declared source revisions, an invalid argument, +and an extra performance argument all exited 2; the existing usage substring remains. +One separately admitted fresh offline receiver then passed at source +`13a8cdeb821e43bcd94701902d5b6b2a3e6f42d4`. It used only the seven newly owned, +previously public-qualified package payloads in an isolated local feed and empty new +caches. Locked restore preserved the genuine lock, the actual package boundary passed, +and Release compilation reported no warnings or errors. Default/presentation/invalid +entrypoints exited 0/0/2; the explicit performance entrypoint exited 0. + +All twenty raw samples and 24,000 checked views were retained under .NET `10.0.12`, +SDK `10.0.401`, CPU 1 and a 1.5 GiB managed heap cap. Elapsed time per view had a +median of 886.82 ns and a range of 777.68–9,159.78 ns. Process CPU per view had a +median of 885 ns and a range of 776.67–9,117.5 ns. Measurement-thread allocations +per view had a median of 1,970.07 bytes and a range of 1,970.07–1,970.17 bytes. +Process collection deltas summed to 2/1/0 for generations 0/1/2. Outliers were retained; +there was no rerun. These are scoped current smoke observations, not a budget verdict, +speedup claim or proof of steady-state behavior. Workload SHA256 is +`66113918829ada39ae1a4fdfd32255e300e3986db4bbabc0b6e69807f34d4fd4`; +the measured assembly SHA256 is +`cd1fcc61c20cce56e37cbfcf0a382d27eb74642f39d8af79c61f69b76b2530d1`. + +The original pre-edit performance baseline remains unknown. The receiver completed +in 3.32 seconds; that whole-operation duration is separate from the view samples. +A separate post-terminal census found no owned process-group/session member; +total RSS and detached descendants were not independently measured. Physics stepping, +raster/GPU timing and installed-product/adoption are outside the measured scope. +Performance release requirements and fresh/retained product adoption remain open; +P3 is not done. +Original resume04/resume05 effects and custody remain unchanged. + +Native PR709 checks on Ubuntu and Windows subsequently failed compilation at the +nullable source-revision guard; the surface-baseline job stopped at the same build, +before baseline refresh. The guard now uses `Option.ofObj` to refine the environment +value before string operations. Direct compilation with null checking, latest language +version and warnings as errors passes, as do the six compiled refusal controls. +The measured computational body is byte-identical to source `13a8cdeb`; no measurement +was repeated. The raw observations above remain bound to that original source and +assembly, rather than certifying the successor assembly's exact timing. + A crate straddling an opening and colliding on both sides, or a rope/joint spanning areas, requires a separately designed and measured solver/constraint strategy. Body recreation does not preserve hidden contacts, warm-start state or exact solver rollback. diff --git a/examples/Box2D.Portals/Program.fs b/examples/Box2D.Portals/Program.fs index c2e0ac26..6c159a67 100644 --- a/examples/Box2D.Portals/Program.fs +++ b/examples/Box2D.Portals/Program.fs @@ -1,5 +1,12 @@ module Box2D.Portals.Program +open System +open System.Diagnostics +open System.IO +open System.Security.Cryptography +open System.Text +open System.Text.Json + let private qualifyDefault () = let observed = Scene.qualify () let event = observed.Head.Traversals.Head @@ -10,6 +17,87 @@ let private qualifyDefault () = event.DestinationPose.LinearVelocity.X event.DestinationPose.LinearVelocity.Y event.DestinationPose.AngularVelocity 0 +let private sha256 (bytes: byte array) = SHA256.HashData bytes |> Convert.ToHexStringLower + +let private qualifyPerformance sourceRevision = + // Worlds are disposed inside qualify, before any warmup or timed view invocation. + let snapshots, referenceFrames = Presentation.qualify () + if snapshots.Length <> 5 || referenceFrames.Length <> 12 then + failwith "Performance workload must have five snapshots and twelve reference frames." + let inputs = + snapshots |> List.pairwise |> List.collect (fun (previous, current) -> + [ for alpha in [ 0.; 0.5; 1. ] do yield previous, current, alpha ]) |> List.toArray + let expected = List.toArray referenceFrames + let check index (frame: Presentation.Frame) = + let _, current, _ = inputs[index % inputs.Length] + let nodes, points = Presentation.inspect current frame + let reference = expected[index % expected.Length] + let bindings (value: Presentation.Frame) = value.Areas |> List.map (fun area -> area.Area, area.Bindings) + if frame.Tick <> reference.Tick || frame.Alpha <> reference.Alpha || bindings frame <> bindings reference then + failwith "Measured view differs from the qualified reference." + if nodes < 1 || nodes > 2 || points <> 2 then failwith "Performance structural bounds differ." + nodes, points + for index in 0 .. expected.Length - 1 do check index expected[index] |> ignore + for _ in 1 .. 5 do + for index in 0 .. inputs.Length - 1 do + let previous, current, alpha = inputs[index] + Presentation.view alpha previous current |> check index |> ignore + + // Storage is allocated once outside all timed intervals. Every returned frame is + // retained and inspected afterwards; inspection/serialization are not timed. + let frames = Array.zeroCreate (100 * inputs.Length) + use measuredProcess = Process.GetCurrentProcess () + let samples = + [| for sample in 1 .. 20 do + let collectionsBefore = [| for generation in 0 .. 2 -> GC.CollectionCount generation |] + let cpuBefore = measuredProcess.TotalProcessorTime.Ticks + let allocationBefore = GC.GetAllocatedBytesForCurrentThread () + let started = Stopwatch.GetTimestamp () + for index in 0 .. frames.Length - 1 do + let previous, current, alpha = inputs[index % inputs.Length] + frames[index] <- Presentation.view alpha previous current + let elapsedTicks = Stopwatch.GetTimestamp () - started + let allocatedBytes = GC.GetAllocatedBytesForCurrentThread () - allocationBefore + let processCpuTicks = measuredProcess.TotalProcessorTime.Ticks - cpuBefore + let collections = [| for generation in 0 .. 2 -> GC.CollectionCount generation - collectionsBefore[generation] |] + let mutable areaNodes = 0 + let mutable points = 0 + for index in 0 .. frames.Length - 1 do + let nodes, count = check index frames[index] + areaNodes <- areaNodes + nodes + points <- points + count + if areaNodes <> 2400 || points <> 2400 then failwith "Measured workload totals differ." + yield + {| sample = sample; views = frames.Length; elapsedTicks = elapsedTicks + elapsedNanosecondsPerView = float elapsedTicks * 1e9 / float Stopwatch.Frequency / float frames.Length + processCpuTicks = processCpuTicks + processCpuNanosecondsPerView = float processCpuTicks * 100. / float frames.Length + currentThreadAllocatedBytes = allocatedBytes + currentThreadAllocatedBytesPerView = float allocatedBytes / float frames.Length + processGcCollections = collections; areaNodes = areaNodes; points = points |} |] + let distribution values = + let sorted = Array.sort values + {| minimum = sorted[0]; median = (sorted[9] + sorted[10]) / 2.; maximum = sorted[19] |} + let definition = "GAME-PORTAL-01.P3;Presentation.view;areas=2;entities=2;setupTicks=5;pairs=4;alpha=0,0.5,1;viewsPerCycle=12;warmupCycles=5;samples=20;cyclesPerSample=100;maxAreaNodesPerView=2;pointsPerView=2;noSimulationDuringMeasurement;v1" + let assembly = typeof.Assembly.Location + let result = + {| schema = "fsgg.portal.presentation-smoke/1"; originalItem = "GAME-PORTAL-01.P3" + declaredSourceRevision = sourceRevision; assemblySha256 = File.ReadAllBytes assembly |> sha256 + workloadDefinition = definition; workloadSha256 = Encoding.UTF8.GetBytes definition |> sha256 + historicalPreEditBaseline = "unknown"; verdict = "current-smoke-observed-no-timing-threshold" + scope = "Actual Presentation.view interpolation+scene projection; loop, frame storage and counter overhead included. Setup, validation and JSON outside timing. No physics, raster, GPU or product-adoption timing." + counterScope = "Elapsed stopwatch; process CPU and GC collections; current synchronous measurement-thread allocations. Process CPU has coarse resolution and may include runtime threads. No overhead subtraction." + runtime = Environment.Version.ToString (); os = System.Runtime.InteropServices.RuntimeInformation.OSDescription + architecture = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture.ToString () + processorCount = Environment.ProcessorCount; stopwatchFrequency = Stopwatch.Frequency + stopwatchHighResolution = Stopwatch.IsHighResolution; warmupCycles = 5; samples = samples + measuredViews = samples |> Array.sumBy _.views + elapsedNanosecondsPerView = samples |> Array.map _.elapsedNanosecondsPerView |> distribution + processCpuNanosecondsPerView = samples |> Array.map _.processCpuNanosecondsPerView |> distribution + currentThreadAllocatedBytesPerView = samples |> Array.map _.currentThreadAllocatedBytesPerView |> distribution |} + JsonSerializer.Serialize(result, JsonSerializerOptions(WriteIndented = true)) |> printfn "%s" + 0 + [] let main argv = if argv = [| "--presentation" |] then @@ -18,8 +106,16 @@ let main argv = printfn "PASS portal presentation: ticks=%d frames=%d areaNodes=%d points=%d alpha=0,0.5,1 local-metres=+Y-up" snapshots.Length frames.Length (counts |> List.sumBy fst) (counts |> List.sumBy snd) 0 + elif argv = [| "--presentation-performance" |] then + match Environment.GetEnvironmentVariable "FSGG_PORTAL_SOURCE_REVISION" |> Option.ofObj with + | Some revision when revision.Length = 40 && revision |> Seq.forall (fun c -> c >= '0' && c <= '9' || c >= 'a' && c <= 'f') -> + qualifyPerformance revision + | _ -> + eprintfn "--presentation-performance requires FSGG_PORTAL_SOURCE_REVISION (exact lowercase 40-hex; declared externally, not inferred)." + 2 elif argv.Length <> 0 then eprintfn "Usage: Box2D.Portals [--presentation]" + eprintfn "Performance smoke: --presentation-performance (requires FSGG_PORTAL_SOURCE_REVISION)." 2 else qualifyDefault ()