using System.Security.Cryptography; using System.Text; using Mono.Cecil; using Mono.Cecil.Cil; /// /// Skips periodic debug output and explicitly uses a 1 ms forced timeslice. /// The real elapsed-time accounting and 10 ms Stride weight divisor stay intact. /// internal static class PeriodicSchedulerLogPatch { internal const string SchedulerType = "Cosmos.Kernel.Core.Scheduler.Stride.StrideScheduler"; internal const int ChangedInstructionIndex = 63; internal const int ResumeInstructionIndex = 82; internal const int TimesliceInstructionIndex = 103; internal const long ForcedTimesliceNanoseconds = 1_000_000; internal const string OriginalMethodSha256 = "e1a70ae8c5e8c5bbd17e0255a98f0d40fd1529a3d32cf33cd0fa475dea3d396c"; public static bool Apply(AssemblyDefinition assembly) { if (assembly.Name.Name != "Cosmos.Kernel.Core" || assembly.Name.Version != new Version(3, 0, 94, 0)) throw new InvalidOperationException("Periodic scheduler logging patch requires Cosmos.Kernel.Core 3.0.94."); var type = assembly.MainModule.Types.Single(candidate => candidate.FullName == SchedulerType); var method = type.Methods.Single(candidate => candidate.Name == "OnTick"); var counter = type.Fields.Single(candidate => candidate.Name == "s_onTickLogCount"); if (method.IsStatic || method.ReturnType.MetadataType != MetadataType.Boolean || method.Parameters.Count != 3 || method.Parameters[0].ParameterType.FullName != "Cosmos.Kernel.Core.Scheduler.PerCpuState" || method.Parameters[1].ParameterType.FullName != "Cosmos.Kernel.Core.Scheduler.SchedulerThread" || method.Parameters[2].ParameterType.MetadataType != MetadataType.UInt64 || !method.HasBody || !counter.IsPrivate || !counter.IsStatic || counter.FieldType.MetadataType != MetadataType.UInt32 || method.Body.Instructions.Sum(instruction => instruction.GetSize()) is not (269 or 277) || method.Body.Instructions.Count != 108) throw new InvalidOperationException("The pinned scheduler timer callback contract changed."); var instructions = method.Body.Instructions; var change = instructions[ChangedInstructionIndex]; var resume = instructions[ResumeInstructionIndex]; bool alreadyPatched = change.OpCode == OpCodes.Br && ReferenceEquals(change.Operand, resume); var timeslice = instructions[TimesliceInstructionIndex]; bool timeslicePatched = timeslice.OpCode == OpCodes.Ldc_I8 && Equals(timeslice.Operand, ForcedTimesliceNanoseconds); if (!timeslicePatched && timeslice.OpCode != OpCodes.Ldloc_2) throw new InvalidOperationException("Unknown forced timeslice expression; refusing to change scheduler arithmetic."); if (instructions.Sum(instruction => instruction.GetSize()) != (timeslicePatched ? 277 : 269) || instructions[102].OpCode != OpCodes.Ldarg_3 || instructions[104].OpCode != OpCodes.Clt_Un || instructions[105].OpCode != OpCodes.Ldc_I4_0 || instructions[106].OpCode != OpCodes.Ceq || instructions[107].OpCode != OpCodes.Ret) throw new InvalidOperationException("The exact terminal elapsed-time comparison changed."); if (!alreadyPatched && (change.OpCode != OpCodes.Ldsfld || change.Operand is not FieldReference field || field.FullName != counter.FullName)) throw new InvalidOperationException("Unknown periodic scheduler log entry; refusing to patch it."); // Bind the whole method, including every original branch destination and // local type. Normalization accepts exactly the one five-byte replacement. if (HashCanonical(method, counter) != OriginalMethodSha256) throw new InvalidOperationException("The pinned scheduler OnTick IL differs outside the periodic logging patch."); if (alreadyPatched && timeslicePatched) return true; // The log-entry ldsfld and br both occupy five bytes. Apart from // the explicit terminal threshold below, preserve all instructions, // branch targets, locals, exception regions and metadata entries. change.OpCode = OpCodes.Br; change.Operand = resume; // Only the final preemption comparison changes. The local quantum // remains 10 ms everywhere used for Pass/Stride runtime accounting. // A per-interrupt 1 ms elapsed value can never reach the old terminal // 10 ms test: it did not accumulate a timeslice and could starve a // ready UI behind a higher virtual pass after IRQ-off work. timeslice.OpCode = OpCodes.Ldc_I8; timeslice.Operand = ForcedTimesliceNanoseconds; return false; } private static string HashCanonical(MethodDefinition method, FieldReference counter) { var body = method.Body; // Restore the two precisely validated source instructions in the // fingerprint, including the eight-byte tail growth, so every other // instruction/local/branch remains bound to the fixed official hash. string canonical = 269 + "|" + body.MaxStackSize + "|" + body.InitLocals + "|" + body.ExceptionHandlers.Count + "|" + string.Join(";", body.Variables.Select(variable => variable.VariableType.FullName)) + "|" + string.Join("\n", body.Instructions.Select((instruction, index) => index == ChangedInstructionIndex ? Code.Ldsfld + ":" + counter.FullName : index == TimesliceInstructionIndex ? Code.Ldloc_2 + ":" : instruction.OpCode.Code + ":" + (instruction.Operand switch { Instruction target => "#" + body.Instructions.IndexOf(target), VariableDefinition local => "$" + local.Index, MemberReference member => member.FullName, null => "", var operand => operand.ToString() }))); return Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(canonical))); } }