Skip to content

Commit

Permalink
a mature solution
Browse files Browse the repository at this point in the history
  • Loading branch information
mmomtchev committed May 5, 2024
1 parent a1220e7 commit 4c4e9e7
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 55 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [1.2.0]

- Implement a solution to [#110](https://github.com/mmomtchev/everything-json/issues/110), report allocated memory to the GC
This important improvement can greatly reduce the memory requirements of applications the make heavy use of `everything-json`, in particular when using with Next.js during `next build` with lots of static pages

## [1.1.0] 2024-04-26

Expand Down
31 changes: 14 additions & 17 deletions src/JSON.cc
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#include "jsonAsync.h"
#include <sstream>

JSONElementContext::JSONElementContext(const shared_ptr<padded_string> &_input_text, const shared_ptr<parser> &_parser_,
const shared_ptr<element> &_document, const element &_root)
: input_text(_input_text), parser_(_parser_), document(_document), store_json(make_shared<ObjectStore>()),
store_get(make_shared<ObjectStore>()), store_expand(make_shared<ObjectStore>()), root(_root) {}
JSONElementContext::JSONElementContext(Napi::Env env, const Napi::TrackingPtr<padded_string> &_input_text,
const Napi::TrackingPtr<parser> &_parser_,
const Napi::TrackingPtr<element> &_document, const element &_root)
: input_text(_input_text), parser_(_parser_), document(_document), store_json(Napi::MakeTracking<ObjectStore>(env)),
store_get(Napi::MakeTracking<ObjectStore>(env)), store_expand(Napi::MakeTracking<ObjectStore>(env)), root(_root) {
}

JSONElementContext::JSONElementContext(const JSONElementContext &parent, const element &_root)
: input_text(parent.input_text), parser_(parent.parser_), document(parent.document), store_json(parent.store_json),
Expand Down Expand Up @@ -32,24 +34,24 @@ JSON::JSON(const CallbackInfo &info) : ObjectWrap<JSON>(info), external_memory(0

JSON::~JSON() {}

shared_ptr<padded_string> JSON::GetString(const CallbackInfo &info) {
Napi::TrackingPtr<padded_string> JSON::GetString(const CallbackInfo &info) {
Napi::Env env(info.Env());

if (info.Length() != 1 || (!info[0].IsString() && !info[0].IsBuffer())) {
throw TypeError::New(env, "JSON.parse{Async} expects a single string or Buffer argument");
}

auto parser_ = make_shared<parser>();
auto parser_ = Napi::MakeTracking<parser>(env);

size_t json_len;
if (info[0].IsString()) {
napi_get_value_string_utf8(env, info[0], nullptr, 0, &json_len);
auto json = make_shared<padded_string>(json_len);
auto json = Napi::MakeTracking<padded_string>(env, json_len, json_len);
napi_get_value_string_utf8(env, info[0], json->data(), json_len + 1, nullptr);
return json;
} else if (info[0].IsBuffer()) {
auto buffer = info[0].As<Buffer<char>>();
auto json = make_shared<padded_string>(buffer.Data() + buffer.ByteOffset(), buffer.ByteLength());
auto json = Napi::MakeTracking<padded_string>(env, 0, buffer.Data() + buffer.ByteOffset(), buffer.ByteLength());
return json;
}

Expand Down Expand Up @@ -92,18 +94,13 @@ Value JSON::Parse(const CallbackInfo &info) {
auto instance = env.GetInstanceData<InstanceData>();

try {
auto parser_ = make_shared<parser>();
auto parser_ = Napi::MakeTracking<parser>(env);
auto json = GetString(info);
// This overreports memory to the GC
uint64_t external_memory = json->length() * 2;
Napi::MemoryManagement::AdjustExternalMemory(env, external_memory);
auto document = shared_ptr<element>(new element(parser_->parse(*json)), [env, external_memory](void *p) {
Napi::MemoryManagement::AdjustExternalMemory(env, -external_memory);
delete static_cast<element *>(p);
});
// This needs https://github.com/simdjson/simdjson/issues/1017 for optimal solution
auto document = Napi::MakeTracking<element>(env, json->length() * 2, parser_->parse(*json));

element root = *document.get();
JSONElementContext context(json, parser_, document, root);
JSONElementContext context(env, json, parser_, document, root);
napi_value ctor_args = External<JSONElementContext>::New(env, &context);
return New(instance, root, context.store_json.get(), &ctor_args);
} catch (const exception &err) {
Expand Down
23 changes: 12 additions & 11 deletions src/jsonAsync.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <string>

#define NAPI_VERSION 8
#include "trackingPtr.h"
#include <napi.h>
#include <uv.h>

Expand All @@ -28,25 +29,25 @@ typedef map<element, ObjectReference> ObjectStore;
* All JSON elements in the same document share the same pointers
* to the input text, the parser and the main root element (the document root).
* They also share the same object stores.
*
*
* root is the root of the element.
*/
struct JSONElementContext {
// The input string
shared_ptr<padded_string> input_text;
Napi::TrackingPtr<padded_string> input_text;

// The containing document
shared_ptr<parser> parser_;
shared_ptr<element> document;
Napi::TrackingPtr<parser> parser_;
Napi::TrackingPtr<element> document;

// The object store - contains weak refs to objects returned to JS
shared_ptr<ObjectStore> store_json, store_get, store_expand;
Napi::TrackingPtr<ObjectStore> store_json, store_get, store_expand;

// The root of this subvalue
element root;

JSONElementContext(const shared_ptr<padded_string> &, const shared_ptr<parser> &, const shared_ptr<element> &,
const element &);
JSONElementContext(Napi::Env env, const Napi::TrackingPtr<padded_string> &, const Napi::TrackingPtr<parser> &,
const Napi::TrackingPtr<element> &, const element &);
JSONElementContext(const JSONElementContext &parent, const element &);
JSONElementContext();
};
Expand Down Expand Up @@ -94,7 +95,7 @@ struct Context {
}; // namespace ToObjectAsync

struct InstanceData {
queue<shared_ptr<ToObjectAsync::Context>> runQueue;
queue<Napi::TrackingPtr<ToObjectAsync::Context>> runQueue;
FunctionReference JSON_ctor;
uv_async_t runQueueJob;
};
Expand All @@ -109,8 +110,8 @@ class JSON : public ObjectWrap<JSON>, JSONElementContext {
static inline Napi::Value New(InstanceData *, const element &, ObjectStore *store, const napi_value *);

static Napi::Value ToObject(Napi::Env, const element &);
static void ToObjectAsync(shared_ptr<ToObjectAsync::Context>, high_resolution_clock::time_point);
static shared_ptr<padded_string> GetString(const CallbackInfo &);
static void ToObjectAsync(Napi::TrackingPtr<ToObjectAsync::Context>, high_resolution_clock::time_point);
static Napi::TrackingPtr<padded_string> GetString(const CallbackInfo &);
static inline bool CanRun(const high_resolution_clock::time_point &);
static inline Napi::Value GetPrimitive(Napi::Env, const element &);
Napi::Value Get(Napi::Env, bool);
Expand Down Expand Up @@ -144,7 +145,7 @@ class JSON : public ObjectWrap<JSON>, JSONElementContext {
#define TRY_RETURN_FROM_STORE(store, el) \
if ((store)->count(el)) { \
assert((store)->count(el) == 1); \
auto &ref = (store) -> find(el) -> second; \
auto &ref = (store)->find(el)->second; \
if (!ref.IsEmpty() && !ref.Value().IsEmpty()) { \
return ref.Value(); \
} else { \
Expand Down
28 changes: 10 additions & 18 deletions src/parseAsync.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,24 @@
Value JSON::ParseAsync(const CallbackInfo &info) {
class ParserAsyncWorker : public AsyncWorker {
Promise::Deferred deferred;
shared_ptr<padded_string> json_text;
shared_ptr<parser> parser_;
shared_ptr<element> document;
uint64_t external_memory;
Napi::TrackingPtr<padded_string> json_text;
Napi::TrackingPtr<parser> parser_;
Napi::TrackingPtr<element> document;

public:
ParserAsyncWorker(Napi::Env env, shared_ptr<padded_string> text)
: AsyncWorker(env, "JSONAsyncWorker"), deferred(env), json_text(text), external_memory(0) {}
ParserAsyncWorker(Napi::Env env, Napi::TrackingPtr<padded_string> text)
: AsyncWorker(env, "JSONAsyncWorker"), deferred(env), json_text(text) {}
virtual void Execute() override {
parser_ = make_shared<parser>();
napi_env env = Env();
document = shared_ptr<element>(new element(parser_->parse(*json_text)),
[env, external_memory = this->external_memory](void *p) {
// Normally this should always get called on the main thread?
Napi::MemoryManagement::AdjustExternalMemory(env, -external_memory);
delete static_cast<element *>(p);
});
parser_ = Napi::MakeTracking<parser>(env);
// This needs https://github.com/simdjson/simdjson/issues/1017 for optimal solution
document = Napi::MakeTracking<element>(env, json_text->length() * 2, parser_->parse(*json_text));
}
virtual void OnOK() override {
Napi::Env env = Env();
auto instance = env.GetInstanceData<InstanceData>();
element root = *document.get();
JSONElementContext context(json_text, parser_, document, root);
// This overreports memory to the GC
external_memory = json_text->length() * 2;
Napi::MemoryManagement::AdjustExternalMemory(env, external_memory);
JSONElementContext context(env, json_text, parser_, document, root);
napi_value ctor_args = External<JSONElementContext>::New(env, &context);
auto result = New(instance, root, context.store_json.get(), &ctor_args);
deferred.Resolve(result);
Expand All @@ -39,7 +31,7 @@ Value JSON::ParseAsync(const CallbackInfo &info) {

Napi::Env env(info.Env());

auto parser_ = make_shared<parser>();
auto parser_ = Napi::MakeTracking<parser>(env);
auto json_text = GetString(info);
auto worker = new ParserAsyncWorker(env, json_text);

Expand Down
16 changes: 7 additions & 9 deletions src/toObjectAsync.cc
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Value JSON::ToObjectAsync(const CallbackInfo &info) {

// The ToObjectAsync state is created here and it exists
// as long as it sits on the queue
auto state = make_shared<ToObjectAsync::Context>(env, info.This());
auto state = Napi::MakeTracking<ToObjectAsync::Context>(env, 0, env, info.This());
state->stack.emplace_back(root);
ToObjectAsync(state, high_resolution_clock::now());

Expand All @@ -58,9 +58,9 @@ Value JSON::ToObjectAsync(const CallbackInfo &info) {

// The actual implementation, called from the JS entry point
// and the task queue loop, runs until it is allowed, keeps its
// context in shared_ptr<ToObjectAsync::Context> state
// context in Napi::TrackingPtr<ToObjectAsync::Context> state
// (this is an iterative heterogenous tree traversal)
void JSON::ToObjectAsync(shared_ptr<ToObjectAsync::Context> state, high_resolution_clock::time_point start) {
void JSON::ToObjectAsync(Napi::TrackingPtr<ToObjectAsync::Context> state, high_resolution_clock::time_point start) {
Napi::Env env = state->env;
auto &stack = state->stack;

Expand Down Expand Up @@ -120,9 +120,8 @@ void JSON::ToObjectAsync(shared_ptr<ToObjectAsync::Context> state, high_resoluti
Array array = previous->ref.Value().As<Array>();
array.Set(previous->idx, result);
#ifdef DEBUG_VERBOSE
printf("%.*s [%u] = %s\n",
(int)stack.size(), " ",
(unsigned)previous->idx, result.As<String>().Utf8Value().c_str());
printf("%.*s [%u] = %s\n", (int)stack.size(), " ", (unsigned)previous->idx,
result.As<String>().Utf8Value().c_str());
#endif
break;
}
Expand All @@ -131,9 +130,8 @@ void JSON::ToObjectAsync(shared_ptr<ToObjectAsync::Context> state, high_resoluti
auto key = (*previous->iterator.object.idx).key.data();
object.Set(key, result);
#ifdef DEBUG_VERBOSE
printf("%.*s {%s} = %s\n",
(int)stack.size(), " ",
key, result.As<String>().Utf8Value().c_str());
printf("%.*s {%s} = %s\n", (int)stack.size(), " ", key,
result.As<String>().Utf8Value().c_str());
#endif
break;
}
Expand Down
38 changes: 38 additions & 0 deletions src/trackingPtr.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#pragma once

namespace Napi {
/**
* An std::shared_ptr derivative that can track memory
* through the V8 GC
*/
template <typename T> class TrackingPtr : public std::shared_ptr<T> {
Env env_;
int64_t size_;

public:
inline TrackingPtr(Env env, int64_t size, T *ptr) : std::shared_ptr<T>{ptr}, env_{env}, size_(size) {
MemoryManagement::AdjustExternalMemory(env_, sizeof(T) + size_);
}
inline TrackingPtr(Env env, T *ptr) : std::shared_ptr<T>{ptr}, env_{env}, size_(0) {
MemoryManagement::AdjustExternalMemory(env_, sizeof(T));
}
inline TrackingPtr() : std::shared_ptr<T>{}, env_{nullptr}, size_(0) {}
virtual ~TrackingPtr() {
if (this->get() != nullptr) {
MemoryManagement::AdjustExternalMemory(env_, -(sizeof(T) + size_));
}
};
virtual TrackingPtr &operator=(T *ptr) {
if (this->get() != nullptr) {
MemoryManagement::AdjustExternalMemory(env_, -(sizeof(T) + size_));
}
size_ = 0;
MemoryManagement::AdjustExternalMemory(env_, sizeof(T));
return *this;
};
};
template <typename T, typename... ARGS> inline TrackingPtr<T> MakeTracking(Env env, int64_t size, ARGS &&...args) {
return TrackingPtr<T>{env, size, new T(std::forward<ARGS>(args)...)};
}
template <typename T> inline TrackingPtr<T> MakeTracking(Env env) { return TrackingPtr<T>{env, new T}; }
} // namespace Napi

0 comments on commit 4c4e9e7

Please sign in to comment.