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) + ''; }