File: trace.rb

package info (click to toggle)
ruby-rspec-memory 1.0.4-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 108 kB
  • sloc: ruby: 171; makefile: 4
file content (124 lines) | stat: -rw-r--r-- 2,701 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2019-2023, by Samuel Williams.
# Copyright, 2021, by Daniel Leidert.

require 'objspace'

module RSpec
	module Memory
		Allocation = Struct.new(:count, :size) do
			SLOT_SIZE = ObjectSpace.memsize_of(Object.new)
			
			def << object
				self.count += 1
				
				# We don't want to force specs to take the slot size into account.
				self.size += ObjectSpace.memsize_of(object) - SLOT_SIZE
			end
			
			def self.default_hash
				Hash.new{|h,k| h[k] = Allocation.new(0, 0)}
			end
		end
		
		class Trace
			def self.supported?
				# There are issues on truffleruby-1.0.0rc9
				return false if RUBY_ENGINE == "truffleruby"
				
				ObjectSpace.respond_to?(:trace_object_allocations)
			end
			
			if supported?
				def self.capture(*args, &block)
					self.new(*args).tap do |trace|
						trace.capture(&block)
					end
				end
			else
				def self.capture(*args, &block)
					yield
					
					return nil
				end
			end
			
			def initialize(klasses)
				@klasses = klasses
				
				@allocated = Allocation.default_hash
				@retained = Allocation.default_hash
				
				@ignored = Allocation.default_hash
				
				@total = Allocation.new(0, 0)
			end
			
			attr :allocated
			attr :retained
			
			attr :ignored
			
			attr :total
			
			def current_objects(generation)
				allocations = []
				
				ObjectSpace.each_object do |object|
					if ObjectSpace.allocation_generation(object) == generation
						allocations << object
					end
				end
				
				return allocations
			end
			
			def find_base(object)
				@klasses.find{|klass| object.is_a? klass}
			end
			
			def capture(&block)
				GC.start
				
				begin
					GC.disable
					
					generation = GC.count
					ObjectSpace.trace_object_allocations(&block)
					
					allocated = current_objects(generation)
				ensure
					GC.enable
				end
				
				GC.start
				retained = current_objects(generation)
				
				# All allocated objects, including those freed in the last GC:
				allocated.each do |object|
					if klass = find_base(object)
						@allocated[klass] << object
					else
						# If the user specified classes, but we can't pin this allocation to a specific class, we issue a warning.
						if @klasses.any?
							warn "Ignoring allocation of #{object.class} at #{ObjectSpace.allocation_sourcefile(object)}:#{ObjectSpace.allocation_sourceline(object)}"
						end
						
						@ignored[object.class] << object
					end
					
					@total << object
				end
				
				# Retained objects are still alive after a final GC:
				retained.each do |object|
					if klass = find_base(object)
						@retained[klass] << object
					end
				end
			end
		end
	end
end