summaryrefslogtreecommitdiffstats
path: root/js/src/devtools
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /js/src/devtools
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--js/src/devtools/Instruments.cpp223
-rw-r--r--js/src/devtools/Instruments.h23
-rw-r--r--js/src/devtools/automation/README39
-rw-r--r--js/src/devtools/automation/arm64-jittests-timeouts.txt2
-rw-r--r--js/src/devtools/automation/arm64-jstests-slow.txt52
-rwxr-xr-xjs/src/devtools/automation/autospider.py688
-rw-r--r--js/src/devtools/automation/cgc-jittest-timeouts.txt53
-rw-r--r--js/src/devtools/automation/cgc-jstests-slow.txt65
-rw-r--r--js/src/devtools/automation/smoosh-jittest-known-failure.txt4
-rw-r--r--js/src/devtools/automation/smoosh-jstests-known-failure-and-slow.txt81
-rw-r--r--js/src/devtools/automation/smoosh-jstests-known-failure.txt73
-rw-r--r--js/src/devtools/automation/smoosh-jstests-slow.txt0
-rw-r--r--js/src/devtools/automation/tsan-slow.txt23
-rw-r--r--js/src/devtools/automation/variants/arm-sim7
-rw-r--r--js/src/devtools/automation/variants/arm-sim-osx6
-rw-r--r--js/src/devtools/automation/variants/arm64-sim10
-rw-r--r--js/src/devtools/automation/variants/asan10
-rw-r--r--js/src/devtools/automation/variants/compacting14
-rw-r--r--js/src/devtools/automation/variants/dtrace5
-rw-r--r--js/src/devtools/automation/variants/fuzzilli14
-rw-r--r--js/src/devtools/automation/variants/fuzzing13
-rw-r--r--js/src/devtools/automation/variants/gdb16
-rw-r--r--js/src/devtools/automation/variants/msan14
-rw-r--r--js/src/devtools/automation/variants/nojit4
-rw-r--r--js/src/devtools/automation/variants/nojit-debug4
-rw-r--r--js/src/devtools/automation/variants/nonunified10
-rw-r--r--js/src/devtools/automation/variants/pbl4
-rw-r--r--js/src/devtools/automation/variants/plain9
-rw-r--r--js/src/devtools/automation/variants/plaindebug7
-rw-r--r--js/src/devtools/automation/variants/rootanalysis9
-rw-r--r--js/src/devtools/automation/variants/rtdebug8
-rw-r--r--js/src/devtools/automation/variants/smoosh8
-rw-r--r--js/src/devtools/automation/variants/smooshdebug8
-rw-r--r--js/src/devtools/automation/variants/temporaldebug7
-rw-r--r--js/src/devtools/automation/variants/tsan12
-rw-r--r--js/src/devtools/automation/variants/warnaserr4
-rw-r--r--js/src/devtools/automation/variants/warnaserrdebug4
-rw-r--r--js/src/devtools/automation/variants/wasi14
-rw-r--r--js/src/devtools/automation/variants/wasi-intl14
-rw-r--r--js/src/devtools/automation/variants/wasi-pbl14
-rw-r--r--js/src/devtools/automation/variants/wasm-noexperimental7
-rw-r--r--js/src/devtools/gc-ubench/argparse.js127
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/bigTextNodes.js42
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/deepWeakMap.js40
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/events.js40
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/expandoEvents.js41
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/globalArrayArrayLiteral.js32
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/globalArrayBuffer.js36
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/globalArrayFgFinalized.js37
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/globalArrayLargeArray.js34
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/globalArrayLargeObject.js36
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/globalArrayNewObject.js33
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/globalArrayObjectLiteral.js32
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/globalArrayReallocArray.js34
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/largeArrayPropertyAndElements.js40
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/noAllocation.js10
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/pairCyclicWeakMap.js46
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/propertyTreeSplitting.js24
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/selfCyclicWeakMap.js42
-rw-r--r--js/src/devtools/gc-ubench/benchmarks/textNodes.js38
-rw-r--r--js/src/devtools/gc-ubench/harness.js328
-rw-r--r--js/src/devtools/gc-ubench/index.html90
-rw-r--r--js/src/devtools/gc-ubench/perf.js217
-rw-r--r--js/src/devtools/gc-ubench/scheduler.js64
-rw-r--r--js/src/devtools/gc-ubench/sequencer.js298
-rw-r--r--js/src/devtools/gc-ubench/shell-bench.js147
-rw-r--r--js/src/devtools/gc-ubench/spidermonkey.js57
-rw-r--r--js/src/devtools/gc-ubench/test_list.js20
-rw-r--r--js/src/devtools/gc-ubench/ui.js700
-rw-r--r--js/src/devtools/gc-ubench/v8.js42
-rw-r--r--js/src/devtools/gc/README.txt6
-rw-r--r--js/src/devtools/gc/gc-test.py191
-rw-r--r--js/src/devtools/gc/tests/clock.js35
-rw-r--r--js/src/devtools/gc/tests/dslots.js26
-rw-r--r--js/src/devtools/gc/tests/loops.js55
-rw-r--r--js/src/devtools/gc/tests/objGraph.js37
-rw-r--r--js/src/devtools/gnuplot/gcTimer.gnu24
-rw-r--r--js/src/devtools/iongraph/.gitignore3
-rw-r--r--js/src/devtools/iongraph/LICENSE7
-rw-r--r--js/src/devtools/iongraph/README.md32
-rwxr-xr-xjs/src/devtools/iongraph/iongraph486
-rw-r--r--js/src/devtools/javascript-trace.d34
-rwxr-xr-xjs/src/devtools/octane-csv.sh76
-rwxr-xr-xjs/src/devtools/plot-octane.R38
-rw-r--r--js/src/devtools/release/release-notes195
-rw-r--r--js/src/devtools/rootAnalysis/CFG.js1178
-rw-r--r--js/src/devtools/rootAnalysis/README.md3
-rwxr-xr-xjs/src/devtools/rootAnalysis/analyze.py462
-rw-r--r--js/src/devtools/rootAnalysis/analyzeHeapWrites.js1396
-rw-r--r--js/src/devtools/rootAnalysis/analyzeRoots.js963
-rw-r--r--js/src/devtools/rootAnalysis/annotations.js489
-rw-r--r--js/src/devtools/rootAnalysis/build.js15
-rw-r--r--js/src/devtools/rootAnalysis/build/sixgill-b2g.manifest10
-rw-r--r--js/src/devtools/rootAnalysis/build/sixgill.manifest10
-rw-r--r--js/src/devtools/rootAnalysis/callgraph.js233
-rw-r--r--js/src/devtools/rootAnalysis/computeCallgraph.js434
-rw-r--r--js/src/devtools/rootAnalysis/computeGCFunctions.js113
-rw-r--r--js/src/devtools/rootAnalysis/computeGCTypes.js550
-rw-r--r--js/src/devtools/rootAnalysis/dumpCFG.js273
-rw-r--r--js/src/devtools/rootAnalysis/expect.b2g.json3
-rw-r--r--js/src/devtools/rootAnalysis/expect.browser.json3
-rw-r--r--js/src/devtools/rootAnalysis/expect.shell.json3
-rwxr-xr-xjs/src/devtools/rootAnalysis/explain.py345
-rwxr-xr-xjs/src/devtools/rootAnalysis/gen-hazards.sh15
-rw-r--r--js/src/devtools/rootAnalysis/loadCallgraph.js590
-rw-r--r--js/src/devtools/rootAnalysis/mach_commands.py690
-rw-r--r--js/src/devtools/rootAnalysis/mergeJSON.js26
-rw-r--r--js/src/devtools/rootAnalysis/mozconfig.browser19
-rw-r--r--js/src/devtools/rootAnalysis/mozconfig.common37
-rw-r--r--js/src/devtools/rootAnalysis/mozconfig.haz_shell18
-rw-r--r--js/src/devtools/rootAnalysis/mozconfig.js16
-rwxr-xr-xjs/src/devtools/rootAnalysis/run-analysis.sh4
-rwxr-xr-xjs/src/devtools/rootAnalysis/run-test.py154
-rwxr-xr-xjs/src/devtools/rootAnalysis/run_complete384
-rw-r--r--js/src/devtools/rootAnalysis/t/exceptions/source.cpp57
-rw-r--r--js/src/devtools/rootAnalysis/t/exceptions/test.py21
-rw-r--r--js/src/devtools/rootAnalysis/t/graph/source.cpp90
-rw-r--r--js/src/devtools/rootAnalysis/t/graph/test.py54
-rw-r--r--js/src/devtools/rootAnalysis/t/hazards/source.cpp566
-rw-r--r--js/src/devtools/rootAnalysis/t/hazards/test.py121
-rw-r--r--js/src/devtools/rootAnalysis/t/sixgill-tree/source.cpp76
-rw-r--r--js/src/devtools/rootAnalysis/t/sixgill-tree/test.py63
-rw-r--r--js/src/devtools/rootAnalysis/t/sixgill.py70
-rw-r--r--js/src/devtools/rootAnalysis/t/suppression/source.cpp72
-rw-r--r--js/src/devtools/rootAnalysis/t/suppression/test.py21
-rw-r--r--js/src/devtools/rootAnalysis/t/testlib.py249
-rw-r--r--js/src/devtools/rootAnalysis/t/types/source.cpp167
-rw-r--r--js/src/devtools/rootAnalysis/t/types/test.py16
-rw-r--r--js/src/devtools/rootAnalysis/t/virtual/source.cpp366
-rw-r--r--js/src/devtools/rootAnalysis/t/virtual/test.py99
-rw-r--r--js/src/devtools/rootAnalysis/utility.js422
-rw-r--r--js/src/devtools/vprof/manifest.mk7
-rw-r--r--js/src/devtools/vprof/readme.txt97
-rw-r--r--js/src/devtools/vprof/testVprofMT.c88
-rw-r--r--js/src/devtools/vprof/vprof.cpp359
-rw-r--r--js/src/devtools/vprof/vprof.h270
136 files changed, 17341 insertions, 0 deletions
diff --git a/js/src/devtools/Instruments.cpp b/js/src/devtools/Instruments.cpp
new file mode 100644
index 0000000000..39fbe882b8
--- /dev/null
+++ b/js/src/devtools/Instruments.cpp
@@ -0,0 +1,223 @@
+/* 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/. */
+
+#include "Instruments.h"
+#include "mozilla/Attributes.h"
+
+#ifdef __APPLE__
+
+# include <dlfcn.h>
+# include <CoreFoundation/CoreFoundation.h>
+# include <unistd.h>
+
+// There are now 2 paths to the DTPerformanceSession framework. We try to load
+// the one contained in /Applications/Xcode.app first, falling back to the one
+// contained in /Library/Developer/4.0/Instruments.
+# define DTPerformanceLibraryPath \
+ "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/" \
+ "DTPerformanceSession.framework/Versions/Current/DTPerformanceSession"
+# define OldDTPerformanceLibraryPath \
+ "/Library/Developer/4.0/Instruments/Frameworks/" \
+ "DTPerformanceSession.framework/Versions/Current/DTPerformanceSession"
+
+extern "C" {
+
+typedef CFTypeRef DTPerformanceSessionRef;
+
+# define DTPerformanceSession_TimeProfiler \
+ "com.apple.instruments.dtps.timeprofiler"
+// DTPerformanceSession_Option_SamplingInterval is measured in microseconds
+# define DTPerformanceSession_Option_SamplingInterval \
+ "com.apple.instruments.dtps.option.samplinginterval"
+
+typedef void (*dtps_errorcallback_t)(CFStringRef, CFErrorRef);
+typedef DTPerformanceSessionRef (*DTPerformanceSessionCreateFunction)(
+ CFStringRef, CFStringRef, CFDictionaryRef, CFErrorRef*);
+typedef bool (*DTPerformanceSessionAddInstrumentFunction)(
+ DTPerformanceSessionRef, CFStringRef, CFDictionaryRef, dtps_errorcallback_t,
+ CFErrorRef*);
+typedef bool (*DTPerformanceSessionIsRecordingFunction)(
+ DTPerformanceSessionRef);
+typedef bool (*DTPerformanceSessionStartFunction)(DTPerformanceSessionRef,
+ CFArrayRef, CFErrorRef*);
+typedef bool (*DTPerformanceSessionStopFunction)(DTPerformanceSessionRef,
+ CFArrayRef, CFErrorRef*);
+typedef bool (*DTPerformanceSessionSaveFunction)(DTPerformanceSessionRef,
+ CFStringRef, CFErrorRef*);
+
+} // extern "C"
+
+namespace Instruments {
+
+static const int kSamplingInterval = 20; // microseconds
+
+template <typename T>
+class AutoReleased {
+ public:
+ MOZ_IMPLICIT AutoReleased(T aTypeRef) : mTypeRef(aTypeRef) {}
+ ~AutoReleased() {
+ if (mTypeRef) {
+ CFRelease(mTypeRef);
+ }
+ }
+
+ operator T() { return mTypeRef; }
+
+ private:
+ T mTypeRef;
+};
+
+# define DTPERFORMANCE_SYMBOLS \
+ SYMBOL(DTPerformanceSessionCreate) \
+ SYMBOL(DTPerformanceSessionAddInstrument) \
+ SYMBOL(DTPerformanceSessionIsRecording) \
+ SYMBOL(DTPerformanceSessionStart) \
+ SYMBOL(DTPerformanceSessionStop) \
+ SYMBOL(DTPerformanceSessionSave)
+
+# define SYMBOL(_sym) _sym##Function _sym = nullptr;
+
+DTPERFORMANCE_SYMBOLS
+
+# undef SYMBOL
+
+void* LoadDTPerformanceLibraries(bool dontLoad) {
+ int flags = RTLD_LAZY | RTLD_LOCAL | RTLD_NODELETE;
+ if (dontLoad) {
+ flags |= RTLD_NOLOAD;
+ }
+
+ void* DTPerformanceLibrary = dlopen(DTPerformanceLibraryPath, flags);
+ if (!DTPerformanceLibrary) {
+ DTPerformanceLibrary = dlopen(OldDTPerformanceLibraryPath, flags);
+ }
+ return DTPerformanceLibrary;
+}
+
+bool LoadDTPerformanceLibrary() {
+ void* DTPerformanceLibrary = LoadDTPerformanceLibraries(true);
+ if (!DTPerformanceLibrary) {
+ DTPerformanceLibrary = LoadDTPerformanceLibraries(false);
+ if (!DTPerformanceLibrary) {
+ return false;
+ }
+ }
+
+# define SYMBOL(_sym) \
+ _sym = \
+ reinterpret_cast<_sym##Function>(dlsym(DTPerformanceLibrary, #_sym)); \
+ if (!_sym) { \
+ dlclose(DTPerformanceLibrary); \
+ DTPerformanceLibrary = nullptr; \
+ return false; \
+ }
+
+ DTPERFORMANCE_SYMBOLS
+
+# undef SYMBOL
+
+ dlclose(DTPerformanceLibrary);
+
+ return true;
+}
+
+static DTPerformanceSessionRef gSession;
+
+bool Error(CFErrorRef error) {
+ if (gSession) {
+ CFErrorRef unused = nullptr;
+ DTPerformanceSessionStop(gSession, nullptr, &unused);
+ CFRelease(gSession);
+ gSession = nullptr;
+ }
+# ifdef DEBUG
+ AutoReleased<CFDataRef> data = CFStringCreateExternalRepresentation(
+ nullptr, CFErrorCopyDescription(error), kCFStringEncodingUTF8, '?');
+ if (data != nullptr) {
+ printf("%.*s\n\n", (int)CFDataGetLength(data), CFDataGetBytePtr(data));
+ }
+# endif
+ return false;
+}
+
+bool Start(pid_t pid) {
+ if (gSession) {
+ return false;
+ }
+
+ if (!LoadDTPerformanceLibrary()) {
+ return false;
+ }
+
+ AutoReleased<CFStringRef> process =
+ CFStringCreateWithFormat(kCFAllocatorDefault, nullptr, CFSTR("%d"), pid);
+ if (!process) {
+ return false;
+ }
+ CFErrorRef error = nullptr;
+ gSession = DTPerformanceSessionCreate(nullptr, process, nullptr, &error);
+ if (!gSession) {
+ return Error(error);
+ }
+
+ AutoReleased<CFNumberRef> interval =
+ CFNumberCreate(0, kCFNumberIntType, &kSamplingInterval);
+ if (!interval) {
+ return false;
+ }
+ CFStringRef keys[1] = {CFSTR(DTPerformanceSession_Option_SamplingInterval)};
+ CFNumberRef values[1] = {interval};
+ AutoReleased<CFDictionaryRef> options = CFDictionaryCreate(
+ kCFAllocatorDefault, (const void**)keys, (const void**)values, 1,
+ &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+ if (!options) {
+ return false;
+ }
+
+ if (!DTPerformanceSessionAddInstrument(
+ gSession, CFSTR(DTPerformanceSession_TimeProfiler), options, nullptr,
+ &error)) {
+ return Error(error);
+ }
+
+ return Resume();
+}
+
+void Pause() {
+ if (gSession && DTPerformanceSessionIsRecording(gSession)) {
+ CFErrorRef error = nullptr;
+ if (!DTPerformanceSessionStop(gSession, nullptr, &error)) {
+ Error(error);
+ }
+ }
+}
+
+bool Resume() {
+ if (!gSession) {
+ return false;
+ }
+
+ CFErrorRef error = nullptr;
+ return DTPerformanceSessionStart(gSession, nullptr, &error) || Error(error);
+}
+
+void Stop(const char* profileName) {
+ Pause();
+
+ CFErrorRef error = nullptr;
+ AutoReleased<CFStringRef> name =
+ CFStringCreateWithFormat(kCFAllocatorDefault, nullptr, CFSTR("%s%s"),
+ "/tmp/", profileName ? profileName : "mozilla");
+ if (!DTPerformanceSessionSave(gSession, name, &error)) {
+ Error(error);
+ return;
+ }
+
+ CFRelease(gSession);
+ gSession = nullptr;
+}
+
+} // namespace Instruments
+
+#endif /* __APPLE__ */
diff --git a/js/src/devtools/Instruments.h b/js/src/devtools/Instruments.h
new file mode 100644
index 0000000000..9e8f197755
--- /dev/null
+++ b/js/src/devtools/Instruments.h
@@ -0,0 +1,23 @@
+/* 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 devtools_Instruments_h
+#define devtools_Instruments_h
+
+#ifdef __APPLE__
+
+# include <unistd.h>
+
+namespace Instruments {
+
+bool Start(pid_t pid);
+void Pause();
+bool Resume();
+void Stop(const char* profileName);
+
+} // namespace Instruments
+
+#endif /* __APPLE__ */
+
+#endif /* devtools_Instruments_h */
diff --git a/js/src/devtools/automation/README b/js/src/devtools/automation/README
new file mode 100644
index 0000000000..70f56fd230
--- /dev/null
+++ b/js/src/devtools/automation/README
@@ -0,0 +1,39 @@
+autospider.py is intended both as the harness for running automation builds, as
+well as a way to easily reproduce automation builds on a developer workstation.
+Some brave souls also use it as the basis for doing their normal local builds.
+
+Basic usage:
+
+ ./js/src/devtools/automation/autospider.py plain
+
+The script may be run from any directory. This will configure and build the
+source, then run a series of tests. See the --help message for many different
+ways of suppressing various actions or changing the output.
+
+The different possible build+test configurations are controlled mostly by JSON
+files in a variants/ directory (eg there is a .../variants/plain file for the
+above command).
+
+In automation, the test jobs will run with a dynamically loaded library that
+catches crashes and generates minidumps, so that autospider.py can produce a
+readable stack trace at the end of the run. Currently this library is only
+available on linux64, and is built via the following procedure:
+
+ % git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
+ % export PATH=$PATH:$(pwd)/depot_tools
+ % mkdir breakpad
+ % cd breakpad
+ # python must be python2.7
+ % fetch breakpad
+ % cd src
+ % git fetch https://github.com/hotsphink/breakpad injector
+ % git checkout FETCH_HEAD
+ % cd ..
+ % mkdir obj
+ % cd obj
+ # Possibly set $PATH to include a recent gcc
+ % ../src/configure --enable-static
+ % mkdir ../root
+ % make install DESTDIR=$(pwd)/../root
+
+The shared library will now be in root/usr/local/lib64/libbreakpadinjector.so
diff --git a/js/src/devtools/automation/arm64-jittests-timeouts.txt b/js/src/devtools/automation/arm64-jittests-timeouts.txt
new file mode 100644
index 0000000000..2c23ca3535
--- /dev/null
+++ b/js/src/devtools/automation/arm64-jittests-timeouts.txt
@@ -0,0 +1,2 @@
+basic/bug1610192.js
+ion/pow-base-power-of-two.js
diff --git a/js/src/devtools/automation/arm64-jstests-slow.txt b/js/src/devtools/automation/arm64-jstests-slow.txt
new file mode 100644
index 0000000000..c4c09ac79b
--- /dev/null
+++ b/js/src/devtools/automation/arm64-jstests-slow.txt
@@ -0,0 +1,52 @@
+non262/object/15.2.3.6-dictionary-redefinition-01-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-02-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-03-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-04-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-05-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-06-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-07-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-08-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-09-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-10-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-11-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-12-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-13-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-14-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-15-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-16-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-17-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-18-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-19-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-20-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-21-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-22-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-23-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-24-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-25-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-26-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-27-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-30-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-31-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-32-of-32.js
+non262/object/15.2.3.6-middle-redefinition-1-of-8.js
+non262/object/15.2.3.6-middle-redefinition-2-of-8.js
+non262/object/15.2.3.6-middle-redefinition-3-of-8.js
+non262/object/15.2.3.6-middle-redefinition-4-of-8.js
+non262/object/15.2.3.6-middle-redefinition-5-of-8.js
+non262/object/15.2.3.6-middle-redefinition-6-of-8.js
+non262/object/15.2.3.6-middle-redefinition-7-of-8.js
+non262/object/15.2.3.6-middle-redefinition-8-of-8.js
+non262/object/15.2.3.6-redefinition-1-of-4.js
+non262/object/15.2.3.6-redefinition-2-of-4.js
+non262/object/15.2.3.6-redefinition-3-of-4.js
+non262/object/15.2.3.6-redefinition-4-of-4.js
+non262/extensions/clone-complex-object.js
+non262/reflect-parse/classes.js
+non262/reflect-parse/destructuring-variable-declarations.js
+test262/built-ins/RegExp/CharacterClassEscapes/character-class-non-whitespace-class-escape-flags-u.js
+test262/built-ins/RegExp/CharacterClassEscapes/character-class-non-digit-class-escape-flags-u.js
+test262/built-ins/RegExp/CharacterClassEscapes/character-class-non-word-class-escape-flags-u.js
+test262/built-ins/RegExp/property-escapes/generated/
+test262/built-ins/RegExp/property-escapes/generated/General_Category_-_Letter.js
+test262/built-ins/RegExp/property-escapes/generated/General_Category_-_Other.js
+test262/built-ins/RegExp/property-escapes/generated/General_Category_-_Unassigned.js
diff --git a/js/src/devtools/automation/autospider.py b/js/src/devtools/automation/autospider.py
new file mode 100755
index 0000000000..a49d2fd505
--- /dev/null
+++ b/js/src/devtools/automation/autospider.py
@@ -0,0 +1,688 @@
+#!/usr/bin/env python3
+# 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/.
+
+
+import argparse
+import json
+import logging
+import multiprocessing
+import os
+import platform
+import shlex
+import shutil
+import subprocess
+import sys
+from collections import Counter, namedtuple
+from logging import info
+from os import environ as env
+from pathlib import Path
+from subprocess import Popen
+from threading import Timer
+
+Dirs = namedtuple("Dirs", ["scripts", "js_src", "source", "fetches"])
+
+
+def directories(pathmodule, cwd, fixup=lambda s: s):
+ scripts = pathmodule.join(fixup(cwd), fixup(pathmodule.dirname(__file__)))
+ js_src = pathmodule.abspath(pathmodule.join(scripts, "..", ".."))
+ source = pathmodule.abspath(pathmodule.join(js_src, "..", ".."))
+ mozbuild = pathmodule.abspath(
+ # os.path.expanduser does not work on Windows.
+ env.get("MOZBUILD_STATE_PATH")
+ or pathmodule.join(Path.home(), ".mozbuild")
+ )
+ fetches = pathmodule.abspath(env.get("MOZ_FETCHES_DIR", mozbuild))
+ return Dirs(scripts, js_src, source, fetches)
+
+
+def quote(s):
+ # shlex quotes for the purpose of passing to the native shell, which is cmd
+ # on Windows, and therefore will not replace backslashed paths with forward
+ # slashes. When such a path is passed to sh, the backslashes will be
+ # interpreted as escape sequences.
+ return shlex.quote(s).replace("\\", "/")
+
+
+# Some scripts will be called with sh, which cannot use backslashed
+# paths. So for direct subprocess.* invocation, use normal paths from
+# DIR, but when running under the shell, use POSIX style paths.
+DIR = directories(os.path, os.getcwd())
+
+AUTOMATION = env.get("AUTOMATION", False)
+
+parser = argparse.ArgumentParser(description="Run a spidermonkey shell build job")
+parser.add_argument(
+ "--verbose",
+ action="store_true",
+ default=AUTOMATION,
+ help="display additional logging info",
+)
+parser.add_argument(
+ "--dep", action="store_true", help="do not clobber the objdir before building"
+)
+parser.add_argument(
+ "--keep",
+ action="store_true",
+ help="do not delete the sanitizer output directory (for testing)",
+)
+parser.add_argument(
+ "--platform",
+ "-p",
+ type=str,
+ metavar="PLATFORM",
+ default="",
+ help='build platform, including a suffix ("-debug" or "") used '
+ 'by buildbot to override the variant\'s "debug" setting. The platform can be '
+ "used to specify 32 vs 64 bits.",
+)
+parser.add_argument(
+ "--timeout",
+ "-t",
+ type=int,
+ metavar="TIMEOUT",
+ default=12600,
+ help="kill job after TIMEOUT seconds",
+)
+parser.add_argument(
+ "--objdir",
+ type=str,
+ metavar="DIR",
+ # The real default must be set later so that OBJDIR can be
+ # relative to the srcdir.
+ default=env.get("OBJDIR"),
+ help="object directory",
+)
+group = parser.add_mutually_exclusive_group()
+group.add_argument(
+ "--optimize",
+ action="store_true",
+ help="generate an optimized build. Overrides variant setting.",
+)
+group.add_argument(
+ "--no-optimize",
+ action="store_false",
+ dest="optimize",
+ help="generate a non-optimized build. Overrides variant setting.",
+)
+group.set_defaults(optimize=None)
+group = parser.add_mutually_exclusive_group()
+group.add_argument(
+ "--debug",
+ action="store_true",
+ help="generate a debug build. Overrides variant setting.",
+)
+group.add_argument(
+ "--no-debug",
+ action="store_false",
+ dest="debug",
+ help="generate a non-debug build. Overrides variant setting.",
+)
+group.set_defaults(debug=None)
+group = parser.add_mutually_exclusive_group()
+group.add_argument(
+ "--jemalloc",
+ action="store_true",
+ dest="jemalloc",
+ help="use mozilla's jemalloc instead of the default allocator",
+)
+group.add_argument(
+ "--no-jemalloc",
+ action="store_false",
+ dest="jemalloc",
+ help="use the default allocator instead of mozilla's jemalloc",
+)
+group.set_defaults(jemalloc=None)
+parser.add_argument(
+ "--run-tests",
+ "--tests",
+ type=str,
+ metavar="TESTSUITE",
+ default="",
+ help="comma-separated set of test suites to add to the variant's default set",
+)
+parser.add_argument(
+ "--skip-tests",
+ "--skip",
+ type=str,
+ metavar="TESTSUITE",
+ default="",
+ help="comma-separated set of test suites to remove from the variant's default "
+ "set",
+)
+parser.add_argument(
+ "--build-only",
+ "--build",
+ dest="skip_tests",
+ action="store_const",
+ const="all",
+ help="only do a build, do not run any tests",
+)
+parser.add_argument(
+ "--nobuild",
+ action="store_true",
+ help="Do not do a build. Rerun tests on existing build.",
+)
+parser.add_argument(
+ "variant", type=str, help="type of job requested, see variants/ subdir"
+)
+args = parser.parse_args()
+
+logging.basicConfig(level=logging.INFO, format="%(message)s")
+
+env["CPP_UNIT_TESTS_DIR_JS_SRC"] = DIR.js_src
+if AUTOMATION and platform.system() == "Windows":
+ # build/win{32,64}/mozconfig.vs-latest uses TOOLTOOL_DIR to set VSPATH.
+ env["TOOLTOOL_DIR"] = DIR.fetches
+
+OBJDIR = args.objdir or os.path.join(DIR.source, "obj-spider")
+OBJDIR = os.path.abspath(OBJDIR)
+OUTDIR = os.path.join(OBJDIR, "out")
+MAKE = env.get("MAKE", "make")
+PYTHON = sys.executable
+
+for d in DIR._fields:
+ info("DIR.{name} = {dir}".format(name=d, dir=getattr(DIR, d)))
+
+
+def ensure_dir_exists(
+ name, clobber=True, creation_marker_filename="CREATED-BY-AUTOSPIDER"
+):
+ if creation_marker_filename is None:
+ marker = None
+ else:
+ marker = os.path.join(name, creation_marker_filename)
+ if clobber:
+ if (
+ not AUTOMATION
+ and marker
+ and os.path.exists(name)
+ and not os.path.exists(marker)
+ ):
+ raise Exception(
+ "Refusing to delete objdir %s because it was not created by autospider"
+ % name
+ )
+ shutil.rmtree(name, ignore_errors=True)
+ try:
+ os.mkdir(name)
+ if marker:
+ open(marker, "a").close()
+ except OSError:
+ if clobber:
+ raise
+
+
+with open(os.path.join(DIR.scripts, "variants", args.variant)) as fh:
+ variant = json.load(fh)
+
+CONFIGURE_ARGS = variant["configure-args"]
+
+compiler = variant.get("compiler")
+if compiler != "gcc" and "clang-plugin" not in CONFIGURE_ARGS:
+ CONFIGURE_ARGS += " --enable-clang-plugin"
+
+if compiler == "gcc":
+ if AUTOMATION:
+ fetches = env["MOZ_FETCHES_DIR"]
+ env["CC"] = os.path.join(fetches, "gcc", "bin", "gcc")
+ env["CXX"] = os.path.join(fetches, "gcc", "bin", "g++")
+ else:
+ env["CC"] = "gcc"
+ env["CXX"] = "g++"
+
+opt = args.optimize
+if opt is None:
+ opt = variant.get("optimize")
+if opt is not None:
+ CONFIGURE_ARGS += " --enable-optimize" if opt else " --disable-optimize"
+
+opt = args.debug
+if opt is None:
+ opt = variant.get("debug")
+if opt is not None:
+ CONFIGURE_ARGS += " --enable-debug" if opt else " --disable-debug"
+
+opt = args.jemalloc
+if opt is not None:
+ CONFIGURE_ARGS += " --enable-jemalloc" if opt else " --disable-jemalloc"
+
+# By default, we build with NSPR, even if not specified. But we actively allow
+# builds to disable NSPR.
+opt = variant.get("nspr")
+if opt is None or opt:
+ CONFIGURE_ARGS += " --enable-nspr-build"
+
+# Some of the variants request a particular word size (eg ARM simulators).
+word_bits = variant.get("bits")
+
+# On Linux and Windows, we build 32- and 64-bit versions on a 64 bit
+# host, so the caller has to specify what is desired.
+if word_bits is None and args.platform:
+ platform_arch = args.platform.split("-")[0]
+ if platform_arch in ("win32", "linux"):
+ word_bits = 32
+ elif platform_arch in ("win64", "linux64"):
+ word_bits = 64
+
+# Fall back to the word size of the host.
+if word_bits is None:
+ word_bits = 64 if platform.architecture()[0] == "64bit" else 32
+
+# Need a platform name to use as a key in variant files.
+if args.platform:
+ variant_platform = args.platform.split("-")[0]
+elif platform.system() == "Windows":
+ variant_platform = "win64" if word_bits == 64 else "win32"
+elif platform.system() == "Linux":
+ variant_platform = "linux64" if word_bits == 64 else "linux"
+elif platform.system() == "Darwin":
+ variant_platform = "macosx64"
+else:
+ variant_platform = "other"
+
+env["LD_LIBRARY_PATH"] = ":".join(
+ d
+ for d in [
+ # for libnspr etc.
+ os.path.join(OBJDIR, "dist", "bin"),
+ # existing search path, if any
+ env.get("LD_LIBRARY_PATH"),
+ ]
+ if d is not None
+)
+
+os.environ["SOURCE"] = DIR.source
+if platform.system() == "Windows":
+ MAKE = env.get("MAKE", "mozmake")
+
+# Configure flags, based on word length and cross-compilation
+if word_bits == 32:
+ if platform.system() == "Windows":
+ CONFIGURE_ARGS += " --target=i686-pc-windows-msvc"
+ elif platform.system() == "Linux":
+ if not platform.machine().startswith("arm"):
+ CONFIGURE_ARGS += " --target=i686-pc-linux"
+
+ # Add SSE2 support for x86/x64 architectures.
+ if not platform.machine().startswith("arm"):
+ if platform.system() == "Windows":
+ sse_flags = "-arch:SSE2"
+ else:
+ sse_flags = "-msse -msse2 -mfpmath=sse"
+ env["CCFLAGS"] = "{0} {1}".format(env.get("CCFLAGS", ""), sse_flags)
+ env["CXXFLAGS"] = "{0} {1}".format(env.get("CXXFLAGS", ""), sse_flags)
+else:
+ if platform.system() == "Windows":
+ CONFIGURE_ARGS += " --target=x86_64-pc-windows-msvc"
+
+if platform.system() == "Linux" and AUTOMATION:
+ CONFIGURE_ARGS = "--enable-stdcxx-compat " + CONFIGURE_ARGS
+
+# Timeouts.
+ACTIVE_PROCESSES = set()
+
+
+def killall():
+ for proc in ACTIVE_PROCESSES:
+ proc.kill()
+ ACTIVE_PROCESSES.clear()
+
+
+timer = Timer(args.timeout, killall)
+timer.daemon = True
+timer.start()
+
+ensure_dir_exists(OBJDIR, clobber=not args.dep and not args.nobuild)
+ensure_dir_exists(OUTDIR, clobber=not args.keep)
+
+# Any jobs that wish to produce additional output can save them into the upload
+# directory if there is such a thing, falling back to OBJDIR.
+env.setdefault("MOZ_UPLOAD_DIR", OBJDIR)
+ensure_dir_exists(env["MOZ_UPLOAD_DIR"], clobber=False, creation_marker_filename=None)
+info("MOZ_UPLOAD_DIR = {}".format(env["MOZ_UPLOAD_DIR"]))
+
+
+def run_command(command, check=False, **kwargs):
+ kwargs.setdefault("cwd", OBJDIR)
+ info("in directory {}, running {}".format(kwargs["cwd"], command))
+ if platform.system() == "Windows":
+ # Windows will use cmd for the shell, which causes all sorts of
+ # problems. Use sh instead, quoting appropriately. (Use sh in all
+ # cases, not just when shell=True, because we want to be able to use
+ # paths that sh understands and cmd does not.)
+ if not isinstance(command, list):
+ if kwargs.get("shell"):
+ command = shlex.split(command)
+ else:
+ command = [command]
+
+ command = " ".join(quote(c) for c in command)
+ command = ["sh", "-c", command]
+ kwargs["shell"] = False
+ proc = Popen(command, **kwargs)
+ ACTIVE_PROCESSES.add(proc)
+ stdout, stderr = None, None
+ try:
+ stdout, stderr = proc.communicate()
+ finally:
+ ACTIVE_PROCESSES.discard(proc)
+ status = proc.wait()
+ if check and status != 0:
+ raise subprocess.CalledProcessError(status, command, output=stderr)
+ return stdout, stderr, status
+
+
+# Replacement strings in environment variables.
+REPLACEMENTS = {
+ "DIR": DIR.scripts,
+ "MOZ_FETCHES_DIR": DIR.fetches,
+ "MOZ_UPLOAD_DIR": env["MOZ_UPLOAD_DIR"],
+ "OUTDIR": OUTDIR,
+}
+
+# Add in environment variable settings for this variant. Normally used to
+# modify the flags passed to the shell or to set the GC zeal mode.
+for k, v in variant.get("env", {}).items():
+ env[k] = v.format(**REPLACEMENTS)
+
+if AUTOMATION:
+ # Currently only supported on linux64.
+ if platform.system() == "Linux" and word_bits == 64:
+ use_minidump = variant.get("use_minidump", True)
+ else:
+ use_minidump = False
+else:
+ use_minidump = False
+
+
+def resolve_path(dirs, *components):
+ if None in components:
+ return None
+ for dir in dirs:
+ path = os.path.join(dir, *components)
+ if os.path.exists(path):
+ return path
+
+
+if use_minidump:
+ env.setdefault("MINIDUMP_SAVE_PATH", env["MOZ_UPLOAD_DIR"])
+
+ injector_basename = {
+ "Linux": "libbreakpadinjector.so",
+ "Darwin": "breakpadinjector.dylib",
+ }.get(platform.system())
+
+ injector_lib = resolve_path((DIR.fetches,), "injector", injector_basename)
+ stackwalk = resolve_path((DIR.fetches,), "minidump-stackwalk", "minidump-stackwalk")
+ if stackwalk is not None:
+ env.setdefault("MINIDUMP_STACKWALK", stackwalk)
+ dump_syms = resolve_path((DIR.fetches,), "dump_syms", "dump_syms")
+ if dump_syms is not None:
+ env.setdefault("DUMP_SYMS", dump_syms)
+
+ if injector_lib is None:
+ use_minidump = False
+
+ info("use_minidump is {}".format(use_minidump))
+ info(" MINIDUMP_SAVE_PATH={}".format(env["MINIDUMP_SAVE_PATH"]))
+ info(" injector lib is {}".format(injector_lib))
+ info(" MINIDUMP_STACKWALK={}".format(env.get("MINIDUMP_STACKWALK")))
+
+
+mozconfig = os.path.join(DIR.source, "mozconfig.autospider")
+CONFIGURE_ARGS += " --prefix={OBJDIR}/dist".format(OBJDIR=quote(OBJDIR))
+
+# Generate a mozconfig.
+with open(mozconfig, "wt") as fh:
+ if AUTOMATION and platform.system() == "Windows":
+ fh.write('. "$topsrcdir/build/%s/mozconfig.vs-latest"\n' % variant_platform)
+ fh.write("ac_add_options --enable-project=js\n")
+ fh.write("ac_add_options " + CONFIGURE_ARGS + "\n")
+ fh.write("mk_add_options MOZ_OBJDIR=" + quote(OBJDIR) + "\n")
+
+env["MOZCONFIG"] = mozconfig
+
+mach = os.path.join(DIR.source, "mach")
+
+if not args.nobuild:
+ # Do the build
+ run_command([sys.executable, mach, "build"], check=True)
+
+ if use_minidump:
+ # Convert symbols to breakpad format.
+ cmd_env = env.copy()
+ cmd_env["MOZ_SOURCE_REPO"] = "file://" + DIR.source
+ cmd_env["RUSTC_COMMIT"] = "0"
+ cmd_env["MOZ_CRASHREPORTER"] = "1"
+ cmd_env["MOZ_AUTOMATION_BUILD_SYMBOLS"] = "1"
+ run_command(
+ [
+ sys.executable,
+ mach,
+ "build",
+ "recurse_syms",
+ ],
+ check=True,
+ env=cmd_env,
+ )
+
+COMMAND_PREFIX = []
+# On Linux, disable ASLR to make shell builds a bit more reproducible.
+# Bug 1795718 - Disable in automation for now as call to setarch requires extra
+# docker privileges.
+if not AUTOMATION and subprocess.call("type setarch >/dev/null 2>&1", shell=True) == 0:
+ COMMAND_PREFIX.extend(["setarch", platform.machine(), "-R"])
+
+
+def run_test_command(command, **kwargs):
+ _, _, status = run_command(COMMAND_PREFIX + command, check=False, **kwargs)
+ return status
+
+
+def run_jsapitests(args):
+ jsapi_test_binary = os.path.join(OBJDIR, "dist", "bin", "jsapi-tests")
+ test_env = env.copy()
+ test_env["TOPSRCDIR"] = DIR.source
+ if use_minidump and platform.system() == "Linux":
+ test_env["LD_PRELOAD"] = injector_lib
+ st = run_test_command([jsapi_test_binary] + args, env=test_env)
+ if st < 0:
+ print(
+ "PROCESS-CRASH | {} | application crashed".format(
+ " ".join(["jsapi-tests"] + args)
+ )
+ )
+ print("Return code: {}".format(st))
+ return st
+
+
+default_test_suites = frozenset(["jstests", "jittest", "jsapitests", "checks"])
+nondefault_test_suites = frozenset(["gdb"])
+all_test_suites = default_test_suites | nondefault_test_suites
+
+test_suites = set(default_test_suites)
+
+
+def normalize_tests(tests):
+ if "all" in tests:
+ return default_test_suites
+ return tests
+
+
+# Override environment variant settings conditionally.
+for k, v in variant.get("conditional-env", {}).get(variant_platform, {}).items():
+ env[k] = v.format(**REPLACEMENTS)
+
+# Skip any tests that are not run on this platform (or the 'all' platform).
+test_suites -= set(
+ normalize_tests(variant.get("skip-tests", {}).get(variant_platform, []))
+)
+test_suites -= set(normalize_tests(variant.get("skip-tests", {}).get("all", [])))
+
+# Add in additional tests for this platform (or the 'all' platform).
+test_suites |= set(
+ normalize_tests(variant.get("extra-tests", {}).get(variant_platform, []))
+)
+test_suites |= set(normalize_tests(variant.get("extra-tests", {}).get("all", [])))
+
+# Now adjust the variant's default test list with command-line arguments.
+test_suites |= set(normalize_tests(args.run_tests.split(",")))
+test_suites -= set(normalize_tests(args.skip_tests.split(",")))
+if "all" in args.skip_tests.split(","):
+ test_suites = []
+
+# Bug 1557130 - Atomics tests can create many additional threads which can
+# lead to resource exhaustion, resulting in intermittent failures. This was
+# only seen on beefy machines (> 32 cores), so limit the number of parallel
+# workers for now.
+#
+# Bug 1391877 - Windows test runs are getting mysterious timeouts when run
+# through taskcluster, but only when running many jit-test jobs in parallel.
+# Even at 16, some tests can overflow the paging file.
+worker_max = multiprocessing.cpu_count()
+jstest_workers = worker_max
+jittest_workers = worker_max
+if platform.system() == "Windows":
+ jstest_workers = min(worker_max, 16)
+ env["JSTESTS_EXTRA_ARGS"] = "-j{} ".format(jstest_workers) + env.get(
+ "JSTESTS_EXTRA_ARGS", ""
+ )
+ jittest_workers = min(worker_max, 8)
+ env["JITTEST_EXTRA_ARGS"] = "-j{} ".format(jittest_workers) + env.get(
+ "JITTEST_EXTRA_ARGS", ""
+ )
+print(
+ f"using {jstest_workers}/{worker_max} workers for jstests, "
+ f"{jittest_workers}/{worker_max} for jittest"
+)
+
+if use_minidump:
+ # Set up later js invocations to run with the breakpad injector loaded.
+ # Originally, I intended for this to be used with LD_PRELOAD, but when
+ # cross-compiling from 64- to 32-bit, that will fail and produce stderr
+ # output when running any 64-bit commands, which breaks eg mozconfig
+ # processing. So use the --dll command line mechanism universally.
+ for v in ("JSTESTS_EXTRA_ARGS", "JITTEST_EXTRA_ARGS"):
+ env[v] = "--args='--dll %s' %s" % (injector_lib, env.get(v, ""))
+
+# Always run all enabled tests, even if earlier ones failed. But return the
+# first failed status.
+results = [("(make-nonempty)", 0)]
+
+if "checks" in test_suites:
+ results.append(("make check", run_test_command([MAKE, "check"])))
+
+if "jittest" in test_suites:
+ results.append(("make check-jit-test", run_test_command([MAKE, "check-jit-test"])))
+if "jsapitests" in test_suites:
+ st = run_jsapitests([])
+ if st == 0:
+ st = run_jsapitests(["--frontend-only"])
+ results.append(("jsapi-tests", st))
+if "jstests" in test_suites:
+ results.append(("jstests", run_test_command([MAKE, "check-jstests"])))
+if "gdb" in test_suites:
+ test_script = os.path.join(DIR.js_src, "gdb", "run-tests.py")
+ auto_args = ["-s", "-o", "--no-progress"] if AUTOMATION else []
+ extra_args = env.get("GDBTEST_EXTRA_ARGS", "").split(" ")
+ results.append(
+ (
+ "gdb",
+ run_test_command([PYTHON, test_script, *auto_args, *extra_args, OBJDIR]),
+ )
+ )
+
+# FIXME bug 1291449: This would be unnecessary if we could run msan with -mllvm
+# -msan-keep-going, but in clang 3.8 it causes a hang during compilation.
+if variant.get("ignore-test-failures"):
+ logging.warning("Ignoring test results %s" % (results,))
+ results = [("ignored", 0)]
+
+if args.variant == "msan":
+ files = filter(lambda f: f.startswith("sanitize_log."), os.listdir(OUTDIR))
+ fullfiles = [os.path.join(OUTDIR, f) for f in files]
+
+ # Summarize results
+ sites = Counter()
+ errors = Counter()
+ for filename in fullfiles:
+ with open(os.path.join(OUTDIR, filename), "rb") as fh:
+ for line in fh:
+ m = re.match(
+ r"^SUMMARY: \w+Sanitizer: (?:data race|use-of-uninitialized-value) (.*)", # NOQA: E501
+ line.strip(),
+ )
+ if m:
+ # Some reports include file:line:column, some just
+ # file:line. Just in case it's nondeterministic, we will
+ # canonicalize to just the line number.
+ site = re.sub(r"^(\S+?:\d+)(:\d+)* ", r"\1 ", m.group(1))
+ sites[site] += 1
+
+ # Write a summary file and display it to stdout.
+ summary_filename = os.path.join(
+ env["MOZ_UPLOAD_DIR"], "%s_summary.txt" % args.variant
+ )
+ with open(summary_filename, "wb") as outfh:
+ for location, count in sites.most_common():
+ print >> outfh, "%d %s" % (count, location)
+ print(open(summary_filename, "rb").read())
+
+ if "max-errors" in variant:
+ max_allowed = variant["max-errors"]
+ print("Found %d errors out of %d allowed" % (len(sites), max_allowed))
+ if len(sites) > max_allowed:
+ results.append(("too many msan errors", 1))
+
+ # Gather individual results into a tarball. Note that these are
+ # distinguished only by pid of the JS process running within each test, so
+ # given the 16-bit limitation of pids, it's totally possible that some of
+ # these files will be lost due to being overwritten.
+ command = [
+ "tar",
+ "-C",
+ OUTDIR,
+ "-zcf",
+ os.path.join(env["MOZ_UPLOAD_DIR"], "%s.tar.gz" % args.variant),
+ ]
+ command += files
+ subprocess.call(command)
+
+# Upload dist/bin/js as js.wasm for the WASI build.
+if args.variant == "wasi":
+ command = [
+ "cp",
+ os.path.join(OBJDIR, "dist/bin/js"),
+ os.path.join(env["MOZ_UPLOAD_DIR"], "js.wasm"),
+ ]
+ subprocess.call(command)
+
+# Generate stacks from minidumps.
+if use_minidump:
+ run_command(
+ [
+ mach,
+ "python",
+ "virtualenv=build",
+ os.path.join(DIR.source, "testing/mozbase/mozcrash/mozcrash/mozcrash.py"),
+ os.getenv("TMPDIR", "/tmp"),
+ os.path.join(OBJDIR, "dist/crashreporter-symbols"),
+ ]
+ )
+
+for name, st in results:
+ print("exit status %d for '%s'" % (st, name))
+
+# Pick the "worst" exit status. SIGSEGV might give a status of -11, so use the
+# maximum absolute value instead of just the maximum.
+exit_status = max((st for _, st in results), key=abs)
+
+# The exit status on Windows can be something like 2147483651 (0x80000003),
+# which will be converted to status zero in the caller. Mask off the high bits,
+# but if the result is zero then fall back to returning 1.
+if exit_status & 0xFF:
+ sys.exit(exit_status & 0xFF)
+else:
+ sys.exit(1 if exit_status else 0)
diff --git a/js/src/devtools/automation/cgc-jittest-timeouts.txt b/js/src/devtools/automation/cgc-jittest-timeouts.txt
new file mode 100644
index 0000000000..0ab8350bbf
--- /dev/null
+++ b/js/src/devtools/automation/cgc-jittest-timeouts.txt
@@ -0,0 +1,53 @@
+asm.js/testBug1117235.js
+asm.js/testParallelCompile.js
+auto-regress/bug653395.js
+auto-regress/bug654392.js
+auto-regress/bug675251.js
+auto-regress/bug729797.js
+baseline/bug847446.js
+baseline/bug852175.js
+basic/bug1610192.js
+basic/bug632964-regexp.js
+basic/bug656261.js
+basic/bug677957-2.js
+basic/bug753283.js
+basic/bug867946.js
+basic/destructuring-iterator.js
+basic/offThreadCompileToStencil-01.js
+basic/testAtomize.js
+basic/testBug614653.js
+basic/testBug686274.js
+basic/testManyVars.js
+basic/testTypedArrayInit.js
+debug/DebuggeeWouldRun-01.js
+debug/DebuggeeWouldRun-02.js
+gc/bug-1014972.js
+gc/bug-1246593.js
+gc/bug-906236.js
+gc/bug-906241.js
+ion/bug1197769.js
+ion/bug779245.js
+ion/bug787921.js
+ion/bug977966.js
+ion/close-iterators-1.js
+parallel/alloc-many-objs.js
+parallel/alloc-too-many-objs.js
+parser/bug-1263881-1.js
+parser/bug-1263881-2.js
+parser/bug-1263881-3.js
+parser/bug-1355046.js
+parser/modifier-yield-without-operand-2.js
+saved-stacks/bug-1006876-too-much-recursion.js
+self-test/assertDeepEq.js
+sunspider/check-string-unpack-code.js
+v8-v5/check-earley-boyer.js
+v8-v5/check-raytrace.js
+v8-v5/check-regexp.js
+v8-v5/check-splay.js
+wasm/spec/br_table.wast.js
+wasm/spec/f32.wast.js
+wasm/spec/f32_cmp.wast.js
+wasm/spec/f64.wast.js
+wasm/spec/f64_cmp.wast.js
+wasm/spec/float_exprs.wast.js
+xdr/decode-off-thread.js
diff --git a/js/src/devtools/automation/cgc-jstests-slow.txt b/js/src/devtools/automation/cgc-jstests-slow.txt
new file mode 100644
index 0000000000..16290cb047
--- /dev/null
+++ b/js/src/devtools/automation/cgc-jstests-slow.txt
@@ -0,0 +1,65 @@
+non262/object/15.2.3.6-dictionary-redefinition-01-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-02-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-03-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-04-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-05-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-06-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-07-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-08-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-09-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-10-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-11-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-12-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-13-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-14-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-15-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-16-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-17-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-18-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-19-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-20-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-21-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-22-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-23-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-24-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-25-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-26-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-27-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-28-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-29-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-30-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-31-of-32.js
+non262/object/15.2.3.6-dictionary-redefinition-32-of-32.js
+non262/object/15.2.3.6-middle-redefinition-1-of-8.js
+non262/object/15.2.3.6-middle-redefinition-2-of-8.js
+non262/object/15.2.3.6-middle-redefinition-3-of-8.js
+non262/object/15.2.3.6-middle-redefinition-4-of-8.js
+non262/object/15.2.3.6-middle-redefinition-5-of-8.js
+non262/object/15.2.3.6-middle-redefinition-6-of-8.js
+non262/object/15.2.3.6-middle-redefinition-7-of-8.js
+non262/object/15.2.3.6-middle-redefinition-8-of-8.js
+non262/object/15.2.3.6-redefinition-1-of-4.js
+non262/object/15.2.3.6-redefinition-2-of-4.js
+non262/object/15.2.3.6-redefinition-3-of-4.js
+non262/object/15.2.3.6-redefinition-4-of-4.js
+non262/extensions/array-isArray-proxy-recursion.js
+non262/String/normalize-generateddata-part0.js
+non262/String/normalize-generateddata-part1-not-listed.js
+non262/String/normalize-generateddata-part1.js
+non262/String/normalize-generateddata-part2.js
+non262/String/normalize-generateddata-part3.js
+non262/GC/regress-203278-2.js
+non262/GC/regress-203278-3.js
+non262/GC/regress-278725.js
+non262/regress/regress-312588.js
+non262/regress/regress-321971.js
+non262/regress/regress-360969-01.js
+non262/regress/regress-360969-02.js
+non262/regress/regress-360969-03.js
+non262/regress/regress-360969-04.js
+non262/regress/regress-360969-05.js
+non262/regress/regress-360969-06.js
+non262/extensions/regress-477187.js
+non262/regress/regress-452498-052-a.js
+non262/extensions/clone-complex-object.js
+non262/extensions/clone-object-deep.js
diff --git a/js/src/devtools/automation/smoosh-jittest-known-failure.txt b/js/src/devtools/automation/smoosh-jittest-known-failure.txt
new file mode 100644
index 0000000000..8d32545934
--- /dev/null
+++ b/js/src/devtools/automation/smoosh-jittest-known-failure.txt
@@ -0,0 +1,4 @@
+# https://github.com/mozilla-spidermonkey/jsparagus/issues/641
+class/class-static-01.js
+class/class-static-02.js
+class/class-static-03.js
diff --git a/js/src/devtools/automation/smoosh-jstests-known-failure-and-slow.txt b/js/src/devtools/automation/smoosh-jstests-known-failure-and-slow.txt
new file mode 100644
index 0000000000..7d905ca6c7
--- /dev/null
+++ b/js/src/devtools/automation/smoosh-jstests-known-failure-and-slow.txt
@@ -0,0 +1,81 @@
+# https://github.com/mozilla-spidermonkey/jsparagus/issues/635
+test262/language/expressions/assignmenttargettype/parenthesized-primaryexpression-objectliteral.js
+
+# https://github.com/mozilla-spidermonkey/jsparagus/issues/641
+test262/language/expressions/function/static-init-await-binding.js
+test262/language/expressions/generators/static-init-await-binding.js
+test262/language/expressions/object/ident-name-prop-name-literal-await-static-init.js
+test262/language/expressions/object/identifier-shorthand-static-init-await-valid.js
+test262/language/expressions/object/method-definition/static-init-await-binding-accessor.js
+test262/language/expressions/object/method-definition/static-init-await-binding-generator.js
+test262/language/expressions/object/method-definition/static-init-await-binding-normal.js
+test262/language/statements/class/static-init-abrupt.js
+test262/language/statements/class/static-init-arguments-functions.js
+test262/language/statements/class/static-init-arguments-methods.js
+test262/language/statements/class/static-init-await-binding-valid.js
+test262/language/statements/class/static-init-expr-new-target.js
+test262/language/statements/class/static-init-expr-this.js
+test262/language/statements/class/static-init-scope-lex-close.js
+test262/language/statements/class/static-init-scope-var-close.js
+test262/language/statements/class/static-init-scope-var-derived.js
+test262/language/statements/class/static-init-scope-var-open.js
+test262/language/statements/class/static-init-statement-list-optional.js
+test262/language/statements/class/static-init-super-property.js
+test262/language/statements/const/static-init-await-binding-valid.js
+test262/language/statements/function/static-init-await-binding-valid.js
+test262/language/statements/let/static-init-await-binding-valid.js
+test262/language/statements/try/static-init-await-binding-valid.js
+test262/language/statements/variable/dstr/ary-ptrn-elem-id-static-init-await-valid.js
+test262/language/statements/variable/dstr/obj-ptrn-elem-id-static-init-await-valid.js
+test262/language/statements/variable/static-init-await-binding-valid.js
+
+# https://github.com/mozilla-spidermonkey/jsparagus/issues/650
+test262/language/expressions/dynamic-import/2nd-param-assert-enumeration-abrupt.js
+test262/language/expressions/dynamic-import/2nd-param-assert-enumeration.js
+test262/language/expressions/dynamic-import/2nd-param-assert-non-object.js
+test262/language/expressions/dynamic-import/2nd-param-assert-undefined.js
+test262/language/expressions/dynamic-import/2nd-param-assert-value-abrupt.js
+test262/language/expressions/dynamic-import/2nd-param-assert-value-non-string.js
+test262/language/expressions/dynamic-import/2nd-param-evaluation-abrupt-return.js
+test262/language/expressions/dynamic-import/2nd-param-evaluation-abrupt-throw.js
+test262/language/expressions/dynamic-import/2nd-param-evaluation-sequence.js
+test262/language/expressions/dynamic-import/2nd-param-get-assert-error.js
+test262/language/expressions/dynamic-import/2nd-param-in.js
+test262/language/expressions/dynamic-import/2nd-param-non-object.js
+test262/language/expressions/dynamic-import/2nd-param-trailing-comma-fulfill.js
+test262/language/expressions/dynamic-import/2nd-param-trailing-comma-reject.js
+test262/language/expressions/dynamic-import/2nd-param-yield-expr.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-block-labeled-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-block-labeled-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-block-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-block-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-do-while-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-do-while-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-else-braceless-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-else-braceless-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-else-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-else-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-function-return-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-function-return-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-function-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-function-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-if-braceless-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-if-braceless-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-if-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-if-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-with-expression-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-with-expression-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-with-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-with-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/top-level-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/top-level-trailing-comma-second.js
+test262/language/expressions/dynamic-import/trailing-comma-fulfill.js
+test262/language/expressions/dynamic-import/trailing-comma-reject.js
+
+# slow
+non262/String/normalize-generateddata-part0.js
+non262/String/normalize-generateddata-part1-not-listed.js
+non262/String/normalize-generateddata-part1.js
+non262/String/normalize-generateddata-part2.js
+non262/String/normalize-generateddata-part3.js
+non262/regress/regress-155081-2.js
diff --git a/js/src/devtools/automation/smoosh-jstests-known-failure.txt b/js/src/devtools/automation/smoosh-jstests-known-failure.txt
new file mode 100644
index 0000000000..7a3be5c211
--- /dev/null
+++ b/js/src/devtools/automation/smoosh-jstests-known-failure.txt
@@ -0,0 +1,73 @@
+# https://github.com/mozilla-spidermonkey/jsparagus/issues/635
+test262/language/expressions/assignmenttargettype/parenthesized-primaryexpression-objectliteral.js
+
+# https://github.com/mozilla-spidermonkey/jsparagus/issues/641
+test262/language/expressions/function/static-init-await-binding.js
+test262/language/expressions/generators/static-init-await-binding.js
+test262/language/expressions/object/ident-name-prop-name-literal-await-static-init.js
+test262/language/expressions/object/identifier-shorthand-static-init-await-valid.js
+test262/language/expressions/object/method-definition/static-init-await-binding-accessor.js
+test262/language/expressions/object/method-definition/static-init-await-binding-generator.js
+test262/language/expressions/object/method-definition/static-init-await-binding-normal.js
+test262/language/statements/class/static-init-abrupt.js
+test262/language/statements/class/static-init-arguments-functions.js
+test262/language/statements/class/static-init-arguments-methods.js
+test262/language/statements/class/static-init-await-binding-valid.js
+test262/language/statements/class/static-init-expr-new-target.js
+test262/language/statements/class/static-init-expr-this.js
+test262/language/statements/class/static-init-scope-lex-close.js
+test262/language/statements/class/static-init-scope-var-close.js
+test262/language/statements/class/static-init-scope-var-derived.js
+test262/language/statements/class/static-init-scope-var-open.js
+test262/language/statements/class/static-init-statement-list-optional.js
+test262/language/statements/class/static-init-super-property.js
+test262/language/statements/const/static-init-await-binding-valid.js
+test262/language/statements/function/static-init-await-binding-valid.js
+test262/language/statements/let/static-init-await-binding-valid.js
+test262/language/statements/try/static-init-await-binding-valid.js
+test262/language/statements/variable/dstr/ary-ptrn-elem-id-static-init-await-valid.js
+test262/language/statements/variable/dstr/obj-ptrn-elem-id-static-init-await-valid.js
+test262/language/statements/variable/static-init-await-binding-valid.js
+
+# https://github.com/mozilla-spidermonkey/jsparagus/issues/650
+test262/language/expressions/dynamic-import/2nd-param-assert-enumeration-abrupt.js
+test262/language/expressions/dynamic-import/2nd-param-assert-enumeration.js
+test262/language/expressions/dynamic-import/2nd-param-assert-non-object.js
+test262/language/expressions/dynamic-import/2nd-param-assert-undefined.js
+test262/language/expressions/dynamic-import/2nd-param-assert-value-abrupt.js
+test262/language/expressions/dynamic-import/2nd-param-assert-value-non-string.js
+test262/language/expressions/dynamic-import/2nd-param-evaluation-abrupt-return.js
+test262/language/expressions/dynamic-import/2nd-param-evaluation-abrupt-throw.js
+test262/language/expressions/dynamic-import/2nd-param-evaluation-sequence.js
+test262/language/expressions/dynamic-import/2nd-param-get-assert-error.js
+test262/language/expressions/dynamic-import/2nd-param-in.js
+test262/language/expressions/dynamic-import/2nd-param-non-object.js
+test262/language/expressions/dynamic-import/2nd-param-trailing-comma-fulfill.js
+test262/language/expressions/dynamic-import/2nd-param-trailing-comma-reject.js
+test262/language/expressions/dynamic-import/2nd-param-yield-expr.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-block-labeled-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-block-labeled-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-block-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-block-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-do-while-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-do-while-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-else-braceless-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-else-braceless-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-else-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-else-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-function-return-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-function-return-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-function-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-function-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-if-braceless-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-if-braceless-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-if-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-if-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-with-expression-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-with-expression-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-with-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/nested-with-trailing-comma-second.js
+test262/language/expressions/dynamic-import/syntax/valid/top-level-trailing-comma-first.js
+test262/language/expressions/dynamic-import/syntax/valid/top-level-trailing-comma-second.js
+test262/language/expressions/dynamic-import/trailing-comma-fulfill.js
+test262/language/expressions/dynamic-import/trailing-comma-reject.js
diff --git a/js/src/devtools/automation/smoosh-jstests-slow.txt b/js/src/devtools/automation/smoosh-jstests-slow.txt
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/js/src/devtools/automation/smoosh-jstests-slow.txt
diff --git a/js/src/devtools/automation/tsan-slow.txt b/js/src/devtools/automation/tsan-slow.txt
new file mode 100644
index 0000000000..06dc763c08
--- /dev/null
+++ b/js/src/devtools/automation/tsan-slow.txt
@@ -0,0 +1,23 @@
+# Skip tests that run too slowly under tsan.
+basic/spread-call-maxarg.js
+basic/spread-call-near-maxarg.js
+arrays/too-long-array-splice.js
+bug-1698543.js
+# Skip tests that use too much memory under tsan - see bug 1519263.
+bug1355573.js
+max-string-length.js
+expr-decompiler-bug1475953.js
+regress-303213.js
+f32.wast.js
+f64.wast.js
+f32_cmp.wast.js
+f64_cmp.wast.js
+bug1470732.js
+bug1238815.js
+bug1315943.js
+bug-1382431.js
+float_exprs.wast.js
+bug858586.js
+bug1296667.js
+bug-1465695.js
+integer.js
diff --git a/js/src/devtools/automation/variants/arm-sim b/js/src/devtools/automation/variants/arm-sim
new file mode 100644
index 0000000000..80ca937e01
--- /dev/null
+++ b/js/src/devtools/automation/variants/arm-sim
@@ -0,0 +1,7 @@
+{
+ "__comment1": "Bug 1700372: --disable-clang-plugin is needed to avoid a warning (that we treat as an error) for comparing a number to itself to test for NaN",
+ "configure-args": "--enable-simulator=arm --target=i686-pc-linux --enable-rust-simd --disable-clang-plugin",
+ "optimize": true,
+ "debug": true,
+ "bits": 32
+}
diff --git a/js/src/devtools/automation/variants/arm-sim-osx b/js/src/devtools/automation/variants/arm-sim-osx
new file mode 100644
index 0000000000..4e03dadd9f
--- /dev/null
+++ b/js/src/devtools/automation/variants/arm-sim-osx
@@ -0,0 +1,6 @@
+{
+ "configure-args": "--enable-simulator=arm --target=i686-apple-darwin10.0.0 --enable-rust-simd",
+ "optimize": true,
+ "debug": true,
+ "bits": 32
+}
diff --git a/js/src/devtools/automation/variants/arm64-sim b/js/src/devtools/automation/variants/arm64-sim
new file mode 100644
index 0000000000..bbaec58fac
--- /dev/null
+++ b/js/src/devtools/automation/variants/arm64-sim
@@ -0,0 +1,10 @@
+{
+ "configure-args": "--enable-simulator=arm64 --enable-rust-simd",
+ "optimize": true,
+ "debug": true,
+ "env": {
+ "JSTESTS_EXTRA_ARGS": "--exclude-file={DIR}/arm64-jstests-slow.txt",
+ "JITTEST_EXTRA_ARGS": "--ignore-timeouts={DIR}/arm64-jittests-timeouts.txt --jitflags=none --args=--baseline-eager -x ion/ -x asm.js/"
+ },
+ "bits": 64
+}
diff --git a/js/src/devtools/automation/variants/asan b/js/src/devtools/automation/variants/asan
new file mode 100644
index 0000000000..62091639f6
--- /dev/null
+++ b/js/src/devtools/automation/variants/asan
@@ -0,0 +1,10 @@
+{
+ "configure-args": "--enable-debug-symbols='-gline-tables-only' --enable-gczeal --disable-jemalloc --enable-address-sanitizer --enable-rust-simd",
+ "optimize": true,
+ "debug": false,
+ "compiler": "clang",
+ "env": {
+ "LLVM_SYMBOLIZER": "{MOZ_FETCHES_DIR}/llvm-symbolizer/bin/llvm-symbolizer"
+ },
+ "use_minidump": false
+}
diff --git a/js/src/devtools/automation/variants/compacting b/js/src/devtools/automation/variants/compacting
new file mode 100644
index 0000000000..911cfa1c49
--- /dev/null
+++ b/js/src/devtools/automation/variants/compacting
@@ -0,0 +1,14 @@
+{
+ "configure-args": "--enable-ctypes --enable-rust-simd",
+ "optimize": true,
+ "debug": true,
+ "env": {
+ "JS_GC_ZEAL": "IncrementalMultipleSlices",
+ "JITTEST_EXTRA_ARGS": "--jitflags=debug --ignore-timeouts={DIR}/cgc-jittest-timeouts.txt",
+ "JSTESTS_EXTRA_ARGS": "--exclude-file={DIR}/cgc-jstests-slow.txt"
+ },
+ "skip-tests": {
+ "win32": ["jstests"],
+ "win64": ["jstests"]
+ }
+}
diff --git a/js/src/devtools/automation/variants/dtrace b/js/src/devtools/automation/variants/dtrace
new file mode 100644
index 0000000000..0678819225
--- /dev/null
+++ b/js/src/devtools/automation/variants/dtrace
@@ -0,0 +1,5 @@
+{
+ "configure-args": "--enable-dtrace --enable-debug-symbols --enable-rust-simd",
+ "optimize": true,
+ "debug": true,
+}
diff --git a/js/src/devtools/automation/variants/fuzzilli b/js/src/devtools/automation/variants/fuzzilli
new file mode 100644
index 0000000000..61605e3a96
--- /dev/null
+++ b/js/src/devtools/automation/variants/fuzzilli
@@ -0,0 +1,14 @@
+{
+ "configure-args": "--enable-js-fuzzilli --enable-fuzzing --enable-gczeal --disable-shared-js",
+ "optimize": true,
+ "debug": true,
+ "nspr": false,
+ "compiler": "clang",
+ "env": {
+ "JITTEST_EXTRA_ARGS": "--jitflags=ion"
+ },
+ "skip-tests": {
+ "all": ["jstests"]
+ },
+ "use_minidump": false
+}
diff --git a/js/src/devtools/automation/variants/fuzzing b/js/src/devtools/automation/variants/fuzzing
new file mode 100644
index 0000000000..146b89e355
--- /dev/null
+++ b/js/src/devtools/automation/variants/fuzzing
@@ -0,0 +1,13 @@
+{
+ "configure-args": "--enable-fuzzing --enable-gczeal --enable-debug-symbols='-gline-tables-only -gdwarf-2' --disable-jemalloc --disable-stdcxx-compat --enable-address-sanitizer --enable-ctypes --enable-nspr-build --enable-rust-simd",
+ "optimize": true,
+ "debug": false,
+ "compiler": "clang",
+ "env": {
+ "JITTEST_EXTRA_ARGS": "--jitflags=none",
+ "JSTESTS_EXTRA_ARGS": "--jitflags=none",
+ "LLVM_SYMBOLIZER": "{MOZ_FETCHES_DIR}/llvm-symbolizer/bin/llvm-symbolizer",
+ "ASAN_SYMBOLIZER_PATH": "{MOZ_FETCHES_DIR}/llvm-symbolizer/bin/llvm-symbolizer"
+ },
+ "use_minidump": false
+}
diff --git a/js/src/devtools/automation/variants/gdb b/js/src/devtools/automation/variants/gdb
new file mode 100644
index 0000000000..d86d265680
--- /dev/null
+++ b/js/src/devtools/automation/variants/gdb
@@ -0,0 +1,16 @@
+{
+ "configure-args": "--enable-rust-simd",
+ "debug": true,
+ "optimize": false,
+ "compiler": "gcc",
+ "skip-tests": {
+ "all": ["jstests", "jittest", "jsapitests"]
+ },
+ "extra-tests": {
+ "all": ["gdb"]
+ },
+ "env": {
+ "GDBTEST_EXTRA_ARGS": "--exclude=unwind"
+ },
+ "use_minidump": false
+}
diff --git a/js/src/devtools/automation/variants/msan b/js/src/devtools/automation/variants/msan
new file mode 100644
index 0000000000..e85e67dfb5
--- /dev/null
+++ b/js/src/devtools/automation/variants/msan
@@ -0,0 +1,14 @@
+{
+ "configure-args": "--enable-debug-symbols='-gline-tables-only' --disable-jemalloc --enable-memory-sanitizer --without-system-zlib --enable-rust-simd",
+ "optimize": true,
+ "debug": false,
+ "compiler": "clang",
+ "env": {
+ "JITTEST_EXTRA_ARGS": "--jitflags=interp --ignore-timeouts={DIR}/cgc-jittest-timeouts.txt",
+ "JSTESTS_EXTRA_ARGS": "--jitflags=interp --exclude-file={DIR}/cgc-jstests-slow.txt",
+ "MSAN_OPTIONS": "external_symbolizer_path={MOZ_FETCHES_DIR}/llvm-symbolizer/bin/llvm-symbolizer:log_path={OUTDIR}/sanitize_log"
+ },
+ "ignore-test-failures": "true",
+ "max-errors": 7,
+ "use_minidump": false
+}
diff --git a/js/src/devtools/automation/variants/nojit b/js/src/devtools/automation/variants/nojit
new file mode 100644
index 0000000000..9cebcfcd16
--- /dev/null
+++ b/js/src/devtools/automation/variants/nojit
@@ -0,0 +1,4 @@
+{
+ "configure-args": "--disable-jit --enable-warnings-as-errors --enable-rust-simd",
+ "optimize": true
+}
diff --git a/js/src/devtools/automation/variants/nojit-debug b/js/src/devtools/automation/variants/nojit-debug
new file mode 100644
index 0000000000..c067d617b1
--- /dev/null
+++ b/js/src/devtools/automation/variants/nojit-debug
@@ -0,0 +1,4 @@
+{
+ "configure-args": "--disable-jit --enable-warnings-as-errors --enable-rust-simd",
+ "optimize": false
+}
diff --git a/js/src/devtools/automation/variants/nonunified b/js/src/devtools/automation/variants/nonunified
new file mode 100644
index 0000000000..f675a0c670
--- /dev/null
+++ b/js/src/devtools/automation/variants/nonunified
@@ -0,0 +1,10 @@
+{
+ "configure-args": "--enable-warnings-as-errors --enable-rust-simd --disable-unified-build",
+ "debug": true,
+ "env": {
+ "JS_SMOOSH_DISABLE_OPCODE_CHECK": "1"
+ },
+ "skip-tests": {
+ "all": ["jstests", "jittest", "checks"]
+ }
+}
diff --git a/js/src/devtools/automation/variants/pbl b/js/src/devtools/automation/variants/pbl
new file mode 100644
index 0000000000..9fe392e4f6
--- /dev/null
+++ b/js/src/devtools/automation/variants/pbl
@@ -0,0 +1,4 @@
+{
+ "configure-args": "--enable-portable-baseline-interp --enable-portable-baseline-interp-force",
+ "optimize": true
+}
diff --git a/js/src/devtools/automation/variants/plain b/js/src/devtools/automation/variants/plain
new file mode 100644
index 0000000000..f2aaf776c9
--- /dev/null
+++ b/js/src/devtools/automation/variants/plain
@@ -0,0 +1,9 @@
+{
+ "configure-args": "--enable-rust-simd",
+ "optimize": true,
+ "compiler": "gcc",
+ "env": {
+ "JSTESTS_EXTRA_ARGS": "--jitflags=jstests --run-slow-tests",
+ "JITTEST_EXTRA_ARGS": "--slow"
+ }
+}
diff --git a/js/src/devtools/automation/variants/plaindebug b/js/src/devtools/automation/variants/plaindebug
new file mode 100644
index 0000000000..c03419f8f9
--- /dev/null
+++ b/js/src/devtools/automation/variants/plaindebug
@@ -0,0 +1,7 @@
+{
+ "configure-args": "--enable-rust-simd",
+ "debug": true,
+ "env": {
+ "JSTESTS_EXTRA_ARGS": "--jitflags=debug"
+ }
+}
diff --git a/js/src/devtools/automation/variants/rootanalysis b/js/src/devtools/automation/variants/rootanalysis
new file mode 100644
index 0000000000..7c0fb5242b
--- /dev/null
+++ b/js/src/devtools/automation/variants/rootanalysis
@@ -0,0 +1,9 @@
+{
+ "configure-args": "--enable-ctypes --enable-rust-simd",
+ "optimize": true,
+ "debug": true,
+ "env": {
+ "JS_GC_ZEAL": "GenerationalGC",
+ "JSTESTS_EXTRA_ARGS": "--jitflags=debug"
+ }
+}
diff --git a/js/src/devtools/automation/variants/rtdebug b/js/src/devtools/automation/variants/rtdebug
new file mode 100644
index 0000000000..d3450261ff
--- /dev/null
+++ b/js/src/devtools/automation/variants/rtdebug
@@ -0,0 +1,8 @@
+{
+ "debug": true,
+ "optimize": true,
+ "configure-args": "--enable-record-tuple",
+ "skip-tests": {
+ "all": ["jittest", "jsapitests", "checks"]
+ }
+}
diff --git a/js/src/devtools/automation/variants/smoosh b/js/src/devtools/automation/variants/smoosh
new file mode 100644
index 0000000000..148c2f8c21
--- /dev/null
+++ b/js/src/devtools/automation/variants/smoosh
@@ -0,0 +1,8 @@
+{
+ "configure-args": "--enable-rust-simd --enable-smoosh",
+ "optimize": true,
+ "env": {
+ "JSTESTS_EXTRA_ARGS": "--args='--smoosh' --jitflags=jstests --exclude-file={DIR}/smoosh-jstests-known-failure.txt",
+ "JITTEST_EXTRA_ARGS": "--args='--smoosh' --exclude-file={DIR}/smoosh-jittest-known-failure.txt"
+ }
+}
diff --git a/js/src/devtools/automation/variants/smooshdebug b/js/src/devtools/automation/variants/smooshdebug
new file mode 100644
index 0000000000..07b50cf917
--- /dev/null
+++ b/js/src/devtools/automation/variants/smooshdebug
@@ -0,0 +1,8 @@
+{
+ "configure-args": "--enable-rust-simd --enable-smoosh",
+ "debug": true,
+ "env": {
+ "JSTESTS_EXTRA_ARGS": "--args='--smoosh' --jitflags=debug --exclude-file={DIR}/smoosh-jstests-known-failure-and-slow.txt",
+ "JITTEST_EXTRA_ARGS": "--args='--smoosh' --exclude-file={DIR}/smoosh-jittest-known-failure.txt"
+ }
+}
diff --git a/js/src/devtools/automation/variants/temporaldebug b/js/src/devtools/automation/variants/temporaldebug
new file mode 100644
index 0000000000..c4582c0af5
--- /dev/null
+++ b/js/src/devtools/automation/variants/temporaldebug
@@ -0,0 +1,7 @@
+{
+ "configure-args": "--with-temporal-api --enable-rust-simd",
+ "debug": true,
+ "env": {
+ "JSTESTS_EXTRA_ARGS": "--jitflags=debug"
+ }
+}
diff --git a/js/src/devtools/automation/variants/tsan b/js/src/devtools/automation/variants/tsan
new file mode 100644
index 0000000000..69b2d87ef9
--- /dev/null
+++ b/js/src/devtools/automation/variants/tsan
@@ -0,0 +1,12 @@
+{
+ "configure-args": "--enable-debug-symbols='-gline-tables-only' --disable-jemalloc --enable-thread-sanitizer --enable-rust-simd",
+ "optimize": true,
+ "debug": false,
+ "compiler": "clang",
+ "env": {
+ "LLVM_SYMBOLIZER": "{MOZ_FETCHES_DIR}/llvm-symbolizer/bin/llvm-symbolizer",
+ "JITTEST_EXTRA_ARGS": "--jitflags=tsan --ignore-timeouts={DIR}/cgc-jittest-timeouts.txt --unusable-error-status --exclude-from={DIR}/tsan-slow.txt",
+ "JSTESTS_EXTRA_ARGS": "--exclude-file={DIR}/cgc-jstests-slow.txt"
+ },
+ "use_minidump": false
+}
diff --git a/js/src/devtools/automation/variants/warnaserr b/js/src/devtools/automation/variants/warnaserr
new file mode 100644
index 0000000000..98d5e96fe1
--- /dev/null
+++ b/js/src/devtools/automation/variants/warnaserr
@@ -0,0 +1,4 @@
+{
+ "configure-args": "--enable-warnings-as-errors --enable-rust-simd",
+ "optimize": true
+}
diff --git a/js/src/devtools/automation/variants/warnaserrdebug b/js/src/devtools/automation/variants/warnaserrdebug
new file mode 100644
index 0000000000..ca1f14fef1
--- /dev/null
+++ b/js/src/devtools/automation/variants/warnaserrdebug
@@ -0,0 +1,4 @@
+{
+ "configure-args": "--enable-warnings-as-errors",
+ "debug": true
+}
diff --git a/js/src/devtools/automation/variants/wasi b/js/src/devtools/automation/variants/wasi
new file mode 100644
index 0000000000..18fb91ba4e
--- /dev/null
+++ b/js/src/devtools/automation/variants/wasi
@@ -0,0 +1,14 @@
+{
+ "configure-args": "--enable-project=js --target=wasm32-unknown-wasi --disable-stdcxx-compat --without-system-zlib --without-intl-api --disable-jit --disable-shared-js --disable-shared-memory --disable-tests --disable-clang-plugin --disable-debug-symbols --enable-jitspew",
+ "optimize": true,
+ "debug": false,
+ "nspr": false,
+ "compiler": "clang",
+ "env": {
+ "JITTEST_EXTRA_ARGS": "--jitflags=none"
+ },
+ "skip-tests": {
+ "all": ["jstests", "jittest", "jsapitests"]
+ },
+ "use_minidump": false
+}
diff --git a/js/src/devtools/automation/variants/wasi-intl b/js/src/devtools/automation/variants/wasi-intl
new file mode 100644
index 0000000000..dbaaffff66
--- /dev/null
+++ b/js/src/devtools/automation/variants/wasi-intl
@@ -0,0 +1,14 @@
+{
+ "configure-args": "--enable-project=js --target=wasm32-unknown-wasi --disable-stdcxx-compat --without-system-zlib --disable-jit --disable-shared-js --disable-shared-memory --disable-tests --disable-clang-plugin --disable-debug-symbols --enable-jitspew",
+ "optimize": true,
+ "debug": false,
+ "nspr": false,
+ "compiler": "clang",
+ "env": {
+ "JITTEST_EXTRA_ARGS": "--jitflags=none"
+ },
+ "skip-tests": {
+ "all": ["jstests", "jittest", "jsapitests"]
+ },
+ "use_minidump": false
+}
diff --git a/js/src/devtools/automation/variants/wasi-pbl b/js/src/devtools/automation/variants/wasi-pbl
new file mode 100644
index 0000000000..2c2cf31f35
--- /dev/null
+++ b/js/src/devtools/automation/variants/wasi-pbl
@@ -0,0 +1,14 @@
+{
+ "configure-args": "--enable-project=js --target=wasm32-unknown-wasi --disable-stdcxx-compat --without-system-zlib --without-intl-api --disable-jit --disable-shared-js --disable-shared-memory --disable-tests --disable-clang-plugin --disable-debug-symbols --enable-jitspew --enable-portable-baseline-interp --enable-portable-baseline-interp-force",
+ "optimize": true,
+ "debug": false,
+ "nspr": false,
+ "compiler": "clang",
+ "env": {
+ "JITTEST_EXTRA_ARGS": "--jitflags=none"
+ },
+ "skip-tests": {
+ "all": ["jstests", "jittest", "jsapitests"]
+ },
+ "use_minidump": false
+}
diff --git a/js/src/devtools/automation/variants/wasm-noexperimental b/js/src/devtools/automation/variants/wasm-noexperimental
new file mode 100644
index 0000000000..2d1fcb4e58
--- /dev/null
+++ b/js/src/devtools/automation/variants/wasm-noexperimental
@@ -0,0 +1,7 @@
+{
+ "configure-args": "--wasm-no-experimental",
+ "debug": true,
+ "skip-tests": {
+ "all": ["jstests", "jsapitests"]
+ }
+}
diff --git a/js/src/devtools/gc-ubench/argparse.js b/js/src/devtools/gc-ubench/argparse.js
new file mode 100644
index 0000000000..454b53fb71
--- /dev/null
+++ b/js/src/devtools/gc-ubench/argparse.js
@@ -0,0 +1,127 @@
+/* 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/. */
+
+// Command-line argument parser, modeled after but not identical to Python's
+// argparse.
+
+var ArgParser = class {
+ constructor(desc) {
+ this._params = [];
+ this._doc = desc;
+
+ this.add_argument("--help", {
+ help: "display this help message",
+ });
+ }
+
+ // name is '--foo', '-f', or an array of aliases.
+ //
+ // spec is an options object with keys:
+ // dest: key name to store the result in (optional for long options)
+ // default: value to use if not passed on command line (optional)
+ // help: description of the option to show in --help
+ // options: array of valid choices
+ //
+ // Prefixes of long option names are allowed. If a prefix is ambiguous, it
+ // will match the first parameter added to the ArgParser.
+ add_argument(name, spec) {
+ const names = Array.isArray(name) ? name : [name];
+
+ spec = Object.assign({}, spec);
+ spec.aliases = names;
+ for (const name of names) {
+ if (!name.startsWith("-")) {
+ throw new Error(`unhandled argument syntax '${name}'`);
+ }
+ if (name.startsWith("--")) {
+ spec.dest = spec.dest || name.substr(2);
+ }
+ this._params.push({ name, spec });
+ }
+ }
+
+ parse_args(args) {
+ const opts = {};
+ const rest = [];
+
+ for (const { spec } of this._params) {
+ if (spec.default !== undefined) {
+ opts[spec.dest] = spec.default;
+ }
+ }
+
+ const seen = new Set();
+ for (let i = 0; i < args.length; i++) {
+ const arg = args[i];
+ if (!arg.startsWith("-")) {
+ rest.push(arg);
+ continue;
+ } else if (arg === "--") {
+ rest.push(args.slice(i+1));
+ break;
+ }
+
+ if (arg == "--help" || arg == "-h") {
+ this.help();
+ }
+
+ let parameter;
+ let [passed, value] = arg.split("=");
+ for (const { name, spec } of this._params) {
+ if (passed.startsWith("--")) {
+ if (name.startsWith(passed)) {
+ parameter = spec;
+ }
+ } else if (passed.startsWith("-") && passed === name) {
+ parameter = spec;
+ }
+ if (parameter) {
+ if (value === undefined) {
+ value = args[++i];
+ }
+ opts[parameter.dest] = value;
+ break;
+ }
+ }
+
+ if (parameter) {
+ if (seen.has(parameter)) {
+ throw new Error(`${parameter.aliases[0]} given multiple times`);
+ }
+ seen.add(parameter);
+ } else {
+ throw new Error(`invalid command-line argument '${arg}'`);
+ }
+ }
+
+ for (const { name, spec } of this._params) {
+ if (spec.options && !spec.options.includes(opts[spec.dest])) {
+ throw new Error(`invalid ${name} value '${opts[spec.dest]}'`);
+ opts[spec.dest] = spec.default;
+ }
+ }
+
+ return { opts, rest };
+ }
+
+ help() {
+ print(`Usage: ${this._doc}`);
+ const specs = new Set(this._params.map(p => p.spec));
+ const optstrs = [...specs].map(p => p.aliases.join(", "));
+ let maxlen = Math.max(...optstrs.map(s => s.length));
+ for (const spec of specs) {
+ const name = spec.aliases[0];
+ let helptext = spec.help ?? "undocumented";
+ if ("options" in spec) {
+ helptext += ` (one of ${spec.options.map(x => `'${x}'`).join(", ")})`;
+ }
+ if ("default" in spec) {
+ helptext += ` (default '${spec.default}')`;
+ }
+ const optstr = spec.aliases.join(", ");
+ print(` ${optstr.padEnd(maxlen)} ${helptext}`);
+ }
+ quit(0);
+ }
+};
diff --git a/js/src/devtools/gc-ubench/benchmarks/bigTextNodes.js b/js/src/devtools/gc-ubench/benchmarks/bigTextNodes.js
new file mode 100644
index 0000000000..0a66be03d3
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/bigTextNodes.js
@@ -0,0 +1,42 @@
+/* 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/. */
+
+tests.set(
+ "bigTextNodes",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = [ textNode, textNode, ... ]",
+
+ enabled: "document" in globalThis,
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ defaultGarbagePerFrame: "8",
+ defaultGarbagePiles: "8",
+
+ makeGarbage: N => {
+ var a = [];
+ var s = "x";
+ for (var i = 0; i < 16; i++) {
+ s = s + s;
+ }
+ for (var i = 0; i < N; i++) {
+ a.push(document.createTextNode(s));
+ }
+ garbage[garbageIndex++] = a;
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/deepWeakMap.js b/js/src/devtools/gc-ubench/benchmarks/deepWeakMap.js
new file mode 100644
index 0000000000..bf46d1d97b
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/deepWeakMap.js
@@ -0,0 +1,40 @@
+/* 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/. */
+
+tests.set(
+ "deepWeakMap",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "o={wm,k}; w.mk[k]=o2={wm2,k2}; wm2[k2]=....",
+
+ defaultGarbagePerFrame: "1K",
+ defaultGarbagePiles: "1K",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ makeGarbage: M => {
+ var initial = {};
+ var prev = initial;
+ for (var i = 0; i < M; i++) {
+ var obj = [new WeakMap(), Object.create(null)];
+ obj[0].set(obj[1], prev);
+ prev = obj;
+ }
+ garbage[garbageIndex++] = initial;
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/events.js b/js/src/devtools/gc-ubench/benchmarks/events.js
new file mode 100644
index 0000000000..0fe91b7596
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/events.js
@@ -0,0 +1,40 @@
+/* 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/. */
+
+tests.set(
+ "events",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = [ textNode, textNode, ... ]",
+
+ enabled: "document" in globalThis,
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ defaultGarbagePerFrame: "100K",
+ defaultGarbagePiles: "8",
+
+ makeGarbage: N => {
+ var a = [];
+ for (var i = 0; i < N; i++) {
+ var e = document.createEvent("Events");
+ e.initEvent("TestEvent", true, true);
+ a.push(e);
+ }
+ garbage[garbageIndex++] = a;
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/expandoEvents.js b/js/src/devtools/gc-ubench/benchmarks/expandoEvents.js
new file mode 100644
index 0000000000..4c6a53c8fe
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/expandoEvents.js
@@ -0,0 +1,41 @@
+/* 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/. */
+
+tests.set(
+ "expandoEvents",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = [ textNode, textNode, ... ]",
+
+ enabled: "document" in globalThis,
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ defaultGarbagePerFrame: "100K",
+ defaultGarbagePiles: "8",
+
+ makeGarbage: N => {
+ var a = [];
+ for (var i = 0; i < N; i++) {
+ var e = document.createEvent("Events");
+ e.initEvent("TestEvent", true, true);
+ e.color = ["tuna"];
+ a.push(e);
+ }
+ garbage[garbageIndex++] = a;
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/globalArrayArrayLiteral.js b/js/src/devtools/gc-ubench/benchmarks/globalArrayArrayLiteral.js
new file mode 100644
index 0000000000..a9003d8be6
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/globalArrayArrayLiteral.js
@@ -0,0 +1,32 @@
+/* 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/. */
+
+tests.set(
+ "globalArrayArrayLiteral",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = [[], ....]",
+ defaultGarbagePerFrame: "1M",
+ defaultGarbagePiles: "1K",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+ makeGarbage: N => {
+ for (var i = 0; i < N; i++) {
+ garbage[garbageIndex++] = ["foo", "bar", "baz", "baa"];
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/globalArrayBuffer.js b/js/src/devtools/gc-ubench/benchmarks/globalArrayBuffer.js
new file mode 100644
index 0000000000..06bf73eea8
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/globalArrayBuffer.js
@@ -0,0 +1,36 @@
+/* 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/. */
+
+tests.set(
+ "globalArrayBuffer",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = ArrayBuffer(N); # (large malloc data)",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ defaultGarbagePerFrame: "4M",
+ defaultGarbagePiles: "8K",
+
+ makeGarbage: N => {
+ var ab = new ArrayBuffer(N);
+ var view = new Uint8Array(ab);
+ view[0] = 1;
+ view[N - 1] = 2;
+ garbage[garbageIndex++] = ab;
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/globalArrayFgFinalized.js b/js/src/devtools/gc-ubench/benchmarks/globalArrayFgFinalized.js
new file mode 100644
index 0000000000..d61d56b81c
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/globalArrayFgFinalized.js
@@ -0,0 +1,37 @@
+/* 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/. */
+
+tests.set(
+ "globalArrayFgFinalized",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description:
+ "var foo = [ new Map, new Map, ... ]; # (foreground finalized)",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ defaultGarbagePiles: "8K",
+ defaultGarbagePerFrame: "48K",
+
+ makeGarbage: N => {
+ var arr = [];
+ for (var i = 0; i < N; i++) {
+ arr.push(new Map());
+ }
+ garbage[garbageIndex++] = arr;
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/globalArrayLargeArray.js b/js/src/devtools/gc-ubench/benchmarks/globalArrayLargeArray.js
new file mode 100644
index 0000000000..38e4f2a85b
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/globalArrayLargeArray.js
@@ -0,0 +1,34 @@
+/* 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/. */
+
+tests.set(
+ "globalArrayLargeArray",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = [[...], ....]",
+ defaultGarbagePerFrame: "3M",
+ defaultGarbagePiles: "1K",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+ makeGarbage: N => {
+ var a = new Array(N);
+ for (var i = 0; i < N; i++) {
+ a[i] = N - i;
+ }
+ garbage[garbageIndex++] = a;
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/globalArrayLargeObject.js b/js/src/devtools/gc-ubench/benchmarks/globalArrayLargeObject.js
new file mode 100644
index 0000000000..ed1c38b271
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/globalArrayLargeObject.js
@@ -0,0 +1,36 @@
+/* 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/. */
+
+tests.set(
+ "globalArrayLargeObject",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = { LARGE }; # (large slots)",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ defaultGarbagePiles: "8K",
+ defaultGarbagePerFrame: "64K",
+
+ makeGarbage: N => {
+ var obj = {};
+ for (var i = 0; i < N; i++) {
+ obj["key" + i] = i;
+ }
+ garbage[garbageIndex++] = obj;
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/globalArrayNewObject.js b/js/src/devtools/gc-ubench/benchmarks/globalArrayNewObject.js
new file mode 100644
index 0000000000..04487aac20
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/globalArrayNewObject.js
@@ -0,0 +1,33 @@
+/* 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/. */
+
+tests.set(
+ "globalArrayNewObject",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = [new Object(), ....]",
+ defaultGarbagePerFrame: "128K",
+ defaultGarbagePiles: "1K",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ makeGarbage: N => {
+ for (var i = 0; i < N; i++) {
+ garbage[garbageIndex++] = new Object();
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/globalArrayObjectLiteral.js b/js/src/devtools/gc-ubench/benchmarks/globalArrayObjectLiteral.js
new file mode 100644
index 0000000000..a31e76bdc6
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/globalArrayObjectLiteral.js
@@ -0,0 +1,32 @@
+/* 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/. */
+
+tests.set(
+ "globalArrayObjectLiteral",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = [{}, ....]",
+ defaultGarbagePerFrame: "384K",
+ defaultGarbagePiles: "1K",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+ makeGarbage: N => {
+ for (var i = 0; i < N; i++) {
+ garbage[garbageIndex++] = { a: "foo", b: "bar", 0: "foo", 1: "bar" };
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/globalArrayReallocArray.js b/js/src/devtools/gc-ubench/benchmarks/globalArrayReallocArray.js
new file mode 100644
index 0000000000..54316736e1
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/globalArrayReallocArray.js
@@ -0,0 +1,34 @@
+/* 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/. */
+
+tests.set(
+ "globalArrayReallocArray",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = [[,,,], ....]",
+ defaultGarbagePerFrame: "2M",
+ defaultGarbagePiles: "1K",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+ makeGarbage: N => {
+ var a = [];
+ for (var i = 0; i < N; i++) {
+ a[i] = N - i;
+ }
+ garbage[garbageIndex++] = a;
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/largeArrayPropertyAndElements.js b/js/src/devtools/gc-ubench/benchmarks/largeArrayPropertyAndElements.js
new file mode 100644
index 0000000000..0202f56e40
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/largeArrayPropertyAndElements.js
@@ -0,0 +1,40 @@
+/* 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/. */
+
+tests.set(
+ "largeArrayPropertyAndElements",
+ (function() {
+ var garbage;
+ var index;
+
+ return {
+ description: "Large array with both properties and elements",
+
+ load: n => {
+ garbage = new Array(n);
+ garbage.fill(null);
+ index = 0;
+ },
+
+ unload: () => {
+ garbage = null;
+ index = 0;
+ },
+
+ defaultGarbagePiles: "100K",
+ defaultGarbagePerFrame: "48K",
+
+ makeGarbage: n => {
+ for (var i = 0; i < n; i++) {
+ index++;
+ index %= garbage.length;
+
+ var obj = {};
+ garbage[index] = obj;
+ garbage["key-" + index] = obj;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/noAllocation.js b/js/src/devtools/gc-ubench/benchmarks/noAllocation.js
new file mode 100644
index 0000000000..8e6ba53943
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/noAllocation.js
@@ -0,0 +1,10 @@
+/* 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/. */
+
+tests.set("noAllocation", {
+ description: "Do not generate any garbage.",
+ load: N => {},
+ unload: () => {},
+ makeGarbage: N => {},
+});
diff --git a/js/src/devtools/gc-ubench/benchmarks/pairCyclicWeakMap.js b/js/src/devtools/gc-ubench/benchmarks/pairCyclicWeakMap.js
new file mode 100644
index 0000000000..ac43325b6f
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/pairCyclicWeakMap.js
@@ -0,0 +1,46 @@
+/* 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/. */
+
+tests.set(
+ "pairCyclicWeakMap",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "wm1[k1] = k2; wm2[k2] = k3; wm1[k3] = k4; wm2[k4] = ...",
+
+ defaultGarbagePerFrame: "10K",
+ defaultGarbagePiles: "1K",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ makeGarbage: M => {
+ var wm1 = new WeakMap();
+ var wm2 = new WeakMap();
+ var initialKey = {};
+ var key = initialKey;
+ var value = {};
+ for (var i = 0; i < M / 2; i++) {
+ wm1.set(key, value);
+ key = value;
+ value = {};
+ wm2.set(key, value);
+ key = value;
+ value = {};
+ }
+ garbage[garbageIndex++] = [initialKey, wm1, wm2];
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/propertyTreeSplitting.js b/js/src/devtools/gc-ubench/benchmarks/propertyTreeSplitting.js
new file mode 100644
index 0000000000..9fc62b29e2
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/propertyTreeSplitting.js
@@ -0,0 +1,24 @@
+/* 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/. */
+
+tests.set(
+ "propertyTreeSplitting",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ var obj = {};
+ return {
+ description: "use delete to generate Shape garbage (piles are unused)",
+ load: N => {},
+ unload: () => {},
+ makeGarbage: N => {
+ for (var a = 0; a < N; ++a) {
+ obj.x = 1;
+ obj.y = 2;
+ delete obj.x;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/selfCyclicWeakMap.js b/js/src/devtools/gc-ubench/benchmarks/selfCyclicWeakMap.js
new file mode 100644
index 0000000000..c7736c72b9
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/selfCyclicWeakMap.js
@@ -0,0 +1,42 @@
+/* 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/. */
+
+tests.set(
+ "selfCyclicWeakMap",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var wm = new WeakMap(); wm[k1] = k2; wm[k2] = k3; ...",
+
+ defaultGarbagePerFrame: "10K",
+ defaultGarbagePiles: "1K",
+
+ load: N => {
+ garbage = new Array(N);
+ },
+
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ makeGarbage: M => {
+ var wm = new WeakMap();
+ var initialKey = {};
+ var key = initialKey;
+ var value = {};
+ for (var i = 0; i < M; i++) {
+ wm.set(key, value);
+ key = value;
+ value = {};
+ }
+ garbage[garbageIndex++] = [initialKey, wm];
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/benchmarks/textNodes.js b/js/src/devtools/gc-ubench/benchmarks/textNodes.js
new file mode 100644
index 0000000000..07fd07e7b7
--- /dev/null
+++ b/js/src/devtools/gc-ubench/benchmarks/textNodes.js
@@ -0,0 +1,38 @@
+/* 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/. */
+
+tests.set(
+ "textNodes",
+ (function() {
+ var garbage = [];
+ var garbageIndex = 0;
+ return {
+ description: "var foo = [ textNode, textNode, ... ]",
+
+ enabled: "document" in globalThis,
+
+ load: N => {
+ garbage = new Array(N);
+ },
+ unload: () => {
+ garbage = [];
+ garbageIndex = 0;
+ },
+
+ defaultGarbagePerFrame: "100K",
+ defaultGarbagePiles: "8",
+
+ makeGarbage: N => {
+ var a = [];
+ for (var i = 0; i < N; i++) {
+ a.push(document.createTextNode("t" + i));
+ }
+ garbage[garbageIndex++] = a;
+ if (garbageIndex == garbage.length) {
+ garbageIndex = 0;
+ }
+ },
+ };
+ })()
+);
diff --git a/js/src/devtools/gc-ubench/harness.js b/js/src/devtools/gc-ubench/harness.js
new file mode 100644
index 0000000000..db7fa06d63
--- /dev/null
+++ b/js/src/devtools/gc-ubench/harness.js
@@ -0,0 +1,328 @@
+/* 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/. */
+
+// Global defaults
+
+// Allocate this much "garbage" per frame. This might correspond exactly to a
+// number of objects/values, or it might be some set of objects, depending on
+// the mutator in question.
+var gDefaultGarbagePerFrame = "8K";
+
+// In order to avoid a performance cliff between when the per-frame garbage
+// fits in the nursery and when it doesn't, most mutators will collect multiple
+// "piles" of garbage and round-robin through them, so that the per-frame
+// garbage stays alive for some number of frames. There will still be some
+// internal temporary allocations that don't end up in the piles; presumably,
+// the nursery will take care of those.
+//
+// If the per-frame garbage is K and the number of piles is P, then some of the
+// garbage will start getting tenured as long as P*K > size(nursery).
+var gDefaultGarbagePiles = "8";
+
+var gDefaultTestDuration = 8.0;
+
+// The Host interface that provides functionality needed by the test harnesses
+// (web + various shells). Subclasses should override with the appropriate
+// functionality. The methods that throw an error must be implemented. The ones
+// that return undefined are optional.
+//
+// Note that currently the web UI doesn't really use the scheduling pieces of
+// this.
+var Host = class {
+ constructor() {}
+ start_turn() {
+ throw new Error("unimplemented");
+ }
+ end_turn() {
+ throw new Error("unimplemented");
+ }
+ suspend(duration) {
+ throw new Error("unimplemented");
+ } // Shell driver only
+ now() {
+ return performance.now();
+ }
+
+ minorGCCount() {
+ return undefined;
+ }
+ majorGCCount() {
+ return undefined;
+ }
+ GCSliceCount() {
+ return undefined;
+ }
+
+ features = {
+ haveMemorySizes: false,
+ haveGCCounts: false,
+ };
+};
+
+function percent(x) {
+ return `${(x*100).toFixed(2)}%`;
+}
+
+function parse_units(v) {
+ if (!v.length) {
+ return NaN;
+ }
+ var lastChar = v[v.length - 1].toLowerCase();
+ if (!isNaN(parseFloat(lastChar))) {
+ return parseFloat(v);
+ }
+ var units = parseFloat(v.substr(0, v.length - 1));
+ if (lastChar == "k") {
+ return units * 1e3;
+ }
+ if (lastChar == "m") {
+ return units * 1e6;
+ }
+ if (lastChar == "g") {
+ return units * 1e9;
+ }
+ return NaN;
+}
+
+var AllocationLoad = class {
+ constructor(info, name) {
+ this.load = info;
+ this.load.name = this.load.name ?? name;
+
+ this._garbagePerFrame =
+ info.garbagePerFrame ||
+ parse_units(info.defaultGarbagePerFrame || gDefaultGarbagePerFrame);
+ this._garbagePiles =
+ info.garbagePiles ||
+ parse_units(info.defaultGarbagePiles || gDefaultGarbagePiles);
+ }
+
+ get name() {
+ return this.load.name;
+ }
+ get description() {
+ return this.load.description;
+ }
+ get garbagePerFrame() {
+ return this._garbagePerFrame;
+ }
+ set garbagePerFrame(amount) {
+ this._garbagePerFrame = amount;
+ }
+ get garbagePiles() {
+ return this._garbagePiles;
+ }
+ set garbagePiles(amount) {
+ this._garbagePiles = amount;
+ }
+
+ start() {
+ this.load.load(this._garbagePiles);
+ }
+
+ stop() {
+ this.load.unload();
+ }
+
+ reload() {
+ this.stop();
+ this.start();
+ }
+
+ tick() {
+ this.load.makeGarbage(this._garbagePerFrame);
+ }
+
+ is_dummy_load() {
+ return this.load.name == "noAllocation";
+ }
+};
+
+var AllocationLoadManager = class {
+ constructor(tests) {
+ this._loads = new Map();
+ for (const [name, info] of tests.entries()) {
+ this._loads.set(name, new AllocationLoad(info, name));
+ }
+ this._active = undefined;
+ this._paused = false;
+
+ // Public API
+ this.sequencer = null;
+ this.testDurationMS = gDefaultTestDuration * 1000;
+ }
+
+ getByName(name) {
+ const mutator = this._loads.get(name);
+ if (!mutator) {
+ throw new Error(`invalid mutator '${name}'`);
+ }
+ return mutator;
+ }
+
+ activeLoad() {
+ return this._active;
+ }
+
+ setActiveLoad(mutator) {
+ if (this._active) {
+ this._active.stop();
+ }
+ this._active = mutator;
+ this._active.start();
+ }
+
+ deactivateLoad() {
+ this._active.stop();
+ this._active = undefined;
+ }
+
+ get paused() {
+ return this._paused;
+ }
+ set paused(pause) {
+ this._paused = pause;
+ }
+
+ load_running() {
+ return this._active;
+ }
+
+ change_garbagePiles(amount) {
+ if (this._active) {
+ this._active.garbagePiles = amount;
+ this._active.reload();
+ }
+ }
+
+ change_garbagePerFrame(amount) {
+ if (this._active) {
+ this._active.garbagePerFrame = amount;
+ }
+ }
+
+ tick(now = gHost.now()) {
+ this.lastActive = this._active;
+ let completed = false;
+
+ if (this.sequencer) {
+ if (this.sequencer.tick(now)) {
+ completed = true;
+ if (this.sequencer.current) {
+ this.setActiveLoad(this.sequencer.current);
+ } else {
+ this.deactivateLoad();
+ }
+ if (this.sequencer.done()) {
+ this.sequencer = null;
+ }
+ }
+ }
+
+ if (this._active && !this._paused) {
+ this._active.tick();
+ }
+
+ return completed;
+ }
+
+ startSequencer(sequencer, now = gHost.now()) {
+ this.sequencer = sequencer;
+ this.sequencer.start(now);
+ this.setActiveLoad(this.sequencer.current);
+ }
+
+ stopped() {
+ return !this.sequencer || this.sequencer.done();
+ }
+
+ currentLoadRemaining(now = gHost.now()) {
+ if (this.stopped()) {
+ return 0;
+ }
+
+ // TODO: The web UI displays a countdown to the end of the current mutator.
+ // This won't work for potential future things like "run until 3 major GCs
+ // have been seen", so the API will need to be modified to provide
+ // information in that case.
+ return this.testDurationMS - this.sequencer.currentLoadElapsed(now);
+ }
+};
+
+// Current test state.
+var gLoadMgr = undefined;
+
+function format_with_units(n, label, shortlabel, kbase) {
+ if (n < kbase * 4) {
+ return `${n} ${label}`;
+ } else if (n < kbase ** 2 * 4) {
+ return `${(n / kbase).toFixed(2)}K${shortlabel}`;
+ } else if (n < kbase ** 3 * 4) {
+ return `${(n / kbase ** 2).toFixed(2)}M${shortlabel}`;
+ }
+ return `${(n / kbase ** 3).toFixed(2)}G${shortlabel}`;
+}
+
+function format_bytes(bytes) {
+ return format_with_units(bytes, "bytes", "B", 1024);
+}
+
+function format_num(n) {
+ return format_with_units(n, "", "", 1000);
+}
+
+function update_histogram(histogram, delay) {
+ // Round to a whole number of 10us intervals to provide enough resolution to
+ // capture a 16ms target with adequate accuracy.
+ delay = Math.round(delay * 100) / 100;
+ var current = histogram.has(delay) ? histogram.get(delay) : 0;
+ histogram.set(delay, ++current);
+}
+
+// Compute a score based on the total ms we missed frames by per second.
+function compute_test_score(histogram) {
+ var score = 0;
+ for (let [delay, count] of histogram) {
+ score += Math.abs((delay - 1000 / 60) * count);
+ }
+ score = score / (gLoadMgr.testDurationMS / 1000);
+ return Math.round(score * 1000) / 1000;
+}
+
+// Build a spark-lines histogram for the test results to show with the aggregate score.
+function compute_spark_histogram_percents(histogram) {
+ var ranges = [
+ [-99999999, 16.6],
+ [16.6, 16.8],
+ [16.8, 25],
+ [25, 33.4],
+ [33.4, 60],
+ [60, 100],
+ [100, 300],
+ [300, 99999999],
+ ];
+ var rescaled = new Map();
+ for (let [delay] of histogram) {
+ for (var i = 0; i < ranges.length; ++i) {
+ var low = ranges[i][0];
+ var high = ranges[i][1];
+ if (low <= delay && delay < high) {
+ update_histogram(rescaled, i);
+ break;
+ }
+ }
+ }
+ var total = 0;
+ for (const [, count] of rescaled) {
+ total += count;
+ }
+
+ var spark = [];
+ for (let i = 0; i < ranges.length; ++i) {
+ const amt = rescaled.has(i) ? rescaled.get(i) : 0;
+ spark.push(amt / total);
+ }
+
+ return spark;
+}
diff --git a/js/src/devtools/gc-ubench/index.html b/js/src/devtools/gc-ubench/index.html
new file mode 100644
index 0000000000..4abce385f2
--- /dev/null
+++ b/js/src/devtools/gc-ubench/index.html
@@ -0,0 +1,90 @@
+<!-- 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/. -->
+
+<html>
+<head>
+ <title>GC uBench</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+ <!-- Benchmark harness and UI -->
+ <script src="harness.js"></script>
+ <script src="perf.js"></script>
+ <script src="sequencer.js"></script>
+ <script src="ui.js"></script>
+
+ <!-- List of garbage-creating test loads -->
+ <script src="test_list.js"></script>
+
+ <!-- Collect all test loads into a `tests` Map -->
+ <script>
+ var tests = new Map();
+ foreach_test_file(path => import("./" + path));
+ </script>
+
+</head>
+
+<body onload="onload()" onunload="onunload()">
+
+<canvas id="graph" width="1080" height="400" style="padding-left:10px"></canvas>
+<canvas id="memgraph" width="1080" height="400" style="padding-left:10px"></canvas>
+<div id="memgraph-disabled" style="display: none"><i>No performance.mozMemory object available. If running Firefox, set dom.enable_memory_stats to True to see heap size info.</i></div>
+
+<hr>
+
+<div id='track-sizes-div'>
+ Show heap size graph: <input id='track-sizes' type='checkbox' onclick="trackHeapSizes(this.checked)">
+</div>
+
+<div>
+ Update display:
+ <input type="checkbox" id="do-graph" onchange="onUpdateDisplayChanged()" checked></input>
+</div>
+
+<div>
+ Run allocation load
+ <input type="checkbox" id="do-load" onchange="onDoLoadChange()" checked></input>
+</div>
+
+<div>
+ Allocation load:
+ <select id="test-selection" required onchange="onLoadChange()"></select>
+ <span id="load-running">(init)</span>
+</div>
+
+<div>
+ &nbsp;&nbsp;&nbsp;&nbsp;Garbage items per frame:
+ <input type="text" id="garbage-per-frame" size="5" value="8K"
+ onchange="garbage_per_frame_changed()"></input>
+</div>
+<div>
+ &nbsp;&nbsp;&nbsp;&nbsp;Garbage piles:
+ <input type="text" id="garbage-piles" size="5" value="8"
+ onchange="garbage_piles_changed()"></input>
+</div>
+
+<hr>
+
+<div>
+ Duration: <input type="text" id="test-duration" size="3" value="8" onchange="duration_changed()"></input>s
+ <input type="button" id="test-one" value="Run Test" onclick="run_one_test()"></input>
+ <input type="button" id="test-all" value="Run All Tests" onclick="run_all_tests()"></input>
+</div>
+
+<div>
+ &nbsp;&nbsp;&nbsp;&nbsp;Time remaining: <span id="test-progress">(not running)</span>
+</div
+
+<div>
+ &nbsp;&nbsp;&nbsp;&nbsp;60 fps: <span id="pct60">n/a</span>
+ &nbsp;&nbsp;&nbsp;&nbsp;45 fps: <span id="pct45">n/a</span>
+ &nbsp;&nbsp;&nbsp;&nbsp;30 fps: <span id="pct30">n/a</span>
+</div
+
+<div id="results-Area">
+ Test Results:
+ <div id="results-display" style="padding-left: 10px; border: 1px solid black;"></div>
+</div>
+
+</body>
+</html>
diff --git a/js/src/devtools/gc-ubench/perf.js b/js/src/devtools/gc-ubench/perf.js
new file mode 100644
index 0000000000..dc370ee0da
--- /dev/null
+++ b/js/src/devtools/gc-ubench/perf.js
@@ -0,0 +1,217 @@
+/* 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/. */
+
+// Performance monitoring and calculation.
+
+function round_up(val, interval) {
+ return val + (interval - (val % interval));
+}
+
+// Class for inter-frame timing, which handles being paused and resumed.
+var FrameTimer = class {
+ constructor() {
+ // Start time of the current active test, adjusted for any time spent
+ // stopped (so `now - this.start` is how long the current active test
+ // has run for.)
+ this.start = undefined;
+
+ // Timestamp of callback following the previous frame.
+ this.prev = undefined;
+
+ // Timestamp when drawing was paused, or zero if drawing is active.
+ this.stopped = 0;
+ }
+
+ is_stopped() {
+ return this.stopped != 0;
+ }
+
+ start_recording(now = gHost.now()) {
+ this.start = this.prev = now;
+ }
+
+ on_frame_finished(now = gHost.now()) {
+ const delay = now - this.prev;
+ this.prev = now;
+ return delay;
+ }
+
+ pause(now = gHost.now()) {
+ this.stopped = now;
+ // Abuse this.prev to store the time elapsed since the previous frame.
+ // This will be used to adjust this.prev when we resume.
+ this.prev = now - this.prev;
+ }
+
+ resume(now = gHost.now()) {
+ this.prev = now - this.prev;
+ const stop_duration = now - this.stopped;
+ this.start += stop_duration;
+ this.stopped = 0;
+ }
+};
+
+// Per-frame time sampling infra.
+var sampleTime = 16.666667; // ms
+var sampleIndex = 0;
+
+// Class for maintaining a rolling window of per-frame GC-related counters:
+// inter-frame delay, minor/major/slice GC counts, cumulative bytes, etc.
+var FrameHistory = class {
+ constructor(numSamples) {
+ // Private
+ this._frameTimer = new FrameTimer();
+ this._numSamples = numSamples;
+
+ // Public API
+ this.delays = new Array(numSamples);
+ this.gcBytes = new Array(numSamples);
+ this.mallocBytes = new Array(numSamples);
+ this.gcs = new Array(numSamples);
+ this.minorGCs = new Array(numSamples);
+ this.majorGCs = new Array(numSamples);
+ this.slices = new Array(numSamples);
+
+ sampleIndex = 0;
+ this.reset();
+ }
+
+ start(now = gHost.now()) {
+ this._frameTimer.start_recording(now);
+ }
+
+ reset() {
+ this.delays.fill(0);
+ this.gcBytes.fill(0);
+ this.mallocBytes.fill(0);
+ this.gcs.fill(this.gcs[sampleIndex]);
+ this.minorGCs.fill(this.minorGCs[sampleIndex]);
+ this.majorGCs.fill(this.majorGCs[sampleIndex]);
+ this.slices.fill(this.slices[sampleIndex]);
+
+ sampleIndex = 0;
+ }
+
+ get numSamples() {
+ return this._numSamples;
+ }
+
+ findMax(collection) {
+ // Depends on having at least one non-negative entry, and unfilled
+ // entries being <= max.
+ var maxIndex = 0;
+ for (let i = 0; i < this._numSamples; i++) {
+ if (collection[i] >= collection[maxIndex]) {
+ maxIndex = i;
+ }
+ }
+ return maxIndex;
+ }
+
+ findMaxDelay() {
+ return this.findMax(this.delays);
+ }
+
+ on_frame(now = gHost.now()) {
+ const delay = this._frameTimer.on_frame_finished(now);
+
+ // Total time elapsed while the active test has been running.
+ var t = now - this._frameTimer.start;
+ var newIndex = Math.round(t / sampleTime);
+ while (sampleIndex < newIndex) {
+ sampleIndex++;
+ var idx = sampleIndex % this._numSamples;
+ this.delays[idx] = delay;
+ if (gHost.features.haveMemorySizes) {
+ this.gcBytes[idx] = gHost.gcBytes;
+ this.mallocBytes[idx] = gHost.mallocBytes;
+ }
+ if (gHost.features.haveGCCounts) {
+ this.minorGCs[idx] = gHost.minorGCCount;
+ this.majorGCs[idx] = gHost.majorGCCount;
+ this.slices[idx] = gHost.GCSliceCount;
+ }
+ }
+
+ return delay;
+ }
+
+ pause() {
+ this._frameTimer.pause();
+ }
+
+ resume() {
+ this._frameTimer.resume();
+ }
+
+ is_stopped() {
+ return this._frameTimer.is_stopped();
+ }
+};
+
+var PerfTracker = class {
+ constructor() {
+ // Private
+ this._currentLoadStart = undefined;
+ this._frameCount = undefined;
+ this._mutating_ms = undefined;
+ this._suspend_sec = undefined;
+ this._minorGCs = undefined;
+ this._majorGCs = undefined;
+
+ // Public
+ this.results = [];
+ }
+
+ on_load_start(load, now = gHost.now()) {
+ this._currentLoadStart = now;
+ this._frameCount = 0;
+ this._mutating_ms = 0;
+ this._suspend_sec = 0;
+ this._majorGCs = gHost.majorGCCount;
+ this._minorGCs = gHost.minorGCCount;
+ }
+
+ on_load_end(load, now = gHost.now()) {
+ const elapsed_time = (now - this._currentLoadStart) / 1000;
+ const full_time = round_up(elapsed_time, 1 / 60);
+ const frame_60fps_limit = Math.round(full_time * 60);
+ const dropped_60fps_frames = frame_60fps_limit - this._frameCount;
+ const dropped_60fps_fraction = dropped_60fps_frames / frame_60fps_limit;
+
+ const mutating_and_gc_fraction = this._mutating_ms / (full_time * 1000);
+
+ const result = {
+ load,
+ elapsed_time,
+ mutating: this._mutating_ms / 1000,
+ mutating_and_gc_fraction,
+ suspended: this._suspend_sec,
+ full_time,
+ frames: this._frameCount,
+ dropped_60fps_frames,
+ dropped_60fps_fraction,
+ majorGCs: gHost.majorGCCount - this._majorGCs,
+ minorGCs: gHost.minorGCCount - this._minorGCs,
+ };
+ this.results.push(result);
+
+ this._currentLoadStart = undefined;
+ this._frameCount = 0;
+
+ return result;
+ }
+
+ after_suspend(wait_sec) {
+ this._suspend_sec += wait_sec;
+ }
+
+ before_mutator(now = gHost.now()) {
+ this._frameCount++;
+ }
+
+ after_mutator(start_time, end_time = gHost.now()) {
+ this._mutating_ms += end_time - start_time;
+ }
+};
diff --git a/js/src/devtools/gc-ubench/scheduler.js b/js/src/devtools/gc-ubench/scheduler.js
new file mode 100644
index 0000000000..8f4a483b33
--- /dev/null
+++ b/js/src/devtools/gc-ubench/scheduler.js
@@ -0,0 +1,64 @@
+/* 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/. */
+
+// Frame schedulers: executing a frame's worth of mutation, and possibly
+// waiting for a later frame. (These schedulers will halt the main thread,
+// allowing background threads to continue working.)
+
+var Scheduler = class {
+ constructor(perfMonitor) {
+ this._perfMonitor = perfMonitor;
+ }
+
+ start(loadMgr, timestamp) {
+ return loadMgr.start(timestamp);
+ }
+ tick(loadMgr, timestamp) {}
+ wait_for_next_frame(t0, tick_start, tick_end) {}
+};
+
+// "Sync to vsync" scheduler: after the mutator is done for a frame, wait until
+// the beginning of the next 60fps frame.
+var VsyncScheduler = class extends Scheduler {
+ tick(loadMgr, timestamp) {
+ this._perfMonitor.before_mutator(timestamp);
+ gHost.start_turn();
+ const completed = loadMgr.tick(timestamp);
+ gHost.end_turn();
+ this._perfMonitor.after_mutator(timestamp);
+ return completed;
+ }
+
+ wait_for_next_frame(t0, tick_start, tick_end) {
+ // Compute how long until the next 60fps vsync event, and wait that long.
+ const elapsed = (tick_end - t0) / 1000;
+ const period = 1 / FPS;
+ const used = elapsed % period;
+ const delay = period - used;
+ gHost.suspend(delay);
+ this._perfMonitor.after_suspend(delay);
+ }
+};
+
+// Try to maintain 60fps, but if we overrun a frame, do more processing
+// immediately to make the next frame come up as soon as possible.
+var OptimizeForFrameRate = class extends Scheduler {
+ tick(loadMgr, timestamp) {
+ this._perfMonitor.before_mutator(timestamp);
+ gHost.start_turn();
+ const completed = loadMgr.tick(timestamp);
+ gHost.end_turn();
+ this._perfMonitor.after_mutator(timestamp);
+ return completed;
+ }
+
+ wait_for_next_frame(t0, tick_start, tick_end) {
+ const next_frame_ms = round_up(tick_start, 1000 / FPS);
+ if (tick_end < next_frame_ms) {
+ const delay = (next_frame_ms - tick_end) / 1000;
+ gHost.suspend(delay);
+ this._perfMonitor.after_suspend(delay);
+ }
+ }
+};
diff --git a/js/src/devtools/gc-ubench/sequencer.js b/js/src/devtools/gc-ubench/sequencer.js
new file mode 100644
index 0000000000..0271140c04
--- /dev/null
+++ b/js/src/devtools/gc-ubench/sequencer.js
@@ -0,0 +1,298 @@
+/* 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/. */
+
+// A Sequencer handles transitioning between different mutators. Typically, it
+// will base the decision to transition on things like elapsed time, number of
+// GCs observed, or similar. However, they might also implement a search for
+// some result value by running for some time while measuring, tweaking
+// parameters, and re-running until an in-range result is found.
+
+var Sequencer = class {
+ // Return the current mutator (of class AllocationLoad).
+ get current() {
+ throw new Error("unimplemented");
+ }
+
+ start(now = gHost.now()) {
+ this.started = now;
+ }
+
+ // Called by user to handle advancing time. Subclasses will normally override
+ // do_tick() instead. Returns the results of a trial if complete (the mutator
+ // reached its allotted time or otherwise determined that its timing data
+ // should be valid), and falsy otherwise.
+ tick(now = gHost.now()) {
+ if (this.done()) {
+ throw new Error("tick() called on completed sequencer");
+ }
+
+ return this.do_tick(now);
+ }
+
+ // Implement in subclass to handle time advancing. Must return trial's result
+ // if complete. Called by tick(), above.
+ do_tick(now = gHost.now()) {
+ throw new Error("unimplemented");
+ }
+
+ // Returns whether this sequencer is done running trials.
+ done() {
+ throw new Error("unimplemented");
+ }
+
+ restart(now = gHost.now()) {
+ this.reset();
+ this.start(now);
+ }
+
+ // Returns how long the current load has been running.
+ currentLoadElapsed(now = gHost.now()) {
+ return now - this.started;
+ }
+};
+
+// Run a single trial of a mutator and be done.
+var SingleMutatorSequencer = class extends Sequencer {
+ constructor(mutator, perf, duration_sec) {
+ super();
+ this.mutator = mutator;
+ this.perf = perf;
+ if (!(duration_sec > 0)) {
+ throw new Error(`invalid duration '${duration_sec}'`);
+ }
+ this.duration = duration_sec * 1000;
+ this.state = 'init'; // init -> running -> done
+ this.lastResult = undefined;
+ }
+
+ get current() {
+ return this.state === 'done' ? undefined : this.mutator;
+ }
+
+ reset() {
+ this.state = 'init';
+ }
+
+ start(now = gHost.now()) {
+ if (this.state !== 'init') {
+ throw new Error("cannot restart a single-mutator sequencer");
+ }
+ super.start(now);
+ this.state = 'running';
+ this.perf.on_load_start(this.current, now);
+ }
+
+ do_tick(now) {
+ if (this.currentLoadElapsed(now) < this.duration) {
+ return false;
+ }
+
+ const load = this.current;
+ this.state = 'done';
+ return this.perf.on_load_end(load, now);
+ }
+
+ done() {
+ return this.state === 'done';
+ }
+};
+
+// For each of series of sequencers, run until done.
+var ChainSequencer = class extends Sequencer {
+ constructor(sequencers) {
+ super();
+ this.sequencers = sequencers;
+ this.idx = -1;
+ this.state = sequencers.length ? 'init' : 'done'; // init -> running -> done
+ }
+
+ get current() {
+ return this.idx >= 0 ? this.sequencers[this.idx].current : undefined;
+ }
+
+ reset() {
+ this.state = 'init';
+ this.idx = -1;
+ }
+
+ start(now = gHost.now()) {
+ super.start(now);
+ if (this.sequencers.length === 0) {
+ this.state = 'done';
+ return;
+ }
+
+ this.idx = 0;
+ this.sequencers[0].start(now);
+ this.state = 'running';
+ }
+
+ do_tick(now) {
+ const sequencer = this.sequencers[this.idx];
+ const trial_result = sequencer.do_tick(now);
+ if (!trial_result) {
+ return false; // Trial is still going.
+ }
+
+ if (!sequencer.done()) {
+ // A single trial has completed, but the sequencer is not yet done.
+ return trial_result;
+ }
+
+ this.idx++;
+ if (this.idx < this.sequencers.length) {
+ this.sequencers[this.idx].start();
+ } else {
+ this.idx = -1;
+ this.state = 'done';
+ }
+
+ return trial_result;
+ }
+
+ done() {
+ return this.state === 'done';
+ }
+};
+
+var RunUntilSequencer = class extends Sequencer {
+ constructor(sequencer, loadMgr) {
+ super();
+ this.loadMgr = loadMgr;
+ this.sequencer = sequencer;
+
+ // init -> running -> done
+ this.state = sequencer.done() ? 'done' : 'init';
+ }
+
+ get current() {
+ return this.sequencer?.current;
+ }
+
+ reset() {
+ this.sequencer.reset();
+ this.state = 'init';
+ }
+
+ start(now) {
+ super.start(now);
+ this.sequencer.start(now);
+ this.initSearch(now);
+ this.state = 'running';
+ }
+
+ initSearch(now) {}
+
+ done() {
+ return this.state === 'done';
+ }
+
+ do_tick(now) {
+ const trial_result = this.sequencer.do_tick(now);
+ if (trial_result) {
+ if (this.searchComplete(trial_result)) {
+ this.state = 'done';
+ } else {
+ this.sequencer.restart(now);
+ }
+ }
+ return trial_result;
+ }
+
+ // Take the result of the last mutator run into account (only notified after
+ // a mutator is complete, so cannot be used to decide when to end the
+ // mutator.)
+ searchComplete(result) {
+ throw new Error("must implement in subclass");
+ }
+};
+
+// Run trials, adjusting garbagePerFrame, until 50% of the frames are dropped.
+var Find50Sequencer = class extends RunUntilSequencer {
+ constructor(sequencer, loadMgr, goal=0.5, low_range=0.45, high_range=0.55) {
+ super(sequencer, loadMgr);
+
+ // Run trials with varying garbagePerFrame, looking for a setting that
+ // drops 50% of the frames, until we have been searching in the range for
+ // `persistence` times.
+ this.low_range = low_range;
+ this.goal = goal;
+ this.high_range = high_range;
+ this.persistence = 3;
+
+ this.clear();
+ }
+
+ reset() {
+ super.reset();
+ this.clear();
+ }
+
+ clear() {
+ this.garbagePerFrame = undefined;
+
+ this.good = undefined;
+ this.goodAt = undefined;
+ this.bad = undefined;
+ this.badAt = undefined;
+
+ this.numInRange = 0;
+ }
+
+ start(now) {
+ super.start(now);
+ if (!this.done()) {
+ this.garbagePerFrame = this.sequencer.current.garbagePerFrame;
+ }
+ }
+
+ searchComplete(result) {
+ print(
+ `Saw ${percent(result.dropped_60fps_fraction)} with garbagePerFrame=${this.garbagePerFrame}`
+ );
+
+ // This is brittle with respect to noise. It might be better to do a linear
+ // regression and stop at an error threshold.
+ if (result.dropped_60fps_fraction < this.goal) {
+ if (this.goodAt === undefined || this.goodAt < this.garbagePerFrame) {
+ this.goodAt = this.garbagePerFrame;
+ this.good = result.dropped_60fps_fraction;
+ }
+ if (this.badAt !== undefined) {
+ this.garbagePerFrame = Math.trunc(
+ (this.garbagePerFrame + this.badAt) / 2
+ );
+ } else {
+ this.garbagePerFrame *= 2;
+ }
+ } else {
+ if (this.badAt === undefined || this.badAt > this.garbagePerFrame) {
+ this.badAt = this.garbagePerFrame;
+ this.bad = result.dropped_60fps_fraction;
+ }
+ if (this.goodAt !== undefined) {
+ this.garbagePerFrame = Math.trunc(
+ (this.garbagePerFrame + this.goodAt) / 2
+ );
+ } else {
+ this.garbagePerFrame = Math.trunc(this.garbagePerFrame / 2);
+ }
+ }
+
+ if (
+ this.low_range < result.dropped_60fps_fraction &&
+ result.dropped_60fps_fraction < this.high_range
+ ) {
+ this.numInRange++;
+ if (this.numInRange >= this.persistence) {
+ return true;
+ }
+ }
+
+ print(`next run with ${this.garbagePerFrame}`);
+ this.loadMgr.change_garbagePerFrame(this.garbagePerFrame);
+
+ return false;
+ }
+};
diff --git a/js/src/devtools/gc-ubench/shell-bench.js b/js/src/devtools/gc-ubench/shell-bench.js
new file mode 100644
index 0000000000..9640cddce9
--- /dev/null
+++ b/js/src/devtools/gc-ubench/shell-bench.js
@@ -0,0 +1,147 @@
+/* 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/. */
+
+var FPS = 60;
+var gNumSamples = 500;
+
+// This requires a gHost to have been created that provides host-specific
+// facilities. See eg spidermonkey.js.
+
+loadRelativeToScript("argparse.js");
+loadRelativeToScript("harness.js");
+loadRelativeToScript("sequencer.js");
+loadRelativeToScript("scheduler.js");
+loadRelativeToScript("perf.js");
+loadRelativeToScript("test_list.js");
+
+var gPerf = new PerfTracker();
+
+var tests = new Map();
+foreach_test_file(f => loadRelativeToScript(f));
+for (const [name, info] of tests.entries()) {
+ if ("enabled" in info && !info.enabled) {
+ tests.delete(name);
+ }
+}
+
+function tick(loadMgr, timestamp) {
+ gPerf.before_mutator(timestamp);
+ gHost.start_turn();
+ const events = loadMgr.tick(timestamp);
+ gHost.end_turn();
+ gPerf.after_mutator(timestamp);
+ return events;
+}
+
+function run(opts, loads) {
+ const sequence = [];
+ for (const mut of loads) {
+ if (tests.has(mut)) {
+ sequence.push(mut);
+ } else if (mut === "all") {
+ sequence.push(...tests.keys());
+ } else {
+ sequence.push(...[...tests.keys()].filter(t => t.includes(mut)));
+ }
+ }
+ if (loads.length === 0) {
+ sequence.push(...tests.keys());
+ }
+
+ const loadMgr = new AllocationLoadManager(tests);
+ const perf = new FrameHistory(gNumSamples);
+
+ const mutators = sequence.map(name => new SingleMutatorSequencer(loadMgr.getByName(name), gPerf, opts.duration));
+ let sequencer;
+ if (opts.sequencer == 'cycle') {
+ sequencer = new ChainSequencer(mutators);
+ } else if (opts.sequencer == 'find50') {
+ const seekers = mutators.map(s => new Find50Sequencer(s, loadMgr));
+ sequencer = new ChainSequencer(seekers);
+ }
+
+ const schedulerCtors = {
+ keepup: OptimizeForFrameRate,
+ vsync: VsyncScheduler,
+ };
+ const scheduler = new schedulerCtors[opts.sched](gPerf);
+
+ perf.start();
+
+ const t0 = gHost.now();
+
+ let possible = 0;
+ let frames = 0;
+ loadMgr.startSequencer(sequencer);
+ print(`${loadMgr.activeLoad().name} starting`);
+ while (loadMgr.load_running()) {
+ const timestamp = gHost.now();
+ const completed = scheduler.tick(loadMgr, timestamp);
+ const after_tick = gHost.now();
+
+ perf.on_frame(timestamp);
+
+ if (completed) {
+ print(`${loadMgr.lastActive.name} ended`);
+ if (loadMgr.load_running()) {
+ print(`${loadMgr.activeLoad().name} starting`);
+ }
+ }
+
+ frames++;
+ if (completed) {
+ possible += (loadMgr.testDurationMS / 1000) * FPS;
+ const elapsed = ((after_tick - t0) / 1000).toFixed(2);
+ print(` observed ${frames} / ${possible} frames in ${elapsed} seconds`);
+ }
+
+ scheduler.wait_for_next_frame(t0, timestamp, after_tick);
+ }
+}
+
+function report_results() {
+ for (const result of gPerf.results) {
+ const {
+ load,
+ elapsed_time,
+ mutating,
+ mutating_and_gc_fraction,
+ suspended,
+ full_time,
+ frames,
+ dropped_60fps_frames,
+ dropped_60fps_fraction,
+ minorGCs,
+ majorGCs,
+ } = result;
+
+ const drop_pct = percent(dropped_60fps_fraction);
+ const mut_pct = percent(mutating_and_gc_fraction);
+ const mut_sec = mutating.toFixed(2);
+ const full_sec = full_time.toFixed(2);
+ const susp_sec = suspended.toFixed(2);
+ print(`${load.name}:
+ ${frames} (60fps) frames seen out of expected ${Math.floor(full_time * 60)}
+ ${dropped_60fps_frames} = ${drop_pct} 60fps frames dropped
+ ${mut_pct} of run spent mutating and GCing (${mut_sec}sec out of ${full_sec}sec vs ${susp_sec} sec waiting)
+ ${minorGCs} minor GCs, ${majorGCs} major GCs
+`);
+ }
+}
+
+var argparse = new ArgParser("JS shell microbenchmark runner");
+argparse.add_argument(["--duration", "-d"], {
+ default: gDefaultTestDuration,
+ help: "how long to run mutators for (in seconds)"
+});
+argparse.add_argument("--sched", {
+ default: "keepup",
+ options: ["keepup", "vsync"],
+ help: "frame scheduler"
+});
+argparse.add_argument("--sequencer", {
+ default: "cycle",
+ options: ["cycle", "find50"],
+ help: "mutator sequencer"
+});
diff --git a/js/src/devtools/gc-ubench/spidermonkey.js b/js/src/devtools/gc-ubench/spidermonkey.js
new file mode 100644
index 0000000000..0ad0aa9771
--- /dev/null
+++ b/js/src/devtools/gc-ubench/spidermonkey.js
@@ -0,0 +1,57 @@
+/* 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/. */
+
+// SpiderMonkey JS shell benchmark script
+//
+// Usage: run $JS spidermonkey.js --help
+
+loadRelativeToScript("shell-bench.js");
+
+var SpiderMonkey = class extends Host {
+ start_turn() {}
+
+ end_turn() {
+ clearKeptObjects();
+ maybegc();
+ drainJobQueue();
+ }
+
+ suspend(duration) {
+ sleep(duration);
+ }
+
+ get minorGCCount() {
+ return performance.mozMemory.gc.minorGCCount;
+ }
+ get majorGCCount() {
+ return performance.mozMemory.gc.majorGCCount;
+ }
+ get GCSliceCount() {
+ return performance.mozMemory.gc.sliceCount;
+ }
+ get gcBytes() {
+ return performance.mozMemory.gc.zone.gcBytes;
+ }
+ get gcAllocTrigger() {
+ return performance.mozMemory.gc.zone.gcAllocTrigger;
+ }
+
+ features = {
+ haveMemorySizes: true,
+ haveGCCounts: true,
+ };
+};
+
+var gHost = new SpiderMonkey();
+var { opts, rest: mutators } = argparse.parse_args(scriptArgs);
+run(opts, mutators);
+
+print("\nTest results:\n");
+report_results();
+
+var outfile = "spidermonkey-results.json";
+var origOut = redirect(outfile);
+print(JSON.stringify(gPerf.results));
+redirect(origOut);
+print(`Wrote detailed results to ${outfile}`);
diff --git a/js/src/devtools/gc-ubench/test_list.js b/js/src/devtools/gc-ubench/test_list.js
new file mode 100644
index 0000000000..03ed30cf9e
--- /dev/null
+++ b/js/src/devtools/gc-ubench/test_list.js
@@ -0,0 +1,20 @@
+function foreach_test_file(callback) {
+ callback("benchmarks/noAllocation.js");
+ callback("benchmarks/globalArrayNewObject.js");
+ callback("benchmarks/globalArrayArrayLiteral.js");
+ callback("benchmarks/globalArrayLargeArray.js");
+ callback("benchmarks/globalArrayLargeObject.js");
+ callback("benchmarks/globalArrayObjectLiteral.js");
+ callback("benchmarks/globalArrayReallocArray.js");
+ callback("benchmarks/globalArrayBuffer.js");
+ callback("benchmarks/globalArrayFgFinalized.js");
+ callback("benchmarks/largeArrayPropertyAndElements.js");
+ callback("benchmarks/selfCyclicWeakMap.js");
+ callback("benchmarks/pairCyclicWeakMap.js");
+ callback("benchmarks/deepWeakMap.js");
+ callback("benchmarks/textNodes.js");
+ callback("benchmarks/bigTextNodes.js");
+ callback("benchmarks/events.js");
+ callback("benchmarks/expandoEvents.js");
+ callback("benchmarks/propertyTreeSplitting.js");
+}
diff --git a/js/src/devtools/gc-ubench/ui.js b/js/src/devtools/gc-ubench/ui.js
new file mode 100644
index 0000000000..4905f97904
--- /dev/null
+++ b/js/src/devtools/gc-ubench/ui.js
@@ -0,0 +1,700 @@
+/* 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/. */
+
+var stroke = {
+ gcslice: "rgb(255,100,0)",
+ minor: "rgb(0,255,100)",
+ initialMajor: "rgb(180,60,255)",
+};
+
+var numSamples = 500;
+
+var gHistogram = new Map(); // {ms: count}
+var gHistory = new FrameHistory(numSamples);
+var gPerf = new PerfTracker();
+
+var latencyGraph;
+var memoryGraph;
+var ctx;
+var memoryCtx;
+
+var loadState = "(init)"; // One of '(active)', '(inactive)', '(N/A)'
+var testState = "idle"; // One of 'idle' or 'running'.
+var enabled = { trackingSizes: false };
+
+var gMemory = performance.mozMemory?.gc || performance.mozMemory || {};
+
+var Firefox = class extends Host {
+ start_turn() {
+ // Handled by Gecko.
+ }
+
+ end_turn() {
+ // Handled by Gecko.
+ }
+
+ suspend(duration) {
+ // Not used; requestAnimationFrame takes its place.
+ throw new Error("unimplemented");
+ }
+
+ get minorGCCount() {
+ return gMemory.minorGCCount;
+ }
+ get majorGCCount() {
+ return gMemory.majorGCCount;
+ }
+ get GCSliceCount() {
+ return gMemory.sliceCount;
+ }
+ get gcBytes() {
+ return gMemory.zone.gcBytes;
+ }
+ get gcAllocTrigger() {
+ return gMemory.zone.gcAllocTrigger;
+ }
+
+ features = {
+ haveMemorySizes: 'gcBytes' in gMemory,
+ haveGCCounts: 'majorGCCount' in gMemory,
+ };
+};
+
+var gHost = new Firefox();
+
+function parse_units(v) {
+ if (!v.length) {
+ return NaN;
+ }
+ var lastChar = v[v.length - 1].toLowerCase();
+ if (!isNaN(parseFloat(lastChar))) {
+ return parseFloat(v);
+ }
+ var units = parseFloat(v.substr(0, v.length - 1));
+ if (lastChar == "k") {
+ return units * 1e3;
+ }
+ if (lastChar == "m") {
+ return units * 1e6;
+ }
+ if (lastChar == "g") {
+ return units * 1e9;
+ }
+ return NaN;
+}
+
+var Graph = class {
+ constructor(ctx) {
+ this.ctx = ctx;
+
+ var { height } = ctx.canvas;
+ this.layout = {
+ xAxisLabel_Y: height - 20,
+ };
+ }
+
+ xpos(index) {
+ return index * 2;
+ }
+
+ clear() {
+ const { width, height } = this.ctx.canvas;
+ this.ctx.clearRect(0, 0, width, height);
+ }
+
+ drawScale(delay) {
+ this.drawHBar(delay, `${delay}ms`, "rgb(150,150,150)");
+ }
+
+ draw60fps() {
+ this.drawHBar(1000 / 60, "60fps", "#00cf61", 25);
+ }
+
+ draw30fps() {
+ this.drawHBar(1000 / 30, "30fps", "#cf0061", 25);
+ }
+
+ drawAxisLabels(x_label, y_label) {
+ const ctx = this.ctx;
+ const { width, height } = ctx.canvas;
+
+ ctx.fillText(x_label, width / 2, this.layout.xAxisLabel_Y);
+
+ ctx.save();
+ ctx.rotate(Math.PI / 2);
+ var start = height / 2 - ctx.measureText(y_label).width / 2;
+ ctx.fillText(y_label, start, -width + 20);
+ ctx.restore();
+ }
+
+ drawFrame() {
+ const ctx = this.ctx;
+ const { width, height } = ctx.canvas;
+
+ // Draw frame to show size
+ ctx.strokeStyle = "rgb(0,0,0)";
+ ctx.fillStyle = "rgb(0,0,0)";
+ ctx.beginPath();
+ ctx.moveTo(0, 0);
+ ctx.lineTo(width, 0);
+ ctx.lineTo(width, height);
+ ctx.lineTo(0, height);
+ ctx.closePath();
+ ctx.stroke();
+ }
+};
+
+var LatencyGraph = class extends Graph {
+ constructor(ctx) {
+ super(ctx);
+ console.log(this.ctx);
+ }
+
+ ypos(delay) {
+ const { height } = this.ctx.canvas;
+
+ const r = height + 100 - Math.log(delay) * 64;
+ if (r < 5) {
+ return 5;
+ }
+ return r;
+ }
+
+ drawHBar(delay, label, color = "rgb(0,0,0)", label_offset = 0) {
+ const ctx = this.ctx;
+
+ ctx.fillStyle = color;
+ ctx.strokeStyle = color;
+ ctx.fillText(
+ label,
+ this.xpos(numSamples) + 4 + label_offset,
+ this.ypos(delay) + 3
+ );
+
+ ctx.beginPath();
+ ctx.moveTo(this.xpos(0), this.ypos(delay));
+ ctx.lineTo(this.xpos(numSamples) + label_offset, this.ypos(delay));
+ ctx.stroke();
+ ctx.strokeStyle = "rgb(0,0,0)";
+ ctx.fillStyle = "rgb(0,0,0)";
+ }
+
+ draw() {
+ const ctx = this.ctx;
+
+ this.clear();
+ this.drawFrame();
+
+ for (var delay of [10, 20, 30, 50, 100, 200, 400, 800]) {
+ this.drawScale(delay);
+ }
+ this.draw60fps();
+ this.draw30fps();
+
+ var worst = 0,
+ worstpos = 0;
+ ctx.beginPath();
+ for (let i = 0; i < numSamples; i++) {
+ ctx.lineTo(this.xpos(i), this.ypos(gHistory.delays[i]));
+ if (gHistory.delays[i] >= worst) {
+ worst = gHistory.delays[i];
+ worstpos = i;
+ }
+ }
+ ctx.stroke();
+
+ // Draw vertical lines marking minor and major GCs
+ if (gHost.features.haveGCCounts) {
+ ctx.strokeStyle = stroke.gcslice;
+ let idx = sampleIndex % numSamples;
+ const count = {
+ major: gHistory.majorGCs[idx],
+ minor: 0,
+ slice: gHistory.slices[idx],
+ };
+ for (let i = 0; i < numSamples; i++) {
+ idx = (sampleIndex + i) % numSamples;
+ const isMajorStart = count.major < gHistory.majorGCs[idx];
+ if (count.slice < gHistory.slices[idx]) {
+ if (isMajorStart) {
+ ctx.strokeStyle = stroke.initialMajor;
+ }
+ ctx.beginPath();
+ ctx.moveTo(this.xpos(idx), 0);
+ ctx.lineTo(this.xpos(idx), this.layout.xAxisLabel_Y);
+ ctx.stroke();
+ if (isMajorStart) {
+ ctx.strokeStyle = stroke.gcslice;
+ }
+ }
+ count.major = gHistory.majorGCs[idx];
+ count.slice = gHistory.slices[idx];
+ }
+
+ ctx.strokeStyle = stroke.minor;
+ idx = sampleIndex % numSamples;
+ count.minor = gHistory.minorGCs[idx];
+ for (let i = 0; i < numSamples; i++) {
+ idx = (sampleIndex + i) % numSamples;
+ if (count.minor < gHistory.minorGCs[idx]) {
+ ctx.beginPath();
+ ctx.moveTo(this.xpos(idx), 0);
+ ctx.lineTo(this.xpos(idx), 20);
+ ctx.stroke();
+ }
+ count.minor = gHistory.minorGCs[idx];
+ }
+ }
+
+ ctx.fillStyle = "rgb(255,0,0)";
+ if (worst) {
+ ctx.fillText(
+ `${worst.toFixed(2)}ms`,
+ this.xpos(worstpos) - 10,
+ this.ypos(worst) - 14
+ );
+ }
+
+ // Mark and label the slowest frame
+ ctx.beginPath();
+ var where = sampleIndex % numSamples;
+ ctx.arc(
+ this.xpos(where),
+ this.ypos(gHistory.delays[where]),
+ 5,
+ 0,
+ Math.PI * 2,
+ true
+ );
+ ctx.fill();
+ ctx.fillStyle = "rgb(0,0,0)";
+
+ this.drawAxisLabels("Time", "Pause between frames (log scale)");
+ }
+};
+
+var MemoryGraph = class extends Graph {
+ constructor(ctx) {
+ super(ctx);
+ this.worstEver = this.bestEver = gHost.gcBytes;
+ this.limit = Math.max(this.worstEver, gHost.gcAllocTrigger);
+ }
+
+ ypos(size) {
+ const { height } = this.ctx.canvas;
+
+ const range = this.limit - this.bestEver;
+ const percent = (size - this.bestEver) / range;
+
+ return (1 - percent) * height * 0.9 + 20;
+ }
+
+ drawHBar(size, label, color = "rgb(150,150,150)") {
+ const ctx = this.ctx;
+
+ const y = this.ypos(size);
+
+ ctx.fillStyle = color;
+ ctx.strokeStyle = color;
+ ctx.fillText(label, this.xpos(numSamples) + 4, y + 3);
+
+ ctx.beginPath();
+ ctx.moveTo(this.xpos(0), y);
+ ctx.lineTo(this.xpos(numSamples), y);
+ ctx.stroke();
+ ctx.strokeStyle = "rgb(0,0,0)";
+ ctx.fillStyle = "rgb(0,0,0)";
+ }
+
+ draw() {
+ const ctx = this.ctx;
+
+ this.clear();
+ this.drawFrame();
+
+ var worst = 0,
+ worstpos = 0;
+ for (let i = 0; i < numSamples; i++) {
+ if (gHistory.gcBytes[i] >= worst) {
+ worst = gHistory.gcBytes[i];
+ worstpos = i;
+ }
+ if (gHistory.gcBytes[i] < this.bestEver) {
+ this.bestEver = gHistory.gcBytes[i];
+ }
+ }
+
+ if (this.worstEver < worst) {
+ this.worstEver = worst;
+ this.limit = Math.max(this.worstEver, gHost.gcAllocTrigger);
+ }
+
+ this.drawHBar(
+ this.bestEver,
+ `${format_bytes(this.bestEver)} min`,
+ "#00cf61"
+ );
+ this.drawHBar(
+ this.worstEver,
+ `${format_bytes(this.worstEver)} max`,
+ "#cc1111"
+ );
+ this.drawHBar(
+ gHost.gcAllocTrigger,
+ `${format_bytes(gHost.gcAllocTrigger)} trigger`,
+ "#cc11cc"
+ );
+
+ ctx.fillStyle = "rgb(255,0,0)";
+ if (worst) {
+ ctx.fillText(
+ format_bytes(worst),
+ this.xpos(worstpos) - 10,
+ this.ypos(worst) - 14
+ );
+ }
+
+ ctx.beginPath();
+ var where = sampleIndex % numSamples;
+ ctx.arc(
+ this.xpos(where),
+ this.ypos(gHistory.gcBytes[where]),
+ 5,
+ 0,
+ Math.PI * 2,
+ true
+ );
+ ctx.fill();
+
+ ctx.beginPath();
+ for (let i = 0; i < numSamples; i++) {
+ if (i == (sampleIndex + 1) % numSamples) {
+ ctx.moveTo(this.xpos(i), this.ypos(gHistory.gcBytes[i]));
+ } else {
+ ctx.lineTo(this.xpos(i), this.ypos(gHistory.gcBytes[i]));
+ }
+ if (i == where) {
+ ctx.stroke();
+ }
+ }
+ ctx.stroke();
+
+ this.drawAxisLabels("Time", "Heap Memory Usage");
+ }
+};
+
+function onUpdateDisplayChanged() {
+ const do_graph = document.getElementById("do-graph");
+ if (do_graph.checked) {
+ window.requestAnimationFrame(handler);
+ gHistory.resume();
+ } else {
+ gHistory.pause();
+ }
+ update_load_state_indicator();
+}
+
+function onDoLoadChange() {
+ const do_load = document.getElementById("do-load");
+ gLoadMgr.paused = !do_load.checked;
+ console.log(`load paused: ${gLoadMgr.paused}`);
+ update_load_state_indicator();
+}
+
+var previous = 0;
+function handler(timestamp) {
+ if (gHistory.is_stopped()) {
+ return;
+ }
+
+ const completed = gLoadMgr.tick(timestamp);
+ if (completed) {
+ end_test(timestamp, gLoadMgr.lastActive);
+ if (!gLoadMgr.stopped()) {
+ start_test();
+ }
+ update_load_display();
+ }
+
+ if (testState == "running") {
+ document.getElementById("test-progress").textContent =
+ (gLoadMgr.currentLoadRemaining(timestamp) / 1000).toFixed(1) + " sec";
+ }
+
+ const delay = gHistory.on_frame(timestamp);
+
+ update_histogram(gHistogram, delay);
+
+ latencyGraph.draw();
+ if (memoryGraph) {
+ memoryGraph.draw();
+ }
+ window.requestAnimationFrame(handler);
+}
+
+// For interactive debugging.
+//
+// ['a', 'b', 'b', 'b', 'c', 'c'] => ['a', 'b x 3', 'c x 2']
+function summarize(arr) {
+ if (!arr.length) {
+ return [];
+ }
+
+ var result = [];
+ var run_start = 0;
+ var prev = arr[0];
+ for (let i = 1; i <= arr.length; i++) {
+ if (i == arr.length || arr[i] != prev) {
+ if (i == run_start + 1) {
+ result.push(arr[i]);
+ } else {
+ result.push(prev + " x " + (i - run_start));
+ }
+ run_start = i;
+ }
+ if (i != arr.length) {
+ prev = arr[i];
+ }
+ }
+
+ return result;
+}
+
+function reset_draw_state() {
+ gHistory.reset();
+}
+
+function onunload() {
+ gLoadMgr.deactivateLoad();
+}
+
+function onload() {
+ // The order of `tests` is currently based on their asynchronous load
+ // order, rather than the listed order. Rearrange by extracting the test
+ // names from their filenames, which is kind of gross.
+ _tests = tests;
+ tests = new Map();
+ foreach_test_file(fn => {
+ // "benchmarks/foo.js" => "foo"
+ const name = fn.split(/\//)[1].split(/\./)[0];
+ tests.set(name, _tests.get(name));
+ });
+ _tests = undefined;
+
+ gLoadMgr = new AllocationLoadManager(tests);
+
+ // Load initial test duration.
+ duration_changed();
+
+ // Load initial garbage size.
+ garbage_piles_changed();
+ garbage_per_frame_changed();
+
+ // Populate the test selection dropdown.
+ var select = document.getElementById("test-selection");
+ for (var [name, test] of tests) {
+ test.name = name;
+ var option = document.createElement("option");
+ option.id = name;
+ option.text = name;
+ option.title = test.description;
+ select.add(option);
+ }
+
+ // Load the initial test.
+ gLoadMgr.setActiveLoad(gLoadMgr.getByName("noAllocation"));
+ update_load_display();
+ document.getElementById("test-selection").value = "noAllocation";
+
+ // Polyfill rAF.
+ var requestAnimationFrame =
+ window.requestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ window.msRequestAnimationFrame;
+ window.requestAnimationFrame = requestAnimationFrame;
+
+ // Acquire our canvas.
+ var canvas = document.getElementById("graph");
+ latencyGraph = new LatencyGraph(canvas.getContext("2d"));
+
+ if (!gHost.features.haveMemorySizes) {
+ document.getElementById("memgraph-disabled").style.display = "block";
+ document.getElementById("track-sizes-div").style.display = "none";
+ }
+
+ trackHeapSizes(document.getElementById("track-sizes").checked);
+
+ update_load_state_indicator();
+ gHistory.start();
+
+ // Start drawing.
+ reset_draw_state();
+ window.requestAnimationFrame(handler);
+}
+
+function run_one_test() {
+ start_test_cycle([gLoadMgr.activeLoad().name]);
+}
+
+function run_all_tests() {
+ start_test_cycle([...tests.keys()]);
+}
+
+function start_test_cycle(tests_to_run) {
+ // Convert from an iterable to an array for pop.
+ const duration = gLoadMgr.testDurationMS / 1000;
+ const mutators = tests_to_run.map(name => new SingleMutatorSequencer(gLoadMgr.getByName(name), gPerf, duration));
+ const sequencer = new ChainSequencer(mutators);
+ gLoadMgr.startSequencer(sequencer);
+ testState = "running";
+ gHistogram.clear();
+ reset_draw_state();
+}
+
+function update_load_state_indicator() {
+ if (
+ !gLoadMgr.load_running() ||
+ gLoadMgr.activeLoad().name == "noAllocation"
+ ) {
+ loadState = "(none)";
+ } else if (gHistory.is_stopped() || gLoadMgr.paused) {
+ loadState = "(inactive)";
+ } else {
+ loadState = "(active)";
+ }
+ document.getElementById("load-running").textContent = loadState;
+}
+
+function start_test() {
+ console.log(`Running test: ${gLoadMgr.activeLoad().name}`);
+ document.getElementById("test-selection").value = gLoadMgr.activeLoad().name;
+ update_load_state_indicator();
+}
+
+function end_test(timestamp, load) {
+ document.getElementById("test-progress").textContent = "(not running)";
+ report_test_result(load, gHistogram);
+ gHistogram.clear();
+ console.log(`Ending test ${load.name}`);
+ if (gLoadMgr.stopped()) {
+ testState = "idle";
+ }
+ update_load_state_indicator();
+ reset_draw_state();
+}
+
+function compute_test_spark_histogram(histogram) {
+ const percents = compute_spark_histogram_percents(histogram);
+
+ var sparks = "▁▂▃▄▅▆▇█";
+ var colors = [
+ "#aaaa00",
+ "#007700",
+ "#dd0000",
+ "#ff0000",
+ "#ff0000",
+ "#ff0000",
+ "#ff0000",
+ "#ff0000",
+ ];
+ var line = "";
+ for (let i = 0; i < percents.length; ++i) {
+ var spark = sparks.charAt(parseInt(percents[i] * sparks.length));
+ line += `<span style="color:${colors[i]}">${spark}</span>`;
+ }
+ return line;
+}
+
+function report_test_result(load, histogram) {
+ var resultList = document.getElementById("results-display");
+ var resultElem = document.createElement("div");
+ var score = compute_test_score(histogram);
+ var sparks = compute_test_spark_histogram(histogram);
+ var params = `(${format_num(load.garbagePerFrame)},${format_num(
+ load.garbagePiles
+ )})`;
+ resultElem.innerHTML = `${score.toFixed(3)} ms/s : ${sparks} : ${
+ load.name
+ }${params} - ${load.description}`;
+ resultList.appendChild(resultElem);
+}
+
+function update_load_display() {
+ const garbage = gLoadMgr.activeLoad()
+ ? gLoadMgr.activeLoad().garbagePerFrame
+ : parse_units(gDefaultGarbagePerFrame);
+ document.getElementById("garbage-per-frame").value = format_num(garbage);
+ const piles = gLoadMgr.activeLoad()
+ ? gLoadMgr.activeLoad().garbagePiles
+ : parse_units(gDefaultGarbagePiles);
+ document.getElementById("garbage-piles").value = format_num(piles);
+ update_load_state_indicator();
+}
+
+function duration_changed() {
+ var durationInput = document.getElementById("test-duration");
+ gLoadMgr.testDurationMS = parseInt(durationInput.value) * 1000;
+ console.log(
+ `Updated test duration to: ${gLoadMgr.testDurationMS / 1000} seconds`
+ );
+}
+
+function onLoadChange() {
+ var select = document.getElementById("test-selection");
+ console.log(`Switching to test: ${select.value}`);
+ gLoadMgr.setActiveLoad(gLoadMgr.getByName(select.value));
+ update_load_display();
+ gHistogram.clear();
+ reset_draw_state();
+}
+
+function garbage_piles_changed() {
+ const input = document.getElementById("garbage-piles");
+ const value = parse_units(input.value);
+ if (isNaN(value)) {
+ update_load_display();
+ return;
+ }
+
+ if (gLoadMgr.load_running()) {
+ gLoadMgr.change_garbagePiles(value);
+ console.log(
+ `Updated garbage-piles to ${gLoadMgr.activeLoad().garbagePiles} items`
+ );
+ }
+ gHistogram.clear();
+ reset_draw_state();
+}
+
+function garbage_per_frame_changed() {
+ const input = document.getElementById("garbage-per-frame");
+ var value = parse_units(input.value);
+ if (isNaN(value)) {
+ update_load_display();
+ return;
+ }
+ if (gLoadMgr.load_running()) {
+ gLoadMgr.change_garbagePerFrame = value;
+ console.log(
+ `Updated garbage-per-frame to ${
+ gLoadMgr.activeLoad().garbagePerFrame
+ } items`
+ );
+ }
+}
+
+function trackHeapSizes(track) {
+ enabled.trackingSizes = track && gHost.features.haveMemorySizes;
+
+ var canvas = document.getElementById("memgraph");
+
+ if (enabled.trackingSizes) {
+ canvas.style.display = "block";
+ memoryGraph = new MemoryGraph(canvas.getContext("2d"));
+ } else {
+ canvas.style.display = "none";
+ memoryGraph = null;
+ }
+}
diff --git a/js/src/devtools/gc-ubench/v8.js b/js/src/devtools/gc-ubench/v8.js
new file mode 100644
index 0000000000..0c46c2b4c4
--- /dev/null
+++ b/js/src/devtools/gc-ubench/v8.js
@@ -0,0 +1,42 @@
+/* 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/. */
+
+// V8 JS shell benchmark script
+//
+// Usage: run d8 v8.js -- --help
+
+globalThis.loadRelativeToScript = load;
+
+load("shell-bench.js");
+
+var V8 = class extends Host {
+ constructor() {
+ super();
+ this.waitTA = new Int32Array(new SharedArrayBuffer(4));
+ }
+
+ start_turn() {}
+
+ end_turn() {}
+
+ suspend(duration) {
+ const response = Atomics.wait(this.waitTA, 0, 0, duration * 1000);
+ if (response !== 'timed-out') {
+ throw new Exception(`unexpected response from Atomics.wait: ${response}`);
+ }
+ }
+
+ features = {
+ haveMemorySizes: false,
+ haveGCCounts: false
+ };
+};
+
+var gHost = new V8();
+
+var { opts, rest: mutators } = argparse.parse_args(arguments);
+run(opts, mutators);
+
+print("\nTest results:\n");
+report_results();
diff --git a/js/src/devtools/gc/README.txt b/js/src/devtools/gc/README.txt
new file mode 100644
index 0000000000..f4f37efbaa
--- /dev/null
+++ b/js/src/devtools/gc/README.txt
@@ -0,0 +1,6 @@
+Usage:
+
+Requirements:
+1) The shell has to be compiled with --enable-gctimer
+
+Tested with python2.6
diff --git a/js/src/devtools/gc/gc-test.py b/js/src/devtools/gc/gc-test.py
new file mode 100644
index 0000000000..80a8d78b77
--- /dev/null
+++ b/js/src/devtools/gc/gc-test.py
@@ -0,0 +1,191 @@
+# 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/.
+
+# Works with python2.6
+
+import json
+import math
+import os
+import sys
+from operator import itemgetter
+from subprocess import PIPE, Popen
+
+
+class Test:
+ def __init__(self, path, name):
+ self.path = path
+ self.name = name
+
+ @classmethod
+ def from_file(cls, path, name, options):
+ return cls(path, name)
+
+
+def find_tests(dir, substring=None):
+ ans = []
+ for dirpath, dirnames, filenames in os.walk(dir):
+ if dirpath == ".":
+ continue
+ for filename in filenames:
+ if not filename.endswith(".js"):
+ continue
+ test = os.path.join(dirpath, filename)
+ if substring is None or substring in os.path.relpath(test, dir):
+ ans.append([test, filename])
+ return ans
+
+
+def get_test_cmd(path):
+ return [JS, "-f", path]
+
+
+def avg(seq):
+ return sum(seq) / len(seq)
+
+
+def stddev(seq, mean):
+ diffs = ((float(item) - mean) ** 2 for item in seq)
+ return math.sqrt(sum(diffs) / len(seq))
+
+
+def run_test(test):
+ env = os.environ.copy()
+ env["MOZ_GCTIMER"] = "stderr"
+ cmd = get_test_cmd(test.path)
+ total = []
+ mark = []
+ sweep = []
+ close_fds = sys.platform != "win32"
+ p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=close_fds, env=env)
+ out, err = p.communicate()
+ out, err = out.decode(), err.decode()
+
+ float_array = [float(_) for _ in err.split()]
+
+ if len(float_array) == 0:
+ print("Error: No data from application. Configured with --enable-gctimer?")
+ sys.exit(1)
+
+ for i, currItem in enumerate(float_array):
+ if i % 3 == 0:
+ total.append(currItem)
+ else:
+ if i % 3 == 1:
+ mark.append(currItem)
+ else:
+ sweep.append(currItem)
+
+ return max(total), avg(total), max(mark), avg(mark), max(sweep), avg(sweep)
+
+
+def run_tests(tests, test_dir):
+ bench_map = {}
+
+ try:
+ for i, test in enumerate(tests):
+ filename_str = '"%s"' % test.name
+ TMax, TAvg, MMax, MAvg, SMax, SAvg = run_test(test)
+ bench_map[test.name] = [TMax, TAvg, MMax, MAvg, SMax, SAvg]
+ fmt = '%20s: {"TMax": %4.1f, "TAvg": %4.1f, "MMax": %4.1f, "MAvg": %4.1f, "SMax": %4.1f, "SAvg": %4.1f}' # NOQA: E501
+ if i != len(tests) - 1:
+ fmt += ","
+ print(fmt % (filename_str, TMax, TAvg, MMax, MAvg, SMax, MAvg))
+ except KeyboardInterrupt:
+ print("fail")
+
+ return dict(
+ (
+ filename,
+ dict(TMax=TMax, TAvg=TAvg, MMax=MMax, MAvg=MAvg, SMax=SMax, SAvg=SAvg),
+ )
+ for filename, (TMax, TAvg, MMax, MAvg, SMax, SAvg) in bench_map.iteritems()
+ )
+
+
+def compare(current, baseline):
+ percent_speedups = []
+ for key, current_result in current.iteritems():
+ try:
+ baseline_result = baseline[key]
+ except KeyError:
+ print(key, "missing from baseline")
+ continue
+
+ val_getter = itemgetter("TMax", "TAvg", "MMax", "MAvg", "SMax", "SAvg")
+ BTMax, BTAvg, BMMax, BMAvg, BSMax, BSAvg = val_getter(baseline_result)
+ CTMax, CTAvg, CMMax, CMAvg, CSMax, CSAvg = val_getter(current_result)
+
+ if CTAvg <= BTAvg:
+ speedup = (CTAvg / BTAvg - 1) * 100
+ result = "faster: %6.2f < baseline %6.2f (%+6.2f%%)" % (
+ CTAvg,
+ BTAvg,
+ speedup,
+ )
+ percent_speedups.append(speedup)
+ else:
+ slowdown = (CTAvg / BTAvg - 1) * 100
+ result = "SLOWER: %6.2f > baseline %6.2f (%+6.2f%%) " % (
+ CTAvg,
+ BTAvg,
+ slowdown,
+ )
+ percent_speedups.append(slowdown)
+ print("%30s: %s" % (key, result))
+ if percent_speedups:
+ print("Average speedup: %.2f%%" % avg(percent_speedups))
+
+
+if __name__ == "__main__":
+ script_path = os.path.abspath(__file__)
+ script_dir = os.path.dirname(script_path)
+ test_dir = os.path.join(script_dir, "tests")
+
+ from optparse import OptionParser
+
+ op = OptionParser(usage="%prog [options] JS_SHELL [TESTS]")
+
+ op.add_option(
+ "-b",
+ "--baseline",
+ metavar="JSON_PATH",
+ dest="baseline_path",
+ help="json file with baseline values to " "compare against",
+ )
+
+ (OPTIONS, args) = op.parse_args()
+ if len(args) < 1:
+ op.error("missing JS_SHELL argument")
+ # We need to make sure we are using backslashes on Windows.
+ JS, test_args = os.path.normpath(args[0]), args[1:]
+
+ test_list = []
+ bench_map = {}
+
+ test_list = find_tests(test_dir)
+
+ if not test_list:
+ print >> sys.stderr, "No tests found matching command line arguments."
+ sys.exit(0)
+
+ test_list = [Test.from_file(tst, name, OPTIONS) for tst, name in test_list]
+
+ try:
+ print("{")
+ bench_map = run_tests(test_list, test_dir)
+ print("}")
+
+ except OSError:
+ if not os.path.exists(JS):
+ print >> sys.stderr, "JS shell argument: file does not exist: '%s'" % JS
+ sys.exit(1)
+ else:
+ raise
+
+ if OPTIONS.baseline_path:
+ baseline_map = []
+ fh = open(OPTIONS.baseline_path, "r")
+ baseline_map = json.load(fh)
+ fh.close()
+ compare(current=bench_map, baseline=baseline_map)
diff --git a/js/src/devtools/gc/tests/clock.js b/js/src/devtools/gc/tests/clock.js
new file mode 100644
index 0000000000..fd2fb985f1
--- /dev/null
+++ b/js/src/devtools/gc/tests/clock.js
@@ -0,0 +1,35 @@
+//Shell version of Clock Benchmark: https://bug548388.bugzilla.mozilla.org/attachment.cgi?id=434576
+
+var t0;
+var tl;
+
+function alloc(dt) {
+ if (dt > 100)
+ dt = 100;
+ for (var i = 0; i < dt * 1000; ++i) {
+ var o = new String("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+ }
+}
+
+function cycle() {
+ if (!running)
+ return;
+
+ var t1 = new Date;
+ if (t0 == undefined) t0 = t1;
+
+ if (tl != undefined) {
+ var dt = t1 - tl;
+ alloc(dt);
+ }
+
+ tl = t1;
+
+ if(t1 - t0 > (5 * 1000))
+ running = false;
+}
+
+var running = true;
+while(running)
+ cycle();
+
diff --git a/js/src/devtools/gc/tests/dslots.js b/js/src/devtools/gc/tests/dslots.js
new file mode 100644
index 0000000000..8fcb6e8aa3
--- /dev/null
+++ b/js/src/devtools/gc/tests/dslots.js
@@ -0,0 +1,26 @@
+//Benchmark to measure overhead of dslots allocation and deallocation
+
+function Object0() {};
+function Object1() { this.a=1; };
+function Object2() { this.a=1; this.b=1; };
+function Object3() { this.a=1; this.b=1; this.c=1; };
+function Object4() { this.a=1; this.b=1; this.c=1; this.d=1; };
+function Object5() { this.a=1; this.b=1; this.c=1; this.d=1; this.e=1; };
+
+function test() {
+ var N = 1e5;
+ gc();
+
+ for(var i = 0; i<=5; i++)
+ {
+ var tmp = i==0 ? Object0 : i==1 ? Object1 : i==2 ? Object2 : i==3 ? Object3 : i==4 ? Object4 : Object5;
+ for (var j = 0; j != N; j++) {
+ var a = new tmp();
+ }
+ gc();
+ }
+}
+
+for(var i = 0; i<=5; i++) {
+ test();
+}
diff --git a/js/src/devtools/gc/tests/loops.js b/js/src/devtools/gc/tests/loops.js
new file mode 100644
index 0000000000..a99961a3ef
--- /dev/null
+++ b/js/src/devtools/gc/tests/loops.js
@@ -0,0 +1,55 @@
+//Measure plain GC.
+
+var t = [];
+var N = 500000
+
+for(var i = 0; i < N; i++)
+ t[i] = {};
+
+gc()
+
+t = [];
+
+gc();
+
+for(var i = 0; i < N; i++)
+ t[i] = ({});
+
+gc();
+
+t = [];
+
+gc();
+
+
+for(var i = 0; i < N; i++)
+ t[i] = "asdf";
+
+gc();
+
+t = [];
+
+gc();
+
+
+for(var i = 0; i < N; i++)
+ t[i] = 1.12345;
+
+gc();
+
+t=[];
+
+gc();
+
+for(var i = 0; i < N; i++) {
+ t[i] = ({});
+ if (i != 0)
+ t[i].a = t[i-1];
+}
+
+gc();
+
+t = [];
+
+gc();
+
diff --git a/js/src/devtools/gc/tests/objGraph.js b/js/src/devtools/gc/tests/objGraph.js
new file mode 100644
index 0000000000..607633173b
--- /dev/null
+++ b/js/src/devtools/gc/tests/objGraph.js
@@ -0,0 +1,37 @@
+test();
+
+function test()
+{
+ function generate_big_object_graph()
+ {
+ var root = {};
+ f(root, 17);
+ return root;
+ function f(parent, depth) {
+ if (depth == 0)
+ return;
+ --depth;
+
+ f(parent.a = {}, depth);
+ f(parent.b = {}, depth);
+ }
+ }
+
+ function f(obj) {
+ with (obj)
+ return arguments;
+ }
+
+ for(var i = 0; i != 10; ++i)
+ {
+ gc();
+ var x = null;
+ x = f(generate_big_object_graph());
+
+ gc(); //all used
+
+ x = null;
+
+ gc(); //all free
+ }
+}
diff --git a/js/src/devtools/gnuplot/gcTimer.gnu b/js/src/devtools/gnuplot/gcTimer.gnu
new file mode 100644
index 0000000000..b8b3ac9d85
--- /dev/null
+++ b/js/src/devtools/gnuplot/gcTimer.gnu
@@ -0,0 +1,24 @@
+# gnuplot script to visualize GCMETER results.
+# usage: "gnuplot gcTimer.gnu >outputfile.png"
+
+set terminal png
+# set Title
+set title "Title goes here!"
+set datafile missing "-"
+set noxtics
+#set ytics nomirror
+set ylabel "msec"
+set key below
+set style data linespoints
+
+#set data file
+plot 'gcTimer.dat' using 2 title columnheader(2), \
+'' u 3 title columnheader(3) with points, \
+'' u 4 title columnheader(4), \
+'' u 5 title columnheader(5), \
+'' u 6 title columnheader(6) with points, \
+'' u 7 title columnheader(7) with points, \
+'' u 8 title columnheader(8) with points, \
+'' u 9 title columnheader(9) with points, \
+'' u 10 title columnheader(10) with points, \
+'' u 11 title columnheader(11) with points
diff --git a/js/src/devtools/iongraph/.gitignore b/js/src/devtools/iongraph/.gitignore
new file mode 100644
index 0000000000..7964ec85a6
--- /dev/null
+++ b/js/src/devtools/iongraph/.gitignore
@@ -0,0 +1,3 @@
+*.pdf
+*.png
+*.gv
diff --git a/js/src/devtools/iongraph/LICENSE b/js/src/devtools/iongraph/LICENSE
new file mode 100644
index 0000000000..c2625f439a
--- /dev/null
+++ b/js/src/devtools/iongraph/LICENSE
@@ -0,0 +1,7 @@
+Copyright (c) 2012 Sean Stangl <sstangl@mozilla.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/js/src/devtools/iongraph/README.md b/js/src/devtools/iongraph/README.md
new file mode 100644
index 0000000000..df5e2822c2
--- /dev/null
+++ b/js/src/devtools/iongraph/README.md
@@ -0,0 +1,32 @@
+# iongraph
+
+Visualizer for IonMonkey graphs using GraphViz.
+
+## Usage
+
+1. Make a debug build of SpiderMonkey.
+2. Run with the env var `IONFLAGS=logs`. SpiderMonkey will dump observations about graph state into `/tmp/ion.json`.
+3. Run `iongraph`. A PDF will be generated in your working directory for each function in the logs.
+
+```
+IONFLAGS=logs js -m mymodule.mjs
+iongraph
+```
+
+For convenience, you can run the `iongraph js` command, which will invoke `js` and `iongraph` together. For example:
+
+```
+iongraph js -- -m mymodule.mjs
+```
+
+Arguments before the `--` separator are for `iongraph`, while args after `--` are for `js`.
+
+## Graph properties
+
+Blocks with green borders are loop headers. Blocks with red borders contain loop backedges (successor is a header). Blocks with dashed borders were created during critical edge splitting.
+
+Instructions that are movable are blue. Guard instructions are underlined.
+
+MResumePoints are placed as instructions, colored gray.
+
+Edges from blocks ending with conditional branches are annotated with the truth value associated with each edge, given as '0' or '1'.
diff --git a/js/src/devtools/iongraph/iongraph b/js/src/devtools/iongraph/iongraph
new file mode 100755
index 0000000000..e90e2460e7
--- /dev/null
+++ b/js/src/devtools/iongraph/iongraph
@@ -0,0 +1,486 @@
+#!/usr/bin/env python3
+# vim: set ts=4 sw=4 tw=99 et:
+
+# iongraph -- Translate IonMonkey JSON to GraphViz.
+# Originally by Sean Stangl. See LICENSE.
+
+import argparse
+import html
+import json
+import glob
+import os
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+
+def quote(s):
+ return '"%s"' % str(s)
+
+# Simple classes for the used subset of GraphViz' Dot format.
+# There are more complicated constructors out there, but they all
+# pull in annoying dependencies (and are annoying dependencies themselves).
+class GraphWidget:
+ def __init__(self):
+ self.name = ''
+ self.props = {}
+
+ def addprops(self, propdict):
+ for p in propdict:
+ self.props[p] = propdict[p]
+
+
+class Node(GraphWidget):
+ def __init__(self, name):
+ GraphWidget.__init__(self)
+ self.name = str(name)
+
+class Edge(GraphWidget):
+ def __init__(self, nfrom, nto):
+ GraphWidget.__init__(self)
+ self.nfrom = str(nfrom)
+ self.nto = str(nto)
+
+class Graph(GraphWidget):
+ def __init__(self, name, func, type):
+ GraphWidget.__init__(self)
+ self.name = name
+ self.func = func
+ self.type = str(type)
+ self.props = {}
+ self.nodes = []
+ self.edges = []
+
+ def addnode(self, n):
+ self.nodes.append(n)
+
+ def addedge(self, e):
+ self.edges.append(e)
+
+ def writeprops(self, f, o):
+ if len(o.props) == 0:
+ return
+
+ print('[', end=' ', file=f)
+ for p in o.props:
+ print(str(p) + '=' + str(o.props[p]), end=' ', file=f)
+ print(']', end=' ', file=f)
+
+ def write(self, f):
+ print(self.type, '{', file=f)
+
+ # Use the pass name as the graph title (at the top).
+ print('labelloc = t;', file=f)
+ print('labelfontsize = 30;', file=f)
+ print('label = "%s - %s";' % (self.func['name'], self.name), file=f)
+
+ # Output graph properties.
+ for p in self.props:
+ print(' ' + str(p) + '=' + str(self.props[p]), file=f)
+ print('', file=f)
+
+ # Output node list.
+ for n in self.nodes:
+ print(' ' + n.name, end=' ', file=f)
+ self.writeprops(f, n)
+ print(';', file=f)
+ print('', file=f)
+
+ # Output edge list.
+ for e in self.edges:
+ print(' ' + e.nfrom, '->', e.nto, end=' ', file=f)
+ self.writeprops(f, e)
+ print(';', file=f)
+
+ print('}', file=f)
+
+
+# block obj -> node string with quotations
+def getBlockNodeName(b):
+ return blockNumToNodeName(b['number'])
+
+# int -> node string with quotations
+def blockNumToNodeName(i):
+ return quote('Block' + str(i))
+
+# resumePoint obj -> HTML-formatted string
+def getResumePointRow(rp, mode):
+ if mode != None and mode != rp['mode']:
+ return ''
+
+ # Left column: caller.
+ rpCaller = '<td align="left"></td>'
+ if 'caller' in rp:
+ rpCaller = '<td align="left">&#40;&#40;%s&#41;&#41;</td>' % str(rp['caller'])
+
+ # Middle column: ordered contents of the MResumePoint.
+ insts = ''.join('%s ' % t for t in rp['operands'])
+ rpContents = '<td align="left"><font color="grey50">resumepoint %s</font></td>' % insts
+
+ # Right column: unused.
+ rpRight = '<td></td>'
+
+ return '<tr>%s%s%s</tr>' % (rpCaller, rpContents, rpRight)
+
+# memInputs obj -> HTML-formatted string
+def getMemInputsRow(list):
+ if len(list) == 0:
+ return ''
+
+ # Left column: caller.
+ memLeft = '<td align="left"></td>'
+
+ # Middle column: ordered contents of the MResumePoint.
+ insts = ''.join('%s ' % str(t) for t in list)
+ memContents = '<td align="left"><font color="grey50">memory %s</font></td>' % insts
+
+ # Right column: unused.
+ memRight = '<td></td>'
+
+ return '<tr>%s%s%s</tr>' % (memLeft, memContents, memRight)
+
+# Outputs a single row for an instruction, excluding MResumePoints.
+# instruction -> HTML-formatted string
+def getInstructionRow(inst):
+ # Left column: instruction ID.
+ instId = str(inst['id'])
+ instLabel = '<td align="right" port="i%s">%s</td>' % (instId, instId)
+
+ # Middle column: instruction name.
+ instName = html.escape(inst['opcode'])
+ if 'attributes' in inst:
+ if 'RecoveredOnBailout' in inst['attributes']:
+ instName = '<font color="gray50">%s</font>' % instName
+ elif 'Movable' in inst['attributes']:
+ instName = '<font color="blue">%s</font>' % instName
+ if 'Guard' in inst['attributes']:
+ instName = '<u>%s</u>' % instName
+ if 'InWorklist' in inst['attributes']:
+ instName = '<font color="red">%s</font>' % instName
+ instName = '<td align="left">%s</td>' % instName
+
+ # Right column: instruction MIRType.
+ instType = ''
+ if 'type' in inst and inst['type'] != "None":
+ instType = '<td align="left">%s</td>' % html.escape(inst['type'])
+
+ return '<tr>%s%s%s</tr>' % (instLabel, instName, instType)
+
+# block obj -> HTML-formatted string
+def getBlockLabel(b):
+ s = '<<table border="0" cellborder="0" cellpadding="1">'
+
+ if 'blockUseCount' in b:
+ blockUseCount = "(Count: %s)" % str(b['blockUseCount'])
+ else:
+ blockUseCount = ""
+
+ blockTitle = '<font color="white">Block %s %s</font>' % (str(b['number']), blockUseCount)
+ blockTitle = '<td align="center" bgcolor="black" colspan="3">%s</td>' % blockTitle
+ s += '<tr>%s</tr>' % blockTitle
+
+ if 'resumePoint' in b:
+ s += getResumePointRow(b['resumePoint'], None)
+
+ for inst in b['instructions']:
+ if 'resumePoint' in inst:
+ s += getResumePointRow(inst['resumePoint'], 'At')
+
+ s += getInstructionRow(inst)
+
+ if 'memInputs' in inst:
+ s += getMemInputsRow(inst['memInputs'])
+
+ if 'resumePoint' in inst:
+ s += getResumePointRow(inst['resumePoint'], 'After')
+
+ s += '</table>>'
+ return s
+
+# str -> ir obj -> ir obj -> Graph
+# 'ir' is the IR to be used.
+# 'mir' is always the MIR.
+# This is because the LIR graph does not contain successor information.
+def buildGraphForIR(name, func, ir, mir):
+ if len(ir['blocks']) == 0:
+ return None
+
+ g = Graph(name, func, 'digraph')
+ g.addprops({'rankdir':'TB', 'splines':'true'})
+
+ for i in range(0, len(ir['blocks'])):
+ bactive = ir['blocks'][i] # Used for block contents.
+ b = mir['blocks'][i] # Used for drawing blocks and edges.
+
+ node = Node(getBlockNodeName(bactive))
+ node.addprops({'shape':'box', 'label':getBlockLabel(bactive)})
+
+ if 'backedge' in b['attributes']:
+ node.addprops({'color':'red'})
+ if 'loopheader' in b['attributes']:
+ node.addprops({'color':'green'})
+ if 'splitedge' in b['attributes']:
+ node.addprops({'style':'dashed'})
+
+ g.addnode(node)
+
+ for succ in b['successors']: # which are integers
+ edge = Edge(getBlockNodeName(bactive), blockNumToNodeName(succ))
+
+ if len(b['successors']) == 2:
+ if succ == b['successors'][0]:
+ edge.addprops({'label':'1'})
+ else:
+ edge.addprops({'label':'0'})
+
+ g.addedge(edge)
+
+ return g
+
+# pass obj -> output file -> (Graph OR None, Graph OR None)
+# The return value is (MIR, LIR); either one may be absent.
+def buildGraphsForPass(p, func):
+ name = p['name']
+ mir = p['mir']
+ lir = p['lir']
+ return (buildGraphForIR(name, func, mir, mir), buildGraphForIR(name, func, lir, mir))
+
+# function obj -> (Graph OR None, Graph OR None) list
+# First entry in each tuple corresponds to MIR; second, to LIR.
+def buildGraphs(func):
+ graphstup = []
+ for p in func['passes']:
+ gtup = buildGraphsForPass(p, func)
+ graphstup.append(gtup)
+ return graphstup
+
+# function obj -> (Graph OR None, Graph OR None) list
+# Only builds the final pass.
+def buildOnlyFinalPass(func):
+ if len(func['passes']) == 0:
+ return [None, None]
+ p = func['passes'][-1]
+ return [buildGraphsForPass(p, func)]
+
+# Write out a graph, constructing a nice filename.
+# function id -> pass id -> IR string -> Graph -> void
+def outputPass(dir, fnum, pnum, irname, g):
+ funcid = str(fnum).zfill(2)
+ passid = str(pnum).zfill(2)
+
+ filename = os.path.join(dir, 'func%s-pass%s-%s-%s.gv' % (funcid, passid, g.name, str(irname)))
+ with open(filename, 'w') as fd:
+ g.write(fd)
+
+# Add in closing } and ] braces to close a JSON file in case of error.
+def parenthesize(s):
+ stack = []
+ inString = False
+
+ for c in s:
+ if c == '"': # Doesn't handle escaped strings.
+ inString = not inString
+
+ if not inString:
+ if c == '{' or c == '[':
+ stack.append(c)
+ elif c == '}' or c == ']':
+ stack.pop()
+
+ while stack:
+ c = stack.pop()
+ if c == '{': s += '}'
+ elif c == '[': s += ']'
+
+ return s
+
+
+def genfiles(format, indir, outdir):
+ gvs = glob.glob(os.path.join(indir, '*.gv'))
+ gvs.sort()
+ for gv in gvs:
+ with open(os.path.join(outdir, '%s.%s' % (os.path.basename(gv), format)), 'w') as outfile:
+ sys.stderr.write(' writing %s\n' % (outfile.name))
+ subprocess.run(['dot', gv, '-T%s' % (format)], stdout=outfile, check=True)
+
+
+def gengvs(indir, outdir):
+ gvs = glob.glob(os.path.join(indir, '*.gv'))
+ gvs.sort()
+ for gv in gvs:
+ sys.stderr.write(' writing %s\n' % (os.path.basename(gv)))
+ shutil.copy(gv, outdir)
+
+
+def genmergedpdfs(indir, outdir):
+ gvs = glob.glob(os.path.join(indir, '*.gv'))
+ gvs.sort()
+ for gv in gvs:
+ with open(os.path.join(indir, '%s.pdf' % (os.path.basename(gv))), 'w') as outfile:
+ sys.stderr.write(' writing pdf %s\n' % (outfile.name))
+ subprocess.run(['dot', gv, '-Tpdf'], stdout=outfile, check=True)
+
+ sys.stderr.write('combining pdfs...\n')
+ which = (shutil.which('pdftk') and 'pdftk') or (shutil.which('qpdf') and 'qpdf')
+ prefixes = [os.path.basename(x) for x in glob.glob(os.path.join(indir, 'func*.pdf'))]
+ prefixes = [re.match(r'func[^-]*', x).group(0) for x in prefixes]
+ prefixes = list(set(prefixes))
+ prefixes.sort()
+ for prefix in prefixes:
+ pages = glob.glob(os.path.join(indir, '%s-*.pdf' % (prefix)))
+ pages.sort()
+ outfile = os.path.join(outdir, '%s.pdf' % (prefix))
+ sys.stderr.write(' writing pdf %s\n' % (outfile))
+ if which == 'pdftk':
+ subprocess.run(['pdftk', *pages, 'cat', 'output', outfile], check=True)
+ elif which == 'qpdf':
+ subprocess.run(['qpdf', '--empty', '--pages', *pages, '--', outfile], check=True)
+ else:
+ raise Exception("unknown pdf program")
+
+
+def parsenums(numstr):
+ if numstr is None:
+ return None
+ return [int(x) for x in numstr.split(',')]
+
+
+def validate(args):
+ if not shutil.which('dot'):
+ sys.stderr.write("ERROR: graphviz (dot) is not installed\n")
+ exit(1)
+ if args.format == 'pdf':
+ if not (shutil.which('pdftk') or shutil.which('qpdf')):
+ sys.stderr.write("ERROR: either pdftk or qpdf must be installed in order to combine the generated PDFs.\n")
+ sys.stderr.write("If you don't care and just want individual PDFs, use `--format pdfs`.\n")
+ exit(1)
+ if not os.path.isdir(args.outdir):
+ sys.stderr.write("ERROR: could not find a directory at %s\n" % (args.outdir))
+ exit(1)
+
+
+def gen(args):
+ validate(args)
+
+ with open(args.input, 'r') as fd:
+ s = fd.read()
+
+ sys.stderr.write("loading %s...\n" % (args.input))
+ ion = json.loads(parenthesize(s))
+
+ sys.stderr.write("generating graphviz...\n")
+
+ funcnums = parsenums(args.funcnum)
+ passnums = parsenums(args.passnum)
+ with tempfile.TemporaryDirectory() as tmpdir:
+ for i in range(0, len(ion['functions'])):
+ func = ion['functions'][i]
+
+ if funcnums and i not in funcnums:
+ continue
+
+ gtl = buildOnlyFinalPass(func) if args.final else buildGraphs(func)
+
+ if len(gtl) == 0:
+ sys.stderr.write(" function %d (%s): abort during SSA construction.\n" % (i, func['name']))
+ else:
+ sys.stderr.write(" function %d (%s): success; %d passes.\n" % (i, func['name'], len(gtl)))
+
+ for j in range(0, len(gtl)):
+ gt = gtl[j]
+ if gt == None:
+ continue
+
+ mir = gt[0]
+ lir = gt[1]
+
+ if passnums and j in passnums:
+ if lir != None and args.out_lir:
+ lir.write(args.out_lir)
+ if mir != None and args.out_mir:
+ mir.write(args.out_mir)
+ if args.out_lir and args.out_mir:
+ break
+ elif passnums:
+ continue
+
+ # If only the final pass is requested, output both MIR and LIR.
+ if args.final:
+ if lir != None:
+ outputPass(tmpdir, i, j, 'lir', lir)
+ if mir != None:
+ outputPass(tmpdir, i, j, 'mir', mir)
+ continue
+
+ # Normally, only output one of (MIR, LIR), preferring LIR.
+ if lir != None:
+ outputPass(tmpdir, i, j, 'lir', lir)
+ elif mir != None:
+ outputPass(tmpdir, i, j, 'mir', mir)
+
+ if args.format == 'pdf':
+ sys.stderr.write("generating pdfs...\n")
+ genmergedpdfs(tmpdir, args.outdir)
+ elif args.format == 'gv':
+ sys.stderr.write("copying gvs...\n")
+ gengvs(tmpdir, args.outdir)
+ else:
+ format = 'pdf' if args.format == 'pdfs' else args.format
+ sys.stderr.write("generating %ss...\n" % (format))
+ genfiles(format, tmpdir, args.outdir)
+
+
+def js_and_gen(args):
+ validate(args)
+
+ jsargs = args.remaining[1:]
+ jsenv = os.environ.copy()
+ flags = (jsenv['IONFLAGS'] if 'IONFLAGS' in jsenv else 'logs').split(',')
+ if 'logs' not in flags:
+ flags.append('logs')
+ jsenv['IONFLAGS'] = ','.join(flags)
+ subprocess.run([args.jspath or 'js', *jsargs], env=jsenv, check=True)
+
+ args.input = '/tmp/ion.json'
+ gen(args)
+
+
+def add_main_arguments(parser):
+ parser.add_argument('-o', '--outdir', help='The directory in which to store the output file(s).',
+ default='.')
+ parser.add_argument('-f', '--funcnum', help='Only operate on the specified function(s), by index. Multiple functions can be separated by commas, e.g. `1,5,234`.')
+ parser.add_argument('-p', '--passnum', help='Only operate on the specified pass(es), by index. Multiple passes can be separated by commas, e.g. `1,5,234`.')
+ parser.add_argument('--format', help='The output file format (pdf by default). `pdf` will merge all the graphs for each function into a single PDF; all other formats will produce a single file per graph.',
+ choices=['gv', 'pdf', 'pdfs', 'png', 'svg'], default='pdf')
+ parser.add_argument('--final', help='Only generate the final optimized MIR/LIR graphs.',
+ action='store_true')
+ parser.add_argument('--out-mir', help='Select the file where the MIR output would be written to.',
+ type=argparse.FileType('w'))
+ parser.add_argument('--out-lir', help='Select the file where the LIR output would be written to.',
+ type=argparse.FileType('w'))
+
+
+def main():
+ parser = argparse.ArgumentParser(description='Visualize Ion graphs using GraphViz.')
+ subparsers = parser.add_subparsers()
+ js = subparsers.add_parser('js', formatter_class=argparse.RawDescriptionHelpFormatter,
+ help='Subcommand: Run js and iongraph together in one call.',
+ description='Run js and iongraph together in one call. Arguments before the -- separator are for iongraph, while arguments after the -- will be passed to js.\n\nexample:\n iongraph js --funcnum 1 -- -m mymodule.mjs')
+
+ add_main_arguments(parser)
+ parser.add_argument('input', help='The JSON log file generated by IonMonkey. (Default: /tmp/ion.json)',
+ nargs='?', default='/tmp/ion.json')
+ parser.set_defaults(func=gen)
+
+ js.add_argument('--jspath', help='The path to the js executable.')
+ add_main_arguments(js)
+ js.add_argument('remaining', nargs=argparse.REMAINDER)
+ js.set_defaults(func=js_and_gen)
+
+ args = parser.parse_args()
+ args.func(args)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/js/src/devtools/javascript-trace.d b/js/src/devtools/javascript-trace.d
new file mode 100644
index 0000000000..db759291c9
--- /dev/null
+++ b/js/src/devtools/javascript-trace.d
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+ * javascript provider probes
+ *
+ * function-entry (filename, classname, funcname)
+ * function-return (filename, classname, funcname)
+ * object-create (classname, *object)
+ * object-finalize (NULL, classname, *object)
+ * execute-start (filename, lineno)
+ * execute-done (filename, lineno)
+ */
+
+provider javascript {
+ probe function__entry(const char *, const char *, const char *);
+ probe function__return(const char *, const char *, const char *);
+ /* XXX must use unsigned longs here instead of uintptr_t for OS X
+ (Apple radar: 5194316 & 5565198) */
+ probe object__create(const char *, unsigned long);
+ probe object__finalize(const char *, const char *, unsigned long);
+ probe execute__start(const char *, int);
+ probe execute__done(const char *, int);
+};
+
+/*
+#pragma D attributes Unstable/Unstable/Common provider mozilla provider
+#pragma D attributes Private/Private/Unknown provider mozilla module
+#pragma D attributes Private/Private/Unknown provider mozilla function
+#pragma D attributes Unstable/Unstable/Common provider mozilla name
+*/
+
diff --git a/js/src/devtools/octane-csv.sh b/js/src/devtools/octane-csv.sh
new file mode 100755
index 0000000000..1049a2b47e
--- /dev/null
+++ b/js/src/devtools/octane-csv.sh
@@ -0,0 +1,76 @@
+#!/usr/bin/env bash
+
+set -e -o pipefail
+
+function echo_to_stderr {
+ echo "$1" 1>&2
+}
+
+function usage_and_exit {
+ echo_to_stderr "Usage:"
+ echo_to_stderr " $0 <path-to-js> <number-of-iterations>"
+ echo_to_stderr
+ echo_to_stderr "Run octane <number-of-iterations> times, and aggregate the results"
+ echo_to_stderr "into one CSV file, which is written to stdout."
+ echo_to_stderr
+ echo_to_stderr "See the js/src/devtools/plot-octane.R script for plotting the"
+ echo_to_stderr "results."
+ echo_to_stderr
+ echo_to_stderr "Complete example usage with plotting:"
+ echo_to_stderr
+ echo_to_stderr " \$ ./js/src/devtools/octane-csv.sh path/to/js 20 > control.csv"
+ echo_to_stderr
+ echo_to_stderr " Next, apply some patch you'd like to test."
+ echo_to_stderr
+ echo_to_stderr " \$ ./js/src/devtools/octane-csv.sh path/to/js 20 > variable.csv"
+ echo_to_stderr " \$ ./js/src/devtools/plot-octane.R control.csv variable.csv"
+ echo_to_stderr
+ echo_to_stderr " Open Rplots.pdf to view the results."
+ exit 1
+}
+
+if [[ "$#" != "2" ]]; then
+ usage_and_exit
+fi
+
+# Get the absolute, normalized $JS path, and ensure its an executable.
+
+JS_DIR=$(dirname $1)
+if [[ ! -d "$JS_DIR" ]]; then
+ echo_to_stderr "error: no such directory $JS_DIR"
+ echo_to_stderr
+ usage_and_exit
+fi
+
+JS=$(basename $1)
+cd "$JS_DIR" > /dev/null
+JS="$(pwd)/$JS"
+if [[ ! -e "$JS" ]]; then
+ echo_to_stderr "error: '$JS' is not executable"
+ echo_to_stderr
+ usage_and_exit
+fi
+cd - > /dev/null
+
+# Go to the js/src/octane directory.
+
+cd $(dirname $0)/../octane > /dev/null
+
+# Run octane and transform the results into CSV.
+#
+# Run once as a warm up, and to grab the column headers. Then run the benchmark
+# $ITERS times, grabbing just the data rows.
+
+echo_to_stderr "Warm up"
+"$JS" ./run.js | grep -v -- "----" | cut -f 1 -d ':' | tr '\n' ','
+echo
+
+ITERS=$2
+while [[ "$ITERS" -ge "1" ]]; do
+ echo_to_stderr "Iterations left: $ITERS"
+ "$JS" ./run.js | grep -v -- "----" | cut -f 2 -d ':' | tr '\n' ','
+ echo
+ ITERS=$((ITERS - 1))
+done
+
+echo_to_stderr "All done :)"
diff --git a/js/src/devtools/plot-octane.R b/js/src/devtools/plot-octane.R
new file mode 100755
index 0000000000..cd7ac7303a
--- /dev/null
+++ b/js/src/devtools/plot-octane.R
@@ -0,0 +1,38 @@
+#!/usr/bin/env Rscript
+
+# Usage:
+#
+# octane.R control.csv variable.csv
+#
+# Output will be placed in Rplots.pdf
+#
+# Remember: on Octane, higher is better!
+
+library(ggplot2)
+
+args <- commandArgs(trailingOnly = TRUE)
+
+# Reading in data.
+control <- read.table(args[1], sep=",", header=TRUE)
+variable <- read.table(args[2], sep=",", header=TRUE)
+
+# Pulling out columns that we want to plot.
+# Not totally necessary.
+ctrl <- control$Score..version.9.
+var <- variable$Score..version.9.
+
+# Concatenating the values we want to plot.
+score <- c(ctrl, var)
+# Creating a vector of labels for the data points.
+label <- c(rep("control", length(ctrl)), rep("variable", length(var)))
+
+# Creating a data frame of the score and label.
+data <- data.frame(label, score)
+
+# Now plotting!
+ggplot(data, aes(label, score, color=label, pch=label)) +
+ # Adding boxplot without the outliers.
+ geom_boxplot(outlier.shape=NA) +
+ # Adding jitter plot on top of the boxplot. If you want to spread the points
+ # more, increase jitter.
+ geom_jitter(position=position_jitter(width=0.05))
diff --git a/js/src/devtools/release/release-notes b/js/src/devtools/release/release-notes
new file mode 100644
index 0000000000..48cc53ac9e
--- /dev/null
+++ b/js/src/devtools/release/release-notes
@@ -0,0 +1,195 @@
+#!/usr/bin/perl
+
+# How to use:
+#
+# Step 1: run release-notes diff old-jsapi.h new-jsapi.h > diff.txt
+#
+# Step 2: edit diff.txt
+# - when a function has been renamed, get the - and + lines adjacent and mark the - line with [renamed] at the end
+# - when a function has been replaced, do the same (replacements behave differently)
+# - for anything that isn't a simple addition, deletion, rename, or replace, tag with [other]
+# (things tagged [other] will be put in a separate section for manual fixup)
+#
+# Step 3: run release-notes < diff.txt > changes.txt
+# - this will group changes into sections and annotate them with bug numbers
+# - the bugs chosen are just the bug that last touched each line, and are unlikely to be entirely accurate
+#
+# Step 4: run release-notes mdn < changes.txt > final.txt
+# - this will add an MDN link to every list item, first checking whether such a link is valid
+#
+# Step 5: paste into the MDN page, eg https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Releases/45
+
+# Upcoming: basing everything off of jsapi.h is probably not going to work for
+# much longer, given that more stuff is moving into js/public. Scan
+# js/public/*.h too and record where everything comes from (to automate header
+# changes in the notes)?
+#
+# This is only looking at C style APIs. Dump out all methods too?
+#
+# The enbuggification should be split out into a separate phase because it is
+# wrong a fair amount of the time (whitespace changes, parameter changes,
+# etc.), and should have a way of running repeatedly so you can incrementally
+# fix stuff up.
+#
+# It would be very nice to have an example program that links against mozjs,
+# tested in CI, so we can diff that for release notes.
+
+use strict;
+use warnings;
+
+if (@ARGV && $ARGV[0] eq 'diff') {
+ my ($orig_file, $new_file) = @ARGV[1..2];
+ my $orig_api = grab_api($orig_file);
+ my $new_api = grab_api($new_file);
+ diff_apis($orig_api, $new_api);
+ exit 0;
+}
+
+my $path = "/en-US/docs/Mozilla/Projects/SpiderMonkey/JSAPI_Reference";
+my $url_prefix = "https://developer.mozilla.org$path";
+
+if (@ARGV && $ARGV[0] eq 'mdn') {
+ shift(@ARGV);
+ while(<>) {
+ if (/<li>([\w:]+)/) {
+ print STDERR "Checking $1...\n";
+ system("wget", "-q", "$url_prefix/$1");
+ if ($? == 0) {
+ s!<li>([\w:]+)!<li><a href="$path/$1">$1</a>!;
+ }
+ }
+ print;
+ }
+ exit 0;
+}
+
+sub grab_api {
+ my ($file) = @_;
+ open(my $fh, "<", $file) or die "open $file: $!";
+ my $grabbing;
+ my @api;
+ while(<$fh>) {
+ if ($grabbing && /^(\w+)/) {
+ push @api, $1;
+ }
+ $grabbing = /JS_PUBLIC_API/;
+ }
+ return \@api;
+}
+
+sub diff_apis {
+ my ($old, $new) = @_;
+ my %old;
+ @old{@$old} = ();
+ my %new;
+ @new{@$new} = ();
+
+ open(my $ofh, ">", "/tmp/r-c.diff.1");
+ print $ofh "$_\n" foreach (@$old);
+ close $ofh;
+ open(my $nfh, ">", "/tmp/r-c.diff.2");
+ print $nfh "$_\n" foreach (@$new);
+ close $nfh;
+ open(my $diff, "diff -u /tmp/r-c.diff.1 /tmp/r-c.diff.2 |");
+ while(<$diff>) {
+ if (/^-(\w+)/) {
+ next if exists $new{$1}; # Still exists, so skip it
+ } elsif (/^\+(\w+)/) {
+ next if exists $old{$1}; # It was already there, skip it
+ }
+ print;
+ }
+}
+
+my @added;
+my @renamed;
+my @replaced;
+my @deleted;
+my @other;
+
+my %N;
+
+my $renaming;
+my $replacing;
+while (<>) {
+ my $name;
+ if (/^[ +-](\w+)/) {
+ $name = $1;
+ $N{$name} = $name =~ /^JS_/ ? $name : "JS::$name";
+ }
+
+ if (/^-/) {
+ die if ! $name;
+ if (/\[rename\]/) {
+ $renaming = $name;
+ } elsif (/\[replace\]/) {
+ $replacing = $name;
+ } elsif (/\[other\]/) {
+ push @other, $name;
+ } else {
+ push @deleted, $name;
+ }
+ } elsif (/^\+/) {
+ die if ! $name;
+ if ($renaming) {
+ push @renamed, [ $renaming, $name ];
+ undef $renaming;
+ } elsif ($replacing) {
+ push @replaced, [ $replacing, $name ];
+ undef $replacing;
+ } elsif (/\[other\]/) {
+ push @other, $name;
+ } else {
+ push @added, $name;
+ }
+ }
+}
+
+open(my $fh, "<", "jsapi.blame") or die "open jsapi.blame: $!";
+my $grabbing;
+my %changerev;
+my %revs;
+while(<$fh>) {
+ if ($grabbing && /^\s*(\d+): (\w+)/ ) {
+ $changerev{$2} = $1;
+ $revs{$1} = 1;
+ }
+ $grabbing = /JS_PUBLIC_API/;
+}
+
+my %bug;
+for my $rev (keys %revs) {
+ open(my $fh, "hg log -r $rev -T '{desc}' |");
+ while(<$fh>) {
+ if (/[bB]ug (\d+)/) {
+ $bug{$rev} = $1;
+ }
+ }
+}
+
+sub get_bug_suffix {
+ my ($api) = @_;
+ $DB::single = 1 if ! $changerev{$api};
+ my $bug = $bug{$changerev{$api}};
+ return $bug ? " {{{bug($bug)}}}" : "";
+}
+
+print "(new apis)\n";
+print "<ul>\n";
+print " <li>$N{$_}" . get_bug_suffix($_) . "</li>\n" foreach @added;
+print " <li>$N{$_->[0]} renamed to $N{$_->[1]}" . get_bug_suffix($_->[1]) . "</li>\n" foreach @renamed;
+print " <li>$N{$_->[0]} replaced with $N{$_->[1]}" . get_bug_suffix($_->[1]) . "</li>\n" foreach @replaced;
+print "</ul>\n";
+print "\n";
+
+print qq(<h2 id="Deleted_APIs">Deleted APIs</h2>\n);
+print "<ul>\n";
+print " <li>$N{$_}</li>\n" foreach @deleted;
+print "</ul>\n";
+print "\n";
+
+print qq(<h2 id="Changed_APIs">Changed APIs</h2>\n);
+print "<ul>\n";
+print " <li>$N{$_}" . get_bug_suffix($_) . "</li>\n" foreach @other;
+print "</ul>\n";
+print "\n";
diff --git a/js/src/devtools/rootAnalysis/CFG.js b/js/src/devtools/rootAnalysis/CFG.js
new file mode 100644
index 0000000000..1b6f714279
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/CFG.js
@@ -0,0 +1,1178 @@
+/* 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/. */
+
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+
+// Utility code for traversing the JSON data structures produced by sixgill.
+
+"use strict";
+
+var TRACING = false;
+
+// When edge.Kind == "Pointer", these are the meanings of the edge.Reference field.
+var PTR_POINTER = 0;
+var PTR_REFERENCE = 1;
+var PTR_RVALUE_REF = 2;
+
+// Find all points (positions within the code) of the body given by the list of
+// bodies and the blockId to match (which will specify an outer function or a
+// loop within it), recursing into loops if needed.
+function findAllPoints(bodies, blockId, bits)
+{
+ var points = [];
+ var body;
+
+ for (var xbody of bodies) {
+ if (sameBlockId(xbody.BlockId, blockId)) {
+ assert(!body);
+ body = xbody;
+ }
+ }
+ assert(body);
+
+ if (!("PEdge" in body))
+ return;
+ for (var edge of body.PEdge) {
+ points.push([body, edge.Index[0], bits]);
+ if (edge.Kind == "Loop")
+ points.push(...findAllPoints(bodies, edge.BlockId, bits));
+ }
+
+ return points;
+}
+
+// Visitor of a graph of <body, ppoint> vertexes and sixgill-generated edges,
+// where the edges represent the actual computation happening.
+//
+// Uses the syntax `var Visitor = class { ... }` rather than `class Visitor`
+// to allow reloading this file with the JS debugger.
+var Visitor = class {
+ constructor(bodies) {
+ this.visited_bodies = new Map();
+ for (const body of bodies) {
+ this.visited_bodies.set(body, new Map());
+ }
+ }
+
+ // Prepend `edge` to the info stored at the successor node, returning
+ // the updated info value. This should be overridden by pretty much any
+ // subclass, as a traversal's semantics are largely determined by this method.
+ extend_path(edge, body, ppoint, successor_value) { return true; }
+
+ // Default implementation does a basic "only visit nodes once" search.
+ // (Whether this is BFS/DFS/other is determined by the caller.)
+
+ // Override if you need to revisit nodes. Valid actions are "continue",
+ // "prune", and "done". "continue" means continue with the search. "prune"
+ // means do not continue to predecessors of this node, only continue with
+ // the remaining entries in the work queue. "done" means the
+ // whole search is complete even if unvisited nodes remain.
+ next_action(prev, current) { return prev ? "prune" : "continue"; }
+
+ // Update the info at a node. If this is the first time the node has been
+ // seen, `prev` will be undefined. `current` will be the info computed by
+ // `extend_path`. The node will be updated with the return value.
+ merge_info(prev, current) { return true; }
+
+ // Default visit() implementation. Subclasses will usually leave this alone
+ // and use the other methods as extension points.
+ //
+ // Take a body, a point within that body, and the info computed by
+ // extend_path() for that point when traversing an edge. Return whether the
+ // search should continue ("continue"), the search should be pruned and
+ // other paths followed ("prune"), or that the whole search is complete and
+ // it is time to return a value ("done", and the value returned by
+ // merge_info() will be returned by the overall search).
+ //
+ // Persistently record the value computed so far at each point, and call
+ // (overridable) next_action() and merge_info() methods with the previous
+ // and freshly-computed value for each point.
+ //
+ // Often, extend_path() will decide how/whether to continue the search and
+ // will return the search action to take, and next_action() will blindly
+ // return it if the point has not yet been visited. (And if it has, it will
+ // prune this branch of the search so that no point is visited multiple
+ // times.)
+ visit(body, ppoint, info) {
+ const visited_value_table = this.visited_bodies.get(body);
+ const existing_value_if_visited = visited_value_table.get(ppoint);
+ const action = this.next_action(existing_value_if_visited, info);
+ const merged = this.merge_info(existing_value_if_visited, info);
+ visited_value_table.set(ppoint, merged);
+ return [action, merged];
+ }
+};
+
+function findMatchingBlock(bodies, blockId) {
+ for (const body of bodies) {
+ if (sameBlockId(body.BlockId, blockId)) {
+ return body;
+ }
+ }
+ assert(false);
+}
+
+// For a given function containing a set of bodies, each containing a set of
+// ppoints, perform a mostly breadth-first traversal through the complete graph
+// of all <body, ppoint> nodes throughout all the bodies of the function.
+//
+// When traversing, every <body, ppoint> node is associated with a value that
+// is assigned or updated whenever it is visited. The overall traversal
+// terminates when a given condition is reached, and an arbitrary custom value
+// is returned. If the search completes without the termination condition
+// being reached, it will return the value associated with the entrypoint
+// node, which is initialized to `entrypoint_fallback_value` (and thus serves as
+// the fallback return value if all search paths are pruned before reaching
+// the entrypoint.)
+//
+// The traversal is only *mostly* breadth-first because the visitor decides
+// whether to stop searching when it sees a node. If a node is visited for a
+// second time, the visitor can choose to continue (and thus revisit the node)
+// in order to find "better" paths that may include a node more than once.
+// The search is done in the "upwards" direction -- as in, it starts at the
+// exit point and searches through predecessors.
+//
+// Override visitor.visit() to return an action and a value. The action
+// determines whether the overall search should terminate ('done'), or
+// continues looking through the predecessors of the current node ('continue'),
+// or whether it should just continue processing the work queue without
+// looking at predecessors ('prune').
+//
+// This allows this function to be used in different ways. If the visitor
+// associates a value with each node that chains onto its forward-flow successors
+// (predecessors in the "upwards" search order), then a complete path through
+// the graph will be returned.
+//
+// Alternatively, BFS_upwards() can be used to test whether a condition holds
+// (eg "the exit point is reachable only after calling SomethingImportant()"),
+// in which case no path is needed and the visitor can compute a simple boolean
+// every time it encounters a point. Note that `entrypoint_fallback_value` will
+// still be returned if the search terminates without ever reaching the
+// entrypoint, which is useful for dominator analyses.
+//
+// See the Visitor base class's implementation of visit(), above, for the
+// most commonly used visit logic.
+function BFS_upwards(start_body, start_ppoint, bodies, visitor,
+ initial_successor_value = {},
+ entrypoint_fallback_value=null)
+{
+ let entrypoint_value = entrypoint_fallback_value;
+
+ const work = [[start_body, start_ppoint, null, initial_successor_value]];
+ if (TRACING) {
+ printErr(`BFS start at ${blockIdentifier(start_body)}:${start_ppoint}`);
+ }
+
+ while (work.length > 0) {
+ const [body, ppoint, edgeToAdd, successor_value] = work.shift();
+ if (TRACING) {
+ const s = edgeToAdd ? " : " + str(edgeToAdd) : "";
+ printErr(`prepending edge from ${ppoint} to state '${successor_value}'${s}`);
+ }
+ let value = visitor.extend_path(edgeToAdd, body, ppoint, successor_value);
+
+ const [action, merged_value] = visitor.visit(body, ppoint, value);
+ if (action === "done") {
+ return merged_value;
+ }
+ if (action === "prune") {
+ // Do not push anything else to the work queue, but continue processing
+ // other branches.
+ continue;
+ }
+ assert(action == "continue");
+ value = merged_value;
+
+ const predecessors = getPredecessors(body);
+ for (const edge of (predecessors[ppoint] || [])) {
+ if (edge.Kind == "Loop") {
+ // Propagate the search into the exit point of the loop body.
+ const loopBody = findMatchingBlock(bodies, edge.BlockId);
+ const loopEnd = loopBody.Index[1];
+ work.push([loopBody, loopEnd, null, value]);
+ // Don't continue to predecessors here without going through
+ // the loop. (The points in this body that enter the loop will
+ // be traversed when we reach the entry point of the loop.)
+ }
+ work.push([body, edge.Index[0], edge, value]);
+ }
+
+ // Check for hitting the entry point of a loop body.
+ if (ppoint == body.Index[0] && body.BlockId.Kind == "Loop") {
+ // Propagate to outer body parents that enter the loop body.
+ for (const parent of (body.BlockPPoint || [])) {
+ const parentBody = findMatchingBlock(bodies, parent.BlockId);
+ work.push([parentBody, parent.Index, null, value]);
+ }
+
+ // This point is also preceded by the *end* of this loop, for the
+ // previous iteration.
+ work.push([body, body.Index[1], null, value]);
+ }
+
+ // Check for reaching the entrypoint of the function.
+ if (body === start_body && ppoint == body.Index[0]) {
+ entrypoint_value = value;
+ }
+ }
+
+ // The search space was exhausted without finding a 'done' state. That
+ // might be because all search paths were pruned before reaching the entry
+ // point of the function, in which case entrypoint_value will still be its initial
+ // value. (If entrypoint_value has been set, then we may still not have visited the
+ // entire graph, if some paths were pruned but at least one made it to the entrypoint.)
+ return entrypoint_value;
+}
+
+// Given the CFG for the constructor call of some RAII, return whether the
+// given edge is the matching destructor call.
+function isMatchingDestructor(constructor, edge)
+{
+ if (edge.Kind != "Call")
+ return false;
+ var callee = edge.Exp[0];
+ if (callee.Kind != "Var")
+ return false;
+ var variable = callee.Variable;
+ assert(variable.Kind == "Func");
+ if (variable.Name[1].charAt(0) != '~')
+ return false;
+
+ // Note that in some situations, a regular function can begin with '~', so
+ // we don't necessarily have a destructor in hand. This is probably a
+ // sixgill artifact, but in js::wasm::ModuleGenerator::~ModuleGenerator, a
+ // templatized static inline EraseIf is invoked, and it gets named ~EraseIf
+ // for some reason.
+ if (!("PEdgeCallInstance" in edge))
+ return false;
+
+ var constructExp = constructor.PEdgeCallInstance.Exp;
+ assert(constructExp.Kind == "Var");
+
+ var destructExp = edge.PEdgeCallInstance.Exp;
+ if (destructExp.Kind != "Var")
+ return false;
+
+ return sameVariable(constructExp.Variable, destructExp.Variable);
+}
+
+// Return all calls within the RAII scope of any constructor matched by
+// isConstructor(). (Note that this would be insufficient if you needed to
+// treat each instance separately, such as when different regions of a function
+// body were guarded by these constructors and you needed to do something
+// different with each.)
+function allRAIIGuardedCallPoints(typeInfo, bodies, body, isConstructor)
+{
+ if (!("PEdge" in body))
+ return [];
+
+ var points = [];
+
+ for (var edge of body.PEdge) {
+ if (edge.Kind != "Call")
+ continue;
+ var callee = edge.Exp[0];
+ if (callee.Kind != "Var")
+ continue;
+ var variable = callee.Variable;
+ assert(variable.Kind == "Func");
+ const bits = isConstructor(typeInfo, edge.Type, variable.Name);
+ if (!bits)
+ continue;
+ if (!("PEdgeCallInstance" in edge))
+ continue;
+ if (edge.PEdgeCallInstance.Exp.Kind != "Var")
+ continue;
+
+ points.push(...pointsInRAIIScope(bodies, body, edge, bits));
+ }
+
+ return points;
+}
+
+// Test whether the given edge is the constructor corresponding to the given
+// destructor edge.
+function isMatchingConstructor(destructor, edge)
+{
+ if (edge.Kind != "Call")
+ return false;
+ var callee = edge.Exp[0];
+ if (callee.Kind != "Var")
+ return false;
+ var variable = callee.Variable;
+ if (variable.Kind != "Func")
+ return false;
+ var name = readable(variable.Name[0]);
+ var destructorName = readable(destructor.Exp[0].Variable.Name[0]);
+ var match = destructorName.match(/^(.*?::)~(\w+)\(/);
+ if (!match) {
+ printErr("Unhandled destructor syntax: " + destructorName);
+ return false;
+ }
+ var constructorSubstring = match[1] + match[2];
+ if (name.indexOf(constructorSubstring) == -1)
+ return false;
+
+ var destructExp = destructor.PEdgeCallInstance.Exp;
+ if (destructExp.Kind != "Var")
+ return false;
+
+ var constructExp = edge.PEdgeCallInstance.Exp;
+ if (constructExp.Kind != "Var")
+ return false;
+
+ return sameVariable(constructExp.Variable, destructExp.Variable);
+}
+
+function findMatchingConstructor(destructorEdge, body, warnIfNotFound=true)
+{
+ var worklist = [destructorEdge];
+ var predecessors = getPredecessors(body);
+ while(worklist.length > 0) {
+ var edge = worklist.pop();
+ if (isMatchingConstructor(destructorEdge, edge))
+ return edge;
+ if (edge.Index[0] in predecessors) {
+ for (var e of predecessors[edge.Index[0]])
+ worklist.push(e);
+ }
+ }
+ if (warnIfNotFound)
+ printErr("Could not find matching constructor!");
+ return undefined;
+}
+
+function pointsInRAIIScope(bodies, body, constructorEdge, bits) {
+ var seen = {};
+ var worklist = [constructorEdge.Index[1]];
+ var points = [];
+ while (worklist.length) {
+ var point = worklist.pop();
+ if (point in seen)
+ continue;
+ seen[point] = true;
+ points.push([body, point, bits]);
+ var successors = getSuccessors(body);
+ if (!(point in successors))
+ continue;
+ for (var nedge of successors[point]) {
+ if (isMatchingDestructor(constructorEdge, nedge))
+ continue;
+ if (nedge.Kind == "Loop")
+ points.push(...findAllPoints(bodies, nedge.BlockId, bits));
+ worklist.push(nedge.Index[1]);
+ }
+ }
+
+ return points;
+}
+
+function isImmobileValue(exp) {
+ if (exp.Kind == "Int" && exp.String == "0") {
+ return true;
+ }
+ return false;
+}
+
+// Returns whether decl is a body.DefineVariable[] entry for a non-temporary reference.
+function isReferenceDecl(decl) {
+ return decl.Type.Kind == "Pointer" && decl.Type.Reference != PTR_POINTER && decl.Variable.Kind != "Temp";
+}
+
+function expressionIsVariableAddress(exp, variable)
+{
+ while (exp.Kind == "Fld")
+ exp = exp.Exp[0];
+ return exp.Kind == "Var" && sameVariable(exp.Variable, variable);
+}
+
+function edgeTakesVariableAddress(edge, variable, body)
+{
+ if (ignoreEdgeUse(edge, variable, body))
+ return false;
+ if (ignoreEdgeAddressTaken(edge))
+ return false;
+ switch (edge.Kind) {
+ case "Assign":
+ return expressionIsVariableAddress(edge.Exp[1], variable);
+ case "Call":
+ if ("PEdgeCallArguments" in edge) {
+ for (var exp of edge.PEdgeCallArguments.Exp) {
+ if (expressionIsVariableAddress(exp, variable))
+ return true;
+ }
+ }
+ return false;
+ default:
+ return false;
+ }
+}
+
+// Look at an invocation of a virtual method or function pointer contained in a
+// field, and return the static type of the invocant (or the containing struct,
+// for a function pointer field.)
+function getFieldCallInstanceCSU(edge, field)
+{
+ if ("FieldInstanceFunction" in field) {
+ // We have a 'this'.
+ const instanceExp = edge.PEdgeCallInstance.Exp;
+ if (instanceExp.Kind == 'Drf') {
+ // somevar->foo()
+ return edge.Type.TypeFunctionCSU.Type.Name;
+ } else if (instanceExp.Kind == 'Fld') {
+ // somevar.foo()
+ return instanceExp.Field.FieldCSU.Type.Name;
+ } else if (instanceExp.Kind == 'Index') {
+ // A strange construct.
+ // C++ code: static_cast<JS::CustomAutoRooter*>(this)->trace(trc);
+ // CFG: Call(21,30, this*[-1]{JS::CustomAutoRooter}.trace*(trc*))
+ return instanceExp.Type.Name;
+ } else if (instanceExp.Kind == 'Var') {
+ // C++: reinterpret_cast<SimpleTimeZone*>(gRawGMT)->~SimpleTimeZone();
+ // CFG:
+ // # icu_64::SimpleTimeZone::icu_64::SimpleTimeZone.__comp_dtor
+ // [6,7] Call gRawGMT.icu_64::SimpleTimeZone.__comp_dtor ()
+ return field.FieldCSU.Type.Name;
+ } else {
+ printErr("------------------ edge -------------------");
+ printErr(JSON.stringify(edge, null, 4));
+ printErr("------------------ field -------------------");
+ printErr(JSON.stringify(field, null, 4));
+ assert(false, `unrecognized FieldInstanceFunction Kind ${instanceExp.Kind}`);
+ }
+ } else {
+ // somefar.foo() where somevar is a field of some CSU.
+ return field.FieldCSU.Type.Name;
+ }
+}
+
+function expressionUsesVariable(exp, variable)
+{
+ if (exp.Kind == "Var" && sameVariable(exp.Variable, variable))
+ return true;
+ if (!("Exp" in exp))
+ return false;
+ for (var childExp of exp.Exp) {
+ if (expressionUsesVariable(childExp, variable))
+ return true;
+ }
+ return false;
+}
+
+function expressionUsesVariableContents(exp, variable)
+{
+ if (!("Exp" in exp))
+ return false;
+ for (var childExp of exp.Exp) {
+ if (childExp.Kind == 'Drf') {
+ if (expressionUsesVariable(childExp, variable))
+ return true;
+ } else if (expressionUsesVariableContents(childExp, variable)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// Detect simple |return nullptr;| statements.
+function isReturningImmobileValue(edge, variable)
+{
+ if (variable.Kind == "Return") {
+ if (edge.Exp[0].Kind == "Var" && sameVariable(edge.Exp[0].Variable, variable)) {
+ if (isImmobileValue(edge.Exp[1]))
+ return true;
+ }
+ }
+ return false;
+}
+
+// If the edge uses the given variable's value, return the earliest point at
+// which the use is definite. Usually, that means the source of the edge
+// (anything that reaches that source point will end up using the variable, but
+// there may be other ways to reach the destination of the edge.)
+//
+// Return values are implicitly used at the very last point in the function.
+// This makes a difference: if an RAII class GCs in its destructor, we need to
+// start looking at the final point in the function, not one point back from
+// that, since that would skip over the GCing call.
+//
+// Certain references may be annotated to be live to the end of the function
+// as well (eg AutoCheckCannotGC&& parameters).
+//
+// Note that this returns a nonzero value only if the variable's incoming value is used.
+// So this would return 0 for 'obj':
+//
+// obj = someFunction();
+//
+// but these would return a positive value:
+//
+// obj = someFunction(obj);
+// obj->foo = someFunction();
+//
+function edgeUsesVariable(edge, variable, body, liveToEnd=false)
+{
+ if (ignoreEdgeUse(edge, variable, body))
+ return 0;
+
+ if (variable.Kind == "Return") {
+ liveToEnd = true;
+ }
+
+ if (liveToEnd && body.Index[1] == edge.Index[1] && body.BlockId.Kind == "Function") {
+ // The last point in the function body is treated as using the return
+ // value. This is the only time the destination point is returned
+ // rather than the source point.
+ return edge.Index[1];
+ }
+
+ var src = edge.Index[0];
+
+ switch (edge.Kind) {
+
+ case "Assign": {
+ // Detect `Return := nullptr`.
+ if (isReturningImmobileValue(edge, variable))
+ return 0;
+ const [lhs, rhs] = edge.Exp;
+ // Detect `lhs := ...variable...`
+ if (expressionUsesVariable(rhs, variable))
+ return src;
+ // Detect `...variable... := rhs` but not `variable := rhs`. The latter
+ // overwrites the previous value of `variable` without using it.
+ if (expressionUsesVariable(lhs, variable) && !expressionIsVariable(lhs, variable))
+ return src;
+ return 0;
+ }
+
+ case "Assume":
+ return expressionUsesVariableContents(edge.Exp[0], variable) ? src : 0;
+
+ case "Call": {
+ const callee = edge.Exp[0];
+ if (expressionUsesVariable(callee, variable))
+ return src;
+ if ("PEdgeCallInstance" in edge) {
+ if (expressionUsesVariable(edge.PEdgeCallInstance.Exp, variable)) {
+ if (edgeStartsValueLiveRange(edge, variable)) {
+ // If the variable is being constructed, then the incoming
+ // value is not used here; it didn't exist before
+ // construction. (The analysis doesn't get told where
+ // variables are defined, so must infer it from
+ // construction. If the variable does not have a
+ // constructor, its live range may be larger than it really
+ // ought to be if it is defined within a loop body, but
+ // that is conservative.)
+ } else {
+ return src;
+ }
+ }
+ }
+ if ("PEdgeCallArguments" in edge) {
+ for (var exp of edge.PEdgeCallArguments.Exp) {
+ if (expressionUsesVariable(exp, variable))
+ return src;
+ }
+ }
+ if (edge.Exp.length == 1)
+ return 0;
+
+ // Assigning call result to a variable.
+ const lhs = edge.Exp[1];
+ if (expressionUsesVariable(lhs, variable) && !expressionIsVariable(lhs, variable))
+ return src;
+ return 0;
+ }
+
+ case "Loop":
+ return 0;
+
+ case "Assembly":
+ return 0;
+
+ default:
+ assert(false);
+ }
+}
+
+// If `decl` is the body.DefineVariable[] declaration of a reference type, then
+// return the expression without the outer dereference. Otherwise, return the
+// original expression.
+function maybeDereference(exp, decl) {
+ if (exp.Kind == "Drf" && exp.Exp[0].Kind == "Var") {
+ if (isReferenceDecl(decl)) {
+ return exp.Exp[0];
+ }
+ }
+ return exp;
+}
+
+function expressionIsVariable(exp, variable)
+{
+ return exp.Kind == "Var" && sameVariable(exp.Variable, variable);
+}
+
+// Similar to the above, except treat uses of a reference as if they were uses
+// of the dereferenced contents. This requires knowing the type of the
+// variable, and so takes its declaration rather than the variable itself.
+function expressionIsDeclaredVariable(exp, decl)
+{
+ exp = maybeDereference(exp, decl);
+ return expressionIsVariable(exp, decl.Variable);
+}
+
+function expressionIsMethodOnVariableDecl(exp, decl)
+{
+ // This might be calling a method on a base class, in which case exp will
+ // be an unnamed field of the variable instead of the variable itself.
+ while (exp.Kind == "Fld" && exp.Field.Name[0].startsWith("field:"))
+ exp = exp.Exp[0];
+ return expressionIsDeclaredVariable(exp, decl);
+}
+
+// Return whether the edge starts the live range of a variable's value, by setting
+// it to some new value. Examples of starting obj's live range:
+//
+// obj = foo;
+// obj = foo();
+// obj = foo(obj); // uses previous value but then sets to new value
+// SomeClass obj(true, 1); // constructor
+//
+function edgeStartsValueLiveRange(edge, variable)
+{
+ // Direct assignments start live range of lhs: var = value
+ if (edge.Kind == "Assign") {
+ const [lhs, rhs] = edge.Exp;
+ return (expressionIsVariable(lhs, variable) &&
+ !isReturningImmobileValue(edge, variable));
+ }
+
+ if (edge.Kind != "Call")
+ return false;
+
+ // Assignments of call results start live range: var = foo()
+ if (1 in edge.Exp) {
+ var lhs = edge.Exp[1];
+ if (expressionIsVariable(lhs, variable))
+ return true;
+ }
+
+ // Constructor calls start live range of instance: SomeClass var(...)
+ if ("PEdgeCallInstance" in edge) {
+ var instance = edge.PEdgeCallInstance.Exp;
+
+ // Kludge around incorrect dereference on some constructor calls.
+ if (instance.Kind == "Drf")
+ instance = instance.Exp[0];
+
+ if (!expressionIsVariable(instance, variable))
+ return false;
+
+ var callee = edge.Exp[0];
+ if (callee.Kind != "Var")
+ return false;
+
+ assert(callee.Variable.Kind == "Func");
+ var calleeName = readable(callee.Variable.Name[0]);
+
+ // Constructor calls include the text 'Name::Name(' or 'Name<...>::Name('.
+ var openParen = calleeName.indexOf('(');
+ if (openParen < 0)
+ return false;
+ calleeName = calleeName.substring(0, openParen);
+
+ var lastColon = calleeName.lastIndexOf('::');
+ if (lastColon < 0)
+ return false;
+ var constructorName = calleeName.substr(lastColon + 2);
+ calleeName = calleeName.substr(0, lastColon);
+
+ var lastTemplateOpen = calleeName.lastIndexOf('<');
+ if (lastTemplateOpen >= 0)
+ calleeName = calleeName.substr(0, lastTemplateOpen);
+
+ if (calleeName.endsWith(constructorName))
+ return true;
+ }
+
+ return false;
+}
+
+// Return the result of a `matcher` callback on the call found in the given
+// `edge`, if the edge is a direct call to a named function (if not, return false).
+// `matcher` is given the name of the callee (actually, a tuple
+// [fully qualified name, base name]), an array of expressions containing the
+// arguments, and if the result of the call is assigned to a variable,
+// the expression representing that variable(the lhs).
+//
+// https://firefox-source-docs.mozilla.org/js/HazardAnalysis/CFG.html for
+// documentation of the data structure used here.
+function matchEdgeCall(edge, matcher) {
+ if (edge.Kind != "Call") {
+ return false;
+ }
+
+ const callee = edge.Exp[0];
+
+ if (edge.Type.Kind == 'Function' &&
+ edge.Exp[0].Kind == 'Var' &&
+ edge.Exp[0].Variable.Kind == 'Func') {
+ const calleeName = edge.Exp[0].Variable.Name;
+ const args = edge.PEdgeCallArguments;
+ const argExprs = args ? args.Exp : [];
+ const lhs = edge.Exp[1]; // May be undefined
+ return matcher(calleeName, argExprs, lhs);
+ }
+
+ return false;
+}
+
+function edgeMarksVariableGCSafe(edge, variable) {
+ return matchEdgeCall(edge, (calleeName, argExprs, _lhs) => {
+ // explicit JS_HAZ_VARIABLE_IS_GC_SAFE annotation
+ return (calleeName[1] == 'MarkVariableAsGCSafe' &&
+ calleeName[0].includes("JS::detail::MarkVariableAsGCSafe") &&
+ argExprs.length == 1 &&
+ expressionIsVariable(argExprs[0], variable));
+ });
+}
+
+// Match an optional <namespace>:: followed by the class name,
+// and then an optional template parameter marker.
+//
+// Example: mozilla::dom::UniquePtr<...
+//
+function parseTypeName(typeName) {
+ const m = typeName.match(/^(((?:\w|::)+::)?(\w+))\b(\<)?/);
+ if (!m) {
+ return undefined;
+ }
+ const [, type, raw_namespace, classname, is_specialized] = m;
+ const namespace = raw_namespace === null ? "" : raw_namespace;
+ return { type, namespace, classname, is_specialized }
+}
+
+// Return whether an edge "clears out" a variable's value. A simple example
+// would be
+//
+// var = nullptr;
+//
+// for analyses for which nullptr is a "safe" value (eg GC rooting hazards; you
+// can't get in trouble by holding a nullptr live across a GC.) A more complex
+// example is a Maybe<T> that gets reset:
+//
+// Maybe<AutoCheckCannotGC> nogc;
+// nogc.emplace(cx);
+// nogc.reset();
+// gc(); // <-- not a problem; nogc is invalidated by prev line
+// nogc.emplace(cx);
+// foo(nogc);
+//
+// Yet another example is a UniquePtr being passed by value, which means the
+// receiver takes ownership:
+//
+// UniquePtr<JSObject*> uobj(obj);
+// foo(uobj);
+// gc();
+//
+function edgeEndsValueLiveRange(edge, variable, body)
+{
+ // var = nullptr;
+ if (edge.Kind == "Assign") {
+ const [lhs, rhs] = edge.Exp;
+ return expressionIsVariable(lhs, variable) && isImmobileValue(rhs);
+ }
+
+ if (edge.Kind != "Call")
+ return false;
+
+ if (edgeMarksVariableGCSafe(edge, variable)) {
+ // explicit JS_HAZ_VARIABLE_IS_GC_SAFE annotation
+ return true;
+ }
+
+ const decl = lookupVariable(body, variable);
+
+ if (matchEdgeCall(edge, (calleeName, argExprs, lhs) => {
+ return calleeName[1] == 'move' && calleeName[0].includes('std::move(') &&
+ expressionIsDeclaredVariable(argExprs[0], decl) &&
+ lhs &&
+ lhs.Kind == 'Var' &&
+ lhs.Variable.Kind == 'Temp';
+ })) {
+ // temp = std::move(var)
+ //
+ // If var is a UniquePtr, and we pass it into something that takes
+ // ownership, then it should be considered to be invalid. Example:
+ //
+ // consume(std::move(var));
+ //
+ // where consume takes a UniquePtr. This will compile to something like
+ //
+ // UniquePtr* __temp_1 = &std::move(var);
+ // UniquePtr&& __temp_2(*temp_1); // move constructor
+ // consume(__temp_2);
+ // ~UniquePtr(__temp_2);
+ //
+ // The line commented with "// move constructor" is a result of passing
+ // a UniquePtr as a parameter. If consume() took a UniquePtr&&
+ // directly, this would just be:
+ //
+ // UniquePtr* __temp_1 = &std::move(var);
+ // consume(__temp_1);
+ //
+ // which is not guaranteed to move from the reference. It might just
+ // ignore the parameter. We can't predict what consume(UniquePtr&&)
+ // will do. We do know that UniquePtr(UniquePtr&& other) moves out of
+ // `other`.
+ //
+ // The std::move() technically is irrelevant, but because we only care
+ // about bare variables, it has to be used, which is fortunate because
+ // the UniquePtr&& constructor operates on a temporary, not the
+ // variable we care about.
+
+ const lhs = edge.Exp[1].Variable;
+ if (basicBlockEatsVariable(lhs, body, edge.Index[1]))
+ return true;
+ }
+
+ const callee = edge.Exp[0];
+
+ if (edge.Type.Kind == 'Function' &&
+ edge.Type.TypeFunctionCSU &&
+ edge.PEdgeCallInstance &&
+ expressionIsMethodOnVariableDecl(edge.PEdgeCallInstance.Exp, decl))
+ {
+ const typeName = edge.Type.TypeFunctionCSU.Type.Name;
+
+ // Synthesize a zero-arg constructor name like
+ // mozilla::dom::UniquePtr<T>::UniquePtr(). Note that the `<T>` is
+ // literal -- the pretty name from sixgill will render the actual
+ // constructor name as something like
+ //
+ // UniquePtr<T>::UniquePtr() [where T = int]
+ //
+ const parsed = parseTypeName(typeName);
+ if (parsed) {
+ const { type, namespace, classname, is_specialized } = parsed;
+
+ // special-case: the initial constructor that doesn't provide a value.
+ // Useful for things like Maybe<T>.
+ const template = is_specialized ? '<T>' : '';
+ const ctorName = `${namespace}${classname}${template}::${classname}()`;
+ if (callee.Kind == 'Var' &&
+ typesWithSafeConstructors.has(type) &&
+ callee.Variable.Name[0].includes(ctorName))
+ {
+ return true;
+ }
+
+ // special-case: UniquePtr::reset() and similar.
+ if (callee.Kind == 'Var' &&
+ type in resetterMethods &&
+ resetterMethods[type].has(callee.Variable.Name[1]))
+ {
+ return true;
+ }
+ }
+ }
+
+ // special-case: passing UniquePtr<T> by value.
+ if (edge.Type.Kind == 'Function' &&
+ edge.Type.TypeFunctionArgument &&
+ edge.PEdgeCallArguments)
+ {
+ for (const i in edge.Type.TypeFunctionArgument) {
+ const param = edge.Type.TypeFunctionArgument[i];
+ if (param.Type.Kind != 'CSU')
+ continue;
+ if (!param.Type.Name.startsWith("mozilla::UniquePtr<"))
+ continue;
+ const arg = edge.PEdgeCallArguments.Exp[i];
+ if (expressionIsVariable(arg, variable)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+// Look up a variable in the list of declarations for this body.
+function lookupVariable(body, variable) {
+ for (const decl of (body.DefineVariable || [])) {
+ if (sameVariable(decl.Variable, variable)) {
+ return decl;
+ }
+ }
+ return undefined;
+}
+
+function edgeMovesVariable(edge, variable, body)
+{
+ if (edge.Kind != 'Call')
+ return false;
+ const callee = edge.Exp[0];
+ if (callee.Kind == 'Var' &&
+ callee.Variable.Kind == 'Func')
+ {
+ const { Variable: { Name: [ fullname, shortname ] } } = callee;
+
+ // Match an rvalue parameter.
+
+ if (!edge || !edge.PEdgeCallArguments || !edge.PEdgeCallArguments.Exp) {
+ return false;
+ }
+
+ for (const arg of edge.PEdgeCallArguments.Exp) {
+ if (arg.Kind != 'Drf') continue;
+ const val = arg.Exp[0];
+ if (val.Kind == 'Var' && sameVariable(val.Variable, variable)) {
+ // This argument is the variable we're looking for. Return true
+ // if it is passed as an rvalue reference.
+ const type = lookupVariable(body, variable).Type;
+ if (type.Kind == "Pointer" && type.Reference == PTR_RVALUE_REF) {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+// Scan forward through the basic block in 'body' starting at 'startpoint',
+// looking for a call that passes 'variable' to a move constructor that
+// "consumes" it (eg UniquePtr::UniquePtr(UniquePtr&&)).
+function basicBlockEatsVariable(variable, body, startpoint)
+{
+ const successors = getSuccessors(body);
+ let point = startpoint;
+ while (point in successors) {
+ // Only handle a single basic block. If it forks, stop looking.
+ const edges = successors[point];
+ if (edges.length != 1) {
+ return false;
+ }
+ const edge = edges[0];
+
+ if (edgeMovesVariable(edge, variable, body)) {
+ return true;
+ }
+
+ // edgeStartsValueLiveRange will find places where 'variable' is given
+ // a new value. Never observed in practice, since this function is only
+ // called with a temporary resulting from std::move(), which is used
+ // immediately for a call. But just to be robust to future uses:
+ if (edgeStartsValueLiveRange(edge, variable)) {
+ return false;
+ }
+
+ point = edge.Index[1];
+ }
+
+ return false;
+}
+
+var PROP_REFCNT = 1 << 0;
+var PROP_SHARED_PTR_DTOR = 1 << 1;
+
+function getCalleeProperties(calleeName) {
+ let props = 0;
+
+ if (isRefcountedDtor(calleeName)) {
+ props |= PROP_REFCNT;
+ }
+ if (calleeName.includes("~shared_ptr()")) {
+ props |= PROP_SHARED_PTR_DTOR;
+ }
+ return props;
+}
+
+// Basic C++ ABI mangling: prefix an identifier with its length, in decimal.
+function mangle(name) {
+ return name.length + name;
+}
+
+var TriviallyDestructibleTypes = new Set([
+ // Single-token types from
+ // https://itanium-cxx-abi.github.io/cxx-abi/abi.html#mangling-builtin
+ "void", "wchar_t", "bool", "char", "short", "int", "long", "float", "double",
+ "__int64", "__int128", "__float128", "char32_t", "char16_t", "char8_t",
+ // Remaining observed cases. These are types T in shared_ptr<T> that have
+ // been observed, where the types themselves have trivial destructors, and
+ // the custom deleter doesn't do anything nontrivial that we might care about.
+ "_IO_FILE"
+]);
+function synthesizeDestructorName(className) {
+ if (className.includes("<") || className.includes(" ") || className.includes("{")) {
+ return;
+ }
+ if (TriviallyDestructibleTypes.has(className)) {
+ return;
+ }
+ const parts = className.split("::");
+ const mangled_dtor = "_ZN" + parts.map(p => mangle(p)).join("") + "D2Ev";
+ const pretty_dtor = `void ${className}::~${parts.at(-1)}()`;
+ // Note that there will be a later check to verify that the function name
+ // synthesized here is an actual function, and assert if not (see
+ // assertFunctionExists() in computeCallgraph.js.)
+ return mangled_dtor + "$" + pretty_dtor;
+}
+
+function getCallEdgeProperties(body, edge, calleeName, functionBodies) {
+ let attrs = 0;
+ let extraCalls = [];
+
+ if (edge.Kind !== "Call") {
+ return { attrs, extraCalls };
+ }
+
+ const props = getCalleeProperties(calleeName);
+ if (props & PROP_REFCNT) {
+ // std::swap of two refcounted values thinks it can drop the
+ // ref count to zero. Or rather, it just calls operator=() in a context
+ // where the refcount will never drop to zero.
+ const blockId = blockIdentifier(body);
+ if (blockId.includes("std::swap") || blockId.includes("mozilla::Swap")) {
+ // Replace the refcnt release call with nothing. It's not going to happen.
+ attrs |= ATTR_REPLACED;
+ }
+ }
+
+ if (props & PROP_SHARED_PTR_DTOR) {
+ // Replace shared_ptr<T>::~shared_ptr() calls to T::~T() calls.
+ // Note that this will only apply to simple cases.
+ // Any templatized type, in particular, will be ignored and the original
+ // call tree will be left alone. If this triggers a hazard, then we can
+ // consider extending the mangling support.
+ //
+ // If the call to ~shared_ptr is not replaced, then it might end up calling
+ // an unknown function pointer. This does not always happen-- in some cases,
+ // the call tree below ~shared_ptr will invoke the correct destructor without
+ // going through function pointers.
+ const m = calleeName.match(/shared_ptr<(.*?)>::~shared_ptr\(\)(?: \[with T = ([\w:]+))?/);
+ assert(m);
+ let className = m[1] == "T" ? m[2] : m[1];
+ assert(className != "");
+ // cv qualification does not apply to destructors.
+ className = className.replace("const ", "");
+ className = className.replace("volatile ", "");
+ const dtor = synthesizeDestructorName(className);
+ if (dtor) {
+ attrs |= ATTR_REPLACED;
+ extraCalls.push({
+ attrs: ATTR_SYNTHETIC,
+ name: dtor,
+ });
+ }
+ }
+
+ if ((props & PROP_REFCNT) == 0) {
+ return { attrs, extraCalls };
+ }
+
+ let callee = edge.Exp[0];
+ while (callee.Kind === "Drf") {
+ callee = callee.Exp[0];
+ }
+
+ const instance = edge.PEdgeCallInstance.Exp;
+ if (instance.Kind !== "Var") {
+ // TODO: handle field destructors
+ return { attrs, extraCalls };
+ }
+
+ // Test whether the dtor call is dominated by operations on the variable
+ // that mean it will not go to a zero refcount in the dtor: either because
+ // it's already dead (eg r.forget() was called) or because it can be proven
+ // to have a ref count of greater than 1. This is implemented by looking
+ // for the reverse: find a path scanning backwards from the dtor call where
+ // the variable is used in any way that does *not* ensure that it is
+ // trivially destructible.
+
+ const variable = instance.Variable;
+
+ const visitor = new class DominatorVisitor extends Visitor {
+ // Do not revisit nodes. For new nodes, relay the decision made by
+ // extend_path.
+ next_action(seen, current) { return seen ? "prune" : current; }
+
+ // We don't revisit, so always use the new.
+ merge_info(seen, current) { return current; }
+
+ // Return the action to take from this node.
+ extend_path(edge, body, ppoint, successor_value) {
+ if (!edge) {
+ // Dummy edge to join two points.
+ return "continue";
+ }
+
+ if (!edgeUsesVariable(edge, variable, body)) {
+ // Nothing of interest on this edge, keep searching.
+ return "continue";
+ }
+
+ if (edgeEndsValueLiveRange(edge, variable, body)) {
+ // This path is safe!
+ return "prune";
+ }
+
+ // Unsafe. Found a use that might set the variable to a
+ // nonzero refcount.
+ return "done";
+ }
+ }(functionBodies);
+
+ // Searching upwards from a destructor call, return the opposite of: is
+ // there a path to a use or the start of the function that does NOT hit a
+ // safe assignment like refptr.forget() first?
+ //
+ // In graph terms: return whether the destructor call is dominated by forget() calls (or similar).
+ const edgeIsNonReleasingDtor = !BFS_upwards(
+ body, edge.Index[0], functionBodies, visitor, "start",
+ false // Return value if we do not reach the root without finding a non-forget() use.
+ );
+ if (edgeIsNonReleasingDtor) {
+ attrs |= ATTR_GC_SUPPRESSED | ATTR_NONRELEASING;
+ }
+ return { attrs, extraCalls };
+}
+
+// gcc uses something like "__dt_del " for virtual destructors that it
+// generates.
+function isSyntheticVirtualDestructor(funcName) {
+ return funcName.endsWith(" ");
+}
+
+function typedField(field)
+{
+ if ("FieldInstanceFunction" in field) {
+ // Virtual call
+ //
+ // This makes a minimal attempt at dealing with overloading, by
+ // incorporating the number of parameters. So far, that is all that has
+ // been needed. If more is needed, sixgill will need to produce a full
+ // mangled type.
+ const {Type, Name: [name]} = field;
+
+ // Virtual destructors don't need a type or argument count,
+ // and synthetic ones don't have them filled in.
+ if (isSyntheticVirtualDestructor(name)) {
+ return name;
+ }
+
+ var nargs = 0;
+ if (Type.Kind == "Function" && "TypeFunctionArguments" in Type)
+ nargs = Type.TypeFunctionArguments.Type.length;
+ return name + ":" + nargs;
+ } else {
+ // Function pointer field
+ return field.Name[0];
+ }
+}
+
+function fieldKey(csuName, field)
+{
+ return csuName + "." + typedField(field);
+}
diff --git a/js/src/devtools/rootAnalysis/README.md b/js/src/devtools/rootAnalysis/README.md
new file mode 100644
index 0000000000..08a4fcde29
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/README.md
@@ -0,0 +1,3 @@
+# Spidermonkey JSAPI rooting analysis
+
+See js/src/docs/HazardAnalysis/index.md
diff --git a/js/src/devtools/rootAnalysis/analyze.py b/js/src/devtools/rootAnalysis/analyze.py
new file mode 100755
index 0000000000..dd37991d41
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/analyze.py
@@ -0,0 +1,462 @@
+#!/usr/bin/env python3
+
+#
+# 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/.
+
+"""
+Runs the static rooting analysis
+"""
+
+import argparse
+import os
+import subprocess
+import sys
+from subprocess import Popen
+
+try:
+ from shlex import quote
+except ImportError:
+ from pipes import quote
+
+
+def execfile(thefile, globals):
+ exec(compile(open(thefile).read(), filename=thefile, mode="exec"), globals)
+
+
+# Label a string as an output.
+class Output(str):
+ pass
+
+
+# Label a string as a pattern for multiple inputs.
+class MultiInput(str):
+ pass
+
+
+# Construct a new environment by merging in some settings needed for running the individual scripts.
+def env(config):
+ # Add config['sixgill_bin'] to $PATH if not already there.
+ path = os.environ["PATH"].split(":")
+ if dir := config.get("sixgill_bin"):
+ if dir not in path:
+ path.insert(0, dir)
+
+ return dict(
+ os.environ,
+ PATH=":".join(path),
+ XDB=f"{config['sixgill_bin']}/xdb.so",
+ SOURCE=config["source"],
+ )
+
+
+def fill(command, config):
+ filled = []
+ for s in command:
+ try:
+ rep = s.format(**config)
+ except KeyError:
+ print("Substitution failed: %s" % s)
+ filled = None
+ break
+
+ if isinstance(s, Output):
+ filled.append(Output(rep))
+ elif isinstance(s, MultiInput):
+ N = int(config["jobs"])
+ for i in range(1, N + 1):
+ filled.append(rep.format(i=i, n=N))
+ else:
+ filled.append(rep)
+
+ if filled is None:
+ raise Exception("substitution failure")
+
+ return tuple(filled)
+
+
+def print_command(job, config, env=None):
+ # Display a command to run that has roughly the same effect as what was
+ # actually run. The actual command uses temporary files that get renamed at
+ # the end, and run some commands in parallel chunks. The printed command
+ # will substitute in the actual output and run in a single chunk, so that
+ # it is easier to cut & paste and add a --function flag for debugging.
+ cfg = dict(config, n=1, i=1, jobs=1)
+ cmd = job_command_with_final_output_names(job)
+ cmd = fill(cmd, cfg)
+
+ cmd = [quote(s) for s in cmd]
+ if outfile := job.get("redirect-output"):
+ cmd.extend([">", quote(outfile.format(**cfg))])
+ if HOME := os.environ.get("HOME"):
+ cmd = [s.replace(HOME, "~") for s in cmd]
+
+ if env:
+ # Try to keep the command as short as possible by only displaying
+ # modified environment variable settings.
+ e = os.environ
+ changed = {key: value for key, value in env.items() if value != e.get(key)}
+ if changed:
+ settings = []
+ for key, value in changed.items():
+ if key in e and e[key] in value:
+ # Display modifications as V=prefix${V}suffix when
+ # possible. This can make a huge different for $PATH.
+ start = value.index(e[key])
+ end = start + len(e[key])
+ setting = '%s="%s${%s}%s"' % (key, value[:start], key, value[end:])
+ else:
+ setting = '%s="%s"' % (key, value)
+ if HOME:
+ setting = setting.replace(HOME, "$HOME")
+ settings.append(setting)
+
+ cmd = settings + cmd
+
+ print(" " + " ".join(cmd))
+
+
+JOBS = {
+ "list-dbs": {"command": ["ls", "-l"]},
+ "rawcalls": {
+ "command": [
+ "{js}",
+ "{analysis_scriptdir}/computeCallgraph.js",
+ "{typeInfo}",
+ Output("{rawcalls}"),
+ "{i}",
+ "{n}",
+ ],
+ "multi-output": True,
+ "outputs": ["rawcalls.{i}.of.{n}"],
+ },
+ "gcFunctions": {
+ "command": [
+ "{js}",
+ "{analysis_scriptdir}/computeGCFunctions.js",
+ MultiInput("{rawcalls}"),
+ "--outputs",
+ Output("{callgraph}"),
+ Output("{gcFunctions}"),
+ Output("{gcFunctions_list}"),
+ Output("{limitedFunctions_list}"),
+ ],
+ "outputs": [
+ "callgraph.txt",
+ "gcFunctions.txt",
+ "gcFunctions.lst",
+ "limitedFunctions.lst",
+ ],
+ },
+ "gcTypes": {
+ "command": [
+ "{js}",
+ "{analysis_scriptdir}/computeGCTypes.js",
+ Output("{gcTypes}"),
+ Output("{typeInfo}"),
+ ],
+ "outputs": ["gcTypes.txt", "typeInfo.txt"],
+ },
+ "allFunctions": {
+ "command": ["{sixgill_bin}/xdbkeys", "src_body.xdb"],
+ "redirect-output": "allFunctions.txt",
+ },
+ "hazards": {
+ "command": [
+ "{js}",
+ "{analysis_scriptdir}/analyzeRoots.js",
+ "{gcFunctions_list}",
+ "{limitedFunctions_list}",
+ "{gcTypes}",
+ "{typeInfo}",
+ "{i}",
+ "{n}",
+ "tmp.{i}.of.{n}",
+ ],
+ "multi-output": True,
+ "redirect-output": "rootingHazards.{i}.of.{n}",
+ },
+ "gather-hazards": {
+ "command": [
+ "{js}",
+ "{analysis_scriptdir}/mergeJSON.js",
+ MultiInput("{hazards}"),
+ Output("{all_hazards}"),
+ ],
+ "outputs": ["rootingHazards.json"],
+ },
+ "explain": {
+ "command": [
+ sys.executable,
+ "{analysis_scriptdir}/explain.py",
+ "{all_hazards}",
+ "{gcFunctions}",
+ Output("{explained_hazards}"),
+ Output("{unnecessary}"),
+ Output("{refs}"),
+ Output("{html}"),
+ ],
+ "outputs": ["hazards.txt", "unnecessary.txt", "refs.txt", "hazards.html"],
+ },
+ "heapwrites": {
+ "command": ["{js}", "{analysis_scriptdir}/analyzeHeapWrites.js"],
+ "redirect-output": "heapWriteHazards.txt",
+ },
+}
+
+
+# Generator of (i, j, item) tuples corresponding to outputs:
+# - i is just the index of the yielded tuple (a la enumerate())
+# - j is the index of the item in the command list
+# - item is command[j]
+def out_indexes(command):
+ i = 0
+ for j, fragment in enumerate(command):
+ if isinstance(fragment, Output):
+ yield (i, j, fragment)
+ i += 1
+
+
+def job_command_with_final_output_names(job):
+ outfiles = job.get("outputs", [])
+ command = list(job["command"])
+ for i, j, name in out_indexes(job["command"]):
+ command[j] = outfiles[i]
+ return command
+
+
+def run_job(name, config):
+ job = JOBS[name]
+ outs = job.get("outputs") or job.get("redirect-output")
+ print("Running " + name + " to generate " + str(outs))
+ if "function" in job:
+ job["function"](config, job["redirect-output"])
+ return
+
+ N = int(config["jobs"]) if job.get("multi-output") else 1
+ config["n"] = N
+ jobs = {}
+ for i in range(1, N + 1):
+ config["i"] = i
+ cmd = fill(job["command"], config)
+ info = spawn_command(cmd, job, name, config)
+ jobs[info["proc"].pid] = info
+
+ if config["verbose"] > 0:
+ print_command(job, config, env=env(config))
+
+ final_status = 0
+ while jobs:
+ pid, status = os.wait()
+ final_status = final_status or status
+ info = jobs[pid]
+ del jobs[pid]
+ if "redirect" in info:
+ info["redirect"].close()
+
+ # Rename the temporary files to their final names.
+ for temp, final in info["rename_map"].items():
+ try:
+ if config["verbose"] > 1:
+ print("Renaming %s -> %s" % (temp, final))
+ os.rename(temp, final)
+ except OSError:
+ print("Error renaming %s -> %s" % (temp, final))
+ raise
+
+ if final_status != 0:
+ raise Exception("job {} returned status {}".format(name, final_status))
+
+
+def spawn_command(cmdspec, job, name, config):
+ rename_map = {}
+
+ if "redirect-output" in job:
+ stdout_filename = "{}.tmp{}".format(name, config.get("i", ""))
+ final_outfile = job["redirect-output"].format(**config)
+ rename_map[stdout_filename] = final_outfile
+ command = cmdspec
+ else:
+ outfiles = fill(job["outputs"], config)
+ stdout_filename = None
+
+ # Replace the Outputs with temporary filenames, and record a mapping
+ # from those temp names to their actual final names that will be used
+ # if the command succeeds.
+ command = list(cmdspec)
+ for i, j, raw_name in out_indexes(cmdspec):
+ [name] = fill([raw_name], config)
+ command[j] = "{}.tmp{}".format(name, config.get("i", ""))
+ rename_map[command[j]] = outfiles[i]
+
+ sys.stdout.flush()
+ info = {"rename_map": rename_map}
+ if stdout_filename:
+ info["redirect"] = open(stdout_filename, "w")
+ info["proc"] = Popen(command, stdout=info["redirect"], env=env(config))
+ else:
+ info["proc"] = Popen(command, env=env(config))
+
+ if config["verbose"] > 1:
+ print("Spawned process {}".format(info["proc"].pid))
+
+ return info
+
+
+# Default to conservatively assuming 4GB/job.
+def max_parallel_jobs(job_size=4 * 2**30):
+ """Return the max number of parallel jobs we can run without overfilling
+ memory, assuming heavyweight jobs."""
+ from_cores = int(subprocess.check_output(["nproc", "--ignore=1"]).strip())
+ mem_bytes = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES")
+ from_mem = round(mem_bytes / job_size)
+ return min(from_cores, from_mem)
+
+
+config = {"analysis_scriptdir": os.path.dirname(__file__)}
+
+defaults = [
+ "%s/defaults.py" % config["analysis_scriptdir"],
+ "%s/defaults.py" % os.getcwd(),
+]
+
+parser = argparse.ArgumentParser(
+ description="Statically analyze build tree for rooting hazards."
+)
+parser.add_argument(
+ "step", metavar="STEP", type=str, nargs="?", help="run only step STEP"
+)
+parser.add_argument(
+ "--source", metavar="SOURCE", type=str, nargs="?", help="source code to analyze"
+)
+parser.add_argument(
+ "--js",
+ metavar="JSSHELL",
+ type=str,
+ nargs="?",
+ help="full path to ctypes-capable JS shell",
+)
+parser.add_argument(
+ "--first",
+ metavar="STEP",
+ type=str,
+ nargs="?",
+ help="execute all jobs starting with STEP",
+)
+parser.add_argument(
+ "--last", metavar="STEP", type=str, nargs="?", help="stop at step STEP"
+)
+parser.add_argument(
+ "--jobs",
+ "-j",
+ default=None,
+ metavar="JOBS",
+ type=int,
+ help="number of simultaneous analyzeRoots.js jobs",
+)
+parser.add_argument(
+ "--list", const=True, nargs="?", type=bool, help="display available steps"
+)
+parser.add_argument(
+ "--expect-file",
+ type=str,
+ nargs="?",
+ help="deprecated option, temporarily still present for backwards " "compatibility",
+)
+parser.add_argument(
+ "--verbose",
+ "-v",
+ action="count",
+ default=1,
+ help="Display cut & paste commands to run individual steps (give twice for more output)",
+)
+parser.add_argument("--quiet", "-q", action="count", default=0, help="Suppress output")
+
+args = parser.parse_args()
+args.verbose = max(0, args.verbose - args.quiet)
+
+for default in defaults:
+ try:
+ execfile(default, config)
+ if args.verbose > 1:
+ print("Loaded %s" % default)
+ except Exception:
+ pass
+
+# execfile() used config as the globals for running the
+# defaults.py script, and will have set a __builtins__ key as a side effect.
+del config["__builtins__"]
+data = config.copy()
+
+for k, v in vars(args).items():
+ if v is not None:
+ data[k] = v
+
+if args.jobs is not None:
+ data["jobs"] = args.jobs
+if not data.get("jobs"):
+ data["jobs"] = max_parallel_jobs()
+
+if "GECKO_PATH" in os.environ:
+ data["source"] = os.environ["GECKO_PATH"]
+if "SOURCE" in os.environ:
+ data["source"] = os.environ["SOURCE"]
+
+steps = [
+ "gcTypes",
+ "rawcalls",
+ "gcFunctions",
+ "allFunctions",
+ "hazards",
+ "gather-hazards",
+ "explain",
+ "heapwrites",
+]
+
+if args.list:
+ for step in steps:
+ job = JOBS[step]
+ outfiles = job.get("outputs") or job.get("redirect-output")
+ if outfiles:
+ print(
+ "%s\n ->%s %s"
+ % (step, "*" if job.get("multi-output") else "", outfiles)
+ )
+ else:
+ print(step)
+ sys.exit(0)
+
+for step in steps:
+ job = JOBS[step]
+ if "redirect-output" in job:
+ data[step] = job["redirect-output"]
+ elif "outputs" in job and "command" in job:
+ outfiles = job["outputs"]
+ num_outputs = 0
+ for i, j, name in out_indexes(job["command"]):
+ # Trim the {curly brackets} off of the output keys.
+ data[name[1:-1]] = outfiles[i]
+ num_outputs += 1
+ assert (
+ len(outfiles) == num_outputs
+ ), 'step "%s": mismatched number of output files (%d) and params (%d)' % (
+ step,
+ num_outputs,
+ len(outfiles),
+ ) # NOQA: E501
+
+if args.step:
+ if args.first or args.last:
+ raise Exception(
+ "--first and --last cannot be used when a step argument is given"
+ )
+ steps = [args.step]
+else:
+ if args.first:
+ steps = steps[steps.index(args.first) :]
+ if args.last:
+ steps = steps[: steps.index(args.last) + 1]
+
+for step in steps:
+ run_job(step, data)
diff --git a/js/src/devtools/rootAnalysis/analyzeHeapWrites.js b/js/src/devtools/rootAnalysis/analyzeHeapWrites.js
new file mode 100644
index 0000000000..28679676a5
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/analyzeHeapWrites.js
@@ -0,0 +1,1396 @@
+/* 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/. */
+
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+
+"use strict";
+
+loadRelativeToScript('utility.js');
+loadRelativeToScript('annotations.js');
+loadRelativeToScript('callgraph.js');
+loadRelativeToScript('dumpCFG.js');
+
+///////////////////////////////////////////////////////////////////////////////
+// Annotations
+///////////////////////////////////////////////////////////////////////////////
+
+function checkExternalFunction(entry)
+{
+ var whitelist = [
+ "__builtin_clz",
+ "__builtin_expect",
+ "isprint",
+ "ceilf",
+ "floorf",
+ /^rusturl/,
+ "memcmp",
+ "strcmp",
+ "fmod",
+ "floor",
+ "ceil",
+ "atof",
+ /memchr/,
+ "strlen",
+ /Servo_DeclarationBlock_GetCssText/,
+ "Servo_GetArcStringData",
+ "Servo_IsWorkerThread",
+ /nsIFrame::AppendOwnedAnonBoxes/,
+ // Assume that atomic accesses are threadsafe.
+ /^__atomic_/,
+ ];
+ if (entry.matches(whitelist))
+ return;
+
+ // memcpy and memset are safe if the target pointer is threadsafe.
+ const simpleWrites = [
+ "memcpy",
+ "memset",
+ "memmove",
+ ];
+
+ if (entry.isSafeArgument(1) && simpleWrites.includes(entry.name))
+ return;
+
+ dumpError(entry, null, "External function");
+}
+
+function hasThreadsafeReferenceCounts(entry, regexp)
+{
+ // regexp should match some nsISupports-operating function and produce the
+ // name of the nsISupports class via exec().
+
+ // nsISupports classes which have threadsafe reference counting.
+ var whitelist = [
+ "nsIRunnable",
+
+ // I don't know if these always have threadsafe refcounts.
+ "nsAtom",
+ "nsIPermissionManager",
+ "nsIURI",
+ ];
+
+ var match = regexp.exec(entry.name);
+ return match && nameMatchesArray(match[1], whitelist);
+}
+
+function checkOverridableVirtualCall(entry, location, callee)
+{
+ // We get here when a virtual call is made on a structure which might be
+ // overridden by script or by a binary extension. This includes almost
+ // everything under nsISupports, however, so for the most part we ignore
+ // this issue. The exception is for nsISupports AddRef/Release, which are
+ // not in general threadsafe and whose overrides will not be generated by
+ // the callgraph analysis.
+ if (callee != "nsISupports.AddRef" && callee != "nsISupports.Release")
+ return;
+
+ if (hasThreadsafeReferenceCounts(entry, /::~?nsCOMPtr\(.*?\[with T = (.*?)\]$/))
+ return;
+ if (hasThreadsafeReferenceCounts(entry, /RefPtrTraits.*?::Release.*?\[with U = (.*?)\]/))
+ return;
+ if (hasThreadsafeReferenceCounts(entry, /nsCOMPtr<T>::assign_assuming_AddRef.*?\[with T = (.*?)\]/))
+ return;
+ if (hasThreadsafeReferenceCounts(entry, /nsCOMPtr<T>::assign_with_AddRef.*?\[with T = (.*?)\]/))
+ return;
+
+ // Watch for raw addref/release.
+ var whitelist = [
+ "Gecko_AddRefAtom",
+ "Gecko_ReleaseAtom",
+ /nsPrincipal::Get/,
+ /CounterStylePtr::Reset/,
+ ];
+ if (entry.matches(whitelist))
+ return;
+
+ dumpError(entry, location, "AddRef/Release on nsISupports");
+}
+
+function checkIndirectCall(entry, location, callee)
+{
+ var name = entry.name;
+
+ // These hash table callbacks should be threadsafe.
+ if (/PLDHashTable/.test(name) && (/matchEntry/.test(callee) || /hashKey/.test(callee)))
+ return;
+ if (/PL_HashTable/.test(name) && /keyCompare/.test(callee))
+ return;
+
+ dumpError(entry, location, "Indirect call " + callee);
+}
+
+function checkVariableAssignment(entry, location, variable)
+{
+ var name = entry.name;
+
+ dumpError(entry, location, "Variable assignment " + variable);
+}
+
+// Annotations for function parameters, based on function name and parameter
+// name + type.
+function treatAsSafeArgument(entry, varName, csuName)
+{
+ var whitelist = [
+ // These iterator classes should all be thread local. They are passed
+ // in to some Servo bindings and are created on the heap by others, so
+ // just ignore writes to them.
+ [null, null, /StyleChildrenIterator/],
+ [null, null, /ExplicitChildIterator/],
+
+ // The use of BeginReading() to instantiate this class confuses the
+ // analysis.
+ [null, null, /nsReadingIterator/],
+
+ // These classes are passed to some Servo bindings to fill in.
+ [/^Gecko_/, null, "nsStyleImageLayers"],
+ [/^Gecko_/, null, /FontFamilyList/],
+
+ // Various Servo binding out parameters. This is a mess and there needs
+ // to be a way to indicate which params are out parameters, either using
+ // an attribute or a naming convention.
+ ["Gecko_SetCounterStyleToName", "aPtr", null],
+ ["Gecko_SetCounterStyleToSymbols", "aPtr", null],
+ ["Gecko_SetCounterStyleToString", "aPtr", null],
+ ["Gecko_CopyCounterStyle", "aDst", null],
+ ["Gecko_SetMozBinding", "aDisplay", null],
+ [/ClassOrClassList/, /aClass/, null],
+ ["Gecko_GetAtomAsUTF16", "aLength", null],
+ ["Gecko_CopyMozBindingFrom", "aDest", null],
+ ["Gecko_SetNullImageValue", "aImage", null],
+ ["Gecko_SetGradientImageValue", "aImage", null],
+ ["Gecko_SetImageElement", "aImage", null],
+ ["Gecko_SetLayerImageImageValue", "aImage", null],
+ ["Gecko_CopyImageValueFrom", "aImage", null],
+ ["Gecko_SetCursorArrayLength", "aStyleUI", null],
+ ["Gecko_CopyCursorArrayFrom", "aDest", null],
+ ["Gecko_SetCursorImageValue", "aCursor", null],
+ ["Gecko_SetListStyleImageImageValue", "aList", null],
+ ["Gecko_SetListStyleImageNone", "aList", null],
+ ["Gecko_CopyListStyleImageFrom", "aList", null],
+ ["Gecko_ClearStyleContents", "aContent", null],
+ ["Gecko_CopyStyleContentsFrom", "aContent", null],
+ ["Gecko_CopyStyleGridTemplateValues", "aGridTemplate", null],
+ ["Gecko_ResetStyleCoord", null, null],
+ ["Gecko_CopyClipPathValueFrom", "aDst", null],
+ ["Gecko_DestroyClipPath", "aClip", null],
+ ["Gecko_ResetFilters", "effects", null],
+ [/Gecko_CSSValue_Set/, "aCSSValue", null],
+ ["Gecko_CSSValue_Drop", "aCSSValue", null],
+ ["Gecko_CSSFontFaceRule_GetCssText", "aResult", null],
+ ["Gecko_EnsureTArrayCapacity", "aArray", null],
+ ["Gecko_ClearPODTArray", "aArray", null],
+ ["Gecko_SetStyleGridTemplate", "aGridTemplate", null],
+ ["Gecko_ResizeTArrayForStrings", "aArray", null],
+ ["Gecko_ClearAndResizeStyleContents", "aContent", null],
+ [/Gecko_ClearAndResizeCounter/, "aContent", null],
+ [/Gecko_CopyCounter.*?From/, "aContent", null],
+ [/Gecko_SetContentDataImageValue/, "aList", null],
+ [/Gecko_SetContentData/, "aContent", null],
+ ["Gecko_SetCounterFunction", "aContent", null],
+ [/Gecko_EnsureStyle.*?ArrayLength/, "aArray", null],
+ ["Gecko_GetOrCreateKeyframeAtStart", "aKeyframes", null],
+ ["Gecko_GetOrCreateInitialKeyframe", "aKeyframes", null],
+ ["Gecko_GetOrCreateFinalKeyframe", "aKeyframes", null],
+ ["Gecko_AppendPropertyValuePair", "aProperties", null],
+ ["Gecko_SetStyleCoordCalcValue", null, null],
+ ["Gecko_StyleClipPath_SetURLValue", "aClip", null],
+ ["Gecko_nsStyleFilter_SetURLValue", "aEffects", null],
+ ["Gecko_nsStyleSVG_SetDashArrayLength", "aSvg", null],
+ ["Gecko_nsStyleSVG_CopyDashArray", "aDst", null],
+ ["Gecko_nsStyleFont_SetLang", "aFont", null],
+ ["Gecko_nsStyleFont_CopyLangFrom", "aFont", null],
+ ["Gecko_ClearWillChange", "aDisplay", null],
+ ["Gecko_AppendWillChange", "aDisplay", null],
+ ["Gecko_CopyWillChangeFrom", "aDest", null],
+ ["Gecko_InitializeImageCropRect", "aImage", null],
+ ["Gecko_CopyShapeSourceFrom", "aDst", null],
+ ["Gecko_DestroyShapeSource", "aShape", null],
+ ["Gecko_StyleShapeSource_SetURLValue", "aShape", null],
+ ["Gecko_NewBasicShape", "aShape", null],
+ ["Gecko_NewShapeImage", "aShape", null],
+ ["Gecko_nsFont_InitSystem", "aDest", null],
+ ["Gecko_nsFont_SetFontFeatureValuesLookup", "aFont", null],
+ ["Gecko_nsFont_ResetFontFeatureValuesLookup", "aFont", null],
+ ["Gecko_nsStyleFont_FixupNoneGeneric", "aFont", null],
+ ["Gecko_StyleTransition_SetUnsupportedProperty", "aTransition", null],
+ ["Gecko_AddPropertyToSet", "aPropertySet", null],
+ ["Gecko_CalcStyleDifference", "aAnyStyleChanged", null],
+ ["Gecko_CalcStyleDifference", "aOnlyResetStructsChanged", null],
+ ["Gecko_nsStyleSVG_CopyContextProperties", "aDst", null],
+ ["Gecko_nsStyleFont_PrefillDefaultForGeneric", "aFont", null],
+ ["Gecko_nsStyleSVG_SetContextPropertiesLength", "aSvg", null],
+ ["Gecko_ClearAlternateValues", "aFont", null],
+ ["Gecko_AppendAlternateValues", "aFont", null],
+ ["Gecko_CopyAlternateValuesFrom", "aDest", null],
+ ["Gecko_CounterStyle_GetName", "aResult", null],
+ ["Gecko_CounterStyle_GetSingleString", "aResult", null],
+ ["Gecko_nsTArray_FontFamilyName_AppendNamed", "aNames", null],
+ ["Gecko_nsTArray_FontFamilyName_AppendGeneric", "aNames", null],
+ ];
+ for (var [entryMatch, varMatch, csuMatch] of whitelist) {
+ assert(entryMatch || varMatch || csuMatch);
+ if (entryMatch && !nameMatches(entry.name, entryMatch))
+ continue;
+ if (varMatch && !nameMatches(varName, varMatch))
+ continue;
+ if (csuMatch && (!csuName || !nameMatches(csuName, csuMatch)))
+ continue;
+ return true;
+ }
+ return false;
+}
+
+function isSafeAssignment(entry, edge, variable)
+{
+ if (edge.Kind != 'Assign')
+ return false;
+
+ var [mangled, unmangled] = splitFunction(entry.name);
+
+ // The assignment
+ //
+ // nsFont* font = fontTypes[eType];
+ //
+ // ends up with 'font' pointing to a member of 'this', so it should inherit
+ // the safety of 'this'.
+ if (unmangled.includes("mozilla::LangGroupFontPrefs::Initialize") &&
+ variable == 'font')
+ {
+ const [lhs, rhs] = edge.Exp;
+ const {Kind, Exp: [{Kind: indexKind, Exp: [collection, index]}]} = rhs;
+ if (Kind == 'Drf' &&
+ indexKind == 'Index' &&
+ collection.Kind == 'Var' &&
+ collection.Variable.Name[0] == 'fontTypes')
+ {
+ return entry.isSafeArgument(0); // 'this'
+ }
+ }
+
+ return false;
+}
+
+function checkFieldWrite(entry, location, fields)
+{
+ var name = entry.name;
+ for (var field of fields) {
+ // The analysis is having some trouble keeping track of whether
+ // already_AddRefed and nsCOMPtr structures are safe to access.
+ // Hopefully these will be thread local, but it would be better to
+ // improve the analysis to handle these.
+ if (/already_AddRefed.*?.mRawPtr/.test(field))
+ return;
+ if (/nsCOMPtr<.*?>.mRawPtr/.test(field))
+ return;
+
+ if (/\bThreadLocal<\b/.test(field))
+ return;
+
+ // Debugging check for string corruption.
+ if (field == "nsStringBuffer.mCanary")
+ return;
+ }
+
+ var str = "";
+ for (var field of fields)
+ str += " " + field;
+
+ dumpError(entry, location, "Field write" + str);
+}
+
+function checkDereferenceWrite(entry, location, variable)
+{
+ var name = entry.name;
+
+ // Maybe<T> uses placement new on local storage in a way we don't understand.
+ // Allow this if the Maybe<> value itself is threadsafe.
+ if (/Maybe.*?::emplace/.test(name) && entry.isSafeArgument(0))
+ return;
+
+ // UniquePtr writes through temporaries referring to its internal storage.
+ // Allow this if the UniquePtr<> is threadsafe.
+ if (/UniquePtr.*?::reset/.test(name) && entry.isSafeArgument(0))
+ return;
+
+ // Operations on nsISupports reference counts.
+ if (hasThreadsafeReferenceCounts(entry, /nsCOMPtr<T>::swap\(.*?\[with T = (.*?)\]/))
+ return;
+
+ // ConvertToLowerCase::write writes through a local pointer into the first
+ // argument.
+ if (/ConvertToLowerCase::write/.test(name) && entry.isSafeArgument(0))
+ return;
+
+ dumpError(entry, location, "Dereference write " + (variable ? variable : "<unknown>"));
+}
+
+function ignoreCallEdge(entry, callee)
+{
+ var name = entry.name;
+
+ // nsPropertyTable::GetPropertyInternal has the option of removing data
+ // from the table, but when it is called by nsPropertyTable::GetProperty
+ // this will not occur.
+ if (/nsPropertyTable::GetPropertyInternal/.test(callee) &&
+ /nsPropertyTable::GetProperty/.test(name))
+ {
+ return true;
+ }
+
+ // Document::PropertyTable calls GetExtraPropertyTable (which has side
+ // effects) if the input category is non-zero. If a literal zero was passed
+ // in for the category then we treat it as a safe argument, per
+ // isEdgeSafeArgument, so just watch for that.
+ if (/Document::GetExtraPropertyTable/.test(callee) &&
+ /Document::PropertyTable/.test(name) &&
+ entry.isSafeArgument(1))
+ {
+ return true;
+ }
+
+ // This function has an explicit test for being on the main thread if the
+ // style has non-threadsafe refcounts, but the analysis isn't smart enough
+ // to understand what the actual styles that can be involved are.
+ if (/nsStyleList::SetCounterStyle/.test(callee))
+ return true;
+
+ // CachedBorderImageData is exclusively owned by nsStyleImage, but the
+ // analysis is not smart enough to know this.
+ if (/CachedBorderImageData::PurgeCachedImages/.test(callee) &&
+ /nsStyleImage::/.test(name) &&
+ entry.isSafeArgument(0))
+ {
+ return true;
+ }
+
+ // StyleShapeSource exclusively owns its UniquePtr<nsStyleImage>.
+ if (/nsStyleImage::SetURLValue/.test(callee) &&
+ /StyleShapeSource::SetURL/.test(name) &&
+ entry.isSafeArgument(0))
+ {
+ return true;
+ }
+
+ // The AddRef through a just-assigned heap pointer here is not handled by
+ // the analysis.
+ if (/nsCSSValue::Array::AddRef/.test(callee) &&
+ /nsStyleContentData::SetCounters/.test(name) &&
+ entry.isSafeArgument(2))
+ {
+ return true;
+ }
+
+ // AllChildrenIterator asks AppendOwnedAnonBoxes to append into an nsTArray
+ // local variable.
+ if (/nsIFrame::AppendOwnedAnonBoxes/.test(callee) &&
+ /AllChildrenIterator::AppendNativeAnonymousChildren/.test(name))
+ {
+ return true;
+ }
+
+ // Runnables are created and named on one thread, then dispatched
+ // (possibly to another). Writes on the origin thread are ok.
+ if (/::SetName/.test(callee) &&
+ /::UnlabeledDispatch/.test(name))
+ {
+ return true;
+ }
+
+ // We manually lock here
+ if (name == "Gecko_nsFont_InitSystem" ||
+ name == "Gecko_GetFontMetrics" ||
+ name == "Gecko_nsStyleFont_FixupMinFontSize" ||
+ /ThreadSafeGetDefaultFontHelper/.test(name))
+ {
+ return true;
+ }
+
+ return false;
+}
+
+function ignoreContents(entry)
+{
+ var whitelist = [
+ // We don't care what happens when we're about to crash.
+ "abort",
+ /MOZ_ReportAssertionFailure/,
+ /MOZ_ReportCrash/,
+ /MOZ_Crash/,
+ /MOZ_CrashPrintf/,
+ /AnnotateMozCrashReason/,
+ /InvalidArrayIndex_CRASH/,
+ /NS_ABORT_OOM/,
+
+ // These ought to be threadsafe.
+ "NS_DebugBreak",
+ /mozalloc_handle_oom/,
+ /^NS_Log/, /log_print/, /LazyLogModule::operator/,
+ /SprintfLiteral/, "PR_smprintf", "PR_smprintf_free",
+ /NS_DispatchToMainThread/, /NS_ReleaseOnMainThread/,
+ /NS_NewRunnableFunction/, /NS_Atomize/,
+ /nsCSSValue::BufferFromString/,
+ /NS_xstrdup/,
+ /Assert_NoQueryNeeded/,
+ /AssertCurrentThreadOwnsMe/,
+ /PlatformThread::CurrentId/,
+ /imgRequestProxy::GetProgressTracker/, // Uses an AutoLock
+ /Smprintf/,
+ "malloc",
+ "calloc",
+ "free",
+ "realloc",
+ "memalign",
+ "strdup",
+ "strndup",
+ "moz_xmalloc",
+ "moz_xcalloc",
+ "moz_xrealloc",
+ "moz_xmemalign",
+ "moz_xstrdup",
+ "moz_xstrndup",
+ "jemalloc_thread_local_arena",
+
+ // These all create static strings in local storage, which is threadsafe
+ // to do but not understood by the analysis yet.
+ / EmptyString\(\)/,
+
+ // These could probably be handled by treating the scope of PSAutoLock
+ // aka BaseAutoLock<PSMutex> as threadsafe.
+ /profiler_register_thread/,
+ /profiler_unregister_thread/,
+
+ // The analysis thinks we'll write to mBits in the DoGetStyleFoo<false>
+ // call. Maybe the template parameter confuses it?
+ /ComputedStyle::PeekStyle/,
+
+ // The analysis can't cope with the indirection used for the objects
+ // being initialized here, from nsCSSValue::Array::Create to the return
+ // value of the Item(i) getter.
+ /nsCSSValue::SetCalcValue/,
+
+ // Unable to analyze safety of linked list initialization.
+ "Gecko_NewCSSValueSharedList",
+ "Gecko_CSSValue_InitSharedList",
+
+ // Unable to trace through dataflow, but straightforward if inspected.
+ "Gecko_NewNoneTransform",
+
+ // Need main thread assertions or other fixes.
+ /EffectCompositor::GetServoAnimationRule/,
+ ];
+ if (entry.matches(whitelist))
+ return true;
+
+ if (entry.isSafeArgument(0)) {
+ var heapWhitelist = [
+ // Operations on heap structures pointed to by arrays and strings are
+ // threadsafe as long as the array/string itself is threadsafe.
+ /nsTArray_Impl.*?::AppendElement/,
+ /nsTArray_Impl.*?::RemoveElementsAt/,
+ /nsTArray_Impl.*?::ReplaceElementsAt/,
+ /nsTArray_Impl.*?::InsertElementAt/,
+ /nsTArray_Impl.*?::SetCapacity/,
+ /nsTArray_Impl.*?::SetLength/,
+ /nsTArray_base.*?::EnsureCapacity/,
+ /nsTArray_base.*?::ShiftData/,
+ /AutoTArray.*?::Init/,
+ /(nsTSubstring<T>|nsAC?String)::SetCapacity/,
+ /(nsTSubstring<T>|nsAC?String)::SetLength/,
+ /(nsTSubstring<T>|nsAC?String)::Assign/,
+ /(nsTSubstring<T>|nsAC?String)::Append/,
+ /(nsTSubstring<T>|nsAC?String)::Replace/,
+ /(nsTSubstring<T>|nsAC?String)::Trim/,
+ /(nsTSubstring<T>|nsAC?String)::Truncate/,
+ /(nsTSubstring<T>|nsAC?String)::StripTaggedASCII/,
+ /(nsTSubstring<T>|nsAC?String)::operator=/,
+ /nsTAutoStringN<T, N>::nsTAutoStringN/,
+
+ // Similar for some other data structures
+ /nsCOMArray_base::SetCapacity/,
+ /nsCOMArray_base::Clear/,
+ /nsCOMArray_base::AppendElement/,
+
+ // UniquePtr is similar.
+ /mozilla::UniquePtr/,
+
+ // The use of unique pointers when copying mCropRect here confuses
+ // the analysis.
+ /nsStyleImage::DoCopy/,
+ ];
+ if (entry.matches(heapWhitelist))
+ return true;
+ }
+
+ if (entry.isSafeArgument(1)) {
+ var firstArgWhitelist = [
+ /nsTextFormatter::snprintf/,
+ /nsTextFormatter::ssprintf/,
+ /_ASCIIToUpperInSitu/,
+
+ // Handle some writes into an array whose safety we don't have a good way
+ // of tracking currently.
+ /FillImageLayerList/,
+ /FillImageLayerPositionCoordList/,
+ ];
+ if (entry.matches(firstArgWhitelist))
+ return true;
+ }
+
+ if (entry.isSafeArgument(2)) {
+ var secondArgWhitelist = [
+ /nsStringBuffer::ToString/,
+ /AppendUTF\d+toUTF\d+/,
+ /AppendASCIItoUTF\d+/,
+ ];
+ if (entry.matches(secondArgWhitelist))
+ return true;
+ }
+
+ return false;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Sixgill Utilities
+///////////////////////////////////////////////////////////////////////////////
+
+function variableName(variable)
+{
+ return (variable && variable.Name) ? variable.Name[0] : null;
+}
+
+function stripFields(exp)
+{
+ // Fields and index operations do not involve any dereferences. Remove them
+ // from the expression but remember any encountered fields for use by
+ // annotations later on.
+ var fields = [];
+ while (true) {
+ if (exp.Kind == "Index") {
+ exp = exp.Exp[0];
+ continue;
+ }
+ if (exp.Kind == "Fld") {
+ var csuName = exp.Field.FieldCSU.Type.Name;
+ var fieldName = exp.Field.Name[0];
+ assert(csuName && fieldName);
+ fields.push(csuName + "." + fieldName);
+ exp = exp.Exp[0];
+ continue;
+ }
+ break;
+ }
+ return [exp, fields];
+}
+
+function isLocalVariable(variable)
+{
+ switch (variable.Kind) {
+ case "Return":
+ case "Temp":
+ case "Local":
+ case "Arg":
+ return true;
+ }
+ return false;
+}
+
+function isDirectCall(edge, regexp)
+{
+ return edge.Kind == "Call"
+ && edge.Exp[0].Kind == "Var"
+ && regexp.test(variableName(edge.Exp[0].Variable));
+}
+
+function isZero(exp)
+{
+ return exp.Kind == "Int" && exp.String == "0";
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Analysis Structures
+///////////////////////////////////////////////////////////////////////////////
+
+// Safe arguments are those which may be written through (directly, not through
+// pointer fields etc.) without concerns about thread safety. This includes
+// pointers to stack data, null pointers, and other data we know is thread
+// local, such as certain arguments to the root functions.
+//
+// Entries in the worklist keep track of the pointer arguments to the function
+// which are safe using a sorted array, so that this can be propagated down the
+// stack. Zero is |this|, and arguments are indexed starting at one.
+
+function WorklistEntry(name, safeArguments, stack, parameterNames)
+{
+ this.name = name;
+ this.safeArguments = safeArguments;
+ this.stack = stack;
+ this.parameterNames = parameterNames;
+}
+
+WorklistEntry.prototype.readable = function()
+{
+ const [ mangled, readable ] = splitFunction(this.name);
+ return readable;
+}
+
+WorklistEntry.prototype.mangledName = function()
+{
+ var str = this.name;
+ for (var safe of this.safeArguments)
+ str += " SAFE " + safe;
+ return str;
+}
+
+WorklistEntry.prototype.isSafeArgument = function(index)
+{
+ for (var safe of this.safeArguments) {
+ if (index == safe)
+ return true;
+ }
+ return false;
+}
+
+WorklistEntry.prototype.setParameterName = function(index, name)
+{
+ this.parameterNames[index] = name;
+}
+
+WorklistEntry.prototype.addSafeArgument = function(index)
+{
+ if (this.isSafeArgument(index))
+ return;
+ this.safeArguments.push(index);
+
+ // Sorting isn't necessary for correctness but makes printed stack info tidier.
+ this.safeArguments.sort();
+}
+
+function safeArgumentIndex(variable)
+{
+ if (variable.Kind == "This")
+ return 0;
+ if (variable.Kind == "Arg")
+ return variable.Index + 1;
+ return -1;
+}
+
+function nameMatches(name, match)
+{
+ if (typeof match == "string") {
+ if (name == match)
+ return true;
+ } else {
+ assert(match instanceof RegExp);
+ if (match.test(name))
+ return true;
+ }
+ return false;
+}
+
+function nameMatchesArray(name, matchArray)
+{
+ for (var match of matchArray) {
+ if (nameMatches(name, match))
+ return true;
+ }
+ return false;
+}
+
+WorklistEntry.prototype.matches = function(matchArray)
+{
+ return nameMatchesArray(this.name, matchArray);
+}
+
+function CallSite(callee, safeArguments, location, parameterNames)
+{
+ this.callee = callee;
+ this.safeArguments = safeArguments;
+ this.location = location;
+ this.parameterNames = parameterNames;
+}
+
+CallSite.prototype.safeString = function()
+{
+ if (this.safeArguments.length) {
+ var str = "";
+ for (var i = 0; i < this.safeArguments.length; i++) {
+ var arg = this.safeArguments[i];
+ if (arg in this.parameterNames)
+ str += " " + this.parameterNames[arg];
+ else
+ str += " <" + ((arg == 0) ? "this" : "arg" + (arg - 1)) + ">";
+ }
+ return " ### SafeArguments:" + str;
+ }
+ return "";
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Analysis Core
+///////////////////////////////////////////////////////////////////////////////
+
+var errorCount = 0;
+var errorLimit = 100;
+
+// We want to suppress output for functions that ended up not having any
+// hazards, for brevity of the final output. So each new toplevel function will
+// initialize this to a string, which should be printed only if an error is
+// seen.
+var errorHeader;
+
+var startTime = new Date;
+function elapsedTime()
+{
+ var seconds = (new Date - startTime) / 1000;
+ return "[" + seconds.toFixed(2) + "s] ";
+}
+
+var options = parse_options([
+ {
+ name: '--strip-prefix',
+ default: os.getenv('SOURCE') || '',
+ type: 'string'
+ },
+ {
+ name: '--add-prefix',
+ default: os.getenv('URLPREFIX') || '',
+ type: 'string'
+ },
+ {
+ name: '--verbose',
+ type: 'bool'
+ },
+]);
+
+function add_trailing_slash(str) {
+ if (str == '')
+ return str;
+ return str.endsWith("/") ? str : str + "/";
+}
+
+var removePrefix = add_trailing_slash(options.strip_prefix);
+var addPrefix = add_trailing_slash(options.add_prefix);
+
+if (options.verbose) {
+ printErr(`Removing prefix ${removePrefix} from paths`);
+ printErr(`Prepending ${addPrefix} to paths`);
+}
+
+print(elapsedTime() + "Loading types...");
+if (os.getenv("TYPECACHE"))
+ loadTypesWithCache('src_comp.xdb', os.getenv("TYPECACHE"));
+else
+ loadTypes('src_comp.xdb');
+print(elapsedTime() + "Starting analysis...");
+
+var xdb = xdbLibrary();
+xdb.open("src_body.xdb");
+
+var minStream = xdb.min_data_stream();
+var maxStream = xdb.max_data_stream();
+var roots = [];
+
+var [flag, arg] = scriptArgs;
+if (flag && (flag == '-f' || flag == '--function')) {
+ roots = [arg];
+} else {
+ for (var bodyIndex = minStream; bodyIndex <= maxStream; bodyIndex++) {
+ var key = xdb.read_key(bodyIndex);
+ var name = key.readString();
+ if (/^Gecko_/.test(name)) {
+ var data = xdb.read_entry(key);
+ if (/ServoBindings.cpp/.test(data.readString()))
+ roots.push(name);
+ xdb.free_string(data);
+ }
+ xdb.free_string(key);
+ }
+}
+
+print(elapsedTime() + "Found " + roots.length + " roots.");
+for (var i = 0; i < roots.length; i++) {
+ var root = roots[i];
+ errorHeader = elapsedTime() + "#" + (i + 1) + " Analyzing " + root + " ...";
+ try {
+ processRoot(root);
+ } catch (e) {
+ if (e != "Error!")
+ throw e;
+ }
+}
+
+print(`${elapsedTime()}Completed analysis, found ${errorCount}/${errorLimit} allowed errors`);
+
+var currentBody;
+
+// All local variable assignments we have seen in either the outer or inner
+// function. This crosses loop boundaries, and currently has an unsoundness
+// where later assignments in a loop are not taken into account.
+var assignments;
+
+// All loops in the current function which are reachable off main thread.
+var reachableLoops;
+
+// Functions that are reachable from the current root.
+var reachable = {};
+
+function dumpError(entry, location, text)
+{
+ if (errorHeader) {
+ print(errorHeader);
+ errorHeader = undefined;
+ }
+
+ var stack = entry.stack;
+ print("Error: " + text);
+ print("Location: " + entry.name + (location ? " @ " + location : "") + stack[0].safeString());
+ print("Stack Trace:");
+ // Include the callers in the stack trace instead of the callees. Make sure
+ // the dummy stack entry we added for the original roots is in place.
+ assert(stack[stack.length - 1].location == null);
+ for (var i = 0; i < stack.length - 1; i++)
+ print(stack[i + 1].callee + " @ " + stack[i].location + stack[i + 1].safeString());
+ print("\n");
+
+ if (++errorCount == errorLimit) {
+ print("Maximum number of errors encountered, exiting...");
+ quit();
+ }
+
+ throw "Error!";
+}
+
+// If edge is an assignment from a local variable, return the rhs variable.
+function variableAssignRhs(edge)
+{
+ if (edge.Kind == "Assign" && edge.Exp[1].Kind == "Drf" && edge.Exp[1].Exp[0].Kind == "Var") {
+ var variable = edge.Exp[1].Exp[0].Variable;
+ if (isLocalVariable(variable))
+ return variable;
+ }
+ return null;
+}
+
+function processAssign(body, entry, location, lhs, edge)
+{
+ var fields;
+ [lhs, fields] = stripFields(lhs);
+
+ switch (lhs.Kind) {
+ case "Var":
+ var name = variableName(lhs.Variable);
+ if (isLocalVariable(lhs.Variable)) {
+ // Remember any assignments to local variables in this function.
+ // Note that we ignore any points where the variable's address is
+ // taken and indirect assignments might occur. This is an
+ // unsoundness in the analysis.
+
+ let assign = [body, edge];
+
+ // Chain assignments if the RHS has only been assigned once.
+ var rhsVariable = variableAssignRhs(edge);
+ if (rhsVariable) {
+ var rhsAssign = singleAssignment(variableName(rhsVariable));
+ if (rhsAssign)
+ assign = rhsAssign;
+ }
+
+ if (!(name in assignments))
+ assignments[name] = [];
+ assignments[name].push(assign);
+ } else {
+ checkVariableAssignment(entry, location, name);
+ }
+ return;
+ case "Drf":
+ var variable = null;
+ if (lhs.Exp[0].Kind == "Var") {
+ variable = lhs.Exp[0].Variable;
+ if (isSafeVariable(entry, variable))
+ return;
+ } else if (lhs.Exp[0].Kind == "Fld") {
+ const {
+ Name: [ fieldName ],
+ Type: {Kind, Type: fieldType},
+ FieldCSU: {Type: {Kind: containerTypeKind,
+ Name: containerTypeName}}
+ } = lhs.Exp[0].Field;
+ const [containerExpr] = lhs.Exp[0].Exp;
+
+ if (containerTypeKind == 'CSU' &&
+ Kind == 'Pointer' &&
+ isEdgeSafeArgument(entry, containerExpr) &&
+ isSafeMemberPointer(containerTypeName, fieldName, fieldType))
+ {
+ return;
+ }
+ }
+ if (fields.length)
+ checkFieldWrite(entry, location, fields);
+ else
+ checkDereferenceWrite(entry, location, variableName(variable));
+ return;
+ case "Int":
+ if (isZero(lhs)) {
+ // This shows up under MOZ_ASSERT, to crash the process.
+ return;
+ }
+ }
+ dumpError(entry, location, "Unknown assignment " + JSON.stringify(lhs));
+}
+
+function get_location(rawLocation) {
+ const filename = rawLocation.CacheString.replace(removePrefix, '');
+ return addPrefix + filename + "#" + rawLocation.Line;
+}
+
+function process(entry, body, addCallee)
+{
+ if (!("PEdge" in body))
+ return;
+
+ // Add any arguments which are safe due to annotations.
+ if ("DefineVariable" in body) {
+ for (var defvar of body.DefineVariable) {
+ var index = safeArgumentIndex(defvar.Variable);
+ if (index >= 0) {
+ var varName = index ? variableName(defvar.Variable) : "this";
+ assert(varName);
+ entry.setParameterName(index, varName);
+ var csuName = null;
+ var type = defvar.Type;
+ if (type.Kind == "Pointer" && type.Type.Kind == "CSU")
+ csuName = type.Type.Name;
+ if (treatAsSafeArgument(entry, varName, csuName))
+ entry.addSafeArgument(index);
+ }
+ }
+ }
+
+ // Points in the body which are reachable if we are not on the main thread.
+ var nonMainThreadPoints = [];
+ nonMainThreadPoints[body.Index[0]] = true;
+
+ for (var edge of body.PEdge) {
+ // Ignore code that only executes on the main thread.
+ if (!(edge.Index[0] in nonMainThreadPoints))
+ continue;
+
+ var location = get_location(body.PPoint[edge.Index[0] - 1].Location);
+
+ var callees = getCallees(edge);
+ for (var callee of callees) {
+ switch (callee.kind) {
+ case "direct":
+ var safeArguments = getEdgeSafeArguments(entry, edge, callee.name);
+ addCallee(new CallSite(callee.name, safeArguments, location, {}));
+ break;
+ case "resolved-field":
+ break;
+ case "field":
+ var field = callee.csu + "." + callee.field;
+ if (callee.isVirtual)
+ checkOverridableVirtualCall(entry, location, field);
+ else
+ checkIndirectCall(entry, location, field);
+ break;
+ case "indirect":
+ checkIndirectCall(entry, location, callee.variable);
+ break;
+ default:
+ dumpError(entry, location, "Unknown call " + callee.kind);
+ break;
+ }
+ }
+
+ var fallthrough = true;
+
+ if (edge.Kind == "Assign") {
+ assert(edge.Exp.length == 2);
+ processAssign(body, entry, location, edge.Exp[0], edge);
+ } else if (edge.Kind == "Call") {
+ assert(edge.Exp.length <= 2);
+ if (edge.Exp.length == 2)
+ processAssign(body, entry, location, edge.Exp[1], edge);
+
+ // Treat assertion failures as if they don't return, so that
+ // asserting NS_IsMainThread() is sufficient to prevent the
+ // analysis from considering a block of code.
+ if (isDirectCall(edge, /MOZ_ReportAssertionFailure/))
+ fallthrough = false;
+ } else if (edge.Kind == "Loop") {
+ reachableLoops[edge.BlockId.Loop] = true;
+ } else if (edge.Kind == "Assume") {
+ if (testFailsOffMainThread(edge.Exp[0], edge.PEdgeAssumeNonZero))
+ fallthrough = false;
+ }
+
+ if (fallthrough)
+ nonMainThreadPoints[edge.Index[1]] = true;
+ }
+}
+
+function maybeProcessMissingFunction(entry, addCallee)
+{
+ // If a function is missing it might be because a destructor Foo::~Foo() is
+ // being called but GCC only gave us an implementation for
+ // Foo::~Foo(int32). See computeCallgraph.js for a little more info.
+ var name = entry.name;
+ if (name.indexOf("::~") > 0 && name.indexOf("()") > 0) {
+ var callee = name.replace("()", "(int32)");
+ addCallee(new CallSite(name, entry.safeArguments, entry.stack[0].location, entry.parameterNames));
+ return true;
+ }
+
+ // Similarly, a call to a C1 constructor might invoke the C4 constructor. A
+ // mangled constructor will be something like _ZN<length><name>C1E... or in
+ // the case of a templatized constructor, _ZN<length><name>C1I...EE... so
+ // we hack it and look for "C1E" or "C1I" and replace them with their C4
+ // variants. This will have rare false matches, but so far we haven't hit
+ // any external function calls of that sort.
+ if (entry.mangledName().includes("C1E") || entry.mangledName().includes("C1I")) {
+ var callee = name.replace("C1E", "C4E").replace("C1I", "C4I");
+ addCallee(new CallSite(name, entry.safeArguments, entry.stack[0].location, entry.parameterNames));
+ return true;
+ }
+
+ // Hack to manually follow some typedefs that show up on some functions.
+ // This is a bug in the sixgill GCC plugin I think, since sixgill is
+ // supposed to follow any typedefs itself.
+ if (/mozilla::dom::Element/.test(name)) {
+ var callee = name.replace("mozilla::dom::Element", "Document::Element");
+ addCallee(new CallSite(name, entry.safeArguments, entry.stack[0].location, entry.parameterNames));
+ return true;
+ }
+
+ // Hack for contravariant return types. When overriding a virtual method
+ // with a method that returns a different return type (a subtype of the
+ // original return type), we are getting the right mangled name but the
+ // wrong return type in the unmangled name.
+ if (/\$nsTextFrame*/.test(name)) {
+ var callee = name.replace("nsTextFrame", "nsIFrame");
+ addCallee(new CallSite(name, entry.safeArguments, entry.stack[0].location, entry.parameterNames));
+ return true;
+ }
+
+ return false;
+}
+
+function processRoot(name)
+{
+ var safeArguments = [];
+ var parameterNames = {};
+ var worklist = [new WorklistEntry(name, safeArguments, [new CallSite(name, safeArguments, null, parameterNames)], parameterNames)];
+
+ reachable = {};
+
+ while (worklist.length > 0) {
+ var entry = worklist.pop();
+
+ // In principle we would be better off doing a meet-over-paths here to get
+ // the common subset of arguments which are safe to write through. However,
+ // analyzing functions separately for each subset if simpler, ensures that
+ // the stack traces we produce accurately characterize the stack arguments,
+ // and should be fast enough for now.
+
+ if (entry.mangledName() in reachable)
+ continue;
+ reachable[entry.mangledName()] = true;
+
+ if (ignoreContents(entry))
+ continue;
+
+ var data = xdb.read_entry(entry.name);
+ var dataString = data.readString();
+ var callees = [];
+ if (dataString.length) {
+ // Reverse the order of the bodies we process so that we visit the
+ // outer function and see its assignments before the inner loops.
+ assignments = {};
+ reachableLoops = {};
+ var bodies = JSON.parse(dataString).reverse();
+ for (var body of bodies) {
+ if (!body.BlockId.Loop || body.BlockId.Loop in reachableLoops) {
+ currentBody = body;
+ process(entry, body, Array.prototype.push.bind(callees));
+ }
+ }
+ } else {
+ if (!maybeProcessMissingFunction(entry, Array.prototype.push.bind(callees)))
+ checkExternalFunction(entry);
+ }
+ xdb.free_string(data);
+
+ for (var callee of callees) {
+ if (!ignoreCallEdge(entry, callee.callee)) {
+ var nstack = [callee, ...entry.stack];
+ worklist.push(new WorklistEntry(callee.callee, callee.safeArguments, nstack, callee.parameterNames));
+ }
+ }
+ }
+}
+
+function isEdgeSafeArgument(entry, exp)
+{
+ var fields;
+ [exp, fields] = stripFields(exp);
+
+ if (exp.Kind == "Var" && isLocalVariable(exp.Variable))
+ return true;
+ if (exp.Kind == "Drf" && exp.Exp[0].Kind == "Var") {
+ var variable = exp.Exp[0].Variable;
+ return isSafeVariable(entry, variable);
+ }
+ if (isZero(exp))
+ return true;
+ return false;
+}
+
+function getEdgeSafeArguments(entry, edge, callee)
+{
+ assert(edge.Kind == "Call");
+ var res = [];
+ if ("PEdgeCallInstance" in edge) {
+ if (isEdgeSafeArgument(entry, edge.PEdgeCallInstance.Exp))
+ res.push(0);
+ }
+ if ("PEdgeCallArguments" in edge) {
+ var args = edge.PEdgeCallArguments.Exp;
+ for (var i = 0; i < args.length; i++) {
+ if (isEdgeSafeArgument(entry, args[i]))
+ res.push(i + 1);
+ }
+ }
+ return res;
+}
+
+function singleAssignment(name)
+{
+ if (name in assignments) {
+ var edges = assignments[name];
+ if (edges.length == 1)
+ return edges[0];
+ }
+ return null;
+}
+
+function expressionValueEdge(exp) {
+ if (!(exp.Kind == "Var" && exp.Variable.Kind == "Temp"))
+ return null;
+ const assign = singleAssignment(variableName(exp.Variable));
+ if (!assign)
+ return null;
+ const [body, edge] = assign;
+ return edge;
+}
+
+// Examples:
+//
+// void foo(type* aSafe) {
+// type* safeBecauseNew = new type(...);
+// type* unsafeBecauseMultipleAssignments = new type(...);
+// if (rand())
+// unsafeBecauseMultipleAssignments = bar();
+// type* safeBecauseSingleAssignmentOfSafe = aSafe;
+// }
+//
+function isSafeVariable(entry, variable)
+{
+ var index = safeArgumentIndex(variable);
+ if (index >= 0)
+ return entry.isSafeArgument(index);
+
+ if (variable.Kind != "Temp" && variable.Kind != "Local")
+ return false;
+ var name = variableName(variable);
+
+ if (!entry.safeLocals)
+ entry.safeLocals = new Map;
+ if (entry.safeLocals.has(name))
+ return entry.safeLocals.get(name);
+
+ const safe = isSafeLocalVariable(entry, name);
+ entry.safeLocals.set(name, safe);
+ return safe;
+}
+
+function isSafeLocalVariable(entry, name)
+{
+ // If there is a single place where this variable has been assigned on
+ // edges we are considering, look at that edge.
+ var assign = singleAssignment(name);
+ if (assign) {
+ const [body, edge] = assign;
+
+ // Treat temporary pointers to DebugOnly contents as thread local.
+ if (isDirectCall(edge, /DebugOnly.*?::operator/))
+ return true;
+
+ // Treat heap allocated pointers as thread local during construction.
+ // Hopefully the construction code doesn't leak pointers to the object
+ // to places where other threads might access it.
+ if (isDirectCall(edge, /operator new/) ||
+ isDirectCall(edge, /nsCSSValue::Array::Create/))
+ {
+ return true;
+ }
+
+ if ("PEdgeCallInstance" in edge) {
+ // References to the contents of an array are threadsafe if the array
+ // itself is threadsafe.
+ if ((isDirectCall(edge, /operator\[\]/) ||
+ isDirectCall(edge, /nsTArray.*?::InsertElementAt\b/) ||
+ isDirectCall(edge, /nsStyleContent::ContentAt/) ||
+ isDirectCall(edge, /nsTArray_base.*?::GetAutoArrayBuffer\b/)) &&
+ isEdgeSafeArgument(entry, edge.PEdgeCallInstance.Exp))
+ {
+ return true;
+ }
+
+ // Watch for the coerced result of a getter_AddRefs or getter_Copies call.
+ if (isDirectCall(edge, /operator /)) {
+ var otherEdge = expressionValueEdge(edge.PEdgeCallInstance.Exp);
+ if (otherEdge &&
+ isDirectCall(otherEdge, /getter_(?:AddRefs|Copies)/) &&
+ isEdgeSafeArgument(entry, otherEdge.PEdgeCallArguments.Exp[0]))
+ {
+ return true;
+ }
+ }
+
+ // RefPtr::operator->() and operator* transmit the safety of the
+ // RefPtr to the return value.
+ if (isDirectCall(edge, /RefPtr<.*?>::operator(->|\*)\(\)/) &&
+ isEdgeSafeArgument(entry, edge.PEdgeCallInstance.Exp))
+ {
+ return true;
+ }
+
+ // Placement-new returns a pointer that is as safe as the pointer
+ // passed to it. Exp[0] is the size, Exp[1] is the pointer/address.
+ // Note that the invocation of the constructor is a separate call,
+ // and so need not be considered here.
+ if (isDirectCall(edge, /operator new/) &&
+ edge.PEdgeCallInstance.Exp.length == 2 &&
+ isEdgeSafeArgument(entry, edge.PEdgeCallInstance.Exp[1]))
+ {
+ return true;
+ }
+
+ // Coercion via AsAString preserves safety.
+ if (isDirectCall(edge, /AsAString/) &&
+ isEdgeSafeArgument(entry, edge.PEdgeCallInstance.Exp))
+ {
+ return true;
+ }
+
+ // Special case:
+ //
+ // keyframe->mTimingFunction.emplace()
+ // keyframe->mTimingFunction->Init()
+ //
+ // The object calling Init should be considered safe here because
+ // we just emplaced it, though in general keyframe::operator->
+ // could do something crazy.
+ if (isDirectCall(edge, /operator->/)) do {
+ const predges = getPredecessors(body)[edge.Index[0]];
+ if (!predges || predges.length != 1)
+ break;
+ const predge = predges[0];
+ if (!isDirectCall(predge, /\bemplace\b/))
+ break;
+ const instance = predge.PEdgeCallInstance;
+ if (JSON.stringify(instance) == JSON.stringify(edge.PEdgeCallInstance))
+ return true;
+ } while (false);
+ }
+
+ if (isSafeAssignment(entry, edge, name))
+ return true;
+
+ // Watch out for variables which were assigned arguments.
+ var rhsVariable = variableAssignRhs(edge);
+ if (rhsVariable)
+ return isSafeVariable(entry, rhsVariable);
+ }
+
+ // When temporary stack structures are created (either to return or to call
+ // methods on without assigning them a name), the generated sixgill JSON is
+ // rather strange. The temporary has structure type and is never assigned
+ // to, but is dereferenced. GCC is probably not showing us everything it is
+ // doing to compile this code. Pattern match for this case here.
+
+ // The variable should have structure type.
+ var type = null;
+ for (var defvar of currentBody.DefineVariable) {
+ if (variableName(defvar.Variable) == name) {
+ type = defvar.Type;
+ break;
+ }
+ }
+ if (!type || type.Kind != "CSU")
+ return false;
+
+ // The variable should not have been written to anywhere up to this point.
+ // If it is initialized at this point we should have seen *some* write
+ // already, since the CFG edges are visited in reverse post order.
+ if (name in assignments)
+ return false;
+
+ return true;
+}
+
+function isSafeMemberPointer(containerType, memberName, memberType)
+{
+ // nsTArray owns its header.
+ if (containerType.includes("nsTArray_base") && memberName == "mHdr")
+ return true;
+
+ if (memberType.Kind != 'Pointer')
+ return false;
+
+ // Special-cases go here :)
+ return false;
+}
+
+// Return whether 'exp == value' holds only when execution is on the main thread.
+function testFailsOffMainThread(exp, value) {
+ switch (exp.Kind) {
+ case "Drf":
+ var edge = expressionValueEdge(exp.Exp[0]);
+ if (edge) {
+ if (isDirectCall(edge, /NS_IsMainThread/) && value)
+ return true;
+ if (isDirectCall(edge, /IsInServoTraversal/) && !value)
+ return true;
+ if (isDirectCall(edge, /IsCurrentThreadInServoTraversal/) && !value)
+ return true;
+ if (isDirectCall(edge, /__builtin_expect/))
+ return testFailsOffMainThread(edge.PEdgeCallArguments.Exp[0], value);
+ if (edge.Kind == "Assign")
+ return testFailsOffMainThread(edge.Exp[1], value);
+ }
+ break;
+ case "Unop":
+ if (exp.OpCode == "LogicalNot")
+ return testFailsOffMainThread(exp.Exp[0], !value);
+ break;
+ case "Binop":
+ if (exp.OpCode == "NotEqual" || exp.OpCode == "Equal") {
+ var cmpExp = isZero(exp.Exp[0])
+ ? exp.Exp[1]
+ : (isZero(exp.Exp[1]) ? exp.Exp[0] : null);
+ if (cmpExp)
+ return testFailsOffMainThread(cmpExp, exp.OpCode == "NotEqual" ? value : !value);
+ }
+ break;
+ case "Int":
+ if (exp.String == "0" && value)
+ return true;
+ if (exp.String == "1" && !value)
+ return true;
+ break;
+ }
+ return false;
+}
diff --git a/js/src/devtools/rootAnalysis/analyzeRoots.js b/js/src/devtools/rootAnalysis/analyzeRoots.js
new file mode 100644
index 0000000000..46bc7ea1fb
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/analyzeRoots.js
@@ -0,0 +1,963 @@
+/* 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/. */
+
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+
+"use strict";
+
+loadRelativeToScript('utility.js');
+loadRelativeToScript('annotations.js');
+loadRelativeToScript('callgraph.js');
+loadRelativeToScript('CFG.js');
+loadRelativeToScript('dumpCFG.js');
+
+var sourceRoot = (os.getenv('SOURCE') || '') + '/';
+
+var functionName;
+var functionBodies;
+
+try {
+ var options = parse_options([
+ {
+ name: "--function",
+ type: 'string',
+ },
+ {
+ name: "-f",
+ type: "string",
+ dest: "function",
+ },
+ {
+ name: "gcFunctions",
+ default: "gcFunctions.lst"
+ },
+ {
+ name: "limitedFunctions",
+ default: "limitedFunctions.lst"
+ },
+ {
+ name: "gcTypes",
+ default: "gcTypes.txt"
+ },
+ {
+ name: "typeInfo",
+ default: "typeInfo.txt"
+ },
+ {
+ name: "batch",
+ type: "number",
+ default: 1
+ },
+ {
+ name: "numBatches",
+ type: "number",
+ default: 1
+ },
+ {
+ name: "tmpfile",
+ default: "tmp.txt"
+ },
+ ]);
+} catch (e) {
+ printErr(e);
+ printErr("Usage: analyzeRoots.js [-f function_name] <gcFunctions.lst> <limitedFunctions.lst> <gcTypes.txt> <typeInfo.txt> [start end [tmpfile]]");
+ quit(1);
+}
+var gcFunctions = {};
+var text = snarf(options.gcFunctions).split("\n");
+assert(text.pop().length == 0);
+for (const line of text)
+ gcFunctions[mangled(line)] = readable(line);
+
+var limitedFunctions = JSON.parse(snarf(options.limitedFunctions));
+text = null;
+
+var typeInfo = loadTypeInfo(options.typeInfo);
+
+var match;
+var gcThings = new Set();
+var gcPointers = new Set();
+var gcRefs = new Set(typeInfo.GCRefs);
+
+text = snarf(options.gcTypes).split("\n");
+for (var line of text) {
+ if (match = /^GCThing: (.*)/.exec(line))
+ gcThings.add(match[1]);
+ if (match = /^GCPointer: (.*)/.exec(line))
+ gcPointers.add(match[1]);
+}
+text = null;
+
+function isGCRef(type)
+{
+ if (type.Kind == "CSU")
+ return gcRefs.has(type.Name);
+ return false;
+}
+
+function isGCType(type)
+{
+ if (type.Kind == "CSU")
+ return gcThings.has(type.Name);
+ else if (type.Kind == "Array")
+ return isGCType(type.Type);
+ return false;
+}
+
+function isUnrootedPointerDeclType(decl)
+{
+ // Treat non-temporary T& references as if they were the underlying type T.
+ // For now, restrict this to only the types specifically annotated with JS_HAZ_GC_REF
+ // to avoid lots of false positives with other types.
+ let type = isReferenceDecl(decl) && isGCRef(decl.Type.Type) ? decl.Type.Type : decl.Type;
+
+ while (type.Kind == "Array") {
+ type = type.Type;
+ }
+
+ if (type.Kind == "Pointer") {
+ return isGCType(type.Type);
+ } else if (type.Kind == "CSU") {
+ return gcPointers.has(type.Name);
+ } else {
+ return false;
+ }
+}
+
+function edgeCanGC(functionName, body, edge, scopeAttrs, functionBodies)
+{
+ if (edge.Kind != "Call") {
+ return false;
+ }
+
+ for (const { callee, attrs } of getCallees(body, edge, scopeAttrs, functionBodies)) {
+ if (attrs & (ATTR_GC_SUPPRESSED | ATTR_REPLACED)) {
+ continue;
+ }
+
+ if (callee.kind == "direct") {
+ const func = mangled(callee.name);
+ if ((func in gcFunctions) || ((func + internalMarker) in gcFunctions))
+ return `'${func}$${gcFunctions[func]}'`;
+ return false;
+ } else if (callee.kind == "indirect") {
+ if (!indirectCallCannotGC(functionName, callee.variable)) {
+ return "'*" + callee.variable + "'";
+ }
+ } else if (callee.kind == "field") {
+ if (fieldCallCannotGC(callee.staticCSU, callee.field)) {
+ continue;
+ }
+ const fieldkey = callee.fieldKey;
+ if (fieldkey in gcFunctions) {
+ return `'${fieldkey}'`;
+ }
+ } else {
+ return "<unknown>";
+ }
+ }
+
+ return false;
+}
+
+// Search upwards through a function's control flow graph (CFG) to find a path containing:
+//
+// - a use of a variable, preceded by
+//
+// - a function call that can GC, preceded by
+//
+// - a use of the variable that shows that the live range starts at least that
+// far back, preceded by
+//
+// - an informative use of the variable (which might be the same use), one that
+// assigns to it a value that might contain a GC pointer (or is the start of
+// the function for parameters or 'this'.) This is not necessary for
+// correctness, it just makes it easier to understand why something might be
+// a hazard. The output of the analysis will include the whole path from the
+// informative use to the post-GC use, to make the problem as understandable
+// as possible.
+//
+// A canonical example might be:
+//
+// void foo() {
+// JS::Value* val = lookupValue(); <-- informative use
+// if (!val.isUndefined()) { <-- any use
+// GC(); <-- GC call
+// }
+// putValue(val); <-- a use after a GC
+// }
+//
+// The search is performed on an underlying CFG that we traverse in
+// breadth-first order (to find the shortest path). We build a path starting
+// from an empty path and conditionally lengthening and improving it according
+// to the computation occurring on each incoming edge. (If that path so far
+// does not have a GC call and we traverse an edge with a GC call, then we
+// lengthen the path by that edge and record it as including a GC call.) The
+// resulting path may include a point or edge more than once! For example, in:
+//
+// void foo(JS::Value val) {
+// for (int i = 0; i < N; i++) {
+// GC();
+// val = processValue(val);
+// }
+// }
+//
+// the path would start at the point after processValue(), go through the GC(),
+// then back to the processValue() (for the call in the previous loop
+// iteration).
+//
+// While searching, each point is annotated with a path node corresponding to
+// the best path found to that node so far. When a later search ends up at the
+// same point, the best path node is kept. (But the path that it heads may
+// include an earlier path node for the same point, as in the case above.)
+//
+// What info we want depends on whether the variable turns out to be live
+// across a GC call. We are looking for both hazards (unrooted variables live
+// across GC calls) and unnecessary roots (rooted variables that have no GC
+// calls in their live ranges.)
+//
+// If not:
+//
+// - 'minimumUse': the earliest point in each body that uses the variable, for
+// reporting on unnecessary roots.
+//
+// If so:
+//
+// - 'successor': a path from the GC call to a use of the variable after the GC
+// call, chained through 'successor' field in the returned edge descriptor
+//
+// - 'gcInfo': a direct pointer to the GC call edge
+//
+function findGCBeforeValueUse(start_body, start_point, funcAttrs, variable)
+{
+ // Scan through all edges preceding an unrooted variable use, using an
+ // explicit worklist, looking for a GC call and a preceding point where the
+ // variable is known to be live. A worklist contains an incoming edge
+ // together with a description of where it or one of its successors GC'd
+ // (if any).
+
+ class Path {
+ get ProgressProperties() { return ["informativeUse", "anyUse", "gcInfo"]; }
+
+ constructor(successor_path, body, ppoint) {
+ Object.assign(this, {body, ppoint});
+ if (successor_path !== undefined) {
+ this.successor = successor_path;
+ for (const prop of this.ProgressProperties) {
+ if (prop in successor_path) {
+ this[prop] = successor_path[prop];
+ }
+ }
+ }
+ }
+
+ toString() {
+ const trail = [];
+ for (let path = this; path.ppoint; path = path.successor) {
+ trail.push(path.ppoint);
+ }
+ return trail.join();
+ }
+
+ // Return -1, 0, or 1 to indicate how complete this Path is compared
+ // to another one.
+ compare(other) {
+ for (const prop of this.ProgressProperties) {
+ const a = this.hasOwnProperty(prop);
+ const b = other.hasOwnProperty(prop);
+ if (a != b) {
+ return a - b;
+ }
+ }
+ return 0;
+ }
+ };
+
+ // In case we never find an informative use, keep track of the best path
+ // found with any use.
+ let bestPathWithAnyUse = null;
+
+ const visitor = new class extends Visitor {
+ constructor() {
+ super(functionBodies);
+ }
+
+ // Do a BFS upwards through the CFG, starting from a use of the
+ // variable and searching for a path containing a GC followed by an
+ // initializing use of the variable (or, in forward direction, a start
+ // of the variable's live range, a GC within that live range, and then
+ // a use showing that the live range extends past the GC call.)
+ // Actually, possibly two uses: any use at all, and then if available
+ // an "informative" use that is more convincing (they may be the same).
+ //
+ // The CFG is a graph (a 'body' here is acyclic, but they can contain
+ // loop nodes that bridge to additional bodies for the loop, so the
+ // overall graph can by cyclic.) That means there may be multiple paths
+ // from point A to point B, and we want paths with a GC on them. This
+ // can be thought of as searching for a "maximal GCing" path from a use
+ // A to an initialization B.
+ //
+ // This is implemented as a BFS search that when it reaches a point
+ // that has been visited before, stops if and only if the current path
+ // being advanced is a less GC-ful path. The traversal pushes a
+ // `gcInfo` token, initially empty, up through the graph and stores the
+ // maximal one visited so far at every point.
+ //
+ // Note that this means we may traverse through the same point more
+ // than once, and so in theory this scan is superlinear -- if you visit
+ // every point twice, once for a non GC path and once for a GC path, it
+ // would be 2^n. But that is unlikely to matter, since you'd need lots
+ // of split/join pairs that GC on one side and not the other, and you'd
+ // have to visit them in an unlucky order. This could be fixed by
+ // updating the gcInfo for past points in a path when a GC is found,
+ // but it hasn't been found to matter in practice yet.
+
+ next_action(prev, current) {
+ // Continue if first visit, or the new path is more complete than the old path. This
+ // could be enhanced at some point to choose paths with 'better'
+ // examples of GC (eg a call that invokes GC through concrete functions rather than going through a function pointer that is conservatively assumed to GC.)
+
+ if (!current) {
+ // This search path has been terminated.
+ return "prune";
+ }
+
+ if (current.informativeUse) {
+ // We have a path with an informative use leading to a GC
+ // leading to the starting point.
+ assert(current.gcInfo);
+ return "done";
+ }
+
+ if (prev === undefined) {
+ // first visit
+ return "continue";
+ }
+
+ if (!prev.gcInfo && current.gcInfo) {
+ // More GC.
+ return "continue";
+ } else {
+ return "prune";
+ }
+ }
+
+ merge_info(prev, current) {
+ // Keep the most complete path.
+
+ if (!prev || !current) {
+ return prev || current;
+ }
+
+ // Tie goes to the first found, since it will be shorter when doing a BFS-like search.
+ return prev.compare(current) >= 0 ? prev : current;
+ }
+
+ extend_path(edge, body, ppoint, successor_path) {
+ // Clone the successor path node and then tack on the new point. Other values
+ // will be updated during the rest of this function, according to what is
+ // happening on the edge.
+ const path = new Path(successor_path, body, ppoint);
+ if (edge === null) {
+ // Artificial edge to connect loops to their surrounding nodes in the outer body.
+ // Does not influence "completeness" of path.
+ return path;
+ }
+
+ assert(ppoint == edge.Index[0]);
+
+ if (edgeEndsValueLiveRange(edge, variable, body)) {
+ // Terminate the search through this point.
+ return null;
+ }
+
+ const edge_starts = edgeStartsValueLiveRange(edge, variable);
+ const edge_uses = edgeUsesVariable(edge, variable, body);
+
+ if (edge_starts || edge_uses) {
+ if (!body.minimumUse || ppoint < body.minimumUse)
+ body.minimumUse = ppoint;
+ }
+
+ if (edge_starts) {
+ // This is a beginning of the variable's live range. If we can
+ // reach a GC call from here, then we're done -- we have a path
+ // from the beginning of the live range, through the GC call, to a
+ // use after the GC call that proves its live range extends at
+ // least that far.
+ if (path.gcInfo) {
+ path.anyUse = path.anyUse || edge;
+ path.informativeUse = path.informativeUse || edge;
+ return path;
+ }
+
+ // Otherwise, truncate this particular branch of the search at this
+ // edge -- there is no GC after this use, and traversing the edge
+ // would lead to a different live range.
+ return null;
+ }
+
+ // The value is live across this edge. Check whether this edge can
+ // GC (if we don't have a GC yet on this path.)
+ const had_gcInfo = Boolean(path.gcInfo);
+ const edgeAttrs = body.attrs[ppoint] | funcAttrs;
+ if (!path.gcInfo && !(edgeAttrs & (ATTR_GC_SUPPRESSED | ATTR_REPLACED))) {
+ var gcName = edgeCanGC(functionName, body, edge, edgeAttrs, functionBodies);
+ if (gcName) {
+ path.gcInfo = {name:gcName, body, ppoint, edge: edge.Index};
+ }
+ }
+
+ // Beginning of function?
+ if (ppoint == body.Index[0] && body.BlockId.Kind != "Loop") {
+ if (path.gcInfo && (variable.Kind == "Arg" || variable.Kind == "This")) {
+ // The scope of arguments starts at the beginning of the
+ // function.
+ path.anyUse = path.informativeUse = true;
+ }
+
+ if (path.anyUse) {
+ // We know the variable was live across the GC. We may or
+ // may not have found an "informative" explanation
+ // beginning of the live range. (This can happen if the
+ // live range started when a variable is used as a
+ // retparam.)
+ return path;
+ }
+ }
+
+ if (!path.gcInfo) {
+ // We haven't reached a GC yet, so don't start looking for uses.
+ return path;
+ }
+
+ if (!edge_uses) {
+ // We have a GC. If this edge doesn't use the value, then there
+ // is no change to the completeness of the path.
+ return path;
+ }
+
+ // The live range starts at least this far back, so we're done for
+ // the same reason as with edge_starts. The only difference is that
+ // a GC on this edge indicates a hazard, whereas if we're killing a
+ // live range in the GC call then it's not live *across* the call.
+ //
+ // However, we may want to generate a longer usage chain for the
+ // variable than is minimally necessary. For example, consider:
+ //
+ // Value v = f();
+ // if (v.isUndefined())
+ // return false;
+ // gc();
+ // return v;
+ //
+ // The call to .isUndefined() is considered to be a use and
+ // therefore indicates that v must be live at that point. But it's
+ // more helpful to the user to continue the 'successor' path to
+ // include the ancestor where the value was generated. So we will
+ // only stop here if edge.Kind is Assign; otherwise, we'll pass a
+ // "preGCLive" value up through the worklist to remember that the
+ // variable *is* alive before the GC and so this function should be
+ // returning a true value even if we don't find an assignment.
+
+ // One special case: if the use of the variable is on the
+ // destination part of the edge (which currently only happens for
+ // the return value and a terminal edge in the body), and this edge
+ // is also GCing, then that usage happens *after* the GC and so
+ // should not be used for anyUse or informativeUse. This matters
+ // for a hazard involving a destructor GC'ing after an immobile
+ // return value has been assigned:
+ //
+ // GCInDestructor guard(cx);
+ // if (cond()) {
+ // return nullptr;
+ // }
+ //
+ // which boils down to
+ //
+ // p1 --(construct guard)-->
+ // p2 --(call cond)-->
+ // p3 --(returnval := nullptr) -->
+ // p4 --(destruct guard, possibly GCing)-->
+ // p5
+ //
+ // The return value is considered to be live at p5. The live range
+ // of the return value would ordinarily be from p3->p4->p5, except
+ // that the nullptr assignment means it needn't be considered live
+ // back that far, and so the live range is *just* p5. The GC on the
+ // 4->5 edge happens just before that range, so the value was not
+ // live across the GC.
+ //
+ if (!had_gcInfo && edge_uses == edge.Index[1]) {
+ return path; // New GC does not cross this variable use.
+ }
+
+ path.anyUse = path.anyUse || edge;
+ bestPathWithAnyUse = bestPathWithAnyUse || path;
+ if (edge.Kind == 'Assign') {
+ path.informativeUse = edge; // Done! Setting this terminates the search.
+ }
+
+ return path;
+ };
+ };
+
+ const result = BFS_upwards(start_body, start_point, functionBodies, visitor, new Path());
+ if (result && result.gcInfo && result.anyUse) {
+ return result;
+ } else {
+ return bestPathWithAnyUse;
+ }
+}
+
+function variableLiveAcrossGC(funcAttrs, variable, liveToEnd=false)
+{
+ // A variable is live across a GC if (1) it is used by an edge (as in, it
+ // was at least initialized), and (2) it is used after a GC in a successor
+ // edge.
+
+ for (var body of functionBodies)
+ body.minimumUse = 0;
+
+ for (var body of functionBodies) {
+ if (!("PEdge" in body))
+ continue;
+ for (var edge of body.PEdge) {
+ // Examples:
+ //
+ // JSObject* obj = NewObject();
+ // cangc();
+ // obj = NewObject(); <-- mentions 'obj' but kills previous value
+ //
+ // This is not a hazard. Contrast this with:
+ //
+ // JSObject* obj = NewObject();
+ // cangc();
+ // obj = LookAt(obj); <-- uses 'obj' and kills previous value
+ //
+ // This is a hazard; the initial value of obj is live across
+ // cangc(). And a third possibility:
+ //
+ // JSObject* obj = NewObject();
+ // obj = CopyObject(obj);
+ //
+ // This is not a hazard, because even though CopyObject can GC, obj
+ // is not live across it. (obj is live before CopyObject, and
+ // probably after, but not across.) There may be a hazard within
+ // CopyObject, of course.
+ //
+
+ // Ignore uses that are just invalidating the previous value.
+ if (edgeEndsValueLiveRange(edge, variable, body))
+ continue;
+
+ var usePoint = edgeUsesVariable(edge, variable, body, liveToEnd);
+ if (usePoint) {
+ var call = findGCBeforeValueUse(body, usePoint, funcAttrs, variable);
+ if (!call)
+ continue;
+
+ call.afterGCUse = usePoint;
+ return call;
+ }
+ }
+ }
+ return null;
+}
+
+// An unrooted variable has its address stored in another variable via
+// assignment, or passed into a function that can GC. If the address is
+// assigned into some other variable, we can't track it to see if it is held
+// live across a GC. If it is passed into a function that can GC, then it's
+// sort of like a Handle to an unrooted location, and the callee could GC
+// before overwriting it or rooting it.
+function unsafeVariableAddressTaken(funcAttrs, variable)
+{
+ for (var body of functionBodies) {
+ if (!("PEdge" in body))
+ continue;
+ for (var edge of body.PEdge) {
+ if (edgeTakesVariableAddress(edge, variable, body)) {
+ if (funcAttrs & (ATTR_GC_SUPPRESSED | ATTR_REPLACED)) {
+ continue;
+ }
+ if (edge.Kind == "Assign" || edgeCanGC(functionName, body, edge, funcAttrs, functionBodies)) {
+ return {body:body, ppoint:edge.Index[0]};
+ }
+ }
+ }
+ }
+ return null;
+}
+
+// Read out the brief (non-JSON, semi-human-readable) CFG description for the
+// given function and store it.
+function loadPrintedLines(functionName)
+{
+ assert(!os.system("xdbfind src_body.xdb '" + functionName + "' > " + options.tmpfile));
+ var lines = snarf(options.tmpfile).split('\n');
+
+ for (var body of functionBodies)
+ body.lines = [];
+
+ // Distribute lines of output to the block they originate from.
+ var currentBody = null;
+ for (var line of lines) {
+ if (/^block:/.test(line)) {
+ if (match = /:(loop#[\d#]+)/.exec(line)) {
+ var loop = match[1];
+ var found = false;
+ for (var body of functionBodies) {
+ if (body.BlockId.Kind == "Loop" && body.BlockId.Loop == loop) {
+ assert(!found);
+ found = true;
+ currentBody = body;
+ }
+ }
+ assert(found);
+ } else {
+ for (var body of functionBodies) {
+ if (body.BlockId.Kind == "Function")
+ currentBody = body;
+ }
+ }
+ }
+ if (currentBody)
+ currentBody.lines.push(line);
+ }
+}
+
+function findLocation(body, ppoint, opts={brief: false})
+{
+ var location = body.PPoint[ppoint ? ppoint - 1 : 0].Location;
+ var file = location.CacheString;
+
+ if (file.indexOf(sourceRoot) == 0)
+ file = file.substring(sourceRoot.length);
+
+ if (opts.brief) {
+ var m = /.*\/(.*)/.exec(file);
+ if (m)
+ file = m[1];
+ }
+
+ return file + ":" + location.Line;
+}
+
+function locationLine(text)
+{
+ if (match = /:(\d+)$/.exec(text))
+ return match[1];
+ return 0;
+}
+
+function getEntryTrace(functionName, entry)
+{
+ const trace = [];
+
+ var gcPoint = entry.gcInfo ? entry.gcInfo.ppoint : 0;
+
+ if (!functionBodies[0].lines)
+ loadPrintedLines(functionName);
+
+ while (entry.successor) {
+ var ppoint = entry.ppoint;
+ var lineText = findLocation(entry.body, ppoint, {"brief": true});
+
+ var edgeText = "";
+ if (entry.successor && entry.successor.body == entry.body) {
+ // If the next point in the trace is in the same block, look for an
+ // edge between them.
+ var next = entry.successor.ppoint;
+
+ if (!entry.body.edgeTable) {
+ var table = {};
+ entry.body.edgeTable = table;
+ for (var line of entry.body.lines) {
+ if (match = /^\w+\((\d+,\d+),/.exec(line))
+ table[match[1]] = line; // May be multiple?
+ }
+ if (entry.body.BlockId.Kind == 'Loop') {
+ const [startPoint, endPoint] = entry.body.Index;
+ table[`${endPoint},${startPoint}`] = '(loop to next iteration)';
+ }
+ }
+
+ edgeText = entry.body.edgeTable[ppoint + "," + next];
+ assert(edgeText);
+ if (ppoint == gcPoint)
+ edgeText += " [[GC call]]";
+ } else {
+ // Look for any outgoing edge from the chosen point.
+ for (var line of entry.body.lines) {
+ if (match = /\((\d+),/.exec(line)) {
+ if (match[1] == ppoint) {
+ edgeText = line;
+ break;
+ }
+ }
+ }
+ if (ppoint == entry.body.Index[1] && entry.body.BlockId.Kind == "Function")
+ edgeText += " [[end of function]]";
+ }
+
+ // TODO: Store this in a more structured form for better markup, and perhaps
+ // linking to line numbers.
+ trace.push({lineText, edgeText});
+ entry = entry.successor;
+ }
+
+ return trace;
+}
+
+function isRootedDeclType(decl)
+{
+ // Treat non-temporary T& references as if they were the underlying type T.
+ const type = isReferenceDecl(decl) ? decl.Type.Type : decl.Type;
+ return type.Kind == "CSU" && ((type.Name in typeInfo.RootedPointers) ||
+ (type.Name in typeInfo.RootedGCThings));
+}
+
+function printRecord(record) {
+ print(JSON.stringify(record));
+}
+
+function processBodies(functionName, wholeBodyAttrs)
+{
+ if (!("DefineVariable" in functionBodies[0]))
+ return;
+ const funcInfo = limitedFunctions[mangled(functionName)] || { attributes: 0 };
+ const funcAttrs = funcInfo.attributes | wholeBodyAttrs;
+
+ // Look for the JS_EXPECT_HAZARDS annotation, so as to output a different
+ // message in that case that won't be counted as a hazard.
+ var annotations = new Set();
+ for (const variable of functionBodies[0].DefineVariable) {
+ if (variable.Variable.Kind == "Func" && variable.Variable.Name[0] == functionName) {
+ for (const { Name: [tag, value] } of (variable.Type.Annotation || [])) {
+ if (tag == 'annotate')
+ annotations.add(value);
+ }
+ }
+ }
+
+ let missingExpectedHazard = annotations.has("Expect Hazards");
+
+ // Awful special case, hopefully temporary:
+ //
+ // The DOM bindings code generator uses "holders" to externally root
+ // variables. So for example:
+ //
+ // StringObjectRecordOrLong arg0;
+ // StringObjectRecordOrLongArgument arg0_holder(arg0);
+ // arg0_holder.TrySetToStringObjectRecord(cx, args[0]);
+ // GC();
+ // self->PassUnion22(cx, arg0);
+ //
+ // This appears to be a rooting hazard on arg0, but it is rooted by
+ // arg0_holder if you set it to any of its union types that requires
+ // rooting.
+ //
+ // Additionally, the holder may be reported as a hazard because it's not
+ // itself a Rooted or a subclass of AutoRooter; it contains a
+ // Maybe<RecordRooter<T>> that will get emplaced if rooting is required.
+ //
+ // Hopefully these will be simplified at some point (see bug 1517829), but
+ // for now we special-case functions in the mozilla::dom namespace that
+ // contain locals with types ending in "Argument". Or
+ // Maybe<SomethingArgument>. Or Maybe<SpiderMonkeyInterfaceRooter<T>>. It's
+ // a harsh world.
+ const ignoreVars = new Set();
+ if (functionName.match(/mozilla::dom::/)) {
+ const vars = functionBodies[0].DefineVariable.filter(
+ v => v.Type.Kind == 'CSU' && v.Variable.Kind == 'Local'
+ ).map(
+ v => [ v.Variable.Name[0], v.Type.Name ]
+ );
+
+ const holders = vars.filter(
+ ([n, t]) => n.match(/^arg\d+_holder$/) &&
+ (t.includes("Argument") || t.includes("Rooter")));
+ for (const [holder,] of holders) {
+ ignoreVars.add(holder); // Ignore the holder.
+ ignoreVars.add(holder.replace("_holder", "")); // Ignore the "managed" arg.
+ }
+ }
+
+ const [mangledSymbol, readable] = splitFunction(functionName);
+
+ for (let decl of functionBodies[0].DefineVariable) {
+ var name;
+ if (decl.Variable.Kind == "This")
+ name = "this";
+ else if (decl.Variable.Kind == "Return")
+ name = "<returnvalue>";
+ else
+ name = decl.Variable.Name[0];
+
+ if (ignoreVars.has(name))
+ continue;
+
+ let liveToEnd = false;
+ if (decl.Variable.Kind == "Arg" && isReferenceDecl(decl) && decl.Type.Reference == 2) {
+ // References won't run destructors, so they would normally not be
+ // considered live at the end of the function. In order to handle
+ // the pattern of moving a GC-unsafe value into a function (eg an
+ // AutoCheckCannotGC&&), assume all argument rvalue references live to the
+ // end of the function unless their liveness is terminated by
+ // calling reset() or moving them into another function call.
+ liveToEnd = true;
+ }
+
+ if (isRootedDeclType(decl)) {
+ if (!variableLiveAcrossGC(funcAttrs, decl.Variable)) {
+ // The earliest use of the variable should be its constructor.
+ var lineText;
+ for (var body of functionBodies) {
+ if (body.minimumUse) {
+ var text = findLocation(body, body.minimumUse);
+ if (!lineText || locationLine(lineText) > locationLine(text))
+ lineText = text;
+ }
+ }
+ const record = {
+ record: "unnecessary",
+ functionName,
+ mangled: mangledSymbol,
+ readable,
+ variable: name,
+ type: str_Type(decl.Type),
+ loc: lineText || "???",
+ }
+ print(",");
+ printRecord(record);
+ }
+ } else if (isUnrootedPointerDeclType(decl)) {
+ var result = variableLiveAcrossGC(funcAttrs, decl.Variable, liveToEnd);
+ if (result) {
+ assert(result.gcInfo);
+ const edge = result.gcInfo.edge;
+ const body = result.gcInfo.body;
+ const lineText = findLocation(body, result.gcInfo.ppoint);
+ const makeLoc = l => [l.Location.CacheString, l.Location.Line];
+ const range = [makeLoc(body.PPoint[edge[0] - 1]), makeLoc(body.PPoint[edge[1] - 1])];
+ const record = {
+ record: "unrooted",
+ expected: annotations.has("Expect Hazards"),
+ functionName,
+ mangled: mangledSymbol,
+ readable,
+ variable: name,
+ type: str_Type(decl.Type),
+ gccall: result.gcInfo.name.replaceAll("'", ""),
+ gcrange: range,
+ loc: lineText,
+ trace: getEntryTrace(functionName, result),
+ };
+ missingExpectedHazard = false;
+ print(",");
+ printRecord(record);
+ }
+ result = unsafeVariableAddressTaken(funcAttrs, decl.Variable);
+ if (result) {
+ var lineText = findLocation(result.body, result.ppoint);
+ const record = {
+ record: "address",
+ functionName,
+ mangled: mangledSymbol,
+ readable,
+ variable: name,
+ loc: lineText,
+ trace: getEntryTrace(functionName, {body:result.body, ppoint:result.ppoint}),
+ };
+ print(",");
+ printRecord(record);
+ }
+ }
+ }
+
+ if (missingExpectedHazard) {
+ const {
+ Location: [
+ { CacheString: startfile, Line: startline },
+ { CacheString: endfile, Line: endline }
+ ]
+ } = functionBodies[0];
+
+ const loc = (startfile == endfile) ? `${startfile}:${startline}-${endline}`
+ : `${startfile}:${startline}`;
+
+ const record = {
+ record: "missing",
+ functionName,
+ mangled: mangledSymbol,
+ readable,
+ loc,
+ }
+ print(",");
+ printRecord(record);
+ }
+}
+
+print("[\n");
+var now = new Date();
+printRecord({record: "time", iso: "" + now, t: now.getTime()});
+
+var xdb = xdbLibrary();
+xdb.open("src_body.xdb");
+
+var minStream = xdb.min_data_stream()|0;
+var maxStream = xdb.max_data_stream()|0;
+
+var start = batchStart(options.batch, options.numBatches, minStream, maxStream);
+var end = batchLast(options.batch, options.numBatches, minStream, maxStream);
+
+function process(name, json) {
+ functionName = name;
+ functionBodies = JSON.parse(json);
+
+ // Annotate body with a table of all points within the body that may be in
+ // a limited scope (eg within the scope of a GC suppression RAII class.)
+ // body.attrs is a plain object indexed by point, with the value being a
+ // bit set stored in an integer.
+ for (var body of functionBodies)
+ body.attrs = [];
+
+ for (var body of functionBodies) {
+ for (var [pbody, id, attrs] of allRAIIGuardedCallPoints(typeInfo, functionBodies, body, isLimitConstructor))
+ {
+ if (attrs)
+ pbody.attrs[id] = attrs;
+ }
+ }
+
+ processBodies(functionName);
+}
+
+if (options.function) {
+ var data = xdb.read_entry(options.function);
+ var json = data.readString();
+ debugger;
+ process(options.function, json);
+ xdb.free_string(data);
+ print("\n]\n");
+ quit(0);
+}
+
+for (var nameIndex = start; nameIndex <= end; nameIndex++) {
+ var name = xdb.read_key(nameIndex);
+ var functionName = name.readString();
+ var data = xdb.read_entry(name);
+ xdb.free_string(name);
+ var json = data.readString();
+ try {
+ process(functionName, json);
+ } catch (e) {
+ printErr("Exception caught while handling " + functionName);
+ throw(e);
+ }
+ xdb.free_string(data);
+}
+
+print("\n]\n");
diff --git a/js/src/devtools/rootAnalysis/annotations.js b/js/src/devtools/rootAnalysis/annotations.js
new file mode 100644
index 0000000000..7aedc7edac
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/annotations.js
@@ -0,0 +1,489 @@
+/* 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/. */
+
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+
+"use strict";
+
+// Ignore calls made through these function pointers
+var ignoreIndirectCalls = {
+ "mallocSizeOf" : true,
+ "aMallocSizeOf" : true,
+ "__conv" : true,
+ "__convf" : true,
+ "callback_newtable" : true,
+};
+
+// Types that when constructed with no arguments, are "safe" values (they do
+// not contain GC pointers, or values with nontrivial destructors.)
+var typesWithSafeConstructors = new Set([
+ "mozilla::Maybe",
+ "mozilla::dom::Nullable",
+ "mozilla::dom::Optional",
+ "mozilla::UniquePtr",
+ "js::UniquePtr"
+]);
+
+var resetterMethods = {
+ 'mozilla::Maybe': new Set(["reset"]),
+ 'mozilla::UniquePtr': new Set(["reset"]),
+ 'js::UniquePtr': new Set(["reset"]),
+ 'mozilla::dom::Nullable': new Set(["SetNull"]),
+ 'mozilla::dom::TypedArray_base': new Set(["Reset"]),
+ 'RefPtr': new Set(["forget"]),
+ 'nsCOMPtr': new Set(["forget"]),
+ 'JS::AutoAssertNoGC': new Set(["reset"]),
+};
+
+function isRefcountedDtor(name) {
+ return name.includes("::~RefPtr(") || name.includes("::~nsCOMPtr(");
+}
+
+function indirectCallCannotGC(fullCaller, fullVariable)
+{
+ var caller = readable(fullCaller);
+
+ // This is usually a simple variable name, but sometimes a full name gets
+ // passed through. And sometimes that name is truncated. Examples:
+ // _ZL13gAbortHandler$mozalloc_oom.cpp:void (* gAbortHandler)(size_t)
+ // _ZL14pMutexUnlockFn$umutex.cpp:void (* pMutexUnlockFn)(const void*
+ var name = readable(fullVariable);
+
+ if (name in ignoreIndirectCalls)
+ return true;
+
+ if (name == "mapper" && caller == "ptio.c:pt_MapError")
+ return true;
+
+ if (name == "params" && caller == "PR_ExplodeTime")
+ return true;
+
+ // hook called during script finalization which cannot GC.
+ if (/CallDestroyScriptHook/.test(caller))
+ return true;
+
+ // Call through a 'callback' function pointer, in a place where we're going
+ // to be throwing a JS exception.
+ if (name == "callback" && caller.includes("js::ErrorToException"))
+ return true;
+
+ // The math cache only gets called with non-GC math functions.
+ if (name == "f" && caller.includes("js::MathCache::lookup"))
+ return true;
+
+ // It would probably be better to somehow rewrite PR_CallOnce(foo) into a
+ // call of foo, but for now just assume that nobody is crazy enough to use
+ // PR_CallOnce with a function that can GC.
+ if (name == "func" && caller == "PR_CallOnce")
+ return true;
+
+ return false;
+}
+
+// Ignore calls through functions pointers with these types
+var ignoreClasses = {
+ "JSStringFinalizer" : true,
+ "SprintfState" : true,
+ "SprintfStateStr" : true,
+ "JSLocaleCallbacks" : true,
+ "JSC::ExecutableAllocator" : true,
+ "PRIOMethods": true,
+ "_MD_IOVector" : true,
+ "malloc_table_t": true, // replace_malloc
+ "malloc_hook_table_t": true, // replace_malloc
+ "mozilla::MallocSizeOf": true,
+ "MozMallocSizeOf": true,
+};
+
+// Ignore calls through TYPE.FIELD, where TYPE is the class or struct name containing
+// a function pointer field named FIELD.
+var ignoreCallees = {
+ "js::Class.trace" : true,
+ "js::Class.finalize" : true,
+ "JSClassOps.trace" : true,
+ "JSClassOps.finalize" : true,
+ "JSRuntime.destroyPrincipals" : true,
+ "icu_50::UObject.__deleting_dtor" : true, // destructors in ICU code can't cause GC
+ "mozilla::CycleCollectedJSRuntime.DescribeCustomObjects" : true, // During tracing, cannot GC.
+ "mozilla::CycleCollectedJSRuntime.NoteCustomGCThingXPCOMChildren" : true, // During tracing, cannot GC.
+ "PLDHashTableOps.hashKey" : true,
+ "PLDHashTableOps.clearEntry" : true,
+ "z_stream_s.zfree" : true,
+ "z_stream_s.zalloc" : true,
+ "GrGLInterface.fCallback" : true,
+ "std::strstreambuf._M_alloc_fun" : true,
+ "std::strstreambuf._M_free_fun" : true,
+ "struct js::gc::Callback<void (*)(JSContext*, void*)>.op" : true,
+ "mozilla::ThreadSharedFloatArrayBufferList::Storage.mFree" : true,
+ "mozilla::SizeOfState.mMallocSizeOf": true,
+ "mozilla::gfx::SourceSurfaceRawData.mDeallocator": true,
+};
+
+function fieldCallCannotGC(csu, fullfield)
+{
+ if (csu in ignoreClasses)
+ return true;
+ if (fullfield in ignoreCallees)
+ return true;
+ return false;
+}
+
+function ignoreEdgeUse(edge, variable, body)
+{
+ // Horrible special case for ignoring a false positive in xptcstubs: there
+ // is a local variable 'paramBuffer' holding an array of nsXPTCMiniVariant
+ // on the stack, which appears to be live across a GC call because its
+ // constructor is called when the array is initialized, even though the
+ // constructor is a no-op. So we'll do a very narrow exclusion for the use
+ // that incorrectly started the live range, which was basically "__temp_1 =
+ // paramBuffer".
+ //
+ // By scoping it so narrowly, we can detect most hazards that would be
+ // caused by modifications in the PrepareAndDispatch code. It just barely
+ // avoids having a hazard already.
+ if (('Name' in variable) && (variable.Name[0] == 'paramBuffer')) {
+ if (body.BlockId.Kind == 'Function' && body.BlockId.Variable.Name[0] == 'PrepareAndDispatch')
+ if (edge.Kind == 'Assign' && edge.Type.Kind == 'Pointer')
+ if (edge.Exp[0].Kind == 'Var' && edge.Exp[1].Kind == 'Var')
+ if (edge.Exp[1].Variable.Kind == 'Local' && edge.Exp[1].Variable.Name[0] == 'paramBuffer')
+ return true;
+ }
+
+ // Functions which should not be treated as using variable.
+ if (edge.Kind == "Call") {
+ var callee = edge.Exp[0];
+ if (callee.Kind == "Var") {
+ var name = callee.Variable.Name[0];
+ if (/~DebugOnly/.test(name))
+ return true;
+ if (/~ScopedThreadSafeStringInspector/.test(name))
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function ignoreEdgeAddressTaken(edge)
+{
+ // Functions which may take indirect pointers to unrooted GC things,
+ // but will copy them into rooted locations before calling anything
+ // that can GC. These parameters should usually be replaced with
+ // handles or mutable handles.
+ if (edge.Kind == "Call") {
+ var callee = edge.Exp[0];
+ if (callee.Kind == "Var") {
+ var name = callee.Variable.Name[0];
+ if (/js::Invoke\(/.test(name))
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// Ignore calls of these functions (so ignore any stack containing these)
+var ignoreFunctions = {
+ "ptio.c:pt_MapError" : true,
+ "je_malloc_printf" : true,
+ "malloc_usable_size" : true,
+ "vprintf_stderr" : true,
+ "PR_ExplodeTime" : true,
+ "PR_ErrorInstallTable" : true,
+ "PR_SetThreadPrivate" : true,
+ "uint8 NS_IsMainThread()" : true,
+
+ // Has an indirect call under it by the name "__f", which seemed too
+ // generic to ignore by itself.
+ "void* std::_Locale_impl::~_Locale_impl(int32)" : true,
+
+ // Bug 1056410 - devirtualization prevents the standard nsISupports::Release heuristic from working
+ "uint32 nsXPConnect::Release()" : true,
+ "uint32 nsAtom::Release()" : true,
+
+ // Allocation API
+ "malloc": true,
+ "calloc": true,
+ "realloc": true,
+ "free": true,
+
+ // FIXME!
+ "NS_LogInit": true,
+ "NS_LogTerm": true,
+ "NS_LogAddRef": true,
+ "NS_LogRelease": true,
+ "NS_LogCtor": true,
+ "NS_LogDtor": true,
+ "NS_LogCOMPtrAddRef": true,
+ "NS_LogCOMPtrRelease": true,
+
+ // FIXME!
+ "NS_DebugBreak": true,
+
+ // Similar to heap snapshot mock classes, and GTests below. This posts a
+ // synchronous runnable when a GTest fails, and we are pretty sure that the
+ // particular runnable it posts can't even GC, but the analysis isn't
+ // currently smart enough to determine that. In either case, this is (a)
+ // only in GTests, and (b) only when the Gtest has already failed. We have
+ // static and dynamic checks for no GC in the non-test code, and in the test
+ // code we fall back to only the dynamic checks.
+ "void test::RingbufferDumper::OnTestPartResult(testing::TestPartResult*)" : true,
+
+ "float64 JS_GetCurrentEmbedderTime()" : true,
+
+ // This calls any JSObjectMovedOp for the tenured object via an indirect call.
+ "JSObject* js::TenuringTracer::moveToTenuredSlow(JSObject*)" : true,
+
+ "void js::Nursery::freeMallocedBuffers()" : true,
+
+ "void js::AutoEnterOOMUnsafeRegion::crash(uint64, int8*)" : true,
+ "void js::AutoEnterOOMUnsafeRegion::crash_impl(uint64, int8*)" : true,
+
+ "void mozilla::dom::WorkerPrivate::AssertIsOnWorkerThread() const" : true,
+
+ // It would be cool to somehow annotate that nsTHashtable<T> will use
+ // nsTHashtable<T>::s_MatchEntry for its matchEntry function pointer, but
+ // there is no mechanism for that. So we will just annotate a particularly
+ // troublesome logging-related usage.
+ "EntryType* nsTHashtable<EntryType>::PutEntry(nsTHashtable<EntryType>::KeyType, const fallible_t&) [with EntryType = nsBaseHashtableET<nsCharPtrHashKey, nsAutoPtr<mozilla::LogModule> >; nsTHashtable<EntryType>::KeyType = const char*; nsTHashtable<EntryType>::fallible_t = mozilla::fallible_t]" : true,
+ "EntryType* nsTHashtable<EntryType>::GetEntry(nsTHashtable<EntryType>::KeyType) const [with EntryType = nsBaseHashtableET<nsCharPtrHashKey, nsAutoPtr<mozilla::LogModule> >; nsTHashtable<EntryType>::KeyType = const char*]" : true,
+ "EntryType* nsTHashtable<EntryType>::PutEntry(nsTHashtable<EntryType>::KeyType) [with EntryType = nsBaseHashtableET<nsPtrHashKey<const mozilla::BlockingResourceBase>, nsAutoPtr<mozilla::DeadlockDetector<mozilla::BlockingResourceBase>::OrderingEntry> >; nsTHashtable<EntryType>::KeyType = const mozilla::BlockingResourceBase*]" : true,
+ "EntryType* nsTHashtable<EntryType>::GetEntry(nsTHashtable<EntryType>::KeyType) const [with EntryType = nsBaseHashtableET<nsPtrHashKey<const mozilla::BlockingResourceBase>, nsAutoPtr<mozilla::DeadlockDetector<mozilla::BlockingResourceBase>::OrderingEntry> >; nsTHashtable<EntryType>::KeyType = const mozilla::BlockingResourceBase*]" : true,
+
+ // VTune internals that lazy-load a shared library and make IndirectCalls.
+ "iJIT_IsProfilingActive" : true,
+ "iJIT_NotifyEvent": true,
+
+ // The big hammers.
+ "PR_GetCurrentThread" : true,
+ "calloc" : true,
+
+ // This will happen early enough in initialization to not matter.
+ "_PR_UnixInit" : true,
+
+ "uint8 nsContentUtils::IsExpandedPrincipal(nsIPrincipal*)" : true,
+
+ "void mozilla::AutoProfilerLabel::~AutoProfilerLabel(int32)" : true,
+
+ // Stores a function pointer in an AutoProfilerLabelData struct and calls it.
+ // And it's in mozglue, which doesn't have access to the attributes yet.
+ "void mozilla::ProfilerLabelEnd(std::tuple<void*, unsigned int>*)" : true,
+
+ // This gets into PLDHashTable function pointer territory, and should get
+ // set up early enough to not do anything when it matters anyway.
+ "mozilla::LogModule* mozilla::LogModule::Get(int8*)": true,
+
+ // This annotation is correct, but the reasoning is still being hashed out
+ // in bug 1582326 comment 8 and on.
+ "nsCycleCollector.cpp:nsISupports* CanonicalizeXPCOMParticipant(nsISupports*)": true,
+
+ // PLDHashTable again
+ "void mozilla::DeadlockDetector<T>::Add(const T*) [with T = mozilla::BlockingResourceBase]": true,
+
+ // OOM handling during logging
+ "void mozilla::detail::log_print(mozilla::LogModule*, int32, int8*)": true,
+
+ // This would need to know that the nsCOMPtr refcount will not go to zero.
+ "uint8 XPCJSRuntime::DescribeCustomObjects(JSObject*, JSClass*, int8[72]*)[72]) const": true,
+
+ // As the comment says "Refcount isn't zero, so Suspect won't delete anything."
+ "uint64 nsCycleCollectingAutoRefCnt::incr(void*, nsCycleCollectionParticipant*) [with void (* suspect)(void*, nsCycleCollectionParticipant*, nsCycleCollectingAutoRefCnt*, bool*) = NS_CycleCollectorSuspect3; uintptr_t = long unsigned int]": true,
+
+ // Calls MergeSort
+ "uint8 v8::internal::RegExpDisjunction::SortConsecutiveAtoms(v8::internal::RegExpCompiler*)": true,
+
+ // nsIEventTarget.IsOnCurrentThreadInfallible does not get resolved, and
+ // this is called on non-JS threads so cannot use AutoSuppressGCAnalysis.
+ "uint8 nsAutoOwningEventTarget::IsCurrentThread() const": true,
+
+ // ~JSStreamConsumer calls 2 ~RefCnt/~nsCOMPtr destructors for its fields,
+ // but the body of the destructor is written so that all Releases
+ // are proxied, and the members will all be empty at destruction time.
+ "void mozilla::dom::JSStreamConsumer::~JSStreamConsumer() [[base_dtor]]": true,
+};
+
+function extraGCFunctions(readableNames) {
+ return ["ffi_call"].filter(f => f in readableNames);
+}
+
+function isProtobuf(name)
+{
+ return name.match(/\bgoogle::protobuf\b/) ||
+ name.match(/\bmozilla::devtools::protobuf\b/);
+}
+
+function isHeapSnapshotMockClass(name)
+{
+ return name.match(/\bMockWriter\b/) ||
+ name.match(/\bMockDeserializedNode\b/);
+}
+
+function isGTest(name)
+{
+ return name.match(/\btesting::/);
+}
+
+function isICU(name)
+{
+ return name.match(/\bicu_\d+::/) ||
+ name.match(/u(prv_malloc|prv_realloc|prv_free|case_toFullLower)_\d+/)
+}
+
+function ignoreGCFunction(mangled, readableNames)
+{
+ // Field calls will not be in readableNames
+ if (!(mangled in readableNames))
+ return false;
+
+ const fun = readableNames[mangled][0];
+
+ if (fun in ignoreFunctions)
+ return true;
+
+ // The protobuf library, and [de]serialization code generated by the
+ // protobuf compiler, uses a _ton_ of function pointers but they are all
+ // internal. The same is true for ICU. Easiest to just ignore that mess
+ // here.
+ if (isProtobuf(fun) || isICU(fun))
+ return true;
+
+ // Ignore anything that goes through heap snapshot GTests or mocked classes
+ // used in heap snapshot GTests. GTest and GMock expose a lot of virtual
+ // methods and function pointers that could potentially GC after an
+ // assertion has already failed (depending on user-provided code), but don't
+ // exhibit that behavior currently. For non-test code, we have dynamic and
+ // static checks that ensure we don't GC. However, for test code we opt out
+ // of static checks here, because of the above stated GMock/GTest issues,
+ // and rely on only the dynamic checks provided by AutoAssertCannotGC.
+ if (isHeapSnapshotMockClass(fun) || isGTest(fun))
+ return true;
+
+ // Templatized function
+ if (fun.includes("void nsCOMPtr<T>::Assert_NoQueryNeeded()"))
+ return true;
+
+ // Bug 1577915 - Sixgill is ignoring a template param that makes its CFG
+ // impossible.
+ if (fun.includes("UnwrapObjectInternal") && fun.includes("mayBeWrapper = false"))
+ return true;
+
+ // These call through an 'op' function pointer.
+ if (fun.includes("js::WeakMap<Key, Value, HashPolicy>::getDelegate("))
+ return true;
+
+ // TODO: modify refillFreeList<NoGC> to not need data flow analysis to
+ // understand it cannot GC. As of gcc 6, the same problem occurs with
+ // tryNewTenuredThing, tryNewNurseryObject, and others.
+ if (/refillFreeList|tryNew/.test(fun) && /= js::NoGC/.test(fun))
+ return true;
+
+ return false;
+}
+
+function stripUCSAndNamespace(name)
+{
+ name = name.replace(/(struct|class|union|const) /g, "");
+ name = name.replace(/(js::ctypes::|js::|JS::|mozilla::dom::|mozilla::)/g, "");
+ return name;
+}
+
+function extraRootedGCThings()
+{
+ return [ 'JSAddonId' ];
+}
+
+function extraRootedPointers()
+{
+ return [
+ ];
+}
+
+function isRootedGCPointerTypeName(name)
+{
+ name = stripUCSAndNamespace(name);
+
+ if (name.startsWith('MaybeRooted<'))
+ return /\(js::AllowGC\)1u>::RootType/.test(name);
+
+ return false;
+}
+
+function isUnsafeStorage(typeName)
+{
+ typeName = stripUCSAndNamespace(typeName);
+ return typeName.startsWith('UniquePtr<');
+}
+
+// If edgeType is a constructor type, return whatever bits it implies for its
+// scope (or zero if not matching).
+function isLimitConstructor(typeInfo, edgeType, varName)
+{
+ // Check whether this could be a constructor
+ if (edgeType.Kind != 'Function')
+ return 0;
+ if (!('TypeFunctionCSU' in edgeType))
+ return 0;
+ if (edgeType.Type.Kind != 'Void')
+ return 0;
+
+ // Check whether the type is a known suppression type.
+ var type = edgeType.TypeFunctionCSU.Type.Name;
+ let attrs = 0;
+ if (type in typeInfo.GCSuppressors)
+ attrs = attrs | ATTR_GC_SUPPRESSED;
+
+ // And now make sure this is the constructor, not some other method on a
+ // suppression type. varName[0] contains the qualified name.
+ var [ mangled, unmangled ] = splitFunction(varName[0]);
+ if (mangled.search(/C\d[EI]/) == -1)
+ return 0; // Mangled names of constructors have C<num>E or C<num>I
+ var m = unmangled.match(/([~\w]+)(?:<.*>)?\(/);
+ if (!m)
+ return 0;
+ var type_stem = type.replace(/\w+::/g, '').replace(/\<.*\>/g, '');
+ if (m[1] != type_stem)
+ return 0;
+
+ return attrs;
+}
+
+// XPIDL-generated methods may invoke JS code, depending on the IDL
+// attributes. This is not visible in the static callgraph since it
+// goes through generated asm code. We can use the JS_HAZ_CAN_RUN_SCRIPT
+// annotation to tell whether this is possible, which is set programmatically
+// by the code generator when needed (bug 1347999):
+// https://searchfox.org/mozilla-central/rev/81c52abeec336685330af5956c37b4bcf8926476/xpcom/idl-parser/xpidl/header.py#213-219
+//
+// Note that WebIDL callbacks can also invoke JS code, but our code generator
+// produces regular C++ code and so does not need any annotations. (There will
+// be a call to JS::Call() or similar.)
+function virtualCanRunJS(csu, field)
+{
+ const tags = typeInfo.OtherFieldTags;
+ const iface = tags[csu]
+ if (!iface) {
+ return false;
+ }
+ const virtual_method_tags = iface[field];
+ return virtual_method_tags && virtual_method_tags.includes("Can run script");
+}
+
+function listNonGCPointers() {
+ return [
+ // Safe only because jsids are currently only made from pinned strings.
+ 'NPIdentifier',
+ ];
+}
+
+function isJSNative(mangled)
+{
+ // _Z...E = function
+ // 9JSContext = JSContext*
+ // j = uint32
+ // PN2JS5Value = JS::Value*
+ // P = pointer
+ // N2JS = JS::
+ // 5Value = Value
+ return mangled.endsWith("P9JSContextjPN2JS5ValueE") && mangled.startsWith("_Z");
+}
diff --git a/js/src/devtools/rootAnalysis/build.js b/js/src/devtools/rootAnalysis/build.js
new file mode 100644
index 0000000000..78ef04fea1
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/build.js
@@ -0,0 +1,15 @@
+#!/bin/sh
+/* 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/. */
+
+
+set -e
+
+cd $SOURCE
+./mach configure
+./mach build export
+./mach build -X nsprpub mfbt memory memory/mozalloc modules/zlib mozglue js/src xpcom/glue js/xpconnect/loader js/xpconnect/wrappers js/xpconnect/src
+status=$?
+echo "[[[[ build.js complete, exit code $status ]]]]"
+exit $status
diff --git a/js/src/devtools/rootAnalysis/build/sixgill-b2g.manifest b/js/src/devtools/rootAnalysis/build/sixgill-b2g.manifest
new file mode 100644
index 0000000000..1ecb5d0665
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/build/sixgill-b2g.manifest
@@ -0,0 +1,10 @@
+[
+{
+"hg_id" : "ec7b7d2442e8",
+"algorithm" : "sha512",
+"digest" : "49627d734df52cb9e7319733da5a6be1812b9373355dc300ee5600b431122570e00d380d50c7c5b5003c462c2c2cb022494b42c4ad00f8eba01c2259cbe6e502",
+"filename" : "sixgill.tar.xz",
+"size" : 2628868,
+"unpack" : true
+}
+]
diff --git a/js/src/devtools/rootAnalysis/build/sixgill.manifest b/js/src/devtools/rootAnalysis/build/sixgill.manifest
new file mode 100644
index 0000000000..49ccdcbd3f
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/build/sixgill.manifest
@@ -0,0 +1,10 @@
+[
+{
+"digest" : "2e56a3cf84764b8e63720e5f961cff7ba8ba5cf2f353dac55c69486489bcd89f53a757e09469a07700b80cd09f09666c2db4ce375b67060ac3be967714597231",
+"size" : 2629600,
+"hg_id" : "221d0d2eead9",
+"unpack" : true,
+"filename" : "sixgill.tar.xz",
+"algorithm" : "sha512"
+}
+]
diff --git a/js/src/devtools/rootAnalysis/callgraph.js b/js/src/devtools/rootAnalysis/callgraph.js
new file mode 100644
index 0000000000..750324f0ed
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/callgraph.js
@@ -0,0 +1,233 @@
+/* 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/. */
+
+loadRelativeToScript('utility.js');
+loadRelativeToScript('annotations.js');
+loadRelativeToScript('CFG.js');
+
+// Map from csu => set of immediate subclasses
+var subclasses = new Map();
+
+// Map from csu => set of immediate superclasses
+var superclasses = new Map();
+
+// Map from "csu.name:nargs" => set of full method name
+var virtualDefinitions = new Map();
+
+// Every virtual method declaration, anywhere.
+//
+// Map from csu => Set of function-info.
+// function-info: {
+// name : simple string
+// typedfield : "name:nargs" ("mangled" field name)
+// field: full Field datastructure
+// annotations : Set of [annotation-name, annotation-value] 2-element arrays
+// inherited : whether the method is inherited from a base class
+// pureVirtual : whether the method is pure virtual on this CSU
+// dtor : if this is a virtual destructor with a definition in this class or
+// a superclass, then the full name of the definition as if it were defined
+// in this class. This is weird, but it's how gcc emits it. We will add a
+// synthetic call from this function to its immediate base classes' dtors,
+// so even if the function does not actually exist and is inherited from a
+// base class, we will get a path to the inherited function. (Regular
+// virtual methods are *not* claimed to exist when they don't.)
+// }
+var virtualDeclarations = new Map();
+
+var virtualResolutionsSeen = new Set();
+
+var ID = {
+ jscode: 1,
+ anyfunc: 2,
+ nogcfunc: 3,
+ gc: 4,
+};
+
+// map is a map from names to sets of entries.
+function addToNamedSet(map, name, entry)
+{
+ if (!map.has(name))
+ map.set(name, new Set());
+ const s = map.get(name);
+ s.add(entry);
+ return s;
+}
+
+// CSU is "Class/Struct/Union"
+function processCSU(csuName, csu)
+{
+ if (!("FunctionField" in csu))
+ return;
+
+ for (const {Base} of (csu.CSUBaseClass || [])) {
+ addToNamedSet(subclasses, Base, csuName);
+ addToNamedSet(superclasses, csuName, Base);
+ }
+
+ for (const {Field, Variable} of csu.FunctionField) {
+ // Virtual method
+ const info = Field[0];
+ const name = info.Name[0];
+ const annotations = new Set();
+ const funcInfo = {
+ name,
+ typedfield: typedField(info),
+ field: info,
+ annotations,
+ inherited: (info.FieldCSU.Type.Name != csuName), // Always false for virtual dtors
+ pureVirtual: Boolean(Variable),
+ dtor: false,
+ };
+
+ if (Variable && isSyntheticVirtualDestructor(name)) {
+ // This is one of gcc's artificial dtors.
+ funcInfo.dtor = Variable.Name[0];
+ funcInfo.pureVirtual = false;
+ }
+
+ addToNamedSet(virtualDeclarations, csuName, funcInfo);
+ if ('Annotation' in info) {
+ for (const {Name: [annType, annValue]} of info.Annotation) {
+ annotations.add([annType, annValue]);
+ }
+ }
+
+ if (Variable) {
+ // Note: not dealing with overloading correctly.
+ const name = Variable.Name[0];
+ addToNamedSet(virtualDefinitions, fieldKey(csuName, Field[0]), name);
+ }
+ }
+}
+
+// Return a list of all callees that the given edge might be a call to. Each
+// one is represented by an object with a 'kind' field that is one of
+// ('direct', 'field', 'resolved-field', 'indirect', 'unknown'), though note
+// that 'resolved-field' is really a global record of virtual method
+// resolutions, indepedent of this particular edge.
+function translateCallees(edge)
+{
+ if (edge.Kind != "Call")
+ return [];
+
+ const callee = edge.Exp[0];
+ if (callee.Kind == "Var") {
+ assert(callee.Variable.Kind == "Func");
+ return [{'kind': 'direct', 'name': callee.Variable.Name[0]}];
+ }
+
+ // At some point, we were intentionally invoking invalid function pointers
+ // (as in, a small integer cast to a function pointer type) to convey a
+ // small amount of information in the crash address.
+ if (callee.Kind == "Int")
+ return []; // Intentional crash
+
+ assert(callee.Kind == "Drf");
+ let called = callee.Exp[0];
+ let indirection = 1;
+ if (called.Kind == "Drf") {
+ // This is probably a reference to a function pointer (`func*&`). It
+ // would be possible to determine that for certain by looking up the
+ // variable's type, which is doable but unnecessary. Indirect calls
+ // are assumed to call anything (any function in the codebase) unless they
+ // are annotated otherwise, and the `funkyName` annotation applies to
+ // `(**funkyName)(args)` as well as `(*funkyName)(args)`, it's ok.
+ called = called.Exp[0];
+ indirection += 1;
+ }
+
+ if (called.Kind == "Var") {
+ // indirect call through a variable. Note that the `indirection` field is
+ // currently unused by the later analysis. It is the number of dereferences
+ // applied to the variable before invoking the resulting function.
+ //
+ // The variable name passed through is the simplified one, since that is
+ // what annotations.js uses and we don't want the annotation to be missed
+ // if eg there is another variable of the same name in a sibling scope such
+ // that the fully decorated name no longer matches.
+ const [decorated, bare] = called.Variable.Name;
+ return [{'kind': "indirect", 'variable': bare, indirection}];
+ }
+
+ if (called.Kind != "Fld") {
+ // unknown call target.
+ return [{'kind': "unknown"}];
+ }
+
+ // Return one 'field' callee record giving the full description of what's
+ // happening here (which is either a virtual method call, or a call through
+ // a function pointer stored in a field), and then boil the call down to a
+ // synthetic function that incorporates both the name of the field and the
+ // static type of whatever you're calling the method on. Both refer to the
+ // same call; they're just different ways of describing it.
+ const callees = [];
+ const field = called.Field;
+ const staticCSU = getFieldCallInstanceCSU(edge, field);
+ callees.push({'kind': "field", 'csu': field.FieldCSU.Type.Name, staticCSU,
+ 'field': field.Name[0], 'fieldKey': fieldKey(staticCSU, field),
+ 'isVirtual': ("FieldInstanceFunction" in field)});
+ callees.push({'kind': "direct", 'name': fieldKey(staticCSU, field)});
+
+ return callees;
+}
+
+function getCallees(body, edge, scopeAttrs, functionBodies) {
+ const calls = [];
+
+ // getCallEdgeProperties can set the ATTR_REPLACED attribute, which
+ // means that the call in the edge has been replaced by zero or
+ // more edges to other functions. This is used when the original
+ // edge will end up calling through a function pointer or something
+ // (eg ~shared_ptr<T> calls a function pointer that can only be
+ // T::~T()). The original call edges are left in the graph in case
+ // they are useful for other purposes.
+ for (const callee of translateCallees(edge)) {
+ if (callee.kind != "direct") {
+ calls.push({ callee, attrs: scopeAttrs });
+ } else {
+ const edgeInfo = getCallEdgeProperties(body, edge, callee.name, functionBodies);
+ for (const extra of (edgeInfo.extraCalls || [])) {
+ calls.push({ attrs: scopeAttrs | extra.attrs, callee: { name: extra.name, 'kind': "direct", } });
+ }
+ calls.push({ callee, attrs: scopeAttrs | edgeInfo.attrs});
+ }
+ }
+
+ return calls;
+}
+
+function loadTypes(type_xdb_filename) {
+ const xdb = xdbLibrary();
+ xdb.open(type_xdb_filename);
+
+ const minStream = xdb.min_data_stream();
+ const maxStream = xdb.max_data_stream();
+
+ for (var csuIndex = minStream; csuIndex <= maxStream; csuIndex++) {
+ const csu = xdb.read_key(csuIndex);
+ const data = xdb.read_entry(csu);
+ const json = JSON.parse(data.readString());
+ processCSU(csu.readString(), json[0]);
+
+ xdb.free_string(csu);
+ xdb.free_string(data);
+ }
+}
+
+function loadTypesWithCache(type_xdb_filename, cache_filename) {
+ try {
+ const cacheAB = os.file.readFile(cache_filename, "binary");
+ const cb = serialize();
+ cb.clonebuffer = cacheAB.buffer;
+ const cacheData = deserialize(cb);
+ subclasses = cacheData.subclasses;
+ superclasses = cacheData.superclasses;
+ virtualDefinitions = cacheData.virtualDefinitions;
+ } catch (e) {
+ loadTypes(type_xdb_filename);
+ const cb = serialize({subclasses, superclasses, virtualDefinitions});
+ os.file.writeTypedArrayToFile(cache_filename,
+ new Uint8Array(cb.arraybuffer));
+ }
+}
diff --git a/js/src/devtools/rootAnalysis/computeCallgraph.js b/js/src/devtools/rootAnalysis/computeCallgraph.js
new file mode 100644
index 0000000000..d847465678
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/computeCallgraph.js
@@ -0,0 +1,434 @@
+/* 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/. */
+
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+
+"use strict";
+
+loadRelativeToScript('callgraph.js');
+
+var options = parse_options([
+ {
+ name: '--verbose',
+ type: 'bool'
+ },
+ {
+ name: '--function',
+ type: 'string'
+ },
+ {
+ name: 'typeInfo_filename',
+ type: 'string',
+ default: "typeInfo.txt"
+ },
+ {
+ name: 'callgraphOut_filename',
+ type: 'string',
+ default: "rawcalls.txt"
+ },
+ {
+ name: 'batch',
+ default: 1,
+ type: 'number'
+ },
+ {
+ name: 'numBatches',
+ default: 1,
+ type: 'number'
+ },
+]);
+
+var origOut = os.file.redirect(options.callgraphOut_filename);
+
+var memoized = new Map();
+
+var unmangled2id = new Set();
+
+// Insert a string into the name table and return the ID. Do not use for
+// functions, which must be handled specially.
+function getId(name)
+{
+ let id = memoized.get(name);
+ if (id !== undefined)
+ return id;
+
+ id = memoized.size + 1;
+ memoized.set(name, id);
+ print(`#${id} ${name}`);
+
+ return id;
+}
+
+// Split a function into mangled and unmangled parts and return the ID for the
+// function.
+function functionId(name)
+{
+ const [mangled, unmangled] = splitFunction(name);
+ const id = getId(mangled);
+
+ // Only produce a mangled -> unmangled mapping once, unless there are
+ // multiple unmangled names for the same mangled name.
+ if (unmangled2id.has(unmangled))
+ return id;
+
+ print(`= ${id} ${unmangled}`);
+ unmangled2id.add(unmangled);
+ return id;
+}
+
+var lastline;
+function printOnce(line)
+{
+ if (line != lastline) {
+ print(line);
+ lastline = line;
+ }
+}
+
+// Returns a table mapping function name to lists of
+// [annotation-name, annotation-value] pairs:
+// { function-name => [ [annotation-name, annotation-value] ] }
+//
+// Note that sixgill will only store certain attributes (annotation-names), so
+// this won't be *all* the attributes in the source, just the ones that sixgill
+// watches for.
+function getAllAttributes(body)
+{
+ var all_annotations = {};
+ for (var v of (body.DefineVariable || [])) {
+ if (v.Variable.Kind != 'Func')
+ continue;
+ var name = v.Variable.Name[0];
+ var annotations = all_annotations[name] = [];
+
+ for (var ann of (v.Type.Annotation || [])) {
+ annotations.push(ann.Name);
+ }
+ }
+
+ return all_annotations;
+}
+
+// Get just the annotations understood by the hazard analysis.
+function getAnnotations(functionName, body) {
+ var tags = new Set();
+ var attributes = getAllAttributes(body);
+ if (functionName in attributes) {
+ for (var [ annName, annValue ] of attributes[functionName]) {
+ if (annName == 'annotate')
+ tags.add(annValue);
+ }
+ }
+ return tags;
+}
+
+// Scan through a function body, pulling out all annotations and calls and
+// recording them in callgraph.txt.
+function processBody(functionName, body, functionBodies)
+{
+ if (!('PEdge' in body))
+ return;
+
+ for (var tag of getAnnotations(functionName, body).values()) {
+ const id = functionId(functionName);
+ print(`T ${id} ${tag}`);
+ if (tag == "Calls JSNatives")
+ printOnce(`D ${id} ${functionId("(js-code)")}`);
+ }
+
+ // Set of all callees that have been output so far, in order to suppress
+ // repeated callgraph edges from being recorded. This uses a Map from
+ // callees to limit sets, because we don't want a limited edge to prevent
+ // an unlimited edge from being recorded later. (So an edge will be skipped
+ // if it exists and is at least as limited as the previously seen edge.)
+ //
+ // Limit sets are implemented as integers interpreted as bitfields.
+ //
+ var seen = new Map();
+
+ lastline = null;
+ for (var edge of body.PEdge) {
+ if (edge.Kind != "Call")
+ continue;
+
+ // The attrs (eg ATTR_GC_SUPPRESSED) are determined by whatever RAII
+ // scopes might be active, which have been computed previously for all
+ // points in the body.
+ const scopeAttrs = body.attrs[edge.Index[0]] | 0;
+
+ for (const { callee, attrs } of getCallees(body, edge, scopeAttrs, functionBodies)) {
+ // Some function names will be synthesized by manually constructing
+ // their names. Verify that we managed to synthesize an existing function.
+ // This cannot be done later with either the callees or callers tables,
+ // because the function may be an otherwise uncalled leaf.
+ if (attrs & ATTR_SYNTHETIC) {
+ assertFunctionExists(callee.name);
+ }
+
+ // Individual callees may have additional attrs. The only such
+ // bit currently is that nsISupports.{AddRef,Release} are assumed
+ // to never GC.
+ let prologue = attrs ? `/${attrs} ` : "";
+ prologue += functionId(functionName) + " ";
+ if (callee.kind == 'direct') {
+ const prev_attrs = seen.has(callee.name) ? seen.get(callee.name) : ATTRS_UNVISITED;
+ if (prev_attrs & ~attrs) {
+ // Only output an edge if it loosens a limit.
+ seen.set(callee.name, prev_attrs & attrs);
+ printOnce("D " + prologue + functionId(callee.name));
+ }
+ } else if (callee.kind == 'field') {
+ var { csu, field, isVirtual } = callee;
+ const tag = isVirtual ? 'V' : 'F';
+ const fullfield = `${csu}.${field}`;
+ printOnce(`${tag} ${prologue}${getId(fullfield)} CLASS ${csu} FIELD ${field}`);
+ } else if (callee.kind == 'resolved-field') {
+ // Fully-resolved field (virtual method) call. Record the
+ // callgraph edges. Do not consider attrs, since they are local
+ // to this callsite and we are writing out a global record
+ // here.
+ //
+ // Any field call that does *not* have an R entry must be
+ // assumed to call anything.
+ var { csu, field, callees } = callee;
+ var fullFieldName = csu + "." + field;
+ if (!virtualResolutionsSeen.has(fullFieldName)) {
+ virtualResolutionsSeen.add(fullFieldName);
+ for (var target of callees)
+ printOnce("R " + getId(fullFieldName) + " " + functionId(target.name));
+ }
+ } else if (callee.kind == 'indirect') {
+ printOnce("I " + prologue + "VARIABLE " + callee.variable);
+ } else if (callee.kind == 'unknown') {
+ printOnce("I " + prologue + "VARIABLE UNKNOWN");
+ } else {
+ printErr("invalid " + callee.kind + " callee");
+ debugger;
+ }
+ }
+ }
+}
+
+// Reserve IDs for special function names.
+
+// represents anything that can run JS
+assert(ID.jscode == functionId("(js-code)"));
+
+// function pointers will get an edge to this in loadCallgraph.js; only the ID
+// reservation is present in callgraph.txt
+assert(ID.anyfunc == functionId("(any-function)"));
+
+// same as above, but for fields annotated to never GC
+assert(ID.nogcfunc == functionId("(nogc-function)"));
+
+// garbage collection
+assert(ID.gc == functionId("(GC)"));
+
+var typeInfo = loadTypeInfo(options.typeInfo_filename);
+
+loadTypes("src_comp.xdb");
+
+// Arbitrary JS code must always be assumed to GC. In real code, there would
+// always be a path anyway through some arbitrary JSNative, but this route will be shorter.
+print(`D ${ID.jscode} ${ID.gc}`);
+
+// An unknown function is assumed to GC.
+print(`D ${ID.anyfunc} ${ID.gc}`);
+
+// Output call edges for all virtual methods defined anywhere, from
+// Class.methodname to what a (dynamic) instance of Class would run when
+// methodname was called (either Class::methodname() if defined, or some
+// Base::methodname() for inherited method definitions).
+for (const [fieldkey, methods] of virtualDefinitions) {
+ const caller = getId(fieldkey);
+ for (const name of methods) {
+ const callee = functionId(name);
+ printOnce(`D ${caller} ${callee}`);
+ }
+}
+
+// Output call edges from C.methodname -> S.methodname for all subclasses S of
+// class C. This is for when you are calling methodname on a pointer/ref of
+// dynamic type C, so that the callgraph contains calls to all descendant
+// subclasses' implementations.
+for (const [csu, methods] of virtualDeclarations) {
+ for (const {field, dtor} of methods) {
+ const caller = getId(fieldKey(csu, field));
+ if (virtualCanRunJS(csu, field.Name[0]))
+ printOnce(`D ${caller} ${functionId("(js-code)")}`);
+ if (dtor)
+ printOnce(`D ${caller} ${functionId(dtor)}`);
+ if (!subclasses.has(csu))
+ continue;
+ for (const sub of subclasses.get(csu)) {
+ printOnce(`D ${caller} ${getId(fieldKey(sub, field))}`);
+ }
+ }
+}
+
+var xdb = xdbLibrary();
+xdb.open("src_body.xdb");
+
+if (options.verbose) {
+ printErr("Finished loading data structures");
+}
+
+var minStream = xdb.min_data_stream();
+var maxStream = xdb.max_data_stream();
+
+if (options.function) {
+ var index = xdb.lookup_key(options.function);
+ if (!index) {
+ printErr("Function not found");
+ quit(1);
+ }
+ minStream = maxStream = index;
+}
+
+function assertFunctionExists(name) {
+ var data = xdb.read_entry(name);
+ assert(data.contents != 0, `synthetic function '${name}' not found!`);
+}
+
+function process(functionName, functionBodies)
+{
+ for (var body of functionBodies)
+ body.attrs = [];
+
+ for (var body of functionBodies) {
+ for (var [pbody, id, attrs] of allRAIIGuardedCallPoints(typeInfo, functionBodies, body, isLimitConstructor)) {
+ pbody.attrs[id] = attrs;
+ }
+ }
+
+ if (options.function) {
+ debugger;
+ }
+ for (var body of functionBodies) {
+ processBody(functionName, body, functionBodies);
+ }
+
+ // Not strictly necessary, but add an edge from the synthetic "(js-code)"
+ // to RunScript to allow better stacks than just randomly selecting a
+ // JSNative to blame things on.
+ if (functionName.includes("js::RunScript"))
+ print(`D ${functionId("(js-code)")} ${functionId(functionName)}`);
+
+ // GCC generates multiple constructors and destructors ("in-charge" and
+ // "not-in-charge") to handle virtual base classes. They are normally
+ // identical, and it appears that GCC does some magic to alias them to the
+ // same thing. But this aliasing is not visible to the analysis. So we'll
+ // add a dummy call edge from "foo" -> "foo *INTERNAL* ", since only "foo"
+ // will show up as called but only "foo *INTERNAL* " will be emitted in the
+ // case where the constructors are identical.
+ //
+ // This is slightly conservative in the case where they are *not*
+ // identical, but that should be rare enough that we don't care.
+ var markerPos = functionName.indexOf(internalMarker);
+ if (markerPos > 0) {
+ var inChargeXTor = functionName.replace(internalMarker, "");
+ printOnce("D " + functionId(inChargeXTor) + " " + functionId(functionName));
+ }
+
+ const [ mangled, unmangled ] = splitFunction(functionName);
+
+ // Further note: from https://itanium-cxx-abi.github.io/cxx-abi/abi.html the
+ // different kinds of constructors/destructors are:
+ // C1 # complete object constructor
+ // C2 # base object constructor
+ // C3 # complete object allocating constructor
+ // D0 # deleting destructor
+ // D1 # complete object destructor
+ // D2 # base object destructor
+ //
+ // In actual practice, I have observed C4 and D4 xtors generated by gcc
+ // 4.9.3 (but not 4.7.3). The gcc source code says:
+ //
+ // /* This is the old-style "[unified]" constructor.
+ // In some cases, we may emit this function and call
+ // it from the clones in order to share code and save space. */
+ //
+ // Unfortunately, that "call... from the clones" does not seem to appear in
+ // the CFG we get from GCC. So if we see a C4 constructor or D4 destructor,
+ // inject an edge to it from C1, C2, and C3 (or D1, D2, and D3). (Note that
+ // C3 isn't even used in current GCC, but add the edge anyway just in
+ // case.)
+ //
+ // from gcc/cp/mangle.c:
+ //
+ // <special-name> ::= D0 # deleting (in-charge) destructor
+ // ::= D1 # complete object (in-charge) destructor
+ // ::= D2 # base object (not-in-charge) destructor
+ // <special-name> ::= C1 # complete object constructor
+ // ::= C2 # base object constructor
+ // ::= C3 # complete object allocating constructor
+ //
+ // Currently, allocating constructors are never used.
+ //
+ if (functionName.indexOf("C4") != -1) {
+ // E terminates the method name (and precedes the method parameters).
+ // If eg "C4E" shows up in the mangled name for another reason, this
+ // will create bogus edges in the callgraph. But it will affect little
+ // and is somewhat difficult to avoid, so we will live with it.
+ //
+ // Another possibility! A templatized constructor will contain C4I...E
+ // for template arguments.
+ //
+ for (let [synthetic, variant, desc] of [
+ ['C4E', 'C1E', 'complete_ctor'],
+ ['C4E', 'C2E', 'base_ctor'],
+ ['C4E', 'C3E', 'complete_alloc_ctor'],
+ ['C4I', 'C1I', 'complete_ctor'],
+ ['C4I', 'C2I', 'base_ctor'],
+ ['C4I', 'C3I', 'complete_alloc_ctor']])
+ {
+ if (mangled.indexOf(synthetic) == -1)
+ continue;
+
+ let variant_mangled = mangled.replace(synthetic, variant);
+ let variant_full = `${variant_mangled}$${unmangled} [[${desc}]]`;
+ printOnce("D " + functionId(variant_full) + " " + functionId(functionName));
+ }
+ }
+
+ // For destructors:
+ //
+ // I've never seen D4Ev() + D4Ev(int32), only one or the other. So
+ // for a D4Ev of any sort, create:
+ //
+ // D0() -> D1() # deleting destructor calls complete destructor, then deletes
+ // D1() -> D2() # complete destructor calls base destructor, then destroys virtual bases
+ // D2() -> D4(?) # base destructor might be aliased to unified destructor
+ // # use whichever one is defined, in-charge or not.
+ // # ('?') means either () or (int32).
+ //
+ // Note that this doesn't actually make sense -- D0 and D1 should be
+ // in-charge, but gcc doesn't seem to give them the in-charge parameter?!
+ //
+ if (functionName.indexOf("D4Ev") != -1 && functionName.indexOf("::~") != -1) {
+ const not_in_charge_dtor = functionName.replace("(int32)", "()");
+ const D0 = not_in_charge_dtor.replace("D4Ev", "D0Ev") + " [[deleting_dtor]]";
+ const D1 = not_in_charge_dtor.replace("D4Ev", "D1Ev") + " [[complete_dtor]]";
+ const D2 = not_in_charge_dtor.replace("D4Ev", "D2Ev") + " [[base_dtor]]";
+ printOnce("D " + functionId(D0) + " " + functionId(D1));
+ printOnce("D " + functionId(D1) + " " + functionId(D2));
+ printOnce("D " + functionId(D2) + " " + functionId(functionName));
+ }
+
+ if (isJSNative(mangled))
+ printOnce(`D ${functionId("(js-code)")} ${functionId(functionName)}`);
+}
+
+var start = batchStart(options.batch, options.numBatches, minStream, maxStream);
+var end = batchLast(options.batch, options.numBatches, minStream, maxStream);
+
+for (var nameIndex = start; nameIndex <= end; nameIndex++) {
+ var name = xdb.read_key(nameIndex);
+ var data = xdb.read_entry(name);
+ process(name.readString(), JSON.parse(data.readString()));
+ xdb.free_string(name);
+ xdb.free_string(data);
+}
+
+os.file.close(os.file.redirect(origOut));
diff --git a/js/src/devtools/rootAnalysis/computeGCFunctions.js b/js/src/devtools/rootAnalysis/computeGCFunctions.js
new file mode 100644
index 0000000000..99410efdf8
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/computeGCFunctions.js
@@ -0,0 +1,113 @@
+/* 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/. */
+
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+"use strict";
+
+loadRelativeToScript('utility.js');
+loadRelativeToScript('annotations.js');
+loadRelativeToScript('loadCallgraph.js');
+
+function usage() {
+ throw "Usage: computeGCFunctions.js <rawcalls1.txt> <rawcalls2.txt>... --outputs <out:callgraph.txt> <out:gcFunctions.txt> <out:gcFunctions.lst> <out:gcEdges.txt> <out:limitedFunctions.lst>";
+}
+
+if (typeof scriptArgs[0] != 'string')
+ usage();
+
+var start = "Time: " + new Date;
+
+try {
+ var options = parse_options([
+ {
+ name: '--verbose',
+ type: 'bool'
+ },
+ {
+ name: 'inputs',
+ dest: 'rawcalls_filenames',
+ nargs: '+'
+ },
+ {
+ name: '--outputs',
+ type: 'bool'
+ },
+ {
+ name: 'callgraph',
+ type: 'string',
+ default: 'callgraph.txt'
+ },
+ {
+ name: 'gcFunctions',
+ type: 'string',
+ default: 'gcFunctions.txt'
+ },
+ {
+ name: 'gcFunctionsList',
+ type: 'string',
+ default: 'gcFunctions.lst'
+ },
+ {
+ name: 'limitedFunctions',
+ type: 'string',
+ default: 'limitedFunctions.lst'
+ },
+ ]);
+} catch {
+ printErr("Usage: computeGCFunctions.js [--verbose] <rawcalls1.txt> <rawcalls2.txt>... --outputs <out:callgraph.txt> <out:gcFunctions.txt> <out:gcFunctions.lst> <out:gcEdges.txt> <out:limitedFunctions.lst>");
+ quit(1);
+};
+
+function info(message) {
+ if (options.verbose) {
+ printErr(message);
+ }
+}
+
+var {
+ gcFunctions,
+ functions,
+ calleesOf,
+ limitedFunctions
+} = loadCallgraph(options.rawcalls_filenames, options.verbose);
+
+info("Writing " + options.gcFunctions);
+redirect(options.gcFunctions);
+
+for (var name in gcFunctions) {
+ for (let readable of (functions.readableName[name] || [name])) {
+ print("");
+ const fullname = (name == readable) ? name : name + "$" + readable;
+ print("GC Function: " + fullname);
+ let current = name;
+ do {
+ current = gcFunctions[current];
+ if (current === 'internal')
+ ; // Hit the end
+ else if (current in functions.readableName)
+ print(" " + functions.readableName[current][0]);
+ else
+ print(" " + current);
+ } while (current in gcFunctions);
+ }
+}
+
+info("Writing " + options.gcFunctionsList);
+redirect(options.gcFunctionsList);
+for (var name in gcFunctions) {
+ if (name in functions.readableName) {
+ for (var readable of functions.readableName[name])
+ print(name + "$" + readable);
+ } else {
+ print(name);
+ }
+}
+
+info("Writing " + options.limitedFunctions);
+redirect(options.limitedFunctions);
+print(JSON.stringify(limitedFunctions, null, 4));
+
+info("Writing " + options.callgraph);
+redirect(options.callgraph);
+saveCallgraph(functions, calleesOf);
diff --git a/js/src/devtools/rootAnalysis/computeGCTypes.js b/js/src/devtools/rootAnalysis/computeGCTypes.js
new file mode 100644
index 0000000000..c38a13dabb
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/computeGCTypes.js
@@ -0,0 +1,550 @@
+/* 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/. */
+
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+
+"use strict";
+
+loadRelativeToScript('utility.js');
+loadRelativeToScript('annotations.js');
+
+var options = parse_options([
+ { name: '--verbose', type: 'bool' },
+ { name: "gcTypes", default: "gcTypes.txt" },
+ { name: "typeInfo", default: "typeInfo.txt" }
+]);
+
+var typeInfo = {
+ 'GCPointers': [],
+ 'GCThings': [],
+ 'GCInvalidated': [],
+ 'GCRefs': [],
+ 'NonGCTypes': {}, // unused
+ 'NonGCPointers': {},
+ 'RootedGCThings': {},
+ 'RootedPointers': {},
+ 'RootedBases': {'JS::AutoGCRooter': true},
+ 'InheritFromTemplateArgs': {},
+ 'OtherCSUTags': {},
+ 'OtherFieldTags': {},
+
+ // RAII types within which we should assume GC is suppressed, eg
+ // AutoSuppressGC.
+ 'GCSuppressors': {},
+};
+
+var gDescriptors = new Map; // Map from descriptor string => Set of typeName
+
+var structureParents = {}; // Map from field => list of <parent, fieldName>
+var pointerParents = {}; // Map from field => list of <parent, fieldName>
+var baseClasses = {}; // Map from struct name => list of base class name strings
+var subClasses = {}; // Map from struct name => list of subclass name strings
+
+var gcTypes = {}; // map from parent struct => Set of GC typed children
+var gcPointers = {}; // map from parent struct => Set of GC typed children
+var gcFields = new Map;
+
+var rootedPointers = {};
+
+// Accumulate the base GC types before propagating info through the type graph,
+// so that we can include edges from types processed later
+// (eg MOZ_INHERIT_TYPE_ANNOTATIONS_FROM_TEMPLATE_ARGS).
+var pendingGCTypes = []; // array of [name, reason, ptrdness]
+
+function processCSU(csu, body)
+{
+ for (let { 'Name': [ annType, tag ] } of (body.Annotation || [])) {
+ if (annType != 'annotate')
+ continue;
+
+ if (tag == 'GC Pointer')
+ typeInfo.GCPointers.push(csu);
+ else if (tag == 'Invalidated by GC')
+ typeInfo.GCInvalidated.push(csu);
+ else if (tag == 'GC Pointer or Reference')
+ typeInfo.GCRefs.push(csu);
+ else if (tag == 'GC Thing')
+ typeInfo.GCThings.push(csu);
+ else if (tag == 'Suppressed GC Pointer')
+ typeInfo.NonGCPointers[csu] = true;
+ else if (tag == 'Rooted Pointer')
+ typeInfo.RootedPointers[csu] = true;
+ else if (tag == 'Rooted Base')
+ typeInfo.RootedBases[csu] = true;
+ else if (tag == 'Suppress GC')
+ typeInfo.GCSuppressors[csu] = true;
+ else if (tag == 'moz_inherit_type_annotations_from_template_args')
+ typeInfo.InheritFromTemplateArgs[csu] = true;
+ else
+ addToKeyedList(typeInfo.OtherCSUTags, csu, tag);
+ }
+
+ for (let { 'Base': base } of (body.CSUBaseClass || []))
+ addBaseClass(csu, base);
+
+ for (const field of (body.DataField || [])) {
+ var type = field.Field.Type;
+ var fieldName = field.Field.Name[0];
+ if (type.Kind == "Pointer") {
+ var target = type.Type;
+ if (target.Kind == "CSU")
+ addNestedPointer(csu, target.Name, fieldName);
+ }
+ if (type.Kind == "Array") {
+ var target = type.Type;
+ if (target.Kind == "CSU")
+ addNestedStructure(csu, target.Name, fieldName);
+ }
+ if (type.Kind == "CSU")
+ addNestedStructure(csu, type.Name, fieldName);
+
+ for (const { 'Name': [ annType, tag ] } of (field.Annotation || [])) {
+ if (!(csu in typeInfo.OtherFieldTags))
+ typeInfo.OtherFieldTags[csu] = [];
+ addToKeyedList(typeInfo.OtherFieldTags[csu], fieldName, tag);
+ }
+ }
+
+ for (const funcfield of (body.FunctionField || [])) {
+ const fields = funcfield.Field;
+ // Pure virtual functions will not have field.Variable; others will.
+ for (const field of funcfield.Field) {
+ for (const {'Name': [annType, tag]} of (field.Annotation || [])) {
+ if (!(csu in typeInfo.OtherFieldTags))
+ typeInfo.OtherFieldTags[csu] = {};
+ addToKeyedList(typeInfo.OtherFieldTags[csu], field.Name[0], tag);
+ }
+ }
+ }
+}
+
+// csu.field is of type inner
+function addNestedStructure(csu, inner, field)
+{
+ if (!(inner in structureParents))
+ structureParents[inner] = [];
+
+ // Skip fields that are really base classes, to avoid duplicating the base
+ // fields; addBaseClass already added a "base-N" name.
+ if (field.match(/^field:\d+$/) && (csu in baseClasses) && (baseClasses[csu].indexOf(inner) != -1))
+ return;
+
+ structureParents[inner].push([ csu, field ]);
+}
+
+function addBaseClass(csu, base) {
+ if (!(csu in baseClasses))
+ baseClasses[csu] = [];
+ baseClasses[csu].push(base);
+ if (!(base in subClasses))
+ subClasses[base] = [];
+ subClasses[base].push(csu);
+ var k = baseClasses[csu].length;
+ addNestedStructure(csu, base, `<base-${k}>`);
+}
+
+function addNestedPointer(csu, inner, field)
+{
+ if (!(inner in pointerParents))
+ pointerParents[inner] = [];
+ pointerParents[inner].push([ csu, field ]);
+}
+
+var xdb = xdbLibrary();
+xdb.open("src_comp.xdb");
+
+var minStream = xdb.min_data_stream();
+var maxStream = xdb.max_data_stream();
+
+for (var csuIndex = minStream; csuIndex <= maxStream; csuIndex++) {
+ var csu = xdb.read_key(csuIndex);
+ var data = xdb.read_entry(csu);
+ var json = JSON.parse(data.readString());
+ assert(json.length == 1);
+ processCSU(csu.readString(), json[0]);
+
+ xdb.free_string(csu);
+ xdb.free_string(data);
+}
+
+for (const typename of extraRootedGCThings())
+ typeInfo.RootedGCThings[typename] = true;
+
+for (const typename of extraRootedPointers())
+ typeInfo.RootedPointers[typename] = true;
+
+// Everything that inherits from a "Rooted Base" is considered to be rooted.
+// This is for things like CustomAutoRooter and its subclasses.
+var basework = Object.keys(typeInfo.RootedBases);
+while (basework.length) {
+ const base = basework.pop();
+ typeInfo.RootedPointers[base] = true;
+ if (base in subClasses)
+ basework.push(...subClasses[base]);
+}
+
+// Now that we have the whole hierarchy set up, add all the types and propagate
+// info.
+for (const csu of typeInfo.GCThings)
+ addGCType(csu);
+for (const csu of typeInfo.GCPointers)
+ addGCPointer(csu);
+for (const csu of typeInfo.GCInvalidated)
+ addGCPointer(csu);
+
+function parseTemplateType(typeName, validate=false) {
+ // We only want templatized types. `Foo<U, T>::Member` doesn't count.
+ // Foo<U, T>::Bar<X, Y> does count. Which turns out to be a simple rule:
+ // check whether the type ends in '>'.
+ if (!typeName.endsWith(">")) {
+ return [typeName, undefined];
+ }
+
+ // "Tokenize" into angle brackets, commas, and everything else. We store
+ // match objects as tokens because we'll need the string offset after we
+ // finish grabbing the template parameters.
+ const tokens = [];
+ const tokenizer = /[<>,]|[^<>,]+/g;
+ let match;
+ while ((match = tokenizer.exec(typeName)) !== null) {
+ tokens.push(match);
+ }
+
+ // Walk backwards through the tokens, stopping when we find the matching
+ // open bracket.
+ const args = [];
+ let depth = 0;
+ let arg;
+ let first_result;
+ for (const match of tokens.reverse()) {
+ const token = match[0];
+ if (depth == 1 && (token == ',' || token == '<')) {
+ // We've walked back to the beginning of a template parameter,
+ // where we will see either a comma or open bracket.
+ args.unshift(arg);
+ arg = '';
+ } else if (depth == 0 && token == '>') {
+ arg = ''; // We just started.
+ } else {
+ arg = token + arg;
+ }
+
+ // Maintain the depth.
+ if (token == '<') {
+ // This could be bug 1728151.
+ assert(depth > 0, `Invalid type: too many '<' signs in '${typeName}'`);
+ depth--;
+ } else if (token == '>') {
+ depth++;
+ }
+
+ if (depth == 0) {
+ // We've walked out of the template parameter list.
+ // Record the results.
+ assert(args.length > 0);
+ const templateName = typeName.substr(0, match.index);
+ const result = [templateName, args.map(arg => arg.trim())];
+ if (!validate) {
+ // Normal processing is to return the result the first time we
+ // get to the '<' that matches the terminal '>', without validating
+ // that the rest of the type name is balanced.
+ return result;
+ } else if (!first_result) {
+ // If we are validating, remember the result when we hit the
+ // first matching '<', but then keep processing the rest of
+ // the input string to count brackets.
+ first_result = result;
+ }
+ }
+ }
+
+ // This could be bug 1728151.
+ assert(depth == 0, `Invalid type: too many '>' signs in '${typeName}'`);
+ return first_result;
+}
+
+if (os.getenv("HAZARD_RUN_INTERNAL_TESTS")) {
+ function check_parse(typeName, result) {
+ assertEq(JSON.stringify(parseTemplateType(typeName)), JSON.stringify(result));
+ }
+
+ check_parse("int", ["int", undefined]);
+ check_parse("Type<int>", ["Type", ["int"]]);
+ check_parse("Container<int, double>", ["Container", ["int", "double"]]);
+ check_parse("Container<Container<void, void>, double>", ["Container", ["Container<void, void>", "double"]]);
+ check_parse("Foo<Bar<a,b>,Bar<a,b>>::Container<Container<void, void>, double>", ["Foo<Bar<a,b>,Bar<a,b>>::Container", ["Container<void, void>", "double"]]);
+ check_parse("AlignedStorage2<TypedArray<foo>>", ["AlignedStorage2", ["TypedArray<foo>"]]);
+ check_parse("mozilla::AlignedStorage2<mozilla::dom::TypedArray<unsigned char, JS::UnwrapArrayBufferMaybeShared, JS::GetArrayBufferMaybeSharedData, JS::GetArrayBufferMaybeSharedLengthAndData, JS::NewArrayBuffer> >",
+ [
+ "mozilla::AlignedStorage2",
+ [
+ "mozilla::dom::TypedArray<unsigned char, JS::UnwrapArrayBufferMaybeShared, JS::GetArrayBufferMaybeSharedData, JS::GetArrayBufferMaybeSharedLengthAndData, JS::NewArrayBuffer>"
+ ]
+ ]
+ );
+ check_parse(
+ "mozilla::ArrayIterator<const mozilla::dom::binding_detail::RecordEntry<nsTString<char16_t>, mozilla::dom::Nullable<mozilla::dom::TypedArray<unsigned char, JS::UnwrapArrayBufferMaybeShared, JS::GetArrayBufferMaybeSharedData, JS::GetArrayBufferMaybeSharedLengthAndData, JS::NewArrayBuffer> > >&, nsTArray_Impl<mozilla::dom::binding_detail::RecordEntry<nsTString<char16_t>, mozilla::dom::Nullable<mozilla::dom::TypedArray<unsigned char, JS::UnwrapArrayBufferMaybeShared, JS::GetArrayBufferMaybeSharedData, JS::GetArrayBufferMaybeSharedLengthAndData, JS::NewArrayBuffer> > >, nsTArrayInfallibleAllocator> >",
+ [
+ "mozilla::ArrayIterator",
+ [
+ "const mozilla::dom::binding_detail::RecordEntry<nsTString<char16_t>, mozilla::dom::Nullable<mozilla::dom::TypedArray<unsigned char, JS::UnwrapArrayBufferMaybeShared, JS::GetArrayBufferMaybeSharedData, JS::GetArrayBufferMaybeSharedLengthAndData, JS::NewArrayBuffer> > >&",
+ "nsTArray_Impl<mozilla::dom::binding_detail::RecordEntry<nsTString<char16_t>, mozilla::dom::Nullable<mozilla::dom::TypedArray<unsigned char, JS::UnwrapArrayBufferMaybeShared, JS::GetArrayBufferMaybeSharedData, JS::GetArrayBufferMaybeSharedLengthAndData, JS::NewArrayBuffer> > >, nsTArrayInfallibleAllocator>"
+ ]
+ ]
+ );
+
+ function check_throws(f, exc) {
+ try {
+ f();
+ } catch (e) {
+ assertEq(e.message.includes(exc), true, "incorrect exception: " + e.message);
+ return;
+ }
+ assertEq(undefined, exc);
+ }
+ // Note that these need to end in '>' or the whole thing will be ignored.
+ check_throws(() => parseTemplateType("foo>", true), "too many '>' signs");
+ check_throws(() => parseTemplateType("foo<<>", true), "too many '<' signs");
+ check_throws(() => parseTemplateType("foo<a::bar<a,b>", true), "too many '<' signs");
+ check_throws(() => parseTemplateType("foo<a>*>::bar<a,b>", true), "too many '>' signs");
+}
+
+// GC Thing and GC Pointer annotations can be inherited from template args if
+// this annotation is used. Think of Maybe<T> for example: Maybe<JSObject*> has
+// the same GC rules as JSObject*.
+
+var inheritors = Object.keys(typeInfo.InheritFromTemplateArgs).sort((a, b) => a.length - b.length);
+for (const csu of inheritors) {
+ const [templateName, templateArgs] = parseTemplateType(csu);
+ for (const param of (templateArgs || [])) {
+ const pos = param.search(/\**$/);
+ const ptrdness = param.length - pos;
+ const core_type = param.substr(0, pos);
+ if (ptrdness == 0) {
+ addToKeyedList(structureParents, core_type, [csu, "template-param-" + param]);
+ } else if (ptrdness == 1) {
+ addToKeyedList(pointerParents, core_type, [csu, "template-param-" + param]);
+ }
+ }
+}
+
+function Ptr(level) {
+ if (level < 0)
+ return Array(-level).fill("&").join("");
+ else
+ return Array(level).fill("*").join("");
+}
+
+// "typeName is a (pointer to a)^'typePtrLevel' GC type because it contains a field
+// named 'child' of type 'childType' (or pointer to 'childType' if fieldPtrLevel == 1),
+// which is itself a GCThing or GCPointer."
+function markGCType(typeName, child, childType, typePtrLevel, fieldPtrLevel, indent = "") {
+ // Some types, like UniquePtr, do not mark/trace/relocate their contained
+ // pointers and so should not hold them live across a GC. UniquePtr in
+ // particular should be the only thing pointing to a structure containing a
+ // GCPointer, so nothing else can possibly trace it and it'll die when the
+ // UniquePtr goes out of scope. So we say that memory pointed to by a
+ // UniquePtr is just as unsafe as the stack for storing GC pointers.
+ if (isUnsafeStorage(typeName)) {
+ // If a UniquePtr<T> itself is on the stack, then there's a problem if
+ // T contains a Cell*. But the UniquePtr itself stores a T*, not a T,
+ // so set fieldPtrLevel=-1 to "undo" the pointer. When the type T is
+ // scanned for pointers and a Cell* is found, then when unwrapping the
+ // types, UniquePtr<T> will be seen as a T*=Cell** that should be
+ // treated as a Cell*.
+ //
+ // However, that creates the possibility of an infinite loop, if you
+ // have a type T that contains a UniquePtr<T> (which is allowed, because
+ // it's storing a T* not a T.)
+ const ptrLevel = typePtrLevel + fieldPtrLevel - 1;
+ if (options.verbose) {
+ printErr(`.${child} : (${childType} : "Cell${Ptr(typePtrLevel)}")${Ptr(fieldPtrLevel)} is-field-of ${typeName} : "Cell${Ptr(ptrLevel)}" [unsafe]`);
+ }
+ markGCTypeImpl(typeName, child, childType, ptrLevel, indent);
+
+ // Also treat UniquePtr<T> as if it were any other struct.
+ }
+
+ // Example: with:
+ // struct Pair { JSObject* foo; int bar; };
+ // struct { Pair** info }***
+ // make a call to:
+ // child='info' typePtrLevel=3 fieldPtrLevel=2
+ // for a final ptrLevel of 5, used to later call:
+ // child='foo' typePtrLevel=5 fieldPtrLevel=1
+ //
+ const ptrLevel = typePtrLevel + fieldPtrLevel;
+ if (options.verbose) {
+ printErr(`.${child} : (${childType} : "Cell${Ptr(typePtrLevel)}")${Ptr(fieldPtrLevel)} is-field-of ${typeName} : "Cell${Ptr(ptrLevel)}"`);
+ }
+ markGCTypeImpl(typeName, child, childType, ptrLevel, indent);
+}
+
+function markGCTypeImpl(typeName, child, childType, ptrLevel, indent) {
+ // ...except when > 2 levels of pointers away from an actual GC thing, stop
+ // searching the graph. (This would just be > 1, except that a UniquePtr
+ // field might still have a GC pointer.)
+ if (ptrLevel > 2)
+ return;
+
+ if (isRootedGCPointerTypeName(typeName) && !(typeName in typeInfo.RootedPointers))
+ printErr("FIXME: use in-source annotation for " + typeName);
+
+ if (ptrLevel == 0 && (typeName in typeInfo.RootedGCThings))
+ return;
+ if (ptrLevel == 1 && (isRootedGCPointerTypeName(typeName) || (typeName in typeInfo.RootedPointers)))
+ return;
+
+ if (ptrLevel == 0) {
+ if (typeName in typeInfo.NonGCTypes)
+ return;
+ if (!(typeName in gcTypes))
+ gcTypes[typeName] = new Set();
+ gcTypes[typeName].add(childType);
+ } else if (ptrLevel == 1) {
+ if (typeName in typeInfo.NonGCPointers)
+ return;
+ if (!(typeName in gcPointers))
+ gcPointers[typeName] = new Set();
+ gcPointers[typeName].add(childType);
+ }
+
+ if (ptrLevel < 2) {
+ if (!gcFields.has(typeName))
+ gcFields.set(typeName, new Map());
+ const fields = gcFields.get(typeName);
+ if (fields.has(child)) {
+ const [orig_childType, orig_ptrLevel] = fields.get(child);
+ if (ptrLevel >= orig_ptrLevel) {
+ // Do not recurse for things more levels of pointers away from Cell.
+ // This will prevent infinite loops when types are defined recursively
+ // (eg a struct containing a UniquePtr of itself).
+ return;
+ }
+ }
+ fields.set(child, [childType, ptrLevel]);
+ }
+
+ if (typeName in structureParents) {
+ for (var field of structureParents[typeName]) {
+ var [ holderType, fieldName ] = field;
+ markGCType(holderType, fieldName, typeName, ptrLevel, 0, indent + " ");
+ }
+ }
+ if (typeName in pointerParents) {
+ for (var field of pointerParents[typeName]) {
+ var [ holderType, fieldName ] = field;
+ markGCType(holderType, fieldName, typeName, ptrLevel, 1, indent + " ");
+ }
+ }
+}
+
+function addGCType(typeName)
+{
+ pendingGCTypes.push([typeName, '<annotation>', '(annotation)', 0, 0]);
+}
+
+function addGCPointer(typeName)
+{
+ pendingGCTypes.push([typeName, '<pointer-annotation>', '(annotation)', 1, 0]);
+}
+
+for (const pending of pendingGCTypes) {
+ markGCType(...pending);
+}
+
+// Call a function for a type and every type that contains the type in a field
+// or as a base class (which internally is pretty much the same thing --
+// subclasses are structs beginning with the base class and adding on their
+// local fields.)
+function foreachContainingStruct(typeName, func, seen = new Set())
+{
+ function recurse(container, typeName) {
+ if (seen.has(typeName))
+ return;
+ seen.add(typeName);
+
+ func(container, typeName);
+
+ if (typeName in subClasses) {
+ for (const sub of subClasses[typeName])
+ recurse("subclass of " + typeName, sub);
+ }
+ if (typeName in structureParents) {
+ for (const [holder, field] of structureParents[typeName])
+ recurse(field + " : " + typeName, holder);
+ }
+ }
+
+ recurse('<annotation>', typeName);
+}
+
+for (var type of listNonGCPointers())
+ typeInfo.NonGCPointers[type] = true;
+
+function explain(csu, indent, seen) {
+ if (!seen)
+ seen = new Set();
+ seen.add(csu);
+ if (!gcFields.has(csu))
+ return;
+ var fields = gcFields.get(csu);
+
+ if (fields.has('<annotation>')) {
+ print(indent + "which is annotated as a GCThing");
+ return;
+ }
+ if (fields.has('<pointer-annotation>')) {
+ print(indent + "which is annotated as a GCPointer");
+ return;
+ }
+ for (var [ field, [ child, ptrdness ] ] of fields) {
+ var msg = indent;
+ if (field[0] == '<')
+ msg += "inherits from ";
+ else {
+ if (field.startsWith("template-param-")) {
+ msg += "inherits annotations from template parameter '" + field.substr(15) + "' ";
+ } else {
+ msg += "contains field '" + field + "' ";
+ }
+ if (ptrdness == -1)
+ msg += "(with a pointer to unsafe storage) holding a ";
+ else if (ptrdness == 0)
+ msg += "of type ";
+ else
+ msg += "pointing to type ";
+ }
+ msg += child;
+ print(msg);
+ if (!seen.has(child))
+ explain(child, indent + " ", seen);
+ }
+}
+
+var origOut = os.file.redirect(options.gcTypes);
+
+for (var csu in gcTypes) {
+ print("GCThing: " + csu);
+ explain(csu, " ");
+}
+for (var csu in gcPointers) {
+ print("GCPointer: " + csu);
+ explain(csu, " ");
+}
+
+// Redirect output to the typeInfo file and close the gcTypes file.
+os.file.close(os.file.redirect(options.typeInfo));
+
+// Compute the set of types that suppress GC within their RAII scopes (eg
+// AutoSuppressGC, AutoSuppressGCForAnalysis).
+var seen = new Set();
+for (let csu in typeInfo.GCSuppressors)
+ foreachContainingStruct(csu,
+ (holder, typeName) => { typeInfo.GCSuppressors[typeName] = holder },
+ seen);
+
+print(JSON.stringify(typeInfo, null, 4));
+
+os.file.close(os.file.redirect(origOut));
diff --git a/js/src/devtools/rootAnalysis/dumpCFG.js b/js/src/devtools/rootAnalysis/dumpCFG.js
new file mode 100644
index 0000000000..0ac220840c
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/dumpCFG.js
@@ -0,0 +1,273 @@
+/* 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/. */
+
+// const cfg = loadCFG(scriptArgs[0]);
+// dump_CFG(cfg);
+
+function loadCFG(filename) {
+ const data = os.file.readFile(filename);
+ return JSON.parse(data);
+}
+
+function dump_CFG(cfg) {
+ for (const body of cfg)
+ dump_body(body);
+}
+
+function dump_body(body, src, dst) {
+ const {BlockId,Command,DefineVariable,Index,Location,PEdge,PPoint,Version} = body;
+
+ const [mangled, unmangled] = splitFunction(BlockId.Variable.Name[0]);
+ print(`${unmangled} at ${Location[0].CacheString}:${Location[0].Line}`);
+
+ if (src === undefined) {
+ for (const def of DefineVariable)
+ print(str_definition(def));
+ print("");
+ }
+
+ for (const edge of PEdge) {
+ if (src === undefined || edge.Index[0] == src) {
+ if (dst == undefined || edge.Index[1] == dst)
+ print(str_edge(edge, body));
+ }
+ }
+}
+
+function str_definition(def) {
+ const {Type, Variable} = def;
+ return `define ${str_Variable(Variable)} : ${str_Type(Type)}`;
+}
+
+function badFormat(what, val) {
+ printErr("Bad format of " + what + ": " + JSON.stringify(val, null, 4));
+ printErr((new Error).stack);
+}
+
+function str_Variable(variable) {
+ if (variable.Kind == 'Return')
+ return '<returnval>';
+ else if (variable.Kind == 'This')
+ return 'this';
+
+ try {
+ return variable.Name[1];
+ } catch(e) {
+ badFormat("variable", variable);
+ }
+}
+
+function str_Type(type) {
+ try {
+ const {Kind, Type, Name, TypeFunctionArguments} = type;
+ if (Kind == 'Pointer')
+ return str_Type(Type) + ["*", "&", "&&"][type.Reference];
+ else if (Kind == 'CSU')
+ return Name;
+ else if (Kind == 'Array')
+ return str_Type(Type) + "[]";
+ else if (Kind == 'Function')
+ return str_Type(Type) + "()";
+
+ return Kind;
+ } catch(e) {
+ badFormat("type", type);
+ }
+}
+
+var OpCodeNames = {
+ 'LessEqual': ['<=', '>'],
+ 'LessThan': ['<', '>='],
+ 'GreaterEqual': ['>=', '<'],
+ 'Greater': ['>', '<='],
+ 'Plus': '+',
+ 'Minus': '-',
+};
+
+function opcode_name(opcode, invert) {
+ if (opcode in OpCodeNames) {
+ const name = OpCodeNames[opcode];
+ if (invert === undefined)
+ return name;
+ return name[invert ? 1 : 0];
+ } else {
+ if (invert === undefined)
+ return opcode;
+ return (invert ? '!' : '') + opcode;
+ }
+}
+
+function str_value(val, env, options) {
+ const {Kind, Variable, String, Exp} = val;
+ if (Kind == 'Var')
+ return str_Variable(Variable);
+ else if (Kind == 'Drf') {
+ // Suppress the vtable lookup dereference
+ if (Exp[0].Kind == 'Fld' && "FieldInstanceFunction" in Exp[0].Field)
+ return str_value(Exp[0], env);
+ const exp = str_value(Exp[0], env);
+ if (options && options.noderef)
+ return exp;
+ return "*" + exp;
+ } else if (Kind == 'Fld') {
+ const {Exp, Field} = val;
+ const name = Field.Name[0];
+ if ("FieldInstanceFunction" in Field) {
+ return Field.FieldCSU.Type.Name + "." + name;
+ }
+ const container = str_value(Exp[0]);
+ if (container.startsWith("*"))
+ return container.substring(1) + "->" + name;
+ return container + "." + name;
+ } else if (Kind == 'Empty') {
+ return '<unknown>';
+ } else if (Kind == 'Binop') {
+ const {OpCode} = val;
+ const op = opcode_name(OpCode);
+ return `${str_value(Exp[0], env)} ${op} ${str_value(Exp[1], env)}`;
+ } else if (Kind == 'Unop') {
+ const exp = str_value(Exp[0], env);
+ const {OpCode} = val;
+ if (OpCode == 'LogicalNot')
+ return `not ${exp}`;
+ return `${OpCode}(${exp})`;
+ } else if (Kind == 'Index') {
+ const index = str_value(Exp[1], env);
+ if (Exp[0].Kind == 'Drf')
+ return `${str_value(Exp[0], env, {noderef:true})}[${index}]`;
+ else
+ return `&${str_value(Exp[0], env)}[${index}]`;
+ } else if (Kind == 'NullTest') {
+ return `nullptr == ${str_value(Exp[0], env)}`;
+ } else if (Kind == "String") {
+ return '"' + String + '"';
+ } else if (String !== undefined) {
+ return String;
+ }
+ badFormat("value", val);
+}
+
+function str_thiscall_Exp(exp) {
+ return exp.Kind == 'Drf' ? str_value(exp.Exp[0]) + "->" : str_value(exp) + ".";
+}
+
+function stripcsu(s) {
+ return s.replace("class ", "").replace("struct ", "").replace("union ");
+}
+
+function str_call(prefix, edge, env) {
+ const {Exp, Type, PEdgeCallArguments, PEdgeCallInstance} = edge;
+ const {Kind, Type:cType, TypeFunctionArguments, TypeFunctionCSU} = Type;
+
+ if (Kind == 'Function') {
+ const params = PEdgeCallArguments ? PEdgeCallArguments.Exp : [];
+ const strParams = params.map(str_value);
+
+ let func;
+ let comment = "";
+ let assign_exp;
+ if (PEdgeCallInstance) {
+ const csu = TypeFunctionCSU.Type.Name;
+ const method = str_value(Exp[0], env);
+
+ // Heuristic to only display the csu for constructors
+ if (csu.includes(method)) {
+ func = stripcsu(csu) + "::" + method;
+ } else {
+ func = method;
+ comment = "# " + csu + "::" + method + "\n";
+ }
+
+ const {Exp: thisExp} = PEdgeCallInstance;
+ func = str_thiscall_Exp(thisExp) + func;
+ } else {
+ func = str_value(Exp[0]);
+ }
+ assign_exp = Exp[1];
+
+ let assign = "";
+ if (assign_exp) {
+ assign = str_value(assign_exp) + " := ";
+ }
+ return `${comment}${prefix} Call ${assign}${func}(${strParams.join(", ")})`;
+ }
+
+ print(JSON.stringify(edge, null, 4));
+ throw new Error("unhandled format error");
+}
+
+function str_assign(prefix, edge) {
+ const {Exp} = edge;
+ const [lhs, rhs] = Exp;
+ return `${prefix} Assign ${str_value(lhs)} := ${str_value(rhs)}`;
+}
+
+function str_loop(prefix, edge) {
+ const {BlockId: {Loop}} = edge;
+ return `${prefix} Loop ${Loop}`;
+}
+
+function str_assume(prefix, edge) {
+ const {Exp, PEdgeAssumeNonZero} = edge;
+ const cmp = PEdgeAssumeNonZero ? "" : "!";
+
+ const {Exp: aExp, Kind, OpCode} = Exp[0];
+ if (Kind == 'Binop') {
+ const [lhs, rhs] = aExp;
+ const op = opcode_name(OpCode, !PEdgeAssumeNonZero);
+ return `${prefix} Assume ${str_value(lhs)} ${op} ${str_value(rhs)}`;
+ } else if (Kind == 'Unop') {
+ return `${prefix} Assume ${cmp}${OpCode} ${str_value(aExp[0])}`;
+ } else if (Kind == 'NullTest') {
+ return `${prefix} Assume nullptr ${cmp}== ${str_value(aExp[0])}`;
+ } else if (Kind == 'Drf') {
+ return `${prefix} Assume ${cmp}${str_value(Exp[0])}`;
+ }
+
+ print(JSON.stringify(edge, null, 4));
+ throw new Error("unhandled format error");
+}
+
+function str_edge(edge, env) {
+ const {Index, Kind} = edge;
+ const [src, dst] = Index;
+ const prefix = `[${src},${dst}]`;
+
+ if (Kind == "Call")
+ return str_call(prefix, edge, env);
+ if (Kind == 'Assign')
+ return str_assign(prefix, edge);
+ if (Kind == 'Assume')
+ return str_assume(prefix, edge);
+ if (Kind == 'Loop')
+ return str_loop(prefix, edge);
+
+ print(JSON.stringify(edge, null, 4));
+ throw "unhandled edge type";
+}
+
+function str(unknown) {
+ if ("Name" in unknown) {
+ return str_Variable(unknown);
+ } else if ("Index" in unknown) {
+ // Note: Variable also has .Index, with a different meaning.
+ return str_edge(unknown);
+ } else if ("Type" in unknown) {
+ if ("Variable" in unknown) {
+ return str_definition(unknown);
+ } else {
+ return str_Type(unknown);
+ }
+ } else if ("Kind" in unknown) {
+ if ("BlockId" in unknown)
+ return str_Variable(unknown);
+ return str_value(unknown);
+ }
+ return "unknown";
+}
+
+function jdump(x) {
+ print(JSON.stringify(x, null, 4));
+ quit(0);
+}
diff --git a/js/src/devtools/rootAnalysis/expect.b2g.json b/js/src/devtools/rootAnalysis/expect.b2g.json
new file mode 100644
index 0000000000..06f2beb36f
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/expect.b2g.json
@@ -0,0 +1,3 @@
+{
+ "expect-hazards": 0
+}
diff --git a/js/src/devtools/rootAnalysis/expect.browser.json b/js/src/devtools/rootAnalysis/expect.browser.json
new file mode 100644
index 0000000000..06f2beb36f
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/expect.browser.json
@@ -0,0 +1,3 @@
+{
+ "expect-hazards": 0
+}
diff --git a/js/src/devtools/rootAnalysis/expect.shell.json b/js/src/devtools/rootAnalysis/expect.shell.json
new file mode 100644
index 0000000000..06f2beb36f
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/expect.shell.json
@@ -0,0 +1,3 @@
+{
+ "expect-hazards": 0
+}
diff --git a/js/src/devtools/rootAnalysis/explain.py b/js/src/devtools/rootAnalysis/explain.py
new file mode 100755
index 0000000000..2fb45e07f9
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/explain.py
@@ -0,0 +1,345 @@
+#!/usr/bin/python3
+# 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/.
+
+
+import argparse
+import json
+import pathlib
+import re
+from html import escape
+
+SRCDIR = pathlib.Path(__file__).parent.parent.parent.absolute()
+
+parser = argparse.ArgumentParser(
+ description="Convert the JSON output of the hazard analysis into various text files describing the results.",
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+)
+parser.add_argument("--verbose", type=bool, default=False, help="verbose output")
+
+inputs = parser.add_argument_group("Input")
+inputs.add_argument(
+ "rootingHazards",
+ nargs="?",
+ default="rootingHazards.json",
+ help="JSON input file describing the output of the hazard analysis",
+)
+
+outputs = parser.add_argument_group("Output")
+outputs.add_argument(
+ "gcFunctions",
+ nargs="?",
+ default="gcFunctions.txt",
+ help="file containing a list of functions that can GC",
+)
+outputs.add_argument(
+ "hazards",
+ nargs="?",
+ default="hazards.txt",
+ help="file containing the rooting hazards found",
+)
+outputs.add_argument(
+ "extra",
+ nargs="?",
+ default="unnecessary.txt",
+ help="file containing unnecessary roots",
+)
+outputs.add_argument(
+ "refs",
+ nargs="?",
+ default="refs.txt",
+ help="file containing a list of unsafe references to unrooted values",
+)
+outputs.add_argument(
+ "html",
+ nargs="?",
+ default="hazards.html",
+ help="HTML-formatted file with the hazards found",
+)
+
+args = parser.parse_args()
+
+
+# Imitate splitFunction from utility.js.
+def splitfunc(full):
+ idx = full.find("$")
+ if idx == -1:
+ return (full, full)
+ return (full[0:idx], full[idx + 1 :])
+
+
+def print_header(outfh):
+ print(
+ """\
+<!DOCTYPE html>
+<head>
+<meta charset="utf-8">
+<style>
+input {
+ position: absolute;
+ opacity: 0;
+ z-index: -1;
+}
+tt {
+ background: #eee;
+}
+.tab-label {
+ cursor: s-resize;
+}
+.tab-label a {
+ color: #222;
+}
+.tab-label:hover {
+ background: #eee;
+}
+.tab-label::after {
+ content: " \\25B6";
+ width: 1em;
+ height: 1em;
+ color: #75f;
+ text-align: center;
+ transition: all 0.35s;
+}
+.accorntent {
+ max-height: 0;
+ padding: 0 1em;
+ color: #2c3e50;
+ overflow: hidden;
+ background: white;
+ transition: all 0.35s;
+}
+
+input:checked + .tab-label::after {
+ transform: rotate(90deg);
+ content: " \\25BC";
+}
+input:checked + .tab-label {
+ cursor: n-resize;
+}
+input:checked ~ .accorntent {
+ max-height: 100vh;
+}
+</style>
+</head>
+<body>""",
+ file=outfh,
+ )
+
+
+def print_footer(outfh):
+ print("</ol></body>", file=outfh)
+
+
+def sourcelink(symbol=None, loc=None, range=None):
+ if symbol:
+ return f"https://searchfox.org/mozilla-central/search?q=symbol:{symbol}"
+ elif range:
+ filename, lineno = loc.split(":")
+ [f0, l0] = range[0]
+ [f1, l1] = range[1]
+ if f0 == f1 and l1 > l0:
+ return f"../{filename}?L={l0}-{l1 - 1}#{l0}"
+ else:
+ return f"../{filename}?L={l0}#{l0}"
+ elif loc:
+ filename, lineno = loc.split(":")
+ return f"../{filename}?L={lineno}#{lineno}"
+ else:
+ raise Exception("missing argument to sourcelink()")
+
+
+def quoted_dict(d):
+ return {k: escape(v) for k, v in d.items() if type(v) == str}
+
+
+num_hazards = 0
+num_refs = 0
+num_missing = 0
+
+try:
+ with open(args.rootingHazards) as rootingHazards, open(
+ args.hazards, "w"
+ ) as hazards, open(args.extra, "w") as extra, open(args.refs, "w") as refs, open(
+ args.html, "w"
+ ) as html:
+ current_gcFunction = None
+
+ hazardousGCFunctions = set()
+
+ results = json.load(rootingHazards)
+ print_header(html)
+
+ when = min((r for r in results if r["record"] == "time"), key=lambda r: r["t"])[
+ "iso"
+ ]
+ line = f"Time: {when}"
+ print(line, file=hazards)
+ print(line, file=extra)
+ print(line, file=refs)
+
+ checkboxCounter = 0
+ hazard_results = []
+ seen_time = False
+ for result in results:
+ if result["record"] == "unrooted":
+ hazard_results.append(result)
+ gccall_mangled, _ = splitfunc(result["gccall"])
+ hazardousGCFunctions.add(gccall_mangled)
+ if not result.get("expected"):
+ num_hazards += 1
+
+ elif result["record"] == "unnecessary":
+ print(
+ "\nFunction '{mangled}' has unnecessary root '{variable}' of type {type} at {loc}".format(
+ **result
+ ),
+ file=extra,
+ )
+
+ elif result["record"] == "address":
+ print(
+ (
+ "\nFunction '{functionName}'"
+ " takes unsafe address of unrooted '{variable}'"
+ " at {loc}"
+ ).format(**result),
+ file=refs,
+ )
+ num_refs += 1
+
+ elif result["record"] == "missing":
+ print(
+ "\nFunction '{functionName}' expected hazard(s) but none were found at {loc}".format(
+ **result
+ ),
+ file=hazards,
+ )
+ num_missing += 1
+
+ readable2mangled = {}
+ with open(args.gcFunctions) as gcFunctions:
+ gcExplanations = {} # gcFunction => stack showing why it can GC
+
+ current_func = None
+ explanation = []
+ for line in gcFunctions:
+ if m := re.match(r"^GC Function: (.*)", line):
+ if current_func:
+ gcExplanations[splitfunc(current_func)[0]] = explanation
+ functionName = m.group(1)
+ mangled, readable = splitfunc(functionName)
+ if mangled not in hazardousGCFunctions:
+ current_func = None
+ continue
+ current_func = functionName
+ if readable != mangled:
+ readable2mangled[readable] = mangled
+ # TODO: store the mangled name here, and change
+ # gcFunctions.txt -> gcFunctions.json and key off of the mangled name.
+ explanation = [readable]
+ elif current_func:
+ explanation.append(line.strip())
+ if current_func:
+ gcExplanations[splitfunc(current_func)[0]] = explanation
+
+ print(
+ "Found %d hazards, %d unsafe references, %d missing."
+ % (num_hazards, num_refs, num_missing),
+ file=html,
+ )
+ print("<ol>", file=html)
+
+ for result in hazard_results:
+ (result["gccall_mangled"], result["gccall_readable"]) = splitfunc(
+ result["gccall"]
+ )
+ # Attempt to extract out the function name. Won't handle `Foo<int, Bar<int>>::Foo()`.
+ if m := re.search(r"((?:\w|:|<[^>]*?>)+)\(", result["gccall_readable"]):
+ result["gccall_short"] = m.group(1) + "()"
+ else:
+ result["gccall_short"] = result["gccall_readable"]
+ if result.get("expected"):
+ print("\nThis is expected, but ", end="", file=hazards)
+ else:
+ print("\nFunction ", end="", file=hazards)
+ print(
+ "'{readable}' has unrooted '{variable}'"
+ " of type '{type}' live across GC call '{gccall_readable}' at {loc}".format(
+ **result
+ ),
+ file=hazards,
+ )
+ for edge in result["trace"]:
+ print(" {lineText}: {edgeText}".format(**edge), file=hazards)
+ explanation = gcExplanations.get(result["gccall_mangled"])
+ explanation = explanation or gcExplanations.get(
+ readable2mangled.get(
+ result["gccall_readable"], result["gccall_readable"]
+ ),
+ [],
+ )
+ if explanation:
+ print("GC Function: " + explanation[0], file=hazards)
+ for func in explanation[1:]:
+ print(" " + func, file=hazards)
+ print(file=hazards)
+
+ if result.get("expected"):
+ continue
+
+ cfgid = f"CFG_{checkboxCounter}"
+ gcid = f"GC_{checkboxCounter}"
+ checkboxCounter += 1
+ print(
+ (
+ "<li><ul>\n"
+ "<li>Function <a href='{symbol_url}'>{readable}</a>\n"
+ "<li>has unrooted <tt>{variable}</tt> of type '<tt>{type}</tt>'\n"
+ "<li><input type='checkbox' id='{cfgid}'><label class='tab-label' for='{cfgid}'>"
+ "live across GC call to"
+ "</label>\n"
+ "<div class='accorntent'>\n"
+ ).format(
+ **quoted_dict(result),
+ symbol_url=sourcelink(symbol=result["mangled"]),
+ cfgid=cfgid,
+ ),
+ file=html,
+ )
+ for edge in result["trace"]:
+ print(
+ "<pre> {lineText}: {edgeText}</pre>".format(**quoted_dict(edge)),
+ file=html,
+ )
+ print("</div>", file=html)
+ print(
+ "<li><input type='checkbox' id='{gcid}'><label class='tab-label' for='{gcid}'>"
+ "<a href='{loc_url}'><tt>{gccall_short}</tt></a> at {loc}"
+ "</label>\n"
+ "<div class='accorntent'>".format(
+ **quoted_dict(result),
+ loc_url=sourcelink(range=result["gcrange"], loc=result["loc"]),
+ gcid=gcid,
+ ),
+ file=html,
+ )
+ for func in explanation:
+ print(f"<pre>{escape(func)}</pre>", file=html)
+ print("</div><hr></ul>", file=html)
+
+ print_footer(html)
+
+except IOError as e:
+ print("Failed: %s" % str(e))
+
+if args.verbose:
+ print("Wrote %s" % args.hazards)
+ print("Wrote %s" % args.extra)
+ print("Wrote %s" % args.refs)
+ print("Wrote %s" % args.html)
+
+print(
+ "Found %d hazards %d unsafe references %d missing"
+ % (num_hazards, num_refs, num_missing)
+)
diff --git a/js/src/devtools/rootAnalysis/gen-hazards.sh b/js/src/devtools/rootAnalysis/gen-hazards.sh
new file mode 100755
index 0000000000..7007969a14
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/gen-hazards.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+set -e
+
+JOBS="$1"
+
+for j in $(seq $JOBS); do
+ env PATH=$PATH:$SIXGILL/bin XDB=$SIXGILL/bin/xdb.so $JS $ANALYZE gcFunctions.lst suppressedFunctions.lst gcTypes.txt $j $JOBS tmp.$j > rootingHazards.$j &
+done
+
+wait
+
+for j in $(seq $JOBS); do
+ cat rootingHazards.$j
+done
diff --git a/js/src/devtools/rootAnalysis/loadCallgraph.js b/js/src/devtools/rootAnalysis/loadCallgraph.js
new file mode 100644
index 0000000000..0a388f4de1
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/loadCallgraph.js
@@ -0,0 +1,590 @@
+/* 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/. */
+
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+
+"use strict";
+
+loadRelativeToScript('utility.js');
+loadRelativeToScript('callgraph.js');
+
+// Functions come out of sixgill in the form "mangled$readable". The mangled
+// name is Truth. One mangled name might correspond to multiple readable names,
+// for multiple reasons, including (1) sixgill/gcc doesn't always qualify types
+// the same way or de-typedef the same amount; (2) sixgill's output treats
+// references and pointers the same, and so doesn't distinguish them, but C++
+// treats them as separate for overloading and linking; (3) (identical)
+// destructors sometimes have an int32 parameter, sometimes not.
+//
+// The readable names are useful because they're far more meaningful to the
+// user, and are what should show up in reports and questions to mrgiggles. At
+// least in most cases, it's fine to have the extra mangled name tacked onto
+// the beginning for these.
+//
+// The strategy used is to separate out the pieces whenever they are read in,
+// create a table mapping mangled names to all readable names, and use the
+// mangled names in all computation -- except for limited circumstances when
+// the readable name is used in annotations.
+//
+// Note that callgraph.txt uses a compressed representation -- each name is
+// mapped to an integer, and those integers are what is recorded in the edges.
+// But the integers depend on the full name, whereas the true edge should only
+// consider the mangled name. And some of the names encoded in callgraph.txt
+// are FieldCalls, not just function names.
+
+var gcEdges = {};
+
+// Returns whether the function was added. (It will be refused if it was
+// already there, or if attrs or annotations say it shouldn't be added.)
+function addGCFunction(caller, reason, gcFunctions, functionAttrs, functions)
+{
+ if (functionAttrs[caller] && functionAttrs[caller][1] & ATTR_GC_SUPPRESSED)
+ return false;
+
+ if (ignoreGCFunction(functions.name[caller], functions.readableName))
+ return false;
+
+ if (!(caller in gcFunctions)) {
+ gcFunctions[caller] = reason;
+ return true;
+ }
+
+ return false;
+}
+
+// Every caller->callee callsite is associated with attrs saying what is
+// allowed at that callsite (eg if it's in a GC suppression zone, it would have
+// ATTR_GC_SUPPRESSED set.) A given caller might call the same callee multiple
+// times, with different attributes. Associate the <caller,callee> edge with
+// the intersection (AND) and disjunction (OR) of all of the callsites' attrs.
+// The AND ('all') says what attributes are present for all callers; the OR
+// ('any') says what attributes are present on any caller. Preserve the
+// original order.
+//
+// During the same scan, build callersOf from calleesOf.
+function generate_callgraph(rawCallees) {
+ const callersOf = new Map();
+ const calleesOf = new Map();
+
+ for (const [caller, callee_attrs] of rawCallees) {
+ const ordered_callees = [];
+
+ // callee_attrs is a list of {callee,any,all} objects.
+ const callee2any = new Map();
+ const callee2all = new Map();
+ for (const {callee, any, all} of callee_attrs) {
+ const prev_any = callee2any.get(callee);
+ if (prev_any === undefined) {
+ assert(!callee2all.has(callee));
+ callee2any.set(callee, any);
+ callee2all.set(callee, all);
+ ordered_callees.push(callee);
+ } else {
+ const prev_all = callee2all.get(callee);
+ callee2any.set(callee, prev_any | any);
+ callee2all.set(callee, prev_all & all);
+ }
+ }
+
+ // Update the contents of callee_attrs to contain a single entry for
+ // each callee, with its attrs set to the AND of the attrs observed at
+ // all callsites within this caller function.
+ callee_attrs.length = 0;
+ for (const callee of ordered_callees) {
+ const any = callee2any.get(callee);
+ const all = callee2all.get(callee);
+ if (!calleesOf.has(caller))
+ calleesOf.set(caller, new Map());
+ calleesOf.get(caller).set(callee, {any, all});
+ if (!callersOf.has(callee))
+ callersOf.set(callee, new Map());
+ callersOf.get(callee).set(caller, {any, all});
+ }
+ }
+
+ return {callersOf, calleesOf};
+}
+
+// Returns object mapping mangled => reason for GCing
+function loadRawCallgraphFile(file, verbose)
+{
+ const functions = {
+ // "Map" from identifier to mangled name, or sometimes to a Class.Field name.
+ name: [""],
+
+ // map from mangled name => list of readable names
+ readableName: {},
+
+ mangledToId: {}
+ };
+
+ const fieldCallAttrs = {};
+ const fieldCallCSU = new Map(); // map from full field name id => csu name
+
+ // set of mangled names (map from mangled name => [any,all])
+ var functionAttrs = {};
+
+ const gcCalls = [];
+ const indirectCalls = [];
+
+ // map from mangled => list of tuples of {'callee':mangled, 'any':intset, 'all':intset}
+ const rawCallees = new Map();
+
+ for (let line of readFileLines_gen(file)) {
+ line = line.replace(/\n/, "");
+
+ let match;
+ if (match = line.charAt(0) == "#" && /^\#(\d+) (.*)/.exec(line)) {
+ const [ _, id, mangled ] = match;
+ assert(functions.name.length == id);
+ functions.name.push(mangled);
+ functions.mangledToId[mangled] = id|0;
+ continue;
+ }
+ if (match = line.charAt(0) == "=" && /^= (\d+) (.*)/.exec(line)) {
+ const [ _, id, readable ] = match;
+ const mangled = functions.name[id];
+ if (mangled in functions.readableName)
+ functions.readableName[mangled].push(readable);
+ else
+ functions.readableName[mangled] = [ readable ];
+ continue;
+ }
+
+ let attrs = 0;
+ // Example line: D /17 6 7
+ //
+ // This means a direct call from 6 -> 7, but within a scope that
+ // applies attrs 0x1 and 0x10 to the callee.
+ //
+ // Look for a bit specifier and remove it from the line if found.
+ if (line.indexOf("/") != -1) {
+ match = /^(..)\/(\d+) (.*)/.exec(line);
+ line = match[1] + match[3];
+ attrs = match[2]|0;
+ }
+ const tag = line.charAt(0);
+ if (match = tag == 'I' && /^I (\d+) VARIABLE ([^\,]*)/.exec(line)) {
+ const caller = match[1]|0;
+ const name = match[2];
+ if (indirectCallCannotGC(functions.name[caller], name))
+ attrs |= ATTR_GC_SUPPRESSED;
+ indirectCalls.push([caller, "IndirectCall: " + name, attrs]);
+ } else if (match = tag == 'F' && /^F (\d+) (\d+) CLASS (.*?) FIELD (.*)/.exec(line)) {
+ const caller = match[1]|0;
+ const fullfield = match[2]|0;
+ const csu = match[3];
+ const fullfield_str = csu + "." + match[4];
+ assert(functions.name[fullfield] == fullfield_str);
+ if (attrs)
+ fieldCallAttrs[fullfield] = attrs;
+ addToMappedList(rawCallees, caller, {callee:fullfield, any:attrs, all:attrs});
+ fieldCallCSU.set(fullfield, csu);
+
+ if (fieldCallCannotGC(csu, fullfield_str))
+ addToMappedList(rawCallees, fullfield, {callee:ID.nogcfunc, any:0, all:0});
+ else
+ addToMappedList(rawCallees, fullfield, {callee:ID.anyfunc, any:0, all:0});
+ } else if (match = tag == 'V' && /^V (\d+) (\d+) CLASS (.*?) FIELD (.*)/.exec(line)) {
+ // V tag is no longer used, but we are still emitting it becasue it
+ // can be helpful to understand what's going on.
+ } else if (match = tag == 'D' && /^D (\d+) (\d+)/.exec(line)) {
+ const caller = match[1]|0;
+ const callee = match[2]|0;
+ addToMappedList(rawCallees, caller, {callee, any:attrs, all:attrs});
+ } else if (match = tag == 'R' && /^R (\d+) (\d+)/.exec(line)) {
+ assert(false, "R tag is no longer used");
+ } else if (match = tag == 'T' && /^T (\d+) (.*)/.exec(line)) {
+ const id = match[1]|0;
+ let tag = match[2];
+ if (tag == 'GC Call')
+ gcCalls.push(id);
+ } else {
+ assert(false, "Invalid format in callgraph line: " + line);
+ }
+ }
+
+ if (verbose) {
+ printErr("Loaded[verbose=" + verbose + "] " + file);
+ }
+
+ return {
+ fieldCallAttrs,
+ fieldCallCSU,
+ gcCalls,
+ indirectCalls,
+ rawCallees,
+ functions
+ };
+}
+
+// Take a set of rawcalls filenames (as in, the raw callgraph data output by
+// computeCallgraph.js) and combine them into a global callgraph, renumbering
+// the IDs as needed.
+function mergeRawCallgraphs(filenames, verbose) {
+ let d;
+ for (const filename of filenames) {
+ const raw = loadRawCallgraphFile(filename, verbose);
+ if (!d) {
+ d = raw;
+ continue;
+ }
+
+ const {
+ fieldCallAttrs,
+ fieldCallCSU,
+ gcCalls,
+ indirectCalls,
+ rawCallees,
+ functions
+ } = raw;
+
+ // Compute the ID mapping. Incoming functions that already have an ID
+ // will be mapped to that ID; new ones will allocate a fresh ID.
+ const remap = new Array(functions.name.length);
+ for (let i = 1; i < functions.name.length; i++) {
+ const mangled = functions.name[i];
+ const old_id = d.functions.mangledToId[mangled]
+ if (old_id) {
+ remap[i] = old_id;
+ } else {
+ const newid = d.functions.name.length;
+ d.functions.mangledToId[mangled] = newid;
+ d.functions.name.push(mangled);
+ remap[i] = newid;
+ assert(!(mangled in d.functions.readableName), mangled + " readable name is already found");
+ const readables = functions.readableName[mangled];
+ if (readables !== undefined)
+ d.functions.readableName[mangled] = readables;
+ }
+ }
+
+ for (const [fullfield, attrs] of Object.entries(fieldCallAttrs))
+ d.fieldCallAttrs[remap[fullfield]] = attrs;
+ for (const [fullfield, csu] of fieldCallCSU.entries())
+ d.fieldCallCSU.set(remap[fullfield], csu);
+ for (const call of gcCalls)
+ d.gcCalls.push(remap[call]);
+ for (const [caller, name, attrs] of indirectCalls)
+ d.indirectCalls.push([remap[caller], name, attrs]);
+ for (const [caller, callees] of rawCallees) {
+ for (const {callee, any, all} of callees) {
+ addToMappedList(d.rawCallees, remap[caller]|0, {callee:remap[callee], any, all});
+ }
+ }
+ }
+
+ return d;
+}
+
+function loadCallgraph(files, verbose)
+{
+ const {
+ fieldCallAttrs,
+ fieldCallCSU,
+ gcCalls,
+ indirectCalls,
+ rawCallees,
+ functions
+ } = mergeRawCallgraphs(files, verbose);
+
+ assert(ID.jscode == functions.mangledToId["(js-code)"]);
+ assert(ID.anyfunc == functions.mangledToId["(any-function)"]);
+ assert(ID.nogcfunc == functions.mangledToId["(nogc-function)"]);
+ assert(ID.gc == functions.mangledToId["(GC)"]);
+
+ addToMappedList(rawCallees, functions.mangledToId["(any-function)"], {callee:ID.gc, any:0, all:0});
+
+ // Compute functionAttrs: it should contain the set of functions that
+ // are *always* called within some sort of limited context (eg GC
+ // suppression).
+
+ // set of mangled names (map from mangled name => [any,all])
+ const functionAttrs = {};
+
+ // Initialize to field calls with attrs set.
+ for (var [name, attrs] of Object.entries(fieldCallAttrs))
+ functionAttrs[name] = [attrs, attrs];
+
+ // map from ID => reason
+ const gcFunctions = { [ID.gc]: 'internal' };
+
+ // Add in any extra functions at the end. (If we did this early, it would
+ // mess up the id <-> name correspondence. Also, we need to know if the
+ // functions even exist in the first place.)
+ for (var func of extraGCFunctions(functions.readableName)) {
+ addGCFunction(functions.mangledToId[func], "annotation", gcFunctions, functionAttrs, functions);
+ }
+
+ for (const func of gcCalls)
+ addToMappedList(rawCallees, func, {callee:ID.gc, any:0, all:0});
+
+ for (const [caller, indirect, attrs] of indirectCalls) {
+ const id = functions.name.length;
+ functions.name.push(indirect);
+ functions.mangledToId[indirect] = id;
+ addToMappedList(rawCallees, caller, {callee:id, any:attrs, all:attrs});
+ addToMappedList(rawCallees, id, {callee:ID.anyfunc, any:0, all:0});
+ }
+
+ // Callers have a list of callees, with duplicates (if the same function is
+ // called more than once.) Merge the repeated calls, only keeping attrs
+ // that are in force for *every* callsite of that callee. Also, generate
+ // the callersOf table at the same time.
+ //
+ // calleesOf : map from mangled => {mangled callee => {'any':intset, 'all':intset}}
+ // callersOf : map from mangled => {mangled caller => {'any':intset, 'all':intset}}
+ const {callersOf, calleesOf} = generate_callgraph(rawCallees);
+
+ // Compute functionAttrs: it should contain the set of functions that
+ // are *always* called within some sort of limited context (eg GC
+ // suppression).
+
+ // Initialize to field calls with attrs set.
+ for (var [name, attrs] of Object.entries(fieldCallAttrs))
+ functionAttrs[name] = [attrs, attrs];
+
+ // Initialize functionAttrs to the set of all functions, where each one is
+ // maximally attributed, and return a worklist containing all simple roots
+ // (nodes with no callers).
+ const simple_roots = gather_simple_roots(functionAttrs, calleesOf, callersOf);
+
+ // Traverse the graph, spreading the attrs down from the roots.
+ propagate_attrs(simple_roots, functionAttrs, calleesOf);
+
+ // There are a surprising number of "recursive roots", where there is a
+ // cycle of functions calling each other but not called by anything else,
+ // and these roots may also have descendants. Now that the above traversal
+ // has eliminated everything reachable from simple roots, traverse the
+ // remaining graph to gather up a representative function from each root
+ // cycle.
+ //
+ // Simple example: in the JS shell build, moz_xstrdup calls itself, but
+ // there are no calls to it from within js/src.
+ const recursive_roots = gather_recursive_roots(functionAttrs, calleesOf, callersOf, functions);
+
+ // And do a final traversal starting with the recursive roots.
+ propagate_attrs(recursive_roots, functionAttrs, calleesOf);
+
+ for (const [f, [any, all]] of Object.entries(functionAttrs)) {
+ // Throw out all functions with no attrs set, to reduce the size of the
+ // output. From now on, "not in functionAttrs" means [any=0, all=0].
+ if (any == 0 && all == 0)
+ delete functionAttrs[f];
+
+ // Remove GC-suppressed functions from the set of functions known to GC.
+ // Also remove functions only reachable through calls that have been
+ // replaced.
+ if (all & (ATTR_GC_SUPPRESSED | ATTR_REPLACED))
+ delete gcFunctions[name];
+ }
+
+ // functionAttrs now contains all functions that are ever called in an
+ // attributed context, based on the known callgraph (i.e., calls through
+ // function pointers are not taken into consideration.)
+
+ // Sanity check to make sure the callgraph has some functions annotated as
+ // GC Calls. This is mostly a check to be sure the earlier processing
+ // succeeded (as opposed to, say, running on empty xdb files because you
+ // didn't actually compile anything interesting.)
+ assert(gcCalls.length > 0, "No GC functions found!");
+
+ // Initialize the worklist to all known gcFunctions.
+ const worklist = [ID.gc];
+
+ // Include all field calls (but not virtual method calls).
+ for (const [name, csuName] of fieldCallCSU) {
+ const fullFieldName = functions.name[name];
+ if (!fieldCallCannotGC(csuName, fullFieldName)) {
+ gcFunctions[name] = 'arbitrary function pointer ' + fullFieldName;
+ worklist.push(name);
+ }
+ }
+
+ // Recursively find all callers not always called in a GC suppression
+ // context, and add them to the set of gcFunctions.
+ while (worklist.length) {
+ name = worklist.shift();
+ assert(name in gcFunctions, "gcFunctions does not contain " + name);
+ if (!callersOf.has(name))
+ continue;
+ for (const [caller, {any, all}] of callersOf.get(name)) {
+ if ((all & (ATTR_GC_SUPPRESSED | ATTR_REPLACED)) == 0) {
+ if (addGCFunction(caller, name, gcFunctions, functionAttrs, functions))
+ worklist.push(caller);
+ }
+ }
+ }
+
+ // Convert functionAttrs to limitedFunctions (using mangled names instead
+ // of ids.)
+
+ // set of mangled names (map from mangled name => {any,all,recursive_root:bool}
+ var limitedFunctions = {};
+
+ for (const [id, [any, all]] of Object.entries(functionAttrs)) {
+ if (all) {
+ limitedFunctions[functions.name[id]] = { attributes: all };
+ }
+ }
+
+ for (const [id, limits, label] of recursive_roots) {
+ const name = functions.name[id];
+ const s = limitedFunctions[name] || (limitedFunctions[name] = {});
+ s.recursive_root = true;
+ }
+
+ // Remap ids to mangled names.
+ const namedGCFunctions = {};
+ for (const [caller, reason] of Object.entries(gcFunctions)) {
+ namedGCFunctions[functions.name[caller]] = functions.name[reason] || reason;
+ }
+
+ return {
+ gcFunctions: namedGCFunctions,
+ functions,
+ calleesOf,
+ callersOf,
+ limitedFunctions
+ };
+}
+
+function saveCallgraph(functions, calleesOf) {
+ // Write out all the ids and their readable names.
+ let id = -1;
+ for (const name of functions.name) {
+ id += 1;
+ if (id == 0) continue;
+ print(`#${id} ${name}`);
+ for (const readable of (functions.readableName[name] || [])) {
+ if (readable != name)
+ print(`= ${id} ${readable}`);
+ }
+ }
+
+ // Omit field calls for now; let them appear as if they were functions.
+
+ const attrstring = range => range.any || range.all ? `${range.all}:${range.any} ` : '';
+ for (const [caller, callees] of calleesOf) {
+ for (const [callee, attrs] of callees) {
+ print(`D ${attrstring(attrs)}${caller} ${callee}`);
+ }
+ }
+
+ // Omit tags for now. This really should preserve all tags. The "GC Call"
+ // tag will already be represented in the graph by having an edge to the
+ // "(GC)" node.
+}
+
+// Return a worklist of functions with no callers, and also initialize
+// functionAttrs to the set of all functions, each mapped to
+// [ATTRS_NONE, ATTRS_UNVISITED].
+function gather_simple_roots(functionAttrs, calleesOf, callersOf) {
+ const roots = [];
+ for (const callee of callersOf.keys())
+ functionAttrs[callee] = [ATTRS_NONE, ATTRS_UNVISITED];
+ for (const caller of calleesOf.keys()) {
+ functionAttrs[caller] = [ATTRS_NONE, ATTRS_UNVISITED];
+ if (!callersOf.has(caller))
+ roots.push([caller, ATTRS_NONE, 'root']);
+ }
+
+ return roots;
+}
+
+// Recursively traverse the callgraph from the roots. Recurse through every
+// edge that weakens the attrs. (Attrs that entirely disappear, ie go to a zero
+// intset, will be removed from functionAttrs.)
+function propagate_attrs(roots, functionAttrs, calleesOf) {
+ const worklist = Array.from(roots);
+ let top = worklist.length;
+ while (top > 0) {
+ // Consider caller where (graph) -> caller -> (0 or more callees)
+ // 'callercaller' is for debugging.
+ const [caller, edge_attrs, callercaller] = worklist[--top];
+ assert(caller in functionAttrs);
+ const [prev_any, prev_all] = functionAttrs[caller];
+ assert(prev_any !== undefined);
+ assert(prev_all !== undefined);
+ const [new_any, new_all] = [prev_any | edge_attrs, prev_all & edge_attrs];
+ if (prev_any != new_any || prev_all != new_all) {
+ // Update function attrs, then recurse to the children if anything
+ // was updated.
+ functionAttrs[caller] = [new_any, new_all];
+ for (const [callee, {any, all}] of (calleesOf.get(caller) || new Map))
+ worklist[top++] = [callee, all | edge_attrs, caller];
+ }
+ }
+}
+
+// Mutually-recursive roots and their descendants will not have been visited,
+// and will still be set to [0, ATTRS_UNVISITED]. Scan through and gather them.
+function gather_recursive_roots(functionAttrs, calleesOf, callersOf, functions) {
+ const roots = [];
+
+ // Pick any node. Mark everything reachable by adding to a 'seen' set. At
+ // the end, if there are any incoming edges to that node from an unmarked
+ // node, then it is not a root. Otherwise, mark the node as a root. (There
+ // will be at least one back edge coming into the node from a marked node
+ // in this case, since otherwise it would have already been considered to
+ // be a root.)
+ //
+ // Repeat with remaining unmarked nodes until all nodes are marked.
+ const seen = new Set();
+ for (let [func, [any, all]] of Object.entries(functionAttrs)) {
+ func = func|0;
+ if (all != ATTRS_UNVISITED)
+ continue;
+
+ // We should only be looking at nodes with callers, since otherwise
+ // they would have been handled in the previous pass!
+ assert(callersOf.has(func));
+ assert(callersOf.get(func).size > 0);
+
+ if (seen.has(func))
+ continue;
+
+ const work = [func];
+ while (work.length > 0) {
+ const f = work.pop();
+ if (!calleesOf.has(f)) continue;
+ for (const callee of calleesOf.get(f).keys()) {
+ if (!seen.has(callee) &&
+ callee != func &&
+ functionAttrs[callee][1] == ATTRS_UNVISITED)
+ {
+ work.push(callee);
+ seen.add(callee);
+ }
+ }
+ }
+
+ assert(!seen.has(func));
+ seen.add(func);
+ if ([...callersOf.get(func).keys()].findIndex(f => !seen.has(f)) == -1) {
+ // No unmarked incoming edges, including self-edges, so this is a
+ // (recursive) root.
+ roots.push([func, ATTRS_NONE, 'recursive-root']);
+ }
+ }
+
+ return roots;
+
+ tmp = calleesOf;
+ calleesOf = {};
+ for (const [callerId, callees] of Object.entries(calleesOf)) {
+ const caller = functionNames[callerId];
+ for (const {calleeId, limits} of callees)
+ calleesOf[caller][functionNames[calleeId]] = limits;
+ }
+
+ tmp = callersOf;
+ callersOf = {};
+ for (const [calleeId, callers] of Object.entries(callersOf)) {
+ const callee = functionNames[calleeId];
+ callersOf[callee] = {};
+ for (const {callerId, limits} of callers)
+ callersOf[callee][functionNames[caller]] = limits;
+ }
+}
diff --git a/js/src/devtools/rootAnalysis/mach_commands.py b/js/src/devtools/rootAnalysis/mach_commands.py
new file mode 100644
index 0000000000..c2fc1980c9
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/mach_commands.py
@@ -0,0 +1,690 @@
+# -*- coding: utf-8 -*-
+
+# 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/.
+
+
+import argparse
+import html
+import json
+import logging
+import os
+import re
+import textwrap
+import webbrowser
+
+# Command files like this are listed in build/mach_initialize.py in alphabetical
+# order, but we need to access commands earlier in the sorted order to grab
+# their arguments. Force them to load now.
+import mozbuild.artifact_commands # NOQA: F401
+import mozbuild.build_commands # NOQA: F401
+import mozhttpd
+from mach.base import FailedCommandError, MachError
+from mach.decorators import Command, CommandArgument, SubCommand
+from mach.registrar import Registrar
+from mozbuild.base import BuildEnvironmentNotFoundException
+from mozbuild.mozconfig import MozconfigLoader
+
+
+# Use a decorator to copy command arguments off of the named command. Instead
+# of a decorator, this could be straight code that edits eg
+# MachCommands.build_shell._mach_command.arguments, but that looked uglier.
+def inherit_command_args(command, subcommand=None):
+ """Decorator for inheriting all command-line arguments from `mach build`.
+
+ This should come earlier in the source file than @Command or @SubCommand,
+ because it relies on that decorator having run first."""
+
+ def inherited(func):
+ handler = Registrar.command_handlers.get(command)
+ if handler is not None and subcommand is not None:
+ handler = handler.subcommand_handlers.get(subcommand)
+ if handler is None:
+ raise MachError(
+ "{} command unknown or not yet loaded".format(
+ command if subcommand is None else command + " " + subcommand
+ )
+ )
+ func._mach_command.arguments.extend(handler.arguments)
+ return func
+
+ return inherited
+
+
+def state_dir():
+ return os.environ.get("MOZBUILD_STATE_PATH", os.path.expanduser("~/.mozbuild"))
+
+
+def tools_dir():
+ if os.environ.get("MOZ_FETCHES_DIR"):
+ # In automation, tools are provided by toolchain dependencies.
+ return os.path.join(os.environ["HOME"], os.environ["MOZ_FETCHES_DIR"])
+
+ # In development, `mach hazard bootstrap` installs the tools separately
+ # to avoid colliding with the "main" compiler versions, which can
+ # change separately (and the precompiled sixgill and compiler version
+ # must match exactly).
+ return os.path.join(state_dir(), "hazard-tools")
+
+
+def sixgill_dir():
+ return os.path.join(tools_dir(), "sixgill")
+
+
+def gcc_dir():
+ return os.path.join(tools_dir(), "gcc")
+
+
+def script_dir(command_context):
+ return os.path.join(command_context.topsrcdir, "js/src/devtools/rootAnalysis")
+
+
+def get_work_dir(command_context, project, given):
+ if given is not None:
+ return given
+ return os.path.join(command_context.topsrcdir, "haz-" + project)
+
+
+def get_objdir(command_context, kwargs):
+ project = kwargs["project"]
+ objdir = kwargs["haz_objdir"]
+ if objdir is None:
+ objdir = os.environ.get("HAZ_OBJDIR")
+ if objdir is None:
+ objdir = os.path.join(command_context.topsrcdir, "obj-analyzed-" + project)
+ return objdir
+
+
+def ensure_dir_exists(dir):
+ os.makedirs(dir, exist_ok=True)
+ return dir
+
+
+# Force the use of hazard-compatible installs of tools.
+def setup_env_for_tools(env):
+ gccbin = os.path.join(gcc_dir(), "bin")
+ env["CC"] = os.path.join(gccbin, "gcc")
+ env["CXX"] = os.path.join(gccbin, "g++")
+ env["PATH"] = "{sixgill_dir}/usr/bin:{gccbin}:{PATH}".format(
+ sixgill_dir=sixgill_dir(), gccbin=gccbin, PATH=env["PATH"]
+ )
+
+
+def setup_env_for_shell(env, shell):
+ """Add JS shell directory to dynamic lib search path"""
+ for var in ("LD_LIBRARY_PATH", "DYLD_LIBRARY_PATH"):
+ env[var] = ":".join(p for p in (env.get(var), os.path.dirname(shell)) if p)
+
+
+@Command(
+ "hazards",
+ category="build",
+ order="declaration",
+ description="Commands for running the static analysis for GC rooting hazards",
+)
+def hazards(command_context):
+ """Commands related to performing the GC rooting hazard analysis"""
+ print("See `mach hazards --help` for a list of subcommands")
+
+
+@inherit_command_args("artifact", "toolchain")
+@SubCommand(
+ "hazards",
+ "bootstrap",
+ description="Install prerequisites for the hazard analysis",
+)
+def bootstrap(command_context, **kwargs):
+ orig_dir = os.getcwd()
+ os.chdir(ensure_dir_exists(tools_dir()))
+ try:
+ kwargs["from_build"] = ("linux64-gcc-sixgill", "linux64-gcc-9")
+ command_context._mach_context.commands.dispatch(
+ "artifact", command_context._mach_context, subcommand="toolchain", **kwargs
+ )
+ finally:
+ os.chdir(orig_dir)
+
+
+CLOBBER_CHOICES = {"objdir", "work", "shell", "all"}
+
+
+@SubCommand("hazards", "clobber", description="Clean up hazard-related files")
+@CommandArgument("--project", default="browser", help="Build the given project.")
+@CommandArgument("--application", dest="project", help="Build the given project.")
+@CommandArgument("--haz-objdir", default=None, help="Hazard analysis objdir.")
+@CommandArgument(
+ "--work-dir", default=None, help="Directory for output and working files."
+)
+@CommandArgument(
+ "what",
+ default=["objdir", "work"],
+ nargs="*",
+ help="Target to clobber, must be one of {{{}}} (default "
+ "objdir and work).".format(", ".join(CLOBBER_CHOICES)),
+)
+def clobber(command_context, what, **kwargs):
+ from mozbuild.controller.clobber import Clobberer
+
+ what = set(what)
+ if "all" in what:
+ what.update(CLOBBER_CHOICES)
+ invalid = what - CLOBBER_CHOICES
+ if invalid:
+ print(
+ "Unknown clobber target(s): {}. Choose from {{{}}}".format(
+ ", ".join(invalid), ", ".join(CLOBBER_CHOICES)
+ )
+ )
+ return 1
+
+ try:
+ substs = command_context.substs
+ except BuildEnvironmentNotFoundException:
+ substs = {}
+
+ if "objdir" in what:
+ objdir = get_objdir(command_context, kwargs)
+ print(f"removing {objdir}")
+ Clobberer(command_context.topsrcdir, objdir, substs).remove_objdir(full=True)
+ if "work" in what:
+ project = kwargs["project"]
+ work_dir = get_work_dir(command_context, project, kwargs["work_dir"])
+ print(f"removing {work_dir}")
+ Clobberer(command_context.topsrcdir, work_dir, substs).remove_objdir(full=True)
+ if "shell" in what:
+ objdir = os.path.join(command_context.topsrcdir, "obj-haz-shell")
+ print(f"removing {objdir}")
+ Clobberer(command_context.topsrcdir, objdir, substs).remove_objdir(full=True)
+
+
+@inherit_command_args("build")
+@SubCommand(
+ "hazards", "build-shell", description="Build a shell for the hazard analysis"
+)
+@CommandArgument(
+ "--mozconfig",
+ default=None,
+ metavar="FILENAME",
+ help="Build with the given mozconfig.",
+)
+def build_shell(command_context, **kwargs):
+ """Build a JS shell to use to run the rooting hazard analysis."""
+ # The JS shell requires some specific configuration settings to execute
+ # the hazard analysis code, and configuration is done via mozconfig.
+ # Subprocesses find MOZCONFIG in the environment, so we can't just
+ # modify the settings in this process's loaded version. Pass it through
+ # the environment.
+
+ default_mozconfig = "js/src/devtools/rootAnalysis/mozconfig.haz_shell"
+ mozconfig_path = (
+ kwargs.pop("mozconfig", None)
+ or os.environ.get("MOZCONFIG")
+ or default_mozconfig
+ )
+ mozconfig_path = os.path.join(command_context.topsrcdir, mozconfig_path)
+ loader = MozconfigLoader(command_context.topsrcdir)
+ mozconfig = loader.read_mozconfig(mozconfig_path)
+
+ # Validate the mozconfig settings in case the user overrode the default.
+ configure_args = mozconfig["configure_args"]
+ if "--enable-ctypes" not in configure_args:
+ raise FailedCommandError(
+ "ctypes required in hazard JS shell, mozconfig=" + mozconfig_path
+ )
+
+ # Transmit the mozconfig location to build subprocesses.
+ os.environ["MOZCONFIG"] = mozconfig_path
+
+ setup_env_for_tools(os.environ)
+
+ # Set a default objdir for the shell, for developer builds.
+ os.environ.setdefault(
+ "MOZ_OBJDIR", os.path.join(command_context.topsrcdir, "obj-haz-shell")
+ )
+
+ return command_context._mach_context.commands.dispatch(
+ "build", command_context._mach_context, **kwargs
+ )
+
+
+def read_json_file(filename):
+ with open(filename) as fh:
+ return json.load(fh)
+
+
+def ensure_shell(command_context, objdir):
+ if objdir is None:
+ objdir = os.path.join(command_context.topsrcdir, "obj-haz-shell")
+
+ try:
+ binaries = read_json_file(os.path.join(objdir, "binaries.json"))
+ info = [b for b in binaries["programs"] if b["program"] == "js"][0]
+ return os.path.join(objdir, info["install_target"], "js")
+ except (OSError, KeyError):
+ raise FailedCommandError(
+ """\
+no shell found in %s -- must build the JS shell with `mach hazards build-shell` first"""
+ % objdir
+ )
+
+
+def validate_mozconfig(command_context, kwargs):
+ app = kwargs.pop("project")
+ default_mozconfig = "js/src/devtools/rootAnalysis/mozconfig.%s" % app
+ mozconfig_path = (
+ kwargs.pop("mozconfig", None)
+ or os.environ.get("MOZCONFIG")
+ or default_mozconfig
+ )
+ mozconfig_path = os.path.join(command_context.topsrcdir, mozconfig_path)
+
+ loader = MozconfigLoader(command_context.topsrcdir)
+ mozconfig = loader.read_mozconfig(mozconfig_path)
+ configure_args = mozconfig["configure_args"]
+
+ # Require an explicit --enable-project/application=APP (even if you just
+ # want to build the default browser project.)
+ if (
+ "--enable-project=%s" % app not in configure_args
+ and "--enable-application=%s" % app not in configure_args
+ ):
+ raise FailedCommandError(
+ textwrap.dedent(
+ f"""\
+ mozconfig {mozconfig_path} builds wrong project.
+ unset MOZCONFIG to use the default {default_mozconfig}\
+ """
+ )
+ )
+
+ if not any("--with-compiler-wrapper" in a for a in configure_args):
+ raise FailedCommandError(
+ "mozconfig must wrap compiles with --with-compiler-wrapper"
+ )
+
+ return mozconfig_path
+
+
+@inherit_command_args("build")
+@SubCommand(
+ "hazards",
+ "gather",
+ description="Gather analysis data by compiling the given project",
+)
+@CommandArgument("--project", default="browser", help="Build the given project.")
+@CommandArgument("--application", dest="project", help="Build the given project.")
+@CommandArgument(
+ "--haz-objdir", default=None, help="Write object files to this directory."
+)
+@CommandArgument(
+ "--work-dir", default=None, help="Directory for output and working files."
+)
+def gather_hazard_data(command_context, **kwargs):
+ """Gather analysis information by compiling the tree"""
+ project = kwargs["project"]
+ objdir = get_objdir(command_context, kwargs)
+
+ work_dir = get_work_dir(command_context, project, kwargs["work_dir"])
+ ensure_dir_exists(work_dir)
+ with open(os.path.join(work_dir, "defaults.py"), "wt") as fh:
+ data = textwrap.dedent(
+ """\
+ analysis_scriptdir = "{script_dir}"
+ objdir = "{objdir}"
+ source = "{srcdir}"
+ sixgill = "{sixgill_dir}/usr/libexec/sixgill"
+ sixgill_bin = "{sixgill_dir}/usr/bin"
+ """
+ ).format(
+ script_dir=script_dir(command_context),
+ objdir=objdir,
+ srcdir=command_context.topsrcdir,
+ sixgill_dir=sixgill_dir(),
+ gcc_dir=gcc_dir(),
+ )
+ fh.write(data)
+
+ buildscript = " ".join(
+ [
+ command_context.topsrcdir + "/mach hazards compile",
+ *kwargs.get("what", []),
+ "--job-size=3.0", # Conservatively estimate 3GB/process
+ "--project=" + project,
+ "--haz-objdir=" + objdir,
+ ]
+ )
+ args = [
+ os.path.join(script_dir(command_context), "run_complete"),
+ "--foreground",
+ "--no-logs",
+ "--build-root=" + objdir,
+ "--wrap-dir=" + sixgill_dir() + "/usr/libexec/sixgill/scripts/wrap_gcc",
+ "--work-dir=work",
+ "-b",
+ sixgill_dir() + "/usr/bin",
+ "--buildcommand=" + buildscript,
+ ".",
+ ]
+
+ return command_context.run_process(args=args, cwd=work_dir, pass_thru=True)
+
+
+@inherit_command_args("build")
+@SubCommand("hazards", "compile", description=argparse.SUPPRESS)
+@CommandArgument(
+ "--mozconfig",
+ default=None,
+ metavar="FILENAME",
+ help="Build with the given mozconfig.",
+)
+@CommandArgument("--project", default="browser", help="Build the given project.")
+@CommandArgument("--application", dest="project", help="Build the given project.")
+@CommandArgument(
+ "--haz-objdir",
+ default=os.environ.get("HAZ_OBJDIR"),
+ help="Write object files to this directory.",
+)
+def inner_compile(command_context, **kwargs):
+ """Build a source tree and gather analysis information while running
+ under the influence of the analysis collection server."""
+
+ env = os.environ
+
+ # Check whether we are running underneath the manager (and therefore
+ # have a server to talk to).
+ if "XGILL_CONFIG" not in env:
+ raise FailedCommandError(
+ "no sixgill manager detected. `mach hazards compile` "
+ + "should only be run from `mach hazards gather`"
+ )
+
+ mozconfig_path = validate_mozconfig(command_context, kwargs)
+
+ # Communicate mozconfig to build subprocesses.
+ env["MOZCONFIG"] = os.path.join(command_context.topsrcdir, mozconfig_path)
+
+ # hazard mozconfigs need to find binaries in .mozbuild
+ env["MOZBUILD_STATE_PATH"] = state_dir()
+
+ # Suppress the gathering of sources, to save disk space and memory.
+ env["XGILL_NO_SOURCE"] = "1"
+
+ setup_env_for_tools(env)
+
+ if "haz_objdir" in kwargs:
+ env["MOZ_OBJDIR"] = kwargs.pop("haz_objdir")
+
+ return command_context._mach_context.commands.dispatch(
+ "build", command_context._mach_context, **kwargs
+ )
+
+
+@SubCommand(
+ "hazards", "analyze", description="Analyzed gathered data for rooting hazards"
+)
+@CommandArgument(
+ "--project",
+ default="browser",
+ help="Analyze the output for the given project.",
+)
+@CommandArgument("--application", dest="project", help="Build the given project.")
+@CommandArgument(
+ "--shell-objdir",
+ default=None,
+ help="objdir containing the optimized JS shell for running the analysis.",
+)
+@CommandArgument(
+ "--work-dir", default=None, help="Directory for output and working files."
+)
+@CommandArgument(
+ "--jobs", "-j", default=None, type=int, help="Number of parallel analyzers."
+)
+@CommandArgument(
+ "--verbose",
+ "-v",
+ default=False,
+ action="store_true",
+ help="Display executed commands.",
+)
+@CommandArgument(
+ "--from-stage",
+ default=None,
+ help="Stage to begin running at ('list' to see all).",
+)
+@CommandArgument(
+ "extra",
+ nargs=argparse.REMAINDER,
+ default=(),
+ help="Remaining non-optional arguments to analyze.py script",
+)
+def analyze(
+ command_context,
+ project,
+ shell_objdir,
+ work_dir,
+ jobs,
+ verbose,
+ from_stage,
+ extra,
+):
+ """Analyzed gathered data for rooting hazards"""
+
+ shell = ensure_shell(command_context, shell_objdir)
+ args = [
+ os.path.join(script_dir(command_context), "analyze.py"),
+ "--js",
+ shell,
+ *extra,
+ ]
+
+ if from_stage is None:
+ pass
+ elif from_stage == "list":
+ args.append("--list")
+ else:
+ args.extend(["--first", from_stage])
+
+ if jobs is not None:
+ args.extend(["-j", jobs])
+
+ if verbose:
+ args.append("-v")
+
+ setup_env_for_tools(os.environ)
+ setup_env_for_shell(os.environ, shell)
+
+ work_dir = get_work_dir(command_context, project, work_dir)
+ return command_context.run_process(args=args, cwd=work_dir, pass_thru=True)
+
+
+@SubCommand(
+ "hazards",
+ "self-test",
+ description="Run a self-test to verify hazards are detected",
+)
+@CommandArgument(
+ "--shell-objdir",
+ default=None,
+ help="objdir containing the optimized JS shell for running the analysis.",
+)
+@CommandArgument(
+ "extra",
+ nargs=argparse.REMAINDER,
+ help="Remaining non-optional arguments to pass to run-test.py",
+)
+def self_test(command_context, shell_objdir, extra):
+ """Analyzed gathered data for rooting hazards"""
+ shell = ensure_shell(command_context, shell_objdir)
+ args = [
+ os.path.join(script_dir(command_context), "run-test.py"),
+ "-v",
+ "--js",
+ shell,
+ "--sixgill",
+ os.path.join(tools_dir(), "sixgill"),
+ "--gccdir",
+ gcc_dir(),
+ ]
+ args.extend(extra)
+
+ setup_env_for_tools(os.environ)
+ setup_env_for_shell(os.environ, shell)
+
+ return command_context.run_process(args=args, pass_thru=True)
+
+
+def annotated_source(filename, query):
+ """The index page has URLs of the format <http://.../path/to/source.cpp?L=m-n#m>.
+ The `#m` part will be stripped off and used by the browser to jump to the correct line.
+ The `?L=m-n` or `?L=m` parameter will be processed here on the server to highlight
+ the given line range."""
+ linequery = query.replace("L=", "")
+ if "-" in linequery:
+ line0, line1 = linequery.split("-", 1)
+ else:
+ line0, line1 = linequery or "0", linequery or "0"
+ line0 = int(line0)
+ line1 = int(line1)
+
+ fh = open(filename, "rt")
+
+ out = "<pre>"
+ for lineno, line in enumerate(fh, 1):
+ processed = f"{lineno} <span id='{lineno}'"
+ if line0 <= lineno and lineno <= line1:
+ processed += " style='background: yellow'"
+ processed += ">" + html.escape(line.rstrip()) + "</span>\n"
+ out += processed
+
+ return out
+
+
+@SubCommand(
+ "hazards", "view", description="Display a web page describing any hazards found"
+)
+@CommandArgument(
+ "--project",
+ default="browser",
+ help="Analyze the output for the given project.",
+)
+@CommandArgument("--application", dest="project", help="Build the given project.")
+@CommandArgument(
+ "--haz-objdir", default=None, help="Write object files to this directory."
+)
+@CommandArgument(
+ "--work-dir", default=None, help="Directory for output and working files."
+)
+@CommandArgument("--port", default=6006, help="Port of the web server")
+@CommandArgument(
+ "--serve-only",
+ default=False,
+ action="store_true",
+ help="Serve only, do not navigate to page",
+)
+def view_hazards(command_context, project, haz_objdir, work_dir, port, serve_only):
+ work_dir = get_work_dir(command_context, project, work_dir)
+ haztop = os.path.basename(work_dir)
+ if haz_objdir is None:
+ haz_objdir = os.environ.get("HAZ_OBJDIR")
+ if haz_objdir is None:
+ haz_objdir = os.path.join(command_context.topsrcdir, "obj-analyzed-" + project)
+
+ httpd = None
+
+ def serve_source_file(request, path):
+ info = {"req": path}
+
+ def log(fmt, level=logging.INFO):
+ return command_context.log(level, "view-hazards", info, fmt)
+
+ if path in ("", f"{haztop}"):
+ info["dest"] = f"/{haztop}/hazards.html"
+ info["code"] = 301
+ log("serve '{req}' -> {code} {dest}")
+ return (info["code"], {"Location": info["dest"]}, "")
+
+ # Allow files to be served from the source directory or the objdir.
+ roots = (command_context.topsrcdir, haz_objdir)
+
+ try:
+ # Validate the path. Some source files have weird characters in their paths (eg "+"), but they
+ # all start with an alphanumeric or underscore.
+ command_context.log(
+ logging.DEBUG, "view-hazards", {"path": path}, "Raw path: {path}"
+ )
+ path_component = r"\w[\w\-\.\+]*"
+ if not re.match(f"({path_component}/)*{path_component}$", path):
+ raise ValueError("invalid path")
+
+ # Resolve the path to under one of the roots, and
+ # ensure that the actual file really is underneath a root directory.
+ for rootdir in roots:
+ fullpath = os.path.join(rootdir, path)
+ info["path"] = fullpath
+ fullpath = os.path.realpath(fullpath)
+ if os.path.isfile(fullpath):
+ # symlinks between roots are ok, but not symlinks outside of the roots.
+ tops = [
+ d
+ for d in roots
+ if fullpath.startswith(os.path.realpath(d) + "/")
+ ]
+ if len(tops) > 0:
+ break # Found a file underneath a root.
+ else:
+ raise IOError("not found")
+
+ html = annotated_source(fullpath, request.query)
+ log("serve '{req}' -> 200 {path}")
+ return (
+ 200,
+ {"Content-type": "text/html", "Content-length": len(html)},
+ html,
+ )
+ except (IOError, ValueError):
+ log("serve '{req}' -> 404 {path}", logging.ERROR)
+ return (
+ 404,
+ {"Content-type": "text/plain"},
+ "We don't have that around here. Don't be asking for it.",
+ )
+
+ httpd = mozhttpd.MozHttpd(
+ port=port,
+ docroot=None,
+ path_mappings={"/" + haztop: work_dir},
+ urlhandlers=[
+ # Treat everything not starting with /haz-browser/ (or /haz-js/)
+ # as a source file to be processed. Everything else is served
+ # as a plain file.
+ {
+ "method": "GET",
+ "path": "/(?!haz-" + project + "/)(.*)",
+ "function": serve_source_file,
+ },
+ ],
+ log_requests=True,
+ )
+
+ # The mozhttpd request handler class eats log messages.
+ httpd.handler_class.log_message = lambda self, format, *args: command_context.log(
+ logging.INFO, "view-hazards", {}, format % args
+ )
+
+ print("Serving at %s:%s" % (httpd.host, httpd.port))
+
+ httpd.start(block=False)
+ url = httpd.get_url(f"/{haztop}/hazards.html")
+ display_url = True
+ if not serve_only:
+ try:
+ webbrowser.get().open_new_tab(url)
+ display_url = False
+ except Exception:
+ pass
+ if display_url:
+ print("Please open %s in a browser." % url)
+
+ print("Hit CTRL+c to stop server.")
+ httpd.server.join()
diff --git a/js/src/devtools/rootAnalysis/mergeJSON.js b/js/src/devtools/rootAnalysis/mergeJSON.js
new file mode 100644
index 0000000000..2ac5a983db
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/mergeJSON.js
@@ -0,0 +1,26 @@
+/* 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/. */
+
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+
+var infiles = [...scriptArgs];
+var outfile = infiles.pop();
+
+let output;
+for (const filename of infiles) {
+ const data = JSON.parse(os.file.readFile(filename));
+ if (!output) {
+ output = data;
+ } else if (Array.isArray(data) != Array.isArray(output)) {
+ throw new Error('mismatched types');
+ } else if (Array.isArray(output)) {
+ output.push(...data);
+ } else {
+ Object.assign(output, data);
+ }
+}
+
+var origOut = os.file.redirect(outfile);
+print(JSON.stringify(output, null, 4));
+os.file.close(os.file.redirect(origOut));
diff --git a/js/src/devtools/rootAnalysis/mozconfig.browser b/js/src/devtools/rootAnalysis/mozconfig.browser
new file mode 100644
index 0000000000..6c3517865b
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/mozconfig.browser
@@ -0,0 +1,19 @@
+# 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/.
+
+# This mozconfig is used when analyzing the source code of the Firefox browser
+# for GC rooting hazards. See
+# <https://wiki.mozilla.org/Javascript:SpiderMonkey:ExactStackRooting>.
+
+ac_add_options --enable-project=browser
+ac_add_options --enable-js-shell
+
+# the sixgill wrapper is not compatible with building wasm objects with clang.
+export WASM_SANDBOXED_LIBRARIES=
+
+# the hazard analysis is not happy with std::filesystem uses in relrhack host
+# tool.
+ac_add_options --disable-elf-hack
+
+. $topsrcdir/js/src/devtools/rootAnalysis/mozconfig.common
diff --git a/js/src/devtools/rootAnalysis/mozconfig.common b/js/src/devtools/rootAnalysis/mozconfig.common
new file mode 100644
index 0000000000..c68fb6a26c
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/mozconfig.common
@@ -0,0 +1,37 @@
+# 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/.
+
+# Configuration shared between browser and shell builds.
+
+# The configuration options are chosen to compile the most code
+# (--enable-debug, --enable-tests) in the trickiest way possible
+# (--enable-optimize) to maximize the chance of seeing tricky static orderings.
+ac_add_options --enable-debug
+ac_add_options --enable-tests
+ac_add_options --enable-optimize
+
+# Wrap all compiler invocations in order to enable the plugin and send
+# information to a common database.
+if [ -z "$AUTOMATION" ]; then
+ # Developer build: `mach hazards bootstrap` puts tools here:
+ TOOLS_DIR="$MOZBUILD_STATE_PATH/hazard-tools"
+else
+ # Automation build: tools are downloaded from upstream tasks.
+ TOOLS_DIR="$MOZ_FETCHES_DIR"
+fi
+ac_add_options --with-compiler-wrapper="${TOOLS_DIR}"/sixgill/usr/libexec/sixgill/scripts/wrap_gcc/basecc
+
+# Stuff that gets in the way.
+ac_add_options --without-ccache
+ac_add_options --disable-replace-malloc
+
+# -Wattributes is very verbose due to attributes being ignored on template
+# instantiations.
+#
+# -Wignored-attributes is very verbose due to attributes being
+# ignored on template parameters.
+ANALYSIS_EXTRA_CFLAGS="-Wno-attributes -Wno-ignored-attributes"
+CFLAGS="$CFLAGS $ANALYSIS_EXTRA_CFLAGS"
+CPPFLAGS="$CPPFLAGS $ANALYSIS_EXTRA_CFLAGS"
+CXXFLAGS="$CXXFLAGS $ANALYSIS_EXTRA_CFLAGS"
diff --git a/js/src/devtools/rootAnalysis/mozconfig.haz_shell b/js/src/devtools/rootAnalysis/mozconfig.haz_shell
new file mode 100644
index 0000000000..68741f0454
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/mozconfig.haz_shell
@@ -0,0 +1,18 @@
+# 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/.
+
+# This mozconfig is for compiling the JS shell that runs the static rooting
+# hazard analysis. See
+# <https://wiki.mozilla.org/Javascript:SpiderMonkey:ExactStackRooting>.
+
+ac_add_options --enable-ctypes
+ac_add_options --enable-optimize
+ac_add_options --disable-debug
+ac_add_options --enable-project=js
+ac_add_options --enable-nspr-build
+ac_add_options --disable-jemalloc
+
+if [ -n "$AUTOMATION" ]; then
+ mk_add_options MOZ_OBJDIR="${HAZARD_SHELL_OBJDIR}"
+fi
diff --git a/js/src/devtools/rootAnalysis/mozconfig.js b/js/src/devtools/rootAnalysis/mozconfig.js
new file mode 100644
index 0000000000..07e584c210
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/mozconfig.js
@@ -0,0 +1,16 @@
+# 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/.
+
+# This mozconfig is used when analyzing the source code of the js/src tree for
+# GC rooting hazards. See
+# <https://wiki.mozilla.org/Javascript:SpiderMonkey:ExactStackRooting>.
+
+ac_add_options --enable-project=js
+
+# Also compile NSPR to see through its part of the control flow graph (not
+# currently needed, but also helps with weird problems finding the right
+# headers.)
+ac_add_options --enable-nspr-build
+
+. $topsrcdir/js/src/devtools/rootAnalysis/mozconfig.common
diff --git a/js/src/devtools/rootAnalysis/run-analysis.sh b/js/src/devtools/rootAnalysis/run-analysis.sh
new file mode 100755
index 0000000000..157821cc92
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/run-analysis.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+SRCDIR=$(cd $(dirname $0)/../../../..; pwd)
+GECKO_PATH=$SRCDIR $SRCDIR/taskcluster/scripts/builder/build-haz-linux.sh $(pwd) "$@"
diff --git a/js/src/devtools/rootAnalysis/run-test.py b/js/src/devtools/rootAnalysis/run-test.py
new file mode 100755
index 0000000000..b4835efec5
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/run-test.py
@@ -0,0 +1,154 @@
+#!/usr/bin/env python3
+# 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/.
+
+import argparse
+import os
+import site
+import subprocess
+import sys
+from glob import glob
+
+scriptdir = os.path.abspath(os.path.dirname(__file__))
+testdir = os.path.join(scriptdir, "t")
+
+site.addsitedir(testdir)
+from testlib import Test, equal
+
+parser = argparse.ArgumentParser(description="run hazard analysis tests")
+parser.add_argument(
+ "--js", default=os.environ.get("JS"), help="JS binary to run the tests with"
+)
+parser.add_argument(
+ "--sixgill",
+ default=os.environ.get("SIXGILL", os.path.join(testdir, "sixgill")),
+ help="Path to root of sixgill installation",
+)
+parser.add_argument(
+ "--sixgill-bin",
+ default=os.environ.get("SIXGILL_BIN"),
+ help="Path to sixgill binary dir",
+)
+parser.add_argument(
+ "--sixgill-plugin",
+ default=os.environ.get("SIXGILL_PLUGIN"),
+ help="Full path to sixgill gcc plugin",
+)
+parser.add_argument(
+ "--gccdir", default=os.environ.get("GCCDIR"), help="Path to GCC installation dir"
+)
+parser.add_argument("--cc", default=os.environ.get("CC"), help="Path to gcc")
+parser.add_argument("--cxx", default=os.environ.get("CXX"), help="Path to g++")
+parser.add_argument(
+ "--verbose",
+ "-v",
+ default=0,
+ action="count",
+ help="Display verbose output, including commands executed",
+)
+ALL_TESTS = [
+ "sixgill-tree",
+ "suppression",
+ "hazards",
+ "exceptions",
+ "virtual",
+ "graph",
+ "types",
+]
+parser.add_argument(
+ "tests",
+ nargs="*",
+ default=ALL_TESTS,
+ help="tests to run",
+)
+
+cfg = parser.parse_args()
+
+if not cfg.js:
+ sys.exit("Must specify JS binary through environment variable or --js option")
+if not cfg.cc:
+ if cfg.gccdir:
+ cfg.cc = os.path.join(cfg.gccdir, "bin", "gcc")
+ else:
+ cfg.cc = "gcc"
+if not cfg.cxx:
+ if cfg.gccdir:
+ cfg.cxx = os.path.join(cfg.gccdir, "bin", "g++")
+ else:
+ cfg.cxx = "g++"
+if not cfg.sixgill_bin:
+ cfg.sixgill_bin = os.path.join(cfg.sixgill, "usr", "bin")
+if not cfg.sixgill_plugin:
+ cfg.sixgill_plugin = os.path.join(
+ cfg.sixgill, "usr", "libexec", "sixgill", "gcc", "xgill.so"
+ )
+
+subprocess.check_call(
+ [cfg.js, "-e", 'if (!getBuildConfiguration("has-ctypes")) quit(1)']
+)
+
+
+def binpath(prog):
+ return os.path.join(cfg.sixgill_bin, prog)
+
+
+def make_dir(dirname, exist_ok=True):
+ try:
+ os.mkdir(dirname)
+ except OSError as e:
+ if exist_ok and e.strerror == "File exists":
+ pass
+ else:
+ raise
+
+
+outroot = os.path.join(testdir, "out")
+make_dir(outroot)
+
+os.environ["HAZARD_RUN_INTERNAL_TESTS"] = "1"
+
+exclude = []
+tests = []
+for t in cfg.tests:
+ if t.startswith("!"):
+ exclude.append(t[1:])
+ else:
+ tests.append(t)
+if len(tests) == 0:
+ tests = filter(lambda t: t not in exclude, ALL_TESTS)
+
+failed = set()
+passed = set()
+for path in tests:
+ name = os.path.basename(path)
+ indir = os.path.join(testdir, name)
+ outdir = os.path.join(outroot, name)
+ make_dir(outdir)
+
+ test = Test(indir, outdir, cfg, verbose=cfg.verbose)
+
+ os.chdir(outdir)
+ for xdb in glob("*.xdb"):
+ os.unlink(xdb)
+ print("START TEST {}".format(name), flush=True)
+ testpath = os.path.join(indir, "test.py")
+ testscript = open(testpath).read()
+ testcode = compile(testscript, testpath, "exec")
+ try:
+ exec(testcode, {"test": test, "equal": equal})
+ except subprocess.CalledProcessError:
+ print("TEST-FAILED: %s" % name)
+ failed.add(name)
+ except AssertionError:
+ print("TEST-FAILED: %s" % name)
+ failed.add(name)
+ raise
+ else:
+ print("TEST-PASSED: %s" % name)
+ passed.add(name)
+
+if failed:
+ raise Exception("Failed tests: " + " ".join(failed))
+
+print(f"All {len(passed)} tests passed.")
diff --git a/js/src/devtools/rootAnalysis/run_complete b/js/src/devtools/rootAnalysis/run_complete
new file mode 100755
index 0000000000..c9355267db
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/run_complete
@@ -0,0 +1,384 @@
+#!/usr/bin/perl
+
+# Sixgill: Static assertion checker for C/C++ programs.
+# Copyright (C) 2009-2010 Stanford University
+# Author: Brian Hackett
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# do a complete run of the system from raw source to reports. this requires
+# various run_monitor processes to be running in the background (maybe on other
+# machines) and watching a shared poll_file for jobs. if the output directory
+# for this script already exists then an incremental analysis will be performed
+# and the reports will only reflect the changes since the earlier run.
+
+use strict;
+use warnings;
+use IO::Handle;
+use File::Basename qw(basename dirname);
+use Getopt::Long;
+use Cwd;
+
+#################################
+# environment specific settings #
+#################################
+
+my $WORKDIR;
+my $SIXGILL_BIN;
+
+# poll file shared with the run_monitor script.
+my $poll_file;
+
+# root directory of the project.
+my $build_dir;
+
+# directory containing gcc wrapper scripts.
+my $wrap_dir;
+
+# optional file with annotations from the web interface.
+my $ann_file = "";
+
+# optional output directory to do a diff against.
+my $old_dir = "";
+
+# run in the foreground
+my $foreground;
+
+my $builder = "make -j4";
+
+my $suppress_logs;
+GetOptions("build-root|b=s" => \$build_dir,
+ "poll-file=s" => \$poll_file,
+ "no-logs!" => \$suppress_logs,
+ "work-dir=s" => \$WORKDIR,
+ "sixgill-binaries|binaries|b=s" => \$SIXGILL_BIN,
+ "wrap-dir=s" => \$wrap_dir,
+ "annotations-file|annotations|a=s" => \$ann_file,
+ "old-dir|old=s" => \$old_dir,
+ "foreground!" => \$foreground,
+ "buildcommand=s" => \$builder,
+ )
+ or die;
+
+if (not -d $build_dir) {
+ mkdir($build_dir);
+}
+if ($old_dir ne "" && not -d $old_dir) {
+ die "Old directory '$old_dir' does not exist\n";
+}
+
+$WORKDIR ||= "sixgill-work";
+mkdir($WORKDIR, 0755) if ! -d $WORKDIR;
+$poll_file ||= "$WORKDIR/poll.file";
+$build_dir ||= "$WORKDIR/js-inbound-xgill";
+
+if (!defined $SIXGILL_BIN) {
+ chomp(my $path = `which xmanager`);
+ if ($path) {
+ use File::Basename qw(dirname);
+ $SIXGILL_BIN = dirname($path);
+ } else {
+ die "Cannot find sixgill binaries. Use the -b option.";
+ }
+}
+
+$wrap_dir ||= "$WORKDIR/xgill-inbound/wrap_gcc";
+$wrap_dir = "$SIXGILL_BIN/../scripts/wrap_gcc" if not (-e "$wrap_dir/basecc");
+die "Bad wrapper directory: $wrap_dir" if not (-e "$wrap_dir/basecc");
+
+# code to clean the project from $build_dir.
+sub clean_project {
+ system("make clean");
+}
+
+# code to build the project from $build_dir.
+sub build_project {
+ return system($builder) >> 8;
+}
+
+our %kill_on_exit;
+END {
+ for my $pid (keys %kill_on_exit) {
+ kill($pid);
+ }
+}
+
+# commands to start the various xgill binaries. timeouts can be specified
+# for the backend analyses here, and a memory limit can be specified for
+# xmanager if desired (and USE_COUNT_ALLOCATOR is defined in util/alloc.h).
+my $xmanager = "$SIXGILL_BIN/xmanager";
+my $xsource = "$SIXGILL_BIN/xsource";
+my $xmemlocal = "$SIXGILL_BIN/xmemlocal -timeout=20";
+my $xinfer = "$SIXGILL_BIN/xinfer -timeout=60";
+my $xcheck = "$SIXGILL_BIN/xcheck -timeout=30";
+
+# prefix directory to strip off source files.
+my $prefix_dir = $build_dir;
+
+##########################
+# general purpose script #
+##########################
+
+# Prevent ccache from being used. I don't think this does any good. The problem
+# I'm struggling with is that if autoconf.mk still has 'ccache gcc' in it, the
+# builds fail in a mysterious way.
+$ENV{CCACHE_COMPILERCHECK} = 'date +%s.%N';
+delete $ENV{CCACHE_PREFIX};
+
+my $usage = "USAGE: run_complete result-dir\n";
+my $result_dir = shift or die $usage;
+
+if (not $foreground) {
+ my $pid = fork();
+ if ($pid != 0) {
+ print "Forked, exiting...\n";
+ exit(0);
+ }
+}
+
+# if the result directory does not already exist, mark for a clean build.
+my $do_clean = 0;
+if (not (-d $result_dir)) {
+ $do_clean = 1;
+ mkdir $result_dir;
+}
+
+if (!$suppress_logs) {
+ my $log_file = "$result_dir/complete.log";
+ open(OUT, ">>", $log_file) or die "append to $log_file: $!";
+ OUT->autoflush(1); # don't buffer writes to the main log.
+
+ # redirect stdout and stderr to the log.
+ STDOUT->fdopen(\*OUT, "w");
+ STDERR->fdopen(\*OUT, "w");
+}
+
+# pids to wait on before exiting. these are collating worker output.
+my @waitpids;
+
+chdir $result_dir;
+
+# to do a partial run, comment out the commands here you don't want to do.
+
+my $status = run_build();
+
+# end of run commands.
+
+for my $pid (@waitpids) {
+ waitpid($pid, 0);
+ $status ||= $? >> 8;
+}
+
+print "Exiting run_complete with status $status\n";
+exit $status;
+
+# get the IP address which a freshly created manager is listening on.
+sub get_manager_address
+{
+ my $log_file = shift or die;
+
+ # give the manager one second to start, any longer and something's broken.
+ sleep(1);
+
+ my $log_data = `cat $log_file`;
+ my ($port) = $log_data =~ /Listening on ([\.\:0-9]*)/
+ or die "no manager found";
+ print OUT "Connecting to manager on port $port\n" unless $suppress_logs;
+ print "Connecting to manager on port $port.\n";
+ return $1;
+}
+
+sub logging_suffix {
+ my ($show_logs, $log_file) = @_;
+ return $show_logs ? "2>&1 | tee $log_file"
+ : "> $log_file 2>&1";
+}
+
+sub run_build
+{
+ print "build started: ";
+ print scalar(localtime());
+ print "\n";
+
+ # fork off a process to run the build.
+ defined(my $pid = fork) or die;
+
+ # log file for the manager.
+ my $manager_log_file = "$result_dir/build_manager.log";
+
+ if (!$pid) {
+ # this is the child process, fork another process to run a manager.
+ defined(my $pid = fork) or die;
+ my $logging = logging_suffix($suppress_logs, $manager_log_file);
+ exec("$xmanager -terminate-on-assert $logging") if (!$pid);
+ $kill_on_exit{$pid} = 1;
+
+ if (!$suppress_logs) {
+ # open new streams to redirect stdout and stderr.
+ open(LOGOUT, "> $result_dir/build.log");
+ open(LOGERR, "> $result_dir/build_err.log");
+ STDOUT->fdopen(\*LOGOUT, "w");
+ STDERR->fdopen(\*LOGERR, "w");
+ }
+
+ my $address = get_manager_address($manager_log_file);
+
+ # write the configuration file for the wrapper script.
+ my $config_file = "$WORKDIR/xgill.config";
+ open(CONFIG, ">", $config_file) or die "create $config_file: $!";
+ print CONFIG "$prefix_dir\n";
+ print CONFIG Cwd::abs_path("$result_dir/build_xgill.log")."\n";
+ print CONFIG "$address\n";
+ my @extra = ("-fplugin-arg-xgill-mangle=1");
+ push(@extra, "-fplugin-arg-xgill-annfile=$ann_file")
+ if ($ann_file ne "" && -e $ann_file);
+ print CONFIG join(" ", @extra) . "\n";
+ close(CONFIG);
+
+ # Tell the wrapper where to find the config
+ $ENV{"XGILL_CONFIG"} = Cwd::abs_path($config_file);
+
+ # If overriding $CC, use GCCDIR to tell the wrapper scripts where the
+ # real compiler is. If $CC is not set, then the wrapper script will
+ # search $PATH anyway.
+ if (exists $ENV{CC}) {
+ $ENV{GCCDIR} = dirname($ENV{CC});
+ }
+
+ # Force the wrapper scripts to be run in place of the compiler during
+ # whatever build process we use.
+ $ENV{CC} = "$wrap_dir/" . basename($ENV{CC} // "gcc");
+ $ENV{CXX} = "$wrap_dir/" . basename($ENV{CXX} // "g++");
+
+ # do the build, cleaning if necessary.
+ chdir $build_dir;
+ clean_project() if ($do_clean);
+ my $exit_status = build_project();
+
+ # signal the manager that it's over.
+ system("$xsource -remote=$address -end-manager");
+
+ # wait for the manager to clean up and terminate.
+ print "Waiting for manager to finish (build status $exit_status)...\n";
+ waitpid($pid, 0);
+ my $manager_status = $?;
+ delete $kill_on_exit{$pid};
+
+ # build is finished, the complete run can resume.
+ # return value only useful if --foreground
+ print "Exiting with status " . ($manager_status || $exit_status) . "\n";
+ exit($manager_status || $exit_status);
+ }
+
+ # this is the complete process, wait for the build to finish.
+ waitpid($pid, 0);
+ my $status = $? >> 8;
+ print "build finished (status $status): ";
+ print scalar(localtime());
+ print "\n";
+
+ return $status;
+}
+
+sub run_pass
+{
+ my ($name, $command) = @_;
+ my $log_file = "$result_dir/manager.$name.log";
+
+ # extra commands to pass to the manager.
+ my $manager_extra = "";
+ $manager_extra .= "-modset-wait=10" if ($name eq "xmemlocal");
+
+ # fork off a manager process for the analysis.
+ defined(my $pid = fork) or die;
+ my $logging = logging_suffix($suppress_logs, $log_file);
+ exec("$xmanager $manager_extra $logging") if (!$pid);
+
+ my $address = get_manager_address($log_file);
+
+ # write the poll file for this pass.
+ if (! -d dirname($poll_file)) {
+ system("mkdir", "-p", dirname($poll_file));
+ }
+ open(POLL, "> $poll_file");
+ print POLL "$command\n";
+ print POLL "$result_dir/$name\n";
+ print POLL "$address\n";
+ close(POLL);
+
+ print "$name started: ";
+ print scalar(localtime());
+ print "\n";
+
+ waitpid($pid, 0);
+ unlink($poll_file);
+
+ print "$name finished: ";
+ print scalar(localtime());
+ print "\n";
+
+ # collate the worker's output into a single file. make this asynchronous
+ # so we can wait a bit and make sure we get all worker output.
+ defined($pid = fork) or die;
+
+ if (!$pid) {
+ sleep(20);
+ exec("cat $name.*.log > $name.log");
+ }
+
+ push(@waitpids, $pid);
+}
+
+# the names of all directories containing reports to archive.
+my $indexes;
+
+sub run_index
+{
+ my ($name, $kind) = @_;
+
+ return if (not (-e "report_$kind.xdb"));
+
+ print "$name started: ";
+ print scalar(localtime());
+ print "\n";
+
+ # make an index for the report diff if applicable.
+ if ($old_dir ne "") {
+ system("make_index $kind $old_dir > $name.diff.log");
+ system("mv $kind diff_$kind");
+ $indexes .= " diff_$kind";
+ }
+
+ # make an index for the full set of reports.
+ system("make_index $kind > $name.log");
+ $indexes .= " $kind";
+
+ print "$name finished: ";
+ print scalar(localtime());
+ print "\n";
+}
+
+sub archive_indexes
+{
+ print "archive started: ";
+ print scalar(localtime());
+ print "\n";
+
+ system("tar -czf reports.tgz $indexes");
+ system("rm -rf $indexes");
+
+ print "archive finished: ";
+ print scalar(localtime());
+ print "\n";
+}
diff --git a/js/src/devtools/rootAnalysis/t/exceptions/source.cpp b/js/src/devtools/rootAnalysis/t/exceptions/source.cpp
new file mode 100644
index 0000000000..8d38a790a1
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/exceptions/source.cpp
@@ -0,0 +1,57 @@
+/* -*- 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/. */
+
+// Simply including <exception> was enough to crash sixgill at one point.
+#include <exception>
+
+#define ANNOTATE(property) __attribute__((annotate(property)))
+
+struct Cell {
+ int f;
+} ANNOTATE("GC Thing");
+
+extern void GC() ANNOTATE("GC Call");
+
+void GC() {
+ // If the implementation is too trivial, the function body won't be emitted at
+ // all.
+ asm("");
+}
+
+class RAII_GC {
+ public:
+ RAII_GC() {}
+ ~RAII_GC() { GC(); }
+};
+
+// ~AutoSomething calls GC because of the RAII_GC field. The constructor,
+// though, should *not* GC -- unless it throws an exception. Which is not
+// possible when compiled with -fno-exceptions. This test will try it both
+// ways.
+class AutoSomething {
+ RAII_GC gc;
+
+ public:
+ AutoSomething() : gc() {
+ asm(""); // Ooh, scary, this might throw an exception
+ }
+ ~AutoSomething() { asm(""); }
+};
+
+extern Cell* getcell();
+
+extern void usevar(Cell* cell);
+
+void f() {
+ Cell* thing = getcell(); // Live range starts here
+
+ // When compiling with -fexceptions, there should be a hazard below. With
+ // -fno-exceptions, there should not be one. We will check both.
+ {
+ AutoSomething smth; // Constructor can GC only if exceptions are enabled
+ usevar(thing); // Live range ends here
+ } // In particular, 'thing' is dead at the destructor, so no hazard
+}
diff --git a/js/src/devtools/rootAnalysis/t/exceptions/test.py b/js/src/devtools/rootAnalysis/t/exceptions/test.py
new file mode 100644
index 0000000000..a40753d87a
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/exceptions/test.py
@@ -0,0 +1,21 @@
+# flake8: noqa: F821
+
+test.compile("source.cpp", "-fno-exceptions")
+test.run_analysis_script("gcTypes")
+
+hazards = test.load_hazards()
+assert len(hazards) == 0
+
+# If we compile with exceptions, then there *should* be a hazard because
+# AutoSomething::AutoSomething might throw an exception, which would cause the
+# partially-constructed value to be torn down, which will call ~RAII_GC.
+
+test.compile("source.cpp", "-fexceptions")
+test.run_analysis_script("gcTypes")
+
+hazards = test.load_hazards()
+assert len(hazards) == 1
+hazard = hazards[0]
+assert hazard.function == "void f()"
+assert hazard.variable == "thing"
+assert "AutoSomething::AutoSomething" in hazard.GCFunction
diff --git a/js/src/devtools/rootAnalysis/t/graph/source.cpp b/js/src/devtools/rootAnalysis/t/graph/source.cpp
new file mode 100644
index 0000000000..0adff8d532
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/graph/source.cpp
@@ -0,0 +1,90 @@
+#define ANNOTATE(property) __attribute__((annotate(property)))
+
+extern void GC() ANNOTATE("GC Call");
+
+void GC() {
+ // If the implementation is too trivial, the function body won't be emitted at
+ // all.
+ asm("");
+}
+
+extern void g(int x);
+extern void h(int x);
+
+void f(int x) {
+ if (x % 3) {
+ GC();
+ g(x);
+ }
+ h(x);
+}
+
+void g(int x) {
+ if (x % 2) f(x);
+ h(x);
+}
+
+void h(int x) {
+ if (x) {
+ f(x - 1);
+ g(x - 1);
+ }
+}
+
+void leaf() { asm(""); }
+
+void nonrecursive_root() {
+ leaf();
+ leaf();
+ GC();
+}
+
+void self_recursive(int x) {
+ if (x) self_recursive(x - 1);
+}
+
+// Set up the graph
+//
+// n1 <--> n2 n4 <--> n5
+// \ /
+// --> n3 <---------
+// \
+// ---> n6 --> n7 <---> n8 --> n9
+//
+// So recursive roots are one of (n1, n2) plus one of (n4, n5).
+extern void n1(int x);
+extern void n2(int x);
+extern void n3(int x);
+extern void n4(int x);
+extern void n5(int x);
+extern void n6(int x);
+extern void n7(int x);
+extern void n8(int x);
+extern void n9(int x);
+
+void n1(int x) { n2(x); }
+
+void n2(int x) {
+ if (x) n1(x - 1);
+ n3(x);
+}
+
+void n4(int x) { n5(x); }
+
+void n5(int x) {
+ if (x) n4(x - 1);
+ n3(x);
+}
+
+void n3(int x) { n6(x); }
+
+void n6(int x) { n7(x); }
+
+void n7(int x) { n8(x); }
+
+void n8(int x) {
+ if (x) n7(x - 1);
+ n9(x);
+}
+
+void n9(int x) { asm(""); }
diff --git a/js/src/devtools/rootAnalysis/t/graph/test.py b/js/src/devtools/rootAnalysis/t/graph/test.py
new file mode 100644
index 0000000000..f78500f200
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/graph/test.py
@@ -0,0 +1,54 @@
+# 'test' is provided by the calling script.
+# flake8: noqa: F821
+
+test.compile("source.cpp")
+test.run_analysis_script("gcTypes")
+
+info = test.load_typeInfo()
+
+gcFunctions = test.load_gcFunctions()
+
+f = "void f(int32)"
+g = "void g(int32)"
+h = "void h(int32)"
+
+assert f in gcFunctions
+assert g in gcFunctions
+assert h in gcFunctions
+assert "void leaf()" not in gcFunctions
+assert "void nonrecursive_root()" in gcFunctions
+
+callgraph = test.load_callgraph()
+assert callgraph.calleeGraph[f][g]
+assert callgraph.calleeGraph[f][h]
+assert callgraph.calleeGraph[g][f]
+assert callgraph.calleeGraph[g][h]
+
+node = ["void n{}(int32)".format(i) for i in range(10)]
+mnode = [callgraph.unmangledToMangled.get(f) for f in node]
+for src, dst in [
+ (1, 2),
+ (2, 1),
+ (4, 5),
+ (5, 4),
+ (2, 3),
+ (5, 3),
+ (3, 6),
+ (6, 7),
+ (7, 8),
+ (8, 7),
+ (8, 9),
+]:
+ assert callgraph.calleeGraph[node[src]][node[dst]]
+
+funcInfo = test.load_funcInfo()
+rroots = set(
+ [
+ callgraph.mangledToUnmangled[f]
+ for f in funcInfo
+ if funcInfo[f].get("recursive_root")
+ ]
+)
+assert len(set([node[1], node[2]]) & rroots) == 1
+assert len(set([node[4], node[5]]) & rroots) == 1
+assert len(rroots) == 4, "rroots = {}".format(rroots) # n1, n4, f, self_recursive
diff --git a/js/src/devtools/rootAnalysis/t/hazards/source.cpp b/js/src/devtools/rootAnalysis/t/hazards/source.cpp
new file mode 100644
index 0000000000..fe991653af
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/hazards/source.cpp
@@ -0,0 +1,566 @@
+/* -*- 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/. */
+
+#include <utility>
+
+#define ANNOTATE(property) __attribute__((annotate(property)))
+
+// MarkVariableAsGCSafe is a magic function name used as an
+// explicit annotation.
+
+namespace JS {
+namespace detail {
+template <typename T>
+static void MarkVariableAsGCSafe(T&) {
+ asm("");
+}
+} // namespace detail
+} // namespace JS
+
+#define JS_HAZ_VARIABLE_IS_GC_SAFE(var) JS::detail::MarkVariableAsGCSafe(var)
+
+struct Cell {
+ int f;
+} ANNOTATE("GC Thing");
+
+template <typename T, typename U>
+struct UntypedContainer {
+ char data[sizeof(T) + sizeof(U)];
+} ANNOTATE("moz_inherit_type_annotations_from_template_args");
+
+struct RootedCell {
+ RootedCell(Cell*) {}
+} ANNOTATE("Rooted Pointer");
+
+class AutoSuppressGC_Base {
+ public:
+ AutoSuppressGC_Base() {}
+ ~AutoSuppressGC_Base() {}
+} ANNOTATE("Suppress GC");
+
+class AutoSuppressGC_Child : public AutoSuppressGC_Base {
+ public:
+ AutoSuppressGC_Child() : AutoSuppressGC_Base() {}
+};
+
+class AutoSuppressGC {
+ AutoSuppressGC_Child helpImBeingSuppressed;
+
+ public:
+ AutoSuppressGC() {}
+};
+
+class AutoCheckCannotGC {
+ public:
+ AutoCheckCannotGC() {}
+ ~AutoCheckCannotGC() { asm(""); }
+} ANNOTATE("Invalidated by GC");
+
+extern void GC() ANNOTATE("GC Call");
+extern void invisible();
+
+void GC() {
+ // If the implementation is too trivial, the function body won't be emitted at
+ // all.
+ asm("");
+ invisible();
+}
+
+extern void usecell(Cell*);
+
+extern bool flipcoin();
+
+void suppressedFunction() {
+ GC(); // Calls GC, but is always called within AutoSuppressGC
+}
+
+void halfSuppressedFunction() {
+ GC(); // Calls GC, but is sometimes called within AutoSuppressGC
+}
+
+void unsuppressedFunction() {
+ GC(); // Calls GC, never within AutoSuppressGC
+}
+
+class IDL_Interface {
+ public:
+ ANNOTATE("Can run script") virtual void canScriptThis() {}
+ virtual void cannotScriptThis() {}
+ ANNOTATE("Can run script") virtual void overridden_canScriptThis() = 0;
+ virtual void overridden_cannotScriptThis() = 0;
+};
+
+class IDL_Subclass : public IDL_Interface {
+ ANNOTATE("Can run script") void overridden_canScriptThis() override {}
+ void overridden_cannotScriptThis() override {}
+};
+
+volatile static int x = 3;
+volatile static int* xp = &x;
+struct GCInDestructor {
+ ~GCInDestructor() {
+ invisible();
+ asm("");
+ *xp = 4;
+ GC();
+ }
+};
+
+template <typename T>
+void usecontainer(T* value) {
+ if (value) asm("");
+}
+
+Cell* cell() {
+ static Cell c;
+ return &c;
+}
+
+Cell* f() {
+ GCInDestructor kaboom;
+
+ Cell* cell1 = cell();
+ Cell* cell2 = cell();
+ Cell* cell3 = cell();
+ Cell* cell4 = cell();
+ {
+ AutoSuppressGC nogc;
+ suppressedFunction();
+ halfSuppressedFunction();
+ }
+ usecell(cell1);
+ halfSuppressedFunction();
+ usecell(cell2);
+ unsuppressedFunction();
+ {
+ // Old bug: it would look from the first AutoSuppressGC constructor it
+ // found to the last destructor. This statement *should* have no effect.
+ AutoSuppressGC nogc;
+ }
+ usecell(cell3);
+ Cell* cell5 = cell();
+ usecell(cell5);
+
+ {
+ // Templatized container that inherits attributes from Cell*, should
+ // report a hazard.
+ UntypedContainer<int, Cell*> container1;
+ usecontainer(&container1);
+ GC();
+ usecontainer(&container1);
+ }
+
+ {
+ // As above, but with a non-GC type.
+ UntypedContainer<int, double> container2;
+ usecontainer(&container2);
+ GC();
+ usecontainer(&container2);
+ }
+
+ // Hazard in return value due to ~GCInDestructor
+ Cell* cell6 = cell();
+ return cell6;
+}
+
+Cell* copy_and_gc(Cell* src) {
+ GC();
+ return reinterpret_cast<Cell*>(88);
+}
+
+void use(Cell* cell) {
+ static int x = 0;
+ if (cell) x++;
+}
+
+struct CellContainer {
+ Cell* cell;
+ CellContainer() { asm(""); }
+};
+
+void loopy() {
+ Cell cell;
+
+ // No hazard: haz1 is not live during call to copy_and_gc.
+ Cell* haz1;
+ for (int i = 0; i < 10; i++) {
+ haz1 = copy_and_gc(haz1);
+ }
+
+ // No hazard: haz2 is live up to just before the GC, and starting at the
+ // next statement after it, but not across the GC.
+ Cell* haz2 = &cell;
+ for (int j = 0; j < 10; j++) {
+ use(haz2);
+ GC();
+ haz2 = &cell;
+ }
+
+ // Hazard: haz3 is live from the final statement in one iteration, across
+ // the GC in the next, to the use in the 2nd statement.
+ Cell* haz3;
+ for (int k = 0; k < 10; k++) {
+ GC();
+ use(haz3);
+ haz3 = &cell;
+ }
+
+ // Hazard: haz4 is live across a GC hidden in a loop.
+ Cell* haz4 = &cell;
+ for (int i2 = 0; i2 < 10; i2++) {
+ GC();
+ }
+ use(haz4);
+
+ // Hazard: haz5 is live from within a loop across a GC.
+ Cell* haz5;
+ for (int i3 = 0; i3 < 10; i3++) {
+ haz5 = &cell;
+ }
+ GC();
+ use(haz5);
+
+ // No hazard: similar to the haz3 case, but verifying that we do not get
+ // into an infinite loop.
+ Cell* haz6;
+ for (int i4 = 0; i4 < 10; i4++) {
+ GC();
+ haz6 = &cell;
+ }
+
+ // No hazard: haz7 is constructed within the body, so it can't make a
+ // hazard across iterations. Note that this requires CellContainer to have
+ // a constructor, because otherwise the analysis doesn't see where
+ // variables are declared. (With the constructor, it knows that
+ // construction of haz7 obliterates any previous value it might have had.
+ // Not that that's possible given its scope, but the analysis doesn't get
+ // that information.)
+ for (int i5 = 0; i5 < 10; i5++) {
+ GC();
+ CellContainer haz7;
+ use(haz7.cell);
+ haz7.cell = &cell;
+ }
+
+ // Hazard: make sure we *can* see hazards across iterations involving
+ // CellContainer;
+ CellContainer haz8;
+ for (int i6 = 0; i6 < 10; i6++) {
+ GC();
+ use(haz8.cell);
+ haz8.cell = &cell;
+ }
+}
+
+namespace mozilla {
+template <typename T>
+class UniquePtr {
+ T* val;
+
+ public:
+ UniquePtr() : val(nullptr) { asm(""); }
+ UniquePtr(T* p) : val(p) {}
+ UniquePtr(UniquePtr<T>&& u) : val(u.val) { u.val = nullptr; }
+ ~UniquePtr() { use(val); }
+ T* get() { return val; }
+ void reset() { val = nullptr; }
+} ANNOTATE("moz_inherit_type_annotations_from_template_args");
+} // namespace mozilla
+
+extern void consume(mozilla::UniquePtr<Cell> uptr);
+
+void safevals() {
+ Cell cell;
+
+ // Simple hazard.
+ Cell* unsafe1 = &cell;
+ GC();
+ use(unsafe1);
+
+ // Safe because it's known to be nullptr.
+ Cell* safe2 = &cell;
+ safe2 = nullptr;
+ GC();
+ use(safe2);
+
+ // Unsafe because it may not be nullptr.
+ Cell* unsafe3 = &cell;
+ if (reinterpret_cast<long>(&cell) & 0x100) {
+ unsafe3 = nullptr;
+ }
+ GC();
+ use(unsafe3);
+
+ // Unsafe because it's not nullptr anymore.
+ Cell* unsafe3b = &cell;
+ unsafe3b = nullptr;
+ unsafe3b = &cell;
+ GC();
+ use(unsafe3b);
+
+ // Hazard involving UniquePtr.
+ {
+ mozilla::UniquePtr<Cell> unsafe4(&cell);
+ GC();
+ // Destructor uses unsafe4.
+ }
+
+ // reset() to safe value before the GC.
+ {
+ mozilla::UniquePtr<Cell> safe5(&cell);
+ safe5.reset();
+ GC();
+ }
+
+ // reset() to safe value after the GC.
+ {
+ mozilla::UniquePtr<Cell> safe6(&cell);
+ GC();
+ safe6.reset();
+ }
+
+ // reset() to safe value after the GC -- but we've already used it, so it's
+ // too late.
+ {
+ mozilla::UniquePtr<Cell> unsafe7(&cell);
+ GC();
+ use(unsafe7.get());
+ unsafe7.reset();
+ }
+
+ // initialized to safe value.
+ {
+ mozilla::UniquePtr<Cell> safe8;
+ GC();
+ }
+
+ // passed to a function that takes ownership before GC.
+ {
+ mozilla::UniquePtr<Cell> safe9(&cell);
+ consume(std::move(safe9));
+ GC();
+ }
+
+ // passed to a function that takes ownership after GC.
+ {
+ mozilla::UniquePtr<Cell> unsafe10(&cell);
+ GC();
+ consume(std::move(unsafe10));
+ }
+
+ // annotated to be safe before the GC. (This doesn't make
+ // a lot of sense here; the annotation is for when some
+ // type is known to only contain safe values, eg it is
+ // initialized as empty, or it is a union and we know
+ // that the GC pointer variants are not in use.)
+ {
+ mozilla::UniquePtr<Cell> safe11(&cell);
+ JS_HAZ_VARIABLE_IS_GC_SAFE(safe11);
+ GC();
+ }
+
+ // annotate as safe value after the GC -- since nothing else
+ // has touched the variable, that means it was already safe
+ // during the GC.
+ {
+ mozilla::UniquePtr<Cell> safe12(&cell);
+ GC();
+ JS_HAZ_VARIABLE_IS_GC_SAFE(safe12);
+ }
+
+ // annotate as safe after the GC -- but we've already used it, so it's
+ // too late.
+ {
+ mozilla::UniquePtr<Cell> unsafe13(&cell);
+ GC();
+ use(unsafe13.get());
+ JS_HAZ_VARIABLE_IS_GC_SAFE(unsafe13);
+ }
+
+ // Check JS_HAZ_CAN_RUN_SCRIPT annotation handling.
+ IDL_Subclass sub;
+ IDL_Subclass* subp = &sub;
+ IDL_Interface* base = &sub;
+ {
+ Cell* unsafe14 = &cell;
+ base->canScriptThis();
+ use(unsafe14);
+ }
+ {
+ Cell* unsafe15 = &cell;
+ subp->canScriptThis();
+ use(unsafe15);
+ }
+ {
+ // Almost the same as the last one, except call using the actual object, not
+ // a pointer. The type is known, so there is no danger of the actual type
+ // being a subclass that has overridden the method with an implementation
+ // that calls script.
+ Cell* safe16 = &cell;
+ sub.canScriptThis();
+ use(safe16);
+ }
+ {
+ Cell* safe17 = &cell;
+ base->cannotScriptThis();
+ use(safe17);
+ }
+ {
+ Cell* safe18 = &cell;
+ subp->cannotScriptThis();
+ use(safe18);
+ }
+ {
+ // A use after a GC, but not before. (This does not initialize safe19 by
+ // setting it to a value, because assignment would start its live range, and
+ // this test is to see if a variable with no known live range start requires
+ // a use before the GC or not. It should.)
+ Cell* safe19;
+ GC();
+ extern void initCellPtr(Cell**);
+ initCellPtr(&safe19);
+ }
+}
+
+// Make sure `this` is live at the beginning of a function.
+class Subcell : public Cell {
+ int method() {
+ GC();
+ return f; // this->f
+ }
+};
+
+template <typename T>
+struct RefPtr {
+ ~RefPtr() { GC(); }
+ bool forget() { return true; }
+ bool use() { return true; }
+ void assign_with_AddRef(T* aRawPtr) { asm(""); }
+};
+
+extern bool flipcoin();
+
+Cell* refptr_test1() {
+ static Cell cell;
+ RefPtr<float> v1;
+ Cell* ref_unsafe1 = &cell;
+ return ref_unsafe1;
+}
+
+Cell* refptr_test2() {
+ static Cell cell;
+ RefPtr<float> v2;
+ Cell* ref_safe2 = &cell;
+ v2.forget();
+ return ref_safe2;
+}
+
+Cell* refptr_test3() {
+ static Cell cell;
+ RefPtr<float> v3;
+ Cell* ref_unsafe3 = &cell;
+ if (x) {
+ v3.forget();
+ }
+ return ref_unsafe3;
+}
+
+Cell* refptr_test4() {
+ static Cell cell;
+ RefPtr<int> r;
+ return &cell; // hazard in return value
+}
+
+Cell* refptr_test5() {
+ static Cell cell;
+ RefPtr<int> r;
+ return nullptr; // returning immobile value, so no hazard
+}
+
+float somefloat = 1.2;
+
+Cell* refptr_test6() {
+ static Cell cell;
+ RefPtr<float> v6;
+ Cell* ref_unsafe6 = &cell;
+ // v6 can be used without an intervening forget() before the end of the
+ // function, even though forget() will be called at least once.
+ v6.forget();
+ if (x) {
+ v6.forget();
+ v6.assign_with_AddRef(&somefloat);
+ }
+ return ref_unsafe6;
+}
+
+Cell* refptr_test7() {
+ static Cell cell;
+ RefPtr<float> v7;
+ Cell* ref_unsafe7 = &cell;
+ // Similar to above, but with a loop.
+ while (flipcoin()) {
+ v7.forget();
+ v7.assign_with_AddRef(&somefloat);
+ }
+ return ref_unsafe7;
+}
+
+Cell* refptr_test8() {
+ static Cell cell;
+ RefPtr<float> v8;
+ Cell* ref_unsafe8 = &cell;
+ // If the loop is traversed, forget() will be called. But that doesn't
+ // matter, because even on the last iteration v8.use() will have been called
+ // (and potentially dropped the refcount or whatever.)
+ while (v8.use()) {
+ v8.forget();
+ }
+ return ref_unsafe8;
+}
+
+Cell* refptr_test9() {
+ static Cell cell;
+ RefPtr<float> v9;
+ Cell* ref_safe9 = &cell;
+ // Even when not going through the loop, forget() will be called and so the
+ // dtor will not Release.
+ while (v9.forget()) {
+ v9.assign_with_AddRef(&somefloat);
+ }
+ return ref_safe9;
+}
+
+Cell* refptr_test10() {
+ static Cell cell;
+ RefPtr<float> v10;
+ Cell* ref_unsafe10 = &cell;
+ // The destructor has a backwards path that skips the loop body.
+ v10.assign_with_AddRef(&somefloat);
+ while (flipcoin()) {
+ v10.forget();
+ }
+ return ref_unsafe10;
+}
+
+std::pair<bool, AutoCheckCannotGC> pair_returning_function() {
+ return std::make_pair(true, AutoCheckCannotGC());
+}
+
+void aggr_init_unsafe() {
+ // nogc will be live after the call, so across the GC.
+ auto [ok, nogc] = pair_returning_function();
+ GC();
+}
+
+void aggr_init_safe() {
+ // The analysis should be able to tell that nogc is only live after the call,
+ // not before. (This is to check for a problem where the return value was
+ // getting stored into a different temporary than the local nogc variable,
+ // and so its initialization was never seen and so it was assumed to be live
+ // throughout the function.)
+ GC();
+ auto [ok, nogc] = pair_returning_function();
+}
diff --git a/js/src/devtools/rootAnalysis/t/hazards/test.py b/js/src/devtools/rootAnalysis/t/hazards/test.py
new file mode 100644
index 0000000000..c4e9549305
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/hazards/test.py
@@ -0,0 +1,121 @@
+# flake8: noqa: F821
+
+from collections import defaultdict
+
+test.compile("source.cpp")
+test.run_analysis_script("gcTypes")
+
+# gcFunctions should be the inverse, but we get to rely on unmangled names here.
+gcFunctions = test.load_gcFunctions()
+assert "void GC()" in gcFunctions
+assert "void suppressedFunction()" not in gcFunctions
+assert "void halfSuppressedFunction()" in gcFunctions
+assert "void unsuppressedFunction()" in gcFunctions
+assert "int32 Subcell::method()" in gcFunctions
+assert "Cell* f()" in gcFunctions
+
+hazards = test.load_hazards()
+hazmap = {haz.variable: haz for haz in hazards}
+assert "cell1" not in hazmap
+assert "cell2" in hazmap
+assert "cell3" in hazmap
+assert "cell4" not in hazmap
+assert "cell5" not in hazmap
+assert "cell6" not in hazmap
+assert "<returnvalue>" in hazmap
+assert "this" in hazmap
+
+assert hazmap["cell2"].function == "Cell* f()"
+
+# Check that the correct GC call is reported for each hazard. (cell3 has a
+# hazard from two different GC calls; it doesn't really matter which is
+# reported.)
+assert hazmap["cell2"].GCFunction == "void halfSuppressedFunction()"
+assert hazmap["cell3"].GCFunction in (
+ "void halfSuppressedFunction()",
+ "void unsuppressedFunction()",
+)
+returnval_hazards = set(
+ haz.function for haz in hazards if haz.variable == "<returnvalue>"
+)
+assert "Cell* f()" in returnval_hazards
+assert "Cell* refptr_test1()" in returnval_hazards
+assert "Cell* refptr_test2()" not in returnval_hazards
+assert "Cell* refptr_test3()" in returnval_hazards
+assert "Cell* refptr_test4()" in returnval_hazards
+assert "Cell* refptr_test5()" not in returnval_hazards
+assert "Cell* refptr_test6()" in returnval_hazards
+assert "Cell* refptr_test7()" in returnval_hazards
+assert "Cell* refptr_test8()" in returnval_hazards
+assert "Cell* refptr_test9()" not in returnval_hazards
+
+assert "container1" in hazmap
+assert "container2" not in hazmap
+
+# Type names are handy to have in the report.
+assert hazmap["cell2"].type == "Cell*"
+assert hazmap["<returnvalue>"].type == "Cell*"
+assert hazmap["this"].type == "Subcell*"
+
+# loopy hazards. See comments in source.
+assert "haz1" not in hazmap
+assert "haz2" not in hazmap
+assert "haz3" in hazmap
+assert "haz4" in hazmap
+assert "haz5" in hazmap
+assert "haz6" not in hazmap
+assert "haz7" not in hazmap
+assert "haz8" in hazmap
+
+# safevals hazards. See comments in source.
+assert "unsafe1" in hazmap
+assert "safe2" not in hazmap
+assert "unsafe3" in hazmap
+assert "unsafe3b" in hazmap
+assert "unsafe4" in hazmap
+assert "safe5" not in hazmap
+assert "safe6" not in hazmap
+assert "unsafe7" in hazmap
+assert "safe8" not in hazmap
+assert "safe9" not in hazmap
+assert "safe10" not in hazmap
+assert "safe11" not in hazmap
+assert "safe12" not in hazmap
+assert "unsafe13" in hazmap
+assert "unsafe14" in hazmap
+assert "unsafe15" in hazmap
+assert "safe16" not in hazmap
+assert "safe17" not in hazmap
+assert "safe18" not in hazmap
+assert "safe19" not in hazmap
+
+# method hazard.
+
+byfunc = defaultdict(lambda: defaultdict(dict))
+for haz in hazards:
+ byfunc[haz.function][haz.variable] = haz
+
+methhaz = byfunc["int32 Subcell::method()"]
+assert "this" in methhaz
+assert methhaz["this"].type == "Subcell*"
+
+haz_functions = set(haz.function for haz in hazards)
+
+# RefPtr<T> tests.
+
+haz_functions = set(haz.function for haz in hazards)
+assert "Cell* refptr_test1()" in haz_functions
+assert "Cell* refptr_test2()" not in haz_functions
+assert "Cell* refptr_test3()" in haz_functions
+assert "Cell* refptr_test4()" in haz_functions
+assert "Cell* refptr_test5()" not in haz_functions
+assert "Cell* refptr_test6()" in haz_functions
+assert "Cell* refptr_test7()" in haz_functions
+assert "Cell* refptr_test8()" in haz_functions
+assert "Cell* refptr_test9()" not in haz_functions
+assert "Cell* refptr_test10()" in haz_functions
+
+# aggr_init tests.
+
+assert "void aggr_init_safe()" not in haz_functions
+assert "void aggr_init_unsafe()" in haz_functions
diff --git a/js/src/devtools/rootAnalysis/t/sixgill-tree/source.cpp b/js/src/devtools/rootAnalysis/t/sixgill-tree/source.cpp
new file mode 100644
index 0000000000..149d77b03a
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/sixgill-tree/source.cpp
@@ -0,0 +1,76 @@
+/* -*- 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/. */
+
+#define ANNOTATE(property) __attribute__((annotate(property)))
+
+namespace js {
+namespace gc {
+struct Cell {
+ int f;
+} ANNOTATE("GC Thing");
+} // namespace gc
+} // namespace js
+
+struct Bogon {};
+
+struct JustACell : public js::gc::Cell {
+ bool iHaveNoDataMembers() { return true; }
+};
+
+struct JSObject : public js::gc::Cell, public Bogon {
+ int g;
+};
+
+struct SpecialObject : public JSObject {
+ int z;
+};
+
+struct ErrorResult {
+ bool hasObj;
+ JSObject* obj;
+ void trace() {}
+} ANNOTATE("Suppressed GC Pointer");
+
+struct OkContainer {
+ ErrorResult res;
+ bool happy;
+};
+
+struct UnrootedPointer {
+ JSObject* obj;
+};
+
+template <typename T>
+class Rooted {
+ T data;
+} ANNOTATE("Rooted Pointer");
+
+extern void js_GC() ANNOTATE("GC Call") ANNOTATE("Slow");
+
+void js_GC() {}
+
+void root_arg(JSObject* obj, JSObject* random) {
+ // Use all these types so they get included in the output.
+ SpecialObject so;
+ UnrootedPointer up;
+ Bogon b;
+ OkContainer okc;
+ Rooted<JSObject*> ro;
+ Rooted<SpecialObject*> rso;
+
+ obj = random;
+
+ JSObject* other1 = obj;
+ js_GC();
+
+ float MARKER1 = 0;
+ JSObject* other2 = obj;
+ other1->f = 1;
+ other2->f = -1;
+
+ unsigned int u1 = 1;
+ unsigned int u2 = -1;
+}
diff --git a/js/src/devtools/rootAnalysis/t/sixgill-tree/test.py b/js/src/devtools/rootAnalysis/t/sixgill-tree/test.py
new file mode 100644
index 0000000000..5e99fff908
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/sixgill-tree/test.py
@@ -0,0 +1,63 @@
+# flake8: noqa: F821
+import re
+
+test.compile("source.cpp")
+test.computeGCTypes()
+body = test.process_body(test.load_db_entry("src_body", re.compile(r"root_arg"))[0])
+
+# Rendering positive and negative integers
+marker1 = body.assignment_line("MARKER1")
+equal(body.edge_from_line(marker1 + 2)["Exp"][1]["String"], "1")
+equal(body.edge_from_line(marker1 + 3)["Exp"][1]["String"], "-1")
+
+equal(body.edge_from_point(body.assignment_point("u1"))["Exp"][1]["String"], "1")
+equal(
+ body.edge_from_point(body.assignment_point("u2"))["Exp"][1]["String"], "4294967295"
+)
+
+assert "obj" in body["Variables"]
+assert "random" in body["Variables"]
+assert "other1" in body["Variables"]
+assert "other2" in body["Variables"]
+
+# Test function annotations
+js_GC = test.process_body(test.load_db_entry("src_body", re.compile(r"js_GC"))[0])
+annotations = js_GC["Variables"]["void js_GC()"]["Annotation"]
+assert annotations
+found_call_annotate = False
+for annotation in annotations:
+ (annType, value) = annotation["Name"]
+ if annType == "annotate" and value == "GC Call":
+ found_call_annotate = True
+assert found_call_annotate
+
+# Test type annotations
+
+# js::gc::Cell first
+cell = test.load_db_entry("src_comp", "js::gc::Cell")[0]
+assert cell["Kind"] == "Struct"
+annotations = cell["Annotation"]
+assert len(annotations) == 1
+(tag, value) = annotations[0]["Name"]
+assert tag == "annotate"
+assert value == "GC Thing"
+
+# Check JSObject inheritance.
+JSObject = test.load_db_entry("src_comp", "JSObject")[0]
+bases = [b["Base"] for b in JSObject["CSUBaseClass"]]
+assert "js::gc::Cell" in bases
+assert "Bogon" in bases
+assert len(bases) == 2
+
+# Check type analysis
+gctypes = test.load_gcTypes()
+assert "js::gc::Cell" in gctypes["GCThings"]
+assert "JustACell" in gctypes["GCThings"]
+assert "JSObject" in gctypes["GCThings"]
+assert "SpecialObject" in gctypes["GCThings"]
+assert "UnrootedPointer" in gctypes["GCPointers"]
+assert "Bogon" not in gctypes["GCThings"]
+assert "Bogon" not in gctypes["GCPointers"]
+assert "ErrorResult" not in gctypes["GCPointers"]
+assert "OkContainer" not in gctypes["GCPointers"]
+assert "class Rooted<JSObject*>" not in gctypes["GCPointers"]
diff --git a/js/src/devtools/rootAnalysis/t/sixgill.py b/js/src/devtools/rootAnalysis/t/sixgill.py
new file mode 100644
index 0000000000..307f13fae5
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/sixgill.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+# 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/.
+
+from collections import defaultdict
+
+# Simplified version of the body info.
+
+
+class Body(dict):
+ def __init__(self, body):
+ self["BlockIdKind"] = body["BlockId"]["Kind"]
+ if "Variable" in body["BlockId"]:
+ self["BlockName"] = body["BlockId"]["Variable"]["Name"][0].split("$")[-1]
+ loc = body["Location"]
+ self["LineRange"] = (loc[0]["Line"], loc[1]["Line"])
+ self["Filename"] = loc[0]["CacheString"]
+ self["Edges"] = body.get("PEdge", [])
+ self["Points"] = {
+ i: p["Location"]["Line"] for i, p in enumerate(body["PPoint"], 1)
+ }
+ self["Index"] = body["Index"]
+ self["Variables"] = {
+ x["Variable"]["Name"][0].split("$")[-1]: x["Type"]
+ for x in body["DefineVariable"]
+ }
+
+ # Indexes
+ self["Line2Points"] = defaultdict(list)
+ for point, line in self["Points"].items():
+ self["Line2Points"][line].append(point)
+ self["SrcPoint2Edges"] = defaultdict(list)
+ for edge in self["Edges"]:
+ src, dst = edge["Index"]
+ self["SrcPoint2Edges"][src].append(edge)
+ self["Line2Edges"] = defaultdict(list)
+ for src, edges in self["SrcPoint2Edges"].items():
+ line = self["Points"][src]
+ self["Line2Edges"][line].extend(edges)
+
+ def edges_from_line(self, line):
+ return self["Line2Edges"][line]
+
+ def edge_from_line(self, line):
+ edges = self.edges_from_line(line)
+ assert len(edges) == 1
+ return edges[0]
+
+ def edges_from_point(self, point):
+ return self["SrcPoint2Edges"][point]
+
+ def edge_from_point(self, point):
+ edges = self.edges_from_point(point)
+ assert len(edges) == 1
+ return edges[0]
+
+ def assignment_point(self, varname):
+ for edge in self["Edges"]:
+ if edge["Kind"] != "Assign":
+ continue
+ dst = edge["Exp"][0]
+ if dst["Kind"] != "Var":
+ continue
+ if dst["Variable"]["Name"][0] == varname:
+ return edge["Index"][0]
+ raise Exception("assignment to variable %s not found" % varname)
+
+ def assignment_line(self, varname):
+ return self["Points"][self.assignment_point(varname)]
diff --git a/js/src/devtools/rootAnalysis/t/suppression/source.cpp b/js/src/devtools/rootAnalysis/t/suppression/source.cpp
new file mode 100644
index 0000000000..56e458bdaa
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/suppression/source.cpp
@@ -0,0 +1,72 @@
+/* -*- 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/. */
+
+#define ANNOTATE(property) __attribute__((annotate(property)))
+
+struct Cell {
+ int f;
+} ANNOTATE("GC Thing");
+
+class AutoSuppressGC_Base {
+ public:
+ AutoSuppressGC_Base() {}
+ ~AutoSuppressGC_Base() {}
+} ANNOTATE("Suppress GC");
+
+class AutoSuppressGC_Child : public AutoSuppressGC_Base {
+ public:
+ AutoSuppressGC_Child() : AutoSuppressGC_Base() {}
+};
+
+class AutoSuppressGC {
+ AutoSuppressGC_Child helpImBeingSuppressed;
+
+ public:
+ AutoSuppressGC() {}
+};
+
+extern void GC() ANNOTATE("GC Call");
+
+void GC() {
+ // If the implementation is too trivial, the function body won't be emitted at
+ // all.
+ asm("");
+}
+
+extern void foo(Cell*);
+
+void suppressedFunction() {
+ GC(); // Calls GC, but is always called within AutoSuppressGC
+}
+
+void halfSuppressedFunction() {
+ GC(); // Calls GC, but is sometimes called within AutoSuppressGC
+}
+
+void unsuppressedFunction() {
+ GC(); // Calls GC, never within AutoSuppressGC
+}
+
+void f() {
+ Cell* cell1 = nullptr;
+ Cell* cell2 = nullptr;
+ Cell* cell3 = nullptr;
+ {
+ AutoSuppressGC nogc;
+ suppressedFunction();
+ halfSuppressedFunction();
+ }
+ foo(cell1);
+ halfSuppressedFunction();
+ foo(cell2);
+ unsuppressedFunction();
+ {
+ // Old bug: it would look from the first AutoSuppressGC constructor it
+ // found to the last destructor. This statement *should* have no effect.
+ AutoSuppressGC nogc;
+ }
+ foo(cell3);
+}
diff --git a/js/src/devtools/rootAnalysis/t/suppression/test.py b/js/src/devtools/rootAnalysis/t/suppression/test.py
new file mode 100644
index 0000000000..118ae422ab
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/suppression/test.py
@@ -0,0 +1,21 @@
+# flake8: noqa: F821
+test.compile("source.cpp")
+test.run_analysis_script("gcTypes", upto="gcFunctions")
+
+# The suppressions file uses mangled names.
+info = test.load_funcInfo()
+suppressed = [f for f, v in info.items() if v.get("limits", 0) | 1]
+
+# Only one of these is fully suppressed (ie, *always* called within the scope
+# of an AutoSuppressGC).
+assert len(list(filter(lambda f: "suppressedFunction" in f, suppressed))) == 1
+assert len(list(filter(lambda f: "halfSuppressedFunction" in f, suppressed))) == 0
+assert len(list(filter(lambda f: "unsuppressedFunction" in f, suppressed))) == 0
+
+# gcFunctions should be the inverse, but we get to rely on unmangled names here.
+gcFunctions = test.load_gcFunctions()
+assert "void GC()" in gcFunctions
+assert "void suppressedFunction()" not in gcFunctions
+assert "void halfSuppressedFunction()" in gcFunctions
+assert "void unsuppressedFunction()" in gcFunctions
+assert "void f()" in gcFunctions
diff --git a/js/src/devtools/rootAnalysis/t/testlib.py b/js/src/devtools/rootAnalysis/t/testlib.py
new file mode 100644
index 0000000000..e08b236e4f
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/testlib.py
@@ -0,0 +1,249 @@
+import json
+import os
+import re
+import subprocess
+import sys
+from collections import defaultdict, namedtuple
+
+from sixgill import Body
+
+scriptdir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+
+HazardSummary = namedtuple(
+ "HazardSummary", ["function", "variable", "type", "GCFunction", "location"]
+)
+
+Callgraph = namedtuple(
+ "Callgraph",
+ [
+ "functionNames",
+ "nameToId",
+ "mangledToUnmangled",
+ "unmangledToMangled",
+ "calleesOf",
+ "callersOf",
+ "tags",
+ "calleeGraph",
+ "callerGraph",
+ ],
+)
+
+
+def equal(got, expected):
+ if got != expected:
+ print("Got '%s', expected '%s'" % (got, expected))
+
+
+def extract_unmangled(func):
+ return func.split("$")[-1]
+
+
+class Test(object):
+ def __init__(self, indir, outdir, cfg, verbose=0):
+ self.indir = indir
+ self.outdir = outdir
+ self.cfg = cfg
+ self.verbose = verbose
+
+ def infile(self, path):
+ return os.path.join(self.indir, path)
+
+ def binpath(self, prog):
+ return os.path.join(self.cfg.sixgill_bin, prog)
+
+ def compile(self, source, options=""):
+ env = os.environ
+ env["CCACHE_DISABLE"] = "1"
+ if "-fexceptions" not in options and "-fno-exceptions" not in options:
+ options += " -fno-exceptions"
+ cmd = "{CXX} -c {source} -O3 -std=c++17 -fplugin={sixgill} -fplugin-arg-xgill-mangle=1 {options}".format( # NOQA: E501
+ source=self.infile(source),
+ CXX=self.cfg.cxx,
+ sixgill=self.cfg.sixgill_plugin,
+ options=options,
+ )
+ if self.cfg.verbose > 0:
+ print("Running %s" % cmd)
+ subprocess.check_call(["sh", "-c", cmd])
+
+ def load_db_entry(self, dbname, pattern):
+ """Look up an entry from an XDB database file, 'pattern' may be an exact
+ matching string, or an re pattern object matching a single entry."""
+
+ if hasattr(pattern, "match"):
+ output = subprocess.check_output(
+ [self.binpath("xdbkeys"), dbname + ".xdb"], universal_newlines=True
+ )
+ matches = list(filter(lambda _: re.search(pattern, _), output.splitlines()))
+ if len(matches) == 0:
+ raise Exception("entry not found")
+ if len(matches) > 1:
+ raise Exception("multiple entries found")
+ pattern = matches[0]
+
+ output = subprocess.check_output(
+ [self.binpath("xdbfind"), "-json", dbname + ".xdb", pattern],
+ universal_newlines=True,
+ )
+ return json.loads(output)
+
+ def run_analysis_script(self, startPhase="gcTypes", upto=None):
+ open("defaults.py", "w").write(
+ """\
+analysis_scriptdir = '{scriptdir}'
+sixgill_bin = '{bindir}'
+""".format(
+ scriptdir=scriptdir, bindir=self.cfg.sixgill_bin
+ )
+ )
+ cmd = [
+ sys.executable,
+ os.path.join(scriptdir, "analyze.py"),
+ ["-q", "", "-v"][min(self.verbose, 2)],
+ ]
+ cmd += ["--first", startPhase]
+ if upto:
+ cmd += ["--last", upto]
+ cmd.append("--source=%s" % self.indir)
+ cmd.append("--js=%s" % self.cfg.js)
+ if self.cfg.verbose:
+ print("Running " + " ".join(cmd))
+ subprocess.check_call(cmd)
+
+ def computeGCTypes(self):
+ self.run_analysis_script("gcTypes", upto="gcTypes")
+
+ def computeHazards(self):
+ self.run_analysis_script("gcTypes")
+
+ def load_text_file(self, filename, extract=lambda l: l):
+ fullpath = os.path.join(self.outdir, filename)
+ values = (extract(line.strip()) for line in open(fullpath, "r"))
+ return list(filter(lambda _: _ is not None, values))
+
+ def load_json_file(self, filename, reviver=None):
+ fullpath = os.path.join(self.outdir, filename)
+ with open(fullpath) as fh:
+ return json.load(fh, object_hook=reviver)
+
+ def load_gcTypes(self):
+ def grab_type(line):
+ m = re.match(r"^(GC\w+): (.*)", line)
+ if m:
+ return (m.group(1) + "s", m.group(2))
+ return None
+
+ gctypes = defaultdict(list)
+ for collection, typename in self.load_text_file(
+ "gcTypes.txt", extract=grab_type
+ ):
+ gctypes[collection].append(typename)
+ return gctypes
+
+ def load_typeInfo(self, filename="typeInfo.txt"):
+ return self.load_json_file(filename)
+
+ def load_funcInfo(self, filename="limitedFunctions.lst"):
+ return self.load_json_file(filename)
+
+ def load_gcFunctions(self):
+ return self.load_text_file("gcFunctions.lst", extract=extract_unmangled)
+
+ def load_callgraph(self):
+ data = Callgraph(
+ functionNames=["dummy"],
+ nameToId={},
+ mangledToUnmangled={},
+ unmangledToMangled={},
+ calleesOf=defaultdict(list),
+ callersOf=defaultdict(list),
+ tags=defaultdict(set),
+ calleeGraph=defaultdict(dict),
+ callerGraph=defaultdict(dict),
+ )
+
+ def lookup(id):
+ mangled = data.functionNames[int(id)]
+ return data.mangledToUnmangled.get(mangled, mangled)
+
+ def add_call(caller, callee, limit):
+ data.calleesOf[caller].append(callee)
+ data.callersOf[callee].append(caller)
+ data.calleeGraph[caller][callee] = True
+ data.callerGraph[callee][caller] = True
+
+ def process(line):
+ if line.startswith("#"):
+ name = line.split(" ", 1)[1]
+ data.nameToId[name] = len(data.functionNames)
+ data.functionNames.append(name)
+ return
+
+ if line.startswith("="):
+ m = re.match(r"^= (\d+) (.*)", line)
+ mangled = data.functionNames[int(m.group(1))]
+ unmangled = m.group(2)
+ data.nameToId[unmangled] = id
+ data.mangledToUnmangled[mangled] = unmangled
+ data.unmangledToMangled[unmangled] = mangled
+ return
+
+ # Sample lines:
+ # D 10 20
+ # D /3 10 20
+ # D 3:3 10 20
+ # All of these mean that there is a direct call from function #10
+ # to function #20. The latter two mean that the call is made in a
+ # context where the 0x1 and 0x2 properties (3 == 0x1 | 0x2) are in
+ # effect. The `/n` syntax was the original, which was then expanded
+ # to `m:n` to allow multiple calls to be combined together when not
+ # all calls have the same properties in effect. The `/n` syntax is
+ # deprecated.
+ #
+ # The properties usually refer to "limits", eg "GC is suppressed
+ # in the scope surrounding this call". For testing purposes, the
+ # difference between `m` and `n` in `m:n` is currently ignored.
+ tokens = line.split(" ")
+ limit = 0
+ if tokens[1].startswith("/"):
+ attr_str = tokens.pop(1)
+ limit = int(attr_str[1:])
+ elif ":" in tokens[1]:
+ attr_str = tokens.pop(1)
+ limit = int(attr_str[0 : attr_str.index(":")])
+
+ if tokens[0] in ("D", "R"):
+ _, caller, callee = tokens
+ add_call(lookup(caller), lookup(callee), limit)
+ elif tokens[0] == "T":
+ data.tags[tokens[1]].add(line.split(" ", 2)[2])
+ elif tokens[0] in ("F", "V"):
+ pass
+
+ elif tokens[0] == "I":
+ m = re.match(r"^I (\d+) VARIABLE ([^\,]*)", line)
+ pass
+
+ self.load_text_file("callgraph.txt", extract=process)
+ return data
+
+ def load_hazards(self):
+ def grab_hazard(line):
+ m = re.match(
+ r"Function '(.*?)' has unrooted '(.*?)' of type '(.*?)' live across GC call '(.*?)' at (.*)", # NOQA: E501
+ line,
+ )
+ if m:
+ info = list(m.groups())
+ info[0] = info[0].split("$")[-1]
+ info[3] = info[3].split("$")[-1]
+ return HazardSummary(*info)
+ return None
+
+ return self.load_text_file("hazards.txt", extract=grab_hazard)
+
+ def process_body(self, body):
+ return Body(body)
+
+ def process_bodies(self, bodies):
+ return [self.process_body(b) for b in bodies]
diff --git a/js/src/devtools/rootAnalysis/t/types/source.cpp b/js/src/devtools/rootAnalysis/t/types/source.cpp
new file mode 100644
index 0000000000..c8a2d4aa73
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/types/source.cpp
@@ -0,0 +1,167 @@
+/* -*- 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/. */
+
+#include <memory>
+#include <utility>
+
+#define ANNOTATE(property) __attribute__((annotate(property)))
+
+struct Cell {
+ int f;
+} ANNOTATE("GC Thing");
+
+namespace World {
+namespace NS {
+struct Unsafe {
+ int g;
+ ~Unsafe() { asm(""); }
+} ANNOTATE("Invalidated by GC") ANNOTATE("GC Pointer or Reference");
+} // namespace NS
+} // namespace World
+
+extern void GC() ANNOTATE("GC Call");
+extern void invisible();
+
+void GC() {
+ // If the implementation is too trivial, the function body won't be emitted at
+ // all.
+ asm("");
+ invisible();
+}
+
+struct GCOnDestruction {
+ ~GCOnDestruction() { GC(); }
+};
+
+struct NoGCOnDestruction {
+ ~NoGCOnDestruction() { asm(""); }
+};
+
+extern void usecell(Cell*);
+
+Cell* cell() {
+ static Cell c;
+ return &c;
+}
+
+template <typename T, typename U>
+struct SimpleTemplate {
+ int member;
+};
+
+template <typename T, typename U>
+class ANNOTATE("moz_inherit_type_annotations_from_template_args") Container {
+ public:
+ template <typename V, typename W>
+ void foo(V& v, W& w) {
+ class InnerClass {};
+ InnerClass xxx;
+ return;
+ }
+
+ struct Entry {
+ T t;
+ U u;
+ }* ent;
+};
+
+Cell* f() {
+ Container<int, double> c1;
+ Container<SimpleTemplate<int, int>, SimpleTemplate<double, double>> c2;
+ Container<Container<int, double>, Container<float, float>> c3;
+ Container<Container<SimpleTemplate<int, int>, float>,
+ Container<float, SimpleTemplate<char, char>>>
+ c4;
+
+ return nullptr;
+}
+
+// Define a set of classes for verifying that there is no infinite loop
+// when a class contains itself via mozilla::UniquePtr.
+
+namespace mozilla {
+
+template <typename A>
+struct JustAField {
+ A field;
+
+ // Hack to allow UniquePtr and SimpleUniquePtr to be swapped.
+ A& operator->() { return field; }
+};
+
+template <typename T>
+struct UniquePtr {
+ JustAField<T*> holder;
+};
+
+// This did not trigger the infinite loop, because the pointer here
+// caused the UniquePtr special handling to be skipped. It requires
+// the above definition to be triggered, which matches the actual
+// implementation (JustAField maps to CompactPair, more or less).
+// The bugfix for the infinite loop also drops this requirement, so
+// now this *would* trigger the bug if it weren't fixed in the same
+// commit.
+template <typename T>
+struct SimpleUniquePtr {
+ T* holder;
+};
+
+} // namespace mozilla
+
+class Recursive {
+ public:
+ using EntryMap = Container<Cell*, Recursive>;
+ mozilla::UniquePtr<EntryMap> entries;
+};
+
+void rvalue_ref(World::NS::Unsafe&& arg1) { GC(); }
+
+void ref(const World::NS::Unsafe& arg2) {
+ Recursive* foo;
+ // Must actually use a type for the compiler to instantiate the
+ // template specializations.
+ foo->entries.holder->ent;
+ GC();
+ static int use = arg2.g;
+}
+
+// A function that consumes a parameter, but only if passed by rvalue reference.
+extern void eat(World::NS::Unsafe&&);
+extern void eat(World::NS::Unsafe&);
+
+void rvalue_ref_ok() {
+ World::NS::Unsafe unsafe1;
+ eat(std::move(unsafe1));
+ GC();
+}
+
+void rvalue_ref_not_ok() {
+ World::NS::Unsafe unsafe2;
+ eat(unsafe2);
+ GC();
+}
+
+void rvalue_ref_arg_ok(World::NS::Unsafe&& unsafe3) {
+ eat(std::move(unsafe3));
+ GC();
+}
+
+void rvalue_ref_arg_not_ok(World::NS::Unsafe&& unsafe4) {
+ eat(unsafe4);
+ GC();
+}
+
+void shared_ptr_hazard() {
+ Cell* unsafe5 = f();
+ { auto p = std::make_shared<GCOnDestruction>(); }
+ usecell(unsafe5);
+}
+
+void shared_ptr_no_hazard() {
+ Cell* safe6 = f();
+ { auto p = std::make_shared<NoGCOnDestruction>(); }
+ usecell(safe6);
+}
diff --git a/js/src/devtools/rootAnalysis/t/types/test.py b/js/src/devtools/rootAnalysis/t/types/test.py
new file mode 100644
index 0000000000..4a2b985abf
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/types/test.py
@@ -0,0 +1,16 @@
+# flake8: noqa: F821
+
+from collections import defaultdict
+
+test.compile("source.cpp")
+test.run_analysis_script()
+hazards = test.load_hazards()
+hazmap = {haz.variable: haz for haz in hazards}
+assert "arg1" in hazmap
+assert "arg2" in hazmap
+assert "unsafe1" not in hazmap
+assert "unsafe2" in hazmap
+assert "unsafe3" not in hazmap
+assert "unsafe4" in hazmap
+assert "unsafe5" in hazmap
+assert "safe6" not in hazmap
diff --git a/js/src/devtools/rootAnalysis/t/virtual/source.cpp b/js/src/devtools/rootAnalysis/t/virtual/source.cpp
new file mode 100644
index 0000000000..980546f38d
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/virtual/source.cpp
@@ -0,0 +1,366 @@
+/* -*- 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/. */
+
+#define ANNOTATE(property) __attribute__((annotate(property)))
+
+extern void GC() ANNOTATE("GC Call");
+
+void GC() {
+ // If the implementation is too trivial, the function body won't be emitted at
+ // all.
+ asm("");
+}
+
+// Special-cased function -- code that can run JS has an artificial edge to
+// js::RunScript.
+namespace js {
+void RunScript() { GC(); }
+} // namespace js
+
+struct Cell {
+ int f;
+} ANNOTATE("GC Thing");
+
+extern void foo();
+
+void bar() { GC(); }
+
+typedef void (*func_t)();
+
+class Base {
+ public:
+ int ANNOTATE("field annotation") dummy;
+ virtual void someGC() ANNOTATE("Base pure virtual method") = 0;
+ virtual void someGC(int) ANNOTATE("overloaded Base pure virtual method") = 0;
+ virtual void sibGC() = 0;
+ virtual void onBase() { bar(); }
+ func_t functionField;
+
+ // For now, this is just to verify that the plugin doesn't crash. The
+ // analysis code does not yet look at this annotation or output it anywhere
+ // (though it *is* being recorded.)
+ static float testAnnotations() ANNOTATE("static func");
+
+ // Similar, though sixgill currently completely ignores parameter annotations.
+ static double testParamAnnotations(Cell& ANNOTATE("param annotation")
+ ANNOTATE("second param annot") cell)
+ ANNOTATE("static func") ANNOTATE("second func");
+};
+
+float Base::testAnnotations() {
+ asm("");
+ return 1.1;
+}
+
+double Base::testParamAnnotations(Cell& cell) {
+ asm("");
+ return 1.2;
+}
+
+class Super : public Base {
+ public:
+ virtual void ANNOTATE("Super pure virtual") noneGC() = 0;
+ virtual void allGC() = 0;
+ virtual void onSuper() { asm(""); }
+ void nonVirtualFunc() { asm(""); }
+};
+
+class Sub1 : public Super {
+ public:
+ void noneGC() override { foo(); }
+ void someGC() override ANNOTATE("Sub1 override") ANNOTATE("second attr") {
+ foo();
+ }
+ void someGC(int) override ANNOTATE("Sub1 override for int overload") {
+ foo();
+ }
+ void allGC() override {
+ foo();
+ bar();
+ }
+ void sibGC() override { foo(); }
+ void onBase() override { foo(); }
+} ANNOTATE("CSU1") ANNOTATE("CSU2");
+
+class Sub2 : public Super {
+ public:
+ void noneGC() override { foo(); }
+ void someGC() override {
+ foo();
+ bar();
+ }
+ void someGC(int) override {
+ foo();
+ bar();
+ }
+ void allGC() override {
+ foo();
+ bar();
+ }
+ void sibGC() override { foo(); }
+};
+
+class Sibling : public Base {
+ public:
+ virtual void noneGC() { foo(); }
+ void someGC() override {
+ foo();
+ bar();
+ }
+ void someGC(int) override {
+ foo();
+ bar();
+ }
+ virtual void allGC() {
+ foo();
+ bar();
+ }
+ void sibGC() override { bar(); }
+};
+
+class AutoSuppressGC {
+ public:
+ AutoSuppressGC() {}
+ ~AutoSuppressGC() {}
+} ANNOTATE("Suppress GC");
+
+void use(Cell*) { asm(""); }
+
+class nsISupports {
+ public:
+ virtual ANNOTATE("Can run script") void danger() { asm(""); }
+
+ virtual ~nsISupports() = 0;
+};
+
+class nsIPrincipal : public nsISupports {
+ public:
+ ~nsIPrincipal() override{};
+};
+
+struct JSPrincipals {
+ int debugToken;
+ JSPrincipals() = default;
+ virtual ~JSPrincipals() { GC(); }
+};
+
+class nsJSPrincipals : public nsIPrincipal, public JSPrincipals {
+ public:
+ void Release() { delete this; }
+};
+
+class SafePrincipals : public nsIPrincipal {
+ public:
+ ~SafePrincipals() { foo(); }
+};
+
+void f() {
+ Sub1 s1;
+ Sub2 s2;
+
+ static Cell cell;
+ {
+ Cell* c1 = &cell;
+ s1.noneGC();
+ use(c1);
+ }
+ {
+ Cell* c2 = &cell;
+ s2.someGC();
+ use(c2);
+ }
+ {
+ Cell* c3 = &cell;
+ s1.allGC();
+ use(c3);
+ }
+ {
+ Cell* c4 = &cell;
+ s2.noneGC();
+ use(c4);
+ }
+ {
+ Cell* c5 = &cell;
+ s2.someGC();
+ use(c5);
+ }
+ {
+ Cell* c6 = &cell;
+ s2.allGC();
+ use(c6);
+ }
+
+ Super* super = &s2;
+ {
+ Cell* c7 = &cell;
+ super->noneGC();
+ use(c7);
+ }
+ {
+ Cell* c8 = &cell;
+ super->someGC();
+ use(c8);
+ }
+ {
+ Cell* c9 = &cell;
+ super->allGC();
+ use(c9);
+ }
+
+ {
+ Cell* c10 = &cell;
+ s1.functionField();
+ use(c10);
+ }
+ {
+ Cell* c11 = &cell;
+ super->functionField();
+ use(c11);
+ }
+ {
+ Cell* c12 = &cell;
+ super->sibGC();
+ use(c12);
+ }
+
+ Base* base = &s2;
+ {
+ Cell* c13 = &cell;
+ base->sibGC();
+ use(c13);
+ }
+
+ nsJSPrincipals pals;
+ {
+ Cell* c14 = &cell;
+ nsISupports* p = &pals;
+ p->danger();
+ use(c14);
+ }
+
+ // Base defines, Sub1 overrides, static Super can call either.
+ {
+ Cell* c15 = &cell;
+ super->onBase();
+ use(c15);
+ }
+
+ {
+ Cell* c16 = &cell;
+ s2.someGC(7);
+ use(c16);
+ }
+
+ {
+ Cell* c17 = &cell;
+ super->someGC(7);
+ use(c17);
+ }
+
+ {
+ nsJSPrincipals* princ = new nsJSPrincipals();
+ Cell* c18 = &cell;
+ delete princ; // Can GC
+ use(c18);
+ }
+
+ {
+ nsJSPrincipals* princ = new nsJSPrincipals();
+ nsISupports* supp = static_cast<nsISupports*>(princ);
+ Cell* c19 = &cell;
+ delete supp; // Can GC
+ use(c19);
+ }
+
+ {
+ auto* safe = new SafePrincipals();
+ Cell* c20 = &cell;
+ delete safe; // Cannot GC
+ use(c20);
+ }
+
+ {
+ auto* safe = new SafePrincipals();
+ nsISupports* supp = static_cast<nsISupports*>(safe);
+ Cell* c21 = &cell;
+ delete supp; // Compiler thinks destructor can GC.
+ use(c21);
+ }
+}
+
+template <typename Function>
+void Call1(Function&& f) {
+ f();
+}
+
+template <typename Function>
+void Call2(Function&& f) {
+ f();
+}
+
+void function_pointers() {
+ Cell cell;
+
+ {
+ auto* f = GC;
+ Cell* c22 = &cell;
+ f();
+ use(c22);
+ }
+
+ {
+ auto* f = GC;
+ auto*& g = f;
+ Cell* c23 = &cell;
+ g();
+ use(c23);
+ }
+
+ {
+ auto* f = GC;
+ Call1([&] {
+ Cell* c24 = &cell;
+ f();
+ use(c24);
+ });
+ }
+}
+
+// Use a separate function to test `mallocSizeOf` annotations. Bug 1872197:
+// functions that are specialized on a lambda function and call that function
+// will have that call get mixed up with other calls of lambdas defined within
+// the same function.
+void annotated_function_pointers() {
+ Cell cell;
+
+ // Variables with the specific name "mallocSizeOf" are
+ // annotated to not GC. (Heh... even though here, they
+ // *do* GC!)
+
+ {
+ auto* mallocSizeOf = GC;
+ Cell* c25 = &cell;
+ mallocSizeOf();
+ use(c25);
+ }
+
+ {
+ auto* f = GC;
+ auto*& mallocSizeOf = f;
+ Cell* c26 = &cell;
+ mallocSizeOf();
+ use(c26);
+ }
+
+ {
+ auto* mallocSizeOf = GC;
+ Call2([&] {
+ Cell* c27 = &cell;
+ mallocSizeOf();
+ use(c27);
+ });
+ }
+}
diff --git a/js/src/devtools/rootAnalysis/t/virtual/test.py b/js/src/devtools/rootAnalysis/t/virtual/test.py
new file mode 100644
index 0000000000..26d2e51ed6
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/virtual/test.py
@@ -0,0 +1,99 @@
+# 'test' is provided by the calling script.
+# flake8: noqa: F821
+
+test.compile("source.cpp")
+test.run_analysis_script("gcTypes")
+
+info = test.load_typeInfo()
+
+assert "Sub1" in info["OtherCSUTags"]
+assert ["CSU1", "CSU2"] == sorted(info["OtherCSUTags"]["Sub1"])
+assert "Base" in info["OtherFieldTags"]
+assert "someGC" in info["OtherFieldTags"]["Base"]
+assert "Sub1" in info["OtherFieldTags"]
+assert "someGC" in info["OtherFieldTags"]["Sub1"]
+
+# For now, fields with the same name (eg overloaded virtual methods) just
+# accumulate attributes.
+assert ["Sub1 override", "Sub1 override for int overload", "second attr"] == sorted(
+ info["OtherFieldTags"]["Sub1"]["someGC"]
+)
+
+gcFunctions = test.load_gcFunctions()
+
+assert "void Sub1::noneGC()" not in gcFunctions
+assert "void Sub1::someGC()" not in gcFunctions
+assert "void Sub1::someGC(int32)" not in gcFunctions
+assert "void Sub1::allGC()" in gcFunctions
+assert "void Sub2::noneGC()" not in gcFunctions
+assert "void Sub2::someGC()" in gcFunctions
+assert "void Sub2::someGC(int32)" in gcFunctions
+assert "void Sub2::allGC()" in gcFunctions
+
+callgraph = test.load_callgraph()
+
+assert callgraph.calleeGraph["void f()"]["Super.noneGC:0"]
+assert callgraph.calleeGraph["Super.noneGC:0"]["Sub1.noneGC:0"]
+assert callgraph.calleeGraph["Super.noneGC:0"]["Sub2.noneGC:0"]
+assert callgraph.calleeGraph["Sub1.noneGC:0"]["void Sub1::noneGC()"]
+assert callgraph.calleeGraph["Sub2.noneGC:0"]["void Sub2::noneGC()"]
+assert "void Sibling::noneGC()" not in callgraph.calleeGraph["Super.noneGC:0"]
+assert callgraph.calleeGraph["Super.onBase:0"]["Sub1.onBase:0"]
+assert callgraph.calleeGraph["Sub1.onBase:0"]["void Sub1::onBase()"]
+assert callgraph.calleeGraph["Super.onBase:0"]["void Base::onBase()"]
+assert "void Sibling::onBase()" not in callgraph.calleeGraph["Super.onBase:0"]
+
+hazards = test.load_hazards()
+hazmap = {haz.variable: haz for haz in hazards}
+
+assert "c1" not in hazmap
+assert "c2" in hazmap
+assert "c3" in hazmap
+assert "c4" not in hazmap
+assert "c5" in hazmap
+assert "c6" in hazmap
+assert "c7" not in hazmap
+assert "c8" in hazmap
+assert "c9" in hazmap
+assert "c10" in hazmap
+assert "c11" in hazmap
+
+# Virtual resolution should take the static type into account: the only method
+# implementations considered should be those of descendants, even if the
+# virtual method is inherited and not overridden in the static class. (Base
+# defines sibGC() as pure virtual, Super inherits it without overriding,
+# Sibling and Sub2 both implement it.)
+
+# Call Base.sibGC on a Super pointer: can only call Sub2.sibGC(), which does not GC.
+# In particular, PEdgeCallInstance.Exp.Field.FieldCSU.Type = {Kind: "CSU", Name="Super"}
+assert "c12" not in hazmap
+# Call Base.sibGC on a Base pointer; can call Sibling.sibGC(), which GCs.
+assert "c13" in hazmap
+
+# Call nsISupports.danger() which is annotated to be overridable and hence can GC.
+assert "c14" in hazmap
+
+# someGC(int) overload
+assert "c16" in hazmap
+assert "c17" in hazmap
+
+# Super.onBase() could call the GC'ing Base::onBase().
+assert "c15" in hazmap
+
+# virtual ~nsJSPrincipals calls ~JSPrincipals calls GC.
+assert "c18" in hazmap
+assert "c19" in hazmap
+
+# ~SafePrincipals does not GC.
+assert "c20" not in hazmap
+
+# ...but when cast to a nsISupports*, the compiler can't tell that it won't.
+assert "c21" in hazmap
+
+# Function pointers! References to function pointers! Created by reference-capturing lambdas!
+assert "c22" in hazmap
+assert "c23" in hazmap
+assert "c24" in hazmap
+assert "c25" not in hazmap
+assert "c26" not in hazmap
+assert "c27" not in hazmap
diff --git a/js/src/devtools/rootAnalysis/utility.js b/js/src/devtools/rootAnalysis/utility.js
new file mode 100644
index 0000000000..94b5391c02
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/utility.js
@@ -0,0 +1,422 @@
+/* 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/. */
+
+/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
+
+"use strict";
+
+loadRelativeToScript('dumpCFG.js');
+
+// Attribute bits - each call edge may carry a set of 'attrs' bits, saying eg
+// that the edge takes place within a scope where GC is suppressed, for
+// example.
+var ATTR_GC_SUPPRESSED = 1 << 0;
+var ATTR_CANSCRIPT_BOUNDED = 1 << 1; // Unimplemented
+var ATTR_DOM_ITERATING = 1 << 2; // Unimplemented
+var ATTR_NONRELEASING = 1 << 3; // ~RefPtr of value whose refcount will not go to zero
+var ATTR_REPLACED = 1 << 4; // Ignore edge, it was replaced by zero or more better edges.
+var ATTR_SYNTHETIC = 1 << 5; // Call was manufactured in some way.
+
+var ATTR_LAST = 1 << 5;
+var ATTRS_NONE = 0;
+var ATTRS_ALL = (ATTR_LAST << 1) - 1; // All possible bits set
+
+// The traversal algorithms we run will recurse into children if you change any
+// attrs bit to zero. Use all bits set to maximally attributed, including
+// additional bits that all just mean "unvisited", so that the first time we
+// see a node with this attrs, we're guaranteed to turn at least one bit off
+// and thereby keep going.
+var ATTRS_UNVISITED = 0xffff;
+
+// gcc appends this to mangled function names for "not in charge"
+// constructors/destructors.
+var internalMarker = " *INTERNAL* ";
+
+if (! Set.prototype.hasOwnProperty("update")) {
+ Object.defineProperty(Set.prototype, "update", {
+ value: function (collection) {
+ for (let elt of collection)
+ this.add(elt);
+ }
+ });
+}
+
+function assert(x, msg)
+{
+ if (x)
+ return;
+ debugger;
+ if (msg)
+ throw new Error("assertion failed: " + msg + "\n");
+ else
+ throw new Error("assertion failed");
+}
+
+function defined(x) {
+ return x !== undefined;
+}
+
+function xprint(x, padding)
+{
+ if (!padding)
+ padding = "";
+ if (x instanceof Array) {
+ print(padding + "[");
+ for (var elem of x)
+ xprint(elem, padding + " ");
+ print(padding + "]");
+ } else if (x instanceof Object) {
+ print(padding + "{");
+ for (var prop in x) {
+ print(padding + " " + prop + ":");
+ xprint(x[prop], padding + " ");
+ }
+ print(padding + "}");
+ } else {
+ print(padding + x);
+ }
+}
+
+// Command-line argument parser.
+//
+// `parameters` is a dict of parameters specs, each of which is a dict with keys:
+//
+// - name: name of option, prefixed with "--" if it is named (otherwise, it
+// is interpreted as a positional parameter.)
+// - dest: key to store the result in, defaulting to the parameter name without
+// any leading "--"" and with dashes replaced with underscores.
+// - default: value of option if no value is given. Positional parameters with
+// a default value are optional. If no default is given, the parameter's name
+// is not included in the return value.
+// - type: `bool` if it takes no argument, otherwise an argument is required.
+// Named arguments default to 'bool', positional arguments to 'string'.
+// - nargs: the only supported value is `+`, which means to grab all following
+// arguments, up to the next named option, and store them as a list.
+//
+// The command line is parsed for `--foo=value` and `--bar` arguments.
+//
+// Return value is a dict of parameter values, keyed off of `dest` as determined
+// above. An extra option named "rest" will be set to the list of all remaining
+// arguments passed in.
+//
+function parse_options(parameters, inArgs = scriptArgs) {
+ const options = {};
+
+ const named = {};
+ const positional = [];
+ for (const param of parameters) {
+ if (param.name.startsWith("-")) {
+ named[param.name] = param;
+ if (!param.dest) {
+ if (!param.name.startsWith("--")) {
+ throw new Error(`parameter '${param.name}' requires param.dest to be set`);
+ }
+ param.dest = param.name.substring(2).replace("-", "_");
+ }
+ } else {
+ if (!('default' in param) && positional.length > 0 && ('default' in positional.at(-1))) {
+ throw new Error(`required parameter '${param.name}' follows optional parameter`);
+ }
+ param.positional = true;
+ positional.push(param);
+ param.dest = param.dest || param.name.replace("-", "_");
+ }
+
+ if (!param.type) {
+ if (param.nargs === "+") {
+ param.type = "list";
+ } else if (param.positional) {
+ param.type = "string";
+ } else {
+ param.type = "bool";
+ }
+ }
+
+ if ('default' in param) {
+ options[param.dest] = param.default;
+ }
+ }
+
+ options.rest = [];
+ const args = [...inArgs];
+ let grabbing_into = undefined;
+ while (args.length > 0) {
+ let arg = args.shift();
+ let param;
+ if (arg.startsWith("-") && arg in named) {
+ param = named[arg];
+ if (param.type !== 'bool') {
+ if (args.length == 0) {
+ throw(new Error(`${param.name} requires an argument`));
+ }
+ arg = args.shift();
+ }
+ } else {
+ const pos = arg.indexOf("=");
+ if (pos != -1) {
+ const name = arg.substring(0, pos);
+ param = named[name];
+ if (!param) {
+ throw(new Error(`Unknown option '${name}'`));
+ } else if (param.type === 'bool') {
+ throw(new Error(`--${param.name} does not take an argument`));
+ }
+ arg = arg.substring(pos + 1);
+ }
+ }
+
+ // If this isn't a --named param, and we're not accumulating into a nargs="+" param, then
+ // use the next positional.
+ if (!param && !grabbing_into && positional.length > 0) {
+ param = positional.shift();
+ }
+
+ // If a parameter was identified, then any old accumulator is done and we might start a new one.
+ if (param) {
+ if (param.type === 'list') {
+ grabbing_into = options[param.dest] = options[param.dest] || [];
+ } else {
+ grabbing_into = undefined;
+ }
+ }
+
+ if (grabbing_into) {
+ grabbing_into.push(arg);
+ } else if (param) {
+ if (param.type === 'bool') {
+ options[param.dest] = true;
+ } else {
+ options[param.dest] = arg;
+ }
+ } else {
+ options.rest.push(arg);
+ }
+ }
+
+ for (const param of positional) {
+ if (!('default' in param)) {
+ throw(new Error(`'${param.name}' option is required`));
+ }
+ }
+
+ for (const param of parameters) {
+ if (param.nargs === '+' && options[param.dest].length == 0) {
+ throw(new Error(`at least one value required for option '${param.name}'`));
+ }
+ }
+
+ return options;
+}
+
+function sameBlockId(id0, id1)
+{
+ if (id0.Kind != id1.Kind)
+ return false;
+ if (!sameVariable(id0.Variable, id1.Variable))
+ return false;
+ if (id0.Kind == "Loop" && id0.Loop != id1.Loop)
+ return false;
+ return true;
+}
+
+function sameVariable(var0, var1)
+{
+ assert("Name" in var0 || var0.Kind == "This" || var0.Kind == "Return");
+ assert("Name" in var1 || var1.Kind == "This" || var1.Kind == "Return");
+ if ("Name" in var0)
+ return "Name" in var1 && var0.Name[0] == var1.Name[0];
+ return var0.Kind == var1.Kind;
+}
+
+function blockIdentifier(body)
+{
+ if (body.BlockId.Kind == "Loop")
+ return body.BlockId.Loop;
+ assert(body.BlockId.Kind == "Function", "body.Kind should be Function, not " + body.BlockId.Kind);
+ return body.BlockId.Variable.Name[0];
+}
+
+function collectBodyEdges(body)
+{
+ body.predecessors = [];
+ body.successors = [];
+ if (!("PEdge" in body))
+ return;
+
+ for (var edge of body.PEdge) {
+ var [ source, target ] = edge.Index;
+ if (!(target in body.predecessors))
+ body.predecessors[target] = [];
+ body.predecessors[target].push(edge);
+ if (!(source in body.successors))
+ body.successors[source] = [];
+ body.successors[source].push(edge);
+ }
+}
+
+function getPredecessors(body)
+{
+ if (!('predecessors' in body))
+ collectBodyEdges(body);
+ return body.predecessors;
+}
+
+function getSuccessors(body)
+{
+ if (!('successors' in body))
+ collectBodyEdges(body);
+ return body.successors;
+}
+
+// Split apart a function from sixgill into its mangled and unmangled name. If
+// no mangled name was given, use the unmangled name as its mangled name
+function splitFunction(func)
+{
+ var split = func.indexOf("$");
+ if (split != -1)
+ return [ func.substr(0, split), func.substr(split+1) ];
+ split = func.indexOf("|");
+ if (split != -1)
+ return [ func.substr(0, split), func.substr(split+1) ];
+ return [ func, func ];
+}
+
+function mangled(fullname)
+{
+ var [ mangled, unmangled ] = splitFunction(fullname);
+ return mangled;
+}
+
+function readable(fullname)
+{
+ var [ mangled, unmangled ] = splitFunction(fullname);
+ return unmangled;
+}
+
+function xdbLibrary()
+{
+ var lib = ctypes.open(os.getenv('XDB'));
+ var api = {
+ open: lib.declare("xdb_open", ctypes.default_abi, ctypes.void_t, ctypes.char.ptr),
+ min_data_stream: lib.declare("xdb_min_data_stream", ctypes.default_abi, ctypes.int),
+ max_data_stream: lib.declare("xdb_max_data_stream", ctypes.default_abi, ctypes.int),
+ read_key: lib.declare("xdb_read_key", ctypes.default_abi, ctypes.char.ptr, ctypes.int),
+ read_entry: lib.declare("xdb_read_entry", ctypes.default_abi, ctypes.char.ptr, ctypes.char.ptr),
+ free_string: lib.declare("xdb_free", ctypes.default_abi, ctypes.void_t, ctypes.char.ptr)
+ };
+ try {
+ api.lookup_key = lib.declare("xdb_lookup_key", ctypes.default_abi, ctypes.int, ctypes.char.ptr);
+ } catch (e) {
+ // lookup_key is for development use only and is not strictly necessary.
+ }
+ return api;
+}
+
+function openLibrary(names) {
+ for (const name of names) {
+ try {
+ return ctypes.open(name);
+ } catch(e) {
+ }
+ }
+ return undefined;
+}
+
+function cLibrary()
+{
+ const lib = openLibrary(['libc.so.6', 'libc.so', 'libc.dylib']);
+ if (!lib) {
+ throw new Error("Unable to open libc");
+ }
+
+ if (getBuildConfiguration("moz-memory")) {
+ throw new Error("cannot use libc functions with --enable-jemalloc, since they will be routed " +
+ "through jemalloc, but calling libc.free() directly will bypass it and the " +
+ "malloc/free will be mismatched");
+ }
+
+ return {
+ fopen: lib.declare("fopen", ctypes.default_abi, ctypes.void_t.ptr, ctypes.char.ptr, ctypes.char.ptr),
+ getline: lib.declare("getline", ctypes.default_abi, ctypes.ssize_t, ctypes.char.ptr.ptr, ctypes.size_t.ptr, ctypes.void_t.ptr),
+ fclose: lib.declare("fclose", ctypes.default_abi, ctypes.int, ctypes.void_t.ptr),
+ free: lib.declare("free", ctypes.default_abi, ctypes.void_t, ctypes.void_t.ptr),
+ };
+}
+
+function* readFileLines_gen(filename)
+{
+ var libc = cLibrary();
+ var linebuf = ctypes.char.ptr();
+ var bufsize = ctypes.size_t(0);
+ var fp = libc.fopen(filename, "r");
+ if (fp.isNull())
+ throw new Error("Unable to open '" + filename + "'");
+
+ while (libc.getline(linebuf.address(), bufsize.address(), fp) > 0)
+ yield linebuf.readString();
+ libc.fclose(fp);
+ libc.free(ctypes.void_t.ptr(linebuf));
+}
+
+function addToKeyedList(collection, key, entry)
+{
+ if (!(key in collection))
+ collection[key] = [];
+ collection[key].push(entry);
+ return collection[key];
+}
+
+function addToMappedList(map, key, entry)
+{
+ if (!map.has(key))
+ map.set(key, []);
+ map.get(key).push(entry);
+ return map.get(key);
+}
+
+function loadTypeInfo(filename)
+{
+ return JSON.parse(os.file.readFile(filename));
+}
+
+// Given the range `first` .. `last`, break it down into `count` batches and
+// return the start of the (1-based) `num` batch.
+function batchStart(num, count, first, last) {
+ const N = (last - first) + 1;
+ return Math.floor((num - 1) / count * N) + first;
+}
+
+// As above, but return the last value in the (1-based) `num` batch.
+function batchLast(num, count, first, last) {
+ const N = (last - first) + 1;
+ return Math.floor(num / count * N) + first - 1;
+}
+
+// Debugging tool. See usage below.
+function PropertyTracer(traced_prop, check) {
+ return {
+ matches(prop, value) {
+ if (prop != traced_prop)
+ return false;
+ if ('value' in check)
+ return value == check.value;
+ return true;
+ },
+
+ // Also called when defining a property.
+ set(obj, prop, value) {
+ if (this.matches(prop, value))
+ debugger;
+ return Reflect.set(...arguments);
+ },
+ };
+}
+
+// Usage: var myobj = traced({}, 'name', {value: 'Bob'})
+//
+// This will execute a `debugger;` statement when myobj['name'] is defined or
+// set to 'Bob'.
+function traced(obj, traced_prop, check) {
+ return new Proxy(obj, PropertyTracer(traced_prop, check));
+}
diff --git a/js/src/devtools/vprof/manifest.mk b/js/src/devtools/vprof/manifest.mk
new file mode 100644
index 0000000000..e18a17fb5d
--- /dev/null
+++ b/js/src/devtools/vprof/manifest.mk
@@ -0,0 +1,7 @@
+# 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/.
+
+avmplus_CXXSRCS := $(avmplus_CXXSRCS) \
+ $(curdir)/vprof.cpp \
+ $(NULL)
diff --git a/js/src/devtools/vprof/readme.txt b/js/src/devtools/vprof/readme.txt
new file mode 100644
index 0000000000..f84bfc27e5
--- /dev/null
+++ b/js/src/devtools/vprof/readme.txt
@@ -0,0 +1,97 @@
+# 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/.
+
+The two files vprof.h and vprof.cpp implement a simple value-profiling mechanism. By including these two files in avmplus (or any other project), you can value profile data as you wish (currently integers).
+
+Usage:
+#include "vprof.h" // in the source file you want to use it
+
+_vprof (value);
+
+At the end of the execution, for each probe you'll get the data associated with the probe, such as:
+
+File line avg [min : max] total count
+..\..\pcre\pcre_valid_utf8.cpp 182 50222.75916 [0 : 104947] 4036955604 80381
+
+The probe is defined at line 182 of file pcre_vali_utf8.cpp. It was called 80381 times. The min value of the probe was 0 while its max was 10497 and its average was 50222.75916. The total sum of all values of the probe is 4036955604. Later, I plan to add more options on the spectrum of data among others.
+
+A few typical uses
+------------------
+
+To see how many times a given function gets executed do:
+
+void f()
+{
+ _vprof(1);
+ ...
+}
+
+void f()
+{
+ _vprof(1);
+ ...
+ if (...) {
+ _vprof(1);
+ ...
+ } else {
+ _vprof(1);
+ ...
+ }
+}
+
+Here are a few examples of using the value-profiling utility:
+
+ _vprof (e);
+ at the end of program execution, you'll get a dump of the source location of this probe,
+ its min, max, average, the total sum of all instances of e, and the total number of times this probe was called.
+
+ _vprof (x > 0);
+ shows how many times and what percentage of the cases x was > 0,
+ that is the probablitiy that x > 0.
+
+ _vprof (n % 2 == 0);
+ shows how many times n was an even number
+ as well as th probablitiy of n being an even number.
+
+ _hprof (n, 4, 1000, 5000, 5001, 10000);
+ gives you the histogram of n over the given 4 bucket boundaries:
+ # cases < 1000
+ # cases >= 1000 and < 5000
+ # cases >= 5000 and < 5001
+ # cases >= 5001 and < 10000
+ # cases >= 10000
+
+ _nvprof ("event name", value);
+ all instances with the same name are merged
+ so, you can call _vprof with the same event name at difference places
+
+ _vprof (e, myProbe);
+ value profile e and call myProbe (void* vprofID) at the profiling point.
+ inside the probe, the client has the predefined variables:
+ _VAL, _COUNT, _SUM, _MIN, _MAX, and the general purpose registers
+ _IVAR1, ..., IVAR4 general integer registrs
+ _I64VAR1, ..., I64VAR4 general integer64 registrs
+ _DVAR1, ..., _DVAR4 general double registers
+ _GENPTR a generic pointer that can be used by the client
+ the number of registers can be changed in vprof.h
+
+Named Events
+------------
+_nvprof ("event name", value);
+ all instances with the same name are merged
+ so, you can call _vprof with the same event name at difference places
+
+
+Custom Probes
+--------------
+You can call your own custom probe at the profiling point.
+_vprof (v, myProbe);
+ value profile v and call myProbe (void* vprofID) at the profiling point
+ inside the probe, the client has the predefined variables:
+ _VAL, _COUNT, _SUM, _MIN, _MAX, and the general purpose registers
+ _IVAR1, ..., IVAR4 general integer registrs
+ _I64VAR1, ..., I64VAR4 general integer64 registrs
+ _DVAR1, ..., _DVAR4 general double registers
+ the number of registers can be changed in vprof.h
+ _GENPTR a generic pointer that can be used for almost anything
diff --git a/js/src/devtools/vprof/testVprofMT.c b/js/src/devtools/vprof/testVprofMT.c
new file mode 100644
index 0000000000..da85389565
--- /dev/null
+++ b/js/src/devtools/vprof/testVprofMT.c
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: t; tab-width: 4 -*- */
+/* 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/. */
+
+#include <windows.h>
+#include <stdio.h>
+#include <time.h>
+
+#include "vprof.h"
+
+static void cProbe(void* vprofID) {
+ if (_VAL == _IVAR1) _I64VAR1++;
+ _IVAR1 = _IVAR0;
+
+ if (_VAL == _IVAR0) _I64VAR0++;
+ _IVAR0 = (int)_VAL;
+
+ _DVAR0 = ((double)_I64VAR0) / _COUNT;
+ _DVAR1 = ((double)_I64VAR1) / _COUNT;
+}
+
+//__declspec (thread) boolean cv;
+// #define if(c) cv = (c); _vprof (cv); if (cv)
+// #define if(c) cv = (c); _vprof (cv, cProbe); if (cv)
+
+#define THREADS 1
+#define COUNT 100000
+#define SLEEPTIME 0
+
+static int64_t evens = 0;
+static int64_t odds = 0;
+
+void sub(int val) {
+ int i;
+ //_vprof (1);
+ for (i = 0; i < COUNT; i++) {
+ //_nvprof ("Iteration", 1);
+ //_nvprof ("Iteration", 1);
+ _vprof(i);
+ //_vprof (i);
+ //_hprof(i, 3, (int64_t) 1000, (int64_t)2000, (int64_t)3000);
+ //_hprof(i, 3, 10000, 10001, 3000000);
+ //_nhprof("Event", i, 3, 10000, 10001, 3000000);
+ //_nhprof("Event", i, 3, 10000, 10001, 3000000);
+ // Sleep(SLEEPTIME);
+ if (i % 2 == 0) {
+ //_vprof (i);
+ ////_hprof(i, 3, 10000, 10001, 3000000);
+ //_nvprof ("Iteration", i);
+ evens++;
+ } else {
+ //_vprof (1);
+ _vprof(i, cProbe);
+ odds++;
+ }
+ //_nvprof ("Iterate", 1);
+ }
+ // printf("sub %d done.\n", val);
+}
+
+HANDLE array[THREADS];
+
+static int run(void) {
+ int i;
+
+ time_t start_time = time(0);
+
+ for (i = 0; i < THREADS; i++) {
+ array[i] = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)sub, (LPVOID)i, 0, 0);
+ }
+
+ for (i = 0; i < THREADS; i++) {
+ WaitForSingleObject(array[i], INFINITE);
+ }
+
+ return 0;
+}
+
+int main() {
+ DWORD start, end;
+
+ start = GetTickCount();
+ run();
+ end = GetTickCount();
+
+ printf("\nRun took %d msecs\n\n", end - start);
+}
diff --git a/js/src/devtools/vprof/vprof.cpp b/js/src/devtools/vprof/vprof.cpp
new file mode 100644
index 0000000000..873c354b7e
--- /dev/null
+++ b/js/src/devtools/vprof/vprof.cpp
@@ -0,0 +1,359 @@
+/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: t; tab-width: 4 -*- */
+/* 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/. */
+
+#include "VMPI.h"
+
+// Note, this is not supported in configurations with more than one AvmCore
+// running in the same process.
+
+#ifdef WIN32
+# include "util/WindowsWrapper.h"
+#else
+# define __cdecl
+# include <stdarg.h>
+# include <string.h>
+#endif
+
+#include "vprof.h"
+
+#ifndef MIN
+# define MIN(x, y) ((x) <= (y) ? x : y)
+#endif
+#ifndef MAX
+# define MAX(x, y) ((x) >= (y) ? x : y)
+#endif
+
+#ifndef MAXINT
+# define MAXINT int(unsigned(-1) >> 1)
+#endif
+
+#ifndef MAXINT64
+# define MAXINT64 int64_t(uint64_t(-1) >> 1)
+#endif
+
+#ifndef __STDC_WANT_SECURE_LIB__
+# define sprintf_s(b, size, fmt, ...) sprintf((b), (fmt), __VA_ARGS__)
+#endif
+
+#if THREADED
+# define DO_LOCK(lock) \
+ Lock(lock); \
+ {
+# define DO_UNLOCK(lock) \
+ } \
+ ; \
+ Unlock(lock)
+#else
+# define DO_LOCK(lock) \
+ { \
+ (void)(lock);
+# define DO_UNLOCK(lock) }
+#endif
+
+#if THREAD_SAFE
+# define LOCK(lock) DO_LOCK(lock)
+# define UNLOCK(lock) DO_UNLOCK(lock)
+#else
+# define LOCK(lock) \
+ { \
+ (void)(lock);
+# define UNLOCK(lock) }
+#endif
+
+static entry* entries = nullptr;
+static bool notInitialized = true;
+static long glock = LOCK_IS_FREE;
+
+#define Lock(lock) \
+ while (_InterlockedCompareExchange(lock, LOCK_IS_TAKEN, LOCK_IS_FREE) == \
+ LOCK_IS_TAKEN) { \
+ };
+#define Unlock(lock) \
+ _InterlockedCompareExchange(lock, LOCK_IS_FREE, LOCK_IS_TAKEN);
+
+#if defined(WIN32)
+static void vprof_printf(const char* format, ...) {
+ va_list args;
+ va_start(args, format);
+
+ char buf[1024];
+ vsnprintf(buf, sizeof(buf), format, args);
+
+ va_end(args);
+
+ printf(buf);
+ ::OutputDebugStringA(buf);
+}
+#else
+# define vprof_printf printf
+#endif
+
+static inline entry* reverse(entry* s) {
+ entry_t e, n, p;
+
+ p = nullptr;
+ for (e = s; e; e = n) {
+ n = e->next;
+ e->next = p;
+ p = e;
+ }
+
+ return p;
+}
+
+static char* f(double d) {
+ static char s[80];
+ char* p;
+ sprintf_s(s, sizeof(s), "%lf", d);
+ p = s + VMPI_strlen(s) - 1;
+ while (*p == '0') {
+ *p = '\0';
+ p--;
+ if (p == s) break;
+ }
+ if (*p == '.') *p = '\0';
+ return s;
+}
+
+static void dumpProfile(void) {
+ entry_t e;
+
+ entries = reverse(entries);
+ vprof_printf("event avg [min : max] total count\n");
+ for (e = entries; e; e = e->next) {
+ if (e->count == 0) continue; // ignore entries with zero count.
+ vprof_printf("%s", e->file);
+ if (e->line >= 0) {
+ vprof_printf(":%d", e->line);
+ }
+ vprof_printf(" %s [%lld : %lld] %lld %lld ",
+ f(((double)e->sum) / ((double)e->count)),
+ (long long int)e->min, (long long int)e->max,
+ (long long int)e->sum, (long long int)e->count);
+ if (e->h) {
+ int j = MAXINT;
+ for (j = 0; j < e->h->nbins; j++) {
+ vprof_printf("(%lld < %lld) ", (long long int)e->h->count[j],
+ (long long int)e->h->lb[j]);
+ }
+ vprof_printf("(%lld >= %lld) ", (long long int)e->h->count[e->h->nbins],
+ (long long int)e->h->lb[e->h->nbins - 1]);
+ }
+ if (e->func) {
+ int j;
+ for (j = 0; j < NUM_EVARS; j++) {
+ if (e->ivar[j] != 0) {
+ vprof_printf("IVAR%d %d ", j, e->ivar[j]);
+ }
+ }
+ for (j = 0; j < NUM_EVARS; j++) {
+ if (e->i64var[j] != 0) {
+ vprof_printf("I64VAR%d %lld ", j, (long long int)e->i64var[j]);
+ }
+ }
+ for (j = 0; j < NUM_EVARS; j++) {
+ if (e->dvar[j] != 0) {
+ vprof_printf("DVAR%d %lf ", j, e->dvar[j]);
+ }
+ }
+ }
+ vprof_printf("\n");
+ }
+ entries = reverse(entries);
+}
+
+static inline entry_t findEntry(char* file, int line) {
+ for (entry_t e = entries; e; e = e->next) {
+ if ((e->line == line) && (VMPI_strcmp(e->file, file) == 0)) {
+ return e;
+ }
+ }
+ return nullptr;
+}
+
+// Initialize the location pointed to by 'id' to a new value profile entry
+// associated with 'file' and 'line', or do nothing if already initialized.
+// An optional final argument provides a user-defined probe function.
+
+int initValueProfile(void** id, char* file, int line, ...) {
+ DO_LOCK(&glock);
+ entry_t e = (entry_t)*id;
+ if (notInitialized) {
+ atexit(dumpProfile);
+ notInitialized = false;
+ }
+
+ if (e == nullptr) {
+ e = findEntry(file, line);
+ if (e) {
+ *id = e;
+ }
+ }
+
+ if (e == nullptr) {
+ va_list va;
+ e = (entry_t)malloc(sizeof(entry));
+ e->lock = LOCK_IS_FREE;
+ e->file = file;
+ e->line = line;
+ e->value = 0;
+ e->sum = 0;
+ e->count = 0;
+ e->min = 0;
+ e->max = 0;
+ // optional probe function argument
+ va_start(va, line);
+ e->func = (void(__cdecl*)(void*))va_arg(va, void*);
+ va_end(va);
+ e->h = nullptr;
+ e->genptr = nullptr;
+ VMPI_memset(&e->ivar, 0, sizeof(e->ivar));
+ VMPI_memset(&e->i64var, 0, sizeof(e->i64var));
+ VMPI_memset(&e->dvar, 0, sizeof(e->dvar));
+ e->next = entries;
+ entries = e;
+ *id = e;
+ }
+ DO_UNLOCK(&glock);
+
+ return 0;
+}
+
+// Record a value profile event.
+
+int profileValue(void* id, int64_t value) {
+ entry_t e = (entry_t)id;
+ long* lock = &(e->lock);
+ LOCK(lock);
+ e->value = value;
+ if (e->count == 0) {
+ e->sum = value;
+ e->count = 1;
+ e->min = value;
+ e->max = value;
+ } else {
+ e->sum += value;
+ e->count++;
+ e->min = MIN(e->min, value);
+ e->max = MAX(e->max, value);
+ }
+ if (e->func) e->func(e);
+ UNLOCK(lock);
+
+ return 0;
+}
+
+// Initialize the location pointed to by 'id' to a new histogram profile entry
+// associated with 'file' and 'line', or do nothing if already initialized.
+
+int initHistProfile(void** id, char* file, int line, int nbins, ...) {
+ DO_LOCK(&glock);
+ entry_t e = (entry_t)*id;
+ if (notInitialized) {
+ atexit(dumpProfile);
+ notInitialized = false;
+ }
+
+ if (e == nullptr) {
+ e = findEntry(file, line);
+ if (e) {
+ *id = e;
+ }
+ }
+
+ if (e == nullptr) {
+ va_list va;
+ hist_t h;
+ int b, n, s;
+ int64_t* lb;
+
+ e = (entry_t)malloc(sizeof(entry));
+ e->lock = LOCK_IS_FREE;
+ e->file = file;
+ e->line = line;
+ e->value = 0;
+ e->sum = 0;
+ e->count = 0;
+ e->min = 0;
+ e->max = 0;
+ e->func = nullptr;
+ e->h = h = (hist_t)malloc(sizeof(hist));
+ n = 1 + MAX(nbins, 0);
+ h->nbins = n - 1;
+ s = n * sizeof(int64_t);
+ lb = (int64_t*)malloc(s);
+ h->lb = lb;
+ VMPI_memset(h->lb, 0, s);
+ h->count = (int64_t*)malloc(s);
+ VMPI_memset(h->count, 0, s);
+
+ va_start(va, nbins);
+ for (b = 0; b < nbins; b++) {
+ // lb[b] = va_arg (va, int64_t);
+ lb[b] = va_arg(va, int);
+ }
+ lb[b] = MAXINT64;
+ va_end(va);
+
+ e->genptr = nullptr;
+ VMPI_memset(&e->ivar, 0, sizeof(e->ivar));
+ VMPI_memset(&e->i64var, 0, sizeof(e->i64var));
+ VMPI_memset(&e->dvar, 0, sizeof(e->dvar));
+ e->next = entries;
+ entries = e;
+ *id = e;
+ }
+ DO_UNLOCK(&glock);
+
+ return 0;
+}
+
+// Record a histogram profile event.
+
+int histValue(void* id, int64_t value) {
+ entry_t e = (entry_t)id;
+ long* lock = &(e->lock);
+ hist_t h = e->h;
+ int nbins = h->nbins;
+ int64_t* lb = h->lb;
+ int b;
+
+ LOCK(lock);
+ e->value = value;
+ if (e->count == 0) {
+ e->sum = value;
+ e->count = 1;
+ e->min = value;
+ e->max = value;
+ } else {
+ e->sum += value;
+ e->count++;
+ e->min = MIN(e->min, value);
+ e->max = MAX(e->max, value);
+ }
+ for (b = 0; b < nbins; b++) {
+ if (value < lb[b]) break;
+ }
+ h->count[b]++;
+ UNLOCK(lock);
+
+ return 0;
+}
+
+#if defined(_MSC_VER) && defined(_M_IX86)
+uint64_t readTimestampCounter() {
+ // read the cpu cycle counter. 1 tick = 1 cycle on IA32
+ _asm rdtsc;
+}
+#elif defined(__GNUC__) && (__i386__ || __x86_64__)
+uint64_t readTimestampCounter() {
+ uint32_t lo, hi;
+ __asm__ __volatile__("rdtsc" : "=a"(lo), "=d"(hi));
+ return (uint64_t(hi) << 32) | lo;
+}
+#else
+// add stub for platforms without it, so fat builds don't fail
+uint64_t readTimestampCounter() { return 0; }
+#endif
diff --git a/js/src/devtools/vprof/vprof.h b/js/src/devtools/vprof/vprof.h
new file mode 100644
index 0000000000..3d7b290d04
--- /dev/null
+++ b/js/src/devtools/vprof/vprof.h
@@ -0,0 +1,270 @@
+/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: t; tab-width: 4 -*- */
+/* 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/. */
+
+//
+// Here are a few examples of using the value-profiling utility:
+//
+// _vprof (e);
+// at the end of program execution, you'll get a dump of the source location
+// of this probe, its min, max, average, the total sum of all instances of e,
+// and the total number of times this probe was called.
+//
+// _vprof (x > 0);
+// shows how many times and what percentage of the cases x was > 0,
+// that is the probablitiy that x > 0.
+//
+// _vprof (n % 2 == 0);
+// shows how many times n was an even number
+// as well as th probablitiy of n being an even number.
+//
+// _hprof (n, 4, 1000, 5000, 5001, 10000);
+// gives you the histogram of n over the given 4 bucket boundaries:
+// # cases < 1000
+// # cases >= 1000 and < 5000
+// # cases >= 5000 and < 5001
+// # cases >= 5001 and < 10000
+// # cases >= 10000
+//
+// _nvprof ("event name", value);
+// all instances with the same name are merged
+// so, you can call _vprof with the same event name at difference places
+//
+// _vprof (e, myProbe);
+// value profile e and call myProbe (void* vprofID) at the profiling point.
+// inside the probe, the client has the predefined variables:
+// _VAL, _COUNT, _SUM, _MIN, _MAX, and the general purpose registers
+// _IVAR1, ..., IVAR4 general integer registrs
+// _I64VAR1, ..., I64VAR4 general integer64 registrs
+// _DVAR1, ..., _DVAR4 general double registers
+// _GENPTR a generic pointer that can be used by the client
+// the number of registers can be changed in vprof.h
+//
+
+#ifndef devtools_vprof_vprof_h
+#define devtools_vprof_vprof_h
+//
+// If the application for which you want to use vprof is threaded, THREADED must
+// be defined as 1, otherwise define it as 0
+//
+// If your application is not threaded, define THREAD_SAFE 0,
+// otherwise, you have the option of setting THREAD_SAFE to 1 which results in
+// exact counts or to 0 which results in a much more efficient but non-exact
+// counts
+//
+#define THREADED 0
+#define THREAD_SAFE 0
+
+#include "VMPI.h"
+
+// Note, this is not supported in configurations with more than one AvmCore
+// running in the same process.
+
+// portable align macro
+#if defined(_MSC_VER)
+# define vprof_align8(t) __declspec(align(8)) t
+#elif defined(__GNUC__)
+# define vprof_align8(t) t __attribute__((aligned(8)))
+#elif defined(__SUNPRO_C) || defined(__SUNPRO_CC)
+# define vprof_align8(t) t __attribute__((aligned(8)))
+#elif defined(VMCFG_SYMBIAN)
+# define vprof_align8(t) t __attribute__((aligned(8)))
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+int initValueProfile(void** id, char* file, int line, ...);
+int profileValue(void* id, int64_t value);
+int initHistProfile(void** id, char* file, int line, int nbins, ...);
+int histValue(void* id, int64_t value);
+uint64_t readTimestampCounter();
+
+#ifdef __cplusplus
+}
+#endif
+
+// #define DOPROF
+
+#ifndef DOPROF
+# define _nvprof(e, v)
+# ifndef VMCFG_SYMBIAN
+# define _vprof(v, ...)
+# define _hprof(v, n, ...)
+# define _nhprof(e, v, n, ...)
+# define _ntprof_begin(e)
+# define _ntprof_end(e)
+# define _jvprof_init(id, ...)
+# define _jnvprof_init(id, e, ...)
+# define _jhprof_init(id, n, ...)
+# define _jnhprof_init(id, e, n, ...)
+# define _jvprof(id, v)
+# define _jhprof(id, v)
+# endif // ! VMCFG_SYMBIAN
+#else
+
+// Historical/compatibility note:
+// The macros below were originally written using conditional expressions, not
+// if/else. The original author said that this was done to allow _vprof and
+// _nvprof to be used in an expression context, but the old code had already
+// wrapped the macro bodies in { }, so it is not clear how this could have
+// worked. At present, the profiling macros must appear in a statement context
+// only.
+
+# define _vprof(v, ...) \
+ do { \
+ static void* id = 0; \
+ if (id == 0) \
+ initValueProfile(&id, __FILE__, __LINE__, ##__VA_ARGS__, NULL); \
+ profileValue(id, (int64_t)(v)); \
+ } while (0)
+
+# define _nvprof(e, v) \
+ do { \
+ static void* id = 0; \
+ if (id == 0) initValueProfile(&id, (char*)(e), -1, NULL); \
+ profileValue(id, (int64_t)(v)); \
+ } while (0)
+
+# define _hprof(v, n, ...) \
+ do { \
+ static void* id = 0; \
+ if (id == 0) \
+ initHistProfile(&id, __FILE__, __LINE__, (int)(n), ##__VA_ARGS__); \
+ histValue(id, (int64_t)(v)); \
+ } while (0)
+
+# define _nhprof(e, v, n, ...) \
+ do { \
+ static void* id = 0; \
+ if (id == 0) \
+ initHistProfile(&id, (char*)(e), -1, (int)(n), ##__VA_ARGS__); \
+ histValue(id, (int64_t)(v)); \
+ } while (0)
+
+// Profile execution time between _ntprof_begin(e) and _ntprof_end(e).
+// The tag 'e' must match at the beginning and end of the region to
+// be timed. Regions may be nested or overlap arbitrarily, as it is
+// the tag alone that defines the begin/end correspondence.
+
+# define _ntprof_begin(e) \
+ do { \
+ static void* id = 0; \
+ if (id == 0) initValueProfile(&id, (char*)(e), -1, NULL); \
+ ((entry_t)id)->i64var[0] = readTimestampCounter(); \
+ } while (0)
+
+// Assume 2.6 Ghz CPU
+# define TICKS_PER_USEC 2600
+
+# define _ntprof_end(e) \
+ do { \
+ static void* id = 0; \
+ uint64_t stop = readTimestampCounter(); \
+ if (id == 0) initValueProfile(&id, (char*)(e), -1, NULL); \
+ uint64_t start = ((entry_t)id)->i64var[0]; \
+ uint64_t usecs = (stop - start) / TICKS_PER_USEC; \
+ profileValue(id, usecs); \
+ } while (0)
+
+// These macros separate the creation of a profile record from its later usage.
+// They are intended for profiling JIT-generated code. Once created, the JIT
+// can bind a pointer to the profile record into the generated code, which can
+// then record profile events during execution.
+
+# define _jvprof_init(id, ...) \
+ if (*(id) == 0) \
+ initValueProfile((id), __FILE__, __LINE__, ##__VA_ARGS__, NULL)
+
+# define _jnvprof_init(id, e, ...) \
+ if (*(id) == 0) initValueProfile((id), (char*)(e), -1, ##__VA_ARGS__, NULL)
+
+# define _jhprof_init(id, n, ...) \
+ if (*(id) == 0) \
+ initHistProfile((id), __FILE__, __LINE__, (int)(n), ##__VA_ARGS__)
+
+# define _jnhprof_init(id, e, n, ...) \
+ if (*(id) == 0) \
+ initHistProfile((id), (char*)(e), -1, (int)(n), ##__VA_ARGS__)
+
+// Calls to the _jvprof and _jhprof macros must be wrapped in a non-inline
+// function in order to be invoked from JIT-compiled code.
+
+# define _jvprof(id, v) profileValue((id), (int64_t)(v))
+
+# define _jhprof(id, v) histValue((id), (int64_t)(v))
+
+#endif
+
+#define NUM_EVARS 4
+
+enum { LOCK_IS_FREE = 0, LOCK_IS_TAKEN = 1 };
+
+extern
+#ifdef __cplusplus
+ "C"
+#endif
+ long
+ _InterlockedCompareExchange(long volatile* Destination, long Exchange,
+ long Comperand);
+
+typedef struct hist hist;
+
+typedef struct hist {
+ int nbins;
+ int64_t* lb;
+ int64_t* count;
+}* hist_t;
+
+typedef struct entry entry;
+
+typedef struct entry {
+ long lock;
+ char* file;
+ int line;
+ int64_t value;
+ int64_t count;
+ int64_t sum;
+ int64_t min;
+ int64_t max;
+ void (*func)(void*);
+ hist* h;
+
+ entry* next;
+
+ // exposed to the clients
+ void* genptr;
+ int ivar[NUM_EVARS];
+ vprof_align8(int64_t) i64var[NUM_EVARS];
+ vprof_align8(double) dvar[NUM_EVARS];
+ //
+
+ char pad[128]; // avoid false sharing
+}* entry_t;
+
+#define _VAL ((entry_t)vprofID)->value
+#define _COUNT ((entry_t)vprofID)->count
+#define _SUM ((entry_t)vprofID)->sum
+#define _MIN ((entry_t)vprofID)->min
+#define _MAX ((entry_t)vprofID)->max
+
+#define _GENPTR ((entry_t)vprofID)->genptr
+
+#define _IVAR0 ((entry_t)vprofID)->ivar[0]
+#define _IVAR1 ((entry_t)vprofID)->ivar[1]
+#define _IVAR2 ((entry_t)vprofID)->ivar[2]
+#define _IVAR3 ((entry_t)vprofID)->ivar[3]
+
+#define _I64VAR0 ((entry_t)vprofID)->i64var[0]
+#define _I64VAR1 ((entry_t)vprofID)->i64var[1]
+#define _I64VAR2 ((entry_t)vprofID)->i64var[2]
+#define _I64VAR3 ((entry_t)vprofID)->i64var[3]
+
+#define _DVAR0 ((entry_t)vprofID)->dvar[0]
+#define _DVAR1 ((entry_t)vprofID)->dvar[1]
+#define _DVAR2 ((entry_t)vprofID)->dvar[2]
+#define _DVAR3 ((entry_t)vprofID)->dvar[3]
+
+#endif /* devtools_vprof_vprof_h */