functiontrace 0.3.10__tar.gz → 0.4.0__tar.gz
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.
- functiontrace-0.4.0/Cargo.toml +19 -0
- functiontrace-0.4.0/MANIFEST.in +3 -0
- {functiontrace-0.3.10/functiontrace.egg-info → functiontrace-0.4.0}/PKG-INFO +1 -1
- {functiontrace-0.3.10 → functiontrace-0.4.0}/_functiontrace.c +109 -15
- {functiontrace-0.3.10 → functiontrace-0.4.0/functiontrace.egg-info}/PKG-INFO +1 -1
- {functiontrace-0.3.10 → functiontrace-0.4.0}/functiontrace.egg-info/SOURCES.txt +3 -1
- {functiontrace-0.3.10 → functiontrace-0.4.0}/pyproject.toml +6 -2
- functiontrace-0.4.0/rust/lib.rs +100 -0
- functiontrace-0.3.10/MANIFEST.in +0 -1
- {functiontrace-0.3.10 → functiontrace-0.4.0}/README.rst +0 -0
- {functiontrace-0.3.10 → functiontrace-0.4.0}/functiontrace.egg-info/dependency_links.txt +0 -0
- {functiontrace-0.3.10 → functiontrace-0.4.0}/functiontrace.egg-info/entry_points.txt +0 -0
- {functiontrace-0.3.10 → functiontrace-0.4.0}/functiontrace.egg-info/top_level.txt +0 -0
- {functiontrace-0.3.10 → functiontrace-0.4.0}/functiontrace.py +0 -0
- {functiontrace-0.3.10 → functiontrace-0.4.0}/mpack/LICENSE +0 -0
- {functiontrace-0.3.10 → functiontrace-0.4.0}/mpack/mpack.c +0 -0
- {functiontrace-0.3.10 → functiontrace-0.4.0}/mpack/mpack.h +0 -0
- {functiontrace-0.3.10 → functiontrace-0.4.0}/setup.cfg +0 -0
- {functiontrace-0.3.10 → functiontrace-0.4.0}/setup.py +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "_functiontrace-rs"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
7
|
+
|
|
8
|
+
[dependencies]
|
|
9
|
+
pyo3 = "0.24"
|
|
10
|
+
|
|
11
|
+
[profile.release-lto]
|
|
12
|
+
inherits = "release"
|
|
13
|
+
lto = true
|
|
14
|
+
|
|
15
|
+
[lib]
|
|
16
|
+
# See https://github.com/PyO3/pyo3 for details
|
|
17
|
+
name = "_lib" # private module to be nested into Python package
|
|
18
|
+
crate-type = ["cdylib"]
|
|
19
|
+
path = "rust/lib.rs"
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
#include "mpack/mpack.h"
|
|
12
12
|
|
|
13
|
+
// We use the Rust implementation for all data transport.
|
|
14
|
+
#define RUST_SOCKETS
|
|
15
|
+
|
|
13
16
|
//#include <assert.h>
|
|
14
17
|
// By default, python c extensions compile with asserts disable and it is not
|
|
15
18
|
// super easy to disable this behavior (AFAIK). Use this custom assert for now.
|
|
@@ -56,7 +59,9 @@ typedef struct {
|
|
|
56
59
|
// Prototypes
|
|
57
60
|
////////////////////////////////////////////////////////////////////////////////
|
|
58
61
|
static Writer* Fprofile_Writer(void);
|
|
62
|
+
#if !defined(RUST_SOCKETS)
|
|
59
63
|
static void Mpack_Flush(Writer* writer, const char* buffer, size_t bytes);
|
|
64
|
+
#endif
|
|
60
65
|
|
|
61
66
|
static Timestamp Fprofile_Time(void);
|
|
62
67
|
|
|
@@ -117,6 +122,11 @@ static struct {
|
|
|
117
122
|
// The Unix socket address we're using to communicate with the profile
|
|
118
123
|
// server.
|
|
119
124
|
struct sockaddr_un socket;
|
|
125
|
+
|
|
126
|
+
#if defined(RUST_SOCKETS)
|
|
127
|
+
// The Rust extension.
|
|
128
|
+
PyObject* rust;
|
|
129
|
+
#endif
|
|
120
130
|
} moduleConfiguration;
|
|
121
131
|
|
|
122
132
|
// The set of methods exposed by this module to Python.
|
|
@@ -146,7 +156,7 @@ static PyMethodDef methods[] = {
|
|
|
146
156
|
.ml_flags = METH_VARARGS,
|
|
147
157
|
.ml_doc = "INTERNAL USE ONLY"
|
|
148
158
|
},
|
|
149
|
-
[METHOD_TERMINATE
|
|
159
|
+
[METHOD_TERMINATE] = {
|
|
150
160
|
.ml_name = "terminate",
|
|
151
161
|
.ml_meth = (PyCFunction) Fprofile_Terminate,
|
|
152
162
|
.ml_flags = METH_NOARGS,
|
|
@@ -181,6 +191,7 @@ static Writer* Fprofile_Writer(void) {
|
|
|
181
191
|
return &state->writer;
|
|
182
192
|
}
|
|
183
193
|
|
|
194
|
+
#if !defined(RUST_SOCKETS)
|
|
184
195
|
static void Mpack_Flush(Writer* writer, const char* buffer, size_t bytes) {
|
|
185
196
|
// The context passed to Mpack is the socket to send on.
|
|
186
197
|
intptr_t socket = (intptr_t) writer->context;
|
|
@@ -190,6 +201,7 @@ static void Mpack_Flush(Writer* writer, const char* buffer, size_t bytes) {
|
|
|
190
201
|
exit(-1);
|
|
191
202
|
}
|
|
192
203
|
}
|
|
204
|
+
#endif
|
|
193
205
|
|
|
194
206
|
////////////////////////////////////////////////////////////////////////////////
|
|
195
207
|
// Misc
|
|
@@ -732,6 +744,9 @@ static void Fprofile_ThreadTeardown(void* key) {
|
|
|
732
744
|
}
|
|
733
745
|
|
|
734
746
|
mpack_writer_flush_message(&state->writer);
|
|
747
|
+
|
|
748
|
+
// TODO: We should tear down the socket to avoid leaking O(# threads)
|
|
749
|
+
// resources.
|
|
735
750
|
}
|
|
736
751
|
|
|
737
752
|
static PyObject* Fprofile_MpexitHook(HookedFunction* hook, PyObject* args, PyObject* kwargs) {
|
|
@@ -836,10 +851,12 @@ static PyObject* Fprofile_StartTrace(PyObject* obj, PyObject* args, PyObject* kw
|
|
|
836
851
|
Timestamp time = Fprofile_Time();
|
|
837
852
|
char traceInitialization[1024] = { 0 };
|
|
838
853
|
Writer initWriter = { 0 };
|
|
839
|
-
intptr_t sock = -1;
|
|
840
854
|
const char* version = Py_GetVersion();
|
|
841
855
|
const char* platform = Py_GetPlatform();
|
|
842
856
|
char* outputDirectory = NULL;
|
|
857
|
+
#if !defined(RUST_SOCKETS)
|
|
858
|
+
intptr_t sock = -1;
|
|
859
|
+
#endif
|
|
843
860
|
|
|
844
861
|
// Parse the args to figure out what our output directory is.
|
|
845
862
|
if (!PyArg_ParseTuple(args, "s", &outputDirectory)) {
|
|
@@ -848,7 +865,22 @@ static PyObject* Fprofile_StartTrace(PyObject* obj, PyObject* args, PyObject* kw
|
|
|
848
865
|
exit(-1);
|
|
849
866
|
}
|
|
850
867
|
|
|
851
|
-
|
|
868
|
+
#if defined(RUST_SOCKETS)
|
|
869
|
+
// NOTE: We only import this module once. Importing while tracing is
|
|
870
|
+
// live will be recorded, which exposes internals users don't care
|
|
871
|
+
// about and breaks the invariant that we send a `RegisterThread`
|
|
872
|
+
// message first when threads are traced.
|
|
873
|
+
moduleConfiguration.rust = PyImport_ImportModule("_functiontrace_rs._lib");
|
|
874
|
+
|
|
875
|
+
// Verify that the rust extension loads if we're using it.
|
|
876
|
+
if (moduleConfiguration.rust == NULL) {
|
|
877
|
+
perror("Failed to load internal Rust extension");
|
|
878
|
+
exit(-1);
|
|
879
|
+
}
|
|
880
|
+
#endif
|
|
881
|
+
|
|
882
|
+
// Spawn the server. We'll wait a bit for it to start accepting
|
|
883
|
+
// sockets.
|
|
852
884
|
pid_t server = fork();
|
|
853
885
|
if (server == 0) {
|
|
854
886
|
// Detach ourselves as a daemon
|
|
@@ -876,6 +908,17 @@ static PyObject* Fprofile_StartTrace(PyObject* obj, PyObject* args, PyObject* kw
|
|
|
876
908
|
sizeof(moduleConfiguration.socket.sun_path),
|
|
877
909
|
"/tmp/functiontrace-server.sock.%d", server);
|
|
878
910
|
|
|
911
|
+
// Register an env var so subprocesses know how to connect to our
|
|
912
|
+
// profile.
|
|
913
|
+
if (setenv(breadcrumb, (char*) &moduleConfiguration.socket.sun_path, 0)) {
|
|
914
|
+
perror("Failed to set recursive breadcrumb");
|
|
915
|
+
exit(-1);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Setup a local Mpack context for this first message.
|
|
919
|
+
mpack_writer_init(&initWriter, (char*) &traceInitialization, sizeof(traceInitialization));
|
|
920
|
+
|
|
921
|
+
#if !defined(RUST_SOCKETS)
|
|
879
922
|
// Generate the TraceInitialization message to the profiler server telling
|
|
880
923
|
// them about the profile we'll be creating.
|
|
881
924
|
if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
|
|
@@ -902,17 +945,26 @@ static PyObject* Fprofile_StartTrace(PyObject* obj, PyObject* args, PyObject* kw
|
|
|
902
945
|
usleep(1000);
|
|
903
946
|
}
|
|
904
947
|
|
|
905
|
-
// Register an env var so subprocesses know how to connect to our
|
|
906
|
-
// profile.
|
|
907
|
-
if (setenv(breadcrumb, (char*) &moduleConfiguration.socket.sun_path, 0)) {
|
|
908
|
-
perror("Failed to set recursive breadcrumb");
|
|
909
|
-
exit(-1);
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
// Setup a local Mpack context for this first message.
|
|
913
|
-
mpack_writer_init(&initWriter, (char*) &traceInitialization, sizeof(traceInitialization));
|
|
914
948
|
mpack_writer_set_context(&initWriter, (void*) sock);
|
|
915
949
|
mpack_writer_set_flush(&initWriter, &Mpack_Flush);
|
|
950
|
+
#else
|
|
951
|
+
{
|
|
952
|
+
PyObject* msg_init = PyObject_GetAttrString(moduleConfiguration.rust, "message_initialization");
|
|
953
|
+
assert(msg_init != NULL);
|
|
954
|
+
|
|
955
|
+
PyObject* arg = PyLong_FromVoidPtr(&moduleConfiguration.socket.sun_path);
|
|
956
|
+
PyObject* context = PyObject_CallFunctionObjArgs(msg_init, arg, NULL);
|
|
957
|
+
assert(context != NULL);
|
|
958
|
+
|
|
959
|
+
PyObject* msg_flush = PyObject_GetAttrString(moduleConfiguration.rust, "message_flush");
|
|
960
|
+
assert(msg_flush != NULL);
|
|
961
|
+
PyObject* mpack_flush = PyObject_CallFunctionObjArgs(msg_flush, NULL);
|
|
962
|
+
assert(mpack_flush != NULL);
|
|
963
|
+
|
|
964
|
+
mpack_writer_set_context(&initWriter, PyLong_AsVoidPtr(context));
|
|
965
|
+
mpack_writer_set_flush(&initWriter, PyLong_AsVoidPtr(mpack_flush));
|
|
966
|
+
}
|
|
967
|
+
#endif
|
|
916
968
|
|
|
917
969
|
mpack_start_map(&initWriter, 5);
|
|
918
970
|
{
|
|
@@ -947,10 +999,22 @@ static PyObject* Fprofile_StartTrace(PyObject* obj, PyObject* args, PyObject* kw
|
|
|
947
999
|
// out first.
|
|
948
1000
|
mpack_writer_flush_message(&initWriter);
|
|
949
1001
|
|
|
1002
|
+
// Close the initialization socket to trigger `functiontrace-server` to
|
|
1003
|
+
// read our message, since otherwise it'll wait forever for more data.
|
|
1004
|
+
#if !defined(RUST_SOCKETS)
|
|
950
1005
|
if (shutdown(sock, SHUT_WR) != 0) {
|
|
951
1006
|
perror("Failed to close TraceInitialization socket");
|
|
952
1007
|
exit(-1);
|
|
953
1008
|
}
|
|
1009
|
+
#else
|
|
1010
|
+
{
|
|
1011
|
+
PyObject* msg_shutdown = PyObject_GetAttrString(moduleConfiguration.rust, "message_shutdown");
|
|
1012
|
+
assert(msg_shutdown != NULL);
|
|
1013
|
+
|
|
1014
|
+
PyObject* socket = PyLong_FromVoidPtr(mpack_writer_context(&initWriter));
|
|
1015
|
+
assert(PyObject_CallFunctionObjArgs(msg_shutdown, socket, NULL) != NULL);
|
|
1016
|
+
}
|
|
1017
|
+
#endif
|
|
954
1018
|
} else {
|
|
955
1019
|
// Read the socket addr out of `existingProfile` so our threads know
|
|
956
1020
|
// how to talk to the profile server.
|
|
@@ -1033,6 +1097,14 @@ static ThreadState* Fprofile_CreateThreadState(void) {
|
|
|
1033
1097
|
exit(-1);
|
|
1034
1098
|
}
|
|
1035
1099
|
|
|
1100
|
+
// Setup msgpack and a socket for this thread to report tracing info on.
|
|
1101
|
+
//
|
|
1102
|
+
// We need to do this before any allocations or Python function calls
|
|
1103
|
+
// occur, since consumers can't tell whether mpack is fully initialized or
|
|
1104
|
+
// not.
|
|
1105
|
+
mpack_writer_init(&state->writer, (char*) &state->buffer, sizeof(state->buffer));
|
|
1106
|
+
|
|
1107
|
+
#if !defined(RUST_SOCKETS)
|
|
1036
1108
|
// Allocate a socket to report this thread's information back to the
|
|
1037
1109
|
// profile server.
|
|
1038
1110
|
if ((state->socket = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
|
|
@@ -1046,11 +1118,33 @@ static ThreadState* Fprofile_CreateThreadState(void) {
|
|
|
1046
1118
|
exit(-1);
|
|
1047
1119
|
}
|
|
1048
1120
|
|
|
1049
|
-
//
|
|
1050
|
-
//
|
|
1051
|
-
mpack_writer_init(&state->writer, (char*) &state->buffer, sizeof(state->buffer));
|
|
1121
|
+
// Configure Mpack to flush events to this thread's socket once the buffer
|
|
1122
|
+
// fills up.
|
|
1052
1123
|
mpack_writer_set_context(&state->writer, (void*) state->socket);
|
|
1053
1124
|
mpack_writer_set_flush(&state->writer, &Mpack_Flush);
|
|
1125
|
+
#else
|
|
1126
|
+
{
|
|
1127
|
+
PyObject* msg_init = PyObject_GetAttrString(moduleConfiguration.rust, "message_initialization");
|
|
1128
|
+
assert(msg_init != NULL);
|
|
1129
|
+
|
|
1130
|
+
// TODO: Convert this to a shared Rust configuration object so we don't
|
|
1131
|
+
// need to pass it back and forth.
|
|
1132
|
+
PyObject* arg = PyLong_FromVoidPtr(&moduleConfiguration.socket.sun_path);
|
|
1133
|
+
PyObject* context = PyObject_CallFunctionObjArgs(msg_init, arg, NULL);
|
|
1134
|
+
assert(context != NULL);
|
|
1135
|
+
|
|
1136
|
+
PyObject* msg_flush = PyObject_GetAttrString(moduleConfiguration.rust, "message_flush");
|
|
1137
|
+
assert(msg_flush != NULL);
|
|
1138
|
+
PyObject* mpack_flush = PyObject_CallFunctionObjArgs(msg_flush, NULL);
|
|
1139
|
+
assert(mpack_flush != NULL);
|
|
1140
|
+
|
|
1141
|
+
// Re-initialize msgpack, since we've done some allocations and Python
|
|
1142
|
+
// calls that we don't want to actually record.
|
|
1143
|
+
mpack_writer_init(&state->writer, (char*) &state->buffer, sizeof(state->buffer));
|
|
1144
|
+
mpack_writer_set_context(&state->writer, PyLong_AsVoidPtr(context));
|
|
1145
|
+
mpack_writer_set_flush(&state->writer, PyLong_AsVoidPtr(mpack_flush));
|
|
1146
|
+
}
|
|
1147
|
+
#endif
|
|
1054
1148
|
|
|
1055
1149
|
// Send a message about our thread's registration.
|
|
1056
1150
|
Timestamp time = Fprofile_Time();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "functiontrace"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "A profiling tool that traces your application's execution"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Matt Bryant", email = "functiontrace@programsareproofs.com"},
|
|
@@ -41,5 +41,9 @@ functiontrace = "functiontrace:main"
|
|
|
41
41
|
functiontrace = "functiontrace"
|
|
42
42
|
|
|
43
43
|
[build-system]
|
|
44
|
-
requires = ["setuptools", "toml"]
|
|
44
|
+
requires = ["setuptools", "toml", "setuptools-rust"]
|
|
45
45
|
build-backend = "setuptools.build_meta"
|
|
46
|
+
|
|
47
|
+
[[tool.setuptools-rust.ext-modules]]
|
|
48
|
+
target = "_functiontrace_rs._lib" # Must match `lib.name` in Cargo.toml
|
|
49
|
+
binding = "PyO3"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#[pyo3::pymodule]
|
|
2
|
+
mod _lib {
|
|
3
|
+
use pyo3::prelude::*;
|
|
4
|
+
use std::ffi::{CStr, OsStr};
|
|
5
|
+
use std::os::raw::c_char;
|
|
6
|
+
use std::os::unix::ffi::OsStrExt;
|
|
7
|
+
use std::os::unix::net::UnixStream;
|
|
8
|
+
|
|
9
|
+
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
10
|
+
// Socket Communication
|
|
11
|
+
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
12
|
+
|
|
13
|
+
/// Initializes and returns a pointer to the [`UnixStream`] we'll communicate to
|
|
14
|
+
/// `functiontrace-server` on.
|
|
15
|
+
///
|
|
16
|
+
/// The returned value will be stored as [`MpackWriter.context`] and used by [`Mpack_Flush`].
|
|
17
|
+
#[pyfunction]
|
|
18
|
+
fn message_initialization(socket: usize) -> usize {
|
|
19
|
+
let c_socket = unsafe {
|
|
20
|
+
// SAFETY: socket is a valid `char*` converted via `PyLong_FromVoidPtr`.
|
|
21
|
+
CStr::from_ptr(socket as *const c_char)
|
|
22
|
+
};
|
|
23
|
+
let sockaddr = OsStr::from_bytes(c_socket.to_bytes());
|
|
24
|
+
|
|
25
|
+
// The functiontrace-server might not be ready to receive connections yet, so we retry for
|
|
26
|
+
// a bit.
|
|
27
|
+
let start = std::time::Instant::now();
|
|
28
|
+
let socket = loop {
|
|
29
|
+
match UnixStream::connect(sockaddr) {
|
|
30
|
+
Ok(s) => {
|
|
31
|
+
break Box::new(s);
|
|
32
|
+
}
|
|
33
|
+
_ => {
|
|
34
|
+
if start.elapsed() > std::time::Duration::from_millis(1000) {
|
|
35
|
+
panic!("Timed out trying to connect to functiontrace-server");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
std::thread::sleep(std::time::Duration::from_millis(10));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// This must be freed by `message_shutdown`
|
|
44
|
+
Box::into_raw(socket) as usize
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Shutdown a socket that was opened by [`message_initialize`], triggering
|
|
48
|
+
/// `functiontrace-server` to read the data sent over it.
|
|
49
|
+
///
|
|
50
|
+
/// If we forget this, we'll leak sockets and a bit of memory.
|
|
51
|
+
#[pyfunction]
|
|
52
|
+
fn message_shutdown(socket: usize) {
|
|
53
|
+
let socket = unsafe {
|
|
54
|
+
// SAFETY: socket is a `UnixStream` emitted from `message_initialization` encoded via
|
|
55
|
+
// `PyLong_FromVoidPtr`.
|
|
56
|
+
Box::from_raw(socket as *mut UnixStream)
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if let Err(e) = socket.shutdown(std::net::Shutdown::Both) {
|
|
60
|
+
panic!("Failed to close socket: {e}");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// The initial subset of the `mpack_writer_t` struct, which `mpack.h` will use to pass us our
|
|
65
|
+
/// per-thread context in.
|
|
66
|
+
#[repr(C)]
|
|
67
|
+
struct MpackWriter {
|
|
68
|
+
flush: *const (),
|
|
69
|
+
error_fn: *const (),
|
|
70
|
+
teardown: *const (),
|
|
71
|
+
context: *mut UnixStream,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Flush events to this socket once the given buffer is full according to `mpack`.
|
|
75
|
+
#[unsafe(no_mangle)]
|
|
76
|
+
extern "C" fn Mpack_Flush(writer: *const MpackWriter, buffer: *const u8, bytes: usize) {
|
|
77
|
+
use std::io::Write;
|
|
78
|
+
|
|
79
|
+
let data = unsafe {
|
|
80
|
+
// SAFETY: `bytes` represents the initialized number of bytes in `buffer` when called
|
|
81
|
+
// by `mpack_writer_flush_message`.
|
|
82
|
+
std::slice::from_raw_parts(buffer, bytes)
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
let mut socket = unsafe {
|
|
86
|
+
// SAFETY: `writer` is valid and `writer->context` contains a `UnixStream` emitted from
|
|
87
|
+
// `message_initialization`.
|
|
88
|
+
&*(*writer).context
|
|
89
|
+
};
|
|
90
|
+
if let Err(e) = socket.write_all(data) {
|
|
91
|
+
panic!("Socket send failed: {e}");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// C accessor for [`Mpack_Flush`]
|
|
96
|
+
#[pyfunction]
|
|
97
|
+
fn message_flush() -> PyResult<usize> {
|
|
98
|
+
Ok(Mpack_Flush as usize)
|
|
99
|
+
}
|
|
100
|
+
}
|
functiontrace-0.3.10/MANIFEST.in
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
graft mpack
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|