lumitrace 0.4.2 → 0.6.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.
@@ -7,6 +7,8 @@ module Lumitrace
7
7
  end
8
8
 
9
9
  module RecordInstrument
10
+ IDENTIFIER_METHOD_NAME_RE = /\A[a-z_]\w*[!?=]?\z/.freeze
11
+
10
12
  SKIP_NODE_CLASSES = [
11
13
  Prism::DefNode,
12
14
  Prism::ClassNode,
@@ -99,7 +101,7 @@ module RecordInstrument
99
101
  end
100
102
  end
101
103
 
102
- node.child_nodes.each { |child| stack << [child, node] }
104
+ instrumentable_child_nodes(node).each { |child| stack << [child, node] }
103
105
  end
104
106
  locs
105
107
  end
@@ -136,7 +138,7 @@ module RecordInstrument
136
138
  end
137
139
  end
138
140
 
139
- node.child_nodes.each { |child| stack << [child, node] }
141
+ instrumentable_child_nodes(node).each { |child| stack << [child, node] }
140
142
  end
141
143
 
142
144
  inserts
@@ -177,6 +179,8 @@ module RecordInstrument
177
179
  def self.wrap_expr?(node, parent = nil)
178
180
  return false unless node.respond_to?(:location)
179
181
  return false if literal_value_node?(node)
182
+ return false if command_style_call_node?(node)
183
+ return false if parent.is_a?(Prism::DefinedNode)
180
184
  if parent.is_a?(Prism::AliasGlobalVariableNode) || parent.is_a?(Prism::AliasMethodNode)
181
185
  return false
182
186
  end
@@ -196,6 +200,27 @@ module RecordInstrument
196
200
  WRAP_NODE_CLASSES.include?(node.class)
197
201
  end
198
202
 
203
+ def self.command_style_call_node?(node)
204
+ return false unless node.is_a?(Prism::CallNode)
205
+ return false unless node.respond_to?(:arguments) && node.arguments
206
+ return false if node.respond_to?(:opening_loc) && node.opening_loc
207
+
208
+ name = node.respond_to?(:name) ? node.name : nil
209
+ return false unless name
210
+
211
+ IDENTIFIER_METHOD_NAME_RE.match?(name.to_s)
212
+ end
213
+
214
+ def self.instrumentable_child_nodes(node)
215
+ return [] unless node
216
+ return [] if node.is_a?(Prism::DefinedNode)
217
+ if command_style_call_node?(node) && node.respond_to?(:block) && node.block
218
+ [node.block]
219
+ else
220
+ node.child_nodes
221
+ end
222
+ end
223
+
199
224
  def self.expr_location(node)
200
225
  loc = node.location
201
226
  return {
@@ -341,22 +366,31 @@ module RecordInstrument
341
366
 
342
367
  def self.events_from_ids
343
368
  out = []
344
- @events_by_id.each_with_index do |e, id|
345
- next unless e
346
- loc = @loc_by_id[id]
369
+ summary_cache = {}
370
+ @loc_by_id.each_with_index do |loc, id|
347
371
  next unless loc
372
+
373
+ e = @events_by_id[id]
348
374
  case collect_mode
349
375
  when :history
350
- raw_values = values_from_ring(e)
351
- all_types = history_type_set(e)
352
- if all_types.nil? || all_types.empty?
353
- all_types = {}
354
- raw_values.each do |v|
355
- t = value_type_name(v)
356
- all_types[t] = (all_types[t] || 0) + 1
376
+ if e
377
+ raw_values = values_from_ring(e)
378
+ all_types = history_type_set(e)
379
+ if all_types.nil? || all_types.empty?
380
+ all_types = {}
381
+ raw_values.each do |v|
382
+ t = value_type_name(v)
383
+ all_types[t] = (all_types[t] || 0) + 1
384
+ end
357
385
  end
386
+ max = history_ring_size(e)
387
+ total = e[max + 1]
388
+ else
389
+ raw_values = []
390
+ all_types = {}
391
+ total = 0
358
392
  end
359
- max = history_ring_size(e)
393
+
360
394
  out << {
361
395
  file: loc[:file],
362
396
  start_line: loc[:start_line],
@@ -365,9 +399,20 @@ module RecordInstrument
365
399
  end_col: loc[:end_col],
366
400
  kind: loc[:kind].to_s,
367
401
  name: loc[:name],
368
- sampled_values: raw_values.map { |v| summarize_value(v, type: value_type_name(v)) },
402
+ sampled_values: raw_values.map do |v|
403
+ key = v.__id__
404
+ cached = summary_cache[key]
405
+ if cached
406
+ cached.dup
407
+ else
408
+ type = value_type_name(v)
409
+ summary = summarize_value(v, type: type)
410
+ summary_cache[key] = summary
411
+ summary.dup
412
+ end
413
+ end,
369
414
  types: sorted_type_counts(all_types),
370
- total: e[max + 1]
415
+ total: total
371
416
  }
372
417
  when :types
373
418
  out << {
@@ -378,12 +423,12 @@ module RecordInstrument
378
423
  end_col: loc[:end_col],
379
424
  kind: loc[:kind].to_s,
380
425
  name: loc[:name],
381
- types: sorted_type_counts(e[:types]),
382
- total: e[:total]
426
+ types: sorted_type_counts(e ? e[:types] : nil),
427
+ total: e ? e[:total] : 0
383
428
  }
384
429
  else # :last
385
- last_raw = e[:last_value]
386
- last_type = value_type_name(last_raw)
430
+ last_raw = e && e[:last_value]
431
+ last_type = value_type_name(last_raw) if e
387
432
  out << {
388
433
  file: loc[:file],
389
434
  start_line: loc[:start_line],
@@ -392,9 +437,21 @@ module RecordInstrument
392
437
  end_col: loc[:end_col],
393
438
  kind: loc[:kind].to_s,
394
439
  name: loc[:name],
395
- last_value: summarize_value(last_raw, type: last_type),
396
- types: sorted_type_counts(e[:types]),
397
- total: e[:total]
440
+ last_value: if e
441
+ key = last_raw.__id__
442
+ cached = summary_cache[key]
443
+ if cached
444
+ cached.dup
445
+ else
446
+ summary = summarize_value(last_raw, type: last_type)
447
+ summary_cache[key] = summary
448
+ summary.dup
449
+ end
450
+ else
451
+ nil
452
+ end,
453
+ types: sorted_type_counts(e ? e[:types] : nil),
454
+ total: e ? e[:total] : 0
398
455
  }
399
456
  end
400
457
  end
@@ -438,10 +495,42 @@ module RecordInstrument
438
495
 
439
496
  def self.dump_events_json(events, path = nil)
440
497
  path ||= File.expand_path("lumitrace_recorded.json", Dir.pwd)
441
- File.write(path, JSON.dump(events), perm: 0o600)
498
+ payload = {
499
+ version: 1,
500
+ events: events,
501
+ coverage: compute_coverage(events)
502
+ }
503
+ File.write(path, JSON.dump(payload), perm: 0o600)
442
504
  path
443
505
  end
444
506
 
507
+ def self.compute_coverage(events)
508
+ by_file = {}
509
+ events.each do |e|
510
+ file = e[:file] || e["file"]
511
+ next unless file
512
+ total = e[:total] || e["total"] || 0
513
+ start_line = e[:start_line] || e["start_line"]
514
+ next unless start_line
515
+
516
+ entry = (by_file[file] ||= { lines: {}, covered_lines: {} })
517
+ entry[:lines][start_line] = true
518
+ entry[:covered_lines][start_line] = true if total > 0
519
+ end
520
+
521
+ by_file.map do |file, entry|
522
+ total_lines = entry[:lines].size
523
+ covered_lines = entry[:covered_lines].size
524
+ pct = total_lines > 0 ? (covered_lines * 100.0 / total_lines).round(1) : 0.0
525
+ {
526
+ file: file,
527
+ total_lines: total_lines,
528
+ covered_lines: covered_lines,
529
+ coverage_percent: pct
530
+ }
531
+ end.sort_by { |e| e[:file] }
532
+ end
533
+
445
534
  def self.load_events_json(path)
446
535
  JSON.parse(File.read(path))
447
536
  end
@@ -629,25 +718,36 @@ module RecordInstrument
629
718
  events_from_ids
630
719
  end
631
720
 
721
+ KERNEL_INSPECT = ::Kernel.instance_method(:inspect)
722
+
723
+ def self.safe_inspect(v)
724
+ v.inspect
725
+ rescue ::NoMethodError
726
+ KERNEL_INSPECT.bind_call(v)
727
+ end
728
+
632
729
  def self.safe_value(v)
633
730
  case v
634
731
  when Numeric, TrueClass, FalseClass, NilClass
635
732
  v
636
733
  else
637
- s = v.inspect
734
+ s = safe_inspect(v)
638
735
  s.bytesize > 1000 ? s[0, 1000] + "..." : s
639
736
  end
640
737
  end
641
738
 
739
+ KERNEL_CLASS = ::Kernel.instance_method(:class)
740
+
642
741
  def self.value_type_name(v)
643
- name = v.class.name
644
- name && !name.empty? ? name : v.class.to_s
742
+ klass = KERNEL_CLASS.bind_call(v)
743
+ name = klass.name
744
+ name && !name.empty? ? name : klass.to_s
645
745
  end
646
746
 
647
747
  def self.summarize_value(v, type: nil)
648
748
  type ||= value_type_name(v)
649
749
  preview_limit = 120
650
- inspected = v.inspect
750
+ inspected = safe_inspect(v)
651
751
  if inspected.length > preview_limit
652
752
  {
653
753
  type: type,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lumitrace
4
- VERSION = "0.4.2"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/lumitrace.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "shellwords"
4
5
  require "tmpdir"
5
6
  require_relative "lumitrace/version"
6
7
  require_relative "lumitrace/record_instrument"
@@ -12,6 +13,8 @@ require_relative "lumitrace/ai_docs"
12
13
 
13
14
  module Lumitrace
14
15
  class Error < StandardError; end
16
+ KERNEL_CLASS = ::Kernel.instance_method(:class)
17
+
15
18
  @atexit_registered = false
16
19
  @atexit_output_root = nil
17
20
  @atexit_ranges_by_file = nil
@@ -34,6 +37,16 @@ module Lumitrace
34
37
  $stderr.puts("[lumitrace] #{message}")
35
38
  end
36
39
 
40
+ def self.current_command_text
41
+ argv0 = $0.to_s
42
+ args = Array(ARGV).map(&:to_s)
43
+ parts = []
44
+ parts << argv0 unless argv0.empty?
45
+ parts.concat(args)
46
+ return nil if parts.empty?
47
+ Shellwords.join(parts)
48
+ end
49
+
37
50
  def self.normalize_collect_mode(mode)
38
51
  m = mode.to_s.strip
39
52
  m = "last" if m.empty?
@@ -51,7 +64,7 @@ module Lumitrace
51
64
  def R(id, value)
52
65
  events_by_id = RecordInstrument.events_by_id
53
66
  entry = events_by_id[id]
54
- klass = value.class
67
+ klass = KERNEL_CLASS.bind_call(value)
55
68
  type = klass.name
56
69
  type = klass.to_s if type.nil? || type.empty?
57
70
  if entry
@@ -70,7 +83,7 @@ module Lumitrace
70
83
  def R(id, value)
71
84
  events_by_id = RecordInstrument.events_by_id
72
85
  entry = events_by_id[id]
73
- klass = value.class
86
+ klass = KERNEL_CLASS.bind_call(value)
74
87
  type = klass.name
75
88
  type = klass.to_s if type.nil? || type.empty?
76
89
  if entry
@@ -88,7 +101,7 @@ module Lumitrace
88
101
  def R(id, value)
89
102
  events_by_id = RecordInstrument.events_by_id
90
103
  entry = events_by_id[id]
91
- klass = value.class
104
+ klass = KERNEL_CLASS.bind_call(value)
92
105
  type = klass.name
93
106
  type = klass.to_s if type.nil? || type.empty?
94
107
  if entry
@@ -459,13 +472,22 @@ module Lumitrace
459
472
  next
460
473
  end
461
474
 
475
+ events_collect_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
462
476
  events = RecordInstrument.events
477
+ events_collect_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - events_collect_start) * 1000.0
478
+ verbose_log(format("events: collect %.1fms count=%d", events_collect_ms, events.length)) if @verbose_level.to_i > 0
479
+
480
+ detail_logger = (@verbose_level.to_i > 1 ? method(:verbose_log) : nil)
481
+
482
+ events_merge_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
463
483
  events = RecordInstrument.merge_child_events(
464
484
  events,
465
485
  @results_dir,
466
486
  max_samples: @atexit_max_samples,
467
- logger: method(:verbose_log)
487
+ logger: detail_logger
468
488
  )
489
+ events_merge_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - events_merge_start) * 1000.0
490
+ verbose_log(format("events: merge_children %.1fms count=%d", events_merge_ms, events.length)) if @verbose_level.to_i > 0
469
491
 
470
492
  if @atexit_json
471
493
  json_path = @atexit_json == true ? "lumitrace_recorded.json" : @atexit_json
@@ -476,7 +498,7 @@ module Lumitrace
476
498
  end
477
499
  if @atexit_text
478
500
  tty = @atexit_text == true ? $stdout.tty? : false
479
- text = GenerateResultedHtml.render_text_all_from_events(
501
+ text = GenerateResultedHtml.render_text_all_from_normalized_events(
480
502
  events,
481
503
  root: @atexit_output_root,
482
504
  ranges_by_file: @atexit_ranges_by_file,
@@ -493,16 +515,23 @@ module Lumitrace
493
515
  end
494
516
  end
495
517
  if @atexit_html
496
- html = GenerateResultedHtml.render_all_from_events(
518
+ html_render_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
519
+ html = GenerateResultedHtml.render_all_from_normalized_events(
497
520
  events,
498
521
  root: @atexit_output_root,
499
522
  ranges_by_file: @atexit_ranges_by_file,
500
523
  collect_mode: @atexit_collect_mode,
501
- max_samples: @atexit_max_samples
524
+ max_samples: @atexit_max_samples,
525
+ logger: detail_logger,
526
+ command_text: current_command_text
502
527
  )
528
+ html_render_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - html_render_start) * 1000.0
503
529
  out_path = @atexit_html == true ? "lumitrace_recorded.html" : @atexit_html
504
530
  out_path = File.expand_path(out_path, @atexit_output_root)
531
+ html_write_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
505
532
  File.write(out_path, html)
533
+ html_write_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - html_write_start) * 1000.0
534
+ verbose_log(format("html: render %.1fms write %.1fms bytes=%d", html_render_ms, html_write_ms, html.bytesize)) if @verbose_level.to_i > 0
506
535
  verbose_log("html: #{out_path}")
507
536
  notify_output_path("html", out_path)
508
537
  end