[gjs: 6/7] system: Add System.dumpMemoryInfo() API




commit 2ee6e2e64a05f3601c53e65ebbb2b8ce266c4f03
Author: Philip Chimento <philip chimento gmail com>
Date:   Sun Mar 14 22:01:19 2021 -0700

    system: Add System.dumpMemoryInfo() API
    
    SpiderMonkey provides a "memory info object" with a bunch of slightly
    obscure GC statistics on it. We don't want to expose this object directly
    to JS code, but we can dump it to a file or stdout, as we do with
    System.dumpHeap().
    
    This adds a System.dumpMemoryInfo() API that implements this.
    
    We also take the two most pertinent statistics (bytes allocated in
    garbage-collectable objects and malloc bytes associated with them) and
    pipe these to Sysprof when System.dumpMemoryInfo() is called.
    
    We can't query these statistics in the SIGPROF handler because doing so
    would allocate memory. So if you want a continuous display of these
    statistics in Sysprof, then you need to call System.dumpMemoryInfo() in a
    timeout callback or something like that.
    
    See: #292

 gjs/atoms.h                      |  5 ++-
 gjs/profiler-private.h           |  7 +++
 gjs/profiler.cpp                 | 61 +++++++++++++++++++++++++-
 installed-tests/js/testSystem.js | 13 +++++-
 modules/esm/system.js            |  2 +
 modules/system.cpp               | 93 ++++++++++++++++++++++++++++++++++++++++
 6 files changed, 177 insertions(+), 4 deletions(-)
---
diff --git a/gjs/atoms.h b/gjs/atoms.h
index 0a699965..4d3f0faf 100644
--- a/gjs/atoms.h
+++ b/gjs/atoms.h
@@ -28,6 +28,7 @@ class JSTracer;
     macro(file, "__file__") \
     macro(file_name, "fileName") \
     macro(func, "func") \
+    macro(gc_bytes, "gcBytes") \
     macro(gi, "gi") \
     macro(gio, "Gio") \
     macro(glib, "GLib") \
@@ -42,6 +43,7 @@ class JSTracer;
     macro(internal, "internal") \
     macro(length, "length") \
     macro(line_number, "lineNumber") \
+    macro(malloc_bytes, "mallocBytes") \
     macro(message, "message") \
     macro(module_init, "__init__") \
     macro(module_name, "__moduleName__") \
@@ -68,7 +70,8 @@ class JSTracer;
     macro(width, "width") \
     macro(window, "window") \
     macro(x, "x") \
-    macro(y, "y")
+    macro(y, "y") \
+    macro(zone, "zone")
 
 #define FOR_EACH_SYMBOL_ATOM(macro) \
     macro(hook_up_vfunc, "__GObject__hook_up_vfunc") \
diff --git a/gjs/profiler-private.h b/gjs/profiler-private.h
index d68048db..1285185c 100644
--- a/gjs/profiler-private.h
+++ b/gjs/profiler-private.h
@@ -37,6 +37,10 @@ class AutoProfilerLabel {
     ProfilingStack* m_stack;
 };
 
+namespace Gjs {
+enum GCCounters { GC_HEAP_BYTES, MALLOC_HEAP_BYTES, N_COUNTERS };
+}  // namespace Gjs
+
 GjsProfiler *_gjs_profiler_new(GjsContext *context);
 void _gjs_profiler_free(GjsProfiler *self);
 
@@ -44,6 +48,9 @@ void _gjs_profiler_add_mark(GjsProfiler* self, int64_t time, int64_t duration,
                             const char* group, const char* name,
                             const char* message);
 
+[[nodiscard]] bool _gjs_profiler_sample_gc_memory_info(
+    GjsProfiler* self, int64_t gc_counters[Gjs::GCCounters::N_COUNTERS]);
+
 [[nodiscard]] bool _gjs_profiler_is_running(GjsProfiler* self);
 
 void _gjs_profiler_setup_signals(GjsProfiler *self, GjsContext *context);
diff --git a/gjs/profiler.cpp b/gjs/profiler.cpp
index c4518bfa..b5dc0aaa 100644
--- a/gjs/profiler.cpp
+++ b/gjs/profiler.cpp
@@ -35,6 +35,7 @@
 #include "gjs/context.h"
 #include "gjs/jsapi-util.h"
 #include "gjs/mem-private.h"
+#include "gjs/profiler-private.h"
 #include "gjs/profiler.h"
 
 #define FLUSH_DELAY_SECONDS 3
@@ -106,6 +107,7 @@ struct _GjsProfiler {
     /* GLib signal handler ID for SIGUSR2 */
     unsigned sigusr2_id;
     unsigned counter_base;  // index of first GObject memory counter
+    unsigned gc_counter_base;  // index of first GC stats counter
 #endif  /* ENABLE_PROFILER */
 
     /* If we are currently sampling */
@@ -194,8 +196,36 @@ static void setup_counter_helper(SysprofCaptureCounter* counter,
     GJS_FOR_EACH_COUNTER(SETUP_COUNTER);
 #    undef SETUP_COUNTER
 
-    return sysprof_capture_writer_define_counters(
-        self->capture, now, -1, self->pid, counters, GJS_N_COUNTERS);
+    if (!sysprof_capture_writer_define_counters(
+            self->capture, now, -1, self->pid, counters, GJS_N_COUNTERS))
+        return false;
+
+    SysprofCaptureCounter gc_counters[Gjs::GCCounters::N_COUNTERS];
+    self->gc_counter_base = sysprof_capture_writer_request_counter(
+        self->capture, Gjs::GCCounters::N_COUNTERS);
+
+    constexpr size_t category_size = sizeof gc_counters[0].category;
+    constexpr size_t name_size = sizeof gc_counters[0].name;
+    constexpr size_t description_size = sizeof gc_counters[0].description;
+
+    for (size_t ix = 0; ix < Gjs::GCCounters::N_COUNTERS; ix++) {
+        g_snprintf(gc_counters[ix].category, category_size, "GJS");
+        gc_counters[ix].id = uint32_t(self->gc_counter_base + ix);
+        gc_counters[ix].type = SYSPROF_CAPTURE_COUNTER_INT64;
+        gc_counters[ix].value.v64 = 0;
+    }
+    g_snprintf(gc_counters[Gjs::GCCounters::GC_HEAP_BYTES].name, name_size,
+               "GC bytes");
+    g_snprintf(gc_counters[Gjs::GCCounters::GC_HEAP_BYTES].description,
+               description_size, "Bytes used in GC heap");
+    g_snprintf(gc_counters[Gjs::GCCounters::MALLOC_HEAP_BYTES].name, name_size,
+               "Malloc bytes");
+    g_snprintf(gc_counters[Gjs::GCCounters::MALLOC_HEAP_BYTES].description,
+               description_size, "Malloc bytes owned by tenured GC things");
+
+    return sysprof_capture_writer_define_counters(self->capture, now, -1,
+                                                  self->pid, gc_counters,
+                                                  Gjs::GCCounters::N_COUNTERS);
 }
 
 #endif  /* ENABLE_PROFILER */
@@ -776,6 +806,33 @@ void _gjs_profiler_add_mark(GjsProfiler* self, gint64 time_nsec,
 #endif
 }
 
+bool _gjs_profiler_sample_gc_memory_info(
+    GjsProfiler* self, int64_t gc_counters[Gjs::GCCounters::N_COUNTERS]) {
+    g_return_val_if_fail(self, false);
+
+#ifdef ENABLE_PROFILER
+    if (self->running && self->capture) {
+        unsigned ids[Gjs::GCCounters::N_COUNTERS];
+        SysprofCaptureCounterValue values[Gjs::GCCounters::N_COUNTERS];
+
+        for (size_t ix = 0; ix < Gjs::GCCounters::N_COUNTERS; ix++) {
+            ids[ix] = self->gc_counter_base + ix;
+            values[ix].v64 = gc_counters[ix];
+        }
+
+        int64_t now = g_get_monotonic_time() * 1000L;
+        if (!sysprof_capture_writer_set_counters(self->capture, now, -1,
+                                                 self->pid, ids, values,
+                                                 Gjs::GCCounters::N_COUNTERS))
+            return false;
+    }
+#else
+    // Unused in the no-profiler case
+    (void)gc_counters;
+#endif
+    return true;
+}
+
 void gjs_profiler_set_fd(GjsProfiler* self, int fd) {
     g_return_if_fail(self);
     g_return_if_fail(!self->filename);
diff --git a/installed-tests/js/testSystem.js b/installed-tests/js/testSystem.js
index ce4963db..c7bab2ef 100644
--- a/installed-tests/js/testSystem.js
+++ b/installed-tests/js/testSystem.js
@@ -6,7 +6,7 @@
 // SPDX-FileCopyrightText: 2019 Canonical, Ltd.
 
 const System = imports.system;
-const GObject = imports.gi.GObject;
+const {Gio, GObject} = imports.gi;
 
 describe('System.addressOf()', function () {
     it('gives different results for different objects', function () {
@@ -56,6 +56,17 @@ describe('System.dumpHeap()', function () {
     });
 });
 
+describe('System.dumpMemoryInfo()', function () {
+    it('', function () {
+        expect(() => System.dumpMemoryInfo('memory.md')).not.toThrow();
+        expect(() => Gio.File.new_for_path('memory.md').delete(null)).not.toThrow();
+    });
+
+    it('throws but does not crash when given a nonexistent path', function () {
+        expect(() => System.dumpMemoryInfo('/does/not/exist')).toThrowError(/\/does\/not\/exist/);
+    });
+});
+
 describe('System.programPath', function () {
     it('is null when executed from minijasmine', function () {
         expect(System.programPath).toBe(null);
diff --git a/modules/esm/system.js b/modules/esm/system.js
index 5b28cfb6..3d46e477 100644
--- a/modules/esm/system.js
+++ b/modules/esm/system.js
@@ -9,6 +9,7 @@ export let {
     breakpoint,
     clearDateCaches,
     dumpHeap,
+    dumpMemoryInfo,
     exit,
     gc,
     programArgs,
@@ -24,6 +25,7 @@ export default {
     breakpoint,
     clearDateCaches,
     dumpHeap,
+    dumpMemoryInfo,
     exit,
     gc,
     programArgs,
diff --git a/modules/system.cpp b/modules/system.cpp
index 29eb7770..0cd16b6e 100644
--- a/modules/system.cpp
+++ b/modules/system.cpp
@@ -2,9 +2,13 @@
 // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
 // SPDX-FileCopyrightText: 2008 litl, LLC
 // SPDX-FileCopyrightText: 2012 Red Hat, Inc.
+// SPDX-FileCopyrightText: 2019 Endless Mobile, Inc.
+// SPDX-FileCopyrightText: 2021 Philip Chimento <philip chimento gmail com>
 
 #include <config.h>  // for GJS_VERSION
 
+#include <stdint.h>
+#include <stdio.h>
 #include <time.h>    // for tzset
 
 #include <glib-object.h>
@@ -13,6 +17,7 @@
 #include <js/CallArgs.h>
 #include <js/Date.h>                // for ResetTimeZone
 #include <js/GCAPI.h>               // for JS_GC
+#include <js/JSON.h>
 #include <js/PropertyDescriptor.h>  // for JSPROP_READONLY
 #include <js/PropertySpec.h>
 #include <js/RootingAPI.h>
@@ -26,6 +31,7 @@
 #include "gjs/context-private.h"
 #include "gjs/jsapi-util-args.h"
 #include "gjs/jsapi-util.h"
+#include "gjs/profiler-private.h"
 #include "modules/system.h"
 #include "util/log.h"
 #include "util/misc.h"  // for LogFile
@@ -179,12 +185,99 @@ static bool gjs_clear_date_caches(JSContext*, unsigned argc, JS::Value* vp) {
     return true;
 }
 
+static bool write_gc_info(const char16_t* buf, uint32_t len, void* data) {
+    auto* fp = static_cast<FILE*>(data);
+
+    long bytes_written;  // NOLINT(runtime/int): the GLib API requires this type
+    GjsAutoChar utf8 = g_utf16_to_utf8(reinterpret_cast<const uint16_t*>(buf),
+                                       len, /* items_read = */ nullptr,
+                                       &bytes_written, /* error = */ nullptr);
+    if (!utf8)
+        utf8 = g_strdup("<invalid string>");
+
+    fwrite(utf8, 1, bytes_written, fp);
+    return true;
+}
+
+static bool gjs_dump_memory_info(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+    GjsAutoChar filename;
+    if (!gjs_parse_call_args(cx, "dumpMemoryInfo", args, "|F", "filename",
+                             &filename))
+        return false;
+
+    int64_t gc_counters[Gjs::GCCounters::N_COUNTERS];
+
+    // The object returned from NewMemoryInfoObject has gcBytes and mallocBytes
+    // properties which are the sum (over all zones) of bytes used. gcBytes is
+    // the number of bytes in garbage-collectable things (GC things).
+    // mallocBytes is the number of bytes allocated with malloc (reported with
+    // JS::AddAssociatedMemory).
+    //
+    // This info leaks internal state of the JS engine, which is why it is not
+    // returned to the caller, only dumped to a file and piped to Sysprof.
+    //
+    // The object also has a zone property with its own gcBytes and mallocBytes
+    // properties, representing the bytes used in the zone that the memory
+    // object belongs to. We only have one zone in GJS's context, so
+    // zone.gcBytes and zone.mallocBytes are a good measure for how much memory
+    // the actual user program is occupying. These are the values that we expose
+    // as counters in Sysprof. The difference between these values and the sum
+    // values is due to the self-hosting zone and atoms zone, that represent
+    // overhead of the JS engine.
+
+    JS::RootedObject gc_info(cx, js::gc::NewMemoryInfoObject(cx));
+    if (!gc_info)
+        return false;
+
+    const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+    int32_t val;
+    JS::RootedObject zone_info(cx);
+    if (!gjs_object_require_property(cx, gc_info, "gc.zone", atoms.zone(),
+                                     &zone_info) ||
+        !gjs_object_require_property(cx, zone_info, "gc.zone.gcBytes",
+                                     atoms.gc_bytes(), &val))
+        return false;
+    gc_counters[Gjs::GCCounters::GC_HEAP_BYTES] = int64_t(val);
+    if (!gjs_object_require_property(cx, zone_info, "gc.zone.mallocBytes",
+                                     atoms.malloc_bytes(), &val))
+        return false;
+    gc_counters[Gjs::GCCounters::MALLOC_HEAP_BYTES] = int64_t(val);
+
+    auto* gjs = GjsContextPrivate::from_cx(cx);
+    if (gjs->profiler() &&
+        !_gjs_profiler_sample_gc_memory_info(gjs->profiler(), gc_counters)) {
+        gjs_throw(cx, "Could not write GC counters to profiler");
+        return false;
+    }
+
+    LogFile file(filename);
+    if (file.has_error()) {
+        gjs_throw(cx, "Cannot dump memory info to %s: %s", filename.get(),
+                  file.errmsg());
+        return false;
+    }
+
+    fprintf(file.fp(), "# GC Memory Info Object #\n\n```json\n");
+    JS::RootedValue v_gc_info(cx, JS::ObjectValue(*gc_info));
+    JS::RootedValue spacing(cx, JS::Int32Value(2));
+    if (!JS_Stringify(cx, &v_gc_info, nullptr, spacing, write_gc_info,
+                      file.fp()))
+        return false;
+    fprintf(file.fp(), "\n```\n");
+
+    args.rval().setUndefined();
+    return true;
+}
+
 static JSFunctionSpec module_funcs[] = {
     JS_FN("addressOf", gjs_address_of, 1, GJS_MODULE_PROP_FLAGS),
     JS_FN("addressOfGObject", gjs_address_of_gobject, 1, GJS_MODULE_PROP_FLAGS),
     JS_FN("refcount", gjs_refcount, 1, GJS_MODULE_PROP_FLAGS),
     JS_FN("breakpoint", gjs_breakpoint, 0, GJS_MODULE_PROP_FLAGS),
     JS_FN("dumpHeap", gjs_dump_heap, 1, GJS_MODULE_PROP_FLAGS),
+    JS_FN("dumpMemoryInfo", gjs_dump_memory_info, 0, GJS_MODULE_PROP_FLAGS),
     JS_FN("gc", gjs_gc, 0, GJS_MODULE_PROP_FLAGS),
     JS_FN("exit", gjs_exit, 0, GJS_MODULE_PROP_FLAGS),
     JS_FN("clearDateCaches", gjs_clear_date_caches, 0, GJS_MODULE_PROP_FLAGS),


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]