polyphony 0.28 → 0.29

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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +0 -4
  3. data/CHANGELOG.md +12 -0
  4. data/Gemfile.lock +1 -1
  5. data/LICENSE +1 -1
  6. data/README.md +23 -21
  7. data/Rakefile +2 -0
  8. data/TODO.md +0 -3
  9. data/docs/_includes/prevnext.html +17 -0
  10. data/docs/_layouts/default.html +106 -0
  11. data/docs/_sass/custom/custom.scss +21 -0
  12. data/docs/faq.md +13 -10
  13. data/docs/getting-started/installing.md +2 -0
  14. data/docs/getting-started/tutorial.md +5 -3
  15. data/docs/index.md +4 -5
  16. data/docs/technical-overview/concurrency.md +21 -19
  17. data/docs/technical-overview/design-principles.md +12 -20
  18. data/docs/technical-overview/exception-handling.md +70 -1
  19. data/docs/technical-overview/extending.md +1 -0
  20. data/docs/technical-overview/fiber-scheduling.md +109 -88
  21. data/docs/user-guide/all-about-timers.md +126 -0
  22. data/docs/user-guide/web-server.md +2 -2
  23. data/docs/user-guide.md +1 -1
  24. data/examples/core/xx-deferring-an-operation.rb +2 -2
  25. data/examples/core/xx-sleep-forever.rb +9 -0
  26. data/examples/core/xx-snooze-starve.rb +16 -0
  27. data/examples/core/xx-spin_error_backtrace.rb +1 -1
  28. data/examples/core/xx-trace.rb +1 -2
  29. data/examples/core/xx-worker-thread.rb +30 -0
  30. data/examples/io/xx-happy-eyeballs.rb +37 -0
  31. data/ext/gyro/gyro.c +8 -3
  32. data/ext/gyro/gyro.h +7 -1
  33. data/ext/gyro/queue.c +35 -3
  34. data/ext/gyro/selector.c +31 -2
  35. data/ext/gyro/thread.c +18 -16
  36. data/lib/polyphony/core/global_api.rb +0 -1
  37. data/lib/polyphony/core/thread_pool.rb +5 -0
  38. data/lib/polyphony/core/throttler.rb +0 -1
  39. data/lib/polyphony/extensions/fiber.rb +14 -3
  40. data/lib/polyphony/extensions/thread.rb +16 -4
  41. data/lib/polyphony/irb.rb +7 -1
  42. data/lib/polyphony/trace.rb +44 -11
  43. data/lib/polyphony/version.rb +1 -1
  44. data/lib/polyphony.rb +1 -0
  45. data/test/helper.rb +1 -3
  46. data/test/test_async.rb +1 -1
  47. data/test/test_cancel_scope.rb +3 -3
  48. data/test/test_fiber.rb +157 -54
  49. data/test/test_global_api.rb +51 -1
  50. data/test/test_gyro.rb +4 -156
  51. data/test/test_io.rb +1 -1
  52. data/test/test_supervisor.rb +2 -2
  53. data/test/test_thread.rb +72 -1
  54. data/test/test_thread_pool.rb +6 -2
  55. data/test/test_throttler.rb +7 -5
  56. data/test/test_trace.rb +6 -6
  57. metadata +10 -5
  58. data/examples/core/xx-extended_fibers.rb +0 -150
  59. data/examples/core/xx-mt-scheduler.rb +0 -349
data/test/test_fiber.rb CHANGED
@@ -25,6 +25,62 @@ class FiberTest < MiniTest::Test
25
25
  f&.stop
26
26
  end
27
27
 
28
+ def test_schedule
29
+ values = []
30
+ fibers = (0..2).map { |i| spin { suspend; values << i } }
31
+ snooze
32
+
33
+ fibers[0].schedule
34
+ assert_equal [], values
35
+
36
+ snooze
37
+
38
+ assert_equal [0], values
39
+ assert_equal :dead, fibers[0].state
40
+
41
+ fibers[1].schedule
42
+ fibers[2].schedule
43
+
44
+ assert_equal [0], values
45
+ snooze
46
+ assert_equal [0, 1, 2], values
47
+ end
48
+
49
+ def test_cross_thread_schedule
50
+ buffer = []
51
+ worker_fiber = nil
52
+ async = Gyro::Async.new
53
+ worker = Thread.new do
54
+ worker_fiber = Fiber.current
55
+ async.signal!
56
+ suspend
57
+ buffer << :foo
58
+ end
59
+
60
+ async.await
61
+ assert worker_fiber
62
+ worker_fiber.schedule
63
+ worker.join
64
+ assert_equal [:foo], buffer
65
+ ensure
66
+ worker&.kill
67
+ end
68
+
69
+ def test_ev_loop_anti_starve_mechanism
70
+ async = Gyro::Async.new
71
+ t = Thread.new do
72
+ f = spin_loop { snooze }
73
+ sleep 0.001
74
+ async.signal!(:foo)
75
+ end
76
+
77
+ result = move_on_after(0.05) { async.await }
78
+
79
+ assert_equal :foo, result
80
+ ensure
81
+ t.kill if t.alive?
82
+ end
83
+
28
84
  def test_tag
29
85
  assert_equal :main, Fiber.current.tag
30
86
  Fiber.current.tag = :foo
@@ -63,7 +119,7 @@ class FiberTest < MiniTest::Test
63
119
  2.times { snooze }
64
120
  result << 2
65
121
  end
66
- defer { f.raise }
122
+ spin { f.raise }
67
123
  assert_equal 0, result.size
68
124
  begin
69
125
  f.await
@@ -88,7 +144,7 @@ class FiberTest < MiniTest::Test
88
144
  2.times { snooze }
89
145
  result << 2
90
146
  end
91
- defer { f.raise MyError }
147
+ spin { f.raise MyError }
92
148
  assert_equal 0, result.size
93
149
  begin
94
150
  f.await
@@ -110,7 +166,7 @@ class FiberTest < MiniTest::Test
110
166
  2.times { snooze }
111
167
  result << 2
112
168
  end
113
- defer { f.raise(MyError, 'foo') }
169
+ spin { f.raise(MyError, 'foo') }
114
170
  assert_equal 0, result.size
115
171
  begin
116
172
  f.await
@@ -133,7 +189,7 @@ class FiberTest < MiniTest::Test
133
189
  2.times { snooze }
134
190
  result << 2
135
191
  end
136
- defer { f.raise 'foo' }
192
+ spin { f.raise 'foo' }
137
193
  assert_equal 0, result.size
138
194
  begin
139
195
  f.await
@@ -156,7 +212,7 @@ class FiberTest < MiniTest::Test
156
212
  2.times { snooze }
157
213
  result << 2
158
214
  end
159
- defer { f.raise MyError.new('bar') }
215
+ spin { f.raise MyError.new('bar') }
160
216
  assert_equal 0, result.size
161
217
  begin
162
218
  f.await
@@ -179,7 +235,7 @@ class FiberTest < MiniTest::Test
179
235
  2.times { snooze }
180
236
  result << 2
181
237
  end
182
- defer { f.cancel! }
238
+ spin { f.cancel! }
183
239
  assert_equal 0, result.size
184
240
  begin
185
241
  f.await
@@ -202,7 +258,7 @@ class FiberTest < MiniTest::Test
202
258
  result << 2
203
259
  3
204
260
  end
205
- defer { f.interrupt(42) }
261
+ spin { f.interrupt(42) }
206
262
 
207
263
  await_result = f.await
208
264
  assert_equal 1, result.size
@@ -220,7 +276,7 @@ class FiberTest < MiniTest::Test
220
276
  result << 2
221
277
  3
222
278
  end
223
- defer { f.stop(42) }
279
+ spin { f.stop(42) }
224
280
 
225
281
  await_result = f.await
226
282
  assert_equal 1, result.size
@@ -253,7 +309,7 @@ class FiberTest < MiniTest::Test
253
309
  f2.await
254
310
  result && result += 1
255
311
  end
256
- defer { f2.interrupt }
312
+ spin { f2.interrupt }
257
313
  suspend
258
314
  assert_nil result
259
315
  assert_equal :dead, f1.state
@@ -337,6 +393,7 @@ class FiberTest < MiniTest::Test
337
393
  end
338
394
 
339
395
  def test_select_from_multiple_fibers
396
+ sleep 0
340
397
  buffer = []
341
398
  f1 = spin { sleep 0.01; buffer << :foo; :foo }
342
399
  f2 = spin { sleep 0.03; buffer << :bar; :bar }
@@ -388,51 +445,6 @@ class FiberTest < MiniTest::Test
388
445
  assert_equal [42], values
389
446
  assert !f.running?
390
447
  end
391
- end
392
-
393
- class MailboxTest < MiniTest::Test
394
- def test_that_fiber_can_receive_messages
395
- msgs = []
396
- f = spin { loop { msgs << receive } }
397
-
398
- snooze # allow fiber to start
399
-
400
- 3.times do |i|
401
- f << i
402
- snooze
403
- end
404
-
405
- assert_equal [0, 1, 2], msgs
406
- ensure
407
- f&.stop
408
- end
409
-
410
- def test_that_multiple_messages_sent_at_once_arrive_in_order
411
- msgs = []
412
- f = spin { loop { msgs << receive } }
413
-
414
- snooze # allow coproc to start
415
-
416
- 3.times { |i| f << i }
417
-
418
- snooze
419
-
420
- assert_equal [0, 1, 2], msgs
421
- ensure
422
- f&.stop
423
- end
424
-
425
- def test_that_sent_message_are_queued_before_calling_receive
426
- buffer = []
427
- receiver = spin { suspend; 3.times { buffer << receive } }
428
- sender = spin { 3.times { |i| receiver << (i * 10) } }
429
-
430
- sender.await
431
- receiver.schedule
432
- receiver.await
433
-
434
- assert_equal [0, 10, 20], buffer
435
- end
436
448
 
437
449
  def test_list_and_count
438
450
  assert_equal 1, Fiber.count
@@ -510,4 +522,95 @@ class MailboxTest < MiniTest::Test
510
522
  assert_nil parent_error
511
523
  assert_kind_of Interrupt, main_fiber_error
512
524
  end
525
+
526
+ def test_signal_exception_in_fiber
527
+ parent_error = nil
528
+ main_fiber_error = nil
529
+ f2 = nil
530
+ f1 = spin do
531
+ f2 = spin { raise SignalException.new('HUP') }
532
+ suspend
533
+ rescue Exception => parent_error
534
+ end
535
+
536
+ begin
537
+ suspend
538
+ rescue Exception => main_fiber_error
539
+ end
540
+
541
+ assert_nil parent_error
542
+ assert_kind_of SignalException, main_fiber_error
543
+ end
544
+ end
545
+
546
+ class MailboxTest < MiniTest::Test
547
+ def test_that_fiber_can_receive_messages
548
+ msgs = []
549
+ f = spin { loop { msgs << receive } }
550
+
551
+ snooze # allow fiber to start
552
+
553
+ 3.times do |i|
554
+ f << i
555
+ snooze
556
+ end
557
+
558
+ assert_equal [0, 1, 2], msgs
559
+ ensure
560
+ f&.stop
561
+ end
562
+
563
+ def test_that_multiple_messages_sent_at_once_arrive_in_order
564
+ msgs = []
565
+ f = spin { loop { msgs << receive } }
566
+
567
+ snooze # allow coproc to start
568
+
569
+ 3.times { |i| f << i }
570
+
571
+ snooze
572
+
573
+ assert_equal [0, 1, 2], msgs
574
+ ensure
575
+ f&.stop
576
+ end
577
+
578
+ def test_that_sent_message_are_queued_before_calling_receive
579
+ buffer = []
580
+ receiver = spin { suspend; 3.times { buffer << receive } }
581
+ sender = spin { 3.times { |i| receiver << (i * 10) } }
582
+
583
+ sender.await
584
+ receiver.schedule
585
+ receiver.await
586
+
587
+ assert_equal [0, 10, 20], buffer
588
+ end
589
+
590
+ def test_cross_thread_send_receive
591
+ skip "There's currently a race condition in cross-thread send/receive. We're going to rewrite it in C"
592
+ ping_receive_buffer = []
593
+ pong_receive_buffer = []
594
+ pong = Thread.new do
595
+ loop do
596
+ peer, data = receive
597
+ pong_receive_buffer << data
598
+ peer << 'pong'
599
+ end
600
+ end
601
+
602
+ ping = Thread.new do
603
+ 3.times do
604
+ pong << [Fiber.current, 'ping']
605
+ data = receive
606
+ ping_receive_buffer << data
607
+ end
608
+ end
609
+
610
+ ping.join
611
+ pong.kill
612
+
613
+ assert_equal %w{pong pong pong}, ping_receive_buffer
614
+ assert_equal %w{ping ping ping}, pong_receive_buffer
615
+ end
513
616
  end
@@ -36,7 +36,7 @@ class SpinTest < MiniTest::Test
36
36
  sleep(1)
37
37
  42
38
38
  end
39
- defer { fiber.interrupt }
39
+ spin { fiber.interrupt }
40
40
  suspend
41
41
  assert_nil fiber.result
42
42
  end
@@ -321,4 +321,54 @@ class MoveOnAfterTest < MiniTest::Test
321
321
  f.stop
322
322
  assert !f.running?
323
323
  end
324
+
325
+ def test_snooze
326
+ values = []
327
+ 3.times.map do |i|
328
+ spin do
329
+ 3.times do
330
+ snooze
331
+ values << i
332
+ end
333
+ suspend
334
+ end
335
+ end
336
+ suspend
337
+
338
+ assert_equal [0, 1, 2, 0, 1, 2, 0, 1, 2], values
339
+ end
340
+
341
+ def test_defer
342
+ values = []
343
+ spin { values << 1 }
344
+ spin { values << 2 }
345
+ spin { values << 3 }
346
+ suspend
347
+
348
+ assert_equal [1, 2, 3], values
349
+ end
350
+
351
+ def test_suspend
352
+ values = []
353
+ spin do
354
+ values << :foo
355
+ suspend
356
+ end
357
+ suspend
358
+
359
+ assert_equal [:foo], values
360
+ end
361
+
362
+ def test_schedule_and_suspend
363
+ values = []
364
+ 3.times.map do |i|
365
+ spin do
366
+ values << i
367
+ suspend
368
+ end
369
+ end
370
+ suspend
371
+
372
+ assert_equal [0, 1, 2], values
373
+ end
324
374
  end
data/test/test_gyro.rb CHANGED
@@ -3,175 +3,23 @@
3
3
  require_relative 'helper'
4
4
 
5
5
  class GyroTest < MiniTest::Test
6
- def test_fiber_state
7
- assert_equal :running, Fiber.current.state
8
-
9
- f = Fiber.new {}
10
-
11
- assert_equal :waiting, f.state
12
- f.resume
13
- assert_equal :dead, f.state
14
-
15
- f = Fiber.new { }
16
- f.schedule
17
- assert_equal :runnable, f.state
18
- snooze
19
- assert_equal :dead, f.state
20
- end
21
-
22
- def test_schedule
23
- values = []
24
- fibers = 3.times.map { |i| Fiber.new { values << i } }
25
- fibers[0].schedule
26
-
27
- assert_equal [], values
28
- snooze
29
- assert_equal [0], values
30
-
31
- fibers[1].schedule
32
- fibers[2].schedule
33
-
34
- assert_equal [0], values
35
- snooze
36
- assert_equal [0, 1, 2], values
37
- end
38
-
39
- def test_that_run_loop_returns_immediately_if_no_watchers
40
- t0 = Time.now
41
- suspend
42
- t1 = Time.now
43
- assert((t1 - t0) < 0.01)
44
- end
45
-
46
- def test_defer
47
- values = []
48
- defer { values << 1 }
49
- defer { values << 2 }
50
- defer { values << 3 }
51
- suspend
52
-
53
- assert_equal [1, 2, 3], values
54
- end
55
-
56
- def test_schedule
57
- values = []
58
- f = Fiber.new do
59
- values << :foo
60
- # We *have* to suspend the fiber in order to yield to the reactor,
61
- # otherwise control will transfer back to root fiber.
62
- suspend
63
- end
64
- assert_equal [], values
65
- f.schedule
66
- suspend
67
-
68
- assert_equal [:foo], values
69
- end
70
-
71
- def test_suspend
72
- values = []
73
- Fiber.new do
74
- values << :foo
75
- suspend
76
- end.schedule
77
- suspend
78
-
79
- assert_equal [:foo], values
80
- end
81
-
82
- def test_schedule_and_suspend
83
- values = []
84
- 3.times.map do |i|
85
- Fiber.new do
86
- values << i
87
- suspend
88
- end.schedule
89
- end
90
- suspend
91
-
92
- assert_equal [0, 1, 2], values
93
- end
94
-
95
- def test_snooze
96
- values = []
97
- 3.times.map do |i|
98
- Fiber.new do
99
- 3.times do
100
- snooze
101
- values << i
102
- end
103
- suspend
104
- end.schedule
105
- end
106
- suspend
107
-
108
- assert_equal [0, 1, 2, 0, 1, 2, 0, 1, 2], values
109
- end
110
-
111
6
  def test_break
112
7
  skip "break is still not implemented for new scheduler"
113
8
  values = []
114
- Fiber.new do
9
+ Fiber.spin do
115
10
  values << :foo
116
11
  snooze
117
12
  # here will never be reached
118
13
  values << :bar
119
14
  suspend
120
- end.schedule
15
+ end
121
16
 
122
- Fiber.new do
17
+ Fiber.spin do
123
18
  Gyro.break!
124
- end.schedule
19
+ end
125
20
 
126
21
  suspend
127
22
 
128
23
  assert_equal [:foo], values
129
24
  end
130
-
131
- def test_reset
132
- values = []
133
- f1 = Fiber.new do
134
- values << :foo
135
- snooze
136
- values << :bar
137
- suspend
138
- end.schedule
139
-
140
- f2 = Fiber.new do
141
- Thread.current.reset_fiber_scheduling
142
- values << :restarted
143
- snooze
144
- values << :baz
145
- end.schedule
146
-
147
- suspend
148
-
149
- f1.schedule
150
- suspend
151
- assert_equal %i[foo restarted baz], values
152
- end
153
-
154
- def test_restart
155
- values = []
156
- Fiber.new do
157
- values << :foo
158
- snooze
159
- # this part will not be reached, as Gyro state is reset
160
- values << :bar
161
- suspend
162
- end.schedule
163
-
164
- Fiber.new do
165
- Thread.current.reset_fiber_scheduling
166
-
167
- # control is transfer to the fiber that called Gyro.restart
168
- values << :restarted
169
- snooze
170
- values << :baz
171
- end.schedule
172
-
173
- suspend
174
-
175
- assert_equal %i[foo restarted baz], values
176
- end
177
25
  end
data/test/test_io.rb CHANGED
@@ -16,7 +16,7 @@ class GyroIOTest < MiniTest::Test
16
16
  }
17
17
  snooze
18
18
  sequence << 3
19
- defer do
19
+ spin do
20
20
  o << 'hello'
21
21
  sequence << 4
22
22
  end
@@ -106,7 +106,7 @@ class SupervisorTest < MiniTest::Test
106
106
  buffer = []
107
107
  supervisor = nil
108
108
  supervisor = Polyphony::Supervisor.new
109
- defer { supervisor.interrupt(42) }
109
+ spin { supervisor.interrupt(42) }
110
110
  buffer << supervisor.await { |s|
111
111
  (1..3).each { |i|
112
112
  s.spin {
@@ -125,7 +125,7 @@ class SupervisorTest < MiniTest::Test
125
125
  buffer = []
126
126
  supervisor = nil
127
127
  supervisor = Polyphony::Supervisor.new
128
- defer { supervisor.interrupt(42) }
128
+ spin { supervisor.interrupt(42) }
129
129
  buffer << supervisor.select { |s|
130
130
  (1..3).each { |i|
131
131
  s.spin {
data/test/test_thread.rb CHANGED
@@ -5,25 +5,35 @@ require_relative 'helper'
5
5
  class ThreadTest < MiniTest::Test
6
6
  def test_thread_spin
7
7
  buffer = []
8
- spin { (1..3).each { |i| snooze; buffer << i } }
8
+ f = spin { (1..3).each { |i| snooze; buffer << i } }
9
9
  t = Thread.new do
10
10
  s1 = spin { (11..13).each { |i| snooze; buffer << i } }
11
11
  s2 = spin { (21..23).each { |i| snooze; buffer << i } }
12
12
  Fiber.join(s1, s2)
13
13
  end
14
+ f.join
14
15
  t.join
15
16
 
16
17
  assert_equal [1, 2, 3, 11, 12, 13, 21, 22, 23], buffer.sort
17
18
  end
18
19
 
19
20
  def test_thread_join
21
+ tr = nil
22
+ # tr = Polyphony::Trace.new(:fiber_all) { |r| p r[:event] }
23
+ # Gyro.trace(true)
24
+ # tr.enable
25
+
20
26
  buffer = []
21
27
  spin { (1..3).each { |i| snooze; buffer << i } }
22
28
  t = Thread.new { sleep 0.01; buffer << 4 }
29
+
23
30
  r = t.join
24
31
 
25
32
  assert_equal [1, 2, 3, 4], buffer
26
33
  assert_equal t, r
34
+ ensure
35
+ tr&.disable
36
+ Gyro.trace(nil)
27
37
  end
28
38
 
29
39
  def test_thread_join_with_timeout
@@ -53,4 +63,65 @@ class ThreadTest < MiniTest::Test
53
63
  )
54
64
  assert_equal str, t.inspect
55
65
  end
66
+
67
+ def test_that_suspend_returns_immediately_if_no_watchers
68
+ records = []
69
+ t = Polyphony::Trace.new(:fiber_all) { |r| records << r if r[:event] =~ /^fiber_/ }
70
+ t.enable
71
+ Gyro.trace(true)
72
+
73
+ suspend
74
+ t.disable
75
+ assert_equal [:fiber_switchpoint], records.map { |r| r[:event] }
76
+ ensure
77
+ t&.disable
78
+ Gyro.trace(false)
79
+ end
80
+
81
+ def test_reset
82
+ values = []
83
+ f1 = spin do
84
+ values << :foo
85
+ snooze
86
+ values << :bar
87
+ suspend
88
+ end
89
+
90
+ f2 = spin do
91
+ Thread.current.reset_fiber_scheduling
92
+ values << :restarted
93
+ snooze
94
+ values << :baz
95
+ end
96
+
97
+ suspend
98
+
99
+ f1.schedule
100
+ suspend
101
+ assert_equal %i[foo restarted baz], values
102
+ end
103
+
104
+ def test_restart
105
+ values = []
106
+ spin do
107
+ values << :foo
108
+ snooze
109
+ # this part will not be reached, as Gyro state is reset
110
+ values << :bar
111
+ suspend
112
+ end
113
+
114
+ spin do
115
+ Thread.current.reset_fiber_scheduling
116
+
117
+ # control is transfer to the fiber that called Gyro.restart
118
+ values << :restarted
119
+ snooze
120
+ values << :baz
121
+ end
122
+
123
+ suspend
124
+
125
+ assert_equal %i[foo restarted baz], values
126
+ end
56
127
  end
@@ -5,10 +5,11 @@ require_relative 'helper'
5
5
  class ThreadPoolTest < MiniTest::Test
6
6
  def setup
7
7
  super
8
- @pool = Polyphony::ThreadPool.new
8
+ # @pool = Polyphony::ThreadPool.new
9
9
  end
10
10
 
11
11
  def test_process
12
+ skip
12
13
  current_thread = Thread.current
13
14
 
14
15
  processing_thread = nil
@@ -21,6 +22,7 @@ class ThreadPoolTest < MiniTest::Test
21
22
  end
22
23
 
23
24
  def test_multi_process
25
+ skip
24
26
  current_thread = Thread.current
25
27
  threads = []
26
28
  results = []
@@ -42,6 +44,7 @@ class ThreadPoolTest < MiniTest::Test
42
44
  end
43
45
 
44
46
  def test_process_with_exception
47
+ skip
45
48
  result = nil
46
49
  begin
47
50
  result = @pool.process { raise 'foo' }
@@ -53,6 +56,7 @@ class ThreadPoolTest < MiniTest::Test
53
56
  end
54
57
 
55
58
  def test_cast
59
+ skip
56
60
  t0 = Time.now
57
61
  threads = []
58
62
  buffer = []
@@ -68,7 +72,7 @@ class ThreadPoolTest < MiniTest::Test
68
72
  assert elapsed < 0.005
69
73
  assert buffer.size < 2
70
74
 
71
- sleep 0.04
75
+ sleep 0.05
72
76
  assert_equal @pool.size, threads.uniq.size
73
77
  assert_equal (0..9).to_a, buffer.sort
74
78
  end