Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
L
libzmq
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Packages
Packages
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
submodule
libzmq
Commits
87ccbb9f
Commit
87ccbb9f
authored
Sep 07, 2009
by
Martin Sustrik
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' of git@github.com:sustrik/zeromq2
parents
67253f31
d62c7423
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
373 additions
and
171 deletions
+373
-171
local_lat.rb
perf/ruby/local_lat.rb
+14
-48
local_thr.rb
perf/ruby/local_thr.rb
+25
-59
remote_lat.rb
perf/ruby/remote_lat.rb
+27
-35
remote_thr.rb
perf/ruby/remote_thr.rb
+14
-28
Makefile.am
ruby/Makefile.am
+1
-1
rbzmq.cpp
ruby/rbzmq.cpp
+292
-0
zmq.cpp
ruby/zmq.cpp
+0
-0
No files found.
perf/ruby/local_lat.rb
View file @
87ccbb9f
...
@@ -18,58 +18,24 @@
...
@@ -18,58 +18,24 @@
require
'librbzmq'
require
'librbzmq'
class
AssertionFailure
<
StandardError
if
ARGV
.
length
!=
3
end
puts
"usage: local_lat <bind-to> <message-size> <roundtrip-count>"
def
assert
(
bool
,
message
=
'assertion failure'
)
raise
AssertionFailure
.
new
(
message
)
unless
bool
end
if
ARGV
.
length
!=
4
puts
"usage: local_lat <in-interface> <out-interface> <message-size>
<roundtrip-count>"
Process
.
exit
Process
.
exit
end
end
in_interface
=
ARGV
[
0
]
out_interface
=
ARGV
[
1
]
message_size
=
ARGV
[
2
]
roundtrip_count
=
ARGV
[
3
]
# Print out the test parameters.
puts
"message size:
#{
message_size
}
[B]"
puts
"roundtrip count:
#{
roundtrip_count
}
"
# Create 0MQ transport.
rb_zmq
=
Zmq
.
new
()
# Create the wiring.
context
=
rb_zmq
.
context
(
1
,
1
)
in_socket
=
rb_zmq
.
socket
(
context
,
ZMQ_SUB
)
out_socket
=
rb_zmq
.
socket
(
context
,
ZMQ_PUB
)
# Bind.
rb_zmq
.
bind
(
in_socket
,
in_interface
.
to_s
)
rb_zmq
.
bind
(
out_socket
,
out_interface
.
to_s
)
# Create message data to send.
bind_to
=
ARGV
[
0
]
out_msg
=
rb_zmq
.
msg_init_size
(
message_size
.
to_i
)
message_size
=
ARGV
[
1
].
to_i
roundtrip_count
=
ARGV
[
2
].
to_i
# Get initial timestamp.
ctx
=
Context
.
new
(
1
,
1
)
start_time
=
Time
.
now
s
=
Socket
.
new
(
ctx
,
REP
);
s
.
bind
(
bind_to
);
# The message loop.
for
i
in
0
...
roundtrip_count
do
for
i
in
0
...
roundtrip_count
.
to_i
do
msg
=
s
.
recv
(
0
)
rb_zmq
.
send
(
out_socket
,
out_msg
,
ZMQ_NOBLOCK
)
s
.
send
(
msg
,
0
)
in_buf
=
rb_zmq
.
recv
(
in_socket
,
ZMQ_NOBLOCK
)
end
assert
(
rb_zmq
.
msg_size
(
in_buf
.
msg
)
==
message_size
.
to_i
)
end
# Get final timestamp.
sleep
1
end_time
=
Time
.
now
# Compute and print out the latency.
latency
=
(
end_time
.
to_f
-
start_time
.
to_f
)
*
1000000
/
roundtrip_count
.
to_i
/
2
puts
"Your average latency is "
+
"%0.2f"
%
latency
+
"[us]"
perf/ruby/local_thr.rb
View file @
87ccbb9f
...
@@ -17,74 +17,40 @@
...
@@ -17,74 +17,40 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# along with this program. If not, see <http://www.gnu.org/licenses/>.
require
'librbzmq'
require
'librbzmq'
class
Context
end
class
Socket
end
class
AssertionFailure
<
StandardError
end
def
assert
(
bool
,
message
=
'assertion failure'
)
raise
AssertionFailure
.
new
(
message
)
unless
bool
end
if
ARGV
.
length
!=
3
if
ARGV
.
length
!=
3
puts
"usage: local_thr <in-interface> <message-size>"
+
\
puts
"usage: local_thr <bind-to> <message-size> <message-count>"
" <message-count>"
Process
.
exit
Process
.
exit
end
end
in_interface
=
ARGV
[
0
]
message_size
=
ARGV
[
1
]
message_count
=
ARGV
[
2
]
# Print out the test parameters.
puts
"message size: "
+
message_size
.
to_s
+
" [B]"
puts
"message count: "
+
message_count
.
to_s
# Create 0MQ transport.
rb_zmq
=
Zmq
.
new
();
# Create context.
context
=
rb_zmq
.
context
(
1
,
1
);
# Create the socket.
bind_to
=
ARGV
[
0
]
in_socket
=
rb_zmq
.
socket
(
context
,
ZMQ_SUB
);
message_size
=
ARGV
[
1
].
to_i
message_count
=
ARGV
[
2
].
to_i
# Connect.
ctx
=
Context
.
new
(
1
,
1
)
rb_zmq
.
connect
(
in_socket
,
in_interface
.
to_s
);
s
=
Socket
.
new
(
ctx
,
SUB
);
s
.
bind
(
bind_to
);
# Receive first message
msg
=
s
.
recv
(
0
)
data
=
rb_zmq
.
recv
(
in_socket
,
ZMQ_NOBLOCK
);
assert
(
rb_zmq
.
msg_size
(
data
.
msg
)
==
message_size
.
to_i
)
# Get initial timestamp.
start_time
=
Time
.
now
start_time
=
Time
.
now
# The message loop.
for
i
in
1
...
message_count
.
to_i
do
for
i
in
0
...
message_count
.
to_i
-
1
do
msg
=
s
.
recv
(
0
)
data
=
rb_zmq
.
recv
(
in_socket
,
ZMQ_NOBLOCK
);
end
assert
(
rb_zmq
.
msg_size
(
data
.
msg
)
==
message_size
.
to_i
)
end
# Get terminal timestamp.
end_time
=
Time
.
now
end_time
=
Time
.
now
# Compute and print out the throughput.
elapsed
=
(
end_time
.
to_f
-
start_time
.
to_f
)
*
1000000
if
end_time
.
to_f
-
start_time
.
to_f
!=
0
if
elapsed
==
0
message_throughput
=
message_count
.
to_i
/
elapsed
=
1
(
end_time
.
to_f
-
start_time
.
to_f
);
end
else
message_throughput
=
message_count
.
to_i
end
megabit_throughput
=
message_throughput
.
to_f
*
message_size
.
to_i
*
8
/
throughput
=
message_count
*
1000000
/
elapsed
1000000
;
megabits
=
throughput
*
message_size
*
8
/
1000000
puts
"Your average throughput is "
+
"%0.2f"
%
message_throughput
.
to_s
+
" [msg/s]"
puts
"Your average throughput is "
+
"%0.2f"
%
megabit_throughput
.
to_s
+
" [Mb/s]"
puts
"message size: %i [B]"
%
message_size
puts
"message count: %i"
%
message_count
puts
"mean throughput: %i [msg/s]"
%
throughput
puts
"mean throughput: %.3f [Mb/s]"
%
megabits
perf/ruby/remote_lat.rb
View file @
87ccbb9f
...
@@ -18,44 +18,36 @@
...
@@ -18,44 +18,36 @@
require
'librbzmq'
require
'librbzmq'
class
AssertionFailure
<
StandardError
if
ARGV
.
length
!=
3
puts
"usage: remote_lat <connect-to> <message-size> <roundtrip-count>"
Process
.
exit
end
end
def
assert
(
bool
,
message
=
'assertion failure'
)
connect_to
=
ARGV
[
0
]
raise
AssertionFailure
.
new
(
message
)
unless
bool
message_size
=
ARGV
[
1
].
to_i
roundtrip_count
=
ARGV
[
2
].
to_i
ctx
=
Context
.
new
(
1
,
1
)
s
=
Socket
.
new
(
ctx
,
REQ
);
s
.
connect
(
connect_to
);
msg
=
"
#{
'0'
*
message_size
}
"
start_time
=
Time
.
now
for
i
in
0
...
roundtrip_count
do
s
.
send
(
msg
,
0
)
msg
=
s
.
recv
(
0
)
end
end
if
ARGV
.
length
!=
4
end_time
=
Time
.
now
puts
"usage: remote_lat <in-interface> <out-interface>"
+
\
" <message-size> <roundtrip-count>"
elapsed
=
(
end_time
.
to_f
-
start_time
.
to_f
)
*
1000000
Process
.
exit
latency
=
elapsed
/
roundtrip_count
/
2
end
puts
"message size: %i [B]"
%
message_size
in_interface
=
ARGV
[
0
]
puts
"roundtrip count: %i"
%
roundtrip_count
out_interface
=
ARGV
[
1
]
puts
"mean latency: %.3f [us]"
%
latency
message_size
=
ARGV
[
2
]
roundtrip_count
=
ARGV
[
3
]
# Create 0MQ transport.
rb_zmq
=
Zmq
.
new
()
# Create the wiring.
context
=
rb_zmq
.
context
(
1
,
1
)
in_socket
=
rb_zmq
.
socket
(
context
,
ZMQ_SUB
)
out_socket
=
rb_zmq
.
socket
(
context
,
ZMQ_PUB
)
# Connect.
rb_zmq
.
connect
(
in_socket
,
in_interface
.
to_s
)
rb_zmq
.
connect
(
out_socket
,
out_interface
.
to_s
)
# The message loop.
for
i
in
0
...
roundtrip_count
.
to_i
do
data
=
rb_zmq
.
recv
(
in_socket
,
ZMQ_NOBLOCK
)
assert
(
rb_zmq
.
msg_size
(
data
.
msg
)
==
message_size
.
to_i
)
rb_zmq
.
send
(
out_socket
,
data
.
msg
,
ZMQ_NOBLOCK
)
end
# Wait till all messages are sent.
sleep
2
perf/ruby/remote_thr.rb
View file @
87ccbb9f
...
@@ -18,38 +18,24 @@
...
@@ -18,38 +18,24 @@
require
'librbzmq'
require
'librbzmq'
class
AssertionFailure
<
StandardError
if
ARGV
.
length
!=
3
end
puts
"usage: remote_thr <connect-to> <message-size> <message-count>"
def
assert
(
bool
,
message
=
'assertion failure'
)
raise
AssertionFailure
.
new
(
message
)
unless
bool
end
if
ARGV
.
length
!=
3
puts
"usage: remote_thr <out-interface> <message-size> <message-count>"
Process
.
exit
Process
.
exit
end
end
out_interface
=
ARGV
[
0
]
message_size
=
ARGV
[
1
]
message_count
=
ARGV
[
2
]
# Create 0MQ transport.
connect_to
=
ARGV
[
0
]
rb_zmq
=
Zmq
.
new
();
message_size
=
ARGV
[
1
].
to_i
message_count
=
ARGV
[
2
].
to_i
# Create the wiring.
ctx
=
Context
.
new
(
1
,
1
)
context
=
rb_zmq
.
context
(
1
,
1
);
s
=
Socket
.
new
(
ctx
,
PUB
);
out_socket
=
rb_zmq
.
socket
(
context
,
ZMQ_PUB
);
s
.
connect
(
connect_to
);
rb_zmq
.
bind
(
out_socket
,
out_interface
.
to_s
);
# Create message data to send.
msg
=
"
#{
'0'
*
message_size
}
"
out_msg
=
rb_zmq
.
msg_init_size
(
message_size
.
to_s
);
# The message loop.
for
i
in
0
...
message_count
do
for
i
in
0
...
message_count
.
to_i
+
1
do
s
.
send
(
msg
,
0
)
rb_zmq
.
send
(
out_socket
,
out_msg
,
ZMQ_NOBLOCK
);
end
end
# Wait till all messages are sent.
sleep
10
sleep
2
ruby/Makefile.am
View file @
87ccbb9f
...
@@ -3,7 +3,7 @@ INCLUDES = -I$(top_builddir) -I$(top_srcdir)/include -I$(top_builddir)/include
...
@@ -3,7 +3,7 @@ INCLUDES = -I$(top_builddir) -I$(top_srcdir)/include -I$(top_builddir)/include
rblib_LTLIBRARIES
=
librbzmq.la
rblib_LTLIBRARIES
=
librbzmq.la
rblibdir
=
@RUBYDIR@
rblibdir
=
@RUBYDIR@
librbzmq_la_SOURCES
=
zmq.cpp
librbzmq_la_SOURCES
=
rb
zmq.cpp
librbzmq_la_LDFLAGS
=
-version-info
@RBLTVER@
librbzmq_la_LDFLAGS
=
-version-info
@RBLTVER@
librbzmq_la_CXXFLAGS
=
-Wall
-pedantic
-Werror
-Wno-long-long
librbzmq_la_CXXFLAGS
=
-Wall
-pedantic
-Werror
-Wno-long-long
...
...
ruby/rbzmq.cpp
0 → 100644
View file @
87ccbb9f
/*
Copyright (c) 2007-2009 FastMQ Inc.
This file is part of 0MQ.
0MQ is free software; you can redistribute it and/or modify it under
the terms of the Lesser GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
0MQ is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <assert.h>
#include <errno.h>
#include <string.h>
#include <zmq.h>
#include <ruby.h>
static
void
context_free
(
void
*
ctx
)
{
if
(
ctx
)
{
int
rc
=
zmq_term
(
ctx
);
assert
(
rc
==
0
);
}
}
static
VALUE
context_alloc
(
VALUE
class_
)
{
return
rb_data_object_alloc
(
class_
,
NULL
,
0
,
context_free
);
}
static
VALUE
context_initialize
(
VALUE
self_
,
VALUE
app_threads_
,
VALUE
io_threads_
)
{
assert
(
!
DATA_PTR
(
self_
));
void
*
ctx
=
zmq_init
(
NUM2INT
(
app_threads_
),
NUM2INT
(
io_threads_
));
if
(
!
ctx
)
{
rb_raise
(
rb_eRuntimeError
,
strerror
(
errno
));
return
Qnil
;
}
DATA_PTR
(
self_
)
=
(
void
*
)
ctx
;
return
self_
;
}
static
void
socket_free
(
void
*
s
)
{
if
(
s
)
{
int
rc
=
zmq_close
(
s
);
assert
(
rc
==
0
);
}
}
static
VALUE
socket_alloc
(
VALUE
class_
)
{
return
rb_data_object_alloc
(
class_
,
NULL
,
0
,
socket_free
);
}
static
VALUE
socket_initialize
(
VALUE
self_
,
VALUE
context_
,
VALUE
type_
)
{
assert
(
!
DATA_PTR
(
self_
));
if
(
strcmp
(
rb_obj_classname
(
context_
),
"Context"
)
!=
0
)
{
rb_raise
(
rb_eArgError
,
"expected Context object"
);
return
Qnil
;
}
void
*
s
=
zmq_socket
(
DATA_PTR
(
context_
),
NUM2INT
(
type_
));
if
(
!
s
)
{
rb_raise
(
rb_eRuntimeError
,
strerror
(
errno
));
return
Qnil
;
}
DATA_PTR
(
self_
)
=
(
void
*
)
s
;
return
self_
;
}
/*
static VALUE rb_setsockopt (VALUE self_, VALUE socket_, VALUE option_,
VALUE optval_)
{
// Get the socket.
void* socket;
Data_Get_Struct (socket_, void*, socket);
int rc = 0;
if (TYPE (optval_) == T_STRING) {
// Forward the code to native 0MQ library.
rc = zmq_setsockopt (socket, NUM2INT (option_),
(void *) StringValueCStr (optval_), RSTRING_LEN (optval_));
}
else if (TYPE (optval_) == T_FLOAT) {
double optval = NUM2DBL (optval_);
// Forward the code to native 0MQ library.
rc = zmq_setsockopt (socket, NUM2INT (option_),
(void*) &optval, 8);
}
else if (TYPE (optval_) == T_FIXNUM) {
long optval = FIX2LONG (optval_);
// Forward the code to native 0MQ library.
rc = zmq_setsockopt (socket, NUM2INT (option_),
(void *) &optval, 4);
}
else if (TYPE (optval_) == T_BIGNUM) {
long optval = NUM2LONG (optval_);
// Forward the code to native 0MQ library.
rc = zmq_setsockopt (socket, NUM2INT (option_),
(void *) &optval, 4);
}
else if (TYPE (optval_) == T_ARRAY) {
// Forward the code to native 0MQ library.
rc = zmq_setsockopt (socket, NUM2INT (option_),
(void *) RARRAY_PTR (optval_), RARRAY_LEN (optval_));
}
else if (TYPE (optval_) == T_STRUCT) {
// Forward the code to native 0MQ library.
rc = zmq_setsockopt (socket, NUM2INT (option_),
(void *) RSTRUCT_PTR (optval_), RSTRUCT_LEN (optval_));
}
else
rb_raise(rb_eRuntimeError, "Unknown type");
assert (rc == 0);
return self_;
}
*/
static
VALUE
socket_bind
(
VALUE
self_
,
VALUE
addr_
)
{
assert
(
DATA_PTR
(
self_
));
int
rc
=
zmq_bind
(
DATA_PTR
(
self_
),
rb_string_value_cstr
(
&
addr_
));
if
(
rc
!=
0
)
{
rb_raise
(
rb_eRuntimeError
,
strerror
(
errno
));
return
Qnil
;
}
return
Qnil
;
}
static
VALUE
socket_connect
(
VALUE
self_
,
VALUE
addr_
)
{
assert
(
DATA_PTR
(
self_
));
int
rc
=
zmq_connect
(
DATA_PTR
(
self_
),
rb_string_value_cstr
(
&
addr_
));
if
(
rc
!=
0
)
{
rb_raise
(
rb_eRuntimeError
,
strerror
(
errno
));
return
Qnil
;
}
return
Qnil
;
}
static
VALUE
socket_send
(
VALUE
self_
,
VALUE
msg_
,
VALUE
flags_
)
{
assert
(
DATA_PTR
(
self_
));
Check_Type
(
msg_
,
T_STRING
);
zmq_msg_t
msg
;
int
rc
=
zmq_msg_init_size
(
&
msg
,
RSTRING_LEN
(
msg_
));
if
(
rc
!=
0
)
{
rb_raise
(
rb_eRuntimeError
,
strerror
(
errno
));
return
Qnil
;
}
memcpy
(
zmq_msg_data
(
&
msg
),
RSTRING_PTR
(
msg_
),
RSTRING_LEN
(
msg_
));
rc
=
zmq_send
(
DATA_PTR
(
self_
),
&
msg
,
NUM2INT
(
flags_
));
if
(
rc
!=
0
&&
errno
==
EAGAIN
)
{
rc
=
zmq_msg_close
(
&
msg
);
assert
(
rc
==
0
);
return
Qfalse
;
}
if
(
rc
!=
0
)
{
rb_raise
(
rb_eRuntimeError
,
strerror
(
errno
));
rc
=
zmq_msg_close
(
&
msg
);
assert
(
rc
==
0
);
return
Qnil
;
}
rc
=
zmq_msg_close
(
&
msg
);
assert
(
rc
==
0
);
return
Qtrue
;
}
static
VALUE
socket_flush
(
VALUE
self_
)
{
assert
(
DATA_PTR
(
self_
));
int
rc
=
zmq_flush
(
DATA_PTR
(
self_
));
if
(
rc
!=
0
)
{
rb_raise
(
rb_eRuntimeError
,
strerror
(
errno
));
return
Qnil
;
}
return
Qnil
;
}
static
VALUE
socket_recv
(
VALUE
self_
,
VALUE
flags_
)
{
assert
(
DATA_PTR
(
self_
));
zmq_msg_t
msg
;
int
rc
=
zmq_msg_init
(
&
msg
);
assert
(
rc
==
0
);
rc
=
zmq_recv
(
DATA_PTR
(
self_
),
&
msg
,
NUM2INT
(
flags_
));
if
(
rc
!=
0
&&
errno
==
EAGAIN
)
{
rc
=
zmq_msg_close
(
&
msg
);
assert
(
rc
==
0
);
return
Qnil
;
}
if
(
rc
!=
0
)
{
rb_raise
(
rb_eRuntimeError
,
strerror
(
errno
));
rc
=
zmq_msg_close
(
&
msg
);
assert
(
rc
==
0
);
return
Qnil
;
}
VALUE
message
=
rb_str_new
((
char
*
)
zmq_msg_data
(
&
msg
),
zmq_msg_size
(
&
msg
));
rc
=
zmq_msg_close
(
&
msg
);
assert
(
rc
==
0
);
return
message
;
}
extern
"C"
void
Init_librbzmq
()
{
VALUE
context_type
=
rb_define_class
(
"Context"
,
rb_cObject
);
rb_define_alloc_func
(
context_type
,
context_alloc
);
rb_define_method
(
context_type
,
"initialize"
,
(
VALUE
(
*
)(...))
context_initialize
,
2
);
VALUE
socket_type
=
rb_define_class
(
"Socket"
,
rb_cObject
);
rb_define_alloc_func
(
socket_type
,
socket_alloc
);
rb_define_method
(
socket_type
,
"initialize"
,
(
VALUE
(
*
)(...))
socket_initialize
,
2
);
// rb_define_method (socket_type, "setsockopt",
// (VALUE(*)(...)) socket_setsockopt, 2);
rb_define_method
(
socket_type
,
"bind"
,
(
VALUE
(
*
)(...))
socket_bind
,
1
);
rb_define_method
(
socket_type
,
"connect"
,
(
VALUE
(
*
)(...))
socket_connect
,
1
);
rb_define_method
(
socket_type
,
"send"
,
(
VALUE
(
*
)(...))
socket_send
,
2
);
rb_define_method
(
socket_type
,
"flush"
,
(
VALUE
(
*
)(...))
socket_flush
,
0
);
rb_define_method
(
socket_type
,
"recv"
,
(
VALUE
(
*
)(...))
socket_recv
,
1
);
rb_define_global_const
(
"HWM"
,
INT2NUM
(
ZMQ_HWM
));
rb_define_global_const
(
"LWM"
,
INT2NUM
(
ZMQ_LWM
));
rb_define_global_const
(
"SWAP"
,
INT2NUM
(
ZMQ_SWAP
));
rb_define_global_const
(
"MASK"
,
INT2NUM
(
ZMQ_MASK
));
rb_define_global_const
(
"AFFINITY"
,
INT2NUM
(
ZMQ_AFFINITY
));
rb_define_global_const
(
"IDENTITY"
,
INT2NUM
(
ZMQ_IDENTITY
));
rb_define_global_const
(
"NOBLOCK"
,
INT2NUM
(
ZMQ_NOBLOCK
));
rb_define_global_const
(
"NOFLUSH"
,
INT2NUM
(
ZMQ_NOFLUSH
));
rb_define_global_const
(
"P2P"
,
INT2NUM
(
ZMQ_P2P
));
rb_define_global_const
(
"SUB"
,
INT2NUM
(
ZMQ_SUB
));
rb_define_global_const
(
"PUB"
,
INT2NUM
(
ZMQ_PUB
));
rb_define_global_const
(
"REQ"
,
INT2NUM
(
ZMQ_REQ
));
rb_define_global_const
(
"REP"
,
INT2NUM
(
ZMQ_REP
));
}
ruby/zmq.cpp
deleted
100644 → 0
View file @
67253f31
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment