pg_peek 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/pg_peek/application.js +25 -1
  3. data/app/assets/stylesheets/pg_peek/application.css +48 -2
  4. data/app/controllers/pg_peek/databases_controller.rb +12 -10
  5. data/app/controllers/pg_peek/endpoints_controller.rb +15 -3
  6. data/app/controllers/pg_peek/jobs_controller.rb +1 -1
  7. data/app/controllers/pg_peek/pg_stat_statements_controller.rb +17 -6
  8. data/app/controllers/pg_peek/tables_controller.rb +11 -1
  9. data/app/helpers/pg_peek/application_helper.rb +27 -11
  10. data/app/helpers/pg_peek/report_helper.rb +32 -8
  11. data/app/models/pg_peek/pg_stat_statements.rb +51 -4
  12. data/app/models/pg_peek/sql_format.rb +74 -0
  13. data/app/models/pg_peek/table.rb +4 -3
  14. data/app/queries/pg_peek/pg14/pg_stat_statements/outliers.sql +1 -1
  15. data/app/queries/pg_peek/pg14/pg_stat_statements/outliers_by_endpoint.sql +24 -0
  16. data/app/queries/pg_peek/pg14/pg_stat_statements/outliers_by_job.sql +1 -1
  17. data/app/queries/pg_peek/pg14/tables/dead_tuples.sql +7 -3
  18. data/app/queries/pg_peek/pg15/pg_stat_statements/outliers.sql +1 -1
  19. data/app/queries/pg_peek/pg15/pg_stat_statements/outliers_by_endpoint.sql +24 -0
  20. data/app/queries/pg_peek/pg15/pg_stat_statements/outliers_by_job.sql +1 -1
  21. data/app/queries/pg_peek/pg15/tables/dead_tuples.sql +7 -3
  22. data/app/queries/pg_peek/pg16/pg_stat_statements/outliers.sql +1 -1
  23. data/app/queries/pg_peek/pg16/pg_stat_statements/outliers_by_endpoint.sql +24 -0
  24. data/app/queries/pg_peek/pg16/pg_stat_statements/outliers_by_job.sql +1 -1
  25. data/app/queries/pg_peek/pg16/tables/dead_tuples.sql +7 -3
  26. data/app/queries/pg_peek/pg17/pg_stat_statements/outliers.sql +1 -1
  27. data/app/queries/pg_peek/pg17/pg_stat_statements/outliers_by_endpoint.sql +24 -0
  28. data/app/queries/pg_peek/pg17/pg_stat_statements/outliers_by_job.sql +1 -1
  29. data/app/queries/pg_peek/pg17/tables/dead_tuples.sql +7 -3
  30. data/app/queries/pg_peek/pg18/pg_stat_statements/outliers.sql +1 -1
  31. data/app/queries/pg_peek/pg18/pg_stat_statements/outliers_by_endpoint.sql +24 -0
  32. data/app/queries/pg_peek/pg18/pg_stat_statements/outliers_by_job.sql +1 -1
  33. data/app/queries/pg_peek/pg18/tables/dead_tuples.sql +7 -3
  34. data/app/reports/pg_peek/column.rb +6 -1
  35. data/app/reports/pg_peek/report.rb +1 -2
  36. data/app/reports/pg_peek/reports/endpoint_queries.rb +14 -0
  37. data/app/reports/pg_peek/reports/endpoints.rb +5 -8
  38. data/app/reports/pg_peek/reports/jobs.rb +7 -7
  39. data/app/reports/pg_peek/reports/queries.rb +2 -1
  40. data/app/views/layouts/pg_peek/application.html.erb +1 -0
  41. data/app/views/pg_peek/databases/show.html.erb +8 -60
  42. data/app/views/pg_peek/endpoints/index.html.erb +14 -1
  43. data/app/views/pg_peek/endpoints/show.html.erb +18 -0
  44. data/app/views/pg_peek/jobs/index.html.erb +1 -8
  45. data/app/views/pg_peek/jobs/show.html.erb +2 -1
  46. data/app/views/pg_peek/pg_stat_statements/show.html.erb +87 -0
  47. data/app/views/pg_peek/tables/index.html.erb +18 -0
  48. data/app/views/pg_peek/tables/show.html.erb +2 -2
  49. data/config/routes.rb +5 -2
  50. data/lib/generators/pg_peek/install/templates/initializer.rb +9 -0
  51. data/lib/pg_peek/configuration.rb +8 -1
  52. data/lib/pg_peek/version.rb +1 -1
  53. metadata +12 -2
@@ -0,0 +1,24 @@
1
+ -- The query shapes one controller#action issued. The endpoint is read from the
2
+ -- tags exactly as endpoints.sql reads it, so this page lists the same shapes
3
+ -- that endpoints.sql counts for it. An equality, not a LIKE: with both tags on,
4
+ -- controller='posts' also appears in the queries of admin/posts.
5
+ SELECT
6
+ interval '1 millisecond' * total_exec_time AS total_exec_time,
7
+ COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
8
+ to_char(calls, 'FM999G999G999G990') AS ncalls,
9
+ ROUND(total_exec_time/NULLIF(calls, 0)) AS avg_exec_ms,
10
+ interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time,
11
+ query
12
+ FROM pg_stat_statements
13
+ WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
14
+ AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
15
+ AND query NOT LIKE '%/* pg_peek */%'
16
+ AND COALESCE(
17
+ substring(query from 'namespaced_controller=''([^'']+)'''),
18
+ substring(query from 'controller=''([^'']+)''')
19
+ ) = {{controller}}
20
+ AND substring(query from 'action=''([^'']+)''') = {{action}}
21
+ AND query !~* 'pg_catalog|information_schema|pg_index|pg_class|pg_attribute'
22
+ AND query !~* '^\s*SHOW '
23
+ ORDER BY total_exec_time DESC
24
+ LIMIT {{limit}}
@@ -2,7 +2,7 @@ SELECT
2
2
  interval '1 millisecond' * total_exec_time AS total_exec_time,
3
3
  COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
4
4
  to_char(calls, 'FM999G999G999G990') AS ncalls,
5
- ROUND(total_exec_time/NULLIF(calls, 0)) AS avg_exec_ms,
5
+ total_exec_time/NULLIF(calls, 0) AS avg_exec_ms,
6
6
  interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time,
7
7
  query
8
8
  FROM pg_stat_statements
@@ -1,4 +1,7 @@
1
- -- Tables whose dead-tuple share is at or above the warning threshold.
1
+ -- Tables that autovacuum should have cleaned by now: more dead tuples than its
2
+ -- own trigger point (autovacuum_vacuum_threshold + scale_factor * live rows),
3
+ -- and a dead-tuple share at or above the warning threshold. Small tables never
4
+ -- reach the trigger point, so a high share alone is not a problem.
2
5
  SELECT
3
6
  relname,
4
7
  n_live_tup,
@@ -6,6 +9,7 @@ SELECT
6
9
  round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 1) AS dead_ratio,
7
10
  last_autovacuum
8
11
  FROM pg_stat_user_tables
9
- WHERE n_live_tup + n_dead_tup > 0
10
- AND 100.0 * n_dead_tup / (n_live_tup + n_dead_tup) >= {{threshold}}
12
+ WHERE n_dead_tup > current_setting('autovacuum_vacuum_threshold')::int
13
+ + current_setting('autovacuum_vacuum_scale_factor')::float * n_live_tup
14
+ AND 100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0) >= {{threshold}}
11
15
  ORDER BY dead_ratio DESC
@@ -2,7 +2,7 @@ SELECT
2
2
  interval '1 millisecond' * total_exec_time AS total_exec_time,
3
3
  COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
4
4
  to_char(calls, 'FM999G999G999G990') AS ncalls,
5
- ROUND(total_exec_time/NULLIF(calls, 0)) AS avg_exec_ms,
5
+ total_exec_time/NULLIF(calls, 0) AS avg_exec_ms,
6
6
  interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time,
7
7
  query
8
8
  FROM pg_stat_statements
@@ -0,0 +1,24 @@
1
+ -- The query shapes one controller#action issued. The endpoint is read from the
2
+ -- tags exactly as endpoints.sql reads it, so this page lists the same shapes
3
+ -- that endpoints.sql counts for it. An equality, not a LIKE: with both tags on,
4
+ -- controller='posts' also appears in the queries of admin/posts.
5
+ SELECT
6
+ interval '1 millisecond' * total_exec_time AS total_exec_time,
7
+ COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
8
+ to_char(calls, 'FM999G999G999G990') AS ncalls,
9
+ ROUND(total_exec_time/NULLIF(calls, 0)) AS avg_exec_ms,
10
+ interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time,
11
+ query
12
+ FROM pg_stat_statements
13
+ WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
14
+ AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
15
+ AND query NOT LIKE '%/* pg_peek */%'
16
+ AND COALESCE(
17
+ substring(query from 'namespaced_controller=''([^'']+)'''),
18
+ substring(query from 'controller=''([^'']+)''')
19
+ ) = {{controller}}
20
+ AND substring(query from 'action=''([^'']+)''') = {{action}}
21
+ AND query !~* 'pg_catalog|information_schema|pg_index|pg_class|pg_attribute'
22
+ AND query !~* '^\s*SHOW '
23
+ ORDER BY total_exec_time DESC
24
+ LIMIT {{limit}}
@@ -2,7 +2,7 @@ SELECT
2
2
  interval '1 millisecond' * total_exec_time AS total_exec_time,
3
3
  COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
4
4
  to_char(calls, 'FM999G999G999G990') AS ncalls,
5
- ROUND(total_exec_time/NULLIF(calls, 0)) AS avg_exec_ms,
5
+ total_exec_time/NULLIF(calls, 0) AS avg_exec_ms,
6
6
  interval '1 millisecond' * (blk_read_time + blk_write_time) AS sync_io_time,
7
7
  query
8
8
  FROM pg_stat_statements
@@ -1,4 +1,7 @@
1
- -- Tables whose dead-tuple share is at or above the warning threshold.
1
+ -- Tables that autovacuum should have cleaned by now: more dead tuples than its
2
+ -- own trigger point (autovacuum_vacuum_threshold + scale_factor * live rows),
3
+ -- and a dead-tuple share at or above the warning threshold. Small tables never
4
+ -- reach the trigger point, so a high share alone is not a problem.
2
5
  SELECT
3
6
  relname,
4
7
  n_live_tup,
@@ -6,6 +9,7 @@ SELECT
6
9
  round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 1) AS dead_ratio,
7
10
  last_autovacuum
8
11
  FROM pg_stat_user_tables
9
- WHERE n_live_tup + n_dead_tup > 0
10
- AND 100.0 * n_dead_tup / (n_live_tup + n_dead_tup) >= {{threshold}}
12
+ WHERE n_dead_tup > current_setting('autovacuum_vacuum_threshold')::int
13
+ + current_setting('autovacuum_vacuum_scale_factor')::float * n_live_tup
14
+ AND 100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0) >= {{threshold}}
11
15
  ORDER BY dead_ratio DESC
@@ -2,7 +2,7 @@ SELECT
2
2
  interval '1 millisecond' * total_exec_time AS total_exec_time,
3
3
  COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
4
4
  to_char(calls, 'FM999G999G999G990') AS ncalls,
5
- ROUND(total_exec_time/NULLIF(calls, 0)) AS avg_exec_ms,
5
+ total_exec_time/NULLIF(calls, 0) AS avg_exec_ms,
6
6
  interval '1 millisecond' * (shared_blk_read_time + shared_blk_write_time) AS sync_io_time,
7
7
  query
8
8
  FROM pg_stat_statements
@@ -0,0 +1,24 @@
1
+ -- The query shapes one controller#action issued. The endpoint is read from the
2
+ -- tags exactly as endpoints.sql reads it, so this page lists the same shapes
3
+ -- that endpoints.sql counts for it. An equality, not a LIKE: with both tags on,
4
+ -- controller='posts' also appears in the queries of admin/posts.
5
+ SELECT
6
+ interval '1 millisecond' * total_exec_time AS total_exec_time,
7
+ COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
8
+ to_char(calls, 'FM999G999G999G990') AS ncalls,
9
+ ROUND(total_exec_time/NULLIF(calls, 0)) AS avg_exec_ms,
10
+ interval '1 millisecond' * (shared_blk_read_time + shared_blk_write_time) AS sync_io_time,
11
+ query
12
+ FROM pg_stat_statements
13
+ WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
14
+ AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
15
+ AND query NOT LIKE '%/* pg_peek */%'
16
+ AND COALESCE(
17
+ substring(query from 'namespaced_controller=''([^'']+)'''),
18
+ substring(query from 'controller=''([^'']+)''')
19
+ ) = {{controller}}
20
+ AND substring(query from 'action=''([^'']+)''') = {{action}}
21
+ AND query !~* 'pg_catalog|information_schema|pg_index|pg_class|pg_attribute'
22
+ AND query !~* '^\s*SHOW '
23
+ ORDER BY total_exec_time DESC
24
+ LIMIT {{limit}}
@@ -2,7 +2,7 @@ SELECT
2
2
  interval '1 millisecond' * total_exec_time AS total_exec_time,
3
3
  COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
4
4
  to_char(calls, 'FM999G999G999G990') AS ncalls,
5
- ROUND(total_exec_time/NULLIF(calls, 0)) AS avg_exec_ms,
5
+ total_exec_time/NULLIF(calls, 0) AS avg_exec_ms,
6
6
  interval '1 millisecond' * (shared_blk_read_time + shared_blk_write_time) AS sync_io_time,
7
7
  query
8
8
  FROM pg_stat_statements
@@ -1,4 +1,7 @@
1
- -- Tables whose dead-tuple share is at or above the warning threshold.
1
+ -- Tables that autovacuum should have cleaned by now: more dead tuples than its
2
+ -- own trigger point (autovacuum_vacuum_threshold + scale_factor * live rows),
3
+ -- and a dead-tuple share at or above the warning threshold. Small tables never
4
+ -- reach the trigger point, so a high share alone is not a problem.
2
5
  SELECT
3
6
  relname,
4
7
  n_live_tup,
@@ -6,6 +9,7 @@ SELECT
6
9
  round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 1) AS dead_ratio,
7
10
  last_autovacuum
8
11
  FROM pg_stat_user_tables
9
- WHERE n_live_tup + n_dead_tup > 0
10
- AND 100.0 * n_dead_tup / (n_live_tup + n_dead_tup) >= {{threshold}}
12
+ WHERE n_dead_tup > current_setting('autovacuum_vacuum_threshold')::int
13
+ + current_setting('autovacuum_vacuum_scale_factor')::float * n_live_tup
14
+ AND 100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0) >= {{threshold}}
11
15
  ORDER BY dead_ratio DESC
@@ -2,7 +2,7 @@ SELECT
2
2
  interval '1 millisecond' * total_exec_time AS total_exec_time,
3
3
  COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
4
4
  to_char(calls, 'FM999G999G999G990') AS ncalls,
5
- ROUND(total_exec_time/NULLIF(calls, 0)) AS avg_exec_ms,
5
+ total_exec_time/NULLIF(calls, 0) AS avg_exec_ms,
6
6
  interval '1 millisecond' * (shared_blk_read_time + shared_blk_write_time) AS sync_io_time,
7
7
  query
8
8
  FROM pg_stat_statements
@@ -0,0 +1,24 @@
1
+ -- The query shapes one controller#action issued. The endpoint is read from the
2
+ -- tags exactly as endpoints.sql reads it, so this page lists the same shapes
3
+ -- that endpoints.sql counts for it. An equality, not a LIKE: with both tags on,
4
+ -- controller='posts' also appears in the queries of admin/posts.
5
+ SELECT
6
+ interval '1 millisecond' * total_exec_time AS total_exec_time,
7
+ COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
8
+ to_char(calls, 'FM999G999G999G990') AS ncalls,
9
+ ROUND(total_exec_time/NULLIF(calls, 0)) AS avg_exec_ms,
10
+ interval '1 millisecond' * (shared_blk_read_time + shared_blk_write_time) AS sync_io_time,
11
+ query
12
+ FROM pg_stat_statements
13
+ WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
14
+ AND dbid = (SELECT oid FROM pg_database WHERE datname = current_database())
15
+ AND query NOT LIKE '%/* pg_peek */%'
16
+ AND COALESCE(
17
+ substring(query from 'namespaced_controller=''([^'']+)'''),
18
+ substring(query from 'controller=''([^'']+)''')
19
+ ) = {{controller}}
20
+ AND substring(query from 'action=''([^'']+)''') = {{action}}
21
+ AND query !~* 'pg_catalog|information_schema|pg_index|pg_class|pg_attribute'
22
+ AND query !~* '^\s*SHOW '
23
+ ORDER BY total_exec_time DESC
24
+ LIMIT {{limit}}
@@ -2,7 +2,7 @@ SELECT
2
2
  interval '1 millisecond' * total_exec_time AS total_exec_time,
3
3
  COALESCE(to_char((total_exec_time/NULLIF(sum(total_exec_time) OVER(), 0)) * 100, 'FM990D0'), '0') || '%' AS prop_exec_time,
4
4
  to_char(calls, 'FM999G999G999G990') AS ncalls,
5
- ROUND(total_exec_time/NULLIF(calls, 0)) AS avg_exec_ms,
5
+ total_exec_time/NULLIF(calls, 0) AS avg_exec_ms,
6
6
  interval '1 millisecond' * (shared_blk_read_time + shared_blk_write_time) AS sync_io_time,
7
7
  query
8
8
  FROM pg_stat_statements
@@ -1,4 +1,7 @@
1
- -- Tables whose dead-tuple share is at or above the warning threshold.
1
+ -- Tables that autovacuum should have cleaned by now: more dead tuples than its
2
+ -- own trigger point (autovacuum_vacuum_threshold + scale_factor * live rows),
3
+ -- and a dead-tuple share at or above the warning threshold. Small tables never
4
+ -- reach the trigger point, so a high share alone is not a problem.
2
5
  SELECT
3
6
  relname,
4
7
  n_live_tup,
@@ -6,6 +9,7 @@ SELECT
6
9
  round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 1) AS dead_ratio,
7
10
  last_autovacuum
8
11
  FROM pg_stat_user_tables
9
- WHERE n_live_tup + n_dead_tup > 0
10
- AND 100.0 * n_dead_tup / (n_live_tup + n_dead_tup) >= {{threshold}}
12
+ WHERE n_dead_tup > current_setting('autovacuum_vacuum_threshold')::int
13
+ + current_setting('autovacuum_vacuum_scale_factor')::float * n_live_tup
14
+ AND 100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0) >= {{threshold}}
11
15
  ORDER BY dead_ratio DESC
@@ -7,7 +7,9 @@ class PgPeek::Column
7
7
 
8
8
  attr_reader :key, :align, :format, :title
9
9
 
10
- def initialize(key, header: nil, align: :left, format: :text, title: nil)
10
+ # bar: draw the value with a bar next to it, scaled against the largest value
11
+ # in the column, so the biggest rows stand out without reading every figure.
12
+ def initialize(key, header: nil, align: :left, format: :text, title: nil, bar: false)
11
13
  raise ArgumentError, "unknown alignment #{align.inspect}" unless ALIGNMENTS.include?(align)
12
14
 
13
15
  @key = key
@@ -15,12 +17,15 @@ class PgPeek::Column
15
17
  @align = align
16
18
  @format = format
17
19
  @title = title
20
+ @bar = bar
18
21
  end
19
22
 
20
23
  def header
21
24
  @header || key.to_s.tr("_", " ")
22
25
  end
23
26
 
27
+ def bar? = @bar
28
+
24
29
  def numeric?
25
30
  align == :right
26
31
  end
@@ -43,8 +43,7 @@ class PgPeek::Report
43
43
 
44
44
  def title = self.class.title
45
45
 
46
- # Relative formats -- an intensity bar, a share of total -- need the extent of
47
- # the column, not just the cell.
46
+ # A bar is relative: it needs the extent of the column, not just the cell.
48
47
  def max_for(column)
49
48
  @maxima ||= {}
50
49
  @maxima[column.key] ||= rows.filter_map { |row| row.value(column)&.to_f }.max
@@ -0,0 +1,14 @@
1
+ # The query shapes one controller#action issues, most expensive first.
2
+ class PgPeek::Reports::EndpointQueries < PgPeek::Reports::Queries
3
+ attr_reader :endpoint
4
+
5
+ def initialize(database:, endpoint:)
6
+ super(database: database)
7
+ @endpoint = endpoint
8
+ end
9
+
10
+ private
11
+ def source_rows
12
+ pg_stat_statements.outliers_by_endpoint(endpoint)
13
+ end
14
+ end
@@ -6,9 +6,9 @@ class PgPeek::Reports::Endpoints < PgPeek::Report
6
6
  # the shape of an N+1 rather than a page that legitimately asks twice.
7
7
  N_PLUS_ONE_THRESHOLD = 5
8
8
 
9
- column :endpoint
10
- column :total_exec_time_ms, header: "db time", align: :right, format: :duration_ms
11
- column :share, align: :right, format: :intensity, title: "Share of database time across endpoints"
9
+ column :endpoint, format: :name
10
+ column :total_exec_time_ms, header: "db time", align: :right, format: :duration_ms, bar: true,
11
+ title: "Database time; the bar compares it with the other endpoints listed"
12
12
  column :request_count, header: "requests", align: :right, format: :number,
13
13
  title: "Estimated from the least-called query of this endpoint"
14
14
  column :total_calls, header: "queries", align: :right, format: :number
@@ -26,10 +26,7 @@ class PgPeek::Reports::Endpoints < PgPeek::Report
26
26
  rows = pg_stat_statements.endpoints
27
27
  return [] if rows.blank?
28
28
 
29
- rows.each do |row|
30
- # namespaced_controller arrives url-encoded: staff%2Fprocesses#show.
31
- row["endpoint"] = CGI.unescape(row["endpoint"].to_s)
32
- row["share"] = row["total_exec_time_ms"]
33
- end
29
+ # namespaced_controller arrives url-encoded: staff%2Fprocesses#show.
30
+ rows.each { |row| row["endpoint"] = CGI.unescape(row["endpoint"].to_s) }
34
31
  end
35
32
  end
@@ -4,18 +4,18 @@
4
4
  class PgPeek::Reports::Jobs < PgPeek::Report
5
5
  title "Jobs"
6
6
 
7
- column :job_class, header: "job"
8
- column :total_exec_time_ms, header: "db time", align: :right, format: :duration_ms
9
- column :share, align: :right, format: :intensity, title: "Share of database time across jobs"
7
+ column :job_class, header: "job", format: :name
8
+ column :total_exec_time_ms, header: "db time", align: :right, format: :duration_ms, bar: true,
9
+ title: "Database time; the bar compares it with the other jobs listed"
10
10
  column :total_calls, header: "queries", align: :right, format: :number
11
11
  column :query_count, header: "shapes", align: :right, format: :number,
12
12
  title: "Distinct query shapes issued by this job"
13
13
 
14
14
  private
15
15
  def fetch_rows
16
- rows = pg_stat_statements.jobs
17
- return [] if rows.blank?
18
-
19
- rows.each { |row| row["share"] = row["total_exec_time_ms"] }
16
+ pg_stat_statements.jobs.to_a.each do |row|
17
+ # The job tag arrives url-encoded: Reports%3A%3ADigestJob.
18
+ row["job_class"] = CGI.unescape(row["job_class"].to_s)
19
+ end
20
20
  end
21
21
  end
@@ -2,7 +2,8 @@
2
2
  class PgPeek::Reports::Queries < PgPeek::Report
3
3
  title "Queries"
4
4
 
5
- column :calls, align: :right, format: :intensity, title: "Call count relative to the other queries listed"
5
+ column :calls, align: :right, format: :number, bar: true,
6
+ title: "Call count; the bar compares it with the other queries listed"
6
7
  column :total_exec_time, header: "db time", align: :right, format: :duration
7
8
  column :prop_exec_time, header: "share", align: :right
8
9
  column :avg_exec_ms, header: "avg", align: :right, format: :duration_ms
@@ -26,6 +26,7 @@
26
26
  <li><%= link_to "queries", database_queries_path(@database) %></li>
27
27
  <li><%= link_to "endpoints", database_endpoints_path(@database) %></li>
28
28
  <li><%= link_to "jobs", database_jobs_path(@database) %></li>
29
+ <li><%= link_to "tables", database_tables_path(@database) %></li>
29
30
  <li><%= link_to "indexes", database_indexes_path(@database) %></li>
30
31
  <li><%= link_to "activity", database_activity_path(@database) %></li>
31
32
  <li class="current-database">
@@ -63,8 +63,10 @@ end</code></pre>
63
63
  <p class="dim">No endpoint has run a query yet.</p>
64
64
  <% else %>
65
65
  <%= render "pg_peek/reports/report", report: @endpoints,
66
- columns: %i[endpoint share total_exec_time_ms max_queries_per_request],
67
- limit: PgPeek::DatabasesController::OVERVIEW_ROWS %>
66
+ columns: %i[endpoint total_exec_time_ms max_queries_per_request],
67
+ limit: PgPeek::DatabasesController::OVERVIEW_ROWS,
68
+ link_column: :endpoint,
69
+ link: ->(row) { endpoint_path(@database, row["endpoint"]) } %>
68
70
  <% end %>
69
71
  </section>
70
72
 
@@ -74,8 +76,10 @@ end</code></pre>
74
76
  <p class="dim">No job has run a query yet.</p>
75
77
  <% else %>
76
78
  <%= render "pg_peek/reports/report", report: @jobs,
77
- columns: %i[job_class share total_exec_time_ms],
78
- limit: PgPeek::DatabasesController::OVERVIEW_ROWS %>
79
+ columns: %i[job_class total_exec_time_ms],
80
+ limit: PgPeek::DatabasesController::OVERVIEW_ROWS,
81
+ link_column: :job_class,
82
+ link: ->(row) { database_job_path(@database, row["job_class"]) } %>
79
83
  <% end %>
80
84
  </section>
81
85
 
@@ -103,64 +107,8 @@ end</code></pre>
103
107
  limit: PgPeek::DatabasesController::OVERVIEW_ROWS %>
104
108
  <% end %>
105
109
  </section>
106
-
107
- <section class="wide" id="tables" data-filter>
108
- <h2>tables (<%= @tables.rows.size %>)</h2>
109
- <input type="search"
110
- placeholder="filter tables… (press / to focus)"
111
- data-filter-input
112
- data-filter-noun="table"
113
- autocomplete="off">
114
- <p class="dim" data-filter-count></p>
115
-
116
- <%= render "pg_peek/reports/report",
117
- report: @tables,
118
- link_column: :name,
119
- link: ->(row) { database_table_path(@database, row["name"]) } %>
120
-
121
- <p class="dim" data-filter-empty hidden>No tables match that filter.</p>
122
- </section>
123
110
  </div>
124
111
 
125
- <% if @pg_stat_statements.usable? %>
126
- <section id="pg_stat_statements">
127
- <h2>pg_stat_statements <%= @pg_stat_statements.installed_version %></h2>
128
- <p class="dim">
129
- Queries, endpoints and jobs are cumulative since
130
- <%= @stats_reset_at.strftime("%Y-%m-%d %H:%M:%S %Z") %> (<%= format_time_ago(@stats_reset_at) %>),
131
- <%= pluralize(number_with_delimiter(@pg_stat_statements.statement_count), "statement") %> tracked.
132
- A reset starts a fresh window: exact counts and attribution, at the
133
- price of the history so far.
134
- <% peers = @database.server_peers %>
135
- <% if peers.any? %>
136
- It also clears <%= peers.map(&:name).to_sentence %>, which
137
- <%= peers.one? ? "shares" : "share" %> this server: PostgreSQL keeps
138
- one reset time for all of them.
139
- <% end %>
140
- </p>
141
- <%= button_to "reset statistics",
142
- reset_database_pg_stat_statements_path(@database),
143
- method: :delete,
144
- class: "danger",
145
- disabled: !@pg_stat_statements.resettable?,
146
- form_class: "inline",
147
- form: { data: { confirm: "Reset pg_stat_statements? This clears statement statistics for every database on this server." } } %>
148
- <% unless @pg_stat_statements.resettable? %>
149
- <p class="dim">
150
- <%= @connection_summary["username"] %> may not reset the statistics: the extension
151
- allows it only to superusers and roles granted it. To allow it:
152
- </p>
153
- <pre><code>GRANT EXECUTE ON FUNCTION pg_stat_statements_reset TO <%= @connection_summary["username"] %>;</code></pre>
154
- <% end %>
155
- <% if @pg_stat_statements.outdated? %>
156
- <p class="dim">
157
- Version <%= @pg_stat_statements.default_version %> is available:
158
- <code>ALTER EXTENSION pg_stat_statements UPDATE;</code>
159
- </p>
160
- <% end %>
161
- </section>
162
- <% end %>
163
-
164
112
  <details>
165
113
  <summary>database details</summary>
166
114
  <pre>name: <%= @database.name %>
@@ -13,7 +13,20 @@
13
13
  </p>
14
14
  </article>
15
15
  <% else %>
16
- <%= render "pg_peek/reports/report", report: @report %>
16
+ <input type="search"
17
+ placeholder="filter endpoints… (press / to focus)"
18
+ data-filter-input
19
+ data-filter-noun="endpoint"
20
+ autofocus
21
+ autocomplete="off">
22
+ <p class="dim" data-filter-count></p>
23
+
24
+ <%= render "pg_peek/reports/report",
25
+ report: @report,
26
+ link_column: :endpoint,
27
+ link: ->(row) { endpoint_path(@database, row["endpoint"]) } %>
28
+
29
+ <p class="dim" data-filter-empty hidden>No endpoints match that filter.</p>
17
30
 
18
31
  <p class="dim">
19
32
  <strong>q/req</strong> is an estimate: the request count is inferred from the
@@ -0,0 +1,18 @@
1
+ <div class="toolbar">
2
+ <h1><code class="name"><%= @endpoint %></code></h1>
3
+ <small class="dim"><%= stats_since_link(@database, @pg_stat_statements.reset_at) %></small>
4
+ <%= link_to "← endpoints", database_endpoints_path(@database) %>
5
+ </div>
6
+
7
+ <% if @report.empty? %>
8
+ <article aria-label="No queries found">
9
+ <header>No queries found for this endpoint</header>
10
+ <p>This could mean:</p>
11
+ <ul>
12
+ <li>The endpoint hasn't executed any database queries since statistics were last reset</li>
13
+ <li>The endpoint's queries are not among the top outliers</li>
14
+ </ul>
15
+ </article>
16
+ <% else %>
17
+ <%= render "pg_peek/reports/report", report: @report %>
18
+ <% end %>
@@ -24,13 +24,6 @@
24
24
  collecting, so this page is working &mdash; there is simply nothing in it
25
25
  yet. Jobs appear as they execute their first database query.
26
26
  </p>
27
- <% if @stats_reset_at %>
28
- <p>
29
- Statistics were last reset <strong><%= format_time_ago(@stats_reset_at) %></strong>
30
- (<%= @stats_reset_at %>), so anything that ran before then is gone.
31
- Restarting PostgreSQL clears them too.
32
- </p>
33
- <% end %>
34
27
  </article>
35
28
  <% end %>
36
29
  <% else %>
@@ -45,7 +38,7 @@
45
38
  <%= render "pg_peek/reports/report",
46
39
  report: @report,
47
40
  link_column: :job_class,
48
- link: ->(row) { database_job_path(@database, ERB::Util.url_encode(row["job_class"])) } %>
41
+ link: ->(row) { database_job_path(@database, row["job_class"]) } %>
49
42
 
50
43
  <p class="dim" data-filter-empty hidden>No jobs match that filter.</p>
51
44
  <% end %>
@@ -1,5 +1,6 @@
1
1
  <div class="toolbar">
2
- <h1><%= @job_class %></h1>
2
+ <h1><code class="name"><%= @job_class %></code></h1>
3
+ <small class="dim"><%= stats_since_link(@database, @pg_stat_statements.reset_at) %></small>
3
4
  <%= link_to "← jobs", database_jobs_path(@database) %>
4
5
  </div>
5
6
 
@@ -0,0 +1,87 @@
1
+ <% reset_at = @pg_stat_statements.reset_at %>
2
+ <% settings = @pg_stat_statements.settings %>
3
+ <% dealloc = @pg_stat_statements.dealloc %>
4
+
5
+ <div class="toolbar">
6
+ <h1>pg_stat_statements <small class="dim"><%= @pg_stat_statements.installed_version %></small></h1>
7
+ </div>
8
+
9
+ <section>
10
+ <h2>collection</h2>
11
+ <table>
12
+ <tbody>
13
+ <tr>
14
+ <th>stats since</th>
15
+ <td><%= reset_at.strftime("%Y-%m-%d %H:%M:%S %Z") %> (<%= format_time_ago(reset_at) %>)</td>
16
+ </tr>
17
+ <tr>
18
+ <th>statements tracked</th>
19
+ <td>
20
+ <%= number_with_delimiter(@pg_stat_statements.server_statement_count) %>
21
+ <% if settings["max"] %>of <%= number_with_delimiter(settings["max"].to_i) %><% end %>
22
+ <span class="dim">(this database: <%= number_with_delimiter(@pg_stat_statements.statement_count) %>)</span>
23
+ </td>
24
+ </tr>
25
+ <tr>
26
+ <th>dropped when full</th>
27
+ <td><%= number_with_delimiter(dealloc) %></td>
28
+ </tr>
29
+ </tbody>
30
+ </table>
31
+ <% if dealloc.positive? %>
32
+ <p class="dim">
33
+ The server dropped statements to make room since the last reset, so every
34
+ total here undercounts. Raise <code>pg_stat_statements.max</code>
35
+ (needs a server restart) or reset more often.
36
+ </p>
37
+ <% end %>
38
+ </section>
39
+
40
+ <section>
41
+ <h2>settings</h2>
42
+ <table>
43
+ <tbody>
44
+ <% settings.each do |name, value| %>
45
+ <tr>
46
+ <th><code>pg_stat_statements.<%= name %></code></th>
47
+ <td><%= value %></td>
48
+ </tr>
49
+ <% end %>
50
+ </tbody>
51
+ </table>
52
+ <% if @pg_stat_statements.outdated? %>
53
+ <p class="dim">
54
+ Version <%= @pg_stat_statements.default_version %> is available:
55
+ <code>ALTER EXTENSION pg_stat_statements UPDATE;</code>
56
+ </p>
57
+ <% end %>
58
+ </section>
59
+
60
+ <section id="reset">
61
+ <h2>reset</h2>
62
+ <p class="dim">
63
+ Queries, endpoints and jobs are cumulative since the last reset. A reset
64
+ starts a fresh window: exact counts and attribution, at the price of the
65
+ history so far. Restarting PostgreSQL clears them too.
66
+ <% peers = @database.server_peers %>
67
+ <% if peers.any? %>
68
+ It also clears <%= peers.map(&:name).to_sentence %>, which
69
+ <%= peers.one? ? "shares" : "share" %> this server: PostgreSQL keeps
70
+ one reset time for all of them.
71
+ <% end %>
72
+ </p>
73
+ <%= button_to "reset statistics",
74
+ reset_database_pg_stat_statements_path(@database),
75
+ method: :delete,
76
+ class: "danger",
77
+ disabled: !@pg_stat_statements.resettable?,
78
+ form_class: "inline",
79
+ form: { data: { confirm: "Reset pg_stat_statements? This clears statement statistics for every database on this server." } } %>
80
+ <% unless @pg_stat_statements.resettable? %>
81
+ <p class="dim">
82
+ <%= @connection_summary["username"] %> may not reset the statistics: the extension
83
+ allows it only to superusers and roles granted it. To allow it:
84
+ </p>
85
+ <pre><code>GRANT EXECUTE ON FUNCTION pg_stat_statements_reset TO <%= @connection_summary["username"] %>;</code></pre>
86
+ <% end %>
87
+ </section>