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("

GC Stats for Process {1,5}: {2}

", stats.ProcessID, stats.ProcessID, stats.Name); - writer.WriteLine("
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 @@