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 +4 -4
- data/app/assets/builds/profiler.css +82 -0
- data/app/assets/builds/profiler.js +188 -14
- data/lib/profiler/collectors/flamegraph_collector.rb +11 -0
- data/lib/profiler/version.rb +1 -1
- data/lib/profiler.rb +22 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1530091bc78596f3f4b410c27e7ab7d8ff332c644ea345b6f7e6819a9e87fc62
|
|
4
|
+
data.tar.gz: a733275e9bf99ed4300d85b7fa4a4f88f0249705f3183ead0956bb75a51b6562
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 =
|
|
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", {
|
|
978
|
-
|
|
979
|
-
|
|
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:
|
|
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(
|
data/lib/profiler/version.rb
CHANGED
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.
|
|
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-
|
|
11
|
+
date: 2026-04-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|