Bug 1656638: Add Wasm compile- and run-time telemetry to track Wasm compiler performance. r=lth
☠☠ backed out by 6e35e01646d7 ☠ ☠
authorChris Fallin <cfallin@mozilla.com>
Wed, 05 Aug 2020 23:18:33 +0000
changeset 543493 c8123a3e42497aaa892109bdbb414862a40c9747
parent 543492 f2a8b1d3a7b8f817a38ebb7a515ed404968a5f9d
child 543494 6e35e01646d7c465893a172a0b4fb116c2293d2a
push id37674
push userabutkovits@mozilla.com
push dateThu, 06 Aug 2020 03:34:56 +0000
treeherdermozilla-central@6e35e01646d7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslth
bugs1656638
milestone81.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 1656638: Add Wasm compile- and run-time telemetry to track Wasm compiler performance. r=lth This patch adds telemetry to measure the time spent in Wasm compilers and in the code that they generate (actually, all JS and Wasm code). For simplicity, it measures wallclock time as a proxy for CPU time. Furthermore, it measures runtime for all JS and Wasm code, and all native functions invoked by the JS or Wasm code, by timing from top-level entry to exit. This is for efficiency reasons: we do not want to add a VM call in the transition stubs between native and JS or JS and Wasm; that would be a Very Bad Thing for performance, even for a Nightly build instrumented with telemetry. Because of that, it's difficult to separate JITted JS and JITted Wasm runtime, but observing their sum should still be useful when making comparisons across compiler changes because absolute reductions will still be visible. The plumbing is somewhat awkward, given that Wasm compilers can run on background threads. It appears that the telemetry-callback API that SpiderMonkey includes to avoid a direct dependency on the Gecko embedding had artificially limited the callback to main-thread use only. This patch removes that limitation, which is safe at least for Gecko; the telemetry hooks in Gecko are thread-safe (they take a global mutex). That way, the background threads performing compilation can directly add telemetry incrementally, without having to pass this up through the main thread somehow. Finally, I have chosen to add the relevant metrics as Scalar telemetry values rather than Histograms. This is because what we are really interested in is the sum of all these values (all CPU time spent in compilation + running Wasm code); if this value goes down as a result of a Wasm compiler change, then that Wasm compiler change is good because it reduces CPU time on the user's machine. It is difficult to add two Histograms together because the bins may have different boundaries. If we instead need to use a binned histogram for other reasons, then we could simply report the sum (of all compiler time plus run time) as another histogram. Differential Revision: https://phabricator.services.mozilla.com/D85654
js/moz.configure
js/public/friend/UsageStatistics.h
js/src/vm/Interpreter.cpp
js/src/vm/JSContext.cpp
js/src/vm/JSContext.h
js/src/vm/Runtime.cpp
js/src/vm/Runtime.h
js/src/wasm/WasmCompile.cpp
js/src/wasm/WasmCompile.h
js/src/wasm/WasmGenerator.cpp
js/src/wasm/WasmGenerator.h
js/src/wasm/WasmJS.cpp
js/src/wasm/WasmModule.cpp
js/src/wasm/WasmModule.h
js/xpconnect/src/XPCJSRuntime.cpp
toolkit/components/telemetry/Scalars.yaml
--- a/js/moz.configure
+++ b/js/moz.configure
@@ -476,16 +476,24 @@ set_define('MOZ_RUST_SIMD', rust_simd)
 
 js_option('--enable-cranelift',
           default=milestone.is_nightly,
           help='{Enable|Disable} Cranelift code generator for wasm')
 
 set_config('ENABLE_WASM_CRANELIFT', depends_if('--enable-cranelift')(lambda x: True))
 set_define('ENABLE_WASM_CRANELIFT', depends_if('--enable-cranelift')(lambda x: True))
 
+# Telemetry to measure compile time and generated-code runtime
+# ============================================================
+
+js_option('--enable-spidermonkey-telemetry',
+          default=milestone.is_nightly,
+          help='{Enable|Disable} performance telemetry for SpiderMonkey (e.g. compile and run times)')
+
+set_define('ENABLE_SPIDERMONKEY_TELEMETRY', depends_if('--enable-spidermonkey-telemetry')(lambda x: True))
 
 # Support for debugging code generated by wasm backends
 # =====================================================
 
 js_option('--enable-wasm-codegen-debug',
           default=depends(when=moz_debug)(lambda: True),
           help='{Enable|Disable} debugging for wasm codegen')
 
--- a/js/public/friend/UsageStatistics.h
+++ b/js/public/friend/UsageStatistics.h
@@ -54,16 +54,20 @@ enum {
   JS_TELEMETRY_GC_TENURED_SURVIVAL_RATE,
   JS_TELEMETRY_GC_MARK_RATE_2,
   JS_TELEMETRY_GC_TIME_BETWEEN_S,
   JS_TELEMETRY_GC_TIME_BETWEEN_SLICES_MS,
   JS_TELEMETRY_GC_SLICE_COUNT,
   JS_TELEMETRY_GC_EFFECTIVENESS,
   JS_TELEMETRY_PRIVILEGED_PARSER_COMPILE_LAZY_AFTER_MS,
   JS_TELEMETRY_WEB_PARSER_COMPILE_LAZY_AFTER_MS,
+  JS_TELEMETRY_RUN_TIME_US,
+  JS_TELEMETRY_WASM_COMPILE_TIME_BASELINE_US,
+  JS_TELEMETRY_WASM_COMPILE_TIME_ION_US,
+  JS_TELEMETRY_WASM_COMPILE_TIME_CRANELIFT_US,
   JS_TELEMETRY_END
 };
 
 using JSAccumulateTelemetryDataCallback = void (*)(int, uint32_t, const char*);
 
 extern JS_FRIEND_API void JS_SetAccumulateTelemetryCallback(
     JSContext* cx, JSAccumulateTelemetryDataCallback callback);
 
--- a/js/src/vm/Interpreter.cpp
+++ b/js/src/vm/Interpreter.cpp
@@ -436,32 +436,52 @@ bool js::RunScript(JSContext* cx, RunSta
   MOZ_ASSERT(!cx->enableAccessValidation || cx->realm()->isAccessValid());
 
   if (!DebugAPI::checkNoExecute(cx, state.script())) {
     return false;
   }
 
   GeckoProfilerEntryMarker marker(cx, state.script());
 
+#ifdef ENABLE_SPIDERMONKEY_TELEMETRY
+  bool measuringTime = !cx->isMeasuringExecutionTime();
+  int64_t startTime = 0;
+  if (measuringTime) {
+    cx->setIsMeasuringExecutionTime(true);
+    startTime = PRMJ_Now();
+  }
+#endif
+
   jit::EnterJitStatus status = jit::MaybeEnterJit(cx, state);
   switch (status) {
     case jit::EnterJitStatus::Error:
       return false;
     case jit::EnterJitStatus::Ok:
       return true;
     case jit::EnterJitStatus::NotEntered:
       break;
   }
 
   if (state.isInvoke()) {
     InvokeState& invoke = *state.asInvoke();
     TypeMonitorCall(cx, invoke.args(), invoke.constructing());
   }
 
-  return Interpret(cx, state);
+  bool ok = Interpret(cx, state);
+
+#ifdef ENABLE_SPIDERMONKEY_TELEMETRY
+  if (measuringTime) {
+    int64_t endTime = PRMJ_Now();
+    int64_t runtimeMicros = endTime - startTime;
+    cx->runtime()->addTelemetry(JS_TELEMETRY_RUN_TIME_US, runtimeMicros);
+    cx->setIsMeasuringExecutionTime(false);
+  }
+#endif
+
+  return ok;
 }
 #ifdef _MSC_VER
 #  pragma optimize("", on)
 #endif
 
 STATIC_PRECONDITION_ASSUME(ubound(args.argv_) >= argc)
 MOZ_ALWAYS_INLINE bool CallJSNative(JSContext* cx, Native native,
                                     CallReason reason, const CallArgs& args) {
--- a/js/src/vm/JSContext.cpp
+++ b/js/src/vm/JSContext.cpp
@@ -882,16 +882,17 @@ JSContext::JSContext(JSRuntime* runtime,
     : runtime_(runtime),
       kind_(ContextKind::HelperThread),
       nurserySuppressions_(this),
       options_(this, options),
       freeLists_(this, nullptr),
       atomsZoneFreeLists_(this),
       defaultFreeOp_(this, runtime, true),
       freeUnusedMemory(false),
+      measuringExecutionTime_(this, false),
       jitActivation(this, nullptr),
       isolate(this, nullptr),
       activation_(this, nullptr),
       profilingActivation_(nullptr),
       nativeStackBase(GetNativeStackBase()),
       entryMonitor(this, nullptr),
       noExecuteDebuggerTop(this, nullptr),
 #ifdef DEBUG
--- a/js/src/vm/JSContext.h
+++ b/js/src/vm/JSContext.h
@@ -186,16 +186,20 @@ struct JS_PUBLIC_API JSContext : public 
   js::ThreadId currentThread_;
 
   js::ParseTask* parseTask_;
 
   // When a helper thread is using a context, it may need to periodically
   // free unused memory.
   mozilla::Atomic<bool, mozilla::ReleaseAcquire> freeUnusedMemory;
 
+  // Are we currently timing execution? This flag ensures that we do not
+  // double-count execution time in reentrant situations.
+  js::ContextData<bool> measuringExecutionTime_;
+
  public:
   // This is used by helper threads to change the runtime their context is
   // currently operating on.
   void setRuntime(JSRuntime* rt);
 
   void setHelperThread(js::AutoLockHelperThreadState& locked);
   void clearHelperThread(js::AutoLockHelperThreadState& locked);
 
@@ -205,16 +209,21 @@ struct JS_PUBLIC_API JSContext : public 
   }
 
   void setFreeUnusedMemory(bool shouldFree) { freeUnusedMemory = shouldFree; }
 
   bool shouldFreeUnusedMemory() const {
     return kind_ == js::ContextKind::HelperThread && freeUnusedMemory;
   }
 
+  bool isMeasuringExecutionTime() const { return measuringExecutionTime_; }
+  void setIsMeasuringExecutionTime(bool value) {
+    measuringExecutionTime_ = value;
+  }
+
   bool isMainThreadContext() const {
     return kind_ == js::ContextKind::MainThread;
   }
 
   bool isHelperThreadContext() const {
     return kind_ == js::ContextKind::HelperThread;
   }
 
--- a/js/src/vm/Runtime.cpp
+++ b/js/src/vm/Runtime.cpp
@@ -313,16 +313,20 @@ void JSRuntime::destroyRuntime() {
 }
 
 void JSRuntime::addTelemetry(int id, uint32_t sample, const char* key) {
   if (telemetryCallback) {
     (*telemetryCallback)(id, sample, key);
   }
 }
 
+JSTelemetrySender JSRuntime::getTelemetrySender() const {
+  return JSTelemetrySender(telemetryCallback);
+}
+
 void JSRuntime::setTelemetryCallback(
     JSRuntime* rt, JSAccumulateTelemetryDataCallback callback) {
   rt->telemetryCallback = callback;
 }
 
 void JSRuntime::setElementCallback(JSRuntime* rt,
                                    JSGetElementCallback callback) {
   rt->getElementCallback = callback;
--- a/js/src/vm/Runtime.h
+++ b/js/src/vm/Runtime.h
@@ -230,16 +230,18 @@ struct SelfHostedLazyScript {
 
   static constexpr size_t offsetOfJitCodeRaw() {
     return offsetof(SelfHostedLazyScript, jitCodeRaw_);
   }
 };
 
 }  // namespace js
 
+struct JSTelemetrySender;
+
 struct JSRuntime {
  private:
   friend class js::Activation;
   friend class js::ActivationIterator;
   friend class js::jit::JitActivation;
   friend class js::jit::CompileRuntime;
 
   /* Space for interpreter frames. */
@@ -318,30 +320,33 @@ struct JSRuntime {
     }
     uint64_t rangeStart = profilerSampleBufferRangeStart_;
     return mozilla::Some(rangeStart);
   }
   void setProfilerSampleBufferRangeStart(uint64_t rangeStart) {
     profilerSampleBufferRangeStart_ = rangeStart;
   }
 
-  /* Call this to accumulate telemetry data. */
-  js::MainThreadData<JSAccumulateTelemetryDataCallback> telemetryCallback;
+  /* Call this to accumulate telemetry data. May be called from any thread; the
+   * embedder is responsible for locking. */
+  JSAccumulateTelemetryDataCallback telemetryCallback;
 
   /* Call this to accumulate use counter data. */
   js::MainThreadData<JSSetUseCounterCallback> useCounterCallback;
 
   js::MainThreadData<JSGetElementCallback> getElementCallback;
 
  public:
   // Accumulates data for Firefox telemetry. |id| is the ID of a JS_TELEMETRY_*
   // histogram. |key| provides an additional key to identify the histogram.
   // |sample| is the data to add to the histogram.
   void addTelemetry(int id, uint32_t sample, const char* key = nullptr);
 
+  JSTelemetrySender getTelemetrySender() const;
+
   void setTelemetryCallback(JSRuntime* rt,
                             JSAccumulateTelemetryDataCallback callback);
 
   void setElementCallback(JSRuntime* rt, JSGetElementCallback callback);
 
   // Sets the use counter for a specific feature, measuring the presence or
   // absence of usage of a feature on a specific web page and document which
   // the passed JSObject belongs to.
@@ -1066,16 +1071,48 @@ struct JSRuntime {
     // if non-null, any call to `setPendingException`
     // in this runtime will trigger the call to `interceptor`
     JSErrorInterceptor* interceptor;
   };
   ErrorInterceptionSupport errorInterception;
 #endif  // defined(NIGHTLY_BUILD)
 };
 
+// Context for sending telemetry to the embedder from any thread, main or
+// helper.  Obtain a |JSTelemetrySender| by calling |getTelemetrySender()| on
+// the |JSRuntime|.
+struct JSTelemetrySender {
+ private:
+  friend struct JSRuntime;
+
+  JSAccumulateTelemetryDataCallback callback_;
+
+  JSTelemetrySender(JSAccumulateTelemetryDataCallback callback)
+      : callback_(callback) {}
+
+ public:
+  JSTelemetrySender() : callback_(nullptr) {}
+  JSTelemetrySender(const JSTelemetrySender& other)
+      : callback_(other.callback_) {}
+  explicit JSTelemetrySender(JSRuntime* runtime)
+      : JSTelemetrySender(runtime->getTelemetrySender()) {}
+
+  // Accumulates data for Firefox telemetry. |id| is the ID of a JS_TELEMETRY_*
+  // histogram. |key| provides an additional key to identify the histogram.
+  // |sample| is the data to add to the histogram.
+  void addTelemetry(int id, uint32_t sample, const char* key = nullptr) {
+    if (callback_) {
+      callback_(id, sample, key);
+    }
+  }
+
+  // TODO(cfallin) REMOVE
+  void* get() const { return (void*)callback_; }
+};
+
 namespace js {
 
 static MOZ_ALWAYS_INLINE void MakeRangeGCSafe(Value* vec, size_t len) {
   // Don't PodZero here because JS::Value is non-trivial.
   for (size_t i = 0; i < len; i++) {
     vec[i].setDouble(+0.0);
   }
 }
--- a/js/src/wasm/WasmCompile.cpp
+++ b/js/src/wasm/WasmCompile.cpp
@@ -565,45 +565,47 @@ static bool DecodeCodeSection(const Modu
 
   return mg.finishFuncDefs();
 }
 
 SharedModule wasm::CompileBuffer(const CompileArgs& args,
                                  const ShareableBytes& bytecode,
                                  UniqueChars* error,
                                  UniqueCharsVector* warnings,
-                                 JS::OptimizedEncodingListener* listener) {
+                                 JS::OptimizedEncodingListener* listener,
+                                 JSTelemetrySender telemetrySender) {
   Decoder d(bytecode.bytes, 0, error, warnings);
 
   CompilerEnvironment compilerEnv(args);
   ModuleEnvironment env(&compilerEnv, args.sharedMemoryEnabled
                                           ? Shareable::True
                                           : Shareable::False);
   if (!DecodeModuleEnvironment(d, &env)) {
     return nullptr;
   }
 
   ModuleGenerator mg(args, &env, nullptr, error);
-  if (!mg.init()) {
+  if (!mg.init(nullptr, telemetrySender)) {
     return nullptr;
   }
 
   if (!DecodeCodeSection(env, d, mg)) {
     return nullptr;
   }
 
   if (!DecodeModuleTail(d, &env)) {
     return nullptr;
   }
 
   return mg.finishModule(bytecode, listener);
 }
 
 void wasm::CompileTier2(const CompileArgs& args, const Bytes& bytecode,
-                        const Module& module, Atomic<bool>* cancelled) {
+                        const Module& module, Atomic<bool>* cancelled,
+                        JSTelemetrySender telemetrySender) {
   UniqueChars error;
   Decoder d(bytecode, 0, &error);
 
   bool gcTypesConfigured = false;  // No optimized backend support yet
 #ifdef ENABLE_WASM_REFTYPES
   bool refTypesConfigured = true;
 #else
   bool refTypesConfigured = false;
@@ -623,17 +625,17 @@ void wasm::CompileTier2(const CompileArg
   ModuleEnvironment env(&compilerEnv, args.sharedMemoryEnabled
                                           ? Shareable::True
                                           : Shareable::False);
   if (!DecodeModuleEnvironment(d, &env)) {
     return;
   }
 
   ModuleGenerator mg(args, &env, cancelled, &error);
-  if (!mg.init()) {
+  if (!mg.init(nullptr, telemetrySender)) {
     return;
   }
 
   if (!DecodeCodeSection(env, d, mg)) {
     return;
   }
 
   if (!DecodeModuleTail(d, &env)) {
@@ -723,17 +725,17 @@ static SharedBytes CreateBytecode(const 
   return bytecode;
 }
 
 SharedModule wasm::CompileStreaming(
     const CompileArgs& args, const Bytes& envBytes, const Bytes& codeBytes,
     const ExclusiveBytesPtr& codeBytesEnd,
     const ExclusiveStreamEndData& exclusiveStreamEnd,
     const Atomic<bool>& cancelled, UniqueChars* error,
-    UniqueCharsVector* warnings) {
+    UniqueCharsVector* warnings, JSTelemetrySender telemetrySender) {
   CompilerEnvironment compilerEnv(args);
   ModuleEnvironment env(&compilerEnv, args.sharedMemoryEnabled
                                           ? Shareable::True
                                           : Shareable::False);
 
   {
     Decoder d(envBytes, 0, error, warnings);
 
@@ -746,17 +748,17 @@ SharedModule wasm::CompileStreaming(
       return nullptr;
     }
 
     MOZ_RELEASE_ASSERT(env.codeSection->size == codeBytes.length());
     MOZ_RELEASE_ASSERT(d.done());
   }
 
   ModuleGenerator mg(args, &env, &cancelled, error);
-  if (!mg.init()) {
+  if (!mg.init(nullptr, telemetrySender)) {
     return nullptr;
   }
 
   {
     StreamingDecoder d(env, codeBytes, codeBytesEnd, cancelled, error,
                        warnings);
 
     if (!DecodeCodeSection(env, d, mg)) {
--- a/js/src/wasm/WasmCompile.h
+++ b/js/src/wasm/WasmCompile.h
@@ -95,25 +95,27 @@ struct CompileArgs : ShareableBase<Compi
 double EstimateCompiledCodeSize(Tier tier, size_t bytecodeSize);
 
 // Compile the given WebAssembly bytecode with the given arguments into a
 // wasm::Module. On success, the Module is returned. On failure, the returned
 // SharedModule pointer is null and either:
 //  - *error points to a string description of the error
 //  - *error is null and the caller should report out-of-memory.
 
-SharedModule CompileBuffer(const CompileArgs& args,
-                           const ShareableBytes& bytecode, UniqueChars* error,
-                           UniqueCharsVector* warnings,
-                           JS::OptimizedEncodingListener* listener = nullptr);
+SharedModule CompileBuffer(
+    const CompileArgs& args, const ShareableBytes& bytecode, UniqueChars* error,
+    UniqueCharsVector* warnings,
+    JS::OptimizedEncodingListener* listener = nullptr,
+    JSTelemetrySender telemetrySender = JSTelemetrySender());
 
 // Attempt to compile the second tier of the given wasm::Module.
 
 void CompileTier2(const CompileArgs& args, const Bytes& bytecode,
-                  const Module& module, Atomic<bool>* cancelled);
+                  const Module& module, Atomic<bool>* cancelled,
+                  JSTelemetrySender telemetrySender = JSTelemetrySender());
 
 // Compile the given WebAssembly module which has been broken into three
 // partitions:
 //  - envBytes contains a complete ModuleEnvironment that has already been
 //    copied in from the stream.
 //  - codeBytes is pre-sized to hold the complete code section when the stream
 //    completes.
 //  - The range [codeBytes.begin(), codeBytesEnd) contains the bytes currently
@@ -134,19 +136,19 @@ struct StreamEndData {
   bool reached;
   const Bytes* tailBytes;
   Tier2Listener tier2Listener;
 
   StreamEndData() : reached(false) {}
 };
 using ExclusiveStreamEndData = ExclusiveWaitableData<StreamEndData>;
 
-SharedModule CompileStreaming(const CompileArgs& args, const Bytes& envBytes,
-                              const Bytes& codeBytes,
-                              const ExclusiveBytesPtr& codeBytesEnd,
-                              const ExclusiveStreamEndData& streamEnd,
-                              const Atomic<bool>& cancelled, UniqueChars* error,
-                              UniqueCharsVector* warnings);
+SharedModule CompileStreaming(
+    const CompileArgs& args, const Bytes& envBytes, const Bytes& codeBytes,
+    const ExclusiveBytesPtr& codeBytesEnd,
+    const ExclusiveStreamEndData& streamEnd, const Atomic<bool>& cancelled,
+    UniqueChars* error, UniqueCharsVector* warnings,
+    JSTelemetrySender telemetrySender = JSTelemetrySender());
 
 }  // namespace wasm
 }  // namespace js
 
 #endif  // namespace wasm_compile_h
--- a/js/src/wasm/WasmGenerator.cpp
+++ b/js/src/wasm/WasmGenerator.cpp
@@ -23,16 +23,17 @@
 #include "mozilla/SHA1.h"
 #include "mozilla/Unused.h"
 
 #include <algorithm>
 #include <thread>
 
 #include "util/Memory.h"
 #include "util/Text.h"
+#include "vm/Time.h"
 #include "wasm/WasmBaselineCompile.h"
 #include "wasm/WasmCompile.h"
 #include "wasm/WasmCraneliftCompile.h"
 #include "wasm/WasmIonCompile.h"
 #include "wasm/WasmStubs.h"
 
 #include "jit/MacroAssembler-inl.h"
 
@@ -164,19 +165,22 @@ bool ModuleGenerator::allocateGlobalByte
   if (!newGlobalDataLength.isValid()) {
     return false;
   }
 
   metadata_->globalDataLength = newGlobalDataLength.value();
   return true;
 }
 
-bool ModuleGenerator::init(Metadata* maybeAsmJSMetadata) {
+bool ModuleGenerator::init(Metadata* maybeAsmJSMetadata,
+                           JSTelemetrySender telemetrySender) {
   // Perform fallible metadata, linkdata, assumption allocations.
 
+  telemetrySender_ = telemetrySender;
+
   MOZ_ASSERT(isAsmJS() == !!maybeAsmJSMetadata);
   if (maybeAsmJSMetadata) {
     metadata_ = maybeAsmJSMetadata;
   } else {
     metadata_ = js_new<Metadata>();
     if (!metadata_) {
       return false;
     }
@@ -434,17 +438,18 @@ bool ModuleGenerator::init(Metadata* may
     numTasks = 1;
   }
 
   if (!tasks_.initCapacity(numTasks)) {
     return false;
   }
   for (size_t i = 0; i < numTasks; i++) {
     tasks_.infallibleEmplaceBack(*env_, taskState_,
-                                 COMPILATION_LIFO_DEFAULT_CHUNK_SIZE);
+                                 COMPILATION_LIFO_DEFAULT_CHUNK_SIZE,
+                                 telemetrySender);
   }
 
   if (!freeTasks_.reserve(numTasks)) {
     return false;
   }
   for (size_t i = 0; i < numTasks; i++) {
     freeTasks_.infallibleAppend(&tasks_[i]);
   }
@@ -731,41 +736,62 @@ bool ModuleGenerator::linkCompiledCode(C
 
   return true;
 }
 
 static bool ExecuteCompileTask(CompileTask* task, UniqueChars* error) {
   MOZ_ASSERT(task->lifo.isEmpty());
   MOZ_ASSERT(task->output.empty());
 
+#ifdef ENABLE_SPIDERMONKEY_TELEMETRY
+  int64_t startTime = PRMJ_Now();
+  int compileTimeTelemetryID;
+#endif
+
   switch (task->env.tier()) {
     case Tier::Optimized:
       switch (task->env.optimizedBackend()) {
         case OptimizedBackend::Cranelift:
           if (!CraneliftCompileFunctions(task->env, task->lifo, task->inputs,
                                          &task->output, error)) {
             return false;
           }
+#ifdef ENABLE_SPIDERMONKEY_TELEMETRY
+          compileTimeTelemetryID = JS_TELEMETRY_WASM_COMPILE_TIME_CRANELIFT_US;
+#endif
           break;
         case OptimizedBackend::Ion:
           if (!IonCompileFunctions(task->env, task->lifo, task->inputs,
                                    &task->output, error)) {
             return false;
           }
+#ifdef ENABLE_SPIDERMONKEY_TELEMETRY
+          compileTimeTelemetryID = JS_TELEMETRY_WASM_COMPILE_TIME_ION_US;
+#endif
           break;
       }
       break;
     case Tier::Baseline:
       if (!BaselineCompileFunctions(task->env, task->lifo, task->inputs,
                                     &task->output, error)) {
         return false;
       }
+#ifdef ENABLE_SPIDERMONKEY_TELEMETRY
+      compileTimeTelemetryID = JS_TELEMETRY_WASM_COMPILE_TIME_BASELINE_US;
+#endif
       break;
   }
 
+#ifdef ENABLE_SPIDERMONKEY_TELEMETRY
+  int64_t endTime = PRMJ_Now();
+  int64_t compileTimeMicros = endTime - startTime;
+
+  task->telemetrySender.addTelemetry(compileTimeTelemetryID, compileTimeMicros);
+#endif
+
   MOZ_ASSERT(task->lifo.isEmpty());
   MOZ_ASSERT(task->inputs.length() == task->output.codeRanges.length());
   task->inputs.clear();
   return true;
 }
 
 void wasm::ExecuteCompileTaskFromHelperThread(CompileTask* task) {
   TraceLoggerThread* logger = TraceLoggerForCurrentThread();
@@ -1252,17 +1278,18 @@ SharedModule ModuleGenerator::finishModu
                      std::move(dataSegments), std::move(env_->elemSegments),
                      std::move(customSections), std::move(debugUnlinkedCode),
                      std::move(debugLinkData), debugBytecode);
   if (!module) {
     return nullptr;
   }
 
   if (mode() == CompileMode::Tier1) {
-    module->startTier2(*compileArgs_, bytecode, maybeTier2Listener);
+    module->startTier2(*compileArgs_, bytecode, maybeTier2Listener,
+                       telemetrySender_);
   } else if (tier() == Tier::Serialized && maybeTier2Listener) {
     module->serialize(*linkData_, *maybeTier2Listener);
   }
 
   return module;
 }
 
 bool ModuleGenerator::finishTier2(const Module& module) {
--- a/js/src/wasm/WasmGenerator.h
+++ b/js/src/wasm/WasmGenerator.h
@@ -125,20 +125,24 @@ using ExclusiveCompileTaskState = Exclus
 // helper thread as well as, eventually, the results of compilation.
 
 struct CompileTask : public RunnableTask {
   const ModuleEnvironment& env;
   ExclusiveCompileTaskState& state;
   LifoAlloc lifo;
   FuncCompileInputVector inputs;
   CompiledCode output;
+  JSTelemetrySender telemetrySender;
 
   CompileTask(const ModuleEnvironment& env, ExclusiveCompileTaskState& state,
-              size_t defaultChunkSize)
-      : env(env), state(state), lifo(defaultChunkSize) {}
+              size_t defaultChunkSize, JSTelemetrySender telemetrySender)
+      : env(env),
+        state(state),
+        lifo(defaultChunkSize),
+        telemetrySender(telemetrySender) {}
 
   virtual ~CompileTask() = default;
 
   size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const;
 
   void runTask() override;
   ThreadType threadType() override { return ThreadType::THREAD_TYPE_WASM; }
 };
@@ -159,16 +163,17 @@ class MOZ_STACK_CLASS ModuleGenerator {
   };
   typedef Vector<CallFarJump, 0, SystemAllocPolicy> CallFarJumpVector;
 
   // Constant parameters
   SharedCompileArgs const compileArgs_;
   UniqueChars* const error_;
   const Atomic<bool>* const cancelled_;
   ModuleEnvironment* const env_;
+  JSTelemetrySender telemetrySender_;
 
   // Data that is moved into the result of finish()
   UniqueLinkData linkData_;
   UniqueMetadataTier metadataTier_;
   MutableMetadata metadata_;
 
   // Data scoped to the ModuleGenerator's lifetime
   ExclusiveCompileTaskState taskState_;
@@ -216,17 +221,19 @@ class MOZ_STACK_CLASS ModuleGenerator {
   Tier tier() const { return env_->tier(); }
   CompileMode mode() const { return env_->mode(); }
   bool debugEnabled() const { return env_->debugEnabled(); }
 
  public:
   ModuleGenerator(const CompileArgs& args, ModuleEnvironment* env,
                   const Atomic<bool>* cancelled, UniqueChars* error);
   ~ModuleGenerator();
-  MOZ_MUST_USE bool init(Metadata* maybeAsmJSMetadata = nullptr);
+  MOZ_MUST_USE bool init(
+      Metadata* maybeAsmJSMetadata = nullptr,
+      JSTelemetrySender telemetrySender = JSTelemetrySender());
 
   // Before finishFuncDefs() is called, compileFuncDef() must be called once
   // for each funcIndex in the range [0, env->numFuncDefs()).
 
   MOZ_MUST_USE bool compileFuncDef(
       uint32_t funcIndex, uint32_t lineOrBytecode, const uint8_t* begin,
       const uint8_t* end, Uint32Vector&& callSiteLineNums = Uint32Vector());
 
--- a/js/src/wasm/WasmJS.cpp
+++ b/js/src/wasm/WasmJS.cpp
@@ -770,18 +770,19 @@ bool wasm::Eval(JSContext* cx, Handle<Ty
   SharedCompileArgs compileArgs =
       CompileArgs::build(cx, std::move(scriptedCaller));
   if (!compileArgs) {
     return false;
   }
 
   UniqueChars error;
   UniqueCharsVector warnings;
-  SharedModule module =
-      CompileBuffer(*compileArgs, *bytecode, &error, &warnings);
+  JSTelemetrySender sender(cx->runtime());
+  SharedModule module = CompileBuffer(*compileArgs, *bytecode, &error,
+                                      &warnings, nullptr, sender);
   if (!module) {
     if (error) {
       JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr,
                                JSMSG_WASM_COMPILE_ERROR, error.get());
       return false;
     }
     ReportOutOfMemory(cx);
     return false;
@@ -1513,18 +1514,19 @@ bool WasmModuleObject::construct(JSConte
 
   SharedCompileArgs compileArgs = InitCompileArgs(cx, "WebAssembly.Module");
   if (!compileArgs) {
     return false;
   }
 
   UniqueChars error;
   UniqueCharsVector warnings;
-  SharedModule module =
-      CompileBuffer(*compileArgs, *bytecode, &error, &warnings);
+  JSTelemetrySender sender(cx->runtime());
+  SharedModule module = CompileBuffer(*compileArgs, *bytecode, &error,
+                                      &warnings, nullptr, sender);
   if (!module) {
     if (error) {
       JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr,
                                JSMSG_WASM_COMPILE_ERROR, error.get());
       return false;
     }
     ReportOutOfMemory(cx);
     return false;
@@ -3459,36 +3461,39 @@ static bool ResolveCompile(JSContext* cx
 struct CompileBufferTask : PromiseHelperTask {
   MutableBytes bytecode;
   SharedCompileArgs compileArgs;
   UniqueChars error;
   UniqueCharsVector warnings;
   SharedModule module;
   bool instantiate;
   PersistentRootedObject importObj;
+  JSTelemetrySender sender;
 
   CompileBufferTask(JSContext* cx, Handle<PromiseObject*> promise,
                     HandleObject importObj)
       : PromiseHelperTask(cx, promise),
         instantiate(true),
-        importObj(cx, importObj) {}
+        importObj(cx, importObj),
+        sender(cx->runtime()) {}
 
   CompileBufferTask(JSContext* cx, Handle<PromiseObject*> promise)
       : PromiseHelperTask(cx, promise), instantiate(false) {}
 
   bool init(JSContext* cx, const char* introducer) {
     compileArgs = InitCompileArgs(cx, introducer);
     if (!compileArgs) {
       return false;
     }
     return PromiseHelperTask::init(cx);
   }
 
   void execute() override {
-    module = CompileBuffer(*compileArgs, *bytecode, &error, &warnings);
+    module = CompileBuffer(*compileArgs, *bytecode, &error, &warnings, nullptr,
+                           sender);
   }
 
   bool resolve(JSContext* cx, Handle<PromiseObject*> promise) override {
     if (!module) {
       return Reject(cx, *compileArgs, promise, error);
     }
     if (!ReportCompileWarnings(cx, warnings)) {
       return false;
@@ -3727,16 +3732,18 @@ class CompileStreamTask : public Promise
   SharedModule module_;
   Maybe<size_t> streamError_;
   UniqueChars compileError_;
   UniqueCharsVector warnings_;
 
   // Set on stream thread and read racily on helper thread to abort compilation:
   Atomic<bool> streamFailed_;
 
+  JSTelemetrySender sender_;
+
   // Called on some thread before consumeChunk(), streamEnd(), streamError()):
 
   void noteResponseURLs(const char* url, const char* sourceMapUrl) override {
     if (url) {
       compileArgs_->scriptedCaller.filename = DuplicateString(url);
       compileArgs_->scriptedCaller.filenameIsURL = true;
     }
     if (sourceMapUrl) {
@@ -3872,18 +3879,18 @@ class CompileStreamTask : public Promise
   void streamEnd(JS::OptimizedEncodingListener* tier2Listener) override {
     switch (streamState_.lock().get()) {
       case Env: {
         SharedBytes bytecode = js_new<ShareableBytes>(std::move(envBytes_));
         if (!bytecode) {
           rejectAndDestroyBeforeHelperThreadStarted(StreamOOMCode);
           return;
         }
-        module_ =
-            CompileBuffer(*compileArgs_, *bytecode, &compileError_, &warnings_);
+        module_ = CompileBuffer(*compileArgs_, *bytecode, &compileError_,
+                                &warnings_, nullptr, sender_);
         setClosedAndDestroyBeforeHelperThreadStarted();
         return;
       }
       case Code:
       case Tail:
         // Unlock exclusiveStreamEnd_ before locking streamState_.
         {
           auto streamEnd = exclusiveStreamEnd_.lock();
@@ -3920,19 +3927,20 @@ class CompileStreamTask : public Promise
 
     MOZ_ASSERT(streamState_.lock().get() == Env);
     setClosedAndDestroyBeforeHelperThreadStarted();
   }
 
   // Called on a helper thread:
 
   void execute() override {
-    module_ = CompileStreaming(*compileArgs_, envBytes_, codeBytes_,
-                               exclusiveCodeBytesEnd_, exclusiveStreamEnd_,
-                               streamFailed_, &compileError_, &warnings_);
+    module_ =
+        CompileStreaming(*compileArgs_, envBytes_, codeBytes_,
+                         exclusiveCodeBytesEnd_, exclusiveStreamEnd_,
+                         streamFailed_, &compileError_, &warnings_, sender_);
 
     // When execute() returns, the CompileStreamTask will be dispatched
     // back to its JS thread to call resolve() and then be destroyed. We
     // can't let this happen until the stream has been closed lest
     // consumeChunk() or streamEnd() be called on a dead object.
     auto streamState = streamState_.lock();
     while (streamState != Closed) {
       streamState.wait(/* stream closed */);
@@ -3970,17 +3978,18 @@ class CompileStreamTask : public Promise
         streamState_(mutexid::WasmStreamStatus, Env),
         instantiate_(instantiate),
         importObj_(cx, importObj),
         compileArgs_(&compileArgs),
         codeSection_{},
         codeBytesEnd_(nullptr),
         exclusiveCodeBytesEnd_(mutexid::WasmCodeBytesEnd, nullptr),
         exclusiveStreamEnd_(mutexid::WasmStreamEnd),
-        streamFailed_(false) {
+        streamFailed_(false),
+        sender_(cx->runtime()) {
     MOZ_ASSERT_IF(importObj_, instantiate_);
   }
 };
 
 // A short-lived object that captures the arguments of a
 // WebAssembly.{compileStreaming,instantiateStreaming} while waiting for
 // the Promise<Response> to resolve to a (hopefully) Promise.
 class ResolveResponseClosure : public NativeObject {
--- a/js/src/wasm/WasmModule.cpp
+++ b/js/src/wasm/WasmModule.cpp
@@ -43,24 +43,27 @@ using namespace js;
 using namespace js::jit;
 using namespace js::wasm;
 
 class Module::Tier2GeneratorTaskImpl : public Tier2GeneratorTask {
   SharedCompileArgs compileArgs_;
   SharedBytes bytecode_;
   SharedModule module_;
   Atomic<bool> cancelled_;
+  JSTelemetrySender telemetrySender_;
 
  public:
   Tier2GeneratorTaskImpl(const CompileArgs& compileArgs,
-                         const ShareableBytes& bytecode, Module& module)
+                         const ShareableBytes& bytecode, Module& module,
+                         JSTelemetrySender telemetrySender)
       : compileArgs_(&compileArgs),
         bytecode_(&bytecode),
         module_(&module),
-        cancelled_(false) {}
+        cancelled_(false),
+        telemetrySender_(telemetrySender) {}
 
   ~Tier2GeneratorTaskImpl() override {
     module_->tier2Listener_ = nullptr;
     module_->testingTier2Active_ = false;
   }
 
   void cancel() override { cancelled_ = true; }
 
@@ -75,34 +78,37 @@ class Module::Tier2GeneratorTaskImpl : p
     // CONSUMER condition for the count of finished generators to rise.
     HelperThreadState().incWasmTier2GeneratorsFinished(locked);
 
     // The task is finished, release it.
     js_delete(this);
   }
 
   void runTask() {
-    CompileTier2(*compileArgs_, bytecode_->bytes, *module_, &cancelled_);
+    CompileTier2(*compileArgs_, bytecode_->bytes, *module_, &cancelled_,
+                 telemetrySender_);
   }
   ThreadType threadType() override {
     return ThreadType::THREAD_TYPE_WASM_TIER2;
   }
 };
 
 Module::~Module() {
   // Note: Modules can be destroyed on any thread.
   MOZ_ASSERT(!tier2Listener_);
   MOZ_ASSERT(!testingTier2Active_);
 }
 
 void Module::startTier2(const CompileArgs& args, const ShareableBytes& bytecode,
-                        JS::OptimizedEncodingListener* listener) {
+                        JS::OptimizedEncodingListener* listener,
+                        JSTelemetrySender telemetrySender) {
   MOZ_ASSERT(!testingTier2Active_);
 
-  auto task = MakeUnique<Tier2GeneratorTaskImpl>(args, bytecode, *this);
+  auto task = MakeUnique<Tier2GeneratorTaskImpl>(args, bytecode, *this,
+                                                 telemetrySender);
   if (!task) {
     return;
   }
 
   // These will be cleared asynchronously by ~Tier2GeneratorTaskImpl() if not
   // sooner by finishTier2().
   tier2Listener_ = listener;
   testingTier2Active_ = true;
--- a/js/src/wasm/WasmModule.h
+++ b/js/src/wasm/WasmModule.h
@@ -187,17 +187,18 @@ class Module : public JS::WasmModule {
                    MutableHandleWasmInstanceObject instanceObj) const;
 
   // Tier-2 compilation may be initiated after the Module is constructed at
   // most once. When tier-2 compilation completes, ModuleGenerator calls
   // finishTier2() from a helper thread, passing tier-variant data which will
   // be installed and made visible.
 
   void startTier2(const CompileArgs& args, const ShareableBytes& bytecode,
-                  JS::OptimizedEncodingListener* listener);
+                  JS::OptimizedEncodingListener* listener,
+                  JSTelemetrySender telemetrySender);
   bool finishTier2(const LinkData& linkData2, UniqueCodeTier code2) const;
 
   void testingBlockOnTier2Complete() const;
   bool testingTier2Active() const { return testingTier2Active_; }
 
   // Code caching support.
 
   size_t serializedSize(const LinkData& linkData) const;
--- a/js/xpconnect/src/XPCJSRuntime.cpp
+++ b/js/xpconnect/src/XPCJSRuntime.cpp
@@ -2643,16 +2643,31 @@ static void AccumulateTelemetryCallback(
     case JS_TELEMETRY_PRIVILEGED_PARSER_COMPILE_LAZY_AFTER_MS:
       Telemetry::Accumulate(
           Telemetry::JS_PRIVILEGED_PARSER_COMPILE_LAZY_AFTER_MS, sample);
       break;
     case JS_TELEMETRY_WEB_PARSER_COMPILE_LAZY_AFTER_MS:
       Telemetry::Accumulate(Telemetry::JS_WEB_PARSER_COMPILE_LAZY_AFTER_MS,
                             sample);
       break;
+    case JS_TELEMETRY_RUN_TIME_US:
+      Telemetry::ScalarAdd(Telemetry::ScalarID::JS_RUN_TIME_US, sample);
+      break;
+    case JS_TELEMETRY_WASM_COMPILE_TIME_BASELINE_US:
+      Telemetry::ScalarAdd(Telemetry::ScalarID::WASM_COMPILE_TIME_BASELINE_US,
+                           sample);
+      break;
+    case JS_TELEMETRY_WASM_COMPILE_TIME_ION_US:
+      Telemetry::ScalarAdd(Telemetry::ScalarID::WASM_COMPILE_TIME_ION_US,
+                           sample);
+      break;
+    case JS_TELEMETRY_WASM_COMPILE_TIME_CRANELIFT_US:
+      Telemetry::ScalarAdd(Telemetry::ScalarID::WASM_COMPILE_TIME_CRANELIFT_US,
+                           sample);
+      break;
     default:
       MOZ_ASSERT_UNREACHABLE("Unexpected JS_TELEMETRY id");
   }
 }
 
 static void SetUseCounterCallback(JSObject* obj, JSUseCounter counter) {
   switch (counter) {
     case JSUseCounter::ASMJS:
--- a/toolkit/components/telemetry/Scalars.yaml
+++ b/toolkit/components/telemetry/Scalars.yaml
@@ -4581,16 +4581,94 @@ script.preloader:
     release_channel_collection: opt-out
     products:
       - 'firefox'
       - 'fennec'
     record_in_processes:
       - 'main'
       - 'content'
 
+wasm:
+  compile_time_baseline_us:
+    bug_numbers:
+      - 1656638
+    description:
+      How many microseconds we spent in the baseline WebAssembly compiler.
+    expires: "88"
+    keyed: false
+    kind: uint
+    notification_emails:
+      - cfallin@mozilla.com
+      - bbouvier@mozilla.com
+      - jseward@mozilla.com
+    release_channel_collection: opt-in
+    products:
+      - 'firefox'
+    record_in_processes:
+      - 'main'
+      - 'content'
+
+  compile_time_cranelift_us:
+    bug_numbers:
+      - 1656638
+    description:
+      How many microseconds we spent in the Cranelift WebAssembly compiler.
+    expires: "88"
+    keyed: false
+    kind: uint
+    notification_emails:
+      - cfallin@mozilla.com
+      - bbouvier@mozilla.com
+      - jseward@mozilla.com
+    release_channel_collection: opt-in
+    products:
+      - 'firefox'
+    record_in_processes:
+      - 'main'
+
+  compile_time_ion_us:
+    bug_numbers:
+      - 1656638
+    description:
+      How many microseconds we spent in the IonMonkey WebAssembly compiler.
+    expires: "88"
+    keyed: false
+    kind: uint
+    notification_emails:
+      - cfallin@mozilla.com
+      - bbouvier@mozilla.com
+      - jseward@mozilla.com
+    release_channel_collection: opt-in
+    products:
+      - 'firefox'
+    record_in_processes:
+      - 'main'
+      - 'content'
+
+js:
+  run_time_us:
+    bug_numbers:
+      - 1656638
+    description:
+      How many microseconds we spent in running user code in SpiderMonkey,
+      including JavaScript code and WebAssembly code.
+    expires: "88"
+    keyed: false
+    kind: uint
+    notification_emails:
+      - cfallin@mozilla.com
+      - bbouvier@mozilla.com
+      - jseward@mozilla.com
+    release_channel_collection: opt-in
+    products:
+      - 'firefox'
+    record_in_processes:
+      - 'main'
+      - 'content'
+
 networking:
   nss_initialization:
     bug_numbers:
       - 1628734
     description:
       The time in milliseconds to initialize the NSS component in the
       parent process.
     expires: "never"