From 3a0199e1a9c72b54d3dc5ad82e5fa75c470940c7 Mon Sep 17 00:00:00 2001 From: Hildebrando Chavez Date: Sun, 22 Mar 2026 14:48:27 -0600 Subject: [PATCH] Add build script, README, changelog, and icon assets for VS Code extension --- build-vsix.ps1 | 505 ++++++++++++++++++ vscode-extension/CHANGELOG.md | 22 + vscode-extension/README.md | 56 ++ vscode-extension/media/icon-small.svg | 19 + vscode-extension/media/icon.png | Bin 0 -> 5328 bytes vscode-extension/package.json | 26 +- .../src/views/profiler-panel-provider.ts | 117 +++- 7 files changed, 716 insertions(+), 29 deletions(-) create mode 100644 build-vsix.ps1 create mode 100644 vscode-extension/CHANGELOG.md create mode 100644 vscode-extension/README.md create mode 100644 vscode-extension/media/icon-small.svg create mode 100644 vscode-extension/media/icon.png diff --git a/build-vsix.ps1 b/build-vsix.ps1 new file mode 100644 index 0000000..f86676e --- /dev/null +++ b/build-vsix.ps1 @@ -0,0 +1,505 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Builds and packages the Light Query Profiler VS Code extension (.vsix). + +.DESCRIPTION + This script performs a full release build of the Light Query Profiler extension: + 1. Validates prerequisites (dotnet, node, npm) + 2. Cleans previous build outputs + 3. Publishes the .NET backend (JsonRpc + Shared) in Release mode (framework-dependent, AnyCPU) + 4. Converts the SVG icon to PNG 128x128 using the 'sharp' npm package + 5. Installs npm dependencies + 6. Compiles TypeScript to dist/ + 7. Validates all required output files are present + 8. Packages the extension with vsce + 9. Reports the generated .vsix path, size, and install instructions + +.NOTES + Prerequisites: + - .NET 10 SDK : https://dotnet.microsoft.com/en-us/download/dotnet/10.0 + - Node.js 18+ : https://nodejs.org/ + - npm : included with Node.js + + Before publishing to the VS Code Marketplace: + - Set the correct publisher ID in vscode-extension/package.json + (field: "publisher") matching your account at + https://marketplace.visualstudio.com/manage/publishers + - Obtain a Personal Access Token (PAT) from Azure DevOps and run: + npx vsce login + + Cross-platform support: + The generated .vsix works on Windows, Linux, and macOS. + The .NET backend is published as framework-dependent (AnyCPU), so users + must have .NET 10 Runtime installed on their machine. + Native libraries for all platforms (win-x64, linux-x64, linux-arm64, + osx-x64, osx-arm64, etc.) are included automatically via the runtimes/ + directory produced by 'dotnet publish'. + +.EXAMPLE + .\build-vsix.ps1 + +.EXAMPLE + .\build-vsix.ps1 -SkipClean -Verbose +#> + +[CmdletBinding()] +param( + # Skip cleaning previous build outputs (faster incremental builds) + [switch]$SkipClean, + + # Skip running 'npm install' (use when node_modules is already up to date) + [switch]$SkipNpmInstall +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +function Write-Step { + param([int]$Number, [string]$Message) + Write-Host "" + Write-Host "[$Number/9] $Message" -ForegroundColor Cyan +} + +function Write-Success { + param([string]$Message) + Write-Host " OK $Message" -ForegroundColor Green +} + +function Write-Fail { + param([string]$Message) + Write-Host " FAIL $Message" -ForegroundColor Red +} + +function Write-Info { + param([string]$Message) + Write-Host " $Message" -ForegroundColor Gray +} + +function Assert-Command { + param([string]$Name, [string]$InstallHint) + if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { + Write-Fail "$Name not found in PATH." + Write-Host " Install: $InstallHint" -ForegroundColor Yellow + exit 1 + } + $version = & $Name --version 2>&1 | Select-Object -First 1 + Write-Success "$Name found: $version" +} + +function Assert-FileExists { + param([string]$FilePath, [string]$Description) + if (-not (Test-Path $FilePath)) { + Write-Fail "Missing required file: $Description" + Write-Info "Expected at: $FilePath" + exit 1 + } + Write-Success "$Description" +} + +# --------------------------------------------------------------------------- +# Paths +# --------------------------------------------------------------------------- + +# Resolve repo root robustly: $PSScriptRoot is set when invoked as a .ps1 file, +# but falls back to the current directory when dot-sourced or called inline. +$RepoRoot = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path } +$SrcDir = Join-Path $RepoRoot "src" +$ExtDir = Join-Path $RepoRoot "vscode-extension" +$BinDir = Join-Path $ExtDir "bin" +$DistDir = Join-Path $ExtDir "dist" +$MediaDir = Join-Path $ExtDir "media" +# Note: Join-Path in Windows PowerShell 5.1 only accepts two path arguments. +# Nesting calls is required for paths with more than one child segment. +$JsonRpcCsproj = Join-Path (Join-Path $SrcDir "LightQueryProfiler.JsonRpc") "LightQueryProfiler.JsonRpc.csproj" +$IconSvg = Join-Path $MediaDir "icon.svg" +$IconPng = Join-Path $MediaDir "icon.png" + +# --------------------------------------------------------------------------- +# Header +# --------------------------------------------------------------------------- + +Write-Host "" +Write-Host "================================================" -ForegroundColor White +Write-Host " Light Query Profiler - VSIX Build Script" -ForegroundColor White +Write-Host "================================================" -ForegroundColor White +Write-Host " Repo : $RepoRoot" +Write-Host " ExtDir : $ExtDir" +Write-Host "" + +# --------------------------------------------------------------------------- +# STEP 1 — Validate prerequisites +# --------------------------------------------------------------------------- + +Write-Step 1 "Validating prerequisites" + +Assert-Command "dotnet" "https://dotnet.microsoft.com/en-us/download/dotnet/10.0" +Assert-Command "node" "https://nodejs.org/" +Assert-Command "npm" "https://nodejs.org/" + +# Verify dotnet SDK version is 10.x +$dotnetSdkVersion = & dotnet --version 2>&1 | Select-Object -First 1 +if ($dotnetSdkVersion -notmatch '^10\.') { + Write-Host "" + Write-Host " WARN dotnet SDK version is '$dotnetSdkVersion'. This project targets .NET 10." -ForegroundColor Yellow + Write-Host " Proceeding, but consider installing .NET 10 SDK." -ForegroundColor Yellow +} + +# Verify the JsonRpc project file exists +if (-not (Test-Path $JsonRpcCsproj)) { + Write-Fail "Project file not found: $JsonRpcCsproj" + exit 1 +} +Write-Success "LightQueryProfiler.JsonRpc.csproj found" + +# Verify the SVG icon exists (needed for PNG conversion) +if (-not (Test-Path $IconSvg)) { + Write-Fail "SVG icon not found: $IconSvg" + exit 1 +} +Write-Success "icon.svg found" + +# --------------------------------------------------------------------------- +# STEP 2 — Clean previous outputs +# --------------------------------------------------------------------------- + +Write-Step 2 "Cleaning previous build outputs" + +if ($SkipClean) { + Write-Info "Skipped (--SkipClean flag set)" +} else { + # Remove bin/ (compiled .NET backend) + if (Test-Path $BinDir) { + Write-Info "Removing bin/ ..." + Remove-Item $BinDir -Recurse -Force + Write-Success "bin/ removed" + } else { + Write-Info "bin/ does not exist, nothing to clean" + } + + # Remove dist/ (compiled TypeScript) + if (Test-Path $DistDir) { + Write-Info "Removing dist/ ..." + Remove-Item $DistDir -Recurse -Force + Write-Success "dist/ removed" + } else { + Write-Info "dist/ does not exist, nothing to clean" + } + + # Remove any existing .vsix files in the extension directory + $existingVsix = @(Get-ChildItem -Path $ExtDir -Filter "*.vsix" -ErrorAction SilentlyContinue) + foreach ($vsix in $existingVsix) { + Write-Info "Removing $($vsix.Name) ..." + Remove-Item $vsix.FullName -Force + } + if ($existingVsix.Count -gt 0) { + Write-Success "$($existingVsix.Count) old .vsix file(s) removed" + } + + # Remove previously generated PNG icon (will be regenerated) + if (Test-Path $IconPng) { + Remove-Item $IconPng -Force + Write-Info "Old icon.png removed" + } +} + +# --------------------------------------------------------------------------- +# STEP 3 — Publish .NET backend in Release mode +# --------------------------------------------------------------------------- + +Write-Step 3 "Publishing .NET backend (Release, framework-dependent, AnyCPU)" +Write-Info "Projects: LightQueryProfiler.JsonRpc + LightQueryProfiler.Shared (via ProjectReference)" +Write-Info "Output : $BinDir" +Write-Info "" + +$publishArgs = @( + "publish", + $JsonRpcCsproj, + "--configuration", "Release", + "--no-self-contained", + "--output", $BinDir +) + +Write-Info "Running: dotnet $($publishArgs -join ' ')" +Write-Host "" + +& dotnet @publishArgs + +if ($LASTEXITCODE -ne 0) { + Write-Host "" + Write-Fail "dotnet publish failed with exit code $LASTEXITCODE" + exit 1 +} + +Write-Host "" +Write-Success "dotnet publish completed" + +# Quick sanity: count the DLLs published +$dllCount = (Get-ChildItem -Path $BinDir -Filter "*.dll" -Recurse).Count +Write-Info "$dllCount DLL(s) found in bin/" + +# --------------------------------------------------------------------------- +# STEP 4 — Convert SVG icon to PNG 128x128 +# --------------------------------------------------------------------------- + +Write-Step 4 "Converting icon.svg to icon.png (128x128)" + +# We use a small inline Node.js script that uses 'sharp'. +# 'sharp' may already be in node_modules after npm install, but since we run +# this step before npm install (to keep icon conversion independent), we use +# npx with --yes to auto-install sharp temporarily if needed. +# The sharp package is chosen because it works reliably on Windows/Linux/macOS +# and does not require any system-level libraries when installed via npm. + +$convertScript = @" +const sharp = require('sharp'); +const path = require('path'); +const src = path.join(__dirname, 'media', 'icon.svg'); +const dst = path.join(__dirname, 'media', 'icon.png'); +sharp(src) + .resize(128, 128) + .png() + .toFile(dst) + .then(() => { + console.log('icon.png created successfully (128x128)'); + process.exit(0); + }) + .catch(err => { + console.error('Error converting icon:', err.message); + process.exit(1); + }); +"@ + +# Write the inline script to a temp file inside the extension directory +# so that require('sharp') resolves from node_modules there (if present). +$tempScript = Join-Path $ExtDir "_icon_convert_temp.js" + +try { + Set-Content -Path $tempScript -Value $convertScript -Encoding UTF8 + + Write-Info "Running icon conversion via Node.js + sharp..." + + # First attempt: use sharp from node_modules (if already installed) + $sharpInNodeModules = Join-Path (Join-Path $ExtDir "node_modules") "sharp" + if (Test-Path $sharpInNodeModules) { + Write-Info "Using sharp from existing node_modules" + & node $tempScript + } else { + # Install sharp temporarily via npx + Write-Info "sharp not in node_modules, installing via npx (temporary)..." + & npx --yes --prefix $ExtDir sharp-cli@latest --input $IconSvg --output $IconPng resize 128 128 2>$null + if ($LASTEXITCODE -ne 0) { + # Fallback: install sharp directly and run the script + Write-Info "Falling back to: npm install --no-save sharp in extension dir..." + Push-Location $ExtDir + & npm install --no-save sharp 2>&1 | Out-Null + Pop-Location + & node $tempScript + } + } + + if ($LASTEXITCODE -ne 0) { + Write-Fail "Icon conversion failed. The .vsix cannot be packaged without icon.png." + Write-Info "Manual alternative: convert media/icon.svg to a 128x128 PNG and save as media/icon.png" + exit 1 + } + + if (-not (Test-Path $IconPng)) { + Write-Fail "icon.png was not created at expected path: $IconPng" + exit 1 + } + + $pngSize = (Get-Item $IconPng).Length + Write-Success "icon.png created ($pngSize bytes)" +} finally { + # Always clean up the temp script + if (Test-Path $tempScript) { + Remove-Item $tempScript -Force + } +} + +# --------------------------------------------------------------------------- +# STEP 5 — Install npm dependencies +# --------------------------------------------------------------------------- + +Write-Step 5 "Installing npm dependencies" + +if ($SkipNpmInstall) { + Write-Info "Skipped (--SkipNpmInstall flag set)" + if (-not (Test-Path (Join-Path $ExtDir "node_modules"))) { + Write-Fail "node_modules not found and --SkipNpmInstall was set. Run without --SkipNpmInstall first." + exit 1 + } +} else { + Write-Info "Running: npm install" + Push-Location $ExtDir + try { + & npm install + if ($LASTEXITCODE -ne 0) { + Write-Fail "npm install failed with exit code $LASTEXITCODE" + exit 1 + } + } finally { + Pop-Location + } + Write-Success "npm install completed" +} + +# --------------------------------------------------------------------------- +# STEP 6 — Compile TypeScript +# --------------------------------------------------------------------------- + +Write-Step 6 "Bundling TypeScript with esbuild" +Write-Info "Running: npm run bundle" + +Push-Location $ExtDir +try { + & npm run bundle + if ($LASTEXITCODE -ne 0) { + Write-Fail "esbuild bundle failed with exit code $LASTEXITCODE" + Write-Info "Fix the errors above before packaging." + exit 1 + } +} finally { + Pop-Location +} + +Write-Success "Extension bundled to dist/extension.js (all dependencies inlined)" + +# --------------------------------------------------------------------------- +# STEP 7 — Validate required output files +# --------------------------------------------------------------------------- + +Write-Step 7 "Validating build outputs" + +$requiredFiles = @( + @{ Path = (Join-Path $BinDir "LightQueryProfiler.JsonRpc.dll"); Desc = "JsonRpc server DLL" }, + @{ Path = (Join-Path $BinDir "LightQueryProfiler.Shared.dll"); Desc = "Shared library DLL" }, + @{ Path = (Join-Path $BinDir "LightQueryProfiler.JsonRpc.deps.json"); Desc = "deps.json (dependency manifest)" }, + @{ Path = (Join-Path $BinDir "LightQueryProfiler.JsonRpc.runtimeconfig.json"); Desc = "runtimeconfig.json" }, + @{ Path = (Join-Path $BinDir "Microsoft.Data.SqlClient.dll"); Desc = "Microsoft.Data.SqlClient.dll" }, + @{ Path = (Join-Path $BinDir "StreamJsonRpc.dll"); Desc = "StreamJsonRpc.dll" }, + @{ Path = (Join-Path (Join-Path (Join-Path (Join-Path $BinDir "runtimes") "win-x64") "native") "Microsoft.Data.SqlClient.SNI.dll"); Desc = "Native: win-x64 SqlClient SNI" }, + @{ Path = (Join-Path (Join-Path (Join-Path (Join-Path $BinDir "runtimes") "linux-x64") "native") "libe_sqlite3.so"); Desc = "Native: linux-x64 SQLite" }, + @{ Path = (Join-Path $DistDir "extension.js"); Desc = "Compiled extension entry point" }, + @{ Path = $IconPng; Desc = "icon.png (128x128)" } +) + +$allValid = $true +foreach ($item in $requiredFiles) { + if (Test-Path $item.Path) { + Write-Success $item.Desc + } else { + Write-Fail $item.Desc + Write-Info "Missing: $($item.Path)" + $allValid = $false + } +} + +if (-not $allValid) { + Write-Host "" + Write-Fail "One or more required files are missing. Aborting packaging." + exit 1 +} + +Write-Host "" +Write-Info "All required files present." + +# --------------------------------------------------------------------------- +# STEP 8 — Package with vsce +# --------------------------------------------------------------------------- + +Write-Step 8 "Packaging extension with vsce" + +# Determine version from package.json +$packageJson = Get-Content (Join-Path $ExtDir "package.json") -Raw | ConvertFrom-Json +$version = $packageJson.version +$name = $packageJson.name +$publisher = $packageJson.publisher +$vsixName = "$name-$version.vsix" +$vsixPath = Join-Path $ExtDir $vsixName + +Write-Info "Name : $name" +Write-Info "Version : $version" +Write-Info "Publisher : $publisher" +Write-Info "Output : $vsixPath" + +# Warn if publisher is still the placeholder +if ($publisher -eq "your-publisher-id") { + Write-Host "" + Write-Host " WARN 'publisher' in package.json is still set to 'your-publisher-id'." -ForegroundColor Yellow + Write-Host " The .vsix will be created but cannot be published to the Marketplace" -ForegroundColor Yellow + Write-Host " without a valid publisher ID registered at:" -ForegroundColor Yellow + Write-Host " https://marketplace.visualstudio.com/manage/publishers" -ForegroundColor Yellow + Write-Host "" +} + +Write-Info "Running: npx vsce package --out $vsixName" +Write-Host "" + +# vsce requires a LICENSE file in the extension directory. +# The repo has LICENSE.md at the root; copy it temporarily for packaging. +$rootLicense = Join-Path $RepoRoot "LICENSE.md" +$extLicense = Join-Path $ExtDir "LICENSE.md" +$licenseWasCopied = $false +if ((Test-Path $rootLicense) -and (-not (Test-Path $extLicense))) { + Copy-Item $rootLicense $extLicense + $licenseWasCopied = $true + Write-Info "LICENSE.md copied from repo root for packaging" +} + +Push-Location $ExtDir +try { + & npx vsce package --out $vsixName + if ($LASTEXITCODE -ne 0) { + Write-Host "" + Write-Fail "vsce package failed with exit code $LASTEXITCODE" + Write-Info "Common causes:" + Write-Info " - Missing README.md in vscode-extension/" + Write-Info " - Invalid icon path in package.json" + Write-Info " - TypeScript errors not caught at compile time" + exit 1 + } +} finally { + Pop-Location + # Clean up the temporarily copied LICENSE.md + if ($licenseWasCopied -and (Test-Path $extLicense)) { + Remove-Item $extLicense -Force + Write-Info "Temporary LICENSE.md removed" + } +} + +# --------------------------------------------------------------------------- +# STEP 9 — Report result +# --------------------------------------------------------------------------- + +Write-Step 9 "Build complete" + +if (-not (Test-Path $vsixPath)) { + Write-Fail ".vsix file not found at expected path: $vsixPath" + exit 1 +} + +$vsixSize = (Get-Item $vsixPath).Length +$vsixSizeMB = [math]::Round($vsixSize / 1MB, 2) + +Write-Host "" +Write-Host "================================================" -ForegroundColor Green +Write-Host " VSIX created successfully!" -ForegroundColor Green +Write-Host "================================================" -ForegroundColor Green +Write-Host "" +Write-Host " File : $vsixPath" -ForegroundColor White +Write-Host " Size : $vsixSizeMB MB ($vsixSize bytes)" -ForegroundColor White +Write-Host "" +Write-Host " To install locally:" -ForegroundColor Cyan +Write-Host " code --install-extension `"$vsixPath`"" -ForegroundColor White +Write-Host "" +Write-Host " To publish to VS Code Marketplace:" -ForegroundColor Cyan +Write-Host " 1. Set publisher in vscode-extension/package.json" -ForegroundColor White +Write-Host " 2. npx vsce login " -ForegroundColor White +Write-Host " 3. npx vsce publish" -ForegroundColor White +Write-Host "" diff --git a/vscode-extension/CHANGELOG.md b/vscode-extension/CHANGELOG.md new file mode 100644 index 0000000..83133bf --- /dev/null +++ b/vscode-extension/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog + +All notable changes to the Light Query Profiler extension will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2026-03-21 + +### Added +- Real-time SQL query profiling via Extended Events +- Support for SQL Server (2012+) and Azure SQL Database +- Windows Authentication, SQL Server Authentication, and Azure Active Directory modes +- Syntax-highlighted SQL query viewer using highlight.js +- Collapsible event cards with 15-column event table +- Full-text search and column filtering +- Detailed event inspection pane with tabbed view +- Sortable and resizable columns +- Export and import profiling sessions (JSON format) +- Duplicate event detection +- Cross-platform support: Windows, Linux, macOS (requires .NET 10) +- JSON-RPC communication bridge between VS Code and the .NET backend diff --git a/vscode-extension/README.md b/vscode-extension/README.md new file mode 100644 index 0000000..d3addeb --- /dev/null +++ b/vscode-extension/README.md @@ -0,0 +1,56 @@ +# Light Query Profiler + +A SQL Server and Azure SQL Database query profiler for Visual Studio Code, powered by [Extended Events](https://docs.microsoft.com/en-us/sql/relational-databases/extended-events/quick-start-extended-events-in-sql-server). + +## Features + +- Real-time query profiling for SQL Server and Azure SQL Database +- Support for Windows Authentication, SQL Server Authentication, and Azure Active Directory +- Syntax-highlighted SQL query viewer +- Event filtering and full-text search +- Sortable, resizable event columns +- Detailed event inspection with tabbed view + +## Requirements + +- **[.NET 10 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/10.0)** must be installed and available in your PATH. This is required to run the profiler backend server. +- SQL Server 2012 or later, or Azure SQL Database +- The SQL login must have `ALTER ANY EVENT SESSION` permission to create Extended Events sessions + +## Getting Started + +1. Install the extension +2. Open the Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) +3. Run **Light Query Profiler: Show SQL Profiler** +4. Enter your connection details: + - Server name or IP address + - Database name + - Authentication mode and credentials +5. Click **Start** to begin profiling + +## Authentication Modes + +| Mode | Description | +|---|---| +| Windows Authentication | Uses the current Windows user credentials (Windows only) | +| SQL Server Authentication | Username and password | +| Azure Active Directory | Azure AD authentication for Azure SQL Database | + +## Supported Platforms + +The extension works on **Windows**, **Linux**, and **macOS**, provided .NET 10 is installed. + +> **Note:** Extended Events sessions require appropriate permissions on the SQL Server instance. Azure SQL Database requires at least the `VIEW DATABASE STATE` permission. + +## Extension Settings + +This extension does not contribute any VS Code settings at this time. + +## Known Issues + +- Windows Authentication is only available when running VS Code on Windows +- Azure SQL Database Managed Instance may require additional firewall configuration + +## License + +MIT — see [LICENSE](https://github.com/brandochn/LightQueryProfiler/blob/main/LICENSE.md) diff --git a/vscode-extension/media/icon-small.svg b/vscode-extension/media/icon-small.svg new file mode 100644 index 0000000..1df79fe --- /dev/null +++ b/vscode-extension/media/icon-small.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/vscode-extension/media/icon.png b/vscode-extension/media/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1150e3f05d019eb6dc3042eec4b73edbc201abe2 GIT binary patch literal 5328 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 i5JL
-
- - - - - - - - - SQL Server Query Profiler +
+ Session Duration +
@@ -1845,7 +1853,20 @@ export class ProfilerPanelProvider { let searchAtWrapEnd = false; // true when user just hit next at last match (pending wrap forward) let searchAtWrapStart = false; // true when user just hit prev at first match (pending wrap back) + // Timer state + let sessionStartTime = null; // Date.now() snapshot when current run started + let timerInterval = null; // setInterval handle (1-second tick) + // ── Auth mode visibility ──────────────────────────────────────── + function formatDuration(ms) { + const totalSec = Math.floor(ms / 1000); + const h = Math.floor(totalSec / 3600); + const m = Math.floor((totalSec % 3600) / 60); + const s = totalSec % 60; + const pad = n => String(n).padStart(2, '0'); + return pad(h) + ':' + pad(m) + ':' + pad(s); + } + function updateAuthVisibility() { const mode = parseInt(authMode.value); const isWindows = mode === 0; @@ -2214,6 +2235,31 @@ export class ProfilerPanelProvider { resumeBtn.classList.toggle('hidden', !isPaused); resumeBtn.disabled = !isPaused; stopBtn.disabled = isStopped; + + // Timer + const timerEl = document.getElementById('sessionTimer'); + if (isRunning) { + // Reset and start fresh (also covers Resume → new run) + clearInterval(timerInterval); + sessionStartTime = Date.now(); + timerEl.className = 'session-timer running'; + timerEl.textContent = '00:00:00'; + timerInterval = setInterval(function() { + timerEl.textContent = formatDuration(Date.now() - sessionStartTime); + }, 1000); + } else if (isPaused) { + // Freeze display — stop ticking but keep current value + clearInterval(timerInterval); + timerInterval = null; + timerEl.className = 'session-timer'; + } else { + // Stopped — clear everything + clearInterval(timerInterval); + timerInterval = null; + sessionStartTime = null; + timerEl.className = 'session-timer'; + timerEl.textContent = '\u2014'; // — + } } // ── Add events ────────────────────────────────────────────────── @@ -2222,6 +2268,17 @@ export class ProfilerPanelProvider { const placeholder = eventsTableBody.querySelector('td[colspan]'); if (placeholder) { eventsTableBody.innerHTML = ''; } + // ── Scroll anchoring: capture selected row position before inserting ── + // When new rows are inserted above the selected row, the browser keeps + // scrollTop at the same absolute pixel value, which causes the selected + // row to drift downward visually. We compensate by measuring the delta + // in offsetTop before/after insertion and adjusting scrollTop to match. + // NOTE: this code runs as plain JavaScript in the webview — no TypeScript + // syntax (as casts, type annotations) is allowed here. + const anchorRow = selectedEventRow; + const anchorOffsetBefore = anchorRow ? anchorRow.offsetTop : null; + const scrollTopBefore = eventsContainer ? eventsContainer.scrollTop : 0; + events.forEach(event => { allEvents.push(event); @@ -2287,6 +2344,20 @@ export class ProfilerPanelProvider { eventsTableBody.insertBefore(row, eventsTableBody.firstChild); }); + // ── Scroll anchoring: restore visual position of selected row ── + // After inserting new rows at the top, compensate the container's + // scrollTop by the exact number of pixels the anchor row moved down. + // This keeps the selected row stationary on screen regardless of + // how many new events arrive. If no row is selected, scrollTop is + // left untouched so the table continues showing the newest events. + if (anchorRow !== null && anchorOffsetBefore !== null && eventsContainer) { + const anchorOffsetAfter = anchorRow.offsetTop; + const delta = anchorOffsetAfter - anchorOffsetBefore; + if (delta > 0) { + eventsContainer.scrollTop = scrollTopBefore + delta; + } + } + // Cap allEvents to prevent unbounded memory growth. // The cap only affects the backup array; the DOM table is not trimmed here // because removing oldest DOM rows would conflict with the newest-on-top