lumitrace 0.4.2 → 0.5.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/docs/spec.md +79 -1
- data/docs/tutorial.ja.md +4 -0
- data/docs/tutorial.md +4 -0
- data/lib/lumitrace/generate_resulted_html.rb +323 -391
- data/lib/lumitrace/generate_resulted_html_renderer.js +774 -0
- data/lib/lumitrace/record_instrument.rb +79 -22
- data/lib/lumitrace/version.rb +1 -1
- data/lib/lumitrace.rb +31 -4
- data/runv/index.html +1182 -420
- data/runv/sync_inline.rb +13 -1
- data/test/test_lumitrace.rb +137 -0
- metadata +2 -1
|
@@ -3,6 +3,20 @@ require_relative "record_instrument"
|
|
|
3
3
|
|
|
4
4
|
module Lumitrace
|
|
5
5
|
module GenerateResultedHtml
|
|
6
|
+
RENDERER_JS_PATH = File.expand_path("generate_resulted_html_renderer.js", __dir__)
|
|
7
|
+
|
|
8
|
+
def self.monotonic_now
|
|
9
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.time_step(label, logger = nil)
|
|
13
|
+
start = monotonic_now
|
|
14
|
+
result = yield
|
|
15
|
+
elapsed_ms = (monotonic_now - start) * 1000.0
|
|
16
|
+
logger&.call(format("html render: %s %.1fms", label, elapsed_ms))
|
|
17
|
+
result
|
|
18
|
+
end
|
|
19
|
+
|
|
6
20
|
def self.render(source_path, events_path, ranges: nil, collect_mode: nil, max_samples: nil)
|
|
7
21
|
unless File.exist?(events_path)
|
|
8
22
|
abort "missing #{events_path}"
|
|
@@ -12,50 +26,197 @@ module GenerateResultedHtml
|
|
|
12
26
|
end
|
|
13
27
|
|
|
14
28
|
raw_events = JSON.parse(File.read(events_path))
|
|
29
|
+
src = File.read(source_path)
|
|
15
30
|
mode_info = resolve_mode_info(raw_events, collect_mode: collect_mode, max_samples: max_samples)
|
|
16
|
-
|
|
17
|
-
events =
|
|
31
|
+
normalized_ranges = normalize_ranges(ranges)
|
|
32
|
+
events = normalize_events(raw_events).select { |e| e[:file] == source_path }
|
|
33
|
+
|
|
34
|
+
payload = build_html_payload(
|
|
35
|
+
mode_info: mode_info,
|
|
36
|
+
files: [
|
|
37
|
+
build_html_payload_file(
|
|
38
|
+
path: source_path,
|
|
39
|
+
display_path: File.basename(source_path),
|
|
40
|
+
source: src,
|
|
41
|
+
ranges: normalized_ranges,
|
|
42
|
+
trace_events: events
|
|
43
|
+
)
|
|
44
|
+
]
|
|
45
|
+
)
|
|
18
46
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
ranges = normalize_ranges(ranges)
|
|
22
|
-
expected_by_line, executed_by_line = line_stats(src, ranges, events, source_path)
|
|
47
|
+
render_payload_html(payload)
|
|
48
|
+
end
|
|
23
49
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
last_lineno = lineno
|
|
51
|
-
end
|
|
52
|
-
if first_lineno && first_lineno > 1
|
|
53
|
-
html_lines.unshift("<span class=\"line ellipsis\" data-line=\"...\"><span class=\"ln\">...</span></span>\n")
|
|
50
|
+
def self.esc(s)
|
|
51
|
+
s.to_s
|
|
52
|
+
.gsub("&", "&")
|
|
53
|
+
.gsub("<", "<")
|
|
54
|
+
.gsub(">", ">")
|
|
55
|
+
.gsub('"', """)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.build_html_payload(mode_info:, files:, command_text: nil)
|
|
59
|
+
meta = {
|
|
60
|
+
mode: mode_info[:mode],
|
|
61
|
+
mode_text: mode_info[:text],
|
|
62
|
+
max_samples: mode_info[:max_samples]
|
|
63
|
+
}
|
|
64
|
+
meta[:command] = command_text if command_text && !command_text.to_s.empty?
|
|
65
|
+
{
|
|
66
|
+
version: 1,
|
|
67
|
+
meta: meta,
|
|
68
|
+
files: files
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.build_html_payload_file(path:, display_path:, source:, ranges:, trace_events:, logger: nil)
|
|
73
|
+
sort_start = logger ? monotonic_now : nil
|
|
74
|
+
sorted_events = Array(trace_events).sort_by do |e|
|
|
75
|
+
[e[:start_line].to_i, e[:start_col].to_i, e[:end_line].to_i, e[:end_col].to_i]
|
|
54
76
|
end
|
|
55
|
-
|
|
56
|
-
|
|
77
|
+
sort_ms = sort_start ? (monotonic_now - sort_start) * 1000.0 : nil
|
|
78
|
+
|
|
79
|
+
map_start = logger ? monotonic_now : nil
|
|
80
|
+
trace_payload = sorted_events.map { |e| event_to_html_trace_payload(e) }
|
|
81
|
+
map_ms = map_start ? (monotonic_now - map_start) * 1000.0 : nil
|
|
82
|
+
if logger
|
|
83
|
+
logger.call(format("html render: payload_file %s sort=%.1fms map=%.1fms events=%d", display_path, sort_ms, map_ms, sorted_events.length))
|
|
57
84
|
end
|
|
58
85
|
|
|
86
|
+
{
|
|
87
|
+
path: path,
|
|
88
|
+
display_path: display_path,
|
|
89
|
+
source: source,
|
|
90
|
+
ranges: ranges,
|
|
91
|
+
trace: trace_payload
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.event_to_html_trace_payload(e)
|
|
96
|
+
sampled_values = e[:sampled_values]
|
|
97
|
+
if (sampled_values.nil? || sampled_values.empty?) && e[:last_value]
|
|
98
|
+
sampled_values = [e[:last_value]]
|
|
99
|
+
end
|
|
100
|
+
{
|
|
101
|
+
location: [
|
|
102
|
+
e[:start_line].to_i,
|
|
103
|
+
e[:start_col].to_i,
|
|
104
|
+
e[:end_line].to_i,
|
|
105
|
+
e[:end_col].to_i
|
|
106
|
+
],
|
|
107
|
+
kind: (e[:kind] || "expr").to_s,
|
|
108
|
+
name: e[:name],
|
|
109
|
+
sampled_values: sampled_values || [],
|
|
110
|
+
types: sorted_type_counts(e[:types]),
|
|
111
|
+
total: e[:total].to_i
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.payload_json_for_script(payload)
|
|
116
|
+
JSON.generate(payload)
|
|
117
|
+
.gsub("</", "<\\/")
|
|
118
|
+
.gsub("\u2028", "\\u2028")
|
|
119
|
+
.gsub("\u2029", "\\u2029")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def self.html_renderer_js
|
|
123
|
+
@html_renderer_js ||= File.read(RENDERER_JS_PATH)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.html_report_styles
|
|
127
|
+
<<~CSS
|
|
128
|
+
body { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; background: #f7f5f0; color: #1f1f1f; padding: 24px; }
|
|
129
|
+
.report-layout { display: grid; grid-template-columns: minmax(220px, 320px) minmax(0, 1fr); gap: 16px; align-items: start; }
|
|
130
|
+
.report-layout.single-file { grid-template-columns: minmax(0, 1fr); }
|
|
131
|
+
.report-sidebar { background: #fffdf7; border: 1px solid #e5dfd0; border-radius: 8px; padding: 12px; position: sticky; top: 16px; max-height: calc(100vh - 48px); overflow: hidden; }
|
|
132
|
+
.report-layout.single-file .report-sidebar { display: none; }
|
|
133
|
+
.tree-title { color: #444; font-size: 12px; margin-bottom: 8px; }
|
|
134
|
+
.tree-scroll { overflow: auto; max-height: calc(100vh - 96px); }
|
|
135
|
+
.tree-list { list-style: none; margin: 0; padding: 0; }
|
|
136
|
+
.tree-list[data-level]:not([data-level="0"]) { margin-left: 14px; border-left: 1px dashed #e5dfd0; padding-left: 8px; }
|
|
137
|
+
.tree-dir, .tree-file { margin: 2px 0; }
|
|
138
|
+
.tree-folder { }
|
|
139
|
+
.tree-folder-label { cursor: pointer; color: #4d473f; user-select: none; }
|
|
140
|
+
.tree-folder-label::marker { color: #999; }
|
|
141
|
+
.tree-file-btn { width: 100%; text-align: left; border: 0; background: transparent; color: #2a2a2a; padding: 4px 6px; border-radius: 6px; cursor: pointer; font: inherit; display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
|
|
142
|
+
.tree-file-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
143
|
+
.tree-file-meta { color: #6f6a62; font-size: 12px; white-space: nowrap; }
|
|
144
|
+
.tree-file-btn:hover { background: #fff2c6; }
|
|
145
|
+
.tree-file-btn.active { background: #f0ffe7; color: #1b5e3d; }
|
|
146
|
+
.tree-file-btn.active .tree-file-meta { color: #1b5e3d; }
|
|
147
|
+
.report-main { min-width: 0; }
|
|
148
|
+
.report-main-head { display: flex; gap: 12px; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
|
149
|
+
.current-file { color: #333; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
150
|
+
.report-viewer { min-width: 0; }
|
|
151
|
+
.file-section { min-width: 0; }
|
|
152
|
+
.code { background: #fffdf7; border: 1px solid #e5dfd0; border-radius: 8px; padding: 16px; line-height: 1.5; }
|
|
153
|
+
.line { display: block; box-sizing: border-box; padding: 2px 8px; }
|
|
154
|
+
.line:hover { background: #fff2c6; }
|
|
155
|
+
.line.hit { background: #f0ffe7; }
|
|
156
|
+
.line.miss { background: #ffecec; }
|
|
157
|
+
.line.line-target { box-shadow: inset 3px 0 #2f6f8e; background: #e9f4fb; }
|
|
158
|
+
.line.ellipsis { color: #999; }
|
|
159
|
+
.ln { display: inline-block; width: 3em; color: #888; user-select: none; }
|
|
160
|
+
.ln-link { color: inherit; text-decoration: none; }
|
|
161
|
+
.ln-link:hover { text-decoration: underline; color: #2f6f8e; }
|
|
162
|
+
.hint { color: #666; margin-bottom: 4px; }
|
|
163
|
+
.command { color: #555; margin-bottom: 4px; font-size: 12px; overflow-wrap: anywhere; }
|
|
164
|
+
.mode { color: #444; margin-bottom: 8px; }
|
|
165
|
+
.report-footer { margin-top: 16px; color: #666; font-size: 12px; }
|
|
166
|
+
.report-footer a { color: #2f6f8e; text-decoration: none; }
|
|
167
|
+
.report-footer a:hover { text-decoration: underline; }
|
|
168
|
+
.file { margin: 24px 0 8px; font-size: 16px; color: #333; }
|
|
169
|
+
.expr { position: relative; display: inline-block; padding-bottom: 1px; }
|
|
170
|
+
.expr.hit { }
|
|
171
|
+
.expr.depth-1 { --hl: #7fbf7f; }
|
|
172
|
+
.expr.depth-2 { --hl: #6fa8ff; }
|
|
173
|
+
.expr.depth-3 { --hl: #ffb347; }
|
|
174
|
+
.expr.depth-4 { --hl: #d78bff; }
|
|
175
|
+
.expr.depth-5 { --hl: #ff6f91; }
|
|
176
|
+
.expr.active { background: rgba(127, 191, 127, 0.15); box-shadow: inset 0 -2px var(--hl, #7fbf7f); }
|
|
177
|
+
.expr.miss { background: rgba(255, 120, 120, 0.18); box-shadow: inset 0 -2px rgba(200, 120, 120, 0.6); }
|
|
178
|
+
.marker { position: relative; display: inline-block; margin-left: 4px; cursor: help; font-size: 10px; line-height: 1; user-select: none; -webkit-user-select: none; -moz-user-select: none; }
|
|
179
|
+
.marker.miss { color: #c07070; }
|
|
180
|
+
.marker.arg { color: #2f6f8e; }
|
|
181
|
+
.marker .tooltip {
|
|
182
|
+
display: none;
|
|
183
|
+
position: absolute;
|
|
184
|
+
left: 0;
|
|
185
|
+
bottom: 100%;
|
|
186
|
+
margin-bottom: 6px;
|
|
187
|
+
background: #2b2b2b;
|
|
188
|
+
color: #fff;
|
|
189
|
+
padding: 4px 6px;
|
|
190
|
+
border-radius: 4px;
|
|
191
|
+
font-size: 12px;
|
|
192
|
+
white-space: pre;
|
|
193
|
+
min-width: 16ch;
|
|
194
|
+
max-width: 90vw;
|
|
195
|
+
overflow-x: auto;
|
|
196
|
+
overflow-y: hidden;
|
|
197
|
+
z-index: 10;
|
|
198
|
+
pointer-events: auto;
|
|
199
|
+
}
|
|
200
|
+
.marker:hover .tooltip,
|
|
201
|
+
.marker:focus-within .tooltip,
|
|
202
|
+
.marker .tooltip:hover { display: block; }
|
|
203
|
+
.noscript { color: #666; }
|
|
204
|
+
@media (max-width: 900px) {
|
|
205
|
+
body { padding: 16px; }
|
|
206
|
+
.report-layout { grid-template-columns: 1fr; }
|
|
207
|
+
.report-sidebar { position: static; max-height: none; }
|
|
208
|
+
.tree-scroll { max-height: 220px; }
|
|
209
|
+
.report-main-head { flex-direction: column; align-items: flex-start; }
|
|
210
|
+
}
|
|
211
|
+
CSS
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def self.footer_version_suffix
|
|
215
|
+
return "" unless defined?(Lumitrace::VERSION) && Lumitrace::VERSION
|
|
216
|
+
" v#{Lumitrace::VERSION}"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def self.render_payload_html(payload)
|
|
59
220
|
<<~HTML
|
|
60
221
|
<!doctype html>
|
|
61
222
|
<html>
|
|
@@ -63,71 +224,22 @@ module GenerateResultedHtml
|
|
|
63
224
|
<meta charset="utf-8">
|
|
64
225
|
<title>Recorded Result View</title>
|
|
65
226
|
<style>
|
|
66
|
-
|
|
67
|
-
.code { background: #fffdf7; border: 1px solid #e5dfd0; border-radius: 8px; padding: 16px; line-height: 1.5; }
|
|
68
|
-
.line { display: inline-block; width: 100%; box-sizing: border-box; padding: 2px 8px; }
|
|
69
|
-
.line:hover { background: #fff2c6; }
|
|
70
|
-
.line.hit { background: #f0ffe7; }
|
|
71
|
-
.line.miss { background: #ffecec; }
|
|
72
|
-
.line.ellipsis { color: #999; }
|
|
73
|
-
.ln { display: inline-block; width: 3em; color: #888; user-select: none; }
|
|
74
|
-
.hint { color: #666; margin-bottom: 4px; }
|
|
75
|
-
.mode { color: #444; margin-bottom: 8px; }
|
|
76
|
-
.expr { position: relative; display: inline-block; padding-bottom: 1px; }
|
|
77
|
-
.expr.hit { }
|
|
78
|
-
.expr.depth-1 { --hl: #7fbf7f; }
|
|
79
|
-
.expr.depth-2 { --hl: #6fa8ff; }
|
|
80
|
-
.expr.depth-3 { --hl: #ffb347; }
|
|
81
|
-
.expr.depth-4 { --hl: #d78bff; }
|
|
82
|
-
.expr.depth-5 { --hl: #ff6f91; }
|
|
83
|
-
.expr.active { background: rgba(127, 191, 127, 0.15); box-shadow: inset 0 -2px var(--hl, #7fbf7f); }
|
|
84
|
-
.expr.miss { background: rgba(255, 120, 120, 0.18); box-shadow: inset 0 -2px rgba(200, 120, 120, 0.6); }
|
|
85
|
-
.marker { position: relative; display: inline-block; margin-left: 4px; cursor: help; font-size: 10px; line-height: 1; user-select: none; -webkit-user-select: none; -moz-user-select: none; }
|
|
86
|
-
.marker.miss { color: #c07070; }
|
|
87
|
-
.marker.arg { color: #2f6f8e; }
|
|
88
|
-
.marker .tooltip {
|
|
89
|
-
display: none;
|
|
90
|
-
position: absolute;
|
|
91
|
-
left: 0;
|
|
92
|
-
top: 100%;
|
|
93
|
-
margin-top: 4px;
|
|
94
|
-
background: #2b2b2b;
|
|
95
|
-
color: #fff;
|
|
96
|
-
padding: 4px 6px;
|
|
97
|
-
border-radius: 4px;
|
|
98
|
-
font-size: 12px;
|
|
99
|
-
white-space: pre;
|
|
100
|
-
min-width: 16ch;
|
|
101
|
-
max-width: 90vw;
|
|
102
|
-
overflow-x: auto;
|
|
103
|
-
overflow-y: hidden;
|
|
104
|
-
z-index: 10;
|
|
105
|
-
pointer-events: auto;
|
|
106
|
-
}
|
|
107
|
-
.marker:hover .tooltip,
|
|
108
|
-
.marker:focus-within .tooltip,
|
|
109
|
-
.marker .tooltip:hover { display: block; }
|
|
227
|
+
#{html_report_styles}
|
|
110
228
|
</style>
|
|
111
229
|
</head>
|
|
112
230
|
<body>
|
|
113
|
-
<div
|
|
114
|
-
<div class="
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
231
|
+
<div id="lumitrace-app"></div>
|
|
232
|
+
<div class="report-footer">Generated by <a href="https://ko1.github.io/lumitrace/" target="_blank" rel="noopener noreferrer">lumitrace</a>#{footer_version_suffix}.</div>
|
|
233
|
+
<noscript><p class="noscript">Lumitrace HTML report requires JavaScript to render the source and trace view.</p></noscript>
|
|
234
|
+
<script id="lumitrace-payload" type="application/json">#{payload_json_for_script(payload)}</script>
|
|
235
|
+
<script>
|
|
236
|
+
#{html_renderer_js}
|
|
237
|
+
</script>
|
|
118
238
|
</body>
|
|
119
239
|
</html>
|
|
120
240
|
HTML
|
|
121
241
|
end
|
|
122
242
|
|
|
123
|
-
def self.esc(s)
|
|
124
|
-
s.to_s
|
|
125
|
-
.gsub("&", "&")
|
|
126
|
-
.gsub("<", "<")
|
|
127
|
-
.gsub(">", ">")
|
|
128
|
-
.gsub('"', """)
|
|
129
|
-
end
|
|
130
|
-
|
|
131
243
|
def self.detect_collect_mode(events)
|
|
132
244
|
arr = Array(events)
|
|
133
245
|
return "history" if arr.any? { |e| e.key?(:sampled_values) || e.key?("sampled_values") }
|
|
@@ -277,6 +389,7 @@ module GenerateResultedHtml
|
|
|
277
389
|
def self.comment_value_with_total_for_line(events)
|
|
278
390
|
best = best_event_for_line(events)
|
|
279
391
|
return nil unless best
|
|
392
|
+
return nil if best[:total].to_i <= 0
|
|
280
393
|
|
|
281
394
|
sampled_last = best[:sampled_values]&.last
|
|
282
395
|
v, t = last_value_to_pair(sampled_last)
|
|
@@ -380,9 +493,16 @@ module GenerateResultedHtml
|
|
|
380
493
|
|
|
381
494
|
next if t <= s
|
|
382
495
|
spans << { start_col: s, end_col: t }
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
496
|
+
event_key = e[:key] || [
|
|
497
|
+
e[:file],
|
|
498
|
+
e[:start_line].to_i,
|
|
499
|
+
e[:start_col].to_i,
|
|
500
|
+
e[:end_line].to_i,
|
|
501
|
+
e[:end_col].to_i
|
|
502
|
+
]
|
|
503
|
+
key_id = event_key.join(":")
|
|
504
|
+
buckets[event_key] = {
|
|
505
|
+
key: event_key,
|
|
386
506
|
key_id: key_id,
|
|
387
507
|
start_col: s,
|
|
388
508
|
end_col: t,
|
|
@@ -510,34 +630,6 @@ module GenerateResultedHtml
|
|
|
510
630
|
ranges.any? { |(s, e)| line >= s && line <= e }
|
|
511
631
|
end
|
|
512
632
|
|
|
513
|
-
def self.add_missing_events(events, source, filename, ranges)
|
|
514
|
-
expected = RecordInstrument.collect_locations_from_source(source, ranges || [])
|
|
515
|
-
existing = {}
|
|
516
|
-
events.each do |e|
|
|
517
|
-
key = [e[:file], e[:start_line], e[:start_col], e[:end_line], e[:end_col]]
|
|
518
|
-
existing[key] = true
|
|
519
|
-
end
|
|
520
|
-
expected.each do |loc|
|
|
521
|
-
key = [filename, loc[:start_line], loc[:start_col], loc[:end_line], loc[:end_col]]
|
|
522
|
-
next if existing[key]
|
|
523
|
-
events << {
|
|
524
|
-
key: key,
|
|
525
|
-
file: key[0],
|
|
526
|
-
start_line: key[1],
|
|
527
|
-
start_col: key[2],
|
|
528
|
-
end_line: key[3],
|
|
529
|
-
end_col: key[4],
|
|
530
|
-
kind: loc[:kind],
|
|
531
|
-
name: loc[:name],
|
|
532
|
-
sampled_values: [],
|
|
533
|
-
types: {},
|
|
534
|
-
total: 0
|
|
535
|
-
}
|
|
536
|
-
existing[key] = true
|
|
537
|
-
end
|
|
538
|
-
events
|
|
539
|
-
end
|
|
540
|
-
|
|
541
633
|
def self.line_stats(source, ranges, events, filename)
|
|
542
634
|
expected_by_line = Hash.new(0)
|
|
543
635
|
RecordInstrument.collect_locations_from_source(source, ranges || []).each do |loc|
|
|
@@ -561,293 +653,130 @@ module GenerateResultedHtml
|
|
|
561
653
|
[expected_by_line, executed_by_line]
|
|
562
654
|
end
|
|
563
655
|
|
|
564
|
-
def self.render_all(events_path, root: Dir.pwd, ranges_by_file: nil, collect_mode: nil, max_samples: nil)
|
|
656
|
+
def self.render_all(events_path, root: Dir.pwd, ranges_by_file: nil, collect_mode: nil, max_samples: nil, logger: nil, command_text: nil)
|
|
565
657
|
raw_events = JSON.parse(File.read(events_path))
|
|
566
|
-
render_all_from_events(
|
|
658
|
+
render_all_from_events(
|
|
659
|
+
raw_events,
|
|
660
|
+
root: root,
|
|
661
|
+
ranges_by_file: ranges_by_file,
|
|
662
|
+
collect_mode: collect_mode,
|
|
663
|
+
max_samples: max_samples,
|
|
664
|
+
logger: logger,
|
|
665
|
+
command_text: command_text
|
|
666
|
+
)
|
|
567
667
|
end
|
|
568
668
|
|
|
569
|
-
def self.render_all_from_events(events, root: Dir.pwd, ranges_by_file: nil, collect_mode: nil, max_samples: nil)
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
669
|
+
def self.render_all_from_events(events, root: Dir.pwd, ranges_by_file: nil, collect_mode: nil, max_samples: nil, logger: nil, command_text: nil)
|
|
670
|
+
normalized = time_step("normalize_events", logger) { normalize_events(events) }
|
|
671
|
+
render_all_from_normalized_events(
|
|
672
|
+
normalized,
|
|
673
|
+
root: root,
|
|
674
|
+
ranges_by_file: ranges_by_file,
|
|
675
|
+
collect_mode: collect_mode,
|
|
676
|
+
max_samples: max_samples,
|
|
677
|
+
logger: logger,
|
|
678
|
+
command_text: command_text
|
|
679
|
+
)
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
def self.render_all_from_normalized_events(events, root: Dir.pwd, ranges_by_file: nil, collect_mode: nil, max_samples: nil, logger: nil, command_text: nil)
|
|
683
|
+
total_start = monotonic_now
|
|
574
684
|
|
|
575
|
-
|
|
685
|
+
mode_info = time_step("resolve_mode", logger) do
|
|
686
|
+
resolve_mode_info(events, collect_mode: collect_mode, max_samples: max_samples)
|
|
687
|
+
end
|
|
688
|
+
by_file = time_step("group_by_file", logger) { events.group_by { |e| e[:file] } }
|
|
689
|
+
ranges_by_file = time_step("normalize_ranges_by_file", logger) { normalize_ranges_by_file(ranges_by_file) }
|
|
576
690
|
|
|
577
|
-
|
|
691
|
+
files = []
|
|
692
|
+
by_file.keys.sort.each do |path|
|
|
578
693
|
next unless File.exist?(path)
|
|
694
|
+
file_start = monotonic_now
|
|
695
|
+
read_start = logger ? monotonic_now : nil
|
|
579
696
|
src = File.read(path)
|
|
697
|
+
read_ms = read_start ? (monotonic_now - read_start) * 1000.0 : nil
|
|
580
698
|
if ranges_by_file
|
|
581
699
|
next unless ranges_by_file.key?(path)
|
|
582
700
|
ranges = ranges_by_file[path] || []
|
|
583
701
|
else
|
|
584
702
|
ranges = nil
|
|
585
703
|
end
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
line_class = line_class_for(expected, executed)
|
|
604
|
-
if expected > 0 && executed == 0
|
|
605
|
-
evs.each { |e| e[:suppress_miss] = true }
|
|
606
|
-
end
|
|
607
|
-
if evs.empty?
|
|
608
|
-
html_lines << "<span class=\"line#{line_class}\" data-line=\"#{lineno}\"><span class=\"ln\">#{lineno}</span> #{esc(line_text)}</span>\n"
|
|
609
|
-
else
|
|
610
|
-
rendered = render_line_with_events(line_text, evs)
|
|
611
|
-
html_lines << "<span class=\"line#{line_class}\" data-line=\"#{lineno}\"><span class=\"ln\">#{lineno}</span> #{rendered}</span>\n"
|
|
612
|
-
end
|
|
613
|
-
prev_lineno = lineno
|
|
614
|
-
last_lineno = lineno
|
|
615
|
-
end
|
|
616
|
-
if first_lineno && first_lineno > 1
|
|
617
|
-
html_lines.unshift("<span class=\"line ellipsis\" data-line=\"...\"><span class=\"ln\">...</span></span>\n")
|
|
618
|
-
end
|
|
619
|
-
if last_lineno && last_lineno < src.lines.length
|
|
620
|
-
html_lines << "<span class=\"line ellipsis\" data-line=\"...\"><span class=\"ln\">...</span></span>\n"
|
|
704
|
+
rel = path.start_with?(root) ? path.sub(root + File::SEPARATOR, "") : path
|
|
705
|
+
select_start = logger ? monotonic_now : nil
|
|
706
|
+
file_events = by_file[path] || []
|
|
707
|
+
select_ms = select_start ? (monotonic_now - select_start) * 1000.0 : nil
|
|
708
|
+
payload_file_start = logger ? monotonic_now : nil
|
|
709
|
+
files << build_html_payload_file(
|
|
710
|
+
path: path,
|
|
711
|
+
display_path: rel,
|
|
712
|
+
source: src,
|
|
713
|
+
ranges: ranges,
|
|
714
|
+
trace_events: file_events,
|
|
715
|
+
logger: logger
|
|
716
|
+
)
|
|
717
|
+
payload_file_ms = payload_file_start ? (monotonic_now - payload_file_start) * 1000.0 : nil
|
|
718
|
+
if logger
|
|
719
|
+
elapsed_ms = (monotonic_now - file_start) * 1000.0
|
|
720
|
+
logger.call(format("html render: file %s read=%.1fms select=%.1fms payload=%.1fms events=%d bytes=%d total=%.1fms", rel, read_ms, select_ms, payload_file_ms, file_events.length, src.bytesize, elapsed_ms))
|
|
621
721
|
end
|
|
722
|
+
end
|
|
622
723
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
724
|
+
payload = time_step("build_payload", logger) { build_html_payload(mode_info: mode_info, files: files, command_text: command_text) }
|
|
725
|
+
html = time_step("render_payload_html", logger) { render_payload_html(payload) }
|
|
726
|
+
if logger
|
|
727
|
+
total_ms = (monotonic_now - total_start) * 1000.0
|
|
728
|
+
logger.call(format("html render: total files=%d events=%d html_bytes=%d %.1fms", files.length, events.length, html.bytesize, total_ms))
|
|
729
|
+
end
|
|
730
|
+
html
|
|
731
|
+
end
|
|
631
732
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
.line:hover { background: #fff2c6; }
|
|
643
|
-
.line.hit { background: #f0ffe7; }
|
|
644
|
-
.line.miss { background: #ffecec; }
|
|
645
|
-
.line.ellipsis { color: #999; }
|
|
646
|
-
.ln { display: inline-block; width: 3em; color: #888; user-select: none; }
|
|
647
|
-
.hint { color: #666; margin-bottom: 4px; }
|
|
648
|
-
.mode { color: #444; margin-bottom: 8px; }
|
|
649
|
-
.file { margin: 24px 0 8px; font-size: 16px; color: #333; }
|
|
650
|
-
.expr { position: relative; display: inline-block; padding-bottom: 1px; }
|
|
651
|
-
.expr.hit { }
|
|
652
|
-
.expr.depth-1 { --hl: #7fbf7f; }
|
|
653
|
-
.expr.depth-2 { --hl: #6fa8ff; }
|
|
654
|
-
.expr.depth-3 { --hl: #ffb347; }
|
|
655
|
-
.expr.depth-4 { --hl: #d78bff; }
|
|
656
|
-
.expr.depth-5 { --hl: #ff6f91; }
|
|
657
|
-
.expr.active { background: rgba(127, 191, 127, 0.15); box-shadow: inset 0 -2px var(--hl, #7fbf7f); }
|
|
658
|
-
.expr.miss { background: rgba(255, 120, 120, 0.18); box-shadow: inset 0 -2px rgba(200, 120, 120, 0.6); }
|
|
659
|
-
.marker { position: relative; display: inline-block; margin-left: 4px; cursor: help; font-size: 10px; line-height: 1; user-select: none; -webkit-user-select: none; -moz-user-select: none; }
|
|
660
|
-
.marker.miss { color: #c07070; }
|
|
661
|
-
.marker.arg { color: #2f6f8e; }
|
|
662
|
-
.marker .tooltip {
|
|
663
|
-
display: none;
|
|
664
|
-
position: absolute;
|
|
665
|
-
left: 0;
|
|
666
|
-
top: 100%;
|
|
667
|
-
margin-top: 4px;
|
|
668
|
-
background: #2b2b2b;
|
|
669
|
-
color: #fff;
|
|
670
|
-
padding: 4px 6px;
|
|
671
|
-
border-radius: 4px;
|
|
672
|
-
font-size: 12px;
|
|
673
|
-
white-space: pre;
|
|
674
|
-
min-width: 16ch;
|
|
675
|
-
max-width: 90vw;
|
|
676
|
-
overflow-x: auto;
|
|
677
|
-
overflow-y: hidden;
|
|
678
|
-
z-index: 10;
|
|
679
|
-
pointer-events: auto;
|
|
680
|
-
}
|
|
681
|
-
.marker:hover .tooltip,
|
|
682
|
-
.marker:focus-within .tooltip,
|
|
683
|
-
.marker .tooltip:hover { display: block; }
|
|
684
|
-
</style>
|
|
685
|
-
</head>
|
|
686
|
-
<body>
|
|
687
|
-
<div class="hint">Hover highlighted text to see recorded values.</div>
|
|
688
|
-
<div class="mode">#{esc(mode_info[:text])}</div>
|
|
689
|
-
#{sections}
|
|
690
|
-
<script>
|
|
691
|
-
(function() {
|
|
692
|
-
document.querySelectorAll('.marker').forEach(marker => {
|
|
693
|
-
marker.addEventListener('mouseenter', () => {
|
|
694
|
-
document.querySelectorAll('.expr').forEach(e => e.classList.remove('active'));
|
|
695
|
-
const key = marker.dataset.key;
|
|
696
|
-
if (key) {
|
|
697
|
-
document.querySelectorAll(`.expr[data-key="${key}"]`).forEach(e => e.classList.add('active'));
|
|
698
|
-
} else {
|
|
699
|
-
marker.closest('.expr')?.classList.add('active');
|
|
700
|
-
}
|
|
701
|
-
});
|
|
702
|
-
marker.addEventListener('mouseleave', () => {
|
|
703
|
-
const key = marker.dataset.key;
|
|
704
|
-
if (key) {
|
|
705
|
-
document.querySelectorAll(`.expr[data-key="${key}"]`).forEach(e => e.classList.remove('active'));
|
|
706
|
-
} else {
|
|
707
|
-
marker.closest('.expr')?.classList.remove('active');
|
|
708
|
-
}
|
|
709
|
-
});
|
|
710
|
-
});
|
|
711
|
-
})();
|
|
712
|
-
</script>
|
|
713
|
-
</body>
|
|
714
|
-
</html>
|
|
715
|
-
HTML
|
|
733
|
+
def self.render_source_from_events(source, events, filename: "script.rb", ranges: nil, collect_mode: nil, max_samples: nil, command_text: nil)
|
|
734
|
+
render_source_from_normalized_events(
|
|
735
|
+
source,
|
|
736
|
+
normalize_events(events),
|
|
737
|
+
filename: filename,
|
|
738
|
+
ranges: ranges,
|
|
739
|
+
collect_mode: collect_mode,
|
|
740
|
+
max_samples: max_samples,
|
|
741
|
+
command_text: command_text
|
|
742
|
+
)
|
|
716
743
|
end
|
|
717
744
|
|
|
718
|
-
def self.
|
|
745
|
+
def self.render_source_from_normalized_events(source, events, filename: "script.rb", ranges: nil, collect_mode: nil, max_samples: nil, command_text: nil)
|
|
719
746
|
mode_info = resolve_mode_info(events, collect_mode: collect_mode, max_samples: max_samples)
|
|
720
|
-
events = normalize_events(events)
|
|
721
747
|
ranges = normalize_ranges(ranges)
|
|
722
|
-
target_events =
|
|
723
|
-
expected_by_line, executed_by_line = line_stats(source, ranges, target_events, filename)
|
|
748
|
+
target_events = events.select { |e| e[:file] == filename }
|
|
724
749
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
expected = expected_by_line[lineno]
|
|
739
|
-
executed = executed_by_line[lineno]
|
|
740
|
-
line_class = line_class_for(expected, executed)
|
|
741
|
-
if expected > 0 && executed == 0
|
|
742
|
-
evs.each { |e| e[:suppress_miss] = true }
|
|
743
|
-
end
|
|
744
|
-
if evs.empty?
|
|
745
|
-
html_lines << "<span class=\"line#{line_class}\" data-line=\"#{lineno}\"><span class=\"ln\">#{lineno}</span> #{esc(line_text)}</span>\n"
|
|
746
|
-
else
|
|
747
|
-
rendered = render_line_with_events(line_text, evs)
|
|
748
|
-
html_lines << "<span class=\"line#{line_class}\" data-line=\"#{lineno}\"><span class=\"ln\">#{lineno}</span> #{rendered}</span>\n"
|
|
749
|
-
end
|
|
750
|
-
prev_lineno = lineno
|
|
751
|
-
last_lineno = lineno
|
|
752
|
-
end
|
|
753
|
-
if first_lineno && first_lineno > 1
|
|
754
|
-
html_lines.unshift("<span class=\"line ellipsis\" data-line=\"...\"><span class=\"ln\">...</span></span>\n")
|
|
755
|
-
end
|
|
756
|
-
if last_lineno && last_lineno < source.lines.length
|
|
757
|
-
html_lines << "<span class=\"line ellipsis\" data-line=\"...\"><span class=\"ln\">...</span></span>\n"
|
|
758
|
-
end
|
|
750
|
+
payload = build_html_payload(
|
|
751
|
+
mode_info: mode_info,
|
|
752
|
+
command_text: command_text,
|
|
753
|
+
files: [
|
|
754
|
+
build_html_payload_file(
|
|
755
|
+
path: filename,
|
|
756
|
+
display_path: filename,
|
|
757
|
+
source: source,
|
|
758
|
+
ranges: ranges,
|
|
759
|
+
trace_events: target_events
|
|
760
|
+
)
|
|
761
|
+
]
|
|
762
|
+
)
|
|
759
763
|
|
|
760
|
-
|
|
761
|
-
<!doctype html>
|
|
762
|
-
<html>
|
|
763
|
-
<head>
|
|
764
|
-
<meta charset="utf-8">
|
|
765
|
-
<title>Recorded Result View</title>
|
|
766
|
-
<style>
|
|
767
|
-
body { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; background: #f7f5f0; color: #1f1f1f; padding: 24px; }
|
|
768
|
-
.code { background: #fffdf7; border: 1px solid #e5dfd0; border-radius: 8px; padding: 16px; line-height: 1.5; }
|
|
769
|
-
.line { display: inline-block; width: 100%; box-sizing: border-box; padding: 2px 8px; }
|
|
770
|
-
.line:hover { background: #fff2c6; }
|
|
771
|
-
.line.hit { background: #f0ffe7; }
|
|
772
|
-
.line.miss { background: #ffecec; }
|
|
773
|
-
.line.ellipsis { color: #999; }
|
|
774
|
-
.ln { display: inline-block; width: 3em; color: #888; user-select: none; }
|
|
775
|
-
.hint { color: #666; margin-bottom: 4px; }
|
|
776
|
-
.mode { color: #444; margin-bottom: 8px; }
|
|
777
|
-
.file { margin: 24px 0 8px; font-size: 16px; color: #333; }
|
|
778
|
-
.expr { position: relative; display: inline-block; padding-bottom: 1px; }
|
|
779
|
-
.expr.hit { }
|
|
780
|
-
.expr.depth-1 { --hl: #7fbf7f; }
|
|
781
|
-
.expr.depth-2 { --hl: #6fa8ff; }
|
|
782
|
-
.expr.depth-3 { --hl: #ffb347; }
|
|
783
|
-
.expr.depth-4 { --hl: #d78bff; }
|
|
784
|
-
.expr.depth-5 { --hl: #ff6f91; }
|
|
785
|
-
.expr.active { background: rgba(127, 191, 127, 0.15); box-shadow: inset 0 -2px var(--hl, #7fbf7f); }
|
|
786
|
-
.expr.miss { background: rgba(255, 120, 120, 0.18); box-shadow: inset 0 -2px rgba(200, 120, 120, 0.6); }
|
|
787
|
-
.marker { position: relative; display: inline-block; margin-left: 4px; cursor: help; font-size: 10px; line-height: 1; user-select: none; -webkit-user-select: none; -moz-user-select: none; }
|
|
788
|
-
.marker.miss { color: #c07070; }
|
|
789
|
-
.marker.arg { color: #2f6f8e; }
|
|
790
|
-
.marker .tooltip {
|
|
791
|
-
display: none;
|
|
792
|
-
position: absolute;
|
|
793
|
-
left: 0;
|
|
794
|
-
top: 100%;
|
|
795
|
-
margin-top: 4px;
|
|
796
|
-
background: #2b2b2b;
|
|
797
|
-
color: #fff;
|
|
798
|
-
padding: 4px 6px;
|
|
799
|
-
border-radius: 4px;
|
|
800
|
-
font-size: 12px;
|
|
801
|
-
white-space: pre;
|
|
802
|
-
min-width: 16ch;
|
|
803
|
-
max-width: 90vw;
|
|
804
|
-
overflow-x: auto;
|
|
805
|
-
overflow-y: hidden;
|
|
806
|
-
z-index: 10;
|
|
807
|
-
pointer-events: auto;
|
|
808
|
-
}
|
|
809
|
-
.marker:hover .tooltip,
|
|
810
|
-
.marker:focus-within .tooltip,
|
|
811
|
-
.marker .tooltip:hover { display: block; }
|
|
812
|
-
</style>
|
|
813
|
-
</head>
|
|
814
|
-
<body>
|
|
815
|
-
<div class="hint">Hover highlighted text to see recorded values.</div>
|
|
816
|
-
<div class="mode">#{esc(mode_info[:text])}</div>
|
|
817
|
-
<h2 class="file">#{esc(filename)}</h2>
|
|
818
|
-
<pre class="code"><code>
|
|
819
|
-
#{html_lines.join("")}
|
|
820
|
-
</code></pre>
|
|
821
|
-
<script>
|
|
822
|
-
(function() {
|
|
823
|
-
document.querySelectorAll('.marker').forEach(marker => {
|
|
824
|
-
marker.addEventListener('mouseenter', () => {
|
|
825
|
-
document.querySelectorAll('.expr').forEach(e => e.classList.remove('active'));
|
|
826
|
-
const key = marker.dataset.key;
|
|
827
|
-
if (key) {
|
|
828
|
-
document.querySelectorAll(`.expr[data-key="${key}"]`).forEach(e => e.classList.add('active'));
|
|
829
|
-
} else {
|
|
830
|
-
marker.closest('.expr')?.classList.add('active');
|
|
831
|
-
}
|
|
832
|
-
});
|
|
833
|
-
marker.addEventListener('mouseleave', () => {
|
|
834
|
-
const key = marker.dataset.key;
|
|
835
|
-
if (key) {
|
|
836
|
-
document.querySelectorAll(`.expr[data-key="${key}"]`).forEach(e => e.classList.remove('active'));
|
|
837
|
-
} else {
|
|
838
|
-
marker.closest('.expr')?.classList.remove('active');
|
|
839
|
-
}
|
|
840
|
-
});
|
|
841
|
-
});
|
|
842
|
-
})();
|
|
843
|
-
</script>
|
|
844
|
-
</body>
|
|
845
|
-
</html>
|
|
846
|
-
HTML
|
|
764
|
+
render_payload_html(payload)
|
|
847
765
|
end
|
|
848
766
|
|
|
849
767
|
def self.render_text_from_events(source, events, filename: "script.rb", ranges: nil, with_header: true, header_label: nil, tty: nil)
|
|
850
|
-
|
|
768
|
+
render_text_from_normalized_events(
|
|
769
|
+
source,
|
|
770
|
+
normalize_events(events),
|
|
771
|
+
filename: filename,
|
|
772
|
+
ranges: ranges,
|
|
773
|
+
with_header: with_header,
|
|
774
|
+
header_label: header_label,
|
|
775
|
+
tty: tty
|
|
776
|
+
)
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
def self.render_text_from_normalized_events(source, events, filename: "script.rb", ranges: nil, with_header: true, header_label: nil, tty: nil)
|
|
851
780
|
ranges = normalize_ranges(ranges)
|
|
852
781
|
target_events = events.select { |e| e[:file] == filename }
|
|
853
782
|
term_width = tty ? terminal_width : nil
|
|
@@ -869,7 +798,7 @@ module GenerateResultedHtml
|
|
|
869
798
|
]
|
|
870
799
|
next if seen[key]
|
|
871
800
|
seen[key] = true
|
|
872
|
-
executed_by_line[line] += 1 if line
|
|
801
|
+
executed_by_line[line] += 1 if line && (e[:total] || e["total"]).to_i > 0
|
|
873
802
|
end
|
|
874
803
|
|
|
875
804
|
out = +""
|
|
@@ -970,7 +899,10 @@ module GenerateResultedHtml
|
|
|
970
899
|
end
|
|
971
900
|
|
|
972
901
|
def self.render_text_all_from_events(events, root: Dir.pwd, ranges_by_file: nil, tty: nil)
|
|
973
|
-
|
|
902
|
+
render_text_all_from_normalized_events(normalize_events(events), root: root, ranges_by_file: ranges_by_file, tty: tty)
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
def self.render_text_all_from_normalized_events(events, root: Dir.pwd, ranges_by_file: nil, tty: nil)
|
|
974
906
|
by_file = events.group_by { |e| e[:file] }
|
|
975
907
|
ranges_by_file = normalize_ranges_by_file(ranges_by_file)
|
|
976
908
|
|
|
@@ -984,7 +916,7 @@ module GenerateResultedHtml
|
|
|
984
916
|
ranges = nil
|
|
985
917
|
end
|
|
986
918
|
rel = path.start_with?(root) ? path.sub(root + File::SEPARATOR, "") : path
|
|
987
|
-
|
|
919
|
+
render_text_from_normalized_events(src, events, filename: path, ranges: ranges, with_header: true, header_label: rel, tty: tty)
|
|
988
920
|
end.compact
|
|
989
921
|
|
|
990
922
|
header = "\n=== Lumitrace Results (text) ===\n\n"
|