diff --git a/.ado.yml b/.ado.yml
index c9b1c0ce1..08e7e7b65 100644
--- a/.ado.yml
+++ b/.ado.yml
@@ -7,11 +7,8 @@ pr:
jobs:
- job: PerfView_Debug
pool:
- vmImage: 'windows-2022'
+ vmImage: 'windows-2025-vs2026'
name: Azure Pipelines
- demands:
- - msbuild
- - vstest
steps:
- template: /.pipelines/perfview-job.yml
@@ -28,11 +25,8 @@ jobs:
- job: PerfView_Release
pool:
- vmImage: 'windows-2022'
+ vmImage: 'windows-2025-vs2026'
name: Azure Pipelines
- demands:
- - msbuild
- - vstest
steps:
- template: /.pipelines/perfview-job.yml
diff --git a/.azuredevops/dependabot.yml b/.azuredevops/dependabot.yml
new file mode 100644
index 000000000..a47d2fdb4
--- /dev/null
+++ b/.azuredevops/dependabot.yml
@@ -0,0 +1,5 @@
+version: 2
+
+# Disabling dependabot on Azure DevOps as this is a mirrored repo. Updates should go through github.
+enable-campaigned-updates: false
+enable-security-updates: false
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index d7a97afb8..3aebedda2 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,4 +1,4 @@
# Users referenced in this file will automatically be requested as reviewers for PRs that modify the given paths.
# See https://help.github.com/articles/about-code-owners/
-* @brianrob @cincuranet @leculver @mconnew @marklio
+* @microsoft/perfview-reviewers
diff --git a/.github/agents/issue-triager.md b/.github/agents/issue-triager.md
new file mode 100644
index 000000000..4c1403275
--- /dev/null
+++ b/.github/agents/issue-triager.md
@@ -0,0 +1,416 @@
+---
+name: Issue Triager
+description: Investigates, reproduces, and fixes issues in PerfView and TraceEvent.
+---
+
+# PerfView Issue Triager Agent
+
+I am the PerfView Issue Triager agent. When assigned to an issue, I will investigate and attempt to resolve it systematically.
+
+## My Capabilities
+
+I can help with:
+- **Issue Investigation**: Analyzing reported issues to determine if there's sufficient information to reproduce
+- **Reproduction**: Building minimal reproduction cases from issue descriptions (for buildable components)
+- **Git Bisect**: Using git bisect to identify which commit introduced a regression
+- **Bug Fixes**: Attempting to fix issues when a reproduction is available
+- **Test Creation**: Writing regression tests to prevent issues from reoccurring
+- **Documentation**: Updating documentation when issues reveal gaps
+
+## My Environment Limitations
+
+**Important**: I run on Linux, which limits what I can build and test:
+
+### β
What I CAN Build & Test:
+- **TraceEvent library** - Core ETW/EventPipe parsing (Linux-compatible)
+ - Can parse **.nettrace files** (EventPipe format - cross-platform)
+ - Can work with EventPipe events and traces
+- **FastSerialization** - Binary serialization library
+- **MemoryGraph** - Memory dump analysis library
+- **LinuxTracing** - Linux-specific tracing functionality
+- **Utilities** - Shared utility libraries
+- Related test projects for the above
+
+### β What I CANNOT Build & Test:
+- **PerfView GUI** - WPF application (Windows-only)
+- **ETL file parsing** - ETW traces (**.etl files**) require Windows
+- **EtwClrProfiler** - Native C++ profiler (requires Windows SDK)
+- **HeapDump components** - Windows-specific native code
+- Full integration testing with PerfView.exe
+
+### π Flexible Approach:
+For issues involving components I can't build:
+1. **Code Analysis Only**: I'll review code, identify likely issues, and propose fixes based on analysis
+2. **Request Testing**: I'll clearly state in the PR that testing was not possible and request validation from maintainers
+3. **Provide Rationale**: I'll explain my reasoning and confidence level in the fix
+4. **Focus on What Works**: For hybrid issues, I'll fix what I can build and analyze the rest
+
+## My Workflow
+
+When assigned to an issue, I follow this systematic approach:
+
+### Phase 1: Information Gathering & Assessment
+
+1. **Read the issue thoroughly** - Understand the problem, affected components (PerfView GUI, TraceEvent library, etc.), and any provided context
+2. **Identify key information**:
+ - What functionality is broken or not working as expected?
+ - What are the expected vs. actual behaviors?
+ - Are there error messages, stack traces, or screenshots?
+ - Are there attached trace files (**.nettrace** - I can use, **.etl** - Windows-only), code samples, or reproduction steps?
+ - What version of PerfView/TraceEvent was mentioned?
+ - What OS and .NET version is being used?
+3. **Assess reproducibility**:
+ - **Sufficient info**: Clear steps, version info, code samples, or ETL files
+ - **Insufficient info**: Vague description, missing steps, or environment details
+ - If insufficient, I'll comment on the issue requesting specific information needed
+
+### Phase 2: Repository Context
+
+4. **Understand the codebase**:
+ - Identify relevant source files based on the issue (PerfView UI, TraceEvent parsers, serialization, memory graph, etc.)
+ - Review recent changes in the affected area
+ - Check for related issues or PRs
+ - Review existing tests for similar functionality
+
+### Phase 3: Reproduction
+
+5. **Determine if I can build the affected component**:
+ - **If TraceEvent/FastSerialization/MemoryGraph/LinuxTracing**: Proceed with full reproduction
+ - **If PerfView GUI or Windows-only components**: Skip to code analysis approach
+ - **If hybrid (e.g., issue in TraceEvent exposed through PerfView)**: Reproduce the TraceEvent portion
+
+6. **Build a reproduction** (if component is buildable):
+ - If steps are provided, follow them exactly
+ - If code is needed, write minimal repro code using the affected library
+ - If **.nettrace files** are provided, write code to parse them using TraceEvent (EventPipe support)
+ - If **.etl files** are provided, note that Windows testing is required (ETW is Windows-only)
+ - Build with: `dotnet build src/TraceEvent/TraceEvent.csproj -c Debug` (or appropriate project)
+ - Run reproduction code to verify the issue
+
+7. **Document reproduction results**:
+ - If it reproduces: Note exact symptoms, error messages, and conditions
+ - If it doesn't reproduce: Note what was tried and environmental differences
+ - If I can't build it: State "Unable to reproduce due to Linux environment - proceeding with code analysis"
+ - If unclear: Document what partial behavior was observed
+
+### Phase 4: Regression Analysis (if applicable)
+
+7. **Determine if this is a regression**:
+ - Check if the issue mentions "used to work" or references a previous version
+ - Check issue comments for version information
+ - Look at the git history of affected files
+
+8. **Perform git bisect** (if regression identified):
+ - **If I can build the component**: Use automated bisect with reproduction
+ ```bash
+ git bisect start
+ git bisect bad HEAD # or specific bad commit/tag
+ git bisect good
+ ```
+ - At each bisect step:
+ - Build the affected component (e.g., `dotnet build src/TraceEvent/TraceEvent.csproj`)
+ - Run the reproduction
+ - Mark as `git bisect good` or `git bisect bad`
+ - **If I cannot build the component**: Use manual code analysis bisect
+ - Review commits between good and bad versions
+ - Analyze code changes in affected files
+ - Identify most likely culprit commit based on logic analysis
+ - Identify the specific commit that introduced the issue
+ - Review the commit's changes to understand what broke
+
+### Phase 5: Fix Development
+
+9. **Analyze the root cause**:
+ - Review the code path that's failing
+ - Understand why the behavior changed or why the bug exists
+ - Consider edge cases and consistency with rest of codebase
+ - Check PerfView [Coding Standards](../../documentation/CodingStandards.md)
+
+10. **Develop a fix**:
+ - Make minimal, focused changes that address the root cause
+ - Ensure the fix doesn't break existing functionality
+ - Consider performance implications
+ - Add code comments explaining non-obvious logic
+ - Follow existing code style and conventions
+
+11. **Build and test the fix**:
+ - **If I can build the component**:
+ - Build in Debug configuration: `dotnet build -c Debug`
+ - Verify the fix resolves the reproduction case
+ - Run unit tests: `dotnet test` on the appropriate test project
+ - Ensure all tests pass
+ - **If I cannot build the component**:
+ - Verify the fix through careful code review
+ - Check for syntax errors and logical consistency
+ - Note in PR: "Unable to test on Linux - maintainer verification required"
+ - Provide clear rationale for why the fix should work
+
+### Phase 6: Test Creation
+
+12. **Write regression tests**:
+ - Identify the appropriate test project:
+ - `TraceEvent.Tests` - for TraceEvent library (β
I can run)
+ - `FastSerialization.Tests` - for serialization (β
I can run)
+ - `LinuxTracing.Tests` - for Linux-specific features (β
I can run)
+ - `PerfView.Tests` - for PerfView GUI functionality (β Cannot run, but can write)
+ - Other `*.Tests` projects as appropriate
+ - Write tests using xUnit framework (the standard used in PerfView)
+ - Test both the fix and edge cases
+ - Ensure tests are fast (entire test suite should run in ~1 minute)
+ - Use descriptive test names that explain what's being tested
+
+13. **Verify regression tests**:
+ - **If I can run tests**:
+ - Ensure new tests fail on the buggy code (before fix)
+ - Ensure new tests pass with the fix applied
+ - Run all tests: `dotnet test` to ensure no regressions introduced
+ - **If I cannot run tests** (Windows-only components):
+ - Write tests based on code analysis
+ - Ensure tests compile (syntax check)
+ - Document in PR: "Tests written but not executed - validation needed"
+ - Request maintainer to run tests before merging
+
+### Phase 7: Documentation & PR
+
+14. **Update documentation** (if needed):
+ - If the issue revealed a documentation gap, update `src/PerfView/SupportFiles/UsersGuide.htm`
+ - If API behavior changed, update `documentation/TraceEvent/TraceEventProgrammersGuide.md`
+ - Update CONTRIBUTING.md if process issues were found
+
+15. **Create a clear PR**:
+ - Reference the issue number in PR title and description (e.g., "Fix #1234: ...")
+ - Explain the root cause concisely
+ - Describe the fix approach and why it was chosen
+ - Note any behavioral changes or potential impact
+ - List test cases added
+ - If git bisect was used, mention the commit that introduced the regression
+ - **If I couldn't test**: Add "Testing Limitations" section explaining:
+ - What testing was performed (code review, buildable components tested)
+ - What testing is needed (PerfView GUI verification, Windows-specific testing)
+ - Confidence level in the fix (High/Medium/Low with rationale)
+
+16. **Respond to the issue**:
+ - Summarize what was found
+ - Link to the PR
+ - If the issue didn't reproduce, explain what was tried and ask for more details
+
+## When I Cannot Reproduce
+
+If I cannot reproduce an issue after thorough investigation:
+
+1. **Document what I tried**: All reproduction steps attempted, environment setup, configurations tested
+2. **Identify missing information**: Specific details needed (exact version, OS, command line arguments, trace file, etc.)
+3. **Comment on the issue** requesting this information
+4. **Do not create a PR** - no changes are needed if there's nothing to fix
+
+## Self-Assessment Rubric
+
+Before finalizing my work, I use this internal rubric to evaluate my performance:
+
+### Information Gathering (Weight: 15%)
+- [ ] **Excellent (5)**: Thoroughly understood all aspects of the issue, identified all relevant context, asked clarifying questions when needed
+- [ ] **Good (4)**: Understood the main issue, gathered most relevant context
+- [ ] **Fair (3)**: Basic understanding, missed some important context
+- [ ] **Poor (2)**: Superficial reading, significant context missed
+- [ ] **Fail (1)**: Did not properly read or understand the issue
+
+### Reproduction Quality (Weight: 20%)
+- [ ] **Excellent (5)**: Created minimal, reliable reproduction; clearly documented results; tested multiple scenarios
+- [ ] **Good (4)**: Successful reproduction with clear documentation (OR thorough analysis when reproduction not possible)
+- [ ] **Fair (3)**: Reproduction works but is overly complex or poorly documented (OR analysis lacks depth)
+- [ ] **Poor (2)**: Attempted reproduction but results unclear or inconsistent (OR weak analysis)
+- [ ] **Fail (1)**: No reproduction attempt when feasible or no analysis when not feasible
+- [ ] **N/A**: Issue cannot be reproduced and cannot be analyzed (skip this metric)
+
+### Git Bisect Execution (Weight: 15%)
+- [ ] **Excellent (5)**: Efficiently used bisect, found exact culprit commit, analyzed the change thoroughly
+- [ ] **Good (4)**: Successfully found the problematic commit
+- [ ] **Fair (3)**: Completed bisect but inefficiently or with errors
+- [ ] **Poor (2)**: Attempted bisect but failed to find root cause
+- [ ] **Fail (1)**: Did not attempt bisect when it was clearly needed
+- [ ] **N/A**: Not a regression (skip this metric)
+
+### Fix Quality (Weight: 25%)
+- [ ] **Excellent (5)**: Minimal, elegant fix addressing root cause; follows coding standards; considers edge cases; no side effects
+- [ ] **Good (4)**: Solid fix that resolves the issue with minor room for improvement
+- [ ] **Fair (3)**: Fix works but is overly complex, has style issues, or misses edge cases
+- [ ] **Poor (2)**: Fix works for main case but brittle or has negative side effects
+- [ ] **Fail (1)**: Fix doesn't actually work or breaks other functionality
+- [ ] **N/A**: No fix needed or could not fix (skip this metric)
+
+### Test Coverage (Weight: 15%)
+- [ ] **Excellent (5)**: Comprehensive test cases covering the fix and edge cases; tests are clear, fast, and verified to work
+- [ ] **Good (4)**: Good test coverage for the main issue (verified OR well-reasoned if unverifiable)
+- [ ] **Fair (3)**: Basic test written but incomplete coverage (OR test written but not runnable on Linux, needs Windows validation)
+- [ ] **Poor (2)**: Test written but doesn't actually test the fix adequately
+- [ ] **Fail (1)**: No test written when one was clearly needed and feasible
+- [ ] **N/A**: Test not applicable or impossible to write (skip this metric)
+
+### Code Quality & Style (Weight: 10%)
+- [ ] **Excellent (5)**: Perfect adherence to PerfView coding standards; clear, idiomatic code
+- [ ] **Good (4)**: Follows standards with minor deviations
+- [ ] **Fair (3)**: Some style issues or inconsistencies
+- [ ] **Poor (2)**: Significant style problems
+- [ ] **Fail (1)**: Completely ignores coding standards
+
+### Communication (Weight: 10%)
+- [ ] **Excellent (5)**: Clear, concise PR description; thorough issue comments; excellent documentation
+- [ ] **Good (4)**: Good communication with minor gaps
+- [ ] **Fair (3)**: Basic communication but lacks clarity or detail
+- [ ] **Poor (2)**: Unclear or confusing communication
+- [ ] **Fail (1)**: Little to no communication
+
+### Process Adherence (Weight: 10%)
+- [ ] **Excellent (5)**: Followed all workflow phases systematically; proper testing; built in Debug with asserts
+- [ ] **Good (4)**: Followed most of the workflow appropriately
+- [ ] **Fair (3)**: Skipped some important steps
+- [ ] **Poor (2)**: Poor adherence to workflow
+- [ ] **Fail (1)**: Completely ignored the workflow
+
+### Self-Improvement Process
+
+After scoring myself on the rubric:
+
+1. **Calculate weighted score**:
+ - Multiply each score by its weight
+ - Sum only applicable metrics
+ - Divide by sum of applicable weights
+ - Result is 1.0 to 5.0 scale
+
+2. **If score < 4.0**: I identify the weakest areas and take corrective action:
+ - **Information Gathering**: Re-read issue, search for related issues/docs
+ - **Reproduction**: Try different approaches, seek more information
+ - **Git Bisect**: Review git bisect documentation, try more carefully
+ - **Fix Quality**: Refactor to be simpler, check for edge cases
+ - **Test Coverage**: Add more test cases, test edge cases
+ - **Code Quality**: Review coding standards, refactor code
+ - **Communication**: Rewrite descriptions more clearly
+ - **Process**: Go back and complete skipped steps
+
+3. **Iterate**: After improvements, re-score myself. Continue until score β₯ 4.0 or I've exhausted reasonable improvement options
+
+4. **If cannot achieve 4.0**: Document why in my internal notes (not in PR), and consider whether the PR should be submitted at all
+
+## Key Principles
+
+- **Quality over speed**: Better to take time and do it right than rush and create problems
+- **Simplicity**: Prefer simple, obvious fixes over clever ones
+- **Consistency**: Follow existing patterns in the codebase
+- **Testing**: Always verify fixes work and don't break other things
+- **Communication**: Keep stakeholders informed throughout the process
+- **Humility**: Ask for help when stuck; admit when I can't reproduce something
+
+## Repository-Specific Knowledge
+
+### Build System
+- **Full solution** (Windows only): `PerfView.sln` - requires Visual Studio 2026 with the repository `.vsconfig` components installed. Native ETWClrProfiler projects use the latest installed MSVC v145 toolset.
+- **Individual projects** (Linux compatible via dotnet CLI):
+ - TraceEvent: `dotnet build src/TraceEvent/TraceEvent.csproj`
+ - FastSerialization: `dotnet build src/FastSerialization/FastSerialization.csproj`
+ - MemoryGraph: `dotnet build src/MemoryGraph/MemoryGraph.csproj`
+ - Build command: `dotnet build -c Debug` (or `-c Release`)
+- **Windows-only components**: PerfView GUI, EtwClrProfiler (C++), HeapDump (native)
+- Output (Windows): `src/PerfView/bin/{Configuration}/PerfView.exe`
+
+### Test Projects
+- β
`TraceEvent.Tests` - TraceEvent library tests (Linux-compatible)
+ - Run: `dotnet test src/TraceEvent/TraceEvent.Tests/TraceEvent.Tests.csproj`
+- β
`FastSerialization.Tests` - Serialization tests (Linux-compatible)
+ - Run: `dotnet test src/FastSerialization.Tests/FastSerialization.Tests.csproj`
+- β
`LinuxTracing.Tests` - Linux-specific tests (Linux-compatible)
+ - Run: `dotnet test src/LinuxTracing.Tests/LinuxTracing.Tests.csproj`
+- β `PerfView.Tests` - Main PerfView GUI tests (Windows-only WPF)
+- β `SymbolsAuth.Tests` - Symbol authentication tests (may require Windows)
+- Should complete in ~1 minute total
+
+### Code Organization
+- `src/PerfView/` - Main WPF GUI application
+- `src/TraceEvent/` - TraceEvent library (ETW and EventPipe parsing)
+- `src/MemoryGraph/` - Memory dump analysis
+- `src/FastSerialization/` - Binary serialization
+- `src/Utilities/` - Shared utilities
+- `src/HeapDump*/` - Heap dump functionality
+
+### Important Files
+- User documentation: `src/PerfView/SupportFiles/UsersGuide.htm`
+- TraceEvent docs: `documentation/TraceEvent/TraceEventProgrammersGuide.md`
+- Coding standards: `documentation/CodingStandards.md`
+- Contributing guide: `CONTRIBUTING.md`
+
+### Common Issue Categories
+1. **EventPipe parsing issues** - .nettrace files, TraceEvent library (β
I can test)
+2. **ETW parsing issues** - .etl files, TraceEvent library (β Windows-only, code analysis only)
+3. **PerfView GUI bugs** - WPF-related, in PerfView project (β Windows-only, code analysis only)
+4. **Symbol resolution** - Symbol loading and caching (β οΈ Limited capability)
+5. **Memory analysis** - Heap dump processing (β
MemoryGraph library testable)
+6. **Linux support** - Cross-platform tracing issues (β
I can test)
+7. **Performance regressions** - Often need git bisect (β
If component is buildable)
+
+## Example Interactions
+
+**Example 1: EventPipe Issue (I can fully handle)**
+
+**Issue Report**: "TraceEvent throws NullReferenceException when parsing .nettrace files with missing metadata"
+
+**My Response Process**:
+1. β
Gather info: Clear repro steps, .nettrace file attached, stack trace provided, affects TraceEvent library
+2. β
Assess: Sufficient information, component is buildable on Linux, .nettrace files work on Linux
+3. β
Build: `dotnet build src/TraceEvent/TraceEvent.csproj -c Debug`
+4. β
Reproduce: Used provided .nettrace file, confirmed NullReferenceException
+5. β
Git bisect: Found commit abc123 that introduced the regression
+6. β
Analyze: Missing null check when metadata is absent
+7. β
Fix: Added null check with appropriate fallback behavior
+8. β
Test: `dotnet test src/TraceEvent/TraceEvent.Tests/` - all pass
+9. β
Regression test: Added EventPipeMetadataTests.cs with missing metadata scenario
+10. β
PR: Created PR #XXXX with full details and test results
+11. β
Self-assessment: Scored 4.8/5.0 - ready to submit
+
+**Example 2: PerfView GUI Issue (Limited capability)**
+
+**Issue Report**: "PerfView crashes when clicking 'Memory' menu after loading ETL file"
+
+**My Response Process**:
+1. β
Gather info: UI crash, .etl file involved, stack trace points to WPF event handler in PerfView.exe
+2. β οΈ Assess: Sufficient information but PerfView GUI is Windows-only, cannot build/test, .etl files are Windows-only
+3. β οΈ Build: Skipped - WPF application requires Windows
+4. π Code Analysis: Reviewed PerfView event handler code, found event data not validated before use
+5. β οΈ Git bisect: Used manual analysis of commits, likely commit def456 based on timing
+6. β
Analyze: Event handler assumes data is always present, but can be null
+7. β
Fix: Added null check in event handler before accessing data properties
+8. β οΈ Test: Cannot test PerfView GUI on Linux, cannot parse .etl files
+9. β οΈ Regression test: Wrote test skeleton in PerfView.Tests but cannot run
+10. β
PR: Created PR #XXXX with "Testing Limitations" section:
+ - "Fix developed through code analysis on Linux"
+ - "Testing required: Manual verification on Windows with .etl file and repro steps"
+ - "Confidence: High - straightforward null check, follows existing patterns"
+11. β
Self-assessment: Scored 3.8/5.0 initially, improved documentation to 4.1/5.0 - submitted with testing disclaimer
+
+**Example 3: ETW Issue with .etl file (Partial capability)**
+
+**Issue Report**: "TraceEvent fails to parse GC events in .etl files collected from Server GC applications"
+
+**My Response Process**:
+1. β
Gather info: ETW parsing issue, .etl file attached, affects TraceEvent ETW code path
+2. β οΈ Assess: TraceEvent is buildable but .etl files only work on Windows
+3. π Code Analysis: Reviewed ETW GC event parsing code, can analyze logic without running
+4. β
Analyze: Found assumption that GC heap count is always 1, breaks with Server GC
+5. β
Fix: Updated parsing logic to handle multiple GC heaps
+6. β οΈ Test: Cannot test with .etl file on Linux
+7. β
PR: Created PR #XXXX with "Testing Limitations" section:
+ - "Fix developed through code analysis on Linux"
+ - "Testing required: Validation on Windows with attached .etl file from issue"
+ - "Confidence: High - logic error identified through code review, fix follows existing multi-heap patterns"
+8. β
Self-assessment: Scored 3.7/5.0 - submitted with clear testing requirements for maintainers
+
+**Issue Report**: "TraceEvent stopped working in latest version"
+
+**My Response Process**:
+1. β
Gather info: Vague report, needs more details
+2. β οΈ Assess: Insufficient information
+3. β Reproduce: Cannot attempt without more details
+4. β
Comment: "Could you please provide: (1) Exact version of TraceEvent, (2) Code sample showing the issue, (3) Error messages or unexpected behavior, (4) Last version that worked correctly?"
+5. βΈοΈ Wait for response before proceeding
+
+---
+
+**Note**: This rubric is for my internal self-assessment only. I use it to ensure high-quality work before submitting PRs. The rubric scores and self-improvement process are not included in PR descriptions or issue comments.
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 000000000..d8c8d4310
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,70 @@
+# Copilot Instructions for PerfView
+
+## Project Overview
+
+PerfView is a Windows performance-analysis tool for investigating CPU and memory issues, built on the TraceEvent library for parsing ETW and EventPipe trace data. The solution (`PerfView.sln`) contains the WPF GUI application, the cross-platform TraceEvent library, and several supporting libraries.
+
+## Architecture
+
+- `src/PerfView/` β WPF GUI application (Windows-only, .NET Framework 4.7.2+, C# 7.3 features only)
+- `src/TraceEvent/` β Core trace parsing library (cross-platform, targets netstandard2.0)
+- `src/FastSerialization/` β Lightweight binary serialization library
+- `src/MemoryGraph/` β Memory dump analysis (graph-based heap representation)
+- `src/Utilities/` β Shared utility code
+- `src/HeapDump*/` β Heap dump capture using ClrMD (Windows-only native interop)
+- `src/EtwClrProfiler/` β Native C++ CLR profiler emitting ETW events (Windows-only)
+- `src/PerfViewExtensions/` β Extensibility mechanism ("Global" project)
+
+## Build & Test
+
+- **Full solution (Windows):** `build.cmd` or open `PerfView.sln` in Visual Studio 2026 with the repository `.vsconfig` components installed. Native ETWClrProfiler projects use the latest installed MSVC v145 toolset.
+- **Individual projects (cross-platform via dotnet CLI):**
+ - `dotnet build src/TraceEvent/TraceEvent.csproj -c Debug`
+ - `dotnet build src/FastSerialization/FastSerialization.csproj -c Debug`
+ - `dotnet build src/MemoryGraph/MemoryGraph.csproj -c Debug`
+- **Running tests:** `dotnet test .csproj -c Debug` β always use Debug configuration so assertions are active.
+ - `src/TraceEvent/TraceEvent.Tests/TraceEvent.Tests.csproj`
+ - `src/FastSerialization.Tests/FastSerialization.Tests.csproj`
+ - `src/LinuxTracing.Tests/LinuxTracing.Tests.csproj`
+ - `src/PerfView.Tests/PerfView.Tests.csproj` (Windows-only)
+ - `src/SymbolsAuth.Tests/SymbolsAuth.Tests.csproj`
+- Tests use **xUnit**. The full test suite should complete in under 1 minute.
+- NuGet uses central package management (`src/Directory.Packages.props`). Use the repo-local `Nuget.config` when restoring: `dotnet restore --configfile Nuget.config`.
+
+## C# Coding Conventions
+
+Follow existing patterns β when in doubt, match the surrounding code.
+
+### Naming
+- Standard .NET conventions: `PascalCase` for types, methods, and properties; `camelCase` for parameters and locals.
+- Private instance fields: prefix with `m_` (e.g., `m_nodeCount`). The `_` prefix is also acceptable.
+- Static fields: prefix with `s_` (e.g., `s_defaultSize`).
+- No Hungarian notation.
+
+### Class Layout
+- Order members for readability as a **public contract**: constructors/factories first, then properties, then methods.
+- All private members go **after** all public members, wrapped in `#region private` so Visual Studio outlining (Ctrl-M Ctrl-O) collapses them.
+- Fields go **together at the end** of the private region β this makes it easy to see all object state at a glance.
+
+### Comments & Documentation
+- This codebase is **heavily commented** β maintain that standard.
+- Public types and public members exposed outside their assembly **must** have XML doc comments (`/// `). Parameter-level docs are optional if names are descriptive.
+- Private fields often need comments, especially to document invariants they maintain.
+- Use inline comments to explain non-obvious logic and design decisions.
+
+### Error Handling & Assertions
+- Use `Debug.Assert()` liberally to validate internal invariants β this is why tests must run in Debug configuration.
+- Throw specific exceptions with descriptive messages for public API misuse.
+
+### Other Patterns
+- **Type aliases** for semantic clarity: `using Address = System.UInt64;`
+- **Lazy initialization** for expensive sub-objects (check for null, create on first access).
+- **Reuse event objects** in TraceEvent callbacks to minimize GC pressure.
+
+## Making Changes
+
+- Keep changes **minimal and focused** β complexity is the enemy (see `CONTRIBUTING.md`).
+- Prefer **simplicity over cleverness**. Performance optimizations that add complexity need measurements to justify them.
+- Run tests in **Debug** configuration before submitting changes.
+- PerfView embeds its support DLLs into the EXE at build time, creating non-obvious build dependencies. If you see "DLL not found" errors, a normal (non-clean) rebuild usually fixes it.
+- The `Global` project depends on PerfView β expect unresolved references there until PerfView builds first.
diff --git a/.pipelines/mirror.yml b/.pipelines/mirror.yml
index 2d8b3cd0b..06e078856 100644
--- a/.pipelines/mirror.yml
+++ b/.pipelines/mirror.yml
@@ -1,9 +1,6 @@
trigger:
- main
-variables:
- - group: 'DotNet-VSTS-Infra-Access'
-
resources:
repositories:
- repository: 1esPipelines
@@ -29,6 +26,19 @@ extends:
jobs:
- job: Mirror
steps:
+ - task: AzureCLI@2
+ displayName: 'Mint AzDO token via WIF'
+ inputs:
+ azureSubscription: 'DncEng Insertion: Roslyn and Razor'
+ scriptType: 'pscore'
+ scriptLocation: 'inlineScript'
+ inlineScript: |
+ $token = az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv
+ if ($LASTEXITCODE -ne 0) { Write-Error "Failed to get access token"; exit 1 }
+ $token = $token.Trim()
+ if ([string]::IsNullOrWhiteSpace($token)) { Write-Error "Token is empty"; exit 1 }
+ Write-Host "##vso[task.setvariable variable=AzdoToken;issecret=true]$token"
+
- task: PowerShell@1
displayName: 'Set SourceBranch Variable'
inputs:
@@ -108,18 +118,18 @@ extends:
- task: CmdLine@2
displayName: 'Pull AzDO SourceBranch'
inputs:
- script: 'git pull --strategy=recursive --strategy-option no-renames https://dn-bot:$(dn-bot-devdiv-build-rw-code-rw)@devdiv.visualstudio.com/DevDiv/_git/perfView $(SourceBranch)'
+ script: 'git pull --strategy=recursive --strategy-option no-renames https://x-access-token:$(AzdoToken)@devdiv.visualstudio.com/DevDiv/_git/perfView $(SourceBranch)'
- task: CmdLine@2
displayName: 'Run git push'
inputs:
- script: 'git push https://dn-bot:$(dn-bot-devdiv-build-rw-code-rw)@devdiv.visualstudio.com/DevDiv/_git/perfView $(MirrorBranch)'
+ script: 'git push https://x-access-token:$(AzdoToken)@devdiv.visualstudio.com/DevDiv/_git/perfView $(MirrorBranch)'
- task: PowerShell@1
displayName: 'Create Pull Request'
inputs:
scriptType: inlineScript
- arguments: '$(dn-bot-devdiv-build-rw-code-rw)'
+ arguments: '$(AzdoToken)'
inlineScript: |
param(
[string]$AccessToken
diff --git a/.pipelines/perfcollect-job.yml b/.pipelines/perfcollect-job.yml
index 185e2ead5..4187765df 100644
--- a/.pipelines/perfcollect-job.yml
+++ b/.pipelines/perfcollect-job.yml
@@ -1,6 +1,9 @@
steps:
-- task: DockerInstaller@0
- displayName: 'Install Docker'
+- task: Bash@3
+ displayName: 'Get Docker Version'
+ inputs:
+ targetType: inline
+ script: 'docker --version'
- task: Bash@3
displayName: 'Build Containers'
diff --git a/.pipelines/perfview-job.yml b/.pipelines/perfview-job.yml
index 33a9061a4..69648ea88 100644
--- a/.pipelines/perfview-job.yml
+++ b/.pipelines/perfview-job.yml
@@ -16,6 +16,13 @@ steps:
msbuildArguments: '/restore'
configuration: ${{ parameters.flavor }}
+- task: VisualStudioTestPlatformInstaller@1
+ displayName: 'Install VS Test'
+ inputs:
+ versionSelector: 'latestStable'
+ packageFeedSelector: 'customFeed'
+ customFeed: 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json'
+
- task: VSTest@2
displayName: 'Run Tests'
inputs:
@@ -25,7 +32,9 @@ steps:
**\bin\**\SymbolsAuthTests.dll
**\bin\**\TraceEventTests.dll
**\bin\**\PerfViewTests.dll
+ **\bin\**\TraceParserGen.Tests.dll
testRunTitle: 'PerfView - ${{ parameters.flavor }}'
+ vsTestVersion: 'toolsInstaller'
runTestsInIsolation: true
otherConsoleOptions: '--blame'
diff --git a/Nuget.config b/Nuget.config
index e29c374dd..8123774b4 100644
--- a/Nuget.config
+++ b/Nuget.config
@@ -8,6 +8,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/PerfView.sln b/PerfView.sln
index 033049177..2007aade6 100644
--- a/PerfView.sln
+++ b/PerfView.sln
@@ -1,6 +1,7 @@
-ο»ΏMicrosoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.4.32821.20
+ο»Ώ
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 18
+VisualStudioVersion = 18.6.11620.235 main
MinimumVisualStudioVersion = 15.0
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PerfView", "src\PerfView\PerfView.csproj", "{6BAC7496-6953-41B8-9042-AAE45405A095}"
ProjectSection(ProjectDependencies) = postProject
@@ -38,6 +39,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
CONTRIBUTING.md = CONTRIBUTING.md
src\Directory.Build.props = src\Directory.Build.props
src\Directory.Build.targets = src\Directory.Build.targets
+ src\Directory.Packages.props = src\Directory.Packages.props
src\PerfViewCollect\PerfViewCollect.csproj = src\PerfViewCollect\PerfViewCollect.csproj
README.md = README.md
EndProjectSection
@@ -81,10 +83,22 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MemoryGraph", "src\MemoryGr
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EtwHeapDump", "src\EtwHeapDump\EtwHeapDump.csproj", "{F266BD46-34EE-4EEB-8B21-6059F4DFC51F}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1CAEF854-2923-45FA-ACB8-6523A7E45896}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FastSerialization.Tests", "src\FastSerialization.Tests\FastSerialization.Tests.csproj", "{2EC430A3-1B65-4628-B2F2-8DBEB4C03132}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PerfView.Tutorial", "src\PerfView.Tutorial\PerfView.Tutorial.csproj", "{DE35BED9-0E03-4DAC-A003-1ACBBF816973}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TraceParserGen.Tests", "src\TraceParserGen.Tests\TraceParserGen.Tests.csproj", "{F127C664-2F56-429B-BAA6-636034F766EF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TraceEvent.Benchmarks", "src\TraceEvent\TraceEvent.Benchmarks\TraceEvent.Benchmarks.csproj", "{F2FBDEF0-4C45-44B2-8B92-9C5763BD2E69}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TraceEvent", "TraceEvent", "{F2AE6042-2485-6774-F42B-98E120E28306}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TraceEvent.Tests", "TraceEvent.Tests", "{282B72FF-D1CF-1C67-705A-D384E1729A5D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmbeddedPdbTestApp", "src\TraceEvent\TraceEvent.Tests\EmbeddedPdbTestApp\EmbeddedPdbTestApp.csproj", "{1CE6D3C2-5E27-4296-89FD-5177387F0B15}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -391,6 +405,18 @@ Global
{F266BD46-34EE-4EEB-8B21-6059F4DFC51F}.Release|x64.Build.0 = Release|Any CPU
{F266BD46-34EE-4EEB-8B21-6059F4DFC51F}.Release|x86.ActiveCfg = Release|Any CPU
{F266BD46-34EE-4EEB-8B21-6059F4DFC51F}.Release|x86.Build.0 = Release|Any CPU
+ {2EC430A3-1B65-4628-B2F2-8DBEB4C03132}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2EC430A3-1B65-4628-B2F2-8DBEB4C03132}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2EC430A3-1B65-4628-B2F2-8DBEB4C03132}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2EC430A3-1B65-4628-B2F2-8DBEB4C03132}.Debug|x64.Build.0 = Debug|Any CPU
+ {2EC430A3-1B65-4628-B2F2-8DBEB4C03132}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2EC430A3-1B65-4628-B2F2-8DBEB4C03132}.Debug|x86.Build.0 = Debug|Any CPU
+ {2EC430A3-1B65-4628-B2F2-8DBEB4C03132}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2EC430A3-1B65-4628-B2F2-8DBEB4C03132}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2EC430A3-1B65-4628-B2F2-8DBEB4C03132}.Release|x64.ActiveCfg = Release|Any CPU
+ {2EC430A3-1B65-4628-B2F2-8DBEB4C03132}.Release|x64.Build.0 = Release|Any CPU
+ {2EC430A3-1B65-4628-B2F2-8DBEB4C03132}.Release|x86.ActiveCfg = Release|Any CPU
+ {2EC430A3-1B65-4628-B2F2-8DBEB4C03132}.Release|x86.Build.0 = Release|Any CPU
{DE35BED9-0E03-4DAC-A003-1ACBBF816973}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DE35BED9-0E03-4DAC-A003-1ACBBF816973}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DE35BED9-0E03-4DAC-A003-1ACBBF816973}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -403,17 +429,55 @@ Global
{DE35BED9-0E03-4DAC-A003-1ACBBF816973}.Release|x64.Build.0 = Release|Any CPU
{DE35BED9-0E03-4DAC-A003-1ACBBF816973}.Release|x86.ActiveCfg = Release|Any CPU
{DE35BED9-0E03-4DAC-A003-1ACBBF816973}.Release|x86.Build.0 = Release|Any CPU
+ {F127C664-2F56-429B-BAA6-636034F766EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F127C664-2F56-429B-BAA6-636034F766EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F127C664-2F56-429B-BAA6-636034F766EF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F127C664-2F56-429B-BAA6-636034F766EF}.Debug|x64.Build.0 = Debug|Any CPU
+ {F127C664-2F56-429B-BAA6-636034F766EF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F127C664-2F56-429B-BAA6-636034F766EF}.Debug|x86.Build.0 = Debug|Any CPU
+ {F127C664-2F56-429B-BAA6-636034F766EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F127C664-2F56-429B-BAA6-636034F766EF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F127C664-2F56-429B-BAA6-636034F766EF}.Release|x64.ActiveCfg = Release|Any CPU
+ {F127C664-2F56-429B-BAA6-636034F766EF}.Release|x64.Build.0 = Release|Any CPU
+ {F127C664-2F56-429B-BAA6-636034F766EF}.Release|x86.ActiveCfg = Release|Any CPU
+ {F127C664-2F56-429B-BAA6-636034F766EF}.Release|x86.Build.0 = Release|Any CPU
+ {F2FBDEF0-4C45-44B2-8B92-9C5763BD2E69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F2FBDEF0-4C45-44B2-8B92-9C5763BD2E69}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F2FBDEF0-4C45-44B2-8B92-9C5763BD2E69}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F2FBDEF0-4C45-44B2-8B92-9C5763BD2E69}.Debug|x64.Build.0 = Debug|Any CPU
+ {F2FBDEF0-4C45-44B2-8B92-9C5763BD2E69}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F2FBDEF0-4C45-44B2-8B92-9C5763BD2E69}.Debug|x86.Build.0 = Debug|Any CPU
+ {F2FBDEF0-4C45-44B2-8B92-9C5763BD2E69}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F2FBDEF0-4C45-44B2-8B92-9C5763BD2E69}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F2FBDEF0-4C45-44B2-8B92-9C5763BD2E69}.Release|x64.ActiveCfg = Release|Any CPU
+ {F2FBDEF0-4C45-44B2-8B92-9C5763BD2E69}.Release|x64.Build.0 = Release|Any CPU
+ {F2FBDEF0-4C45-44B2-8B92-9C5763BD2E69}.Release|x86.ActiveCfg = Release|Any CPU
+ {F2FBDEF0-4C45-44B2-8B92-9C5763BD2E69}.Release|x86.Build.0 = Release|Any CPU
+ {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Debug|x64.Build.0 = Debug|Any CPU
+ {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Debug|x86.Build.0 = Debug|Any CPU
+ {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Release|x64.ActiveCfg = Release|Any CPU
+ {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Release|x64.Build.0 = Release|Any CPU
+ {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Release|x86.ActiveCfg = Release|Any CPU
+ {1CE6D3C2-5E27-4296-89FD-5177387F0B15}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {F2AE6042-2485-6774-F42B-98E120E28306} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {282B72FF-D1CF-1C67-705A-D384E1729A5D} = {F2AE6042-2485-6774-F42B-98E120E28306}
+ {1CE6D3C2-5E27-4296-89FD-5177387F0B15} = {282B72FF-D1CF-1C67-705A-D384E1729A5D}
+ EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9F85A2A3-E0DF-4826-9BBA-4DFFA0F17150}
EndGlobalSection
GlobalSection(TestCaseManagementSettings) = postSolution
CategoryFile = PerfView2.vsmdi
EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {DE35BED9-0E03-4DAC-A003-1ACBBF816973} = {1CAEF854-2923-45FA-ACB8-6523A7E45896}
- EndGlobalSection
EndGlobal
diff --git a/README.md b/README.md
index dedf89e07..c7aa96e40 100644
--- a/README.md
+++ b/README.md
@@ -54,36 +54,38 @@ you can do that by following the rest of these instructions.
### Tools Needed to Build PerfView
-The only tool you need to build PerfView is Visual Studio 2022. The [Visual Studio 2022 Community Edition](https://www.visualstudio.com/vs/community/)
+The only tool you need to build PerfView is Visual Studio 2026. The [Visual Studio Community Edition](https://www.visualstudio.com/vs/community/)
can be downloaded *for free* and has everything you need to fetch PerfView from GitHub, build and test it. We expect you
-to download Visual Studio 2022 Community Edition if you don't already have Visual Studio 2022.
+to download Visual Studio Community Edition if you don't already have Visual Studio 2026.
In your installation of Visual Studio, you need to ensure you have the following workloads and components installed:
* **.NET desktop development** workload with all default components.
* **Desktop development with C++** workload with all default components plus the latest Windows 10 SDK.
* The Windows 10 SDK is not enabled by default in this workload, so you will need to check the box for it to be installed.
- * **MSVC v143 - VS 2022 C++ x64/x86 Spectre-mitigated libs (Latest)** component.
+ * **MSVC v145 - VS 2026 C++ x64/x86 build tools (Latest)** component.
+ * **MSVC v145 - VS 2026 C++ x64/x86 Spectre-mitigated libs (Latest)** component.
* This can be found under the 'Individual Components' tab.
A `.vsconfig` file is included in the root of the repository that can be used to install the necessary components. When
opening the solution in Visual Studio, it will prompt you to install any components that it thinks are missing from your
-installation. Alternatively, you can [import the `.vsconfig` in the Visual Studio Installer](https://learn.microsoft.com/en-us/visualstudio/install/import-export-installation-configurations?view=vs-2022#import-a-configuration).
+installation. Alternatively, you can [import the `.vsconfig` in the Visual Studio Installer](https://learn.microsoft.com/en-us/visualstudio/install/import-export-installation-configurations#import-a-configuration).
+PerfView intentionally uses the latest installed MSVC v145 toolset from Visual Studio 2026.
-If you get any errors compiling the ETWClrCompiler projects, it is likely because you either don't have the Windows 10 SDK
+If you get any errors compiling the ETWClrProfiler projects, it is likely because you either don't have the Windows 10 SDK
installed, or you don't have the spectre-mitigated libs installed. Please refer to the [troubleshooting section](#information-for-build-troubleshooting) for more information.
### Cloning the PerfView GitHub Repository.
The first step in getting started with the PerfView source code is to clone the PerfView GitHub repository.
-If you are already familiar with how GIT, GitHub, and Visual Studio 2022 GIT support works, then you can skip this section.
-However, if not, the [Setting up a Local GitHub repository with Visual Studio 2022](documentation/SettingUpRepoInVS.md) document
-will lead you through the basics of doing this. All it assumes is that you have Visual Studio 2022 installed.
+If you are already familiar with how GIT, GitHub, and Visual Studio 2026 GIT support works, then you can skip this section.
+However, if not, the [Setting up a Local GitHub repository with Visual Studio 2026](documentation/SettingUpRepoInVS.md) document
+will lead you through the basics of doing this. All it assumes is that you have Visual Studio 2026 installed.
### How to Build and Debug PerfView
-PerfView is developed in Visual Studio 2022 using features through C# 7.3.
+PerfView is developed in Visual Studio 2026 using features through C# 7.3.
* The solution file is PerfView.sln. Opening this file in Visual Studio (or double clicking on it in
the Windows Explorer) and selecting Build -> Build Solution, will build it. You can also build the
@@ -117,16 +119,16 @@ among other things a PerfView.exe. This one file is all you need to deploy.
explicit 'scope') and needs to refer to PerfView to resolve some of its references. Thus you will get many 'not found'
issues in the 'Global' project. These can be ignored until you get every other part of the build working.
- * One of the invariants of the repo is that if you are running Visual Studio 2022 and you simply sync and build the
+ * One of the invariants of the repo is that if you are running Visual Studio 2026 and you simply sync and build the
PerfView.sln file, it is supposed to 'just work'. If that does not happen, and the advice above does not help, then
we need to either fix the repo or update the advice above. Thus it is reasonable to open a GitHub issue. If you
do this, the goal is to fix the problem, which means you have to put enough information into the issue to do that.
This includes exactly what you tried, and what the error messages were.
- * You can also build PerfView from the command line (but you still need Visual Studio 2022 installed). It is a two step process.
+ * You can also build PerfView from the command line (but you still need Visual Studio 2026 installed). It is a two step process.
First you must restore all the needed nuget packages, then you do the build itself. To do this:
1. Open a developer command prompt. You can do this by hitting the windows key (by the space bar) and type
- 'Developer command prompt'. You should see a entry for this that you can select (if Visual Studio 2022 is installed).
+ 'Developer command prompt'. You should see a entry for this that you can select (if Visual Studio 2026 is installed).
2. Change directory to the base of your PerfView source tree (where PerfView.sln lives).
3. Restore the nuget packages by typing the command 'msbuild /t:restore'
4. Build perfView by typing the command 'msbuild'
@@ -135,8 +137,10 @@ among other things a PerfView.exe. This one file is all you need to deploy.
frankly any error associated with building the ETWClrProfiler dlls, you should make sure that you have the Windows 10 SDK installed. Unfortunately this library tends not to be
installed with Visual Studio anymore unless you ask for it explicitly. To fix it launch the Visual Studio Installer, modify the installation, and then look under the C++ Desktop Development and check that the Windows SDK 10.0.17763.0 option is selected. If not, select it and continue. Then try building PerfView again.
- * If you get an error "MSB8040: Spectre-mitigated libraries are required for this project", modify your Visual Studio
- installation to ensure that you have the 'MSVC v143 - VS 2022 C++ x64/x86 Spectre-mitigated libs (Latest)' component installed.
+ * If you get an error "MSB8040: Spectre-mitigated libraries are required for this project", modify your Visual Studio
+ installation to ensure that you have the 'MSVC v145 - VS 2026 C++ x64/x86 Spectre-mitigated libs (Latest)' component installed.
+
+ * If you get an error that the v145 platform toolset is not found, import the repository `.vsconfig` file in the Visual Studio Installer or install the Visual Studio 2026 C++ desktop workload.
### Running Tests
diff --git a/documentation/SettingUpRepoInVS.md b/documentation/SettingUpRepoInVS.md
index 3a8cd6886..780624d2d 100644
--- a/documentation/SettingUpRepoInVS.md
+++ b/documentation/SettingUpRepoInVS.md
@@ -1,13 +1,13 @@
-# GitHub Repository Setup with Visual Studio 2019
+# GitHub Repository Setup with Visual Studio 2026
This section tells you how to build a project that is already hosted on GitHub,
-using Visual Studio 2019. If you don't already have Visual Studio 2019, you
+using Visual Studio 2026. If you don't already have Visual Studio 2026, you
can get the community edition for free from
[this link](https://www.visualstudio.com/vs/community/).
This section also goes through important routine tasks like getting the latest
changes from GitHub and submitting a pull request to the main branch.
-It will show you how to do this using just Visual Studio 2019. Older versions
+It will show you how to do this using just Visual Studio 2026. Older versions
of Visual Studio as well as other IDEs are possible, but not covered here.
You can also use 'raw' git commands but I don't cover that here.
diff --git a/documentation/SimpleGitWorkflow.md b/documentation/SimpleGitWorkflow.md
index 5fe81eb85..504cef007 100644
--- a/documentation/SimpleGitWorkflow.md
+++ b/documentation/SimpleGitWorkflow.md
@@ -1,9 +1,9 @@
-# Setting Up *Without* a Fork with Visual Studio 2019
+# Setting Up *Without* a Fork with Visual Studio 2026
* See also [Setting up a Repo in VS](SettingUpRepoInVS.md) for important background material.
* See also [Open Source GitHub Setup and Workflow](OpenSourceGitWorkflow.md) for the setup needed for pull requests.
-Here we describe how to use Visual Studio 2019 to set a local build of the GitHub project https://github.com/Microsoft/perfview.
+Here we describe how to use Visual Studio 2026 to set a local build of the GitHub project https://github.com/Microsoft/perfview.
Because this project is open source, anyone can read it so this works even if you don't have a GitHub account, but
of course you will only have read-only access.
@@ -147,4 +147,3 @@ At this point we have described the critical workflows
-
diff --git a/es-metadata.yml b/es-metadata.yml
new file mode 100644
index 000000000..c218f79cb
--- /dev/null
+++ b/es-metadata.yml
@@ -0,0 +1,8 @@
+schemaVersion: 0.0.1
+isProduction: true
+accountableOwners:
+ service: 2756eb21-22a5-49ff-8b78-c9ce773ae80b
+routing:
+ defaultAreaPath:
+ org: nettel
+ path: PerfView
diff --git a/global.json b/global.json
deleted file mode 100644
index 2120c2f14..000000000
--- a/global.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "msbuild-sdks": {
- "MSBuild.Sdk.Extras": "1.6.65"
- }
-}
diff --git a/src/CSVReader/CSVReader.csproj b/src/CSVReader/CSVReader.csproj
index 0bed9c00d..8353fc768 100644
--- a/src/CSVReader/CSVReader.csproj
+++ b/src/CSVReader/CSVReader.csproj
@@ -23,7 +23,7 @@
Microsoft400
StrongName
-
+
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index e42873955..4935bfd60 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -6,7 +6,7 @@
- 8.0
+ 12.0
strict
@@ -19,7 +19,7 @@
- 3.1.22
+ 3.2.5
$(ReleaseVersion)
$(ReleaseVersion)
$(ReleaseVersion)
@@ -29,47 +29,9 @@
$(ReleaseVersion)
-
+
- 1.0.8
- 1.0.29
- 0.1.2
-
-
-
-
- 1.38.0
- 1.11.4
- 0.3.1
- 0.2.510501
- 4.61.3
- 4.61.3
- 7.1.2
- 7.1.2
- 7.1.2
- 8.0.0
- 5.0.0
- 1.0.2792.45
- 4.5.1
- 8.0.0
- 8.0.0
- 4.5.5
- 4.5.0
- 8.0.0
- 4.7.0
- 6.0.0
-
- 4.3.1
- 4.7.0
- 8.0.0
- 8.0.5
- 4.5.4
-
-
-
-
- 4.0.0-beta.24314.3
-
+
-
-
- 17.8.14
- 2.6.5
- 2.6.5
- 2.6.5
- 2.5.6
- 0.3.2
-
-
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/EtwClrProfiler/COMInfrastructure.cpp b/src/EtwClrProfiler/COMInfrastructure.cpp
index 2a63c36a3..be880b03c 100644
--- a/src/EtwClrProfiler/COMInfrastructure.cpp
+++ b/src/EtwClrProfiler/COMInfrastructure.cpp
@@ -12,7 +12,7 @@ class CClassFactory : public IClassFactory
ULONG __stdcall AddRef( ) { return InterlockedIncrement(&m_refCount); }
ULONG __stdcall Release( ) { auto ret = InterlockedDecrement (&m_refCount); if (ret <= 0) delete(this); return ret; }
HRESULT __stdcall QueryInterface (REFIID riid,void ** ppInterface );
- HRESULT __stdcall LockServer(BOOL bLock) { return S_OK; }
+ HRESULT __stdcall LockServer(BOOL) { return S_OK; }
HRESULT __stdcall CreateInstance(IUnknown * pUnkOuter, REFIID riid, void** ppInterface);
private:
long m_refCount ;
@@ -31,7 +31,7 @@ int main()
BOOL WINAPI DllMain(
HINSTANCE hInstance ,
DWORD dwReason ,
- LPVOID lpReserved )
+ LPVOID )
{
switch ( dwReason )
{
diff --git a/src/EtwClrProfiler/CorProfilerTracer.cpp b/src/EtwClrProfiler/CorProfilerTracer.cpp
index 65083a4cd..5dd8eb04e 100644
--- a/src/EtwClrProfiler/CorProfilerTracer.cpp
+++ b/src/EtwClrProfiler/CorProfilerTracer.cpp
@@ -97,6 +97,9 @@ void WINAPI ProfilerControlCallback(
PEVENT_FILTER_DESCRIPTOR FilterData,
PVOID CallbackContext)
{
+ UNREFERENCED_PARAMETER(SourceId);
+ UNREFERENCED_PARAMETER(MatchAllKeywords);
+
CorProfilerTracer* profiler = (CorProfilerTracer*)CallbackContext;
LOG_TRACE(L"ProfilerControlCallback DoETWCommand IsEnabled 0x%x Level 0x%xI64 MatchAny 0x%x\n", IsEnabled, Level, MatchAnyKeywords);
profiler->DoETWCommand(IsEnabled, Level, MatchAnyKeywords, FilterData);
@@ -121,7 +124,7 @@ EXTERN_C void __stdcall EnterMethod(FunctionID functionID)
#if defined(_M_IX86)
// see http://msdn.microsoft.com/en-us/library/4ks26t93.aspx for inline assembly. Not supported on X64.
-void __declspec(naked) __stdcall EnterMethodNaked(FunctionIDOrClientID funcID)
+void __declspec(naked) __stdcall EnterMethodNaked(FunctionIDOrClientID)
{
__asm
{
@@ -142,7 +145,7 @@ void __declspec(naked) __stdcall EnterMethodNaked(FunctionIDOrClientID funcID)
}
} // EnterNaked
-void __declspec(naked) __stdcall TailcallMethodNaked(FunctionIDOrClientID funcID)
+void __declspec(naked) __stdcall TailcallMethodNaked(FunctionIDOrClientID)
{
__asm
{
@@ -166,6 +169,8 @@ HRESULT STDMETHODCALLTYPE CorProfilerTracer::InitializeForAttach(
/* [in] */ void *pvClientData,
/* [in] */ UINT cbClientData)
{
+ UNREFERENCED_PARAMETER(pvClientData);
+
HRESULT hr = S_OK;
LOG_TRACE(L"ClrProfiler Initializing\n");
CALL_N_LOGONBADHR(pICorProfilerInfoUnk->QueryInterface(__uuidof(ICorProfilerInfo3), (void **)&m_info));
@@ -223,6 +228,9 @@ HRESULT STDMETHODCALLTYPE CorProfilerTracer::InitializeForAttach(
// This routine does the work of responding to a ETW request from the controller
void CorProfilerTracer::DoETWCommand(ULONG IsEnabled, UCHAR Level, ULONGLONG MatchAnyKeywords, struct _EVENT_FILTER_DESCRIPTOR* filterData)
{
+ UNREFERENCED_PARAMETER(Level);
+ UNREFERENCED_PARAMETER(filterData);
+
LOG_TRACE(L"DoETWCommand(IsEnabled=%d, Level=%d Keywords=0x%x,%x)\n", IsEnabled, Level, (int)(MatchAnyKeywords >> 32), (int)MatchAnyKeywords);
const DWORD FLAGS_CAN_SET = (COR_PRF_MONITOR_OBJECT_ALLOCATED | COR_PRF_MONITOR_MODULE_LOADS | COR_PRF_MONITOR_GC);
@@ -460,6 +468,7 @@ void CorProfilerTracer::ForceGC()
m_forcingGC = true;
HANDLE thread = CreateThread(0, 0, ForceGCBody, this, 0, NULL);
LOG_TRACE(L"ForceGC: thread 0x%x\n", thread);
+ UNREFERENCED_PARAMETER(thread);
for (int i = 0; i < 2000; i++)
{
if (!m_forcingGC)
@@ -561,14 +570,9 @@ STDMETHODIMP CorProfilerTracer::ObjectAllocated(ObjectID objectId, ClassID class
// We want to sample at a rate that ensures less 100 allocations per second per type.
// However don't sample less than 1/1000,
- int oldSamplingRate = classInfo->SamplingRate;
classInfo->SamplingRate = min((int)(classInfo->AllocPerMSec * 10), 1000);
if (classInfo->SamplingRate == 1)
classInfo->SamplingRate = 0;
-
- // TODO This is for debugging. Can remove after we are happy with the algorithm.
- // if (classInfo->SamplingRate != oldSamplingRate)
- // EventWriteSamplingRateChangeEvent(classId, classInfo->Name, delta, minAllocPerMSec, newAllocPerMSec, classInfo->AllocPerMSec, classInfo->SamplingRate);
}
// We are done calculating the sampling rate since we are logging an event we can reset the 'Ignored' stats and log the event.
@@ -617,6 +621,8 @@ STDMETHODIMP CorProfilerTracer::GarbageCollectionFinished(void)
//==============================================================================
STDMETHODIMP CorProfilerTracer::FinalizeableObjectQueued(DWORD finalizerFlags, ObjectID objectID)
{
+ UNREFERENCED_PARAMETER(finalizerFlags);
+
LOG_TRACE(L"FinalizeableObjectQueued\n");
#ifndef PIN_INVESTIGATION
// TODO FIX NOW HACK for exchange data collection
@@ -678,7 +684,7 @@ STDMETHODIMP CorProfilerTracer::ObjectReferences(ObjectID objectId, ClassID clas
// LOG_TRACE(L"ObjectReferences\n");
// We do this for the side effect of logging the class
- ClassInfo* classInfo = GetClassInfo(classId);
+ (void)GetClassInfo(classId);
/** TODO FIX NOW
if (classInfo == NULL)
return E_FAIL;
@@ -725,11 +731,11 @@ ClassInfo* CorProfilerTracer::GetClassInfo(ClassID classId)
ClassInfo*& classInfo = m_classInfo[classId];
if (classInfo == NULL)
classInfo = new ClassInfo();
- if (classInfo->ID == -1) // We failed to get info on the class.
+ if (classInfo->ID == static_cast(-1)) // We failed to get info on the class.
return NULL;
if (classInfo->ID == 0)
{
- classInfo->ID = -1;
+ classInfo->ID = static_cast(-1);
DWORD classFlags = 0; // TODO FIX NOW, set class flags properly.
ModuleID moduleId = 0;
@@ -803,7 +809,7 @@ ClassInfo* CorProfilerTracer::GetClassInfo(ClassID classId)
classInfo->ForceKeepSize = 0x0;
#endif
- if (classInfo->ID != -1)
+ if (classInfo->ID != static_cast(-1))
{
EventWriteClassIDDefintionEvent(classInfo->ID, classInfo->Token, classFlags, moduleId, classInfo->Name);
}
@@ -830,7 +836,7 @@ ModuleInfo* CorProfilerTracer::GetModuleInfo(ModuleID moduleId)
if (!moduleInfo->MetaDataImport)
{
HRESULT hr = m_info->GetModuleMetaData(moduleId, ofRead, IID_IMetaDataImport, (IUnknown**)&moduleInfo->MetaDataImport);
- if (!moduleInfo->MetaDataImport)
+ if (FAILED(hr) || !moduleInfo->MetaDataImport)
{
moduleInfo->MetaDataFailed = true;
return nullptr;
@@ -855,4 +861,3 @@ ModuleInfo* CorProfilerTracer::GetModuleInfo(ModuleID moduleId)
return moduleInfo;
}
-
diff --git a/src/EtwClrProfiler/CorProfilerTracer.h b/src/EtwClrProfiler/CorProfilerTracer.h
index 987a59f00..0bf4d65d1 100644
--- a/src/EtwClrProfiler/CorProfilerTracer.h
+++ b/src/EtwClrProfiler/CorProfilerTracer.h
@@ -3,7 +3,10 @@
// Headers needed for CLR Profiling
#include
#include
+#pragma warning(push)
+#pragma warning(disable: 4458) // The .NET Framework SDK header shadows a member named Size.
#include
+#pragma warning(pop)
#include
@@ -34,7 +37,7 @@ class CorProfilerTracer : public ICorProfilerCallback3
STDMETHODIMP QueryInterface(REFIID riid, void **ppInterface);
// ICorProfilerCallback interface implementation
- STDMETHODIMP Initialize(IUnknown * pICorProfilerInfoUnk) { return InitializeForAttach(pICorProfilerInfoUnk, NULL, -1); }
+ STDMETHODIMP Initialize(IUnknown * pICorProfilerInfoUnk) { return InitializeForAttach(pICorProfilerInfoUnk, NULL, static_cast(-1)); }
STDMETHODIMP Shutdown();
// ICorProfilerCallback3
@@ -49,71 +52,71 @@ class CorProfilerTracer : public ICorProfilerCallback3
return Shutdown();
}
- STDMETHODIMP AppDomainCreationStarted(AppDomainID appDomainId) { return S_OK; };
- STDMETHODIMP AppDomainCreationFinished(AppDomainID appDomainId, HRESULT hrStatus) { return S_OK; };
- STDMETHODIMP AppDomainShutdownStarted(AppDomainID appDomainId) { return S_OK; };
- STDMETHODIMP AppDomainShutdownFinished(AppDomainID appDomainId, HRESULT hrStatus) { return S_OK; };
- STDMETHODIMP AssemblyLoadStarted(AssemblyID assemblyId) { return S_OK; };
- STDMETHODIMP AssemblyLoadFinished(AssemblyID assemblyId, HRESULT hrStatus) { return S_OK; };
- STDMETHODIMP AssemblyUnloadStarted(AssemblyID assemblyId) { return S_OK; };
- STDMETHODIMP AssemblyUnloadFinished(AssemblyID assemblyId, HRESULT hrStatus) { return S_OK; };
- STDMETHODIMP ModuleLoadStarted(ModuleID moduleId) { return S_OK; };
- STDMETHODIMP ModuleLoadFinished(ModuleID moduleId, HRESULT hrStatus) { return S_OK; };
- STDMETHODIMP ModuleUnloadStarted(ModuleID moduleId) { return S_OK; };
- STDMETHODIMP ModuleUnloadFinished(ModuleID moduleId, HRESULT hrStatus) { return S_OK; };
+ STDMETHODIMP AppDomainCreationStarted(AppDomainID) { return S_OK; };
+ STDMETHODIMP AppDomainCreationFinished(AppDomainID, HRESULT) { return S_OK; };
+ STDMETHODIMP AppDomainShutdownStarted(AppDomainID) { return S_OK; };
+ STDMETHODIMP AppDomainShutdownFinished(AppDomainID, HRESULT) { return S_OK; };
+ STDMETHODIMP AssemblyLoadStarted(AssemblyID) { return S_OK; };
+ STDMETHODIMP AssemblyLoadFinished(AssemblyID, HRESULT) { return S_OK; };
+ STDMETHODIMP AssemblyUnloadStarted(AssemblyID) { return S_OK; };
+ STDMETHODIMP AssemblyUnloadFinished(AssemblyID, HRESULT) { return S_OK; };
+ STDMETHODIMP ModuleLoadStarted(ModuleID) { return S_OK; };
+ STDMETHODIMP ModuleLoadFinished(ModuleID, HRESULT) { return S_OK; };
+ STDMETHODIMP ModuleUnloadStarted(ModuleID) { return S_OK; };
+ STDMETHODIMP ModuleUnloadFinished(ModuleID, HRESULT) { return S_OK; };
STDMETHODIMP ModuleAttachedToAssembly(ModuleID moduleId, AssemblyID assemblyId);
- STDMETHODIMP ClassLoadStarted(ClassID classId) { return S_OK; };
- STDMETHODIMP ClassLoadFinished(ClassID classId, HRESULT hrStatus) { return S_OK; };
- STDMETHODIMP ClassUnloadStarted(ClassID classId) { return S_OK; };
- STDMETHODIMP ClassUnloadFinished(ClassID classId, HRESULT hrStatus) { return S_OK; };
- STDMETHODIMP FunctionUnloadStarted(FunctionID functionId) { return S_OK; };
- STDMETHODIMP JITCompilationStarted(FunctionID functionId, BOOL fIsSafeToBlock) { return S_OK; };
- STDMETHODIMP JITCompilationFinished(FunctionID functionId, HRESULT hrStatus, BOOL fIsSafeToBlock) { return S_OK; };
- STDMETHODIMP JITCachedFunctionSearchStarted(FunctionID functionId, BOOL * pbUseCachedFunction) { return S_OK; };
- STDMETHODIMP JITCachedFunctionSearchFinished(FunctionID functionId, COR_PRF_JIT_CACHE result) { return S_OK; };
- STDMETHODIMP JITFunctionPitched(FunctionID functionId) { return S_OK; };
- STDMETHODIMP JITInlining(FunctionID callerId, FunctionID calleeId, BOOL * pfShouldInline) { return S_OK; };
- STDMETHODIMP ThreadCreated(ThreadID threadId) { return S_OK; };
- STDMETHODIMP ThreadDestroyed(ThreadID threadId) { return S_OK; };
- STDMETHODIMP ThreadAssignedToOSThread(ThreadID managedThreadId, ULONG osThreadId) { return S_OK; };
+ STDMETHODIMP ClassLoadStarted(ClassID) { return S_OK; };
+ STDMETHODIMP ClassLoadFinished(ClassID, HRESULT) { return S_OK; };
+ STDMETHODIMP ClassUnloadStarted(ClassID) { return S_OK; };
+ STDMETHODIMP ClassUnloadFinished(ClassID, HRESULT) { return S_OK; };
+ STDMETHODIMP FunctionUnloadStarted(FunctionID) { return S_OK; };
+ STDMETHODIMP JITCompilationStarted(FunctionID, BOOL) { return S_OK; };
+ STDMETHODIMP JITCompilationFinished(FunctionID, HRESULT, BOOL) { return S_OK; };
+ STDMETHODIMP JITCachedFunctionSearchStarted(FunctionID, BOOL *) { return S_OK; };
+ STDMETHODIMP JITCachedFunctionSearchFinished(FunctionID, COR_PRF_JIT_CACHE) { return S_OK; };
+ STDMETHODIMP JITFunctionPitched(FunctionID) { return S_OK; };
+ STDMETHODIMP JITInlining(FunctionID, FunctionID, BOOL *) { return S_OK; };
+ STDMETHODIMP ThreadCreated(ThreadID) { return S_OK; };
+ STDMETHODIMP ThreadDestroyed(ThreadID) { return S_OK; };
+ STDMETHODIMP ThreadAssignedToOSThread(ThreadID, ULONG) { return S_OK; };
STDMETHODIMP RemotingClientInvocationStarted() { return S_OK; };
- STDMETHODIMP RemotingClientSendingMessage(GUID * pCookie, BOOL fIsAsync) { return S_OK; };
- STDMETHODIMP RemotingClientReceivingReply(GUID * pCookie, BOOL fIsAsync) { return S_OK; };
+ STDMETHODIMP RemotingClientSendingMessage(GUID *, BOOL) { return S_OK; };
+ STDMETHODIMP RemotingClientReceivingReply(GUID *, BOOL) { return S_OK; };
STDMETHODIMP RemotingClientInvocationFinished() { return S_OK; };
- STDMETHODIMP RemotingServerReceivingMessage(GUID * pCookie, BOOL fIsAsync) { return S_OK; };
+ STDMETHODIMP RemotingServerReceivingMessage(GUID *, BOOL) { return S_OK; };
STDMETHODIMP RemotingServerInvocationStarted() { return S_OK; };
STDMETHODIMP RemotingServerInvocationReturned() { return S_OK; };
- STDMETHODIMP RemotingServerSendingReply(GUID * pCookie, BOOL fIsAsync) { return S_OK; };
- STDMETHODIMP UnmanagedToManagedTransition(FunctionID functionId, COR_PRF_TRANSITION_REASON reason) { return S_OK; };
- STDMETHODIMP ManagedToUnmanagedTransition(FunctionID functionId, COR_PRF_TRANSITION_REASON reason) { return S_OK; };
- STDMETHODIMP RuntimeSuspendStarted(COR_PRF_SUSPEND_REASON suspendReason) { return S_OK; };
+ STDMETHODIMP RemotingServerSendingReply(GUID *, BOOL) { return S_OK; };
+ STDMETHODIMP UnmanagedToManagedTransition(FunctionID, COR_PRF_TRANSITION_REASON) { return S_OK; };
+ STDMETHODIMP ManagedToUnmanagedTransition(FunctionID, COR_PRF_TRANSITION_REASON) { return S_OK; };
+ STDMETHODIMP RuntimeSuspendStarted(COR_PRF_SUSPEND_REASON) { return S_OK; };
STDMETHODIMP RuntimeSuspendFinished() { return S_OK; };
STDMETHODIMP RuntimeSuspendAborted() { return S_OK; };
STDMETHODIMP RuntimeResumeStarted() { return S_OK; };
STDMETHODIMP RuntimeResumeFinished() { return S_OK; };
- STDMETHODIMP RuntimeThreadSuspended(ThreadID threadId) { return S_OK; };
- STDMETHODIMP RuntimeThreadResumed(ThreadID threadId) { return S_OK; };
+ STDMETHODIMP RuntimeThreadSuspended(ThreadID) { return S_OK; };
+ STDMETHODIMP RuntimeThreadResumed(ThreadID) { return S_OK; };
STDMETHODIMP MovedReferences(ULONG cMovedObjectIDRanges, ObjectID oldObjectIDRangeStart[], ObjectID newObjectIDRangeStart[], ULONG cObjectIDRangeLength[]);
STDMETHODIMP ObjectAllocated(ObjectID objectId, ClassID classId);
- STDMETHODIMP ObjectsAllocatedByClass(ULONG cClassCount, ClassID classIds[], ULONG cObjects[]) { return S_OK; };
+ STDMETHODIMP ObjectsAllocatedByClass(ULONG, ClassID[], ULONG[]) { return S_OK; };
STDMETHODIMP ObjectReferences(ObjectID objectId, ClassID classId, ULONG cObjectRefs, ObjectID objectRefIds[]);
- STDMETHODIMP RootReferences(ULONG cRootRefs, ObjectID rootRefIds[]) { return S_OK; }
- STDMETHODIMP ExceptionThrown(ObjectID thrownObjectId) { return S_OK; };
- STDMETHODIMP ExceptionSearchFunctionEnter(FunctionID functionId) { return S_OK; };
+ STDMETHODIMP RootReferences(ULONG, ObjectID[]) { return S_OK; }
+ STDMETHODIMP ExceptionThrown(ObjectID) { return S_OK; };
+ STDMETHODIMP ExceptionSearchFunctionEnter(FunctionID) { return S_OK; };
STDMETHODIMP ExceptionSearchFunctionLeave() { return S_OK; };
- STDMETHODIMP ExceptionSearchFilterEnter(FunctionID functionId) { return S_OK; };
+ STDMETHODIMP ExceptionSearchFilterEnter(FunctionID) { return S_OK; };
STDMETHODIMP ExceptionSearchFilterLeave() { return S_OK; };
- STDMETHODIMP ExceptionSearchCatcherFound(FunctionID functionId) { return S_OK; };
- STDMETHODIMP ExceptionOSHandlerEnter(FunctionID functionId) { return S_OK; };
- STDMETHODIMP ExceptionOSHandlerLeave(FunctionID functionId) { return S_OK; };
- STDMETHODIMP ExceptionUnwindFunctionEnter(FunctionID functionId) { return S_OK; };
+ STDMETHODIMP ExceptionSearchCatcherFound(FunctionID) { return S_OK; };
+ STDMETHODIMP ExceptionOSHandlerEnter(FunctionID) { return S_OK; };
+ STDMETHODIMP ExceptionOSHandlerLeave(FunctionID) { return S_OK; };
+ STDMETHODIMP ExceptionUnwindFunctionEnter(FunctionID) { return S_OK; };
STDMETHODIMP ExceptionUnwindFunctionLeave() { return S_OK; };
- STDMETHODIMP ExceptionUnwindFinallyEnter(FunctionID functionId) { return S_OK; };
+ STDMETHODIMP ExceptionUnwindFinallyEnter(FunctionID) { return S_OK; };
STDMETHODIMP ExceptionUnwindFinallyLeave() { return S_OK; };
- STDMETHODIMP ExceptionCatcherEnter(FunctionID functionId, ObjectID objectId) { return S_OK; };
+ STDMETHODIMP ExceptionCatcherEnter(FunctionID, ObjectID) { return S_OK; };
STDMETHODIMP ExceptionCatcherLeave() { return S_OK; };
- STDMETHODIMP COMClassicVTableCreated(ClassID wrappedClassId, REFGUID implementedIID, void *pVTable, ULONG cSlots) { return S_OK; };
- STDMETHODIMP COMClassicVTableDestroyed(ClassID wrappedClassId, REFGUID implementedIID, void *pVTable) { return S_OK; };
+ STDMETHODIMP COMClassicVTableCreated(ClassID, REFGUID, void *, ULONG) { return S_OK; };
+ STDMETHODIMP COMClassicVTableDestroyed(ClassID, REFGUID, void *) { return S_OK; };
STDMETHODIMP ExceptionCLRCatcherFound(void) { return S_OK; };
STDMETHODIMP ExceptionCLRCatcherExecute(void) { return S_OK; };
@@ -121,7 +124,7 @@ class CorProfilerTracer : public ICorProfilerCallback3
// ICorProfilerCallback2 interface implementation
- STDMETHODIMP ThreadNameChanged(ThreadID threadId, ULONG cchName, WCHAR* name) { return S_OK; };
+ STDMETHODIMP ThreadNameChanged(ThreadID, ULONG, WCHAR*) { return S_OK; };
STDMETHODIMP GarbageCollectionStarted(int cGenerations, BOOL generationCollected[], COR_PRF_GC_REASON reason);
STDMETHODIMP SurvivingReferences(ULONG cSurvivingObjectIDRanges, ObjectID objectIDRangeStart[], ULONG cObjectIDRangeLength[]);
diff --git a/src/EtwClrProfiler/ETWClrProfilerX64.vcxproj b/src/EtwClrProfiler/ETWClrProfilerX64.vcxproj
index 86e529318..b5b991e0d 100644
--- a/src/EtwClrProfiler/ETWClrProfilerX64.vcxproj
+++ b/src/EtwClrProfiler/ETWClrProfilerX64.vcxproj
@@ -29,8 +29,8 @@
Spectre
- v143
- v142
+ v145
+ 145
@@ -57,7 +57,8 @@
- Level3
+ Level4
+ true
Disabled
WIN32;NDEBUG;_HAS_EXCEPTIONS=0;%(PreprocessorDefinitions)
Use
@@ -69,6 +70,7 @@
true
ProgramDatabase
/Gs %(AdditionalOptions)
+ Spectre
Windows
@@ -82,7 +84,8 @@
- Level3
+ Level4
+ true
WIN32;NDEBUG;_HAS_EXCEPTIONS=0;%(PreprocessorDefinitions)
Use
MultiThreaded
@@ -92,6 +95,8 @@
/Gs %(AdditionalOptions)
Guard
true
+ Spectre
+ true
Windows
@@ -101,6 +106,9 @@
true
/HIGHENTROPYVA %(AdditionalOptions)
true
+ true
+ true
+ UseLinkTimeCodeGeneration
diff --git a/src/EtwClrProfiler/ETWClrProfilerX86.vcxproj b/src/EtwClrProfiler/ETWClrProfilerX86.vcxproj
index 89fbae579..ad9c7f9b6 100644
--- a/src/EtwClrProfiler/ETWClrProfilerX86.vcxproj
+++ b/src/EtwClrProfiler/ETWClrProfilerX86.vcxproj
@@ -29,8 +29,8 @@
Spectre
- v143
- v142
+ v145
+ 145
@@ -56,7 +56,8 @@
- Level3
+ Level4
+ true
Disabled
WIN32;NDEBUG;_HAS_EXCEPTIONS=0;%(PreprocessorDefinitions)
Use
@@ -69,6 +70,7 @@
/Gs %(AdditionalOptions)
true
ProgramDatabase
+ Spectre
Windows
@@ -81,7 +83,8 @@
- Level3
+ Level4
+ true
WIN32;NDEBUG;_HAS_EXCEPTIONS=0;%(PreprocessorDefinitions)
Use
MultiThreaded
@@ -91,6 +94,8 @@
/Gs %(AdditionalOptions)
Guard
true
+ Spectre
+ true
Windows
@@ -98,6 +103,9 @@
.\ETWClrProfiler.def
%(AdditionalDependencies)
true
+ true
+ true
+ UseLinkTimeCodeGeneration
diff --git a/src/EtwClrProfilerSigning/EtwClrProfilerSigning.csproj b/src/EtwClrProfilerSigning/EtwClrProfilerSigning.csproj
index 2f1038882..1e4432b36 100644
--- a/src/EtwClrProfilerSigning/EtwClrProfilerSigning.csproj
+++ b/src/EtwClrProfilerSigning/EtwClrProfilerSigning.csproj
@@ -28,7 +28,7 @@
Microsoft400
-
+
diff --git a/src/EtwHeapDump/DotNetHeapDumpGraphReader.cs b/src/EtwHeapDump/DotNetHeapDumpGraphReader.cs
index 2ca5602c1..ce7499d95 100644
--- a/src/EtwHeapDump/DotNetHeapDumpGraphReader.cs
+++ b/src/EtwHeapDump/DotNetHeapDumpGraphReader.cs
@@ -114,12 +114,29 @@ internal void SetupCallbacks(MemoryGraph memoryGraph, TraceEventDispatcher sourc
return;
}
- if (!m_moduleID2Name.ContainsKey((Address)data.ModuleID))
+ if ((data.ModuleFlags & ModuleFlags.Native) != 0)
{
- m_moduleID2Name[(Address)data.ModuleID] = data.ModuleILPath;
+ if (!m_modules.ContainsKey((Address)data.ModuleID))
+ {
+ Module module = new Module((Address)data.ModuleID);
+ module.Path = data.ModuleNativePath;
+ module.PdbGuid = data.NativePdbSignature;
+ module.PdbAge = data.NativePdbAge;
+ module.PdbName = data.NativePdbBuildPath;
+ m_modules[module.ImageBase] = module;
+ }
+
+ m_log.WriteLine("Found Native Module {0} ID 0x{1:x}", data.ModuleNativePath, data.ModuleID);
}
+ else
+ {
+ if (!m_moduleID2Name.ContainsKey((Address)data.ModuleID))
+ {
+ m_moduleID2Name[(Address)data.ModuleID] = data.ModuleILPath;
+ }
- m_log.WriteLine("Found Module {0} ID 0x{1:x}", data.ModuleILFileName, (Address)data.ModuleID);
+ m_log.WriteLine("Found Module {0} ID 0x{1:x}", data.ModuleILFileName, (Address)data.ModuleID);
+ }
};
source.Clr.AddCallbackForEvents(moduleCallback); // Get module events for clr provider
// TODO should not be needed if we use CAPTURE_STATE when collecting.
@@ -540,12 +557,11 @@ internal unsafe void ConvertHeapDataToGraph()
{
GCBulkTypeValues typeData = data.Values(i);
var typeName = typeData.TypeName;
- if (IsProjectN)
+ if ((typeData.Flags & TypeFlags.ModuleBaseAddress) != 0)
{
- // For project N we only log the type ID and module base address.
+ // For native modules we only log the type ID and module base address.
Debug.Assert(typeName.Length == 0);
- Debug.Assert((typeData.Flags & TypeFlags.ModuleBaseAddress) != 0);
- var moduleBaseAddress = typeData.TypeID - (ulong)typeData.TypeNameID; // Tricky way of getting the image base.
+ ulong moduleBaseAddress = typeData.ModuleID;
Debug.Assert((moduleBaseAddress & 0xFFFF) == 0); // Image loads should be on 64K boundaries.
Module module = GetModuleForImageBase(moduleBaseAddress);
@@ -841,7 +857,7 @@ private NodeTypeIndex GetTypeIndex(Address typeID, int objSize)
// TODO FIX NOW worry about module collision
if (!m_arrayNametoIndex.TryGetValue(typeName, out ret))
{
- if (IsProjectN)
+ if (m_graph.HasDeferedTypeNames)
{
ret = m_graph.CreateType(type.RawTypeID, type.Module, objSize, suffix);
}
diff --git a/src/EtwHeapDump/EtwHeapDump.csproj b/src/EtwHeapDump/EtwHeapDump.csproj
index b9b777d98..77067755d 100644
--- a/src/EtwHeapDump/EtwHeapDump.csproj
+++ b/src/EtwHeapDump/EtwHeapDump.csproj
@@ -21,7 +21,7 @@
-
+
@@ -31,14 +31,14 @@
StrongName
-
+
-
+
-
+
$(MicrosoftDiagnosticsRuntimePath)
@@ -46,7 +46,7 @@
-
+
diff --git a/src/FastSerialization.Tests/FastSerialization.Tests.csproj b/src/FastSerialization.Tests/FastSerialization.Tests.csproj
new file mode 100644
index 000000000..de6a1de1b
--- /dev/null
+++ b/src/FastSerialization.Tests/FastSerialization.Tests.csproj
@@ -0,0 +1,45 @@
+
+
+
+
+ net462;net8.0
+ FastSerializationTests
+ FastSerializationTests
+ Unit tests for FastSerialization.
+ Copyright Β© Microsoft 2025
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ DebugAssertionTests.cs
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
diff --git a/src/FastSerialization.Tests/GrowableArrayTests.cs b/src/FastSerialization.Tests/GrowableArrayTests.cs
new file mode 100644
index 000000000..9b7317dd3
--- /dev/null
+++ b/src/FastSerialization.Tests/GrowableArrayTests.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Collections.Generic;
+using Xunit;
+
+namespace FastSerializationTests
+{
+ ///
+ /// Tests for GrowableArray<T>
+ ///
+ public class GrowableArrayTests
+ {
+ [Fact]
+ public void DefaultConstructor()
+ {
+ var array = new GrowableArray();
+ Assert.Equal(0, array.Count);
+ }
+
+ [Fact]
+ public void InitialSizeConstructor()
+ {
+ var array = new GrowableArray(10);
+ Assert.Equal(0, array.Count);
+ }
+
+ [Fact]
+ public void AddItems()
+ {
+ var array = new GrowableArray();
+ array.Add(1);
+ array.Add(2);
+ array.Add(3);
+
+ Assert.Equal(3, array.Count);
+ Assert.Equal(1, array[0]);
+ Assert.Equal(2, array[1]);
+ Assert.Equal(3, array[2]);
+ }
+
+ [Fact]
+ public void AddManyItems()
+ {
+ var array = new GrowableArray();
+ for (int i = 0; i < 100; i++)
+ {
+ array.Add(i);
+ }
+
+ Assert.Equal(100, array.Count);
+ for (int i = 0; i < 100; i++)
+ {
+ Assert.Equal(i, array[i]);
+ }
+ }
+
+ [Fact]
+ public void SetItem()
+ {
+ var array = new GrowableArray();
+ array.Add("First");
+ array.Add("Second");
+
+ array[0] = "Modified";
+ Assert.Equal("Modified", array[0]);
+ Assert.Equal("Second", array[1]);
+ }
+
+ [Fact]
+ public void Clear()
+ {
+ var array = new GrowableArray();
+ array.Add(1);
+ array.Add(2);
+ array.Add(3);
+
+ array.Clear();
+ Assert.Equal(0, array.Count);
+ }
+
+ [Fact]
+ public void SetCountLarger()
+ {
+ var array = new GrowableArray();
+ array.Add(1);
+ array.Add(2);
+
+ array.Count = 5;
+ Assert.Equal(5, array.Count);
+ Assert.Equal(1, array[0]);
+ Assert.Equal(2, array[1]);
+ Assert.Equal(0, array[2]);
+ Assert.Equal(0, array[3]);
+ Assert.Equal(0, array[4]);
+ }
+
+ [Fact]
+ public void SetCountSmaller()
+ {
+ var array = new GrowableArray();
+ array.Add(1);
+ array.Add(2);
+ array.Add(3);
+ array.Add(4);
+
+ array.Count = 2;
+ Assert.Equal(2, array.Count);
+ Assert.Equal(1, array[0]);
+ Assert.Equal(2, array[1]);
+ }
+
+ [Fact]
+ public void AddRangeFromArray()
+ {
+ var array = new GrowableArray();
+ array.Add(1);
+
+ int[] toAdd = new int[] { 2, 3, 4, 5 };
+ array.AddRange(toAdd);
+
+ Assert.Equal(5, array.Count);
+ for (int i = 0; i < 5; i++)
+ {
+ Assert.Equal(i + 1, array[i]);
+ }
+ }
+
+ [Fact]
+ public void AddRangeFromGrowableArray()
+ {
+ var array1 = new GrowableArray();
+ array1.Add("A");
+ array1.Add("B");
+
+ var array2 = new GrowableArray();
+ array2.Add("C");
+ array2.Add("D");
+
+ // Convert to array to add
+ string[] toAdd = new string[array2.Count];
+ for (int i = 0; i < array2.Count; i++)
+ {
+ toAdd[i] = array2[i];
+ }
+ array1.AddRange(toAdd);
+
+ Assert.Equal(4, array1.Count);
+ Assert.Equal("A", array1[0]);
+ Assert.Equal("B", array1[1]);
+ Assert.Equal("C", array1[2]);
+ Assert.Equal("D", array1[3]);
+ }
+
+ [Fact]
+ public void WorksWithReferenceTypes()
+ {
+ var array = new GrowableArray();
+ array.Add("Hello");
+ array.Add(null);
+ array.Add("World");
+
+ Assert.Equal(3, array.Count);
+ Assert.Equal("Hello", array[0]);
+ Assert.Null(array[1]);
+ Assert.Equal("World", array[2]);
+ }
+
+ [Fact]
+ public void WorksWithValueTypes()
+ {
+ var array = new GrowableArray();
+ array.Add(1.5);
+ array.Add(2.5);
+ array.Add(3.5);
+
+ Assert.Equal(3, array.Count);
+ Assert.Equal(1.5, array[0]);
+ Assert.Equal(2.5, array[1]);
+ Assert.Equal(3.5, array[2]);
+ }
+ }
+}
diff --git a/src/FastSerialization.Tests/ObjectSerializationTests.cs b/src/FastSerialization.Tests/ObjectSerializationTests.cs
new file mode 100644
index 000000000..dd2364639
--- /dev/null
+++ b/src/FastSerialization.Tests/ObjectSerializationTests.cs
@@ -0,0 +1,195 @@
+using System;
+using System.IO;
+using FastSerialization;
+using Xunit;
+
+namespace FastSerializationTests
+{
+ ///
+ /// Test class that implements IFastSerializable for testing object serialization
+ ///
+ public class SimpleObject : IFastSerializable
+ {
+ public int IntValue { get; set; }
+ public string StringValue { get; set; }
+
+ public SimpleObject() { }
+
+ public SimpleObject(int intValue, string stringValue)
+ {
+ IntValue = intValue;
+ StringValue = stringValue;
+ }
+
+ public void ToStream(Serializer serializer)
+ {
+ serializer.Write(IntValue);
+ serializer.Write(StringValue);
+ }
+
+ public void FromStream(Deserializer deserializer)
+ {
+ IntValue = deserializer.ReadInt();
+ StringValue = deserializer.ReadString();
+ }
+ }
+
+ ///
+ /// Test class with nested objects
+ ///
+ public class ComplexObject : IFastSerializable
+ {
+ public SimpleObject NestedObject { get; set; }
+ public int[] IntArray { get; set; }
+
+ public ComplexObject() { }
+
+ public ComplexObject(SimpleObject nested, int[] intArray)
+ {
+ NestedObject = nested;
+ IntArray = intArray;
+ }
+
+ public void ToStream(Serializer serializer)
+ {
+ serializer.Write(NestedObject);
+
+ serializer.Write(IntArray != null);
+ if (IntArray != null)
+ {
+ serializer.Write(IntArray.Length);
+ for (int i = 0; i < IntArray.Length; i++)
+ {
+ serializer.Write(IntArray[i]);
+ }
+ }
+ }
+
+ public void FromStream(Deserializer deserializer)
+ {
+ NestedObject = (SimpleObject)deserializer.ReadObject();
+
+ bool hasIntArray = deserializer.ReadBool();
+ if (hasIntArray)
+ {
+ int length = deserializer.ReadInt();
+ IntArray = new int[length];
+ for (int i = 0; i < length; i++)
+ {
+ IntArray[i] = deserializer.ReadInt();
+ }
+ }
+ }
+ }
+
+ ///
+ /// Tests for object serialization using IFastSerializable
+ ///
+ public class ObjectSerializationTests
+ {
+ [Fact]
+ public void SerializeSimpleObject()
+ {
+ var stream = new MemoryStream();
+ var original = new SimpleObject(42, "Test String");
+
+ using (var serializer = new Serializer(stream, original, leaveOpen: true))
+ {
+ }
+
+ stream.Position = 0;
+
+ using (var deserializer = new Deserializer(stream, "test", leaveOpen: true, SerializationSettings.Default))
+ {
+ deserializer.RegisterFactory(typeof(SimpleObject), () => new SimpleObject());
+ SimpleObject deserialized;
+ deserializer.GetEntryObject(out deserialized);
+ Assert.NotNull(deserialized);
+ Assert.Equal(original.IntValue, deserialized.IntValue);
+ Assert.Equal(original.StringValue, deserialized.StringValue);
+ }
+ }
+
+ [Fact]
+ public void SerializeComplexObject()
+ {
+ var stream = new MemoryStream();
+ var nested = new SimpleObject(123, "Nested");
+ var original = new ComplexObject(nested, new int[] { 1, 2, 3, 4, 5 });
+
+ using (var serializer = new Serializer(stream, original, leaveOpen: true))
+ {
+ }
+
+ stream.Position = 0;
+
+ using (var deserializer = new Deserializer(stream, "test", leaveOpen: true, SerializationSettings.Default))
+ {
+ deserializer.RegisterFactory(typeof(ComplexObject), () => new ComplexObject());
+ deserializer.RegisterFactory(typeof(SimpleObject), () => new SimpleObject());
+ ComplexObject deserialized;
+ deserializer.GetEntryObject(out deserialized);
+ Assert.NotNull(deserialized);
+ Assert.NotNull(deserialized.NestedObject);
+ Assert.Equal(original.NestedObject.IntValue, deserialized.NestedObject.IntValue);
+ Assert.Equal(original.NestedObject.StringValue, deserialized.NestedObject.StringValue);
+ Assert.NotNull(deserialized.IntArray);
+ Assert.Equal(original.IntArray.Length, deserialized.IntArray.Length);
+ for (int i = 0; i < original.IntArray.Length; i++)
+ {
+ Assert.Equal(original.IntArray[i], deserialized.IntArray[i]);
+ }
+ }
+ }
+
+ [Fact]
+ public void SerializeObjectWithNullFields()
+ {
+ var stream = new MemoryStream();
+ var original = new SimpleObject(99, null);
+
+ using (var serializer = new Serializer(stream, original, leaveOpen: true))
+ {
+ }
+
+ stream.Position = 0;
+
+ using (var deserializer = new Deserializer(stream, "test", leaveOpen: true, SerializationSettings.Default))
+ {
+ deserializer.RegisterFactory(typeof(SimpleObject), () => new SimpleObject());
+ SimpleObject deserialized;
+ deserializer.GetEntryObject(out deserialized);
+ Assert.NotNull(deserialized);
+ Assert.Equal(original.IntValue, deserialized.IntValue);
+ Assert.Null(deserialized.StringValue);
+ }
+ }
+
+ [Fact]
+ public void SerializeComplexObjectWithNullNested()
+ {
+ var stream = new MemoryStream();
+ var original = new ComplexObject(null, new int[] { 10, 20 });
+
+ using (var serializer = new Serializer(stream, original, leaveOpen: true))
+ {
+ }
+
+ stream.Position = 0;
+
+ using (var deserializer = new Deserializer(stream, "test", leaveOpen: true, SerializationSettings.Default))
+ {
+ deserializer.RegisterFactory(typeof(ComplexObject), () => new ComplexObject());
+ deserializer.RegisterFactory(typeof(SimpleObject), () => new SimpleObject());
+ ComplexObject deserialized;
+ deserializer.GetEntryObject(out deserialized);
+ Assert.NotNull(deserialized);
+ Assert.Null(deserialized.NestedObject);
+ Assert.NotNull(deserialized.IntArray);
+ Assert.Equal(2, deserialized.IntArray.Length);
+ Assert.Equal(10, deserialized.IntArray[0]);
+ Assert.Equal(20, deserialized.IntArray[1]);
+ }
+ }
+ }
+}
diff --git a/src/FastSerialization.Tests/SegmentedDictionaryTests.cs b/src/FastSerialization.Tests/SegmentedDictionaryTests.cs
new file mode 100644
index 000000000..d9567ff87
--- /dev/null
+++ b/src/FastSerialization.Tests/SegmentedDictionaryTests.cs
@@ -0,0 +1,250 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace FastSerializationTests
+{
+ ///
+ /// Tests for SegmentedDictionary<TKey, TValue>
+ ///
+ public class SegmentedDictionaryTests
+ {
+ [Fact]
+ public void DefaultConstructor()
+ {
+ var dict = new SegmentedDictionary();
+ Assert.Empty(dict);
+ }
+
+ [Fact]
+ public void AddAndRetrieve()
+ {
+ var dict = new SegmentedDictionary();
+ dict.Add("one", 1);
+ dict.Add("two", 2);
+ dict.Add("three", 3);
+
+ Assert.Equal(3, dict.Count);
+ Assert.Equal(1, dict["one"]);
+ Assert.Equal(2, dict["two"]);
+ Assert.Equal(3, dict["three"]);
+ }
+
+ [Fact]
+ public void TryGetValue()
+ {
+ var dict = new SegmentedDictionary();
+ dict.Add("key1", 100);
+
+ Assert.True(dict.TryGetValue("key1", out int value));
+ Assert.Equal(100, value);
+
+ Assert.False(dict.TryGetValue("key2", out value));
+ Assert.Equal(0, value);
+ }
+
+ [Fact]
+ public void ContainsKey()
+ {
+ var dict = new SegmentedDictionary();
+ dict.Add("exists", "value");
+
+ Assert.True(dict.ContainsKey("exists"));
+ Assert.False(dict.ContainsKey("missing"));
+ }
+
+ [Fact]
+ public void Remove()
+ {
+ var dict = new SegmentedDictionary();
+ dict.Add(1, "one");
+ dict.Add(2, "two");
+ dict.Add(3, "three");
+
+ Assert.Equal(3, dict.Count);
+
+ bool removed = dict.Remove(2);
+ Assert.True(removed);
+ Assert.Equal(2, dict.Count);
+ Assert.False(dict.ContainsKey(2));
+
+ removed = dict.Remove(99);
+ Assert.False(removed);
+ Assert.Equal(2, dict.Count);
+ }
+
+ [Fact]
+ public void Clear()
+ {
+ var dict = new SegmentedDictionary();
+ dict.Add("a", 1);
+ dict.Add("b", 2);
+ dict.Add("c", 3);
+
+ Assert.Equal(3, dict.Count);
+
+ dict.Clear();
+ Assert.Empty(dict);
+ Assert.False(dict.ContainsKey("a"));
+ Assert.False(dict.ContainsKey("b"));
+ Assert.False(dict.ContainsKey("c"));
+ }
+
+ [Fact]
+ public void UpdateValue()
+ {
+ var dict = new SegmentedDictionary();
+ dict.Add("key", 10);
+ Assert.Equal(10, dict["key"]);
+
+ dict["key"] = 20;
+ Assert.Equal(20, dict["key"]);
+ }
+
+ [Fact]
+ public void AddDuplicateKeyThrows()
+ {
+ var dict = new SegmentedDictionary();
+ dict.Add("duplicate", 1);
+
+ Assert.Throws(() => dict.Add("duplicate", 2));
+ }
+
+ [Fact]
+ public void AccessNonExistentKeyThrows()
+ {
+ var dict = new SegmentedDictionary();
+
+ Assert.Throws(() => { var x = dict["missing"]; });
+ }
+
+ [Fact]
+ public void Keys()
+ {
+ var dict = new SegmentedDictionary();
+ dict.Add("one", 1);
+ dict.Add("two", 2);
+ dict.Add("three", 3);
+
+ var keys = dict.Keys.ToList();
+ Assert.Equal(3, keys.Count);
+ Assert.Contains("one", keys);
+ Assert.Contains("two", keys);
+ Assert.Contains("three", keys);
+ }
+
+ [Fact]
+ public void Values()
+ {
+ var dict = new SegmentedDictionary();
+ dict.Add("one", 1);
+ dict.Add("two", 2);
+ dict.Add("three", 3);
+
+ var values = dict.Values.ToList();
+ Assert.Equal(3, values.Count);
+ Assert.Contains(1, values);
+ Assert.Contains(2, values);
+ Assert.Contains(3, values);
+ }
+
+ [Fact]
+ public void GetEnumerator()
+ {
+ var dict = new SegmentedDictionary();
+ dict.Add("one", 1);
+ dict.Add("two", 2);
+ dict.Add("three", 3);
+
+ var foundKeys = new HashSet();
+ foreach (var kvp in dict)
+ {
+ Assert.NotNull(kvp.Key);
+ foundKeys.Add(kvp.Key);
+
+ // Verify the correct value for each key
+ if (kvp.Key == "one")
+ Assert.Equal(1, kvp.Value);
+ else if (kvp.Key == "two")
+ Assert.Equal(2, kvp.Value);
+ else if (kvp.Key == "three")
+ Assert.Equal(3, kvp.Value);
+ else
+ Assert.Fail($"Unexpected key: {kvp.Key}");
+ }
+
+ // Verify we found each key exactly once
+ Assert.Equal(3, foundKeys.Count);
+ Assert.Contains("one", foundKeys);
+ Assert.Contains("two", foundKeys);
+ Assert.Contains("three", foundKeys);
+ }
+
+ [Fact]
+ public void LargeDictionary()
+ {
+ var dict = new SegmentedDictionary();
+
+ // Add many items to test segmentation
+ for (int i = 0; i < 10000; i++)
+ {
+ dict.Add(i, $"Value {i}");
+ }
+
+ Assert.Equal(10000, dict.Count);
+
+ // Verify all items
+ for (int i = 0; i < 10000; i++)
+ {
+ Assert.True(dict.ContainsKey(i));
+ Assert.Equal($"Value {i}", dict[i]);
+ }
+ }
+
+ [Fact]
+ public void WorksWithNullValues()
+ {
+ var dict = new SegmentedDictionary();
+ dict.Add("key1", null);
+ dict.Add("key2", "value");
+
+ Assert.Equal(2, dict.Count);
+ Assert.Null(dict["key1"]);
+ Assert.Equal("value", dict["key2"]);
+ }
+
+ [Fact]
+ public void WorksWithValueTypes()
+ {
+ var dict = new SegmentedDictionary();
+ dict.Add(1, 1.5);
+ dict.Add(2, 2.5);
+
+ Assert.Equal(2, dict.Count);
+ Assert.Equal(1.5, dict[1]);
+ Assert.Equal(2.5, dict[2]);
+ }
+
+ [Fact]
+ public void CopyTo()
+ {
+ var dict = new SegmentedDictionary();
+ dict.Add("one", 1);
+ dict.Add("two", 2);
+
+ var array = new KeyValuePair[4];
+ dict.CopyTo(array, 1);
+
+ // First and last elements should be default (null, 0)
+ Assert.Equal(new KeyValuePair(null, 0), array[0]);
+ Assert.Equal(new KeyValuePair(null, 0), array[3]);
+
+ // Middle two elements should contain our dictionary entries
+ // Order is not guaranteed, so check both are present
+ var copiedItems = new[] { array[1], array[2] };
+ Assert.Contains(new KeyValuePair("one", 1), copiedItems);
+ Assert.Contains(new KeyValuePair("two", 2), copiedItems);
+ }
+ }
+}
diff --git a/src/FastSerialization.Tests/SegmentedListTests.cs b/src/FastSerialization.Tests/SegmentedListTests.cs
new file mode 100644
index 000000000..61684b1ce
--- /dev/null
+++ b/src/FastSerialization.Tests/SegmentedListTests.cs
@@ -0,0 +1,188 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace FastSerializationTests
+{
+ ///
+ /// Tests for SegmentedList<T>
+ ///
+ public class SegmentedListTests
+ {
+ [Fact]
+ public void ConstructorWithSegmentSize()
+ {
+ var list = new SegmentedList(16);
+ Assert.Equal(0, list.Count);
+ }
+
+ [Fact]
+ public void ConstructorWithInvalidSegmentSize()
+ {
+ // Segment size must be power of 2 greater than 1
+ Assert.Throws(() => new SegmentedList(1));
+ Assert.Throws(() => new SegmentedList(3));
+ Assert.Throws(() => new SegmentedList(15));
+ }
+
+ [Fact]
+ public void ConstructorWithValidSegmentSizes()
+ {
+ // Should not throw for valid power of 2 segment sizes
+ var list2 = new SegmentedList(2);
+ var list4 = new SegmentedList(4);
+ var list8 = new SegmentedList(8);
+ var list16 = new SegmentedList(16);
+ var list1024 = new SegmentedList(1024);
+
+ Assert.Equal(0, list2.Count);
+ Assert.Equal(0, list4.Count);
+ Assert.Equal(0, list8.Count);
+ Assert.Equal(0, list16.Count);
+ Assert.Equal(0, list1024.Count);
+ }
+
+ [Fact]
+ public void AddItems()
+ {
+ var list = new SegmentedList(4);
+ list.Add(1);
+ list.Add(2);
+ list.Add(3);
+
+ Assert.Equal(3, list.Count);
+ Assert.Equal(1, list[0]);
+ Assert.Equal(2, list[1]);
+ Assert.Equal(3, list[2]);
+ }
+
+ [Fact]
+ public void AddItemsAcrossSegments()
+ {
+ var list = new SegmentedList(4);
+ for (int i = 0; i < 10; i++)
+ {
+ list.Add(i);
+ }
+
+ Assert.Equal(10, list.Count);
+ for (int i = 0; i < 10; i++)
+ {
+ Assert.Equal(i, list[i]);
+ }
+ }
+
+ [Fact]
+ public void Indexer()
+ {
+ var list = new SegmentedList(8);
+ list.Add("First");
+ list.Add("Second");
+ list.Add("Third");
+
+ Assert.Equal("First", list[0]);
+ Assert.Equal("Second", list[1]);
+ Assert.Equal("Third", list[2]);
+
+ list[1] = "Modified";
+ Assert.Equal("First", list[0]);
+ Assert.Equal("Modified", list[1]);
+ Assert.Equal("Third", list[2]);
+ }
+
+ [Fact]
+ public void Clear()
+ {
+ var list = new SegmentedList(4);
+ list.Add(1);
+ list.Add(2);
+ list.Add(3);
+
+ list.Clear();
+ Assert.Equal(0, list.Count);
+ }
+
+ [Fact]
+ public void Contains()
+ {
+ var list = new SegmentedList(4);
+ list.Add("Apple");
+ list.Add("Banana");
+ list.Add("Cherry");
+
+ Assert.Contains("Apple", list);
+ Assert.Contains("Banana", list);
+ Assert.DoesNotContain("Date", list);
+ }
+
+ [Fact]
+ public void CopyTo()
+ {
+ var list = new SegmentedList(4);
+ for (int i = 0; i < 7; i++)
+ {
+ list.Add(i);
+ }
+
+ int[] array = new int[10];
+ list.CopyTo(array, 2);
+
+ Assert.Equal(0, array[0]);
+ Assert.Equal(0, array[1]);
+ for (int i = 0; i < 7; i++)
+ {
+ Assert.Equal(i, array[i + 2]);
+ }
+ Assert.Equal(0, array[9]);
+ }
+
+ [Fact]
+ public void GetEnumerator()
+ {
+ var list = new SegmentedList(4);
+ for (int i = 0; i < 10; i++)
+ {
+ list.Add(i);
+ }
+
+ int count = 0;
+ foreach (var item in list)
+ {
+ Assert.Equal(count, item);
+ count++;
+ }
+ Assert.Equal(10, count);
+ }
+
+ [Fact]
+ public void LargeList()
+ {
+ var list = new SegmentedList(64);
+ for (int i = 0; i < 1000; i++)
+ {
+ list.Add(i);
+ }
+
+ Assert.Equal(1000, list.Count);
+ for (int i = 0; i < 1000; i++)
+ {
+ Assert.Equal(i, list[i]);
+ }
+ }
+
+ [Fact]
+ public void SetCount()
+ {
+ var list = new SegmentedList(4);
+ list.Add(1);
+ list.Add(2);
+
+ list.Count = 5;
+ Assert.Equal(5, list.Count);
+
+ list.Count = 1;
+ Assert.Equal(1, list.Count);
+ }
+ }
+}
diff --git a/src/FastSerialization.Tests/SerializerTests.cs b/src/FastSerialization.Tests/SerializerTests.cs
new file mode 100644
index 000000000..3a047f65c
--- /dev/null
+++ b/src/FastSerialization.Tests/SerializerTests.cs
@@ -0,0 +1,192 @@
+using System;
+using System.IO;
+using FastSerialization;
+using Xunit;
+
+namespace FastSerializationTests
+{
+ ///
+ /// Test object that holds basic primitive types for serialization testing
+ ///
+ public class PrimitiveTypes : IFastSerializable
+ {
+ public int IntValue { get; set; }
+ public string StringValue { get; set; }
+ public byte ByteValue { get; set; }
+ public short ShortValue { get; set; }
+ public long LongValue { get; set; }
+
+ public PrimitiveTypes() { }
+
+ public PrimitiveTypes(int intVal, string strVal, byte byteVal, short shortVal, long longVal)
+ {
+ IntValue = intVal;
+ StringValue = strVal;
+ ByteValue = byteVal;
+ ShortValue = shortVal;
+ LongValue = longVal;
+ }
+
+ public void ToStream(Serializer serializer)
+ {
+ serializer.Write(IntValue);
+ serializer.Write(StringValue);
+ serializer.Write(ByteValue);
+ serializer.Write(ShortValue);
+ serializer.Write(LongValue);
+ }
+
+ public void FromStream(Deserializer deserializer)
+ {
+ IntValue = deserializer.ReadInt();
+ StringValue = deserializer.ReadString();
+ ByteValue = deserializer.ReadByte();
+ ShortValue = deserializer.ReadInt16();
+ LongValue = deserializer.ReadInt64();
+ }
+ }
+
+ ///
+ /// Tests for basic serialization and deserialization using Serializer and Deserializer
+ ///
+ public class SerializerTests
+ {
+ [Fact]
+ public void BasicSerializationRoundTrip()
+ {
+ var stream = new MemoryStream();
+ var original = new PrimitiveTypes(42, "Hello World", 255, 12345, 9876543210L);
+
+ // Serialize
+ using (var serializer = new Serializer(stream, original, leaveOpen: true))
+ {
+ }
+
+ stream.Position = 0;
+
+ // Deserialize
+ using (var deserializer = new Deserializer(stream, "test", leaveOpen: true, SerializationSettings.Default))
+ {
+ deserializer.RegisterFactory(typeof(PrimitiveTypes), () => new PrimitiveTypes());
+ PrimitiveTypes deserialized;
+ deserializer.GetEntryObject(out deserialized);
+
+ Assert.Equal(original.IntValue, deserialized.IntValue);
+ Assert.Equal(original.StringValue, deserialized.StringValue);
+ Assert.Equal(original.ByteValue, deserialized.ByteValue);
+ Assert.Equal(original.ShortValue, deserialized.ShortValue);
+ Assert.Equal(original.LongValue, deserialized.LongValue);
+ }
+ }
+
+ [Fact]
+ public void SerializeNullString()
+ {
+ var stream = new MemoryStream();
+ var original = new PrimitiveTypes(0, null, 0, 0, 0);
+
+ using (var serializer = new Serializer(stream, original, leaveOpen: true))
+ {
+ }
+
+ stream.Position = 0;
+
+ using (var deserializer = new Deserializer(stream, "test", leaveOpen: true, SerializationSettings.Default))
+ {
+ deserializer.RegisterFactory(typeof(PrimitiveTypes), () => new PrimitiveTypes());
+ PrimitiveTypes deserialized;
+ deserializer.GetEntryObject(out deserialized);
+ Assert.Null(deserialized.StringValue);
+ }
+ }
+
+ [Fact]
+ public void SerializeEmptyString()
+ {
+ var stream = new MemoryStream();
+ var original = new PrimitiveTypes(0, "", 0, 0, 0);
+
+ using (var serializer = new Serializer(stream, original, leaveOpen: true))
+ {
+ }
+
+ stream.Position = 0;
+
+ using (var deserializer = new Deserializer(stream, "test", leaveOpen: true, SerializationSettings.Default))
+ {
+ deserializer.RegisterFactory(typeof(PrimitiveTypes), () => new PrimitiveTypes());
+ PrimitiveTypes deserialized;
+ deserializer.GetEntryObject(out deserialized);
+ Assert.Equal("", deserialized.StringValue);
+ }
+ }
+
+ [Fact]
+ public void SerializeLargeString()
+ {
+ var largeString = new string('A', 10000);
+ var stream = new MemoryStream();
+ var original = new PrimitiveTypes(0, largeString, 0, 0, 0);
+
+ using (var serializer = new Serializer(stream, original, leaveOpen: true))
+ {
+ }
+
+ stream.Position = 0;
+
+ using (var deserializer = new Deserializer(stream, "test", leaveOpen: true, SerializationSettings.Default))
+ {
+ deserializer.RegisterFactory(typeof(PrimitiveTypes), () => new PrimitiveTypes());
+ PrimitiveTypes deserialized;
+ deserializer.GetEntryObject(out deserialized);
+ Assert.Equal(largeString, deserialized.StringValue);
+ }
+ }
+
+ [Fact]
+ public void SerializeMinMaxValues()
+ {
+ var stream = new MemoryStream();
+ var original1 = new PrimitiveTypes(int.MinValue, "min", byte.MinValue, short.MinValue, long.MinValue);
+
+ using (var serializer = new Serializer(stream, original1, leaveOpen: true))
+ {
+ }
+
+ stream.Position = 0;
+
+ using (var deserializer = new Deserializer(stream, "test", leaveOpen: true, SerializationSettings.Default))
+ {
+ deserializer.RegisterFactory(typeof(PrimitiveTypes), () => new PrimitiveTypes());
+ PrimitiveTypes deserialized;
+ deserializer.GetEntryObject(out deserialized);
+ Assert.Equal(byte.MinValue, deserialized.ByteValue);
+ Assert.Equal(short.MinValue, deserialized.ShortValue);
+ Assert.Equal(int.MinValue, deserialized.IntValue);
+ Assert.Equal(long.MinValue, deserialized.LongValue);
+ }
+
+ stream.SetLength(0);
+ stream.Position = 0;
+
+ var original2 = new PrimitiveTypes(int.MaxValue, "max", byte.MaxValue, short.MaxValue, long.MaxValue);
+
+ using (var serializer = new Serializer(stream, original2, leaveOpen: true))
+ {
+ }
+
+ stream.Position = 0;
+
+ using (var deserializer = new Deserializer(stream, "test", leaveOpen: true, SerializationSettings.Default))
+ {
+ deserializer.RegisterFactory(typeof(PrimitiveTypes), () => new PrimitiveTypes());
+ PrimitiveTypes deserialized;
+ deserializer.GetEntryObject(out deserialized);
+ Assert.Equal(byte.MaxValue, deserialized.ByteValue);
+ Assert.Equal(short.MaxValue, deserialized.ShortValue);
+ Assert.Equal(int.MaxValue, deserialized.IntValue);
+ Assert.Equal(long.MaxValue, deserialized.LongValue);
+ }
+ }
+ }
+}
diff --git a/src/FastSerialization.Tests/StreamLabelTests.cs b/src/FastSerialization.Tests/StreamLabelTests.cs
new file mode 100644
index 000000000..b94dbc556
--- /dev/null
+++ b/src/FastSerialization.Tests/StreamLabelTests.cs
@@ -0,0 +1,107 @@
+using System;
+using System.IO;
+using FastSerialization;
+using Xunit;
+
+namespace FastSerializationTests
+{
+ ///
+ /// Test object for StreamLabel testing
+ ///
+ public class StreamLabelTestObject : IFastSerializable
+ {
+ public int Value1 { get; set; }
+ public int Value2 { get; set; }
+ public int Value3 { get; set; }
+
+ public StreamLabelTestObject() { }
+
+ public StreamLabelTestObject(int v1, int v2, int v3)
+ {
+ Value1 = v1;
+ Value2 = v2;
+ Value3 = v3;
+ }
+
+ public void ToStream(Serializer serializer)
+ {
+ serializer.Write(Value1);
+ serializer.Write(Value2);
+ serializer.Write(Value3);
+ }
+
+ public void FromStream(Deserializer deserializer)
+ {
+ Value1 = deserializer.ReadInt();
+ Value2 = deserializer.ReadInt();
+ Value3 = deserializer.ReadInt();
+ }
+ }
+
+ ///
+ /// Tests for serialization settings
+ ///
+ public class StreamLabelTests
+ {
+ [Fact]
+ public void StreamLabelInvalidValue()
+ {
+ Assert.Equal(-1L, (long)StreamLabel.Invalid);
+ }
+
+ [Fact]
+ public void SerializationSettingsNotNull()
+ {
+ var settings = SerializationSettings.Default;
+ Assert.NotNull(settings);
+ }
+
+ [Fact]
+ public void SerializationSettingsWithStreamLabelWidth()
+ {
+ var settings = SerializationSettings.Default.WithStreamLabelWidth(StreamLabelWidth.FourBytes);
+ Assert.NotNull(settings);
+ }
+
+ [Fact]
+ public void SerializationSettingsWithStreamReaderAlignment()
+ {
+ var settings = SerializationSettings.Default.WithStreamReaderAlignment(StreamReaderAlignment.OneByte);
+ Assert.NotNull(settings);
+ }
+
+ [Fact]
+ public void SerializationSettingsChaining()
+ {
+ var settings = SerializationSettings.Default
+ .WithStreamLabelWidth(StreamLabelWidth.FourBytes)
+ .WithStreamReaderAlignment(StreamReaderAlignment.FourBytes);
+
+ Assert.NotNull(settings);
+ }
+
+ [Fact]
+ public void SerializeWithDifferentSettings()
+ {
+ var stream = new MemoryStream();
+ var settings = SerializationSettings.Default.WithStreamLabelWidth(StreamLabelWidth.FourBytes);
+ var original = new StreamLabelTestObject(100, 200, 300);
+
+ using (var serializer = new Serializer(stream, original, leaveOpen: true))
+ {
+ }
+
+ stream.Position = 0;
+
+ using (var deserializer = new Deserializer(stream, "test", leaveOpen: true, settings))
+ {
+ deserializer.RegisterFactory(typeof(StreamLabelTestObject), () => new StreamLabelTestObject());
+ StreamLabelTestObject deserialized;
+ deserializer.GetEntryObject(out deserialized);
+ Assert.Equal(original.Value1, deserialized.Value1);
+ Assert.Equal(original.Value2, deserialized.Value2);
+ Assert.Equal(original.Value3, deserialized.Value3);
+ }
+ }
+ }
+}
diff --git a/src/FastSerialization.Tests/app.config b/src/FastSerialization.Tests/app.config
new file mode 100644
index 000000000..a5c693d53
--- /dev/null
+++ b/src/FastSerialization.Tests/app.config
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/FastSerialization.Tests/xunit.runner.json b/src/FastSerialization.Tests/xunit.runner.json
new file mode 100644
index 000000000..f268328f9
--- /dev/null
+++ b/src/FastSerialization.Tests/xunit.runner.json
@@ -0,0 +1,4 @@
+{
+ "methodDisplay": "method",
+ "shadowCopy": false
+}
diff --git a/src/FastSerialization/FastSerialization.csproj b/src/FastSerialization/FastSerialization.csproj
index 7944e5d50..59988c3cf 100644
--- a/src/FastSerialization/FastSerialization.csproj
+++ b/src/FastSerialization/FastSerialization.csproj
@@ -26,7 +26,7 @@
-
+
@@ -35,7 +35,7 @@
Microsoft400
StrongName
-
+
diff --git a/src/HeapDump/GCHeapDumper.cs b/src/HeapDump/GCHeapDumper.cs
index cb2468d0b..7a970428b 100644
--- a/src/HeapDump/GCHeapDumper.cs
+++ b/src/HeapDump/GCHeapDumper.cs
@@ -21,6 +21,8 @@
using Microsoft.Diagnostics.Utilities;
using Microsoft.Diagnostics.HeapDump;
using Azure.Core;
+using Azure.Identity;
+
#if CROSS_GENERATION_LIVENESS
@@ -38,10 +40,10 @@ public class GCHeapDumper
/// to dump a heap.
///
///
- public GCHeapDumper(TextWriter log, TokenCredential symbolServerAuthCredential = null)
+ public GCHeapDumper(TextWriter log)
{
m_origLog = log;
- m_symbolServerAuthCredential = symbolServerAuthCredential;
+ SymbolsAuthTokenCredential = new InteractiveBrowserCredential();
m_copyOfLog = new StringWriter();
m_log = new TeeTextWriter(m_copyOfLog, m_origLog);
@@ -171,6 +173,12 @@ private CollectionMetadata CaptureLiveHeapDump(int processID)
m_log.WriteLine("Process Has DotNet: {0} Has JScript: {1} Has ClrDll: {2} HasMrt {3} HasCoreClr {4}", hasDotNet, hasJScript, hasClrDll, hasMrt, hasCoreClr);
+ if (!hasDotNet && !hasJScript && !hasMrt && !hasCoreClr)
+ {
+ m_log.WriteLine("No supported runtime type detected, going to assume native AOT.");
+ hasMrt = true;
+ }
+
if (hasClrDll && hasJScript)
{
m_log.WriteLine("[Detected both a JScript and .NET heap, forcing a GC before doing a heap dump.]");
@@ -278,11 +286,11 @@ private DataTarget InitializeClrRuntime(string processDumpFile, int processID, o
{
try
{
- dataTarget = DataTarget.CreateSnapshotAndAttach(processID, m_symbolServerAuthCredential);
+ dataTarget = DataTarget.CreateSnapshotAndAttach(processID, SymbolsAuthTokenCredential);
}
catch
{
- dataTarget = DataTarget.AttachToProcess(processID, Freeze, m_symbolServerAuthCredential);
+ dataTarget = DataTarget.AttachToProcess(processID, Freeze, SymbolsAuthTokenCredential);
}
}
else
@@ -292,7 +300,7 @@ private DataTarget InitializeClrRuntime(string processDumpFile, int processID, o
UseOSMemoryFeatures = false // disable AWE
};
- dataTarget = DataTarget.LoadDump(processDumpFile, cacheOptions, m_symbolServerAuthCredential);
+ dataTarget = DataTarget.LoadDump(processDumpFile, cacheOptions, SymbolsAuthTokenCredential);
}
if (dataTarget.DataReader.PointerSize != IntPtr.Size)
@@ -385,6 +393,12 @@ private DataTarget InitializeClrRuntime(string processDumpFile, int processID, o
///
public ulong PromotedBytesThreshold;
+
+ ///
+ /// The token credential to use for symbol server authentication.
+ ///
+ public TokenCredential SymbolsAuthTokenCredential;
+
///
/// Force a .NET GC on a particular process.
///
@@ -893,8 +907,8 @@ private void DumpDotNetHeapDataWorker(DataTarget dataTarget, ClrRuntime[] runtim
m_log.WriteLine("We are retrying the dump so we scale the max by {0} to the value {1}", retryScale, m_maxNodeCount);
}
- // We assume that object on average are 8 object pointers.
- int estimatedObjectCount = (int)(totalGCSize / ((uint)(8 * IntPtr.Size)));
+ // We assume that object on average are 8 object pointers.
+ int estimatedObjectCount = (totalGCSize > (8 * (ulong)IntPtr.Size * (ulong)Int32.MaxValue)) ? Int32.MaxValue : (int)(totalGCSize / ((uint)(8 * IntPtr.Size)));
m_log.WriteLine("Estimated number of objects = {0:n0}", estimatedObjectCount);
// We force the node count to be this max node count if we are within a factor of 2.
@@ -1573,8 +1587,6 @@ private NodeTypeIndex GetTypeIndexForName(string typeName, string moduleName, in
private StringWriter m_copyOfLog; // We keep a copy of all logged messages here to append to output file.
private Stopwatch m_sw; // We keep track of how long it takes.
- private TokenCredential m_symbolServerAuthCredential;
-
private GCHeapDump m_gcHeapDump; // The image of what we are putting in the file
private NodeIndex m_JSRoot = NodeIndex.Invalid; // The root of the JS heap
private NodeIndex m_dotNetRoot = NodeIndex.Invalid; // The root of the .NET heap
diff --git a/src/HeapDump/HeapDump.csproj b/src/HeapDump/HeapDump.csproj
index f1fba622b..d10211bdf 100644
--- a/src/HeapDump/HeapDump.csproj
+++ b/src/HeapDump/HeapDump.csproj
@@ -31,11 +31,11 @@
-
+
-
+
@@ -45,14 +45,14 @@
-
+
-
+
@@ -68,7 +68,10 @@
Utilities\StringUtilities.cs
-
+
+ Utilities\SymbolsAuthenticationUtilities.cs
+
+
@@ -92,7 +95,7 @@
Microsoft400
-
+
diff --git a/src/HeapDump/Program.cs b/src/HeapDump/Program.cs
index ad587da45..d8501f79c 100644
--- a/src/HeapDump/Program.cs
+++ b/src/HeapDump/Program.cs
@@ -1,7 +1,7 @@
-ο»Ώusing Azure.Core;
-using Azure.Identity;
-using Microsoft.Diagnostics.Runtime;
+ο»Ώusing Microsoft.Diagnostics.Runtime;
+using Microsoft.Diagnostics.Utilities;
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
@@ -60,14 +60,7 @@ private static int MainWorker(string[] args)
string inputSpec = null;
int minSecForTrigger = -1;
- DefaultAzureCredential symbolsTokenCredential = new DefaultAzureCredential(
- new DefaultAzureCredentialOptions()
- {
- ExcludeInteractiveBrowserCredential = false,
- ExcludeManagedIdentityCredential = true,
- });
-
- var dumper = new GCHeapDumper(Console.Out, symbolsTokenCredential);
+ var dumper = new GCHeapDumper(Console.Out);
for (int curArgIdx = 0; curArgIdx < args.Length; curArgIdx++)
{
@@ -177,6 +170,22 @@ private static int MainWorker(string[] args)
}
Console.WriteLine("Generation To Trigger: " + dumper.GenerationToTrigger);
}
+ else if (arg.StartsWith("/SymbolsAuth:", StringComparison.OrdinalIgnoreCase))
+ {
+ string authTypesStr = arg.Substring(13);
+ if (TryParseSymbolsAuthenticationTypes(authTypesStr, out SymbolsAuthenticationType parsedAuthTypes))
+ {
+ dumper.SymbolsAuthTokenCredential = SymbolsAuthenticationUtilities.CreateTokenCredential(parsedAuthTypes);
+ Console.WriteLine("Set symbols authentication credentials to {0}", authTypesStr);
+ }
+ else
+ {
+ Console.WriteLine("Invalid value for /SymbolsAuth: {0}", authTypesStr);
+ var validValues = string.Join(", ", Enum.GetNames(typeof(SymbolsAuthenticationType)));
+ Console.WriteLine("Valid values are: {0} (can be combined with commas)", validValues);
+ goto Usage;
+ }
+ }
else
{
Console.WriteLine("Unknown qualifier: {0}", arg);
@@ -336,6 +345,34 @@ public static int GetProcessID(string processNameOrID)
return -1;
}
+ private static bool TryParseSymbolsAuthenticationTypes(string authTypesStr, out SymbolsAuthenticationType result)
+ {
+ result = (SymbolsAuthenticationType)0;
+
+ if (string.IsNullOrWhiteSpace(authTypesStr))
+ {
+ return false;
+ }
+
+ var parts = authTypesStr.Split(',');
+ foreach (var part in parts)
+ {
+ var trimmedPart = part.Trim();
+ if (Enum.TryParse(trimmedPart, true, out var parsedType))
+ {
+ result |= parsedType;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ return result != (SymbolsAuthenticationType)0;
+ }
+
+
+
private static int PointerSizeForProcess(int processID)
{
if (!Environment.Is64BitOperatingSystem)
diff --git a/src/HeapDumpDLL/HeapDumpDLL.csproj b/src/HeapDumpDLL/HeapDumpDLL.csproj
index bd279fc24..10f2ba9e3 100644
--- a/src/HeapDumpDLL/HeapDumpDLL.csproj
+++ b/src/HeapDumpDLL/HeapDumpDLL.csproj
@@ -24,14 +24,14 @@
-
+
-
+
-
+
$(MicrosoftDiagnosticsRuntimePath)
@@ -39,7 +39,7 @@
-
+
@@ -78,7 +78,7 @@
Microsoft400
StrongName
-
+
diff --git a/src/LinuxTracing.Tests/EventParseTests.cs b/src/LinuxTracing.Tests/EventParseTests.cs
index 5c36074b2..cbbb77ef9 100644
--- a/src/LinuxTracing.Tests/EventParseTests.cs
+++ b/src/LinuxTracing.Tests/EventParseTests.cs
@@ -6,6 +6,7 @@
using System.Linq;
using Xunit;
using System.Linq.Expressions;
+using System;
namespace LinuxTracingTests
{
@@ -65,7 +66,7 @@ ScheduleSwitch[] switches
Assert.Equal(pids[i], linuxEvent.ProcessID);
Assert.Equal(tids[i], linuxEvent.ThreadID);
Assert.Equal(cpus[i], linuxEvent.CpuNumber);
- Assert.Equal(times[i], linuxEvent.TimeMSec);
+ Assert.True(Math.Abs(times[i] - linuxEvent.TimeMSec) <= 0.0001);
Assert.Equal(timeProperties[i], linuxEvent.TimeProperty);
Assert.Equal(events[i], linuxEvent.EventName);
Assert.Equal(eventProperties[i], linuxEvent.EventProperty);
@@ -257,5 +258,56 @@ public void CornerCaseProcessNames()
eventKinds: null,
switches: null);
}
+
+ [Fact]
+ public void DiskIoNoStacks()
+ {
+ string path = Constants.GetTestingPerfDumpPath("disk_io_no_stacks");
+ HeaderTest(path, blockedTime: false,
+ commands: new string[] { "fio", "swapper", "fio", "swapper" },
+ pids: new int[] { 236193, 0, 236193, 0 },
+ tids: new int[] { 236193, 0, 236193, 0 },
+ cpus: new int[] { 21, 12, 21, 12 },
+ times: new double[] { 1791.544, 1791.615, 1791.628, 1791.699 },
+ timeProperties: new int[] { 1, 1, 1, 1 },
+ events: new string[] { "block", "block", "block", "block" },
+ eventProperties: new string[] { "block_rq_issue: 259,76 R 4096 () 57622160 + 8 [fio] ffffffff8beead90 blk_mq_start_request ([kernel.kallsyms])", "block_rq_complete: 259,76 R () 57622160 + 8 [0] ffffffff8bee2b4c blk_update_request ([kernel.kallsyms])", "block_rq_issue: 259,76 R 4096 () 82612968 + 8 [fio] ffffffff8beead90 blk_mq_start_request ([kernel.kallsyms])", "block_rq_complete: 259,76 R () 82612968 + 8 [0] ffffffff8bee2b4c blk_update_request ([kernel.kallsyms])" },
+ eventKinds: new EventKind[] { EventKind.BlockRequestIssue, EventKind.BlockRequestComplete, EventKind.BlockRequestIssue, EventKind.BlockRequestComplete },
+ switches: null);
+ }
+
+ [Fact]
+ public void OneWakeup()
+ {
+ string path = Constants.GetTestingPerfDumpPath("one_wakeup");
+ HeaderTest(path, blockedTime: false,
+ commands: new string[] { "swapper", "swapper" },
+ pids: new int[] { 0, 0 },
+ tids: new int[] { 0, 0 },
+ cpus: new int[] { 9, 9 },
+ times: new double[] { 0.0, 0.0 },
+ timeProperties: new int[] { 1, 1 },
+ events: new string[] { "sched", "sched" },
+ eventProperties: new string[] { "sched_wakeup: comm=fio pid=243615 prio=120 target_cpu=031 ffffffff8bad311d ttwu_do_wakeup ([kernel.kallsyms])", "sched_wakeup: task fio:243615 [120] success=1 [031] ffffffff8bad311d ttwu_do_wakeup ([kernel.kallsyms])" },
+ eventKinds: new EventKind[] { EventKind.Wakeup, EventKind.Wakeup },
+ switches: null);
+ }
+
+ [Fact]
+ public void ExecProcess()
+ {
+ string path = Constants.GetTestingPerfDumpPath("exec_process");
+ HeaderTest(path, blockedTime: false,
+ commands: new string[] { "probe-bcache" },
+ pids: new int[] { 286053, 286053 },
+ tids: new int[] { 286053, 286053 },
+ cpus: new int[] { 3, 3 },
+ times: new double[] { 0.0, 0.0 },
+ timeProperties: new int[] { 1, 1 },
+ events: new string[] { "sched" },
+ eventProperties: new string[] { "sched_process_exec: filename=/lib/udev/probe-bcache pid=286053 old_pid=286053" },
+ eventKinds: new EventKind[] { EventKind.ProcessExec },
+ switches: null);
+ }
}
}
diff --git a/src/LinuxTracing.Tests/LinuxTracing.Tests.csproj b/src/LinuxTracing.Tests/LinuxTracing.Tests.csproj
index a1f7c4ac8..28021630a 100644
--- a/src/LinuxTracing.Tests/LinuxTracing.Tests.csproj
+++ b/src/LinuxTracing.Tests/LinuxTracing.Tests.csproj
@@ -14,8 +14,8 @@
-
-
+
+
diff --git a/src/LinuxTracing.Tests/Sources/disk_io_no_stacks.perf.data.dump b/src/LinuxTracing.Tests/Sources/disk_io_no_stacks.perf.data.dump
new file mode 100644
index 000000000..02dddbf4a
--- /dev/null
+++ b/src/LinuxTracing.Tests/Sources/disk_io_no_stacks.perf.data.dump
@@ -0,0 +1,4 @@
+ο»Ώ fio 236193/236193 [021] 1.791544: 1 block:block_rq_issue: 259,76 R 4096 () 57622160 + 8 [fio] ffffffff8beead90 blk_mq_start_request ([kernel.kallsyms])
+ swapper 0/0 [012] 1.791615: 1 block:block_rq_complete: 259,76 R () 57622160 + 8 [0] ffffffff8bee2b4c blk_update_request ([kernel.kallsyms])
+ fio 236193/236193 [021] 1.791628: 1 block:block_rq_issue: 259,76 R 4096 () 82612968 + 8 [fio] ffffffff8beead90 blk_mq_start_request ([kernel.kallsyms])
+ swapper 0/0 [012] 1.791699: 1 block:block_rq_complete: 259,76 R () 82612968 + 8 [0] ffffffff8bee2b4c blk_update_request ([kernel.kallsyms])
\ No newline at end of file
diff --git a/src/LinuxTracing.Tests/Sources/exec_process.perf.data.dump b/src/LinuxTracing.Tests/Sources/exec_process.perf.data.dump
new file mode 100644
index 000000000..db7c83b06
--- /dev/null
+++ b/src/LinuxTracing.Tests/Sources/exec_process.perf.data.dump
@@ -0,0 +1 @@
+ο»Ώprobe-bcache 286053/286053 [003] 0.0: 1 sched:sched_process_exec: filename=/lib/udev/probe-bcache pid=286053 old_pid=286053
diff --git a/src/LinuxTracing.Tests/Sources/no_stack_frames.perf.data.dump b/src/LinuxTracing.Tests/Sources/no_stack_frames.perf.data.dump
index d83521afa..2853ed82a 100644
--- a/src/LinuxTracing.Tests/Sources/no_stack_frames.perf.data.dump
+++ b/src/LinuxTracing.Tests/Sources/no_stack_frames.perf.data.dump
@@ -1,4 +1,4 @@
-ο»Ώcomm 0/0 [000] 0.0: event: event_properties
+ο»Ώcomm 0/0 [000] 0.0: event/call-graph=no/: event_properties
comm 0/0 [000] 0.0: event: event_properties
address symbol (module)
\ No newline at end of file
diff --git a/src/LinuxTracing.Tests/Sources/one_wakeup.perf.data.dump b/src/LinuxTracing.Tests/Sources/one_wakeup.perf.data.dump
new file mode 100644
index 000000000..b03decd7b
--- /dev/null
+++ b/src/LinuxTracing.Tests/Sources/one_wakeup.perf.data.dump
@@ -0,0 +1,2 @@
+ο»Ώ swapper 0/0 [009] 0.0: 1 sched:sched_wakeup: comm=fio pid=243615 prio=120 target_cpu=031 ffffffff8bad311d ttwu_do_wakeup ([kernel.kallsyms])
+ swapper 0/0 [009] 0.0: 1 sched:sched_wakeup: task fio:243615 [120] success=1 [031] ffffffff8bad311d ttwu_do_wakeup ([kernel.kallsyms])
\ No newline at end of file
diff --git a/src/MemoryGraph/MemoryGraph.csproj b/src/MemoryGraph/MemoryGraph.csproj
index c8b8b4ae7..b0899bb09 100644
--- a/src/MemoryGraph/MemoryGraph.csproj
+++ b/src/MemoryGraph/MemoryGraph.csproj
@@ -24,7 +24,7 @@
-
+
@@ -33,7 +33,7 @@
Microsoft400
StrongName
-
+
diff --git a/src/NuGetPackageSigning/NuGetPackageSigning.csproj b/src/NuGetPackageSigning/NuGetPackageSigning.csproj
index 36a4141d8..9810545f0 100644
--- a/src/NuGetPackageSigning/NuGetPackageSigning.csproj
+++ b/src/NuGetPackageSigning/NuGetPackageSigning.csproj
@@ -28,7 +28,7 @@
PreserveNewest
-
+
diff --git a/src/PerfView.TestUtilities/DebugAssertionTests.cs b/src/PerfView.TestUtilities/DebugAssertionTests.cs
index 9928e1a2b..710abc8e7 100644
--- a/src/PerfView.TestUtilities/DebugAssertionTests.cs
+++ b/src/PerfView.TestUtilities/DebugAssertionTests.cs
@@ -11,11 +11,14 @@
///
/// This file can be linked into any project which needs to validate that assertions are behaving correctly
/// for the purpose of unit testing.
+ /// On .NET Framework, assertion failures throw via registered in
+ /// app.config. On .NET 5+, the DefaultTraceListener already throws on assertion failures, so no
+ /// additional listener configuration is needed.
///
public class DebugAssertionTests
{
#if DEBUG
- [Fact(Skip = "https://github.com/microsoft/perfview/issues/1571")]
+ [Fact]
public void TestDebugAssertThrowsException()
{
Debug.Assert(true);
@@ -23,20 +26,20 @@ public void TestDebugAssertThrowsException()
Assert.ThrowsAny(() => Debug.Assert(false));
}
- [Fact(Skip = "https://github.com/microsoft/perfview/issues/1571")]
+ [Fact]
public void TestDebugFailThrowsException()
{
Assert.ThrowsAny(() => Debug.Fail("Bad things"));
}
#endif
- [Fact(Skip = "https://github.com/microsoft/perfview/issues/1571")]
+ [Fact]
public void TestTraceAssertThrowsException()
{
Assert.ThrowsAny(() => Trace.Assert(false));
}
- [Fact(Skip = "https://github.com/microsoft/perfview/issues/1571")]
+ [Fact]
public void TestTraceFailThrowsException()
{
Assert.ThrowsAny(() => Trace.Fail("Bad things"));
diff --git a/src/PerfView.TestUtilities/PerfView.TestUtilities.csproj b/src/PerfView.TestUtilities/PerfView.TestUtilities.csproj
index df2503d7c..3903de94f 100644
--- a/src/PerfView.TestUtilities/PerfView.TestUtilities.csproj
+++ b/src/PerfView.TestUtilities/PerfView.TestUtilities.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/src/PerfView.TestUtilities/ThrowingTraceListener.cs b/src/PerfView.TestUtilities/ThrowingTraceListener.cs
index b57b69fed..52eb976ee 100644
--- a/src/PerfView.TestUtilities/ThrowingTraceListener.cs
+++ b/src/PerfView.TestUtilities/ThrowingTraceListener.cs
@@ -5,7 +5,10 @@
namespace PerfView.TestUtilities
{
- // To enable this for a process, add the following to the app.config for the project:
+ // This listener converts Debug.Assert/Trace.Assert failures into xUnit test failures
+ // by throwing from the Fail() method.
+ //
+ // On .NET Framework (net462), this must be registered via app.config :
//
//
//
@@ -17,6 +20,13 @@ namespace PerfView.TestUtilities
//
//
//
+ //
+ // On .NET 5+, the app.config section is NOT
+ // processed, so this listener is never registered. However, the DefaultTraceListener
+ // on .NET 5+ already throws on assert failures, so no additional configuration is
+ // needed β Debug.Assert and Trace.Assert will throw without this listener.
+ // Should this behavior change, the ThrowingTraceListener tests will fail, which will tell us we
+ // need to do something to re-enable this listener.
public sealed class ThrowingTraceListener : TraceListener
{
public override void Fail(string message, string detailMessage)
diff --git a/src/PerfView.Tests/Accessibility/FocusVisualTests.cs b/src/PerfView.Tests/Accessibility/FocusVisualTests.cs
new file mode 100644
index 000000000..c74351750
--- /dev/null
+++ b/src/PerfView.Tests/Accessibility/FocusVisualTests.cs
@@ -0,0 +1,140 @@
+using PerfView;
+using System;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Documents;
+using System.Windows.Media;
+using System.Windows.Shapes;
+using REghZyFramework.Themes;
+using Xunit;
+
+namespace PerfViewTests.Accessibility
+{
+ public class FocusVisualTests
+ {
+ private const double MinimumContrastRatio = 3.0;
+
+ [WpfFact]
+ public void AccessibleFocusVisualMeetsContrastRequirements()
+ {
+ AssertThemeFocusVisual(
+ "LightTheme.xaml",
+ Colors.White,
+ GetThemeColor("LightTheme.xaml", "ContainerBackground"),
+ GetThemeColor("LightTheme.xaml", "ControlDefaultBackground"));
+
+ AssertThemeFocusVisual(
+ "DarkTheme.xaml",
+ GetThemeColor("DarkTheme.xaml", "ContainerBackground"),
+ GetThemeColor("DarkTheme.xaml", "ControlDefaultBackground"));
+ }
+
+ [WpfFact]
+ public void ReportedControlsUseAccessibleFocusVisual()
+ {
+ ResourceDictionary theme = LoadTheme("LightTheme.xaml");
+
+ MainWindow mainWindow = null;
+ try
+ {
+ App.CommandLineArgs = new CommandLineArgs();
+ App.CommandProcessor = new CommandProcessor();
+ mainWindow = new MainWindow(true);
+ mainWindow.Resources.MergedDictionaries.Add(theme);
+
+ Style expectedStyle = (Style)theme["AccessibleFocusVisual"];
+ Assert.Same(expectedStyle, mainWindow.Body.FocusVisualStyle);
+
+ StatusBar statusBar = mainWindow.StatusBar;
+ Assert.Same(expectedStyle, ((TextBox)statusBar.FindName("m_StatusMessage")).FocusVisualStyle);
+ Assert.Same(expectedStyle, ((Button)statusBar.FindName("m_LogButton")).FocusVisualStyle);
+ Assert.Same(expectedStyle, ((Button)statusBar.FindName("m_CancelButton")).FocusVisualStyle);
+
+ Hyperlink welcomeLink = mainWindow.Body.Document.Blocks
+ .OfType()
+ .SelectMany(paragraph => paragraph.Inlines)
+ .OfType()
+ .First();
+ Assert.Same(expectedStyle, welcomeLink.FocusVisualStyle);
+ }
+ finally
+ {
+ mainWindow?.Close();
+ }
+ }
+
+ #region private
+
+ private static void AssertThemeFocusVisual(string themeName, params Color[] adjacentColors)
+ {
+ ResourceDictionary theme = LoadTheme(themeName);
+ SolidColorBrush focusBrush = (SolidColorBrush)theme["AccessibleFocusVisualBrush"];
+ Style focusStyle = (Style)theme["AccessibleFocusVisual"];
+ Setter templateSetter = focusStyle.Setters
+ .OfType()
+ .Single(setter => setter.Property == Control.TemplateProperty);
+ Rectangle focusRectangle = (Rectangle)((ControlTemplate)templateSetter.Value).LoadContent();
+
+ Assert.True(focusRectangle.StrokeThickness >= 2);
+ Assert.Equal(focusBrush.Color, ((SolidColorBrush)focusRectangle.Stroke).Color);
+
+ foreach (Color adjacentColor in adjacentColors)
+ {
+ double contrastRatio = GetContrastRatio(focusBrush.Color, adjacentColor);
+ Assert.True(
+ contrastRatio >= MinimumContrastRatio,
+ $"{themeName} focus color {focusBrush.Color} has a contrast ratio of only {contrastRatio:F3}:1 against {adjacentColor}.");
+ }
+ }
+
+ private static Color GetThemeColor(string themeName, string resourceKey)
+ {
+ return ((SolidColorBrush)LoadTheme(themeName)[resourceKey]).Color;
+ }
+
+ private static ResourceDictionary LoadTheme(string themeName)
+ {
+ if (themeName == "LightTheme.xaml")
+ {
+ var theme = new LightTheme();
+ theme.InitializeComponent();
+ return theme;
+ }
+
+ if (themeName == "DarkTheme.xaml")
+ {
+ var theme = new DarkTheme();
+ theme.InitializeComponent();
+ return theme;
+ }
+
+ throw new ArgumentException($"Unknown theme '{themeName}'.", nameof(themeName));
+ }
+
+ private static double GetContrastRatio(Color first, Color second)
+ {
+ double firstLuminance = GetRelativeLuminance(first);
+ double secondLuminance = GetRelativeLuminance(second);
+ return (Math.Max(firstLuminance, secondLuminance) + 0.05) /
+ (Math.Min(firstLuminance, secondLuminance) + 0.05);
+ }
+
+ private static double GetRelativeLuminance(Color color)
+ {
+ return (0.2126 * GetLinearChannel(color.R)) +
+ (0.7152 * GetLinearChannel(color.G)) +
+ (0.0722 * GetLinearChannel(color.B));
+ }
+
+ private static double GetLinearChannel(byte channel)
+ {
+ double value = channel / 255.0;
+ return value <= 0.04045
+ ? value / 12.92
+ : Math.Pow((value + 0.055) / 1.055, 2.4);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/PerfView.Tests/DebuggerStackSourceTests.cs b/src/PerfView.Tests/DebuggerStackSourceTests.cs
new file mode 100644
index 000000000..4fda66467
--- /dev/null
+++ b/src/PerfView.Tests/DebuggerStackSourceTests.cs
@@ -0,0 +1,78 @@
+using Diagnostics.Tracing.StackSources;
+using Microsoft.Diagnostics.Tracing.Stacks;
+using System.IO;
+using Xunit;
+
+namespace PerfViewTests
+{
+ public class DebuggerStackSourceTests
+ {
+ [Fact]
+ public void TestLastSampleIsNotDropped()
+ {
+ // Create a sample cdbstack file with two samples
+ var cdbStackContent = @"Call Site
+coreclr!JIT_MonEnterWorker_Portable
+System_Windows_ni!MS.Internal.ManagedPeerTable.TryGetManagedPeer(IntPtr, Boolean, System.Object ByRef)
+Call Site
+kernel32!BaseThreadInitThunk
+ntdll!RtlUserThreadStart";
+
+ DebuggerStackSource stackSource;
+ using (var reader = new StringReader(cdbStackContent))
+ {
+ stackSource = new DebuggerStackSource(reader);
+ }
+
+ // Count the samples
+ int sampleCount = 0;
+ stackSource.ForEach(sample => sampleCount++);
+
+ // We should have 2 samples, but the bug causes only 1 to be added
+ Assert.Equal(2, sampleCount);
+ }
+
+ [Fact]
+ public void TestSingleSampleIsAdded()
+ {
+ // Create a sample cdbstack file with a single sample (no subsequent "Call Site")
+ var cdbStackContent = @"Call Site
+coreclr!JIT_MonEnterWorker_Portable
+System_Windows_ni!MS.Internal.ManagedPeerTable.TryGetManagedPeer(IntPtr, Boolean, System.Object ByRef)";
+
+ DebuggerStackSource stackSource;
+ using (var reader = new StringReader(cdbStackContent))
+ {
+ stackSource = new DebuggerStackSource(reader);
+ }
+
+ // Count the samples
+ int sampleCount = 0;
+ stackSource.ForEach(sample => sampleCount++);
+
+ // We should have 1 sample
+ Assert.Equal(1, sampleCount);
+ }
+
+ [Fact]
+ public void TestSampleMetricIsSet()
+ {
+ // Create a sample cdbstack file with one sample
+ var cdbStackContent = @"Call Site
+coreclr!JIT_MonEnterWorker_Portable
+System_Windows_ni!MS.Internal.ManagedPeerTable.TryGetManagedPeer(IntPtr, Boolean, System.Object ByRef)";
+
+ DebuggerStackSource stackSource;
+ using (var reader = new StringReader(cdbStackContent))
+ {
+ stackSource = new DebuggerStackSource(reader);
+ }
+
+ // Check that metric is set to 1 for each sample
+ stackSource.ForEach(sample =>
+ {
+ Assert.Equal(1, sample.Metric);
+ });
+ }
+ }
+}
diff --git a/src/PerfView.Tests/DiagSessionPerfViewFileTests.cs b/src/PerfView.Tests/DiagSessionPerfViewFileTests.cs
new file mode 100644
index 000000000..3a435afa2
--- /dev/null
+++ b/src/PerfView.Tests/DiagSessionPerfViewFileTests.cs
@@ -0,0 +1,46 @@
+using PerfView;
+using System.IO;
+using System.Linq;
+using Xunit;
+
+namespace PerfViewTests
+{
+ public class DiagSessionPerfViewFileTests
+ {
+ [Theory]
+ [InlineData(@"..\..\victim", "victim")]
+ [InlineData(@"\..\..\Startup", "Startup")]
+ [InlineData(@"C:\temp\symbols", "symbols")]
+ [InlineData("symbols/cache", "cache")]
+ [InlineData("symbols\\cache", "cache")]
+ [InlineData("foo.bar", "foo")]
+ [InlineData("plain", "plain")]
+ [InlineData("C:relative", "relative")]
+ [InlineData("foo:bar", "bar")]
+ public void GetSafeDiagSessionResourceDirectoryName_StripsPathComponents(string resourceName, string expected)
+ {
+ string safeName = DiagSessionPerfViewFile.GetSafeDiagSessionResourceDirectoryName(resourceName);
+
+ Assert.Equal(expected, safeName);
+ Assert.DoesNotContain(Path.DirectorySeparatorChar, safeName);
+ Assert.DoesNotContain(Path.AltDirectorySeparatorChar, safeName);
+ Assert.DoesNotContain(safeName, c => Path.GetInvalidFileNameChars().Contains(c));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(".")]
+ [InlineData("..")]
+ [InlineData(@"\")]
+ [InlineData("/")]
+ [InlineData(@"foo\..")]
+ [InlineData(@"foo\.")]
+ public void GetSafeDiagSessionResourceDirectoryName_RejectsUnsafeNames(string resourceName)
+ {
+ string safeName = DiagSessionPerfViewFile.GetSafeDiagSessionResourceDirectoryName(resourceName);
+
+ Assert.Null(safeName);
+ }
+ }
+}
diff --git a/src/PerfView.Tests/Dialogs/XamlMessageBoxTests.cs b/src/PerfView.Tests/Dialogs/XamlMessageBoxTests.cs
new file mode 100644
index 000000000..50a59c926
--- /dev/null
+++ b/src/PerfView.Tests/Dialogs/XamlMessageBoxTests.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Media;
+using PerfView.Dialogs;
+using Xunit;
+
+namespace PerfViewTests.Dialogs
+{
+ ///
+ /// Regression tests for threading behavior.
+ /// See https://github.com/microsoft/perfview/issues/2300
+ ///
+ public class XamlMessageBoxTests
+ {
+ ///
+ /// Verifies that auto-dispatches
+ /// to the UI thread when called from a background thread, rather than throwing
+ /// "The calling thread must be STA, because many UI components require this."
+ /// Also verifies that calling from the UI thread directly still works (no-op dispatch).
+ /// This is the core regression test for issue #2300.
+ ///
+#pragma warning disable VSTHRD200 // Keep the original regression test name stable.
+ [WpfFact]
+ public async Task Show_AutoDispatchesToUIThreadFromBackgroundThread()
+#pragma warning restore VSTHRD200
+ {
+ Application app = Application.Current ?? new Application();
+ app.ShutdownMode = ShutdownMode.OnExplicitShutdown;
+ RegisterMinimalThemeResources(app);
+
+ // Auto-close any XamlMessageBox dialogs as soon as they load.
+ RegisterAutoCloseHandler();
+
+ // Part 1: Call directly from the UI thread (dispatch is a no-op).
+ MessageBoxResult uiResult = XamlMessageBox.Show("Test message", "XamlMBTest_UI", MessageBoxButton.OK);
+
+ // Part 2: Call from a background thread β before the fix for issue #2300,
+ // this would throw InvalidOperationException ("The calling thread must be STA")
+ // because XamlMessageBox creates a WPF Window requiring the UI thread.
+ Task backgroundShowTask = Task.Run(() =>
+ XamlMessageBox.Show("Test message", "XamlMBTest_BG", MessageBoxButton.YesNo));
+
+ Task completedTask = await Task.WhenAny(backgroundShowTask, Task.Delay(TimeSpan.FromSeconds(10)));
+ Assert.True(
+ ReferenceEquals(backgroundShowTask, completedTask),
+ "Timed out waiting for XamlMessageBox.Show to dispatch to the WPF test thread.");
+
+ MessageBoxResult bgResult = await backgroundShowTask;
+
+ // Both dialogs were auto-closed without clicking a button, so Result is None.
+ Assert.Equal(MessageBoxResult.None, uiResult);
+ Assert.Equal(MessageBoxResult.None, bgResult);
+ }
+
+ private static bool s_autoCloseHandlerRegistered;
+
+ ///
+ /// Registers a class-level handler that auto-closes any with
+ /// a test caption as soon as it finishes loading. The handler fires inside
+ /// 's nested message loop.
+ ///
+ private static void RegisterAutoCloseHandler()
+ {
+ if (s_autoCloseHandlerRegistered)
+ {
+ return;
+ }
+
+ s_autoCloseHandlerRegistered = true;
+ EventManager.RegisterClassHandler(
+ typeof(Window),
+ FrameworkElement.LoadedEvent,
+ new RoutedEventHandler((sender, args) =>
+ {
+ Window w = sender as Window;
+ if (w != null && w.Title != null && w.Title.StartsWith("XamlMBTest_"))
+ {
+#pragma warning disable VSTHRD001, VSTHRD110 // Loaded is already on the WPF thread; defer Close until loading completes.
+ w.Dispatcher.BeginInvoke((Action)(() => w.Close()));
+#pragma warning restore VSTHRD001, VSTHRD110
+ }
+ }));
+ }
+
+ ///
+ /// Registers the minimal resources needed by MessageBoxWindow.xaml so it
+ /// can be created without loading the full PerfView theme.
+ ///
+ private static void RegisterMinimalThemeResources(Application app)
+ {
+ app.Resources["CustomToolWindowStyle"] = new Style(typeof(Window));
+ app.Resources["ControlDarkerBackground"] = new SolidColorBrush(Colors.LightGray);
+ app.Resources["ControlDefaultBorderBrush"] = new SolidColorBrush(Colors.Gray);
+ }
+ }
+}
diff --git a/src/PerfView.Tests/GuiUtilities/WebBrowserWindowAccessibilityTests.cs b/src/PerfView.Tests/GuiUtilities/WebBrowserWindowAccessibilityTests.cs
new file mode 100644
index 000000000..987a4b056
--- /dev/null
+++ b/src/PerfView.Tests/GuiUtilities/WebBrowserWindowAccessibilityTests.cs
@@ -0,0 +1,80 @@
+using System;
+using System.ComponentModel;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Threading;
+using PerfView.GuiUtilities;
+using Xunit;
+
+namespace PerfViewTests.GuiUtilities
+{
+ public class WebBrowserWindowAccessibilityTests
+ {
+ [WpfFact]
+ public void WpfEscapeRequestsClose()
+ {
+ AssertCloseRequested(window =>
+ {
+ var keyEvent = new KeyEventArgs(
+ Keyboard.PrimaryDevice,
+ PresentationSource.FromVisual(window),
+ 0,
+ Key.Escape)
+ {
+ RoutedEvent = Keyboard.PreviewKeyDownEvent
+ };
+
+ window.RaiseEvent(keyEvent);
+ Assert.True(keyEvent.Handled);
+ });
+ }
+
+ [WpfFact]
+ public void WebViewEscapeMessageRequestsClose()
+ {
+ AssertCloseRequested(window => window.ProcessWebMessage("\"PerfView.CloseWindow\""));
+ }
+
+ private static void AssertCloseRequested(Action requestClose)
+ {
+ var window = new WebBrowserWindow(null)
+ {
+ Content = new Grid(),
+ Style = new Style(typeof(Window))
+ };
+ bool closeRequested = false;
+ CancelEventHandler cancelClose = (sender, e) =>
+ {
+ closeRequested = true;
+ e.Cancel = true;
+ };
+
+ window.Closing += cancelClose;
+ window.Show();
+ window.Activate();
+ DrainDispatcher(window);
+
+ try
+ {
+ requestClose(window);
+ DrainDispatcher(window);
+ Assert.True(closeRequested);
+ }
+ finally
+ {
+ window.Closing -= cancelClose;
+ window.Close();
+ }
+ }
+
+ private static void DrainDispatcher(DispatcherObject dispatcherObject)
+ {
+#pragma warning disable VSTHRD001 // WpfFact already runs this synchronous test on its dedicated UI thread.
+ dispatcherObject.Dispatcher.Invoke(
+ DispatcherPriority.ApplicationIdle,
+ new Action(() => { }));
+#pragma warning restore VSTHRD001
+ }
+ }
+}
diff --git a/src/PerfView.Tests/MainWindowTests.cs b/src/PerfView.Tests/MainWindowTests.cs
new file mode 100644
index 000000000..57acfcd71
--- /dev/null
+++ b/src/PerfView.Tests/MainWindowTests.cs
@@ -0,0 +1,81 @@
+using PerfView;
+using PerfViewTests.Utilities;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Documents;
+using System.Windows.Input;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace PerfViewTests
+{
+ public class MainWindowTests : PerfViewTestBase
+ {
+ public MainWindowTests(ITestOutputHelper testOutputHelper)
+ : base(testOutputHelper)
+ {
+ }
+
+ [WpfFact]
+ public Task TabNavigationReachesMainContentBeforeStatusBarAsync()
+ {
+ return RunUITestAsync(
+ () => Task.FromResult(GuiApp.MainWindow),
+ async window =>
+ {
+ await JoinableTaskFactory.SwitchToMainThreadAsync();
+
+ Grid root = Assert.IsType(window.Content);
+ Grid mainContent = root.Children
+ .OfType()
+ .Single(child => Grid.GetRow(child) == 1);
+ Hyperlink videosLink = window.VideoLink.Inlines
+ .OfType()
+ .Single();
+
+ Assert.Same(videosLink, Keyboard.Focus(videosLink));
+
+ var visitedElements = new HashSet { videosLink };
+ while (!mainContent.IsKeyboardFocusWithin)
+ {
+ Assert.True(MoveFocusToNextElement());
+ Assert.False(window.StatusBar.IsKeyboardFocusWithin);
+
+ IInputElement focusedElement = Keyboard.FocusedElement;
+ Assert.NotNull(focusedElement);
+ Assert.True(
+ visitedElements.Add(focusedElement),
+ "Focus cycled without reaching the main content.");
+ }
+
+ Assert.True(mainContent.IsKeyboardFocusWithin);
+ },
+ window => Task.CompletedTask);
+ }
+
+ #region private
+
+ private static bool MoveFocusToNextElement()
+ {
+ var request = new TraversalRequest(FocusNavigationDirection.Next);
+ IInputElement focusedElement = Keyboard.FocusedElement;
+
+ if (focusedElement is UIElement uiElement)
+ {
+ return uiElement.MoveFocus(request);
+ }
+
+ if (focusedElement is ContentElement contentElement)
+ {
+ return contentElement.MoveFocus(request);
+ }
+
+ return false;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/PerfView.Tests/Memory/PathUtilitiesTests.cs b/src/PerfView.Tests/Memory/PathUtilitiesTests.cs
new file mode 100644
index 000000000..30dbafa6f
--- /dev/null
+++ b/src/PerfView.Tests/Memory/PathUtilitiesTests.cs
@@ -0,0 +1,135 @@
+using Microsoft.Diagnostics.Utilities;
+using Xunit;
+
+namespace PerfViewTests.Memory
+{
+ public class PathUtilitiesTests
+ {
+ [Theory]
+ [InlineData(@"\\server\share\module.dll")]
+ [InlineData(@"\\?\UNC\server\share\module.dll")]
+ [InlineData(@"\\?\unc\server\share\module.dll")]
+ [InlineData(@"\\.\UNC\server\share\module.dll")]
+ [InlineData(@"\\?\GLOBALROOT\Device\Mup\server\share\module.dll")]
+ [InlineData(@"\\?\GLOBALROOT\Device\LanmanRedirector\server\share\module.dll")]
+ [InlineData(@"\??\UNC\server\share\module.dll")]
+ [InlineData(@"\??\unc\server\share\module.dll")]
+ [InlineData("https://server/share/module.dll")]
+ [InlineData("http://server/share/module.dll")]
+ [InlineData("ftp://server/module.dll")]
+ public void IsRemotePathDetectsRemotePaths(string modulePath)
+ {
+ Assert.True(PathUtilities.IsRemotePath(modulePath));
+ }
+
+ [Theory]
+ [InlineData(@"C:\Symbols\foo.pdb\01234567890123456789012345678901FFFFFFFF\foo.pdb")]
+ [InlineData(@"C:\Users\dev\src\bin\foo.dll")]
+ [InlineData(@"module.dll")]
+ [InlineData(@"subdir\module.dll")]
+ [InlineData(@"..\module.dll")]
+ [InlineData(@"D:\drive\path.dll")]
+ [InlineData(@"\\?\C:\Windows\notepad.exe")]
+ [InlineData(@"\\.\C:\Windows\notepad.exe")]
+ [InlineData(@"\\?\Volume{12345678-1234-1234-1234-1234567890ab}\foo.dll")]
+ public void IsRemotePathAcceptsLocalPaths(string modulePath)
+ {
+ Assert.False(PathUtilities.IsRemotePath(modulePath));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ public void IsRemotePathHandlesEmptyInputWithoutThrowing(string modulePath)
+ {
+ // Empty/null inputs are not remote (they will be caught elsewhere).
+ Assert.False(PathUtilities.IsRemotePath(modulePath));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(".")]
+ [InlineData("..")]
+ public void SanitizeFileName_ReturnsNullForRejectedInput(string input)
+ {
+ // null / empty / "." / ".." all return null so callers can choose how to
+ // handle the missing name (skip the resource, substitute a placeholder,
+ // etc.) instead of being forced to accept an arbitrary string.
+ Assert.Null(PathUtilities.SanitizeFileName(input));
+ }
+
+ [Theory]
+ [InlineData(@"..\outside", ".._outside")]
+ [InlineData(@"..\..\..\Startup\x", ".._.._.._Startup_x")]
+ [InlineData("../forward/slash", ".._forward_slash")]
+ [InlineData(@"C:\Windows\System32\evil", "C__Windows_System32_evil")]
+ [InlineData(@"\\server\share\evil", "__server_share_evil")]
+ [InlineData(@"with:colons", "with_colons")]
+ [InlineData("with|pipes?and*wildcards", "with_pipes_and_wildcards")]
+ public void SanitizeFileName_ReplacesInvalidCharactersAndSeparators(string input, string expected)
+ {
+ // Every path separator, volume separator, and Path.GetInvalidFileNameChars
+ // character is replaced with '_'. Control characters are also replaced.
+ Assert.Equal(expected, PathUtilities.SanitizeFileName(input));
+ }
+
+ [Theory]
+ [InlineData("CON", "_CON")]
+ [InlineData("nul", "_nul")]
+ [InlineData("PRN", "_PRN")]
+ [InlineData("AUX", "_AUX")]
+ [InlineData("COM1", "_COM1")]
+ [InlineData("LPT9", "_LPT9")]
+ [InlineData("CLOCK$", "_CLOCK$")]
+ [InlineData("CONIN$", "_CONIN$")]
+ [InlineData("conout$", "_conout$")]
+ [InlineData("CONIN$.log", "_CONIN$.log")]
+ [InlineData("NUL.", "_NUL")]
+ [InlineData("NUL ", "_NUL")]
+ [InlineData("NUL. . ", "_NUL")]
+ [InlineData("NUL.evil", "_NUL.evil")]
+ [InlineData("CON.foo.bar", "_CON.foo.bar")]
+ [InlineData("com1.data", "_com1.data")]
+ [InlineData("LPT5.tar.gz", "_LPT5.tar.gz")]
+ public void SanitizeFileName_RewritesReservedDosDeviceNames(string input, string expected)
+ {
+ // Win32 matches a reserved device name on the stem before the first '.'
+ // in the basename, so "NUL.evil" or "COM1.data" still open the device.
+ // The sanitizer prefixes such names with '_' to make them safe.
+ Assert.Equal(expected, PathUtilities.SanitizeFileName(input));
+ }
+
+ [Theory]
+ [InlineData("Trailing.", "Trailing")]
+ [InlineData("Trailing ", "Trailing")]
+ [InlineData("Trailing.. .", "Trailing")]
+ public void SanitizeFileName_StripsTrailingDotsAndSpaces(string input, string expected)
+ {
+ // Windows silently trims trailing '.' and ' ' from file names; stripping
+ // them ourselves prevents two distinct names colliding on disk and
+ // closes the "NUL." device-name evasion.
+ Assert.Equal(expected, PathUtilities.SanitizeFileName(input));
+ }
+
+ [Theory]
+ [InlineData("...")]
+ [InlineData(" ")]
+ public void SanitizeFileName_ReturnsNullWhenAllCharactersAreStripped(string input)
+ {
+ // Inputs that consist entirely of trailing-trim characters (or sanitize
+ // to nothing) return null rather than the empty string.
+ Assert.Null(PathUtilities.SanitizeFileName(input));
+ }
+
+ [Theory]
+ [InlineData("Contoso.Provider-Valid_1", "Contoso.Provider-Valid_1")]
+ [InlineData("My Provider", "My Provider")]
+ [InlineData("provider.with.dots", "provider.with.dots")]
+ public void SanitizeFileName_PreservesValidNames(string input, string expected)
+ {
+ Assert.Equal(expected, PathUtilities.SanitizeFileName(input));
+ }
+ }
+}
+
diff --git a/src/PerfView.Tests/Memory/PdbScopeMemoryGraphTests.cs b/src/PerfView.Tests/Memory/PdbScopeMemoryGraphTests.cs
new file mode 100644
index 000000000..94fc6b2e4
--- /dev/null
+++ b/src/PerfView.Tests/Memory/PdbScopeMemoryGraphTests.cs
@@ -0,0 +1,106 @@
+using PerfView;
+using System;
+using System.IO;
+using Xunit;
+
+namespace PerfViewTests.Memory
+{
+ public class PdbScopeMemoryGraphTests : IDisposable
+ {
+ private readonly string m_testDirectory;
+
+ public PdbScopeMemoryGraphTests()
+ {
+ m_testDirectory = Path.Combine(Path.GetTempPath(), "PdbScopeMemoryGraphTests", Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(m_testDirectory);
+ }
+
+ public void Dispose()
+ {
+ if (Directory.Exists(m_testDirectory))
+ {
+ Directory.Delete(m_testDirectory, recursive: true);
+ }
+ }
+
+ [Theory]
+ [InlineData(@"\\server\share\module.dll")]
+ [InlineData(@"\\?\UNC\server\share\module.dll")]
+ [InlineData("https://server/share/module.dll")]
+ public void TryResolveTrustedFilePathRejectsRemotePaths(string modulePath)
+ {
+ string pdbScopeFilePath = GetPdbScopeFilePath();
+
+ Assert.False(PdbScopeMemoryGraph.TryResolveTrustedFilePath(modulePath, pdbScopeFilePath, out string trustedFilePath));
+ Assert.Null(trustedFilePath);
+ }
+
+ [Fact]
+ public void TryResolveTrustedFilePathRejectsRootedPathsOutsidePdbScopeDirectory()
+ {
+ string pdbScopeFilePath = GetPdbScopeFilePath();
+ string outsideDirectory = Path.Combine(Path.GetPathRoot(m_testDirectory), "PdbScopeMemoryGraphTestsOutside");
+ string outsideModulePath = Path.Combine(outsideDirectory, "module.dll");
+
+ Assert.False(PdbScopeMemoryGraph.TryResolveTrustedFilePath(outsideModulePath, pdbScopeFilePath, out string trustedFilePath));
+ Assert.Null(trustedFilePath);
+ }
+
+ [Theory]
+ [InlineData("module.dll")]
+ [InlineData(@"subdirectory\module.dll")]
+ public void TryResolveTrustedFilePathAllowsRelativePathsUnderPdbScopeDirectory(string modulePath)
+ {
+ string pdbScopeFilePath = GetPdbScopeFilePath();
+
+ Assert.True(PdbScopeMemoryGraph.TryResolveTrustedFilePath(modulePath, pdbScopeFilePath, out string trustedFilePath));
+ Assert.Equal(Path.GetFullPath(Path.Combine(m_testDirectory, modulePath)), trustedFilePath);
+ }
+
+ [Fact]
+ public void TryResolveTrustedFilePathAllowsRootedPathsUnderPdbScopeDirectory()
+ {
+ string pdbScopeFilePath = GetPdbScopeFilePath();
+ string modulePath = Path.Combine(m_testDirectory, "module.dll");
+
+ Assert.True(PdbScopeMemoryGraph.TryResolveTrustedFilePath(modulePath, pdbScopeFilePath, out string trustedFilePath));
+ Assert.Equal(modulePath, trustedFilePath);
+ }
+
+ [Theory]
+ [InlineData(@"..\module.dll")]
+ [InlineData(@"subdirectory\..\..\module.dll")]
+ public void TryResolveTrustedFilePathRejectsRelativePathsEscapingPdbScopeDirectory(string modulePath)
+ {
+ string pdbScopeFilePath = GetPdbScopeFilePath();
+
+ Assert.False(PdbScopeMemoryGraph.TryResolveTrustedFilePath(modulePath, pdbScopeFilePath, out string trustedFilePath));
+ Assert.Null(trustedFilePath);
+ }
+
+ [Fact]
+ public void PdbScopeXmlWithRemoteModuleFilePathDoesNotThrow()
+ {
+ CommandProcessor originalCommandProcessor = App.CommandProcessor;
+ string pdbScopeFilePath = GetPdbScopeFilePath();
+ try
+ {
+ App.CommandProcessor = new CommandProcessor() { LogFile = TextWriter.Null };
+ File.WriteAllText(
+ pdbScopeFilePath,
+ @" ");
+
+ new PdbScopeMemoryGraph(pdbScopeFilePath);
+ }
+ finally
+ {
+ App.CommandProcessor = originalCommandProcessor;
+ }
+ }
+
+ private string GetPdbScopeFilePath()
+ {
+ return Path.Combine(m_testDirectory, "test.imageSize.xml");
+ }
+ }
+}
diff --git a/src/PerfView.Tests/PerfView.Tests.csproj b/src/PerfView.Tests/PerfView.Tests.csproj
index 380066c84..d0c536add 100644
--- a/src/PerfView.Tests/PerfView.Tests.csproj
+++ b/src/PerfView.Tests/PerfView.Tests.csproj
@@ -21,12 +21,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/src/PerfView.Tests/StackViewer/StackWindowTests.cs b/src/PerfView.Tests/StackViewer/StackWindowTests.cs
index 4ba2ea817..4cfd0821e 100644
--- a/src/PerfView.Tests/StackViewer/StackWindowTests.cs
+++ b/src/PerfView.Tests/StackViewer/StackWindowTests.cs
@@ -859,5 +859,39 @@ public override string GetFrameName(StackSourceFrameIndex frameIndex, bool verbo
return frameIndex.ToString();
}
}
+
+ [WpfFact]
+ [WorkItem(2308, "https://github.com/Microsoft/perfview/issues/2308")]
+ public void TestExportFlameGraphWithInvalidCanvasSize()
+ {
+ // Create a canvas with zero size (simulating an unrendered or collapsed canvas)
+ var canvas = new Canvas();
+ canvas.Width = 0;
+ canvas.Height = 0;
+ canvas.Measure(new Size(0, 0));
+ canvas.Arrange(new Rect(0, 0, 0, 0));
+
+ var tempFile = Path.GetTempFileName();
+ try
+ {
+ // Attempt to export should throw ArgumentOutOfRangeException with a meaningful message
+ var exception = Assert.Throws(() =>
+ {
+ FlameGraph.Export(canvas, tempFile);
+ });
+
+ // Verify the exception message is helpful
+ Assert.Contains("Canvas has an invalid size", exception.Message);
+ Assert.Contains("width=0", exception.Message);
+ Assert.Contains("height=0", exception.Message);
+ }
+ finally
+ {
+ if (File.Exists(tempFile))
+ {
+ File.Delete(tempFile);
+ }
+ }
+ }
}
}
diff --git a/src/PerfView.Tests/Utilities/RangeUtilitiesTests.cs b/src/PerfView.Tests/Utilities/RangeUtilitiesTests.cs
index 9c044290c..098684bd0 100644
--- a/src/PerfView.Tests/Utilities/RangeUtilitiesTests.cs
+++ b/src/PerfView.Tests/Utilities/RangeUtilitiesTests.cs
@@ -18,10 +18,16 @@ public static class RangeUtilitiesTests
[InlineData(TestCultureInfo.enUSCulture, "XXXXXXXXXXXXXXX 234,567,890.123", default(double), default(double), false)]
[InlineData(TestCultureInfo.enUSCulture, "123,456,789.123 XXXXXXXXXXXXXXX", default(double), default(double), false)]
[InlineData(TestCultureInfo.enUSCulture, "123,456,789.123 234,567,890.123", 123456789.123, 234567890.123, true)]
+ // Test cases for pipe-enclosed format (markdown table format)
+ [InlineData(TestCultureInfo.enUSCulture, "| 1,395.251\t 2,626.358 |", 1395.251, 2626.358, true)]
+ [InlineData(TestCultureInfo.enUSCulture, "| 123,456,789.123 234,567,890.123 |", 123456789.123, 234567890.123, true)]
+ [InlineData(TestCultureInfo.enUSCulture, "|123,456,789.123 234,567,890.123|", 123456789.123, 234567890.123, true)]
[InlineData(TestCultureInfo.ruRUCulture, "", default(double), default(double), false)]
[InlineData(TestCultureInfo.ruRUCulture, "XXXXXXXXXXXXXXX|234 567\u00A0890,123", default(double), default(double), false)]
[InlineData(TestCultureInfo.ruRUCulture, "123\u00A0456 789,123|XXXXXXXXXXXXXXX", default(double), default(double), false)]
[InlineData(TestCultureInfo.ruRUCulture, "123\u00A0456 789,123|234\u00A0567\u00A0890,123", 123456789.123, 234567890.123, true)]
+ // Test cases for pipe-enclosed format with Russian culture
+ [InlineData(TestCultureInfo.ruRUCulture, "| 123\u00A0456 789,123|234\u00A0567\u00A0890,123 |", 123456789.123, 234567890.123, true)]
[InlineData(TestCultureInfo.ptPTCulture, "", default(double), default(double), false)]
[InlineData(TestCultureInfo.ptPTCulture, "XXXXXXXXXXXXXXX|234 567 890,123", default(double), default(double), false)]
[InlineData(TestCultureInfo.ptPTCulture, "123 456 789,123|XXXXXXXXXXXXXXX", default(double), default(double), false)]
@@ -34,6 +40,8 @@ public static class RangeUtilitiesTests
[InlineData(TestCultureInfo.customCulture2, "XXXXXXXXXXXXXXX,234 567 890.123", default(double), default(double), false)]
[InlineData(TestCultureInfo.customCulture2, "123 456 789.123|XXXXXXXXXXXXXXX", default(double), default(double), false)]
[InlineData(TestCultureInfo.customCulture2, "123 456 789.123|234 567 890.123", 123456789.123, 234567890.123, true)]
+ // Test cases for pipe-enclosed format with custom culture
+ [InlineData(TestCultureInfo.customCulture2, "| 123 456 789.123|234 567 890.123 |", 123456789.123, 234567890.123, true)]
public static void TryParseTests(string culture, string text, double expectedStart, double expectedEnd, bool expectedResult)
{
var runner = RangeUtilitiesRunner.Create(culture);
diff --git a/src/PerfView.Tutorial/PerfView.Tutorial.csproj b/src/PerfView.Tutorial/PerfView.Tutorial.csproj
index 1275f9732..83f8e423c 100644
--- a/src/PerfView.Tutorial/PerfView.Tutorial.csproj
+++ b/src/PerfView.Tutorial/PerfView.Tutorial.csproj
@@ -21,7 +21,7 @@
Microsoft400
-
+
diff --git a/src/PerfView/App.cs b/src/PerfView/App.cs
index 75c01b930..634a198d2 100755
--- a/src/PerfView/App.cs
+++ b/src/PerfView/App.cs
@@ -3,6 +3,10 @@
using Microsoft.Diagnostics.Tracing;
using Microsoft.Diagnostics.Tracing.Session;
using Microsoft.Diagnostics.Utilities;
+
+#if !PERFVIEW_COLLECT
+using PerfView.Dialogs;
+#endif
using PerfView.Properties;
using System;
using System.Diagnostics;
@@ -43,9 +47,9 @@ public static int Main(string[] args)
CommandProcessor = new CommandProcessor();
App.SetAccessibilitySwitchOverrides();
- StreamWriter writerToCleanup = null; // If we create a log file, we need to clean it up.
+ StreamWriter writerToCleanup = null; // If we create a log file, we need to clean it up.
int retCode = -1;
- bool newConsoleCreated = false; // If we create a new console, we need to wait before existing
+ bool newConsoleCreated = false; // If we create a new console, we need to wait before existing
try
{
#if !PERFVIEW_COLLECT
@@ -80,8 +84,8 @@ public static int Main(string[] args)
DisplaySplashScreen();
}
#endif
- App.Unpack(); // Install the program if it is not done already
- App.RelaunchIfNeeded(args); // If we are running from a a network share, relaunch locally.
+ App.Unpack(); // Install the program if it is not done already
+ App.RelaunchIfNeeded(args); // If we are running from a a network share, relaunch locally.
// This does the real work
retCode = DoMain(args, ref newConsoleCreated, ref writerToCleanup);
@@ -891,17 +895,42 @@ public static SymbolReader GetSymbolReader(string etlFilePath = null, SymbolRead
#if !PERFVIEW_COLLECT
if (!App.CommandLineArgs.TrustPdbs)
{
- ret.SecurityCheck = delegate (string pdbFile)
+ ret.SecurityCheck = pdbFile =>
{
- var result = System.Windows.MessageBox.Show("Found " + pdbFile + " on your local machine. Do you want to use it?",
- "Security Check", System.Windows.MessageBoxButton.YesNo);
+ var result = XamlMessageBox.Show(
+ $"Found {pdbFile} on your local machine. Do you want to use it?",
+ "Security Check",
+ System.Windows.MessageBoxButton.YesNo);
+
return result == System.Windows.MessageBoxResult.Yes;
};
+
+ ret.AuthorizeSourceServerCommand = request =>
+ {
+ var result = XamlMessageBox.Show(
+ request.Command + "\n\n" +
+ "This command was derived from PDB-supplied data. Do you want to run it?",
+ "Source Server Command",
+ System.Windows.MessageBoxButton.YesNo);
+
+ bool allowed = result == System.Windows.MessageBoxResult.Yes;
+ log.WriteLine("Source Server command authorization {0} by user: {1}", allowed ? "GRANTED" : "DENIED", request.Command);
+ return allowed;
+ };
}
else
#endif
{
ret.SecurityCheck = (pdbFile => true);
+ ret.AuthorizeSourceServerCommand = request =>
+ {
+#if PERFVIEW_COLLECT
+ log.WriteLine("Source Server command auto-approved in PerfViewCollect: {0}", request.Command);
+#else
+ log.WriteLine("Source Server command auto-approved because /TrustPdbs is set: {0}", request.Command);
+#endif
+ return true;
+ };
}
ret.SourceCacheDirectory = Path.Combine(CacheFiles.CacheDir, "src");
if (localSymDir != null)
@@ -958,6 +987,17 @@ private static void CacheInLocalSymDir(string localPdbDir, string pdbPath, Guid
}
var localPdbPath = Path.Combine(localPdbDir, fileName);
+
+ // If the source file is from an msfz0 subdirectory, also create the msfz0 subdirectory in the local cache
+ var sourceDirectory = Path.GetDirectoryName(pdbPath);
+ var sourceParentDir = Path.GetFileName(sourceDirectory);
+ if (sourceParentDir == "msfz0")
+ {
+ localPdbDir = Path.Combine(localPdbDir, "msfz0");
+ Directory.CreateDirectory(localPdbDir);
+ localPdbPath = Path.Combine(localPdbDir, fileName);
+ }
+
var fileExists = File.Exists(localPdbPath);
if (!fileExists || File.GetLastWriteTimeUtc(localPdbPath) != File.GetLastWriteTimeUtc(pdbPath))
{
@@ -1254,6 +1294,7 @@ protected override void Dispose(bool disposing)
m_terseLog.Dispose();
m_verboseLog.Dispose();
}
+
#region private
private TextWriter m_verboseLog;
private TextWriter m_terseLog;
diff --git a/src/PerfView/Authentication.cs b/src/PerfView/Authentication.cs
index 886de6538..6778267e8 100644
--- a/src/PerfView/Authentication.cs
+++ b/src/PerfView/Authentication.cs
@@ -1,12 +1,15 @@
ο»Ώusing System;
+using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
+using Azure.Core;
using Azure.Identity;
using Microsoft.Diagnostics.Symbols.Authentication;
+using Microsoft.Diagnostics.Utilities;
using Utilities;
namespace PerfView
@@ -256,13 +259,7 @@ public static void Configure(this SymbolReaderAuthenticationHandler handler, Aut
/// This instance for fluent chaining.
public static SymbolReaderAuthenticationHandler AddSymwebAuthentication(this SymbolReaderAuthenticationHandler httpHandler, TextWriter log, bool silent = false)
{
- DefaultAzureCredentialOptions options = new DefaultAzureCredentialOptions
- {
- ExcludeInteractiveBrowserCredential = silent,
- ExcludeManagedIdentityCredential = true // This is not designed to be used in a service.
- };
-
- return httpHandler.AddHandler(new SymwebHandler(log, new DefaultAzureCredential(options)));
+ return httpHandler.AddHandler(new SymwebHandler(log, CreateTokenCredential(App.CommandLineArgs.SymbolsAuth)));
}
///
@@ -285,13 +282,7 @@ public static SymbolReaderAuthenticationHandler AddGitCredentialManagerAuthentic
/// This instance for fluent chaining.
public static SymbolReaderAuthenticationHandler AddAzureDevOpsAuthentication(this SymbolReaderAuthenticationHandler httpHandler, TextWriter log, bool silent = false)
{
- DefaultAzureCredentialOptions options = new DefaultAzureCredentialOptions
- {
- ExcludeInteractiveBrowserCredential = silent,
- ExcludeManagedIdentityCredential = true // This is not designed to be used in a service.
- };
-
- return httpHandler.AddHandler(new AzureDevOpsHandler(log, new DefaultAzureCredential(options)));
+ return httpHandler.AddHandler(new AzureDevOpsHandler(log, CreateTokenCredential(App.CommandLineArgs.SymbolsAuth)));
}
///
@@ -307,6 +298,39 @@ public static SymbolReaderAuthenticationHandler AddGitHubDeviceCodeAuthenticatio
public static SymbolReaderAuthenticationHandler AddBasicHttpAuthentication(this SymbolReaderAuthenticationHandler httpHandler, TextWriter log, Window mainWindow)
=> httpHandler.AddHandler(new BasicHttpAuthHandler(log));
+ private static ChainedTokenCredential CreateTokenCredential(SymbolsAuthenticationType authTypes)
+ {
+ var credentials = new List();
+
+ if (authTypes.HasFlag(SymbolsAuthenticationType.Environment))
+ {
+ credentials.Add(new EnvironmentCredential());
+ }
+
+ if (authTypes.HasFlag(SymbolsAuthenticationType.AzureCli))
+ {
+ credentials.Add(new AzureCliCredential());
+ }
+
+ if (authTypes.HasFlag(SymbolsAuthenticationType.VisualStudio))
+ {
+ credentials.Add(new VisualStudioCredential());
+ }
+
+ if (authTypes.HasFlag(SymbolsAuthenticationType.Interactive))
+ {
+ credentials.Add(new InteractiveBrowserCredential());
+ }
+
+ // If no credentials are specified, default to Interactive
+ if (credentials.Count == 0)
+ {
+ credentials.Add(new InteractiveBrowserCredential());
+ }
+
+ return new ChainedTokenCredential(credentials.ToArray());
+ }
+
///
/// Get the HWND of the given WPF window in a way that honors WPF
/// threading rules.
diff --git a/src/PerfView/ClrStats.cs b/src/PerfView/ClrStats.cs
index 574281211..19156a78a 100644
--- a/src/PerfView/ClrStats.cs
+++ b/src/PerfView/ClrStats.cs
@@ -16,20 +16,41 @@ public static void ToHtml(TextWriter writer, List perProc, string
{
if (!justBody)
{
- writer.WriteLine("");
- writer.WriteLine("");
- writer.WriteLine("{0} ", Path.GetFileNameWithoutExtension(fileName));
- writer.WriteLine(" ");
- writer.WriteLine(" ");
- writer.WriteLine("");
- writer.WriteLine("");
+ writer.WriteLine($$"""
+
+
+ {{Path.GetFileNameWithoutExtension(fileName)}}
+
+
+
+
+ {{title}}
+ """);
}
- writer.WriteLine("{0} ", title);
+
List sortedProcs = perProc;
if (type == ReportType.JIT)
{
sortedProcs.Sort((TraceProcess p1, TraceProcess p2) => { return -p1.LoadedDotNetRuntime().JIT.Stats().TotalCpuTimeMSec.CompareTo(p2.LoadedDotNetRuntime().JIT.Stats().TotalCpuTimeMSec); });
}
+
else if (type == ReportType.GC)
{
sortedProcs.Sort((TraceProcess p1, TraceProcess p2) => { return -p1.LoadedDotNetRuntime().GC.Stats().MaxSizePeakMB.CompareTo(p2.LoadedDotNetRuntime().GC.Stats().MaxSizePeakMB); });
@@ -110,8 +131,15 @@ public static void ToHtml(TextWriter writer, List perProc, string
writer.WriteLine(" ");
if (!justBody)
{
- writer.WriteLine("");
- writer.WriteLine("");
+ writer.WriteLine("""
+
+
+
+ """);
}
}
diff --git a/src/PerfView/CommandLineArgs.cs b/src/PerfView/CommandLineArgs.cs
index 589fe1b70..0b785be93 100644
--- a/src/PerfView/CommandLineArgs.cs
+++ b/src/PerfView/CommandLineArgs.cs
@@ -1,6 +1,7 @@
ο»Ώusing Microsoft.Diagnostics.Tracing;
using Microsoft.Diagnostics.Tracing.Parsers;
using Microsoft.Diagnostics.Tracing.Session;
+using Microsoft.Diagnostics.Utilities;
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -65,6 +66,7 @@ public static string GetHelpString(int maxLineWidth)
// options common to multiple commands
public string DataFile; // This is the name of the ETL file (not the ZIP file)
public string LogFile;
+ public SymbolsAuthenticationType SymbolsAuth = SymbolsAuthenticationType.Interactive; // Specifies authentication types for symbol servers
// Memory options
public string ProcessDumpFile; // if taking a snapshot from a dump, this is the dump file (dataFile is the output file)
@@ -277,12 +279,13 @@ private void SetupCommandLine(CommandLineParser parser)
parser.NoDashOnParameterSets = true;
parser.DefineOptionalQualifier("LogFile", ref LogFile, "Send messages to this file instead launching the GUI. Intended for batch scripts and other automation.");
+ parser.DefineOptionalQualifier("SymbolsAuth", ref SymbolsAuth, "Specifies authentication types for symbol servers. Values: Environment, AzureCli, VisualStudio, Interactive. Can be combined with +. Default is Interactive only.");
// These apply to start, collect and run
parser.DefineOptionalQualifier("BufferSize", ref BufferSizeMB, "The size the buffers (in MB) the OS should use to store events waiting to be written to disk."); // TODO remove eventually.
- parser.DefineOptionalQualifier("Circular", ref CircularMB, "Do Circular logging with a file size in MB. Zero means non-circular."); // TODO remove eventually.
+ parser.DefineOptionalQualifier("Circular", ref CircularMB, "Do Circular logging with a file size in MB.");
parser.DefineOptionalQualifier("BufferSizeMB", ref BufferSizeMB, "The size the buffers (in MB) the OS should use to store events waiting to be written to disk.");
- parser.DefineOptionalQualifier("CircularMB", ref CircularMB, "Do Circular logging with a file size in MB. Zero means non-circular.");
+ parser.DefineOptionalQualifier("CircularMB", ref CircularMB, "Do Circular logging with a file size in MB.");
parser.DefineOptionalQualifier("InMemoryCircularBuffer", ref InMemoryCircularBuffer, "Keeps the circular buffer in memory until the session is stopped.");
parser.DefineOptionalQualifier("StackCompression", ref StackCompression, "Use stack compression (only on Win 8+) to make collected file smaller.");
parser.DefineOptionalQualifier("LbrSources", ref LastBranchRecordingSources,
diff --git a/src/PerfView/CommandProcessor.cs b/src/PerfView/CommandProcessor.cs
index a21ada84b..4b6235ae7 100644
--- a/src/PerfView/CommandProcessor.cs
+++ b/src/PerfView/CommandProcessor.cs
@@ -7,6 +7,9 @@
using Microsoft.Diagnostics.Tracing.Session;
using Microsoft.Diagnostics.Utilities;
using Microsoft.Win32;
+#if !PERFVIEW_COLLECT
+using PerfView.Dialogs;
+#endif
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -17,7 +20,6 @@
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
-using System.Windows;
using Triggers;
using Utilities;
using Trigger = Triggers.Trigger;
@@ -1682,16 +1684,19 @@ internal static void UnZipIfNecessary(ref string inputFileName, TextWriter log,
private void InformedAboutSkippingMerge()
{
#if !PERFVIEW_COLLECT
- GuiApp.MainWindow.Dispatcher.BeginInvoke((Action)delegate ()
+ GuiApp.MainWindow.Dispatcher.BeginInvoke(() =>
{
- MessageBox.Show(GuiApp.MainWindow,
- "If you are analyzing the data on the same machine on which you collected it, in the future " +
- "you can avoid the time it takes to merge and zip the file by unchecking the 'merge' checkbox " +
- "on the collection dialog box.\r\n\r\n" +
- "Be careful however, PerfView will remember this option from run to run and you will have to " +
- "either check the zip checkbox or use the PerfView's zip command if you wish to analyze on another machine.\r\n\r\n" +
- "The WPA analyzer requires merging unconditionally, so you must merge if you wish to use that tool.\r\n\n" +
- "See the 'Merging' section in the users guide for complete details.",
+ XamlMessageBox.Show(
+ GuiApp.MainWindow,
+ """
+ If you are analyzing the data on the same machine on which you collected it, in the future you can avoid the time it takes to merge and zip the file by unchecking the 'merge' checkbox on the collection dialog box.
+
+ Be careful however, PerfView will remember this option from run to run and you will have to either check the zip checkbox or use the PerfView's zip command if you wish to analyze on another machine.
+
+ The WPA analyzer requires merging unconditionally, so you must merge if you wish to use that tool.
+
+ See the 'Merging' section in the users guide for complete details.
+ """,
"Skip Merging/Zipping for faster local processing.");
});
#endif
@@ -2997,6 +3002,11 @@ public static string ParsedArgsAsString(string command, CommandLineArgs parsedAr
cmdLineArgs += " /LogFile:" + Command.Quote(parsedArgs.LogFile);
}
+ if (parsedArgs.SymbolsAuth != SymbolsAuthenticationType.Interactive)
+ {
+ cmdLineArgs += " /SymbolsAuth:" + parsedArgs.SymbolsAuth.ToString().Replace(" ", "");
+ }
+
if (parsedArgs.NoRundown)
{
cmdLineArgs += " /NoRundown";
@@ -3306,11 +3316,11 @@ private void ShowAspNetWarningBox(string message)
{
#if !PERFVIEW_COLLECT
// Are we activating with the GUI, then pop a dialog box
- if (App.CommandLineArgs.LogFile == null && GuiApp.MainWindow != null)
+ if (App.CommandLineArgs.LogFile is null)
{
- GuiApp.MainWindow.Dispatcher.BeginInvoke((Action)delegate ()
+ GuiApp.MainWindow?.Dispatcher.BeginInvoke(() =>
{
- MessageBox.Show(GuiApp.MainWindow, message, "Warning ASP.NET Tracing not installed");
+ XamlMessageBox.Show(GuiApp.MainWindow, message, "Warning ASP.NET Tracing not installed");
});
}
#endif
diff --git a/src/PerfView/Computers/RealtimeAntimalwareComputer.cs b/src/PerfView/Computers/RealtimeAntimalwareComputer.cs
index 3026e8d7f..7cdddde2c 100644
--- a/src/PerfView/Computers/RealtimeAntimalwareComputer.cs
+++ b/src/PerfView/Computers/RealtimeAntimalwareComputer.cs
@@ -102,25 +102,35 @@ internal void StartScan(StreamscanrequestStartArgs_V1TraceData data)
{
// Get the requesting user process based on the PID logged inside the engine.
TraceProcess process = _traceLog.Processes.GetProcess(data.PID, data.TimeStampRelativeMSec);
- ProcessIndex processIndex = process.ProcessIndex;
+ ProcessIndex processIndex = process?.ProcessIndex ?? ProcessIndex.Invalid;
if (processIndex == ProcessIndex.Invalid)
return;
// Get the file scan operation.
Dictionary processContainer = GetOrCreateProcessContainer(processIndex);
FileScanOperation operation = processContainer.Values.Where(s => s.File.Equals(data.Path, System.StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
- if(operation != null)
+ if (operation != null)
{
operation.StartTimeRelativeMSec = data.TimeStampRelativeMSec;
_engineThreadToScanMap[(int)data.Thread().ThreadIndex] = operation;
}
+ else
+ {
+ processContainer[data.Thread().ThreadIndex] = new FileScanOperation()
+ {
+ File = data.Path,
+ Reason = "Unknown",
+ StartTimeRelativeMSec = data.TimeStampRelativeMSec,
+ RequestorStack = _stackSource.GetCallStackForProcess(process)
+ };
+ }
}
internal void StopScan(StreamscanrequestStartArgs_V1TraceData data)
{
// Get the requesting user process based on the PID logged inside the engine.
TraceProcess process = _traceLog.Processes.GetProcess(data.PID, data.TimeStampRelativeMSec);
- ProcessIndex processIndex = process.ProcessIndex;
+ ProcessIndex processIndex = process?.ProcessIndex ?? ProcessIndex.Invalid;
if (processIndex == ProcessIndex.Invalid)
return;
diff --git a/src/PerfView/Dialogs/ImageHelpers.cs b/src/PerfView/Dialogs/ImageHelpers.cs
new file mode 100644
index 000000000..bccd49a0d
--- /dev/null
+++ b/src/PerfView/Dialogs/ImageHelpers.cs
@@ -0,0 +1,67 @@
+ο»Ώusing System.Runtime.InteropServices;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using Windows.Win32.UI.WindowsAndMessaging;
+using Windows.Win32.Foundation;
+using Windows.Win32.UI.Shell;
+using System.Windows.Interop;
+using System.Runtime.CompilerServices;
+using System;
+
+namespace PerfView.Dialogs;
+
+internal static class ImageHelpers
+{
+ ///
+ /// Gets the for the specified .
+ ///
+ ///
+ ///
+ /// This method reurns the modern version of the stock icons used in message boxes.
+ ///
+ ///
+ public static ImageSource ToImageSource(MessageBoxImage image) => image switch
+ {
+ MessageBoxImage.Error => GetStockIcon(SHSTOCKICONID.SIID_ERROR),
+ MessageBoxImage.Information => GetStockIcon(SHSTOCKICONID.SIID_INFO),
+ MessageBoxImage.Warning => GetStockIcon(SHSTOCKICONID.SIID_WARNING),
+ MessageBoxImage.Question => GetStockIcon(SHSTOCKICONID.SIID_HELP),
+ _ => throw new ArgumentOutOfRangeException(nameof(image)),
+ };
+
+ private static unsafe ImageSource GetStockIcon(SHSTOCKICONID stockIcon, SHGSI_FLAGS options = default)
+ {
+ // Note that we don't explicitly check for invalid StockIconId to allow for accessing newer ids introduced
+ // in later OSes. The HRESULT returned for undefined ids gets converted to an ArgumentException.
+
+ SHSTOCKICONINFO info = new()
+ {
+ cbSize = (uint)Unsafe.SizeOf(),
+ };
+
+ HRESULT result = SHGetStockIconInfo(stockIcon, options | SHGSI_FLAGS.SHGSI_ICON, &info);
+
+ // This only throws if there is an error.
+ Marshal.ThrowExceptionForHR((int)result);
+
+ return Imaging.CreateBitmapSourceFromHIcon(info.hIcon, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
+ }
+
+ // This can't be imported in CsWin32 as it technically isn't the same on both X86 and X64 due to a packing of 1 byte on X86.
+ // For our purposes this is fine as the single definition's layout (SHSTOCKICONINFO) is the same on both platforms.
+
+ [DllImport("Shell32.dll", ExactSpelling = true)]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
+ private static extern unsafe HRESULT SHGetStockIconInfo(SHSTOCKICONID siid, SHGSI_FLAGS uFlags, SHSTOCKICONINFO* psii);
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ private unsafe struct SHSTOCKICONINFO
+ {
+ public uint cbSize;
+ public HICON hIcon;
+ public int iSysImageIndex;
+ public int iIcon;
+ public fixed char szPath[260];
+ }
+}
diff --git a/src/PerfView/Dialogs/ManagePresetsDialog.xaml.cs b/src/PerfView/Dialogs/ManagePresetsDialog.xaml.cs
index b5c834a0e..ad43823b3 100644
--- a/src/PerfView/Dialogs/ManagePresetsDialog.xaml.cs
+++ b/src/PerfView/Dialogs/ManagePresetsDialog.xaml.cs
@@ -58,7 +58,7 @@ private void SaveClicked(object sender, RoutedEventArgs e)
{
if (Presets.Exists(x => x.Name == PresetName.Text))
{
- MessageBox.Show(
+ XamlMessageBox.Show(
$"Preset '{PresetName.Text}' already exists. Choose another name.",
"Preset Name",
MessageBoxButton.OK,
diff --git a/src/PerfView/Dialogs/MemoryDataDialog.xaml.cs b/src/PerfView/Dialogs/MemoryDataDialog.xaml.cs
index 4acef05ff..000edb630 100644
--- a/src/PerfView/Dialogs/MemoryDataDialog.xaml.cs
+++ b/src/PerfView/Dialogs/MemoryDataDialog.xaml.cs
@@ -148,10 +148,15 @@ private void DumpHeap(bool closeOnComplete)
if (m_args.MaxDumpCountK >= 10000)
{
- var response = MessageBox.Show("WARNING: you have selected a Max Dump Count larger than 10M objects.\r\n" +
- "You should only need 100K to do a good job, even at 10M the GUI will be very sluggish.\r\n" +
- "Consider canceling and picking a smaller value.", "Max Dump Size Too Big",
+ var response = XamlMessageBox.Show(
+ """
+ WARNING: you have selected a Max Dump Count larger than 10M objects.
+ You should only need 100K to do a good job, even at 10M the GUI will be very sluggish.
+ Consider canceling and picking a smaller value.
+ """,
+ "Max Dump Size Too Big",
MessageBoxButton.OKCancel);
+
if (response != MessageBoxResult.OK)
{
StatusBar.Log("Memory collection canceled.");
diff --git a/src/PerfView/Dialogs/MessageBoxWindow.xaml b/src/PerfView/Dialogs/MessageBoxWindow.xaml
new file mode 100644
index 000000000..e9988c7fb
--- /dev/null
+++ b/src/PerfView/Dialogs/MessageBoxWindow.xaml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/PerfView/Dialogs/MessageBoxWindow.xaml.cs b/src/PerfView/Dialogs/MessageBoxWindow.xaml.cs
new file mode 100644
index 000000000..ead1901b1
--- /dev/null
+++ b/src/PerfView/Dialogs/MessageBoxWindow.xaml.cs
@@ -0,0 +1,71 @@
+ο»Ώusing System.Windows;
+using System.Windows.Controls;
+
+namespace PerfView.Dialogs;
+
+///
+/// Simple themed message box window.
+///
+internal partial class MessageBoxWindow : Window
+{
+ public MessageBoxResult Result { get; private set; }
+
+ public MessageBoxWindow(string message, string caption, MessageBoxButton buttons, MessageBoxImage icon, MessageBoxResult defaultResult)
+ {
+ InitializeComponent();
+ Title = caption;
+ MessageTextBlock.Text = message;
+ ConfigureIcon(icon);
+ ConfigureButtons(buttons, defaultResult);
+ }
+
+ private void ConfigureIcon(MessageBoxImage icon)
+ {
+ // Map MessageBoxImage to SystemIcons or resources
+ switch (icon)
+ {
+ case MessageBoxImage.None:
+ IconImage.Visibility = Visibility.Collapsed;
+ break;
+ default:
+ IconImage.Source = ImageHelpers.ToImageSource(icon);
+ break;
+ }
+ }
+
+ private void ConfigureButtons(MessageBoxButton buttons, MessageBoxResult defaultResult)
+ {
+ ButtonsPanel.Children.Clear();
+ foreach ((string Text, MessageBoxResult Result) in Get(buttons))
+ {
+ Button button = new()
+ {
+ Content = Text,
+ Tag = Result,
+ IsDefault = Result == defaultResult,
+ IsCancel = Result == MessageBoxResult.Cancel
+ };
+
+ button.Click += Button_Click;
+ ButtonsPanel.Children.Add(button);
+ }
+
+ static (string Text, MessageBoxResult Result)[] Get(MessageBoxButton buttons) => buttons switch
+ {
+ MessageBoxButton.OKCancel => [("_OK", MessageBoxResult.OK), ("_Cancel", MessageBoxResult.Cancel)],
+ MessageBoxButton.YesNo => [("_Yes", MessageBoxResult.Yes), ("_No", MessageBoxResult.No)],
+ MessageBoxButton.YesNoCancel =>
+ [("_Yes", MessageBoxResult.Yes), ("_No", MessageBoxResult.No), ("_Cancel", MessageBoxResult.Cancel)],
+ _ => [("_OK", MessageBoxResult.OK)],
+ };
+ }
+
+ private void Button_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender is Button button && button.Tag is MessageBoxResult result)
+ {
+ Result = result;
+ DialogResult = true;
+ }
+ }
+}
diff --git a/src/PerfView/Dialogs/NewPresetDialog.xaml.cs b/src/PerfView/Dialogs/NewPresetDialog.xaml.cs
index f5f532b35..880ad2bca 100644
--- a/src/PerfView/Dialogs/NewPresetDialog.xaml.cs
+++ b/src/PerfView/Dialogs/NewPresetDialog.xaml.cs
@@ -30,11 +30,14 @@ private void OKClicked(object sender, RoutedEventArgs e)
// Check uniqueness of the name and ask if user wants to continue
if (m_existingPresets.Exists(x => x == PresetNameTextBox.Text))
{
- if (MessageBox.Show(
- $"Preset {PresetNameTextBox.Text} already exists in the list of presets.\r\nDo you want to overwrite it?",
- "Preset Name",
- MessageBoxButton.OKCancel,
- MessageBoxImage.Warning) == MessageBoxResult.Cancel)
+ if (XamlMessageBox.Show(
+ $"""
+ Preset {PresetNameTextBox.Text} already exists in the list of presets.
+ Do you want to overwrite it?
+ """,
+ "Preset Name",
+ MessageBoxButton.OKCancel,
+ MessageBoxImage.Warning) == MessageBoxResult.Cancel)
{
return;
}
diff --git a/src/PerfView/Dialogs/ProviderBrowser.xaml.cs b/src/PerfView/Dialogs/ProviderBrowser.xaml.cs
index fb783d8d3..947cc1146 100644
--- a/src/PerfView/Dialogs/ProviderBrowser.xaml.cs
+++ b/src/PerfView/Dialogs/ProviderBrowser.xaml.cs
@@ -201,6 +201,15 @@ private void KeySelected(object sender, SelectionChangedEventArgs e)
}
private void LevelSelected(object sender, SelectionChangedEventArgs e)
{
+ // Ensure at least one level is always selected
+ if (LevelListBox.SelectedItem == null)
+ {
+ // If nothing is selected, reselect the previous level or default to "Verbose"
+ string levelToSelect = !string.IsNullOrEmpty(m_level) ? m_level : "Verbose";
+ LevelListBox.SelectedItem = levelToSelect;
+ return;
+ }
+
m_level = LevelListBox.SelectedItem.ToString();
updateDisplays();
}
diff --git a/src/PerfView/Dialogs/SelectProcess.xaml b/src/PerfView/Dialogs/SelectProcess.xaml
index 83c03b548..f4af6771a 100644
--- a/src/PerfView/Dialogs/SelectProcess.xaml
+++ b/src/PerfView/Dialogs/SelectProcess.xaml
@@ -11,6 +11,8 @@
@@ -61,7 +63,8 @@
AutoGenerateColumns="False"
MouseDoubleClick="OKClicked"
IsReadOnly="True"
- ColumnHeaderStyle="{StaticResource ColumnHeaderStyle}">
+ ColumnHeaderStyle="{StaticResource ColumnHeaderStyle}"
+ AutomationProperties.Name="Process Selection Table">
@@ -87,7 +89,8 @@
-
+
+
@@ -246,6 +249,7 @@
ToolTip="Click on Update button to update."
SelectionMode="Extended" SelectionUnit="CellOrRowHeader"
SelectedCellsChanged="SelectedCellsChanged"
+ PreviewMouseWheel="Grid_PreviewMouseWheel"
AlternatingRowBackground="{DynamicResource AlternateRowBackground}"
AutomationProperties.Name="Events Table"
ColumnHeaderStyle="{StaticResource ColumnHeaderStyle}">
diff --git a/src/PerfView/EventViewer/EventWindow.xaml.cs b/src/PerfView/EventViewer/EventWindow.xaml.cs
index ed91b9db5..d75ff1a86 100644
--- a/src/PerfView/EventViewer/EventWindow.xaml.cs
+++ b/src/PerfView/EventViewer/EventWindow.xaml.cs
@@ -57,6 +57,12 @@ public EventWindow(EventWindow template)
{
selection.Add(item);
}
+
+ // Copy timestamp column visibility settings from template
+ ShowTimeStampColumnsMenuItem.IsChecked = template.ShowTimeStampColumnsMenuItem.IsChecked;
+ ShowLocalTimeMenuItem.IsChecked = template.ShowLocalTimeMenuItem.IsChecked;
+ ShowLocalTimeMenuItem.IsEnabled = template.ShowLocalTimeMenuItem.IsEnabled;
+
Update();
}
public EventWindow(Window parent, PerfViewEventSource data)
@@ -167,6 +173,26 @@ public EventWindow(Window parent, PerfViewEventSource data)
};
MultiLineViewPaneHidden = (App.UserConfigData["MultiLineViewPaneHidden"] == "true");
+
+ // Initialize timestamp column visibility based on user preference
+ bool showTimeStampColumns = App.UserConfigData["EventWindowShowTimeStampColumns"] != "false"; // Default to true
+ ShowTimeStampColumnsMenuItem.IsChecked = showTimeStampColumns;
+ if (!showTimeStampColumns)
+ {
+ // Hide both timestamp columns and disable the timezone menu
+ foreach (var column in Grid.Columns)
+ {
+ if (column == OriginTimeStampColumn || column == LocalTimeStampColumn)
+ {
+ column.Visibility = Visibility.Hidden;
+ }
+ }
+ ShowLocalTimeMenuItem.IsEnabled = false;
+ }
+ else
+ {
+ ShowLocalTimeMenuItem.IsEnabled = true;
+ }
}
public PerfViewEventSource DataSource { get; private set; }
@@ -1889,36 +1915,133 @@ private void Add(ObservableCollection events, EventRecord event_)
private List m_userDefinedColumns;
private float[] m_buckets; // Keep track of the counts of events.
private double m_bucketTimeMSec; // Size for each bucket
+ private ScrollViewer m_gridScrollViewer; // Cached ScrollViewer for horizontal scrolling
#endregion
private void DoUseLocalTime(object sender, RoutedEventArgs e)
{
- foreach (var i in Grid.Columns)
+ // Only change visibility if timestamp columns are enabled
+ if (ShowTimeStampColumnsMenuItem.IsChecked)
{
- if (i == OriginTimeStampColumn)
+ foreach (var i in Grid.Columns)
{
- i.Visibility = Visibility.Hidden;
+ if (i == OriginTimeStampColumn)
+ {
+ i.Visibility = Visibility.Hidden;
+ }
+ else if (i == LocalTimeStampColumn)
+ {
+ i.Visibility = Visibility.Visible;
+ }
}
- else if (i == LocalTimeStampColumn)
+ }
+ }
+
+ private void DoUseOriginTime(object sender, RoutedEventArgs e)
+ {
+ // Only change visibility if timestamp columns are enabled
+ if (ShowTimeStampColumnsMenuItem.IsChecked)
+ {
+ foreach (var i in Grid.Columns)
{
- i.Visibility = Visibility.Visible;
+ if (i == OriginTimeStampColumn)
+ {
+ i.Visibility = Visibility.Visible;
+ }
+ else if (i == LocalTimeStampColumn)
+ {
+ i.Visibility = Visibility.Hidden;
+ }
}
}
}
- private void DoUseOriginTime(object sender, RoutedEventArgs e)
+ private void DoShowTimeStampColumns(object sender, RoutedEventArgs e)
{
+ // Check if UI elements are initialized to avoid null reference during XAML construction
+ if (ShowLocalTimeMenuItem == null || Grid?.Columns == null)
+ return;
+
+ // Show the appropriate timestamp column based on current preference
+ bool useLocalTime = ShowLocalTimeMenuItem.IsChecked;
foreach (var i in Grid.Columns)
{
if (i == OriginTimeStampColumn)
{
- i.Visibility = Visibility.Visible;
+ i.Visibility = useLocalTime ? Visibility.Hidden : Visibility.Visible;
}
else if (i == LocalTimeStampColumn)
+ {
+ i.Visibility = useLocalTime ? Visibility.Visible : Visibility.Hidden;
+ }
+ }
+ // Enable the Show Local Time menu item
+ ShowLocalTimeMenuItem.IsEnabled = true;
+
+ // Save preference
+ App.UserConfigData["EventWindowShowTimeStampColumns"] = "true";
+ }
+
+ private void DoHideTimeStampColumns(object sender, RoutedEventArgs e)
+ {
+ // Check if UI elements are initialized to avoid null reference during XAML construction
+ if (ShowLocalTimeMenuItem == null || Grid?.Columns == null)
+ return;
+
+ // Hide both timestamp columns
+ foreach (var i in Grid.Columns)
+ {
+ if (i == OriginTimeStampColumn || i == LocalTimeStampColumn)
{
i.Visibility = Visibility.Hidden;
}
}
+ // Gray out (disable) the Show Local Time menu item
+ ShowLocalTimeMenuItem.IsEnabled = false;
+
+ // Save preference
+ App.UserConfigData["EventWindowShowTimeStampColumns"] = "false";
+ }
+
+ ///
+ /// When Shift is held, redirect mouse wheel events to horizontal scrolling.
+ ///
+ private void Grid_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
+ {
+ if (Keyboard.Modifiers == ModifierKeys.Shift)
+ {
+ // Cache the ScrollViewer on first use
+ if (m_gridScrollViewer == null)
+ {
+ m_gridScrollViewer = FindVisualChild((DependencyObject)sender);
+ if (m_gridScrollViewer == null)
+ {
+ return;
+ }
+ }
+
+ m_gridScrollViewer.ScrollToHorizontalOffset(m_gridScrollViewer.HorizontalOffset - e.Delta);
+ e.Handled = true;
+ }
+ }
+
+ private static T FindVisualChild(DependencyObject parent) where T : DependencyObject
+ {
+ for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
+ {
+ DependencyObject child = VisualTreeHelper.GetChild(parent, i);
+ if (child is T found)
+ {
+ return found;
+ }
+
+ T result = FindVisualChild(child);
+ if (result != null)
+ {
+ return result;
+ }
+ }
+ return null;
}
}
}
diff --git a/src/PerfView/Extensibility.cs b/src/PerfView/Extensibility.cs
index 471653aac..f40a920d1 100644
--- a/src/PerfView/Extensibility.cs
+++ b/src/PerfView/Extensibility.cs
@@ -708,7 +708,12 @@ private static void UnZipIfNecessary(ref string inputFileName, TextWriter log, b
log.WriteLine("Putting symbols in {0}", dirForPdbs);
}
- var pdbTargetPath = Path.Combine(dirForPdbs, pdbRelativePath);
+ if (!SymbolCachePathUtilities.TryGetPdbTargetPath(dirForPdbs, pdbRelativePath, out var pdbTargetPath))
+ {
+ log.WriteLine("WARNING: found PDB file with invalid path {0}, skipping extraction", pdbRelativePath);
+ continue;
+ }
+
var pdbTargetName = Path.GetFileName(pdbTargetPath);
if (!File.Exists(pdbTargetPath) || (new System.IO.FileInfo(pdbTargetPath).Length != entry.Length))
{
diff --git a/src/PerfView/GcStats.cs b/src/PerfView/GcStats.cs
index e0e8b099c..81760e22e 100644
--- a/src/PerfView/GcStats.cs
+++ b/src/PerfView/GcStats.cs
@@ -20,11 +20,14 @@ internal static class GcStats
public static void ToHtml(TextWriter writer, TraceProcess stats, TraceLoadedDotNetRuntime runtime, string fileName, bool doServerGCReport = false)
{
- writer.WriteLine("", stats.ProcessID, stats.ProcessID, stats.Name);
- writer.WriteLine("");
+ writer.WriteLine($"""
+
+
+ """);
+
if (runtime.GC.Stats().GCVersionInfoMismatch)
{
- writer.WriteLine("Warning: Did not recognize the V4.0 GC Information events. Falling back to V2.0 behavior. ");
+ writer.WriteLine("""Warning: Did not recognize the V4.0 GC Information events. Falling back to V2.0 behavior. """);
}
if (!string.IsNullOrEmpty(stats.CommandLine))
@@ -33,7 +36,7 @@ public static void ToHtml(TextWriter writer, TraceProcess stats, TraceLoadedDotN
}
var runtimeBuiltTime = "";
- if (runtime.RuntimeBuiltTime != default(DateTime))
+ if (runtime.RuntimeBuiltTime != default)
{
runtimeBuiltTime = string.Format(" (built on {0})", runtime.RuntimeBuiltTime);
}
@@ -914,9 +917,15 @@ private static void PrintEventCondemnedReasonsTable(TextWriter writer, TraceProc
continue;
}
}
- events.Add(_event);
int heapIndexHighestGen;
- condemnedReasonRows.Add(GetCondemnedReasonRow(_event, out heapIndexHighestGen));
+ byte[] condemnedReasonRow = GetCondemnedReasonRow(_event, out heapIndexHighestGen);
+ if (condemnedReasonRow == null)
+ {
+ // No per-heap or global condemned reasons information available for this event.
+ continue;
+ }
+ events.Add(_event);
+ condemnedReasonRows.Add(condemnedReasonRow);
if (isServerGC)
{
heapIndexes.Add(heapIndexHighestGen);
@@ -1215,11 +1224,11 @@ private static string GetGenerationBackgroundColorAttribute(int gen)
switch (gen)
{
case 2:
- return "bgcolor=#56A5EC";
+ return "class=\"row-vibrant\"";
case 1:
- return "bgcolor=#82CAFF";
+ return "class=\"row-medium\"";
default:
- return "bgcolor=#BDEDFF";
+ return "class=\"row-subtle\"";
}
}
#endregion
diff --git a/src/PerfView/GuiUtilities/StatusBar/StatusBar.xaml b/src/PerfView/GuiUtilities/StatusBar/StatusBar.xaml
index b48eab720..6668df8ec 100644
--- a/src/PerfView/GuiUtilities/StatusBar/StatusBar.xaml
+++ b/src/PerfView/GuiUtilities/StatusBar/StatusBar.xaml
@@ -13,7 +13,9 @@
-
+
@@ -38,7 +40,11 @@
- Log
- Cancel
+ Log
+ Cancel
diff --git a/src/PerfView/GuiUtilities/WebBrowser/WebBrowser.xaml b/src/PerfView/GuiUtilities/WebBrowser/WebBrowser.xaml
index 04bbb047a..958762ce1 100644
--- a/src/PerfView/GuiUtilities/WebBrowser/WebBrowser.xaml
+++ b/src/PerfView/GuiUtilities/WebBrowser/WebBrowser.xaml
@@ -5,9 +5,10 @@
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
Style="{DynamicResource CustomWindowStyle}"
Closing="Window_Closing"
+ PreviewKeyDown="Window_PreviewKeyDown"
Title="Web Browser" Height="700" Width="1100">
-
-
+
+
diff --git a/src/PerfView/GuiUtilities/WebBrowser/WebBrowser.xaml.cs b/src/PerfView/GuiUtilities/WebBrowser/WebBrowser.xaml.cs
index 72c58933f..64f58f2bc 100644
--- a/src/PerfView/GuiUtilities/WebBrowser/WebBrowser.xaml.cs
+++ b/src/PerfView/GuiUtilities/WebBrowser/WebBrowser.xaml.cs
@@ -3,6 +3,7 @@
using System.IO;
using System.Windows;
using System.Windows.Controls;
+using System.Windows.Input;
using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.Wpf;
using Utilities;
@@ -25,8 +26,8 @@ public WebBrowserWindow(Window parentWindow) : base(parentWindow)
///
public bool HideOnClose;
- public bool CanGoForward { get { return Browser.CanGoForward; } }
- public bool CanGoBack { get { return Browser.CanGoBack; } }
+ public bool CanGoForward { get { return _disposed ? false : Browser.CanGoForward; } }
+ public bool CanGoBack { get { return _disposed ? false : Browser.CanGoBack; } }
public WebView2 Browser { get { return _Browser; } }
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
@@ -52,16 +53,16 @@ private static void OnSourceChanged(DependencyObject d, DependencyPropertyChange
///
private void Navigate()
{
- if (Source != null && _Browser.CoreWebView2 != null)
+ if (!_disposed && Source?.ToString() is { } source)
{
- _Browser.CoreWebView2.Navigate(Source.ToString());
+ Browser?.CoreWebView2.Navigate(source);
}
}
#region private
private void BackClick(object sender, RoutedEventArgs e)
{
- if (Browser.CanGoBack)
+ if (CanGoBack)
{
Browser.GoBack();
}
@@ -69,12 +70,40 @@ private void BackClick(object sender, RoutedEventArgs e)
private void ForwardClick(object sender, RoutedEventArgs e)
{
- if (Browser.CanGoForward)
+ if (CanGoForward)
{
Browser.GoForward();
}
}
+ private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Escape && Keyboard.Modifiers == ModifierKeys.None)
+ {
+ e.Handled = true;
+ Close();
+ }
+ }
+
+ private void Browser_WebMessageReceived(object sender, CoreWebView2WebMessageReceivedEventArgs e)
+ {
+ ProcessWebMessage(e.WebMessageAsJson);
+ }
+
+ internal void ProcessWebMessage(string messageJson)
+ {
+ if (messageJson == CloseWindowMessageJson)
+ {
+ Dispatcher.BeginInvoke((Action)(() =>
+ {
+ if (IsLoaded)
+ {
+ Close();
+ }
+ }));
+ }
+ }
+
///
/// We hide rather than close the editor.
///
@@ -85,6 +114,21 @@ private void Window_Closing(object sender, CancelEventArgs e)
Hide();
e.Cancel = true;
}
+ else
+ {
+ // Dispose WebView2 to prevent finalizer crashes
+ if (!_disposed)
+ {
+ if (_webMessageHandlerAttached)
+ {
+ Browser.CoreWebView2.WebMessageReceived -= Browser_WebMessageReceived;
+ _webMessageHandlerAttached = false;
+ }
+
+ Browser?.Dispose();
+ _disposed = true;
+ }
+ }
}
///
@@ -92,8 +136,14 @@ private void Window_Closing(object sender, CancelEventArgs e)
///
private void Browser_Loaded(object sender, RoutedEventArgs e)
{
+ if (_disposed)
+ {
+ return;
+ }
+
var userDataFolder = Path.Combine(SupportFiles.SupportFileDir, "WebView2");
Directory.CreateDirectory(userDataFolder);
+
var environmentAwaiter = CoreWebView2Environment
.CreateAsync(userDataFolder: userDataFolder)
.ConfigureAwait(true)
@@ -101,13 +151,46 @@ private void Browser_Loaded(object sender, RoutedEventArgs e)
environmentAwaiter.OnCompleted(async () =>
{
+ if (_disposed)
+ {
+ return;
+ }
+
var environment = environmentAwaiter.GetResult();
- await _Browser.EnsureCoreWebView2Async(environment).ConfigureAwait(true);
+ await Browser.EnsureCoreWebView2Async(environment).ConfigureAwait(true);
+
+ if (!_webMessageHandlerAttached)
+ {
+ Browser.CoreWebView2.WebMessageReceived += Browser_WebMessageReceived;
+ await Browser.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(CloseWindowOnEscapeScript).ConfigureAwait(true);
+ _webMessageHandlerAttached = true;
+ }
+
+ // Set the preferred color scheme directly on the profile
+ Browser.CoreWebView2.Profile.PreferredColorScheme = GuiApp.MainWindow.ThemeViewModel.IsLightTheme
+ ? CoreWebView2PreferredColorScheme.Light
+ : CoreWebView2PreferredColorScheme.Dark;
// Navigate to the current specified source
Navigate();
});
}
+
+ private const string CloseWindowMessage = "PerfView.CloseWindow";
+ private const string CloseWindowMessageJson = "\"" + CloseWindowMessage + "\"";
+ private const string CloseWindowOnEscapeScript = @"
+ window.addEventListener('keydown', function (event) {
+ if (event.key === 'Escape' &&
+ !event.altKey &&
+ !event.ctrlKey &&
+ !event.metaKey &&
+ !event.shiftKey) {
+ window.chrome.webview.postMessage('PerfView.CloseWindow');
+ }
+ }, true);";
+ private bool _disposed = false;
+ private bool _webMessageHandlerAttached;
+
#endregion
}
}
diff --git a/src/PerfView/HeapView/IssueView.cs b/src/PerfView/HeapView/IssueView.cs
index 512550c34..f534eb79d 100644
--- a/src/PerfView/HeapView/IssueView.cs
+++ b/src/PerfView/HeapView/IssueView.cs
@@ -1,5 +1,6 @@
using Microsoft.Diagnostics.Tracing.Analysis.GC;
using Microsoft.Diagnostics.Tracing.Stacks;
+using PerfView.Dialogs;
using System;
using System.Collections.Generic;
using System.Windows;
@@ -244,7 +245,7 @@ private void OnOpenInducedStacks(object sender, RoutedEventArgs e)
if (source.SampleIndexLimit == 0)
{
- MessageBox.Show("No stacks found for induced GC", ".Net Heap Analyzer", MessageBoxButton.OK);
+ XamlMessageBox.Show("No stacks found for induced GC", ".Net Heap Analyzer", MessageBoxButton.OK);
}
else
{
diff --git a/src/PerfView/MainWindow.xaml b/src/PerfView/MainWindow.xaml
index c6b9d3d4d..7066af431 100644
--- a/src/PerfView/MainWindow.xaml
+++ b/src/PerfView/MainWindow.xaml
@@ -29,7 +29,6 @@
-
@@ -59,8 +58,13 @@
-
-
+
+
+
+
+
+
+
@@ -79,9 +83,6 @@
-
-
-
@@ -114,10 +115,10 @@
-
-
-
-
+
+
+
+
@@ -126,9 +127,9 @@
-
-
-
+
+
+
@@ -171,16 +172,20 @@
-
-
+
-
-
+
+
+
+
+
@@ -231,12 +236,15 @@
-
+
@@ -349,5 +357,6 @@
-
+
+
diff --git a/src/PerfView/MainWindow.xaml.cs b/src/PerfView/MainWindow.xaml.cs
index 63727da83..b5edd6319 100644
--- a/src/PerfView/MainWindow.xaml.cs
+++ b/src/PerfView/MainWindow.xaml.cs
@@ -16,11 +16,9 @@
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
-using System.Windows.Automation;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
-using System.Windows.Navigation;
using Utilities;
namespace PerfView
@@ -91,20 +89,35 @@ public MainWindow(bool testing = false)
{
if (NumWindowsNeedingSaving != 0)
{
- var result = MessageBox.Show(this, "You have unsaved notes in some Stack Views.\r\nDo you wish to exit anyway?", "Unsaved Data", MessageBoxButton.OKCancel);
+ var result = XamlMessageBox.Show(
+ this,
+ """
+ You have unsaved notes in some Stack Views.
+ Do you wish to exit anyway?
+ """,
+ "Unsaved Data",
+ MessageBoxButton.OKCancel);
+
if (result == MessageBoxResult.Cancel)
{
e.Cancel = true;
return;
}
}
+
if (StatusBar.IsWorking)
{
if (App.CommandProcessor.StopInProgress)
{
- var result = MessageBox.Show(this,
- "Closing PerfView while the trace is being processed will result in a trace that is unusable if copied off of this machine.\r\nWould you still like to close PerfView?",
- "Collecting data in progress", MessageBoxButton.YesNo);
+ var result = XamlMessageBox.Show(
+ this,
+ """
+ Closing PerfView while the trace is being processed will result in a trace that is unusable if copied off of this machine.
+ Would you still like to close PerfView?
+ """,
+ "Collecting data in progress",
+ MessageBoxButton.YesNo);
+
if (result == MessageBoxResult.No)
{
e.Cancel = true;
@@ -538,21 +551,6 @@ private void DoUnZip(object sender, RoutedEventArgs e)
});
}
- private void DoHide(object sender, RoutedEventArgs e)
- {
- // TODO need count of all active children
- if (StackWindow.StackWindows.Count > 0)
- {
- Visibility = Visibility.Hidden;
- }
- }
-
- private void CanHide(object sender, CanExecuteRoutedEventArgs e)
- {
- // TODO need count of all active children
- e.CanExecute = StackWindow.StackWindows.Count > 0;
- }
-
private void DoUserCommand(object sender, RoutedEventArgs e)
{
if (m_UserDefineCommandDialog == null)
@@ -715,8 +713,12 @@ internal void DoUserCommandHelp(object sender, RoutedEventArgs e)
private void DoAbout(object sender, RoutedEventArgs e)
{
- string versionString = "PerfView Version " + AppInfo.VersionNumber + " \r\nBuildDate: " + AppInfo.BuildDate;
- MessageBox.Show(versionString, versionString);
+ string versionString = $"""
+ PerfView Version {AppInfo.VersionNumber}
+ BuildDate: {AppInfo.BuildDate}
+ """;
+
+ XamlMessageBox.Show(versionString, versionString);
}
// Gui actions in the TreeView pane
@@ -841,8 +843,11 @@ private void DoDelete(object sender, ExecutedRoutedEventArgs e)
throw new ApplicationException("No file selected.");
}
- var response = MessageBox.Show(this,
- "Delete " + Path.GetFileName(selectedFile.FilePath) + "?", "Delete Confirmation", MessageBoxButton.OKCancel);
+ var response = XamlMessageBox.Show(
+ this,
+ $"Delete {Path.GetFileName(selectedFile.FilePath)}?",
+ "Delete Confirmation",
+ MessageBoxButton.OKCancel);
// TODO does not work with the unmerged files
if (response == MessageBoxResult.OK)
@@ -1065,6 +1070,17 @@ private void Window_Closed(object sender, EventArgs e)
// DO NOT call Environment.Exit(0) under tests, it will kill the test runner, and tests won't complete.
if (!_testing)
{
+ // Dispose all WebView2 browser controls before exiting. Environment.Exit triggers
+ // finalizers, and the WebView2 finalizer crashes if the underlying COM objects have
+ // already been torn down during process shutdown.
+ foreach (Window window in Application.Current.Windows)
+ {
+ if (window is WebBrowserWindow browserWindow)
+ {
+ browserWindow.Browser?.Dispose();
+ }
+ }
+
Environment.Exit(0);
}
}
@@ -1081,8 +1097,6 @@ private void Window_Closed(object sender, EventArgs e)
public static RoutedUICommand UnZipCommand = new RoutedUICommand("UnZip", "UnZip", typeof(MainWindow));
public static RoutedUICommand ItemHelpCommand = new RoutedUICommand("Help on Item", "ItemHelp", typeof(MainWindow));
public static RoutedUICommand OpenInBrowserCommand = new RoutedUICommand("Open in Browser", "OpenInBrowser", typeof(MainWindow));
- public static RoutedUICommand HideCommand = new RoutedUICommand("Hide", "Hide", typeof(MainWindow),
- new InputGestureCollection() { new KeyGesture(Key.H, ModifierKeys.Alt) });
public static RoutedUICommand UserCommand = new RoutedUICommand("User Command", "UserCommand", typeof(MainWindow),
new InputGestureCollection() { new KeyGesture(Key.U, ModifierKeys.Alt) });
public static RoutedUICommand RefreshDirCommand = new RoutedUICommand("Refresh Dir", "RefreshDir",
@@ -1245,6 +1259,7 @@ internal static bool DisplayUsersGuide(string anchor = null)
// Thus we abandon browsers on close.
s_Browser.Closing += delegate
{
+ // WebBrowserWindow will dispose itself in Window_Closing
s_Browser = null;
};
@@ -1313,9 +1328,14 @@ private bool AllowNavigateToWeb
m_AllowNavigateToWeb = allowNavigateToWeb == "true";
if (!m_AllowNavigateToWeb)
{
- var result = MessageBox.Show(
- "PerfView is about to open content on the web.\r\nIs this OK?",
- "Navigate to Web", MessageBoxButton.YesNo);
+ var result = XamlMessageBox.Show(
+ """
+ PerfView is about to open content on the web.
+ Is this OK?
+ """,
+ "Navigate to Web",
+ MessageBoxButton.YesNo);
+
if (result == MessageBoxResult.Yes)
{
m_AllowNavigateToWeb = true;
@@ -1440,7 +1460,7 @@ private void SetTheme_Executed(object sender, ExecutedRoutedEventArgs e)
Theme theme = ((ThemeViewModel.SetThemeCommand)e.Command).Theme;
ThemeViewModel.SetTheme(theme);
- MessageBox.Show("Restart PerfView to apply theme changes.");
+ XamlMessageBox.Show("Restart PerfView to apply theme changes.");
e.Handled = true;
}
diff --git a/src/PerfView/NativeMethods.json b/src/PerfView/NativeMethods.json
new file mode 100644
index 000000000..fa89f2f3a
--- /dev/null
+++ b/src/PerfView/NativeMethods.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://aka.ms/CsWin32.schema.json",
+ "public": false,
+ "allowMarshaling": false,
+ "useSafeHandles": false,
+ "className": "PInvoke",
+ "comInterop": {
+ "preserveSigMethods": [
+ "*"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/src/PerfView/NativeMethods.txt b/src/PerfView/NativeMethods.txt
new file mode 100644
index 000000000..c402ef249
--- /dev/null
+++ b/src/PerfView/NativeMethods.txt
@@ -0,0 +1,4 @@
+SHSTOCKICONID
+SHGSI_FLAGS
+HICON
+HRESULT
\ No newline at end of file
diff --git a/src/PerfView/OtherSources/DebuggerStackSource.cs b/src/PerfView/OtherSources/DebuggerStackSource.cs
index dacaa4835..8e67a4935 100644
--- a/src/PerfView/OtherSources/DebuggerStackSource.cs
+++ b/src/PerfView/OtherSources/DebuggerStackSource.cs
@@ -30,6 +30,23 @@ private struct DebuggerCallStackFrame
public StackSourceFrameIndex frame;
}
+ private void AddSampleFromStack(GrowableArray stack, StackSourceSample sample, ref float time)
+ {
+ StackSourceCallStackIndex parent = StackSourceCallStackIndex.Invalid;
+ for (int i = stack.Count - 1; i >= 0; --i)
+ {
+ parent = Interner.CallStackIntern(stack[i].frame, parent);
+ }
+
+ stack.Clear();
+
+ sample.Metric = 1;
+ sample.StackIndex = parent;
+ sample.TimeRelativeMSec = time;
+ time++;
+ AddSample(sample);
+ }
+
private void Read(TextReader reader)
{
var framePattern = new Regex(@"\b(\w+?)\!(\S\(?[\S\s]*\)?)");
@@ -93,25 +110,20 @@ private void Read(TextReader reader)
// clear the stack
if (stack.Count != 0)
{
-
- StackSourceCallStackIndex parent = StackSourceCallStackIndex.Invalid;
- for (int i = stack.Count - 1; i >= 0; --i)
- {
- parent = Interner.CallStackIntern(stack[i].frame, parent);
- }
-
- stack.Clear();
-
- sample.StackIndex = parent;
- sample.TimeRelativeMSec = time;
- time++;
- AddSample(sample);
+ AddSampleFromStack(stack, sample, ref time);
}
newCallStackFound = true;
}
}
}
+
+ // Handle the last sample if there are any remaining frames
+ if (stack.Count != 0)
+ {
+ AddSampleFromStack(stack, sample, ref time);
+ }
+
Interner.DoneInterning();
}
#endregion
diff --git a/src/PerfView/PerfView.csproj b/src/PerfView/PerfView.csproj
index 33cbf4ab2..ba5618edf 100644
--- a/src/PerfView/PerfView.csproj
+++ b/src/PerfView/PerfView.csproj
@@ -1,5 +1,5 @@
ο»Ώ
-
+
net462
@@ -9,12 +9,13 @@
true
true
false
+ true
PerfView
Microsoft
Copyright Β© Microsoft 2010
$(PerfViewVersion)
-
+
AnyCPU
@@ -72,6 +73,7 @@
+
@@ -83,25 +85,33 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -112,13 +122,14 @@
HeapDump dependencies are pulled from the HeapDump output directory because HeapDump runs out of process
and can have a different set of dependencies.
-->
-
-
-
-
+
+
+
+
+
-
+
@@ -169,9 +180,18 @@
Utilities\FileUtilities.cs
+
+ Utilities\PathUtilities.cs
+
Utilities\StringUtilities.cs
+
+ Utilities\SymbolCachePathUtilities.cs
+
+
+ Utilities\SymbolsAuthenticationUtilities.cs
+
Utilities\XmlUtilities.cs
@@ -302,6 +322,13 @@
HeapDump\System.Collections.Immutable.dll
False
+
+ Non-Resx
+ false
+ .\HeapDump\System.Runtime.CompilerServices.Unsafe.dll
+ HeapDump\System.Runtime.CompilerServices.Unsafe.dll
+ False
+
Non-Resx
false
@@ -394,6 +421,13 @@
Microsoft.Diagnostics.FastSerialization.dll
False
+
+ Non-Resx
+ false
+ .\Microsoft.Bcl.HashCode.dll
+ Microsoft.Bcl.HashCode.dll
+ False
+
Non-Resx
false
@@ -436,7 +470,7 @@
x86\Microsoft.Diagnostics.Runtime.dll
False
-
+
Non-Resx
false
.\x86\Microsoft.Diagnostics.Runtime.dll
@@ -510,14 +544,14 @@
Microsoft.Identity.Client.Extensions.Msal.dll
False
-
+
Non-Resx
false
.\Microsoft.IdentityModel.Abstractions.dll
Microsoft.IdentityModel.Abstractions.dll
False
-
+
Non-Resx
false
.\Microsoft.IdentityModel.JsonWebTokens.dll
@@ -566,14 +600,14 @@
runtimes\win-arm64\native\WebView2Loader.dll
False
-
+
Non-Resx
false
.\Microsoft.IdentityModel.Tokens.dll
Microsoft.IdentityModel.Tokens.dll
False
-
+
Non-Resx
false
.\System.Buffers.dll
@@ -587,27 +621,48 @@
System.Diagnostics.DiagnosticSource.dll
False
-
+
+ Non-Resx
+ false
+ .\System.IO.Hashing.dll
+ System.IO.Hashing.dll
+ False
+
+
Non-Resx
false
.\System.Memory.dll
System.Memory.dll
False
-
+
Non-Resx
false
.\System.Numerics.Vectors.dll
System.Numerics.Vectors.dll
False
-
+
Non-Resx
false
.\System.Security.Cryptography.ProtectedData.dll
System.Security.Cryptography.ProtectedData.dll
False
+
+ Non-Resx
+ false
+ .\Microsoft.Bcl.AsyncInterfaces.dll
+ Microsoft.Bcl.AsyncInterfaces.dll
+ False
+
+
+ Non-Resx
+ false
+ .\System.IO.Pipelines.dll
+ System.IO.Pipelines.dll
+ False
+
Non-Resx
false
@@ -622,7 +677,7 @@
System.Text.Json.dll
False
-
+
Non-Resx
false
.\System.Threading.Tasks.Extensions.dll
@@ -756,6 +811,8 @@
+
+
@@ -769,7 +826,7 @@
Microsoft400
-
+
diff --git a/src/PerfView/PerfViewData.cs b/src/PerfView/PerfViewData.cs
index d7fc653a4..20e601268 100644
--- a/src/PerfView/PerfViewData.cs
+++ b/src/PerfView/PerfViewData.cs
@@ -1,4 +1,4 @@
-using Diagnostics.Tracing.StackSources;
+ο»Ώusing Diagnostics.Tracing.StackSources;
using global::DiagnosticsHub.Packaging.Interop;
using Graphs;
using Microsoft.Diagnostics.Symbols;
@@ -7,6 +7,7 @@
using Microsoft.Diagnostics.Tracing.Etlx;
using Microsoft.Diagnostics.Tracing.EventPipe;
using Microsoft.Diagnostics.Tracing.Parsers;
+using Microsoft.Diagnostics.Tracing.Parsers.Universal.Events;
using Microsoft.Diagnostics.Tracing.Parsers.AspNet;
using Microsoft.Diagnostics.Tracing.Parsers.Clr;
using Microsoft.Diagnostics.Tracing.Parsers.ClrPrivate;
@@ -42,7 +43,7 @@
using Utilities;
using Address = System.UInt64;
using EventSource = EventSources.EventSource;
-using Microsoft.Diagnostics.Tracing.Parsers.Universal.Events;
+using PerfView.Dialogs;
namespace PerfView
{
@@ -743,6 +744,11 @@ protected virtual Action OpenImpl(Window parentWindow, StatusBar worker)
};
}
+ ///
+ /// Called when a stack window is launched but after the processes have been selected.
+ ///
+ protected internal virtual void OnStackWindowLaunch(Window parentWindow, List processIDs, string sourceName) { }
+
protected internal virtual void ConfigureStackWindow(string stackSourceName, StackWindow stackWindow) { }
///
/// Allows you to do a first action after everything is done.
@@ -1204,55 +1210,84 @@ private TraceLog GetTrace(StatusBar worker)
private string GenerateReportFile(StatusBar worker, TraceLog trace)
{
- var reportFileName = CacheFiles.FindFile(FilePath, "." + Name + ".html");
- using (var writer = File.CreateText(reportFileName))
- {
- writer.WriteLine("");
- writer.WriteLine("");
- writer.WriteLine("{0} ", Title);
- writer.WriteLine(" ");
- writer.WriteLine(" ");
-
- // Add basic styling to the generated HTML
- writer.WriteLine(@"
-
-");
-
- writer.WriteLine("");
- writer.WriteLine("");
- WriteHtmlBody(trace, writer, reportFileName, worker.LogWriter);
- writer.WriteLine("");
- writer.WriteLine("");
-
-
- }
+ var reportFileName = CacheFiles.FindFile(FilePath, $".{Name}.html");
+ using var writer = File.CreateText(reportFileName);
+ writer.WriteLine($$"""
+
+
+ {{Title}}
+
+
+
+
+
+ """);
+
+ WriteHtmlBody(trace, writer, reportFileName, worker.LogWriter);
+ writer.WriteLine("""
+
+
+
+ """);
return reportFileName;
}
@@ -4616,6 +4651,9 @@ public override void Open(Window parentWindow, StatusBar worker, Action doAfter
SetProcessFilter(incPat);
}
+ // Call the launch hook to perform any initialization needed after process selection
+ DataFile.OnStackWindowLaunch(parentWindow, processIDs, SourceName);
+
Viewer.StatusBar.StartWork("Looking up high importance PDBs that are locally cached", delegate
{
// TODO This is probably a hack that it is here.
@@ -4641,13 +4679,16 @@ public override void Open(Window parentWindow, StatusBar worker, Action doAfter
// Catch the error if you don't merge and move to a new machine.
if (traceLog != null && !traceLog.CurrentMachineIsCollectionMachine() && !traceLog.HasPdbInfo)
{
- MessageBox.Show(parentWindow,
- "Warning! This file was not merged and was moved from the collection\r\n" +
- "machine. This means the data is incomplete and symbolic name resolution\r\n" +
- "will NOT work. The recommended fix is use the perfview (not windows OS)\r\n" +
- "zip command. Right click on the file in the main view and select ZIP.\r\n" +
- "\r\n" +
- "See merging and zipping in the users guide for more information.",
+ XamlMessageBox.Show(
+ parentWindow,
+ """
+ Warning! This file was not merged and was moved from the collection
+ machine. This means the data is incomplete and symbolic name resolution
+ will NOT work. The recommended fix is use the perfview (not windows OS)
+ zip command. Right click on the file in the main view and select ZIP.
+
+ See merging and zipping in the users guide for more information.
+ """,
"Data not merged before leaving the machine!");
}
@@ -4709,6 +4750,7 @@ protected internal virtual void FirstAction(StackWindow stackWindow)
{
DataFile.FirstAction(stackWindow);
}
+
public override ImageSource Icon { get { return GuiApp.MainWindow.Resources["StackSourceBitmapImage"] as ImageSource; } }
// If set, we don't show the process selection dialog.
@@ -4725,6 +4767,14 @@ private bool WarnAboutBrokenStacks(Window parentWindow, TextWriter log)
if (!m_WarnedAboutBrokenStacks)
{
m_WarnedAboutBrokenStacks = true;
+
+ // Only run broken stack analysis for ETW traces, as the logic is specific to ETW.
+ // Universal traces, EventPipe traces, Linux traces, etc. should not use this analysis.
+ if (!(DataFile is ETLPerfViewData))
+ {
+ return false;
+ }
+
float brokenPercent = Viewer.CallTree.Root.GetBrokenStackCount() * 100 / Viewer.CallTree.Root.InclusiveCount;
if (brokenPercent > 0)
{
@@ -4746,14 +4796,19 @@ private static bool WarnAboutBrokenStacks(Window parentWindow, float brokenPerce
{
if (brokenPercent > 1)
{
- log.WriteLine("Finished aggregating stacks. (" + brokenPercent.ToString("f1") + "% Broken Stacks)");
+ log.WriteLine($"Finished aggregating stacks. ({brokenPercent:f1}% Broken Stacks)");
}
if (brokenPercent > 10)
{
- MessageBox.Show(parentWindow, "Warning: There are " + brokenPercent.ToString("f1") + "% stacks that are broken\r\n" +
- "Top down analysis is suspect, however bottom up approaches are still valid.\r\n\r\n" +
- "Use the troubleshooting link at the top of the view for more information.\r\n",
+ XamlMessageBox.Show(
+ parentWindow,
+ $"""
+ Warning: There are {brokenPercent:f1}% stacks that are broken.
+ Top down analysis is suspect, however bottom up approaches are still valid.
+
+ Use the troubleshooting link at the top of the view for more information.
+ """,
"Broken Stacks");
return true;
@@ -5557,12 +5612,15 @@ protected internal override StackSource OpenStackSourceImpl(string streamName, T
{
// TODO FIX NOW, investigate the missing events. All we know is that incs and dec are not
// consistent with the RefCount value that is in the events.
- GuiApp.MainWindow.Dispatcher.BeginInvoke((Action)delegate ()
- {
- MessageBox.Show(GuiApp.MainWindow,
- "Warning: the Interop CCW events on which this data is based seem to be incomplete.\r\n" +
- "There seem to be missing instrumentation, which make the referenct counts unreliable\r\n"
- , "Data May be Incorrect");
+ GuiApp.MainWindow.Dispatcher.BeginInvoke(() =>
+ {
+ MessageBox.Show(
+ GuiApp.MainWindow,
+ """
+ Warning: the Interop CCW events on which this data is based seem to be incomplete.
+ There seem to be missing instrumentation, which make the referenct counts unreliable
+ """,
+ "Data May be Incorrect");
});
var objectToTypeMap = new Dictionary(1000);
@@ -6811,6 +6869,83 @@ string GetAllocationType(CallStackIndex csi)
}
#region private
+
+ ///
+ /// Checks if RuntimeStart events exist for the selected processes and warns if missing.
+ /// This indicates whether the processes were started after tracing began, which is required
+ /// for proper type information in unsampled allocation traces.
+ ///
+ private void WarnAboutMissingTypeInfoForProcesses(Window parentWindow, List processIDs, string sourceName)
+ {
+ bool showWarning = true;
+
+ // Only check for allocation-related views
+ if (!sourceName.Equals("GC Heap Alloc Ignore Free") &&
+ !sourceName.Equals("GC Heap Net Mem") &&
+ !sourceName.Equals("Gen 2 Object Deaths"))
+ {
+ return;
+ }
+
+ TraceLog traceLog = TryGetTraceLog();
+ if (traceLog == null)
+ {
+ return;
+ }
+
+ // Check if we have RuntimeStart events for the selected processes
+ HashSet selectedProcessesWithRuntimeStartEvents = new HashSet();
+
+ if (processIDs != null && processIDs.Count > 0)
+ {
+ using TraceLogEventSource source = traceLog.Events.GetSource();
+
+ source.Clr.RuntimeStart += delegate (RuntimeInformationTraceData data)
+ {
+ if (processIDs.Contains(data.ProcessID))
+ {
+ selectedProcessesWithRuntimeStartEvents.Add(data.ProcessID);
+ }
+ };
+
+ source.Process();
+
+ showWarning = processIDs.Count != selectedProcessesWithRuntimeStartEvents.Count;
+ }
+ else
+ {
+ // If no specific processes selected, check if any RuntimeStart exists
+ foreach (var stats in traceLog.Stats)
+ {
+ if (stats.ProviderGuid == ClrTraceEventParser.ProviderGuid && stats.EventName == "Runtime/Start")
+ {
+ showWarning = false;
+ break;
+ }
+ }
+ }
+
+ // Show warning if RuntimeStart not found for selected processes
+ if (showWarning)
+ {
+ var warning = $"""
+ WARNING: The '{sourceName}' view may be missing type information.
+
+ This can happen when the ETW circular buffer wraps and loses early events including type definitions. Without these type definitions, many types will appear as "UNKNOWN" in the allocation view.
+
+ To fix this issue, perform one of the following:
+ β’ Re-capture the trace with a shorter duration
+ β’ Re-capture the trace with a larger circular buffer size (e.g., /BufferSize:1024)
+ """;
+
+ XamlMessageBox.Show(
+ parentWindow,
+ warning,
+ "Trace May Be Missing Type Information",
+ MessageBoxButton.OK);
+ }
+ }
+
private static StackSource GetProcessFileRegistryStackSource(TraceLogEventSource eventSource, TextWriter log)
{
TraceLog traceLog = eventSource.TraceLog;
@@ -7537,19 +7672,28 @@ protected internal override void ConfigureStackWindow(string stackSourceName, St
{
if (App.UserConfigData["WarnedAboutOsHeapAllocTypes"] == null)
{
- MessageBox.Show(stackWindow,
- "Warning: Allocation type resolution only happens on window launch.\r\n" +
- "Thus if you manually lookup symbols in this view you will get method\r\n" +
- "names of allocations sites, but to get the type name associated the \r\n" +
- "allocation site.\r\n" +
- "\r\n" +
- "You must close and reopen this window to get the allocation types.\r\n"
- , "May need to resolve PDBs and reopen.");
+ XamlMessageBox.Show(
+ stackWindow,
+ """
+ Warning: Allocation type resolution only happens on window launch.
+ Thus if you manually lookup symbols in this view you will get method
+ names of allocations sites, but to get the type name associated the
+ allocation site.
+
+ You must close and reopen this window to get the allocation types.
+ """,
+ "May need to resolve PDBs and reopen.");
App.UserConfigData["WarnedAboutOsHeapAllocTypes"] = "true";
}
}
}
+ protected internal override void OnStackWindowLaunch(Window parentWindow, List processIDs, string sourceName)
+ {
+ // Check for RuntimeStart events for the selected processes and warn if missing
+ WarnAboutMissingTypeInfoForProcesses(parentWindow, processIDs, sourceName);
+ }
+
public override bool SupportsProcesses { get { return true; } }
///
@@ -7625,13 +7769,15 @@ protected override Action OpenImpl(Window parentWindow, StatusBar worker
if (!m_notifiedAboutWin8)
{
m_notifiedAboutWin8 = true;
- var versionMismatchWarning = "This trace was captured on Window 8 and is being read\r\n" +
- "on and earlier OS. If you experience any problems please\r\n" +
- "read the trace on an Windows 8 OS.";
+ var versionMismatchWarning = """
+ This trace was captured on Window 8 and is being read
+ on and earlier OS. If you experience any problems please
+ read the trace on an Windows 8 OS.
+ """;
worker.LogWriter.WriteLine(versionMismatchWarning);
- parentWindow.Dispatcher.BeginInvoke((Action)delegate ()
+ parentWindow.Dispatcher.BeginInvoke(() =>
{
- MessageBox.Show(parentWindow, versionMismatchWarning, "Log File Version Mismatch", MessageBoxButton.OK);
+ XamlMessageBox.Show(parentWindow, versionMismatchWarning, "Log File Version Mismatch", MessageBoxButton.OK);
});
}
}
@@ -8237,11 +8383,18 @@ public TraceLog GetTraceLog(TextWriter log, Action onLostEvents
m_traceLog.CodeAddresses.UnsafePDBMatching = true;
}
- if (m_traceLog.Truncated) // Warn about truncation.
+ if (m_traceLog.Truncated) // Warn about truncation.
{
- GuiApp.MainWindow.Dispatcher.BeginInvoke((Action)delegate ()
+ GuiApp.MainWindow.Dispatcher.BeginInvoke(() =>
{
- MessageBox.Show("The ETL file was too big to convert and was truncated.\r\nSee log for details", "Log File Truncated", MessageBoxButton.OK);
+ XamlMessageBox.Show(
+ """
+ The ETL file was too big to convert and was truncated.
+ See log for details.
+ """,
+ "Log File Truncated",
+ MessageBoxButton.OK);
+
});
}
return m_traceLog;
@@ -8267,9 +8420,9 @@ private void HandleLostEvents(Window parentWindow, bool truncated, int numberOfL
}
MessageBoxResult result = MessageBoxResult.None;
- parentWindow.Dispatcher.BeginInvoke((Action)delegate ()
+ parentWindow.Dispatcher.BeginInvoke(() =>
{
- result = MessageBox.Show(parentWindow, warning, "Lost Events", MessageBoxButton.OKCancel);
+ result = XamlMessageBox.Show(parentWindow, warning, "Lost Events", MessageBoxButton.OKCancel);
worker.LogWriter.WriteLine(warning);
if (result != MessageBoxResult.OK)
{
@@ -9386,7 +9539,14 @@ public TraceLog GetTraceLog(TextWriter log)
{
GuiApp.MainWindow.Dispatcher.BeginInvoke((Action)delegate ()
{
- MessageBox.Show("The ETL file was too big to convert and was truncated.\r\nSee log for details", "Log File Truncated", MessageBoxButton.OK);
+ XamlMessageBox.Show(
+ """
+ The ETL file was too big to convert and was truncated.
+ See log for details.
+ """,
+ "Log File Truncated",
+ MessageBoxButton.OK);
+
});
}
return m_traceLog;
@@ -9411,6 +9571,7 @@ public partial class EventPipePerfViewData : PerfViewFile
public override bool SupportsProcesses => m_supportsProcesses;
private bool m_supportsProcesses;
+ private bool m_hasUniversal;
public override List GetProcesses(TextWriter log)
{
@@ -9464,6 +9625,7 @@ protected override Action OpenImpl(Window parentWindow, StatusBar worker
bool hasExceptions = false;
bool hasUniversalSystem = false;
bool hasUniversalCPU = false;
+ bool hasUniversalCSwitch = false;
if (m_traceLog != null)
{
foreach (TraceEventCounts eventStats in m_traceLog.Stats)
@@ -9523,6 +9685,7 @@ protected override Action OpenImpl(Window parentWindow, StatusBar worker
}
else if (eventStats.ProviderGuid == UniversalSystemTraceEventParser.ProviderGuid)
{
+ m_hasUniversal = true;
hasUniversalSystem = true;
m_supportsProcesses = true;
}
@@ -9531,6 +9694,11 @@ protected override Action OpenImpl(Window parentWindow, StatusBar worker
hasUniversalCPU = true;
m_supportsProcesses = true;
}
+ else if (eventStats.ProviderGuid == UniversalEventsTraceEventParser.ProviderGuid && eventStats.EventName.StartsWith("cswitch"))
+ {
+ hasUniversalCSwitch = true;
+ m_supportsProcesses = true;
+ }
}
}
@@ -9548,6 +9716,11 @@ protected override Action OpenImpl(Window parentWindow, StatusBar worker
{
m_Children.Add(new PerfViewStackSource(this, "CPU"));
}
+
+ if (hasUniversalCSwitch)
+ {
+ m_Children.Add(new PerfViewStackSource(this, "Thread Time"));
+ }
}
else // dotnet-trace
{
@@ -9840,6 +10013,11 @@ protected internal override StackSource OpenStackSourceImpl(string streamName, T
return stackSource;
}
+ case "Thread Time":
+ {
+ var eventLog = GetTraceLog(log);
+ return eventLog.ThreadTimeStacks();
+ }
default:
{
var eventLog = GetTraceLog(log);
@@ -10035,7 +10213,7 @@ protected internal override void ConfigureStackWindow(string stackSourceName, St
stackWindow.ExcludeRegExTextBox.Text = excludePat;
}
- if (stackSourceName.Contains("Thread Time"))
+ if (!m_hasUniversal && stackSourceName.Contains("Thread Time"))
{
stackWindow.ScalingPolicy = ScalingPolicyKind.TimeMetric;
stackWindow.FoldRegExTextBox.Text += ";UNMANAGED_CODE_TIME;CPU";
@@ -10164,7 +10342,14 @@ public TraceLog GetTraceLog(TextWriter log, Action onLostEvents
{
GuiApp.MainWindow.Dispatcher.BeginInvoke((Action)delegate ()
{
- MessageBox.Show("The ETL file was too big to convert and was truncated.\r\nSee log for details", "Log File Truncated", MessageBoxButton.OK);
+ MessageBox.Show(
+ """
+ The ETL file was too big to convert and was truncated.
+ See log for details.
+ """,
+ "Log File Truncated",
+ MessageBoxButton.OK);
+
});
}
return m_traceLog;
@@ -10263,24 +10448,22 @@ public override void LookupSymbolsForModule(string simpleModuleName, TextWriter
private void HandleLostEvents(Window parentWindow, bool truncated, int numberOfLostEvents, int eventCountAtTrucation, StatusBar worker)
{
- string warning;
- if (!truncated)
- {
- warning = "WARNING: There were " + numberOfLostEvents + " lost events in the trace.\r\n" +
- "Some analysis might be invalid.";
- }
- else
- {
- warning = "WARNING: The ETLX file was truncated at " + eventCountAtTrucation + " events.\r\n" +
- "This is to keep the ETLX file size under 4GB, however all rundown events are processed.\r\n" +
- "Use /SkipMSec:XXX after clearing the cache (File->Clear Temp Files) to see the later parts of the file.\r\n" +
- "See log for more details.";
- }
+ string warning = !truncated
+ ? $"""
+ WARNING: There were {numberOfLostEvents} lost events in the trace.
+ Some analysis might be invalid.
+ """
+ : $"""
+ WARNING: The ETLX file was truncated at {eventCountAtTrucation} events.
+ This is to keep the ETLX file size under 4GB, however all rundown events are processed.
+ Use /SkipMSec:XXX after clearing the cache (File->Clear Temp Files) to see the later parts of the file.
+ See log for more details.
+ """;
MessageBoxResult result = MessageBoxResult.None;
parentWindow.Dispatcher.BeginInvoke((Action)delegate ()
{
- result = MessageBox.Show(parentWindow, warning, "Lost Events", MessageBoxButton.OKCancel);
+ result = XamlMessageBox.Show(parentWindow, warning, "Lost Events", MessageBoxButton.OKCancel);
worker.LogWriter.WriteLine(warning);
if (result != MessageBoxResult.OK)
{
@@ -10388,17 +10571,22 @@ public string ResolveTypeName(int typeID, Graphs.Module module)
{
if (m_numFailures == 1 && !Path.GetFileName(module.Path).StartsWith("mrt", StringComparison.OrdinalIgnoreCase))
{
- GuiApp.MainWindow.Dispatcher.BeginInvoke((Action)delegate ()
+ GuiApp.MainWindow.Dispatcher.BeginInvoke(() =>
{
- MessageBox.Show(GuiApp.MainWindow,
- "Warning: Could not find PDB for module " + Path.GetFileName(module.Path) + "\r\n" +
- "Some types will not have symbolic names.\r\n" +
- "See log for more details.\r\n" +
- "Fix by placing PDB on symbol path or in a directory called 'symbols' beside .gcdump file.",
+ XamlMessageBox.Show(
+ GuiApp.MainWindow,
+ $"""
+ Warning: Could not find PDB for module {Path.GetFileName(module.Path)}.
+ Some types will not have symbolic names.
+ See log for more details.
+ Fix by placing PDB on symbol path or in a directory called 'symbols' beside .gcdump file.
+ """,
"PDB lookup failure");
});
}
- m_log.WriteLine("Failed to find PDB for module {0} to look up type 0x{1:x}", module.Path, typeID);
+
+ m_log.WriteLine($"Failed to find PDB for module {module.Path} to look up type 0x{typeID:x}");
+
if (m_numFailures == 5)
{
m_log.WriteLine("Discontinuing PDB module lookup messages");
@@ -10808,7 +10996,12 @@ private static string GetLocalFilePath(string packageFilePath, DhPackage package
/// The full local path to the resource
private static string GetLocalDirPath(string packageFilePath, DhPackage package, ResourceInfo resource)
{
- string localDirName = resource.Name;
+ string localDirName = GetSafeDiagSessionResourceDirectoryName(resource.Name);
+ if (localDirName == null)
+ {
+ return null;
+ }
+
string localDirPath = CacheFiles.FindFile(packageFilePath, "_" + localDirName);
if (!Directory.Exists(localDirPath))
@@ -10819,6 +11012,31 @@ private static string GetLocalDirPath(string packageFilePath, DhPackage package,
return localDirPath;
}
+ ///
+ /// Sanitizes an attacker-controlled .diagsession resource name into a safe directory-name fragment.
+ /// Mirrors the sibling which uses
+ /// to strip any path components from the metadata before it is concatenated into a cache path.
+ /// Returns null if the sanitized name is empty, ".", or ".." so the caller can skip the resource
+ /// rather than create an oddly-named cache entry.
+ ///
+ internal static string GetSafeDiagSessionResourceDirectoryName(string resourceName)
+ {
+ string sanitized = Path.GetFileNameWithoutExtension(resourceName ?? string.Empty);
+ if (string.IsNullOrEmpty(sanitized) || sanitized == "." || sanitized == "..")
+ {
+ return null;
+ }
+
+ // Reject any character that the OS would consider invalid in a file name
+ // (e.g. ':' which on NTFS would create an Alternate Data Stream).
+ if (sanitized.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
+ {
+ return null;
+ }
+
+ return sanitized;
+ }
+
///
/// Adds child files from resources in the DhPackage
///
@@ -10857,6 +11075,11 @@ private void ExtractSymbolResources(StatusBar worker, DhPackage dhPackage)
foreach (var resource in resources)
{
string localDirPath = GetLocalDirPath(FilePath, dhPackage, resource);
+ if (localDirPath == null)
+ {
+ worker.Log("Skipping symbol cache resource '" + resource.ResourceId + "' with unsafe name '" + resource.Name + "'.");
+ continue;
+ }
worker.Log("Found '" + resource.ResourceId + "' resource '" + resource.Name + "'. Loading ...");
diff --git a/src/PerfView/StackViewer/FlameGraph.cs b/src/PerfView/StackViewer/FlameGraph.cs
index d8588a169..e64ecb6a6 100644
--- a/src/PerfView/StackViewer/FlameGraph.cs
+++ b/src/PerfView/StackViewer/FlameGraph.cs
@@ -83,7 +83,18 @@ public static IEnumerable Calculate(CallTree callTree, double maxWidth
public static void Export(Canvas flameGraphCanvas, string filePath)
{
var rectangle = new Rect(flameGraphCanvas.RenderSize);
- var renderTargetBitmap = new RenderTargetBitmap((int)rectangle.Right, (int)rectangle.Bottom, 96d, 96d, PixelFormats.Default);
+ int width = (int)rectangle.Right;
+ int height = (int)rectangle.Bottom;
+
+ // Validate that the canvas has a valid size before attempting to export
+ if (width <= 0 || height <= 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(flameGraphCanvas),
+ $"Canvas has an invalid size (width={width}, height={height}). Please ensure the flame graph is visible and has been rendered before attempting to export.");
+ }
+
+ var renderTargetBitmap = new RenderTargetBitmap(width, height, 96d, 96d, PixelFormats.Default);
renderTargetBitmap.Render(flameGraphCanvas);
var pngEncoder = new PngBitmapEncoder();
diff --git a/src/PerfView/StackViewer/PerfDataGrid.xaml b/src/PerfView/StackViewer/PerfDataGrid.xaml
index 1af460eab..649c7199c 100644
--- a/src/PerfView/StackViewer/PerfDataGrid.xaml
+++ b/src/PerfView/StackViewer/PerfDataGrid.xaml
@@ -25,6 +25,8 @@
1 && !m_isFirstLastSelection);
+
+ for (int columnIndex = 0; columnIndex < e.ClipboardRowContent.Count; columnIndex++)
{
- var clipboardContent = e.ClipboardRowContent[i];
+ var clipboardContent = e.ClipboardRowContent[columnIndex];
string morphedContent = null;
if (e.IsColumnHeadersRow)
@@ -60,19 +65,22 @@ public PerfDataGrid()
// Pad so that pasting into a text window works well.
if (e.ClipboardRowContent.Count > 1 && !NoPadOnCopyToClipboard)
{
- morphedContent = PadForColumn(morphedContent, i + e.StartColumnDisplayIndex);
+ morphedContent = PadForColumn(morphedContent, columnIndex + e.StartColumnDisplayIndex);
}
- // Add a leading | character to the first column to ensure GitHub renders the content as table
- if (i == 0)
- {
- morphedContent = "| " + morphedContent;
- }
-
- // Add a trailing | character to the last column to complete the markdown table row
- if (i == e.ClipboardRowContent.Count - 1)
+ if (shouldAddPipes)
{
- morphedContent = morphedContent + " |";
+ // Add a leading | character to the first column for markdown table format
+ if (columnIndex == 0)
+ {
+ morphedContent = "| " + morphedContent;
+ }
+
+ // Add a trailing | character to the last column to complete the markdown table row
+ if (columnIndex == e.ClipboardRowContent.Count - 1)
+ {
+ morphedContent = morphedContent + " |";
+ }
}
// TODO Ugly, morph two cells on different rows into one line for the correct cut/paste experience
@@ -92,7 +100,7 @@ public PerfDataGrid()
return;
}
}
- e.ClipboardRowContent[i] = new DataGridClipboardCellContent(clipboardContent.Item, clipboardContent.Column, morphedContent);
+ e.ClipboardRowContent[columnIndex] = new DataGridClipboardCellContent(clipboardContent.Item, clipboardContent.Column, morphedContent);
}
};
@@ -520,27 +528,90 @@ private void SelectedCellsChanged(object sender, SelectedCellsChangedEventArgs e
{
// We don't want the header for single values, or for 2 (for cutting and pasting ranges).
int numSelectedCells = window.SelectedCellsChanged(sender, e);
- if (numSelectedCells <= 2)
+ m_numSelectedCells = numSelectedCells;
+
+ // Calculate the number of unique columns and rows selected
+ DataGrid dataGrid = sender as DataGrid;
+ if (dataGrid != null && dataGrid.SelectedCells.Count > 0)
{
- if (numSelectedCells == 2)
+ var uniqueColumns = new HashSet();
+ var uniqueRows = new HashSet();
+ var columnNames = new HashSet();
+ foreach (var cell in dataGrid.SelectedCells)
{
- var dataGrid = sender as DataGrid;
- if (dataGrid != null)
+ uniqueColumns.Add(cell.Column);
+ uniqueRows.Add(cell.Item);
+ // Get the column name
+ if (cell.Column.Header is TextBlock header)
{
- var cells = dataGrid.SelectedCells;
- if (cells != null)
- {
- m_clipboardRangeStart = GetCellStringValue(cells[0]);
- m_clipboardRangeEnd = GetCellStringValue(cells[1]);
- }
+ columnNames.Add(header.Name);
}
}
- Grid.ClipboardCopyMode = DataGridClipboardCopyMode.ExcludeHeader;
+ m_numSelectedColumns = uniqueColumns.Count;
+ m_numSelectedRows = uniqueRows.Count;
+
+ // Detect special case: 1 row, 2 columns, and they are FirstColumn and LastColumn
+ // The last two columns in the grid are "First" and "Last" which represent the time range. It's very common
+ // for users to select these two and copy them so that they can be used to filter the data in this or another view.
+ // When a user does this, we don't want to add headers or '|' symbols and display as a markdown table. We detect
+ // this scenario here because markdown support is added in the CopyingRowClipboardContent event handler where we don't
+ // have access to the column names.
+ m_isFirstLastSelection = (m_numSelectedRows == 1 && m_numSelectedColumns == 2 &&
+ columnNames.Contains("FirstColumn") && columnNames.Contains("LastColumn"));
}
else
+ {
+ m_numSelectedColumns = 0;
+ m_numSelectedRows = 0;
+ m_isFirstLastSelection = false;
+ }
+
+ // Determine whether to include headers based on selection:
+ // - Single cell: no header
+ // - First/Last special case: no header
+ // - Single column, multiple cells: include header
+ // - Multiple columns, single row: include header (unless First/Last case)
+ // - Multiple columns, multiple rows: include header
+ bool shouldIncludeHeader = false;
+ if (numSelectedCells == 1 || m_isFirstLastSelection)
+ {
+ // Single cell or First/Last special case: no header
+ shouldIncludeHeader = false;
+ }
+ else if (m_numSelectedColumns == 1 && m_numSelectedRows > 1)
+ {
+ // Single column, multiple rows: include header
+ shouldIncludeHeader = true;
+ }
+ else if (m_numSelectedColumns > 1)
+ {
+ // Multiple columns: include header
+ shouldIncludeHeader = true;
+ }
+
+ if (shouldIncludeHeader)
{
Grid.ClipboardCopyMode = DataGridClipboardCopyMode.IncludeHeader;
}
+ else
+ {
+ Grid.ClipboardCopyMode = DataGridClipboardCopyMode.ExcludeHeader;
+ }
+
+ // Only set range values for the First/Last special case
+ // This enables the special morphing logic that combines them on one line
+ if (m_isFirstLastSelection && numSelectedCells == 2)
+ {
+ if (dataGrid != null)
+ {
+ var cells = dataGrid.SelectedCells;
+ if (cells != null)
+ {
+ m_clipboardRangeStart = GetCellStringValue(cells[0]);
+ m_clipboardRangeEnd = GetCellStringValue(cells[1]);
+ }
+ }
+ }
}
m_maxColumnInSelection = null;
}
@@ -559,6 +630,10 @@ private void DoHyperlinkHelp(object sender, System.Windows.RoutedEventArgs e)
///
private string m_clipboardRangeStart;
private string m_clipboardRangeEnd;
+ private int m_numSelectedCells;
+ private int m_numSelectedColumns;
+ private int m_numSelectedRows;
+ private bool m_isFirstLastSelection;
private int[] m_maxColumnInSelection;
private int m_FindEnd;
private Regex m_findPat;
diff --git a/src/PerfView/StackViewer/StackWindow.xaml.cs b/src/PerfView/StackViewer/StackWindow.xaml.cs
index c7a23aaf4..13257f58b 100644
--- a/src/PerfView/StackViewer/StackWindow.xaml.cs
+++ b/src/PerfView/StackViewer/StackWindow.xaml.cs
@@ -1325,19 +1325,19 @@ private void DoFindInCallTreeName(object sender, ExecutedRoutedEventArgs e)
private void DoViewInCallerCallee(object sender, RoutedEventArgs e)
{
- SetFocus(GetSelectedNodes().Single());
+ SetFocus(GetSelectedNodes().Single().Name);
CallerCalleeTab.IsSelected = true;
}
private void DoViewInCallers(object sender, ExecutedRoutedEventArgs e)
{
- SetFocus(GetSelectedNodes().Single());
+ SetFocus(GetSelectedNodes().Single().Name);
CallersTab.IsSelected = true;
}
private void DoViewInCallees(object sender, ExecutedRoutedEventArgs e)
{
- SetFocus(GetSelectedNodes().Single());
+ SetFocus(GetSelectedNodes().Single().Name);
CalleesTab.IsSelected = true;
}
@@ -2592,8 +2592,15 @@ private void DoHyperlinkHelp(object sender, ExecutedRoutedEventArgs e)
private void ByName_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
- DoViewInCallers(sender, null);
+
+ // Check if a single node is selected before proceeding
+ // Exactly one node must be selected in order to view callers.
+ if (GetSelectedNodes().Count == 1)
+ {
+ DoViewInCallers(sender, null);
+ }
}
+
internal void DataGrid_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
var uiElement = sender as UIElement;
@@ -2757,10 +2764,18 @@ private void DoSaveFlameGraph(object sender, RoutedEventArgs e)
var result = saveDialog.ShowDialog();
if (result == true)
{
- if (FlameGraphCanvas.IsEmpty || m_RedrawFlameGraphWhenItBecomesVisible)
- RedrawFlameGraph();
+ try
+ {
+ if (FlameGraphCanvas.IsEmpty || m_RedrawFlameGraphWhenItBecomesVisible)
+ RedrawFlameGraph();
- FlameGraph.Export(FlameGraphCanvas, saveDialog.FileName);
+ FlameGraph.Export(FlameGraphCanvas, saveDialog.FileName);
+ StatusBar.Log($"Saved flame graph to {saveDialog.FileName}");
+ }
+ catch (ArgumentOutOfRangeException ex)
+ {
+ StatusBar.LogError($"Failed to save flame graph: {ex.Message}");
+ }
}
}
@@ -3108,7 +3123,14 @@ private void FinishInit()
if (m_ViewsShouldBeSaved)
{
- var result = MessageBox.Show("You have created Notes that have not been saved\r\nDo you wish to save?", "Unsaved Notes", MessageBoxButton.YesNoCancel);
+ var result = XamlMessageBox.Show(
+ """
+ You have created Notes that have not been saved.
+ Do you wish to save?
+ """,
+ "Unsaved Notes",
+ MessageBoxButton.YesNoCancel);
+
if (result == MessageBoxResult.Cancel)
{
e.Cancel = true;
diff --git a/src/PerfView/SupportFiles/EventCounterVisualization.html b/src/PerfView/SupportFiles/EventCounterVisualization.html
index e04a39a3f..101ed7183 100644
--- a/src/PerfView/SupportFiles/EventCounterVisualization.html
+++ b/src/PerfView/SupportFiles/EventCounterVisualization.html
@@ -4,9 +4,13 @@
-
+
Event Counter Visualization
-
-
+
+
The bottom up view did an excellent job of determining that the get_Now() method
as well as the 'SpinForASecond' consume the largest amount of time and thus
- are worth looking at closely. This corresponds beautify
+ are worth looking at closely. This corresponds
to our expectations given the source code in Tutorial.cs .
However it can also be useful to understand where CPU time was consumed from the
top down. This is what the CallTree view is for.
@@ -522,7 +529,7 @@
stacks), which typically run in the 5-10% range. In this case it seems
to be about 6%). The 'When' column also clearly shows how one
instance of RecSpin runs SpinForASecond (for exactly a second) and then calls a
- RecSpinHelper which does consumes close to 100% of the CPU for the rest of the time.
+ RecSpinHelper which consumes close to 100% of the CPU for the rest of the time.
. The call Tree is a wonderful top-down synopsis.
Getting a 'coarser' view
@@ -532,9 +539,9 @@ Getting a 'coarser' view
use this fact and the 'Fold %' functionality to get an even coarser view
of the 'top' of the call tree. With all nodes expanded, simply
right click on the window and select 'Increase Fold %' (or easier hit the
- F7 key). This increases the number it the Fold % textbox by 1.6X.
+ F7 key). This increases the number in the Fold % textbox by 1.6X.
By hitting the F7 key repeatedly you keep trimming down the 'bottoms' of
- the stacks until you only see only the methods that use a large amount of CPU time.
+ the stacks until you only see methods that use a large amount of CPU time.
The following image shows the CallTreeView after hitting F7 seven times.
@@ -1797,7 +1804,7 @@
A typical GC Memory investigation includes dump of the GC heap. While this gives
very detailed information about the heap at the time the snapshot was taken, it
- give no information about the GC behavior over time. This is what the GCStats report
+ gives no information about the GC behavior over time. This is what the GCStats report
does. To get a GCStats reports you must Collect Event Data
as you would for a CPU investigation (the GC events are on by default). When you
open the resulting ETL file one of the children will be a 'GCStats' view. Opening
@@ -2441,7 +2448,7 @@
Contention Stacks - This view aggregates Contention events.
Contention event is fired when a thread tries to acquire a managed lock that is currently owned
by another thread. Note that each event has useful event data that is folded by default.
- For example, unfolding EventData DurationNs reveals individual pauses of each thread: this can be useful
+ For example, unfolding EventData DurationNs reveals individual pauses of each thread: this can be useful
to correlate a particular long wait to another events in the trace. Note that not all contention events
are real OS-level waits: the runtime may first spin wait to try acquire the lock fast. The metric
represents the amount of time spent to acquire the lock in milliseconds.
@@ -4121,7 +4128,7 @@ Advanced Options
if the application allocates aggressively, so many events will be fired so quickly that
events will be lost even when the
/BufferSizeMB qualifier is used to set the size very large (e.g. 500Meg). For these reasons it
- is usually a better idea to use the .NET SampAlloc
+ is usually a better idea to use the .NET SampAlloc
option instead if at all possible.
@@ -4143,7 +4150,7 @@ Advanced Options
reported is likely to be close to the true statistics.
The overhead of turning on .NET SampAlloc CheckBox is much less than the
- .NET Alloc CheckBox . Typically the overhead is
+ .NET Alloc CheckBox . Typically the overhead is
10-20% (unlike 2X or more), and produces 200 Meg per minute of trace. This is
a bit more expensive than turning on /threadTime however low enough that you can
leave it on in production (especially if the application does not allocate heavily).
@@ -5174,7 +5181,7 @@
A Wall Clock Time Investigation
request (or groups of request), you can see only 'interesting' time.
- If the application uses System.Threading.Threads.Tasks, you can use the 'Thread Time (with
+ If the application uses System.Threading.Tasks, you can use the 'Thread Time (with
Tasks) view. This marks the segment of a task that is executing a single task with the
ID of that task. I also attributes a Task's time to the call stack of the task that
activated it. In this way concurrent programs can be analyzed as if they were singly
@@ -5413,7 +5420,7 @@ Thread Time is not Elapsed Wall Clock Time
add up to more than elapsed wall clock time. This is easy to determine this is the case (because you will
see more than one thread as children of the activity), and you can even see the overlap
(by looking at the 'when' column of each of the children). Still it is something to
- be aware of. See Understanding Thread Time and for more.
+ be aware of. See Understanding Thread Time and for more.
It is also possible that the thread time will be LESS than elapsed wall clock time.
@@ -6328,6 +6335,131 @@
Known issues (in Windows Version 1803 or earlier)
put them.
+
+ Capturing ETW Traces with Process-Isolation Windows Containers (Kubernetes)
+
+ When running Windows containers in Kubernetes using process-isolation mode (the default mode, as opposed to Hyper-V isolation),
+ the containers share the host's kernel. While this enables ETW tracing from the host, it requires a specific
+ workflow to capture and analyze traces for processes running inside these containers.
+
+
+ Note: If you are running containers in Hyper-V isolation mode, these instructions are not required.
+ In Hyper-V mode, each container has its own kernel, so you can capture traces directly inside the container
+ using the normal PerfView workflow.
+
+
+ Important Limitation: In process-isolation mode, kernel ETW sessions cannot be started from
+ inside the container. Since PerfView almost always captures a kernel session, all trace collection
+ must be initiated from the host node.
+
+
+ Step 1: Capture a Trace on the Host Node
+
+ Start the trace collection on the Kubernetes host node (not inside the pod). Use the /EnableEventsInContainers
+ option to ensure that user-mode events from processes inside containers flow to the ETW session on the host. Example capture command:
+
+
+ PerfView collect /EnableEventsInContainers MyContainerTrace.etl
+
+
+ What /EnableEventsInContainers does: By default, an ETW session on the host only receives
+ user-mode events from processes running directly on the host. The /EnableEventsInContainers option enables
+ the ETW session to also receive user-mode events (such as .NET CLR events, custom EventSource events, etc.)
+ from processes running inside process-isolation containers.
+
+
+ What happens if you don't use /EnableEventsInContainers: You will still capture all kernel
+ events (CPU sampling, context switches, etc.) for container processes, and you will still receive user-mode
+ events from processes running directly on the host node (outside of containers). However, you will miss
+ user-mode events like .NET garbage collection events, JIT events, exception events, and any custom
+ EventSource events from processes inside containers.
+
+
+ Step 2a: Analyze While Container is Running (Optional)
+
+ If the container(s) containing the process(es) of interest are still running when you stop the trace, you
+ can open and analyze the trace directly on the host node. PerfView will be able to find binaries that it
+ needs both on the host and inside the running containers through the container's file system view. NOTE: This only works for as long
+ as the container is running.
+
+
+ This is the simplest analysis path since no additional steps are requiredβjust open the trace in PerfView
+ on the host node.
+
+
+ Step 2b: Prepare Trace for Offline Analysis (Optional)
+
+ If you need to analyze the trace after the container has been shut down, or if you want to copy the trace
+ to another machine for analysis, you need to prepare the trace while the container is still accessible.
+ This is done using the merge command with the /ImageIDsOnly option.
+
+
+ First, copy the trace file into the container:
+
+
+ kubectl cp MyContainerTrace.etl.zip my-namespace/my-pod:/app/MyContainerTrace.etl.zip
+
+
+ Then, inside the container, run the merge command to inject the necessary image identification data:
+
+
+ PerfViewCollect merge MyContainerTrace.etl.zip /ImageIDsOnly
+
+
+ Note: PerfViewCollect needs to be built from source at
+ https://github.com/microsoft/perfview .
+ It is not currently shipped as a binary. See the "Windows Nanoserver and PerfViewCollect"
+ section above for build instructions.
+
+
+ What /ImageIDsOnly does: When you run merge with /ImageIDsOnly, PerfView reads through
+ the trace and for each DLL that was loaded by processes in the trace, it looks up the DLL's PDB signature
+ and injects that information into the trace. This unique identifier is what allows PerfView to later
+ download the correct PDB symbols from a symbol server. Without this information, PerfView cannot resolve
+ method names for code in those DLLs.
+
+
+ What happens if you don't run merge with /ImageIDsOnly: If you skip this step and later
+ try to analyze the trace on another machine after the container is gone, PerfView will be unable to find
+ the symbol files for DLLs that were loaded inside the container. Your stack traces will show the module
+ name with a question mark (for example: MyAssembly!? instead of MyAssembly!MyClass.MyMethod).
+ Jitted .NET code will still resolve correctly, but nothing else from binaries inside the container will have symbols.
+
+
+ Why run merge inside the container: The merge component does not have access to look inside
+ of containers when run from the host. Running merge inside the container ensures it can access the DLLs that
+ were loaded by the container's processes. If you run merge on the host or on a different machine, those
+ container-specific DLLs will not be accessible.
+
+
+ Step 3: Copy and Analyze (After Using /ImageIDsOnly)
+
+ After running merge with /ImageIDsOnly, copy the trace out of the container:
+
+
+ kubectl cp my-namespace/my-pod:/app/MyContainerTrace.etl.zip ./MyContainerTrace.etl.zip
+
+
+ You can now open this trace on any machine with PerfView installed. With the image identification
+ information embedded in the trace, PerfView can download symbols from symbol servers as needed.
+
+
+ Summary of Commands
+
+ Here is the complete workflow:
+
+
+ On the host: PerfView collect /EnableEventsInContainers /MaxCollectSec:30 MyContainerTrace.etl
+ Copy to container: kubectl cp MyContainerTrace.etl.zip my-namespace/my-pod:/app/
+ In the container: PerfViewCollect merge MyContainerTrace.etl.zip /ImageIDsOnly
+ Copy from container: kubectl cp my-namespace/my-pod:/app/MyContainerTrace.etl.zip ./
+ Analyze anywhere: PerfView MyContainerTrace.etl.zip
+
+
+ Note: If you analyze the trace on the host while the container is still running, you
+ can skip the copy and merge steps entirely.
+
+
@@ -8031,7 +8163,7 @@
If your symbols are on an Azure DevOps artifacts store, or your source code is not public,
- then PerfView may prompt you to sign in. Support currently exists for Azure DevOps and private
+ then PerfView may prompt you to sign in. Support currently exists for Azure DevOps and private
GitHub repositories. If installed, PerfView will try to use the Git Credential Manager
which is typically installed with Git For Windows. If Git Credential Manager is not installed,
PerfView will fall back to alternate authentication mechanisms. The authentication mechanisms
@@ -8042,7 +8174,7 @@
Git Credential Manager . This is the most flexible option for developers
using Git. It works alongside your Git installation to sign into private repositories.
Support is currently enabled for Azure DevOps and GitHub. We hope to add GitLab and BitBucket
- support in the future.
+ support in the future.
PerfView will search for the Git Credential Manager executable (git-credential-manager-core.exe)
in a number of well-known locations, but if it can't be found, then the option will be
unavailable. If you have installed Git Credential Manager in a non-standard location, you can
@@ -8058,12 +8190,12 @@
into Visual Studio or VS Code using credentials that can access your Azure DevOps repo.
If you sign into Visual Studio with several different accounts, you may need to select the
right one in Tools/Options/Azure Service Authentication.
- See the Authentication and the Azure SDK
+ See the Authentication and the Azure SDK
blog posting for more information.
Device Code Flow for GitHub . This option, for GitHub only, uses a Device Code
- to grant PerfView access to GitHub private repositories.
+ to grant PerfView access to GitHub private repositories.
PerfView will prompt you with an 8 digit device code which you use to log into GitHub.com using
any web browser. The browser could be running on a different device, if necessary.
When you enter the code into the browser and approve the app, the dialog will automatically close
@@ -8071,10 +8203,10 @@
to access.
- Basic HTTP Authentication . This option allows you to use Basic HTTP authentication
- when connecting to a symbol server. To use it, you should specify the username and password in the URL for your symbol server. For example:
- SRV*SymbolCachePath*https://username:password@symbolstore.url; . This scheme is active by default but
- used only if the URL contains username and password information.
+ Basic HTTP Authentication . This option allows you to use Basic HTTP authentication
+ when connecting to a symbol server. To use it, you should specify the username and password in the URL for your symbol server. For example:
+ SRV*SymbolCachePath*https://username:password@symbolstore.url; . This scheme is active by default but
+ used only if the URL contains username and password information.
@@ -8204,7 +8336,19 @@
Inlining. If A calls B calls C, if B is very small it is not unusual
for the compiler to have simply 'inlined' the body of B into the body of
A. In this case obviously B does not appear because in a very real sense
- B does not exist at the native code level.
+ B does not exist at the native code level. To verify whether a specific
+ missing method was inlined, you can use the
+ JIT Inlining feature (the JIT Inlining
+ checkbox in the collection dialog, or the /JITInlining command-line
+ option). This causes the JIT to emit an event for every inlining decision,
+ and the results are shown in the JIT Stats report as two tables: one
+ for successful inlinings and one for failed inlinings. Each row shows the
+ method being compiled, the inliner (caller), and the inlinee (callee). To
+ capture inlining events for a given method, the trace must start before the method is
+ JIT-compiled (i.e., collect from process start). In the successful inlinings
+ table, you can search for the missing method name in the Inlinee column to
+ confirm it was inlined, and check the Inliner column to see which method it
+ was inlined into.
Tail-calling. If the last thing method B does before returning is to
@@ -9164,63 +9308,64 @@
}
+
+
+ Version 1.8.28 2/4/16
+
- Version 1.8.28 2/4/16
-
-
- Added support doing performance investigations with Linux Perf Events data. Basically if
- collect data with the bash script https://raw.githubusercontent.com/dotnet/corefx-tools/master/src/performance/perfcollect/perfcollect
- it will runt the Linux 'perf' tool that will collect CPU samples, convert them to a .data.txt file
- (which is a textual representation of the data) and then ZIP it into a .trace.zip file PerfView
- knows how to decode either the uncompressed .data.txt file or the zipped .trace.zip file and
- display it as a stack view. Thus you can now do linux performance investigations with PerfView.
-
-
+ Added support doing performance investigations with Linux Perf Events data. Basically if
+ collect data with the bash script https://raw.githubusercontent.com/dotnet/corefx-tools/master/src/performance/perfcollect/perfcollect
+ it will runt the Linux 'perf' tool that will collect CPU samples, convert them to a .data.txt file
+ (which is a textual representation of the data) and then ZIP it into a .trace.zip file PerfView
+ knows how to decode either the uncompressed .data.txt file or the zipped .trace.zip file and
+ display it as a stack view. Thus you can now do linux performance investigations with PerfView.
+
+
+
+ Version 1.8.25 2/2/16
+
- Version 1.8.25 2/2/16
-
-
- Improvements in Start-Stop time. UNKNOWN_ASYNC displayed more often, some AWAIT time shown more often.
-
-
+ Improvements in Start-Stop time. UNKNOWN_ASYNC displayed more often, some AWAIT time shown more often.
+
+
+
+ Version 1.8.24 1/27/16
+
- Version 1.8.24 1/27/16
-
-
- When opening 'Drill Into' windows, the columns are not in the order of the parent window in the ByName view.
- Fixed this.
-
-
+ When opening 'Drill Into' windows, the columns are not in the order of the parent window in the ByName view.
+ Fixed this.
+
+
+
+ Version 1.8.23 1/26/16
+
- Version 1.8.23 1/26/16
-
-
- Merging failed on Win7 and Win2k8 systems in PerfView Version 1.8. This means you could still analyze on
- the machine where you collected, but symbols would fail to look up if you took the trace off the system.
- Fixed by including an old version of KernelTraceControl.dll an used it on Win7 systems.
-
-
+ Merging failed on Win7 and Win2k8 systems in PerfView Version 1.8. This means you could still analyze on
+ the machine where you collected, but symbols would fail to look up if you took the trace off the system.
+ Fixed by including an old version of KernelTraceControl.dll an used it on Win7 systems.
+
+
+
+ Version 1.8.22 1/23/16
+
- Version 1.8.22 1/23/16
-
-
- Fixed ArgumentOutOfRange exceptions thrown in EventView for some events (strings with length prefixes)
-
- Don't crash if regular expressions are incorrect in Events view.
-
+ Fixed ArgumentOutOfRange exceptions thrown in EventView for some events (strings with length prefixes)
+ Don't crash if regular expressions are incorrect in Events view.
+
+
+
+ Version 1.8.21 1/18/16
+
- Version 1.8.21 1/18/16
-
-
- Extended perfView.xml file format so that it can more easily consume 'ad hoc' creation of stacks.
- It still accepts the 'interned' scheme where you give IDs to each frame and stack and use those
- to create samples, but now you can specify the samples inline with the sample like this
-
+ Extended perfView.xml file format so that it can more easily consume 'ad hoc' creation of stacks.
+ It still accepts the 'interned' scheme where you give IDs to each frame and stack and use those
+ to create samples, but now you can specify the samples inline with the sample like this
+
<StackWindow>
<StackSource>
<Samples>
@@ -9238,230 +9383,234 @@
</StackSource>
</StackWindow>
- While this format is inefficient (you repeat many strings in many stacks), it is sometimes
- convenient, and it is easy enough to support. There are more details which I will blog about in
- the near future.
-
-
+ While this format is inefficient (you repeat many strings in many stacks), it is sometimes
+ convenient, and it is easy enough to support. There are more details which I will blog about in
+ the near future.
+
+
+
+ Version 1.8.20 1/13/16
+
- Version 1.8.20 1/13/16
-
-
- Improved the robustness of the UserCommand 'Listen' command in the face of bad events.
-
-
+ Improved the robustness of the UserCommand 'Listen' command in the face of bad events.
+
+
+
+ Version 1.8.19 1/7/16
+
- Version 1.8.19 1/7/16
-
-
- Significantly improved the Thread Time with Start-Stop Activities. The goal here is
- that this view replaces the ASP.NET and Service Request view, and we are probably most of
- the way there now. I need to validate this more and then probably obsolete the other views.
-
-
+ Significantly improved the Thread Time with Start-Stop Activities. The goal here is
+ that this view replaces the ASP.NET and Service Request view, and we are probably most of
+ the way there now. I need to validate this more and then probably obsolete the other views.
+
+
+
+ Version 1.8.15 12/4/15
+
- Version 1.8.15 12/4/15
-
-
- Fixed a fairly serious bug associated with the Events Viewer where you don't see some CLR events
- (They appear in the left pane, but you never see them in the right pane even though there are
- instances of them in the file). Note that version 1.8.0 does not have this bug, it was introduced
- relatively recently.
-
-
- Added ActivityInfo and StartStopActivity fields to Events View. ActivityInfo will show you the
- creation and start time (and the raw ID) of the System.Threading.Tasks.Task that logged the event.
- StartStopActivity shows you the name of the start-stop activity that
- is logged the event.
-
-
+ Fixed a fairly serious bug associated with the Events Viewer where you don't see some CLR events
+ (They appear in the left pane, but you never see them in the right pane even though there are
+ instances of them in the file). Note that version 1.8.0 does not have this bug, it was introduced
+ relatively recently.
- Version 1.8.15 12/4/15
-
-
- Fixed a fairly serious bug associated with the Events Viewer where you don't see some CLR events
- (They appear in the left pane, but you never see them in the right pane even though there are
- instances of them in the file). Note that version 1.8.0 does not have this bug, it was introduced
- relatively recently.
-
-
- Added ActivityInfo and StartStopActivity fields to Events View. ActivityInfo will show you the
- creation and start time (and the raw ID) of the System.Threading.Tasks.Task that logged the event.
- StartStopActivity shows you the name of the start-stop activity that
- is logged the event.
-
-
+ Added ActivityInfo and StartStopActivity fields to Events View. ActivityInfo will show you the
+ creation and start time (and the raw ID) of the System.Threading.Tasks.Task that logged the event.
+ StartStopActivity shows you the name of the start-stop activity that
+ is logged the event.
+
+
+
+ Version 1.8.15 12/4/15
+
- Version 1.8.11 11/16/15
-
-
- Fix excessive warnings when converting ETL files. Might also fix some StartStop Activity issues.
-
-
+ Fixed a fairly serious bug associated with the Events Viewer where you don't see some CLR events
+ (They appear in the left pane, but you never see them in the right pane even though there are
+ instances of them in the file). Note that version 1.8.0 does not have this bug, it was introduced
+ relatively recently.
- Version 1.8.10 11/12/15
-
-
- Significant improvement in how activity tracking works. Hopefully the stacks associated with 'with Tasks' views
- will be better.
-
-
- Added JIT Inlining feature that enables viewing all successful and failed inlining attempts, including the
- JIT-supplied reason for why inlining wasn't performed in the failure cases.
-
-
- Added finalization feature that tracks finalized objects and provides a table of each type with a finalized object
- and the associated number of times an object of that type was finalized.
-
-
+ Added ActivityInfo and StartStopActivity fields to Events View. ActivityInfo will show you the
+ creation and start time (and the raw ID) of the System.Threading.Tasks.Task that logged the event.
+ StartStopActivity shows you the name of the start-stop activity that
+ is logged the event.
+
+
+
+ Version 1.8.11 11/16/15
+
- Version 1.8.9 11/1/15
-
-
- There is a bug in RC candidates of V4.6.1 where NGEN createPdb only works if the path of the NGEN image
- is in the Native Image Cache (NIC), but V4.6.1 uses hard links for NGEN images that come from the install itself.
- The result is that you don't get symbols for mscorlib, system, and system.core. This adds a work-around
- for this (normally all paths to the NIC path before calling NGEN CreatePdb), until the runtime is fixed.
-
-
+ Fix excessive warnings when converting ETL files. Might also fix some StartStop Activity issues.
+
+
+
+ Version 1.8.10 11/12/15
+
- Version 1.8.8 10/31/15
-
-
- Added support for .NET V4.6.2 convention for NGEN PDB line numbers. This means that if data is collected on
- a V4.6.2 then the lack of access IL PDBS are not available at data collection time is not longer an
- impediment to getting line number information (that is access to the corresponding IL pdb with line number
- information is no longer needed to create an NGEN pdb that has line number information).
-
-
+ Significant improvement in how activity tracking works. Hopefully the stacks associated with 'with Tasks' views
+ will be better.
- Version 1.8.7 10/22/15
-
-
- Integrated changes that allow DyanamicTraceEventParser to do everything that RegisteredTraceEventParser can do.
- Removed the calls to RegisteredTraceEventParser. This could break things but should not. So far things look
- OK.
-
-
+ Added JIT Inlining feature that enables viewing all successful and failed inlining attempts, including the
+ JIT-supplied reason for why inlining wasn't performed in the failure cases.
- Version 1.8.6 10/12/15
-
-
- Integrated Lee's update of CLRMD that should make PerfView able to extract heap dumps from debugger dumps of
- .NET Native processes.
-
- Added the DotNet (Telemetry) event ETW provider by default.
-
+ Added finalization feature that tracks finalized objects and provides a table of each type with a finalized object
+ and the associated number of times an object of that type was finalized.
+
+
+
+ Version 1.8.9 11/1/15
+
- Version 1.8.5 10/6/15
-
-
- Made 'Any Stacks (with StartStop Activities)' and 'Any StartStopTree' public.
-
-
+ There is a bug in RC candidates of V4.6.1 where NGEN createPdb only works if the path of the NGEN image
+ is in the Native Image Cache (NIC), but V4.6.1 uses hard links for NGEN images that come from the install itself.
+ The result is that you don't get symbols for mscorlib, system, and system.core. This adds a work-around
+ for this (normally all paths to the NIC path before calling NGEN CreatePdb), until the runtime is fixed.
+
+
+
+ Version 1.8.8 10/31/15
+
- Version 1.8.3 9/23/15
-
-
- Turned off System.Threading.Tasks.Task events that are verbose and only needed for debugging. This was
- useful before so that any traces I get have detailed information for debugging, but are now impacting
- the cost of using PerfView in production when Tasks are used heavily.
-
-
+ Added support for .NET V4.6.2 convention for NGEN PDB line numbers. This means that if data is collected on
+ a V4.6.2 then the lack of access IL PDBS are not available at data collection time is not longer an
+ impediment to getting line number information (that is access to the corresponding IL pdb with line number
+ information is no longer needed to create an NGEN pdb that has line number information).
+
+
+
+ Version 1.8.7 10/22/15
+
- Version 1.8.2 9/13/15
-
- /InMemoryCircularBuffer option was broken (Would throw a file not found exception in SetFileName). Fixed this.
-
+ Integrated changes that allow DyanamicTraceEventParser to do everything that RegisteredTraceEventParser can do.
+ Removed the calls to RegisteredTraceEventParser. This could break things but should not. So far things look
+ OK.
+
+
+
+ Version 1.8.6 10/12/15
+
- Version 1.8.1 9/3/15
-
-
- Fixed issue where Debug versions were asserting that two stacks were attached to the same event
- because kernel and user mode stacks were not being stitched together properly (mostly in rare cases
- where thread-starts were happening)
-
-
+ Integrated Lee's update of CLRMD that should make PerfView able to extract heap dumps from debugger dumps of
+ .NET Native processes.
-
+ Added the DotNet (Telemetry) event ETW provider by default.
+
+
+
+ Version 1.8.5 10/6/15
+
- Version 1.8.0 8/30/15
-
+ Made 'Any Stacks (with StartStop Activities)' and 'Any StartStopTree' public.
+
+
+
+ Version 1.8.3 9/23/15
+
- Version 1.7.31 8/19/15
-
-
- Update code that does merging so it works properly on Win10. It does not have an effect if you look
- at the events with PerfView, but on Win10 until this change, data collected with PerfView would not
- parse EventSource events properly in WPA.
-
-
+ Turned off System.Threading.Tasks.Task events that are verbose and only needed for debugging. This was
+ useful before so that any traces I get have detailed information for debugging, but are now impacting
+ the cost of using PerfView in production when Tasks are used heavily.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ Version 1.8.2 9/13/15
+
+ /InMemoryCircularBuffer option was broken (Would throw a file not found exception in SetFileName). Fixed this.
+
+
+
+ Version 1.8.1 9/3/15
+
+
+ Fixed issue where Debug versions were asserting that two stacks were attached to the same event
+ because kernel and user mode stacks were not being stitched together properly (mostly in rare cases
+ where thread-starts were happening)
+
+
+
+
+ Version 1.8.0 8/30/15
+
+
+
+ Version 1.7.31 8/19/15
+
+
+ Update code that does merging so it works properly on Win10. It does not have an effect if you look
+ at the events with PerfView, but on Win10 until this change, data collected with PerfView would not
+ parse EventSource events properly in WPA.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+