diff options
Diffstat (limited to 'js/src/devtools')
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> + Garbage items per frame: + <input type="text" id="garbage-per-frame" size="5" value="8K" + onchange="garbage_per_frame_changed()"></input> +</div> +<div> + 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> + Time remaining: <span id="test-progress">(not running)</span> +</div + +<div> + 60 fps: <span id="pct60">n/a</span> + 45 fps: <span id="pct45">n/a</span> + 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 */ |