rails-profiler 0.7.0 → 0.9.0

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: a118e9ba5a79bc87f82ee566e0e2af65a7199fff6694561ad080f990fb7bf8cd
4
- data.tar.gz: 44e2391b361cbcda4ea4a133fefb7db653cd2f0a48e72859216c368ce1867ded
3
+ metadata.gz: 1530091bc78596f3f4b410c27e7ab7d8ff332c644ea345b6f7e6819a9e87fc62
4
+ data.tar.gz: a733275e9bf99ed4300d85b7fa4a4f88f0249705f3183ead0956bb75a51b6562
5
5
  SHA512:
6
- metadata.gz: c69ecc008b9b9c79161cdc18326c9b0c43e99b7a7284b2681980c0c2d2b7a94c10f77dc186b2826f2cf111f53063e10a3790c4245e4c8a4a07bdde7f49b02e7a
7
- data.tar.gz: 22031e28d3fc173b52dab71ed8f1fe8f0faf350134ed683b2847d1ad3a4e44e5de438b6de76d83a3f2b554aa22175cee5b578af0aacc4a8b247f2fd32390d431
6
+ metadata.gz: 492ed61fe90adb99887d0c981010af043f10a645202944f0a0250776c27d5e22c83f0547eaf74cd0829aac02329237701bfd3da365f56b61dfe0cba62e0181c8
7
+ data.tar.gz: 7da1fce0f77cd22f69a42e1cb837dcef63b3a20d26e4c42c95cd193eb3bf0d6b31cb84095d4f64c77fbf4404320485289aa0225a1c3fff9e2debc42f1d49a495
@@ -1896,6 +1896,61 @@ a.profiler-toolbar-item.profiler-text--warning::after {
1896
1896
  cursor: pointer;
1897
1897
  }
1898
1898
 
1899
+ .profiler-preset-btn {
1900
+ height: 28px;
1901
+ padding: 0 10px;
1902
+ background: transparent;
1903
+ border: 1px solid var(--profiler-border);
1904
+ border-radius: var(--profiler-radius-full);
1905
+ color: var(--profiler-text-muted);
1906
+ font-family: var(--profiler-font-sans);
1907
+ font-size: var(--profiler-text-xs);
1908
+ font-weight: 500;
1909
+ cursor: pointer;
1910
+ transition: all var(--profiler-transition-base);
1911
+ white-space: nowrap;
1912
+ }
1913
+ .profiler-preset-btn:hover {
1914
+ border-color: var(--profiler-accent);
1915
+ color: var(--profiler-accent);
1916
+ background: var(--profiler-accent-bg);
1917
+ }
1918
+ .profiler-preset-btn--active {
1919
+ border-color: var(--profiler-accent);
1920
+ background: var(--profiler-accent);
1921
+ color: var(--profiler-text-on-accent, #fff);
1922
+ }
1923
+ .profiler-preset-btn--active:hover {
1924
+ opacity: 0.85;
1925
+ color: var(--profiler-text-on-accent, #fff);
1926
+ }
1927
+
1928
+ .sortable {
1929
+ cursor: pointer;
1930
+ user-select: none;
1931
+ white-space: nowrap;
1932
+ transition: color var(--profiler-transition-base);
1933
+ }
1934
+ .sortable:hover {
1935
+ color: var(--profiler-text) !important;
1936
+ }
1937
+ .sortable--active {
1938
+ color: var(--profiler-text) !important;
1939
+ }
1940
+
1941
+ .sort-icon {
1942
+ font-size: 10px;
1943
+ margin-left: 4px;
1944
+ vertical-align: middle;
1945
+ }
1946
+ .sort-icon--idle {
1947
+ opacity: 0.35;
1948
+ }
1949
+ .sort-icon--active {
1950
+ color: var(--profiler-accent);
1951
+ opacity: 1;
1952
+ }
1953
+
1899
1954
  .profiler-timeline {
1900
1955
  margin: 24px 0;
1901
1956
  padding: 24px;
@@ -2321,6 +2376,33 @@ a.profiler-toolbar-item.profiler-text--warning::after {
2321
2376
  color: var(--profiler-text-subtle);
2322
2377
  font-family: var(--profiler-font-sans);
2323
2378
  }
2379
+ .profiler-flamegraph__search {
2380
+ display: flex;
2381
+ align-items: center;
2382
+ gap: 8px;
2383
+ }
2384
+ .profiler-flamegraph__search-input {
2385
+ height: 28px;
2386
+ padding: 0 8px;
2387
+ background: var(--profiler-bg-lighter);
2388
+ border: 1px solid var(--profiler-border);
2389
+ border-radius: var(--profiler-radius-sm);
2390
+ color: var(--profiler-text);
2391
+ font-family: var(--profiler-font-mono);
2392
+ font-size: var(--profiler-text-xs);
2393
+ width: 200px;
2394
+ transition: border-color var(--profiler-transition-base);
2395
+ }
2396
+ .profiler-flamegraph__search-input:focus {
2397
+ outline: none;
2398
+ border-color: var(--profiler-accent);
2399
+ }
2400
+ .profiler-flamegraph__match-count {
2401
+ font-size: var(--profiler-text-xs);
2402
+ font-family: var(--profiler-font-mono);
2403
+ color: var(--profiler-text-muted);
2404
+ white-space: nowrap;
2405
+ }
2324
2406
  .profiler-flamegraph__canvas-container {
2325
2407
  position: relative;
2326
2408
  border: 1px solid var(--profiler-border);
@@ -718,11 +718,26 @@
718
718
  if (!bytes) return "-";
719
719
  return (bytes / 1024 / 1024).toFixed(2) + " MB";
720
720
  }
721
+ var PRESETS = [
722
+ { key: "slow", label: "Slow" },
723
+ { key: "many_queries", label: "Many queries" },
724
+ { key: "errors", label: "Errors" },
725
+ { key: "has_exception", label: "Has exception" }
726
+ ];
721
727
  function ProfileList() {
728
+ const params = new URLSearchParams(window.location.search);
722
729
  const initialSection = () => {
723
- const s3 = new URLSearchParams(window.location.search).get("section");
730
+ const s3 = params.get("section");
724
731
  return s3 === "http" || s3 === "jobs" || s3 === "outbound" ? s3 : "http";
725
732
  };
733
+ const initialSort = () => {
734
+ const col = params.get("sort");
735
+ const dir = params.get("dir");
736
+ return {
737
+ col: col === "duration" || col === "memory" || col === "status" || col === "queries" ? col : null,
738
+ dir: dir === "desc" ? "desc" : "asc"
739
+ };
740
+ };
726
741
  const [section, setSection] = d2(initialSection);
727
742
  const [profiles, setProfiles] = d2([]);
728
743
  const [jobs, setJobs] = d2([]);
@@ -740,6 +755,11 @@
740
755
  const [httpMethod, setHttpMethod] = d2("");
741
756
  const [httpStatus, setHttpStatus] = d2("");
742
757
  const [httpDuration, setHttpDuration] = d2("");
758
+ const [httpPreset, setHttpPreset] = d2(() => {
759
+ const p3 = params.get("preset");
760
+ return PRESETS.some((pr) => pr.key === p3) ? p3 : "";
761
+ });
762
+ const [httpSort, setHttpSort] = d2(initialSort);
743
763
  const [jobSearch, setJobSearch] = d2("");
744
764
  const [jobStatus, setJobStatus] = d2("");
745
765
  const [jobDuration, setJobDuration] = d2("");
@@ -759,6 +779,30 @@
759
779
  if (section === "jobs") loadJobs();
760
780
  if (section === "outbound") loadOutbound();
761
781
  }, []);
782
+ y2(() => {
783
+ const url = new URL(window.location.href);
784
+ if (httpSort.col) {
785
+ url.searchParams.set("sort", httpSort.col);
786
+ url.searchParams.set("dir", httpSort.dir);
787
+ } else {
788
+ url.searchParams.delete("sort");
789
+ url.searchParams.delete("dir");
790
+ }
791
+ if (httpPreset) {
792
+ url.searchParams.set("preset", httpPreset);
793
+ } else {
794
+ url.searchParams.delete("preset");
795
+ }
796
+ history.replaceState(null, "", url.toString());
797
+ }, [httpSort, httpPreset]);
798
+ const toggleHttpSort = (col) => {
799
+ setHttpSort(
800
+ (prev) => prev.col === col ? { col, dir: prev.dir === "asc" ? "desc" : "asc" } : { col, dir: "asc" }
801
+ );
802
+ };
803
+ const togglePreset = (key) => {
804
+ setHttpPreset((prev) => prev === key ? "" : key);
805
+ };
762
806
  const loadJobs = () => {
763
807
  if (jobsLoaded) return;
764
808
  setLoadingJobs(true);
@@ -796,6 +840,8 @@
796
840
  setHttpMethod("");
797
841
  setHttpStatus("");
798
842
  setHttpDuration("");
843
+ setHttpPreset("");
844
+ setHttpSort({ col: null, dir: "asc" });
799
845
  setJobSearch("");
800
846
  setJobStatus("");
801
847
  setJobDuration("");
@@ -870,8 +916,36 @@
870
916
  if (httpDuration) {
871
917
  if (httpDuration === "lt100" ? p3.duration >= 100 : p3.duration < parseInt(httpDuration)) return false;
872
918
  }
919
+ if (httpPreset === "slow" && p3.duration < 500) return false;
920
+ if (httpPreset === "many_queries" && (p3.collectors_data?.database?.total_queries ?? 0) <= 20) return false;
921
+ if (httpPreset === "errors" && p3.status < 500) return false;
922
+ if (httpPreset === "has_exception" && !p3.collectors_data?.exception) return false;
873
923
  return true;
874
924
  });
925
+ const sortedProfiles = httpSort.col ? [...filteredProfiles].sort((a3, b) => {
926
+ let av, bv;
927
+ switch (httpSort.col) {
928
+ case "duration":
929
+ av = a3.duration;
930
+ bv = b.duration;
931
+ break;
932
+ case "memory":
933
+ av = a3.memory ?? 0;
934
+ bv = b.memory ?? 0;
935
+ break;
936
+ case "status":
937
+ av = a3.status;
938
+ bv = b.status;
939
+ break;
940
+ case "queries":
941
+ av = a3.collectors_data?.database?.total_queries ?? 0;
942
+ bv = b.collectors_data?.database?.total_queries ?? 0;
943
+ break;
944
+ default:
945
+ return 0;
946
+ }
947
+ return httpSort.dir === "asc" ? av - bv : bv - av;
948
+ }) : filteredProfiles;
875
949
  const filteredJobs = jobs.filter((p3) => {
876
950
  if (jobSearch && !p3.path.toLowerCase().includes(jobSearch.toLowerCase())) return false;
877
951
  if (jobStatus === "failed" && p3.status !== 500) return false;
@@ -894,9 +968,13 @@
894
968
  }
895
969
  return true;
896
970
  });
897
- const httpFiltersActive = !!(httpSearch || httpMethod || httpStatus || httpDuration);
971
+ const httpFiltersActive = !!(httpSearch || httpMethod || httpStatus || httpDuration || httpPreset);
898
972
  const jobFiltersActive = !!(jobSearch || jobStatus || jobDuration);
899
973
  const outboundFiltersActive = !!(outboundSearch || outboundMethod || outboundStatus);
974
+ const sortIcon = (col) => {
975
+ if (httpSort.col !== col) return /* @__PURE__ */ u3("span", { class: "sort-icon sort-icon--idle", children: "\u21C5" });
976
+ return /* @__PURE__ */ u3("span", { class: "sort-icon sort-icon--active", children: httpSort.dir === "asc" ? "\u25B2" : "\u25BC" });
977
+ };
900
978
  return /* @__PURE__ */ u3("div", { class: "container", children: [
901
979
  /* @__PURE__ */ u3("div", { class: "header", children: [
902
980
  /* @__PURE__ */ u3("h1", { children: [
@@ -925,6 +1003,15 @@
925
1003
  /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "No profiles found" }),
926
1004
  /* @__PURE__ */ u3("p", { class: "profiler-empty__description", children: "Make some requests to your application to see profiling data" })
927
1005
  ] }) : /* @__PURE__ */ u3(k, { children: [
1006
+ /* @__PURE__ */ u3("div", { class: "profiler-action-bar profiler-mb-2", children: /* @__PURE__ */ u3("div", { class: "profiler-filter-group", children: PRESETS.map((preset) => /* @__PURE__ */ u3(
1007
+ "button",
1008
+ {
1009
+ class: `profiler-preset-btn${httpPreset === preset.key ? " profiler-preset-btn--active" : ""}`,
1010
+ onClick: () => togglePreset(preset.key),
1011
+ children: preset.label
1012
+ },
1013
+ preset.key
1014
+ )) }) }),
928
1015
  /* @__PURE__ */ u3("div", { class: "profiler-action-bar profiler-mb-3", children: [
929
1016
  /* @__PURE__ */ u3("div", { class: "profiler-filter-group", children: [
930
1017
  /* @__PURE__ */ u3(
@@ -974,13 +1061,26 @@
974
1061
  /* @__PURE__ */ u3("th", { children: "Time" }),
975
1062
  /* @__PURE__ */ u3("th", { children: "Method" }),
976
1063
  /* @__PURE__ */ u3("th", { children: "Path" }),
977
- /* @__PURE__ */ u3("th", { children: "Duration" }),
978
- /* @__PURE__ */ u3("th", { children: "Memory" }),
979
- /* @__PURE__ */ u3("th", { children: "Status" }),
1064
+ /* @__PURE__ */ u3("th", { class: `sortable${httpSort.col === "duration" ? " sortable--active" : ""}`, onClick: () => toggleHttpSort("duration"), children: [
1065
+ "Duration ",
1066
+ sortIcon("duration")
1067
+ ] }),
1068
+ /* @__PURE__ */ u3("th", { class: `sortable${httpSort.col === "queries" ? " sortable--active" : ""}`, onClick: () => toggleHttpSort("queries"), children: [
1069
+ "Queries ",
1070
+ sortIcon("queries")
1071
+ ] }),
1072
+ /* @__PURE__ */ u3("th", { class: `sortable${httpSort.col === "memory" ? " sortable--active" : ""}`, onClick: () => toggleHttpSort("memory"), children: [
1073
+ "Memory ",
1074
+ sortIcon("memory")
1075
+ ] }),
1076
+ /* @__PURE__ */ u3("th", { class: `sortable${httpSort.col === "status" ? " sortable--active" : ""}`, onClick: () => toggleHttpSort("status"), children: [
1077
+ "Status ",
1078
+ sortIcon("status")
1079
+ ] }),
980
1080
  /* @__PURE__ */ u3("th", { children: "Token" }),
981
1081
  /* @__PURE__ */ u3("th", {})
982
1082
  ] }) }),
983
- /* @__PURE__ */ u3("tbody", { children: filteredProfiles.map((p3) => /* @__PURE__ */ u3("tr", { children: [
1083
+ /* @__PURE__ */ u3("tbody", { children: sortedProfiles.map((p3) => /* @__PURE__ */ u3("tr", { children: [
984
1084
  /* @__PURE__ */ u3("td", { children: formatTime(p3.started_at) }),
985
1085
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: methodClass(p3.method), children: p3.method }) }),
986
1086
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("a", { href: `${BASE}/profiles/${p3.token}`, children: p3.path }) }),
@@ -988,6 +1088,7 @@
988
1088
  p3.duration.toFixed(2),
989
1089
  " ms"
990
1090
  ] }) }),
1091
+ /* @__PURE__ */ u3("td", { children: p3.collectors_data?.database?.total_queries ?? "\u2014" }),
991
1092
  /* @__PURE__ */ u3("td", { children: formatMemory(p3.memory) }),
992
1093
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: statusClass(p3.status), children: p3.status }) }),
993
1094
  /* @__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" }) }),
@@ -1546,7 +1647,8 @@
1546
1647
  partial: "#f59e0b",
1547
1648
  sql: "#fb923c",
1548
1649
  cache: "#a78bfa",
1549
- http: "#f87171"
1650
+ http: "#f87171",
1651
+ custom: "#e879f9"
1550
1652
  };
1551
1653
  var FRAME_HEIGHT = 24;
1552
1654
  var FRAME_GAP = 1;
@@ -1567,6 +1669,7 @@
1567
1669
  this.dpr = 1;
1568
1670
  this.hoveredFrame = null;
1569
1671
  this.zoomStack = [];
1672
+ this.searchQuery = "";
1570
1673
  this.isPanning = false;
1571
1674
  this.panStartX = 0;
1572
1675
  this.panStartViewport = { start: 0, end: 0 };
@@ -1789,6 +1892,10 @@
1789
1892
  this.render();
1790
1893
  }
1791
1894
  }
1895
+ setSearchQuery(query) {
1896
+ this.searchQuery = query;
1897
+ this.render();
1898
+ }
1792
1899
  render() {
1793
1900
  const ctx = this.ctx;
1794
1901
  const w3 = this.canvas.width / this.dpr;
@@ -1798,6 +1905,15 @@
1798
1905
  const style = getComputedStyle(this.canvas);
1799
1906
  const textColor = style.getPropertyValue("--profiler-text").trim() || "#eef2f7";
1800
1907
  const textMuted = style.getPropertyValue("--profiler-text-muted").trim() || "#5e7080";
1908
+ const searchLower = this.searchQuery.toLowerCase();
1909
+ const hasSearch = searchLower.length > 0;
1910
+ if (hasSearch && this.callbacks.onSearchResults) {
1911
+ let matchCount = 0;
1912
+ for (const f4 of this.frames) {
1913
+ if (f4.node.name.toLowerCase().includes(searchLower)) matchCount++;
1914
+ }
1915
+ this.callbacks.onSearchResults(matchCount, this.frames.length);
1916
+ }
1801
1917
  for (const frame of this.frames) {
1802
1918
  const x2 = (frame.absStart - this.viewport.start) / vpRange * w3;
1803
1919
  const fw = (frame.absEnd - frame.absStart) / vpRange * w3;
@@ -1805,8 +1921,9 @@
1805
1921
  if (x2 + fw < 0 || x2 > w3 || fw < 0.5) continue;
1806
1922
  const color = CATEGORY_COLORS[frame.node.category] || "#a78bfa";
1807
1923
  const isHovered = frame === this.hoveredFrame;
1924
+ const isMatch = !hasSearch || frame.node.name.toLowerCase().includes(searchLower);
1808
1925
  ctx.fillStyle = isHovered ? this.lightenColor(color, 0.2) : color;
1809
- ctx.globalAlpha = isHovered ? 1 : 0.85;
1926
+ ctx.globalAlpha = hasSearch && !isMatch ? 0.2 : isHovered ? 1 : 0.85;
1810
1927
  this.roundRect(ctx, x2, y3, fw, FRAME_HEIGHT, 3);
1811
1928
  ctx.fill();
1812
1929
  ctx.globalAlpha = 1;
@@ -1815,8 +1932,15 @@
1815
1932
  ctx.lineWidth = 1.5;
1816
1933
  this.roundRect(ctx, x2, y3, fw, FRAME_HEIGHT, 3);
1817
1934
  ctx.stroke();
1935
+ } else if (hasSearch && isMatch) {
1936
+ ctx.strokeStyle = "#ffffff";
1937
+ ctx.lineWidth = 1;
1938
+ ctx.globalAlpha = 0.5;
1939
+ this.roundRect(ctx, x2, y3, fw, FRAME_HEIGHT, 3);
1940
+ ctx.stroke();
1941
+ ctx.globalAlpha = 1;
1818
1942
  }
1819
- if (fw > MIN_TEXT_WIDTH) {
1943
+ if (fw > MIN_TEXT_WIDTH && isMatch) {
1820
1944
  ctx.fillStyle = this.getTextColor(color);
1821
1945
  ctx.font = '11px "JetBrains Mono", monospace';
1822
1946
  ctx.textBaseline = "middle";
@@ -1889,7 +2013,8 @@
1889
2013
  partial: "Partial",
1890
2014
  sql: "SQL",
1891
2015
  cache: "Cache",
1892
- http: "HTTP"
2016
+ http: "HTTP",
2017
+ custom: "Custom"
1893
2018
  };
1894
2019
  var CATEGORY_COLORS2 = {
1895
2020
  controller: "#60a5fa",
@@ -1897,7 +2022,8 @@
1897
2022
  partial: "#f59e0b",
1898
2023
  sql: "#fb923c",
1899
2024
  cache: "#a78bfa",
1900
- http: "#f87171"
2025
+ http: "#f87171",
2026
+ custom: "#e879f9"
1901
2027
  };
1902
2028
  var FlameGraphTooltip = class {
1903
2029
  constructor(container, totalDuration) {
@@ -1944,6 +2070,9 @@
1944
2070
  payloadText = `Key: ${node.payload.key}`;
1945
2071
  } else if (category === "http" && node.payload.url) {
1946
2072
  payloadText = node.payload.url;
2073
+ } else if (category === "custom" && Object.keys(node.payload).length > 0) {
2074
+ const entries = Object.entries(node.payload).map(([k3, v3]) => `${k3}: ${JSON.stringify(v3)}`).join("\n");
2075
+ payloadText = entries.length > 200 ? entries.slice(0, 200) + "..." : entries;
1947
2076
  }
1948
2077
  if (payloadText) {
1949
2078
  const payloadDiv = document.createElement("div");
@@ -2025,7 +2154,8 @@
2025
2154
  partial: "#f59e0b",
2026
2155
  sql: "#fb923c",
2027
2156
  cache: "#a78bfa",
2028
- http: "#f87171"
2157
+ http: "#f87171",
2158
+ custom: "#e879f9"
2029
2159
  };
2030
2160
  var CATEGORY_LABELS2 = {
2031
2161
  controller: "Controller",
@@ -2033,7 +2163,8 @@
2033
2163
  partial: "Partial",
2034
2164
  sql: "SQL",
2035
2165
  cache: "Cache",
2036
- http: "HTTP"
2166
+ http: "HTTP",
2167
+ custom: "Custom"
2037
2168
  };
2038
2169
  function FlameGraphTab({ flamegraphData, perfData }) {
2039
2170
  const canvasRef = A2(null);
@@ -2041,7 +2172,11 @@
2041
2172
  const rendererRef = A2(null);
2042
2173
  const tooltipRef = A2(null);
2043
2174
  const breadcrumbsRef = A2(null);
2175
+ const searchInputRef = A2(null);
2044
2176
  const [isZoomed, setIsZoomed] = d2(false);
2177
+ const [searchQuery, setSearchQuery] = d2("");
2178
+ const [matchCount, setMatchCount] = d2(0);
2179
+ const [totalCount, setTotalCount] = d2(0);
2045
2180
  const data = flamegraphData;
2046
2181
  y2(() => {
2047
2182
  if (!data?.root_events?.length || !canvasRef.current || !containerRef.current) return;
@@ -2073,6 +2208,10 @@
2073
2208
  onZoomChange: (ancestors) => {
2074
2209
  breadcrumbs.update(ancestors);
2075
2210
  setIsZoomed(ancestors.length > 0);
2211
+ },
2212
+ onSearchResults: (match, total) => {
2213
+ setMatchCount(match);
2214
+ setTotalCount(total);
2076
2215
  }
2077
2216
  });
2078
2217
  rendererRef.current = renderer;
@@ -2091,6 +2230,23 @@
2091
2230
  breadcrumbsRef.current = null;
2092
2231
  };
2093
2232
  }, [data]);
2233
+ y2(() => {
2234
+ rendererRef.current?.setSearchQuery(searchQuery);
2235
+ if (!searchQuery) {
2236
+ setMatchCount(0);
2237
+ setTotalCount(0);
2238
+ }
2239
+ }, [searchQuery]);
2240
+ y2(() => {
2241
+ const onKeyDown = (e3) => {
2242
+ if ((e3.ctrlKey || e3.metaKey) && e3.key === "f") {
2243
+ e3.preventDefault();
2244
+ searchInputRef.current?.focus();
2245
+ }
2246
+ };
2247
+ document.addEventListener("keydown", onKeyDown, { capture: true });
2248
+ return () => document.removeEventListener("keydown", onKeyDown, { capture: true });
2249
+ }, []);
2094
2250
  if (!data?.root_events?.length) {
2095
2251
  if (!perfData?.events?.length) {
2096
2252
  return /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("p", { class: "profiler-empty__description", children: "No performance events recorded" }) });
@@ -2161,6 +2317,24 @@
2161
2317
  ] }, cat)) }),
2162
2318
  /* @__PURE__ */ u3("div", { class: "profiler-flamegraph__controls", children: [
2163
2319
  isZoomed && /* @__PURE__ */ u3("button", { class: "profiler-flamegraph__reset", onClick: handleReset, children: "Reset Zoom" }),
2320
+ /* @__PURE__ */ u3("div", { class: "profiler-flamegraph__search", children: [
2321
+ /* @__PURE__ */ u3(
2322
+ "input",
2323
+ {
2324
+ ref: searchInputRef,
2325
+ type: "text",
2326
+ class: "profiler-flamegraph__search-input",
2327
+ placeholder: "Search events\u2026 (Ctrl+F)",
2328
+ value: searchQuery,
2329
+ onInput: (e3) => setSearchQuery(e3.target.value)
2330
+ }
2331
+ ),
2332
+ searchQuery && /* @__PURE__ */ u3("span", { class: "profiler-flamegraph__match-count", children: [
2333
+ matchCount,
2334
+ " / ",
2335
+ totalCount
2336
+ ] })
2337
+ ] }),
2164
2338
  /* @__PURE__ */ u3("span", { class: "profiler-flamegraph__hint", children: "Click to zoom, scroll to zoom in/out, drag to pan" })
2165
2339
  ] }),
2166
2340
  /* @__PURE__ */ u3("div", { class: "profiler-flamegraph__canvas-container", ref: containerRef, children: /* @__PURE__ */ u3(
@@ -2853,7 +3027,7 @@
2853
3027
  ] }),
2854
3028
  /* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
2855
3029
  activeTab === "job" && /* @__PURE__ */ u3(JobTab, { jobData: cd["job"] }),
2856
- activeTab === "database" && /* @__PURE__ */ u3(DatabaseTab, { dbData: cd["database"] }),
3030
+ activeTab === "database" && /* @__PURE__ */ u3(DatabaseTab, { dbData: cd["database"], token: profile.token }),
2857
3031
  activeTab === "cache" && /* @__PURE__ */ u3(CacheTab, { cacheData: cd["cache"] }),
2858
3032
  activeTab === "http" && /* @__PURE__ */ u3(HttpTab, { httpData: cd["http"] })
2859
3033
  ] })
@@ -109,6 +109,17 @@ module Profiler
109
109
  end
110
110
  end
111
111
 
112
+ # Called by Profiler.measure to record custom instrumentation events
113
+ def record_custom_event(label:, started_at:, finished_at:, metadata: {})
114
+ @events << Models::TimelineEvent.new(
115
+ name: label,
116
+ started_at: started_at,
117
+ finished_at: finished_at,
118
+ category: "custom",
119
+ payload: metadata
120
+ )
121
+ end
122
+
112
123
  # Called by NetHttpInstrumentation to record outbound HTTP events
113
124
  def record_http_event(started_at:, finished_at:, url:, method:, status:)
114
125
  @events << Models::TimelineEvent.new(
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.7.0"
4
+ VERSION = "0.9.0"
5
5
  end
data/lib/profiler.rb CHANGED
@@ -25,6 +25,28 @@ module Profiler
25
25
  configuration.enabled
26
26
  end
27
27
 
28
+ # Instrument an arbitrary code block and record it in the FlameGraph.
29
+ # Usage: Profiler.measure("payment.stripe_charge", metadata: { amount: 1000 }) { Stripe::Charge.create(...) }
30
+ def measure(label, metadata: {}, &block)
31
+ return yield unless enabled?
32
+
33
+ collector = Thread.current[:profiler_flamegraph_collector]
34
+ return yield unless collector
35
+
36
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
37
+ result = yield
38
+ finished_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
39
+
40
+ collector.record_custom_event(
41
+ label: label,
42
+ started_at: started_at,
43
+ finished_at: finished_at,
44
+ metadata: metadata
45
+ )
46
+
47
+ result
48
+ end
49
+
28
50
  # Dump a variable to the profiler
29
51
  # Usage: Profiler.dump(variable, "optional label")
30
52
  def dump(value, label = nil)
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.7.0
4
+ version: 0.9.0
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-04-05 00:00:00.000000000 Z
11
+ date: 2026-04-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails