summaryrefslogtreecommitdiffstats
path: root/js/src/devtools
diff options
context:
space:
mode:
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.py709
-rw-r--r--js/src/devtools/automation/cgc-jittest-timeouts.txt55
-rw-r--r--js/src/devtools/automation/cgc-jstests-slow.txt65
-rw-r--r--js/src/devtools/automation/macbuildenv.sh14
-rw-r--r--js/src/devtools/automation/smoosh-jstests-slow.txt6
-rw-r--r--js/src/devtools/automation/tsan-slow.txt22
-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-cranelift-sim10
-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/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/nonunified13
-rw-r--r--js/src/devtools/automation/variants/plain8
-rw-r--r--js/src/devtools/automation/variants/plaindebug10
-rw-r--r--js/src/devtools/automation/variants/rootanalysis9
-rw-r--r--js/src/devtools/automation/variants/smoosh8
-rw-r--r--js/src/devtools/automation/variants/smooshdebug12
-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/winbuildenv.sh32
-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/javascript-trace.d34
-rwxr-xr-xjs/src/devtools/octane-csv.sh76
-rwxr-xr-xjs/src/devtools/plot-octane.R38
-rwxr-xr-xjs/src/devtools/release/release-notes195
-rw-r--r--js/src/devtools/rootAnalysis/CFG.js179
-rw-r--r--js/src/devtools/rootAnalysis/Makefile.in79
-rw-r--r--js/src/devtools/rootAnalysis/README.md109
-rwxr-xr-xjs/src/devtools/rootAnalysis/analyze.py414
-rw-r--r--js/src/devtools/rootAnalysis/analyzeHeapWrites.js1404
-rw-r--r--js/src/devtools/rootAnalysis/analyzeRoots.js1166
-rw-r--r--js/src/devtools/rootAnalysis/annotations.js529
-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.js247
-rw-r--r--js/src/devtools/rootAnalysis/computeCallgraph.js342
-rw-r--r--js/src/devtools/rootAnalysis/computeGCFunctions.js76
-rw-r--r--js/src/devtools/rootAnalysis/computeGCTypes.js401
-rw-r--r--js/src/devtools/rootAnalysis/dumpCFG.js267
-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.py129
-rwxr-xr-xjs/src/devtools/rootAnalysis/gen-hazards.sh15
-rw-r--r--js/src/devtools/rootAnalysis/loadCallgraph.js428
-rw-r--r--js/src/devtools/rootAnalysis/mach_commands.py393
-rw-r--r--js/src/devtools/rootAnalysis/mozconfig.browser12
-rw-r--r--js/src/devtools/rootAnalysis/mozconfig.common37
-rw-r--r--js/src/devtools/rootAnalysis/mozconfig.haz_shell17
-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.py124
-rwxr-xr-xjs/src/devtools/rootAnalysis/run_complete384
-rw-r--r--js/src/devtools/rootAnalysis/t/exceptions/source.cpp54
-rw-r--r--js/src/devtools/rootAnalysis/t/exceptions/test.py21
-rw-r--r--js/src/devtools/rootAnalysis/t/hazards/source.cpp326
-rw-r--r--js/src/devtools/rootAnalysis/t/hazards/test.py83
-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.py20
-rw-r--r--js/src/devtools/rootAnalysis/t/testlib.py231
-rw-r--r--js/src/devtools/rootAnalysis/t/virtual/source.cpp169
-rw-r--r--js/src/devtools/rootAnalysis/t/virtual/test.py48
-rw-r--r--js/src/devtools/rootAnalysis/utility.js292
-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
119 files changed, 14037 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..6bd14ea63d
--- /dev/null
+++ b/js/src/devtools/automation/autospider.py
@@ -0,0 +1,709 @@
+#!/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 re
+import os
+import platform
+import posixpath
+import shutil
+import subprocess
+import sys
+
+from collections import Counter, namedtuple
+from logging import info
+from os import environ as env
+from subprocess import Popen
+from threading import Timer
+
+Dirs = namedtuple("Dirs", ["scripts", "js_src", "source", "tooltool", "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, "..", ".."))
+ tooltool = pathmodule.abspath(
+ env.get("TOOLTOOL_CHECKOUT", pathmodule.join(source, "..", ".."))
+ )
+ fetches = pathmodule.abspath(
+ env.get("MOZ_FETCHES_DIR", pathmodule.join(source, "..", ".."))
+ )
+ return Dirs(scripts, js_src, source, tooltool, fetches)
+
+
+# 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())
+PDIR = directories(
+ posixpath, os.environ["PWD"], fixup=lambda s: re.sub(r"^(\w):", r"/\1", s)
+)
+env["CPP_UNIT_TESTS_DIR_JS_SRC"] = DIR.js_src
+
+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",
+ default=env.get("OBJDIR", os.path.join(DIR.source, "obj-spider")),
+ 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(
+ "--noconf", action="store_true", help="skip running configure when doing a build"
+)
+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")
+
+OBJDIR = args.objdir
+OUTDIR = os.path.join(OBJDIR, "out")
+POBJDIR = posixpath.join(PDIR.source, args.objdir)
+MAKE = env.get("MAKE", "make")
+MAKEFLAGS = env.get("MAKEFLAGS", "-j6" + ("" if AUTOMATION else " -s"))
+PYTHON = sys.executable
+
+for d in ("scripts", "js_src", "source", "tooltool", "fetches"):
+ info("DIR.{name} = {dir}".format(name=d, dir=getattr(DIR, d)))
+
+
+def set_vars_from_script(script, vars):
+ """Run a shell script, then dump out chosen environment variables. The build
+ system uses shell scripts to do some configuration that we need to
+ borrow. On Windows, the script itself must output the variable settings
+ (in the form "export FOO=<value>"), since otherwise there will be
+ problems with mismatched Windows/POSIX formats.
+ """
+ script_text = "source %s" % script
+ if platform.system() == "Windows":
+ parse_state = "parsing exports"
+ else:
+ script_text += "; echo VAR SETTINGS:; "
+ script_text += "; ".join("echo $" + var for var in vars)
+ parse_state = "scanning"
+ stdout = subprocess.check_output(["sh", "-x", "-c", script_text]).decode()
+ tograb = vars[:]
+ for line in stdout.splitlines():
+ if parse_state == "scanning":
+ if line == "VAR SETTINGS:":
+ parse_state = "grabbing"
+ elif parse_state == "grabbing":
+ var = tograb.pop(0)
+ env[var] = line
+ elif parse_state == "parsing exports":
+ m = re.match(r"export (\w+)=(.*)", line)
+ if m:
+ var, value = m.groups()
+ if var in tograb:
+ env[var] = value
+ info("Setting %s = %s" % (var, value))
+
+
+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)
+
+if args.variant == "nonunified":
+ # Rewrite js/src/**/moz.build to replace UNIFIED_SOURCES to SOURCES.
+ # Note that this modifies the current checkout.
+ for dirpath, dirnames, filenames in os.walk(DIR.js_src):
+ if "moz.build" in filenames:
+ in_place = ["-i"]
+ if platform.system() == "Darwin":
+ in_place.append("")
+ subprocess.check_call(
+ ["sed"]
+ + in_place
+ + ["s/UNIFIED_SOURCES/SOURCES/", os.path.join(dirpath, "moz.build")]
+ )
+
+CONFIGURE_ARGS = variant["configure-args"]
+
+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"
+
+# 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
+
+if "compiler" in variant:
+ compiler = variant["compiler"]
+elif platform.system() == "Windows":
+ compiler = "clang-cl"
+else:
+ compiler = "clang"
+
+# 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"
+
+
+info("using compiler '{}'".format(compiler))
+
+cxx = {"clang": "clang++", "gcc": "g++", "cl": "cl", "clang-cl": "clang-cl"}.get(
+ compiler
+)
+
+compiler_dir = env.get("GCCDIR", os.path.join(DIR.fetches, compiler))
+info("looking for compiler under {}/".format(compiler_dir))
+compiler_libdir = None
+if os.path.exists(os.path.join(compiler_dir, "bin", compiler)):
+ env.setdefault("CC", os.path.join(compiler_dir, "bin", compiler))
+ env.setdefault("CXX", os.path.join(compiler_dir, "bin", cxx))
+ if compiler == "clang":
+ platlib = "lib"
+ else:
+ platlib = "lib64" if word_bits == 64 else "lib"
+ compiler_libdir = os.path.join(compiler_dir, platlib)
+else:
+ env.setdefault("CC", compiler)
+ env.setdefault("CXX", cxx)
+
+bindir = os.path.join(OBJDIR, "dist", "bin")
+env["LD_LIBRARY_PATH"] = ":".join(
+ p for p in (bindir, compiler_libdir, env.get("LD_LIBRARY_PATH")) if p
+)
+
+for v in ("CC", "CXX", "LD_LIBRARY_PATH"):
+ info("default {name} = {value}".format(name=v, value=env[v]))
+
+rust_dir = os.path.join(DIR.fetches, "rustc")
+if os.path.exists(os.path.join(rust_dir, "bin", "rustc")):
+ env.setdefault("RUSTC", os.path.join(rust_dir, "bin", "rustc"))
+ env.setdefault("CARGO", os.path.join(rust_dir, "bin", "cargo"))
+else:
+ env.setdefault("RUSTC", "rustc")
+ env.setdefault("CARGO", "cargo")
+
+if platform.system() == "Darwin":
+ os.environ["SOURCE"] = DIR.source
+ set_vars_from_script(os.path.join(DIR.scripts, "macbuildenv.sh"), ["CC", "CXX"])
+elif platform.system() == "Windows":
+ MAKE = env.get("MAKE", "mozmake")
+ os.environ["SOURCE"] = DIR.source
+ if word_bits == 64:
+ os.environ["USE_64BIT"] = "1"
+ set_vars_from_script(
+ posixpath.join(PDIR.scripts, "winbuildenv.sh"),
+ ["PATH", "VC_PATH", "DIA_SDK_PATH", "CC", "CXX", "WINDOWSSDKDIR"],
+ )
+
+# Configure flags, based on word length and cross-compilation
+if word_bits == 32:
+ if platform.system() == "Windows":
+ CONFIGURE_ARGS += " --target=i686-pc-mingw32"
+ 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-mingw32"
+
+if platform.system() == "Linux" and AUTOMATION:
+ CONFIGURE_ARGS = "--enable-stdcxx-compat --disable-gold " + CONFIGURE_ARGS
+
+# Override environment variant settings conditionally.
+CONFIGURE_ARGS = "{} {}".format(
+ variant.get("conditional-configure-args", {}).get(variant_platform, ""),
+ 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))
+ 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,
+ "TOOLTOOL_CHECKOUT": DIR.tooltool,
+ "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 platform.machine() == "x86_64":
+ use_minidump = variant.get("use_minidump", True)
+ else:
+ use_minidump = False
+else:
+ use_minidump = False
+
+if use_minidump:
+ env.setdefault("MINIDUMP_SAVE_PATH", env["MOZ_UPLOAD_DIR"])
+ env.setdefault("DUMP_SYMS", os.path.join(DIR.fetches, "dump_syms", "dump_syms"))
+ injector_lib = None
+ if platform.system() == "Linux":
+ injector_lib = os.path.join(
+ DIR.tooltool, "breakpad-tools", "libbreakpadinjector.so"
+ )
+ env.setdefault(
+ "MINIDUMP_STACKWALK",
+ os.path.join(DIR.tooltool, "breakpad-tools", "minidump_stackwalk"),
+ )
+ elif platform.system() == "Darwin":
+ injector_lib = os.path.join(
+ DIR.tooltool, "breakpad-tools", "breakpadinjector.dylib"
+ )
+ if not injector_lib or not os.path.exists(injector_lib):
+ 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")))
+
+
+def need_updating_configure(configure):
+ if not os.path.exists(configure):
+ return True
+
+ dep_files = [
+ os.path.join(DIR.js_src, "configure.in"),
+ os.path.join(DIR.js_src, "old-configure.in"),
+ ]
+ for file in dep_files:
+ if os.path.getmtime(file) > os.path.getmtime(configure):
+ return True
+
+ return False
+
+
+if not args.nobuild:
+ CONFIGURE_ARGS += " --enable-nspr-build"
+ CONFIGURE_ARGS += " --prefix={OBJDIR}/dist".format(OBJDIR=POBJDIR)
+
+ # Generate a configure script from configure.in.
+ configure = os.path.join(DIR.js_src, "configure")
+ if need_updating_configure(configure):
+ shutil.copyfile(configure + ".in", configure)
+ os.chmod(configure, 0o755)
+
+ # Run configure
+ if not args.noconf:
+ run_command(
+ [
+ "sh",
+ "-c",
+ posixpath.join(PDIR.js_src, "configure") + " " + CONFIGURE_ARGS,
+ ],
+ check=True,
+ )
+
+ # Run make
+ run_command("%s -w %s" % (MAKE, MAKEFLAGS), shell=True, check=True)
+
+ if use_minidump:
+ # Convert symbols to breakpad format.
+ run_command(
+ [
+ "make",
+ "recurse_syms",
+ "MOZ_SOURCE_REPO=file://" + DIR.source,
+ "RUSTC_COMMIT=0",
+ "MOZ_CRASHREPORTER=1",
+ "MOZ_AUTOMATION_BUILD_SYMBOLS=1",
+ ],
+ check=True,
+ )
+
+COMMAND_PREFIX = []
+# On Linux, disable ASLR to make shell builds a bit more reproducible.
+if 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
+
+
+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 1391877 - Windows test runs are getting mysterious timeouts when run
+# through taskcluster, but only when running multiple jit-test jobs in
+# parallel. Work around them for now.
+if platform.system() == "Windows":
+ env["JITTEST_EXTRA_ARGS"] = "-j1 " + env.get("JITTEST_EXTRA_ARGS", "")
+
+# 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.
+if platform.system() == "Windows":
+ worker_count = min(multiprocessing.cpu_count(), 16)
+ env["JSTESTS_EXTRA_ARGS"] = "-j{} ".format(worker_count) + env.get(
+ "JSTESTS_EXTRA_ARGS", ""
+ )
+
+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:
+ 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], env=test_env)
+ if st < 0:
+ print("PROCESS-CRASH | jsapi-tests | application crashed")
+ print("Return code: {}".format(st))
+ 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)
+
+# Generate stacks from minidumps.
+if use_minidump:
+ venv_python = os.path.join(OBJDIR, "_virtualenvs", "init_py3", "bin", "python3")
+ run_command(
+ [
+ venv_python,
+ 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))
+
+sys.exit(max((st for _, st in results), key=abs))
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..fb9a806ddb
--- /dev/null
+++ b/js/src/devtools/automation/cgc-jittest-timeouts.txt
@@ -0,0 +1,55 @@
+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/offThreadCompileScript-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
+TypedObject/jit-read-u16-from-struct-array-in-struct.js
+TypedObject/jit-read-u32-from-struct-array-in-struct.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/macbuildenv.sh b/js/src/devtools/automation/macbuildenv.sh
new file mode 100644
index 0000000000..df0f57fdba
--- /dev/null
+++ b/js/src/devtools/automation/macbuildenv.sh
@@ -0,0 +1,14 @@
+# We will be sourcing mozconfig files, which end up calling mk_add_options and
+# ac_add_options with various settings. We only need the variable settings they
+# create along the way.
+mk_add_options() {
+ : do nothing
+}
+ac_add_options() {
+ : do nothing
+}
+
+topsrcdir="$SOURCE"
+
+# Setup CC and CXX variables
+. $topsrcdir/build/macosx/mozconfig.common
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..4ab6f566b4
--- /dev/null
+++ b/js/src/devtools/automation/smoosh-jstests-slow.txt
@@ -0,0 +1,6 @@
+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/tsan-slow.txt b/js/src/devtools/automation/tsan-slow.txt
new file mode 100644
index 0000000000..3c50495ca5
--- /dev/null
+++ b/js/src/devtools/automation/tsan-slow.txt
@@ -0,0 +1,22 @@
+# 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
+# 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..ac932277c8
--- /dev/null
+++ b/js/src/devtools/automation/variants/arm-sim
@@ -0,0 +1,7 @@
+{
+ "configure-args": "--enable-simulator=arm --target=i686-pc-linux --enable-rust-simd",
+ "optimize": true,
+ "debug": true,
+ "bits": 32,
+ "use_minidump": false
+}
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-cranelift-sim b/js/src/devtools/automation/variants/arm64-cranelift-sim
new file mode 100644
index 0000000000..9d81e7ad8e
--- /dev/null
+++ b/js/src/devtools/automation/variants/arm64-cranelift-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 --args=--wasm-compiler=cranelift"
+ },
+ "bits": 64
+}
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..f31c88b9a0
--- /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/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/fuzzing b/js/src/devtools/automation/variants/fuzzing
new file mode 100644
index 0000000000..7e4db4b5a2
--- /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/llvm-symbolizer",
+ "ASAN_SYMBOLIZER_PATH": "{MOZ_FETCHES_DIR}/llvm-symbolizer/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..260998d9f5
--- /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}/clang/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/nonunified b/js/src/devtools/automation/variants/nonunified
new file mode 100644
index 0000000000..c1be74aeb9
--- /dev/null
+++ b/js/src/devtools/automation/variants/nonunified
@@ -0,0 +1,13 @@
+{
+ "configure-args": "--enable-warnings-as-errors --enable-rust-simd",
+ "debug": true,
+ "env": {
+ "JS_SMOOSH_DISABLE_OPCODE_CHECK": "1"
+ },
+ "conditional-configure-args": {
+ "linux64": "--enable-smoosh"
+ },
+ "skip-tests": {
+ "all": ["jstests", "jittest", "checks"]
+ }
+}
diff --git a/js/src/devtools/automation/variants/plain b/js/src/devtools/automation/variants/plain
new file mode 100644
index 0000000000..0c97a5d535
--- /dev/null
+++ b/js/src/devtools/automation/variants/plain
@@ -0,0 +1,8 @@
+{
+ "configure-args": "--enable-rust-simd",
+ "optimize": true,
+ "compiler": "gcc",
+ "env": {
+ "JSTESTS_EXTRA_ARGS": "--jitflags=jstests"
+ }
+}
diff --git a/js/src/devtools/automation/variants/plaindebug b/js/src/devtools/automation/variants/plaindebug
new file mode 100644
index 0000000000..0b6271c8c2
--- /dev/null
+++ b/js/src/devtools/automation/variants/plaindebug
@@ -0,0 +1,10 @@
+{
+ "configure-args": "--enable-rust-simd",
+ "debug": true,
+ "env": {
+ "JSTESTS_EXTRA_ARGS": "--jitflags=debug"
+ },
+ "conditional-configure-args": {
+ "linux64": "--enable-clang-plugin"
+ }
+}
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/smoosh b/js/src/devtools/automation/variants/smoosh
new file mode 100644
index 0000000000..d4a0618d16
--- /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",
+ "JITTEST_EXTRA_ARGS": "--args='--smoosh'"
+ }
+}
diff --git a/js/src/devtools/automation/variants/smooshdebug b/js/src/devtools/automation/variants/smooshdebug
new file mode 100644
index 0000000000..388153f895
--- /dev/null
+++ b/js/src/devtools/automation/variants/smooshdebug
@@ -0,0 +1,12 @@
+{
+ "configure-args": "--enable-rust-simd --enable-smoosh",
+ "debug": true,
+ "compiler": "clang",
+ "env": {
+ "JSTESTS_EXTRA_ARGS": "--args='--smoosh' --jitflags=debug --exclude-file={DIR}/smoosh-jstests-slow.txt",
+ "JITTEST_EXTRA_ARGS": "--args='--smoosh'"
+ },
+ "conditional-configure-args": {
+ "linux64": "--enable-clang-plugin"
+ }
+}
diff --git a/js/src/devtools/automation/variants/tsan b/js/src/devtools/automation/variants/tsan
new file mode 100644
index 0000000000..d07b1d64aa
--- /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/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/winbuildenv.sh b/js/src/devtools/automation/winbuildenv.sh
new file mode 100644
index 0000000000..7fa10807df
--- /dev/null
+++ b/js/src/devtools/automation/winbuildenv.sh
@@ -0,0 +1,32 @@
+mk_export_correct_style() {
+ echo "export $1=$(cmd.exe //c echo %$1%)"
+}
+
+topsrcdir="$SOURCE"
+
+# Tooltool installs in parent of topsrcdir for spidermonkey builds.
+# Resolve that path since the mozconfigs assume tooltool installs in
+# topsrcdir.
+export VSPATH="$(cd ${topsrcdir}/.. && pwd)/vs2017_15.8.4"
+
+if [ -n "$USE_64BIT" ]; then
+ . $topsrcdir/build/win64/mozconfig.vs-latest
+else
+ . $topsrcdir/build/win32/mozconfig.vs-latest
+fi
+
+mk_export_correct_style CC
+mk_export_correct_style CXX
+mk_export_correct_style LINKER
+mk_export_correct_style WINDOWSSDKDIR
+mk_export_correct_style DIA_SDK_PATH
+mk_export_correct_style VC_PATH
+
+# PATH also needs to point to mozmake.exe, which can come from either
+# newer mozilla-build or tooltool.
+if ! which mozmake 2>/dev/null; then
+ export PATH="$PATH:$SOURCE/.."
+ if ! which mozmake 2>/dev/null; then
+ ( cd $SOURCE/..; $SOURCE/mach artifact toolchain -v --tooltool-manifest $SOURCE/browser/config/tooltool-manifests/${platform:-win32}/releng.manifest --retry 4${TOOLTOOL_CACHE:+ --cache-dir ${TOOLTOOL_CACHE}})
+ fi
+fi
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..f4a8357951
--- /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..77314e6698
--- /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 os
+import sys
+import math
+import json
+from subprocess import Popen, PIPE
+from operator import itemgetter
+
+
+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/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 100755
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..1c83628411
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/CFG.js
@@ -0,0 +1,179 @@
+/* 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";
+
+// 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, limits)
+{
+ 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], limits]);
+ if (edge.Kind == "Loop")
+ points.push(...findAllPoints(bodies, edge.BlockId, limits));
+ }
+
+ return points;
+}
+
+// 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 limits = isConstructor(typeInfo, edge.Type, variable.Name);
+ if (!limits)
+ continue;
+ if (!("PEdgeCallInstance" in edge))
+ continue;
+ if (edge.PEdgeCallInstance.Exp.Kind != "Var")
+ continue;
+
+ points.push(...pointsInRAIIScope(bodies, body, edge, limits));
+ }
+
+ 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, limits) {
+ 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, limits]);
+ 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, limits));
+ worklist.push(nedge.Index[1]);
+ }
+ }
+
+ return points;
+}
diff --git a/js/src/devtools/rootAnalysis/Makefile.in b/js/src/devtools/rootAnalysis/Makefile.in
new file mode 100644
index 0000000000..e8bc57471a
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/Makefile.in
@@ -0,0 +1,79 @@
+# -*- Mode: makefile -*-
+#
+# 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 Makefile is used to kick off a static rooting analysis. This Makefile is
+# NOT intended for use as part of the standard Mozilla build. Instead, this
+# Makefile will use $PATH to subvert compiler invocations to add in the sixgill
+# plugin, and then do a regular build of whatever portion of the tree you are
+# analyzing. The plugins will dump out several xdb database files. Various
+# analysis scripts, written in JS, will run over those database files to
+# produce the final analysis output.
+
+include $(topsrcdir)/config/config.mk
+
+# Tree to build and analyze, defaulting to the current tree
+TARGET_JSOBJDIR ?= $(TOPOBJDIR)
+
+# Path to a JS binary to use to run the analysis. You really want this to be an
+# optimized build.
+JS ?= $(DIST)/bin/js
+
+# Path to an xgill checkout containing the GCC plugin, xdb-processing binaries,
+# and compiler wrapper scripts used to automatically integrate into an existing
+# build system.
+SIXGILL ?= @SIXGILL_PATH@
+
+# Path to the JS scripts that will perform the analysis, defaulting to the same
+# place as this Makefile.in, which is probably what you want.
+ANALYSIS_SCRIPT_DIR ?= $(srcdir)
+
+# Number of simultanous analyzeRoots.js scripts to run.
+JOBS ?= 6
+
+all : rootingHazards.txt allFunctions.txt
+
+CALL_JS := time env PATH=$$PATH:$(SIXGILL)/bin XDB=$(SIXGILL)/bin/xdb.so $(JS)
+
+src_body.xdb src_comp.xdb: run_complete
+ @echo Started compilation at $$(date)
+ $(ANALYSIS_SCRIPT_DIR)/run_complete --foreground --build-root=$(TARGET_JSOBJDIR) --work-dir=work -b $(SIXGILL)/bin $(CURDIR)
+ @echo Finished compilation at $$(date)
+
+callgraph.txt: src_body.xdb src_comp.xdb computeCallgraph.js
+ @echo Started computation of $@ at $$(date)
+ $(CALL_JS) $(ANALYSIS_SCRIPT_DIR)/computeCallgraph.js > $@.tmp
+ mv $@.tmp $@
+ @echo Finished computation of $@ at $$(date)
+
+gcFunctions.txt: callgraph.txt computeGCFunctions.js annotations.js
+ @echo Started computation of $@ at $$(date)
+ $(CALL_JS) $(ANALYSIS_SCRIPT_DIR)/computeGCFunctions.js ./callgraph.txt > $@.tmp
+ mv $@.tmp $@
+ @echo Finished computation of $@ at $$(date)
+
+gcFunctions.lst: gcFunctions.txt
+ perl -lne 'print $$1 if /^GC Function: (.*)/' gcFunctions.txt > $@
+
+suppressedFunctions.lst: gcFunctions.txt
+ perl -lne 'print $$1 if /^Suppressed Function: (.*)/' gcFunctions.txt > $@
+
+gcTypes.txt: src_comp.xdb computeGCTypes.js annotations.js
+ @echo Started computation of $@ at $$(date)
+ $(CALL_JS) $(ANALYSIS_SCRIPT_DIR)/computeGCTypes.js > $@.tmp
+ mv $@.tmp $@
+ @echo Finished computation of $@ at $$(date)
+
+allFunctions.txt: src_body.xdb
+ @echo Started computation of $@ at $$(date)
+ time $(SIXGILL)/bin/xdbkeys $^ > $@.tmp
+ mv $@.tmp $@
+ @echo Finished computation of $@ at $$(date)
+
+rootingHazards.txt: gcFunctions.lst suppressedFunctions.lst gcTypes.txt analyzeRoots.js annotations.js gen-hazards.sh
+ @echo Started computation of $@ at $$(date)
+ time env JS=$(JS) ANALYZE='$(ANALYSIS_SCRIPT_DIR)/analyzeRoots.js' SIXGILL='$(SIXGILL)' '$(ANALYSIS_SCRIPT_DIR)/gen-hazards.sh' $(JOBS) > $@.tmp
+ mv $@.tmp $@
+ @echo Finished computation of $@ at $$(date)
diff --git a/js/src/devtools/rootAnalysis/README.md b/js/src/devtools/rootAnalysis/README.md
new file mode 100644
index 0000000000..682345a2c9
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/README.md
@@ -0,0 +1,109 @@
+# Spidermonkey JSAPI rooting analysis
+
+This directory contains scripts for running Brian Hackett's static GC rooting
+and thread heap write safety analyses on a JS source directory.
+
+To run the analysis on SpiderMonkey:
+
+1. Install prerequisites.
+
+ mach hazards bootstrap
+
+2. Build the shell to run the analysis.
+
+ mach hazards build-shell
+
+3. Compile all the code to gather info.
+
+ mach hazards gather --application=js
+
+4. Analyze the gathered info.
+
+ mach hazards analyze --application=js
+
+Output goes to `haz-js/hazards.txt`. This will run the analysis on the js/src
+tree only; if you wish to analyze the full browser, use
+
+ --application=browser
+
+(or leave it off; `--application=browser` is the default)
+
+After running the analysis once, you can reuse the `*.xdb` database files
+generated, using modified analysis scripts, by running either the `mach hazards
+analyze` command above, or with `haz-js/run-analysis.sh` (pass `--list` to see
+ways to select even more restrictive parts of the overall analysis; the default
+is `gcTypes` which will do everything but regenerate the xdb files).
+
+Also, you can pass `-v` to get exact command lines to cut & paste for running
+the various stages, which is helpful for running under a debugger.
+
+## Overview of what is going on here
+
+So what does this actually do?
+
+1. It downloads a GCC compiler and plugin ("sixgill") from Mozilla servers.
+
+2. It runs `run_complete`, a script that builds the target codebase with the
+ downloaded GCC, generating a few database files containing control flow
+ graphs of the full compile, along with type information etc.
+
+3. Then it runs `analyze.py`, a Python script, which runs all the scripts
+ which actually perform the analysis -- the tricky parts.
+ (Those scripts are written in JS.)
+
+The easiest way to get this running is to not try to do the instrumented
+compilation locally. Instead, grab the relevant files from a try server push
+and analyze them locally.
+
+## Local Analysis of Downloaded Intermediate Files
+
+Another useful path is to let the continuous integration system do the hard
+work of generating the intermediate files and analyze them locally. This is
+particularly useful if you are working on the analysis itself.
+
+1. Do a try push with "--upload-xdbs" appended to the try: ..." line.
+
+ mach try fuzzy -q "'haz" --upload-xdbs
+
+2. Create an empty directory to run the analysis.
+
+3. When the try job is complete, download the resulting src_body.xdb.bz2, src_comp.xdb.bz2,
+and file_source.xdb.bz2 files into your directory.
+
+4. Fetch a compiler and sixgill plugin to use:
+
+ mach hazards bootstrap
+
+If you are on osx, these will not be available. Instead, build sixgill manually
+(these directions are a little stale):
+
+ hg clone https://hg.mozilla.org/users/sfink_mozilla.com/sixgill
+ cd sixgill
+ CC=$HOME/.mozbuild/hazard-tools/gcc/bin/gcc ./release.sh --build # This will fail horribly.
+ make bin/xdb.so CXX=clang++
+
+5. Build an optimized JS shell with ctypes. Note that this does not need to
+match the source you are analyzing in any way; in fact, you pretty much never
+need to update this once you've built it. (Though I reserve the right to use
+any new JS features implemented in Spidermonkey in the future...)
+
+ mach hazards build-shell
+
+The shell will be placed by default in $topsrcdir/obj-haz-shell.
+
+6. Make a defaults.py file containing the following, with your own paths filled in:
+
+ js = "<objdir>/dist/bin/js"
+ sixgill_bin = "<sixgill-dir>/bin"
+
+7a. For the rooting analysis, run
+
+ python <srcdir>/js/src/devtools/rootAnalysis/analyze.py gcTypes
+
+7b. For the heap write analysis, run
+
+ python <srcdir>/js/src/devtools/rootAnalysis/analyze.py heapwrites
+
+Also, you may wish to run with -v (aka --verbose) to see the exact commands
+executed that you can cut & paste if needed. (I use them to run under the JS
+debugger when I'm working on the analysis.)
diff --git a/js/src/devtools/rootAnalysis/analyze.py b/js/src/devtools/rootAnalysis/analyze.py
new file mode 100755
index 0000000000..594e65a1c5
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/analyze.py
@@ -0,0 +1,414 @@
+#!/usr/bin/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/.
+
+"""
+Runs the static rooting analysis
+"""
+
+from subprocess import Popen
+import argparse
+import os
+import subprocess
+import sys
+import re
+
+try:
+ from shlex import quote
+except ImportError:
+ from pipes import quote
+
+# Python 2/3 version independence polyfills
+
+anystring_t = str if sys.version_info[0] > 2 else basestring
+
+try:
+ execfile
+except Exception:
+
+ def execfile(thefile, globals):
+ exec(compile(open(thefile).read(), filename=thefile, mode="exec"), globals)
+
+
+def env(config):
+ e = dict(os.environ)
+ e["PATH"] = ":".join(
+ p for p in (config.get("gcc_bin"), config.get("sixgill_bin"), e["PATH"]) if p
+ )
+ e["XDB"] = "%(sixgill_bin)s/xdb.so" % config
+ e["SOURCE"] = config["source"]
+ e["ANALYZED_OBJDIR"] = config["objdir"]
+ return e
+
+
+def fill(command, config):
+ try:
+ return tuple(s % config for s in command)
+ except Exception:
+ print("Substitution failed:")
+ problems = []
+ for fragment in command:
+ try:
+ fragment % config
+ except Exception:
+ problems.append(fragment)
+ raise Exception(
+ "\n".join(["Substitution failed:"] + [" %s" % s for s in problems])
+ )
+
+
+def print_command(command, outfile=None, env=None):
+ output = " ".join(quote(s) for s in command)
+ if outfile:
+ output += " > " + outfile
+ if env:
+ changed = {}
+ e = os.environ
+ for key, value in env.items():
+ if (key not in e) or (e[key] != value):
+ changed[key] = value
+ if changed:
+ outputs = []
+ for key, value in changed.items():
+ if key in e and e[key] in value:
+ start = value.index(e[key])
+ end = start + len(e[key])
+ outputs.append(
+ '%s="%s${%s}%s"' % (key, value[:start], key, value[end:])
+ )
+ else:
+ outputs.append("%s='%s'" % (key, value))
+ output = " ".join(outputs) + " " + output
+
+ print(output)
+
+
+def generate_hazards(config, outfilename):
+ jobs = []
+ for i in range(int(config["jobs"])):
+ command = fill(
+ (
+ "%(js)s",
+ "%(analysis_scriptdir)s/analyzeRoots.js",
+ "%(gcFunctions_list)s",
+ "%(gcEdges)s",
+ "%(limitedFunctions_list)s",
+ "%(gcTypes)s",
+ "%(typeInfo)s",
+ str(i + 1),
+ "%(jobs)s",
+ "tmp.%s" % (i + 1,),
+ ),
+ config,
+ )
+ outfile = "rootingHazards.%s" % (i + 1,)
+ output = open(outfile, "w")
+ if config["verbose"]:
+ print_command(command, outfile=outfile, env=env(config))
+ jobs.append((command, Popen(command, stdout=output, env=env(config))))
+
+ final_status = 0
+ while jobs:
+ pid, status = os.wait()
+ jobs = [job for job in jobs if job[1].pid != pid]
+ final_status = final_status or status
+
+ if final_status:
+ raise subprocess.CalledProcessError(final_status, "analyzeRoots.js")
+
+ with open(outfilename, "w") as output:
+ command = ["cat"] + [
+ "rootingHazards.%s" % (i + 1,) for i in range(int(config["jobs"]))
+ ]
+ if config["verbose"]:
+ print_command(command, outfile=outfilename)
+ subprocess.call(command, stdout=output)
+
+
+JOBS = {
+ "dbs": (
+ (
+ "%(analysis_scriptdir)s/run_complete",
+ "--foreground",
+ "--no-logs",
+ "--build-root=%(objdir)s",
+ "--wrap-dir=%(sixgill)s/scripts/wrap_gcc",
+ "--work-dir=work",
+ "-b",
+ "%(sixgill_bin)s",
+ "--buildcommand=%(buildcommand)s",
+ ".",
+ ),
+ (),
+ ),
+ "list-dbs": (("ls", "-l"), ()),
+ "callgraph": (
+ (
+ "%(js)s",
+ "%(analysis_scriptdir)s/computeCallgraph.js",
+ "%(typeInfo)s",
+ "[callgraph]",
+ ),
+ ("callgraph.txt",),
+ ),
+ "gcFunctions": (
+ (
+ "%(js)s",
+ "%(analysis_scriptdir)s/computeGCFunctions.js",
+ "%(callgraph)s",
+ "[gcFunctions]",
+ "[gcFunctions_list]",
+ "[gcEdges]",
+ "[limitedFunctions_list]",
+ ),
+ ("gcFunctions.txt", "gcFunctions.lst", "gcEdges.txt", "limitedFunctions.lst"),
+ ),
+ "gcTypes": (
+ (
+ "%(js)s",
+ "%(analysis_scriptdir)s/computeGCTypes.js",
+ "[gcTypes]",
+ "[typeInfo]",
+ ),
+ ("gcTypes.txt", "typeInfo.txt"),
+ ),
+ "allFunctions": (
+ (
+ "%(sixgill_bin)s/xdbkeys",
+ "src_body.xdb",
+ ),
+ "allFunctions.txt",
+ ),
+ "hazards": (generate_hazards, "rootingHazards.txt"),
+ "explain": (
+ (
+ os.environ.get("PYTHON", "python2.7"),
+ "%(analysis_scriptdir)s/explain.py",
+ "%(hazards)s",
+ "%(gcFunctions)s",
+ "[explained_hazards]",
+ "[unnecessary]",
+ "[refs]",
+ ),
+ ("hazards.txt", "unnecessary.txt", "refs.txt"),
+ ),
+ "heapwrites": (
+ ("%(js)s", "%(analysis_scriptdir)s/analyzeHeapWrites.js"),
+ "heapWriteHazards.txt",
+ ),
+}
+
+
+def out_indexes(command):
+ for i in range(len(command)):
+ m = re.match(r"^\[(.*)\]$", command[i])
+ if m:
+ yield (i, m.group(1))
+
+
+def run_job(name, config):
+ cmdspec, outfiles = JOBS[name]
+ print("Running " + name + " to generate " + str(outfiles))
+ if hasattr(cmdspec, "__call__"):
+ cmdspec(config, outfiles)
+ else:
+ temp_map = {}
+ cmdspec = fill(cmdspec, config)
+ if isinstance(outfiles, anystring_t):
+ stdout_filename = "%s.tmp" % name
+ temp_map[stdout_filename] = outfiles
+ if config["verbose"]:
+ print_command(cmdspec, outfile=outfiles, env=env(config))
+ else:
+ stdout_filename = None
+ pc = list(cmdspec)
+ outfile = 0
+ for (i, name) in out_indexes(cmdspec):
+ pc[i] = outfiles[outfile]
+ outfile += 1
+ if config["verbose"]:
+ print_command(pc, env=env(config))
+
+ command = list(cmdspec)
+ outfile = 0
+ for (i, name) in out_indexes(cmdspec):
+ command[i] = "%s.tmp" % name
+ temp_map[command[i]] = outfiles[outfile]
+ outfile += 1
+
+ sys.stdout.flush()
+ if stdout_filename is None:
+ subprocess.check_call(command, env=env(config))
+ else:
+ with open(stdout_filename, "w") as output:
+ subprocess.check_call(command, stdout=output, env=env(config))
+ for (temp, final) in temp_map.items():
+ try:
+ os.rename(temp, final)
+ except OSError:
+ print("Error renaming %s -> %s" % (temp, final))
+ raise
+
+
+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 starting from this step"
+)
+parser.add_argument(
+ "--source", metavar="SOURCE", type=str, nargs="?", help="source code to analyze"
+)
+parser.add_argument(
+ "--objdir",
+ metavar="DIR",
+ type=str,
+ nargs="?",
+ help="object directory of compiled files",
+)
+parser.add_argument(
+ "--js",
+ metavar="JSSHELL",
+ type=str,
+ nargs="?",
+ help="full path to ctypes-capable JS shell",
+)
+parser.add_argument(
+ "--upto", metavar="UPTO", type=str, nargs="?", help="last step to execute"
+)
+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(
+ "--buildcommand",
+ "--build",
+ "-b",
+ type=str,
+ nargs="?",
+ help="command to build the tree being analyzed",
+)
+parser.add_argument(
+ "--tag",
+ "-t",
+ type=str,
+ nargs="?",
+ help='name of job, also sets build command to "build.<tag>"',
+)
+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",
+)
+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:
+ print("Loaded %s" % default)
+ except Exception:
+ pass
+
+data = config.copy()
+
+for k, v in vars(args).items():
+ if v is not None:
+ data[k] = v
+
+if args.tag and not args.buildcommand:
+ args.buildcommand = "build.%s" % args.tag
+
+if args.jobs is not None:
+ data["jobs"] = args.jobs
+if not data.get("jobs"):
+ data["jobs"] = int(subprocess.check_output(["nproc", "--ignore=1"]).strip())
+
+if args.buildcommand:
+ data["buildcommand"] = args.buildcommand
+elif "BUILD" in os.environ:
+ data["buildcommand"] = os.environ["BUILD"]
+else:
+ data["buildcommand"] = "make -j4 -s"
+
+if "ANALYZED_OBJDIR" in os.environ:
+ data["objdir"] = os.environ["ANALYZED_OBJDIR"]
+
+if "GECKO_PATH" in os.environ:
+ data["source"] = os.environ["GECKO_PATH"]
+if "SOURCE" in os.environ:
+ data["source"] = os.environ["SOURCE"]
+
+steps = [
+ "dbs",
+ "gcTypes",
+ "callgraph",
+ "gcFunctions",
+ "allFunctions",
+ "hazards",
+ "explain",
+ "heapwrites",
+]
+
+if args.list:
+ for step in steps:
+ command, outfilename = JOBS[step]
+ if outfilename:
+ print("%s -> %s" % (step, outfilename))
+ else:
+ print(step)
+ sys.exit(0)
+
+for step in steps:
+ command, outfiles = JOBS[step]
+ if isinstance(outfiles, anystring_t):
+ data[step] = outfiles
+ else:
+ outfile = 0
+ for (i, name) in out_indexes(command):
+ data[name] = outfiles[outfile]
+ outfile += 1
+ assert (
+ len(outfiles) == outfile
+ ), "step '%s': mismatched number of output files (%d) and params (%d)" % (
+ step,
+ outfile,
+ len(outfiles),
+ ) # NOQA: E501
+
+if args.step:
+ steps = steps[steps.index(args.step) :]
+
+if args.upto:
+ steps = steps[: steps.index(args.upto) + 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..cb757f9882
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/analyzeHeapWrites.js
@@ -0,0 +1,1404 @@
+/* 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/],
+
+ // RawGeckoBorrowedNode thread-mutable parameters.
+ ["Gecko_SetNodeFlags", "aNode", null],
+ ["Gecko_UnsetNodeFlags", "aNode", null],
+
+ // 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_CopyAnimationNames", "aDest", null],
+ ["Gecko_CopyFontFamilyFrom", "dst", null],
+ ["Gecko_SetAnimationName", "aStyleAnimation", null],
+ ["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_CopyFiltersFrom", "aDest", 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..6e16d0cf50
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/analyzeRoots.js
@@ -0,0 +1,1166 @@
+/* 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('CFG.js');
+loadRelativeToScript('dumpCFG.js');
+
+var sourceRoot = (os.getenv('SOURCE') || '') + '/'
+
+var functionName;
+var functionBodies;
+
+if (typeof scriptArgs[0] != 'string' || typeof scriptArgs[1] != 'string')
+ throw "Usage: analyzeRoots.js [-f function_name] <gcFunctions.lst> <gcEdges.txt> <limitedFunctions.lst> <gcTypes.txt> <typeInfo.txt> [start end [tmpfile]]";
+
+var theFunctionNameToFind;
+if (scriptArgs[0] == '--function' || scriptArgs[0] == '-f') {
+ theFunctionNameToFind = scriptArgs[1];
+ scriptArgs = scriptArgs.slice(2);
+}
+
+var gcFunctionsFile = scriptArgs[0] || "gcFunctions.lst";
+var gcEdgesFile = scriptArgs[1] || "gcEdges.txt";
+var limitedFunctionsFile = scriptArgs[2] || "limitedFunctions.lst";
+var gcTypesFile = scriptArgs[3] || "gcTypes.txt";
+var typeInfoFile = scriptArgs[4] || "typeInfo.txt";
+var batch = (scriptArgs[5]|0) || 1;
+var numBatches = (scriptArgs[6]|0) || 1;
+var tmpfile = scriptArgs[7] || "tmp.txt";
+
+var gcFunctions = {};
+var text = snarf("gcFunctions.lst").split("\n");
+assert(text.pop().length == 0);
+for (var line of text)
+ gcFunctions[mangled(line)] = true;
+
+var limitedFunctions = {};
+var text = snarf(limitedFunctionsFile).split("\n");
+assert(text.pop().length == 0);
+for (var line of text) {
+ const [_, limits, func] = line.match(/(.*?) (.*)/);
+ assert(limits !== undefined);
+ limitedFunctions[func] = limits | 0;
+}
+text = null;
+
+var typeInfo = loadTypeInfo(typeInfoFile);
+
+var gcEdges = {};
+text = snarf(gcEdgesFile).split('\n');
+assert(text.pop().length == 0);
+for (var line of text) {
+ var [ block, edge, func ] = line.split(" || ");
+ if (!(block in gcEdges))
+ gcEdges[block] = {}
+ gcEdges[block][edge] = func;
+}
+text = null;
+
+var match;
+var gcThings = {};
+var gcPointers = {};
+
+text = snarf(gcTypesFile).split("\n");
+for (var line of text) {
+ if (match = /^GCThing: (.*)/.exec(line))
+ gcThings[match[1]] = true;
+ if (match = /^GCPointer: (.*)/.exec(line))
+ gcPointers[match[1]] = true;
+}
+text = null;
+
+function isGCType(type)
+{
+ if (type.Kind == "CSU")
+ return type.Name in gcThings;
+ else if (type.Kind == "Array")
+ return isGCType(type.Type);
+ return false;
+}
+
+function isUnrootedType(type)
+{
+ if (type.Kind == "Pointer")
+ return isGCType(type.Type);
+ else if (type.Kind == "Array") {
+ if (!type.Type) {
+ printErr("Received Array Kind with no Type");
+ printErr(JSON.stringify(type));
+ printErr(getBacktrace({args: true, locals: true}));
+ }
+ return isUnrootedType(type.Type);
+ } else if (type.Kind == "CSU")
+ return type.Name in gcPointers;
+ else
+ return false;
+}
+
+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;
+}
+
+function isImmobileValue(exp) {
+ if (exp.Kind == "Int" && exp.String == "0") {
+ 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.
+//
+// Note that this returns true only if the variable's incoming value is used.
+// So this would return false for 'obj':
+//
+// obj = someFunction();
+//
+// but these would return true:
+//
+// obj = someFunction(obj);
+// obj->foo = someFunction();
+//
+function edgeUsesVariable(edge, variable, body)
+{
+ if (ignoreEdgeUse(edge, variable, body))
+ return 0;
+
+ if (variable.Kind == "Return" && body.Index[1] == edge.Index[1] && body.BlockId.Kind == "Function")
+ return edge.Index[1]; // Last point in function body uses the return value.
+
+ 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 (edgeKillsVariable(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);
+ }
+}
+
+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;
+ }
+}
+
+function expressionIsVariable(exp, variable)
+{
+ return exp.Kind == "Var" && sameVariable(exp.Variable, variable);
+}
+
+function expressionIsMethodOnVariable(exp, variable)
+{
+ // 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 exp.Kind == "Var" && sameVariable(exp.Variable, variable);
+}
+
+// Return whether the edge terminates the live range of a variable's value when
+// searching in reverse through the CFG, by setting it to some new value.
+// Examples of killing '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 edgeKillsVariable(edge, variable)
+{
+ // Direct assignments kill their 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 kill their lhs.
+ if (1 in edge.Exp) {
+ var lhs = edge.Exp[1];
+ if (expressionIsVariable(lhs, variable))
+ return true;
+ }
+
+ // Constructor calls kill their 'this' value.
+ 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;
+}
+
+function edgeMovesVariable(edge, variable)
+{
+ 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;
+ const [ mangled, unmangled ] = splitFunction(fullname);
+ // Match a UniquePtr move constructor.
+ if (unmangled.match(/::UniquePtr<[^>]*>::UniquePtr\((\w+::)*UniquePtr<[^>]*>&&/))
+ return true;
+ }
+
+ return false;
+}
+
+// Scan forward through the given 'body', starting at 'startpoint', looking for
+// a call that passes 'variable' to a move constructor that "consumes" it (eg
+// UniquePtr::UniquePtr(UniquePtr&&)).
+function bodyEatsVariable(variable, body, startpoint)
+{
+ const successors = getSuccessors(body);
+ const work = [startpoint];
+ while (work.length > 0) {
+ const point = work.shift();
+ if (!(point in successors))
+ continue;
+ for (const edge of successors[point]) {
+ if (edgeMovesVariable(edge, variable))
+ return true;
+ // edgeKillsVariable 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 (!edgeKillsVariable(edge, variable))
+ work.push(edge.Index[1]);
+ }
+ }
+ return false;
+}
+
+// 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();
+//
+// Compare to edgeKillsVariable: killing (in backwards direction) means the
+// variable's value was live and is no longer. Invalidating means it wasn't
+// actually live after all.
+//
+function edgeInvalidatesVariable(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;
+
+ var callee = edge.Exp[0];
+
+ if (edge.Type.Kind == 'Function' &&
+ edge.Exp[0].Kind == 'Var' &&
+ edge.Exp[0].Variable.Kind == 'Func' &&
+ edge.Exp[0].Variable.Name[1] == 'move' &&
+ edge.Exp[0].Variable.Name[0].includes('std::move(') &&
+ expressionIsVariable(edge.PEdgeCallArguments.Exp[0], variable) &&
+ edge.Exp[1].Kind == 'Var' &&
+ edge.Exp[1].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. It really
+ // ought to be invalidated at the point of the function call that calls
+ // the move constructor, but given that we're creating a temporary here
+ // just for the purpose of passing it in, this edge is good enough.
+ const lhs = edge.Exp[1].Variable;
+ if (bodyEatsVariable(lhs, body, edge.Index[1]))
+ return true;
+ }
+
+ if (edge.Type.Kind == 'Function' &&
+ edge.Type.TypeFunctionCSU &&
+ edge.PEdgeCallInstance &&
+ expressionIsMethodOnVariable(edge.PEdgeCallInstance.Exp, variable))
+ {
+ const typeName = edge.Type.TypeFunctionCSU.Type.Name;
+ const m = typeName.match(/^(((\w|::)+?)(\w+))</);
+ if (m) {
+ const [, type, namespace,, classname] = m;
+
+ // special-case: the initial constructor that doesn't provide a value.
+ // Useful for things like Maybe<T>.
+ const ctorName = `${namespace}${classname}<T>::${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;
+}
+
+function edgeCanGC(edge)
+{
+ if (edge.Kind != "Call")
+ return false;
+
+ var callee = edge.Exp[0];
+
+ while (callee.Kind == "Drf")
+ callee = callee.Exp[0];
+
+ if (callee.Kind == "Var") {
+ var variable = callee.Variable;
+
+ if (variable.Kind == "Func") {
+ var func = mangled(variable.Name[0]);
+ if ((func in gcFunctions) || ((func + internalMarker) in gcFunctions))
+ return "'" + variable.Name[0] + "'";
+ return null;
+ }
+
+ var varName = variable.Name[0];
+ return indirectCallCannotGC(functionName, varName) ? null : "'*" + varName + "'";
+ }
+
+ if (callee.Kind == "Fld") {
+ var field = callee.Field;
+ var csuName = field.FieldCSU.Type.Name;
+ var fullFieldName = csuName + "." + field.Name[0];
+ if (fieldCallCannotGC(csuName, fullFieldName))
+ return null;
+
+ if (fullFieldName in gcFunctions)
+ return "'" + fullFieldName + "'";
+
+ return null;
+ }
+}
+
+// Search recursively through predecessors from the use of a variable's value,
+// returning whether a GC call is reachable (in the reverse direction; this
+// means that the variable use is reachable from the GC call, and therefore the
+// variable is live after the GC call), along with some additional information.
+// 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:
+//
+// - 'why': a path from the GC call to a use of the variable after the GC
+// call, chained through a 'why' field in the returned edge descriptor
+//
+// - 'gcInfo': a direct pointer to the GC call edge
+//
+function findGCBeforeValueUse(start_body, start_point, suppressed, variable)
+{
+ // Scan through all edges preceding an unrooted variable use, using an
+ // explicit worklist, looking for a GC call. A worklist contains an
+ // incoming edge together with a description of where it or one of its
+ // successors GC'd (if any).
+
+ var bodies_visited = new Map();
+
+ let worklist = [{body: start_body, ppoint: start_point, preGCLive: false, gcInfo: null, why: null}];
+ while (worklist.length) {
+ // Grab an entry off of the worklist, representing a point within the
+ // CFG identified by <body,ppoint>. If this point has a descendant
+ // later in the CFG that can GC, gcInfo will be set to the information
+ // about that GC call.
+
+ var entry = worklist.pop();
+ var { body, ppoint, gcInfo, preGCLive } = entry;
+
+ // Handle the case where there are multiple ways to reach this point
+ // (traversing backwards).
+ var visited = bodies_visited.get(body);
+ if (!visited)
+ bodies_visited.set(body, visited = new Map());
+ if (visited.has(ppoint)) {
+ var seenEntry = visited.get(ppoint);
+
+ // This point already knows how to GC through some other path, so
+ // we have nothing new to learn. (The other path will consider the
+ // predecessors.)
+ if (seenEntry.gcInfo)
+ continue;
+
+ // If this worklist's entry doesn't know of any way to GC, then
+ // there's no point in continuing the traversal through it. Perhaps
+ // another edge will be found that *can* GC; otherwise, the first
+ // route to the point will traverse through predecessors.
+ //
+ // Note that this means we may visit a point more than once, if the
+ // first time we visit we don't have a known reachable GC call and
+ // the second time we do.
+ if (!gcInfo)
+ continue;
+ }
+ visited.set(ppoint, {body: body, gcInfo: gcInfo});
+
+ // Check for hitting the entry point of the current body (which may be
+ // the outer function or a loop within it.)
+ if (ppoint == body.Index[0]) {
+ if (body.BlockId.Kind == "Loop") {
+ // Propagate to outer body parents that enter the loop body.
+ if ("BlockPPoint" in body) {
+ for (var parent of body.BlockPPoint) {
+ var found = false;
+ for (var xbody of functionBodies) {
+ if (sameBlockId(xbody.BlockId, parent.BlockId)) {
+ assert(!found);
+ found = true;
+ worklist.push({body: xbody, ppoint: parent.Index,
+ gcInfo: gcInfo, why: entry});
+ }
+ }
+ assert(found);
+ }
+ }
+
+ // Also propagate to the *end* of this loop, for the previous
+ // iteration.
+ worklist.push({body: body, ppoint: body.Index[1],
+ gcInfo: gcInfo, why: entry});
+ } else if ((variable.Kind == "Arg" || variable.Kind == "This") && gcInfo) {
+ // The scope of arguments starts at the beginning of the
+ // function
+ return entry;
+ } else if (entry.preGCLive) {
+ // We didn't find a "good" explanation beginning of the live
+ // range, but we do know the variable was live across the GC.
+ // This can happen if the live range started when a variable is
+ // used as a retparam.
+ return entry;
+ }
+ }
+
+ var predecessors = getPredecessors(body);
+ if (!(ppoint in predecessors))
+ continue;
+
+ for (var edge of predecessors[ppoint]) {
+ var source = edge.Index[0];
+
+ if (edgeInvalidatesVariable(edge, variable, body)) {
+ // Terminate the search through this point; we thought we were
+ // within the live range, but it turns out that the variable
+ // was set to a value that we don't care about.
+ continue;
+ }
+
+ var edge_kills = edgeKillsVariable(edge, variable);
+ var edge_uses = edgeUsesVariable(edge, variable, body);
+
+ if (edge_kills || edge_uses) {
+ if (!body.minimumUse || source < body.minimumUse)
+ body.minimumUse = source;
+ }
+
+ if (edge_kills) {
+ // 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 (gcInfo)
+ return {body: body, ppoint: source, gcInfo: gcInfo, why: entry };
+
+ // Otherwise, keep searching through the graph, but truncate
+ // this particular branch of the search at this edge.
+ continue;
+ }
+
+ var src_gcInfo = gcInfo;
+ var src_preGCLive = preGCLive;
+ if (!gcInfo && !(body.limits[source] & LIMIT_CANNOT_GC) && !suppressed) {
+ var gcName = edgeCanGC(edge, body);
+ if (gcName)
+ src_gcInfo = {name:gcName, body:body, ppoint:source};
+ }
+
+ if (edge_uses) {
+ // The live range starts at least this far back, so we're done
+ // for the same reason as with edge_kills. 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 'why' path to
+ // include the ancestor where the value was generated. So we
+ // will only return 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.
+
+ if (src_gcInfo) {
+ src_preGCLive = true;
+ if (edge.Kind == 'Assign')
+ return {body:body, ppoint:source, gcInfo:src_gcInfo, why:entry};
+ }
+ }
+
+ if (edge.Kind == "Loop") {
+ // Additionally propagate the search into a loop body, starting
+ // with the exit point.
+ var found = false;
+ for (var xbody of functionBodies) {
+ if (sameBlockId(xbody.BlockId, edge.BlockId)) {
+ assert(!found);
+ found = true;
+ worklist.push({body:xbody, ppoint:xbody.Index[1],
+ preGCLive: src_preGCLive, gcInfo:src_gcInfo,
+ why:entry});
+ }
+ }
+ assert(found);
+ // 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.)
+ break;
+ }
+
+ // Propagate the search to the predecessors of this edge.
+ worklist.push({body:body, ppoint:source,
+ preGCLive: src_preGCLive, gcInfo:src_gcInfo,
+ why:entry});
+ }
+ }
+
+ return null;
+}
+
+function variableLiveAcrossGC(suppressed, variable)
+{
+ // 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 (edgeInvalidatesVariable(edge, variable, body))
+ continue;
+
+ var usePoint = edgeUsesVariable(edge, variable, body);
+ if (usePoint) {
+ var call = findGCBeforeValueUse(body, usePoint, suppressed, 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(suppressed, variable)
+{
+ for (var body of functionBodies) {
+ if (!("PEdge" in body))
+ continue;
+ for (var edge of body.PEdge) {
+ if (edgeTakesVariableAddress(edge, variable, body)) {
+ if (edge.Kind == "Assign" || (!suppressed && edgeCanGC(edge)))
+ 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 + "' > " + tmpfile));
+ var lines = snarf(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 - 1].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 printEntryTrace(functionName, entry)
+{
+ var gcPoint = entry.gcInfo ? entry.gcInfo.ppoint : 0;
+
+ if (!functionBodies[0].lines)
+ loadPrintedLines(functionName);
+
+ while (entry) {
+ var ppoint = entry.ppoint;
+ var lineText = findLocation(entry.body, ppoint, {"brief": true});
+
+ var edgeText = "";
+ if (entry.why && entry.why.body == entry.body) {
+ // If the next point in the trace is in the same block, look for an edge between them.
+ var next = entry.why.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]]";
+ }
+
+ print(" " + lineText + (edgeText.length ? ": " + edgeText : ""));
+ entry = entry.why;
+ }
+}
+
+function isRootedType(type)
+{
+ return type.Kind == "CSU" && ((type.Name in typeInfo.RootedPointers) ||
+ (type.Name in typeInfo.RootedGCThings));
+}
+
+function typeDesc(type)
+{
+ if (type.Kind == "CSU") {
+ return type.Name;
+ } else if ('Type' in type) {
+ var inner = typeDesc(type.Type);
+ if (type.Kind == 'Pointer')
+ return inner + '*';
+ else if (type.Kind == 'Array')
+ return inner + '[]';
+ else
+ return inner + '?';
+ } else {
+ return '???';
+ }
+}
+
+function processBodies(functionName)
+{
+ if (!("DefineVariable" in functionBodies[0]))
+ return;
+ var suppressed = Boolean(limitedFunctions[mangled(functionName)] & LIMIT_CANNOT_GC);
+
+ // Look for the JS_EXPECT_HAZARDS annotation, and 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);
+ }
+ }
+ }
+
+ var 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>. 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.match(/Argument\b/));
+ for (const [holder,] of holders) {
+ ignoreVars.add(holder); // Ignore the older.
+ ignoreVars.add(holder.replace("_holder", "")); // Ignore the "managed" arg.
+ }
+ }
+
+ for (const variable of functionBodies[0].DefineVariable) {
+ var name;
+ if (variable.Variable.Kind == "This")
+ name = "this";
+ else if (variable.Variable.Kind == "Return")
+ name = "<returnvalue>";
+ else
+ name = variable.Variable.Name[0];
+
+ if (ignoreVars.has(name))
+ continue;
+
+ if (isRootedType(variable.Type)) {
+ if (!variableLiveAcrossGC(suppressed, variable.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;
+ }
+ }
+ print("\nFunction '" + functionName + "'" +
+ " has unnecessary root '" + name + "' at " + lineText);
+ }
+ } else if (isUnrootedType(variable.Type)) {
+ var result = variableLiveAcrossGC(suppressed, variable.Variable);
+ if (result) {
+ var lineText = findLocation(result.gcInfo.body, result.gcInfo.ppoint);
+ if (annotations.has('Expect Hazards')) {
+ print("\nThis is expected, but '" + functionName + "'" +
+ " has unrooted '" + name + "'" +
+ " of type '" + typeDesc(variable.Type) + "'" +
+ " live across GC call " + result.gcInfo.name +
+ " at " + lineText);
+ missingExpectedHazard = false;
+ } else {
+ print("\nFunction '" + functionName + "'" +
+ " has unrooted '" + name + "'" +
+ " of type '" + typeDesc(variable.Type) + "'" +
+ " live across GC call " + result.gcInfo.name +
+ " at " + lineText);
+ }
+ printEntryTrace(functionName, result);
+ }
+ result = unsafeVariableAddressTaken(suppressed, variable.Variable);
+ if (result) {
+ var lineText = findLocation(result.body, result.ppoint);
+ print("\nFunction '" + functionName + "'" +
+ " takes unsafe address of unrooted '" + name + "'" +
+ " at " + lineText);
+ printEntryTrace(functionName, {body:result.body, ppoint:result.ppoint});
+ }
+ }
+ }
+
+ if (missingExpectedHazard) {
+ const {
+ Location: [
+ { CacheString: startfile, Line: startline },
+ { CacheString: endfile, Line: endline }
+ ]
+ } = functionBodies[0];
+
+ const loc = (startfile == endfile) ? `${startfile}:${startline}-${endline}`
+ : `${startfile}:${startline}`;
+
+ print("\nFunction '" + functionName + "' expected hazard(s) but none were found at " + loc);
+ }
+}
+
+if (batch == 1)
+ print("Time: " + new Date);
+
+var xdb = xdbLibrary();
+xdb.open("src_body.xdb");
+
+var minStream = xdb.min_data_stream()|0;
+var maxStream = xdb.max_data_stream()|0;
+
+var N = (maxStream - minStream) + 1;
+var start = Math.floor((batch - 1) / numBatches * N) + minStream;
+var start_next = Math.floor(batch / numBatches * N) + minStream;
+var end = start_next - 1;
+
+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.limits is a plain object indexed by point, with the value being a
+ // bit set stored in an integer of the limit bits.
+ for (var body of functionBodies)
+ body.limits = [];
+
+ for (var body of functionBodies) {
+ for (var [pbody, id, limits] of allRAIIGuardedCallPoints(typeInfo, functionBodies, body, isLimitConstructor))
+ {
+ if (limits)
+ pbody.limits[id] = limits;
+ }
+ }
+ processBodies(functionName);
+}
+
+if (theFunctionNameToFind) {
+ var data = xdb.read_entry(theFunctionNameToFind);
+ var json = data.readString();
+ process(theFunctionNameToFind, json);
+ xdb.free_string(data);
+ 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);
+}
diff --git a/js/src/devtools/rootAnalysis/annotations.js b/js/src/devtools/rootAnalysis/annotations.js
new file mode 100644
index 0000000000..93d022dd83
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/annotations.js
@@ -0,0 +1,529 @@
+/* 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,
+ "prerrortable.c:callback_newtable" : true,
+ "mozalloc_oom.cpp:void (* gAbortHandler)(size_t)" : true,
+};
+
+// Types that when constructed with no arguments, are "safe" values (they do
+// not contain GC pointers).
+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"]),
+};
+
+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,
+};
+
+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;
+}
+
+// Return whether csu.method is one that we claim can never GC.
+function isSuppressedVirtualMethod(csu, method)
+{
+ return csu == "nsISupports" && (method == "AddRef" || method == "Release");
+}
+
+// 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 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(mozilla::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,
+};
+
+function extraGCFunctions() {
+ 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)
+{
+ // 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 limits 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 limit = 0;
+ if (type in typeInfo.GCSuppressors)
+ limit = limit | LIMIT_CANNOT_GC;
+
+ // 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 limit;
+}
+
+// nsISupports subclasses' methods may be scriptable (or overridden
+// via binary XPCOM), and so may GC. But some fields just aren't going
+// to get overridden with something that can GC.
+function isOverridableField(staticCSU, csu, field)
+{
+ if (csu != 'nsISupports')
+ return false;
+
+ // Now that binary XPCOM is dead, all these annotations should be replaced
+ // with something based on bug 1347999.
+ if (field == 'GetCurrentJSContext')
+ return false;
+ if (field == 'IsOnCurrentThread')
+ return false;
+ if (field == 'GetNativeContext')
+ return false;
+ if (field == "GetGlobalJSObject")
+ return false;
+ if (field == "GetGlobalJSObjectPreserveColor")
+ return false;
+ if (field == "GetIsMainThread")
+ return false;
+ if (field == "GetThreadFromPRThread")
+ return false;
+ if (field == "DocAddSizeOfIncludingThis")
+ return false;
+ if (field == "ConstructUbiNode")
+ return false;
+
+ // Fields on the [builtinclass] nsIPrincipal
+ if (field == "GetSiteOrigin")
+ return false;
+ if (field == "GetDomain")
+ return false;
+ if (field == "GetBaseDomain")
+ return false;
+ if (field == "GetOriginNoSuffix")
+ return false;
+
+ // Fields on nsIURI
+ if (field == "GetScheme")
+ return false;
+ if (field == "GetAsciiHostPort")
+ return false;
+ if (field == "GetAsciiSpec")
+ return false;
+ if (field == "SchemeIs")
+ return false;
+
+ if (staticCSU == 'nsIXPCScriptable' && field == "GetScriptableFlags")
+ return false;
+ if (staticCSU == 'nsIXPConnectJSObjectHolder' && field == 'GetJSObject')
+ return false;
+ if (staticCSU == 'nsIXPConnect' && field == 'GetSafeJSContext')
+ return false;
+
+ // nsIScriptSecurityManager is not [builtinclass], but smaug says "the
+ // interface definitely should be builtinclass", which is good enough.
+ if (staticCSU == 'nsIScriptSecurityManager' && field == 'IsSystemPrincipal')
+ return false;
+
+ if (staticCSU == 'nsIScriptContext') {
+ if (field == 'GetWindowProxy' || field == 'GetWindowProxyPreserveColor')
+ return false;
+ }
+ return true;
+}
+
+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..902ae1e32f
--- /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/ductwork/debugger 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..9b198791e0
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/callgraph.js
@@ -0,0 +1,247 @@
+/* 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.
+//
+// field : CFG of the field
+var virtualResolutionsSeen = new Set();
+
+// map is a map from names to sets of entries.
+function addToNamedSet(map, name, entry)
+{
+ if (!map.has(name))
+ map.set(name, new Set());
+ map.get(name).add(entry);
+}
+
+function fieldKey(csuName, field)
+{
+ // This makes a minimal attempt at dealing with overloading: it will not
+ // conflate two virtual methods with differing numbers of arguments. So
+ // far, that is all that has been needed.
+ var nargs = 0;
+ if (field.Type.Kind == "Function" && "TypeFunctionArguments" in field.Type)
+ nargs = field.Type.TypeFunctionArguments.Type.length;
+ return csuName + ":" + field.Name[0] + ":" + nargs;
+}
+
+// CSU is "Class/Struct/Union"
+function processCSU(csuName, csu)
+{
+ if (!("FunctionField" in csu))
+ return;
+ for (const field of csu.FunctionField) {
+ if (1 in field.Field) {
+ const superclass = field.Field[1].Type.Name;
+ const subclass = field.Field[1].FieldCSU.Type.Name;
+ assert(subclass == csuName);
+ addToNamedSet(subclasses, superclass, subclass);
+ addToNamedSet(superclasses, subclass, superclass);
+ }
+
+ if ("Variable" in field) {
+ // Note: not dealing with overloading correctly.
+ const name = field.Variable.Name[0];
+ addToNamedSet(virtualDefinitions, fieldKey(csuName, field.Field[0]), name);
+ }
+ }
+}
+
+// Return the nearest ancestor method definition, or all nearest definitions in
+// the case of multiple inheritance.
+function nearestAncestorMethods(csu, field)
+{
+ const key = fieldKey(csu, field);
+
+ if (virtualDefinitions.has(key))
+ return new Set(virtualDefinitions.get(key));
+
+ const functions = new Set();
+ if (superclasses.has(csu)) {
+ for (const parent of superclasses.get(csu))
+ functions.update(nearestAncestorMethods(parent, field));
+ }
+
+ return functions;
+}
+
+// Return [ instantiations, limits ], where instantiations is a Set of all
+// possible implementations of 'field' given static type 'initialCSU', plus
+// null if arbitrary other implementations are possible, and limits gives
+// information about what things are not possible within it (currently, that it
+// cannot GC).
+function findVirtualFunctions(initialCSU, field)
+{
+ const fieldName = field.Name[0];
+ const worklist = [initialCSU];
+ const functions = new Set();
+
+ // Loop through all methods of initialCSU (by looking at all methods of ancestor csus).
+ //
+ // If field is nsISupports::AddRef or ::Release, return an empty list and a
+ // boolean that says we assert that it cannot GC.
+ //
+ // If this is a method that is annotated to be dangerous (eg, it could be
+ // overridden with an implementation that could GC), then use null as a
+ // signal value that it should be considered to GC, even though we'll also
+ // collect all of the instantiations for other purposes.
+
+ while (worklist.length) {
+ const csu = worklist.pop();
+ if (isSuppressedVirtualMethod(csu, fieldName))
+ return [ new Set(), LIMIT_CANNOT_GC ];
+ if (isOverridableField(initialCSU, csu, fieldName)) {
+ // We will still resolve the virtual function call, because it's
+ // nice to have as complete a callgraph as possible for other uses.
+ // But push a token saying that we can run arbitrary code.
+ functions.add(null);
+ }
+
+ if (superclasses.has(csu))
+ worklist.push(...superclasses.get(csu));
+ }
+
+ // Now return a list of all the instantiations of the method named 'field'
+ // that could execute on an instance of initialCSU or a descendant class.
+
+ // Start with the class itself, or if it doesn't define the method, all
+ // nearest ancestor definitions.
+ functions.update(nearestAncestorMethods(initialCSU, field));
+
+ // Then recurse through all descendants to add in their definitions.
+
+ worklist.push(initialCSU);
+ while (worklist.length) {
+ const csu = worklist.pop();
+ const key = fieldKey(csu, field);
+
+ if (virtualDefinitions.has(key))
+ functions.update(virtualDefinitions.get(key));
+
+ if (subclasses.has(csu))
+ worklist.push(...subclasses.get(csu));
+ }
+
+ return [ functions, LIMIT_NONE ];
+}
+
+// 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 getCallees(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]}];
+ }
+
+ if (callee.Kind == "Int")
+ return []; // Intentional crash
+
+ assert(callee.Kind == "Drf");
+ const called = callee.Exp[0];
+ if (called.Kind == "Var") {
+ // indirect call through a variable.
+ return [{'kind': "indirect", 'variable': callee.Exp[0].Variable.Name[0]}];
+ }
+
+ if (called.Kind != "Fld") {
+ // unknown call target.
+ return [{'kind': "unknown"}];
+ }
+
+ const callees = [];
+ const field = callee.Exp[0].Field;
+ const fieldName = field.Name[0];
+ const csuName = field.FieldCSU.Type.Name;
+ let functions;
+ let limits = LIMIT_NONE;
+ if ("FieldInstanceFunction" in field) {
+ [ functions, limits ] = findVirtualFunctions(csuName, field);
+ callees.push({'kind': "field", 'csu': csuName, 'field': fieldName,
+ 'limits': limits, 'isVirtual': true});
+ } else {
+ functions = new Set([null]); // field call
+ }
+
+ // Known set of virtual call targets. Treat them as direct calls to all
+ // possible resolved types, but also record edges from this field call to
+ // each final callee. When the analysis is checking whether an edge can GC
+ // and it sees an unrooted pointer held live across this field call, it
+ // will know whether any of the direct callees can GC or not.
+ const targets = [];
+ let fullyResolved = true;
+ for (const name of functions) {
+ if (name === null) {
+ // Unknown set of call targets, meaning either a function pointer
+ // call ("field call") or a virtual method that can be overridden
+ // in extensions. Use the isVirtual property so that callers can
+ // tell which case holds.
+ callees.push({'kind': "field", 'csu': csuName, 'field': fieldName,
+ 'limits': limits,
+ 'isVirtual': "FieldInstanceFunction" in field});
+ fullyResolved = false;
+ } else {
+ targets.push({'kind': "direct", name, limits });
+ }
+ }
+ if (fullyResolved)
+ callees.push({'kind': "resolved-field", 'csu': csuName, 'field': fieldName, 'callees': targets});
+
+ return callees;
+}
+
+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..a622d38e1a
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/computeCallgraph.js
@@ -0,0 +1,342 @@
+/* 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 theFunctionNameToFind;
+if (scriptArgs[0] == '--function' || scriptArgs[0] == '-f') {
+ theFunctionNameToFind = scriptArgs[1];
+ scriptArgs = scriptArgs.slice(2);
+}
+
+var typeInfo_filename = scriptArgs[0] || "typeInfo.txt";
+var callgraphOut_filename = scriptArgs[1] || "callgraph.txt";
+
+var origOut = os.file.redirect(callgraphOut_filename);
+
+var memoized = new Map();
+var memoizedCount = 0;
+
+var JSNativeCaller = Object.create(null);
+var JSNatives = [];
+
+var unmangled2id = new Set();
+
+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;
+}
+
+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)
+{
+ if (!('PEdge' in body))
+ return;
+
+
+ for (var tag of getAnnotations(functionName, body).values()) {
+ print("T " + functionId(functionName) + " " + tag);
+ if (tag == "Calls JSNatives")
+ JSNativeCaller[functionName] = true;
+ }
+
+ // 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 limits (eg LIMIT_CANNOT_GC) are determined by whatever RAII
+ // scopes might be active, which have been computed previously for all
+ // points in the body.
+ var edgeLimited = body.limits[edge.Index[0]] | 0;
+
+ for (var callee of getCallees(edge)) {
+ // Individual callees may have additional limits. The only such
+ // limit currently is that nsISupports.{AddRef,Release} are assumed
+ // to never GC.
+ const limits = edgeLimited | callee.limits;
+ let prologue = limits ? `/${limits} ` : "";
+ prologue += functionId(functionName) + " ";
+ if (callee.kind == 'direct') {
+ const prev_limits = seen.has(callee.name) ? seen.get(callee.name) : LIMIT_UNVISITED;
+ if (prev_limits & ~limits) {
+ // Only output an edge if it loosens a limit.
+ seen.set(callee.name, prev_limits & limits);
+ 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 limits, 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;
+ }
+ }
+ }
+}
+
+var typeInfo = loadTypeInfo(typeInfo_filename);
+
+loadTypes("src_comp.xdb");
+
+var xdb = xdbLibrary();
+xdb.open("src_body.xdb");
+
+printErr("Finished loading data structures");
+
+var minStream = xdb.min_data_stream();
+var maxStream = xdb.max_data_stream();
+
+if (theFunctionNameToFind) {
+ var index = xdb.lookup_key(theFunctionNameToFind);
+ if (!index) {
+ printErr("Function not found");
+ quit(1);
+ }
+ minStream = maxStream = index;
+}
+
+function process(functionName, functionBodies)
+{
+ for (var body of functionBodies)
+ body.limits = [];
+
+ for (var body of functionBodies) {
+ for (var [pbody, id, limits] of allRAIIGuardedCallPoints(typeInfo, functionBodies, body, isLimitConstructor)) {
+ pbody.limits[id] = limits;
+ }
+ }
+
+ for (var body of functionBodies)
+ processBody(functionName, body);
+
+ // 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))
+ JSNatives.push(functionName);
+}
+
+function postprocess_callgraph() {
+ for (const caller of Object.keys(JSNativeCaller)) {
+ const caller_id = functionId(caller);
+ for (const callee of JSNatives)
+ printOnce(`D ${caller_id} ${functionId(callee)}`);
+ }
+}
+
+for (var nameIndex = minStream; nameIndex <= maxStream; 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);
+}
+
+postprocess_callgraph();
+
+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..9a693df677
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/computeGCFunctions.js
@@ -0,0 +1,76 @@
+/* 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');
+
+if (typeof scriptArgs[0] != 'string')
+ throw "Usage: computeGCFunctions.js <callgraph.txt> <out:gcFunctions.txt> <out:gcFunctions.lst> <out:gcEdges.txt> <out:limitedFunctions.lst>";
+
+var start = "Time: " + new Date;
+
+var callgraph_filename = scriptArgs[0];
+var gcFunctions_filename = scriptArgs[1] || "gcFunctions.txt";
+var gcFunctionsList_filename = scriptArgs[2] || "gcFunctions.lst";
+var gcEdges_filename = scriptArgs[3] || "gcEdges.txt";
+var limitedFunctionsList_filename = scriptArgs[4] || "limitedFunctions.lst";
+
+loadCallgraph(callgraph_filename);
+
+printErr("Writing " + gcFunctions_filename);
+redirect(gcFunctions_filename);
+
+for (var name in gcFunctions) {
+ for (let readable of (readableNames[name] || [])) {
+ print("");
+ print("GC Function: " + name + "$" + readable);
+ let current = name;
+ do {
+ current = gcFunctions[current];
+ if (current in readableNames)
+ print(" " + readableNames[current][0]);
+ else
+ print(" " + current);
+ } while (current in gcFunctions);
+ }
+}
+
+printErr("Writing " + gcFunctionsList_filename);
+redirect(gcFunctionsList_filename);
+for (var name in gcFunctions) {
+ if (name in readableNames) {
+ for (var readable of readableNames[name])
+ print(name + "$" + readable);
+ } else {
+ print(name);
+ }
+}
+
+// gcEdges is a list of edges that can GC for more specific reasons than just
+// calling a function that is in gcFunctions.txt.
+//
+// Right now, it is unused. It was meant for ~AutoRealm when it might
+// wrap an exception, but anything held live across ~AC will have to be held
+// live across the corresponding constructor (and hence the whole scope of the
+// AC), and in that case it'll be held live across whatever could create an
+// exception within the AC scope. So ~AC edges are redundant. I will leave the
+// stub machinery here for now.
+printErr("Writing " + gcEdges_filename);
+redirect(gcEdges_filename);
+for (var block in gcEdges) {
+ for (var edge in gcEdges[block]) {
+ var func = gcEdges[block][edge];
+ print([ block, edge, func ].join(" || "));
+ }
+}
+
+printErr("Writing " + limitedFunctionsList_filename);
+redirect(limitedFunctionsList_filename);
+for (const [name, limits] of Object.entries(limitedFunctions))
+ print(`${limits} ${name}`);
diff --git a/js/src/devtools/rootAnalysis/computeGCTypes.js b/js/src/devtools/rootAnalysis/computeGCTypes.js
new file mode 100644
index 0000000000..b22fb7c1fb
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/computeGCTypes.js
@@ -0,0 +1,401 @@
+/* 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 gcTypes_filename = scriptArgs[0] || "gcTypes.txt";
+var typeInfo_filename = scriptArgs[1] || "typeInfo.txt";
+
+var typeInfo = {
+ 'GCPointers': [],
+ 'GCThings': [],
+ 'GCInvalidated': [],
+ '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 = {};
+
+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 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);
+
+// 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*. But this needs to be done in a conservative
+// direction: Maybe<AutoSuppressGC> should not be regarding as suppressing GC
+// (because it might still be None).
+//
+// Note that there is an order-dependence here that is being mostly ignored (eg
+// Maybe<Maybe<Cell*>> -- if that is processed before Maybe<Cell*> is
+// processed, we won't get the right answer). We'll at least sort by string
+// length to make it hard to hit that case.
+var inheritors = Object.keys(typeInfo.InheritFromTemplateArgs).sort((a, b) => a.length - b.length);
+for (const csu of inheritors) {
+ // Unfortunately, we just have a string type name, not the full structure
+ // of a templatized type, so we will have to resort to loose (buggy)
+ // pattern matching.
+ //
+ // Currently, the simplest ways I know of to break this are:
+ //
+ // foo<T>::bar<U>
+ // foo<bar<T,U>>
+ //
+ const [_, params_str] = csu.match(/<(.*)>/);
+ for (let param of params_str.split(",")) {
+ param = param.replace(/^\s+/, '')
+ param = param.replace(/\s+$/, '')
+ const pieces = param.split("*");
+ const core_type = pieces[0];
+ const ptrdness = pieces.length - 1;
+ if (ptrdness > 1)
+ continue;
+ const paramDesc = 'template-param-' + param;
+ const why = '(inherited annotations from ' + param + ')';
+ if (core_type in gcTypes)
+ markGCType(csu, paramDesc, why, ptrdness, 0, "");
+ if (core_type in gcPointers)
+ markGCType(csu, paramDesc, why, ptrdness + 1, 0, "");
+ }
+}
+
+// "typeName is a (pointer to a)^'typePtrLevel' GC type because it contains a field
+// named 'child' of type 'why' (or pointer to 'why' if fieldPtrLevel == 1), which is
+// itself a GCThing or GCPointer."
+function markGCType(typeName, child, why, 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 (!fieldPtrLevel && isUnsafeStorage(typeName)) {
+ // The UniquePtr itself is on the stack but when you dereference the
+ // contained pointer, you get to the unsafe memory that we are treating
+ // as if it were the stack (aka ptrLevel 0). Note that
+ // UniquePtr<UniquePtr<JSObject*>> is fine, so we don't want to just
+ // hardcode the ptrLevel.
+ fieldPtrLevel = -1;
+ }
+
+ // 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
+ //
+ var ptrLevel = typePtrLevel + fieldPtrLevel;
+
+ // ...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(why);
+ } else if (ptrLevel == 1) {
+ if (typeName in typeInfo.NonGCPointers)
+ return;
+ if (!(typeName in gcPointers))
+ gcPointers[typeName] = new Set();
+ gcPointers[typeName].add(why);
+ }
+
+ if (ptrLevel < 2) {
+ if (!gcFields.has(typeName))
+ gcFields.set(typeName, new Map());
+ gcFields.get(typeName).set(child, [ why, fieldPtrLevel ]);
+ }
+
+ 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, child, why, depth, fieldPtrLevel)
+{
+ markGCType(typeName, '<annotation>', '(annotation)', 0, 0, "");
+}
+
+function addGCPointer(typeName)
+{
+ markGCType(typeName, '<pointer-annotation>', '(annotation)', 1, 0, "");
+}
+
+// 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 {
+ 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(gcTypes_filename);
+
+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(typeInfo_filename));
+
+// 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..f1d52da017
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/dumpCFG.js
@@ -0,0 +1,267 @@
+/* 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) + "*";
+ else if (Kind == 'CSU')
+ return Name;
+ else if (Kind == 'Array')
+ 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 "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 "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 ("Kind" in unknown) {
+ if ("BlockId" in unknown)
+ return str_Variable(unknown);
+ return str_value(unknown);
+ } else if ("Type" in unknown) {
+ return str_Type(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..993725273c
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/explain.py
@@ -0,0 +1,129 @@
+#!/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/.
+
+
+from __future__ import print_function
+
+import argparse
+import re
+
+from collections import defaultdict
+
+parser = argparse.ArgumentParser(description="Process some integers.")
+parser.add_argument("rootingHazards", nargs="?", default="rootingHazards.txt")
+parser.add_argument("gcFunctions", nargs="?", default="gcFunctions.txt")
+parser.add_argument("hazards", nargs="?", default="hazards.txt")
+parser.add_argument("extra", nargs="?", default="unnecessary.txt")
+parser.add_argument("refs", nargs="?", default="refs.txt")
+args = parser.parse_args()
+
+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:
+ current_gcFunction = None
+
+ # Map from a GC function name to the list of hazards resulting from
+ # that GC function
+ hazardousGCFunctions = defaultdict(list)
+
+ # List of tuples (gcFunction, index of hazard) used to maintain the
+ # ordering of the hazards
+ hazardOrder = []
+
+ # Map from a hazardous GC function to the filename containing it.
+ fileOfFunction = {}
+
+ for line in rootingHazards:
+ m = re.match(r"^Time: (.*)", line)
+ mm = re.match(r"^Run on:", line)
+ if m or mm:
+ print(line, file=hazards)
+ print(line, file=extra)
+ print(line, file=refs)
+ continue
+
+ m = re.match(r"^Function.*has unnecessary root", line)
+ if m:
+ print(line, file=extra)
+ continue
+
+ m = re.match(r"^Function.*takes unsafe address of unrooted", line)
+ if m:
+ num_refs += 1
+ print(line, file=refs)
+ continue
+
+ m = re.match(
+ r"^Function.*has unrooted.*of type.*live across GC call '(.*?)' at (\S+):\d+$",
+ line,
+ ) # NOQA: E501
+ if m:
+ current_gcFunction = m.group(1)
+ hazardousGCFunctions[current_gcFunction].append(line)
+ hazardOrder.append(
+ (
+ current_gcFunction,
+ len(hazardousGCFunctions[current_gcFunction]) - 1,
+ )
+ )
+ num_hazards += 1
+ fileOfFunction[current_gcFunction] = m.group(2)
+ continue
+
+ m = re.match(r"Function.*expected hazard.*but none were found", line)
+ if m:
+ num_missing += 1
+ print(line + "\n", file=hazards)
+ continue
+
+ if current_gcFunction:
+ if not line.strip():
+ # Blank line => end of this hazard
+ current_gcFunction = None
+ else:
+ hazardousGCFunctions[current_gcFunction][-1] += line
+
+ with open(args.gcFunctions) as gcFunctions:
+ gcExplanations = {} # gcFunction => stack showing why it can GC
+
+ current_func = None
+ explanation = None
+ for line in gcFunctions:
+ m = re.match(r"^GC Function: (.*)", line)
+ if m:
+ if current_func:
+ gcExplanations[current_func] = explanation
+ current_func = None
+ if m.group(1) in hazardousGCFunctions:
+ current_func = m.group(1)
+ explanation = line
+ elif current_func:
+ explanation += line
+ if current_func:
+ gcExplanations[current_func] = explanation
+
+ for gcFunction, index in hazardOrder:
+ gcHazards = hazardousGCFunctions[gcFunction]
+
+ if gcFunction in gcExplanations:
+ print(gcHazards[index] + gcExplanations[gcFunction], file=hazards)
+ else:
+ print(gcHazards[index], file=hazards)
+
+except IOError as e:
+ print("Failed: %s" % str(e))
+
+print("Wrote %s" % args.hazards)
+print("Wrote %s" % args.extra)
+print("Wrote %s" % args.refs)
+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..cfe5ab6c58
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/loadCallgraph.js
@@ -0,0 +1,428 @@
+/* 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');
+
+// 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 readableNames = {}; // map from mangled name => list of readable names
+var calleesOf = {}; // map from mangled => list of tuples of {'callee':mangled, 'limits':intset}
+var callersOf; // map from mangled => list of tuples of {'caller':mangled, 'limits':intset}
+var gcFunctions = {}; // map from mangled callee => reason
+var limitedFunctions = {}; // set of mangled names (map from mangled name => limit intset)
+var gcEdges = {};
+
+// "Map" from identifier to mangled name, or sometimes to a Class.Field name.
+var functionNames = [""];
+
+var mangledToId = {};
+
+// Returns whether the function was added. (It will be refused if it was
+// already there, or if limits or annotations say it shouldn't be added.)
+function addGCFunction(caller, reason, functionLimits)
+{
+ if (functionLimits[caller] & LIMIT_CANNOT_GC)
+ return false;
+
+ if (ignoreGCFunction(functionNames[caller]))
+ return false;
+
+ if (!(caller in gcFunctions)) {
+ gcFunctions[caller] = reason;
+ return true;
+ }
+
+ return false;
+}
+
+// Every caller->callee callsite is associated with a limit saying what is
+// allowed at that callsite (eg if it's in a GC suppression zone, it would have
+// LIMIT_CANNOT_GC set.) A given caller might call the same callee multiple
+// times, with different limits, so we want to associate the <caller,callee>
+// edge with the intersection ('AND') of all of the callsites' limits.
+//
+// Scan through all call edges and intersect the limits for all matching
+// <caller,callee> edges (so that the result is the least limiting of all
+// matching edges.) Preserve the original order.
+//
+// During the same scan, build callersOf from calleesOf.
+function merge_repeated_calls(calleesOf) {
+ const callersOf = Object.create(null);
+
+ for (const [caller, callee_limits] of Object.entries(calleesOf)) {
+ const ordered_callees = [];
+
+ // callee_limits is a list of {callee,limit} objects.
+ const callee2limit = new Map();
+ for (const {callee, limits} of callee_limits) {
+ const prev_limits = callee2limit.get(callee);
+ if (prev_limits === undefined) {
+ callee2limit.set(callee, limits);
+ ordered_callees.push(callee);
+ } else {
+ callee2limit.set(callee, prev_limits & limits);
+ }
+ }
+
+ // Update the contents of callee_limits to contain a single entry for
+ // each callee, with its limits set to the AND of the limits observed
+ // at all callsites within this caller function.
+ callee_limits.length = 0;
+ for (const callee of ordered_callees) {
+ const limits = callee2limit.get(callee);
+ callee_limits.push({callee, limits});
+ if (!(callee in callersOf))
+ callersOf[callee] = [];
+ callersOf[callee].push({caller, limits});
+ }
+ }
+
+ return callersOf;
+}
+
+function loadCallgraph(file)
+{
+ const fieldCallLimits = {};
+ const fieldCallCSU = new Map(); // map from full field name id => csu name
+ const resolvedFieldCalls = new Set();
+
+ // set of mangled names (map from mangled name => limit intset)
+ var functionLimits = {};
+
+ let numGCCalls = 0;
+
+ 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(functionNames.length == id);
+ functionNames.push(mangled);
+ mangledToId[mangled] = id;
+ continue;
+ }
+ if (match = line.charAt(0) == "=" && /^= (\d+) (.*)/.exec(line)) {
+ const [ _, id, readable ] = match;
+ const mangled = functionNames[id];
+ if (mangled in readableNames)
+ readableNames[mangled].push(readable);
+ else
+ readableNames[mangled] = [ readable ];
+ continue;
+ }
+
+ let limits = 0;
+ // Example line: D /17 6 7
+ //
+ // This means a direct call from 6 -> 7, but within a scope that
+ // applies limits 0x1 and 0x10 to the callee.
+ //
+ // Look for a limit and remove it from the line if found.
+ if (line.indexOf("/") != -1) {
+ match = /^(..)\/(\d+) (.*)/.exec(line);
+ line = match[1] + match[3];
+ limits = 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(functionNames[caller], name) &&
+ !(limits & LIMIT_CANNOT_GC))
+ {
+ addGCFunction(caller, "IndirectCall: " + name, functionLimits);
+ }
+ } else if (match = (tag == 'F' || tag == 'V') && /^[FV] (\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(functionNames[fullfield] == fullfield_str);
+ if (limits)
+ fieldCallLimits[fullfield] = limits;
+ addToKeyedList(calleesOf, caller, {callee:fullfield, limits});
+ fieldCallCSU.set(fullfield, csu);
+ } else if (match = tag == 'D' && /^D (\d+) (\d+)/.exec(line)) {
+ const caller = match[1]|0;
+ const callee = match[2]|0;
+ addToKeyedList(calleesOf, caller, {callee:callee, limits:limits});
+ } else if (match = tag == 'R' && /^R (\d+) (\d+)/.exec(line)) {
+ const callerField = match[1]|0;
+ const callee = match[2]|0;
+ // Resolved virtual functions create a dummy node for the field
+ // call, and callers call it. It will then call all possible
+ // instantiations. No additional limits are placed on the callees;
+ // it's as if there were a function named BaseClass.foo:
+ //
+ // void BaseClass.foo() {
+ // Subclass1::foo();
+ // Subclass2::foo();
+ // }
+ //
+ addToKeyedList(calleesOf, callerField, {callee:callee, limits:0});
+ // Mark that we resolved this virtual method, so that it isn't
+ // assumed to call some random function that might do anything.
+ resolvedFieldCalls.add(callerField);
+ } else if (match = tag == 'T' && /^T (\d+) (.*)/.exec(line)) {
+ const id = match[1]|0;
+ let tag = match[2];
+ if (tag == 'GC Call') {
+ addGCFunction(id, "GC", functionLimits);
+ numGCCalls++;
+ }
+ } else {
+ assert(false, "Invalid format in callgraph line: " + line);
+ }
+ }
+
+ // Callers have a list of callees, with duplicates (if the same function is
+ // called more than once.) Merge the repeated calls, only keeping limits
+ // that are in force for *every* callsite of that callee. Also, generate
+ // the callersOf table at the same time.
+ callersOf = merge_repeated_calls(calleesOf);
+
+ // 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()) {
+ addGCFunction(mangledToId[func], "annotation", functionLimits);
+ }
+
+ // Compute functionLimits: it should contain the set of functions that
+ // are *always* called within some sort of limited context (eg GC
+ // suppression).
+
+ // Initialize to limited field calls.
+ for (var [name, limits] of Object.entries(fieldCallLimits)) {
+ if (limits)
+ functionLimits[name] = limits;
+ }
+
+ // Initialize functionLimits to the set of all functions, where each one is
+ // maximally limited, and return a worklist containing all simple roots
+ // (nodes with no callers).
+ var roots = gather_simple_roots(functionLimits, callersOf);
+
+ // Traverse the graph, spreading the limits down from the roots.
+ propagate_limits(roots, functionLimits, 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.
+ roots = gather_recursive_roots(roots, functionLimits, callersOf);
+
+ // And do a final traversal starting with the recursive roots.
+ propagate_limits(roots, functionLimits, calleesOf);
+
+ // Eliminate GC-limited functions from the set of functions known to GC.
+ for (var name in gcFunctions) {
+ if (functionLimits[name] & LIMIT_CANNOT_GC)
+ delete gcFunctions[name];
+ }
+
+ // functionLimits should now contain all functions that are always called
+ // in a limited context.
+
+ // 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(numGCCalls > 0, "No GC functions found!");
+
+ // Initialize the worklist to all known gcFunctions.
+ var worklist = [];
+ for (const name in gcFunctions)
+ worklist.push(name);
+
+ // Include all field calls and unresolved virtual method calls.
+ for (const [name, csuName] of fieldCallCSU) {
+ if (resolvedFieldCalls.has(name))
+ continue; // Skip resolved virtual functions.
+ const fullFieldName = functionNames[name];
+ if (!fieldCallCannotGC(csuName, fullFieldName)) {
+ gcFunctions[name] = 'unresolved ' + 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 (!(name in callersOf))
+ continue;
+ for (const {caller, limits} of callersOf[name]) {
+ if (!(limits & LIMIT_CANNOT_GC)) {
+ if (addGCFunction(caller, name, functionLimits))
+ worklist.push(caller);
+ }
+ }
+ }
+
+ // Convert functionLimits to limitedFunctions (using mangled names instead
+ // of ids.)
+
+ for (const [id, limits] of Object.entries(functionLimits))
+ limitedFunctions[functionNames[id]] = limits;
+
+ // The above code uses integer ids for efficiency. External code uses
+ // mangled names. Rewrite the various data structures to convert ids to
+ // mangled names.
+ remap_ids_to_mangled_names();
+}
+
+// Return a worklist of functions with no callers, and also initialize
+// functionLimits to the set of all functions, each mapped to LIMIT_UNVISTED.
+function gather_simple_roots(functionLimits, callersOf) {
+ const roots = [];
+ for (let callee in callersOf)
+ functionLimits[callee] = LIMIT_UNVISITED;
+ for (let caller in calleesOf) {
+ if (!(caller in callersOf)) {
+ functionLimits[caller] = LIMIT_UNVISITED;
+ roots.push([caller, LIMIT_NONE, 'root']);
+ }
+ }
+
+ return roots;
+}
+
+// Recursively traverse the callgraph from the roots. Recurse through every
+// edge that weakens the limits. (Limits that entirely disappear, aka go to a
+// zero intset, will be removed from functionLimits.)
+function propagate_limits(worklist, functionLimits, calleesOf) {
+ let top = worklist.length;
+ while (top > 0) {
+ // Consider caller where (graph) -> caller -> (0 or more callees)
+ // 'callercaller' is for debugging.
+ const [caller, edge_limits, callercaller] = worklist[--top];
+ const prev_limits = functionLimits[caller];
+ if (prev_limits & ~edge_limits) {
+ // Turning off a limit (or unvisited marker). Must recurse to the
+ // children. But first, update this caller's limits: we just found
+ // out it is reachable by an unlimited path, so it must be treated
+ // as unlimited (with respect to that bit).
+ const new_limits = prev_limits & edge_limits;
+ if (new_limits)
+ functionLimits[caller] = new_limits;
+ else
+ delete functionLimits[caller];
+ for (const {callee, limits} of (calleesOf[caller] || []))
+ worklist[top++] = [callee, limits | edge_limits, caller];
+ }
+ }
+}
+
+// Mutually-recursive roots and their descendants will not have been visited,
+// and will still be set to LIMIT_UNVISITED. Scan through and gather them.
+function gather_recursive_roots(functionLimits, callersOf) {
+ const roots = [];
+
+ // 'seen' maps functions to the most recent starting function that each was
+ // first reachable from, to distinguish between the current pass and passes
+ // for preceding functions.
+ //
+ // Consider:
+ //
+ // A <--> B --> C <-- D <--> E
+ // C --> F
+ // C --> G
+ //
+ // So there are two root cycles AB and DE, both calling C that in turn
+ // calls F and G. If we start at F and scan up through callers, we will
+ // keep going until A loops back to B and E loops back to D, and will add B
+ // and D as roots. Then if we scan from G, we encounter C and see that it
+ // was already been seen on an earlier pass. So C and everything reachable
+ // from it is already reachable by some root. (We need to label nodes with
+ // their pass because otherwise we couldn't distinguish "already seen C,
+ // done" from "already seen B, must be a root".)
+ //
+ const seen = new Map();
+ for (var func in functionLimits) {
+ if (functionLimits[func] != LIMIT_UNVISITED)
+ continue;
+
+ // We should only be looking at nodes with callers, since otherwise
+ // they would have been handled in the previous pass!
+ assert(callersOf[func].length > 0);
+
+ const work = [func];
+ while (work.length > 0) {
+ const f = work.pop();
+ if (seen.has(f)) {
+ if (seen.get(f) == func) {
+ // We have traversed a cycle and reached an already-seen
+ // node. Treat it as a root.
+ roots.push([f, LIMIT_NONE, 'root']);
+ print(`recursive root? ${f} = ${functionNames[f]}`);
+ } else {
+ // Otherwise we hit the portion of the graph that is
+ // reachable from a past root.
+ seen.set(f, func);
+ }
+ } else {
+ print(`retained by recursive root? ${f} = ${functionNames[f]}`);
+ work.push(...callersOf[f]);
+ seen.set(f, func);
+ }
+ }
+ }
+
+ return roots;
+}
+
+function remap_ids_to_mangled_names() {
+ var tmp = gcFunctions;
+ gcFunctions = {};
+ for (const [caller, reason] of Object.entries(tmp))
+ gcFunctions[functionNames[caller]] = functionNames[reason] || reason;
+
+ 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..de38df7388
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/mach_commands.py
@@ -0,0 +1,393 @@
+# -*- 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/.
+
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+import argparse
+import json
+import os
+import textwrap
+
+from mach.base import FailedCommandError, MachError
+from mach.decorators import (
+ CommandArgument,
+ CommandProvider,
+ Command,
+ SubCommand,
+)
+from mach.registrar import Registrar
+
+from mozbuild.mozconfig import MozconfigLoader
+from mozbuild.base import MachCommandBase
+
+# Command files like this are listed in build/mach_bootstrap.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
+
+
+# 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
+
+
+@CommandProvider
+class MachCommands(MachCommandBase):
+ @property
+ def state_dir(self):
+ return os.environ.get("MOZBUILD_STATE_PATH", os.path.expanduser("~/.mozbuild"))
+
+ @property
+ def tools_dir(self):
+ 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(self.state_dir, "hazard-tools")
+
+ @property
+ def sixgill_dir(self):
+ return os.path.join(self.tools_dir, "sixgill")
+
+ @property
+ def gcc_dir(self):
+ return os.path.join(self.tools_dir, "gcc")
+
+ @property
+ def script_dir(self):
+ return os.path.join(self.topsrcdir, "js/src/devtools/rootAnalysis")
+
+ def work_dir(self, application, given):
+ if given is not None:
+ return given
+ return os.path.join(self.topsrcdir, "haz-" + application)
+
+ def ensure_dir_exists(self, dir):
+ os.makedirs(dir, exist_ok=True)
+ return dir
+
+ # Force the use of hazard-compatible installs of tools.
+ def setup_env_for_tools(self, env):
+ gccbin = os.path.join(self.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=self.sixgill_dir, gccbin=gccbin, PATH=env["PATH"]
+ )
+ env["LD_LIBRARY_PATH"] = "{}/lib64".format(self.gcc_dir)
+
+ @Command(
+ "hazards",
+ category="build",
+ order="declaration",
+ description="Commands for running the static analysis for GC rooting hazards",
+ )
+ def hazards(self):
+ """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(self, **kwargs):
+ orig_dir = os.getcwd()
+ os.chdir(self.ensure_dir_exists(self.tools_dir))
+ try:
+ kwargs["from_build"] = ("linux64-gcc-sixgill", "linux64-gcc-8")
+ self._mach_context.commands.dispatch(
+ "artifact", self._mach_context, subcommand="toolchain", **kwargs
+ )
+ finally:
+ os.chdir(orig_dir)
+
+ @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(self, **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(self.topsrcdir, mozconfig_path)
+ loader = MozconfigLoader(self.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
+
+ self.setup_env_for_tools(os.environ)
+
+ # Set a default objdir for the shell, for developer builds.
+ os.environ.setdefault(
+ "MOZ_OBJDIR", os.path.join(self.topsrcdir, "obj-haz-shell")
+ )
+
+ return self._mach_context.commands.dispatch(
+ "build", self._mach_context, **kwargs
+ )
+
+ def read_json_file(self, filename):
+ with open(filename) as fh:
+ return json.load(fh)
+
+ def ensure_shell(self, objdir):
+ if objdir is None:
+ objdir = os.path.join(self.topsrcdir, "obj-haz-shell")
+
+ try:
+ binaries = self.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
+ )
+
+ @inherit_command_args("build")
+ @SubCommand(
+ "hazards",
+ "gather",
+ description="Gather analysis data by compiling the given application",
+ )
+ @CommandArgument(
+ "--application", default="browser", help="Build the given application."
+ )
+ @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(self, **kwargs):
+ """Gather analysis information by compiling the tree"""
+ application = kwargs["application"]
+ objdir = kwargs["haz_objdir"]
+ if objdir is None:
+ objdir = os.environ.get("HAZ_OBJDIR")
+ if objdir is None:
+ objdir = os.path.join(self.topsrcdir, "obj-analyzed-" + application)
+
+ work_dir = self.work_dir(application, kwargs["work_dir"])
+ self.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"
+ gcc_bin = "{gcc_dir}/bin"
+ """
+ ).format(
+ script_dir=self.script_dir,
+ objdir=objdir,
+ srcdir=self.topsrcdir,
+ sixgill_dir=self.sixgill_dir,
+ gcc_dir=self.gcc_dir,
+ )
+ fh.write(data)
+
+ buildscript = " ".join(
+ [
+ self.topsrcdir + "/mach hazards compile",
+ "--application=" + application,
+ "--haz-objdir=" + objdir,
+ ]
+ )
+ args = [
+ os.path.join(self.script_dir, "analyze.py"),
+ "dbs",
+ "--upto",
+ "dbs",
+ "-v",
+ "--buildcommand=" + buildscript,
+ ]
+
+ return self.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(
+ "--application", default="browser", help="Build the given application."
+ )
+ @CommandArgument(
+ "--haz-objdir",
+ default=os.environ.get("HAZ_OBJDIR"),
+ help="Write object files to this directory.",
+ )
+ def inner_compile(self, **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 Exception(
+ "no sixgill manager detected. `mach hazards compile` "
+ + "should only be run from `mach hazards gather`"
+ )
+
+ app = kwargs.pop("application")
+ default_mozconfig = "js/src/devtools/rootAnalysis/mozconfig.%s" % app
+ mozconfig_path = (
+ kwargs.pop("mozconfig", None) or env.get("MOZCONFIG") or default_mozconfig
+ )
+ mozconfig_path = os.path.join(self.topsrcdir, mozconfig_path)
+
+ # Validate the mozconfig.
+
+ # Require an explicit --enable-application=APP (even if you just
+ # want to build the default browser application.)
+ loader = MozconfigLoader(self.topsrcdir)
+ mozconfig = loader.read_mozconfig(mozconfig_path)
+ configure_args = mozconfig["configure_args"]
+ if "--enable-application=%s" % app not in configure_args:
+ raise Exception("mozconfig %s builds wrong project" % mozconfig_path)
+ if not any("--with-compiler-wrapper" in a for a in configure_args):
+ raise Exception("mozconfig must wrap compiles")
+
+ # Communicate mozconfig to build subprocesses.
+ env["MOZCONFIG"] = os.path.join(self.topsrcdir, mozconfig_path)
+
+ # hazard mozconfigs need to find binaries in .mozbuild
+ env["MOZBUILD_STATE_PATH"] = self.state_dir
+
+ # Suppress the gathering of sources, to save disk space and memory.
+ env["XGILL_NO_SOURCE"] = "1"
+
+ self.setup_env_for_tools(env)
+
+ if "haz_objdir" in kwargs:
+ env["MOZ_OBJDIR"] = kwargs.pop("haz_objdir")
+
+ return self._mach_context.commands.dispatch(
+ "build", self._mach_context, **kwargs
+ )
+
+ @SubCommand(
+ "hazards", "analyze", description="Analyzed gathered data for rooting hazards"
+ )
+ @CommandArgument(
+ "--application",
+ default="browser",
+ help="Analyze the output for the given application.",
+ )
+ @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."
+ )
+ def analyze(self, application, shell_objdir, work_dir):
+ """Analyzed gathered data for rooting hazards"""
+
+ shell = self.ensure_shell(shell_objdir)
+ args = [
+ os.path.join(self.script_dir, "analyze.py"),
+ "--js",
+ shell,
+ "gcTypes",
+ "-v",
+ ]
+
+ self.setup_env_for_tools(os.environ)
+ os.environ["LD_LIBRARY_PATH"] += ":" + os.path.dirname(shell)
+
+ work_dir = self.work_dir(application, work_dir)
+ return self.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.",
+ )
+ def self_test(self, shell_objdir):
+ """Analyzed gathered data for rooting hazards"""
+ shell = self.ensure_shell(shell_objdir)
+ args = [
+ os.path.join(self.script_dir, "run-test.py"),
+ "-v",
+ "--js",
+ shell,
+ "--sixgill",
+ os.path.join(self.tools_dir, "sixgill"),
+ "--gccdir",
+ self.gcc_dir,
+ ]
+
+ self.setup_env_for_tools(os.environ)
+ os.environ["LD_LIBRARY_PATH"] += ":" + os.path.dirname(shell)
+ return self.run_process(args=args, pass_thru=True)
diff --git a/js/src/devtools/rootAnalysis/mozconfig.browser b/js/src/devtools/rootAnalysis/mozconfig.browser
new file mode 100644
index 0000000000..60fcca048e
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/mozconfig.browser
@@ -0,0 +1,12 @@
+# 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-application=browser
+ac_add_options --enable-js-shell
+
+. $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..76f9d36248
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/mozconfig.haz_shell
@@ -0,0 +1,17 @@
+# 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-application=js
+ac_add_options --enable-nspr-build
+
+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..3699ed3fb0
--- /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-application=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..0ab2be4d8b
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/run-test.py
@@ -0,0 +1,124 @@
+#!/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/.
+
+from __future__ import print_function
+
+import os
+import site
+import subprocess
+import argparse
+
+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",
+ action="store_true",
+ help="Display verbose output, including commands executed",
+)
+parser.add_argument(
+ "tests",
+ nargs="*",
+ default=["sixgill-tree", "suppression", "hazards", "exceptions", "virtual"],
+ help="tests to run",
+)
+
+cfg = parser.parse_args()
+
+if not cfg.js:
+ 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)
+
+for name in cfg.tests:
+ name = os.path.basename(name)
+ 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)
+ except AssertionError:
+ print("TEST-FAILED: %s" % name)
+ raise
+ else:
+ print("TEST-PASSED: %s" % name)
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..70e6ff9841
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/exceptions/source.cpp
@@ -0,0 +1,54 @@
+/* -*- 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");
+
+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/hazards/source.cpp b/js/src/devtools/rootAnalysis/t/hazards/source.cpp
new file mode 100644
index 0000000000..69ed3d4100
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/hazards/source.cpp
@@ -0,0 +1,326 @@
+/* -*- 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)))
+
+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() {}
+};
+
+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*);
+
+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
+}
+
+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));
+ }
+}
+
+// Make sure `this` is live at the beginning of a function.
+class Subcell : public Cell {
+ int method() {
+ GC();
+ return f; // this->f
+ }
+};
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..8d3df8186b
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/hazards/test.py
@@ -0,0 +1,83 @@
+# 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()
+print(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
+
+# All hazards should be in f(), loopy(), safevals(), and method()
+assert hazmap["cell2"].function == "Cell* f()"
+print(len(set(haz.function for haz in hazards)))
+assert len(set(haz.function for haz in hazards)) == 4
+
+# 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()",
+)
+assert hazmap["<returnvalue>"].GCFunction == "void GCInDestructor::~GCInDestructor()"
+
+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
+
+# 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*"
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..0b8c2c7073
--- /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..b1a1c2f21f
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/suppression/test.py
@@ -0,0 +1,20 @@
+# flake8: noqa: F821
+test.compile("source.cpp")
+test.run_analysis_script("gcTypes", upto="gcFunctions")
+
+# The suppressions file uses mangled names.
+suppressed = test.load_suppressed_functions()
+
+# 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..d187164d84
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/testlib.py
@@ -0,0 +1,231 @@
+import json
+import os
+import re
+import subprocess
+
+from sixgill import Body
+from collections import defaultdict, namedtuple
+
+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"
+ cmd = "{CXX} -c {source} -O3 -std=c++11 -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:
+ 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, phase, upto=None):
+ open("defaults.py", "w").write(
+ """\
+analysis_scriptdir = '{scriptdir}'
+sixgill_bin = '{bindir}'
+""".format(
+ scriptdir=scriptdir, bindir=self.cfg.sixgill_bin
+ )
+ )
+ cmd = [
+ os.path.join(scriptdir, "analyze.py"),
+ "-v" if self.verbose else "-q",
+ phase,
+ ]
+ if upto:
+ cmd += ["--upto", upto]
+ cmd.append("--source=%s" % self.indir)
+ cmd.append("--objdir=%s" % self.outdir)
+ cmd.append("--js=%s" % self.cfg.js)
+ if self.cfg.verbose:
+ cmd.append("--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_suppressed_functions(self):
+ return set(
+ self.load_text_file(
+ "limitedFunctions.lst", extract=lambda l: l.split(" ")[1]
+ )
+ )
+
+ 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"):
+ with open(os.path.join(self.outdir, filename)) as fh:
+ return json.load(fh)
+
+ 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
+
+ limit = 0
+ m = re.match(r"^\w (?:/(\d+))? ", line)
+ if m:
+ limit = int(m[1])
+
+ tokens = line.split(" ")
+ 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"):
+ m = re.match(r"^[FV] (\d+) (\d+) CLASS (.*?) FIELD (.*)", line)
+ caller, callee, csu, field = m.groups()
+ add_call(lookup(caller), lookup(callee), limit)
+
+ 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("rootingHazards.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/virtual/source.cpp b/js/src/devtools/rootAnalysis/t/virtual/source.cpp
new file mode 100644
index 0000000000..e3977b07e2
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/virtual/source.cpp
@@ -0,0 +1,169 @@
+/* -*- 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("");
+}
+
+struct Cell {
+ int f;
+} ANNOTATE("GC Thing");
+
+extern void foo();
+
+typedef void (*func_t)();
+
+class Base {
+ public:
+ int ANNOTATE("field annotation") dummy;
+ virtual void someGC() ANNOTATE("Base pure virtual method") = 0;
+ 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 noneGC() = 0;
+ virtual void allGC() = 0;
+};
+
+void bar() { GC(); }
+
+class Sub1 : public Super {
+ public:
+ void noneGC() override { foo(); }
+ void someGC() override ANNOTATE("Sub1 override") ANNOTATE("second attr") {
+ foo();
+ }
+ void allGC() override {
+ foo();
+ bar();
+ }
+} ANNOTATE("CSU1") ANNOTATE("CSU2");
+
+class Sub2 : public Super {
+ public:
+ void noneGC() override { foo(); }
+ void someGC() override {
+ foo();
+ bar();
+ }
+ void allGC() override {
+ foo();
+ bar();
+ }
+};
+
+class Sibling : public Base {
+ public:
+ virtual void noneGC() { foo(); }
+ void someGC() override {
+ foo();
+ bar();
+ }
+ virtual void allGC() {
+ foo();
+ bar();
+ }
+};
+
+class AutoSuppressGC {
+ public:
+ AutoSuppressGC() {}
+ ~AutoSuppressGC() {}
+} ANNOTATE("Suppress GC");
+
+void use(Cell*) { asm(""); }
+
+void f() {
+ Sub1 s1;
+ Sub2 s2;
+
+ 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);
+ }
+}
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..a0e2a410ea
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/t/virtual/test.py
@@ -0,0 +1,48 @@
+# '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"]
+assert ["Sub1 override", "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::allGC()" in gcFunctions
+assert "void Sub2::noneGC()" not in gcFunctions
+assert "void Sub2::someGC()" in gcFunctions
+assert "void Sub2::allGC()" in gcFunctions
+
+callgraph = test.load_callgraph()
+
+assert callgraph.calleeGraph["void f()"]["Super.noneGC"]
+assert callgraph.calleeGraph["Super.noneGC"]["void Sub1::noneGC()"]
+assert callgraph.calleeGraph["Super.noneGC"]["void Sub2::noneGC()"]
+assert "void Sibling::noneGC()" not in callgraph.calleeGraph["Super.noneGC"]
+
+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
diff --git a/js/src/devtools/rootAnalysis/utility.js b/js/src/devtools/rootAnalysis/utility.js
new file mode 100644
index 0000000000..8df860facb
--- /dev/null
+++ b/js/src/devtools/rootAnalysis/utility.js
@@ -0,0 +1,292 @@
+/* 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');
+
+// Limit inset bits - each call edge may carry a set of 'limit' bits, saying eg
+// that the edge takes place within a scope where GC is suppressed, for
+// example.
+var LIMIT_NONE = 0;
+var LIMIT_CANNOT_GC = 1;
+var LIMIT_ALL = 1;
+
+// The traversal algorithms we run will recurse into children if you change any
+// limit bit to zero. Use all bits set to maximally limited, including
+// additional bits that all just mean "unvisited", so that the first time we
+// see a node with this limit, we're guaranteed to turn at least one bit off
+// and thereby keep going.
+var LIMIT_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 "assertion failed: " + msg + "\n" + (Error().stack);
+ else
+ throw "assertion failed: " + (Error().stack);
+}
+
+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);
+ }
+}
+
+function parse_options(parameters, inArgs = scriptArgs) {
+ const options = {};
+
+ const optional = {};
+ const positional = [];
+ for (const param of parameters) {
+ if (param.name.startsWith("-")) {
+ optional[param.name] = param;
+ param.dest = param.dest || param.name.substring(2).replace("-", "_");
+ } else {
+ positional.push(param);
+ param.dest = param.dest || param.name.replace("-", "_");
+ }
+
+ param.type = param.type || 'bool';
+ if ('default' in param)
+ options[param.dest] = param.default;
+ }
+
+ options.rest = [];
+ const args = [...inArgs];
+ while (args.length > 0) {
+ let param;
+ let pos = -1;
+ if (args[0] in optional)
+ param = optional[args[0]];
+ else {
+ pos = args[0].indexOf("=");
+ if (pos != -1) {
+ param = optional[args[0].substring(0, pos)];
+ pos++;
+ }
+ }
+
+ if (!param) {
+ if (positional.length > 0) {
+ param = positional.shift();
+ options[param.dest] = args.shift();
+ } else {
+ options.rest.push(args.shift());
+ }
+ continue;
+ }
+
+ if (param.type != 'bool') {
+ if (pos != -1) {
+ options[param.dest] = args.shift().substring(pos);
+ } else {
+ args.shift();
+ if (args.length == 0)
+ throw(new Error(`--${param.name} requires an argument`));
+ options[param.dest] = args.shift();
+ }
+ } else {
+ if (pos != -1)
+ throw(new Error(`--${param.name} does not take an argument`));
+ options[param.dest] = true;
+ args.shift();
+ }
+ }
+
+ 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)
+{
+ try {
+ if (!('predecessors' in body))
+ collectBodyEdges(body);
+ } catch (e) {
+ debugger;
+ printErr("body is " + 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 cLibrary()
+{
+ var libPossibilities = ['libc.so.6', 'libc.so', 'libc.dylib'];
+ var lib;
+ for (const name of libPossibilities) {
+ try {
+ lib = ctypes.open("libc.so.6");
+ } catch(e) {
+ }
+ }
+
+ 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 "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 loadTypeInfo(filename)
+{
+ return JSON.parse(os.file.readFile(filename));
+}
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..5fb6a80e12
--- /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..72c188d9be
--- /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/Windows.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..946a04c987
--- /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 */