innodb_ruby 0.9.0 → 0.9.5

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.
@@ -6,52 +6,101 @@ require "ostruct"
6
6
  require "set"
7
7
  require "innodb"
8
8
 
9
- def log_summary(log, space_ids)
10
- puts "%-10s%-30s%-10s%-10s" % [
9
+ def log_summary(log_group)
10
+ puts "%-20s%-15s%-10s%-12s%-10s" % [
11
+ "lsn",
11
12
  "block",
13
+ "length",
14
+ "first_rec",
15
+ "checkpoint",
16
+ ]
17
+ lsn = log_group.start_lsn.first
18
+ log_group.each_block do |block_index, block|
19
+ header = block.header
20
+ puts "%-20i%-15i%-10i%-12i%-10i" % [
21
+ lsn,
22
+ header[:block_number],
23
+ header[:data_length],
24
+ header[:first_rec_group],
25
+ header[:checkpoint_no],
26
+ ]
27
+ block.dump if @options.dump
28
+ lsn += Innodb::LogBlock::BLOCK_SIZE
29
+ end
30
+ end
31
+
32
+ def log_reader_record_summary(reader, follow)
33
+ puts "%-10s%-10s%-20s%-10s%-10s%-10s" % [
34
+ "time",
35
+ "lsn",
12
36
  "type",
37
+ "size",
13
38
  "space",
14
- "page",
39
+ "page"
15
40
  ]
16
- log.each_block do |block_index, block|
17
- record = block.first_record_preamble
18
- if record
19
- space_id = record[:space]
20
- if @options.space_ids.empty? or @options.space_ids.include?(space_id)
21
- puts "%-10i%-30s%-10i%-10i" % [
22
- block_index,
23
- record[:type],
24
- space_id,
25
- record[:page_number],
26
- ]
27
- if @options.dump
28
- block.dump
29
- end
30
- end
31
- end
41
+
42
+ reader.each_record(follow) do |rec|
43
+ preamble = rec.preamble.dup
44
+ preamble.default = ""
45
+ puts "%-10s%-10i%-20s%-10i%-10s%-10s" % [
46
+ (Time.now.strftime "%H:%M:%S"),
47
+ rec.lsn.first,
48
+ preamble[:type].to_s,
49
+ rec.size,
50
+ rec.preamble[:space].to_s,
51
+ rec.preamble[:page_number].to_s,
52
+ ]
32
53
  end
33
54
  end
34
55
 
56
+ def log_follow_tail_summary(log_group)
57
+ reader = log_group.reader(log_group.max_checkpoint_lsn)
58
+ reader.checksum = true
59
+ log_reader_record_summary(reader, true)
60
+ end
61
+
62
+ def log_record_summary(log_group, lsn_no)
63
+ reader = log_group.reader(log_group.start_lsn)
64
+ reader.seek(lsn_no)
65
+ log_reader_record_summary(reader, false)
66
+ end
67
+
68
+ def record_dump(log_group, lsn)
69
+ log_group.record(lsn).dump
70
+ end
71
+
35
72
  def usage(exit_code, message = nil)
36
73
  print "Error: #{message}\n" unless message.nil?
37
74
 
38
75
  print <<'END_OF_USAGE'
39
76
 
40
- Usage: innodb_log [-d] [-s] <file> [<mode>]
77
+ Usage: innodb_log [-d] [-s] -f <log file> <mode>
41
78
 
42
79
  --help, -?
43
80
  Print this usage text.
44
81
 
82
+ --log-file, -f
83
+ Load a InnoDB redo log file. Repeat for each log file in the log group.
84
+
45
85
  --dump-blocks, -d
46
86
  Dump block header, trailer, and record.
47
87
 
48
- --space-id, -s
49
- Print only given space id (may provide multiple times).
88
+ --lsn <number>, -l
89
+ Use the log sequence number <number>.
50
90
 
51
91
  The following modes are supported:
52
92
 
53
- log-summary (default)
54
- A summary of all blocks within log that contain at least one record.
93
+ log-summary
94
+ A summary of each block within the logs.
95
+
96
+ log-follow-tail-summary
97
+ Follow the tail of the log and print a summary of each newly logged record.
98
+
99
+ log-record-summary
100
+ Print a summary of each log record following a given LSN.
101
+
102
+ record-dump
103
+ Dump the contents of a log record, using the Ruby pp ("pretty-print") module.
55
104
 
56
105
  END_OF_USAGE
57
106
 
@@ -59,13 +108,15 @@ END_OF_USAGE
59
108
  end
60
109
 
61
110
  @options = OpenStruct.new
111
+ @options.log_files = []
62
112
  @options.dump = false
63
- @options.space_ids = Set.new
113
+ @options.lsn = nil
64
114
 
65
115
  getopt_options = [
66
116
  [ "--help", "-?", GetoptLong::NO_ARGUMENT ],
117
+ [ "--log-file", "-f", GetoptLong::REQUIRED_ARGUMENT ],
67
118
  [ "--dump-blocks", "-d", GetoptLong::NO_ARGUMENT ],
68
- [ "--space-id", "-s", GetoptLong::REQUIRED_ARGUMENT ],
119
+ [ "--lsn", "-l", GetoptLong::REQUIRED_ARGUMENT ],
69
120
  ]
70
121
 
71
122
  getopt = GetoptLong.new(*getopt_options)
@@ -74,25 +125,40 @@ getopt.each do |opt, arg|
74
125
  case opt
75
126
  when "--help"
76
127
  usage 0
128
+ when "--log-file"
129
+ @options.log_files << arg
77
130
  when "--dump-blocks"
78
131
  @options.dump = true
79
- when "--space-id"
80
- @options.space_ids << Integer(arg)
132
+ when "--lsn"
133
+ @options.lsn = arg.to_i
81
134
  end
82
135
  end
83
136
 
84
- filename, mode = ARGV.shift(2)
85
- if filename.nil?
86
- usage 1
137
+ mode = ARGV.shift
138
+
139
+ unless mode
140
+ usage 1, "At least one mode must be provided"
141
+ end
142
+
143
+ if @options.log_files.empty?
144
+ usage 1, "At least one log file (-f) must be specified"
87
145
  end
88
146
 
89
- log = Innodb::Log.new(filename)
147
+ if /^(log-)?record-/.match(mode) and !@options.lsn
148
+ usage 1, "LSN must be specified using -l/--lsn"
149
+ end
90
150
 
91
- mode ||= "log-summary"
151
+ log_group = Innodb::LogGroup.new(@options.log_files.sort)
92
152
 
93
153
  case mode
94
154
  when "log-summary"
95
- log_summary(log, @options.space_ids)
155
+ log_summary(log_group)
156
+ when "log-follow-tail-summary"
157
+ log_follow_tail_summary(log_group)
158
+ when "log-record-summary"
159
+ log_record_summary(log_group, @options.lsn)
160
+ when "record-dump"
161
+ record_dump(log_group, @options.lsn)
96
162
  else
97
163
  usage 1, "Unknown mode: #{mode}"
98
164
  end
@@ -5,6 +5,84 @@ require "getoptlong"
5
5
  require "ostruct"
6
6
  require "innodb"
7
7
 
8
+ # Convert a floating point RGB array into an ANSI color number approximating it.
9
+ def rgb_to_ansi(rgb)
10
+ rgb_n = rgb.map { |c| (c * 5.0).round }
11
+ 16 + (rgb_n[0] * 36) + (rgb_n[1] * 6) + rgb_n[2]
12
+ end
13
+
14
+ # Interpolate intermediate float-arrays between two float-arrays. Do not
15
+ # include the points a and b in the result.
16
+ def interpolate(a, b, count)
17
+ deltas = a.each_index.map { |i| b[i] - a[i] }
18
+ steps = a.each_index.map { |i| deltas[i].to_f / (count.to_f + 1) }
19
+
20
+ count.times.to_a.map { |i| a.each_index.map { |j| a[j] + ((i+1).to_f * steps[j]) } }
21
+ end
22
+
23
+ # Interpolate intermediate float-arrays between each step in a sequence of
24
+ # float-arrays. Include each step in the sequence.
25
+ def interpolate_sequence(sequence, count)
26
+ result = []
27
+ result << sequence.first
28
+ (sequence.size-1).times.map { |n| [sequence[n], sequence[n+1]] }.each do |from, to|
29
+ interpolate(from, to, count).each do |step|
30
+ result << step
31
+ end
32
+ result << to
33
+ end
34
+
35
+ result
36
+ end
37
+
38
+ # The RGB values of the typical heatmap progression.
39
+ HEATMAP_PROGRESSION = [
40
+ [0.0, 0.0, 0.0], # Black
41
+ [0.0, 0.0, 1.0], # Blue
42
+ [0.0, 1.0, 1.0], # Cyan
43
+ [0.0, 1.0, 0.0], # Green
44
+ [1.0, 1.0, 0.0], # Yellow
45
+ [1.0, 0.0, 0.0], # Red
46
+ [1.0, 0.0, 1.0], # Purple
47
+ ]
48
+
49
+ # Typical heatmap color progression.
50
+ ANSI_COLORS_HEATMAP = interpolate_sequence(HEATMAP_PROGRESSION, 6).map { |rgb| rgb_to_ansi(rgb) }
51
+
52
+ # The 24-step grayscale progression.
53
+ ANSI_COLORS_GRAYSCALE = (0xe8..0xff).to_a
54
+
55
+ # Return the text supplied with ANSI 256-color coloring applied.
56
+ def ansi_color(color, text)
57
+ "\x1b[38;5;#{color}m#{text}\x1b[0m"
58
+ end
59
+
60
+ # Zero and 1/8 through 8/8 illustrations.
61
+ BLOCK_CHARS_V = ["░", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]
62
+ BLOCK_CHARS_H = ["░", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"]
63
+
64
+ # A reasonably large prime number to multiply identifiers by in order to
65
+ # space out the colors used for similar identifiers.
66
+ COLOR_SPACING_PRIME = 999983
67
+
68
+ # Return a string with a possibly colored filled block for use in printing
69
+ # to an ANSI-capable Unicode-enabled terminal.
70
+ def filled_block(fraction, identifier=nil, block_chars=BLOCK_CHARS_V)
71
+ if fraction == 0.0
72
+ block = block_chars[0]
73
+ else
74
+ parts = (fraction.to_f * (block_chars.size.to_f-1)).floor
75
+ block = block_chars[[block_chars.size-1, parts+1].min]
76
+ end
77
+ if identifier
78
+ # ANSI 256-color mode, color palette starts at 10 and contains 216 colors.
79
+ color = 16 + ((identifier * COLOR_SPACING_PRIME) % 216)
80
+ ansi_color(color, block)
81
+ else
82
+ block
83
+ end
84
+ end
85
+
8
86
  # Print metadata about each list in an array of InnoDB::List objects.
9
87
  def print_lists(lists)
10
88
  puts "%-20s%-12s%-12s%-12s%-12s%-12s" % [
@@ -30,7 +108,7 @@ end
30
108
 
31
109
  # Print a page usage bitmap for each extent descriptor in an array of
32
110
  # Innodb::XdesEntry objects.
33
- def print_xdes_list(list)
111
+ def print_xdes_list(space, list)
34
112
  puts "%-12s%-64s" % [
35
113
  "start_page",
36
114
  "page_used_bitmap"
@@ -40,7 +118,9 @@ def print_xdes_list(list)
40
118
  puts "%-12i%-64s" % [
41
119
  entry.xdes[:start_page],
42
120
  entry.each_page_status.inject("") { |bitmap, (page_number, page_status)|
43
- bitmap += page_status[:free] ? "." : "#"
121
+ if page_number < space.pages
122
+ bitmap += page_status[:free] ? "." : "#"
123
+ end
44
124
  bitmap
45
125
  },
46
126
  ]
@@ -278,7 +358,7 @@ def space_list_iterate(space, list_name)
278
358
 
279
359
  case fsp[list_name]
280
360
  when Innodb::List::Xdes
281
- print_xdes_list(fsp[list_name])
361
+ print_xdes_list(space, fsp[list_name])
282
362
  when Innodb::List::Inode
283
363
  puts "%-12s" % [
284
364
  "page",
@@ -364,7 +444,144 @@ def space_index_pages_free_plot(space, image, start_page)
364
444
  end
365
445
 
366
446
  def space_extents(space)
367
- print_xdes_list(space.each_xdes)
447
+ print_xdes_list(space, space.each_xdes)
448
+ end
449
+
450
+ # Illustrate the space by printing each extent and for each page, printing a
451
+ # filled block colored based on the index the page is part of. Print a legend
452
+ # for the colors used afterwards.
453
+ def space_extents_illustrate(space)
454
+ puts
455
+ puts "%12s ╭%-64s╮" % [ "Start Page", "─" * 64 ]
456
+
457
+ identifiers = {}
458
+
459
+ space.each_xdes do |entry|
460
+ puts "%12i │%-64s│" % [
461
+ entry.xdes[:start_page],
462
+ entry.each_page_status.inject("") { |bitmap, (page_number, page_status)|
463
+ if page_number < space.pages
464
+ used_fraction = 1.0
465
+ identifier = nil
466
+ if page_status[:free]
467
+ used_fraction = 0.0
468
+ else
469
+ page = space.page(page_number)
470
+ if page.respond_to?(:used_space)
471
+ used_fraction = page.used_space.to_f / page.size.to_f
472
+ end
473
+ if page.respond_to?(:index_id)
474
+ identifier = page.index_id
475
+ unless identifiers[identifier]
476
+ identifiers[identifier] = "Index #{page.index_id}"
477
+ if space.innodb_system
478
+ table, index = space.innodb_system.table_and_index_name_by_id(page.index_id)
479
+ if table && index
480
+ identifiers[identifier] += " (%s.%s)" % [table, index]
481
+ end
482
+ end
483
+ end
484
+ end
485
+ end
486
+ bitmap += filled_block(used_fraction, identifier)
487
+ else
488
+ bitmap += " "
489
+ end
490
+ bitmap
491
+ },
492
+ ]
493
+ end
494
+
495
+ puts "%12s ╰%-64s╯" % [ "", "─" * 64 ]
496
+
497
+ puts
498
+ puts "Legend:"
499
+ puts " %s %s" % [filled_block(1.0, nil), "System"]
500
+ identifiers.sort.each do |identifier, description|
501
+ puts " %s %s" % [filled_block(1.0, identifier), description]
502
+ end
503
+ puts " %s %s" % [filled_block(0.0, nil), "Free space"]
504
+ puts
505
+ end
506
+
507
+ def space_lsn_age_illustrate(space)
508
+ colors = ANSI_COLORS_HEATMAP
509
+
510
+ # Calculate the minimum and maximum LSN in the space. This is pretty
511
+ # inefficient as we end up scanning all pages twice.
512
+ page_lsn = Array.new(space.pages)
513
+
514
+ lsn_min = lsn_max = space.page(0).lsn
515
+ space.each_page do |page_number, page|
516
+ if page.lsn != 0
517
+ page_lsn[page_number] = page.lsn
518
+ lsn_min = page.lsn < lsn_min ? page.lsn : lsn_min
519
+ lsn_max = page.lsn > lsn_max ? page.lsn : lsn_max
520
+ end
521
+ end
522
+ lsn_delta = lsn_max - lsn_min
523
+
524
+ puts
525
+ puts "%12s ╭%-64s╮" % [ "Start Page", "─" * 64 ]
526
+
527
+ start_page = 0
528
+ page_lsn.each_slice(64) do |slice|
529
+ puts "%12i │%-64s│" % [
530
+ start_page,
531
+ slice.inject("") { |line, lsn|
532
+ if lsn
533
+ age_ratio = (lsn - lsn_min).to_f / lsn_delta.to_f
534
+ color = colors[(age_ratio * colors.size.to_f).floor]
535
+ line += ansi_color(color, filled_block(1.0, nil))
536
+ else
537
+ line += " "
538
+ end
539
+ line
540
+ },
541
+ ]
542
+ start_page += 64
543
+ end
544
+
545
+ puts "%12s ╰%-64s╯" % [ "", "─" * 64 ]
546
+
547
+ lsn_legend = "<" + ("─" * (colors.size - 2)) + ">"
548
+
549
+ begin
550
+ # Try to optionally replace the boring lsn_legend with a histogram of
551
+ # page age distribution. If histogram/array is not available, move on.
552
+
553
+ require 'histogram/array'
554
+
555
+ lsn_bins, lsn_freq = page_lsn.select { |lsn| !lsn.nil? }.
556
+ histogram(colors.size, :min => lsn_min, :max => lsn_max)
557
+
558
+ lsn_freq_delta = lsn_freq.max - lsn_freq.min
559
+
560
+ lsn_legend = ""
561
+ lsn_freq.each do |freq|
562
+ freq_norm = freq / lsn_freq_delta
563
+ if freq_norm > 0.0
564
+ lsn_legend << filled_block(freq_norm)
565
+ else
566
+ # Avoid the "empty" block used for 0.0.
567
+ lsn_legend << " "
568
+ end
569
+ end
570
+ rescue LoadError
571
+ # That's okay! Leave the legend boring.
572
+ end
573
+
574
+ puts
575
+ puts "Legend:"
576
+ puts " %12s %s %-12s" % [
577
+ "Min LSN",
578
+ lsn_legend,
579
+ "Max LSN" ]
580
+ puts " %12i %s %-12i" % [
581
+ lsn_min,
582
+ colors.map { |c| ansi_color(c, filled_block(1.0, nil)) }.join,
583
+ lsn_max,
584
+ ]
368
585
  end
369
586
 
370
587
  def print_inode_summary(inode)
@@ -537,6 +754,59 @@ def page_directory_summary(page_number)
537
754
  end
538
755
  end
539
756
 
757
+ def page_illustrate(page)
758
+ width = 256
759
+ blocks = Array.new(page.size, " ")
760
+ identifiers = {}
761
+ identifier_sort = 0
762
+
763
+ page.each_region.sort { |a,b| a[:offset] <=> b[:offset]}.each do |region|
764
+ region[:length].times do |n|
765
+ identifier = nil
766
+ fraction = 0.0
767
+ if region[:name] != :garbage
768
+ if n == region[:length] - 1
769
+ fraction = 0.5
770
+ else
771
+ fraction = 1.0
772
+ end
773
+ identifier = region[:name].hash.abs
774
+ unless identifiers[identifier]
775
+ # Prefix an integer <0123> on each name so that the legend can be
776
+ # sorted by the appearance of each region in the page.
777
+ identifiers[identifier] = "<%04i>%s" % [
778
+ identifier_sort,
779
+ region[:info]
780
+ ]
781
+ identifier_sort += 1
782
+ end
783
+ end
784
+ blocks[region[:offset] + n] = filled_block(fraction, identifier, BLOCK_CHARS_H)
785
+ end
786
+ end
787
+
788
+ puts
789
+ puts "%12s ╭%-#{width}s╮" % [ "Offset", "─" * width ]
790
+ offset = 0
791
+ blocks.each_slice(width) do |slice|
792
+ puts "%12i │%-s│" % [offset, slice.join]
793
+ offset += width
794
+ end
795
+ puts "%12s ╰%-#{width}s╯" % [ "", "─" * width ]
796
+
797
+ puts
798
+ puts "Legend:"
799
+ identifiers.sort { |a,b| a[1] <=> b[1] }.each do |identifier, description|
800
+ puts " %s %s" % [
801
+ filled_block(1.0, identifier),
802
+ description.gsub(/^<\d+>/, "")
803
+ ]
804
+ end
805
+ puts " %s %s" % [filled_block(0.0, nil), "Garbage"]
806
+
807
+ puts
808
+ end
809
+
540
810
  def index_fseg_lists(index, fseg_name)
541
811
  unless index.fseg(fseg_name)
542
812
  raise "File segment '#{fseg_name}' doesn't exist"
@@ -554,7 +824,7 @@ def index_fseg_list_iterate(index, fseg_name, list_name)
554
824
  raise "List '#{list_name}' doesn't exist"
555
825
  end
556
826
 
557
- print_xdes_list(list)
827
+ print_xdes_list(index.space, list)
558
828
  end
559
829
 
560
830
  def index_fseg_frag_pages(index, fseg_name)
@@ -787,6 +1057,11 @@ The following modes are supported:
787
1057
  space-extents
788
1058
  Iterate through all extents, printing the extent descriptor bitmap.
789
1059
 
1060
+ space-extents-illustrate
1061
+ Iterate through all extents, illustrating the extent usage using ANSI
1062
+ color and Unicode box drawing characters to show page usage throughout
1063
+ the space.
1064
+
790
1065
  space-inodes-summary
791
1066
  Iterate through all inodes, printing a short summary of each FSEG.
792
1067
 
@@ -861,9 +1136,9 @@ getopt_options = [
861
1136
  [ "--help", "-?", GetoptLong::NO_ARGUMENT ],
862
1137
  [ "--trace", "-t", GetoptLong::NO_ARGUMENT ],
863
1138
  [ "--system-space-file", "-s", GetoptLong::REQUIRED_ARGUMENT ],
1139
+ [ "--space-file", "-f", GetoptLong::REQUIRED_ARGUMENT ],
864
1140
  [ "--table-name", "-T", GetoptLong::REQUIRED_ARGUMENT ],
865
1141
  [ "--index-name", "-I", GetoptLong::REQUIRED_ARGUMENT ],
866
- [ "--space-file", "-f", GetoptLong::REQUIRED_ARGUMENT ],
867
1142
  [ "--page", "-p", GetoptLong::REQUIRED_ARGUMENT ],
868
1143
  [ "--level", "-l", GetoptLong::REQUIRED_ARGUMENT ],
869
1144
  [ "--list", "-L", GetoptLong::REQUIRED_ARGUMENT ],
@@ -879,9 +1154,7 @@ getopt.each do |opt, arg|
879
1154
  when "--help"
880
1155
  usage 0
881
1156
  when "--trace"
882
- Innodb::Cursor.trace!
883
- when "--mode"
884
- @options.mode = arg
1157
+ BufferCursor.trace!
885
1158
  when "--system-space-file"
886
1159
  @options.system_space_file = arg
887
1160
  when "--space-file"
@@ -1012,6 +1285,10 @@ when "space-indexes"
1012
1285
  space_indexes(innodb_system, space)
1013
1286
  when "space-extents"
1014
1287
  space_extents(space)
1288
+ when "space-extents-illustrate"
1289
+ space_extents_illustrate(space)
1290
+ when "space-lsn-age-illustrate"
1291
+ space_lsn_age_illustrate(space)
1015
1292
  when "space-inodes-summary"
1016
1293
  space_inodes_summary(space)
1017
1294
  when "space-inodes-detail"
@@ -1042,6 +1319,8 @@ when "page-account"
1042
1319
  page_account(innodb_system, space, @options.page)
1043
1320
  when "page-directory-summary"
1044
1321
  page_directory_summary(@options.page)
1322
+ when "page-illustrate"
1323
+ page_illustrate(space.page(@options.page))
1045
1324
  else
1046
1325
  usage 1, "Unknown mode: #{mode}"
1047
1326
  end