markdown-run 0.1.11 → 0.1.12
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/CHANGELOG.md +7 -0
- data/README.md +57 -0
- data/Rakefile +10 -0
- data/lib/code_block_helper.rb +3 -1
- data/lib/code_block_parser.rb +5 -0
- data/lib/code_executor.rb +97 -19
- data/lib/dalibo_helper.rb +2 -2
- data/lib/execution_decider.rb +83 -9
- data/lib/flamegraph_helper.rb +39 -0
- data/lib/language_configs.rb +10 -10
- data/lib/markdown/run/version.rb +1 -1
- data/lib/pg_flamegraph_svg.rb +221 -0
- data/lib/result_helper.rb +29 -3
- data/markdown-run-sample.md +9 -0
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2764390f72c90c38bd4862f78db857bd5585d13e59f1a2c87a3a9c67c2f66529
|
4
|
+
data.tar.gz: 95caace78ee0db7d12ac3a6a7a5a9abcddf32c1e946ff3d9cf71beed12c89a91
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 90aea8dd4f03bc75969643bedd914ed2a8a8a1b45adf777d51fdfabb60deefd5e36cd1b73ac37225848b4663e12872a69c2ba4c3624029ac4c81c333a8eb6a8c
|
7
|
+
data.tar.gz: 7dfc12cda3abca94774e013904e2d9853713dce9c8b4059d6ca6bd90319ddff4f9f0d826c230d508f2438fdce57626f7868c1b94c9756ec3a122e0645be2121c
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.1.12] - 2025-06-04
|
4
|
+
|
5
|
+
- Added `flamegraph` option for psql code blocks to generate PostgreSQL query execution plan flamegraphs as SVG images
|
6
|
+
- PostgreSQL flamegraphs provide interactive, color-coded visualization of query performance with hover tooltips
|
7
|
+
- Flamegraph SVG files follow same directory structure as mermaid diagrams (organized by markdown file basename)
|
8
|
+
- Added flamegraph option support to frontmatter defaults and language-specific configurations
|
9
|
+
|
3
10
|
## [0.1.11] - 2025-06-04
|
4
11
|
|
5
12
|
- options are customizable with the yaml frontmatter
|
data/README.md
CHANGED
@@ -59,6 +59,7 @@ example vscode keybinding
|
|
59
59
|
- `rerun=true` or `rerun=false` to control whether a code block should be re-executed if a result block already exists. `rerun=false` is the default if not specified
|
60
60
|
- `result=true` or `result=false` to control whether the result block should be displayed after execution. `result=true` is the default if not specified. When `result=false`, the code still executes but the result block is hidden
|
61
61
|
- `explain=true` or `explain=false` for psql code blocks to generate query execution plans with Dalibo visualization links. `explain=false` is the default if not specified
|
62
|
+
- `flamegraph=true` or `flamegraph=false` for psql code blocks to generate PostgreSQL query execution plan flamegraphs as SVG images. `flamegraph=false` is the default if not specified
|
62
63
|
|
63
64
|
Options can be combined. If `run=false` is specified, the code block will not execute regardless of the `rerun` setting. The `result` option only affects display of the result block, not code execution.
|
64
65
|
|
@@ -70,6 +71,7 @@ Options can also be specified using standalone syntax without explicit `=true`:
|
|
70
71
|
- `rerun` is equivalent to `rerun=true`
|
71
72
|
- `result` is equivalent to `result=true`
|
72
73
|
- `explain` is equivalent to `explain=true`
|
74
|
+
- `flamegraph` is equivalent to `flamegraph=true`
|
73
75
|
|
74
76
|
Explicit assignments (e.g., `run=false`) take precedence over standalone options.
|
75
77
|
|
@@ -99,11 +101,57 @@ SELECT * FROM users WHERE id = 1;
|
|
99
101
|
EXPLAIN (ANALYZE) SELECT * FROM large_table;
|
100
102
|
```
|
101
103
|
|
104
|
+
```psql flamegraph
|
105
|
+
SELECT u.name, COUNT(o.id) as order_count
|
106
|
+
FROM users u
|
107
|
+
LEFT JOIN orders o ON u.id = o.user_id
|
108
|
+
WHERE u.created_at > '2024-01-01'
|
109
|
+
GROUP BY u.id, u.name
|
110
|
+
ORDER BY order_count DESC
|
111
|
+
LIMIT 10;
|
112
|
+
```
|
113
|
+
|
114
|
+
```psql flamegraph=true result=false
|
115
|
+
-- This will generate a flamegraph but hide the JSON result block
|
116
|
+
SELECT * FROM complex_query_with_joins;
|
117
|
+
```
|
118
|
+
|
102
119
|
```psql result=false explain
|
103
120
|
SELECT * FROM large_table;
|
104
121
|
-- This will execute the explain query and show the Dalibo link but hide the result block
|
105
122
|
```
|
106
123
|
|
124
|
+
### PostgreSQL Flamegraphs
|
125
|
+
|
126
|
+
PostgreSQL flamegraph blocks generate interactive SVG flamegraphs from query execution plans:
|
127
|
+
|
128
|
+
```psql flamegraph
|
129
|
+
SELECT users.*, orders.total
|
130
|
+
FROM users
|
131
|
+
JOIN orders ON users.id = orders.user_id
|
132
|
+
WHERE users.created_at > '2024-01-01';
|
133
|
+
```
|
134
|
+
|
135
|
+
This generates:
|
136
|
+
|
137
|
+
- An SVG flamegraph file in a directory named after the markdown file
|
138
|
+
- Filename format: `my-document-flamegraph-20250118-143022-a1b2c3.svg`
|
139
|
+
- Embedded image tag: ``
|
140
|
+
|
141
|
+
**Flamegraph features:**
|
142
|
+
|
143
|
+
- **Interactive**: Hover over rectangles to see detailed timing information
|
144
|
+
- **Color-coded**: Different colors for operation types (red=seq scans, green=index scans, blue=joins, etc.)
|
145
|
+
- **Hierarchical**: Shows query plan structure visually
|
146
|
+
- **Performance insights**: Width represents execution time, making bottlenecks immediately visible
|
147
|
+
|
148
|
+
**What flamegraphs help identify:**
|
149
|
+
|
150
|
+
- Slow operations (widest rectangles)
|
151
|
+
- Query plan structure (nested relationships)
|
152
|
+
- Inefficient operations (color-coded by type)
|
153
|
+
- Execution time distribution across plan nodes
|
154
|
+
|
107
155
|
### Mermaid diagrams
|
108
156
|
|
109
157
|
Mermaid blocks generate SVG files and insert markdown image tags:
|
@@ -163,6 +211,7 @@ markdown-run:
|
|
163
211
|
- `rerun`: Control whether to re-execute if result exists (default: `false`)
|
164
212
|
- `result`: Control whether to show result blocks (default: `true`)
|
165
213
|
- `explain`: For psql blocks, generate explain plans (default: `false`)
|
214
|
+
- `flamegraph`: For psql blocks, generate flamegraph SVGs (default: `false`)
|
166
215
|
|
167
216
|
**Examples:**
|
168
217
|
|
@@ -184,6 +233,14 @@ markdown-run:
|
|
184
233
|
explain: true
|
185
234
|
```
|
186
235
|
|
236
|
+
Enable flamegraphs by default for all psql blocks:
|
237
|
+
|
238
|
+
```yaml
|
239
|
+
markdown-run:
|
240
|
+
psql:
|
241
|
+
flamegraph: true
|
242
|
+
```
|
243
|
+
|
187
244
|
Language-specific settings override global defaults:
|
188
245
|
|
189
246
|
```yaml
|
data/Rakefile
CHANGED
data/lib/code_block_helper.rb
CHANGED
@@ -10,6 +10,7 @@ module CodeBlockHelper
|
|
10
10
|
@current_block_rerun = false
|
11
11
|
@current_block_run = true
|
12
12
|
@current_block_explain = false
|
13
|
+
@current_block_flamegraph = false
|
13
14
|
@current_block_result = true
|
14
15
|
end
|
15
16
|
|
@@ -20,6 +21,7 @@ module CodeBlockHelper
|
|
20
21
|
@current_block_rerun = @code_block_parser.parse_rerun_option(options_string, @current_block_lang)
|
21
22
|
@current_block_run = @code_block_parser.parse_run_option(options_string, @current_block_lang)
|
22
23
|
@current_block_explain = @code_block_parser.parse_explain_option(options_string, @current_block_lang)
|
24
|
+
@current_block_flamegraph = @code_block_parser.parse_flamegraph_option(options_string, @current_block_lang)
|
23
25
|
@current_block_result = @code_block_parser.parse_result_option(options_string, @current_block_lang)
|
24
26
|
@state = :inside_code_block
|
25
27
|
@current_code_content = ""
|
@@ -46,7 +48,7 @@ module CodeBlockHelper
|
|
46
48
|
end
|
47
49
|
|
48
50
|
def decide_execution(file_enum)
|
49
|
-
decider = ExecutionDecider.new(@current_block_run, @current_block_rerun, @current_block_lang, @current_block_explain, @current_block_result)
|
51
|
+
decider = ExecutionDecider.new(@current_block_run, @current_block_rerun, @current_block_lang, @current_block_explain, @current_block_flamegraph, @current_block_result)
|
50
52
|
decision = decider.decide(file_enum, method(:result_block_regex))
|
51
53
|
|
52
54
|
# Handle the consume_existing flag for rerun scenarios
|
data/lib/code_block_parser.rb
CHANGED
@@ -49,6 +49,11 @@ class CodeBlockParser
|
|
49
49
|
parse_boolean_option(options_string, "explain", default_value)
|
50
50
|
end
|
51
51
|
|
52
|
+
def parse_flamegraph_option(options_string, language = nil)
|
53
|
+
default_value = @frontmatter_parser.get_default_value("flamegraph", language, false)
|
54
|
+
parse_boolean_option(options_string, "flamegraph", default_value)
|
55
|
+
end
|
56
|
+
|
52
57
|
def parse_result_option(options_string, language = nil)
|
53
58
|
default_value = @frontmatter_parser.get_default_value("result", language, true)
|
54
59
|
parse_boolean_option(options_string, "result", default_value)
|
data/lib/code_executor.rb
CHANGED
@@ -3,11 +3,11 @@ require "open3"
|
|
3
3
|
require_relative "language_configs"
|
4
4
|
|
5
5
|
class CodeExecutor
|
6
|
-
def self.execute(code_content, lang, temp_dir, input_file_path = nil, explain = false)
|
7
|
-
new.execute(code_content, lang, temp_dir, input_file_path, explain)
|
6
|
+
def self.execute(code_content, lang, temp_dir, input_file_path = nil, explain = false, flamegraph = false)
|
7
|
+
new.execute(code_content, lang, temp_dir, input_file_path, explain, flamegraph)
|
8
8
|
end
|
9
9
|
|
10
|
-
def execute(code_content, lang, temp_dir, input_file_path = nil, explain = false)
|
10
|
+
def execute(code_content, lang, temp_dir, input_file_path = nil, explain = false, flamegraph = false)
|
11
11
|
lang_key = lang.downcase
|
12
12
|
lang_config = SUPPORTED_LANGUAGES[lang_key]
|
13
13
|
|
@@ -15,14 +15,12 @@ class CodeExecutor
|
|
15
15
|
|
16
16
|
warn "Executing #{lang_key} code block..."
|
17
17
|
|
18
|
-
result = execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path, explain)
|
19
|
-
process_execution_result(result, lang_config, lang_key, explain)
|
18
|
+
result = execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path, explain, flamegraph)
|
19
|
+
process_execution_result(result, lang_config, lang_key, explain, flamegraph)
|
20
20
|
end
|
21
21
|
|
22
22
|
private
|
23
23
|
|
24
|
-
|
25
|
-
|
26
24
|
def stderr_has_content?(stderr_output)
|
27
25
|
stderr_output && !stderr_output.strip.empty?
|
28
26
|
end
|
@@ -32,23 +30,23 @@ class CodeExecutor
|
|
32
30
|
"ERROR: Unsupported language: #{lang}"
|
33
31
|
end
|
34
32
|
|
35
|
-
def execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path = nil, explain = false)
|
33
|
+
def execute_with_config(code_content, lang_config, temp_dir, lang_key, input_file_path = nil, explain = false, flamegraph = false)
|
36
34
|
cmd_lambda = lang_config[:command]
|
37
35
|
temp_file_suffix = lang_config[:temp_file_suffix]
|
38
36
|
|
39
37
|
if temp_file_suffix
|
40
|
-
execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path, explain)
|
38
|
+
execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path, explain, flamegraph)
|
41
39
|
else
|
42
|
-
execute_direct_command(code_content, cmd_lambda, explain)
|
40
|
+
execute_direct_command(code_content, cmd_lambda, input_file_path, explain, flamegraph)
|
43
41
|
end
|
44
42
|
end
|
45
43
|
|
46
|
-
def execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path = nil, explain = false)
|
44
|
+
def execute_with_temp_file(code_content, cmd_lambda, temp_file_suffix, temp_dir, lang_key, input_file_path = nil, explain = false, flamegraph = false)
|
47
45
|
result = nil
|
48
46
|
Tempfile.create([lang_key, temp_file_suffix], temp_dir) do |temp_file|
|
49
47
|
temp_file.write(code_content)
|
50
48
|
temp_file.close
|
51
|
-
command_to_run, exec_options = cmd_lambda.call(code_content, temp_file.path, input_file_path, explain)
|
49
|
+
command_to_run, exec_options = cmd_lambda.call(code_content, temp_file.path, input_file_path, explain, flamegraph)
|
52
50
|
|
53
51
|
# Extract output_path if present (for mermaid)
|
54
52
|
output_path = exec_options.delete(:output_path) if exec_options.is_a?(Hash)
|
@@ -58,27 +56,35 @@ class CodeExecutor
|
|
58
56
|
stdout: captured_stdout,
|
59
57
|
stderr: captured_stderr,
|
60
58
|
status: captured_status_obj,
|
61
|
-
output_path: output_path # For mermaid SVG output
|
59
|
+
output_path: output_path, # For mermaid SVG output
|
60
|
+
input_file_path: input_file_path # Pass through for flamegraph generation
|
62
61
|
}
|
63
62
|
end
|
64
63
|
result
|
65
64
|
end
|
66
65
|
|
67
|
-
def execute_direct_command(code_content, cmd_lambda, explain = false)
|
68
|
-
command_to_run, exec_options = cmd_lambda.call(code_content, nil,
|
66
|
+
def execute_direct_command(code_content, cmd_lambda, input_file_path = nil, explain = false, flamegraph = false)
|
67
|
+
command_to_run, exec_options = cmd_lambda.call(code_content, nil, input_file_path, explain, flamegraph)
|
69
68
|
captured_stdout, captured_stderr, captured_status_obj = Open3.capture3(command_to_run, **exec_options)
|
70
|
-
{ stdout: captured_stdout, stderr: captured_stderr, status: captured_status_obj }
|
69
|
+
{ stdout: captured_stdout, stderr: captured_stderr, status: captured_status_obj, input_file_path: input_file_path }
|
71
70
|
end
|
72
71
|
|
73
|
-
def process_execution_result(result, lang_config, lang_key, explain = false)
|
72
|
+
def process_execution_result(result, lang_config, lang_key, explain = false, flamegraph = false)
|
74
73
|
exit_status, result_output, stderr_output = format_captured_output(result, lang_config)
|
75
74
|
|
76
75
|
if exit_status != 0
|
77
76
|
result_output = add_error_to_output(exit_status, lang_config, lang_key, result_output, stderr_output)
|
78
77
|
elsif lang_config && lang_config[:result_handling] == :mermaid_svg
|
79
78
|
result_output = handle_mermaid_svg_result(result, lang_key)
|
80
|
-
|
81
|
-
|
79
|
+
else
|
80
|
+
# Handle psql explain and flamegraph processing (both can be enabled)
|
81
|
+
if explain && lang_key == "psql"
|
82
|
+
result_output = handle_psql_explain_result(result_output)
|
83
|
+
end
|
84
|
+
|
85
|
+
if flamegraph && lang_key == "psql"
|
86
|
+
result_output = handle_psql_flamegraph_result(result_output, result[:input_file_path])
|
87
|
+
end
|
82
88
|
end
|
83
89
|
|
84
90
|
result_output
|
@@ -169,6 +175,78 @@ class CodeExecutor
|
|
169
175
|
end
|
170
176
|
end
|
171
177
|
|
178
|
+
def handle_psql_flamegraph_result(result_output, input_file_path = nil)
|
179
|
+
require_relative 'pg_flamegraph_svg'
|
180
|
+
|
181
|
+
begin
|
182
|
+
# Extract clean JSON from result_output (might contain Dalibo link prefix)
|
183
|
+
json_text = if result_output.start_with?("DALIBO_LINK:")
|
184
|
+
# Extract the JSON part after the Dalibo link line
|
185
|
+
lines = result_output.split("\n", 2)
|
186
|
+
lines[1] || ""
|
187
|
+
else
|
188
|
+
result_output.strip
|
189
|
+
end
|
190
|
+
|
191
|
+
# Parse the EXPLAIN JSON output
|
192
|
+
json_data = JSON.parse(json_text)
|
193
|
+
|
194
|
+
# Generate SVG flamegraph
|
195
|
+
flamegraph_generator = PostgreSQLFlameGraphSVG.new(JSON.generate(json_data))
|
196
|
+
svg_content = flamegraph_generator.generate_svg
|
197
|
+
|
198
|
+
# Save SVG file following same pattern as mermaid
|
199
|
+
if input_file_path
|
200
|
+
# Extract markdown file basename without extension
|
201
|
+
md_basename = File.basename(input_file_path, ".*")
|
202
|
+
|
203
|
+
# Create directory named after the markdown file
|
204
|
+
output_dir = File.join(File.dirname(input_file_path), md_basename)
|
205
|
+
Dir.mkdir(output_dir) unless Dir.exist?(output_dir)
|
206
|
+
|
207
|
+
# Generate unique filename with markdown basename prefix
|
208
|
+
timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
|
209
|
+
random_suffix = SecureRandom.hex(6)
|
210
|
+
svg_filename = "#{md_basename}-flamegraph-#{timestamp}-#{random_suffix}.svg"
|
211
|
+
output_path = File.join(output_dir, svg_filename)
|
212
|
+
else
|
213
|
+
# Fallback to simple naming
|
214
|
+
timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
|
215
|
+
output_path = "pg-flamegraph-#{timestamp}.svg"
|
216
|
+
end
|
217
|
+
|
218
|
+
# Write SVG file
|
219
|
+
File.write(output_path, svg_content)
|
220
|
+
|
221
|
+
# Generate relative path for markdown
|
222
|
+
if input_file_path
|
223
|
+
relative_path = "#{File.basename(output_dir)}/#{File.basename(output_path)}"
|
224
|
+
else
|
225
|
+
relative_path = File.basename(output_path)
|
226
|
+
end
|
227
|
+
|
228
|
+
warn "Generated PostgreSQL flamegraph: #{relative_path}"
|
229
|
+
|
230
|
+
# Return a special format that the markdown processor can parse
|
231
|
+
# Preserve any existing Dalibo link prefix
|
232
|
+
if result_output.start_with?("DALIBO_LINK:")
|
233
|
+
lines = result_output.split("\n", 2)
|
234
|
+
dalibo_part = lines[0]
|
235
|
+
json_part = lines[1] || ""
|
236
|
+
"#{dalibo_part}\nFLAMEGRAPH_LINK:#{relative_path}\n#{json_part}"
|
237
|
+
else
|
238
|
+
"FLAMEGRAPH_LINK:#{relative_path}\n#{json_text}"
|
239
|
+
end
|
240
|
+
|
241
|
+
rescue JSON::ParserError => e
|
242
|
+
warn "Error parsing EXPLAIN JSON: #{e.message}"
|
243
|
+
result_output
|
244
|
+
rescue => e
|
245
|
+
warn "Error generating flamegraph: #{e.message}"
|
246
|
+
result_output
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
172
250
|
private
|
173
251
|
|
174
252
|
def submit_plan_to_dalibo(plan_json)
|
data/lib/dalibo_helper.rb
CHANGED
@@ -9,7 +9,7 @@ module DaliboHelper
|
|
9
9
|
lines = result_output.split("\n", 2)
|
10
10
|
dalibo_url = lines[0].sub(DALIBO_LINK_PREFIX, "")
|
11
11
|
clean_result = lines[1] || ""
|
12
|
-
dalibo_link = "
|
12
|
+
dalibo_link = "[Dalibo](#{dalibo_url})"
|
13
13
|
[dalibo_link, clean_result]
|
14
14
|
else
|
15
15
|
[nil, result_output]
|
@@ -25,7 +25,7 @@ module DaliboHelper
|
|
25
25
|
|
26
26
|
if is_blank_line?(next_line)
|
27
27
|
consumed_lines << file_enum.next
|
28
|
-
elsif next_line&.start_with?("
|
28
|
+
elsif next_line&.start_with?("[Dalibo]")
|
29
29
|
consumed_lines << file_enum.next
|
30
30
|
else
|
31
31
|
# Hit something that's not a blank line or Dalibo link, stop consuming
|
data/lib/execution_decider.rb
CHANGED
@@ -3,11 +3,12 @@ require_relative "enum_helper"
|
|
3
3
|
class ExecutionDecider
|
4
4
|
include EnumHelper
|
5
5
|
|
6
|
-
def initialize(current_block_run, current_block_rerun, current_block_lang, current_block_explain = false, current_block_result = true)
|
6
|
+
def initialize(current_block_run, current_block_rerun, current_block_lang, current_block_explain = false, current_block_flamegraph = false, current_block_result = true)
|
7
7
|
@current_block_run = current_block_run
|
8
8
|
@current_block_rerun = current_block_rerun
|
9
9
|
@current_block_lang = current_block_lang
|
10
10
|
@current_block_explain = current_block_explain
|
11
|
+
@current_block_flamegraph = current_block_flamegraph
|
11
12
|
@current_block_result = current_block_result
|
12
13
|
end
|
13
14
|
|
@@ -21,8 +22,10 @@ class ExecutionDecider
|
|
21
22
|
handle_immediate_result_block(file_enum)
|
22
23
|
elsif is_blank_line?(peek1)
|
23
24
|
handle_blank_line_scenario(file_enum, expected_header_regex)
|
24
|
-
elsif @current_block_explain && is_dalibo_link?(peek1)
|
25
|
+
elsif (@current_block_explain || @current_block_flamegraph) && is_dalibo_link?(peek1)
|
25
26
|
handle_immediate_dalibo_link(file_enum)
|
27
|
+
elsif @current_block_flamegraph && is_flamegraph_link?(peek1)
|
28
|
+
handle_immediate_flamegraph_link(file_enum)
|
26
29
|
else
|
27
30
|
execute_without_existing_result
|
28
31
|
end
|
@@ -48,11 +51,11 @@ class ExecutionDecider
|
|
48
51
|
|
49
52
|
def handle_blank_line_scenario(file_enum, expected_header_regex)
|
50
53
|
consumed_blank_line = file_enum.next
|
51
|
-
|
54
|
+
|
52
55
|
# Look ahead past multiple blank lines to find actual content
|
53
56
|
peek2 = peek_next_line(file_enum)
|
54
57
|
additional_blanks = []
|
55
|
-
|
58
|
+
|
56
59
|
# Consume consecutive blank lines
|
57
60
|
while is_blank_line?(peek2)
|
58
61
|
additional_blanks << file_enum.next
|
@@ -61,8 +64,10 @@ class ExecutionDecider
|
|
61
64
|
|
62
65
|
if line_matches_pattern?(peek2, expected_header_regex)
|
63
66
|
handle_result_after_blank_lines(file_enum, consumed_blank_line, additional_blanks)
|
64
|
-
elsif @current_block_explain && is_dalibo_link?(peek2)
|
67
|
+
elsif (@current_block_explain || @current_block_flamegraph) && is_dalibo_link?(peek2)
|
65
68
|
handle_dalibo_after_blank_lines(file_enum, consumed_blank_line, additional_blanks)
|
69
|
+
elsif @current_block_flamegraph && is_flamegraph_link?(peek2)
|
70
|
+
handle_flamegraph_after_blank_lines(file_enum, consumed_blank_line, additional_blanks)
|
66
71
|
else
|
67
72
|
execute_with_blank_lines(consumed_blank_line, additional_blanks)
|
68
73
|
end
|
@@ -194,7 +199,7 @@ class ExecutionDecider
|
|
194
199
|
# Consume all consecutive Dalibo links and blank lines
|
195
200
|
loop do
|
196
201
|
next_line = peek_next_line(file_enum)
|
197
|
-
|
202
|
+
|
198
203
|
if is_blank_line?(next_line) || is_dalibo_link?(next_line)
|
199
204
|
consumed_line = file_enum.next
|
200
205
|
consumed_lines << consumed_line
|
@@ -205,7 +210,7 @@ class ExecutionDecider
|
|
205
210
|
end
|
206
211
|
|
207
212
|
def is_dalibo_link?(line)
|
208
|
-
line&.start_with?("
|
213
|
+
line&.start_with?("[Dalibo]")
|
209
214
|
end
|
210
215
|
|
211
216
|
def line_matches_pattern?(line, pattern)
|
@@ -217,9 +222,78 @@ class ExecutionDecider
|
|
217
222
|
end
|
218
223
|
|
219
224
|
def should_auto_replace_dalibo_link?
|
220
|
-
# Auto-replace Dalibo links when using explain with result=false
|
225
|
+
# Auto-replace Dalibo links when using explain or flamegraph with result=false
|
221
226
|
# This makes sense because with result=false, there's only a Dalibo link,
|
222
227
|
# so it should be updated on each run
|
223
|
-
@current_block_explain && !@current_block_result
|
228
|
+
(@current_block_explain || @current_block_flamegraph) && !@current_block_result
|
229
|
+
end
|
230
|
+
|
231
|
+
def is_flamegraph_link?(line)
|
232
|
+
line&.start_with?("![PostgreSQL Query Flamegraph]")
|
233
|
+
end
|
234
|
+
|
235
|
+
def handle_flamegraph_after_blank_lines(file_enum, consumed_blank_line, additional_blanks)
|
236
|
+
# For flamegraph result=false, always replace existing flamegraph links
|
237
|
+
# For flamegraph result=true, follow normal rerun logic
|
238
|
+
if should_auto_replace_flamegraph_link? || @current_block_rerun
|
239
|
+
execute_with_consumed_flamegraph_and_blanks(file_enum, consumed_blank_line, additional_blanks)
|
240
|
+
else
|
241
|
+
skip_with_blanks_and_flamegraph(file_enum, consumed_blank_line, additional_blanks)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def handle_immediate_flamegraph_link(file_enum)
|
246
|
+
# For flamegraph result=false, always replace existing flamegraph links
|
247
|
+
# For flamegraph result=true, follow normal rerun logic
|
248
|
+
if should_auto_replace_flamegraph_link? || @current_block_rerun
|
249
|
+
execute_with_consumed_flamegraph(file_enum)
|
250
|
+
else
|
251
|
+
skip_with_existing_flamegraph(file_enum)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def execute_with_consumed_flamegraph(file_enum)
|
256
|
+
consumed_lines = []
|
257
|
+
consume_flamegraph_links(file_enum, consumed_lines)
|
258
|
+
{ execute: true, consumed_lines: consumed_lines, consume_existing_flamegraph: true }
|
259
|
+
end
|
260
|
+
|
261
|
+
def skip_with_existing_flamegraph(file_enum)
|
262
|
+
consumed_lines = []
|
263
|
+
consume_flamegraph_links(file_enum, consumed_lines)
|
264
|
+
{ execute: false, lines_to_pass_through: consumed_lines, flamegraph_content: true }
|
265
|
+
end
|
266
|
+
|
267
|
+
def execute_with_consumed_flamegraph_and_blanks(file_enum, consumed_blank_line, additional_blanks)
|
268
|
+
consumed_lines = [consumed_blank_line] + additional_blanks
|
269
|
+
consume_flamegraph_links(file_enum, consumed_lines)
|
270
|
+
{ execute: true, consumed_lines: consumed_lines, blank_line: consumed_blank_line, consume_existing_flamegraph: true }
|
271
|
+
end
|
272
|
+
|
273
|
+
def skip_with_blanks_and_flamegraph(file_enum, consumed_blank_line, additional_blanks)
|
274
|
+
consumed_lines = [consumed_blank_line] + additional_blanks
|
275
|
+
consume_flamegraph_links(file_enum, consumed_lines)
|
276
|
+
{ execute: false, lines_to_pass_through: consumed_lines, flamegraph_content: true }
|
277
|
+
end
|
278
|
+
|
279
|
+
def consume_flamegraph_links(file_enum, consumed_lines)
|
280
|
+
# Consume all consecutive flamegraph links and blank lines
|
281
|
+
loop do
|
282
|
+
next_line = peek_next_line(file_enum)
|
283
|
+
|
284
|
+
if is_blank_line?(next_line) || is_flamegraph_link?(next_line)
|
285
|
+
consumed_line = file_enum.next
|
286
|
+
consumed_lines << consumed_line
|
287
|
+
else
|
288
|
+
break
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def should_auto_replace_flamegraph_link?
|
294
|
+
# Auto-replace flamegraph links when using flamegraph with result=false
|
295
|
+
# This makes sense because with result=false, there's only a flamegraph link,
|
296
|
+
# so it should be updated on each run
|
297
|
+
@current_block_flamegraph && !@current_block_result
|
224
298
|
end
|
225
299
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module FlamegraphHelper
|
2
|
+
private
|
3
|
+
|
4
|
+
FLAMEGRAPH_LINK_PREFIX = "FLAMEGRAPH_LINK:"
|
5
|
+
|
6
|
+
def extract_flamegraph_link(result_output)
|
7
|
+
# Check if the result contains a flamegraph link marker
|
8
|
+
if result_output.start_with?(FLAMEGRAPH_LINK_PREFIX)
|
9
|
+
lines = result_output.split("\n", 2)
|
10
|
+
flamegraph_path = lines[0].sub(FLAMEGRAPH_LINK_PREFIX, "")
|
11
|
+
clean_result = lines[1] || ""
|
12
|
+
flamegraph_link = ""
|
13
|
+
[flamegraph_link, clean_result]
|
14
|
+
else
|
15
|
+
[nil, result_output]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def consume_flamegraph_link_if_present(file_enum, consumed_lines)
|
20
|
+
# Look ahead to see if there are flamegraph links after the result block
|
21
|
+
begin
|
22
|
+
# Keep consuming blank lines and flamegraph links until we hit something else
|
23
|
+
loop do
|
24
|
+
next_line = peek_next_line(file_enum)
|
25
|
+
|
26
|
+
if is_blank_line?(next_line)
|
27
|
+
consumed_lines << file_enum.next
|
28
|
+
elsif next_line&.start_with?("![PostgreSQL Query Flamegraph]")
|
29
|
+
consumed_lines << file_enum.next
|
30
|
+
else
|
31
|
+
# Hit something that's not a blank line or flamegraph link, stop consuming
|
32
|
+
break
|
33
|
+
end
|
34
|
+
end
|
35
|
+
rescue StopIteration
|
36
|
+
# End of file reached, nothing more to consume
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/language_configs.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'securerandom'
|
2
2
|
|
3
3
|
JS_CONFIG = {
|
4
|
-
command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false) {
|
4
|
+
command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
|
5
5
|
# Check if bun is available
|
6
6
|
bun_exists = system("command -v bun > /dev/null 2>&1")
|
7
7
|
if bun_exists
|
@@ -16,20 +16,20 @@ JS_CONFIG = {
|
|
16
16
|
}.freeze
|
17
17
|
|
18
18
|
SQLITE_CONFIG = {
|
19
|
-
command: ->(code_content, temp_file_path, input_file_path = nil, explain = false) { [ "sqlite3 #{temp_file_path}", { stdin_data: code_content } ] },
|
19
|
+
command: ->(code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) { [ "sqlite3 #{temp_file_path}", { stdin_data: code_content } ] },
|
20
20
|
temp_file_suffix: ".db" # Temp file is the database
|
21
21
|
}.freeze
|
22
22
|
|
23
23
|
SUPPORTED_LANGUAGES = {
|
24
24
|
"psql" => {
|
25
|
-
command: ->(code_content, _temp_file_path, input_file_path = nil, explain = false) {
|
25
|
+
command: ->(code_content, _temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
|
26
26
|
psql_exists = system("command -v psql > /dev/null 2>&1")
|
27
27
|
unless psql_exists
|
28
28
|
abort "Error: psql command not found. Please install PostgreSQL or ensure psql is in your PATH."
|
29
29
|
end
|
30
30
|
|
31
|
-
# Modify the SQL query if explain option is enabled
|
32
|
-
if explain
|
31
|
+
# Modify the SQL query if explain or flamegraph option is enabled
|
32
|
+
if explain || flamegraph
|
33
33
|
# Wrap the query with EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
|
34
34
|
# Remove any trailing semicolons and whitespace, then add our EXPLAIN wrapper
|
35
35
|
clean_query = code_content.strip.gsub(/;\s*$/, '')
|
@@ -41,7 +41,7 @@ SUPPORTED_LANGUAGES = {
|
|
41
41
|
}
|
42
42
|
},
|
43
43
|
"ruby" => {
|
44
|
-
command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false) {
|
44
|
+
command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
|
45
45
|
xmpfilter_exists = system("command -v xmpfilter > /dev/null 2>&1")
|
46
46
|
unless xmpfilter_exists
|
47
47
|
abort "Error: xmpfilter command not found. Please install xmpfilter or ensure it is in your PATH."
|
@@ -57,7 +57,7 @@ SUPPORTED_LANGUAGES = {
|
|
57
57
|
"sqlite" => SQLITE_CONFIG,
|
58
58
|
"sqlite3" => SQLITE_CONFIG, # Alias for sqlite
|
59
59
|
"bash" => {
|
60
|
-
command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false) {
|
60
|
+
command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
|
61
61
|
bash_exists = system("command -v bash > /dev/null 2>&1")
|
62
62
|
unless bash_exists
|
63
63
|
abort "Error: bash command not found. Please ensure bash is in your PATH."
|
@@ -67,7 +67,7 @@ SUPPORTED_LANGUAGES = {
|
|
67
67
|
temp_file_suffix: ".sh"
|
68
68
|
},
|
69
69
|
"zsh" => {
|
70
|
-
command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false) {
|
70
|
+
command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
|
71
71
|
zsh_exists = system("command -v zsh > /dev/null 2>&1")
|
72
72
|
unless zsh_exists
|
73
73
|
abort "Error: zsh command not found. Please ensure zsh is in your PATH."
|
@@ -77,7 +77,7 @@ SUPPORTED_LANGUAGES = {
|
|
77
77
|
temp_file_suffix: ".zsh"
|
78
78
|
},
|
79
79
|
"sh" => {
|
80
|
-
command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false) {
|
80
|
+
command: ->(_code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
|
81
81
|
sh_exists = system("command -v sh > /dev/null 2>&1")
|
82
82
|
unless sh_exists
|
83
83
|
abort "Error: sh command not found. Please ensure sh is in your PATH."
|
@@ -87,7 +87,7 @@ SUPPORTED_LANGUAGES = {
|
|
87
87
|
temp_file_suffix: ".sh"
|
88
88
|
},
|
89
89
|
"mermaid" => {
|
90
|
-
command: ->(code_content, temp_file_path, input_file_path = nil, explain = false) {
|
90
|
+
command: ->(code_content, temp_file_path, input_file_path = nil, explain = false, flamegraph = false) {
|
91
91
|
mmdc_exists = system("command -v mmdc > /dev/null 2>&1")
|
92
92
|
unless mmdc_exists
|
93
93
|
abort "Error: mmdc command not found. Please install @mermaid-js/mermaid-cli: npm install -g @mermaid-js/mermaid-cli"
|
data/lib/markdown/run/version.rb
CHANGED
@@ -0,0 +1,221 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
class PostgreSQLFlameGraphSVG
|
5
|
+
def initialize(explain_json, width = 1200, height = 600)
|
6
|
+
@explain_data = JSON.parse(explain_json)
|
7
|
+
@width = width
|
8
|
+
@height = height
|
9
|
+
@font_size = 20
|
10
|
+
@min_width = 1
|
11
|
+
@colors = [
|
12
|
+
'#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6',
|
13
|
+
'#1abc9c', '#e67e22', '#95a5a6', '#34495e', '#e91e63'
|
14
|
+
]
|
15
|
+
end
|
16
|
+
|
17
|
+
def generate_svg
|
18
|
+
plan = @explain_data[0]['Plan']
|
19
|
+
|
20
|
+
# Calculate the layout
|
21
|
+
flamegraph_data = build_flamegraph_data(plan)
|
22
|
+
|
23
|
+
# Generate SVG
|
24
|
+
generate_svg_content(flamegraph_data)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def build_flamegraph_data(plan, depth = 0, start_time = 0)
|
30
|
+
node_name = format_node_name(plan)
|
31
|
+
actual_time = plan['Actual Total Time'] || 0
|
32
|
+
|
33
|
+
# Create the current node
|
34
|
+
current_node = {
|
35
|
+
name: node_name,
|
36
|
+
time: actual_time,
|
37
|
+
depth: depth,
|
38
|
+
start: start_time,
|
39
|
+
children: []
|
40
|
+
}
|
41
|
+
|
42
|
+
# Process children
|
43
|
+
if plan['Plans']
|
44
|
+
child_start = start_time
|
45
|
+
plan['Plans'].each do |child_plan|
|
46
|
+
child_node = build_flamegraph_data(child_plan, depth + 1, child_start)
|
47
|
+
current_node[:children] << child_node
|
48
|
+
child_start += child_node[:time]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
current_node
|
53
|
+
end
|
54
|
+
|
55
|
+
def format_node_name(node)
|
56
|
+
node_type = node['Node Type']
|
57
|
+
|
58
|
+
# Add relevant details
|
59
|
+
details = []
|
60
|
+
|
61
|
+
if node['Relation Name']
|
62
|
+
details << node['Relation Name']
|
63
|
+
end
|
64
|
+
|
65
|
+
if node['Index Name']
|
66
|
+
details << "idx:#{node['Index Name']}"
|
67
|
+
end
|
68
|
+
|
69
|
+
if node['Join Type']
|
70
|
+
details << node['Join Type']
|
71
|
+
end
|
72
|
+
|
73
|
+
# Build the final name
|
74
|
+
name = node_type
|
75
|
+
unless details.empty?
|
76
|
+
name += " (#{details.join(', ')})"
|
77
|
+
end
|
78
|
+
|
79
|
+
# Add timing info
|
80
|
+
if node['Actual Total Time']
|
81
|
+
name += " [#{node['Actual Total Time'].round(2)}ms]"
|
82
|
+
end
|
83
|
+
|
84
|
+
name
|
85
|
+
end
|
86
|
+
|
87
|
+
def generate_svg_content(flamegraph_data)
|
88
|
+
max_depth = calculate_max_depth(flamegraph_data)
|
89
|
+
total_time = flamegraph_data[:time]
|
90
|
+
|
91
|
+
svg_height = (max_depth + 1) * (@font_size + 4) + 80
|
92
|
+
|
93
|
+
svg = <<~SVG
|
94
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
95
|
+
<svg width="#{@width}" height="#{svg_height}" xmlns="http://www.w3.org/2000/svg">
|
96
|
+
<style>
|
97
|
+
.frame { stroke: white; stroke-width: 1; cursor: pointer; }
|
98
|
+
.frame:hover { stroke: black; stroke-width: 2; }
|
99
|
+
.frame-text { font-family: monospace; font-size: #{@font_size}px; fill: white; pointer-events: none; }
|
100
|
+
.title { font-family: Arial; font-size: 16px; font-weight: bold; fill: #333; }
|
101
|
+
.subtitle { font-family: Arial; font-size: 12px; fill: #666; }
|
102
|
+
</style>
|
103
|
+
|
104
|
+
<!-- Title -->
|
105
|
+
<text x="#{@width/2}" y="20" class="title" text-anchor="middle">PostgreSQL Query Execution Plan Flamegraph</text>
|
106
|
+
<text x="#{@width/2}" y="35" class="subtitle" text-anchor="middle">Total Execution Time: #{total_time.round(2)}ms</text>
|
107
|
+
|
108
|
+
<!-- Flamegraph -->
|
109
|
+
<g transform="translate(0, 45)">
|
110
|
+
SVG
|
111
|
+
|
112
|
+
# Generate rectangles recursively
|
113
|
+
svg += generate_rectangles(flamegraph_data, total_time, 0)
|
114
|
+
|
115
|
+
svg += <<~SVG
|
116
|
+
</g>
|
117
|
+
</svg>
|
118
|
+
SVG
|
119
|
+
|
120
|
+
svg
|
121
|
+
end
|
122
|
+
|
123
|
+
def generate_rectangles(node, total_time, y_offset)
|
124
|
+
return "" if node[:time] <= 0
|
125
|
+
|
126
|
+
# Calculate dimensions
|
127
|
+
width_ratio = node[:time] / total_time
|
128
|
+
rect_width = [@width * width_ratio, @min_width].max
|
129
|
+
rect_height = @font_size + 4
|
130
|
+
|
131
|
+
x_position = (node[:start] / total_time) * @width
|
132
|
+
y_position = y_offset
|
133
|
+
|
134
|
+
# Choose color based on node type
|
135
|
+
color = get_node_color(node[:name])
|
136
|
+
|
137
|
+
# Generate rectangle and text
|
138
|
+
svg = <<~SVG
|
139
|
+
<rect class="frame"
|
140
|
+
x="#{x_position}"
|
141
|
+
y="#{y_position}"
|
142
|
+
width="#{rect_width}"
|
143
|
+
height="#{rect_height}"
|
144
|
+
fill="#{color}">
|
145
|
+
<title>#{escape_xml(node[:name])}
|
146
|
+
Time: #{node[:time].round(2)}ms
|
147
|
+
Percentage: #{((node[:time] / total_time) * 100).round(1)}%</title>
|
148
|
+
</rect>
|
149
|
+
SVG
|
150
|
+
|
151
|
+
# Add text if rectangle is wide enough
|
152
|
+
if rect_width > 50
|
153
|
+
text_x = x_position + 4
|
154
|
+
text_y = y_position + @font_size + 1
|
155
|
+
|
156
|
+
# Truncate text if necessary
|
157
|
+
display_text = truncate_text(node[:name], rect_width - 8)
|
158
|
+
|
159
|
+
svg += <<~SVG
|
160
|
+
<text class="frame-text" x="#{text_x}" y="#{text_y}">#{escape_xml(display_text)}</text>
|
161
|
+
SVG
|
162
|
+
end
|
163
|
+
|
164
|
+
# Generate children
|
165
|
+
child_y = y_position + rect_height + 2
|
166
|
+
node[:children].each do |child|
|
167
|
+
svg += generate_rectangles(child, total_time, child_y)
|
168
|
+
end
|
169
|
+
|
170
|
+
svg
|
171
|
+
end
|
172
|
+
|
173
|
+
def calculate_max_depth(node, current_depth = 0)
|
174
|
+
max_child_depth = current_depth
|
175
|
+
|
176
|
+
node[:children].each do |child|
|
177
|
+
child_depth = calculate_max_depth(child, current_depth + 1)
|
178
|
+
max_child_depth = [max_child_depth, child_depth].max
|
179
|
+
end
|
180
|
+
|
181
|
+
max_child_depth
|
182
|
+
end
|
183
|
+
|
184
|
+
def get_node_color(node_name)
|
185
|
+
# Color code by operation type
|
186
|
+
case node_name
|
187
|
+
when /Seq Scan/
|
188
|
+
'#e74c3c' # Red - potentially slow
|
189
|
+
when /Index.*Scan/
|
190
|
+
'#2ecc71' # Green - good
|
191
|
+
when /Hash Join|Nested Loop|Merge Join/
|
192
|
+
'#3498db' # Blue - joins
|
193
|
+
when /Sort|Aggregate/
|
194
|
+
'#f39c12' # Orange - processing
|
195
|
+
when /Result/
|
196
|
+
'#95a5a6' # Gray - simple
|
197
|
+
else
|
198
|
+
# Cycle through colors based on hash
|
199
|
+
@colors[node_name.hash.abs % @colors.length]
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def truncate_text(text, max_width)
|
204
|
+
# Rough estimate: 1 character ≈ 7 pixels in monospace
|
205
|
+
max_chars = (max_width / 7).to_i
|
206
|
+
|
207
|
+
if text.length <= max_chars
|
208
|
+
text
|
209
|
+
else
|
210
|
+
text[0, max_chars - 3] + "..."
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def escape_xml(text)
|
215
|
+
text.gsub('&', '&')
|
216
|
+
.gsub('<', '<')
|
217
|
+
.gsub('>', '>')
|
218
|
+
.gsub('"', '"')
|
219
|
+
.gsub("'", ''')
|
220
|
+
end
|
221
|
+
end
|
data/lib/result_helper.rb
CHANGED
@@ -1,4 +1,8 @@
|
|
1
|
+
require_relative "flamegraph_helper"
|
2
|
+
|
1
3
|
module ResultHelper
|
4
|
+
include FlamegraphHelper
|
5
|
+
|
2
6
|
private
|
3
7
|
|
4
8
|
def ruby_style_result?(lang)
|
@@ -62,14 +66,17 @@ module ResultHelper
|
|
62
66
|
|
63
67
|
@output_lines << blank_line_before_new_result if blank_line_before_new_result
|
64
68
|
|
65
|
-
result_output = CodeExecutor.execute(@current_code_content, @current_block_lang, @temp_dir, @input_file_path, @current_block_explain)
|
69
|
+
result_output = CodeExecutor.execute(@current_code_content, @current_block_lang, @temp_dir, @input_file_path, @current_block_explain, @current_block_flamegraph)
|
66
70
|
|
67
71
|
# Check if result contains a Dalibo link for psql explain queries
|
68
|
-
dalibo_link,
|
72
|
+
dalibo_link, result_after_dalibo = extract_dalibo_link(result_output)
|
73
|
+
|
74
|
+
# Check if result contains a flamegraph link for psql flamegraph queries
|
75
|
+
flamegraph_link, clean_result = extract_flamegraph_link(result_after_dalibo)
|
69
76
|
|
70
77
|
# Add the result block only if result=true (default)
|
71
78
|
if @current_block_result
|
72
|
-
add_result_block(clean_result ||
|
79
|
+
add_result_block(clean_result || result_after_dalibo, blank_line_before_new_result)
|
73
80
|
end
|
74
81
|
|
75
82
|
# Always add Dalibo link if it exists, even when result=false
|
@@ -81,6 +88,16 @@ module ResultHelper
|
|
81
88
|
@output_lines << "\n#{dalibo_link}\n\n"
|
82
89
|
end
|
83
90
|
end
|
91
|
+
|
92
|
+
# Always add flamegraph link if it exists, even when result=false
|
93
|
+
if flamegraph_link
|
94
|
+
# Add appropriate spacing based on whether result block was shown
|
95
|
+
if @current_block_result || dalibo_link
|
96
|
+
@output_lines << "#{flamegraph_link}\n\n"
|
97
|
+
else
|
98
|
+
@output_lines << "\n#{flamegraph_link}\n\n"
|
99
|
+
end
|
100
|
+
end
|
84
101
|
end
|
85
102
|
|
86
103
|
def skip_and_pass_through_result(lines_to_pass_through, file_enum, decision = nil)
|
@@ -98,6 +115,14 @@ module ResultHelper
|
|
98
115
|
return
|
99
116
|
end
|
100
117
|
|
118
|
+
# Check if this is flamegraph content
|
119
|
+
if decision && decision[:flamegraph_content]
|
120
|
+
warn "Found existing flamegraph for current #{@current_block_lang} block, skipping execution."
|
121
|
+
@output_lines.concat(lines_to_pass_through)
|
122
|
+
# No additional consumption needed for flamegraph links
|
123
|
+
return
|
124
|
+
end
|
125
|
+
|
101
126
|
if mermaid_style_result?(@current_block_lang)
|
102
127
|
warn "Found existing mermaid SVG image for current #{@current_block_lang} block, skipping execution."
|
103
128
|
@output_lines.concat(lines_to_pass_through)
|
@@ -124,6 +149,7 @@ module ResultHelper
|
|
124
149
|
end
|
125
150
|
|
126
151
|
consume_dalibo_link_if_present(file_enum, consumed_lines)
|
152
|
+
consume_flamegraph_link_if_present(file_enum, consumed_lines)
|
127
153
|
end
|
128
154
|
|
129
155
|
|
data/markdown-run-sample.md
CHANGED
@@ -76,3 +76,12 @@ Page cache spills: 0
|
|
76
76
|
Schema Heap Usage: 0 bytes
|
77
77
|
Statement Heap/Lookaside Usage: 0 bytes
|
78
78
|
```
|
79
|
+
|
80
|
+
```psql rerun flamegraph
|
81
|
+
select 42 as answer;
|
82
|
+
```
|
83
|
+
|
84
|
+
```RESULT
|
85
|
+

|
86
|
+
```
|
87
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: markdown-run
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.12
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aurélien Bottazini
|
@@ -104,12 +104,14 @@ files:
|
|
104
104
|
- lib/dalibo_helper.rb
|
105
105
|
- lib/enum_helper.rb
|
106
106
|
- lib/execution_decider.rb
|
107
|
+
- lib/flamegraph_helper.rb
|
107
108
|
- lib/frontmatter_parser.rb
|
108
109
|
- lib/language_configs.rb
|
109
110
|
- lib/markdown/run/version.rb
|
110
111
|
- lib/markdown_file_writer.rb
|
111
112
|
- lib/markdown_processor.rb
|
112
113
|
- lib/markdown_run.rb
|
114
|
+
- lib/pg_flamegraph_svg.rb
|
113
115
|
- lib/result_helper.rb
|
114
116
|
- markdown-run-sample.md
|
115
117
|
homepage: https://github.com/aurelienbottazini/markdown-run
|