ruby-prof 1.7.2 → 2.0.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.
Files changed (121) hide show
  1. checksums.yaml +4 -4
  2. data/{CHANGES → CHANGELOG.md} +112 -178
  3. data/README.md +5 -5
  4. data/bin/ruby-prof +1 -4
  5. data/docs/advanced-usage.md +132 -0
  6. data/docs/alternatives.md +98 -0
  7. data/docs/architecture.md +122 -0
  8. data/docs/best-practices.md +27 -0
  9. data/docs/getting-started.md +130 -0
  10. data/docs/history.md +11 -0
  11. data/docs/index.md +45 -0
  12. data/docs/profiling-rails.md +64 -0
  13. data/docs/public/examples/example.rb +33 -0
  14. data/docs/public/examples/generate_reports.rb +92 -0
  15. data/docs/public/examples/reports/call_info.txt +27 -0
  16. data/docs/public/examples/reports/call_stack.html +835 -0
  17. data/docs/public/examples/reports/callgrind.out +150 -0
  18. data/docs/public/examples/reports/flame_graph.html +408 -0
  19. data/docs/public/examples/reports/flat.txt +45 -0
  20. data/docs/public/examples/reports/graph.dot +129 -0
  21. data/docs/public/examples/reports/graph.html +1319 -0
  22. data/docs/public/examples/reports/graph.txt +100 -0
  23. data/docs/public/examples/reports/graphviz_viewer.html +1 -0
  24. data/docs/public/images/call_stack.png +0 -0
  25. data/docs/public/images/class_diagram.png +0 -0
  26. data/docs/public/images/dot_printer.png +0 -0
  27. data/docs/public/images/flame_graph.png +0 -0
  28. data/docs/public/images/flat.png +0 -0
  29. data/docs/public/images/graph.png +0 -0
  30. data/docs/public/images/graph_html.png +0 -0
  31. data/docs/public/images/ruby-prof-logo.svg +1 -0
  32. data/docs/reports.md +150 -0
  33. data/docs/stylesheets/extra.css +80 -0
  34. data/ext/ruby_prof/rp_allocation.c +0 -15
  35. data/ext/ruby_prof/rp_allocation.h +29 -33
  36. data/ext/ruby_prof/rp_call_tree.c +3 -0
  37. data/ext/ruby_prof/rp_call_tree.h +1 -4
  38. data/ext/ruby_prof/rp_call_trees.h +1 -4
  39. data/ext/ruby_prof/rp_measurement.c +0 -5
  40. data/ext/ruby_prof/rp_measurement.h +49 -53
  41. data/ext/ruby_prof/rp_method.c +3 -0
  42. data/ext/ruby_prof/rp_method.h +1 -4
  43. data/ext/ruby_prof/rp_profile.c +1 -1
  44. data/ext/ruby_prof/rp_profile.h +1 -5
  45. data/ext/ruby_prof/rp_stack.h +50 -53
  46. data/ext/ruby_prof/rp_thread.h +1 -4
  47. data/ext/ruby_prof/ruby_prof.h +1 -4
  48. data/ext/ruby_prof/vc/ruby_prof.vcxproj +7 -8
  49. data/lib/ruby-prof/assets/call_stack_printer.html.erb +746 -711
  50. data/lib/ruby-prof/assets/flame_graph_printer.html.erb +412 -0
  51. data/lib/ruby-prof/assets/graph_printer.html.erb +355 -355
  52. data/lib/ruby-prof/call_tree.rb +57 -57
  53. data/lib/ruby-prof/call_tree_visitor.rb +36 -36
  54. data/lib/ruby-prof/measurement.rb +17 -17
  55. data/lib/ruby-prof/printers/abstract_printer.rb +19 -33
  56. data/lib/ruby-prof/printers/call_info_printer.rb +53 -53
  57. data/lib/ruby-prof/printers/call_stack_printer.rb +168 -180
  58. data/lib/ruby-prof/printers/call_tree_printer.rb +132 -145
  59. data/lib/ruby-prof/printers/dot_printer.rb +177 -132
  60. data/lib/ruby-prof/printers/flame_graph_printer.rb +79 -0
  61. data/lib/ruby-prof/printers/flat_printer.rb +52 -52
  62. data/lib/ruby-prof/printers/graph_html_printer.rb +62 -63
  63. data/lib/ruby-prof/printers/graph_printer.rb +112 -113
  64. data/lib/ruby-prof/printers/multi_printer.rb +134 -127
  65. data/lib/ruby-prof/profile.rb +13 -0
  66. data/lib/ruby-prof/rack.rb +114 -105
  67. data/lib/ruby-prof/task.rb +147 -147
  68. data/lib/ruby-prof/thread.rb +20 -20
  69. data/lib/ruby-prof/version.rb +1 -1
  70. data/lib/ruby-prof.rb +50 -52
  71. data/lib/unprof.rb +10 -10
  72. data/ruby-prof.gemspec +5 -5
  73. data/test/abstract_printer_test.rb +25 -27
  74. data/test/alias_test.rb +203 -117
  75. data/test/call_tree_builder.rb +126 -126
  76. data/test/call_tree_visitor_test.rb +27 -27
  77. data/test/call_trees_test.rb +66 -66
  78. data/test/duplicate_names_test.rb +32 -32
  79. data/test/dynamic_method_test.rb +50 -50
  80. data/test/exceptions_test.rb +24 -24
  81. data/test/exclude_threads_test.rb +48 -48
  82. data/test/fiber_test.rb +72 -72
  83. data/test/inverse_call_tree_test.rb +174 -174
  84. data/test/line_number_test.rb +138 -1
  85. data/test/marshal_test.rb +144 -145
  86. data/test/measure_allocations.rb +26 -26
  87. data/test/measure_allocations_test.rb +340 -1
  88. data/test/measure_process_time_test.rb +3098 -3142
  89. data/test/measure_times.rb +56 -56
  90. data/test/measure_wall_time_test.rb +511 -372
  91. data/test/measurement_test.rb +82 -82
  92. data/test/merge_test.rb +48 -48
  93. data/test/multi_printer_test.rb +52 -66
  94. data/test/no_method_class_test.rb +15 -15
  95. data/test/pause_resume_test.rb +171 -171
  96. data/test/prime.rb +54 -54
  97. data/test/prime_script.rb +5 -5
  98. data/test/printer_call_stack_test.rb +28 -27
  99. data/test/printer_call_tree_test.rb +30 -30
  100. data/test/printer_flame_graph_test.rb +82 -0
  101. data/test/printer_flat_test.rb +99 -99
  102. data/test/printer_graph_html_test.rb +62 -59
  103. data/test/printer_graph_test.rb +42 -40
  104. data/test/printers_test.rb +28 -44
  105. data/test/printing_recursive_graph_test.rb +81 -81
  106. data/test/profile_test.rb +101 -101
  107. data/test/rack_test.rb +103 -93
  108. data/test/recursive_test.rb +139 -139
  109. data/test/scheduler.rb +4 -0
  110. data/test/singleton_test.rb +39 -38
  111. data/test/stack_printer_test.rb +61 -61
  112. data/test/start_stop_test.rb +106 -106
  113. data/test/test_helper.rb +4 -0
  114. data/test/thread_test.rb +29 -29
  115. data/test/unique_call_path_test.rb +123 -123
  116. data/test/yarv_test.rb +56 -56
  117. metadata +53 -11
  118. data/ext/ruby_prof/rp_measure_memory.c +0 -46
  119. data/lib/ruby-prof/compatibility.rb +0 -113
  120. data/test/compatibility_test.rb +0 -49
  121. data/test/measure_memory_test.rb +0 -1193
@@ -0,0 +1,412 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title><%= h title %></title>
6
+ <style>
7
+ * { box-sizing: border-box; }
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
10
+ margin: 0;
11
+ padding: 0;
12
+ background: #f5f5f5;
13
+ color: #333;
14
+ }
15
+ #header {
16
+ background: #0D2483;
17
+ color: #fff;
18
+ padding: 1rem 1.5rem;
19
+ display: flex;
20
+ align-items: center;
21
+ gap: 16px;
22
+ flex-wrap: wrap;
23
+ }
24
+ #header .header-left {
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: 2px;
28
+ }
29
+ #header h6 {
30
+ margin: 0;
31
+ font-size: 0.75rem;
32
+ text-transform: uppercase;
33
+ letter-spacing: 1.5px;
34
+ color: rgba(255, 255, 255, 0.6);
35
+ font-weight: 600;
36
+ }
37
+ #header h1 {
38
+ margin: 0;
39
+ font-size: 22px;
40
+ font-weight: 600;
41
+ white-space: nowrap;
42
+ }
43
+ .logo {
44
+ width: 140px;
45
+ height: 30px;
46
+ opacity: 0.5;
47
+ background-repeat: no-repeat;
48
+ background-image: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 190 41'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23fff%7D%3C/style%3E%3C/defs%3E%3Cpath class='cls-1' d='M63.49 16.74c0-1.37-.91-2-2.26-2h-3.38v3.93h3.58c1.24.01 2.06-.56 2.06-1.93zM110.43 14.75h-3.6v5h3.6a2.32 2.32 0 0 0 2.5-2.49 2.34 2.34 0 0 0-2.5-2.51zM128.77 14.75h-3.53v4.94h3.53a2.28 2.28 0 0 0 2.47-2.45 2.3 2.3 0 0 0-2.47-2.49zM61.81 21.83h-4v4.32h3.91c1.44 0 2.45-.62 2.45-2.14s-.99-2.18-2.36-2.18zM21 14.75h-3.57v4.94H21a2.28 2.28 0 0 0 2.47-2.45A2.3 2.3 0 0 0 21 14.75z'/%3E%3Cpath class='cls-1' d='M184 .44H5.87A4.93 4.93 0 0 0 .94 5.37V35.5a4.94 4.94 0 0 0 4.93 4.94H184a4.94 4.94 0 0 0 4.94-4.94V5.37A4.94 4.94 0 0 0 184 .44zm-34.38 10.78c4.79 0 8.46 2.89 9.18 7.54h-3.86a5.48 5.48 0 0 0-10.68 0h-3.83c.71-4.65 4.36-7.54 9.19-7.54zM24 29.44L20.46 23h-3v6.46h-3.72v-18h7.68c3.41 0 5.78 2.07 5.78 5.76a5.34 5.34 0 0 1-2.88 5.14l3.82 7.06zm24.31-7.3c0 4.8-2.92 7.56-7.63 7.56S33 26.94 33 22.14V11.48h3.69v10.61c0 2.81 1.37 4.32 4 4.32s3.94-1.51 3.94-4.32V11.48h3.69zm13.92 7.3h-8v-18h7.52c3.24 0 5.59 1.61 5.59 5A4 4 0 0 1 65.51 20 4.22 4.22 0 0 1 68 24.21c0 3.43-2.4 5.23-5.81 5.23zm19.66-6.92v6.92h-3.7v-6.92l-6.81-11h4.29L80 18.85l4.34-7.37h4.32zm16-.5h-7.62v-3.26h7.59zm13 1h-4v6.39h-3.72v-18h7.77c3.41 0 5.79 2.09 5.79 5.79s-2.41 5.85-5.82 5.85zm20.86 6.39L128.26 23h-3v6.46h-3.72v-18h7.69c3.4 0 5.78 2.07 5.78 5.76a5.34 5.34 0 0 1-2.88 5.14l3.87 7.08zm17.85.26c-4.88 0-8.55-2.95-9.21-7.68h3.81A5.49 5.49 0 0 0 155 22h3.84c-.69 4.75-4.38 7.7-9.22 7.7zm18.4-.23h-3.72v-3.7H168zm6.36-7.54h-10.05v-3.26h10.08zm1.44-7.16h-11.49v-3.26h11.52z'/%3E%3C/svg%3E");
49
+ }
50
+ #controls {
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 10px;
54
+ flex-wrap: wrap;
55
+ }
56
+ #controls label {
57
+ font-size: 13px;
58
+ color: rgba(255, 255, 255, 0.6);
59
+ }
60
+ #controls select, #controls input[type="text"] {
61
+ font-size: 13px;
62
+ padding: 4px 8px;
63
+ border: 1px solid rgba(255, 255, 255, 0.3);
64
+ border-radius: 3px;
65
+ background: rgba(255, 255, 255, 0.15);
66
+ color: #fff;
67
+ }
68
+ #controls select option {
69
+ background: #fff;
70
+ color: #333;
71
+ }
72
+ #controls button {
73
+ font-size: 13px;
74
+ padding: 4px 12px;
75
+ border: 1px solid rgba(255, 255, 255, 0.3);
76
+ border-radius: 3px;
77
+ background: rgba(255, 255, 255, 0.15);
78
+ color: #fff;
79
+ cursor: pointer;
80
+ }
81
+ #controls button:hover {
82
+ background: rgba(255, 255, 255, 0.25);
83
+ }
84
+ #chart-container {
85
+ padding: 10px 20px 0 20px;
86
+ overflow-x: auto;
87
+ }
88
+ #tooltip {
89
+ position: fixed;
90
+ display: none;
91
+ background: rgba(0,0,0,0.88);
92
+ color: #fff;
93
+ padding: 8px 12px;
94
+ border-radius: 4px;
95
+ font-size: 12px;
96
+ line-height: 1.5;
97
+ pointer-events: none;
98
+ z-index: 1000;
99
+ max-width: 500px;
100
+ white-space: nowrap;
101
+ }
102
+ #tooltip .tt-name {
103
+ font-weight: 600;
104
+ margin-bottom: 2px;
105
+ white-space: normal;
106
+ word-break: break-all;
107
+ }
108
+ #tooltip .tt-detail {
109
+ color: #ccc;
110
+ }
111
+ svg {
112
+ display: block;
113
+ }
114
+ svg text {
115
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
116
+ font-size: 11px;
117
+ fill: white;
118
+ pointer-events: none;
119
+ }
120
+ svg rect.frame {
121
+ stroke: #fff;
122
+ stroke-width: 0.5;
123
+ cursor: pointer;
124
+ }
125
+ svg rect.frame:hover {
126
+ stroke: #333;
127
+ stroke-width: 1;
128
+ }
129
+ svg rect.frame.highlight {
130
+ stroke: #0984e3;
131
+ stroke-width: 2;
132
+ }
133
+ #zoom-info {
134
+ padding: 6px 20px;
135
+ font-size: 12px;
136
+ color: #636e72;
137
+ display: none;
138
+ }
139
+ #zoom-info a {
140
+ color: #0984e3;
141
+ cursor: pointer;
142
+ text-decoration: underline;
143
+ }
144
+ </style>
145
+ </head>
146
+ <body>
147
+ <div id="header">
148
+ <div class="header-left">
149
+ <h6>Flame Graph</h6>
150
+ <h1><%= h @result.measure_mode_name %></h1>
151
+ </div>
152
+ <div id="controls">
153
+ <label for="thread-select">Thread:</label>
154
+ <select id="thread-select"></select>
155
+ <label for="search-input">Search:</label>
156
+ <input type="text" id="search-input" placeholder="Method name...">
157
+ <button id="reset-zoom-btn">Reset Zoom</button>
158
+ <button id="toggle-icicle-btn">Icicle</button>
159
+ </div>
160
+ <div style="margin-left: auto; display: flex; align-items: flex-end; flex-direction: column;">
161
+ <div style="font-size: 12px; margin-bottom: 0.5rem; color: rgba(255,255,255,0.6);"><%= Time.now.strftime(self.time_format) %></div>
162
+ <div class="logo"></div>
163
+ </div>
164
+ </div>
165
+ <div id="zoom-info">Zoomed into: <span id="zoom-target"></span> &mdash; <a id="zoom-reset-link">reset</a></div>
166
+ <div id="thread-info" style="padding: 6px 20px; font-size: 12px; color: #636e72;"></div>
167
+ <div id="chart-container">
168
+ <svg id="flame-svg"></svg>
169
+ </div>
170
+ <div id="tooltip"></div>
171
+
172
+ <script>
173
+ (function() {
174
+ var threads = <%= flame_data_json %>;
175
+ var currentThreadIdx = 0;
176
+ var zoomNode = null;
177
+ var searchPattern = null;
178
+ var icicleMode = false;
179
+
180
+ var FRAME_HEIGHT = 18;
181
+ var MIN_WIDTH_PX = 1;
182
+ var PADDING_BOTTOM = 4;
183
+ var CHAR_WIDTH = 6.5;
184
+ var TEXT_PADDING = 4;
185
+
186
+ // --- Color ---
187
+ function hashCode(s) {
188
+ var hash = 0;
189
+ for (var i = 0; i < s.length; i++) {
190
+ hash = ((hash << 5) - hash) + s.charCodeAt(i);
191
+ hash = hash & hash;
192
+ }
193
+ return Math.abs(hash);
194
+ }
195
+
196
+ function frameColor(name) {
197
+ var h = hashCode(name);
198
+ var hue = 190 + (h % 50);
199
+ var sat = 50 + (h % 30);
200
+ var lit = 55 + (h % 20);
201
+ return 'hsl(' + hue + ',' + sat + '%,' + lit + '%)';
202
+ }
203
+
204
+ // --- Layout ---
205
+ function maxDepth(node) {
206
+ var max = 0;
207
+ if (node.children) {
208
+ for (var i = 0; i < node.children.length; i++) {
209
+ var d = maxDepth(node.children[i]);
210
+ if (d > max) max = d;
211
+ }
212
+ }
213
+ return max + 1;
214
+ }
215
+
216
+ function render() {
217
+ var thread = threads[currentThreadIdx];
218
+ var root = zoomNode || thread.data;
219
+ var totalTime = root.value;
220
+ var depth = maxDepth(root);
221
+
222
+ // Update thread info text above the SVG
223
+ var threadInfo = 'Thread ' + thread.id + ' | Total: ' + root.value.toFixed(4) + 's';
224
+ if (zoomNode) threadInfo += ' (zoomed)';
225
+ document.getElementById('thread-info').textContent = threadInfo;
226
+
227
+ var container = document.getElementById('chart-container');
228
+ var svgWidth = Math.max(container.clientWidth - 40, 800);
229
+ var svgHeight = depth * FRAME_HEIGHT + PADDING_BOTTOM;
230
+
231
+ var svg = document.getElementById('flame-svg');
232
+ svg.setAttribute('width', svgWidth);
233
+ svg.setAttribute('height', svgHeight);
234
+ svg.innerHTML = '';
235
+
236
+ renderNode(svg, root, 0, svgWidth, 0, totalTime, svgHeight);
237
+ updateZoomInfo();
238
+ }
239
+
240
+ function renderNode(svg, node, x, width, depth, totalTime, svgHeight) {
241
+ if (width < MIN_WIDTH_PX) return;
242
+
243
+ var y = icicleMode
244
+ ? depth * FRAME_HEIGHT
245
+ : svgHeight - PADDING_BOTTOM - (depth + 1) * FRAME_HEIGHT;
246
+ var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
247
+ rect.setAttribute('class', 'frame');
248
+ rect.setAttribute('x', x);
249
+ rect.setAttribute('y', y);
250
+ rect.setAttribute('width', width);
251
+ rect.setAttribute('height', FRAME_HEIGHT - 1);
252
+ rect.setAttribute('fill', frameColor(node.name));
253
+ rect.setAttribute('rx', 1);
254
+
255
+ if (searchPattern && searchPattern.test(node.name)) {
256
+ rect.classList.add('highlight');
257
+ }
258
+
259
+ rect.addEventListener('click', function(e) {
260
+ e.stopPropagation();
261
+ zoomNode = node;
262
+ render();
263
+ });
264
+
265
+ rect.addEventListener('mouseenter', function(e) {
266
+ showTooltip(e, node, totalTime);
267
+ });
268
+ rect.addEventListener('mousemove', function(e) {
269
+ positionTooltip(e);
270
+ });
271
+ rect.addEventListener('mouseleave', hideTooltip);
272
+
273
+ svg.appendChild(rect);
274
+
275
+ // Text label
276
+ var maxChars = Math.floor((width - TEXT_PADDING * 2) / CHAR_WIDTH);
277
+ if (maxChars > 2) {
278
+ var label = node.name;
279
+ if (label.length > maxChars) {
280
+ label = label.substring(0, maxChars - 2) + '..';
281
+ }
282
+ var text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
283
+ text.setAttribute('x', x + TEXT_PADDING);
284
+ text.setAttribute('y', y + FRAME_HEIGHT - 5);
285
+ text.textContent = label;
286
+ svg.appendChild(text);
287
+ }
288
+
289
+ // Children
290
+ if (node.children) {
291
+ var childX = x;
292
+ for (var i = 0; i < node.children.length; i++) {
293
+ var child = node.children[i];
294
+ var childWidth = (child.value / (node.value || 1)) * width;
295
+ renderNode(svg, child, childX, childWidth, depth + 1, totalTime, svgHeight);
296
+ childX += childWidth;
297
+ }
298
+ }
299
+ }
300
+
301
+ // --- Tooltip ---
302
+ var tooltipEl = document.getElementById('tooltip');
303
+
304
+ function showTooltip(e, node, totalTime) {
305
+ var pct = totalTime > 0 ? (node.value / totalTime * 100).toFixed(2) : '0.00';
306
+ var selfPct = totalTime > 0 ? (node.self_value / totalTime * 100).toFixed(2) : '0.00';
307
+ tooltipEl.innerHTML =
308
+ '<div class="tt-name">' + escapeHtml(node.name) + '</div>' +
309
+ '<div class="tt-detail">Total: ' + node.value.toFixed(4) + 's (' + pct + '%)</div>' +
310
+ '<div class="tt-detail">Self: ' + node.self_value.toFixed(4) + 's (' + selfPct + '%)</div>' +
311
+ '<div class="tt-detail">Calls: ' + node.called + '</div>';
312
+ tooltipEl.style.display = 'block';
313
+ positionTooltip(e);
314
+ }
315
+
316
+ function positionTooltip(e) {
317
+ var x = e.clientX + 12;
318
+ var y = e.clientY + 12;
319
+ if (x + tooltipEl.offsetWidth > window.innerWidth - 10) {
320
+ x = e.clientX - tooltipEl.offsetWidth - 12;
321
+ }
322
+ if (y + tooltipEl.offsetHeight > window.innerHeight - 10) {
323
+ y = e.clientY - tooltipEl.offsetHeight - 12;
324
+ }
325
+ tooltipEl.style.left = x + 'px';
326
+ tooltipEl.style.top = y + 'px';
327
+ }
328
+
329
+ function hideTooltip() {
330
+ tooltipEl.style.display = 'none';
331
+ }
332
+
333
+ function escapeHtml(s) {
334
+ var div = document.createElement('div');
335
+ div.appendChild(document.createTextNode(s));
336
+ return div.innerHTML;
337
+ }
338
+
339
+ // --- Zoom ---
340
+ function updateZoomInfo() {
341
+ var info = document.getElementById('zoom-info');
342
+ if (zoomNode && zoomNode !== threads[currentThreadIdx].data) {
343
+ info.style.display = 'block';
344
+ document.getElementById('zoom-target').textContent = zoomNode.name;
345
+ } else {
346
+ info.style.display = 'none';
347
+ }
348
+ }
349
+
350
+ function resetZoom() {
351
+ zoomNode = null;
352
+ render();
353
+ }
354
+
355
+ document.getElementById('reset-zoom-btn').addEventListener('click', resetZoom);
356
+ document.getElementById('zoom-reset-link').addEventListener('click', resetZoom);
357
+
358
+ // --- Icicle toggle ---
359
+ var icicleBtn = document.getElementById('toggle-icicle-btn');
360
+ icicleBtn.addEventListener('click', function() {
361
+ icicleMode = !icicleMode;
362
+ icicleBtn.textContent = icicleMode ? 'Flame' : 'Icicle';
363
+ render();
364
+ });
365
+
366
+ // --- Thread selector ---
367
+ var threadSelect = document.getElementById('thread-select');
368
+ for (var i = 0; i < threads.length; i++) {
369
+ var opt = document.createElement('option');
370
+ opt.value = i;
371
+ opt.textContent = 'Thread ' + threads[i].id + ' (Fiber ' + threads[i].fiber_id + ')';
372
+ threadSelect.appendChild(opt);
373
+ }
374
+ threadSelect.addEventListener('change', function() {
375
+ currentThreadIdx = parseInt(this.value, 10);
376
+ zoomNode = null;
377
+ render();
378
+ });
379
+
380
+ // --- Search ---
381
+ var searchInput = document.getElementById('search-input');
382
+ var searchTimeout = null;
383
+ searchInput.addEventListener('input', function() {
384
+ clearTimeout(searchTimeout);
385
+ searchTimeout = setTimeout(function() {
386
+ var val = searchInput.value.trim();
387
+ if (val.length > 0) {
388
+ try {
389
+ searchPattern = new RegExp(val, 'i');
390
+ } catch(e) {
391
+ searchPattern = new RegExp(val.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
392
+ }
393
+ } else {
394
+ searchPattern = null;
395
+ }
396
+ render();
397
+ }, 200);
398
+ });
399
+
400
+ // --- Resize ---
401
+ var resizeTimeout = null;
402
+ window.addEventListener('resize', function() {
403
+ clearTimeout(resizeTimeout);
404
+ resizeTimeout = setTimeout(render, 150);
405
+ });
406
+
407
+ // --- Init ---
408
+ render();
409
+ })();
410
+ </script>
411
+ </body>
412
+ </html>