Repository navigation
Performance Optimizations
Relevant source files
The following files were used as context for generating this wiki page:
This document details the specific optimization techniques implemented in OfficeScrubNative.dll that achieve 10-50x performance improvements over the original VBScript implementation. These optimizations focus on memory efficiency, CPU utilization, and I/O throughput through modern .NET patterns.
For general architecture of the native layer, see Native Library Overview. For implementation details of individual helper classes, see Helper Classes. For P/Invoke declarations and Win32 API usage, see P/Invoke and Win32 APIs.
The native layer implements five primary optimization strategies, each targeting different performance bottlenecks:
graph TD
Native["OfficeScrubNative.dll<br/>Performance Layer"]
subgraph "Memory Optimizations"
Span["Span<char> Stack Allocation<br/>Zero heap pressure<br/>3-5x faster GUID ops"]
Static["Static Caching<br/>DecodeTable<br/>GetHiveKey results"]
end
subgraph "Algorithm Optimizations"
HashSet["HashSet Collections<br/>O(1) lookups<br/>ValidSkus/SpecialProducts"]
StringComp["StringComparison<br/>.OrdinalIgnoreCase<br/>2x faster comparisons"]
end
subgraph "Concurrency Optimizations"
Parallel["Parallel Processing<br/>Task.Run<br/>Parallel.ForEach"]
end
subgraph "Resource Management"
Dispose["Proper Disposal<br/>Process objects<br/>Registry keys"]
end
Native --> Span
Native --> Static
Native --> HashSet
Native --> StringComp
Native --> Parallel
Native --> Dispose
Span --> GuidHelper["GuidHelper<br/>GetExpandedGuid()<br/>GetCompressedGuid()<br/>GetDecodedGuid()"]
HashSet --> Orchestrator["OfficeScrubOrchestrator<br/>IsInScope()<br/>ValidSkus<br/>SpecialProducts"]
StringComp --> Orchestrator2["OfficeScrubOrchestrator<br/>IsC2RPath()"]
Parallel --> ProcessHelper["ProcessHelper<br/>TerminateProcesses()"]
Dispose --> ProcessHelper2["ProcessHelper<br/>IsProcessRunning()"]
Sources: OfficeScrubC2R-Native.cs:1-1408, docs/OPTIMIZATION_ANALYSIS.md:1-349, docs/CHANGELOG.md:44-102
The most impactful optimization replaces StringBuilder and string.Substring() operations with stack-allocated Span<char> buffers in GUID manipulation methods. This eliminates heap allocations entirely for operations executed thousands of times during cleanup.
The GuidHelper class implements three GUID conversion methods using span-based operations:
| Method | Purpose | Original Allocations | Optimized Allocations |
|---|---|---|---|
GetExpandedGuid() |
Compressed → Standard GUID | 4 (StringBuilder + 3 substrings) | 0 (stack only) |
GetCompressedGuid() |
Standard → Compressed GUID | 3 (StringBuilder + 2 substrings) | 0 (stack only) |
GetDecodedGuid() |
Encoded → Standard GUID | 2 (StringBuilder + format string) | 0 (stack only) |
OfficeScrubC2R-Native.cs:174-214 shows the original implementation using StringBuilder:
// Original: 4 heap allocations per call
var sb = new StringBuilder(38); // Allocation 1: StringBuilder
sb.Append('{');
sb.Append(Reverse(compressedGuid.Substring(0, 8))); // Allocation 2 & 3: substring + reversed string
sb.Append('-');
sb.Append(Reverse(compressedGuid.Substring(8, 4))); // Allocation 4 & 5: substring + reversed string
// ... more allocationsdocs/OfficeScrubC2R-Native-Optimized-Real.cs:188-225 shows the optimized implementation:
// Optimized: 0 heap allocations
Span<char> result = stackalloc char[38]; // Stack allocation (no GC)
result[0] = '{';
result[9] = '-';
result[14] = '-';
result[19] = '-';
result[24] = '-';
result[37] = '}';
ReverseToSpan(compressedGuid.AsSpan(0, 8), result.Slice(1, 8)); // No allocations
ReverseToSpan(compressedGuid.AsSpan(8, 4), result.Slice(10, 4)); // No allocationsgraph LR
subgraph "Original Implementation"
HeapSB["Heap: StringBuilder<br/>38 bytes"]
HeapSub1["Heap: Substring 1<br/>8 bytes"]
HeapRev1["Heap: Reversed 1<br/>8 bytes"]
HeapSub2["Heap: Substring 2<br/>4 bytes"]
HeapRev2["Heap: Reversed 2<br/>4 bytes"]
GC1["Garbage Collector<br/>Must track all objects"]
HeapSB --> GC1
HeapSub1 --> GC1
HeapRev1 --> GC1
HeapSub2 --> GC1
HeapRev2 --> GC1
end
subgraph "Optimized Implementation"
Stack["Stack: Span<char><br/>38 bytes<br/>Deallocated automatically"]
NoGC["No GC Involvement<br/>Zero tracking overhead"]
Stack --> NoGC
end
From docs/OPTIMIZATION_ANALYSIS.md:268-276:
| Metric | Original | Optimized | Improvement |
|---|---|---|---|
| Execution Time (10,000 calls) | 12.4ms | 3.8ms | 3.3x faster |
| Memory Allocations | 4-5 per call | 0 per call | 100% reduction |
| GC Pressure | ~800KB garbage | 0 bytes | Zero GC pressure |
Sources: OfficeScrubC2R-Native.cs:172-328, docs/OfficeScrubC2R-Native-Optimized-Real.cs:173-332, docs/OPTIMIZATION_ANALYSIS.md:66-82
The GetDecodedGuid() method uses a 128-byte lookup table to decode base85-encoded GUIDs. The original implementation allocated this table on every method call.
OfficeScrubC2R-Native.cs:256-274 allocated the table each time:
public static bool GetDecodedGuid(string encodedGuid, out string decodedGuid)
{
// Allocates 128 bytes on every call
var table = new byte[]
{
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
// ... 112 more bytes
};
int ascii = encodedGuid[i];
int chr = table[ascii]; // Use table
}docs/OfficeScrubC2R-Native-Optimized-Real.cs:175-185 caches the table as a static field:
public static class GuidHelper
{
// Allocated once at class initialization
private static readonly byte[] DecodeTable = new byte[128]
{
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
// ... 112 more bytes
};
public static bool GetDecodedGuid(string encodedGuid, out string decodedGuid)
{
int chr = DecodeTable[ascii]; // Use cached table
}
}The RegistryHelper class frequently converts RegistryHiveType enum values to RegistryKey objects through GetHiveKey(). While the optimization of caching these results provides negligible performance benefit (5ns savings on 5ms operations), the code uses a switch expression for clarity.
docs/OfficeScrubC2R-Native-Optimized-Real.cs:545-552 implements the modern pattern:
private RegistryKey GetHiveKey(RegistryHiveType hive) => hive switch
{
RegistryHiveType.ClassesRoot => Registry.ClassesRoot,
RegistryHiveType.CurrentUser => Registry.CurrentUser,
RegistryHiveType.LocalMachine => Registry.LocalMachine,
RegistryHiveType.Users => Registry.Users,
_ => throw new ArgumentException("Invalid hive type")
};Sources: OfficeScrubC2R-Native.cs:255-320, docs/OfficeScrubC2R-Native-Optimized-Real.cs:175-185, docs/OPTIMIZATION_ANALYSIS.md:83-98
The OfficeScrubOrchestrator.IsInScope() method validates Office product codes against known SKU patterns. Converting validation arrays to HashSet<string> provides O(1) lookup complexity.
OfficeScrubC2R-Native.cs:1392-1403 used array search:
public bool IsInScope(string productCode)
{
var sku = upper.Substring(10, 4);
var validSkus = new[] { "007E", "008F", "008C", "24E1", "237A", "00DD" };
// O(n) linear search through array
return validSkus.Contains(sku) ||
upper == "{6C1ADE97-24E1-4AE4-AEDD-86D3A209CE60}" ||
upper == "{9520DDEB-237A-41DB-AA20-F2EF2360DCEB}" ||
upper == "{9AC08E99-230B-47E8-9721-4577B7F124EA}";
}docs/OfficeScrubC2R-Native-Optimized-Real.cs:1318-1381 uses static HashSets:
public class OfficeScrubOrchestrator
{
// Static collections initialized once
private static readonly HashSet<string> ValidSkus = new HashSet<string>
{
"007E", "008F", "008C", "24E1", "237A", "00DD"
};
private static readonly HashSet<string> SpecialProducts = new HashSet<string>
{
"{6C1ADE97-24E1-4AE4-AEDD-86D3A209CE60}",
"{9520DDEB-237A-41DB-AA20-F2EF2360DCEB}",
"{9AC08E99-230B-47E8-9721-4577B7F124EA}"
};
public bool IsInScope(string productCode)
{
// O(1) hash lookup
if (SpecialProducts.Contains(upper))
return true;
var sku = upper.Substring(10, 4);
return ValidSkus.Contains(sku); // O(1) hash lookup
}
}graph TD
subgraph "Array Search - O(n)"
Array["string[] validSkus<br/>6 elements"]
Search1["Compare element 0"]
Search2["Compare element 1"]
Search3["Compare element 2"]
Search4["Compare element 3"]
Search5["Compare element 4"]
Search6["Compare element 5"]
Array --> Search1
Search1 --> Search2
Search2 --> Search3
Search3 --> Search4
Search4 --> Search5
Search5 --> Search6
end
subgraph "HashSet Lookup - O(1)"
HashSet["HashSet<string> ValidSkus<br/>6 elements"]
Hash["Compute hash"]
Lookup["Direct bucket access"]
Compare["Single comparison"]
HashSet --> Hash
Hash --> Lookup
Lookup --> Compare
end
Note: With only 6 items, the performance difference is negligible (microseconds), but the pattern is correct for larger collections.
Sources: OfficeScrubC2R-Native.cs:1381-1404, docs/OfficeScrubC2R-Native-Optimized-Real.cs:1318-1383, docs/OPTIMIZATION_ANALYSIS.md:185-217
The original code used ToLower() or ToUpperInvariant() for case-insensitive comparisons, allocating temporary lowercase/uppercase strings. The optimized version uses StringComparison.OrdinalIgnoreCase to avoid allocations.
graph LR
subgraph "OfficeScrubOrchestrator"
IsC2RPath["IsC2RPath(string path)"]
Contains["path.Contains(pattern, ...)"]
end
subgraph "OfficeConstants"
Patterns["C2R_PATTERNS<br/>string[] array"]
Pattern1["ROOT\\OFFICE1"]
Pattern2["Microsoft Office\\Root\\"]
Pattern3["microsoft shared\\ClickToRun"]
Patterns --> Pattern1
Patterns --> Pattern2
Patterns --> Pattern3
end
IsC2RPath --> Contains
Contains --> Patterns
subgraph "Before: Allocations"
ToLower["path.ToLower()<br/>pattern.ToLower()"]
TempString1["Temporary string 1"]
TempString2["Temporary string 2"]
ToLower --> TempString1
ToLower --> TempString2
end
subgraph "After: No Allocations"
OrdinalIgnoreCase["StringComparison<br/>.OrdinalIgnoreCase"]
DirectCompare["Direct comparison<br/>No allocations"]
OrdinalIgnoreCase --> DirectCompare
end
docs/OfficeScrubC2R-Native-Optimized-Real.cs:1352-1359:
public bool IsC2RPath(string path)
{
if (string.IsNullOrEmpty(path))
return false;
// No allocation - directly compares without case conversion
return OfficeConstants.C2R_PATTERNS.Any(pattern =>
path.Contains(pattern, StringComparison.OrdinalIgnoreCase));
}From docs/OPTIMIZATION_ANALYSIS.md:288-294:
| Operation | ToLower() Method | StringComparison | Improvement |
|---|---|---|---|
| 1M comparisons | 847ms | 423ms | 2x faster |
| Memory allocations | 2MB temporary strings | 0 bytes | 100% reduction |
Sources: OfficeScrubC2R-Native.cs:1372-1379, docs/OfficeScrubC2R-Native-Optimized-Real.cs:1352-1359, docs/OPTIMIZATION_ANALYSIS.md:99-114
The ProcessHelper.TerminateProcesses() method terminates multiple Office processes concurrently using Task.Run() for parallel execution.
graph TD
Start["TerminateProcesses(processNames[])"]
subgraph "Sequential Enumeration"
Enum["foreach processName"]
GetProcs["Process.GetProcessesByName()"]
CreateTasks["Create Task for each process"]
end
subgraph "Parallel Execution"
Task1["Task.Run()<br/>Process 1<br/>winword.exe"]
Task2["Task.Run()<br/>Process 2<br/>excel.exe"]
Task3["Task.Run()<br/>Process 3<br/>powerpnt.exe"]
Task4["Task.Run()<br/>Process 4<br/>outlook.exe"]
Kill1["process.Kill()"]
Kill2["process.Kill()"]
Kill3["process.Kill()"]
Kill4["process.Kill()"]
Wait1["WaitForExit(5000)"]
Wait2["WaitForExit(5000)"]
Wait3["WaitForExit(5000)"]
Wait4["WaitForExit(5000)"]
Task1 --> Kill1
Task2 --> Kill2
Task3 --> Kill3
Task4 --> Kill4
Kill1 --> Wait1
Kill2 --> Wait2
Kill3 --> Wait3
Kill4 --> Wait4
end
WaitAll["Task.WaitAll(tasks, timeout)"]
Return["Return terminated PIDs"]
Start --> Enum
Enum --> GetProcs
GetProcs --> CreateTasks
CreateTasks --> Task1
CreateTasks --> Task2
CreateTasks --> Task3
CreateTasks --> Task4
Wait1 --> WaitAll
Wait2 --> WaitAll
Wait3 --> WaitAll
Wait4 --> WaitAll
WaitAll --> Return
OfficeScrubC2R-Native.cs:775-814 and docs/OfficeScrubC2R-Native-Optimized-Real.cs:747-789:
public List<int> TerminateProcesses(string[] processNames, int timeoutMs = 5000)
{
var terminatedPids = new List<int>();
var tasks = new List<Task>();
foreach (var processName in processNames)
{
var processes = Process.GetProcessesByName(Path.GetFileNameWithoutExtension(processName));
foreach (var process in processes)
{
var pid = process.Id;
// Create parallel task for each process
tasks.Add(Task.Run(() =>
{
try
{
if (!process.HasExited)
{
process.Kill();
if (process.WaitForExit(timeoutMs))
{
lock (terminatedPids) // Thread-safe list update
{
terminatedPids.Add(pid);
}
}
}
}
finally
{
process.Dispose(); // Proper resource cleanup
}
}));
}
}
// Wait for all parallel tasks with extended timeout
Task.WaitAll(tasks.ToArray(), timeoutMs * 2);
return terminatedPids;
}| Aspect | Sequential | Parallel | Improvement |
|---|---|---|---|
| 4 processes | 20 seconds (4×5s) | 5 seconds | 4x faster |
| CPU utilization | 25% (1 core) | 100% (4 cores) | 4x utilization |
| Responsiveness | Blocking | Non-blocking | User can continue |
Sources: OfficeScrubC2R-Native.cs:774-814, docs/OfficeScrubC2R-Native-Optimized-Real.cs:746-789
The optimized code ensures all Process objects are properly disposed to prevent resource leaks. The original code sometimes left processes undisposed if exceptions occurred.
docs/OfficeScrubC2R-Native-Optimized-Real.cs:820-834:
public bool IsProcessRunning(string processName)
{
try
{
var processes = Process.GetProcessesByName(Path.GetFileNameWithoutExtension(processName));
var hasRunning = processes.Length > 0;
// CRITICAL: Dispose all process objects before returning
foreach (var p in processes)
p.Dispose();
return hasRunning;
}
catch
{
return false;
}
}Before: Process handles leaked if exception thrown during array enumeration.
After: All processes disposed in all code paths.
The RegistryHelper uses C# 8.0 using declarations for automatic disposal:
docs/OfficeScrubC2R-Native-Optimized-Real.cs:424-431:
private void AddKeysFromPath(RegistryKey hiveKey, string subKey, HashSet<string> keys)
{
try
{
// C# 8.0 using declaration - automatically disposed at scope exit
using var key = hiveKey.OpenSubKey(subKey, false);
if (key != null)
{
foreach (var name in key.GetSubKeyNames())
keys.Add(name);
}
}
catch { }
}Sources: OfficeScrubC2R-Native.cs:844-857, docs/OfficeScrubC2R-Native-Optimized-Real.cs:820-834, docs/OPTIMIZATION_ANALYSIS.md:164-181
Based on typical Office uninstall scenario with 1000 registry keys, 5000 files, and 10 processes:
graph LR
subgraph "Operation Breakdown"
GUID["GUID Operations<br/>1000 calls"]
Registry["Registry Enumeration<br/>1000 keys"]
Files["File Deletion<br/>5000 files"]
Processes["Process Termination<br/>10 processes"]
Strings["String Comparisons<br/>10000 ops"]
end
subgraph "Original Performance"
GUID_Old["12ms"]
Reg_Old["2500ms"]
File_Old["8000ms"]
Proc_Old["50ms<br/>sequential"]
Str_Old["8ms"]
Total_Old["Total: 10,570ms<br/>~10.6 seconds"]
end
subgraph "Optimized Performance"
GUID_New["4ms<br/>3x faster"]
Reg_New["2500ms<br/>no change"]
File_New["8000ms<br/>no change"]
Proc_New["13ms<br/>4x faster"]
Str_New["4ms<br/>2x faster"]
Total_New["Total: 10,521ms<br/>~10.5 seconds"]
end
GUID --> GUID_Old
GUID --> GUID_New
Registry --> Reg_Old
Registry --> Reg_New
Files --> File_Old
Files --> File_New
Processes --> Proc_Old
Processes --> Proc_New
Strings --> Str_Old
Strings --> Str_New
GUID_Old --> Total_Old
Reg_Old --> Total_Old
File_Old --> Total_Old
Proc_Old --> Total_Old
Str_Old --> Total_Old
GUID_New --> Total_New
Reg_New --> Total_New
File_New --> Total_New
Proc_New --> Total_New
Str_New --> Total_New
| Component | Method | Original | Optimized | Improvement | Frequency | Total Savings |
|---|---|---|---|---|---|---|
| GuidHelper | GetExpandedGuid() |
12µs | 4µs | 3.0x | 1000 calls | 8ms |
| GuidHelper | GetDecodedGuid() |
15µs | 5µs | 3.0x | 500 calls | 5ms |
| ProcessHelper | TerminateProcesses() |
50ms | 13ms | 3.8x | 1 call | 37ms |
| OfficeScrubOrchestrator | String comparisons | 0.8µs | 0.4µs | 2.0x | 10000 calls | 4ms |
| RegistryHelper | I/O-bound operations | 2500ms | 2500ms | 1.0x | N/A | 0ms |
| FileHelper | I/O-bound operations | 8000ms | 8000ms | 1.0x | N/A | 0ms |
Key Insight: CPU-bound operations (GUID, string, process) show 2-4x improvements. I/O-bound operations (registry, files) are unchanged because the bottleneck is disk/system speed, not code efficiency.
Sources: docs/OPTIMIZATION_ANALYSIS.md:247-297, docs/CHANGELOG.md:46-102
docs/OPTIMIZATION_ANALYSIS.md:203-217 explains why converting C2R_PATTERNS to HashSet provides no benefit:
// This does NOT benefit from HashSet because .Any() performs sequential scan
OfficeConstants.C2R_PATTERNS.Any(pattern =>
path.Contains(pattern, StringComparison.OrdinalIgnoreCase))
// HashSet only helps for direct membership tests:
if (OfficeConstants.C2R_PATTERNS.Contains(specificPattern)) // O(1) vs O(n)Verdict: Kept as array. HashSet adds overhead without benefit for .Any() usage.
docs/OPTIMIZATION_ANALYSIS.md:219-236 analyzes the claimed "2-3x faster registry operations":
Savings from caching GetHiveKey():
- Switch statement execution: ~5 nanoseconds
Actual registry operation cost:
- RegOpenKeyEx() Win32 API: ~5,000,000 nanoseconds (5ms)
Performance improvement:
- 5ns / 5,000,000ns = 0.0001% (unmeasurable)
Verdict: Switch expression used for code clarity, but caching provides no measurable benefit.
docs/OPTIMIZATION_ANALYSIS.md:239-246 identifies the most critical issue:
- ShellHelper, TypeLibHelper, ServiceHelper classes were removed in a "optimized" version
- This is deletion, not optimization
- Breaks 40% of functionality
- Real optimization preserves 100% functionality while improving performance
Verdict: All classes retained. Optimization must never remove functionality.
Sources: docs/OPTIMIZATION_ANALYSIS.md:203-246
graph TD
Start["Identify Performance Issue"]
Profile["Profile with Measurement<br/>Find actual bottleneck"]
CPU{"CPU-bound<br/>operation?"}
IO{"I/O-bound<br/>operation?"}
OptCPU["Apply CPU optimizations:<br/>- Span<char><br/>- Static caching<br/>- Parallel processing"]
OptIO["I/O optimizations:<br/>- Batch operations<br/>- Async I/O<br/>- Native APIs"]
NoOpt["No optimization needed<br/>I/O dominates runtime"]
Measure["Measure improvement<br/>Before vs After"]
Decision{"Measurable<br/>improvement?"}
Keep["Keep optimization"]
Revert["Revert to original<br/>Complexity not justified"]
Start --> Profile
Profile --> CPU
Profile --> IO
CPU --> OptCPU
IO --> OptIO
IO --> NoOpt
OptCPU --> Measure
OptIO --> Measure
Measure --> Decision
Decision -->|Yes >= 10%| Keep
Decision -->|No < 10%| Revert
- Measure first: Profile before optimizing to find real bottlenecks
- Preserve functionality: Never delete features in the name of optimization
- Document assumptions: Explain why optimization is valid for this use case
- Measure results: Quantify improvement with benchmarks
- Consider maintainability: Optimization should not significantly increase complexity