diff --git a/example/pgcompare.yaml b/example/pgcompare.yaml
index 1802527..cf1e326 100644
--- a/example/pgcompare.yaml
+++ b/example/pgcompare.yaml
@@ -8,10 +8,10 @@ setup:
benchmark:
before_queries: "queries_before.sql"
after_queries: "queries_after.sql"
- warmup_iterations: 5
iterations: 1000
+ warmup_iterations: 500
concurrency: 1
- repeats: 3
+ repeats: 5
report:
description:
diff --git a/example/queries_after.sql b/example/queries_after.sql
index ec89133..5075e9e 100644
--- a/example/queries_after.sql
+++ b/example/queries_after.sql
@@ -1,15 +1,15 @@
-- name: top_rated_blocked_drivers
SELECT d.id, u.display_name, d.rating
FROM drivers d
- JOIN users u ON u.id = d.user_id
+JOIN users u ON u.id = d.user_id
WHERE d.status = 'blocked'
ORDER BY d.rating DESC
- LIMIT 10;
+LIMIT 10;
-- name: latest_ride_status
SELECT DISTINCT ON (ride_id)
- ride_id, to_status, created_at
+ ride_id, to_status, created_at
FROM ride_status_events
ORDER BY ride_id, created_at DESC, id DESC;
@@ -17,25 +17,25 @@ ORDER BY ride_id, created_at DESC, id DESC;
-- name: failed_payments_by_amount
SELECT p.id, p.amount, pm.user_id
FROM payments p
- JOIN payment_methods pm ON pm.id = p.payment_method_id
+JOIN payment_methods pm ON pm.id = p.payment_method_id
WHERE p.status = 'failed'
ORDER BY p.amount DESC
- LIMIT 100;
+LIMIT 100;
-- name: top_final_fare_quotes
SELECT rfq.ride_id, rfq.amount, r.status AS ride_status
FROM ride_fare_quotes rfq
- JOIN rides r ON r.id = rfq.ride_id
+JOIN rides r ON r.id = rfq.ride_id
WHERE rfq.kind = 'final'
ORDER BY rfq.amount DESC
- LIMIT 10;
+LIMIT 10;
-- name: pickup_stops_with_coordinates
SELECT rs.ride_id, a.lat, a.lon
FROM ride_stops rs
- JOIN addresses a ON a.id = rs.address_id
+JOIN addresses a ON a.id = rs.address_id
WHERE rs.kind = 'pickup'
ORDER BY rs.ride_id
- LIMIT 100;
\ No newline at end of file
+LIMIT 100;
diff --git a/example/queries_before.sql b/example/queries_before.sql
index ec89133..5075e9e 100644
--- a/example/queries_before.sql
+++ b/example/queries_before.sql
@@ -1,15 +1,15 @@
-- name: top_rated_blocked_drivers
SELECT d.id, u.display_name, d.rating
FROM drivers d
- JOIN users u ON u.id = d.user_id
+JOIN users u ON u.id = d.user_id
WHERE d.status = 'blocked'
ORDER BY d.rating DESC
- LIMIT 10;
+LIMIT 10;
-- name: latest_ride_status
SELECT DISTINCT ON (ride_id)
- ride_id, to_status, created_at
+ ride_id, to_status, created_at
FROM ride_status_events
ORDER BY ride_id, created_at DESC, id DESC;
@@ -17,25 +17,25 @@ ORDER BY ride_id, created_at DESC, id DESC;
-- name: failed_payments_by_amount
SELECT p.id, p.amount, pm.user_id
FROM payments p
- JOIN payment_methods pm ON pm.id = p.payment_method_id
+JOIN payment_methods pm ON pm.id = p.payment_method_id
WHERE p.status = 'failed'
ORDER BY p.amount DESC
- LIMIT 100;
+LIMIT 100;
-- name: top_final_fare_quotes
SELECT rfq.ride_id, rfq.amount, r.status AS ride_status
FROM ride_fare_quotes rfq
- JOIN rides r ON r.id = rfq.ride_id
+JOIN rides r ON r.id = rfq.ride_id
WHERE rfq.kind = 'final'
ORDER BY rfq.amount DESC
- LIMIT 10;
+LIMIT 10;
-- name: pickup_stops_with_coordinates
SELECT rs.ride_id, a.lat, a.lon
FROM ride_stops rs
- JOIN addresses a ON a.id = rs.address_id
+JOIN addresses a ON a.id = rs.address_id
WHERE rs.kind = 'pickup'
ORDER BY rs.ride_id
- LIMIT 100;
\ No newline at end of file
+LIMIT 100;
diff --git a/example/report.html b/example/report.html
index 93db5a6..b943345 100644
--- a/example/report.html
+++ b/example/report.html
@@ -121,24 +121,24 @@
window.reportData = (function() {
var beforeStats = [];
- beforeStats.push({QueryName:"top_rated_blocked_drivers",Min: 475 ,Max: 1177 ,P50: 528 ,P95: 576 ,P99: 695 ,Mean: 534 ,StdDev: 44 ,QPS: 1869.5648256768307 ,ErrorRate: 0 });
- beforeStats.push({QueryName:"latest_ride_status",Min: 2273 ,Max: 6822 ,P50: 2503 ,P95: 3362 ,P99: 3715 ,Mean: 2636 ,StdDev: 384 ,QPS: 379.294179048093 ,ErrorRate: 0 });
- beforeStats.push({QueryName:"failed_payments_by_amount",Min: 1194 ,Max: 2280 ,P50: 1287 ,P95: 1347 ,P99: 1419 ,Mean: 1296 ,StdDev: 51 ,QPS: 771.053148602549 ,ErrorRate: 0 });
- beforeStats.push({QueryName:"top_final_fare_quotes",Min: 1220 ,Max: 3209 ,P50: 1304 ,P95: 1347 ,P99: 1529 ,Mean: 1312 ,StdDev: 100 ,QPS: 761.6940027701256 ,ErrorRate: 0 });
- beforeStats.push({QueryName:"pickup_stops_with_coordinates",Min: 191 ,Max: 414 ,P50: 228 ,P95: 258 ,P99: 282 ,Mean: 230 ,StdDev: 23 ,QPS: 4332.600762754364 ,ErrorRate: 0 });
+ beforeStats.push({QueryName:"top_rated_blocked_drivers",Min: 490 ,Max: 1675 ,P50: 566 ,P95: 616 ,P99: 701 ,Mean: 581 ,StdDev: 57 ,QPS: 1720.0433145435572 ,ErrorRate: 0 });
+ beforeStats.push({QueryName:"latest_ride_status",Min: 2274 ,Max: 6716 ,P50: 2526 ,P95: 3435 ,P99: 4006 ,Mean: 2791 ,StdDev: 365 ,QPS: 358.264951543861 ,ErrorRate: 0 });
+ beforeStats.push({QueryName:"failed_payments_by_amount",Min: 1250 ,Max: 3552 ,P50: 1347 ,P95: 1407 ,P99: 1553 ,Mean: 1356 ,StdDev: 102 ,QPS: 737.1581972238112 ,ErrorRate: 0 });
+ beforeStats.push({QueryName:"top_final_fare_quotes",Min: 1257 ,Max: 4939 ,P50: 1343 ,P95: 1458 ,P99: 1904 ,Mean: 1376 ,StdDev: 175 ,QPS: 726.446119290833 ,ErrorRate: 0 });
+ beforeStats.push({QueryName:"pickup_stops_with_coordinates",Min: 163 ,Max: 363 ,P50: 199 ,P95: 231 ,P99: 250 ,Mean: 203 ,StdDev: 17 ,QPS: 4923.963733490522 ,ErrorRate: 0 });
var afterStats = [];
- afterStats.push({QueryName:"top_rated_blocked_drivers",Min: 85 ,Max: 245 ,P50: 113 ,P95: 138 ,P99: 156 ,Mean: 115 ,StdDev: 16 ,QPS: 8645.869482974498 ,ErrorRate: 0 });
- afterStats.push({QueryName:"latest_ride_status",Min: 3015 ,Max: 5710 ,P50: 3264 ,P95: 3621 ,P99: 4210 ,Mean: 3317 ,StdDev: 222 ,QPS: 301.389564180672 ,ErrorRate: 0 });
- afterStats.push({QueryName:"failed_payments_by_amount",Min: 161 ,Max: 575 ,P50: 187 ,P95: 212 ,P99: 271 ,Mean: 192 ,StdDev: 23 ,QPS: 5196.733169313002 ,ErrorRate: 0 });
- afterStats.push({QueryName:"top_final_fare_quotes",Min: 85 ,Max: 212 ,P50: 113 ,P95: 133 ,P99: 150 ,Mean: 114 ,StdDev: 12 ,QPS: 8758.523137828499 ,ErrorRate: 0 });
- afterStats.push({QueryName:"pickup_stops_with_coordinates",Min: 159 ,Max: 348 ,P50: 176 ,P95: 196 ,P99: 224 ,Mean: 179 ,StdDev: 12 ,QPS: 5583.66777176758 ,ErrorRate: 0 });
+ afterStats.push({QueryName:"top_rated_blocked_drivers",Min: 90 ,Max: 225 ,P50: 116 ,P95: 136 ,P99: 151 ,Mean: 118 ,StdDev: 12 ,QPS: 8409.435854593488 ,ErrorRate: 0 });
+ afterStats.push({QueryName:"latest_ride_status",Min: 2146 ,Max: 6025 ,P50: 3241 ,P95: 3384 ,P99: 3921 ,Mean: 3144 ,StdDev: 262 ,QPS: 318.02284353505587 ,ErrorRate: 0 });
+ afterStats.push({QueryName:"failed_payments_by_amount",Min: 169 ,Max: 308 ,P50: 189 ,P95: 209 ,P99: 223 ,Mean: 190 ,StdDev: 10 ,QPS: 5235.034517828297 ,ErrorRate: 0 });
+ afterStats.push({QueryName:"top_final_fare_quotes",Min: 92 ,Max: 194 ,P50: 116 ,P95: 130 ,P99: 141 ,Mean: 116 ,StdDev: 8 ,QPS: 8575.492742453298 ,ErrorRate: 0 });
+ afterStats.push({QueryName:"pickup_stops_with_coordinates",Min: 148 ,Max: 640 ,P50: 175 ,P95: 199 ,P99: 235 ,Mean: 176 ,StdDev: 27 ,QPS: 5671.03329425084 ,ErrorRate: 0 });
var diffs = [];
- diffs.push({QueryName:"top_rated_blocked_drivers",Summary:["Sort -\u003e Nested Loop","Explicit Sort removed","Merge Join -\u003e Index Scan on drivers","Index added: idx_drivers_status_rating","Actual rows changed: 294 -\u003e 10",],BeforeText:"-\u003e Limit (rows=10 time=605µs)\n -\u003e Sort (rows=10 time=605µs)\n -\u003e Merge Join (rows=294 time=578µs)\n -\u003e Index Scan on users using users_pkey (rows=4999 time=267µs)\n -\u003e Sort (rows=294 time=152µs)\n -\u003e Seq Scan on drivers (rows=294 time=131µs)\n",AfterText:"-\u003e Limit (rows=10 time=28µs)\n -\u003e Nested Loop (rows=10 time=27µs)\n -\u003e Index Scan on drivers using idx_drivers_status_rating (rows=10 time=10µs)\n -\u003e Index Scan on users using users_pkey (rows=1 time=1µs)\n"});
- diffs.push({QueryName:"latest_ride_status",Summary:["Unique -\u003e Result","Sort -\u003e Unique","Explicit Sort removed","Seq Scan -\u003e Index Only Scan on ride_status_events","Index added: idx_ride_status_events_ride_created_id",],BeforeText:"-\u003e Unique (rows=10000 time=1.598ms)\n -\u003e Sort (rows=10000 time=1.105ms)\n -\u003e Seq Scan on ride_status_events (rows=10000 time=457µs)\n",AfterText:"-\u003e Result (rows=10000 time=1.627ms)\n -\u003e Unique (rows=10000 time=1.168ms)\n -\u003e Index Only Scan on ride_status_events using idx_ride_status_events_ride_created_id (rows=10000 time=465µs)\n"});
- diffs.push({QueryName:"failed_payments_by_amount",Summary:["Sort -\u003e Nested Loop","Explicit Sort removed","Merge Join -\u003e Index Scan on payments","Index added: idx_payments_status_amount","Actual rows changed: 2000 -\u003e 100",],BeforeText:"-\u003e Limit (rows=100 time=1.651ms)\n -\u003e Sort (rows=100 time=1.648ms)\n -\u003e Merge Join (rows=2000 time=1.464ms)\n -\u003e Index Scan on payment_methods using payment_methods_pkey (rows=10001 time=536µs)\n -\u003e Sort (rows=2000 time=485µs)\n -\u003e Seq Scan on payments (rows=2000 time=350µs)\n",AfterText:"-\u003e Limit (rows=100 time=140µs)\n -\u003e Nested Loop (rows=100 time=136µs)\n -\u003e Index Scan on payments using idx_payments_status_amount (rows=100 time=46µs)\n -\u003e Index Scan on payment_methods using payment_methods_pkey (rows=1 time=1µs)\n"});
- diffs.push({QueryName:"top_final_fare_quotes",Summary:["Sort -\u003e Nested Loop","Explicit Sort removed","Hash Join -\u003e Index Only Scan on ride_fare_quotes","Index added: idx_rfq_kind_amount","Actual rows changed: 5000 -\u003e 10",],BeforeText:"-\u003e Limit (rows=10 time=1.814ms)\n -\u003e Sort (rows=10 time=1.813ms)\n -\u003e Hash Join (rows=5000 time=1.532ms)\n -\u003e Seq Scan on ride_fare_quotes (rows=5000 time=324µs)\n -\u003e Hash (rows=10000 time=832µs)\n -\u003e Seq Scan on rides (rows=10000 time=414µs)\n",AfterText:"-\u003e Limit (rows=10 time=14µs)\n -\u003e Nested Loop (rows=10 time=13µs)\n -\u003e Index Only Scan on ride_fare_quotes using idx_rfq_kind_amount (rows=10 time=4µs)\n -\u003e Index Scan on rides using rides_pkey (rows=1 time=1µs)\n"});
- diffs.push({QueryName:"pickup_stops_with_coordinates",Summary:["Index Scan -\u003e Index Only Scan on ride_stops","Index changed: uq_ride_stops_ride_stop_index -\u003e idx_ride_stops_pickup_ride","Index Scan -\u003e Index Only Scan on addresses","Index changed: addresses_pkey -\u003e idx_addresses_id_coords",],BeforeText:"-\u003e Limit (rows=100 time=63µs)\n -\u003e Nested Loop (rows=100 time=61µs)\n -\u003e Index Scan on ride_stops using uq_ride_stops_ride_stop_index (rows=100 time=18µs)\n -\u003e Index Scan on addresses using addresses_pkey (rows=1 time=0s)\n",AfterText:"-\u003e Limit (rows=100 time=46µs)\n -\u003e Nested Loop (rows=100 time=41µs)\n -\u003e Index Only Scan on ride_stops using idx_ride_stops_pickup_ride (rows=100 time=6µs)\n -\u003e Index Only Scan on addresses using idx_addresses_id_coords (rows=1 time=0s)\n"});
- var speedups = [ 4.150610261026102 , 0.9284661373835001 , 6.3320396742986285 , 10.110694183864915 , 1.3166944491173338 ,];
+ diffs.push({QueryName:"top_rated_blocked_drivers",Summary:["Sort -\u003e Nested Loop","Explicit Sort removed","Merge Join -\u003e Index Scan on drivers","Index added: idx_drivers_status_rating","Actual rows changed: 294 -\u003e 10",],BeforeText:"-\u003e Limit (rows=10 time=613µs)\n -\u003e Sort (rows=10 time=613µs)\n -\u003e Merge Join (rows=294 time=587µs)\n -\u003e Index Scan on users using users_pkey (rows=4999 time=270µs)\n -\u003e Sort (rows=294 time=158µs)\n -\u003e Seq Scan on drivers (rows=294 time=136µs)\n",AfterText:"-\u003e Limit (rows=10 time=26µs)\n -\u003e Nested Loop (rows=10 time=26µs)\n -\u003e Index Scan on drivers using idx_drivers_status_rating (rows=10 time=10µs)\n -\u003e Index Scan on users using users_pkey (rows=1 time=1µs)\n"});
+ diffs.push({QueryName:"latest_ride_status",Summary:["Unique -\u003e Result","Sort -\u003e Unique","Explicit Sort removed","Seq Scan -\u003e Index Only Scan on ride_status_events","Index added: idx_ride_status_events_ride_created_id",],BeforeText:"-\u003e Unique (rows=10000 time=1.696ms)\n -\u003e Sort (rows=10000 time=1.183ms)\n -\u003e Seq Scan on ride_status_events (rows=10000 time=476µs)\n",AfterText:"-\u003e Result (rows=10000 time=1.492ms)\n -\u003e Unique (rows=10000 time=1.044999ms)\n -\u003e Index Only Scan on ride_status_events using idx_ride_status_events_ride_created_id (rows=10000 time=434µs)\n"});
+ diffs.push({QueryName:"failed_payments_by_amount",Summary:["Sort -\u003e Nested Loop","Explicit Sort removed","Merge Join -\u003e Index Scan on payments","Index added: idx_payments_status_amount","Actual rows changed: 2000 -\u003e 100",],BeforeText:"-\u003e Limit (rows=100 time=1.688ms)\n -\u003e Sort (rows=100 time=1.685ms)\n -\u003e Merge Join (rows=2000 time=1.512ms)\n -\u003e Index Scan on payment_methods using payment_methods_pkey (rows=10001 time=549µs)\n -\u003e Sort (rows=2000 time=502µs)\n -\u003e Seq Scan on payments (rows=2000 time=363µs)\n",AfterText:"-\u003e Limit (rows=100 time=144µs)\n -\u003e Nested Loop (rows=100 time=141µs)\n -\u003e Index Scan on payments using idx_payments_status_amount (rows=100 time=47µs)\n -\u003e Index Scan on payment_methods using payment_methods_pkey (rows=1 time=1µs)\n"});
+ diffs.push({QueryName:"top_final_fare_quotes",Summary:["Sort -\u003e Nested Loop","Explicit Sort removed","Hash Join -\u003e Index Only Scan on ride_fare_quotes","Index added: idx_rfq_kind_amount","Actual rows changed: 5000 -\u003e 10",],BeforeText:"-\u003e Limit (rows=10 time=1.916ms)\n -\u003e Sort (rows=10 time=1.915ms)\n -\u003e Hash Join (rows=5000 time=1.594ms)\n -\u003e Seq Scan on ride_fare_quotes (rows=5000 time=343µs)\n -\u003e Hash (rows=10000 time=867µs)\n -\u003e Seq Scan on rides (rows=10000 time=430µs)\n",AfterText:"-\u003e Limit (rows=10 time=14µs)\n -\u003e Nested Loop (rows=10 time=14µs)\n -\u003e Index Only Scan on ride_fare_quotes using idx_rfq_kind_amount (rows=10 time=5µs)\n -\u003e Index Scan on rides using rides_pkey (rows=1 time=1µs)\n"});
+ diffs.push({QueryName:"pickup_stops_with_coordinates",Summary:["Index Scan -\u003e Index Only Scan on ride_stops","Index changed: uq_ride_stops_ride_stop_index -\u003e idx_ride_stops_pickup_ride","Index Scan -\u003e Index Only Scan on addresses","Index changed: addresses_pkey -\u003e idx_addresses_id_coords",],BeforeText:"-\u003e Limit (rows=100 time=65µs)\n -\u003e Nested Loop (rows=100 time=62µs)\n -\u003e Index Scan on ride_stops using uq_ride_stops_ride_stop_index (rows=100 time=18µs)\n -\u003e Index Scan on addresses using addresses_pkey (rows=1 time=0s)\n",AfterText:"-\u003e Limit (rows=100 time=45µs)\n -\u003e Nested Loop (rows=100 time=42µs)\n -\u003e Index Only Scan on ride_stops using idx_ride_stops_pickup_ride (rows=100 time=6µs)\n -\u003e Index Only Scan on addresses using idx_addresses_id_coords (rows=1 time=0s)\n"});
+ var speedups = [ 4.517431972789115 , 1.0149449348155262 , 6.709973778307509 , 11.213046554190186 , 1.159796997123624 ,];
var description = [];
description.push({Query:"top_rated_blocked_drivers",What:"Добавлен покрывающий индекс idx_drivers_status_rating по status и rating DESC",Changes:"\"CREATE INDEX idx_drivers_status_rating ON drivers(status, rating DESC) INCLUDE(user_id)\"",Expected:"Index Only Scan с ранним выходом при LIMIT 10 вместо Seq Scan \u002b Sort по всем заблокированным водителям"});
description.push({Query:"latest_ride_status",What:"Добавлен покрывающий индекс idx_ride_status_events_ride_created_id под DISTINCT ON по ride_id",Changes:"\"CREATE INDEX idx_ride_status_events_ride_created_id ON ride_status_events(ride_id, created_at DESC, id DESC) INCLUDE(to_status)\"",Expected:"DISTINCT ON использует Index Only Scan без шага Sort вместо Seq Scan \u002b Sort \u002b WindowAgg"});
@@ -146,11 +146,11 @@
description.push({Query:"top_final_fare_quotes",What:"Добавлен покрывающий индекс idx_rfq_kind_amount по kind и amount DESC",Changes:"\"CREATE INDEX idx_rfq_kind_amount ON ride_fare_quotes(kind, amount DESC) INCLUDE(ride_id)\"",Expected:"Index Only Scan с ранним выходом при LIMIT 10 вместо Seq Scan \u002b Filter \u002b Sort по 5000 строкам"});
description.push({Query:"pickup_stops_with_coordinates",What:"Добавлены индексы idx_ride_stops_pickup_ride и idx_addresses_id_coords для выборки pickup и координат",Changes:"\"CREATE INDEX idx_ride_stops_pickup_ride ON ride_stops(ride_id) INCLUDE(address_id) WHERE kind='pickup'; CREATE INDEX idx_addresses_id_coords ON addresses(id) INCLUDE(lat, lon)\"",Expected:"Partial Index Only Scan на ride_stops с ранним выходом при LIMIT 100 вместо Seq Scan 30k строк \u002b Sort; NL join на addresses через Index Only Scan"});
return {
- GeneratedAt: "19.04.2026, 18:02:05",
+ GeneratedAt: "19.04.2026, 22:55:37",
Iterations: 1000 ,
- WarmupIterations: 50 ,
+ WarmupIterations: 500 ,
Concurrency: 1 ,
- Repeats: 3 ,
+ Repeats: 5 ,
Speedups: speedups,
Description: description,
Before: {Phase: "before", Stats: beforeStats},
@@ -174,8 +174,10 @@
}
function fmtSpeedup(f) {
- if (!f || f < 1.05) return { text: '1.0\u00d7', cls: 'badge-neutral' };
- return { text: f.toFixed(1) + '\u00d7', cls: 'badge-speedup' };
+ if (!f) return { text: '1.0\u00d7', cls: 'badge-neutral' };
+ if (f >= 1.05) return { text: f.toFixed(1) + '\u00d7', cls: 'badge-speedup' };
+ if (f <= 0.95) return { text: f.toFixed(1) + '\u00d7', cls: 'badge-bad' };
+ return { text: '1.0\u00d7', cls: 'badge-neutral' };
}
function badge(d) { return '' + esc(d.text) + ''; }
diff --git a/internal/pgcompare/report_test.go b/internal/pgcompare/report_test.go
index cf8b3b0..3c1345a 100644
--- a/internal/pgcompare/report_test.go
+++ b/internal/pgcompare/report_test.go
@@ -1,10 +1,13 @@
package pgcompare
import (
+ "os"
+ "path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestRenderPlan(t *testing.T) {
@@ -47,3 +50,32 @@ func TestRenderPlan(t *testing.T) {
assert.Contains(t, rendered, " -> Index Scan on users using idx_users")
})
}
+
+func TestFmtSpeedupMarksRegressions(t *testing.T) {
+ dir := t.TempDir()
+ out := filepath.Join(dir, "report.html")
+
+ err := Generate(ReportData{
+ GeneratedAt: time.Now(),
+ Speedups: []float64{2.0, 0.5, 1.0},
+ Before: &BenchResult{Phase: "before"},
+ After: &BenchResult{Phase: "after"},
+ }, out)
+ require.NoError(t, err)
+
+ html, err := os.ReadFile(out)
+ require.NoError(t, err)
+
+ body := string(html)
+
+ assert.Contains(t, body, "f >= 1.05",
+ "fmtSpeedup must treat values >= 1.05 as speedup")
+ assert.Contains(t, body, "f <= 0.95",
+ "fmtSpeedup must treat values <= 0.95 as regression")
+ assert.Contains(t, body, "'badge-bad'",
+ "fmtSpeedup must render regressions with badge-bad")
+ assert.NotRegexp(t,
+ `if \(!f \|\| f < 1\.05\) return \{ text: '1\.0\\u00d7', cls: 'badge-neutral' \};`,
+ body,
+ "old single-branch logic must be removed so regressions are no longer shown as neutral 1.0×")
+}
diff --git a/internal/pgcompare/templates/report.html b/internal/pgcompare/templates/report.html
index cb2a258..e60a293 100644
--- a/internal/pgcompare/templates/report.html
+++ b/internal/pgcompare/templates/report.html
@@ -158,8 +158,10 @@
}
function fmtSpeedup(f) {
- if (!f || f < 1.05) return { text: '1.0\u00d7', cls: 'badge-neutral' };
- return { text: f.toFixed(1) + '\u00d7', cls: 'badge-speedup' };
+ if (!f) return { text: '1.0\u00d7', cls: 'badge-neutral' };
+ if (f >= 1.05) return { text: f.toFixed(1) + '\u00d7', cls: 'badge-speedup' };
+ if (f <= 0.95) return { text: f.toFixed(1) + '\u00d7', cls: 'badge-bad' };
+ return { text: '1.0\u00d7', cls: 'badge-neutral' };
}
function badge(d) { return '' + esc(d.text) + ''; }