innodb_ruby 0.9.0 → 0.9.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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