_TESTS/benchmark-comparison.ps1
|
# =========================================================================== # PSLocalBase vs Flat JSON Benchmark Suite # Compares PSLocalBase (embedded SQLite NoSQL) against Native PowerShell JSON File # Test Scale: 5,000 records (Single Collection & Dual Collections) # =========================================================================== [CmdletBinding()] param( [int]$RecordCount = 5000, [string]$OutputPath = "" ) $ErrorActionPreference = "Stop" $swTotal = [System.Diagnostics.Stopwatch]::StartNew() $modulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PSLocalBase.psd1" Import-Module $modulePath -Force $benchDir = Join-Path $PSScriptRoot "temp_bench_data" if (Test-Path $benchDir) { Remove-Item -Recurse -Force $benchDir } New-Item -ItemType Directory -Path $benchDir -Force | Out-Null $results = [ordered]@{} function Measure-Operation { param( [string]$Name, [scriptblock]$Action ) [System.GC]::Collect() [System.GC]::WaitForPendingFinalizers() $memBefore = [System.GC]::GetTotalMemory($false) $sw = [System.Diagnostics.Stopwatch]::StartNew() $res = & $Action $sw.Stop() $memAfter = [System.GC]::GetTotalMemory($false) $memDeltaKB = [math]::Max(0, [math]::Round(($memAfter - $memBefore) / 1KB, 2)) return @{ DurationMs = [math]::Round($sw.Elapsed.TotalMilliseconds, 2) MemDeltaKB = $memDeltaKB Result = $res } } Write-Host "==========================================================" -ForegroundColor Cyan Write-Host " PSLocalBase vs Flat JSON File Benchmark ($RecordCount Records)" -ForegroundColor Cyan Write-Host " PowerShell Version: $($PSVersionTable.PSVersion)" -ForegroundColor DarkGray Write-Host "==========================================================" -ForegroundColor Cyan # --------------------------------------------------------------------------- # Data Generation (5,000 records) # --------------------------------------------------------------------------- Write-Host "[1/6] Generating synthetic dataset of $RecordCount records..." -ForegroundColor Yellow $cities = @("New York", "London", "Tokyo", "Paris", "Berlin", "Madrid", "Toronto", "Sydney") $departments = @("Engineering", "Sales", "Support", "Finance", "HR", "Marketing") $domains = @("cloud.com", "tech.org", "enterprise.net", "dev.io") $singleColData = [System.Collections.Generic.List[hashtable]]::new($RecordCount) for ($i = 1; $i -le $RecordCount; $i++) { $dept = $departments[$i % $departments.Length] $city = $cities[$i % $cities.Length] $active = ($i % 3 -ne 0) $score = ($i * 17) % 100 $singleColData.Add(@{ id = "usr-$i" name = "User $i" email = "user$i@$($domains[$i % $domains.Length])" department = $dept city = $city active = $active score = $score tags = @("tag$($i % 10)", "role$($i % 5)", "tier$($i % 3)") profile = @{ loginCount = ($i * 7) % 500 theme = $(if ($i % 2 -eq 0) { "dark" } else { "light" }) } }) } # --------------------------------------------------------------------------- # BENCHMARK SCENARIO 1: Single Collection (5,000 Records) # --------------------------------------------------------------------------- Write-Host "`n>>> Running SCENARIO 1: Single Collection (5,000 Records)" -ForegroundColor Green $dbPath = Join-Path $benchDir "pslocalbase_single.db" $jsonPath = Join-Path $benchDir "flat_single.json" # --- Operation 1: Bulk Insert / File Write --- Write-Host " -> Operation 1: Bulk Ingestion (5,000 records)..." -NoNewline $m_pslb_insert = Measure-Operation "PSLB Bulk Set" { $db = [PSLocalBase]::new($dbPath) try { $db.collection("users").set($singleColData) } finally { $db.close() } } Write-Host " PSLB: $($m_pslb_insert.DurationMs) ms |" -ForegroundColor Cyan -NoNewline $m_json_insert = Measure-Operation "JSON File Write" { $jsonStr = ConvertTo-Json -InputObject $singleColData -Compress -Depth 10 [System.IO.File]::WriteAllText($jsonPath, $jsonStr, [System.Text.Encoding]::UTF8) } Write-Host " Flat JSON: $($m_json_insert.DurationMs) ms" -ForegroundColor Magenta # --- Operation 2: Point Lookups (100 individual lookups by ID) --- $lookupKeys = 1..100 | ForEach-Object { "usr-$((($_ * 47) % $RecordCount) + 1)" } Write-Host " -> Operation 2: Point Lookups (100 random IDs)..." -NoNewline $m_pslb_lookup = Measure-Operation "PSLB 100 Lookups" { $db = [PSLocalBase]::new($dbPath) try { $count = 0 foreach ($k in $lookupKeys) { $doc = $db.collection("users").doc($k).get() if ($null -ne $doc) { $count++ } } $count } finally { $db.close() } } Write-Host " PSLB: $($m_pslb_lookup.DurationMs) ms |" -ForegroundColor Cyan -NoNewline $m_json_lookup = Measure-Operation "JSON 100 Lookups" { $raw = [System.IO.File]::ReadAllText($jsonPath, [System.Text.Encoding]::UTF8) $items = ConvertFrom-Json -InputObject $raw $count = 0 foreach ($k in $lookupKeys) { $found = $null for ($j = 0; $j -lt $items.Count; $j++) { if ($items[$j].id -eq $k) { $found = $items[$j] break } } if ($null -ne $found) { $count++ } } $count } Write-Host " Flat JSON: $($m_json_lookup.DurationMs) ms" -ForegroundColor Magenta # --- Operation 3: Filtered Query (active = true AND score >= 80) --- Write-Host " -> Operation 3: Multi-criteria Query Filtering..." -NoNewline $m_pslb_query = Measure-Operation "PSLB Filter" { $db = [PSLocalBase]::new($dbPath) try { $res = $db.collection("users").where("active", "=", $true).where("score", ">=", 80).get() $res.Count } finally { $db.close() } } Write-Host " PSLB: $($m_pslb_query.DurationMs) ms |" -ForegroundColor Cyan -NoNewline $m_json_query = Measure-Operation "JSON Filter" { $raw = [System.IO.File]::ReadAllText($jsonPath, [System.Text.Encoding]::UTF8) $items = ConvertFrom-Json -InputObject $raw $matched = [System.Collections.Generic.List[object]]::new() foreach ($item in $items) { if ($item.active -eq $true -and $item.score -ge 80) { $matched.Add($item) } } $matched.Count } Write-Host " Flat JSON: $($m_json_query.DurationMs) ms" -ForegroundColor Magenta # --- Operation 4: OrderBy & Pagination (Top 10 by score desc) --- Write-Host " -> Operation 4: Sort & Pagination (Top 10 DESC)..." -NoNewline $m_pslb_sort = Measure-Operation "PSLB Sort Limit" { $db = [PSLocalBase]::new($dbPath) try { $res = $db.collection("users").orderBy("score", "desc").limit(10).get() $res.Count } finally { $db.close() } } Write-Host " PSLB: $($m_pslb_sort.DurationMs) ms |" -ForegroundColor Cyan -NoNewline $m_json_sort = Measure-Operation "JSON Sort Limit" { $raw = [System.IO.File]::ReadAllText($jsonPath, [System.Text.Encoding]::UTF8) $items = ConvertFrom-Json -InputObject $raw $sorted = $items | Sort-Object -Property score -Descending | Select-Object -First 10 ($sorted | Measure-Object).Count } Write-Host " Flat JSON: $($m_json_sort.DurationMs) ms" -ForegroundColor Magenta # --- Operation 5: Scalar Record Count --- Write-Host " -> Operation 5: Record Count..." -NoNewline $m_pslb_count = Measure-Operation "PSLB Count" { $db = [PSLocalBase]::new($dbPath) try { $db.collection("users").where("department", "=", "Engineering").count() } finally { $db.close() } } Write-Host " PSLB: $($m_pslb_count.DurationMs) ms |" -ForegroundColor Cyan -NoNewline $m_json_count = Measure-Operation "JSON Count" { $raw = [System.IO.File]::ReadAllText($jsonPath, [System.Text.Encoding]::UTF8) $items = ConvertFrom-Json -InputObject $raw $count = 0 foreach ($item in $items) { if ($item.department -eq "Engineering") { $count++ } } $count } Write-Host " Flat JSON: $($m_json_count.DurationMs) ms" -ForegroundColor Magenta # --- Operation 6: Partial Updates (10 random documents updated) --- $updateKeys = 1..10 | ForEach-Object { "usr-$((($_ * 89) % $RecordCount) + 1)" } Write-Host " -> Operation 6: Partial Updates (10 documents)..." -NoNewline $m_pslb_update = Measure-Operation "PSLB 10 Updates" { $db = [PSLocalBase]::new($dbPath) try { foreach ($k in $updateKeys) { $db.collection("users").doc($k).update(@{ lastLogin = (Get-Date).ToString("o") status = "verified" }) } } finally { $db.close() } } Write-Host " PSLB: $($m_pslb_update.DurationMs) ms |" -ForegroundColor Cyan -NoNewline $m_json_update = Measure-Operation "JSON 10 Updates" { # In flat JSON persistence, each persistent update requires reading the file, # updating the target record in memory, serializing to JSON, and overwriting disk. foreach ($k in $updateKeys) { $raw = [System.IO.File]::ReadAllText($jsonPath, [System.Text.Encoding]::UTF8) $items = ConvertFrom-Json -InputObject $raw for ($j = 0; $j -lt $items.Count; $j++) { if ($items[$j].id -eq $k) { $items[$j] | Add-Member -MemberType NoteProperty -Name "lastLogin" -Value (Get-Date).ToString("o") -Force $items[$j] | Add-Member -MemberType NoteProperty -Name "status" -Value "verified" -Force break } } $updatedJson = ConvertTo-Json -InputObject $items -Compress -Depth 10 [System.IO.File]::WriteAllText($jsonPath, $updatedJson, [System.Text.Encoding]::UTF8) } } Write-Host " Flat JSON: $($m_json_update.DurationMs) ms" -ForegroundColor Magenta # --- Operation 7: Single Document Deletions (10 documents deleted) --- $deleteKeys = 1..10 | ForEach-Object { "usr-$((($_ * 113) % $RecordCount) + 1)" } Write-Host " -> Operation 7: Individual Deletions (10 documents)..." -NoNewline $m_pslb_delete = Measure-Operation "PSLB 10 Deletes" { $db = [PSLocalBase]::new($dbPath) try { foreach ($k in $deleteKeys) { $db.collection("users").doc($k).delete() } } finally { $db.close() } } Write-Host " PSLB: $($m_pslb_delete.DurationMs) ms |" -ForegroundColor Cyan -NoNewline $m_json_delete = Measure-Operation "JSON 10 Deletes" { foreach ($k in $deleteKeys) { $raw = [System.IO.File]::ReadAllText($jsonPath, [System.Text.Encoding]::UTF8) $items = ConvertFrom-Json -InputObject $raw $filteredList = [System.Collections.Generic.List[object]]::new() foreach ($item in $items) { if ($item.id -ne $k) { $filteredList.Add($item) } } $updatedJson = ConvertTo-Json -InputObject $filteredList -Compress -Depth 10 [System.IO.File]::WriteAllText($jsonPath, $updatedJson, [System.Text.Encoding]::UTF8) } } Write-Host " Flat JSON: $($m_json_delete.DurationMs) ms" -ForegroundColor Magenta # --------------------------------------------------------------------------- # BENCHMARK SCENARIO 2: Dual Collections (5,000 Records Total: 2,500 Users + 2,500 Orders) # --------------------------------------------------------------------------- Write-Host "`n>>> Running SCENARIO 2: Dual Collections & Cross-Collection JOIN (5,000 Records Total)" -ForegroundColor Green $dbPathDual = Join-Path $benchDir "pslocalbase_dual.db" $usersJsonPath = Join-Path $benchDir "flat_users.json" $ordersJsonPath = Join-Path $benchDir "flat_orders.json" $half = [int]($RecordCount / 2) $usersList = [System.Collections.Generic.List[hashtable]]::new($half) $ordersList = [System.Collections.Generic.List[hashtable]]::new($half) for ($i = 1; $i -le $half; $i++) { $usersList.Add(@{ id = "u-$i" name = "Customer $i" tier = $(if ($i % 5 -eq 0) { "Platinum" } elseif ($i % 2 -eq 0) { "Gold" } else { "Silver" }) city = $cities[$i % $cities.Length] }) $ordersList.Add(@{ id = "ord-$i" userId = "u-$((($i * 3) % $half) + 1)" item = "SKU-$((($i * 11) % 500) + 1)" amount = [math]::Round((($i * 23.45) % 1500) + 10, 2) status = $(if ($i % 4 -eq 0) { "Delivered" } elseif ($i % 2 -eq 0) { "Shipped" } else { "Pending" }) }) } # --- Operation 8: Dual Collection Ingestion --- Write-Host " -> Operation 8: Ingestion Across 2 Collections (5,000 total)..." -NoNewline $m_pslb_dual_insert = Measure-Operation "PSLB Dual Ingestion" { $db = [PSLocalBase]::new($dbPathDual) try { $db.collection("users").set($usersList) $db.collection("orders").set($ordersList) } finally { $db.close() } } Write-Host " PSLB: $($m_pslb_dual_insert.DurationMs) ms |" -ForegroundColor Cyan -NoNewline $m_json_dual_insert = Measure-Operation "JSON Dual Ingestion" { $uJson = ConvertTo-Json -InputObject $usersList -Compress -Depth 10 $oJson = ConvertTo-Json -InputObject $ordersList -Compress -Depth 10 [System.IO.File]::WriteAllText($usersJsonPath, $uJson, [System.Text.Encoding]::UTF8) [System.IO.File]::WriteAllText($ordersJsonPath, $oJson, [System.Text.Encoding]::UTF8) } Write-Host " Flat JSON: $($m_json_dual_insert.DurationMs) ms" -ForegroundColor Magenta # --- Operation 9: Cross-Collection Relational JOIN (Orders with Customer Details) --- Write-Host " -> Operation 9: Relational Cross-Collection Query (JOIN)..." -NoNewline $m_pslb_join = Measure-Operation "PSLB SQL JOIN" { $db = [PSLocalBase]::new($dbPathDual) try { $sql = @" SELECT json_extract(o.doc, '$.id') AS OrderId, json_extract(u.doc, '$.name') AS CustomerName, json_extract(u.doc, '$.tier') AS Tier, CAST(json_extract(o.doc, '$.amount') AS REAL) AS OrderAmount, json_extract(o.doc, '$.status') AS Status FROM orders o INNER JOIN users u ON json_extract(o.doc, '$.userId') = u.key WHERE CAST(json_extract(o.doc, '$.amount') AS REAL) > 500 ORDER BY OrderAmount DESC LIMIT 100; "@ $res = $db.query($sql) $res.Count } finally { $db.close() } } Write-Host " PSLB: $($m_pslb_join.DurationMs) ms |" -ForegroundColor Cyan -NoNewline $m_json_join = Measure-Operation "JSON Client-side JOIN" { # Flat JSON requires reading both files, indexing in a dictionary, and iterating $uRaw = [System.IO.File]::ReadAllText($usersJsonPath, [System.Text.Encoding]::UTF8) $oRaw = [System.IO.File]::ReadAllText($ordersJsonPath, [System.Text.Encoding]::UTF8) $uItems = ConvertFrom-Json -InputObject $uRaw $oItems = ConvertFrom-Json -InputObject $oRaw # Build lookup hashtable $userMap = @{} foreach ($u in $uItems) { $userMap[$u.id] = $u } $joined = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($ord in $oItems) { if ($ord.amount -gt 500) { $matchedUser = $userMap[$ord.userId] if ($null -ne $matchedUser) { $joined.Add([PSCustomObject]@{ OrderId = $ord.id CustomerName = $matchedUser.name Tier = $matchedUser.tier OrderAmount = $ord.amount Status = $ord.status }) } } } $sorted = $joined | Sort-Object -Property OrderAmount -Descending | Select-Object -First 100 ($sorted | Measure-Object).Count } Write-Host " Flat JSON: $($m_json_join.DurationMs) ms" -ForegroundColor Magenta # --------------------------------------------------------------------------- # Storage & Disk Metrics # --------------------------------------------------------------------------- $pslbFileSize = (Get-Item $dbPath).Length $jsonFileSize = (Get-Item $jsonPath).Length $pslbFileSizeKB = [math]::Round($pslbFileSize / 1KB, 2) $jsonFileSizeKB = [math]::Round($jsonFileSize / 1KB, 2) # --------------------------------------------------------------------------- # Summary Data Consolidation # --------------------------------------------------------------------------- $benchData = [ordered]@{ Metadata = @{ Date = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") PowerShellVersion = $PSVersionTable.PSVersion.ToString() OS = [System.Runtime.InteropServices.RuntimeInformation]::OSDescription TotalRecords = $RecordCount } Scenario1_SingleCollection = @( @{ Operation = "Bulk Insertion (5,000 docs)" PSLocalBaseMs = $m_pslb_insert.DurationMs FlatJsonMs = $m_json_insert.DurationMs Speedup = [math]::Round($m_json_insert.DurationMs / [math]::Max(0.01, $m_pslb_insert.DurationMs), 2) Winner = if ($m_pslb_insert.DurationMs -le $m_json_insert.DurationMs) { "PSLocalBase" } else { "Flat JSON" } }, @{ Operation = "Point Lookups (100 random IDs)" PSLocalBaseMs = $m_pslb_lookup.DurationMs FlatJsonMs = $m_json_lookup.DurationMs Speedup = [math]::Round($m_json_lookup.DurationMs / [math]::Max(0.01, $m_pslb_lookup.DurationMs), 2) Winner = if ($m_pslb_lookup.DurationMs -le $m_json_lookup.DurationMs) { "PSLocalBase" } else { "Flat JSON" } }, @{ Operation = "Multi-filter Query (active AND score >= 80)" PSLocalBaseMs = $m_pslb_query.DurationMs FlatJsonMs = $m_json_query.DurationMs Speedup = [math]::Round($m_json_query.DurationMs / [math]::Max(0.01, $m_pslb_query.DurationMs), 2) Winner = if ($m_pslb_query.DurationMs -le $m_json_query.DurationMs) { "PSLocalBase" } else { "Flat JSON" } }, @{ Operation = "Sorting & Pagination (Top 10 DESC)" PSLocalBaseMs = $m_pslb_sort.DurationMs FlatJsonMs = $m_json_sort.DurationMs Speedup = [math]::Round($m_json_sort.DurationMs / [math]::Max(0.01, $m_pslb_sort.DurationMs), 2) Winner = if ($m_pslb_sort.DurationMs -le $m_json_sort.DurationMs) { "PSLocalBase" } else { "Flat JSON" } }, @{ Operation = "Scalar Aggregate Count" PSLocalBaseMs = $m_pslb_count.DurationMs FlatJsonMs = $m_json_count.DurationMs Speedup = [math]::Round($m_json_count.DurationMs / [math]::Max(0.01, $m_pslb_count.DurationMs), 2) Winner = if ($m_pslb_count.DurationMs -le $m_json_count.DurationMs) { "PSLocalBase" } else { "Flat JSON" } }, @{ Operation = "Partial Updates (10 documents)" PSLocalBaseMs = $m_pslb_update.DurationMs FlatJsonMs = $m_json_update.DurationMs Speedup = [math]::Round($m_json_update.DurationMs / [math]::Max(0.01, $m_pslb_update.DurationMs), 2) Winner = if ($m_pslb_update.DurationMs -le $m_json_update.DurationMs) { "PSLocalBase" } else { "Flat JSON" } }, @{ Operation = "Individual Deletions (10 documents)" PSLocalBaseMs = $m_pslb_delete.DurationMs FlatJsonMs = $m_json_delete.DurationMs Speedup = [math]::Round($m_json_delete.DurationMs / [math]::Max(0.01, $m_pslb_delete.DurationMs), 2) Winner = if ($m_pslb_delete.DurationMs -le $m_json_delete.DurationMs) { "PSLocalBase" } else { "Flat JSON" } } ) Scenario2_DualCollections = @( @{ Operation = "Dual Collection Ingestion (5,000 total)" PSLocalBaseMs = $m_pslb_dual_insert.DurationMs FlatJsonMs = $m_json_dual_insert.DurationMs Speedup = [math]::Round($m_json_dual_insert.DurationMs / [math]::Max(0.01, $m_pslb_dual_insert.DurationMs), 2) Winner = if ($m_pslb_dual_insert.DurationMs -le $m_json_dual_insert.DurationMs) { "PSLocalBase" } else { "Flat JSON" } }, @{ Operation = "Relational JOIN & Filter (Orders + Users)" PSLocalBaseMs = $m_pslb_join.DurationMs FlatJsonMs = $m_json_join.DurationMs Speedup = [math]::Round($m_json_join.DurationMs / [math]::Max(0.01, $m_pslb_join.DurationMs), 2) Winner = if ($m_pslb_join.DurationMs -le $m_json_join.DurationMs) { "PSLocalBase" } else { "Flat JSON" } } ) DiskMetrics = @{ PSLocalBaseSizeKB = $pslbFileSizeKB FlatJsonSizeKB = $jsonFileSizeKB Ratio = [math]::Round($pslbFileSizeKB / $jsonFileSizeKB, 2) } } $swTotal.Stop() Write-Host "`n==========================================================" -ForegroundColor Cyan Write-Host " Benchmark Suite Finished in $([math]::Round($swTotal.Elapsed.TotalSeconds, 2)) seconds" -ForegroundColor Cyan Write-Host " Database File Size: $pslbFileSizeKB KB | Flat JSON File Size: $jsonFileSizeKB KB" -ForegroundColor DarkGray Write-Host "==========================================================" -ForegroundColor Cyan # Output results as JSON $jsonResultPath = Join-Path $benchDir "benchmark_summary.json" $benchData | ConvertTo-Json -Depth 5 | Set-Content $jsonResultPath -Encoding UTF8 Write-Host "Results serialized to: $jsonResultPath" -ForegroundColor Green return $benchData |