Browse Source

async_hooks: implement C++ embedder API

PR-URL: https://github.com/nodejs/node/pull/13142
Reviewed-By: Matthew Loring <mattloring@google.com>
Reviewed-By: Andreas Madsen <amwebdk@gmail.com>
Reviewed-By: Daniel Bevenius <daniel.bevenius@gmail.com>
v6
Anna Henningsen 8 years ago
parent
commit
a86323dc64
No known key found for this signature in database GPG Key ID: D8B9F5AEAE84E4CF
  1. 121
      src/async-wrap.cc
  2. 9
      src/async-wrap.h
  3. 4
      src/inspector_agent.cc
  4. 200
      src/node.cc
  5. 133
      src/node.h
  6. 6
      src/node_crypto.cc
  7. 21
      src/node_internals.h

121
src/async-wrap.cc

@ -47,6 +47,7 @@ using v8::Object;
using v8::Promise;
using v8::PromiseHookType;
using v8::RetainedObjectInfo;
using v8::String;
using v8::Symbol;
using v8::TryCatch;
using v8::Uint32Array;
@ -216,23 +217,28 @@ bool DomainExit(Environment* env, v8::Local<v8::Object> object) {
static bool PreCallbackExecution(AsyncWrap* wrap, bool run_domain_cbs) {
AsyncHooks* async_hooks = wrap->env()->async_hooks();
if (wrap->env()->using_domains() && run_domain_cbs) {
bool is_disposed = DomainEnter(wrap->env(), wrap->object());
if (is_disposed)
return false;
}
return AsyncWrap::EmitBefore(wrap->env(), wrap->get_id());
}
bool AsyncWrap::EmitBefore(Environment* env, double async_id) {
AsyncHooks* async_hooks = env->async_hooks();
if (async_hooks->fields()[AsyncHooks::kBefore] > 0) {
Local<Value> uid = Number::New(wrap->env()->isolate(), wrap->get_id());
Local<Function> fn = wrap->env()->async_hooks_before_function();
TryCatch try_catch(wrap->env()->isolate());
Local<Value> uid = Number::New(env->isolate(), async_id);
Local<Function> fn = env->async_hooks_before_function();
TryCatch try_catch(env->isolate());
MaybeLocal<Value> ar = fn->Call(
wrap->env()->context(), Undefined(wrap->env()->isolate()), 1, &uid);
env->context(), Undefined(env->isolate()), 1, &uid);
if (ar.IsEmpty()) {
ClearFatalExceptionHandlers(wrap->env());
FatalException(wrap->env()->isolate(), try_catch);
ClearFatalExceptionHandlers(env);
FatalException(env->isolate(), try_catch);
return false;
}
}
@ -242,29 +248,36 @@ static bool PreCallbackExecution(AsyncWrap* wrap, bool run_domain_cbs) {
static bool PostCallbackExecution(AsyncWrap* wrap, bool run_domain_cbs) {
AsyncHooks* async_hooks = wrap->env()->async_hooks();
if (!AsyncWrap::EmitAfter(wrap->env(), wrap->get_id()))
return false;
if (wrap->env()->using_domains() && run_domain_cbs) {
bool is_disposed = DomainExit(wrap->env(), wrap->object());
if (is_disposed)
return false;
}
return true;
}
bool AsyncWrap::EmitAfter(Environment* env, double async_id) {
AsyncHooks* async_hooks = env->async_hooks();
// If the callback failed then the after() hooks will be called at the end
// of _fatalException().
if (async_hooks->fields()[AsyncHooks::kAfter] > 0) {
Local<Value> uid = Number::New(wrap->env()->isolate(), wrap->get_id());
Local<Function> fn = wrap->env()->async_hooks_after_function();
TryCatch try_catch(wrap->env()->isolate());
Local<Value> uid = Number::New(env->isolate(), async_id);
Local<Function> fn = env->async_hooks_after_function();
TryCatch try_catch(env->isolate());
MaybeLocal<Value> ar = fn->Call(
wrap->env()->context(), Undefined(wrap->env()->isolate()), 1, &uid);
env->context(), Undefined(env->isolate()), 1, &uid);
if (ar.IsEmpty()) {
ClearFatalExceptionHandlers(wrap->env());
FatalException(wrap->env()->isolate(), try_catch);
ClearFatalExceptionHandlers(env);
FatalException(env->isolate(), try_catch);
return false;
}
}
if (wrap->env()->using_domains() && run_domain_cbs) {
bool is_disposed = DomainExit(wrap->env(), wrap->object());
if (is_disposed)
return false;
}
return true;
}
@ -526,32 +539,44 @@ AsyncWrap::~AsyncWrap() {
// and reused over their lifetime. This way a new uid can be assigned when
// the resource is pulled out of the pool and put back into use.
void AsyncWrap::AsyncReset() {
AsyncHooks* async_hooks = env()->async_hooks();
async_id_ = env()->new_async_id();
trigger_id_ = env()->get_init_trigger_id();
EmitAsyncInit(env(), object(),
env()->async_hooks()->provider_string(provider_type()),
async_id_, trigger_id_);
}
void AsyncWrap::EmitAsyncInit(Environment* env,
Local<Object> object,
Local<String> type,
double async_id,
double trigger_id) {
AsyncHooks* async_hooks = env->async_hooks();
// Nothing to execute, so can continue normally.
if (async_hooks->fields()[AsyncHooks::kInit] == 0) {
return;
}
HandleScope scope(env()->isolate());
Local<Function> init_fn = env()->async_hooks_init_function();
HandleScope scope(env->isolate());
Local<Function> init_fn = env->async_hooks_init_function();
Local<Value> argv[] = {
Number::New(env()->isolate(), get_id()),
env()->async_hooks()->provider_string(provider_type()),
object(),
Number::New(env()->isolate(), get_trigger_id()),
Number::New(env->isolate(), async_id),
type,
object,
Number::New(env->isolate(), trigger_id),
};
TryCatch try_catch(env()->isolate());
TryCatch try_catch(env->isolate());
MaybeLocal<Value> ret = init_fn->Call(
env()->context(), object(), arraysize(argv), argv);
env->context(), object, arraysize(argv), argv);
if (ret.IsEmpty()) {
ClearFatalExceptionHandlers(env());
FatalException(env()->isolate(), try_catch);
ClearFatalExceptionHandlers(env);
FatalException(env->isolate(), try_catch);
}
}
@ -620,6 +645,38 @@ Local<Value> AsyncWrap::MakeCallback(const Local<Function> cb,
return rcheck.IsEmpty() ? Local<Value>() : ret_v;
}
/* Public C++ embedder API */
async_uid AsyncHooksGetCurrentId(Isolate* isolate) {
return Environment::GetCurrent(isolate)->current_async_id();
}
async_uid AsyncHooksGetTriggerId(Isolate* isolate) {
return Environment::GetCurrent(isolate)->get_init_trigger_id();
}
async_uid EmitAsyncInit(Isolate* isolate,
Local<Object> resource,
const char* name,
async_uid trigger_id) {
Environment* env = Environment::GetCurrent(isolate);
async_uid async_id = env->new_async_id();
Local<String> type =
String::NewFromUtf8(isolate, name, v8::NewStringType::kInternalized)
.ToLocalChecked();
AsyncWrap::EmitAsyncInit(env, resource, type, async_id, trigger_id);
return async_id;
}
void EmitAsyncDestroy(Isolate* isolate, async_uid id) {
PushBackDestroyId(Environment::GetCurrent(isolate), id);
}
} // namespace node
NODE_MODULE_CONTEXT_AWARE_BUILTIN(async_wrap, node::AsyncWrap::Initialize)

9
src/async-wrap.h

@ -101,6 +101,15 @@ class AsyncWrap : public BaseObject {
static void AsyncReset(const v8::FunctionCallbackInfo<v8::Value>& args);
static void QueueDestroyId(const v8::FunctionCallbackInfo<v8::Value>& args);
static void EmitAsyncInit(Environment* env,
v8::Local<v8::Object> object,
v8::Local<v8::String> type,
double id,
double trigger_id);
static bool EmitBefore(Environment* env, double id);
static bool EmitAfter(Environment* env, double id);
inline ProviderType provider_type() const;
inline double get_id() const;

4
src/inspector_agent.cc

@ -597,8 +597,8 @@ bool Agent::StartIoThread(bool wait_for_connect) {
FIXED_ONE_BYTE_STRING(isolate, "internalMessage"),
message
};
MakeCallback(parent_env_, process_object.As<Value>(), emit_fn.As<Function>(),
arraysize(argv), argv);
MakeCallback(parent_env_->isolate(), process_object, emit_fn.As<Function>(),
arraysize(argv), argv, 0, 0);
return true;
}

200
src/node.cc

@ -348,7 +348,12 @@ static void CheckImmediate(uv_check_t* handle) {
Environment* env = Environment::from_immediate_check_handle(handle);
HandleScope scope(env->isolate());
Context::Scope context_scope(env->context());
MakeCallback(env, env->process_object(), env->immediate_callback_string());
MakeCallback(env->isolate(),
env->process_object(),
env->immediate_callback_string(),
0,
nullptr,
0, 0).ToLocalChecked();
}
@ -1281,18 +1286,20 @@ void AddPromiseHook(v8::Isolate* isolate, promise_hook_func fn, void* arg) {
}
Local<Value> MakeCallback(Environment* env,
Local<Value> recv,
const Local<Function> callback,
int argc,
Local<Value> argv[]) {
MaybeLocal<Value> MakeCallback(Environment* env,
Local<Value> recv,
const Local<Function> callback,
int argc,
Local<Value> argv[],
double async_id,
double trigger_id) {
// If you hit this assertion, you forgot to enter the v8::Context first.
CHECK_EQ(env->context(), env->isolate()->GetCurrentContext());
Local<Object> object, domain;
bool has_domain = false;
Local<Object> object;
Environment::AsyncCallbackScope callback_scope(env);
bool disposed_domain = false;
if (recv->IsObject()) {
object = recv.As<Object>();
@ -1300,51 +1307,38 @@ Local<Value> MakeCallback(Environment* env,
if (env->using_domains()) {
CHECK(recv->IsObject());
Local<Value> domain_v = object->Get(env->domain_string());
has_domain = domain_v->IsObject();
if (has_domain) {
domain = domain_v.As<Object>();
if (domain->Get(env->disposed_string())->IsTrue())
return Undefined(env->isolate());
}
disposed_domain = DomainEnter(env, object);
if (disposed_domain) return Undefined(env->isolate());
}
if (has_domain) {
Local<Value> enter_v = domain->Get(env->enter_string());
if (enter_v->IsFunction()) {
if (enter_v.As<Function>()->Call(domain, 0, nullptr).IsEmpty()) {
FatalError("node::MakeCallback",
"domain enter callback threw, please report this");
}
}
}
MaybeLocal<Value> ret;
// TODO(trevnorris): Correct this once node::MakeCallback() support id and
// triggerId. Consider completely removing it until then so the async id can
// propagate through to the fatalException after hook calls.
AsyncHooks::ExecScope exec_scope(env, 0, 0);
{
AsyncHooks::ExecScope exec_scope(env, async_id, trigger_id);
Local<Value> ret = callback->Call(recv, argc, argv);
if (async_id != 0) {
if (!AsyncWrap::EmitBefore(env, async_id)) return Local<Value>();
}
if (ret.IsEmpty()) {
// NOTE: For backwards compatibility with public API we return Undefined()
// if the top level call threw.
return callback_scope.in_makecallback() ?
ret : Undefined(env->isolate()).As<Value>();
}
ret = callback->Call(env->context(), recv, argc, argv);
exec_scope.Dispose();
if (ret.IsEmpty()) {
// NOTE: For backwards compatibility with public API we return Undefined()
// if the top level call threw.
return callback_scope.in_makecallback() ?
ret : Undefined(env->isolate());
}
if (has_domain) {
Local<Value> exit_v = domain->Get(env->exit_string());
if (exit_v->IsFunction()) {
if (exit_v.As<Function>()->Call(domain, 0, nullptr).IsEmpty()) {
FatalError("node::MakeCallback",
"domain exit callback threw, please report this");
}
if (async_id != 0) {
if (!AsyncWrap::EmitAfter(env, async_id)) return Local<Value>();
}
}
if (env->using_domains()) {
disposed_domain = DomainExit(env, object);
if (disposed_domain) return Undefined(env->isolate());
}
if (callback_scope.in_makecallback()) {
return ret;
}
@ -1357,8 +1351,8 @@ Local<Value> MakeCallback(Environment* env,
// Make sure the stack unwound properly. If there are nested MakeCallback's
// then it should return early and not reach this code.
CHECK_EQ(env->current_async_id(), 0);
CHECK_EQ(env->trigger_id(), 0);
CHECK_EQ(env->current_async_id(), async_id);
CHECK_EQ(env->trigger_id(), trigger_id);
Local<Object> process = env->process_object();
@ -1375,70 +1369,96 @@ Local<Value> MakeCallback(Environment* env,
}
Local<Value> MakeCallback(Environment* env,
Local<Object> recv,
Local<String> symbol,
int argc,
Local<Value> argv[]) {
Local<Value> cb_v = recv->Get(symbol);
CHECK(cb_v->IsFunction());
return MakeCallback(env, recv.As<Value>(), cb_v.As<Function>(), argc, argv);
// Public MakeCallback()s
MaybeLocal<Value> MakeCallback(Isolate* isolate,
Local<Object> recv,
const char* method,
int argc,
Local<Value> argv[],
async_uid async_id,
async_uid trigger_id) {
Local<String> method_string =
String::NewFromUtf8(isolate, method, v8::NewStringType::kNormal)
.ToLocalChecked();
return MakeCallback(isolate, recv, method_string, argc, argv,
async_id, trigger_id);
}
Local<Value> MakeCallback(Environment* env,
Local<Object> recv,
const char* method,
int argc,
Local<Value> argv[]) {
Local<String> method_string = OneByteString(env->isolate(), method);
return MakeCallback(env, recv, method_string, argc, argv);
MaybeLocal<Value> MakeCallback(Isolate* isolate,
Local<Object> recv,
Local<String> symbol,
int argc,
Local<Value> argv[],
async_uid async_id,
async_uid trigger_id) {
Local<Value> callback_v = recv->Get(symbol);
if (callback_v.IsEmpty()) return Local<Value>();
if (!callback_v->IsFunction()) return Local<Value>();
Local<Function> callback = callback_v.As<Function>();
return MakeCallback(isolate, recv, callback, argc, argv,
async_id, trigger_id);
}
MaybeLocal<Value> MakeCallback(Isolate* isolate,
Local<Object> recv,
Local<Function> callback,
int argc,
Local<Value> argv[],
async_uid async_id,
async_uid trigger_id) {
// Observe the following two subtleties:
//
// 1. The environment is retrieved from the callback function's context.
// 2. The context to enter is retrieved from the environment.
//
// Because of the AssignToContext() call in src/node_contextify.cc,
// the two contexts need not be the same.
Environment* env = Environment::GetCurrent(callback->CreationContext());
Context::Scope context_scope(env->context());
return MakeCallback(env, recv.As<Value>(), callback, argc, argv,
async_id, trigger_id);
}
// Legacy MakeCallback()s
Local<Value> MakeCallback(Isolate* isolate,
Local<Object> recv,
const char* method,
int argc,
Local<Value> argv[]) {
Local<Value>* argv) {
EscapableHandleScope handle_scope(isolate);
Local<String> method_string = OneByteString(isolate, method);
return handle_scope.Escape(
MakeCallback(isolate, recv, method_string, argc, argv));
MakeCallback(isolate, recv, method, argc, argv, 0, 0)
.FromMaybe(Local<Value>()));
}
Local<Value> MakeCallback(Isolate* isolate,
Local<Object> recv,
Local<String> symbol,
int argc,
Local<Value> argv[]) {
Local<Object> recv,
Local<String> symbol,
int argc,
Local<Value>* argv) {
EscapableHandleScope handle_scope(isolate);
Local<Value> callback_v = recv->Get(symbol);
if (callback_v.IsEmpty()) return Local<Value>();
if (!callback_v->IsFunction()) return Local<Value>();
Local<Function> callback = callback_v.As<Function>();
return handle_scope.Escape(MakeCallback(isolate, recv, callback, argc, argv));
return handle_scope.Escape(
MakeCallback(isolate, recv, symbol, argc, argv, 0, 0)
.FromMaybe(Local<Value>()));
}
Local<Value> MakeCallback(Isolate* isolate,
Local<Object> recv,
Local<Function> callback,
int argc,
Local<Value> argv[]) {
// Observe the following two subtleties:
//
// 1. The environment is retrieved from the callback function's context.
// 2. The context to enter is retrieved from the environment.
//
// Because of the AssignToContext() call in src/node_contextify.cc,
// the two contexts need not be the same.
Local<Object> recv,
Local<Function> callback,
int argc,
Local<Value>* argv) {
EscapableHandleScope handle_scope(isolate);
Environment* env = Environment::GetCurrent(callback->CreationContext());
Context::Scope context_scope(env->context());
return handle_scope.Escape(
MakeCallback(env, recv.As<Value>(), callback, argc, argv));
MakeCallback(isolate, recv, callback, argc, argv, 0, 0)
.FromMaybe(Local<Value>()));
}
@ -4382,7 +4402,9 @@ void EmitBeforeExit(Environment* env) {
FIXED_ONE_BYTE_STRING(env->isolate(), "beforeExit"),
process_object->Get(exit_code)->ToInteger(env->isolate())
};
MakeCallback(env, process_object, "emit", arraysize(args), args);
MakeCallback(env->isolate(),
process_object, "emit", arraysize(args), args,
0, 0).ToLocalChecked();
}
@ -4401,7 +4423,9 @@ int EmitExit(Environment* env) {
Integer::New(env->isolate(), code)
};
MakeCallback(env, process_object, "emit", arraysize(args), args);
MakeCallback(env->isolate(),
process_object, "emit", arraysize(args), args,
0, 0).ToLocalChecked();
// Reload exit code, it may be changed by `emit('exit')`
return process_object->Get(exitCode)->Int32Value();

133
src/node.h

@ -142,14 +142,10 @@ inline v8::Local<v8::Value> UVException(int errorno,
}
/*
* MakeCallback doesn't have a HandleScope. That means the callers scope
* will retain ownership of created handles from MakeCallback and related.
* There is by default a wrapping HandleScope before uv_run, if the caller
* doesn't have a HandleScope on the stack the global will take ownership
* which won't be reaped until the uv loop exits.
* These methods need to be called in a HandleScope.
*
* If a uv callback is fired, and there is no enclosing HandleScope in the
* cb, you will appear to leak 4-bytes for every invocation. Take heed.
* It is preferred that you use the `MakeCallback` overloads taking
* `async_uid` arguments.
*/
NODE_EXTERN v8::Local<v8::Value> MakeCallback(
@ -521,12 +517,135 @@ typedef void (*promise_hook_func) (v8::PromiseHookType type,
v8::Local<v8::Value> parent,
void* arg);
typedef double async_uid;
/* Registers an additional v8::PromiseHook wrapper. This API exists because V8
* itself supports only a single PromiseHook. */
NODE_EXTERN void AddPromiseHook(v8::Isolate* isolate,
promise_hook_func fn,
void* arg);
/* Returns the id of the current execution context. If the return value is
* zero then no execution has been set. This will happen if the user handles
* I/O from native code. */
NODE_EXTERN async_uid AsyncHooksGetCurrentId(v8::Isolate* isolate);
/* Return same value as async_hooks.triggerId(); */
NODE_EXTERN async_uid AsyncHooksGetTriggerId(v8::Isolate* isolate);
/* If the native API doesn't inherit from the helper class then the callbacks
* must be triggered manually. This triggers the init() callback. The return
* value is the uid assigned to the resource.
*
* The `trigger_id` parameter should correspond to the resource which is
* creating the new resource, which will usually be the return value of
* `AsyncHooksGetTriggerId()`. */
NODE_EXTERN async_uid EmitAsyncInit(v8::Isolate* isolate,
v8::Local<v8::Object> resource,
const char* name,
async_uid trigger_id);
/* Emit the destroy() callback. */
NODE_EXTERN void EmitAsyncDestroy(v8::Isolate* isolate, async_uid id);
/* An API specific to emit before/after callbacks is unnecessary because
* MakeCallback will automatically call them for you.
*
* These methods may create handles on their own, so run them inside a
* HandleScope.
*
* `asyncId` and `triggerId` should correspond to the values returned by
* `EmitAsyncInit()` and `AsyncHooksGetTriggerId()`, respectively, when the
* invoking resource was created. If these values are unknown, 0 can be passed.
* */
NODE_EXTERN
v8::MaybeLocal<v8::Value> MakeCallback(v8::Isolate* isolate,
v8::Local<v8::Object> recv,
v8::Local<v8::Function> callback,
int argc,
v8::Local<v8::Value>* argv,
async_uid asyncId,
async_uid triggerId);
NODE_EXTERN
v8::MaybeLocal<v8::Value> MakeCallback(v8::Isolate* isolate,
v8::Local<v8::Object> recv,
const char* method,
int argc,
v8::Local<v8::Value>* argv,
async_uid asyncId,
async_uid triggerId);
NODE_EXTERN
v8::MaybeLocal<v8::Value> MakeCallback(v8::Isolate* isolate,
v8::Local<v8::Object> recv,
v8::Local<v8::String> symbol,
int argc,
v8::Local<v8::Value>* argv,
async_uid asyncId,
async_uid triggerId);
/* Helper class users can optionally inherit from. If
* `AsyncResource::MakeCallback()` is used, then all four callbacks will be
* called automatically. */
class AsyncResource {
public:
AsyncResource(v8::Isolate* isolate,
v8::Local<v8::Object> resource,
const char* name,
async_uid trigger_id = -1)
: isolate_(isolate),
resource_(isolate, resource),
trigger_id_(trigger_id) {
if (trigger_id_ == -1)
trigger_id_ = AsyncHooksGetTriggerId(isolate);
uid_ = EmitAsyncInit(isolate, resource, name, trigger_id_);
}
~AsyncResource() {
EmitAsyncDestroy(isolate_, uid_);
}
v8::MaybeLocal<v8::Value> MakeCallback(
v8::Local<v8::Function> callback,
int argc,
v8::Local<v8::Value>* argv) {
return node::MakeCallback(isolate_, get_resource(),
callback, argc, argv,
uid_, trigger_id_);
}
v8::MaybeLocal<v8::Value> MakeCallback(
const char* method,
int argc,
v8::Local<v8::Value>* argv) {
return node::MakeCallback(isolate_, get_resource(),
method, argc, argv,
uid_, trigger_id_);
}
v8::MaybeLocal<v8::Value> MakeCallback(
v8::Local<v8::String> symbol,
int argc,
v8::Local<v8::Value>* argv) {
return node::MakeCallback(isolate_, get_resource(),
symbol, argc, argv,
uid_, trigger_id_);
}
v8::Local<v8::Object> get_resource() {
return resource_.Get(isolate_);
}
async_uid get_uid() const {
return uid_;
}
private:
v8::Isolate* isolate_;
v8::Persistent<v8::Object> resource_;
async_uid uid_;
async_uid trigger_id_;
};
} // namespace node
#endif // SRC_NODE_H_

6
src/node_crypto.cc

@ -1237,11 +1237,13 @@ int SecureContext::TicketKeyCallback(SSL* ssl,
kTicketPartSize).ToLocalChecked(),
Boolean::New(env->isolate(), enc != 0)
};
Local<Value> ret = node::MakeCallback(env,
Local<Value> ret = node::MakeCallback(env->isolate(),
sc->object(),
env->ticketkeycallback_string(),
arraysize(argv),
argv);
argv,
0, 0).ToLocalChecked();
Local<Array> arr = ret.As<Array>();
int r = arr->Get(kTicketKeyReturnIndex)->Int32Value();

21
src/node_internals.h

@ -94,27 +94,6 @@ inline v8::Local<TypeName> PersistentToLocal(
v8::Isolate* isolate,
const v8::Persistent<TypeName>& persistent);
// Call with valid HandleScope and while inside Context scope.
v8::Local<v8::Value> MakeCallback(Environment* env,
v8::Local<v8::Object> recv,
const char* method,
int argc = 0,
v8::Local<v8::Value>* argv = nullptr);
// Call with valid HandleScope and while inside Context scope.
v8::Local<v8::Value> MakeCallback(Environment* env,
v8::Local<v8::Object> recv,
v8::Local<v8::String> symbol,
int argc = 0,
v8::Local<v8::Value>* argv = nullptr);
// Call with valid HandleScope and while inside Context scope.
v8::Local<v8::Value> MakeCallback(Environment* env,
v8::Local<v8::Value> recv,
v8::Local<v8::Function> callback,
int argc = 0,
v8::Local<v8::Value>* argv = nullptr);
// Convert a struct sockaddr to a { address: '1.2.3.4', port: 1234 } JS object.
// Sets address and port properties on the info object and returns it.
// If |info| is omitted, a new object is returned.

Loading…
Cancel
Save