rails-profiler 0.32.2 → 0.32.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 403d9ff1c8dc1f63d91b2ae71490ab29ef762da12f035ca1cd566d2e036083e5
4
- data.tar.gz: d63bf66a090688c87efbcfa4c84140c25c0d611f1f760c3c4ff8c24410ce98f1
3
+ metadata.gz: 97fa343641a35a6bb20283e405ed8955b7039d4da924a7ce4195feeaeffacec6
4
+ data.tar.gz: d7f9d92015e47e0f20c2b344e2d297b169fcf5d9b184aad17f8d3c68f109f5f2
5
5
  SHA512:
6
- metadata.gz: db9b3d2fbc40310a01d3bde8675f218931bcdb35409c677ae1fc9ca8b490fe0103aba880db1bee5a16d6dda96ebc5caa7763cecc50e3638b3ee74eb60c4a35d4
7
- data.tar.gz: fe872b2809ec74b20f449cbafbfe3ee440f78ad54e909e0415ce90c222206c8108d191689366655ddcdaf1bbca83cdda489f51e41f59d190fc25fb43945c936b
6
+ metadata.gz: cd484fea1576a382bc4a6ecef0a39bad2d25b0197d672c62b41e83fed0ef39dfbaf96e8d6100d9d34ed8fca48d765dacbf97099b8963f505503cfc56058be646
7
+ data.tar.gz: de36e884fb0fd828d6d498c93858c73998762ec5823c7be1806897bd66ab36a97e61c2a1aa9d5a854694a6d184495855c812afcc0b6646cfe5318aac0f0dd037
data/CHANGELOG.md CHANGED
@@ -19,6 +19,31 @@ every commit of every tag interval is accounted for one way or the other.
19
19
 
20
20
  ## [Unreleased]
21
21
 
22
+ ## [0.32.3] - 2026-10-14
23
+
24
+ <!-- stamped -->
25
+
26
+ ### Fixed
27
+
28
+ - **Jobs:** the toolbar of a request now sees the jobs it enqueued start and end after its
29
+ response, without a page reload. A job enqueued by a request is saved as `running` as soon as
30
+ it starts, so it appears in the `child_jobs` of that request while it runs, and every save of
31
+ the job tells the request's toolbar that its profile changed, so it reads the job again when it
32
+ ends, `completed` or `failed`. Without Redis storage, this reaches the toolbar only when the job
33
+ runs in a process serving the profiler's pages (the `async` or `inline` adapters); a job run by
34
+ another process is still listed on the next reload.
35
+ With several Puma workers and no Redis, the toolbar sees a job start and end only when its
36
+ polling happens to reach the worker that ran the job.
37
+ - **Jobs:** a job that is still running is shown as running, in the JOB panel of the toolbar and
38
+ in the Background Jobs list of `/_profiler`: a turning ◐ with `running` and no duration, a blue
39
+ **Running** badge in the list, instead of a green check mark and a duration of 0 ms that read as
40
+ an instant success. The page of the job itself and the Jobs tab of a profile do the same: a blue
41
+ **Running** badge and no duration. The JOB item of the toolbar is blue while one of its jobs runs, the list has
42
+ a **Running** status filter, and its **Completed** filter no longer includes running jobs. The
43
+ `status` filter of the MCP tool `query_jobs` accepts `running`. A job killed while it performs
44
+ (`SIGKILL`, out of memory, container stopped) never records its end, so it stays `running` in the
45
+ toolbar and the list for good.
46
+
22
47
  ## [0.32.2] - 2026-10-13
23
48
 
24
49
  <!-- stamped -->
@@ -3723,7 +3723,23 @@
3723
3723
  }
3724
3724
 
3725
3725
  // app/assets/typescript/profiler/components/toolbar/panels/JobsPanel.tsx
3726
+ function JobStatus({ job }) {
3727
+ if (job.status === "running") {
3728
+ return /* @__PURE__ */ u3("strong", { class: "profiler-text--xs profiler-text--mono profiler-text--info", children: [
3729
+ /* @__PURE__ */ u3("span", { class: "profiler-spinner", "aria-hidden": "true", children: "\u25D0" }),
3730
+ " running"
3731
+ ] });
3732
+ }
3733
+ const failed = job.status === "failed";
3734
+ return /* @__PURE__ */ u3("strong", { class: `profiler-text--xs profiler-text--mono ${failed ? "profiler-text--error" : "profiler-text--success"}`, children: [
3735
+ failed ? "\u2717" : "\u2713",
3736
+ " ",
3737
+ job.duration?.toFixed(0),
3738
+ "ms"
3739
+ ] });
3740
+ }
3726
3741
  function JobsPanel({ jobs }) {
3742
+ const running = jobs.filter((j3) => j3.status === "running").length;
3727
3743
  const completed = jobs.filter((j3) => j3.status === "completed").length;
3728
3744
  const failed = jobs.filter((j3) => j3.status === "failed").length;
3729
3745
  const preview = jobs.slice(0, 5);
@@ -3739,9 +3755,13 @@
3739
3755
  ] }),
3740
3756
  /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-content", children: [
3741
3757
  /* @__PURE__ */ u3("div", { class: "profiler-section__header", children: "Summary" }),
3758
+ running > 0 && /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
3759
+ /* @__PURE__ */ u3("span", { children: "Running" }),
3760
+ /* @__PURE__ */ u3("strong", { class: "profiler-text--info", children: running })
3761
+ ] }),
3742
3762
  /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
3743
3763
  /* @__PURE__ */ u3("span", { children: "Completed" }),
3744
- /* @__PURE__ */ u3("strong", { class: "profiler-text--success", children: completed })
3764
+ /* @__PURE__ */ u3("strong", { class: completed > 0 ? "profiler-text--success" : "profiler-text--muted", children: completed })
3745
3765
  ] }),
3746
3766
  /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
3747
3767
  /* @__PURE__ */ u3("span", { children: "Failed" }),
@@ -3750,12 +3770,7 @@
3750
3770
  /* @__PURE__ */ u3("div", { class: "profiler-section__header profiler-mt-3", children: "Last jobs" }),
3751
3771
  preview.map((job, i3) => /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
3752
3772
  /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--mono", children: job.job_class }),
3753
- /* @__PURE__ */ u3("strong", { class: `profiler-text--xs profiler-text--mono ${job.status === "failed" ? "profiler-text--error" : "profiler-text--success"}`, children: [
3754
- job.status === "failed" ? "\u2717" : "\u2713",
3755
- " ",
3756
- job.duration?.toFixed(0),
3757
- "ms"
3758
- ] })
3773
+ /* @__PURE__ */ u3(JobStatus, { job })
3759
3774
  ] }, i3)),
3760
3775
  remaining > 0 && /* @__PURE__ */ u3("div", { class: "profiler-more", children: [
3761
3776
  "+ ",
@@ -4102,13 +4117,14 @@
4102
4117
  ToolbarItem,
4103
4118
  {
4104
4119
  href: `/_profiler/profiles/${token}?tab=jobs`,
4105
- className: childJobs.some((j3) => j3.status === "failed") ? "profiler-text--error" : "profiler-text--success",
4120
+ className: childJobs.some((j3) => j3.status === "failed") ? "profiler-text--error" : childJobs.some((j3) => j3.status === "running") ? "profiler-text--info" : "profiler-text--success",
4106
4121
  panelLarge: true,
4107
4122
  panel: /* @__PURE__ */ u3(JobsPanel, { jobs: childJobs }),
4108
4123
  children: [
4109
4124
  /* @__PURE__ */ u3("span", { class: "profiler-text--muted profiler-text--xs", children: "JOB" }),
4110
4125
  /* @__PURE__ */ u3("span", { children: childJobs.length }),
4111
- childJobs.some((j3) => j3.status === "failed") && /* @__PURE__ */ u3("span", { class: "profiler-text--error profiler-text--xs", children: "\u2717" })
4126
+ childJobs.some((j3) => j3.status === "failed") && /* @__PURE__ */ u3("span", { class: "profiler-text--error profiler-text--xs", children: "\u2717" }),
4127
+ childJobs.some((j3) => j3.status === "running") && /* @__PURE__ */ u3("span", { class: "profiler-spinner profiler-text--info profiler-text--xs", "aria-hidden": "true", children: "\u25D0" })
4112
4128
  ]
4113
4129
  }
4114
4130
  ),
@@ -606,6 +606,11 @@ tr:hover .btn-row-delete {
606
606
  transform: rotate(360deg);
607
607
  }
608
608
  }
609
+ .profiler-spinner {
610
+ display: inline-block;
611
+ animation: spin 1s linear infinite;
612
+ }
613
+
609
614
  @keyframes pulse {
610
615
  0%, 100% {
611
616
  opacity: 1;
@@ -1558,6 +1563,10 @@ a.profiler-toolbar-item {
1558
1563
  color: var(--profiler-error) !important;
1559
1564
  }
1560
1565
 
1566
+ .profiler-text--info {
1567
+ color: var(--profiler-info) !important;
1568
+ }
1569
+
1561
1570
  .profiler-text--muted {
1562
1571
  color: var(--profiler-text-muted) !important;
1563
1572
  }
@@ -1578,6 +1587,10 @@ a.profiler-toolbar-item {
1578
1587
  background: var(--profiler-success);
1579
1588
  }
1580
1589
 
1590
+ .profiler-toolbar-item.profiler-text--info::after {
1591
+ background: var(--profiler-info);
1592
+ }
1593
+
1581
1594
  .profiler-toolbar-hoverable {
1582
1595
  position: relative;
1583
1596
  }
@@ -5569,6 +5569,11 @@
5569
5569
  if (duration >= 100) return "badge-warning";
5570
5570
  return "badge-success";
5571
5571
  }
5572
+ function jobStatusOf(p3) {
5573
+ const status = p3.collectors_data?.job?.status;
5574
+ if (status) return status;
5575
+ return p3.status === 500 ? "failed" : "completed";
5576
+ }
5572
5577
  function formatTime(iso) {
5573
5578
  return new Date(iso).toLocaleTimeString("en", { hour12: false });
5574
5579
  }
@@ -5827,8 +5832,7 @@
5827
5832
  }) : filteredProfiles;
5828
5833
  const filteredJobs = jobs.filter((p3) => {
5829
5834
  if (jobSearch && !p3.path.toLowerCase().includes(jobSearch.toLowerCase())) return false;
5830
- if (jobStatus === "failed" && p3.status !== 500) return false;
5831
- if (jobStatus === "completed" && p3.status === 500) return false;
5835
+ if (jobStatus && jobStatusOf(p3) !== jobStatus) return false;
5832
5836
  if (jobDuration) {
5833
5837
  if (jobDuration === "lt100" ? p3.duration >= 100 : p3.duration < parseInt(jobDuration)) return false;
5834
5838
  }
@@ -6104,6 +6108,7 @@
6104
6108
  ),
6105
6109
  /* @__PURE__ */ u3("select", { class: "profiler-filter-select", value: jobStatus, onChange: (e3) => setJobStatus(e3.target.value), children: [
6106
6110
  /* @__PURE__ */ u3("option", { value: "", children: "All Statuses" }),
6111
+ /* @__PURE__ */ u3("option", { value: "running", children: "Running" }),
6107
6112
  /* @__PURE__ */ u3("option", { value: "completed", children: "Completed" }),
6108
6113
  /* @__PURE__ */ u3("option", { value: "failed", children: "Failed" })
6109
6114
  ] }),
@@ -6147,7 +6152,7 @@
6147
6152
  ] }) }),
6148
6153
  /* @__PURE__ */ u3("tbody", { children: sortedJobs.map((p3) => {
6149
6154
  const jobData = p3.collectors_data?.job;
6150
- const isFailed = p3.status === 500;
6155
+ const status = jobStatusOf(p3);
6151
6156
  return /* @__PURE__ */ u3("tr", { children: [
6152
6157
  /* @__PURE__ */ u3("td", { children: formatTime(p3.started_at) }),
6153
6158
  /* @__PURE__ */ u3("td", { children: [
@@ -6155,11 +6160,14 @@
6155
6160
  p3.gem_version && p3.gem_version !== currentVersion && /* @__PURE__ */ u3("span", { class: "profiler-version-warn", title: `Captur\xE9 avec v${p3.gem_version} (actuel : v${currentVersion})`, children: "\u26A0\uFE0F" })
6156
6161
  ] }),
6157
6162
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--mono", children: jobData?.queue || "-" }) }),
6158
- /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: durationClass(p3.duration), children: [
6163
+ /* @__PURE__ */ u3("td", { children: status === "running" ? /* @__PURE__ */ u3("span", { class: "profiler-text--muted", children: "-" }) : /* @__PURE__ */ u3("span", { class: durationClass(p3.duration), children: [
6159
6164
  p3.duration.toFixed(2),
6160
6165
  " ms"
6161
6166
  ] }) }),
6162
- /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: isFailed ? "badge-error" : "badge-success", children: isFailed ? "\u2717 Failed" : "\u2713 Completed" }) }),
6167
+ /* @__PURE__ */ u3("td", { children: status === "running" ? /* @__PURE__ */ u3("span", { class: "badge-info", children: [
6168
+ /* @__PURE__ */ u3("span", { class: "profiler-spinner", "aria-hidden": "true", children: "\u25D0" }),
6169
+ " Running"
6170
+ ] }) : /* @__PURE__ */ u3("span", { class: status === "failed" ? "badge-error" : "badge-success", children: status === "failed" ? "\u2717 Failed" : "\u2713 Completed" }) }),
6163
6171
  /* @__PURE__ */ u3("td", { children: jobData?.executions ?? "-" }),
6164
6172
  /* @__PURE__ */ u3("td", { class: "profiler-text--xs profiler-text--mono profiler-text--muted", children: /* @__PURE__ */ u3("button", { class: "token-copy", onClick: () => copyToken(p3.token), title: "Copy full token", children: copiedToken === p3.token ? "\u2713" : p3.token.substring(0, 8) + "\u2026" }) }),
6165
6173
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("button", { class: "btn-row-delete", onClick: () => deleteJob2(p3.token), title: "Delete", children: "\xD7" }) })
@@ -8631,10 +8639,10 @@
8631
8639
  jobs.map((job, index) => /* @__PURE__ */ u3("div", { class: `profiler-ajax-card profiler-ajax-card--${job.status === "completed" ? "success" : job.status === "failed" ? "error" : "default"}`, children: [
8632
8640
  /* @__PURE__ */ u3("div", { class: "profiler-ajax-card__row", children: [
8633
8641
  /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-3", children: [
8634
- /* @__PURE__ */ u3("span", { class: `badge-${job.status === "completed" ? "success" : job.status === "failed" ? "error" : "warning"}`, children: job.status ?? "unknown" }),
8642
+ /* @__PURE__ */ u3("span", { class: `badge-${job.status === "completed" ? "success" : job.status === "failed" ? "error" : job.status === "running" ? "info" : "warning"}`, children: job.status ?? "unknown" }),
8635
8643
  /* @__PURE__ */ u3("strong", { class: "profiler-ajax-card__path", children: job.job_class })
8636
8644
  ] }),
8637
- /* @__PURE__ */ u3("span", { class: job.duration >= 1e3 ? "badge-error" : job.duration >= 200 ? "badge-warning" : "badge-success", children: [
8645
+ job.status === "running" ? /* @__PURE__ */ u3("span", { class: "badge-info", children: "running" }) : /* @__PURE__ */ u3("span", { class: job.duration >= 1e3 ? "badge-error" : job.duration >= 200 ? "badge-warning" : "badge-success", children: [
8638
8646
  job.duration?.toFixed(2),
8639
8647
  " ms"
8640
8648
  ] })
@@ -9063,6 +9071,7 @@
9063
9071
  const [activeTab, setActiveTab] = d2(hasException ? "exception" : defaultTab);
9064
9072
  const jobData = cd["job"];
9065
9073
  const isFailed = jobData?.status === "failed";
9074
+ const isRunning = jobData?.status === "running";
9066
9075
  const parent = profile.parent_profile;
9067
9076
  const handleTabClick = (tab) => (e3) => {
9068
9077
  e3.preventDefault();
@@ -9082,14 +9091,11 @@
9082
9091
  /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-4 profiler-mt-2", children: [
9083
9092
  /* @__PURE__ */ u3("span", { children: [
9084
9093
  "Duration: ",
9085
- /* @__PURE__ */ u3("strong", { children: [
9086
- profile.duration.toFixed(2),
9087
- " ms"
9088
- ] })
9094
+ /* @__PURE__ */ u3("strong", { children: isRunning ? "running" : `${profile.duration.toFixed(2)} ms` })
9089
9095
  ] }),
9090
9096
  /* @__PURE__ */ u3("span", { children: [
9091
9097
  "Status: ",
9092
- /* @__PURE__ */ u3("strong", { children: /* @__PURE__ */ u3("span", { class: `badge-${isFailed ? "error" : "success"}`, children: isFailed ? "Failed" : "Completed" }) })
9098
+ /* @__PURE__ */ u3("strong", { children: /* @__PURE__ */ u3("span", { class: `badge-${isFailed ? "error" : isRunning ? "info" : "success"}`, children: isFailed ? "Failed" : isRunning ? "Running" : "Completed" }) })
9093
9099
  ] }),
9094
9100
  allocatedObjects(profile) != null && /* @__PURE__ */ u3("span", { title: ALLOCATIONS_HINT, children: [
9095
9101
  "Allocations: ",
@@ -72,6 +72,7 @@ module Profiler
72
72
  return block.call unless Collectors::Lifecycle.subscribe_all(collectors, "JobProfiler")
73
73
 
74
74
  exception_collector = collectors.find { |c| c.is_a?(Collectors::ExceptionCollector) }
75
+ save_running(profile, job_collector) if @parent_token
75
76
 
76
77
  allocations_before = AllocationCounter.current if Profiler.configuration.track_memory
77
78
 
@@ -118,6 +119,16 @@ module Profiler
118
119
 
119
120
  private
120
121
 
122
+ # A job enqueued by a request runs after that request's response: saved now, running, the
123
+ # toolbar of the request lists it before it ends. The save at the end replaces it.
124
+ def save_running(profile, job_collector)
125
+ job_collector.collect
126
+ profile.duration = 0.0
127
+ Profiler.save_profile(profile, from: "JobProfiler")
128
+ rescue StandardError => e
129
+ Profiler.log_error("JobProfiler: could not record job #{@job_class} as running", e)
130
+ end
131
+
121
132
  def sanitize_arguments(args)
122
133
  return [] unless args
123
134
 
@@ -230,7 +230,7 @@ module Profiler
230
230
  input_schema: {
231
231
  properties: {
232
232
  queue: { type: "string", description: "Filter by queue name" },
233
- status: { type: "string", description: "Filter by status (completed, failed)" },
233
+ status: { type: "string", description: "Filter by status (running, completed, failed). A job enqueued by a request is listed as running while it performs." },
234
234
  limit: { type: "number", description: "Maximum number of results (default 20)" },
235
235
  fields: { type: "array", items: { type: "string" }, description: "Columns to include. Valid values: time, job_class, queue, status, duration, token. Omit for all." },
236
236
  cursor: { type: "string", description: "Pagination cursor: ISO8601 timestamp of the last item seen. Returns jobs older than this." },
@@ -233,6 +233,8 @@ module Profiler
233
233
  from { transform: translateY(100%); opacity: 0; }
234
234
  to { transform: translateY(0); opacity: 1; }
235
235
  }
236
+ @keyframes pfSpin { to { transform: rotate(360deg); } }
237
+ #profiler-toolbar .profiler-spinner { display: inline-block; animation: pfSpin 1s linear infinite; }
236
238
  @keyframes pfPop {
237
239
  0% { transform: scaleX(1); }
238
240
  15% { transform: scaleX(1.18); }
@@ -303,11 +305,13 @@ module Profiler
303
305
  #profiler-toolbar .profiler-text--error::after { background: var(--pf-error); }
304
306
  #profiler-toolbar .profiler-text--warning::after { background: var(--pf-warning); }
305
307
  #profiler-toolbar .profiler-text--success::after { background: var(--pf-success); }
308
+ #profiler-toolbar .profiler-text--info::after { background: var(--pf-info); }
306
309
 
307
310
  /* Semantic colors */
308
311
  #profiler-toolbar .profiler-text--success { color: var(--pf-success) !important; }
309
312
  #profiler-toolbar .profiler-text--warning { color: var(--pf-warning) !important; }
310
313
  #profiler-toolbar .profiler-text--error { color: var(--pf-error) !important; }
314
+ #profiler-toolbar .profiler-text--info { color: var(--pf-info) !important; }
311
315
  #profiler-toolbar .profiler-text--accent { color: var(--pf-amber) !important; }
312
316
  #profiler-toolbar .profiler-text--muted { color: var(--pf-muted) !important; }
313
317
 
@@ -6,13 +6,15 @@ module Profiler
6
6
  module Storage
7
7
  class BaseStore
8
8
  # Public interface: persists the profile then records the save, for the pages showing it
9
- # to see it changed (Profiler::SSE).
9
+ # to see it changed (Profiler::SSE). A job's save is also recorded for the profile that
10
+ # enqueued it: the toolbar of that request lists its jobs, and has to see them start and end.
10
11
  # Subclasses implement #do_save, not #save. The token must be one the gem issued
11
12
  # (Storage::Token); every other public method answers "not found" for any other token.
12
13
  def save(token, profile)
13
14
  Token.validate!(token)
14
15
  result = do_save(token, profile)
15
16
  broadcast_event(token)
17
+ broadcast_event(profile.parent_token) if job_with_parent?(profile)
16
18
  result
17
19
  end
18
20
 
@@ -53,6 +55,11 @@ module Profiler
53
55
 
54
56
  private
55
57
 
58
+ # Not for an AJAX request linked to its page: the toolbar reads those on its own links.
59
+ def job_with_parent?(profile)
60
+ profile.profile_type == "job" && Token.valid?(profile.parent_token)
61
+ end
62
+
56
63
  def broadcast_event(token)
57
64
  Profiler::SSE.current.broadcast(token)
58
65
  rescue StandardError => e
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.32.2"
4
+ VERSION = "0.32.3"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.32.2
4
+ version: 0.32.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sébastien Duplessy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-13 00:00:00.000000000 Z
11
+ date: 2026-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails