diff --git a/artifacts/perf/issue38-refresh-spinner.md b/artifacts/perf/issue38-refresh-spinner.md new file mode 100644 index 00000000..914a118c --- /dev/null +++ b/artifacts/perf/issue38-refresh-spinner.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 | 4.41, 4.11, 4.36 | +| 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 | 11.59 GiB (`12,448,333,824 bytes`) | +| Memory | Online Physical RAM | 66.00 GiB (`70,866,960,384 bytes`) | +| Memory | Swap | total 120 GiB (`128,848,973,824 bytes`); free 99.16 GiB (`106,472,206,336 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 | +| --- | --- | --- | --- | --- | +| tree-view-repeat-click-burst | user-flow | Repeatedly click the same view button while the tree state is still mutating. | Overlapping command handling and workspace-state writes. | Command burst against the extension command handlers 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 | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | +| tree-view-repeat-click-burst | user-flow | 0.25 | 0.3 | 0.35 | 0.53 | 0.41 | 0.61 | + +## 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 | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| tree-view-repeat-click-burst | user-flow | 1.13 | 1.63 | 1.38 | 1.88 | 1.5 | 2.13 | 1.5 | 2.13 | + +## 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 | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| tree-view-repeat-click-burst | user-flow | 56.55 | 57.12 | 56.93 | 57.61 | 57 | 57.69 | 57 | 57.69 | diff --git a/package.json b/package.json index 71a0ad1b..1447f100 100644 --- a/package.json +++ b/package.json @@ -383,13 +383,19 @@ "command": "better-todo-tree.treeStateBusy", "title": "%better-todo-tree.command.treeStateBusy.title%", "category": "%better-todo-tree.command.category%", - "icon": "$(loading)" + "icon": { + "light": "resources/button-icons/refresh-spin-light.svg", + "dark": "resources/button-icons/refresh-spin-dark.svg" + } }, { "command": "better-todo-tree.scanBusy", "title": "%better-todo-tree.command.scanBusy.title%", "category": "%better-todo-tree.command.category%", - "icon": "$(loading)" + "icon": { + "light": "resources/button-icons/refresh-spin-light.svg", + "dark": "resources/button-icons/refresh-spin-dark.svg" + } }, { "command": "better-todo-tree.filter", diff --git a/resources/button-icons/refresh-spin-dark.svg b/resources/button-icons/refresh-spin-dark.svg new file mode 100644 index 00000000..0b320575 --- /dev/null +++ b/resources/button-icons/refresh-spin-dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/resources/button-icons/refresh-spin-light.svg b/resources/button-icons/refresh-spin-light.svg new file mode 100644 index 00000000..0cc8f08b --- /dev/null +++ b/resources/button-icons/refresh-spin-light.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/extension.js b/src/extension.js index 0fd5b332..5b6256cb 100644 --- a/src/extension.js +++ b/src/extension.js @@ -562,7 +562,7 @@ function activate( context ) { var message = buildScanProgressMessage( state, snapshot ); - statusBarIndicator.text = "$(loading~spin) " + identity.DISPLAY_NAME + " " + snapshot.percent + "%"; + statusBarIndicator.text = identity.STATUS_BUSY_ICON_LABEL + " " + identity.DISPLAY_NAME + " " + snapshot.percent + "%"; statusBarIndicator.show(); statusBarIndicator.command = identity.COMMANDS.stopScan; statusBarIndicator.tooltip = message || 'Click to interrupt scan'; @@ -1119,7 +1119,7 @@ function activate( context ) interrupted = false; cancelledScanGenerations.delete( activeScanGeneration ); - statusBarIndicator.text = identity.DISPLAY_NAME + ": Scanning..."; + statusBarIndicator.text = identity.STATUS_BUSY_ICON_LABEL + " " + identity.DISPLAY_NAME + ": Scanning..."; statusBarIndicator.show(); statusBarIndicator.command = identity.COMMANDS.stopScan; statusBarIndicator.tooltip = "Click to interrupt scan"; diff --git a/src/extensionIdentity.js b/src/extensionIdentity.js index 9d36334b..cadd7302 100644 --- a/src/extensionIdentity.js +++ b/src/extensionIdentity.js @@ -5,6 +5,8 @@ var LEGACY_NAMESPACE = 'todo-tree'; var DISPLAY_NAME = 'Better Todo Tree'; var LEGACY_DISPLAY_NAME = 'Todo Tree'; +var STATUS_BUSY_ICON = 'sync~spin'; +var STATUS_BUSY_ICON_LABEL = '$(' + STATUS_BUSY_ICON + ')'; var VIEW_CONTAINER_ID = 'todo-tree-container'; var VIEW_ID = 'todo-tree-view'; @@ -242,6 +244,8 @@ module.exports.CURRENT_NAMESPACE = CURRENT_NAMESPACE; module.exports.LEGACY_NAMESPACE = LEGACY_NAMESPACE; module.exports.DISPLAY_NAME = DISPLAY_NAME; module.exports.LEGACY_DISPLAY_NAME = LEGACY_DISPLAY_NAME; +module.exports.STATUS_BUSY_ICON = STATUS_BUSY_ICON; +module.exports.STATUS_BUSY_ICON_LABEL = STATUS_BUSY_ICON_LABEL; module.exports.VIEW_CONTAINER_ID = VIEW_CONTAINER_ID; module.exports.VIEW_ID = VIEW_ID; module.exports.EXPORT_SCHEME = EXPORT_SCHEME; diff --git a/test/extension.scan-parity.test.js b/test/extension.scan-parity.test.js index 8c052716..8ae812c2 100644 --- a/test/extension.scan-parity.test.js +++ b/test/extension.scan-parity.test.js @@ -3585,6 +3585,7 @@ QUnit.test( "workspace scans publish progress, current target, and clear the tre return typeof report.message === 'string' && report.message.indexOf( 'tracked.js' ) >= 0; } ), true ); assert.equal( harness.vscode.treeViews[ 0 ].message.indexOf( 'tracked.js' ) >= 0, true ); + assert.equal( harness.vscode.statusBarItems[ 0 ].text.indexOf( '$(sync~spin) Better Todo Tree' ), 0 ); assert.equal( harness.vscode.statusBarItems[ 0 ].text.indexOf( 'Better Todo Tree' ) >= 0, true ); assert.equal( harness.vscode.statusBarItems[ 0 ].text.indexOf( '100%' ) === -1, true ); @@ -3617,6 +3618,7 @@ QUnit.test( "scan progress cancellation interrupts the active scan and surfaces return matrixHelpers.flushAsyncWork().then( function() { assert.equal( harness.vscode.progressSessions.length, 1 ); + assert.equal( harness.vscode.statusBarItems[ 0 ].text.indexOf( '$(sync~spin) Better Todo Tree' ), 0 ); harness.vscode.progressSessions[ 0 ].cancel(); return matrixHelpers.flushAsyncWork(); } ).then( function() diff --git a/test/package.manifest.test.js b/test/package.manifest.test.js index a037b394..3f54c98e 100644 --- a/test/package.manifest.test.js +++ b/test/package.manifest.test.js @@ -12,6 +12,19 @@ function readPackageNls( fileName ) return JSON.parse( fs.readFileSync( path.join( __dirname, '..', fileName ), 'utf8' ) ); } +function readWorkspaceText( relativePath ) +{ + return fs.readFileSync( path.join( __dirname, '..', relativePath ), 'utf8' ); +} + +function findCommandContribution( packageJson, commandId ) +{ + return packageJson.contributes.commands.find( function( entry ) + { + return entry.command === commandId; + } ); +} + function getConfigurationProperty( propertyName ) { return languageMatrix.findConfigurationProperty( propertyName ); @@ -193,24 +206,39 @@ QUnit.test( 'busy and composite tree commands have localization entries in both assert.notOk( chinese[ 'better-todo-tree.command.treeStateBusy.title' ].indexOf( '$(' ) >= 0 ); } ); -QUnit.test( 'busy placeholder commands use static product icons with plain localized titles', function( assert ) +QUnit.test( 'issue #38 busy placeholder commands animate only their svg glyphs', function( assert ) { var packageJson = readPackageJson(); - var treeStateBusy = packageJson.contributes.commands.find( function( entry ) - { - return entry.command === 'better-todo-tree.treeStateBusy'; - } ); - var scanBusy = packageJson.contributes.commands.find( function( entry ) - { - return entry.command === 'better-todo-tree.scanBusy'; - } ); - - assert.equal( treeStateBusy.icon, '$(loading)' ); - assert.equal( scanBusy.icon, '$(loading)' ); - assert.equal( treeStateBusy.icon.indexOf( '~spin' ), -1 ); - assert.equal( scanBusy.icon.indexOf( '~spin' ), -1 ); + var expectedBusyIcon = { + light: 'resources/button-icons/refresh-spin-light.svg', + dark: 'resources/button-icons/refresh-spin-dark.svg' + }; + var treeStateBusy = findCommandContribution( packageJson, 'better-todo-tree.treeStateBusy' ); + var scanBusy = findCommandContribution( packageJson, 'better-todo-tree.scanBusy' ); + + assert.deepEqual( treeStateBusy.icon, expectedBusyIcon ); + assert.deepEqual( scanBusy.icon, expectedBusyIcon ); + assert.equal( JSON.stringify( treeStateBusy.icon ).indexOf( '~spin' ), -1 ); + assert.equal( JSON.stringify( scanBusy.icon ).indexOf( '~spin' ), -1 ); + assert.equal( JSON.stringify( treeStateBusy.icon ).indexOf( 'loading' ), -1 ); + assert.equal( JSON.stringify( scanBusy.icon ).indexOf( 'loading' ), -1 ); assert.equal( treeStateBusy.title, '%better-todo-tree.command.treeStateBusy.title%' ); assert.equal( scanBusy.title, '%better-todo-tree.command.scanBusy.title%' ); + + Object.keys( expectedBusyIcon ).forEach( function( theme ) + { + var svg = readWorkspaceText( expectedBusyIcon[ theme ] ); + var groupStart = svg.indexOf( '' ); + var groupEnd = svg.indexOf( '' ); + var animateTransform = svg.indexOf( '= 0, theme + ' icon has a glyph group' ); + assert.ok( groupEnd > groupStart, theme + ' icon closes the glyph group' ); + assert.ok( animateTransform > groupStart, theme + ' icon animates inside the glyph group' ); + assert.ok( animateTransform < groupEnd, theme + ' icon animation stays inside the glyph group' ); + assert.equal( svg.substring( 0, groupStart ).indexOf( '