void UpdateLocalScopes(Instruction removedInstruction, Instruction existingInstruction) { var debug_info = method.debug_info; if (debug_info == null) { return; } // Local scopes store start/end pair of "instruction offsets". Instruction offset can be either resolved, in which case it // has a reference to Instruction, or unresolved in which case it stores numerical offset (instruction offset in the body). // Typically local scopes loaded from PE/PDB files will be resolved, but it's not a requirement. // Each instruction has its own offset, which is populated on load, but never updated (this would be pretty expensive to do). // Instructions created during the editting will typically have offset 0 (so incorrect). // Local scopes created during editing will also likely be resolved (so no numerical offsets). // So while local scopes which are unresolved are relatively rare if they appear, manipulating them based // on the offsets allone is pretty hard (since we can't rely on correct offsets of instructions). // On the other hand resolved local scopes are easy to maintain, since they point to instructions and thus inserting // instructions is basically a no-op and removing instructions is as easy as changing the pointer. // For this reason the algorithm here is: // - First make sure that all instruction offsets are resolved - if not - resolve them // - First time this will be relatively expensinve as it will walk the entire method body to convert offsets to instruction pointers // Almost all local scopes are stored in the "right" order (sequentially per start offsets), so the code uses a simple one-item // cache instruction<->offset to avoid walking instructions multiple times (that would only happen for scopes which are out of order). // - Subsequent calls should be cheap as it will only walk all local scopes without doing anything // - If there was an edit on local scope which makes some of them unresolved, the cost is proportional // - Then update as necessary by manipulaitng instruction references alone InstructionOffsetCache cache = new InstructionOffsetCache() { Offset = 0, Index = 0, Instruction = items [0] }; UpdateLocalScope(debug_info.Scope, removedInstruction, existingInstruction, ref cache); }
InstructionOffset ResolveInstructionOffset(InstructionOffset inputOffset, ref InstructionOffsetCache cache) { if (inputOffset.IsResolved) { return(inputOffset); } int offset = inputOffset.Offset; if (cache.Offset == offset) { return(new InstructionOffset(cache.Instruction)); } if (cache.Offset > offset) { // This should be rare - we're resolving offset pointing to a place before the current cache position // resolve by walking the instructions from start and don't cache the result. int size = 0; for (int i = 0; i < items.Length; i++) { if (size == offset) { return(new InstructionOffset(items [i])); } if (size > offset) { return(new InstructionOffset(items [i - 1])); } size += items [i].GetSize(); } // Offset is larger than the size of the body - so it points after the end return(new InstructionOffset()); } else { // The offset points after the current cache position - so continue counting and update the cache int size = cache.Offset; for (int i = cache.Index; i < items.Length; i++) { cache.Index = i; cache.Offset = size; cache.Instruction = items [i]; if (cache.Offset == offset) { return(new InstructionOffset(cache.Instruction)); } if (cache.Offset > offset) { return(new InstructionOffset(items [i - 1])); } size += items [i].GetSize(); } return(new InstructionOffset()); } }
void UpdateLocalScope(ScopeDebugInformation scope, Instruction removedInstruction, Instruction existingInstruction, ref InstructionOffsetCache cache) { if (scope == null) { return; } if (!scope.Start.IsResolved) { scope.Start = ResolveInstructionOffset(scope.Start, ref cache); } if (!scope.Start.IsEndOfMethod && scope.Start.ResolvedInstruction == removedInstruction) { scope.Start = new InstructionOffset(existingInstruction); } if (scope.HasScopes) { foreach (var subScope in scope.Scopes) { UpdateLocalScope(subScope, removedInstruction, existingInstruction, ref cache); } } if (!scope.End.IsResolved) { scope.End = ResolveInstructionOffset(scope.End, ref cache); } if (!scope.End.IsEndOfMethod && scope.End.ResolvedInstruction == removedInstruction) { scope.End = new InstructionOffset(existingInstruction); } }
InstructionOffset ResolveInstructionOffset(InstructionOffset inputOffset, ref InstructionOffsetCache cache) { if (inputOffset.IsResolved) { return(inputOffset); } int offset = inputOffset.Offset; if (cache.Offset == offset) { return(new InstructionOffset(cache.Instruction)); } if (cache.Offset > offset) { // This should be rare - we're resolving offset pointing to a place before the current cache position // resolve by walking the instructions from start and don't cache the result. int size = 0; for (int i = 0; i < items.Length; i++) { // The array can be larger than the actual size, in which case its padded with nulls at the end // so when we reach null, treat it as an end of the IL. if (items [i] == null) { return(new InstructionOffset(i == 0 ? items [0] : items [i - 1])); } if (size == offset) { return(new InstructionOffset(items [i])); } if (size > offset) { return(new InstructionOffset(i == 0 ? items [0] : items [i - 1])); } size += items [i].GetSize(); } // Offset is larger than the size of the body - so it points after the end return(new InstructionOffset()); } else { // The offset points after the current cache position - so continue counting and update the cache int size = cache.Offset; for (int i = cache.Index; i < items.Length; i++) { cache.Index = i; cache.Offset = size; var item = items [i]; // Allow for trailing null values in the case of // instructions.Size < instructions.Capacity if (item == null) { return(new InstructionOffset(i == 0 ? items [0] : items [i - 1])); } cache.Instruction = item; if (cache.Offset == offset) { return(new InstructionOffset(cache.Instruction)); } if (cache.Offset > offset) { return(new InstructionOffset(i == 0 ? items [0] : items [i - 1])); } size += item.GetSize(); } return(new InstructionOffset()); } }