rbsim 0.0.3
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.
- checksums.yaml +7 -0
- data/.hgignore +6 -0
- data/.rspec +2 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +66 -0
- data/LICENSE.txt +674 -0
- data/README.md +960 -0
- data/TODO +28 -0
- data/basic_sim.rb +62 -0
- data/fast-tcpn.rb +3 -0
- data/lib/rbsim.rb +14 -0
- data/lib/rbsim/dsl.rb +30 -0
- data/lib/rbsim/dsl/infrastructure.rb +48 -0
- data/lib/rbsim/dsl/mapping.rb +32 -0
- data/lib/rbsim/dsl/process.rb +129 -0
- data/lib/rbsim/dsl/program.rb +10 -0
- data/lib/rbsim/experiment.rb +110 -0
- data/lib/rbsim/hlmodel.rb +25 -0
- data/lib/rbsim/hlmodel/infrastructure.rb +116 -0
- data/lib/rbsim/hlmodel/mapping.rb +5 -0
- data/lib/rbsim/hlmodel/process.rb +152 -0
- data/lib/rbsim/numeric_units.rb +107 -0
- data/lib/rbsim/simulator.rb +184 -0
- data/lib/rbsim/statistics.rb +77 -0
- data/lib/rbsim/tokens.rb +146 -0
- data/lib/rbsim/version.rb +3 -0
- data/new_process.rb +49 -0
- data/rbsim.gemspec +42 -0
- data/show_readme.rb +15 -0
- data/sim.rb +142 -0
- data/sim_bamboo.rb +251 -0
- data/sim_process.rb +83 -0
- data/sim_process_dsl.rb +58 -0
- data/spec/dsl/infrastructure_nets_spec.rb +39 -0
- data/spec/dsl/infrastructure_nodes_spec.rb +72 -0
- data/spec/dsl/infrastructure_routes_spec.rb +44 -0
- data/spec/dsl/mapping_spec.rb +70 -0
- data/spec/dsl/process_spec.rb +56 -0
- data/spec/dsl/program_spec.rb +36 -0
- data/spec/dsl_and_hlmodel/new_process_spec.rb +235 -0
- data/spec/hlmodel/net_spec.rb +112 -0
- data/spec/hlmodel/process_spec.rb +242 -0
- data/spec/hlmodel/route_spec.rb +47 -0
- data/spec/hlmodel/routes_spec.rb +44 -0
- data/spec/integration/basic_simulation_spec.rb +104 -0
- data/spec/integration/net_spec.rb +44 -0
- data/spec/integration/process_spec.rb +117 -0
- data/spec/integration/rbsim_spec.rb +40 -0
- data/spec/simulator/logger_spec.rb +35 -0
- data/spec/simulator/stats_spec.rb +93 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/statistics_spec.rb +300 -0
- data/spec/tcpn/add_route_spec.rb +55 -0
- data/spec/tcpn/cpu_spec.rb +53 -0
- data/spec/tcpn/map_data_spec.rb +37 -0
- data/spec/tcpn/network_spec.rb +163 -0
- data/spec/tcpn/register_event_spec.rb +48 -0
- data/spec/tcpn/route_to_self_spec.rb +53 -0
- data/spec/tcpn/stats_spec.rb +77 -0
- data/spec/tokens/data_queue_obsolete.rb +121 -0
- data/spec/tokens/data_queue_spec.rb +111 -0
- data/spec/units_spec.rb +48 -0
- data/tcpn/model.rb +6 -0
- data/tcpn/model/add_route.rb +78 -0
- data/tcpn/model/application.rb +250 -0
- data/tcpn/model/cpu.rb +75 -0
- data/tcpn/model/map_data.rb +42 -0
- data/tcpn/model/network.rb +108 -0
- data/tcpn/model/register_event.rb +89 -0
- data/tcpn/model/stats.rb +46 -0
- metadata +221 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
module RBSim
|
2
|
+
class Statistics
|
3
|
+
UnknownStatsType = Class.new RuntimeError
|
4
|
+
attr_accessor :clock
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@counter_events = {}
|
8
|
+
@duration_events = {}
|
9
|
+
@saved_values = {}
|
10
|
+
@clock = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def event(type, params, time)
|
14
|
+
if type == :stats
|
15
|
+
@counter_events[params] ||= []
|
16
|
+
@counter_events[params] << time
|
17
|
+
elsif type == :save
|
18
|
+
tags = params[:tags]
|
19
|
+
value = params[:value]
|
20
|
+
@saved_values[tags] ||= {}
|
21
|
+
@saved_values[tags][time] ||= []
|
22
|
+
@saved_values[tags][time] << value
|
23
|
+
else
|
24
|
+
raise UnknownStatsType.new(type) unless [:start, :stop].include? type
|
25
|
+
@duration_events[params] ||= {}
|
26
|
+
@duration_events[params][type] ||= []
|
27
|
+
@duration_events[params][type] << time
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def durations(filters = {})
|
32
|
+
return enum_for(:durations, filters) unless block_given?
|
33
|
+
data = @duration_events.select &events_filter(filters)
|
34
|
+
data.each do |tags, times|
|
35
|
+
starts_and_stops = times[:start].zip times[:stop]
|
36
|
+
starts_and_stops.each do |start, stop|
|
37
|
+
yield tags, start, stop
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def counters(filters = {})
|
43
|
+
return enum_for(:counters, filters) unless block_given?
|
44
|
+
data = @counter_events.select &events_filter(filters)
|
45
|
+
data.each do |tags, events|
|
46
|
+
yield tags, events
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def values(filters = {})
|
51
|
+
return enum_for(:values, filters) unless block_given?
|
52
|
+
data = @saved_values.select &events_filter(filters)
|
53
|
+
data.each do |tags, times_and_values|
|
54
|
+
times_and_values.each do |time, values|
|
55
|
+
yield tags, time, values
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# generic event filter for all statistics
|
63
|
+
def events_filter(filters)
|
64
|
+
lambda do |tags, event_list|
|
65
|
+
filters.reduce(true) do |acc, filter_item|
|
66
|
+
filter_key, filter_value = filter_item;
|
67
|
+
if filter_value.is_a? Regexp
|
68
|
+
acc && tags.has_key?(filter_key) && tags[filter_key].to_s =~ filter_value
|
69
|
+
else
|
70
|
+
acc && tags[filter_key] == filter_value
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
data/lib/rbsim/tokens.rb
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'rbsim/hlmodel'
|
2
|
+
|
3
|
+
module RBSim
|
4
|
+
module Tokens
|
5
|
+
|
6
|
+
class ProcessToken < HLModel::Process
|
7
|
+
#include TCPN::TokenMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
class RoutesToken < HLModel::Routes
|
11
|
+
#include TCPN::TokenMethods
|
12
|
+
end
|
13
|
+
|
14
|
+
class NetToken < HLModel::Net
|
15
|
+
#include TCPN::TokenMethods
|
16
|
+
end
|
17
|
+
|
18
|
+
class CPUToken < HLModel::Node::CPU
|
19
|
+
#include TCPN::TokenMethods
|
20
|
+
end
|
21
|
+
|
22
|
+
class Data
|
23
|
+
IncompleteDataDefinition = Class.new RuntimeError
|
24
|
+
attr_reader :data_id # id of data required to collect fragments
|
25
|
+
attr_reader :src, :dst, :src_node, :size, :type, :content, :id
|
26
|
+
attr_accessor :dst_node, :route
|
27
|
+
attr_accessor :fragments # no. of fragments this data was between
|
28
|
+
|
29
|
+
def initialize(data_id, node, process, opts)
|
30
|
+
@data_id = data_id
|
31
|
+
@src_node = node
|
32
|
+
@src = process
|
33
|
+
@dst = opts[:to]
|
34
|
+
[ :size, :type, :content].each do |a|
|
35
|
+
self.instance_variable_set "@#{a}".to_sym, opts[a]
|
36
|
+
end
|
37
|
+
if @size.nil?
|
38
|
+
raise IncompleteDataDefinition.new("Must define size of data package!");
|
39
|
+
end
|
40
|
+
if @dst.nil?
|
41
|
+
raise IncompleteDataDefinition.new("Must define destination of data package!");
|
42
|
+
end
|
43
|
+
@id = self.object_id
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_s
|
47
|
+
v = [:src, :dst, :src_node, :size, :type, :content].map do |k|
|
48
|
+
"#{k}: #{self.send(k).inspect}"
|
49
|
+
end.join ', '
|
50
|
+
"{#{v}}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def ==(o)
|
54
|
+
return false unless o.kind_of? Data
|
55
|
+
o.id == id
|
56
|
+
end
|
57
|
+
|
58
|
+
def has_next_net?
|
59
|
+
@route.has_next_net?
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
class DataToken < Data
|
65
|
+
#include TCPN::TokenMethods
|
66
|
+
end
|
67
|
+
|
68
|
+
class DataQueue
|
69
|
+
class CannotEnqueueDataError < RuntimeError
|
70
|
+
def initialize(data, message)
|
71
|
+
super message
|
72
|
+
@data = data
|
73
|
+
end
|
74
|
+
|
75
|
+
def to_s
|
76
|
+
super + " " + @data.inspect
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
attr_reader :process_name, :process_tags
|
81
|
+
def initialize(process_name, process_tags = {})
|
82
|
+
@process_name = process_name
|
83
|
+
@process_tags = process_tags
|
84
|
+
@queue = []
|
85
|
+
@incomplete_data = Hash.new { { fragments: 0, data: nil } }
|
86
|
+
end
|
87
|
+
|
88
|
+
def put(o)
|
89
|
+
if o.fragments.nil?
|
90
|
+
raise CannotEnqueueDataError.new(o, "Tried to enqueue data without fragment count set!")
|
91
|
+
end
|
92
|
+
enqueue_fragment(o)
|
93
|
+
check_if_complete(o)
|
94
|
+
end
|
95
|
+
|
96
|
+
def get
|
97
|
+
@queue.shift
|
98
|
+
end
|
99
|
+
|
100
|
+
def length
|
101
|
+
@queue.length
|
102
|
+
end
|
103
|
+
|
104
|
+
def empty?
|
105
|
+
length == 0
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def enqueue_fragment(o)
|
111
|
+
already_received = @incomplete_data[o.data_id]
|
112
|
+
already_received[:fragments] += 1
|
113
|
+
already_received[:data] ||= o
|
114
|
+
@incomplete_data[o.data_id] = already_received
|
115
|
+
end
|
116
|
+
|
117
|
+
def check_if_complete(o)
|
118
|
+
already_received = @incomplete_data[o.data_id]
|
119
|
+
if already_received[:fragments] == o.fragments
|
120
|
+
@incomplete_data.delete o.data_id
|
121
|
+
@queue << already_received[:data]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
class DataQueueToken < DataQueue
|
128
|
+
#include TCPN::TokenMethods
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
class Event
|
133
|
+
attr_reader :process_id, :name, :args
|
134
|
+
def initialize(process_id, name, args)
|
135
|
+
@process_id = process_id
|
136
|
+
@name = name
|
137
|
+
@args = args
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class EventToken < Event
|
142
|
+
#include TCPN::TokenMethods
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
end
|
data/new_process.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rbsim'
|
2
|
+
|
3
|
+
# new_process statement example usage
|
4
|
+
# use this to create specs!
|
5
|
+
# This will be a kind of integration specs for dsl and hlmodel:
|
6
|
+
# Test behavior of hlmodel defined by the DSL statements
|
7
|
+
|
8
|
+
model = RBSim.dsl do
|
9
|
+
new_process :worker do
|
10
|
+
on_event :data do |volume|
|
11
|
+
delay_for 100
|
12
|
+
cpu do |c|
|
13
|
+
12/c.performance
|
14
|
+
end
|
15
|
+
end
|
16
|
+
delay_for 100
|
17
|
+
cpu do |cpu|
|
18
|
+
100/cpu.performance
|
19
|
+
end
|
20
|
+
register_event :data, 1000
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
puts "Model: "
|
25
|
+
p model
|
26
|
+
|
27
|
+
puts "="*60
|
28
|
+
puts "Serving process events"
|
29
|
+
p = model.processes[:worker]
|
30
|
+
p.node = :node01
|
31
|
+
|
32
|
+
puts "-"*60
|
33
|
+
e = p.serve_system_event :delay_for
|
34
|
+
puts "delay_for: #{e.inspect}"
|
35
|
+
|
36
|
+
|
37
|
+
puts "-"*60
|
38
|
+
cpu = Object.new
|
39
|
+
def cpu.performance
|
40
|
+
20
|
41
|
+
end
|
42
|
+
e = p.serve_system_event :cpu
|
43
|
+
puts "cpu: #{e.inspect} computed CPU delay: #{e[:args][:block].call cpu}"
|
44
|
+
|
45
|
+
puts "-"*60
|
46
|
+
puts "Process after serving user event :data (note event_queue modified by this event)"
|
47
|
+
p.serve_user_event # returns process object (self)!
|
48
|
+
puts p.inspect
|
49
|
+
|
data/rbsim.gemspec
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# Used this istruction to create the gem:
|
2
|
+
# http://guides.rubygems.org/make-your-own-gem/
|
3
|
+
# some things below were based on docile gem.
|
4
|
+
|
5
|
+
$:.push File.expand_path('../lib', __FILE__)
|
6
|
+
require 'rbsim/version'
|
7
|
+
|
8
|
+
Gem::Specification.new do |s|
|
9
|
+
s.name = 'rbsim'
|
10
|
+
s.version = RBSim::VERSION
|
11
|
+
s.authors = ['Wojciech Rząsa']
|
12
|
+
s.email = %w(me@wojciechrzasa.pl)
|
13
|
+
s.homepage = 'https://github.com/wrzasa/rbsim'
|
14
|
+
s.summary = 'Distributed inftastructure and system simulator with convenient DSL.'
|
15
|
+
s.description = 'You can model your distributed infrastructora and application and simulate its behavior and observe its efficiency easily.'
|
16
|
+
s.license = 'GPL-3.0'
|
17
|
+
|
18
|
+
s.platform = 'ruby'
|
19
|
+
s.required_ruby_version = '~> 2.0'
|
20
|
+
|
21
|
+
# s.rubyforge_project = ''
|
22
|
+
|
23
|
+
s.files = `git ls-files -z`.split("\x0")
|
24
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
25
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
26
|
+
s.require_paths = %w(lib)
|
27
|
+
|
28
|
+
s.add_runtime_dependency 'docile', '~> 1.1'
|
29
|
+
s.add_runtime_dependency 'fast-tcpn', '~> 0'
|
30
|
+
|
31
|
+
# Running rspec tests from rake
|
32
|
+
s.add_development_dependency 'rspec', '~> 3.1'
|
33
|
+
s.add_development_dependency 'rspec-its', '~> 1.0'
|
34
|
+
s.add_development_dependency 'simplecov', '~> 0'
|
35
|
+
|
36
|
+
s.extra_rdoc_files << 'README.md'
|
37
|
+
s.rdoc_options << '--main' << 'README.md'
|
38
|
+
s.rdoc_options << '--title' << 'RBSim -- Distributed system modeling and simulation tool'
|
39
|
+
s.rdoc_options << '--line-numbers'
|
40
|
+
s.rdoc_options << '-A'
|
41
|
+
s.rdoc_options << '-x coverage'
|
42
|
+
end
|
data/show_readme.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
if RUBY_ENGINE == 'jruby'
|
2
|
+
puts "Unfortunatelly as far as I know markdown does not work in JRuby, so this script won't work too."
|
3
|
+
exit
|
4
|
+
end
|
5
|
+
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'sinatra'
|
9
|
+
|
10
|
+
get '/' do
|
11
|
+
markdown File.read('README.md')
|
12
|
+
end
|
13
|
+
rescue LoadError
|
14
|
+
puts "This script requires sinatra gem. You have to do:\ngem install sinatra"
|
15
|
+
end
|
data/sim.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'rbsim'
|
2
|
+
|
3
|
+
model = RBSim.model do
|
4
|
+
|
5
|
+
program :wget do |opts|
|
6
|
+
sent = 0
|
7
|
+
on_event :send do
|
8
|
+
cpu do |cpu|
|
9
|
+
(150 / cpu.performance).miliseconds
|
10
|
+
end
|
11
|
+
send_data to: opts[:target], size: 1024.bytes, type: :request, content: sent
|
12
|
+
log "Sent data in process #{process.name} #{sent}"
|
13
|
+
sent += 1
|
14
|
+
# delay_for 5.miliseconds
|
15
|
+
register_event :send, delay: 5.miliseconds if sent < opts[:count]
|
16
|
+
end
|
17
|
+
|
18
|
+
on_event :data_received do |data|
|
19
|
+
log "Got data #{data} in process #{process.name}"
|
20
|
+
stats :request_served, process.name
|
21
|
+
end
|
22
|
+
|
23
|
+
register_event :send
|
24
|
+
end
|
25
|
+
|
26
|
+
program :apache_static do
|
27
|
+
on_event :data_received do |data|
|
28
|
+
log "Got #{data.type} from: #{data.src}, size: #{data.size}, content: #{data.content}"
|
29
|
+
cpu do |cpu|
|
30
|
+
(100 * data.size.in_bytes / cpu.performance).miliseconds
|
31
|
+
end
|
32
|
+
send_data to: data.src, size: data.size * 10, type: :response, content: data.content
|
33
|
+
log "Responded to: #{data.src} with content: #{data.content}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
program :apache_php do |name|
|
38
|
+
on_event :data_received do |data|
|
39
|
+
stats_start :working, name
|
40
|
+
log "#{name} start #{data.type} from #{data.src} #{data.content}"
|
41
|
+
if data.type == :request
|
42
|
+
cpu do |cpu|
|
43
|
+
(100 * data.size.in_bytes / cpu.performance).miliseconds
|
44
|
+
end
|
45
|
+
send_data to: :db, size: data.size/10, type: :sql, content: { client: data.src, content: data.content }
|
46
|
+
else
|
47
|
+
cpu do |cpu|
|
48
|
+
(500*data.size / cpu.performance).miliseconds
|
49
|
+
end
|
50
|
+
send_data to: data.content[:client], size: data.size*2, type: :response, content: data.content[:content]
|
51
|
+
end
|
52
|
+
log "#{name} finished #{data.type} from #{data.src} #{data.content}"
|
53
|
+
stats_stop :working, name
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
program :mysql do
|
58
|
+
on_event :data_received do |data|
|
59
|
+
stats_start :query, :mysql
|
60
|
+
log "DB start #{data.src} #{data.content}"
|
61
|
+
delay_for (data.size.in_bytes * rand).miliseconds
|
62
|
+
cpu do |cpu|
|
63
|
+
(data.size / 10 * rand).miliseconds
|
64
|
+
end
|
65
|
+
send_data to: data.src, size: data.size*1000, type: :db_response, content: data.content
|
66
|
+
log "DB finish #{data.src} #{data.content}"
|
67
|
+
stats_stop :query, :mysql
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
node :desktop do
|
72
|
+
cpu 100
|
73
|
+
end
|
74
|
+
|
75
|
+
node :laptop do
|
76
|
+
cpu 100
|
77
|
+
end
|
78
|
+
|
79
|
+
node :gandalf do
|
80
|
+
cpu 140000
|
81
|
+
cpu 140000
|
82
|
+
end
|
83
|
+
|
84
|
+
node :dbserver do
|
85
|
+
cpu 500
|
86
|
+
end
|
87
|
+
|
88
|
+
new_process :client1, program: :wget, args: { target: :server1, count: 10 }
|
89
|
+
new_process :client2, program: :wget, args: { target: :server2, count: 10 }
|
90
|
+
|
91
|
+
new_process :server1, program: :apache_php, args: 'apache1'
|
92
|
+
new_process :server2, program: :apache_php, args: 'apache2'
|
93
|
+
new_process :db, program: :mysql
|
94
|
+
|
95
|
+
net :net01, bw: 1024.bps
|
96
|
+
net :net02, bw: 768.bps
|
97
|
+
net :lan, bw: 10240.bps
|
98
|
+
|
99
|
+
route from: :desktop, to: :gandalf, via: [ :net01, :net02 ], twoway: true
|
100
|
+
route from: :laptop, to: :gandalf, via: [ :net01, :net02 ], twoway: true
|
101
|
+
route from: :gandalf, to: :dbserver, via: [ :lan ], twoway: true
|
102
|
+
|
103
|
+
put :server1, on: :gandalf
|
104
|
+
put :server2, on: :gandalf
|
105
|
+
put :db, on: :dbserver
|
106
|
+
|
107
|
+
put :client1, on: :desktop
|
108
|
+
put :client2, on: :laptop
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
# TODO: modify CPU load event handling, to enable random CPU load time!
|
113
|
+
# TODO: potrzebne gotowe narzędzie generujące raport obciążenia poszczególnych zasobów
|
114
|
+
|
115
|
+
# TODO: use this proof-of-concept to embedd logger into RBSim!
|
116
|
+
=begin
|
117
|
+
model.simulator.cb_for :transition, :after do |t, e|
|
118
|
+
# puts ">> #{e.clock} #{e.transition}"##{e.binding.map {|k, v| "#{k}: #{v}" }}" #if e.clock > 90000
|
119
|
+
if e.transition == 'event::log'
|
120
|
+
message = e.binding[:process][:val].serve_system_event(:log)[:args]
|
121
|
+
puts "#{e.clock} #{message}"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
=end
|
125
|
+
|
126
|
+
#model.simulator.cb_for :clock, :after do |t, e|
|
127
|
+
# puts e.clock
|
128
|
+
#end
|
129
|
+
|
130
|
+
=begin
|
131
|
+
model.logger do |clock, message|
|
132
|
+
puts "MY LOGGER: #{clock} #{message}"
|
133
|
+
end
|
134
|
+
=end
|
135
|
+
|
136
|
+
model.run
|
137
|
+
|
138
|
+
#p model.stats_summary
|
139
|
+
#p model.stats_data
|
140
|
+
|
141
|
+
model.stats_print
|
142
|
+
|