rails-profiler 0.7.0 → 0.8.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: 6c1f16fad7d5792dda0491401519802bd402be3b6c952b898cc0930e675a0d49
4
+ data.tar.gz: 6ea6e3ca98b297f89b32da94a56cf73ff38788e8d31a9570dd58c072bfb58930
5
5
  SHA512:
6
- metadata.gz: c69ecc008b9b9c79161cdc18326c9b0c43e99b7a7284b2681980c0c2d2b7a94c10f77dc186b2826f2cf111f53063e10a3790c4245e4c8a4a07bdde7f49b02e7a
7
- data.tar.gz: 22031e28d3fc173b52dab71ed8f1fe8f0faf350134ed683b2847d1ad3a4e44e5de438b6de76d83a3f2b554aa22175cee5b578af0aacc4a8b247f2fd32390d431
6
+ metadata.gz: 3e487bc2fc57bd9a1dee6eabdc45db629dd752e0df2ce95e74fc31701583ab5d8de72fa57b7b19dc81d6c412c6bec2b4a814768af84ab80da5af6c61b3fb063a
7
+ data.tar.gz: 0e3572af0b7f7bca2d1bc132da45173ad9dd2f29010d5685a987d247a89c114d5c76846781a5383aa7bee2fdef038059aa66ac8c9e8bf7ed1245c8a6eaf50354
@@ -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" }) }),
@@ -1567,6 +1668,7 @@
1567
1668
  this.dpr = 1;
1568
1669
  this.hoveredFrame = null;
1569
1670
  this.zoomStack = [];
1671
+ this.searchQuery = "";
1570
1672
  this.isPanning = false;
1571
1673
  this.panStartX = 0;
1572
1674
  this.panStartViewport = { start: 0, end: 0 };
@@ -1789,6 +1891,10 @@
1789
1891
  this.render();
1790
1892
  }
1791
1893
  }
1894
+ setSearchQuery(query) {
1895
+ this.searchQuery = query;
1896
+ this.render();
1897
+ }
1792
1898
  render() {
1793
1899
  const ctx = this.ctx;
1794
1900
  const w3 = this.canvas.width / this.dpr;
@@ -1798,6 +1904,15 @@
1798
1904
  const style = getComputedStyle(this.canvas);
1799
1905
  const textColor = style.getPropertyValue("--profiler-text").trim() || "#eef2f7";
1800
1906
  const textMuted = style.getPropertyValue("--profiler-text-muted").trim() || "#5e7080";
1907
+ const searchLower = this.searchQuery.toLowerCase();
1908
+ const hasSearch = searchLower.length > 0;
1909
+ if (hasSearch && this.callbacks.onSearchResults) {
1910
+ let matchCount = 0;
1911
+ for (const f4 of this.frames) {
1912
+ if (f4.node.name.toLowerCase().includes(searchLower)) matchCount++;
1913
+ }
1914
+ this.callbacks.onSearchResults(matchCount, this.frames.length);
1915
+ }
1801
1916
  for (const frame of this.frames) {
1802
1917
  const x2 = (frame.absStart - this.viewport.start) / vpRange * w3;
1803
1918
  const fw = (frame.absEnd - frame.absStart) / vpRange * w3;
@@ -1805,8 +1920,9 @@
1805
1920
  if (x2 + fw < 0 || x2 > w3 || fw < 0.5) continue;
1806
1921
  const color = CATEGORY_COLORS[frame.node.category] || "#a78bfa";
1807
1922
  const isHovered = frame === this.hoveredFrame;
1923
+ const isMatch = !hasSearch || frame.node.name.toLowerCase().includes(searchLower);
1808
1924
  ctx.fillStyle = isHovered ? this.lightenColor(color, 0.2) : color;
1809
- ctx.globalAlpha = isHovered ? 1 : 0.85;
1925
+ ctx.globalAlpha = hasSearch && !isMatch ? 0.2 : isHovered ? 1 : 0.85;
1810
1926
  this.roundRect(ctx, x2, y3, fw, FRAME_HEIGHT, 3);
1811
1927
  ctx.fill();
1812
1928
  ctx.globalAlpha = 1;
@@ -1815,8 +1931,15 @@
1815
1931
  ctx.lineWidth = 1.5;
1816
1932
  this.roundRect(ctx, x2, y3, fw, FRAME_HEIGHT, 3);
1817
1933
  ctx.stroke();
1934
+ } else if (hasSearch && isMatch) {
1935
+ ctx.strokeStyle = "#ffffff";
1936
+ ctx.lineWidth = 1;
1937
+ ctx.globalAlpha = 0.5;
1938
+ this.roundRect(ctx, x2, y3, fw, FRAME_HEIGHT, 3);
1939
+ ctx.stroke();
1940
+ ctx.globalAlpha = 1;
1818
1941
  }
1819
- if (fw > MIN_TEXT_WIDTH) {
1942
+ if (fw > MIN_TEXT_WIDTH && isMatch) {
1820
1943
  ctx.fillStyle = this.getTextColor(color);
1821
1944
  ctx.font = '11px "JetBrains Mono", monospace';
1822
1945
  ctx.textBaseline = "middle";
@@ -2041,7 +2164,11 @@
2041
2164
  const rendererRef = A2(null);
2042
2165
  const tooltipRef = A2(null);
2043
2166
  const breadcrumbsRef = A2(null);
2167
+ const searchInputRef = A2(null);
2044
2168
  const [isZoomed, setIsZoomed] = d2(false);
2169
+ const [searchQuery, setSearchQuery] = d2("");
2170
+ const [matchCount, setMatchCount] = d2(0);
2171
+ const [totalCount, setTotalCount] = d2(0);
2045
2172
  const data = flamegraphData;
2046
2173
  y2(() => {
2047
2174
  if (!data?.root_events?.length || !canvasRef.current || !containerRef.current) return;
@@ -2073,6 +2200,10 @@
2073
2200
  onZoomChange: (ancestors) => {
2074
2201
  breadcrumbs.update(ancestors);
2075
2202
  setIsZoomed(ancestors.length > 0);
2203
+ },
2204
+ onSearchResults: (match, total) => {
2205
+ setMatchCount(match);
2206
+ setTotalCount(total);
2076
2207
  }
2077
2208
  });
2078
2209
  rendererRef.current = renderer;
@@ -2091,6 +2222,23 @@
2091
2222
  breadcrumbsRef.current = null;
2092
2223
  };
2093
2224
  }, [data]);
2225
+ y2(() => {
2226
+ rendererRef.current?.setSearchQuery(searchQuery);
2227
+ if (!searchQuery) {
2228
+ setMatchCount(0);
2229
+ setTotalCount(0);
2230
+ }
2231
+ }, [searchQuery]);
2232
+ y2(() => {
2233
+ const onKeyDown = (e3) => {
2234
+ if ((e3.ctrlKey || e3.metaKey) && e3.key === "f") {
2235
+ e3.preventDefault();
2236
+ searchInputRef.current?.focus();
2237
+ }
2238
+ };
2239
+ document.addEventListener("keydown", onKeyDown, { capture: true });
2240
+ return () => document.removeEventListener("keydown", onKeyDown, { capture: true });
2241
+ }, []);
2094
2242
  if (!data?.root_events?.length) {
2095
2243
  if (!perfData?.events?.length) {
2096
2244
  return /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("p", { class: "profiler-empty__description", children: "No performance events recorded" }) });
@@ -2161,6 +2309,24 @@
2161
2309
  ] }, cat)) }),
2162
2310
  /* @__PURE__ */ u3("div", { class: "profiler-flamegraph__controls", children: [
2163
2311
  isZoomed && /* @__PURE__ */ u3("button", { class: "profiler-flamegraph__reset", onClick: handleReset, children: "Reset Zoom" }),
2312
+ /* @__PURE__ */ u3("div", { class: "profiler-flamegraph__search", children: [
2313
+ /* @__PURE__ */ u3(
2314
+ "input",
2315
+ {
2316
+ ref: searchInputRef,
2317
+ type: "text",
2318
+ class: "profiler-flamegraph__search-input",
2319
+ placeholder: "Search events\u2026 (Ctrl+F)",
2320
+ value: searchQuery,
2321
+ onInput: (e3) => setSearchQuery(e3.target.value)
2322
+ }
2323
+ ),
2324
+ searchQuery && /* @__PURE__ */ u3("span", { class: "profiler-flamegraph__match-count", children: [
2325
+ matchCount,
2326
+ " / ",
2327
+ totalCount
2328
+ ] })
2329
+ ] }),
2164
2330
  /* @__PURE__ */ u3("span", { class: "profiler-flamegraph__hint", children: "Click to zoom, scroll to zoom in/out, drag to pan" })
2165
2331
  ] }),
2166
2332
  /* @__PURE__ */ u3("div", { class: "profiler-flamegraph__canvas-container", ref: containerRef, children: /* @__PURE__ */ u3(
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.7.0"
4
+ VERSION = "0.8.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
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.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sébastien Duplessy