├── .gitattributes ├── .gitignore ├── .npmignore ├── .yamato └── upm-ci.yml ├── CHANGELOG.md ├── CHANGELOG.md.meta ├── Documentation~ ├── TableOfContents.md ├── analytics.md ├── api.md ├── images │ ├── action-bug-report.gif │ ├── action-open-tracker-view.gif │ ├── action-start-profiler.gif │ ├── add-package.png │ ├── all-warnings.png │ ├── benchmark-log.png │ ├── download.png │ ├── emded.png │ ├── git-url.png │ ├── notification-partial-tracking-tick.png │ ├── performance-notification-in-console.png │ ├── performance-tracker-window.png │ ├── performance-tracking-monitoring.png │ ├── pin-workflow.png │ ├── preference-notification.png │ ├── preference-spike-highlight.png │ ├── profiler-console-trace.png │ ├── profiler-marker.png │ ├── profiler-workflow.gif │ ├── profiler_trace_sample.png │ ├── profiling-options.png │ ├── profiling_actions.png │ ├── profiling_snippet_window.gif │ ├── repaint_snippet.png │ ├── select-package.png │ ├── static_snippet.png │ ├── task-manager.png │ ├── tracker-window-actions.png │ ├── tracker-window-add-notification-action.gif │ ├── tracker-window-profiler.gif │ ├── window-actions.png │ └── window-configuration-toolbar.png ├── index.md ├── monitoring.md ├── performance-tracker-window.md ├── performance-window-actions.md └── profiling-snippet-window.md ├── Editor.meta ├── Editor ├── BugReporterUtils.cs ├── BugReporterUtils.cs.meta ├── Icons.cs ├── Icons.cs.meta ├── Icons.meta ├── Icons │ ├── settings.png │ └── settings.png.meta ├── PerformanceTracker.cs ├── PerformanceTracker.cs.meta ├── PerformanceTrackerActions.cs ├── PerformanceTrackerActions.cs.meta ├── PerformanceTrackerModel.cs ├── PerformanceTrackerModel.cs.meta ├── PerformanceTrackerMonitoringService.cs ├── PerformanceTrackerMonitoringService.cs.meta ├── PerformanceTrackerReportUtils.cs ├── PerformanceTrackerReportUtils.cs.meta ├── PerformanceTrackerSettings.cs ├── PerformanceTrackerSettings.cs.meta ├── PerformanceTrackerWindow.cs ├── PerformanceTrackerWindow.cs.meta ├── PerformanceTrackingAnalytics.cs ├── PerformanceTrackingAnalytics.cs.meta ├── ProfilerHelpers.cs ├── ProfilerHelpers.cs.meta ├── ProfilingActions.cs ├── ProfilingActions.cs.meta ├── ProfilingSnippetListView.cs ├── ProfilingSnippetListView.cs.meta ├── ProfilingSnippetUtils.cs ├── ProfilingSnippetUtils.cs.meta ├── ProfilingSnippetWindow.cs ├── ProfilingSnippetWindow.cs.meta ├── StyleSheets.meta ├── StyleSheets │ ├── PerformanceTrackerWindow.uss │ └── PerformanceTrackerWindow.uss.meta ├── Unity.PerformanceTracking.Editor.asmdef ├── Unity.PerformanceTracking.Editor.asmdef.meta ├── Utils.cs └── Utils.cs.meta ├── LICENSE.md ├── LICENSE.md.meta ├── README.md ├── README.md.meta ├── Tests.meta ├── Tests ├── .tests.json ├── Editor.meta └── Editor │ ├── PerformanceTrackerActionsTests.cs │ ├── PerformanceTrackerActionsTests.cs.meta │ ├── ProfilerHelpersTests.cs │ ├── ProfilerHelpersTests.cs.meta │ ├── TestUtils.cs │ ├── TestUtils.cs.meta │ ├── Unity.PerformanceTracking.Editor.Tests.asmdef │ └── Unity.PerformanceTracking.Editor.Tests.asmdef.meta ├── package.json └── package.json.meta /.gitattributes: -------------------------------------------------------------------------------- 1 | ## Unity ## 2 | 3 | *.cs diff=csharp text 4 | *.cginc text 5 | *.shader text 6 | 7 | *.mat merge=unityyamlmerge eol=lf 8 | *.anim merge=unityyamlmerge eol=lf 9 | *.unity merge=unityyamlmerge eol=lf 10 | *.prefab merge=unityyamlmerge eol=lf 11 | *.physicsMaterial2D merge=unityyamlmerge eol=lf 12 | *.physicsMaterial merge=unityyamlmerge eol=lf 13 | *.asset merge=unityyamlmerge eol=lf 14 | *.meta merge=unityyamlmerge eol=lf 15 | *.controller merge=unityyamlmerge eol=lf 16 | 17 | 18 | ## git-lfs ## 19 | 20 | #Image 21 | *.jpg filter=lfs diff=lfs merge=lfs -text 22 | *.jpeg filter=lfs diff=lfs merge=lfs -text 23 | *.png filter=lfs diff=lfs merge=lfs -text 24 | *.gif filter=lfs diff=lfs merge=lfs -text 25 | *.psd filter=lfs diff=lfs merge=lfs -text 26 | *.ai filter=lfs diff=lfs merge=lfs -text 27 | 28 | #Audio 29 | *.mp3 filter=lfs diff=lfs merge=lfs -text 30 | *.wav filter=lfs diff=lfs merge=lfs -text 31 | *.ogg filter=lfs diff=lfs merge=lfs -text 32 | 33 | #Video 34 | *.mp4 filter=lfs diff=lfs merge=lfs -text 35 | *.mov filter=lfs diff=lfs merge=lfs -text 36 | 37 | #3D Object 38 | *.FBX filter=lfs diff=lfs merge=lfs -text 39 | *.fbx filter=lfs diff=lfs merge=lfs -text 40 | *.blend filter=lfs diff=lfs merge=lfs -text 41 | *.obj filter=lfs diff=lfs merge=lfs -text 42 | 43 | #ETC 44 | *.a filter=lfs diff=lfs merge=lfs -text 45 | *.exr filter=lfs diff=lfs merge=lfs -text 46 | *.tga filter=lfs diff=lfs merge=lfs -text 47 | *.pdf filter=lfs diff=lfs merge=lfs -text 48 | *.zip filter=lfs diff=lfs merge=lfs -text 49 | *.dll filter=lfs diff=lfs merge=lfs -text 50 | *.unitypackage filter=lfs diff=lfs merge=lfs -text 51 | *.aif filter=lfs diff=lfs merge=lfs -text 52 | *.ttf filter=lfs diff=lfs merge=lfs -text 53 | *.rns filter=lfs diff=lfs merge=lfs -text 54 | *.reason filter=lfs diff=lfs merge=lfs -text 55 | *.lxo filter=lfs diff=lfs merge=lfs -text 56 | 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /[Ll]ibrary/ 2 | /[Tt]emp/ 3 | /[Oo]bj/ 4 | /[Bb]uild/ 5 | /[Bb]uilds/ 6 | /[Ll]ogs/ 7 | /_ReSharper.Caches/ 8 | 9 | # Private assets 10 | Assets/Temp.meta 11 | Assets/Temp/ 12 | 13 | # Visual Studio 2015 cache directory 14 | /.vs/ 15 | /.vscode/ 16 | 17 | # Autogenerated VS/MD/Consulo solution and project files 18 | ExportedObj/ 19 | .consulo/ 20 | *.csproj 21 | *.unityproj 22 | *.sln 23 | *.suo 24 | *.tmp 25 | *.user 26 | *.userprefs 27 | *.pidb 28 | *.booproj 29 | *.svd 30 | *.pdb 31 | 32 | # Unity3D generated meta files 33 | *.pidb.meta 34 | 35 | # Unity3D Generated File On Crash Reports 36 | sysinfo.txt 37 | 38 | # Builds 39 | *.apk 40 | *.unitypackage 41 | .npmrc 42 | packages.json 43 | packages.json.meta -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .build_script/** 2 | .DS_Store 3 | .Editor/** 4 | .gitattributes 5 | .gitignore 6 | .gitlab-ci.yml 7 | .npmignore 8 | .npmrc 9 | .yamato/** 10 | 11 | *.zip* 12 | 13 | artifacts/** 14 | automation/** 15 | build.bat 16 | build.bat.meta 17 | build.sh 18 | build.sh.meta 19 | build/** 20 | CONTRIBUTING.md 21 | CONTRIBUTING.md.meta 22 | Documentation/ApiDocs/** 23 | Documentation~/ApiDocs/** 24 | node_modules/** 25 | packages.json 26 | packages.json.meta 27 | QAReport.md 28 | QAReport.md.meta 29 | utr_output/** 30 | -------------------------------------------------------------------------------- /.yamato/upm-ci.yml: -------------------------------------------------------------------------------- 1 | test_editors: 2 | - version: 2019.4 3 | - version: 2020.2 4 | - version: 2021.1 5 | test_platforms: 6 | - name: win 7 | type: Unity::VM 8 | image: package-ci/win10:stable 9 | flavor: b1.large 10 | runtime: editmode 11 | # - name: mac 12 | # type: Unity::VM::osx 13 | # image: package-ci/mac:stable 14 | # flavor: m1.mac 15 | # runtime: editmode 16 | 17 | --- 18 | pack: 19 | name: Pack 20 | agent: 21 | type: Unity::VM 22 | image: package-ci/ubuntu:stable 23 | flavor: b1.large 24 | commands: 25 | - npm install upm-ci-utils@stable -g --registry https://artifactory-upload.prd.it.unity3d.com/artifactory/api/npm/upm-npm 26 | - upm-ci package pack 27 | artifacts: 28 | packages: 29 | paths: 30 | - "upm-ci~/**/*" 31 | 32 | # Run tests per platforms 33 | {% for editor in test_editors %} 34 | {% for platform in test_platforms %} 35 | test_{{ platform.name }}_{{ editor.version }}: 36 | name : {{ editor.version }} Tests ({{ platform.name }}) 37 | agent: 38 | type: {{ platform.type }} 39 | image: {{ platform.image }} 40 | flavor: {{ platform.flavor}} 41 | variables: 42 | DISPLAY: ":0" 43 | commands: 44 | {% if platform.name == 'linux' %} 45 | - sudo apt install ocl-icd-opencl-dev -y 46 | {% endif %} 47 | - npm install upm-ci-utils@stable -g --registry https://artifactory-upload.prd.it.unity3d.com/artifactory/api/npm/upm-npm 48 | - {% if platform.name == "centos" %}DISPLAY=:0 {% endif %}upm-ci package test -u {{ editor.version }} --type package-tests --platform {{ platform.runtime }} 49 | artifacts: 50 | logs: 51 | paths: 52 | - "upm-ci~/test-results/**/*" 53 | dependencies: 54 | - .yamato/upm-ci.yml#pack 55 | {% endfor %} 56 | {% endfor %} 57 | 58 | test_trigger: 59 | name: Tests Trigger 60 | triggers: 61 | branches: 62 | only: 63 | - "/.*/" 64 | dependencies: 65 | - .yamato/upm-ci.yml#pack 66 | {% for editor in test_editors %} 67 | {% for platform in test_platforms %} 68 | - .yamato/upm-ci.yml#test_{{platform.name}}_{{editor.version}} 69 | {% endfor %} 70 | {% endfor %} 71 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.0.0-preview.3] - 2021-03-01 4 | - Fix UIElements namespace compilation issue 5 | 6 | ## [1.0.0-preview.2] - 2020-11-30 7 | - Remove support for 2018.4 8 | - Fix compilation issues with 2021.1 9 | 10 | ## [1.0.0-preview.1] - 2020-01-24 11 | - First version 12 | -------------------------------------------------------------------------------- /CHANGELOG.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: def28d47a9d697b46aeb3149612d8e99 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Documentation~/TableOfContents.md: -------------------------------------------------------------------------------- 1 | * [About Performance Tracking](index) 2 | * [Performance Tracker Window](performance-tracker-window) 3 | * [Monitoring](monitoring) 4 | * [Performance Window Actions](performance-window-actions) 5 | * [Analytics](analytics) 6 | * [API](api) -------------------------------------------------------------------------------- /Documentation~/analytics.md: -------------------------------------------------------------------------------- 1 | # Analytics 2 | 3 | When using the Performance Tracking package Analytics will be sent to Unity so we can aggregate statistics on the editor performance. Each time a user closes Unity we send a report of all Performance Tracker statistics. 4 | 5 | This report can also be consulted in the editor log file of Unity (Editor log file can be found at `/AppData/Local/Unity/Editor/Editor.log` on Windows). 6 | 7 | The report looks like this and can be found at the end of the Editor.log after Unity has closed: 8 | 9 | ``` 10 | WinEditorMain : 1 samples, Peak. 1.45 s (1.0x), Avg. 1.45 s, Total. 1.446 s (0.2%) 11 | WinEditorMain.ConfigureCrashHandler: 1 samples, Peak. 20.1 us (1.0x), Avg. 20.1 us, Total. 20.10 us (0.0%) 12 | WinEditorMain.CurlRequestInitialize: 1 samples, Peak. 2.12 ms (1.0x), Avg. 2.12 ms, Total. 2.123 ms (0.0%) 13 | WinEditorMain.SetupLogFile: 1 samples, Peak. 1.33 ms (1.0x), Avg. 1.33 ms, Total. 1.334 ms (0.0%) 14 | WinEditorMain.RunNativeTestsIfRequiredAndExit: samples, Peak. 2.80 us (1.0x), Avg. 2.80 us, Total. 2.800 us (0.0%) 15 | CurlRequestCheck: 19 samples, Peak. 92.1 ms (10.7x), Avg. 8.64 ms, Total. 164.1 ms (0.0%) 16 | PackageManager::RunRequestSynchronously: 10 samples, Peak. 11.8 ms (3.0x), Avg. 3.96 ms, Total. 39.56 ms (0.0%) 17 | .... 18 | ``` 19 | 20 | -------------------------------------------------------------------------------- /Documentation~/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ## Tracker API 4 | 5 | Performance Tracker are internals for now as their implementation is subject to change. But here are the most importat API the Performance Tracking package is using to access their data. 6 | 7 | ### Querying Trackers 8 | 9 | ```CSharp 10 | internal struct EditorPerformanceTracker 11 | { 12 | public static extern string[] GetAvailableTrackers(); 13 | public static extern bool Exists(string trackerName); 14 | public static extern void Reset(string trackerName); 15 | public static extern int GetSampleCount(string trackerName); 16 | public static extern double GetLastTime(string trackerName); 17 | public static extern double GetPeakTime(string trackerName); 18 | public static extern double GetAverageTime(string trackerName); 19 | public static extern double GetTotalTime(string trackerName); 20 | public static extern double GetTotalUsage(string trackerName); 21 | public static extern double GetTimestamp(string trackerName); 22 | public static extern void LogCallstack(string trackerName); 23 | public static extern void GetCallstack(string trackerName, Action onCallstackCaptured); 24 | 25 | internal static extern int StartTracker(string trackerName); 26 | internal static extern void StopTracker(int trackerToken); 27 | } 28 | ``` 29 | 30 | ### Using tracker in C# code 31 | 32 | You can create a new Performance tracker in C# with a `using` block: 33 | 34 | ```CSharp 35 | using (new EditorPerformanceTracker("Tracker.PerformNotifications")) 36 | { 37 | PerformNotifications(now, trackerNames); 38 | } 39 | ``` 40 | 41 | This tracker will then be monitored by the [Performance Tracker Window](performance-tracker-window.html). 42 | 43 | ## Performance Actions 44 | 45 | The Performance Tracking package expose all its performance actions as public API. 46 | 47 | ```CSharp 48 | public static class PerformanceTrackerActions 49 | { 50 | public static void StartProfilerRecording(bool editorProfile, bool deepProfile); 51 | 52 | public static void StopProfilerRecording( 53 | string marker, 54 | bool openProfiler = true, 55 | Analytics.ActionSource source = Analytics.ActionSource.Scripting); 56 | 57 | public static EditorWindow OpenProfiler( 58 | string marker = "", 59 | Analytics.ActionSource source = Analytics.ActionSource.Scripting); 60 | 61 | public static EditorWindow OpenProfilerData( 62 | string profileDataPath, 63 | string marker = "", 64 | Analytics.ActionSource source = Analytics.ActionSource.Scripting); 65 | 66 | public static PerformanceTrackerWindow OpenPerformanceTrackerViewer( 67 | string marker = "", 68 | Analytics.ActionSource source = Analytics.ActionSource.Scripting); 69 | 70 | public static void LogCallstack( 71 | string marker, 72 | Analytics.ActionSource source = Analytics.ActionSource.Scripting); 73 | 74 | public static void GetCallstack( 75 | string marker, 76 | Action handler, 77 | bool formatForConsole = false); 78 | 79 | public static void AddNewPerformanceNotification( 80 | string marker, 81 | float threshold = 500, 82 | Analytics.ActionSource source = Analytics.ActionSource.Scripting); 83 | 84 | 85 | public static void OpenBugReportingTool( 86 | string marker = "", 87 | Analytics.ActionSource source = Analytics.ActionSource.Scripting); 88 | } 89 | ``` -------------------------------------------------------------------------------- /Documentation~/images/action-bug-report.gif: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:01c510fcdb90fce645dd50e4198f887bf8ed99dce788c9e20f595a618f49af8b 3 | size 547542 4 | -------------------------------------------------------------------------------- /Documentation~/images/action-open-tracker-view.gif: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1c7c834694e2b0f745ecf359a29269b040ed19d5dbf27766762436fb95905ad4 3 | size 224061 4 | -------------------------------------------------------------------------------- /Documentation~/images/action-start-profiler.gif: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:fa418083561576ab1bcd02aef59af2f6393cfd10cd773967e1b310b4a575c876 3 | size 964692 4 | -------------------------------------------------------------------------------- /Documentation~/images/add-package.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:eb325601a874d90c16f4fd0cb9d9d991a979c9d9b4b66f48c7dbeaab4a390f6c 3 | size 32250 4 | -------------------------------------------------------------------------------- /Documentation~/images/all-warnings.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4c983630829a6da4f96c4b9492ea446eeb26da9b3c34fc02d09a50d26aa93d03 3 | size 896821 4 | -------------------------------------------------------------------------------- /Documentation~/images/benchmark-log.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:7ea8e32c87e951aceafc840a8ddcc65b449492e48070f51388774f612eb495ba 3 | size 20422 4 | -------------------------------------------------------------------------------- /Documentation~/images/download.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:c43f363a0c4022ad037600264477f55129e53cb21cac1aa70f566f9faced4fb2 3 | size 21637 4 | -------------------------------------------------------------------------------- /Documentation~/images/emded.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:780693780f410b3943fb9867ef95acbd633dfc953c9767c9905f8e1410989ab2 3 | size 11667 4 | -------------------------------------------------------------------------------- /Documentation~/images/git-url.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f48767c18a36e71ec21d93139b8d923cba28b59957696b70280ab6a11fef39c5 3 | size 12085 4 | -------------------------------------------------------------------------------- /Documentation~/images/notification-partial-tracking-tick.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e84cfc3dff4c8e9417b55b7b62aa5938e329f7c3350d481257f7f21bc768675a 3 | size 61492 4 | -------------------------------------------------------------------------------- /Documentation~/images/performance-notification-in-console.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4ee2b7315c91013df2f4f4b12b24bb8c9efe92af05b007df6cf250822f7f7788 3 | size 50912 4 | -------------------------------------------------------------------------------- /Documentation~/images/performance-tracker-window.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:48289b119436bce82a4abf2968f254c8ba85424dfb16a95c200070b32b69d376 3 | size 127045 4 | -------------------------------------------------------------------------------- /Documentation~/images/performance-tracking-monitoring.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:33ec1f7e5f60dbeacea3744bc0de8752086222e20944854f8ee1f5a41203f43f 3 | size 4588 4 | -------------------------------------------------------------------------------- /Documentation~/images/pin-workflow.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e8e326c54c6f9e70e3aa27b8c9760e3eaf5903d4b1fe34ce7a67a683600b2a8d 3 | size 63651 4 | -------------------------------------------------------------------------------- /Documentation~/images/preference-notification.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:dcefc2e767e8235a2f62c0dd672c6dfeae751fe06c71fb22ce8a91e0b660ee16 3 | size 12175 4 | -------------------------------------------------------------------------------- /Documentation~/images/preference-spike-highlight.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5f68561ba49f692513a69bf4bd7d4cd2e987bac6d85db286f3837343c9e94b8d 3 | size 8021 4 | -------------------------------------------------------------------------------- /Documentation~/images/profiler-console-trace.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:b5424ed881e07bb44e8f2d8c73b6e45b9489333d61500a706e53adcd157bd15d 3 | size 9693 4 | -------------------------------------------------------------------------------- /Documentation~/images/profiler-marker.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:daa226a767f0dea5f0f0359f417b64004cfe2cf7eb14796b4ea1be76ac9a9b60 3 | size 52924 4 | -------------------------------------------------------------------------------- /Documentation~/images/profiler-workflow.gif: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:86582ae5a6b36b866d7c16d7bea4704a1060e66517a2cd726b189ab478f30d5d 3 | size 480267 4 | -------------------------------------------------------------------------------- /Documentation~/images/profiler_trace_sample.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:243d8e20884d62b2913ef37fdcb49a28aec85549518b5cfd3e91a2009060f8f3 3 | size 87421 4 | -------------------------------------------------------------------------------- /Documentation~/images/profiling-options.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:96fb0a26b72b4480a0f750036dfec244069923aa42544adcb953a5c5d061c6f3 3 | size 7218 4 | -------------------------------------------------------------------------------- /Documentation~/images/profiling_actions.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:144bf9727ac9febb8a77904c8b9ae2179b61a651f59d491aee0edfb4e1ceb7e7 3 | size 7457 4 | -------------------------------------------------------------------------------- /Documentation~/images/profiling_snippet_window.gif: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:b31feab7103f199d30dab93eaf975356f5c44c9a5157c0db14680f2adb91413b 3 | size 1724864 4 | -------------------------------------------------------------------------------- /Documentation~/images/repaint_snippet.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:bac0d53cc93101be962b9b87ab650b7db1a6da00f3249baed25d4bc9c39bbbf5 3 | size 63057 4 | -------------------------------------------------------------------------------- /Documentation~/images/select-package.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:648015d97e9d6caab23d70590db792061bc698f7b7ebce653da76870db85585c 3 | size 51849 4 | -------------------------------------------------------------------------------- /Documentation~/images/static_snippet.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:335037412caab21e5fc52b6287a384efec60d7c7027ef52006bb279095361591 3 | size 67388 4 | -------------------------------------------------------------------------------- /Documentation~/images/task-manager.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f1eee0ebaba38dc9befb06ec67d82532b74b9755e7470c36e7df5daae225a591 3 | size 49198 4 | -------------------------------------------------------------------------------- /Documentation~/images/tracker-window-actions.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:aac24ed9a44abc31751e4e895e03abb187ecd57982c51299ee47730af08b4230 3 | size 9403 4 | -------------------------------------------------------------------------------- /Documentation~/images/tracker-window-add-notification-action.gif: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:930260b99390593c0a7b31c1406f42c1db4a697d674281f625722f6ebcfd2dfb 3 | size 344353 4 | -------------------------------------------------------------------------------- /Documentation~/images/tracker-window-profiler.gif: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:c0710f9d8089f2d203b0878300e76d4ddb1abf36c483b9b3d5dd858c497de2b8 3 | size 1984225 4 | -------------------------------------------------------------------------------- /Documentation~/images/window-actions.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:fc5efd1aaf23d22582af9f25dcafd7fbca0f125fb5392b56aa12514fe0cff1f8 3 | size 79002 4 | -------------------------------------------------------------------------------- /Documentation~/images/window-configuration-toolbar.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:c10b37483412b7f697b9e2f87c83128b407ee3b8469a0685cbb12f453a73f40e 3 | size 31994 4 | -------------------------------------------------------------------------------- /Documentation~/index.md: -------------------------------------------------------------------------------- 1 | # About Performance Tracking 2 | 3 | This package contains multiples utilities and workflows to help track performance in the editor. Most of this work is in prototype form and is expected to evolve with user feedback. 4 | 5 | ## Performance Tracker 6 | 7 | Most of the workflows of this package are based on data gathered from the multiple Performance Trackers that are instantied in the Editor code. Performance tracker are a really low cost abstraction that is always live in the editor. 8 | 9 | You can get more information about the tracker [api](api.md). 10 | 11 | ## Which workflows are available? 12 | 13 | You can find further information on all performance tracking workflows here: 14 | 15 | * [Performance Tracker Window](performance-tracker-window.md) 16 | * [Monitoring](monitoring.md) 17 | * [Performance Window Actions](performance-window-actions.md) 18 | * [Analytics](analytics.md) 19 | * [API](api.md) 20 | -------------------------------------------------------------------------------- /Documentation~/monitoring.md: -------------------------------------------------------------------------------- 1 | # Performance Monitoring 2 | 3 | When the Performance Tracking package is included in a project, it can track and automatically reports Performance spikes in the editor. 4 | 5 | Performance monitoring happens in the background and can be scheduled using the Performance Tracking preference page: 6 | 7 | ![monitor](images/performance-tracking-monitoring.png) 8 | 9 | By default we monitor performance events each 5 seconds. 10 | 11 | ## Performance Notifications 12 | 13 | You can setup notifications that will trigger when a specific Tracker (or a filtered) Tracker has its *Average Time* value be over a specific threshold. This can be great to track under the hood events that might stall the editor (Application.Reload, Application.Tick...). 14 | 15 | If you open the Performance Tracking Preferences page you can tweak which Tracker will fire Performance notification: 16 | 17 | ![pref-notif](images/preference-notification.png) 18 | 19 | Notice how using a *partial* tracker name (ex: `.Tick`) will track **ALL** markers containing this partial name. In this example all these *ticking* trackers would be monitored: 20 | 21 | ![track](images/notification-partial-tracking-tick.png) 22 | 23 | When a performance notification is triggered, it will be printed in the console: 24 | 25 | ![notif](images/performance-notification-in-console.png) 26 | 27 | ## Editor Window Paint Spike Highlight 28 | 29 | If you enable *Spike Window Highlight* from the Performance tracking preference page, each Editor Window will will be monitored for performance spike happening while repainting the window (repaints include all OnGUI, Layout, and painting operations). A spike happens when the `Average Time` of a *Paint* Tracker is over a specific customizable threshold. 30 | 31 | When a spike happens, the EditorWindow will be highlighted either in yellow (*warning*) or in red (*critical*). 32 | 33 | You can also enable Inspector Components highlight and we will monitor if any *Inspector Editor* is taking too much time in their `OnInspectorGUI` function. 34 | 35 | ![spike highlight](images/preference-spike-highlight.png) 36 | 37 | This editor is being warned! : 38 | 39 | ![spike highlight](images/all-warnings.png) -------------------------------------------------------------------------------- /Documentation~/performance-tracker-window.md: -------------------------------------------------------------------------------- 1 | # Performance Tracker Window 2 | 3 | The Performance Tracker Window monitors all [Performance Trackers](api.html#tracker-api) similar to how *Task Manager* monitors Windows processes: 4 | 5 | ![task manager](images/task-manager.png) 6 | 7 | ![task manager](images/performance-tracker-window.png) 8 | 9 | Note that this window is available in Unity 2020.1 and onward. 10 | 11 | ## Window configuration 12 | 13 | The Performance Tracker Window is mostly about monitoring but it can be customized to make performance gathering more efficient. The toolbar allows to configure how we report performance data and which tracker are to be displayed. 14 | 15 | ![config](images/window-configuration-toolbar.png) 16 | 17 | - **Udpate Speed**: Speed at which we refresh all the tracker and the view itself. 18 | - **Columns Selector**: Decide which columns to show. Tracker name and Action columns are always visible. 19 | - **Sorty By**: Which columns is used for sorting. *Alternatively you can click a column header to sort all tracker according to this column.* 20 | - **Cog Icon**: Clicking this button will open up the Performance tracking preferences. 21 | 22 | ## Tracker Categories 23 | 24 | A lot of different Editor workflows have been instrumented with Performance Tracker. Some trackers have name that have common rprefix/suffix to identify common workflows: 25 | 26 | - `WinEditorMain.*` : Unity initialization sequence 27 | - `Application.*` : (mostly) Unity initialization sequence 28 | - `Application.DomainReload`: you know what it is 29 | - `.Paint` : Paint sequence for a given EditorWindow 30 | - `.OnGUI.repaint` 31 | - `.OnGUI.mouseMove` 32 | - `.OnGUI.mouseLeaveWindow` 33 | - `.OnGUI.mouseenterWindow` 34 | - `.OnGUI.layout` 35 | - `*.Tick`: all the various Tick Timers of the Editor 36 | 37 | 38 | ## Tracker Columns 39 | - **Pin**: can be used to pin a marker so it stays always on top 40 | - **Sample Count**: number of time this tracker was hit. Clicking on the Sample Count resets it to zero (and trigger an average computation) 41 | - **Age**: how much time since that trakcer was last hit 42 | - **Peak Time**: Highest time for this tracker 43 | - **Average Time**: Average time for all Samples 44 | - **Last Time**: last tracker time 45 | - **Actions**: See below for more information on which actions are possible for a specific Tracker. 46 | 47 | All the columns can be sorted. 48 | 49 | ## Pins 50 | 51 | Using the first column you can *pin* tracker you want to keep on top of the view for closer monitoring. When sorting all pinned tracker are sorted together and all the rest of the trackers are sorted separately. 52 | 53 | ![pin](images/pin-workflow.png) 54 | 55 | ## Action Column 56 | 57 | The last column has an action button allowing you to execute a specific set of actions on a specific tracker: 58 | 59 | ![actions](images/tracker-window-actions.png) 60 | 61 | - **set as search filter**: take the current tracker name and set it as the filter of the Window (effectively keeping this tracker the only one on screen) 62 | - **Add performance notification**: Add a [performance notification](monitoring.html#performance-notifications) for this tracker 63 | ![add notif](images/tracker-window-add-notification-action.gif) 64 | - **Profile...**: start the profiler (without popping the window) and filter all samples **so we only keep the data relative to this tracker**. 65 | - **Deep Profile...**: start the profiler in deep profile mode (without popping the window) and filter all samples *so we only keep the data relative to this tracker*. 66 | 67 | For both profiling action, the action button will change to a *Stop profiling* button. Pressing this button will stop recording AND will open up the Profiler window. 68 | 69 | ![profiler](images/tracker-window-profiler.gif) -------------------------------------------------------------------------------- /Documentation~/performance-window-actions.md: -------------------------------------------------------------------------------- 1 | # Performance Actions 2 | 3 | When using the Performance Tracking package, new specific actions are added to each Editor Window menu. 4 | 5 | ![window-action](images/window-actions.png) 6 | 7 | ## Start Profiler Recording 8 | 9 | Open up the profiler and start recording. The *Tracker* (`.Paint`) of the Editor Window will already be set as search filter in the profiler. 10 | 11 | ![window-action](images/action-start-profiler.gif) 12 | 13 | ## Open Profiler 14 | 15 | Open up the profiler (**no recording**). The *Tracker* (`.Paint`) of the Editor Window will already be set as search filter in the profiler. 16 | 17 | ## Open Performance Tracking View 18 | 19 | Open the Performance Tracker Window with the The *Tracker* (`.Paint`) of the Editor Window already set as a search filter. 20 | 21 | ![window-action](images/action-open-tracker-view.gif) 22 | 23 | ## Report Performance Bug 24 | 25 | Open the Bug Reporting tool ready to report a performance bug. We will package a performance report as well as all the Tracker statistics for the Editor Window. See [Analitycs](analytics.html) for an example of the performance report that will be attached with the bug report. 26 | 27 | ![window-action](images/action-bug-report.gif) 28 | -------------------------------------------------------------------------------- /Documentation~/profiling-snippet-window.md: -------------------------------------------------------------------------------- 1 | # Profiling Snippet Window 2 | 3 | The goal of this window is to help benchmark or profile specific snippet of code in a more deterministic fashion. 4 | 5 | Here is a quick example of the Window in action: 6 | 7 | ![images](images/profiling_snippet_window.gif) 8 | 9 | As you can see there are mainly 2 workflows: 10 | 11 | - **Profiling**: execute the snippet of code while profiling it in the background. We then open the profiler ready to investigate the snippet: the frame is already selected and the filter search field is already populated. 12 | - **Benchmark**: execute the snippet of code and benchmark its execution. It then logs the result in file (and at the console). 13 | 14 | ## Snippet List View 15 | 16 | This list view shows all snippets registered in your project. By default we are populated the list with 2 types of snippet: 17 | 18 | - Repaint: for each EditorWindow in the Editor, we generate a snippet that will be able to open the Window and benchmark its `RepaintImmediatly` function. 19 | 20 | ![repaint](images/repaint_snippet.png) 21 | 22 | - Static: we extract all static C# API with no parameters. You can then execute those snippet and run benchmarks. 23 | 24 | ![static](images/static_snippet.png) 25 | 26 | ### Registering custom Snippet 27 | 28 | A user can also register its own Snippet using the `ProfilingSnippet` C# Attribute: 29 | 30 | ```CSharp 31 | [ProfilingSnippetAction("Test_something_long_id", 32 | "Test something long", 33 | "Test Category", 34 | "Test_something_long_sample")] 35 | static void TestProfilingSnippetActionAttr(object preExecutePayload, ProfilingSnippet snippet, ProfilingSnippetOptions options) 36 | { 37 | DoSomethingLong(); 38 | } 39 | ``` 40 | 41 | Alternatively you can register a function that will create a new instance of `ProfilingSnippet` if you want more control over the Snippet execution. To do so you use the `ProfilingSnippet` attribute: 42 | 43 | ```CSharp 44 | [ProfilingSnippet] 45 | static ProfilingSnippet TestProfilingSnippetAttr() 46 | { 47 | var snippet = new ProfilingSnippet("Test_something_else_long_id", "Test something else long"); 48 | // Set the snippet category 49 | snippet.category = "Test"; 50 | 51 | // Set the sample name that will be used when profiling the snippet 52 | snippet.sampleName = "Test_something_else_long_sample"; 53 | 54 | // Set the performance tracker name that will be used when profiling 55 | // with marker filtering. 56 | snippet.marker = "Test_something_else_long_marker"; 57 | snippet.executeFunc = (preExecutePayload, s, options) => 58 | { 59 | DoSomethingLong(); 60 | }; 61 | return snippet; 62 | } 63 | ``` 64 | 65 | ## Snippet Profiling Actions 66 | 67 | There are 6 (4 if 18.4->19.3) different actions available with Snippet. Four actions can be used for profiling and 2 for benchmarking. 68 | 69 | ![profiling](images/profiling_actions.png) 70 | 71 | ### Profile (Deep) 72 | 73 | The Profile action, uses the profiler in the background to profile a Snippet. Basically it does the following: 74 | 75 | ```CSharp 76 | Profiler.BeginSample(snippetSampleName); 77 | executeMySnippet(); 78 | Profiler.EndSample(); 79 | ``` 80 | 81 | ![](images/profiler-workflow.gif) 82 | 83 | Notice how the profiler opens already at the right frame with the proper sample name already populated in the the profiler search field. 84 | 85 | The profiling trace into the `Assets/Profiles` folder and open the profile log in the profiler: 86 | 87 | ![](images/profiler-console-trace.png) 88 | 89 | If you chose `Profile - Deep` it record the trace in Deep Profile mode. Note that **the first time** you activate `Profiler - Deep` Unity needs to domain reload all script. You will then need to press the `Profile - Deep` button again. 90 | 91 | ### Profiling Performance Tracker (for Unity 2020.1+) 92 | 93 | `Profile Marker` is similar to profile but instead of creating a Profiler Sample it uses Marker Filtering to record an already existing Performance tracker. In the case of `Repaint` snippet, each EditorWindow rrepaing is already instrumented with a performance tracker named `.Paint`. 94 | 95 | Marker filtering is great because it filers out all the data that is not directly part of the callstack of the marker: 96 | 97 | ![](images/profiler-marker.png) 98 | 99 | Marker filtering **is only available for Unity 2020.1 and onward**. 100 | 101 | ### Benchmark 102 | 103 | Benchmark execute the snippet a given amount of times (see Options below) and record the total time, peak time, min time, average and medium time. Those benchmarks result are logged in the Console Window as well as in the `Assets/Profiles/ProfilingSnippet.log` file: 104 | 105 | ![](images/benchmark-log.png) 106 | 107 | ### Benchmark Marker (for Unity 2020.1+) 108 | 109 | If you use `Benchmark Marker` we actually track Performance Tracker samples invoked in the sample. We effectively reset the samples count then execute the snippet a given amount of time and report about the Performance Marker usage. 110 | 111 | See [Performance Tracker Window](performance-tracker-window.md) for more information on Performance Tracker. 112 | 113 | ## Options 114 | 115 | The options box contains various options that can be used by Profiling Actions: 116 | 117 | ![](images/profiling-options.png) 118 | 119 | - Maximize Window: use by **Repaint Snippets**. If this is toggled it will maximize the window before executing repainting. 120 | - Standalone Window: use by **Repaint Snippets**. If this is toggled it will close ay Editor Windowe corresponding to the Snippet and open a new window undocked. 121 | - Nb Iterations: use by both **Benchmark** actions and **profile** actions. This is the number of time we execute the snippet. 122 | - Log File: where Benchmark results are logged. 123 | - Clear: clear the log file. 124 | - CSV: log the benchmark in CSV mode 125 | 126 | Here is an example of CSV logging: 127 | ``` 128 | 100,0.121397,1.21397,1.171,2.5508,0.9343 129 | 100,0.120124,1.20124,1.1082,3.784,0.9271 130 | ``` 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 259e719be19905248a38ae5ee9c885e4 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/BugReporterUtils.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_2020_1_OR_NEWER 2 | using System.Collections.Generic; 3 | using UnityEditor.BugReporting; 4 | 5 | namespace Unity.PerformanceTracking 6 | { 7 | internal static class BugReporterUtils 8 | { 9 | public static void OpenBugReporter(string description, IEnumerable additionalAttachments) 10 | { 11 | var additionalArguments = new List(); 12 | 13 | // Set performance type 14 | additionalArguments.Add("--bugtype"); 15 | additionalArguments.Add("performance"); 16 | 17 | // Set custom description 18 | additionalArguments.Add("--description"); 19 | additionalArguments.Add(description); 20 | 21 | // Add attachments 22 | foreach (var attachment in additionalAttachments) 23 | { 24 | additionalArguments.Add("--attach"); 25 | additionalArguments.Add(attachment); 26 | } 27 | 28 | // Open the reporter 29 | BugReportingTools.LaunchBugReporter(BugReportMode.ManualOpen, additionalArguments.ToArray()); 30 | } 31 | } 32 | } 33 | #endif -------------------------------------------------------------------------------- /Editor/BugReporterUtils.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2028b5c749ba60943b4c783f161f9776 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Icons.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using UnityEditor; 4 | using UnityEditor.Experimental; 5 | using UnityEngine; 6 | 7 | namespace Unity.PerformanceTracking 8 | { 9 | public static class Icons 10 | { 11 | public static string iconFolder = $"{Utils.packageFolderName}/Editor/Icons"; 12 | public static Texture2D settings = LoadIcon($"{iconFolder}/settings.png"); 13 | 14 | static Icons() 15 | { 16 | if (EditorGUIUtility.isProSkin) 17 | { 18 | settings = LightenTexture(settings); 19 | } 20 | } 21 | 22 | private static Texture2D LoadIcon(string resourcePath, bool autoScale = false) 23 | { 24 | if (String.IsNullOrEmpty(resourcePath)) 25 | return null; 26 | 27 | float systemScale = EditorGUIUtility.pixelsPerPoint; 28 | if (autoScale && systemScale > 1f) 29 | { 30 | int scale = Mathf.RoundToInt(systemScale); 31 | string dirName = Path.GetDirectoryName(resourcePath).Replace('\\', '/'); 32 | string fileName = Path.GetFileNameWithoutExtension(resourcePath); 33 | string fileExt = Path.GetExtension(resourcePath); 34 | for (int s = scale; scale > 1; --scale) 35 | { 36 | string scaledResourcePath = $"{dirName}/{fileName}@{s}x{fileExt}"; 37 | var scaledResource = EditorResources.Load(scaledResourcePath, false); 38 | if (scaledResource) 39 | return scaledResource; 40 | } 41 | } 42 | 43 | return EditorResources.Load(resourcePath, false); 44 | } 45 | 46 | private static Texture2D LightenTexture(Texture2D texture) 47 | { 48 | if (!texture) 49 | return texture; 50 | Texture2D outTexture = new Texture2D(texture.width, texture.height); 51 | var outColorArray = outTexture.GetPixels(); 52 | 53 | var colorArray = texture.GetPixels(); 54 | for (var i = 0; i < colorArray.Length; ++i) 55 | outColorArray[i] = LightenColor(colorArray[i]); 56 | 57 | outTexture.hideFlags = HideFlags.HideAndDontSave; 58 | outTexture.SetPixels(outColorArray); 59 | outTexture.Apply(); 60 | 61 | return outTexture; 62 | } 63 | 64 | public static Color LightenColor(Color color) 65 | { 66 | Color.RGBToHSV(color, out var h, out _, out _); 67 | var outColor = Color.HSVToRGB((h + 0.5f) % 1, 0f, 0.8f); 68 | outColor.a = color.a; 69 | return outColor; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /Editor/Icons.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2247b4000299ded4a8ac2ae41e3bd84d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Icons.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d4f94ed15243a5d43b695c87f7af6e3d 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Icons/settings.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3b792bc8901a640075c896bd79cd5238bc8ec562830cff9e9a827973b1531ab6 3 | size 1419 4 | -------------------------------------------------------------------------------- /Editor/Icons/settings.png.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b309f7fbbf9621a4c83c0fc9eb86723d 3 | TextureImporter: 4 | internalIDToNameTable: [] 5 | externalObjects: {} 6 | serializedVersion: 10 7 | mipmaps: 8 | mipMapMode: 0 9 | enableMipMap: 1 10 | sRGBTexture: 1 11 | linearTexture: 0 12 | fadeOut: 0 13 | borderMipMap: 0 14 | mipMapsPreserveCoverage: 0 15 | alphaTestReferenceValue: 0.5 16 | mipMapFadeDistanceStart: 1 17 | mipMapFadeDistanceEnd: 3 18 | bumpmap: 19 | convertToNormalMap: 0 20 | externalNormalMap: 0 21 | heightScale: 0.25 22 | normalMapFilter: 0 23 | isReadable: 1 24 | streamingMipmaps: 0 25 | streamingMipmapsPriority: 0 26 | grayScaleToAlpha: 0 27 | generateCubemap: 6 28 | cubemapConvolution: 0 29 | seamlessCubemap: 0 30 | textureFormat: 1 31 | maxTextureSize: 2048 32 | textureSettings: 33 | serializedVersion: 2 34 | filterMode: -1 35 | aniso: 2 36 | mipBias: -100 37 | wrapU: -1 38 | wrapV: -1 39 | wrapW: -1 40 | nPOTScale: 1 41 | lightmap: 0 42 | compressionQuality: 50 43 | spriteMode: 0 44 | spriteExtrude: 1 45 | spriteMeshType: 1 46 | alignment: 0 47 | spritePivot: {x: 0.5, y: 0.5} 48 | spritePixelsToUnits: 100 49 | spriteBorder: {x: 0, y: 0, z: 0, w: 0} 50 | spriteGenerateFallbackPhysicsShape: 1 51 | alphaUsage: 1 52 | alphaIsTransparency: 0 53 | spriteTessellationDetail: -1 54 | textureType: 0 55 | textureShape: 1 56 | singleChannelComponent: 0 57 | maxTextureSizeSet: 0 58 | compressionQualitySet: 0 59 | textureFormatSet: 0 60 | ignorePngGamma: 0 61 | platformSettings: 62 | - serializedVersion: 3 63 | buildTarget: DefaultTexturePlatform 64 | maxTextureSize: 8192 65 | resizeAlgorithm: 0 66 | textureFormat: -1 67 | textureCompression: 1 68 | compressionQuality: 50 69 | crunchedCompression: 0 70 | allowsAlphaSplitting: 0 71 | overridden: 0 72 | androidETC2FallbackOverride: 0 73 | forceMaximumCompressionQuality_BC6H_BC7: 0 74 | - serializedVersion: 3 75 | buildTarget: Standalone 76 | maxTextureSize: 8192 77 | resizeAlgorithm: 0 78 | textureFormat: -1 79 | textureCompression: 1 80 | compressionQuality: 50 81 | crunchedCompression: 0 82 | allowsAlphaSplitting: 0 83 | overridden: 0 84 | androidETC2FallbackOverride: 0 85 | forceMaximumCompressionQuality_BC6H_BC7: 0 86 | - serializedVersion: 3 87 | buildTarget: iPhone 88 | maxTextureSize: 8192 89 | resizeAlgorithm: 0 90 | textureFormat: -1 91 | textureCompression: 1 92 | compressionQuality: 50 93 | crunchedCompression: 0 94 | allowsAlphaSplitting: 0 95 | overridden: 0 96 | androidETC2FallbackOverride: 0 97 | forceMaximumCompressionQuality_BC6H_BC7: 0 98 | - serializedVersion: 3 99 | buildTarget: Android 100 | maxTextureSize: 8192 101 | resizeAlgorithm: 0 102 | textureFormat: -1 103 | textureCompression: 1 104 | compressionQuality: 50 105 | crunchedCompression: 0 106 | allowsAlphaSplitting: 0 107 | overridden: 0 108 | androidETC2FallbackOverride: 0 109 | forceMaximumCompressionQuality_BC6H_BC7: 0 110 | - serializedVersion: 3 111 | buildTarget: Windows Store Apps 112 | maxTextureSize: 8192 113 | resizeAlgorithm: 0 114 | textureFormat: -1 115 | textureCompression: 1 116 | compressionQuality: 50 117 | crunchedCompression: 0 118 | allowsAlphaSplitting: 0 119 | overridden: 0 120 | androidETC2FallbackOverride: 0 121 | forceMaximumCompressionQuality_BC6H_BC7: 0 122 | spriteSheet: 123 | serializedVersion: 2 124 | sprites: [] 125 | outline: [] 126 | physicsShape: [] 127 | bones: [] 128 | spriteID: 129 | internalID: 0 130 | vertices: [] 131 | indices: 132 | edges: [] 133 | weights: [] 134 | secondaryTextures: [] 135 | spritePackingTag: 136 | pSDRemoveMatte: 0 137 | pSDShowRemoveMatteOption: 0 138 | userData: 139 | assetBundleName: 140 | assetBundleVariant: 141 | -------------------------------------------------------------------------------- /Editor/PerformanceTracker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace Unity.PerformanceTracking 5 | { 6 | public struct PerformanceTracker : IDisposable 7 | { 8 | private bool m_Disposed; 9 | private readonly int m_WatchHandle; 10 | 11 | public PerformanceTracker(string name) 12 | { 13 | m_Disposed = false; 14 | m_WatchHandle = UnityEditor.Profiling.EditorPerformanceTracker.StartTracker(name); 15 | } 16 | 17 | public void Dispose() 18 | { 19 | if (m_Disposed) 20 | return; 21 | m_Disposed = true; 22 | UnityEditor.Profiling.EditorPerformanceTracker.StopTracker(m_WatchHandle); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /Editor/PerformanceTracker.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: de72cdf2e8bea4b45808f538c4924118 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/PerformanceTrackerActions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.IO; 4 | using System.Reflection; 5 | using System.Runtime.CompilerServices; 6 | using System.Text; 7 | using System.Text.RegularExpressions; 8 | using UnityEditor; 9 | using UnityEditor.Profiling; 10 | using UnityEditorInternal; 11 | using UnityEditorInternal.Profiling; 12 | using UnityEngine; 13 | using UnityEngine.Profiling; 14 | 15 | namespace Unity.PerformanceTracking 16 | { 17 | internal static class PerformanceTrackerActions 18 | { 19 | private const string k_DefaultPerformanceTrackerReportsDirectory = "Assets/EditorTrackerReports"; 20 | 21 | #if UNITY_2020_1_OR_NEWER 22 | [WindowAction] 23 | private static WindowAction StartProfilerRecordingEditorAction() 24 | { 25 | var action = WindowAction.CreateWindowMenuItem("StartProfilerRecordingEditor", (window, _action) => 26 | { 27 | // We need to fix this!! See 28 | 29 | var marker = PerformanceTrackerMonitoringService.GetWindowPaintMarker(window); 30 | ProfilerHelpers.OpenProfiler(marker, profilerWindow => 31 | { 32 | ProfilerHelpers.SetRecordingEnabled(profilerWindow, true); 33 | ProfilerHelpers.StartProfilerRecording("", true, ProfilerDriver.deepProfiling); 34 | }); 35 | }, "Window Performance/Start Profiler Recording"); 36 | return action; 37 | } 38 | 39 | [WindowAction] 40 | private static WindowAction OpenProfilerAction() 41 | { 42 | var action = WindowAction.CreateWindowMenuItem("OpenProfilerForWindow", (v, a) => { 43 | OpenProfiler(PerformanceTrackerMonitoringService.GetWindowPaintMarker(v), Analytics.ActionSource.EditorWindowMenu); 44 | }, "Window Performance/Open Profiler"); 45 | return action; 46 | } 47 | 48 | [WindowAction] 49 | private static WindowAction OpenPerformanceTrackerWindowAction() 50 | { 51 | var action = WindowAction.CreateWindowMenuItem("OpenPerformanceTrackerWindow", (v, a) => { 52 | OpenPerformanceTrackerWindow(v.GetType().Name, Analytics.ActionSource.EditorWindowMenu); 53 | }, "Window Performance/Open Performance Tracker Window"); 54 | return action; 55 | } 56 | 57 | [WindowAction] 58 | private static WindowAction OpenBugReportingToolAction() 59 | { 60 | var action = WindowAction.CreateWindowMenuItem("OpenBugReportingTool", (v, a) => 61 | { 62 | OpenBugReportingTool(PerformanceTrackerMonitoringService.GetWindowPaintMarker(v), Analytics.ActionSource.EditorWindowMenu); 63 | }, "Window Performance/Report Performance Bug"); 64 | return action; 65 | } 66 | #endif 67 | public static void OpenProfiler(string marker) 68 | { 69 | OpenProfiler(marker, Analytics.ActionSource.Scripting); 70 | } 71 | 72 | public static void OpenProfiler(string marker = "", Analytics.ActionSource source = Analytics.ActionSource.Scripting) 73 | { 74 | Analytics.SendPerformanceActionEvent(new Analytics.PerformanceActionEvent( 75 | Analytics.ActionType.OpenProfilerOnMarker, 76 | marker, source)); 77 | ProfilerHelpers.OpenProfiler(marker); 78 | } 79 | 80 | public static void OpenProfileReport(string profileDataPath, string searchString = "", Analytics.ActionSource source = Analytics.ActionSource.Scripting) 81 | { 82 | Analytics.SendPerformanceActionEvent(new Analytics.PerformanceActionEvent( 83 | Analytics.ActionType.OpenProfilerData, 84 | searchString, source)); 85 | ProfilerHelpers.OpenProfileReport(profileDataPath, searchString); 86 | } 87 | 88 | public static void StopProfilerRecordingAndCreateReport(string profileTitle, Analytics.ActionSource source = Analytics.ActionSource.Scripting) 89 | { 90 | ProfilerHelpers.StopProfilerRecordingAndCreateReport(profileTitle); 91 | 92 | Analytics.SendPerformanceActionEvent(new Analytics.PerformanceActionEvent( 93 | ProfilerDriver.deepProfiling ? Analytics.ActionType.DeedProfile : Analytics.ActionType.Profile, 94 | profileTitle, 95 | source 96 | )); 97 | } 98 | 99 | public static PerformanceTrackerWindow OpenPerformanceTrackerWindow(string marker, Analytics.ActionSource source = Analytics.ActionSource.Scripting) 100 | { 101 | Analytics.SendPerformanceActionEvent(new Analytics.PerformanceActionEvent( 102 | Analytics.ActionType.OpenPerformanceTrackerViewer, 103 | marker, source)); 104 | var window = EditorWindow.GetWindow(); 105 | window.Show(); 106 | window.SetSearchString(""); 107 | window.SetSearchString(marker); 108 | return window; 109 | } 110 | 111 | public static void LogCallstack(string marker, Analytics.ActionSource source = Analytics.ActionSource.Scripting) 112 | { 113 | Analytics.SendPerformanceActionEvent(new Analytics.PerformanceActionEvent( 114 | Analytics.ActionType.LogCallstack, 115 | marker, source)); 116 | GetCallstack(marker, callstack => Debug.Log(callstack), true); 117 | } 118 | 119 | public static void GetCallstack(string marker, Action handler, bool formatForConsole = false) 120 | { 121 | EditorPerformanceTracker.GetCallstack(marker, (callstack => 122 | { 123 | handler(formatForConsole ? FormatCallstackForConsole(callstack) : callstack); 124 | })); 125 | } 126 | 127 | public static void AddNewPerformanceNotification(string marker, float threshold = 500, Analytics.ActionSource source = Analytics.ActionSource.Scripting) 128 | { 129 | Analytics.SendPerformanceActionEvent(new Analytics.PerformanceActionEvent( 130 | Analytics.ActionType.AddNotification, 131 | marker, source)); 132 | 133 | var collection = PerformanceTrackerSettings.LoadPerformanceNotificationCollection(); 134 | PerformanceTrackerSettings.AddPerformanceNotification(collection, new PerformanceNotification() 135 | { 136 | enabled = true, 137 | name = marker, 138 | threshold = threshold 139 | }); 140 | PerformanceTrackerSettings.SavePerformanceNotificationCollection(collection); 141 | SettingsService.OpenUserPreferences(PerformanceTrackerSettings.settingsKey); 142 | SettingsService.NotifySettingsProviderChanged(); 143 | } 144 | 145 | #if UNITY_2020_1_OR_NEWER 146 | public static void OpenBugReportingTool(string marker, Analytics.ActionSource source = Analytics.ActionSource.Scripting) 147 | { 148 | Analytics.SendPerformanceActionEvent(new Analytics.PerformanceActionEvent( 149 | Analytics.ActionType.LogPerformanceBug, 150 | marker, source)); 151 | var options = new TrackerReportOptions(); 152 | options.showSamples = true; 153 | options.showPeak = true; 154 | options.showAvg = true; 155 | options.showTotal = true; 156 | options.sort = true; 157 | options.sortBy = ColumnId.PeakTime; 158 | options.sortAsc = false; 159 | var report = PerformanceTrackerReportUtils.GetAllTrackersReport(options); 160 | 161 | var reportFilePath = SavePerformanceTrackerReport(report); 162 | 163 | var descriptionBuilder = new StringBuilder(); 164 | descriptionBuilder.AppendLine("1. What is slow (complete performance tracker report attached)"); 165 | if (!string.IsNullOrEmpty(marker)) 166 | { 167 | options.trackerFilter = $"^{marker}$"; 168 | var markerReport = PerformanceTrackerReportUtils.GetTrackersReport(options); 169 | descriptionBuilder.AppendLine(); 170 | descriptionBuilder.AppendLine(markerReport); 171 | } 172 | descriptionBuilder.AppendLine(); 173 | descriptionBuilder.Append("2. How we can reproduce it using the example you attached"); 174 | 175 | var myAssets = new[] 176 | { 177 | GetFullPath(reportFilePath) 178 | }; 179 | BugReporterUtils.OpenBugReporter(descriptionBuilder.ToString(), myAssets); 180 | } 181 | #endif 182 | 183 | internal static string FormatCallstackForConsole(string callstack) 184 | { 185 | // The callstack we are receiving is not formatted with hyperlinks, nor does 186 | // it have the format that the console expects to display in the active text box. 187 | var formattedCallstack = Regex.Replace(callstack, "\\[(\\S+?):(\\d+)\\]", "[$1:$2]"); 188 | return formattedCallstack; 189 | } 190 | 191 | private static string GetFullPath(string projectRelativePath) 192 | { 193 | var fileInfo = new FileInfo(projectRelativePath); 194 | return fileInfo.FullName; 195 | } 196 | 197 | private static string SavePerformanceTrackerReport(string report) 198 | { 199 | if (!System.IO.Directory.Exists(k_DefaultPerformanceTrackerReportsDirectory)) 200 | System.IO.Directory.CreateDirectory(k_DefaultPerformanceTrackerReportsDirectory); 201 | 202 | var timeId = EditorApplication.timeSinceStartup.ToString(CultureInfo.InvariantCulture).Replace(".", ""); 203 | var reportsFilePath = $"{k_DefaultPerformanceTrackerReportsDirectory}/{timeId}.perf-trackers-report.txt".Replace("\\", "/"); 204 | File.WriteAllText(reportsFilePath, report); 205 | 206 | return reportsFilePath; 207 | } 208 | } 209 | } -------------------------------------------------------------------------------- /Editor/PerformanceTrackerActions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 92d97170f07d5c14db16ec942c185f1b 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/PerformanceTrackerModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | using UnityEditor; 4 | using UnityEditor.Profiling; 5 | using System.Diagnostics; 6 | 7 | namespace Unity.PerformanceTracking 8 | { 9 | [DebuggerDisplay("name={name}")] 10 | internal struct PtInfo 11 | { 12 | public string name; 13 | public int sampleCount; 14 | public double peakTime; 15 | public double avgTime; 16 | public double totalTime; 17 | public double lastTime; 18 | public double usage; 19 | public double timestamp; 20 | public int dtSampleCount; 21 | public double dtPeakTime; 22 | public double dtAvgTime; 23 | public double dtLastTime; 24 | public bool updated; 25 | } 26 | 27 | internal struct RefreshRateInfo 28 | { 29 | public double rate; 30 | public string label; 31 | } 32 | 33 | internal enum ColumnId 34 | { 35 | Name, 36 | SampleCount, 37 | Age, 38 | PeakTime, 39 | AvgTime, 40 | LastTime, 41 | TotalTime, 42 | Actions 43 | } 44 | 45 | internal struct ColumnDescriptor 46 | { 47 | public ColumnDescriptor(ColumnId id, string label, int maskId = 0) 48 | { 49 | columnId = id; 50 | columnsSelectorMaskId = maskId; 51 | this.label = label; 52 | labelAsc = label + " \u21d3"; 53 | labelDesc = label + " \u21d1"; 54 | } 55 | 56 | public ColumnId columnId; 57 | public bool supportsHiding => columnsSelectorMaskId > 0; 58 | public int columnsSelectorMaskId; 59 | public string label; 60 | public string labelAsc; 61 | public string labelDesc; 62 | } 63 | 64 | internal static class PtStyles 65 | { 66 | public static bool isDarkTheme => EditorGUIUtility.isProSkin; 67 | public static Color normalColor = isDarkTheme ? new Color(196 / 255f, 196 / 255f, 196 / 255f) : new Color(32 / 255f, 32 / 255f, 32 / 255f); 68 | public static Color warningColor = isDarkTheme ? new Color(255 / 255f, 204 / 255f, 0 / 255f) : new Color(240 / 255f, 105 / 255f, 53 / 255f); 69 | public static Color criticalColor = new Color(204 / 255f, 51 / 255f, 0 / 255f); 70 | 71 | public static string criticalHexCode = Utils.ColorToHexCode(criticalColor); 72 | public static string warningHexCode = Utils.ColorToHexCode(warningColor); 73 | 74 | public static Color fasterColor = isDarkTheme ? new Color(153 / 255f, 204 / 255f, 51 / 255f) : new Color(1 / 255f, 169 / 255f, 87 / 255f); 75 | public static Color slowerColor = isDarkTheme ? new Color(255 / 255f, 153 / 255f, 102 / 255f) : criticalColor; 76 | public static Color oddRowColor = isDarkTheme ? new Color(56 / 255f, 56 / 255f, 56 / 255f) : new Color(202 / 255f, 202 / 255f, 202 / 255f); 77 | public static Color evenRowColor = isDarkTheme ? new Color(63 / 255f, 63 / 255f, 63 / 255f) : new Color(194 / 255f, 194 / 255f, 194 / 255f); 78 | public static int itemHeight = 23; 79 | } 80 | 81 | internal static class PtModel 82 | { 83 | public static RefreshRateInfo[] RefreshRates = 84 | { 85 | new RefreshRateInfo { rate = 0.5, label = "0.5 second" }, 86 | new RefreshRateInfo { rate = 1, label = "1 second" }, 87 | new RefreshRateInfo { rate = 2, label = "2 seconds" }, 88 | new RefreshRateInfo { rate = 5, label = "5 seconds" }, 89 | new RefreshRateInfo { rate = 10, label = "10 seconds" }, 90 | new RefreshRateInfo { rate = 1000000000, label = "No refresh" } 91 | }; 92 | 93 | public static ColumnDescriptor[] ColumnDescriptors = 94 | { 95 | new ColumnDescriptor(ColumnId.Name, "Tracker"), 96 | new ColumnDescriptor(ColumnId.SampleCount, "Sample Count", 1), 97 | new ColumnDescriptor(ColumnId.Age, "Age", 2), 98 | new ColumnDescriptor(ColumnId.PeakTime, "Peak Time", 4), 99 | new ColumnDescriptor(ColumnId.AvgTime, "Average Time", 8), 100 | new ColumnDescriptor(ColumnId.LastTime, "Last Time", 16), 101 | new ColumnDescriptor(ColumnId.TotalTime, "Total Time", 32), 102 | new ColumnDescriptor(ColumnId.Actions, ""), 103 | }; 104 | 105 | public static int showAllColumns; 106 | 107 | static PtModel() 108 | { 109 | foreach (var desc in ColumnDescriptors) 110 | { 111 | showAllColumns |= desc.columnsSelectorMaskId; 112 | } 113 | } 114 | 115 | public static ColumnDescriptor GetColumnDescriptor(ColumnId id) 116 | { 117 | return ColumnDescriptors[(int)id]; 118 | } 119 | 120 | public static PtInfo[] BuildTrackerList(PtInfo[] previousTrackers, ColumnId sortBy, bool sortAsc, bool sort = true) 121 | { 122 | var trackerNames = EditorPerformanceTracker.GetAvailableTrackers(); 123 | var trackers = new PtInfo[trackerNames.Length]; 124 | for (int i = 0; i < trackerNames.Length; ++i) 125 | { 126 | var trackerName = trackerNames[i]; 127 | if (!EditorPerformanceTracker.Exists(trackerName)) 128 | continue; 129 | trackers[i].name = trackerName; 130 | trackers[i].sampleCount = EditorPerformanceTracker.GetSampleCount(trackerName); 131 | trackers[i].peakTime = EditorPerformanceTracker.GetPeakTime(trackerName); 132 | trackers[i].avgTime = EditorPerformanceTracker.GetAverageTime(trackerName); 133 | trackers[i].totalTime = EditorPerformanceTracker.GetTotalTime(trackerName); 134 | trackers[i].lastTime = EditorPerformanceTracker.GetLastTime(trackerName); 135 | trackers[i].usage = EditorPerformanceTracker.GetTotalUsage(trackerName); 136 | trackers[i].timestamp = EditorPerformanceTracker.GetTimestamp(trackerName); 137 | trackers[i].updated = false; 138 | 139 | // Tracker previous changes 140 | var pti = FindTrackerIndex(trackerName, previousTrackers); 141 | if (pti == -1) 142 | continue; 143 | 144 | var ppt = previousTrackers[pti]; 145 | trackers[i].dtSampleCount = trackers[i].sampleCount - ppt.sampleCount; 146 | trackers[i].dtPeakTime = trackers[i].peakTime - ppt.peakTime; 147 | trackers[i].dtLastTime = trackers[i].lastTime - ppt.lastTime; 148 | trackers[i].dtAvgTime = trackers[i].avgTime - ppt.avgTime; 149 | trackers[i].updated = trackers[i].dtSampleCount > 0; 150 | } 151 | 152 | if (sort) 153 | { 154 | Sort(trackers, sortBy, sortAsc); 155 | } 156 | 157 | return trackers; 158 | } 159 | 160 | public static void Sort(PtInfo[] trackers, ColumnId sortBy, bool sortAsc) 161 | { 162 | int dirm = sortAsc ? 1 : -1; 163 | Array.Sort(trackers, (x, y) => 164 | { 165 | switch (sortBy) 166 | { 167 | case ColumnId.AvgTime: return dirm * x.avgTime.CompareTo(y.avgTime); 168 | case ColumnId.SampleCount: return dirm * x.sampleCount.CompareTo(y.sampleCount); 169 | case ColumnId.PeakTime: return dirm * x.peakTime.CompareTo(y.peakTime); 170 | case ColumnId.LastTime: return dirm * x.lastTime.CompareTo(y.lastTime); 171 | case ColumnId.Age: return dirm * x.timestamp.CompareTo(y.timestamp); 172 | case ColumnId.TotalTime: return dirm * x.usage.CompareTo(y.usage); 173 | } 174 | 175 | return dirm * String.Compare(x.name, y.name, StringComparison.Ordinal); 176 | }); 177 | } 178 | 179 | public static int FindTrackerIndex(string name, PtInfo[] trackers) 180 | { 181 | for (int i = 0; i < trackers.Length; ++i) 182 | { 183 | if (trackers[i].name == name) 184 | return i; 185 | } 186 | 187 | return -1; 188 | } 189 | 190 | public static string FormatAge(PtInfo t, double currentTime) 191 | { 192 | return t.updated ? $"{ToEngineeringNotation(currentTime - t.timestamp)}s" : "---"; 193 | } 194 | 195 | public static string FormatAgeToolTip(PtInfo t) 196 | { 197 | return $"Occurred {t.timestamp} second(s) after startup"; 198 | } 199 | 200 | public static string FormatTimeChange(double time, double dt) 201 | { 202 | return $"{ToEngineeringNotation(time)}s ({ToEngineeringNotation(dt, true)}s)"; 203 | } 204 | 205 | public static string FormatTime(double time, double dt) 206 | { 207 | return $"{ToEngineeringNotation(time)}s ({ToEngineeringNotation(dt, true)}s)"; 208 | } 209 | 210 | public static string FormatTimeRate(double time, double rate) 211 | { 212 | return $"{ToEngineeringNotation(time)}s ({rate:0.00} %)"; 213 | } 214 | 215 | public static string ToEngineeringNotation(double d, bool printSign = false) 216 | { 217 | var sign = !printSign || d < 0 ? "" : "+"; 218 | if (Math.Abs(d) >= 1) 219 | return $"{sign}{d.ToString("###.0", System.Globalization.CultureInfo.InvariantCulture)}"; 220 | 221 | if (Math.Abs(d) > 0) 222 | { 223 | double exponent = Math.Log10(Math.Abs(d)); 224 | switch ((int)Math.Floor(exponent)) 225 | { 226 | case -1: 227 | case -2: 228 | case -3: 229 | return $"{sign}{(d * 1e3):###.0} m"; 230 | case -4: 231 | case -5: 232 | case -6: 233 | return $"{sign}{(d * 1e6):###.0} µ"; 234 | case -7: 235 | case -8: 236 | case -9: 237 | return $"{sign}{(d * 1e9):###.0} n"; 238 | case -10: 239 | case -11: 240 | case -12: 241 | return $"{sign}{(d * 1e12):###.0} p"; 242 | case -13: 243 | case -14: 244 | case -15: 245 | return $"{sign}{(d * 1e15):###.0} f"; 246 | case -16: 247 | case -17: 248 | case -18: 249 | return $"{sign}{(d * 1e15):###.0} a"; 250 | case -19: 251 | case -20: 252 | case -21: 253 | return $"{sign}{(d * 1e15):###.0} z"; 254 | default: 255 | return $"{sign}{(d * 1e15):###.0} y"; 256 | } 257 | } 258 | 259 | return "0"; 260 | } 261 | } 262 | } -------------------------------------------------------------------------------- /Editor/PerformanceTrackerModel.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 75d68c067163d7a4aa7e6c5826ab61c7 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/PerformanceTrackerMonitoringService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using JetBrains.Annotations; 5 | using UnityEditor; 6 | using UnityEditor.Profiling; 7 | using UnityEngine; 8 | using UnityEngine.UIElements; 9 | 10 | namespace Unity.PerformanceTracking 11 | { 12 | [Serializable] 13 | class PerformanceNotification 14 | { 15 | public string name; 16 | public bool enabled; 17 | public float threshold; 18 | } 19 | 20 | [Serializable] 21 | class PerformanceNotificationCollection 22 | { 23 | public PerformanceNotificationCollection() 24 | { 25 | items = new PerformanceNotification[0]; 26 | } 27 | 28 | public PerformanceNotification[] items; 29 | } 30 | 31 | class WindowBorderState 32 | { 33 | public WindowBorderState(VisualElement element) 34 | { 35 | bottomColor = element.style.borderBottomColor; 36 | topColor = element.style.borderTopColor; 37 | leftColor = element.style.borderLeftColor; 38 | rightColor = element.style.borderRightColor; 39 | } 40 | 41 | public static void ApplyToStyle(VisualElement element, WindowBorderState state) 42 | { 43 | element.style.borderBottomColor = state.bottomColor; 44 | element.style.borderTopColor = state.topColor; 45 | element.style.borderLeftColor = state.leftColor; 46 | element.style.borderRightColor = state.rightColor; 47 | } 48 | 49 | public static void ApplyToStyle(VisualElement element, Color color) 50 | { 51 | element.style.borderBottomColor = color; 52 | element.style.borderTopColor = color; 53 | element.style.borderLeftColor = color; 54 | element.style.borderRightColor = color; 55 | } 56 | 57 | public StyleColor bottomColor; 58 | public StyleColor topColor; 59 | public StyleColor rightColor; 60 | public StyleColor leftColor; 61 | } 62 | 63 | class SpikeWindowInfo 64 | { 65 | public SpikeWindowInfo(EditorWindow window, string trackerName, bool isInspectorElement = false, VisualElement spikeOverlayParent = null) 66 | { 67 | this.window = window; 68 | this.trackerName = trackerName; 69 | this.isInspectorElement = isInspectorElement; 70 | spikeOverlayExplicitParent = spikeOverlayParent; 71 | isInspectorWindow = window.GetType().Name == "InspectorWindow"; 72 | } 73 | 74 | public double lastAvgTime; 75 | public double lastPeakTime; 76 | public double lastTime; 77 | public int lastSampleCount; 78 | public double lastSpikeTime; 79 | 80 | public VisualElement spikeOverlay; 81 | public bool paintTriggeredByStateChanged; 82 | public string trackerName; 83 | public EditorWindow window; 84 | public bool isInspectorWindow; 85 | 86 | public bool isInspectorElement; 87 | public bool inUse; 88 | 89 | public VisualElement spikeOverlayParent 90 | { 91 | get 92 | { 93 | if (spikeOverlayExplicitParent != null) 94 | return spikeOverlayExplicitParent; 95 | 96 | if (!isInspectorElement && window && window != null && window.rootVisualElement?.parent != null) 97 | { 98 | return window.rootVisualElement.parent; 99 | } 100 | 101 | return null; 102 | } 103 | } 104 | 105 | public bool isSpiking => spikeOverlay != null; 106 | public bool supportsFading => !isInspectorElement; 107 | 108 | internal VisualElement spikeOverlayExplicitParent; 109 | } 110 | 111 | [UsedImplicitly, InitializeOnLoad] 112 | static class PerformanceTrackerMonitoringService 113 | { 114 | private static double s_NextCheck = 0; 115 | private static double s_NextCacheCleanup = 0; 116 | private const int k_SpikeBorderWidth = 2; 117 | private static Dictionary s_NotifChecks = new Dictionary(); 118 | private static Dictionary s_SpikeWindowInfos = new Dictionary(); 119 | private static Dictionary s_InspectorWindowInfos = new Dictionary(); 120 | 121 | static PerformanceTrackerMonitoringService() 122 | { 123 | EditorApplication.update -= MonitorTrackers; 124 | EditorApplication.update += MonitorTrackers; 125 | 126 | PerformanceTrackerSettings.settingsChanged -= PreferencesChanged; 127 | PerformanceTrackerSettings.settingsChanged += PreferencesChanged; 128 | 129 | Analytics.SendPerformanceTrackingEvent(Analytics.PerformanceTrackingEventType.Startup); 130 | } 131 | 132 | private static void PreferencesChanged() 133 | { 134 | if (!PerformanceTrackerSettings.spikeHighlightEnabled) 135 | { 136 | foreach (var info in s_SpikeWindowInfos.Values.ToArray()) 137 | { 138 | RemoveSpikeOverlay(info); 139 | } 140 | } 141 | 142 | if (!PerformanceTrackerSettings.spikeHighlightEnabled || !PerformanceTrackerSettings.inspectorSpikeHighlightEnabled) 143 | { 144 | foreach (var info in s_InspectorWindowInfos.Values.ToArray()) 145 | { 146 | RemoveSpikeOverlay(info); 147 | } 148 | } 149 | 150 | Analytics.SendPerformanceTrackingEvent(Analytics.PerformanceTrackingEventType.PreferenceChanges); 151 | } 152 | 153 | private static void MonitorTrackers() 154 | { 155 | if (!PerformanceTrackerSettings.monitoringNeeded) 156 | return; 157 | 158 | var now = EditorApplication.timeSinceStartup; 159 | if (now < s_NextCheck) 160 | return; 161 | 162 | var trackerNames = EditorPerformanceTracker.GetAvailableTrackers(); 163 | using (new PerformanceTracker("Tracker.PerformNotifications")) 164 | { 165 | PerformNotifications(now, trackerNames); 166 | } 167 | 168 | using (new PerformanceTracker("Tracker.PerformSpikeWindowHighlight")) 169 | { 170 | PerformSpikeWindowHighlight(now, trackerNames); 171 | } 172 | s_NextCheck = now + PerformanceTrackerSettings.monitoringUpdateSpeed; 173 | } 174 | 175 | static void PerformNotifications(double now, string[] trackerNames) 176 | { 177 | if (!PerformanceTrackerSettings.notificationEnabled) 178 | return; 179 | 180 | if (now > s_NextCacheCleanup) 181 | { 182 | s_NotifChecks.Clear(); 183 | s_NextCacheCleanup = now + 60; 184 | } 185 | 186 | var notificationNames = PerformanceTrackerSettings.notificationNames; 187 | var thresholds = PerformanceTrackerSettings.notificationThresholds; 188 | for (int i = 0; i < notificationNames.Length; ++i) 189 | { 190 | var notifName = notificationNames[i]; 191 | foreach (var trackerName in trackerNames) 192 | { 193 | if (!trackerName.Contains(notifName)) 194 | continue; 195 | 196 | var checkKey = notifName.GetHashCode(); 197 | var avgTime = EditorPerformanceTracker.GetAverageTime(trackerName); 198 | if (!s_NotifChecks.TryGetValue(checkKey, out var threshold)) 199 | threshold = thresholds[i]; 200 | if (avgTime > threshold) 201 | { 202 | s_NotifChecks[checkKey] = avgTime; 203 | var avgString = PtModel.ToEngineeringNotation(avgTime); 204 | var thresholdString = PtModel.ToEngineeringNotation(thresholds[i]); 205 | Debug.LogFormat(LogType.Warning, LogOption.NoStacktrace, null, $"{trackerName} is slower than expected ({avgString}s > {thresholdString}s)"); 206 | EditorPerformanceTracker.LogCallstack(trackerName); 207 | } 208 | } 209 | } 210 | } 211 | 212 | internal static string GetWindowPaintMarker(EditorWindow window) 213 | { 214 | var windowType = window.GetType().Name; 215 | return $"{windowType}.Paint"; 216 | } 217 | 218 | static void PerformSpikeWindowHighlight(double now, string[] trackerNames) 219 | { 220 | if (!PerformanceTrackerSettings.spikeHighlightEnabled) 221 | return; 222 | 223 | var allEditorWindows = Resources.FindObjectsOfTypeAll(); 224 | foreach (var window in allEditorWindows) 225 | { 226 | var windowMarker = GetWindowPaintMarker(window); 227 | if (EditorPerformanceTracker.Exists(windowMarker)) 228 | { 229 | if (!s_SpikeWindowInfos.TryGetValue(windowMarker, out var spikeInfo)) 230 | { 231 | spikeInfo = new SpikeWindowInfo(window, windowMarker); 232 | s_SpikeWindowInfos.Add(windowMarker, spikeInfo); 233 | } 234 | UpdateSpikeWindow(now, spikeInfo); 235 | 236 | if (spikeInfo.isInspectorWindow && PerformanceTrackerSettings.inspectorSpikeHighlightEnabled) 237 | { 238 | using (new PerformanceTracker("Tracker.UpdateInspectors")) 239 | { 240 | UpdateInspectors(now, spikeInfo); 241 | } 242 | } 243 | } 244 | else if (s_SpikeWindowInfos.TryGetValue(windowMarker, out var spikeInfo)) 245 | { 246 | RemoveSpikeOverlay(spikeInfo); 247 | } 248 | } 249 | 250 | // Window is hidden or closed: clean up all spikeOverlay 251 | var infos = s_SpikeWindowInfos.Values.ToArray(); 252 | foreach (var info in infos) 253 | { 254 | if (!info.window) 255 | { 256 | RemoveSpikeOverlay(info); 257 | } 258 | } 259 | } 260 | 261 | static void UpdateInspectors(double now, SpikeWindowInfo inspectorInfo) 262 | { 263 | var editorsList = inspectorInfo.window.rootVisualElement.Q(null, "unity-inspector-editors-list"); 264 | if (editorsList != null) 265 | { 266 | foreach (var editorElement in editorsList.Children()) 267 | { 268 | var name = editorElement.name; 269 | var markerName = $"Editor.{name}.OnInspectorGUI"; 270 | s_InspectorWindowInfos.TryGetValue(markerName, out var componentInfo); 271 | if (!EditorPerformanceTracker.Exists(markerName)) 272 | { 273 | if (componentInfo != null) 274 | componentInfo.inUse = false; 275 | continue; 276 | } 277 | 278 | if (componentInfo == null) 279 | { 280 | componentInfo = new SpikeWindowInfo(inspectorInfo.window, markerName, true, editorElement); 281 | s_InspectorWindowInfos.Add(markerName, componentInfo); 282 | } 283 | else if (editorElement != componentInfo.spikeOverlayParent) 284 | { 285 | InvalidateParent(componentInfo, editorElement); 286 | } 287 | 288 | componentInfo.inUse = true; 289 | UpdateSpikeWindow(now, componentInfo); 290 | } 291 | } 292 | 293 | var inspectorInfos = s_InspectorWindowInfos.Values.ToArray(); 294 | foreach (var info in inspectorInfos) 295 | { 296 | if (!info.inUse) 297 | { 298 | RemoveSpikeOverlay(info); 299 | } 300 | } 301 | } 302 | 303 | static void UpdateSpikeWindow(double now, SpikeWindowInfo info) 304 | { 305 | var avgTime = EditorPerformanceTracker.GetAverageTime(info.trackerName); 306 | var trackerLastTime = EditorPerformanceTracker.GetLastTime(info.trackerName); 307 | var sampleCount = EditorPerformanceTracker.GetSampleCount(info.trackerName); 308 | var peakTime = EditorPerformanceTracker.GetPeakTime(info.trackerName); 309 | var spikeDuration = info.lastSpikeTime == 0 ? 0 : now - info.lastSpikeTime; 310 | 311 | var currentTime = PerformanceTrackerSettings.spikeHighlightStrategy == SpikeHighlightStrategy.LastTime ? trackerLastTime : avgTime; 312 | var infoLastTime = PerformanceTrackerSettings.spikeHighlightStrategy == SpikeHighlightStrategy.LastTime ? info.lastTime : info.lastAvgTime; 313 | 314 | if (info.isSpiking && info.spikeOverlay.parent != info.spikeOverlayParent) 315 | { 316 | // Spike Overlay is not attached to the right window: probably a window that got docked or undocked. 317 | InvalidateParent(info); 318 | } 319 | 320 | if (!info.isSpiking && sampleCount == info.lastSampleCount) 321 | { 322 | // Nothing to do 323 | } 324 | else if (info.isSpiking && sampleCount == info.lastSampleCount) 325 | { 326 | // Check if needs to fade out 327 | if (spikeDuration > PerformanceTrackerSettings.spikeDuration) 328 | { 329 | ResetSpikeOverlay(info); 330 | } 331 | // Do not fade if we are already long to redraw 332 | else if (currentTime < PerformanceTrackerSettings.spikeCriticalThreshold && info.supportsFading) 333 | { 334 | // Try to fade out gently: 335 | var alphaFading = (float)((PerformanceTrackerSettings.spikeDuration - spikeDuration) / PerformanceTrackerSettings.spikeDuration); 336 | var color = info.spikeOverlay.style.borderBottomColor.value; 337 | color.a = alphaFading; 338 | ApplySpikeOverlay(info, color); 339 | } 340 | } 341 | else 342 | { 343 | // We just had an update of sample count: 344 | if (info.paintTriggeredByStateChanged) 345 | { 346 | // Discard this Paint event since it was caused by one of our state change 347 | info.paintTriggeredByStateChanged = false; 348 | } 349 | else if (currentTime > PerformanceTrackerSettings.spikeCriticalThreshold) 350 | { 351 | if (!info.isSpiking) 352 | { 353 | ApplySpikeOverlay(info, ComputeSpikeColor((float)currentTime, PerformanceTrackerSettings.spikeCriticalThreshold, 10.0f, PtStyles.criticalColor, Color.black)); 354 | } 355 | info.lastSpikeTime = now; 356 | } 357 | else if (currentTime > PerformanceTrackerSettings.spikeWarningThreshold) 358 | { 359 | if (!info.isSpiking) 360 | { 361 | ApplySpikeOverlay(info, ComputeSpikeColor((float)currentTime, PerformanceTrackerSettings.spikeWarningThreshold, PerformanceTrackerSettings.spikeCriticalThreshold, PtStyles.warningColor, PtStyles.criticalColor)); 362 | } 363 | info.lastSpikeTime = now; 364 | } 365 | else if (infoLastTime > PerformanceTrackerSettings.spikeWarningThreshold) 366 | { 367 | ResetSpikeOverlay(info); 368 | } 369 | } 370 | 371 | info.lastAvgTime = avgTime; 372 | info.lastTime = trackerLastTime; 373 | info.lastSampleCount = sampleCount; 374 | info.lastPeakTime = peakTime; 375 | } 376 | 377 | static Color ComputeSpikeColor(float time, float lowerTime, float upperTime, Color lowerColor, Color upperColor) 378 | { 379 | return Color.Lerp(lowerColor, upperColor, (time - lowerTime) / (upperTime - lowerTime)); 380 | } 381 | 382 | static VisualElement CreateSpikeOverlay() 383 | { 384 | var spikeOverlay = new VisualElement(); 385 | spikeOverlay.pickingMode = PickingMode.Ignore; 386 | spikeOverlay.focusable = false; 387 | spikeOverlay.style.position = Position.Absolute; 388 | spikeOverlay.style.borderBottomWidth = k_SpikeBorderWidth; 389 | spikeOverlay.style.borderTopWidth = k_SpikeBorderWidth; 390 | spikeOverlay.style.borderLeftWidth = k_SpikeBorderWidth; 391 | spikeOverlay.style.borderRightWidth = k_SpikeBorderWidth; 392 | spikeOverlay.style.height = new StyleLength(new Length(100, LengthUnit.Percent)); 393 | spikeOverlay.style.width = new StyleLength(new Length(100, LengthUnit.Percent)); 394 | spikeOverlay.AddToClassList("perf-spike-overlay"); 395 | return spikeOverlay; 396 | } 397 | 398 | static void InvalidateParent(SpikeWindowInfo info, VisualElement parent = null) 399 | { 400 | info.spikeOverlayExplicitParent = parent; 401 | // We will be processing the event so do not prevent it. 402 | ResetSpikeOverlay(info, false); 403 | } 404 | 405 | static void ApplySpikeOverlay(SpikeWindowInfo info, Color spikeColor) 406 | { 407 | var spikeOverlayParent = info.spikeOverlayParent; 408 | if (info.isInspectorElement) 409 | { 410 | if (info.spikeOverlay == null) 411 | { 412 | info.spikeOverlay = CreateSpikeOverlay(); 413 | info.spikeOverlay.name = info.trackerName; 414 | spikeOverlayParent.Add(info.spikeOverlay); 415 | } 416 | 417 | var color = spikeColor; 418 | color.a = 0.05f; 419 | info.spikeOverlay.style.backgroundColor = color; 420 | } 421 | else 422 | { 423 | if (spikeOverlayParent == null) 424 | return; 425 | 426 | if (info.spikeOverlay == null) 427 | { 428 | info.spikeOverlay = CreateSpikeOverlay(); 429 | spikeOverlayParent.Add(info.spikeOverlay); 430 | } 431 | else 432 | { 433 | // Check if it is still the last element: 434 | var childCount = spikeOverlayParent.childCount; 435 | if (childCount > 0 && spikeOverlayParent.ElementAt(childCount - 1) == info.spikeOverlay) 436 | { 437 | // All is good, perf Overlay is the last item. Nothing to do. 438 | } 439 | else 440 | { 441 | // Ensure it is last: 442 | if (spikeOverlayParent.Contains(info.spikeOverlay)) 443 | { 444 | spikeOverlayParent.Remove(info.spikeOverlay); 445 | } 446 | spikeOverlayParent.Add(info.spikeOverlay); 447 | } 448 | } 449 | WindowBorderState.ApplyToStyle(info.spikeOverlay, spikeColor); 450 | } 451 | 452 | info.paintTriggeredByStateChanged = true; 453 | if (PerformanceTrackerSettings.spikeHighlightStrategy == SpikeHighlightStrategy.AvgTime) 454 | { 455 | EditorPerformanceTracker.Reset(info.trackerName); 456 | } 457 | } 458 | 459 | static void ResetSpikeOverlay(SpikeWindowInfo info, bool ignoreNextEvent = true) 460 | { 461 | info.spikeOverlay?.RemoveFromHierarchy(); 462 | info.spikeOverlay = null; 463 | info.lastSpikeTime = 0; 464 | info.lastTime = 0; 465 | if (ignoreNextEvent) 466 | info.paintTriggeredByStateChanged = true; 467 | 468 | if (info.window) 469 | info.window.Repaint(); 470 | 471 | if (PerformanceTrackerSettings.spikeHighlightStrategy == SpikeHighlightStrategy.AvgTime) 472 | { 473 | EditorPerformanceTracker.Reset(info.trackerName); 474 | } 475 | } 476 | 477 | static void RemoveSpikeOverlay(SpikeWindowInfo info) 478 | { 479 | ResetSpikeOverlay(info); 480 | if (info.isInspectorElement) 481 | { 482 | s_InspectorWindowInfos.Remove(info.trackerName); 483 | } 484 | else 485 | { 486 | s_SpikeWindowInfos.Remove(info.trackerName); 487 | if (info.isInspectorWindow) 488 | { 489 | var inspectorInfos = s_InspectorWindowInfos.Values.ToArray(); 490 | foreach (var inspectorInfo in inspectorInfos) 491 | { 492 | RemoveSpikeOverlay(inspectorInfo); 493 | } 494 | } 495 | } 496 | } 497 | } 498 | } -------------------------------------------------------------------------------- /Editor/PerformanceTrackerMonitoringService.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ec0d7fc5d4022ae45bcb52a09c32e0d9 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/PerformanceTrackerReportUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.RegularExpressions; 6 | using UnityEditor.Profiling; 7 | using UnityEngine; 8 | 9 | namespace Unity.PerformanceTracking 10 | { 11 | [Serializable] 12 | internal struct TrackerReportOptions 13 | { 14 | public string trackerFilter; 15 | public bool showSamples; 16 | public bool showPeak; 17 | public bool showAvg; 18 | public bool showTotal; 19 | public bool sort; 20 | public ColumnId sortBy; 21 | public bool sortAsc; 22 | } 23 | 24 | [Serializable] 25 | internal struct TrackerInfo 26 | { 27 | public int samples; 28 | public float peakTime; 29 | public float avgTime; 30 | public float totalTime; 31 | public string trackerName; 32 | } 33 | 34 | [Serializable] 35 | internal struct TrackersReport 36 | { 37 | public List trackers; 38 | } 39 | 40 | internal static class PerformanceTrackerReportUtils 41 | { 42 | public static string GetTrackersReport(TrackerReportOptions options) 43 | { 44 | var trackerRx = new Regex(options.trackerFilter, RegexOptions.Compiled | RegexOptions.IgnoreCase); 45 | var trackers = PtModel.BuildTrackerList(new PtInfo[] { }, options.sortBy, options.sortAsc, options.sort); 46 | var filteredTrackers = trackers.Where(tracker => trackerRx.IsMatch(tracker.name)).ToList(); 47 | return GetTrackersReport(filteredTrackers, options); 48 | } 49 | 50 | public static string GetAllTrackersReport(TrackerReportOptions options) 51 | { 52 | options.trackerFilter = ".+"; 53 | return GetTrackersReport(options); 54 | } 55 | 56 | public static TrackersReport GenerateTrackerReport(TrackerReportOptions options) 57 | { 58 | if (string.IsNullOrEmpty(options.trackerFilter)) 59 | { 60 | options.trackerFilter = ".+"; 61 | } 62 | var trackerRx = new Regex(options.trackerFilter, RegexOptions.Compiled | RegexOptions.IgnoreCase); 63 | var trackers = PtModel.BuildTrackerList(new PtInfo[] { }, options.sortBy, options.sortAsc, options.sort); 64 | var filteredTrackers = trackers.Where(tracker => trackerRx.IsMatch(tracker.name)).ToList(); 65 | var report = new TrackersReport() 66 | { 67 | trackers = new List() 68 | }; 69 | 70 | foreach(var tracker in filteredTrackers) 71 | { 72 | TrackerInfo info = new TrackerInfo 73 | { 74 | avgTime = (float)tracker.avgTime, 75 | totalTime = (float)tracker.totalTime, 76 | samples = tracker.sampleCount, 77 | peakTime = (float)tracker.peakTime, 78 | trackerName = tracker.name 79 | }; 80 | report.trackers.Add(info); 81 | } 82 | 83 | return report; 84 | } 85 | 86 | public static string GetTrackersReport(IEnumerable trackerNames, TrackerReportOptions options) 87 | { 88 | if (!ValidateReportOptions(options)) 89 | return null; 90 | 91 | var sb = new StringBuilder(); 92 | var maxTrackerNameLength = trackerNames.Max(name => name.Length); 93 | 94 | var moreThanOneLine = false; 95 | foreach (var trackerName in trackerNames) 96 | { 97 | if (!EditorPerformanceTracker.Exists(trackerName)) 98 | continue; 99 | 100 | var shownTokens = new List(); 101 | if (options.showSamples) 102 | shownTokens.Add($"{EditorPerformanceTracker.GetSampleCount(trackerName)} samples"); 103 | if (options.showPeak) 104 | shownTokens.Add($"Peak {PtModel.ToEngineeringNotation(EditorPerformanceTracker.GetPeakTime(trackerName))}s"); 105 | if (options.showAvg) 106 | shownTokens.Add($"Avg. {PtModel.ToEngineeringNotation(EditorPerformanceTracker.GetAverageTime(trackerName))}s"); 107 | if (options.showTotal) 108 | shownTokens.Add($"Total {PtModel.ToEngineeringNotation(EditorPerformanceTracker.GetTotalTime(trackerName))}s"); 109 | 110 | if (moreThanOneLine) 111 | sb.AppendLine(); 112 | 113 | sb.AppendFormat($"{{0,{maxTrackerNameLength}}}: ", trackerName); 114 | sb.Append(string.Join(", ", shownTokens)); 115 | moreThanOneLine = true; 116 | } 117 | 118 | return sb.ToString(); 119 | } 120 | 121 | public static string GetTrackersReport(IEnumerable trackers, TrackerReportOptions options) 122 | { 123 | if (!ValidateReportOptions(options)) 124 | return null; 125 | 126 | var sb = new StringBuilder(); 127 | var maxTrackerNameLength = trackers.Max(tracker => tracker.name.Length); 128 | 129 | var moreThanOneLine = false; 130 | foreach (var tracker in trackers) 131 | { 132 | var shownTokens = new List(); 133 | if (options.showSamples) 134 | shownTokens.Add($"{tracker.sampleCount} samples"); 135 | if (options.showPeak) 136 | shownTokens.Add($"Peak {PtModel.ToEngineeringNotation(tracker.peakTime)}s"); 137 | if (options.showAvg) 138 | shownTokens.Add($"Avg. {PtModel.ToEngineeringNotation(tracker.avgTime)}s"); 139 | if (options.showTotal) 140 | shownTokens.Add($"Total {PtModel.ToEngineeringNotation(tracker.totalTime)}s"); 141 | 142 | if (moreThanOneLine) 143 | sb.AppendLine(); 144 | 145 | sb.AppendFormat($"{{0,{maxTrackerNameLength}}}: ", tracker.name); 146 | sb.Append(string.Join(", ", shownTokens)); 147 | moreThanOneLine = true; 148 | } 149 | 150 | return sb.ToString(); 151 | } 152 | 153 | private static bool ValidateReportOptions(TrackerReportOptions options) 154 | { 155 | var showSomething = options.showSamples || options.showPeak || options.showAvg || options.showTotal; 156 | if (!showSomething) 157 | { 158 | Debug.LogError("You should have at least one shown value in the report"); 159 | return false; 160 | } 161 | 162 | return true; 163 | } 164 | } 165 | } -------------------------------------------------------------------------------- /Editor/PerformanceTrackerReportUtils.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e5c2968d99f1f7c489b4b7144e1ad275 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/PerformanceTrackerSettings.cs: -------------------------------------------------------------------------------- 1 | // #define PERFORMANCE_TRACKING_DEBUG 2 | using System; 3 | using System.Linq; 4 | using System.Collections.Generic; 5 | using JetBrains.Annotations; 6 | using UnityEditor; 7 | using UnityEngine; 8 | 9 | namespace Unity.PerformanceTracking 10 | { 11 | enum SpikeHighlightStrategy 12 | { 13 | LastTime, 14 | AvgTime 15 | } 16 | 17 | static class PerformanceTrackerSettings 18 | { 19 | private const string k_KeyPrefix = "perftracking"; 20 | public const string settingsKey = "Preferences/Analysis/Performance Tracking"; 21 | 22 | public static event Action settingsChanged; 23 | public static float monitoringUpdateSpeed { get; private set; } 24 | public static bool monitoringNeeded => notificationEnabled || spikeHighlightEnabled; 25 | 26 | public static bool notificationEnabled { get; private set; } 27 | public static PerformanceNotificationCollection notifications { get; private set; } 28 | 29 | public static SpikeHighlightStrategy spikeHighlightStrategy = SpikeHighlightStrategy.AvgTime; 30 | public static bool spikeHighlightEnabled { get; private set; } 31 | public static bool inspectorSpikeHighlightEnabled { get; private set; } 32 | public static float spikeWarningThreshold { get; private set; } 33 | public static float spikeCriticalThreshold { get; private set; } 34 | public static float spikeDuration { get; private set; } 35 | 36 | public static string[] notificationNames = new string[0]; 37 | public static float[] notificationThresholds = new float[0]; 38 | 39 | private static string s_NewNotifName = ""; 40 | private static float s_NewNotifThreshold = 500f; 41 | 42 | private const float k_MinThreshold = 1f; 43 | private const float k_MaxThreshold = 5000; 44 | 45 | static class Defaults 46 | { 47 | public static bool spikeHighlightEnabled = false; 48 | public static bool notificationEnabled = false; 49 | public static bool inspectorSpikeHighlightEnabled = false; 50 | public static float monitoringUpdateSpeed = 5; 51 | public static float spikeWarningThreshold = 0.050f; 52 | public static float spikeCriticalThreshold = 0.10f; 53 | 54 | public static SpikeHighlightStrategy spikeHighlightStrategy = SpikeHighlightStrategy.AvgTime; 55 | public static float spikeDuration = 0.0f; 56 | 57 | public static PerformanceNotificationCollection GetDefaultNotifications() 58 | { 59 | var collection = new PerformanceNotificationCollection(); 60 | collection.items = new[] 61 | { 62 | new PerformanceNotification {enabled = true, name = "Application.Tick", threshold = 500}, 63 | new PerformanceNotification {enabled = true, name = "Application.Reload", threshold = 500}, 64 | }; 65 | return collection; 66 | } 67 | } 68 | 69 | static PerformanceTrackerSettings() 70 | { 71 | Load(); 72 | } 73 | 74 | public static PerformanceNotificationCollection LoadPerformanceNotificationCollection() 75 | { 76 | var notificationsJson = EditorPrefs.GetString(GetKeyName(nameof(notifications)), string.Empty); 77 | PerformanceNotificationCollection collection; 78 | if (!string.IsNullOrEmpty(notificationsJson)) 79 | collection = JsonUtility.FromJson(notificationsJson); 80 | else 81 | { 82 | collection = Defaults.GetDefaultNotifications(); 83 | } 84 | 85 | return collection; 86 | } 87 | 88 | public static void SavePerformanceNotificationCollection(PerformanceNotificationCollection collection) 89 | { 90 | EditorPrefs.SetString(GetKeyName(nameof(notifications)), JsonUtility.ToJson(collection)); 91 | } 92 | 93 | public static void AddPerformanceNotification(PerformanceNotificationCollection collection, PerformanceNotification notification) 94 | { 95 | if (Array.Find(collection.items, i => i.name == notification.name) != null) 96 | return; 97 | 98 | var newList = new List(collection.items) 99 | { 100 | notification 101 | }; 102 | collection.items = newList.ToArray(); 103 | } 104 | 105 | private static void BuildCaches() 106 | { 107 | notificationNames = notifications.items.Where(n => n.enabled).Select(n => n.name).ToArray(); 108 | notificationThresholds = notifications.items.Where(n => n.enabled).Select(n => n.threshold / 1000f).ToArray(); 109 | } 110 | 111 | private static string GetKeyName(string name) 112 | { 113 | return $"{k_KeyPrefix}.{name}"; 114 | } 115 | 116 | private static void Load() 117 | { 118 | spikeHighlightEnabled = EditorPrefs.GetBool(GetKeyName(nameof(spikeHighlightEnabled)), Defaults.spikeHighlightEnabled); 119 | notificationEnabled = EditorPrefs.GetBool(GetKeyName(nameof(notificationEnabled)), Defaults.notificationEnabled); 120 | inspectorSpikeHighlightEnabled = EditorPrefs.GetBool(GetKeyName(nameof(inspectorSpikeHighlightEnabled)), Defaults.inspectorSpikeHighlightEnabled); 121 | monitoringUpdateSpeed = EditorPrefs.GetFloat(GetKeyName(nameof(monitoringUpdateSpeed)), Defaults.monitoringUpdateSpeed); 122 | notifications = LoadPerformanceNotificationCollection(); 123 | spikeWarningThreshold = EditorPrefs.GetFloat(GetKeyName(nameof(spikeWarningThreshold)), Defaults.spikeWarningThreshold); 124 | spikeCriticalThreshold = EditorPrefs.GetFloat(GetKeyName(nameof(spikeCriticalThreshold)), Defaults.spikeCriticalThreshold); 125 | 126 | spikeHighlightStrategy = Defaults.spikeHighlightStrategy; 127 | spikeDuration = Defaults.spikeDuration; 128 | #if PERFORMANCE_TRACKING_DEBUG 129 | spikeDuration = EditorPrefs.GetFloat(GetKeyName(nameof(spikeDuration)), Defaults.spikeDuration); 130 | Enum.TryParse(EditorPrefs.GetString(GetKeyName(nameof(spikeHighlightStrategy)), Defaults.spikeHighlightStrategy.ToString()), out spikeHighlightStrategy); 131 | #endif 132 | 133 | BuildCaches(); 134 | } 135 | 136 | private static void Save() 137 | { 138 | EditorPrefs.SetBool(GetKeyName(nameof(spikeHighlightEnabled)), spikeHighlightEnabled); 139 | EditorPrefs.SetBool(GetKeyName(nameof(notificationEnabled)), notificationEnabled); 140 | EditorPrefs.SetBool(GetKeyName(nameof(inspectorSpikeHighlightEnabled)), inspectorSpikeHighlightEnabled); 141 | EditorPrefs.SetFloat(GetKeyName(nameof(monitoringUpdateSpeed)), monitoringUpdateSpeed); 142 | EditorPrefs.SetFloat(GetKeyName(nameof(spikeWarningThreshold)), spikeWarningThreshold); 143 | EditorPrefs.SetFloat(GetKeyName(nameof(spikeCriticalThreshold)), spikeCriticalThreshold); 144 | 145 | SavePerformanceNotificationCollection(notifications); 146 | 147 | #if PERFORMANCE_TRACKING_DEBUG 148 | EditorPrefs.SetFloat(GetKeyName(nameof(spikeDuration)), spikeDuration); 149 | EditorPrefs.SetString(GetKeyName(nameof(spikeHighlightStrategy)), spikeHighlightStrategy.ToString()); 150 | #endif 151 | 152 | BuildCaches(); 153 | 154 | settingsChanged?.Invoke(); 155 | } 156 | 157 | private static void ResetSettings() 158 | { 159 | spikeHighlightEnabled = Defaults.spikeHighlightEnabled; 160 | notificationEnabled = Defaults.notificationEnabled; 161 | inspectorSpikeHighlightEnabled = Defaults.inspectorSpikeHighlightEnabled; 162 | monitoringUpdateSpeed = Defaults.monitoringUpdateSpeed; 163 | notifications = Defaults.GetDefaultNotifications(); 164 | spikeWarningThreshold = Defaults.spikeWarningThreshold; 165 | spikeCriticalThreshold = Defaults.spikeCriticalThreshold; 166 | 167 | spikeHighlightStrategy = Defaults.spikeHighlightStrategy; 168 | spikeDuration = Defaults.spikeDuration; 169 | } 170 | 171 | [UsedImplicitly, SettingsProvider] 172 | private static SettingsProvider CreateSettings() 173 | { 174 | return new SettingsProvider(settingsKey, SettingsScope.User) 175 | { 176 | keywords = SettingsProvider.GetSearchKeywordsFromGUIContentProperties(), 177 | activateHandler = (title, rootElement) => Load(), 178 | guiHandler = searchContext => 179 | { 180 | GUILayout.BeginHorizontal(); 181 | { 182 | GUILayout.Space(10); 183 | GUILayout.BeginVertical(GUILayout.Width(515)); 184 | { 185 | GUILayout.Space(10); 186 | var oldWidth = EditorGUIUtility.labelWidth; 187 | EditorGUIUtility.labelWidth = 250; 188 | using (var _ = new EditorGUI.ChangeCheckScope()) 189 | { 190 | GUILayout.BeginHorizontal(); 191 | { 192 | monitoringUpdateSpeed = DrawSlider(Contents.updateSpeedContent.text, monitoringUpdateSpeed, 0.1f, 10); 193 | } 194 | GUILayout.EndHorizontal(); 195 | GUILayout.Space(30); 196 | 197 | notificationEnabled = EditorGUILayout.Toggle(Contents.enablePerformanceTrackingNotificationContent, notificationEnabled); 198 | using (new EditorGUI.DisabledScope(!notificationEnabled)) 199 | { 200 | DrawNotifications(); 201 | DrawNewNotificationFields(); 202 | } 203 | 204 | GUILayout.Space(30); 205 | spikeHighlightEnabled = EditorGUILayout.Toggle(Contents.enableSpikeHighlighContent, spikeHighlightEnabled); 206 | using (new EditorGUI.DisabledScope(!spikeHighlightEnabled)) 207 | { 208 | DrawSpikeHighlightControls(); 209 | } 210 | 211 | if (_.changed) 212 | Save(); 213 | } 214 | 215 | GUILayout.Space(10); 216 | if (GUILayout.Button(Contents.resetSettings, GUILayout.Width(100))) 217 | { 218 | ResetSettings(); 219 | } 220 | 221 | EditorGUIUtility.labelWidth = oldWidth; 222 | } 223 | GUILayout.EndVertical(); 224 | } 225 | GUILayout.EndHorizontal(); 226 | } 227 | }; 228 | } 229 | 230 | 231 | 232 | private static void DrawNewNotificationFields() 233 | { 234 | EditorGUILayout.LabelField("Add New", EditorStyles.largeLabel); 235 | GUILayout.Space(4); 236 | GUILayout.BeginHorizontal(); 237 | { 238 | GUILayout.Space(41); 239 | s_NewNotifThreshold = EditorGUILayout.Slider(s_NewNotifThreshold, k_MinThreshold, k_MaxThreshold, Styles.sliderLayoutOption); 240 | s_NewNotifName = GUILayout.TextField(s_NewNotifName, 64, Styles.nameLayoutOption); 241 | if (GUILayout.Button(Contents.notifAddNewContent, GUILayout.Width(100)) && !string.IsNullOrEmpty(s_NewNotifName)) 242 | { 243 | AddPerformanceNotification(notifications, new PerformanceNotification { enabled = true, name = s_NewNotifName, threshold = s_NewNotifThreshold }); 244 | s_NewNotifName = ""; 245 | Save(); 246 | } 247 | } 248 | GUILayout.EndHorizontal(); 249 | } 250 | 251 | private static void DrawSpikeHighlightControls() 252 | { 253 | inspectorSpikeHighlightEnabled = EditorGUILayout.Toggle(Contents.enableInspectorSpikeHighlighContent, inspectorSpikeHighlightEnabled); 254 | #if PERFORMANCE_TRACKING_DEBUG 255 | spikeHighlightStrategy = (SpikeHighlightStrategy)EditorGUILayout.EnumPopup(new GUIContent("Spike triggers on"), spikeHighlightStrategy); 256 | spikeDuration = DrawSlider("Spike Duration (seconds)", spikeDuration, 0, 10); 257 | #endif 258 | spikeWarningThreshold = DrawSlider($"Warning threshold (milliseconds)", spikeWarningThreshold * 1000, 1, 50) / 1000; 259 | spikeCriticalThreshold = DrawSlider($"Critical threshold (milliseconds)", spikeCriticalThreshold * 1000, spikeWarningThreshold * 1000, 100) / 1000; 260 | } 261 | 262 | private static void DrawNotifications() 263 | { 264 | EditorGUILayout.LabelField("Notifications (threshold in milliseconds)", EditorStyles.largeLabel); 265 | GUILayout.Space(4); 266 | foreach (var p in notifications.items) 267 | { 268 | GUILayout.BeginHorizontal(); 269 | { 270 | GUILayout.Space(20); 271 | 272 | p.enabled = GUILayout.Toggle(p.enabled, Contents.notifEnabledContent, GUILayout.Width(20)); 273 | 274 | using (new EditorGUI.DisabledGroupScope(!p.enabled)) 275 | { 276 | p.threshold = EditorGUILayout.Slider(p.threshold, k_MinThreshold, k_MaxThreshold, Styles.sliderLayoutOption); 277 | p.name = GUILayout.TextField(p.name, 64, Styles.nameLayoutOption); 278 | if (GUILayout.Button(Contents.notifDeleteContent, GUILayout.Width(100))) 279 | { 280 | notifications.items = notifications.items.Where(n => n != p).ToArray(); 281 | Save(); 282 | GUIUtility.ExitGUI(); 283 | } 284 | } 285 | } 286 | GUILayout.EndHorizontal(); 287 | } 288 | } 289 | 290 | private static float DrawSlider(string label, float value, float leftValue, float rightValue) 291 | { 292 | GUILayout.BeginHorizontal(); 293 | GUILayout.Label(label, Styles.rtfLabel, GUILayout.Width(EditorGUIUtility.labelWidth)); 294 | GUILayout.Space(20); 295 | GUILayout.FlexibleSpace(); 296 | var newValue = EditorGUILayout.Slider(value, leftValue, rightValue); 297 | GUILayout.EndHorizontal(); 298 | return newValue; 299 | } 300 | 301 | static class Styles 302 | { 303 | public static GUIStyle rtfLabel = new GUIStyle(EditorStyles.label) 304 | { 305 | richText = true 306 | }; 307 | 308 | public static readonly GUILayoutOption sliderLayoutOption = GUILayout.Width(140); 309 | public static readonly GUILayoutOption nameLayoutOption = GUILayout.Width(250); 310 | } 311 | 312 | class Contents 313 | { 314 | public static GUIContent resetSettings = new GUIContent("Reset Settings", "Reset all performance tracking settings to defaults."); 315 | public static GUIContent updateSpeedContent = new GUIContent("Monitoring Update Speed (seconds)"); 316 | public static GUIContent notifEnabledContent = new GUIContent("", "Enable or disable this performance tracking notification."); 317 | public static GUIContent notifAddNewContent = new GUIContent("Add", "Add a new notifications"); 318 | public static GUIContent notifDeleteContent = new GUIContent("Remove", "Remove this notification"); 319 | public static GUIContent enablePerformanceTrackingNotificationContent = new GUIContent( 320 | "Enable performance notifications", 321 | "If performance tracking notifications are enabled, the system will log in the console trackers that exceed a given threshold."); 322 | public static GUIContent enableSpikeHighlighContent = new GUIContent( 323 | "Enable spike window highlight", 324 | "Window border will be highlighted to show when a repaint caused an editor spike."); 325 | public static GUIContent enableInspectorSpikeHighlighContent = new GUIContent( 326 | "Enable inspector components highlight", 327 | "Inspector component will be highlighted to show when a repaint caused an editor spike."); 328 | } 329 | } 330 | } -------------------------------------------------------------------------------- /Editor/PerformanceTrackerSettings.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0374bbf5778338542b268f04cb155acc 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/PerformanceTrackerWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 45ed03ced8f010048bfff6118f2f89d4 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/PerformanceTrackingAnalytics.cs: -------------------------------------------------------------------------------- 1 | // #define PERFORMANCE_TRACKING_ANALYTICS_LOGGING 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Threading; 6 | using System.Linq; 7 | using UnityEditor; 8 | using UnityEngine; 9 | using UnityEngine.Analytics; 10 | 11 | namespace Unity.PerformanceTracking 12 | { 13 | public static class Analytics 14 | { 15 | [Serializable] 16 | internal class PerformanceLogEvent 17 | { 18 | private DateTime m_StartTime; 19 | internal long elapsedTimeMs => (long)(DateTime.Now - m_StartTime).TotalMilliseconds; 20 | public long duration; 21 | public TrackersReport report; 22 | public PerformanceLogEvent() 23 | { 24 | m_StartTime = DateTime.Now; 25 | } 26 | 27 | public void Done() 28 | { 29 | if (duration == 0) 30 | duration = elapsedTimeMs; 31 | } 32 | } 33 | 34 | public enum PerformanceTrackingEventType 35 | { 36 | PreferenceChanges, 37 | Startup, 38 | Shutdown 39 | } 40 | 41 | [Serializable] 42 | internal class PerformanceTrackingEvent 43 | { 44 | public float monitoringUpdateSpeed; 45 | public bool monitoringNeeded; 46 | public bool notificationEnabled; 47 | public PerformanceNotification[] notifications; 48 | public SpikeHighlightStrategy spikeHighlightStrategy; 49 | public bool spikeHighlightEnabled; 50 | public float spikeWarningThreshold; 51 | public float spikeCriticalThreshold; 52 | public float spikeDuration; 53 | 54 | public PerformanceTrackingEventType trackingEventType; 55 | 56 | public PerformanceTrackingEvent() 57 | { 58 | monitoringUpdateSpeed = PerformanceTrackerSettings.monitoringUpdateSpeed; 59 | monitoringNeeded = PerformanceTrackerSettings.monitoringNeeded; 60 | notifications = PerformanceTrackerSettings.notifications.items; 61 | notificationEnabled = PerformanceTrackerSettings.notificationEnabled; 62 | spikeHighlightStrategy = PerformanceTrackerSettings.spikeHighlightStrategy; 63 | spikeHighlightEnabled = PerformanceTrackerSettings.spikeHighlightEnabled; 64 | spikeWarningThreshold = PerformanceTrackerSettings.spikeWarningThreshold; 65 | spikeCriticalThreshold = PerformanceTrackerSettings.spikeCriticalThreshold; 66 | spikeDuration = PerformanceTrackerSettings.spikeDuration; 67 | } 68 | } 69 | 70 | public enum WindowUsageType 71 | { 72 | Startup, 73 | Sort, 74 | ChangeColumnLayout, 75 | ChangeMonitoringSpeed, 76 | ChangeSortBy, 77 | Shutdown, 78 | AddPin, 79 | RemovePin, 80 | OpenPreferences, 81 | ResetSampleCount, 82 | SetSearchFieldAction 83 | } 84 | 85 | [Serializable] 86 | internal class PerformanceWindowUsageEvent 87 | { 88 | public string[] pinMarkers; 89 | public string filter; 90 | public float updateSpeed; 91 | public int columns; 92 | public ColumnId sortBy; 93 | public bool sortAsc; 94 | public WindowUsageType usageType; 95 | public string markerName; 96 | } 97 | 98 | public enum ActionSource 99 | { 100 | PerformanceTrackerWindow, 101 | EditorWindowMenu, 102 | MainMenu, 103 | Scripting, 104 | OpenAsset, 105 | PreferencePage 106 | } 107 | 108 | public enum ActionType 109 | { 110 | Profile, 111 | DeedProfile, 112 | LogCallstack, 113 | AddNotification, 114 | LogPerformanceBug, 115 | OpenProfilerOnMarker, 116 | OpenProfilerData, 117 | OpenPerformanceTrackerViewer 118 | } 119 | 120 | [Serializable] 121 | internal class PerformanceActionEvent 122 | { 123 | public ActionType actionType; 124 | public string trackerName; 125 | public ActionSource actionSource; 126 | 127 | public PerformanceActionEvent(ActionType actionType, string trackerName = null, ActionSource source = ActionSource.Scripting) 128 | { 129 | this.actionType = actionType; 130 | this.trackerName = trackerName; 131 | actionSource = source; 132 | } 133 | } 134 | 135 | internal static string Version; 136 | private static bool s_Registered; 137 | 138 | static Analytics() 139 | { 140 | Version = Utils.GetPerformanceTrackingVersion(); 141 | 142 | EditorApplication.quitting += Quit; 143 | } 144 | 145 | enum EventName 146 | { 147 | PerformanceWindowUsageEvent, 148 | PerformanceLogEvent, 149 | PerformanceActionEvent, 150 | PerformanceTrackingEvent 151 | } 152 | 153 | internal static void SendPerformanceWindowUsageEvent(PerformanceWindowUsageEvent evt) 154 | { 155 | Send(EventName.PerformanceWindowUsageEvent, evt); 156 | } 157 | 158 | internal static void SendPerformanceLogEvent() 159 | { 160 | PerformanceLogEvent evt = new PerformanceLogEvent(); 161 | var options = new TrackerReportOptions(); 162 | options.showSamples = true; 163 | options.showPeak = true; 164 | options.showAvg = true; 165 | options.showTotal = true; 166 | options.sort = true; 167 | options.sortBy = ColumnId.AvgTime; 168 | options.sortAsc = false; 169 | evt.report = PerformanceTrackerReportUtils.GenerateTrackerReport(options); 170 | evt.Done(); 171 | 172 | Send(EventName.PerformanceLogEvent, evt); 173 | } 174 | 175 | internal static void SendPerformanceActionEvent(PerformanceActionEvent evt) 176 | { 177 | Send(EventName.PerformanceActionEvent, evt); 178 | } 179 | 180 | internal static void SendPerformanceTrackingEvent(PerformanceTrackingEventType eventType) 181 | { 182 | Send(EventName.PerformanceTrackingEvent, new PerformanceTrackingEvent() { trackingEventType = eventType }); 183 | } 184 | 185 | private static void Quit() 186 | { 187 | SendPerformanceLogEvent(); 188 | SendPerformanceTrackingEvent(PerformanceTrackingEventType.Shutdown); 189 | } 190 | 191 | private static bool RegisterEvents() 192 | { 193 | if (UnityEditorInternal.InternalEditorUtility.inBatchMode) 194 | { 195 | return false; 196 | } 197 | 198 | if (!EditorAnalytics.enabled) 199 | { 200 | Console.WriteLine("[Performance Tracking] Editor analytics are disabled"); 201 | return false; 202 | } 203 | 204 | if (s_Registered) 205 | { 206 | return true; 207 | } 208 | 209 | var allNames = Enum.GetNames(typeof(EventName)); 210 | if (allNames.Any(eventName => !RegisterEvent(eventName))) 211 | { 212 | return false; 213 | } 214 | 215 | s_Registered = true; 216 | return true; 217 | } 218 | 219 | private static bool RegisterEvent(string eventName) 220 | { 221 | const string vendorKey = "unity.scene-template"; 222 | var result = EditorAnalytics.RegisterEventWithLimit(eventName, 100, 1000, vendorKey); 223 | switch (result) 224 | { 225 | case AnalyticsResult.Ok: 226 | { 227 | #if PERFORMANCE_TRACKING_ANALYTICS_LOGGING 228 | Debug.Log($"SceneTemplate: Registered event: {eventName}"); 229 | #endif 230 | return true; 231 | } 232 | case AnalyticsResult.TooManyRequests: 233 | // this is fine - event registration survives domain reload (native) 234 | return true; 235 | default: 236 | { 237 | Console.WriteLine($"[ST] Failed to register analytics event '{eventName}'. Result: '{result}'"); 238 | return false; 239 | } 240 | } 241 | } 242 | 243 | private static void Send(EventName eventName, object eventData) 244 | { 245 | #if PERFORMANCE_TRACKING_ANALYTICS_LOGGING 246 | #else 247 | if (Utils.IsDeveloperMode()) 248 | return; 249 | #endif 250 | 251 | if (!RegisterEvents()) 252 | { 253 | #if PERFORMANCE_TRACKING_ANALYTICS_LOGGING 254 | Console.WriteLine($"[ST] Analytics disabled: event='{eventName}', time='{DateTime.Now:HH:mm:ss}', payload={EditorJsonUtility.ToJson(eventData, true)}"); 255 | #endif 256 | return; 257 | } 258 | try 259 | { 260 | var result = EditorAnalytics.SendEventWithLimit(eventName.ToString(), eventData); 261 | if (result == AnalyticsResult.Ok) 262 | { 263 | #if PERFORMANCE_TRACKING_ANALYTICS_LOGGING 264 | Console.WriteLine($"[ST] Event='{eventName}', time='{DateTime.Now:HH:mm:ss}', payload={EditorJsonUtility.ToJson(eventData, true)}"); 265 | #endif 266 | } 267 | else 268 | { 269 | Console.WriteLine($"[ST] Failed to send event {eventName}. Result: {result}"); 270 | } 271 | } 272 | catch (Exception) 273 | { 274 | // ignored 275 | } 276 | } 277 | 278 | #if PERFORMANCE_TRACKING_ANALYTICS_LOGGING 279 | [MenuItem("Tools/Single PerformanceLog Events", false, 180005)] 280 | static void SinglePerformanceLogEvents() 281 | { 282 | SendPerformanceLogEvent(); 283 | } 284 | 285 | [MenuItem("Tools/Spam PerformanceLog Events", false, 180005)] 286 | static void SpamPerformanceLogEvents() 287 | { 288 | for(var i = 0; i < 25; ++i) 289 | { 290 | SendPerformanceLogEvent(); 291 | Debug.Log($"Send Log event: {i}"); 292 | Thread.Sleep(500); 293 | } 294 | } 295 | 296 | static T RandomValue(T[] values) 297 | { 298 | var index = UnityEngine.Random.Range(0, values.Length - 1); 299 | return values[index]; 300 | } 301 | 302 | static T RandomEnum() where T : struct, IConvertible 303 | { 304 | var values = Enum.GetValues(typeof(T)); 305 | var indexOfLastElement = values.Length - 1; 306 | var index = UnityEngine.Random.Range(0, indexOfLastElement); 307 | return (T)values.GetValue(index); 308 | } 309 | 310 | static bool RandomBool() 311 | { 312 | var index = UnityEngine.Random.Range(0, 1); 313 | return index == 0; 314 | } 315 | 316 | static int[] kShowColumns = new [] 317 | { 318 | 1, 319 | 16, 320 | 15, 321 | 11 322 | }; 323 | 324 | static string[] kSearchFields = new string[] 325 | { 326 | ".Paint", ".OnInspectorGUI", null, "Editor.", "Scene" 327 | }; 328 | 329 | static string[][] kPins = new[] 330 | { 331 | new [] { "WebView.Tick" }, 332 | new [] { "WebView.Tick", "UnityConnect.Tick" }, 333 | new [] { "WebView.Tick", "UnityConnect.Tick", "WinEditorMain" }, 334 | new string [0] 335 | }; 336 | 337 | [MenuItem("Tools/Spam Performance WindowUsage Events", false, 180005)] 338 | static void SpamPerformanceWindowUsageEvents() 339 | { 340 | var trackers = PtModel.BuildTrackerList(new PtInfo[] { }, ColumnId.AvgTime, true, true); 341 | 342 | for (var i = 0; i < 25; ++i) 343 | { 344 | var evt = new PerformanceWindowUsageEvent() 345 | { 346 | columns = RandomValue(kShowColumns), 347 | sortBy = RandomEnum(), 348 | sortAsc = RandomBool(), 349 | filter = RandomValue(kSearchFields), 350 | pinMarkers = RandomValue(kPins), 351 | updateSpeed = (float)RandomValue(PtModel.RefreshRates).rate, 352 | usageType = RandomEnum(), 353 | markerName = RandomValue(trackers).name 354 | }; 355 | 356 | SendPerformanceWindowUsageEvent(evt); 357 | Debug.Log($"Send windowUsage event: {i}"); 358 | Thread.Sleep(500); 359 | } 360 | } 361 | 362 | [MenuItem("Tools/Spam PerformanceAction Events", false, 180005)] 363 | static void SpamPerformanceActionEvents() 364 | { 365 | var trackers = PtModel.BuildTrackerList(new PtInfo[] { }, ColumnId.AvgTime, true, true); 366 | for (var i = 0; i < 25; ++i) 367 | { 368 | var evt = new PerformanceActionEvent( 369 | RandomEnum(), 370 | RandomValue(trackers).name, 371 | RandomEnum() 372 | ); 373 | 374 | SendPerformanceActionEvent(evt); 375 | Debug.Log($"Send action event: {i}"); 376 | Thread.Sleep(500); 377 | } 378 | } 379 | 380 | [MenuItem("Tools/Spam PerformanceTracking Events", false, 180005)] 381 | static void SpamPerformanceTrackingEvents() 382 | { 383 | for (var i = 0; i < 25; ++i) 384 | { 385 | SendPerformanceTrackingEvent(RandomEnum()); 386 | Debug.Log($"Send tracking event: {i}"); 387 | Thread.Sleep(500); 388 | } 389 | } 390 | #endif 391 | } 392 | 393 | } -------------------------------------------------------------------------------- /Editor/PerformanceTrackingAnalytics.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d5819527e9611fe4b8263abc05c3d137 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ProfilerHelpers.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Globalization; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Runtime.CompilerServices; 10 | using UnityEditor; 11 | using UnityEditor.Callbacks; 12 | using UnityEditor.IMGUI.Controls; 13 | using UnityEditor.Profiling; 14 | using UnityEditorInternal; 15 | using UnityEngine.Profiling; 16 | using Debug = UnityEngine.Debug; 17 | using UnityEditorInternal.Profiling; 18 | 19 | #if UNITY_2021_1_OR_NEWER 20 | using CPUModule = UnityEditorInternal.Profiling.CPUOrGPUProfilerModule; 21 | #else 22 | using CPUModule = UnityEditorInternal.Profiling.CPUorGPUProfilerModule; 23 | #endif 24 | 25 | [assembly: InternalsVisibleTo("Unity.PerformanceTracking.Editor.Tests")] 26 | 27 | namespace Unity.PerformanceTracking 28 | { 29 | public static class ProfilerHelpers 30 | { 31 | public const string k_DefaultProfileSaveDirectory = "Assets/Profiles"; 32 | private const string kProfilerEnabledSessionKey = "ProfilerEnabled"; 33 | private static string m_MarkerFilter; 34 | 35 | static ProfilerHelpers() 36 | { 37 | Init(); 38 | } 39 | 40 | public static event Action profileEnabledChanged; 41 | 42 | public static void ToggleProfilerRecording(bool deepProfiler) 43 | { 44 | if (!ProfilerDriver.enabled) 45 | { 46 | StartProfilerRecording(true, deepProfiler); 47 | } 48 | else 49 | { 50 | var currentMarkerFilter = GetCurrentMarkerFilter(); 51 | StopProfilerRecordingAndCreateReport(string.IsNullOrEmpty(currentMarkerFilter) ? "ToggleProfile" : currentMarkerFilter); 52 | } 53 | } 54 | 55 | public static bool RecordWithProfileSample(string sampleName, Action toBenchmark, bool editorProfile = true, bool deepProfile = true, int count = 1, Action recordingDone = null) 56 | { 57 | return StartProfilerRecording(editorProfile, deepProfile, () => 58 | { 59 | for (var i = 0; i < count; ++i) 60 | { 61 | Profiler.BeginSample(sampleName); 62 | toBenchmark(); 63 | Profiler.EndSample(); 64 | } 65 | StopProfilerRecordingAndCreateReport("", sampleName, recordingDone); 66 | }); 67 | } 68 | 69 | public static bool RecordWithMarkerFilter(string markerName, Action toBenchmark, bool editorProfile = true, bool deepProfile = true, int count = 1, Action recordingDone = null) 70 | { 71 | if (!SupportsMarkerFiltering()) 72 | return false; 73 | 74 | #if UNITY_2020_1_OR_NEWER 75 | if (!EditorPerformanceTracker.Exists(markerName)) 76 | { 77 | // Create Marker once so Profiler.SetMarkerFiltering will work 78 | using (new EditorPerformanceTracker(markerName)) 79 | { 80 | 81 | } 82 | } 83 | #endif 84 | 85 | return StartProfilerRecording(markerName, editorProfile, deepProfile, () => 86 | { 87 | for (var i = 0; i < count; ++i) 88 | toBenchmark(); 89 | StopProfilerRecordingAndCreateReport(markerName, "", recordingDone); 90 | }); 91 | } 92 | 93 | public static bool StartProfilerRecording(bool editorProfile, bool deepProfile, Action onProfileEnabled = null) 94 | { 95 | return StartProfilerRecording("", editorProfile, deepProfile, onProfileEnabled); 96 | } 97 | 98 | public static bool StartProfilerRecording(string markerFilter, bool editorProfile, bool deepProfile, Action onProfileEnabled = null) 99 | { 100 | if (ProfilerDriver.deepProfiling != deepProfile) 101 | { 102 | if (deepProfile) 103 | Debug.LogWarning("Enabling deep profiling. Domain reload will occur. Please restart Profiling."); 104 | else 105 | Debug.LogWarning("Disabling deep profiling. Domain reload will occur. Please restart Profiling."); 106 | 107 | SetProfilerDeepProfile(deepProfile); 108 | return false; 109 | } 110 | 111 | var editorProfileStr = editorProfile ? "editor" : "playmode"; 112 | var deepProfileStr = deepProfile ? " - deep profile" : ""; 113 | var hasMarkerFilter = !string.IsNullOrEmpty(markerFilter) && SupportsMarkerFiltering(); 114 | var markerStr = hasMarkerFilter ? $"- MarkerFilter: {markerFilter}" : ""; 115 | Debug.Log($"Start profiler recording: {editorProfileStr} {deepProfileStr} {markerStr}..."); 116 | 117 | EnableProfiler(false); 118 | 119 | EditorApplication.delayCall += () => 120 | { 121 | ProfilerDriver.ClearAllFrames(); 122 | ProfilerDriver.profileEditor = editorProfile; 123 | ProfilerDriver.deepProfiling = deepProfile; 124 | if (hasMarkerFilter) 125 | { 126 | SetMarkerFiltering(markerFilter); 127 | } 128 | 129 | EditorApplication.delayCall += () => 130 | { 131 | EnableProfiler(true); 132 | if (onProfileEnabled != null) 133 | { 134 | EditorApplication.delayCall += () => 135 | { 136 | onProfileEnabled(); 137 | }; 138 | } 139 | }; 140 | }; 141 | 142 | return true; 143 | } 144 | 145 | public static void StopProfilerRecording(Action toProfilerStopped = null) 146 | { 147 | if (SupportsMarkerFiltering()) 148 | SetMarkerFiltering(""); 149 | 150 | EnableProfiler(false); 151 | Debug.Log($"Stop profiler recording."); 152 | 153 | if (toProfilerStopped != null) 154 | { 155 | EditorApplication.delayCall += () => 156 | { 157 | toProfilerStopped(); 158 | }; 159 | }; 160 | } 161 | 162 | public static void StopProfilerRecordingAndOpenProfiler(string searchString = "", Action onProfilerOpened = null) 163 | { 164 | StopProfilerRecording(() => 165 | { 166 | OpenProfiler(searchString, onProfilerOpened); 167 | }); 168 | } 169 | 170 | public static void StopProfilerRecordingAndCreateReport(string profileTitle, string searchString = "", Action onReportOpened = null) 171 | { 172 | StopProfilerRecording(() => 173 | { 174 | var profileSaveFilePath = SaveProfileReport(profileTitle); 175 | OpenProfileReport(profileSaveFilePath, searchString, onReportOpened); 176 | }); 177 | } 178 | 179 | public static string SaveProfileReport(string reportTitle) 180 | { 181 | if (!Directory.Exists(k_DefaultProfileSaveDirectory)) 182 | Directory.CreateDirectory(k_DefaultProfileSaveDirectory); 183 | var timeId = DateTime.Now.ToString("s").Replace(":", "_"); 184 | var formattedId = reportTitle.ToLowerInvariant().Replace(".", "_"); 185 | if (ProfilerDriver.deepProfiling) 186 | formattedId += "_deep"; 187 | var profileSaveFilePath = $"{k_DefaultProfileSaveDirectory}/{formattedId}_{timeId}.profile.data".Replace("\\", "/"); 188 | ProfilerDriver.SaveProfile(profileSaveFilePath); 189 | AssetDatabase.ImportAsset(profileSaveFilePath); 190 | Debug.Log("Saved profiling trace at " + profileSaveFilePath + ""); 191 | return profileSaveFilePath; 192 | } 193 | 194 | public static void OpenProfiler(string searchString = "", Action onOpenProfiler = null) 195 | { 196 | var profilerWindow = OpenProfilerWindow(); 197 | EditorApplication.delayCall += () => 198 | { 199 | EditorApplication.delayCall += () => 200 | { 201 | if (!string.IsNullOrEmpty(searchString)) 202 | { 203 | SetSearchField(profilerWindow, searchString); 204 | profilerWindow.Repaint(); 205 | } 206 | onOpenProfiler?.Invoke(profilerWindow); 207 | }; 208 | }; 209 | } 210 | 211 | public static void OpenProfileReport(string profileDataPath, string searchString = "", Action onProfileReportLoaded = null) 212 | { 213 | var profilerWindow = OpenProfilerWindow(); 214 | EditorApplication.delayCall += () => 215 | { 216 | EnableProfiler(false); 217 | ProfilerDriver.ClearAllFrames(); 218 | if (ProfilerDriver.LoadProfile(profileDataPath, false)) 219 | { 220 | profilerWindow.titleContent.text = System.IO.Path.GetFileNameWithoutExtension(profileDataPath); 221 | } 222 | 223 | SwitchToCPUView(profilerWindow); 224 | profilerWindow.Repaint(); 225 | if (!string.IsNullOrEmpty(searchString)) 226 | { 227 | EditorApplication.delayCall += () => 228 | { 229 | // Wait till switch to CPU before setting the search field. 230 | SetSearchField(profilerWindow, searchString); 231 | Debug.Log("Profiler report ready"); 232 | profilerWindow.Repaint(); 233 | }; 234 | } 235 | onProfileReportLoaded?.Invoke(profilerWindow); 236 | }; 237 | } 238 | 239 | [UsedImplicitly, OnOpenAsset(1)] 240 | private static bool OpenProfileData(int instanceID, int line) 241 | { 242 | var assetPath = AssetDatabase.GetAssetPath(EditorUtility.InstanceIDToObject(instanceID)); 243 | if (System.IO.Path.GetExtension(assetPath) != ".data") 244 | return false; 245 | 246 | OpenProfileReport(assetPath); 247 | return true; 248 | } 249 | 250 | public struct BenchmarkResult 251 | { 252 | public int sampleCount; 253 | public double avgInMs; 254 | public double totalInSecond; 255 | public double peakInMs; 256 | public double minInMs; 257 | public double medianInMs; 258 | } 259 | 260 | public static BenchmarkResult BenchmarkMarker(string markerName, Action action, int count = 100) 261 | { 262 | var peak = 0.0; 263 | var min = 100000000.0; 264 | var samples = new double[count]; 265 | 266 | EditorPerformanceTracker.Reset(markerName); 267 | var sampleCount = EditorPerformanceTracker.GetSampleCount(markerName); 268 | for (var i = 0; i < count; ++i) 269 | { 270 | action(); 271 | sampleCount = EditorPerformanceTracker.GetSampleCount(markerName); 272 | 273 | var elapsed = EditorPerformanceTracker.GetLastTime(markerName) * 1000.0; 274 | samples[i] = elapsed; 275 | if (elapsed > peak) 276 | { 277 | peak = elapsed; 278 | } 279 | if (elapsed < min) 280 | { 281 | min = elapsed; 282 | } 283 | } 284 | 285 | Array.Sort(samples); 286 | 287 | var result = new BenchmarkResult(); 288 | result.sampleCount = count; 289 | result.avgInMs = EditorPerformanceTracker.GetAverageTime(markerName) * 1000.0; 290 | result.totalInSecond = EditorPerformanceTracker.GetTotalTime(markerName); 291 | result.peakInMs = peak; 292 | result.minInMs = min; 293 | result.medianInMs = samples[count / 2]; 294 | return result; 295 | } 296 | 297 | public static BenchmarkResult Benchmark(Action action, int count = 100) 298 | { 299 | var clock = new System.Diagnostics.Stopwatch(); 300 | var peak = 0L; 301 | var total = 0L; 302 | var min = 1000000L; 303 | var samples = new long[count]; 304 | 305 | for (var i = 0; i < count; ++i) 306 | { 307 | clock.Start(); 308 | action(); 309 | clock.Stop(); 310 | 311 | var elapsed = clock.ElapsedTicks; 312 | samples[i] = elapsed; 313 | if (elapsed > peak) 314 | { 315 | peak = elapsed; 316 | } 317 | if (elapsed < min) 318 | { 319 | min = elapsed; 320 | } 321 | 322 | total += elapsed; 323 | clock.Reset(); 324 | } 325 | 326 | var secondsPerTick = 1.0 / Stopwatch.Frequency; 327 | var msPerTick = 1000.0 / Stopwatch.Frequency; 328 | 329 | var avg = (double)(total) / count; 330 | Array.Sort(samples); 331 | 332 | var result = new BenchmarkResult(); 333 | result.sampleCount = count; 334 | result.avgInMs = avg * msPerTick; 335 | result.totalInSecond = total * secondsPerTick; 336 | result.peakInMs = peak * msPerTick; 337 | result.minInMs = min * msPerTick; 338 | result.medianInMs = samples[count / 2] * msPerTick; 339 | return result; 340 | } 341 | 342 | #region Menu 343 | [MenuItem("Edit/Toggle Profiler Recording #E")] 344 | public static void ToggleProfilerRecording() 345 | { 346 | ToggleProfilerRecording(ProfilerDriver.deepProfiling); 347 | } 348 | #endregion 349 | 350 | #region CompatibilityNoInternalAccess 351 | 352 | static Assembly m_UnityEditorAssembly; 353 | static System.Type[] m_Types; 354 | static System.Type m_ProfilerWindowType; 355 | 356 | internal static void EnableProfiler(bool enable) 357 | { 358 | ProfilerDriver.enabled = enable; 359 | SessionState.SetBool(kProfilerEnabledSessionKey, enable); 360 | profileEnabledChanged?.Invoke(enable); 361 | } 362 | 363 | internal static void Init() 364 | { 365 | m_UnityEditorAssembly = typeof(Selection).Assembly; 366 | m_Types = m_UnityEditorAssembly.GetTypes().ToArray(); 367 | m_ProfilerWindowType = GetUnityEditorType("ProfilerWindow"); 368 | } 369 | 370 | internal static EditorWindow OpenProfilerWindow() 371 | { 372 | var profilerWindow = EditorWindow.CreateWindow(); 373 | SwitchToCPUView(profilerWindow); 374 | profilerWindow.Show(); 375 | return profilerWindow; 376 | } 377 | 378 | #if UNITY_2019_3 379 | internal static void GetCpuModule(EditorWindow profiler, out System.Type cpuModuleType, out object cpuModule) 380 | { 381 | var m_ProfilerModulesField = m_ProfilerWindowType.GetField("m_ProfilerModules", BindingFlags.Instance | BindingFlags.NonPublic); 382 | var m_ProfilerModules = m_ProfilerModulesField.GetValue(profiler) as Array; 383 | cpuModule = m_ProfilerModules.GetValue((int)ProfilerArea.CPU); 384 | cpuModuleType = GetUnityEditorType("CPUorGPUProfilerModule"); 385 | } 386 | #endif 387 | 388 | internal static void SetProfilerDeepProfile(bool deepProfile) 389 | { 390 | ProfilerWindow.SetEditorDeepProfiling(deepProfile); 391 | } 392 | 393 | internal static void RequestScriptReload() 394 | { 395 | EditorUtility.RequestScriptReload(); 396 | } 397 | 398 | internal static void SetRecordingEnabled(EditorWindow profiler, bool enableRecording) 399 | { 400 | ((ProfilerWindow)profiler).SetRecordingEnabled(enableRecording); 401 | } 402 | 403 | internal static bool SupportsMarkerFiltering() 404 | { 405 | return true; 406 | } 407 | 408 | internal static string GetCurrentMarkerFilter() 409 | { 410 | return m_MarkerFilter; 411 | } 412 | 413 | internal static void SetMarkerFiltering(string markerName) 414 | { 415 | if (SupportsMarkerFiltering()) 416 | m_MarkerFilter = markerName; 417 | 418 | ProfilerDriver.SetMarkerFiltering(markerName); 419 | } 420 | 421 | internal static void SwitchToCPUView(EditorWindow profilerWindow) 422 | { 423 | var profiler = (ProfilerWindow)profilerWindow; 424 | var cpuProfilerModule = profiler.GetProfilerModule(ProfilerArea.CPU); 425 | cpuProfilerModule.ViewType = ProfilerViewType.Hierarchy; 426 | } 427 | 428 | internal static void SetSearchField(EditorWindow profilerWindow, string searchString) 429 | { 430 | #if UNITY_2021_1_OR_NEWER 431 | var profiler = (ProfilerWindow)profilerWindow; 432 | var cpuModule = profiler.GetProfilerModule(ProfilerArea.CPU); 433 | cpuModule.ClearSelection(); 434 | if (!string.IsNullOrEmpty(searchString)) 435 | cpuModule.FrameDataHierarchyView.treeView.searchString = searchString; 436 | #else 437 | var profiler = (ProfilerWindow)profilerWindow; 438 | var cpuModule = profiler.GetProfilerModule(ProfilerArea.CPU); 439 | cpuModule.FrameDataHierarchyView.SetSelectionFromLegacyPropertyPath(""); 440 | if (!String.IsNullOrEmpty(searchString)) 441 | cpuModule.FrameDataHierarchyView.treeView.searchString = searchString; 442 | #endif 443 | } 444 | 445 | internal static void SetTreeViewSearchString(System.Type typeOwningTreeView, object objOwningTreeView, string searchString) 446 | { 447 | var treeViewField = typeOwningTreeView.GetProperty("treeView", BindingFlags.Instance | BindingFlags.Public); 448 | var treeView = treeViewField.GetValue(objOwningTreeView) as TreeView; 449 | treeView.searchString = searchString; 450 | } 451 | 452 | public static System.Type GetUnityEditorType(string typeName) 453 | { 454 | return m_Types.First(t => t.Name == typeName); 455 | } 456 | 457 | public static IEnumerable GetUnityEditorTypesImplementing(System.Type baseType) 458 | { 459 | var assemblies = AppDomain.CurrentDomain.GetAssemblies(); 460 | return assemblies.SelectMany(a => a.GetTypes()).Where(t => t.IsSubclassOf(baseType)); 461 | } 462 | 463 | #endregion 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /Editor/ProfilerHelpers.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 680b37d1669d30641b6abf40bde9a2bf 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ProfilingActions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using JetBrains.Annotations; 5 | using UnityEditor; 6 | using UnityEngine; 7 | 8 | namespace Unity.PerformanceTracking 9 | { 10 | public static class ProfilingActions 11 | { 12 | [UsedImplicitly, ProfilingAction("Profile")] 13 | static void ProfileSnippet(object preExecutePayload, ProfilingSnippet snippet, ProfilingSnippetOptions options) 14 | { 15 | ProfileSnippet(preExecutePayload, snippet, options, false); 16 | } 17 | 18 | [UsedImplicitly, ProfilingAction("Profile - Deep")] 19 | static void ProfileSnippetDeep(object preExecutePayload, ProfilingSnippet snippet, ProfilingSnippetOptions options) 20 | { 21 | ProfileSnippet(preExecutePayload, snippet, options, true); 22 | } 23 | 24 | static void ProfileSnippet(object preExecutePayload, ProfilingSnippet snippet, ProfilingSnippetOptions options, bool deepProfile) 25 | { 26 | var s = snippet.GetSnippetAction(preExecutePayload, options); 27 | ProfilerHelpers.RecordWithProfileSample(snippet.sampleName, s, true, deepProfile, options.count); 28 | } 29 | 30 | [UsedImplicitly, ProfilingAction("Profile Marker")] 31 | static void ProfileMarker(object preExecutePayload, ProfilingSnippet snippet, ProfilingSnippetOptions options) 32 | { 33 | ProfileSnippetMarker(preExecutePayload, snippet, options, false); 34 | } 35 | 36 | [UsedImplicitly, ProfilingAction("Profile Marker Deep")] 37 | static void ProfileMarkerDeep(object preExecutePayload, ProfilingSnippet snippet, ProfilingSnippetOptions options) 38 | { 39 | ProfileSnippetMarker(preExecutePayload, snippet, options, true); 40 | } 41 | 42 | static void ProfileSnippetMarker(object preExecutePayload, ProfilingSnippet snippet, ProfilingSnippetOptions options, bool deepProfile) 43 | { 44 | var markerName = snippet.GetValidMarkerName(); 45 | var action = ProfilingSnippetUtils.GetMarkerEnclosedSnippetAction(preExecutePayload, snippet, options); 46 | ProfilerHelpers.RecordWithMarkerFilter(markerName, action, true, deepProfile, options.count); 47 | } 48 | 49 | [UsedImplicitly, ProfilingAction("Benchmark")] 50 | static void BenchmarkSnippet(object preExecutePayload, ProfilingSnippet snippet, ProfilingSnippetOptions options) 51 | { 52 | var snippetAction = snippet.GetSnippetAction(preExecutePayload, options); 53 | #if UNITY_2020_1_OR_NEWER 54 | if (!snippet.ranOnce && options.warmup) 55 | { 56 | snippetAction(); // This execution pass should JIT the code first. 57 | EditorApplication.CallDelayed(() => { 58 | var result = ProfilerHelpers.Benchmark(snippetAction, options.count); 59 | LogBenchmark(snippet.label, result, options); 60 | }, 0); 61 | } 62 | else 63 | #endif 64 | { 65 | var result = ProfilerHelpers.Benchmark(snippetAction, options.count); 66 | LogBenchmark(snippet.label, result, options); 67 | } 68 | } 69 | 70 | [UsedImplicitly, ProfilingAction("Benchmark Marker")] 71 | static void BenchmarMarker(object preExecutePayload, ProfilingSnippet snippet, ProfilingSnippetOptions options) 72 | { 73 | var markerName = snippet.GetValidMarkerName(); 74 | var action = ProfilingSnippetUtils.GetMarkerEnclosedSnippetAction(preExecutePayload, snippet, options); 75 | var result = ProfilerHelpers.BenchmarkMarker(markerName, action, options.count); 76 | LogBenchmark($"Benchmark marker {markerName}", result, options); 77 | } 78 | 79 | private static void LogBenchmark(string title, ProfilerHelpers.BenchmarkResult result, ProfilingSnippetOptions options) 80 | { 81 | Debug.Log(ProfilingSnippetUtils.FormatBenchmarkResult(title, result)); 82 | options.Log(ProfilingSnippetUtils.FormatBenchmarkResult(title, result, options.csvLog)); 83 | } 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /Editor/ProfilingActions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 124788d7a53ef8b4eaff34105dfc56b8 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ProfilingSnippetListView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEditor.IMGUI.Controls; 5 | using UnityEngine; 6 | 7 | namespace Unity.PerformanceTracking 8 | { 9 | public class SnippetItem : TreeViewItem 10 | { 11 | public int index; 12 | } 13 | 14 | public class SnippetListView : TreeView 15 | { 16 | IList m_Model; 17 | 18 | public event Action doubleClicked; 19 | 20 | public SnippetListView(IList model) 21 | : base(new TreeViewState(), new MultiColumnHeader(CreateDefaultMultiColumnHeaderState())) 22 | { 23 | showAlternatingRowBackgrounds = true; 24 | showBorder = true; 25 | m_Model = model; 26 | 27 | Reload(); 28 | } 29 | 30 | protected override void DoubleClickedItem(int id) 31 | { 32 | var item = FindItem(id, this.rootItem); 33 | var snippetItem = item as SnippetItem; 34 | var snippet = m_Model[snippetItem.index]; 35 | doubleClicked?.Invoke(snippet); 36 | } 37 | 38 | protected override bool DoesItemMatchSearch(TreeViewItem item, string search) 39 | { 40 | var snippetItem = item as SnippetItem; 41 | var snippet = m_Model[snippetItem.index]; 42 | 43 | if (snippet.category != null && snippet.category.IndexOf(search, StringComparison.OrdinalIgnoreCase) != -1) 44 | return true; 45 | 46 | if (snippet.label != null && snippet.label.IndexOf(search, StringComparison.OrdinalIgnoreCase) != -1) 47 | return true; 48 | 49 | if (snippet.sampleName != null && snippet.sampleName.IndexOf(search, StringComparison.OrdinalIgnoreCase) != -1) 50 | return true; 51 | 52 | if (snippet.markerName != null && snippet.markerName.IndexOf(search, StringComparison.OrdinalIgnoreCase) != -1) 53 | return true; 54 | 55 | return false; 56 | } 57 | 58 | protected override TreeViewItem BuildRoot() 59 | { 60 | var root = new SnippetItem { id = 0, index = -1, depth = -1, displayName = "Root" }; 61 | var index = 0; 62 | var allItems = m_Model.Select(snippet => new SnippetItem { id = snippet.id, index = index++, depth = 0, displayName = $"{snippet.label}" }).Cast().ToList(); 63 | 64 | // Utility method that initializes the TreeViewItem.children and .parent for all items. 65 | SetupParentsAndChildrenFromDepths(root, allItems); 66 | 67 | // Return root of the tree 68 | return root; 69 | } 70 | 71 | public bool IsFirstItemSelected() 72 | { 73 | var selection = GetSelection(); 74 | if (selection.Count == 0) 75 | return false; 76 | 77 | var allRows = GetRows(); 78 | if (allRows.Count == 0) 79 | return false; 80 | var selectedItems = FindRows(selection); 81 | return allRows[0] == selectedItems[0]; 82 | } 83 | 84 | protected override void RowGUI(RowGUIArgs args) 85 | { 86 | var item = args.item; 87 | for (int i = 0; i < args.GetNumVisibleColumns(); ++i) 88 | { 89 | CellGUI(args.GetCellRect(i), item, args.GetColumn(i), ref args); 90 | } 91 | } 92 | 93 | void CellGUI(Rect cellRect, TreeViewItem item, int column, ref RowGUIArgs args) 94 | { 95 | // Center cell rect vertically (makes it easier to place controls, icons etc in the cells) 96 | CenterRectUsingSingleLineHeight(ref cellRect); 97 | 98 | var snippetItem = item as SnippetItem; 99 | var snippet = m_Model[snippetItem.index]; 100 | switch (column) 101 | { 102 | case 0: 103 | { 104 | DefaultGUI.Label(cellRect, snippet.category, args.selected, args.focused); 105 | } 106 | break; 107 | case 1: 108 | { 109 | DefaultGUI.Label(cellRect, snippet.label, args.selected, args.focused); 110 | } 111 | break; 112 | } 113 | } 114 | 115 | public void ResizeColumn(float listViewWidth) 116 | { 117 | multiColumnHeader.state.columns[1].width = listViewWidth - 70 - 25; 118 | } 119 | 120 | static MultiColumnHeaderState CreateDefaultMultiColumnHeaderState() 121 | { 122 | var columns = new[] 123 | { 124 | new MultiColumnHeaderState.Column 125 | { 126 | headerContent = new GUIContent("Category"), 127 | headerTextAlignment = TextAlignment.Left, 128 | canSort = false, 129 | width = 70, 130 | minWidth = 60, 131 | autoResize = false, 132 | allowToggleVisibility = false 133 | }, 134 | new MultiColumnHeaderState.Column 135 | { 136 | headerContent = new GUIContent("Name"), 137 | headerTextAlignment = TextAlignment.Left, 138 | canSort = false, 139 | sortingArrowAlignment = TextAlignment.Left, 140 | width = 400, 141 | minWidth = 100, 142 | autoResize = true 143 | } 144 | }; 145 | 146 | var state = new MultiColumnHeaderState(columns); 147 | return state; 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /Editor/ProfilingSnippetListView.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9ffaee1ad4e9505488bdf85e61b04e27 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ProfilingSnippetUtils.cs: -------------------------------------------------------------------------------- 1 | // #define SNIPPET_DEBUG 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Reflection; 8 | using UnityEditor; 9 | using UnityEngine; 10 | using UnityEditor.Profiling; 11 | 12 | namespace Unity.PerformanceTracking 13 | { 14 | public static class ProfilingSnippetUtils 15 | { 16 | static MethodInfo m_RepaintImmediatly; 17 | static object[] m_NoArgs = new object[0]; 18 | 19 | static ProfilingSnippetUtils() 20 | { 21 | m_RepaintImmediatly = typeof(EditorWindow).GetMethod("RepaintImmediately", BindingFlags.Instance | BindingFlags.NonPublic); 22 | } 23 | 24 | public static void RepaintImmediatlyWindow(EditorWindow window) 25 | { 26 | m_RepaintImmediatly.Invoke(window, m_NoArgs); 27 | } 28 | 29 | public static string FormatBenchmarkResult(string title, ProfilerHelpers.BenchmarkResult result, bool csv = false) 30 | { 31 | if (csv) 32 | return $"{result.sampleCount},{result.totalInSecond},{result.avgInMs},{result.medianInMs},{result.peakInMs},{result.minInMs}"; 33 | else 34 | return $"{title} samples: {result.sampleCount} - total: {FormatNumber(result.totalInSecond)}s - avg: {FormatNumber(result.avgInMs)}ms - median: {FormatNumber(result.medianInMs)}ms - peak: {FormatNumber(result.peakInMs)}ms - min: {FormatNumber(result.minInMs)}ms"; 35 | } 36 | 37 | static string FormatNumber(double d) 38 | { 39 | return d.ToString("F4"); 40 | } 41 | 42 | public static IEnumerator OpenStandaloneWindowMaximized(string viewTypeName) 43 | { 44 | var windowType = ProfilerHelpers.GetUnityEditorType(viewTypeName); 45 | var window = EditorWindow.GetWindow(windowType); 46 | if (window) 47 | window.Close(); 48 | yield return null; 49 | window = EditorWindow.GetWindow(windowType); 50 | yield return null; 51 | } 52 | 53 | public static EditorWindow OpenStandaloneWindow(string viewTypeName) 54 | { 55 | var windowType = ProfilerHelpers.GetUnityEditorType(viewTypeName); 56 | return OpenStandaloneWindow(windowType); 57 | } 58 | 59 | public static EditorWindow OpenStandaloneWindow(System.Type windowType) 60 | { 61 | var window = EditorWindow.GetWindow(windowType); 62 | if (window) 63 | window.Close(); 64 | return EditorWindow.GetWindow(windowType); 65 | } 66 | 67 | public static void MaximizeWindow(EditorWindow window) 68 | { 69 | var m_ParentField = window.GetType().GetField("m_Parent", BindingFlags.Instance | BindingFlags.NonPublic); 70 | var parent = m_ParentField.GetValue(window); 71 | var windowProperty = parent.GetType().GetProperty("window", BindingFlags.Instance | BindingFlags.Public); 72 | var containerWindow = windowProperty.GetValue(parent); 73 | var ToggleMaximizeMethod = containerWindow.GetType().GetMethod("ToggleMaximize", BindingFlags.Instance | BindingFlags.Public); 74 | ToggleMaximizeMethod.Invoke(containerWindow, new object[0]); 75 | } 76 | 77 | public static EditorWindow SetupWindow(System.Type editorWindowType, ProfilingSnippetOptions options) 78 | { 79 | EditorWindow window = null; 80 | window = options.standaloneWindow ? OpenStandaloneWindow(editorWindowType) : EditorWindow.GetWindow(editorWindowType); 81 | 82 | if (options.maximizeWindow) 83 | { 84 | MaximizeWindow(window); 85 | } 86 | 87 | return window; 88 | } 89 | 90 | static ProfilingSnippet[] m_AllSnippets; 91 | 92 | public static ProfilingSnippet[] FetchAllSnippets() 93 | { 94 | if (m_AllSnippets == null) 95 | { 96 | var repaintSnippets = ProfilerHelpers.GetUnityEditorTypesImplementing(typeof(EditorWindow)).OrderBy(t => t.Name) 97 | .Select(type => CreateSnippetFromEditorWindowType(type)); 98 | 99 | bool isDevBuild = UnityEditor.Unsupported.IsDeveloperBuild(); 100 | var staticMethods = AppDomain.CurrentDomain.GetAllStaticMethods(isDevBuild); 101 | var staticSnippets = staticMethods 102 | .Select(mi => CreateSnippetFromStaticMethod(mi)); 103 | 104 | var menuSnippets = GetMenuSnippets(); 105 | 106 | var profilingSnippetActions = Utils.GetAllMethodsWithAttribute().Select(methodInfo => 107 | { 108 | return CreateSnippetFromProfilingSnippetActionAttr(methodInfo); 109 | }); 110 | 111 | var profilingSnippets = Utils.GetAllMethodsWithAttribute().Select(methodInfo => 112 | { 113 | return CreateSnippetFromProfilingSnippetAttr(methodInfo); 114 | }); 115 | 116 | 117 | m_AllSnippets = repaintSnippets 118 | .Concat(staticSnippets) 119 | .Concat(menuSnippets) 120 | .Concat(profilingSnippetActions) 121 | .Concat(profilingSnippets) 122 | .Where(snippet => snippet != null) 123 | .ToArray(); 124 | } 125 | 126 | return m_AllSnippets; 127 | } 128 | 129 | #if SNIPPET_DEBUG 130 | [MenuItem("ProfilerHelpers/Refresh Snippets")] 131 | public static void RefreshSnippets() 132 | { 133 | m_AllSnippets = null; 134 | FetchAllSnippets(); 135 | } 136 | #endif 137 | 138 | public static ProfilingSnippet CreateSnippetFromEditorWindowType(System.Type editorWindowType) 139 | { 140 | var newSnippet = new ProfilingSnippet($"repaint_{editorWindowType.FullName}", $"{editorWindowType.Name} ({editorWindowType.Namespace})"); 141 | newSnippet.category = "Repaint"; 142 | newSnippet.preExecuteFunc = (snippet, options) => SetupWindow(editorWindowType, options); 143 | newSnippet.executeFunc = (editorWindowObj, snippet, options) => 144 | { 145 | var editorWindow = editorWindowObj as EditorWindow; 146 | RepaintImmediatlyWindow(editorWindow); 147 | }; 148 | newSnippet.sampleName = $"{editorWindowType.Name}_Paint"; 149 | newSnippet.markerName = $"{editorWindowType.Name}.Paint"; 150 | 151 | return newSnippet; 152 | } 153 | 154 | static Dictionary staticCache = new Dictionary(); 155 | 156 | static IEnumerable GetMenuSnippets() 157 | { 158 | var outItemNames = new List(); 159 | var outItemDefaultShortcuts = new List(); 160 | Assembly assembly = typeof(Menu).Assembly; 161 | var managerType = assembly.GetTypes().First(t => t.Name == "Menu"); 162 | var method = managerType.GetMethod("GetMenuItemDefaultShortcuts", BindingFlags.NonPublic | BindingFlags.Static); 163 | var arguments = new object[] { outItemNames, outItemDefaultShortcuts }; 164 | method.Invoke(null, arguments); 165 | 166 | return outItemNames.Select(menuItemName => 167 | { 168 | var snippet = new ProfilingSnippet($"menu_{menuItemName}", menuItemName, menuItemName.Replace("/", "_")); 169 | snippet.category = "Menu"; 170 | snippet.executeFunc = (preExecutePayload, s, options) => 171 | { 172 | EditorApplication.ExecuteMenuItem(s.label); 173 | }; 174 | 175 | return snippet; 176 | }); 177 | } 178 | 179 | public static ProfilingSnippet CreateSnippetFromStaticMethod(MethodInfo mi) 180 | { 181 | var fullName = $"{mi.Name} ({mi.DeclaringType.FullName})"; 182 | var newSnippet = new ProfilingSnippet($"{mi.DeclaringType.FullName}_{mi.Name}_{mi.GetHashCode()}", fullName); 183 | newSnippet.category = "Static"; 184 | newSnippet.executeFunc = (dummy, snippet, options) => 185 | { 186 | mi.Invoke(null, null); 187 | }; 188 | newSnippet.sampleName = $"static_{mi.Name}"; 189 | 190 | return newSnippet; 191 | } 192 | 193 | const string kSignatureMismatch = "Signature mismatch: [ProfilingSnippetAction] should be void MySnippet(object preExecutePayload, ProfilingSnippet snippet, ProfilingSnippetOptions options) or void MySnippet()"; 194 | 195 | public static ProfilingSnippet CreateSnippetFromProfilingSnippetActionAttr(MethodInfo mi) 196 | { 197 | var attr = mi.GetCustomAttributes(typeof(ProfilingSnippetActionAttribute), false).Cast().First(); 198 | var snippet = new ProfilingSnippet(attr.idStr ?? mi.Name, attr.label, attr.sampleName, attr.markerName); 199 | try 200 | { 201 | snippet.category = attr.category; 202 | var inputParams = mi.GetParameters(); 203 | if (mi.GetParameters().Length == 3) 204 | { 205 | snippet.executeFunc = Delegate.CreateDelegate(typeof(Action), mi) as 206 | Action; 207 | } 208 | else if (mi.GetParameters().Length == 0) 209 | { 210 | var noParamAction = Delegate.CreateDelegate(typeof(Action), mi) as Action; 211 | snippet.executeFunc = (payload, _snippet, options) => noParamAction(); 212 | } 213 | else 214 | { 215 | Debug.LogError($"Error while creating ProfilingSnippet {snippet.label}. {kSignatureMismatch}"); 216 | return null; 217 | } 218 | } 219 | catch (Exception ex) 220 | { 221 | Debug.LogError($"Error while creating ProfilingSnippet {snippet.label}. {kSignatureMismatch} - {ex.Message}"); 222 | return null; 223 | } 224 | return snippet; 225 | } 226 | 227 | public static ProfilingSnippet CreateSnippetFromProfilingSnippetAttr(MethodInfo mi) 228 | { 229 | var attr = mi.GetCustomAttributes(typeof(ProfilingSnippetAttribute), false).Cast().First(); 230 | try 231 | { 232 | var snippet = mi.Invoke(null, null) as ProfilingSnippet; 233 | return snippet; 234 | } 235 | catch (Exception ex) 236 | { 237 | Debug.LogError($"Error while creating ProfilingSnippet: {ex.Message}"); 238 | return null; 239 | } 240 | 241 | } 242 | 243 | public static Action GetMarkerEnclosedSnippetAction(object preExecutePayload, ProfilingSnippet snippet, ProfilingSnippetOptions options) 244 | { 245 | var snippetAction = snippet.GetSnippetAction(preExecutePayload, options); 246 | if (!snippet.isValidMarker) 247 | { 248 | // Generate marker: 249 | var markerName = snippet.GetValidMarkerName(); 250 | return () => 251 | { 252 | using (new PerformanceTracker(markerName)) 253 | { 254 | snippetAction(); 255 | } 256 | }; 257 | } 258 | 259 | return snippetAction; 260 | } 261 | 262 | #region TestProfilingSnippetAttribute 263 | static void DoSomethingLong() 264 | { 265 | var str = ""; 266 | for (var i = 0; i < 100; ++i) 267 | { 268 | str += GUID.Generate().ToString(); 269 | } 270 | } 271 | 272 | [ProfilingSnippetAction("Test_something_long_id", "Test something long", "Test", "Test_something_long_sample")] 273 | static void TestProfilingSnippetActionAttr(object preExecutePayload, ProfilingSnippet snippet, ProfilingSnippetOptions options) 274 | { 275 | DoSomethingLong(); 276 | } 277 | 278 | [ProfilingSnippetAction("Test_something_long_no_param_id", "Test something long (no param)", "Test")] 279 | static void TestProfilingSnippetActionAttrNoParam() 280 | { 281 | DoSomethingLong(); 282 | } 283 | 284 | [ProfilingSnippet] 285 | static ProfilingSnippet TestProfilingSnippetAttr() 286 | { 287 | var snippet = new ProfilingSnippet("Test_something_else_long_id", "Test something else long"); 288 | snippet.category = "Test"; 289 | snippet.sampleName = "Test_something_else_long_sample"; 290 | snippet.executeFunc = (preExecutePayload, s, options) => 291 | { 292 | DoSomethingLong(); 293 | }; 294 | return snippet; 295 | } 296 | #endregion 297 | } 298 | 299 | public class ProfilingAction 300 | { 301 | public string name; 302 | public Func action; 303 | public bool defaultSet; 304 | } 305 | 306 | public class ProfilingActionAttribute : Attribute 307 | { 308 | public string name; 309 | public ProfilingActionAttribute(string name) 310 | { 311 | this.name = name; 312 | } 313 | } 314 | 315 | public class ProfilingSnippet 316 | { 317 | string m_SampleName; 318 | string m_Category; 319 | 320 | public ProfilingSnippet(string idStr, string label = null, string sampleName = null, string markerName = null) 321 | { 322 | this.idStr = idStr; 323 | this.label = label ?? idStr; 324 | this.sampleName = sampleName; 325 | this.markerName = markerName; 326 | } 327 | 328 | public string label; 329 | public string idStr; 330 | public int id => idStr.GetHashCode(); 331 | 332 | internal bool ranOnce { get; set; } 333 | 334 | public string category 335 | { 336 | get 337 | { 338 | if (string.IsNullOrEmpty(m_Category)) 339 | { 340 | return "General"; 341 | } 342 | 343 | return m_Category; 344 | } 345 | set => m_Category = value; 346 | } 347 | 348 | public string sampleName 349 | { 350 | get 351 | { 352 | if (string.IsNullOrEmpty(m_SampleName)) 353 | { 354 | return label.Replace(" ", "_"); 355 | } 356 | 357 | return m_SampleName; 358 | } 359 | set => m_SampleName = value; 360 | } 361 | 362 | public string markerName; 363 | public bool isValidMarker => !string.IsNullOrEmpty(markerName); 364 | 365 | public string GetValidMarkerName() 366 | { 367 | return isValidMarker ? markerName : sampleName; 368 | } 369 | 370 | public Func preExecuteFunc; 371 | public Action executeFunc; 372 | public Action postExecuteFunc; 373 | 374 | public object PreExecute(ProfilingSnippetOptions options) 375 | { 376 | return preExecuteFunc?.Invoke(this, options); 377 | } 378 | 379 | public Action GetSnippetAction(object preExecutePayload, ProfilingSnippetOptions options) 380 | { 381 | return () => { executeFunc(preExecutePayload, this, options); ranOnce = true; }; 382 | } 383 | 384 | public void PostExecute(object executePayload, ProfilingSnippetOptions options) 385 | { 386 | postExecuteFunc?.Invoke(executePayload, this, options); 387 | } 388 | 389 | public override int GetHashCode() 390 | { 391 | return id; 392 | } 393 | } 394 | 395 | public class ProfilingSnippetAttribute : Attribute 396 | { 397 | public ProfilingSnippetAttribute() 398 | { 399 | } 400 | } 401 | 402 | public class ProfilingSnippetActionAttribute : Attribute 403 | { 404 | public string idStr; 405 | public string label; 406 | public string sampleName; 407 | public string markerName; 408 | public string category; 409 | public ProfilingSnippetActionAttribute(string idStr = null, string label = null, string category = "User", string sampleName = null, string markerName = null) 410 | { 411 | this.idStr = idStr; 412 | this.label = label; 413 | this.category = category; 414 | this.sampleName = sampleName; 415 | this.markerName = markerName; 416 | } 417 | } 418 | 419 | public class ProfilingSnippetOptions 420 | { 421 | public int count; 422 | public bool maximizeWindow; 423 | public bool standaloneWindow; 424 | public bool warmup; 425 | public bool csvLog; 426 | public string logFile; 427 | 428 | public void Log(string str) 429 | { 430 | using (var sw = File.AppendText(logFile)) 431 | { 432 | sw.WriteLine(str); 433 | } 434 | } 435 | } 436 | } -------------------------------------------------------------------------------- /Editor/ProfilingSnippetUtils.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 71aeb7435cfde27428d87bb2c52a8707 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ProfilingSnippetWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: aa7fe656d1901ff4f9a409f457612eb3 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/StyleSheets.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1a78cfcb924bc5e41bf82720722f2e2b 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/StyleSheets/PerformanceTrackerWindow.uss: -------------------------------------------------------------------------------- 1 | .perf-toolbar, .perf-tracker-list { 2 | margin-left: 3px; 3 | margin-right: 0px; 4 | } 5 | 6 | .perf-tracker-name { 7 | flex-grow: 1; 8 | flex-shrink: 1; 9 | min-width: 200px; 10 | overflow: hidden; 11 | } 12 | 13 | /* Make the tracker name textfield look like label */ 14 | .perf-tracker-name > .unity-text-field__input { 15 | background-color: rgba(0,0,0,0); 16 | border-left-width: 0px; 17 | border-right-width: 0px; 18 | border-top-width: 0px; 19 | border-bottom-width: 0px; 20 | } 21 | 22 | .perf-age { 23 | width: 80px; 24 | } 25 | 26 | .perf-actions { 27 | width: 30px; 28 | -unity-text-align: middle-center; 29 | } 30 | 31 | .perf-sample-count { 32 | width: 115px; 33 | } 34 | 35 | .perf-avg-time, .perf-peak-time, .perf-last-time, .perf-total-time { 36 | width: 130px; 37 | } 38 | 39 | .perf-tracker-name, .perf-age, .perf-avg-time, .perf-peak-time, .perf-last-time, .perf-total-time { 40 | -unity-text-align: middle-left; 41 | } 42 | 43 | .perf-tracker-name, .perf-age, .perf-avg-time, .perf-peak-time, .perf-last-time, .perf-total-time, .perf-actions, .perf-sample-count { 44 | margin-left: 3px; 45 | margin-right: 3px; 46 | } 47 | 48 | .perf-header-row-container { 49 | margin-left: 19px; 50 | } 51 | 52 | .perf-header { 53 | font-size: 14px; 54 | -unity-font-style: bold; 55 | height: 25px; 56 | -unity-text-align: middle-left; 57 | } 58 | 59 | .perf-selector-label { 60 | -unity-text-align: middle-center; 61 | } 62 | 63 | .perf-search-box { 64 | flex-grow: 1; 65 | min-width: 300px; 66 | } 67 | 68 | .perf-update-speed-selector { 69 | width: 100px; 70 | } 71 | 72 | .perf-columns-selector { 73 | width: 100px; 74 | } 75 | 76 | .perf-sort-by-selector { 77 | width: 100px; 78 | } 79 | 80 | .perf-stop-profiling-button { 81 | margin-left: 6px; 82 | margin-right: 3px; 83 | } 84 | 85 | .perf-tracker-toolbar-button { 86 | margin-left: 6px; 87 | margin-right: 140px; 88 | padding-top: 3px; 89 | } 90 | 91 | .perf-pin { 92 | width: 16px; 93 | flex-shrink: 0; 94 | flex-grow: 0; 95 | background-image: none; 96 | -unity-background-scale-mode: scale-to-fit; 97 | } 98 | 99 | .perf-pin-on { 100 | background-image: resource("d_Favorite"); 101 | -unity-background-image-tint-color: #fdc008; 102 | } 103 | 104 | .perf-row:hover > .perf-pin-off { 105 | background-image: resource("Favorite"); 106 | } -------------------------------------------------------------------------------- /Editor/StyleSheets/PerformanceTrackerWindow.uss.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 087d8c5f21ac7824cbace9432564cb0d 3 | ScriptedImporter: 4 | fileIDToRecycleName: 5 | 11400000: stylesheet 6 | externalObjects: {} 7 | userData: 8 | assetBundleName: 9 | assetBundleVariant: 10 | script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} 11 | -------------------------------------------------------------------------------- /Editor/Unity.PerformanceTracking.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unity.PerformanceTracking.Editor", 3 | "references": [ 4 | ], 5 | "includePlatforms": [ 6 | "Editor" 7 | ], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": false, 10 | "overrideReferences": false, 11 | "precompiledReferences": [], 12 | "autoReferenced": true, 13 | "defineConstraints": [], 14 | "versionDefines": [] 15 | } 16 | -------------------------------------------------------------------------------- /Editor/Unity.PerformanceTracking.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ee25bd4c5770f7147b2e7abfba78d26e 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/Utils.cs: -------------------------------------------------------------------------------- 1 | // #define PERFORMANCE_TRACKING_DEBUG 2 | using JetBrains.Annotations; 3 | using System; 4 | using System.Linq; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Reflection; 8 | using System.Text.RegularExpressions; 9 | using UnityEditor; 10 | using UnityEngine; 11 | 12 | namespace Unity.PerformanceTracking 13 | { 14 | internal static class Utils 15 | { 16 | public static string packageName = "com.unity.performance-tracking"; 17 | public static string packageFolderName = $"Packages/{packageName}"; 18 | 19 | private static string[] _ignoredAssemblies = 20 | { 21 | "^UnityScript$", "^System$", "^mscorlib$", "^netstandard$", 22 | "^System\\..*", "^nunit\\..*", "^Microsoft\\..*", "^Mono\\..*", "^SyntaxTree\\..*" 23 | }; 24 | 25 | public static string ColorToHexCode(Color color) 26 | { 27 | var r = (int)(color.r * 255); 28 | var g = (int)(color.g * 255); 29 | var b = (int)(color.b * 255); 30 | return $"#{r:X2}{g:X2}{b:X2}"; 31 | } 32 | 33 | public static void DelayCall(float seconds, System.Action callback) 34 | { 35 | DelayCall(EditorApplication.timeSinceStartup, seconds, callback); 36 | } 37 | 38 | public static void DelayCall(double timeStart, float seconds, System.Action callback) 39 | { 40 | var dt = EditorApplication.timeSinceStartup - timeStart; 41 | if (dt >= seconds) 42 | callback(); 43 | else 44 | EditorApplication.delayCall += () => DelayCall(timeStart, seconds, callback); 45 | } 46 | 47 | 48 | #if UNITY_EDITOR 49 | [InitializeOnLoad] 50 | #endif 51 | internal static class UnityVersion 52 | { 53 | enum Candidate 54 | { 55 | Dev = 0, 56 | Alpha = 1 << 8, 57 | Beta = 1 << 16, 58 | Final = 1 << 24 59 | } 60 | 61 | static UnityVersion() 62 | { 63 | var version = Application.unityVersion.Split('.'); 64 | 65 | if (version.Length < 2) 66 | { 67 | Console.WriteLine("Could not parse current Unity version '" + Application.unityVersion + "'; not enough version elements."); 68 | return; 69 | } 70 | 71 | if (int.TryParse(version[0], out Major) == false) 72 | { 73 | Console.WriteLine("Could not parse major part '" + version[0] + "' of Unity version '" + Application.unityVersion + "'."); 74 | } 75 | 76 | if (int.TryParse(version[1], out Minor) == false) 77 | { 78 | Console.WriteLine("Could not parse minor part '" + version[1] + "' of Unity version '" + Application.unityVersion + "'."); 79 | } 80 | 81 | if (version.Length >= 3) 82 | { 83 | try 84 | { 85 | Build = ParseBuild(version[2]); 86 | } 87 | catch 88 | { 89 | Console.WriteLine("Could not parse minor part '" + version[1] + "' of Unity version '" + Application.unityVersion + "'."); 90 | } 91 | } 92 | 93 | #if PERFORMANCE_TRACKING_DEBUG 94 | Debug.Log($"Unity {Major}.{Minor}.{Build}"); 95 | #endif 96 | } 97 | 98 | public static int ParseBuild(string build) 99 | { 100 | var rev = 0; 101 | if (build.Contains("a")) 102 | rev = (int)Candidate.Alpha; 103 | else if (build.Contains("b")) 104 | rev = (int)Candidate.Beta; 105 | if (build.Contains("f")) 106 | rev = (int)Candidate.Final; 107 | var tags = build.Split('a', 'b', 'f', 'p', 'x'); 108 | if (tags.Length == 2) 109 | { 110 | rev += Convert.ToInt32(tags[0], 10) << 4; 111 | rev += Convert.ToInt32(tags[1], 10); 112 | } 113 | return rev; 114 | } 115 | 116 | public static bool IsVersionGreaterOrEqual(int major, int minor) 117 | { 118 | if (Major > major) 119 | return true; 120 | if (Major == major) 121 | { 122 | if (Minor >= minor) 123 | return true; 124 | } 125 | 126 | return false; 127 | } 128 | 129 | public static bool IsVersionGreaterOrEqual(int major, int minor, int build) 130 | { 131 | if (Major > major) 132 | return true; 133 | if (Major == major) 134 | { 135 | if (Minor > minor) 136 | return true; 137 | 138 | if (Minor == minor) 139 | { 140 | if (Build >= build) 141 | return true; 142 | } 143 | } 144 | 145 | return false; 146 | } 147 | 148 | public static readonly int Major; 149 | public static readonly int Minor; 150 | public static readonly int Build; 151 | } 152 | 153 | internal static string JsonSerialize(object obj) 154 | { 155 | var assembly = typeof(Selection).Assembly; 156 | var managerType = assembly.GetTypes().First(t => t.Name == "Json"); 157 | var method = managerType.GetMethod("Serialize", BindingFlags.Public | BindingFlags.Static); 158 | var jsonString = ""; 159 | if (UnityVersion.IsVersionGreaterOrEqual(2019, 1, UnityVersion.ParseBuild("0a10"))) 160 | { 161 | var arguments = new object[] { obj, false, " " }; 162 | jsonString = method.Invoke(null, arguments) as string; 163 | } 164 | else 165 | { 166 | var arguments = new object[] { obj }; 167 | jsonString = method.Invoke(null, arguments) as string; 168 | } 169 | return jsonString; 170 | } 171 | 172 | internal static object JsonDeserialize(object obj) 173 | { 174 | var assembly = typeof(Selection).Assembly; 175 | var managerType = assembly.GetTypes().First(t => t.Name == "Json"); 176 | var method = managerType.GetMethod("Deserialize", BindingFlags.Public | BindingFlags.Static); 177 | var arguments = new object[] { obj }; 178 | return method.Invoke(null, arguments); 179 | } 180 | 181 | internal static bool IsDeveloperMode() 182 | { 183 | #if PERFORMANCE_TRACKING_DEBUG 184 | return true; 185 | #else 186 | return Directory.Exists($"{packageFolderName}/.git"); 187 | #endif 188 | } 189 | 190 | internal static string GetPerformanceTrackingVersion() 191 | { 192 | string version = null; 193 | try 194 | { 195 | var filePath = File.ReadAllText($"{packageFolderName}/package.json"); 196 | if (JsonDeserialize(filePath) is Dictionary manifest && manifest.ContainsKey("version")) 197 | { 198 | version = manifest["version"] as string; 199 | } 200 | } 201 | catch (Exception) 202 | { 203 | // ignored 204 | } 205 | 206 | return version ?? "unknown"; 207 | } 208 | 209 | private static bool IsIgnoredAssembly(AssemblyName assemblyName) 210 | { 211 | var name = assemblyName.Name; 212 | return _ignoredAssemblies.Any(candidate => Regex.IsMatch(name, candidate)); 213 | } 214 | 215 | public static IEnumerable GetLoadableTypes(this Assembly assembly) 216 | { 217 | try 218 | { 219 | return assembly.GetTypes(); 220 | } 221 | catch (ReflectionTypeLoadException e) 222 | { 223 | return e.Types.Where(t => t != null); 224 | } 225 | } 226 | 227 | internal static MethodInfo[] GetAllStaticMethods(this AppDomain aAppDomain, bool showInternalAPIs) 228 | { 229 | var result = new List(); 230 | var assemblies = aAppDomain.GetAssemblies(); 231 | foreach (var assembly in assemblies) 232 | { 233 | if (IsIgnoredAssembly(assembly.GetName())) 234 | continue; 235 | #if QUICKSEARCH_DEBUG 236 | var countBefore = result.Count; 237 | #endif 238 | var types = assembly.GetLoadableTypes(); 239 | foreach (var type in types) 240 | { 241 | var methods = type.GetMethods(BindingFlags.Static | (showInternalAPIs ? BindingFlags.Public | BindingFlags.NonPublic : BindingFlags.Public) | BindingFlags.DeclaredOnly); 242 | foreach (var m in methods) 243 | { 244 | if (m.IsPrivate) 245 | continue; 246 | 247 | if (m.IsGenericMethod) 248 | continue; 249 | 250 | if (m.Name.Contains("Begin") || m.Name.Contains("End")) 251 | continue; 252 | 253 | if (m.GetParameters().Length == 0) 254 | result.Add(m); 255 | } 256 | } 257 | #if QUICKSEARCH_DEBUG 258 | Debug.Log($"{result.Count - countBefore} - {assembly.GetName()}"); 259 | #endif 260 | } 261 | return result.ToArray(); 262 | } 263 | 264 | internal static IEnumerable GetAllMethodsWithAttribute(BindingFlags bindingFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) where T : System.Attribute 265 | { 266 | #if UNITY_2019_2_OR_NEWER 267 | return TypeCache.GetMethodsWithAttribute(); 268 | #else 269 | Assembly assembly = typeof(Selection).Assembly; 270 | var managerType = assembly.GetTypes().First(t => t.Name == "EditorAssemblies"); 271 | var method = managerType.GetMethod("Internal_GetAllMethodsWithAttribute", BindingFlags.NonPublic | BindingFlags.Static); 272 | var arguments = new object[] { typeof(T), bindingFlags }; 273 | return ((method.Invoke(null, arguments) as object[]) ?? throw new InvalidOperationException()).Cast(); 274 | #endif 275 | } 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /Editor/Utils.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4573691298002c14d8bd59c5ca2b4612 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | com.unity.performance-tracking Copyright © 2020 Unity Technologies ApS 2 | 3 | Licensed under the Unity Companion License for Unity-dependent projects--see [Unity Companion License](http://www.unity3d.com/legal/licenses/Unity_Companion_License). 4 | 5 | Unless expressly provided otherwise, the Software under this license is made available strictly on an “AS IS” BASIS WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. Please review the license for details on these and other terms and conditions. 6 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a47bf7bd266fdb94792b5f19f416c857 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About Performance Tracking 2 | 3 | This package expose a bunch of workflows and utilities to track, monitor and help profile the general performance of Unity Editor. 4 | 5 | The core of these monitoring utilities is in the Performance Tracker Window (available in version 2019.3): 6 | 7 | ![task manager](Documentation~/images/performance-tracker-window.png) 8 | 9 | And in the Profiling Snippet Window and API: 10 | 11 | ![snippet](Documentation~/images/profiling_snippet_window.gif) 12 | 13 | For more information see our documentation: 14 | 15 | * [About Performance Tracking](Documentation~/index.md) 16 | * [Performance Tracker Window](Documentation~/performance-tracker-window.md) 17 | * [Performance Monitoring Actions](Documentation~/performance-window-actions.md) 18 | * [Monitoring](Documentation~/monitoring.md) 19 | * [Profiling Snippet Window](Documentation~/profiling-snippet-window.md) 20 | * [API](Documentation~/api.md) 21 | 22 | ## Installation 23 | 24 | ### Git URL 25 | Use the package manager: 26 | - Click the plus icon 27 | - Click **Add package from git URL** 28 | 29 | ![install](Documentation~/images/git-url.png) 30 | 31 | - Enter `https://github.com/Unity-Technologies/com.unity.performance-tracking.git` 32 | 33 | ### Local Installation 34 | - Download the package. Unzip it somewhere on your disk. 35 | ![download](Documentation~/images/download.png) 36 | - Either **embed** the package itself in your project by copying the package in the Packages folder of your project. 37 | ![embed](Documentation~/images/emded.png) 38 | 39 | - Or Use the package manager: 40 | - Click the plus icon 41 | - Click **Add package from disk** 42 | ![install](Documentation~/images/add-package.png) 43 | - Select the package.json file in the com.unity.performance-tracking-master folder. 44 | ![install](Documentation~/images/select-package.png) 45 | 46 | 47 | ## Requirements 48 | 49 | This version of Performance Tracker is compatible with the following versions of the Unity Editor: 50 | 51 | * 2019.4 and later for Performance Tracker Window, Monitoring Service and Performance Bug Reporting 52 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 478a683367d15c14bb0f77f1c47d2497 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Tests.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4a0df1ba6dff0704e82904f6cf4f73c9 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/.tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "createSeparatePackage": false 3 | } 4 | -------------------------------------------------------------------------------- /Tests/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 00e41ed1c22c118489c0ede69ad15884 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Tests/Editor/PerformanceTrackerActionsTests.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.Linq; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using NUnit.Framework; 6 | using UnityEngine.TestTools; 7 | using UnityEngine.UIElements; 8 | using Unity.PerformanceTracking.Tests; 9 | using Unity.PerformanceTracking; 10 | 11 | public class PerformanceTrackerActionsTests 12 | { 13 | [UnityTest] 14 | public IEnumerator OpenPerformanceTrackerWindow() 15 | { 16 | var perfTrackerWindow = PerformanceTrackerActions.OpenPerformanceTrackerWindow(ProfilerHelpersTests.k_ApplicationTickMarker); 17 | Assert.IsNotNull(perfTrackerWindow); 18 | 19 | // Give the window some time to update 20 | var currentRefreshRate = perfTrackerWindow.GetRefreshRate(); 21 | yield return TestUtils.WaitForTime(currentRefreshRate.rate); 22 | 23 | var listView = perfTrackerWindow.rootVisualElement.Q(PerformanceTrackerWindow.k_TrackerList); 24 | var sourceItems = listView.itemsSource as List; 25 | Assert.IsNotNull(sourceItems); 26 | Assert.IsNotEmpty(sourceItems); 27 | Assert.IsTrue(sourceItems.All(item => item.name.StartsWith("Application.Tick"))); 28 | Assert.AreEqual(ProfilerHelpersTests.k_ApplicationTickMarker, sourceItems[0].name); 29 | 30 | perfTrackerWindow.Close(); 31 | } 32 | 33 | [UnityTest] 34 | public IEnumerator GetCallstack() 35 | { 36 | var receivedData = false; 37 | PerformanceTrackerActions.GetCallstack(ProfilerHelpersTests.k_ApplicationTickMarker, callstack => 38 | { 39 | receivedData = true; 40 | Assert.IsFalse(string.IsNullOrEmpty(callstack)); 41 | }); 42 | while (!receivedData) 43 | { 44 | yield return null; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /Tests/Editor/PerformanceTrackerActionsTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b653776154883644081be81d6e4cacf8 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Editor/ProfilerHelpersTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using System.IO; 6 | using NUnit.Framework; 7 | using Unity.PerformanceTracking; 8 | using Unity.PerformanceTracking.Tests; 9 | using UnityEditor; 10 | using UnityEditorInternal; 11 | using UnityEngine; 12 | using UnityEngine.TestTools; 13 | 14 | public class ProfilerHelpersTests 15 | { 16 | public const string k_ApplicationTickMarker = "Application.Tick"; 17 | 18 | [OneTimeSetUp] 19 | public void Init() 20 | { 21 | TestUtils.CreateFolder(TestUtils.testGeneratedFolder); 22 | } 23 | 24 | [OneTimeTearDown] 25 | public void Cleanup() 26 | { 27 | TestUtils.DeleteFolder(TestUtils.testGeneratedFolder); 28 | } 29 | 30 | [UnityTest] 31 | public IEnumerator StartProfilerRecording() 32 | { 33 | ProfilerHelpers.StartProfilerRecording("Application.Tick", true, ProfilerDriver.deepProfiling); 34 | 35 | // Wait for two delay call since we are doing two in the function 36 | yield return TestUtils.WaitForDelayCall(); 37 | yield return TestUtils.WaitForDelayCall(); 38 | 39 | Assert.IsTrue(ProfilerDriver.profileEditor); 40 | Assert.IsTrue(ProfilerDriver.enabled); 41 | 42 | ProfilerDriver.enabled = false; 43 | } 44 | 45 | [UnityTest] 46 | public IEnumerator StopProfilerRecording() 47 | { 48 | ProfilerHelpers.StartProfilerRecording("Application.Tick", true, false); 49 | // Wait for two delay call since we are doing two in the function 50 | yield return TestUtils.WaitForDelayCall(); 51 | yield return TestUtils.WaitForDelayCall(); 52 | 53 | ProfilerHelpers.StopProfilerRecording(); 54 | 55 | Assert.IsFalse(ProfilerDriver.enabled); 56 | } 57 | 58 | [UnityTest] 59 | public IEnumerator OpenProfiler() 60 | { 61 | EditorWindow theProfilerWindow = null; 62 | ProfilerHelpers.OpenProfiler(k_ApplicationTickMarker, profilerWindow => 63 | { 64 | theProfilerWindow = profilerWindow; 65 | }); 66 | yield return TestUtils.WaitForDelayCall(); 67 | yield return TestUtils.WaitForDelayCall(); 68 | 69 | Assert.IsNotNull(theProfilerWindow); 70 | 71 | theProfilerWindow.Close(); 72 | } 73 | 74 | [UnityTest] 75 | public IEnumerator OpenProfilerReport() 76 | { 77 | ProfilerDriver.ClearAllFrames(); 78 | ProfilerDriver.profileEditor = true; 79 | yield return null; 80 | ProfilerDriver.enabled = true; 81 | yield return null; 82 | ProfilerDriver.enabled = false; 83 | yield return null; 84 | 85 | var profileSaveFilePath = ProfilerHelpers.SaveProfileReport(k_ApplicationTickMarker); 86 | yield return null; 87 | 88 | EditorWindow profilerWindow = null; 89 | ProfilerHelpers.OpenProfileReport(profileSaveFilePath, k_ApplicationTickMarker, win => 90 | { 91 | profilerWindow = win; 92 | }); 93 | yield return TestUtils.WaitForDelayCall(); 94 | yield return TestUtils.WaitForDelayCall(); 95 | 96 | Assert.IsNotNull(profilerWindow); 97 | 98 | File.Delete(profileSaveFilePath); 99 | 100 | profilerWindow.Close(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Tests/Editor/ProfilerHelpersTests.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bfabb0ccc398c6a429d657c459464391 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Editor/TestUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.IO; 3 | using UnityEditor; 4 | 5 | namespace Unity.PerformanceTracking.Tests 6 | { 7 | internal static class TestUtils 8 | { 9 | public const string testGeneratedFolder = "Assets/TempTestGeneratedData/"; 10 | 11 | public static void DeleteFolder(string path) 12 | { 13 | Directory.Delete(path, true); 14 | if (path.EndsWith("/")) 15 | { 16 | path = path.Remove(path.Length - 1); 17 | } 18 | File.Delete(path + ".meta"); 19 | AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate); 20 | } 21 | 22 | public static void CreateFolder(string path) 23 | { 24 | if (!Directory.Exists(path)) 25 | { 26 | Directory.CreateDirectory(path); 27 | AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate); 28 | } 29 | } 30 | 31 | public static IEnumerator WaitForDelayCall() 32 | { 33 | var actionCalled = false; 34 | EditorApplication.delayCall += () => 35 | { 36 | actionCalled = true; 37 | }; 38 | while (!actionCalled) 39 | yield return null; 40 | } 41 | 42 | public static IEnumerator WaitForTime(double seconds) 43 | { 44 | var currentTime = EditorApplication.timeSinceStartup; 45 | while (currentTime + seconds >= EditorApplication.timeSinceStartup) 46 | { 47 | yield return null; 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/Editor/TestUtils.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b6fae87db64872b45991443cb961d8fc 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Tests/Editor/Unity.PerformanceTracking.Editor.Tests.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unity.PerformanceTracking.Editor.Tests", 3 | "references": [ 4 | "Unity.PerformanceTracking.Editor" 5 | ], 6 | "optionalUnityReferences": [ 7 | "TestAssemblies" 8 | ], 9 | "includePlatforms": [ 10 | "Editor" 11 | ], 12 | "excludePlatforms": [], 13 | "allowUnsafeCode": false, 14 | "overrideReferences": true, 15 | "precompiledReferences": [ 16 | ], 17 | "autoReferenced": false, 18 | "defineConstraints": [ 19 | "UNITY_INCLUDE_TESTS" 20 | ], 21 | "versionDefines": [], 22 | "noEngineReferences": false 23 | } -------------------------------------------------------------------------------- /Tests/Editor/Unity.PerformanceTracking.Editor.Tests.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d3734ef0f46664e40b70fbc1fe2d9027 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.unity.performance-tracking", 3 | "displayName":"Performance Tracking", 4 | "version": "1.0.0-preview.2", 5 | "unity": "2019.4", 6 | "keywords": ["unity", "editor", "window", "performance"], 7 | "description": "The performance tracking package provides to the users additional profiling tools to help them find exactly how the performance is spread.\n\nThe performance tracker window is the hub to these tools. Once opened, the user can search for any performance tracker and start a filtered profiling session.", 8 | "author": { 9 | "name": "Unity Technologies ApS", 10 | "url": "https://unity.com/" 11 | }, 12 | "dependencies": {} 13 | } 14 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 809f5e8a928f4694b81f8d98f552bc3f 3 | PackageManifestImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------