rails-profiler 0.15.0 → 0.16.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 +47 -0
- data/app/assets/builds/profiler.js +148 -22
- data/app/controllers/profiler/api/function_profiling_controller.rb +16 -4
- data/lib/profiler/collectors/function_profiler_collector.rb +257 -35
- data/lib/profiler/version.rb +1 -1
- data/lib/profiler.rb +5 -1
- 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: 786465dc498178e1ab9256de7b02b397df7db82b4fc817fc3adbe191d8170fcd
|
|
4
|
+
data.tar.gz: acdeb2a1cdb00a59429bdf14a75f0551c4e94e65787b0640096dd5e044520999
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5e6e51705278a372284b5936aab067c5c1fc09d257561b2787e9d8a5847f608c1cdc2149b74259abc9f411b48e8369f5fd368fd934b13eb8c2a47efbafd27651
|
|
7
|
+
data.tar.gz: f2cf3377ba982ee6135a0040d4f9d1c588c7255724d72d5fde8a38ed53cba188a40a669fa28161b906e3cc401b46405a2c24aac5d5438233e854b2e3c2ec3b74
|
|
@@ -2702,6 +2702,53 @@ a.profiler-toolbar-item.profiler-text--warning::after {
|
|
|
2702
2702
|
.profiler-fn-profiling__toggle--active:hover {
|
|
2703
2703
|
background: rgba(245, 158, 11, 0.15);
|
|
2704
2704
|
}
|
|
2705
|
+
.profiler-fn-profiling__mode-selector {
|
|
2706
|
+
display: inline-flex;
|
|
2707
|
+
border: 1px solid var(--profiler-border-strong);
|
|
2708
|
+
border-radius: var(--profiler-radius-sm);
|
|
2709
|
+
overflow: hidden;
|
|
2710
|
+
}
|
|
2711
|
+
.profiler-fn-profiling__mode-btn {
|
|
2712
|
+
height: 28px;
|
|
2713
|
+
padding: 0 12px;
|
|
2714
|
+
background: var(--profiler-bg-lighter);
|
|
2715
|
+
border: none;
|
|
2716
|
+
border-right: 1px solid var(--profiler-border-strong);
|
|
2717
|
+
color: var(--profiler-text-muted);
|
|
2718
|
+
font-size: var(--profiler-text-xs);
|
|
2719
|
+
font-weight: 600;
|
|
2720
|
+
font-family: var(--profiler-font-sans);
|
|
2721
|
+
cursor: pointer;
|
|
2722
|
+
transition: all var(--profiler-transition-base);
|
|
2723
|
+
white-space: nowrap;
|
|
2724
|
+
}
|
|
2725
|
+
.profiler-fn-profiling__mode-btn:last-child {
|
|
2726
|
+
border-right: none;
|
|
2727
|
+
}
|
|
2728
|
+
.profiler-fn-profiling__mode-btn:hover:not(:disabled):not(.profiler-fn-profiling__mode-btn--active) {
|
|
2729
|
+
background: var(--profiler-bg-elevated);
|
|
2730
|
+
color: var(--profiler-text);
|
|
2731
|
+
}
|
|
2732
|
+
.profiler-fn-profiling__mode-btn--active {
|
|
2733
|
+
background: var(--profiler-accent-bg);
|
|
2734
|
+
color: var(--profiler-accent);
|
|
2735
|
+
cursor: default;
|
|
2736
|
+
}
|
|
2737
|
+
.profiler-fn-profiling__mode-btn:disabled {
|
|
2738
|
+
opacity: 0.5;
|
|
2739
|
+
cursor: not-allowed;
|
|
2740
|
+
}
|
|
2741
|
+
.profiler-fn-profiling__cpu-pct {
|
|
2742
|
+
color: var(--profiler-text-subtle);
|
|
2743
|
+
font-size: 0.85em;
|
|
2744
|
+
}
|
|
2745
|
+
.profiler-fn-profiling__gc--medium {
|
|
2746
|
+
color: #f59e0b;
|
|
2747
|
+
}
|
|
2748
|
+
.profiler-fn-profiling__gc--high {
|
|
2749
|
+
color: #ef4444;
|
|
2750
|
+
font-weight: 600;
|
|
2751
|
+
}
|
|
2705
2752
|
.profiler-fn-profiling__hint {
|
|
2706
2753
|
margin: 12px 0 0;
|
|
2707
2754
|
padding: 10px 14px;
|
|
@@ -2513,8 +2513,12 @@
|
|
|
2513
2513
|
const [fnSortDir, setFnSortDir] = d2("desc");
|
|
2514
2514
|
const [fnEnabled, setFnEnabled] = d2(functionProfileData?.enabled ?? false);
|
|
2515
2515
|
const [fnMaxFrames, setFnMaxFrames] = d2(functionProfileData?.max_frames ?? 2e3);
|
|
2516
|
+
const [fnMode, setFnMode] = d2(functionProfileData?.mode === "lite" ? "lite" : "full");
|
|
2517
|
+
const [fnClock, setFnClock] = d2(functionProfileData?.clock ?? "wall");
|
|
2516
2518
|
const [fnToggling, setFnToggling] = d2(false);
|
|
2517
2519
|
const [fnMaxFramesUpdating, setFnMaxFramesUpdating] = d2(false);
|
|
2520
|
+
const [fnModeUpdating, setFnModeUpdating] = d2(false);
|
|
2521
|
+
const [fnClockUpdating, setFnClockUpdating] = d2(false);
|
|
2518
2522
|
const patchFunctionProfiling = async (patch) => {
|
|
2519
2523
|
const res = await fetch("/_profiler/api/function_profiling", {
|
|
2520
2524
|
method: "PATCH",
|
|
@@ -2541,6 +2545,25 @@
|
|
|
2541
2545
|
setFnMaxFramesUpdating(false);
|
|
2542
2546
|
}
|
|
2543
2547
|
};
|
|
2548
|
+
const updateMode = async (value) => {
|
|
2549
|
+
setFnMode(value);
|
|
2550
|
+
setFnModeUpdating(true);
|
|
2551
|
+
try {
|
|
2552
|
+
const json = await patchFunctionProfiling({ mode: value });
|
|
2553
|
+
setFnMode(json.mode ?? value);
|
|
2554
|
+
} finally {
|
|
2555
|
+
setFnModeUpdating(false);
|
|
2556
|
+
}
|
|
2557
|
+
};
|
|
2558
|
+
const updateClock = async (value) => {
|
|
2559
|
+
setFnClockUpdating(true);
|
|
2560
|
+
try {
|
|
2561
|
+
const json = await patchFunctionProfiling({ clock: value });
|
|
2562
|
+
setFnClock(json.clock ?? value);
|
|
2563
|
+
} finally {
|
|
2564
|
+
setFnClockUpdating(false);
|
|
2565
|
+
}
|
|
2566
|
+
};
|
|
2544
2567
|
const data = flamegraphData;
|
|
2545
2568
|
y2(() => {
|
|
2546
2569
|
if (!data?.root_events?.length || !canvasRef.current || !containerRef.current) return;
|
|
@@ -2623,10 +2646,16 @@
|
|
|
2623
2646
|
toggling: fnToggling,
|
|
2624
2647
|
maxFrames: fnMaxFrames,
|
|
2625
2648
|
maxFramesUpdating: fnMaxFramesUpdating,
|
|
2649
|
+
mode: fnMode,
|
|
2650
|
+
modeUpdating: fnModeUpdating,
|
|
2651
|
+
clock: fnClock,
|
|
2652
|
+
clockUpdating: fnClockUpdating,
|
|
2626
2653
|
sortKey: fnSortKey,
|
|
2627
2654
|
sortDir: fnSortDir,
|
|
2628
2655
|
onToggle: toggleFunctionProfiling,
|
|
2629
2656
|
onMaxFramesChange: updateMaxFrames,
|
|
2657
|
+
onModeChange: updateMode,
|
|
2658
|
+
onClockChange: updateClock,
|
|
2630
2659
|
onSortChange: (key, dir) => {
|
|
2631
2660
|
setFnSortKey(key);
|
|
2632
2661
|
setFnSortDir(dir);
|
|
@@ -2666,10 +2695,16 @@
|
|
|
2666
2695
|
toggling: fnToggling,
|
|
2667
2696
|
maxFrames: fnMaxFrames,
|
|
2668
2697
|
maxFramesUpdating: fnMaxFramesUpdating,
|
|
2698
|
+
mode: fnMode,
|
|
2699
|
+
modeUpdating: fnModeUpdating,
|
|
2700
|
+
clock: fnClock,
|
|
2701
|
+
clockUpdating: fnClockUpdating,
|
|
2669
2702
|
sortKey: fnSortKey,
|
|
2670
2703
|
sortDir: fnSortDir,
|
|
2671
2704
|
onToggle: toggleFunctionProfiling,
|
|
2672
2705
|
onMaxFramesChange: updateMaxFrames,
|
|
2706
|
+
onModeChange: updateMode,
|
|
2707
|
+
onClockChange: updateClock,
|
|
2673
2708
|
onSortChange: (key, dir) => {
|
|
2674
2709
|
setFnSortKey(key);
|
|
2675
2710
|
setFnSortDir(dir);
|
|
@@ -2755,10 +2790,16 @@
|
|
|
2755
2790
|
toggling: fnToggling,
|
|
2756
2791
|
maxFrames: fnMaxFrames,
|
|
2757
2792
|
maxFramesUpdating: fnMaxFramesUpdating,
|
|
2793
|
+
mode: fnMode,
|
|
2794
|
+
modeUpdating: fnModeUpdating,
|
|
2795
|
+
clock: fnClock,
|
|
2796
|
+
clockUpdating: fnClockUpdating,
|
|
2758
2797
|
sortKey: fnSortKey,
|
|
2759
2798
|
sortDir: fnSortDir,
|
|
2760
2799
|
onToggle: toggleFunctionProfiling,
|
|
2761
2800
|
onMaxFramesChange: updateMaxFrames,
|
|
2801
|
+
onModeChange: updateMode,
|
|
2802
|
+
onClockChange: updateClock,
|
|
2762
2803
|
onSortChange: (key, dir) => {
|
|
2763
2804
|
setFnSortKey(key);
|
|
2764
2805
|
setFnSortDir(dir);
|
|
@@ -2780,15 +2821,23 @@
|
|
|
2780
2821
|
}
|
|
2781
2822
|
return null;
|
|
2782
2823
|
}
|
|
2783
|
-
function FunctionProfilingSection({ data, enabled, toggling, maxFrames, maxFramesUpdating, sortKey, sortDir, onToggle, onMaxFramesChange, onSortChange }) {
|
|
2824
|
+
function FunctionProfilingSection({ data, enabled, toggling, maxFrames, maxFramesUpdating, mode, modeUpdating, clock, clockUpdating, sortKey, sortDir, onToggle, onMaxFramesChange, onModeChange, onClockChange, onSortChange }) {
|
|
2784
2825
|
const [filterNames, setFilterNames] = d2(null);
|
|
2785
2826
|
const [filterLabel, setFilterLabel] = d2(null);
|
|
2786
2827
|
const [hoveredFnName, setHoveredFnName] = d2(null);
|
|
2787
2828
|
const fnRendererRef = A2(null);
|
|
2788
2829
|
const hasData = enabled && data?.enabled && (data.functions?.length ?? 0) > 0;
|
|
2830
|
+
const dataMode = data?.mode ?? "full";
|
|
2831
|
+
const dataClock = data?.clock ?? "wall";
|
|
2832
|
+
const isSampling = dataMode === "lite";
|
|
2833
|
+
const isObjectClock = dataClock === "object";
|
|
2834
|
+
const showAllocated = !isSampling;
|
|
2835
|
+
const showMemory = dataMode === "full";
|
|
2836
|
+
const showClock = mode === "lite";
|
|
2837
|
+
const effectiveSortKey = sortKey === "memory_bytes" && !showMemory || sortKey === "allocated_objects" && !showAllocated ? "total_duration" : sortKey;
|
|
2789
2838
|
const rootCalls = hasData ? data.root_calls ?? [] : [];
|
|
2790
2839
|
const sortedFunctions = hasData ? [...data.functions].sort(
|
|
2791
|
-
(a3, b) => sortDir === "asc" ? a3[
|
|
2840
|
+
(a3, b) => sortDir === "asc" ? a3[effectiveSortKey] - b[effectiveSortKey] : b[effectiveSortKey] - a3[effectiveSortKey]
|
|
2792
2841
|
) : [];
|
|
2793
2842
|
const displayedFunctions = filterNames ? sortedFunctions.filter((fn) => filterNames.has(fn.name)) : sortedFunctions;
|
|
2794
2843
|
const handleFrameSelect = (node) => {
|
|
@@ -2808,7 +2857,7 @@
|
|
|
2808
2857
|
}
|
|
2809
2858
|
};
|
|
2810
2859
|
const sortIcon = (key) => {
|
|
2811
|
-
if (
|
|
2860
|
+
if (effectiveSortKey !== key) return /* @__PURE__ */ u3("span", { class: "sort-icon sort-icon--idle", children: "\u21C5" });
|
|
2812
2861
|
return /* @__PURE__ */ u3("span", { class: "sort-icon sort-icon--active", children: sortDir === "asc" ? "\u25B2" : "\u25BC" });
|
|
2813
2862
|
};
|
|
2814
2863
|
const handleMaxFramesBlur = (e3) => {
|
|
@@ -2826,6 +2875,28 @@
|
|
|
2826
2875
|
/* @__PURE__ */ u3("div", { class: "profiler-fn-profiling__header", children: [
|
|
2827
2876
|
/* @__PURE__ */ u3("span", { class: `profiler-fn-profiling__title${enabled ? " profiler-fn-profiling__title--active" : ""}`, children: "Function Profiling" }),
|
|
2828
2877
|
/* @__PURE__ */ u3("div", { class: "profiler-fn-profiling__controls", children: [
|
|
2878
|
+
/* @__PURE__ */ u3("div", { class: "profiler-fn-profiling__mode-selector", children: ["full", "lite"].map((m3) => /* @__PURE__ */ u3(
|
|
2879
|
+
"button",
|
|
2880
|
+
{
|
|
2881
|
+
class: `profiler-fn-profiling__mode-btn${mode === m3 ? " profiler-fn-profiling__mode-btn--active" : ""}`,
|
|
2882
|
+
onClick: () => mode !== m3 && onModeChange(m3),
|
|
2883
|
+
disabled: modeUpdating,
|
|
2884
|
+
title: m3 === "full" ? "Exhaustive TracePoint \u2014 timing + allocations + memory bytes" : "StackProf sampling \u2014 very low overhead",
|
|
2885
|
+
children: m3.charAt(0).toUpperCase() + m3.slice(1)
|
|
2886
|
+
},
|
|
2887
|
+
m3
|
|
2888
|
+
)) }),
|
|
2889
|
+
showClock && /* @__PURE__ */ u3("div", { class: "profiler-fn-profiling__mode-selector", children: ["wall", "cpu", "object"].map((c3) => /* @__PURE__ */ u3(
|
|
2890
|
+
"button",
|
|
2891
|
+
{
|
|
2892
|
+
class: `profiler-fn-profiling__mode-btn${clock === c3 ? " profiler-fn-profiling__mode-btn--active" : ""}`,
|
|
2893
|
+
onClick: () => clock !== c3 && onClockChange(c3),
|
|
2894
|
+
disabled: clockUpdating,
|
|
2895
|
+
title: c3 === "wall" ? "Wall-clock time (includes I/O waits)" : c3 === "cpu" ? "CPU time only (excludes I/O waits)" : "Object allocations per function",
|
|
2896
|
+
children: c3 === "wall" ? "Wall" : c3 === "cpu" ? "CPU" : "Alloc"
|
|
2897
|
+
},
|
|
2898
|
+
c3
|
|
2899
|
+
)) }),
|
|
2829
2900
|
/* @__PURE__ */ u3("label", { class: "profiler-fn-profiling__max-frames-label", children: [
|
|
2830
2901
|
"Max frames",
|
|
2831
2902
|
/* @__PURE__ */ u3(
|
|
@@ -2853,7 +2924,15 @@
|
|
|
2853
2924
|
)
|
|
2854
2925
|
] })
|
|
2855
2926
|
] }),
|
|
2856
|
-
!enabled && /* @__PURE__ */ u3("p", { class: "profiler-fn-profiling__hint", children:
|
|
2927
|
+
!enabled && /* @__PURE__ */ u3("p", { class: "profiler-fn-profiling__hint", children: [
|
|
2928
|
+
"Enable function profiling to see where your app spends time.",
|
|
2929
|
+
" ",
|
|
2930
|
+
/* @__PURE__ */ u3("strong", { children: "Lite" }),
|
|
2931
|
+
": statistical sampling via stackprof \u2014 very low overhead (<1%), enabled by default.",
|
|
2932
|
+
" ",
|
|
2933
|
+
/* @__PURE__ */ u3("strong", { children: "Full" }),
|
|
2934
|
+
": exhaustive TracePoint tracing with memory bytes \u2014 significant overhead."
|
|
2935
|
+
] }),
|
|
2857
2936
|
enabled && !hasData && /* @__PURE__ */ u3("p", { class: "profiler-fn-profiling__hint", children: "Function profiling is active. Data will appear on the next request." }),
|
|
2858
2937
|
hasData && /* @__PURE__ */ u3(k, { children: [
|
|
2859
2938
|
data.frame_cap_reached && /* @__PURE__ */ u3("div", { class: "profiler-fn-profiling__cap-warning", children: [
|
|
@@ -2867,18 +2946,54 @@
|
|
|
2867
2946
|
/* @__PURE__ */ u3("span", { class: "stat-value", children: data.functions.length })
|
|
2868
2947
|
] }),
|
|
2869
2948
|
/* @__PURE__ */ u3("div", { class: "stat-item", children: [
|
|
2870
|
-
/* @__PURE__ */ u3("span", { class: "stat-label", children: "Total Calls" }),
|
|
2949
|
+
/* @__PURE__ */ u3("span", { class: "stat-label", children: isSampling ? "Total Samples" : "Total Calls" }),
|
|
2871
2950
|
/* @__PURE__ */ u3("span", { class: "stat-value", children: data.total_calls })
|
|
2872
2951
|
] }),
|
|
2873
|
-
/* @__PURE__ */ u3("div", { class: "stat-item", children: [
|
|
2874
|
-
/* @__PURE__ */ u3("span", { class: "stat-label", children: "
|
|
2952
|
+
!isObjectClock && /* @__PURE__ */ u3("div", { class: "stat-item", children: [
|
|
2953
|
+
/* @__PURE__ */ u3("span", { class: "stat-label", children: "Wall" }),
|
|
2875
2954
|
/* @__PURE__ */ u3("span", { class: "stat-value", children: [
|
|
2876
|
-
data.total_duration
|
|
2955
|
+
(data.elapsed_wall_ms ?? data.total_duration ?? 0).toFixed(2),
|
|
2877
2956
|
" ",
|
|
2878
2957
|
/* @__PURE__ */ u3("small", { children: "ms" })
|
|
2879
2958
|
] })
|
|
2880
2959
|
] }),
|
|
2881
|
-
/* @__PURE__ */ u3("div", { class: "stat-item", children: [
|
|
2960
|
+
isSampling && !isObjectClock && data.elapsed_cpu_ms != null && /* @__PURE__ */ u3("div", { class: "stat-item", children: [
|
|
2961
|
+
/* @__PURE__ */ u3(
|
|
2962
|
+
"span",
|
|
2963
|
+
{
|
|
2964
|
+
class: "stat-label",
|
|
2965
|
+
title: "CPU time excludes I/O waits (DB, network, sleep). Low CPU% = I/O-bound.",
|
|
2966
|
+
children: "CPU"
|
|
2967
|
+
}
|
|
2968
|
+
),
|
|
2969
|
+
/* @__PURE__ */ u3("span", { class: "stat-value", children: [
|
|
2970
|
+
data.elapsed_cpu_ms.toFixed(2),
|
|
2971
|
+
" ",
|
|
2972
|
+
/* @__PURE__ */ u3("small", { children: "ms" }),
|
|
2973
|
+
data.elapsed_wall_ms != null && data.elapsed_wall_ms > 0 && /* @__PURE__ */ u3("small", { class: "profiler-fn-profiling__cpu-pct", children: [
|
|
2974
|
+
" ",
|
|
2975
|
+
"(",
|
|
2976
|
+
Math.round(data.elapsed_cpu_ms / data.elapsed_wall_ms * 100),
|
|
2977
|
+
"%)"
|
|
2978
|
+
] })
|
|
2979
|
+
] })
|
|
2980
|
+
] }),
|
|
2981
|
+
isObjectClock && /* @__PURE__ */ u3("div", { class: "stat-item", children: [
|
|
2982
|
+
/* @__PURE__ */ u3("span", { class: "stat-label", children: "Allocations" }),
|
|
2983
|
+
/* @__PURE__ */ u3("span", { class: "stat-value", children: [
|
|
2984
|
+
data.total_duration?.toLocaleString(),
|
|
2985
|
+
" ",
|
|
2986
|
+
/* @__PURE__ */ u3("small", { children: "obj" })
|
|
2987
|
+
] })
|
|
2988
|
+
] }),
|
|
2989
|
+
isSampling && (data.gc_overhead_pct ?? 0) > 0 && /* @__PURE__ */ u3("div", { class: "stat-item", children: [
|
|
2990
|
+
/* @__PURE__ */ u3("span", { class: "stat-label", title: `${data.gc_samples} samples during GC`, children: "GC" }),
|
|
2991
|
+
/* @__PURE__ */ u3("span", { class: `stat-value${(data.gc_overhead_pct ?? 0) >= 20 ? " profiler-fn-profiling__gc--high" : (data.gc_overhead_pct ?? 0) >= 5 ? " profiler-fn-profiling__gc--medium" : ""}`, children: [
|
|
2992
|
+
data.gc_overhead_pct,
|
|
2993
|
+
"%"
|
|
2994
|
+
] })
|
|
2995
|
+
] }),
|
|
2996
|
+
showAllocated && /* @__PURE__ */ u3("div", { class: "stat-item", children: [
|
|
2882
2997
|
/* @__PURE__ */ u3("span", { class: "stat-label", children: "Allocated" }),
|
|
2883
2998
|
/* @__PURE__ */ u3("span", { class: "stat-value", children: [
|
|
2884
2999
|
data.total_allocated_objects?.toLocaleString(),
|
|
@@ -2886,7 +3001,7 @@
|
|
|
2886
3001
|
/* @__PURE__ */ u3("small", { children: "obj" })
|
|
2887
3002
|
] })
|
|
2888
3003
|
] }),
|
|
2889
|
-
/* @__PURE__ */ u3("div", { class: "stat-item", children: [
|
|
3004
|
+
showMemory && /* @__PURE__ */ u3("div", { class: "stat-item", children: [
|
|
2890
3005
|
/* @__PURE__ */ u3("span", { class: "stat-label", children: "Memory" }),
|
|
2891
3006
|
/* @__PURE__ */ u3("span", { class: "stat-value", children: formatBytes3(data.total_memory_bytes ?? 0) })
|
|
2892
3007
|
] })
|
|
@@ -2918,23 +3033,26 @@
|
|
|
2918
3033
|
/* @__PURE__ */ u3("thead", { children: /* @__PURE__ */ u3("tr", { children: [
|
|
2919
3034
|
/* @__PURE__ */ u3("th", { children: "Function" }),
|
|
2920
3035
|
/* @__PURE__ */ u3("th", { children: "File" }),
|
|
2921
|
-
/* @__PURE__ */ u3("th", { class: `profiler-text--right sortable${
|
|
2922
|
-
"
|
|
3036
|
+
/* @__PURE__ */ u3("th", { class: `profiler-text--right sortable${effectiveSortKey === "calls" ? " sortable--active" : ""}`, onClick: () => handleColClick("calls"), children: [
|
|
3037
|
+
isSampling ? "Samples" : "Calls",
|
|
3038
|
+
" ",
|
|
2923
3039
|
sortIcon("calls")
|
|
2924
3040
|
] }),
|
|
2925
|
-
/* @__PURE__ */ u3("th", { class: `profiler-text--right sortable${
|
|
2926
|
-
"Total
|
|
3041
|
+
/* @__PURE__ */ u3("th", { class: `profiler-text--right sortable${effectiveSortKey === "total_duration" ? " sortable--active" : ""}`, onClick: () => handleColClick("total_duration"), children: [
|
|
3042
|
+
isObjectClock ? "Total Alloc" : "Total Time",
|
|
3043
|
+
" ",
|
|
2927
3044
|
sortIcon("total_duration")
|
|
2928
3045
|
] }),
|
|
2929
|
-
/* @__PURE__ */ u3("th", { class: `profiler-text--right sortable${
|
|
2930
|
-
"Self
|
|
3046
|
+
/* @__PURE__ */ u3("th", { class: `profiler-text--right sortable${effectiveSortKey === "self_duration" ? " sortable--active" : ""}`, onClick: () => handleColClick("self_duration"), children: [
|
|
3047
|
+
isObjectClock ? "Self Alloc" : "Self Time",
|
|
3048
|
+
" ",
|
|
2931
3049
|
sortIcon("self_duration")
|
|
2932
3050
|
] }),
|
|
2933
|
-
/* @__PURE__ */ u3("th", { class: `profiler-text--right sortable${
|
|
3051
|
+
showAllocated && /* @__PURE__ */ u3("th", { class: `profiler-text--right sortable${effectiveSortKey === "allocated_objects" ? " sortable--active" : ""}`, onClick: () => handleColClick("allocated_objects"), children: [
|
|
2934
3052
|
"Objects ",
|
|
2935
3053
|
sortIcon("allocated_objects")
|
|
2936
3054
|
] }),
|
|
2937
|
-
/* @__PURE__ */ u3("th", { class: `profiler-text--right sortable${
|
|
3055
|
+
showMemory && /* @__PURE__ */ u3("th", { class: `profiler-text--right sortable${effectiveSortKey === "memory_bytes" ? " sortable--active" : ""}`, onClick: () => handleColClick("memory_bytes"), children: [
|
|
2938
3056
|
"Memory ",
|
|
2939
3057
|
sortIcon("memory_bytes")
|
|
2940
3058
|
] })
|
|
@@ -2970,20 +3088,28 @@
|
|
|
2970
3088
|
fn.line
|
|
2971
3089
|
] }),
|
|
2972
3090
|
/* @__PURE__ */ u3("td", { class: "profiler-text--right", children: fn.calls }),
|
|
2973
|
-
/* @__PURE__ */ u3("td", { class: "profiler-text--right", children: /* @__PURE__ */ u3("span", { class:
|
|
3091
|
+
/* @__PURE__ */ u3("td", { class: "profiler-text--right", children: isObjectClock ? /* @__PURE__ */ u3("span", { class: "profiler-text--muted", children: [
|
|
3092
|
+
fn.total_duration.toLocaleString(),
|
|
3093
|
+
" ",
|
|
3094
|
+
/* @__PURE__ */ u3("small", { children: "obj" })
|
|
3095
|
+
] }) : /* @__PURE__ */ u3("span", { class: fn.total_duration >= 100 ? "badge-error" : fn.total_duration >= 10 ? "badge-warning" : "badge-success", children: [
|
|
2974
3096
|
fn.total_duration.toFixed(2),
|
|
2975
3097
|
" ms"
|
|
2976
3098
|
] }) }),
|
|
2977
|
-
/* @__PURE__ */ u3("td", { class: "profiler-text--right", children: /* @__PURE__ */ u3("span", { class:
|
|
3099
|
+
/* @__PURE__ */ u3("td", { class: "profiler-text--right", children: isObjectClock ? /* @__PURE__ */ u3("span", { class: "profiler-text--muted", children: [
|
|
3100
|
+
fn.self_duration.toLocaleString(),
|
|
3101
|
+
" ",
|
|
3102
|
+
/* @__PURE__ */ u3("small", { children: "obj" })
|
|
3103
|
+
] }) : /* @__PURE__ */ u3("span", { class: fn.self_duration >= 50 ? "badge-error" : fn.self_duration >= 5 ? "badge-warning" : "badge-success", children: [
|
|
2978
3104
|
fn.self_duration.toFixed(2),
|
|
2979
3105
|
" ms"
|
|
2980
3106
|
] }) }),
|
|
2981
|
-
/* @__PURE__ */ u3("td", { class: "profiler-text--right profiler-text--muted", children: [
|
|
3107
|
+
showAllocated && /* @__PURE__ */ u3("td", { class: "profiler-text--right profiler-text--muted", children: [
|
|
2982
3108
|
fn.allocated_objects.toLocaleString(),
|
|
2983
3109
|
" ",
|
|
2984
3110
|
/* @__PURE__ */ u3("small", { children: "obj" })
|
|
2985
3111
|
] }),
|
|
2986
|
-
/* @__PURE__ */ u3("td", { class: "profiler-text--right profiler-text--muted", children: formatBytes3(fn.memory_bytes) })
|
|
3112
|
+
showMemory && /* @__PURE__ */ u3("td", { class: "profiler-text--right profiler-text--muted", children: formatBytes3(fn.memory_bytes) })
|
|
2987
3113
|
]
|
|
2988
3114
|
},
|
|
2989
3115
|
i3
|
|
@@ -7,8 +7,10 @@ module Profiler
|
|
|
7
7
|
|
|
8
8
|
def show
|
|
9
9
|
render json: {
|
|
10
|
-
enabled:
|
|
11
|
-
max_frames: Profiler.function_profiling_max_frames
|
|
10
|
+
enabled: Profiler.function_profiling_enabled,
|
|
11
|
+
max_frames: Profiler.function_profiling_max_frames,
|
|
12
|
+
mode: Profiler.function_profiling_mode,
|
|
13
|
+
clock: Profiler.function_profiling_clock
|
|
12
14
|
}
|
|
13
15
|
end
|
|
14
16
|
|
|
@@ -22,9 +24,19 @@ module Profiler
|
|
|
22
24
|
Profiler.function_profiling_max_frames = max.positive? ? max : Profiler.function_profiling_max_frames
|
|
23
25
|
end
|
|
24
26
|
|
|
27
|
+
if params.key?(:mode) && %w[full lite].include?(params[:mode])
|
|
28
|
+
Profiler.function_profiling_mode = params[:mode]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if params.key?(:clock) && %w[wall cpu object].include?(params[:clock])
|
|
32
|
+
Profiler.function_profiling_clock = params[:clock]
|
|
33
|
+
end
|
|
34
|
+
|
|
25
35
|
render json: {
|
|
26
|
-
enabled:
|
|
27
|
-
max_frames: Profiler.function_profiling_max_frames
|
|
36
|
+
enabled: Profiler.function_profiling_enabled,
|
|
37
|
+
max_frames: Profiler.function_profiling_max_frames,
|
|
38
|
+
mode: Profiler.function_profiling_mode,
|
|
39
|
+
clock: Profiler.function_profiling_clock
|
|
28
40
|
}
|
|
29
41
|
end
|
|
30
42
|
end
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "base_collector"
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
require "stackprof"
|
|
7
|
+
rescue LoadError
|
|
8
|
+
# stackprof not available — lite mode will fall back to TracePoint without memory tracking
|
|
9
|
+
end
|
|
5
10
|
|
|
6
11
|
module Profiler
|
|
7
12
|
module Collectors
|
|
8
13
|
class FunctionProfilerCollector < BaseCollector
|
|
9
14
|
CallFrame = Struct.new(:name, :file, :line, :started_at, :alloc_before, :memsize_before, :recursive, :children)
|
|
10
15
|
|
|
16
|
+
GC_FRAME_NAME = "(garbage collection)"
|
|
17
|
+
|
|
11
18
|
def name
|
|
12
19
|
"function_profile"
|
|
13
20
|
end
|
|
@@ -37,15 +44,227 @@ module Profiler
|
|
|
37
44
|
|
|
38
45
|
def subscribe
|
|
39
46
|
unless Profiler.function_profiling_enabled
|
|
40
|
-
store_data({
|
|
47
|
+
store_data({
|
|
48
|
+
enabled: false,
|
|
49
|
+
max_frames: Profiler.function_profiling_max_frames,
|
|
50
|
+
mode: Profiler.function_profiling_mode,
|
|
51
|
+
clock: Profiler.function_profiling_clock
|
|
52
|
+
})
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
mode = Profiler.function_profiling_mode
|
|
57
|
+
clock = Profiler.function_profiling_clock
|
|
58
|
+
Thread.current[:fn_profiler_mode] = mode
|
|
59
|
+
Thread.current[:fn_profiler_clock] = clock
|
|
60
|
+
|
|
61
|
+
if mode == "lite" && defined?(StackProf)
|
|
62
|
+
# Always record wall + cpu at request level for the comparison stat
|
|
63
|
+
Thread.current[:fn_profiler_wall_start] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
64
|
+
Thread.current[:fn_profiler_cpu_start] = Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID)
|
|
65
|
+
|
|
66
|
+
sp_mode = case clock
|
|
67
|
+
when "cpu" then :cpu
|
|
68
|
+
when "object" then :object
|
|
69
|
+
else :wall
|
|
70
|
+
end
|
|
71
|
+
StackProf.start(mode: sp_mode, interval: 1000, raw: true)
|
|
72
|
+
else
|
|
73
|
+
subscribe_tracepoint(mode)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def collect
|
|
78
|
+
mode = Thread.current[:fn_profiler_mode] || Profiler.function_profiling_mode
|
|
79
|
+
clock = Thread.current[:fn_profiler_clock] || Profiler.function_profiling_clock
|
|
80
|
+
Thread.current[:fn_profiler_mode] = nil
|
|
81
|
+
Thread.current[:fn_profiler_clock] = nil
|
|
82
|
+
|
|
83
|
+
if mode == "lite" && defined?(StackProf)
|
|
84
|
+
StackProf.stop
|
|
85
|
+
result = StackProf.results
|
|
86
|
+
wall_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - (Thread.current[:fn_profiler_wall_start] || 0)) * 1000
|
|
87
|
+
cpu_ms = (Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID) - (Thread.current[:fn_profiler_cpu_start] || 0)) * 1000
|
|
88
|
+
Thread.current[:fn_profiler_wall_start] = nil
|
|
89
|
+
Thread.current[:fn_profiler_cpu_start] = nil
|
|
90
|
+
collect_stackprof(result, mode, clock, wall_ms, cpu_ms)
|
|
91
|
+
else
|
|
92
|
+
collect_tracepoint(mode)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def toolbar_summary
|
|
97
|
+
return { text: "off", color: "gray" } unless Profiler.function_profiling_enabled
|
|
98
|
+
case Profiler.function_profiling_mode
|
|
99
|
+
when "lite" then { text: "sampling on", color: "blue" }
|
|
100
|
+
else { text: "fn profiling on", color: "purple" }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
# ── StackProf (lite mode) ──────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
def collect_stackprof(result, mode, clock, wall_ms, cpu_ms)
|
|
109
|
+
unless result
|
|
110
|
+
store_data({ enabled: true, mode: mode, clock: clock, max_frames: 0,
|
|
111
|
+
frame_cap_reached: false, total_calls: 0, total_duration: 0,
|
|
112
|
+
functions: [], root_calls: [] })
|
|
41
113
|
return
|
|
42
114
|
end
|
|
43
115
|
|
|
44
|
-
|
|
116
|
+
frames_meta = result[:frames] || {}
|
|
117
|
+
raw = result[:raw] || []
|
|
118
|
+
total_samples = [result[:samples] || 0, 1].max
|
|
119
|
+
elapsed_ms = case clock
|
|
120
|
+
when "object" then total_samples.to_f
|
|
121
|
+
when "cpu" then cpu_ms
|
|
122
|
+
else wall_ms
|
|
123
|
+
end
|
|
124
|
+
app_root = app_root_path
|
|
125
|
+
|
|
126
|
+
# Partition frames: app frames vs GC frame
|
|
127
|
+
# Exclude Ruby meta-frames (<main>, <class:Foo>, #<Class:0x...> generated templates) — noise
|
|
128
|
+
gc_frame_ids = frames_meta.each_with_object(Set.new) { |(id, f), s| s << id if f[:name] == GC_FRAME_NAME }
|
|
129
|
+
app_frame_ids = frames_meta.each_with_object(Set.new) do |(id, f), s|
|
|
130
|
+
s << id if f[:file]&.start_with?(app_root) && !f[:name].to_s.match?(/\A[<#]/)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
gc_samples, gc_overhead_pct = count_gc_samples(raw, gc_frame_ids, total_samples)
|
|
134
|
+
|
|
135
|
+
roots = build_sampling_tree(raw, frames_meta, app_frame_ids, total_samples, elapsed_ms)
|
|
136
|
+
|
|
137
|
+
functions = frames_meta.filter_map do |id, frame|
|
|
138
|
+
next unless app_frame_ids.include?(id)
|
|
139
|
+
next if frame[:name].to_s.match?(/\A[<#]/)
|
|
140
|
+
total_dur = (frame[:total_samples].to_f / total_samples) * elapsed_ms
|
|
141
|
+
self_dur = (frame[:samples].to_f / total_samples) * elapsed_ms
|
|
142
|
+
{
|
|
143
|
+
name: frame[:name] || id.to_s,
|
|
144
|
+
file: relative_path(frame[:file] || ""),
|
|
145
|
+
line: frame[:line] || 0,
|
|
146
|
+
calls: frame[:total_samples] || 0,
|
|
147
|
+
recursive_calls: 0,
|
|
148
|
+
total_duration: total_dur.round(3),
|
|
149
|
+
self_duration: self_dur.round(3),
|
|
150
|
+
allocated_objects: 0,
|
|
151
|
+
memory_bytes: 0,
|
|
152
|
+
self_memory_bytes: 0
|
|
153
|
+
}
|
|
154
|
+
end.sort_by { |f| -f[:total_duration] }
|
|
155
|
+
|
|
156
|
+
# For wall/cpu clocks, total_duration = stackprof elapsed (accurate).
|
|
157
|
+
# For object clock, total_duration = total allocations count.
|
|
158
|
+
total_duration = clock == "object" ? total_samples.to_f : elapsed_ms
|
|
159
|
+
|
|
160
|
+
store_data({
|
|
161
|
+
enabled: true,
|
|
162
|
+
mode: mode,
|
|
163
|
+
clock: clock,
|
|
164
|
+
elapsed_wall_ms: wall_ms.round(2),
|
|
165
|
+
elapsed_cpu_ms: cpu_ms.round(2),
|
|
166
|
+
gc_samples: gc_samples,
|
|
167
|
+
gc_overhead_pct: gc_overhead_pct,
|
|
168
|
+
max_frames: total_samples,
|
|
169
|
+
frame_cap_reached: false,
|
|
170
|
+
total_calls: functions.sum { |f| f[:calls] },
|
|
171
|
+
total_duration: total_duration.round(2),
|
|
172
|
+
total_allocated_objects: 0,
|
|
173
|
+
total_memory_bytes: 0,
|
|
174
|
+
functions: functions,
|
|
175
|
+
root_calls: roots.map { |n| serialize_node(n) }
|
|
176
|
+
})
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Count samples that contain at least one GC frame.
|
|
180
|
+
def count_gc_samples(raw, gc_frame_ids, total_samples)
|
|
181
|
+
return [0, 0.0] if gc_frame_ids.empty?
|
|
182
|
+
|
|
183
|
+
gc_count = 0
|
|
184
|
+
i = 0
|
|
185
|
+
while i < raw.length
|
|
186
|
+
depth = raw[i]
|
|
187
|
+
break if i + depth + 1 >= raw.length
|
|
188
|
+
stack = raw[i + 1, depth]
|
|
189
|
+
count = raw[i + depth + 1]
|
|
190
|
+
i += depth + 2
|
|
191
|
+
gc_count += count if stack.any? { |id| gc_frame_ids.include?(id) }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
pct = (gc_count.to_f / total_samples * 100).round(1)
|
|
195
|
+
[gc_count, pct]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Parse stackprof raw samples and build a call tree filtered to app/ frames.
|
|
199
|
+
#
|
|
200
|
+
# Raw format: [depth, frame[0]=outermost(caller), ..., frame[depth-1]=innermost(callee), count, ...]
|
|
201
|
+
# frames are already outermost-first, so no reversal needed.
|
|
202
|
+
def build_sampling_tree(raw, frames_meta, app_frame_ids, total_samples, elapsed_ms)
|
|
203
|
+
tree = {}
|
|
204
|
+
|
|
205
|
+
i = 0
|
|
206
|
+
while i < raw.length
|
|
207
|
+
depth = raw[i]
|
|
208
|
+
break if i + depth + 1 >= raw.length
|
|
209
|
+
stack = raw[i + 1, depth]
|
|
210
|
+
count = raw[i + depth + 1]
|
|
211
|
+
i += depth + 2
|
|
212
|
+
|
|
213
|
+
app_stack = stack.select { |id| app_frame_ids.include?(id) }
|
|
214
|
+
next if app_stack.empty?
|
|
215
|
+
|
|
216
|
+
current = tree
|
|
217
|
+
app_stack.each do |frame_id|
|
|
218
|
+
current[frame_id] ||= { samples: 0, children: {} }
|
|
219
|
+
current[frame_id][:samples] += count
|
|
220
|
+
current = current[frame_id][:children]
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
nodes_from_sampling_tree(tree, frames_meta, total_samples, elapsed_ms, 0.0)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def nodes_from_sampling_tree(tree_hash, frames_meta, total_samples, elapsed_ms, parent_offset)
|
|
228
|
+
nodes = []
|
|
229
|
+
cursor = parent_offset
|
|
230
|
+
|
|
231
|
+
tree_hash.each do |frame_id, node|
|
|
232
|
+
meta = frames_meta[frame_id] || {}
|
|
233
|
+
duration = (node[:samples].to_f / total_samples) * elapsed_ms
|
|
234
|
+
children = nodes_from_sampling_tree(node[:children], frames_meta, total_samples, elapsed_ms, cursor)
|
|
235
|
+
|
|
236
|
+
nodes << {
|
|
237
|
+
name: meta[:name] || frame_id.to_s,
|
|
238
|
+
started_at: cursor / 1000.0,
|
|
239
|
+
finished_at: (cursor + duration) / 1000.0,
|
|
240
|
+
duration: duration.round(3),
|
|
241
|
+
category: "method",
|
|
242
|
+
payload: {
|
|
243
|
+
file: relative_path(meta[:file] || ""),
|
|
244
|
+
line: meta[:line] || 0,
|
|
245
|
+
allocated_objects: 0,
|
|
246
|
+
memory_bytes: 0,
|
|
247
|
+
recursive: false,
|
|
248
|
+
samples: node[:samples]
|
|
249
|
+
},
|
|
250
|
+
children: children
|
|
251
|
+
}
|
|
252
|
+
cursor += duration
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
nodes
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# ── TracePoint (full mode) ────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
def subscribe_tracepoint(mode)
|
|
261
|
+
app_root = app_root_path
|
|
45
262
|
max_frames = Profiler.function_profiling_max_frames
|
|
263
|
+
|
|
46
264
|
Thread.current[:fn_profiler_stack] = []
|
|
47
265
|
Thread.current[:fn_profiler_roots] = []
|
|
48
266
|
Thread.current[:fn_profiler_count] = 0
|
|
267
|
+
Thread.current[:fn_profiler_depth] = Hash.new(0)
|
|
49
268
|
|
|
50
269
|
@trace = TracePoint.new(:call, :return) do |tp|
|
|
51
270
|
next unless tp.path&.start_with?(app_root)
|
|
@@ -58,8 +277,9 @@ module Profiler
|
|
|
58
277
|
count = (Thread.current[:fn_profiler_count] += 1)
|
|
59
278
|
next if count > max_frames
|
|
60
279
|
|
|
61
|
-
fn_name
|
|
62
|
-
|
|
280
|
+
fn_name = "#{tp.defined_class}##{tp.method_id}"
|
|
281
|
+
depth_map = Thread.current[:fn_profiler_depth]
|
|
282
|
+
is_recursive = (depth_map[fn_name] += 1) > 1
|
|
63
283
|
|
|
64
284
|
stack.push(CallFrame.new(
|
|
65
285
|
fn_name,
|
|
@@ -67,7 +287,7 @@ module Profiler
|
|
|
67
287
|
tp.lineno,
|
|
68
288
|
Process.clock_gettime(Process::CLOCK_MONOTONIC),
|
|
69
289
|
GC.stat[:total_allocated_objects],
|
|
70
|
-
|
|
290
|
+
GC.stat[:oldmalloc_increase_bytes],
|
|
71
291
|
is_recursive,
|
|
72
292
|
[]
|
|
73
293
|
))
|
|
@@ -75,9 +295,10 @@ module Profiler
|
|
|
75
295
|
frame = stack.pop
|
|
76
296
|
next unless frame
|
|
77
297
|
|
|
78
|
-
|
|
298
|
+
Thread.current[:fn_profiler_depth][frame.name] -= 1
|
|
299
|
+
finished_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
79
300
|
allocated = GC.stat[:total_allocated_objects] - frame.alloc_before
|
|
80
|
-
memory_bytes = [
|
|
301
|
+
memory_bytes = [GC.stat[:oldmalloc_increase_bytes] - frame.memsize_before, 0].max
|
|
81
302
|
node = build_node(frame, finished_at, allocated, memory_bytes)
|
|
82
303
|
|
|
83
304
|
if stack.empty?
|
|
@@ -91,22 +312,21 @@ module Profiler
|
|
|
91
312
|
@trace.enable
|
|
92
313
|
end
|
|
93
314
|
|
|
94
|
-
def
|
|
315
|
+
def collect_tracepoint(mode)
|
|
95
316
|
@trace&.disable
|
|
96
317
|
@trace = nil
|
|
97
318
|
|
|
98
|
-
stack
|
|
99
|
-
roots
|
|
100
|
-
count
|
|
319
|
+
stack = Thread.current[:fn_profiler_stack]
|
|
320
|
+
roots = Thread.current[:fn_profiler_roots] || []
|
|
321
|
+
count = Thread.current[:fn_profiler_count] || 0
|
|
101
322
|
max_frames = Profiler.function_profiling_max_frames
|
|
102
323
|
|
|
103
|
-
# Flush any frames still on the stack (e.g. if an exception was raised)
|
|
104
324
|
if stack&.any?
|
|
105
325
|
finished_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
106
326
|
until stack.empty?
|
|
107
327
|
frame = stack.pop
|
|
108
328
|
allocated = GC.stat[:total_allocated_objects] - frame.alloc_before
|
|
109
|
-
memory_bytes = [
|
|
329
|
+
memory_bytes = [GC.stat[:oldmalloc_increase_bytes] - frame.memsize_before, 0].max
|
|
110
330
|
node = build_node(frame, finished_at, allocated, memory_bytes)
|
|
111
331
|
if stack.empty?
|
|
112
332
|
roots << node
|
|
@@ -116,31 +336,38 @@ module Profiler
|
|
|
116
336
|
end
|
|
117
337
|
end
|
|
118
338
|
|
|
119
|
-
Thread.current[:fn_profiler_stack]
|
|
120
|
-
Thread.current[:fn_profiler_roots]
|
|
121
|
-
Thread.current[:fn_profiler_count]
|
|
339
|
+
Thread.current[:fn_profiler_stack] = nil
|
|
340
|
+
Thread.current[:fn_profiler_roots] = nil
|
|
341
|
+
Thread.current[:fn_profiler_count] = nil
|
|
342
|
+
Thread.current[:fn_profiler_depth] = nil
|
|
122
343
|
|
|
123
344
|
stats = {}
|
|
124
345
|
aggregate(roots, stats)
|
|
125
346
|
sorted_functions = stats.values.sort_by { |s| -s[:total_duration] }
|
|
126
347
|
|
|
127
|
-
total_duration
|
|
128
|
-
total_allocated
|
|
129
|
-
total_memory
|
|
348
|
+
total_duration = roots.sum { |n| n[:duration] }
|
|
349
|
+
total_allocated = stats.values.sum { |s| s[:allocated_objects] }
|
|
350
|
+
total_memory = stats.values.sum { |s| s[:memory_bytes] }
|
|
130
351
|
|
|
131
352
|
store_data({
|
|
132
|
-
enabled:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
353
|
+
enabled: true,
|
|
354
|
+
mode: mode,
|
|
355
|
+
clock: "wall",
|
|
356
|
+
elapsed_wall_ms: total_duration.round(2),
|
|
357
|
+
elapsed_cpu_ms: nil,
|
|
358
|
+
gc_samples: 0,
|
|
359
|
+
gc_overhead_pct: 0.0,
|
|
360
|
+
max_frames: max_frames,
|
|
361
|
+
frame_cap_reached: count >= max_frames,
|
|
362
|
+
total_calls: stats.values.sum { |s| s[:calls] },
|
|
363
|
+
total_duration: total_duration.round(2),
|
|
137
364
|
total_allocated_objects: total_allocated,
|
|
138
|
-
total_memory_bytes:
|
|
365
|
+
total_memory_bytes: total_memory,
|
|
139
366
|
functions: sorted_functions.map do |s|
|
|
140
367
|
s.merge(
|
|
141
|
-
total_duration:
|
|
142
|
-
self_duration:
|
|
143
|
-
memory_bytes:
|
|
368
|
+
total_duration: s[:total_duration].round(3),
|
|
369
|
+
self_duration: s[:self_duration].round(3),
|
|
370
|
+
memory_bytes: s[:memory_bytes],
|
|
144
371
|
self_memory_bytes: s[:self_memory_bytes]
|
|
145
372
|
)
|
|
146
373
|
end,
|
|
@@ -148,12 +375,7 @@ module Profiler
|
|
|
148
375
|
})
|
|
149
376
|
end
|
|
150
377
|
|
|
151
|
-
|
|
152
|
-
return { text: "off", color: "gray" } unless Profiler.function_profiling_enabled
|
|
153
|
-
{ text: "fn profiling on", color: "purple" }
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
private
|
|
378
|
+
# ── Shared helpers ─────────────────────────────────────────────────────────
|
|
157
379
|
|
|
158
380
|
def app_root_path
|
|
159
381
|
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
data/lib/profiler/version.rb
CHANGED
data/lib/profiler.rb
CHANGED
|
@@ -10,6 +10,8 @@ module Profiler
|
|
|
10
10
|
attr_writer :configuration
|
|
11
11
|
attr_accessor :function_profiling_enabled
|
|
12
12
|
attr_accessor :function_profiling_max_frames
|
|
13
|
+
attr_accessor :function_profiling_mode
|
|
14
|
+
attr_accessor :function_profiling_clock
|
|
13
15
|
|
|
14
16
|
def configuration
|
|
15
17
|
@configuration ||= Configuration.new
|
|
@@ -75,8 +77,10 @@ module Profiler
|
|
|
75
77
|
end
|
|
76
78
|
end
|
|
77
79
|
|
|
78
|
-
self.function_profiling_enabled =
|
|
80
|
+
self.function_profiling_enabled = true
|
|
79
81
|
self.function_profiling_max_frames = 2000
|
|
82
|
+
self.function_profiling_mode = "lite"
|
|
83
|
+
self.function_profiling_clock = "wall"
|
|
80
84
|
end
|
|
81
85
|
|
|
82
86
|
# Require core components
|
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.16.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-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|