Bug 1545582 - Add a JS runtime level of allocation logging; r=jimb
authorGreg Tatum <gtatum@mozilla.com>
Mon, 01 Jul 2019 21:52:58 +0000
changeset 540505 aaea4c45935f7ba51f277ccca88619e69aff50c5
parent 540504 c68a6b2e01576b844a9b0637b73a501345bb5fef
child 540506 c9cba85e0b624b8c6142455cbf0276d27157ff71
push id11529
push userarchaeopteryx@coole-files.de
push dateThu, 04 Jul 2019 15:22:33 +0000
treeherdermozilla-beta@ebb510a784b8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjimb
bugs1545582
milestone69.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1545582 - Add a JS runtime level of allocation logging; r=jimb Differential Revision: https://phabricator.services.mozilla.com/D28142
js/public/AllocationRecording.h
js/public/UbiNode.h
js/src/jsapi.cpp
js/src/moz.build
js/src/vm/Debugger.cpp
js/src/vm/Debugger.h
js/src/vm/Realm.cpp
js/src/vm/Realm.h
js/src/vm/Runtime.cpp
js/src/vm/Runtime.h
js/src/vm/SavedStacks.cpp
js/src/vm/SavedStacks.h
js/src/vm/UbiNode.cpp
new file mode 100644
--- /dev/null
+++ b/js/public/AllocationRecording.h
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=2 et sw=2 tw=80:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef js_AllocationRecording_h
+#define js_AllocationRecording_h
+
+#include "js/TypeDecls.h"
+#include "js/Utility.h"
+
+namespace JS {
+
+/**
+ * This struct holds the information needed to create a profiler marker payload
+ * that can represent a JS allocation. It translates JS engine specific classes,
+ * into something that can be used in the profiler.
+ */
+struct RecordAllocationInfo {
+  RecordAllocationInfo(const char16_t* typeName, const char* className,
+                       const char16_t* descriptiveTypeName,
+                       const char* scriptFilename, const char* coarseType,
+                       uint64_t size, bool inNursery)
+      : typeName(typeName),
+        className(className),
+        descriptiveTypeName(descriptiveTypeName),
+        scriptFilename(scriptFilename),
+        coarseType(coarseType),
+        size(size),
+        inNursery(inNursery) {}
+
+  // These pointers are borrowed from the UbiNode, and can point to live data.
+  // It is important for the consumers of this struct to correctly
+  // duplicate the strings to take ownership of them.
+  const char16_t* typeName;
+  const char* className;
+  const char16_t* descriptiveTypeName;
+  const char* scriptFilename;
+
+  // The coarseType points to a string literal, so does not need to be
+  // duplicated.
+  const char* coarseType;
+
+  // The size in bytes of the allocation.
+  uint64_t size;
+
+  // Whether or not the allocation is in the nursery or not.
+  bool inNursery;
+};
+
+typedef void (*RecordAllocationsCallback)(RecordAllocationInfo&& info);
+
+/**
+ * Enable recording JS allocations. This feature hooks into the object creation
+ * in the JavaScript engine, and reports back the allocation info through the
+ * callback. This allocation tracking is turned on for all encountered realms.
+ * The JS Debugger API can also turn on allocation tracking with its own
+ * probability. If both allocation tracking mechanisms are turned on at the same
+ * time, the Debugger's probability defers to the EnableRecordingAllocations's
+ * probability setting.
+ */
+JS_FRIEND_API void EnableRecordingAllocations(
+    JSContext* cx, RecordAllocationsCallback callback, double probability);
+
+/**
+ * Turn off JS allocation recording. If any JS Debuggers are also recording
+ * allocations, then the probability will be reset to the Debugger's desired
+ * setting.
+ */
+JS_FRIEND_API void DisableRecordingAllocations(JSContext* cx);
+
+}  // namespace JS
+
+#endif /* js_AllocationRecording_h */
--- a/js/public/UbiNode.h
+++ b/js/public/UbiNode.h
@@ -504,16 +504,21 @@ enum class CoarseType : uint32_t {
   Script = 2,
   String = 3,
   DOMNode = 4,
 
   FIRST = Other,
   LAST = DOMNode
 };
 
+/**
+ * Convert a CoarseType enum into a string. The string is statically allocated.
+ */
+JS_PUBLIC_API const char* CoarseTypeToString(CoarseType type);
+
 inline uint32_t CoarseTypeToUint32(CoarseType type) {
   return static_cast<uint32_t>(type);
 }
 
 inline bool Uint32IsValidCoarseType(uint32_t n) {
   auto first = static_cast<uint32_t>(CoarseType::FIRST);
   auto last = static_cast<uint32_t>(CoarseType::LAST);
   MOZ_ASSERT(first < last);
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -1702,16 +1702,17 @@ JS_PUBLIC_API void JS_FireOnNewGlobalObj
                                             JS::HandleObject global) {
   // This hook is infallible, because we don't really want arbitrary script
   // to be able to throw errors during delicate global creation routines.
   // This infallibility will eat OOM and slow script, but if that happens
   // we'll likely run up into them again soon in a fallible context.
   cx->check(global);
   Rooted<js::GlobalObject*> globalObject(cx, &global->as<GlobalObject>());
   Debugger::onNewGlobalObject(cx, globalObject);
+  cx->runtime()->ensureRealmIsRecordingAllocations(globalObject);
 }
 
 JS_PUBLIC_API JSObject* JS_NewObject(JSContext* cx, const JSClass* jsclasp) {
   MOZ_ASSERT(!cx->zone()->isAtomsZone());
   AssertHeapIsIdle();
   CHECK_THREAD(cx);
 
   const Class* clasp = Valueify(jsclasp);
--- a/js/src/moz.build
+++ b/js/src/moz.build
@@ -111,16 +111,17 @@ EXPORTS += [
     'jsapi.h',
     'jsfriendapi.h',
     'jspubtd.h',
     'jstypes.h',
     'perf/jsperf.h',
 ]
 
 EXPORTS.js += [
+    '../public/AllocationRecording.h',
     '../public/AllocPolicy.h',
     '../public/ArrayBuffer.h',
     '../public/BuildId.h',
     '../public/CallArgs.h',
     '../public/CallNonGenericMethod.h',
     '../public/CharacterEncoding.h',
     '../public/Class.h',
     '../public/CompilationAndEvaluation.h',
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -3293,17 +3293,22 @@ void Debugger::removeAllocationsTracking
   // If there are still Debuggers that are observing allocations, we cannot
   // remove the metadata callback yet. Recompute the sampling probability
   // based on the remaining debuggers' needs.
   if (isObservedByDebuggerTrackingAllocations(global)) {
     global.realm()->chooseAllocationSamplingProbability();
     return;
   }
 
-  global.realm()->forgetAllocationMetadataBuilder();
+  if (!global.realm()->runtimeFromMainThread()->recordAllocationCallback) {
+    // Something like the Gecko Profiler could request from the the JS runtime
+    // to record allocations. If it is recording allocations, then do not
+    // destroy the allocation metadata builder at this time.
+    global.realm()->forgetAllocationMetadataBuilder();
+  }
 }
 
 bool Debugger::addAllocationsTrackingForAllDebuggees(JSContext* cx) {
   MOZ_ASSERT(trackingAllocationSites);
 
   // We don't want to end up in a state where we added allocations
   // tracking to some of our debuggees, but failed to do so for
   // others. Before attempting to start tracking allocations in *any* of
--- a/js/src/vm/Debugger.h
+++ b/js/src/vm/Debugger.h
@@ -82,17 +82,16 @@ enum class ResumeMode {
   /**
    * Force the debuggee to return from the current frame.
    *
    * This corresponds to a resumption value of `{return: <value>}`.
    */
   Return,
 };
 
-
 typedef HashSet<WeakHeapPtrGlobalObject,
                 MovableCellHasher<WeakHeapPtrGlobalObject>, ZoneAllocPolicy>
     WeakGlobalObjectSet;
 
 #ifdef DEBUG
 extern void CheckDebuggeeThing(JSScript* script, bool invisibleOk);
 
 extern void CheckDebuggeeThing(LazyScript* script, bool invisibleOk);
@@ -379,16 +378,22 @@ class Debugger : private mozilla::Linked
   // Barrier methods so we can have WeakHeapPtr<Debugger*>.
   static void readBarrier(Debugger* dbg) {
     InternalBarrierMethods<JSObject*>::readBarrier(dbg->object);
   }
   static void writeBarrierPost(Debugger** vp, Debugger* prev, Debugger* next) {}
 #ifdef DEBUG
   static void assertThingIsNotGray(Debugger* dbg) { return; }
 #endif
+  /*
+   * Return true if the given global is being observed by at least one
+   * Debugger that is tracking allocations.
+   */
+  static bool isObservedByDebuggerTrackingAllocations(
+      const GlobalObject& debuggee);
 
  private:
   GCPtrNativeObject object; /* The Debugger object. Strong reference. */
   WeakGlobalObjectSet
       debuggees; /* Debuggee globals. Cross-compartment weak references. */
   JS::ZoneSet debuggeeZones; /* Set of zones that we have debuggees in. */
   js::GCPtrObject uncaughtExceptionHook; /* Strong reference. */
   bool enabled;
@@ -438,23 +443,16 @@ class Debugger : private mozilla::Linked
   /*
    * Return true if there is an existing object metadata callback for the
    * given global's compartment that will prevent our instrumentation of
    * allocations.
    */
   static bool cannotTrackAllocations(const GlobalObject& global);
 
   /*
-   * Return true if the given global is being observed by at least one
-   * Debugger that is tracking allocations.
-   */
-  static bool isObservedByDebuggerTrackingAllocations(
-      const GlobalObject& global);
-
-  /*
    * Add allocations tracking for objects allocated within the given
    * debuggee's compartment. The given debuggee global must be observed by at
    * least one Debugger that is enabled and tracking allocations.
    */
   static MOZ_MUST_USE bool addAllocationsTracking(
       JSContext* cx, Handle<GlobalObject*> debuggee);
 
   /*
--- a/js/src/vm/Realm.cpp
+++ b/js/src/vm/Realm.cpp
@@ -608,16 +608,21 @@ void Realm::clearTables() {
   MOZ_ASSERT(!debugEnvs_);
   MOZ_ASSERT(objects_.enumerators->next() == objects_.enumerators);
 
   objectGroups_.clearTables();
   savedStacks_.clear();
   varNames_.clear();
 }
 
+// Check to see if this individual realm is recording allocations. Debuggers or
+// runtimes can try and record allocations, so this method can check to see if
+// any initialization is needed.
+bool Realm::isRecordingAllocations() { return !!allocationMetadataBuilder_; }
+
 void Realm::setAllocationMetadataBuilder(
     const js::AllocationMetadataBuilder* builder) {
   // Clear any jitcode in the runtime, which behaves differently depending on
   // whether there is a creation callback.
   ReleaseAllJITCode(runtime_->defaultFreeOp());
 
   allocationMetadataBuilder_ = builder;
 }
--- a/js/src/vm/Realm.h
+++ b/js/src/vm/Realm.h
@@ -600,16 +600,17 @@ class JS::Realm : public JS::shadow::Rea
     return allocationMetadataBuilder_;
   }
   const js::AllocationMetadataBuilder* getAllocationMetadataBuilder() const {
     return allocationMetadataBuilder_;
   }
   const void* addressOfMetadataBuilder() const {
     return &allocationMetadataBuilder_;
   }
+  bool isRecordingAllocations();
   void setAllocationMetadataBuilder(
       const js::AllocationMetadataBuilder* builder);
   void forgetAllocationMetadataBuilder();
   void setNewObjectMetadata(JSContext* cx, JS::HandleObject obj);
 
   bool hasObjectPendingMetadata() const {
     return objectMetadataState_.is<js::PendingMetadata>();
   }
@@ -792,18 +793,19 @@ class JS::Realm : public JS::shadow::Rea
 
   js::DebugEnvironments* debugEnvs() { return debugEnvs_.get(); }
   js::UniquePtr<js::DebugEnvironments>& debugEnvsRef() { return debugEnvs_; }
 
   js::SavedStacks& savedStacks() { return savedStacks_; }
 
   // Recompute the probability with which this realm should record
   // profiling data (stack traces, allocations log, etc.) about each
-  // allocation. We consult the probabilities requested by the Debugger
-  // instances observing us, if any.
+  // allocation. We first consult the JS runtime to see if it is recording
+  // allocations, and if not then check the probabilities requested by the
+  // Debugger instances observing us, if any.
   void chooseAllocationSamplingProbability() {
     savedStacks_.chooseSamplingProbability(this);
   }
 
   void sweepSavedStacks();
 
   static constexpr size_t offsetOfCompartment() {
     return offsetof(JS::Realm, compartment_);
--- a/js/src/vm/Runtime.cpp
+++ b/js/src/vm/Runtime.cpp
@@ -50,16 +50,17 @@
 #include "vm/JSObject.h"
 #include "vm/JSScript.h"
 #include "vm/TraceLogging.h"
 #include "vm/TraceLoggingGraph.h"
 #include "wasm/WasmSignalHandlers.h"
 
 #include "gc/GC-inl.h"
 #include "vm/JSContext-inl.h"
+#include "vm/Realm-inl.h"
 
 using namespace js;
 
 using JS::AutoStableStringChars;
 using mozilla::Atomic;
 using mozilla::DebugOnly;
 using mozilla::NegativeInfinity;
 using mozilla::PodZero;
@@ -844,12 +845,67 @@ JS_FRIEND_API void JS::SetJSContextProfi
   cx->runtime()->setProfilerSampleBufferRangeStart(rangeStart);
 }
 
 JS_FRIEND_API bool JS::IsProfilingEnabledForContext(JSContext* cx) {
   MOZ_ASSERT(cx);
   return cx->runtime()->geckoProfiler().enabled();
 }
 
+JS_FRIEND_API void JS::EnableRecordingAllocations(
+    JSContext* cx, JS::RecordAllocationsCallback callback, double probability) {
+  MOZ_ASSERT(cx);
+  MOZ_ASSERT(cx->isMainThreadContext());
+  cx->runtime()->startRecordingAllocations(probability, callback);
+}
+
+JS_FRIEND_API void JS::DisableRecordingAllocations(JSContext* cx) {
+  MOZ_ASSERT(cx);
+  MOZ_ASSERT(cx->isMainThreadContext());
+  cx->runtime()->stopRecordingAllocations();
+}
+
 JS_PUBLIC_API void JS::shadow::RegisterWeakCache(
     JSRuntime* rt, detail::WeakCacheBase* cachep) {
   rt->registerWeakCache(cachep);
 }
+
+void JSRuntime::startRecordingAllocations(
+    double probability, JS::RecordAllocationsCallback callback) {
+  allocationSamplingProbability = probability;
+  recordAllocationCallback = callback;
+
+  // Go through all of the existing realms, and turn on allocation tracking.
+  for (RealmsIter realm(this); !realm.done(); realm.next()) {
+    realm->setAllocationMetadataBuilder(&SavedStacks::metadataBuilder);
+    realm->chooseAllocationSamplingProbability();
+  }
+}
+
+void JSRuntime::stopRecordingAllocations() {
+  recordAllocationCallback = nullptr;
+  // Go through all of the existing realms, and turn on allocation tracking.
+  for (RealmsIter realm(this); !realm.done(); realm.next()) {
+    js::GlobalObject* global = realm->maybeGlobal();
+    if (!realm->isDebuggee() || !global ||
+        !Debugger::isObservedByDebuggerTrackingAllocations(*global)) {
+      // Only remove the allocation metadata builder if no Debuggers are
+      // tracking allocations.
+      realm->forgetAllocationMetadataBuilder();
+    }
+  }
+}
+
+// This function can run to ensure that when new realms are created
+// they have allocation logging turned on.
+void JSRuntime::ensureRealmIsRecordingAllocations(
+    Handle<GlobalObject*> global) {
+  if (recordAllocationCallback) {
+    if (!global->realm()->isRecordingAllocations()) {
+      // This is a new realm, turn on allocations for it.
+      global->realm()->setAllocationMetadataBuilder(
+          &SavedStacks::metadataBuilder);
+    }
+    // Ensure the probability is up to date with the current combination of
+    // debuggers and runtime profiling.
+    global->realm()->chooseAllocationSamplingProbability();
+  }
+}
--- a/js/src/vm/Runtime.h
+++ b/js/src/vm/Runtime.h
@@ -24,16 +24,17 @@
 #include "builtin/AtomicsObject.h"
 #include "builtin/intl/SharedIntlData.h"
 #include "builtin/Promise.h"
 #include "frontend/BinASTRuntimeSupport.h"
 #include "frontend/NameCollections.h"
 #include "gc/GCRuntime.h"
 #include "gc/Tracer.h"
 #include "irregexp/RegExpStack.h"
+#include "js/AllocationRecording.h"
 #include "js/BuildId.h"  // JS::BuildIdOp
 #include "js/Debug.h"
 #include "js/experimental/SourceHook.h"  // js::SourceHook
 #include "js/GCVector.h"
 #include "js/HashTable.h"
 #include "js/Modules.h"  // JS::Module{DynamicImport,Metadata,Resolve}Hook
 #ifdef DEBUG
 #  include "js/Proxy.h"  // For AutoEnterPolicy
@@ -519,32 +520,42 @@ struct JSRuntime : public js::MallocProv
 
   JS::HeapState heapState() const { return heapState_; }
 
   // How many realms there are across all zones. This number includes
   // off-thread context realms, so it isn't necessarily equal to the
   // number of realms visited by RealmsIter.
   js::MainThreadData<size_t> numRealms;
 
+  // The Gecko Profiler may want to sample the allocations happening across the
+  // browser. This callback can be registered to record the allocation.
+  js::MainThreadData<JS::RecordAllocationsCallback> recordAllocationCallback;
+  js::MainThreadData<double> allocationSamplingProbability;
+
  private:
   // Number of debuggee realms in the runtime.
   js::MainThreadData<size_t> numDebuggeeRealms_;
 
   // Number of debuggee realms in the runtime observing code coverage.
   js::MainThreadData<size_t> numDebuggeeRealmsObservingCoverage_;
 
  public:
   void incrementNumDebuggeeRealms();
   void decrementNumDebuggeeRealms();
 
   size_t numDebuggeeRealms() const { return numDebuggeeRealms_; }
 
   void incrementNumDebuggeeRealmsObservingCoverage();
   void decrementNumDebuggeeRealmsObservingCoverage();
 
+  void startRecordingAllocations(double probability,
+                                 JS::RecordAllocationsCallback callback);
+  void stopRecordingAllocations();
+  void ensureRealmIsRecordingAllocations(JS::Handle<js::GlobalObject*> global);
+
   /* Locale-specific callbacks for string conversion. */
   js::MainThreadData<const JSLocaleCallbacks*> localeCallbacks;
 
   /* Default locale for Internationalization API */
   js::MainThreadData<js::UniqueChars> defaultLocale;
 
   /* If true, new scripts must be created with PC counter information. */
   js::MainThreadOrIonCompileData<bool> profilingScripts;
--- a/js/src/vm/SavedStacks.cpp
+++ b/js/src/vm/SavedStacks.cpp
@@ -1793,16 +1793,26 @@ bool SavedStacks::getLocation(JSContext*
     }
   }
 
   locationp.set(p->value());
   return true;
 }
 
 void SavedStacks::chooseSamplingProbability(Realm* realm) {
+  {
+    JSRuntime* runtime = realm->runtimeFromMainThread();
+    if (runtime->recordAllocationCallback) {
+      // The runtime is tracking allocations across all realms, in this case
+      // ignore all of the debugger values, and use the runtime's probability.
+      this->setSamplingProbability(runtime->allocationSamplingProbability);
+      return;
+    }
+  }
+
   // Use unbarriered version to prevent triggering read barrier while
   // collecting, this is safe as long as global does not escape.
   GlobalObject* global = realm->unsafeUnbarrieredMaybeGlobal();
   if (!global) {
     return;
   }
 
   GlobalObject::DebuggerVector* dbgs = global->getDebuggers();
@@ -1824,16 +1834,20 @@ void SavedStacks::chooseSamplingProbabil
 
     if (dbgp->trackingAllocationSites && dbgp->enabled) {
       foundAnyDebuggers = true;
       probability = std::max(dbgp->allocationSamplingProbability, probability);
     }
   }
   MOZ_ASSERT(foundAnyDebuggers);
 
+  this->setSamplingProbability(probability);
+}
+
+void SavedStacks::setSamplingProbability(double probability) {
   if (!bernoulliSeeded) {
     mozilla::Array<uint64_t, 2> seed;
     GenerateXorShift128PlusSeed(seed);
     bernoulli.setRandomState(seed[0], seed[1]);
     bernoulliSeeded = true;
   }
 
   bernoulli.setProbability(probability);
@@ -1854,16 +1868,33 @@ JSObject* SavedStacks::MetadataBuilder::
     oomUnsafe.crash("SavedStacksMetadataBuilder");
   }
 
   if (!Debugger::onLogAllocationSite(cx, obj, frame,
                                      mozilla::TimeStamp::Now())) {
     oomUnsafe.crash("SavedStacksMetadataBuilder");
   }
 
+  auto recordAllocationCallback =
+      cx->realm()->runtimeFromMainThread()->recordAllocationCallback;
+  if (recordAllocationCallback) {
+    // The following code translates the JS-specific information, into an
+    // RecordAllocationInfo object that can be consumed outside of SpiderMonkey.
+
+    auto node = JS::ubi::Node(obj.get());
+
+    // Pass the non-SpiderMonkey specific information back to the
+    // callback to get it out of the JS engine.
+    recordAllocationCallback(JS::RecordAllocationInfo{
+        node.typeName(), node.jsObjectClassName(), node.descriptiveTypeName(),
+        node.scriptFilename(), JS::ubi::CoarseTypeToString(node.coarseType()),
+        node.size(cx->runtime()->debuggerMallocSizeOf),
+        gc::IsInsideNursery(obj)});
+  }
+
   MOZ_ASSERT_IF(frame, !frame->is<WrapperObject>());
   return frame;
 }
 
 const SavedStacks::MetadataBuilder SavedStacks::metadataBuilder;
 
 /* static */
 ReconstructedSavedFramePrincipals ReconstructedSavedFramePrincipals::IsSystem;
--- a/js/src/vm/SavedStacks.h
+++ b/js/src/vm/SavedStacks.h
@@ -222,16 +222,17 @@ class SavedStacks {
       JSContext* cx, MutableHandleSavedFrame asyncStack, HandleAtom asyncCause,
       const mozilla::Maybe<size_t>& maxFrameCount);
   MOZ_MUST_USE bool checkForEvalInFramePrev(
       JSContext* cx, MutableHandle<SavedFrame::Lookup> lookup);
   SavedFrame* getOrCreateSavedFrame(JSContext* cx,
                                     Handle<SavedFrame::Lookup> lookup);
   SavedFrame* createFrameFromLookup(JSContext* cx,
                                     Handle<SavedFrame::Lookup> lookup);
+  void setSamplingProbability(double probability);
 
   // Cache for memoizing PCToLineNumber lookups.
 
   struct PCKey {
     PCKey(JSScript* script, jsbytecode* pc) : script(script), pc(pc) {}
 
     HeapPtr<JSScript*> script;
     jsbytecode* pc;
--- a/js/src/vm/UbiNode.cpp
+++ b/js/src/vm/UbiNode.cpp
@@ -519,10 +519,27 @@ void Concrete<JSObject>::construct(void*
 }
 
 void SetConstructUbiNodeForDOMObjectCallback(JSContext* cx,
                                              void (*callback)(void*,
                                                               JSObject*)) {
   cx->runtime()->constructUbiNodeForDOMObjectCallback = callback;
 }
 
+JS_PUBLIC_API const char* CoarseTypeToString(CoarseType type) {
+  switch (type) {
+    case CoarseType::Other:
+      return "Other";
+    case CoarseType::Object:
+      return "Object";
+    case CoarseType::Script:
+      return "Script";
+    case CoarseType::String:
+      return "String";
+    case CoarseType::DOMNode:
+      return "DOMNode";
+    default:
+      return "Unknown";
+  }
+};
+
 }  // namespace ubi
 }  // namespace JS