diff --git a/.gitignore b/.gitignore index 48c40c6b..63678835 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,4 @@ artifacts/vsix/ artifacts/release-notes/ artifacts/**/*.log artifacts/perf/runtime-benchmarks.json -artifacts/perf/*.json +artifacts/**/*.json diff --git a/artifacts/issue-41/open-file-default-save-rescan-visible-tree.md b/artifacts/issue-41/open-file-default-save-rescan-visible-tree.md new file mode 100644 index 00000000..66a52639 --- /dev/null +++ b/artifacts/issue-41/open-file-default-save-rescan-visible-tree.md @@ -0,0 +1,62 @@ +# Runtime Benchmarks + +- Baseline ref: `a6f60e0ce830c4649ac34fc05e5a1799ec91d151` +- Current source: working tree +- Node: `v25.2.0` +- Selection mode: `scenario-list` +- Declared suite: `user-flow` +- Result-count validation: `1 rows, suite-consistent=true, all-user-flow=true` + +## Machine Profile + +| Category | Field | Value | +| --- | --- | --- | +| Host | Hostname | n00ne-AERO-17-YD | +| Host | OS | Ubuntu 22.04.5 LTS | +| Host | Kernel | 6.8.0-124-generic | +| Host | Architecture | x64 | +| Host | Load Average | 2.5, 3.34, 3.68 | +| Host | Available Parallelism | - | +| CPU | Model | Intel(R) Core(TM) i9-14900HX | +| CPU | Vendor | GenuineIntel | +| CPU | Topology | 16 logical CPU(s), 2 thread(s)/core, 8 core(s)/socket, 1 socket(s), 1 NUMA node(s) | +| CPU | Frequency | 800 MHz to 5,800 MHz | +| CPU | Cache | L1d 384 KiB (8 instances), L1i 256 KiB (8 instances), L2 16 MiB (8 instances), L3 36 MiB (1 instance) | +| Memory | Total RAM | 62.51 GiB (`67,119,767,552 bytes`) | +| Memory | Available At Collection | 14.21 GiB (`15,261,028,352 bytes`) | +| Memory | Online Physical RAM | 66.00 GiB (`70,866,960,384 bytes`) | +| Memory | Swap | total 120 GiB (`128,848,973,824 bytes`); free 105 GiB (`112,411,762,688 bytes`) | +| Memory | DMI / SPD | Unavailable: /sys/firmware/dmi/tables/smbios_entry_point: Permission denied /dev/mem: Permission denied | +| Storage | Root Device | nvme1n1 (Samsung SSD 9100 PRO 4TB), 3.64 TiB (`4,000,787,030,016 bytes`), transport nvme, rotational=false, readOnly=false | + +## Scenario Model + +| Scenario | Kind | User flow | Measurement scope | Input model | +| --- | --- | --- | --- | --- | +| open-file-default-save-rescan-visible-tree | user-flow | Save an already-open file that uses default tag scanning and redraw the visible tree. | Document save listener, document rescan, search-result replacement, and visible-tree render. | Real document text in a VS Code event harness. | + +## Metric Model + +| Table | Value model | Accuracy model | +| --- | --- | --- | +| Latency | Wall-clock elapsed time around each harness flow iteration, summarized as min/p50/p90/p95/max. | Exact for each sampled iteration in this run. | +| Profiled RSS Burst | Difference between the isolated scenario worker RSS measured immediately before the flow and that worker iteration's OS high-water-mark peak RSS. | Exact for the measured worker iteration, using `process.memoryUsage().rss` at flow start and `process.resourceUsage().maxRSS` for the peak. | +| Profiled Peak RSS | Highest process RSS reached by each isolated scenario worker iteration. | Exact worker-process high-water mark from `process.resourceUsage().maxRSS`. | + +## Latency + +| Scenario | Kind | Baseline p50 ms | Current p50 ms | Baseline p90 ms | Current p90 ms | Baseline p95 ms | Current p95 ms | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | +| open-file-default-save-rescan-visible-tree | user-flow | 104.44 | 2.15 | 110.7 | 2.46 | 111.64 | 3.76 | + +## Profiled RSS Burst + +| Scenario | Kind | Baseline p50 MiB | Current p50 MiB | Baseline p90 MiB | Current p90 MiB | Baseline p95 MiB | Current p95 MiB | Baseline Max MiB | Current Max MiB | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| open-file-default-save-rescan-visible-tree | user-flow | 26.88 | 24.13 | 27.25 | 27.25 | 27.25 | 27.63 | 27.25 | 27.63 | + +## Profiled Peak RSS + +| Scenario | Kind | Baseline p50 RSS MiB | Current p50 RSS MiB | Baseline p90 RSS MiB | Current p90 RSS MiB | Baseline p95 RSS MiB | Current p95 RSS MiB | Baseline Max RSS MiB | Current Max RSS MiB | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| open-file-default-save-rescan-visible-tree | user-flow | 82.47 | 79.63 | 82.62 | 82.76 | 82.68 | 83.16 | 82.68 | 83.16 | diff --git a/src/extension.js b/src/extension.js index 2ba79beb..0fd5b332 100644 --- a/src/extension.js +++ b/src/extension.js @@ -193,11 +193,6 @@ function activate( context ) return undefined; } - changedEntries.forEach( function( entry ) - { - extensionContextValues[ entry.suffix ] = entry.value; - } ); - return Promise.all( changedEntries.map( function( entry ) { var updates = [ @@ -210,7 +205,13 @@ function activate( context ) } return Promise.all( updates ); - } ) ); + } ) ).then( function() + { + changedEntries.forEach( function( entry ) + { + extensionContextValues[ entry.suffix ] = entry.value; + } ); + } ); } ); extensionContextUpdateQueue = scheduled.catch( function( error ) @@ -901,22 +902,26 @@ function activate( context ) }, 200 ); } + function tagCountTotal( counts ) + { + return Object.values( counts ).reduce( function( total, count ) { return total + count; }, 0 ); + } + function updateInformation() { var statusBar = getSetting( 'general.statusBar', 'none' ); - var counts = provider.getTagCountsForActivityBar(); - var total = Object.values( counts ).reduce( function( a, b ) { return a + b; }, 0 ); + var activityBarCounts = provider.getTagCountsForActivityBar(); + var activityBarTotal = tagCountTotal( activityBarCounts ); + var counts = statusBar === STATUS_BAR_CURRENT_FILE ? + provider.getTagCountsForStatusBar( getCurrentFileFilter() ) : + provider.getTagCountsForStatusBar(); + var total = tagCountTotal( counts ); + var titleTotal = statusBar === STATUS_BAR_CURRENT_FILE ? total : activityBarTotal; - var badgeTotal = config.shouldShowActivityBarBadge() ? total : 0; + var badgeTotal = config.shouldShowActivityBarBadge() ? activityBarTotal : 0; todoTreeView.badge = { value: badgeTotal }; - if( statusBar === STATUS_BAR_CURRENT_FILE ) - { - counts = provider.getTagCountsForStatusBar( getCurrentFileFilter() ); - total = Object.values( counts ).reduce( function( a, b ) { return a + b; }, 0 ); - } - var countRegex = new RegExp( "([^(]*)(\\(\\d+\\))*" ); var match = countRegex.exec( todoTreeView.title ); if( match !== null ) @@ -935,9 +940,9 @@ function activate( context ) title = "Tree"; } - if( total > 0 && getSetting( 'tree.showCountsInTree', false ) === true ) + if( titleTotal > 0 && getSetting( 'tree.showCountsInTree', false ) === true ) { - title += " (" + total + ")"; + title += " (" + titleTotal + ")"; } todoTreeView.title = title; } @@ -2891,7 +2896,7 @@ function activate( context ) var buttons = [ MORE_INFO_BUTTON, NEVER_SHOW_AGAIN_BUTTON ]; if( getSetting( 'regex.regex', '' ) === getCurrentConfiguration().inspect( 'regex.regex' ).defaultValue ) { - message += " Would you like to update your settings automatically?"; + message += " Update settings automatically?"; buttons.unshift( YES_BUTTON ); } vscode.window.showInformationMessage( message, ...buttons ).then( function( button ) @@ -3057,7 +3062,7 @@ function activate( context ) destinationId: 'workbench.view.explorer' } ); - vscode.window.showInformationMessage( identity.DISPLAY_NAME + ": 'showInExplorer' has been deprecated. If needed, the view can now be dragged to where you want it.", OPEN_SETTINGS_BUTTON, NEVER_SHOW_AGAIN_BUTTON ).then( function( button ) + vscode.window.showInformationMessage( identity.DISPLAY_NAME + ": 'showInExplorer' has been deprecated. View can be dragged to any VS Code location.", OPEN_SETTINGS_BUTTON, NEVER_SHOW_AGAIN_BUTTON ).then( function( button ) { if( button === OPEN_SETTINGS_BUTTON ) { diff --git a/test/extension.scan-parity.test.js b/test/extension.scan-parity.test.js index ac3685df..8c052716 100644 --- a/test/extension.scan-parity.test.js +++ b/test/extension.scan-parity.test.js @@ -253,8 +253,10 @@ function createSearchResultsStub() }; } -function createProviderStub() +function createProviderStub( options ) { + options = options || {}; + return { replaceCalls: [], latestResultsByUri: new Map(), @@ -276,8 +278,15 @@ function createProviderStub() }, finalizePendingChanges: function( filter, options ) { this.finalizeCalls.push( { filter: filter, options: options } ); }, refresh: function() { this.refreshCalls++; }, - getTagCountsForActivityBar: function() { return {}; }, - getTagCountsForStatusBar: function() { return {}; }, + getTagCountsForActivityBar: function() + { + return Object.assign( {}, options.activityBarCounts || {} ); + }, + getTagCountsForStatusBar: function( fileFilter ) + { + var byFile = options.statusBarCountsByFile || {}; + return Object.assign( {}, fileFilter && byFile[ fileFilter ] ? byFile[ fileFilter ] : options.statusBarCounts || {} ); + }, exportTree: function() { return {}; }, hasSubTags: function() { return false; }, getChildren: function() { return []; }, @@ -386,7 +395,7 @@ function createVscodeStub( options ) groupedBySubTag: false, hideTreeWhenEmpty: false, autoRefresh: true, - scanAtStartup: true, + scanAtStartup: options.scanAtStartup !== false, scanMode: options.scanMode }, filtering: filteringDefaults, @@ -396,7 +405,7 @@ function createVscodeStub( options ) periodicRefreshInterval: periodicRefreshInterval, rootFolder: "", tags: languageMatrix.DEFAULT_TAGS.slice(), - statusBar: 'total' + statusBar: options.statusBar || 'total' }, ripgrep: { ripgrepArgs: '', @@ -411,9 +420,9 @@ function createVscodeStub( options ) periodicRefreshInterval: periodicRefreshInterval, rootFolder: "", exportPath: '/tmp/todo-tree.txt', - statusBar: 'total', + statusBar: options.statusBar || 'total', statusBarClickBehaviour: '', - showActivityBarBadge: false, + showActivityBarBadge: options.showActivityBarBadge === true, tags: languageMatrix.DEFAULT_TAGS.slice(), tagGroups: {}, schemes: [ 'file' ] @@ -425,7 +434,7 @@ function createVscodeStub( options ) showBadges: false, scanMode: options.scanMode, showCurrentScanMode: false, - scanAtStartup: true, + scanAtStartup: options.scanAtStartup !== false, hideTreeWhenEmpty: false, buttons: rootSection.get( 'tree.buttons' ) }, undefined, configurationUpdates ); @@ -615,7 +624,15 @@ function createVscodeStub( options ) commands: { executeCommand: function( command ) { - executedCommands.push( Array.prototype.slice.call( arguments ) ); + var commandArguments = Array.prototype.slice.call( arguments ); + executedCommands.push( commandArguments ); + if( typeof options.executeCommandImpl === 'function' ) + { + return Promise.resolve().then( function() + { + return options.executeCommandImpl.apply( undefined, commandArguments ); + } ); + } return Promise.resolve(); }, registerCommand: function( name, handler ) @@ -703,7 +720,7 @@ function createVscodeStub( options ) function createExtensionHarness( options ) { - var provider = options.useActualTreeProvider === true ? undefined : createProviderStub(); + var provider = options.useActualTreeProvider === true ? undefined : createProviderStub( options ); var searchResults = createSearchResultsStub(); var ripgrepSearchCalls = []; var scanDocumentCalls = []; @@ -833,7 +850,7 @@ function createExtensionHarness( options ) shouldIgnoreGitSubmodules: function() { return false; }, shouldUseBuiltInFileExcludes: function() { return false; }, shouldUseBuiltInSearchExcludes: function() { return false; }, - shouldShowActivityBarBadge: function() { return false; }, + shouldShowActivityBarBadge: function() { return options.showActivityBarBadge === true; }, shouldFlatten: function() { return Object.prototype.hasOwnProperty.call( treeStateOverrides, 'flat' ) ? treeStateOverrides.flat : context.workspaceState.get( 'flat', false ); }, shouldShowTagsOnly: function() { return Object.prototype.hasOwnProperty.call( treeStateOverrides, 'tagsOnly' ) ? treeStateOverrides.tagsOnly : context.workspaceState.get( 'tagsOnly', false ); }, clickingStatusBarShouldRevealTree: function() { return false; }, @@ -1359,6 +1376,99 @@ function fireVisibleNotebookEditorsChanged( harness, notebooksToShow, activeNote QUnit.module( "extension scan parity" ); +QUnit.test( "issue #41 total status bar uses status counts and activity badge uses activity counts", function( assert ) +{ + var harness = createExtensionHarness( { + scanMode: 'open files', + statusBar: 'total', + showActivityBarBadge: true, + statusBarCounts: { TODO: 2 }, + activityBarCounts: { TODO: 2, FIXME: 5 }, + fileContents: {} + } ); + + harness.extension.activate( harness.context ); + + return matrixHelpers.flushAsyncWork().then( function() + { + assert.equal( harness.vscode.statusBarItems[ 0 ].text, '$(check) 2 (in open files)' ); + assert.deepEqual( harness.vscode.treeViews[ 0 ].badge, { value: 7 } ); + } ); +} ); + +QUnit.test( "issue #41 tag status bar ignores activity-bar hidden-count domain", function( assert ) +{ + var harness = createExtensionHarness( { + scanMode: 'open files', + statusBar: 'tags', + statusBarCounts: { TODO: 3, FIXME: 1 }, + activityBarCounts: { TODO: 3 }, + fileContents: {} + } ); + + harness.extension.activate( harness.context ); + + return matrixHelpers.flushAsyncWork().then( function() + { + assert.equal( harness.vscode.statusBarItems[ 0 ].text, '$(check) FIXME: 1 TODO: 3 (in open files)' ); + } ); +} ); + +QUnit.test( "command context cache updates only after VS Code accepts every setContext command", function( assert ) +{ + var flatFailures = 1; + var harness = createExtensionHarness( { + scanMode: 'open files', + scanAtStartup: false, + fileContents: {}, + executeCommandImpl: function( command, contextKey ) + { + if( command === 'setContext' && contextKey === 'better-todo-tree-flat' && flatFailures > 0 ) + { + flatFailures--; + return Promise.reject( new Error( 'setContext test failure' ) ); + } + + return Promise.resolve(); + } + } ); + + harness.extension.activate( harness.context ); + + return matrixHelpers.flushAsyncWork().then( function() + { + var beforeRetry = harness.vscode.executedCommands.length; + + assert.equal( flatFailures, 0 ); + assert.ok( harness.vscode.errorMessages.some( function( message ) + { + return message.indexOf( 'failed to update command contexts' ) >= 0; + } ) ); + + harness.vscode.workspaceListeners.configuration( { + affectsConfiguration: function( section ) + { + return section === 'better-todo-tree' || section === 'todo-tree'; + } + } ); + + return matrixHelpers.flushAsyncWork().then( function() + { + return matrixHelpers.flushAsyncWork(); + } ).then( function() + { + var retryCommands = harness.vscode.executedCommands.slice( beforeRetry ); + + assert.ok( retryCommands.some( function( commandArguments ) + { + return commandArguments[ 0 ] === 'setContext' && + commandArguments[ 1 ] === 'better-todo-tree-flat' && + commandArguments[ 2 ] === false; + } ) ); + } ); + } ); +} ); + QUnit.test( "open-files mode stores canonical document results through the refresh pipeline", function( assert ) { var fixture = [ {