summaryrefslogtreecommitdiffstats
path: root/js/src/jsapi-tests/testScriptSourceCompression.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/jsapi-tests/testScriptSourceCompression.cpp')
-rw-r--r--js/src/jsapi-tests/testScriptSourceCompression.cpp551
1 files changed, 551 insertions, 0 deletions
diff --git a/js/src/jsapi-tests/testScriptSourceCompression.cpp b/js/src/jsapi-tests/testScriptSourceCompression.cpp
new file mode 100644
index 0000000000..3d1dc2c241
--- /dev/null
+++ b/js/src/jsapi-tests/testScriptSourceCompression.cpp
@@ -0,0 +1,551 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ */
+/* 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 "mozilla/Assertions.h" // MOZ_RELEASE_ASSERT
+#include "mozilla/RefPtr.h" // RefPtr
+#include "mozilla/Utf8.h" // mozilla::Utf8Unit
+
+#include <algorithm> // std::all_of, std::equal, std::move, std::transform
+#include <iterator> // std::size
+#include <memory> // std::uninitialized_fill_n
+#include <stddef.h> // size_t
+#include <stdint.h> // uint32_t
+
+#include "jsapi.h" // JS_EnsureLinearString, JS_GC, JS_Get{Latin1,TwoByte}LinearStringChars, JS_GetStringLength, JS_ValueToFunction
+#include "jstypes.h" // JS_PUBLIC_API
+
+#include "gc/GC.h" // js::gc::FinishGC
+#include "js/CompilationAndEvaluation.h" // JS::Evaluate
+#include "js/CompileOptions.h" // JS::CompileOptions, JS::InstantiateOptions
+#include "js/Conversions.h" // JS::ToString
+#include "js/experimental/JSStencil.h" // JS::Stencil, JS::CompileToStencilOffThread, JS::FinishCompileToStencilOffThread, JS::InstantiateGlobalStencil
+#include "js/MemoryFunctions.h" // JS_malloc
+#include "js/OffThreadScriptCompilation.h" // JS::OffThreadToken
+#include "js/RootingAPI.h" // JS::MutableHandle, JS::Rooted
+#include "js/SourceText.h" // JS::SourceOwnership, JS::SourceText
+#include "js/String.h" // JS::GetLatin1LinearStringChars, JS::GetTwoByteLinearStringChars, JS::StringHasLatin1Chars
+#include "js/UniquePtr.h" // js::UniquePtr
+#include "js/Utility.h" // JS::FreePolicy
+#include "js/Value.h" // JS::NullValue, JS::ObjectValue, JS::Value
+#include "jsapi-tests/tests.h"
+#include "util/Text.h" // js_strlen
+#include "vm/Compression.h" // js::Compressor::CHUNK_SIZE
+#include "vm/HelperThreads.h" // js::RunPendingSourceCompressions
+#include "vm/JSFunction.h" // JSFunction::getOrCreateScript
+#include "vm/JSScript.h" // JSScript, js::ScriptSource::MinimumCompressibleLength, js::SynchronouslyCompressSource
+#include "vm/Monitor.h" // js::Monitor, js::AutoLockMonitor
+
+using mozilla::Utf8Unit;
+
+struct JS_PUBLIC_API JSContext;
+class JS_PUBLIC_API JSString;
+
+template <typename Unit>
+using Source = js::UniquePtr<Unit[], JS::FreePolicy>;
+
+constexpr size_t ChunkSize = js::Compressor::CHUNK_SIZE;
+constexpr size_t MinimumCompressibleLength =
+ js::ScriptSource::MinimumCompressibleLength;
+
+// Don't use ' ' to spread stuff across lines.
+constexpr char FillerWhitespace = '\n';
+
+template <typename Unit>
+static Source<Unit> MakeSourceAllWhitespace(JSContext* cx, size_t len) {
+ static_assert(ChunkSize % sizeof(Unit) == 0,
+ "chunk size presumed to be a multiple of char size");
+
+ Source<Unit> source(
+ reinterpret_cast<Unit*>(JS_malloc(cx, len * sizeof(Unit))));
+ if (source) {
+ std::uninitialized_fill_n(source.get(), len, FillerWhitespace);
+ }
+ return source;
+}
+
+template <typename Unit>
+static JSFunction* EvaluateChars(JSContext* cx, Source<Unit> chars, size_t len,
+ char functionName, const char* func) {
+ JS::CompileOptions options(cx);
+ options.setFileAndLine(func, 1);
+
+ // Evaluate the provided source text, containing a function named
+ // |functionName|.
+ JS::SourceText<Unit> sourceText;
+ if (!sourceText.init(cx, std::move(chars), len)) {
+ return nullptr;
+ }
+
+ {
+ JS::Rooted<JS::Value> dummy(cx);
+ if (!JS::Evaluate(cx, options, sourceText, &dummy)) {
+ return nullptr;
+ }
+ }
+
+ // Evaluate the name of that function.
+ JS::Rooted<JS::Value> rval(cx);
+ const char16_t name[] = {char16_t(functionName)};
+ JS::SourceText<char16_t> srcbuf;
+ if (!srcbuf.init(cx, name, std::size(name), JS::SourceOwnership::Borrowed)) {
+ return nullptr;
+ }
+ if (!JS::Evaluate(cx, options, srcbuf, &rval)) {
+ return nullptr;
+ }
+
+ // Return the function.
+ MOZ_RELEASE_ASSERT(rval.isObject());
+ return JS_ValueToFunction(cx, rval);
+}
+
+static void CompressSourceSync(JS::Handle<JSFunction*> fun, JSContext* cx) {
+ JS::Rooted<JSScript*> script(cx, JSFunction::getOrCreateScript(cx, fun));
+ MOZ_RELEASE_ASSERT(script);
+ MOZ_RELEASE_ASSERT(script->scriptSource()->hasSourceText());
+
+ MOZ_RELEASE_ASSERT(js::SynchronouslyCompressSource(cx, script));
+
+ MOZ_RELEASE_ASSERT(script->scriptSource()->hasCompressedSource());
+}
+
+static constexpr char FunctionStart[] = "function @() {";
+constexpr size_t FunctionStartLength = js_strlen(FunctionStart);
+constexpr size_t FunctionNameOffset = 9;
+
+static_assert(FunctionStart[FunctionNameOffset] == '@',
+ "offset must correctly point at the function name location");
+
+static constexpr char FunctionEnd[] = "return 42; }";
+constexpr size_t FunctionEndLength = js_strlen(FunctionEnd);
+
+template <typename Unit>
+static void WriteFunctionOfSizeAtOffset(Source<Unit>& source,
+ size_t usableSourceLen,
+ char functionName,
+ size_t functionLength, size_t offset) {
+ MOZ_RELEASE_ASSERT(functionLength >= MinimumCompressibleLength,
+ "function must be a certain size to be compressed");
+ MOZ_RELEASE_ASSERT(offset <= usableSourceLen,
+ "offset must not exceed usable source");
+ MOZ_RELEASE_ASSERT(functionLength <= usableSourceLen,
+ "function must fit in usable source");
+ MOZ_RELEASE_ASSERT(offset <= usableSourceLen - functionLength,
+ "function must not extend past usable source");
+
+ // Assigning |char| to |char16_t| is permitted, but we deliberately require a
+ // cast to assign |char| to |Utf8Unit|. |std::copy_n| would handle the first
+ // case, but the required transformation for UTF-8 demands |std::transform|.
+ auto TransformToUnit = [](char c) { return Unit(c); };
+
+ // Fill in the function start.
+ std::transform(FunctionStart, FunctionStart + FunctionStartLength,
+ &source[offset], TransformToUnit);
+ source[offset + FunctionNameOffset] = Unit(functionName);
+
+ // Fill in the function end.
+ std::transform(FunctionEnd, FunctionEnd + FunctionEndLength,
+ &source[offset + functionLength - FunctionEndLength],
+ TransformToUnit);
+}
+
+static JSString* DecompressSource(JSContext* cx, JS::Handle<JSFunction*> fun) {
+ JS::Rooted<JS::Value> fval(cx, JS::ObjectValue(*JS_GetFunctionObject(fun)));
+ return JS::ToString(cx, fval);
+}
+
+static bool IsExpectedFunctionString(JS::Handle<JSString*> str,
+ char functionName, JSContext* cx) {
+ JSLinearString* lstr = JS_EnsureLinearString(cx, str);
+ MOZ_RELEASE_ASSERT(lstr);
+
+ size_t len = JS_GetStringLength(str);
+ if (len < FunctionStartLength || len < FunctionEndLength) {
+ return false;
+ }
+
+ JS::AutoAssertNoGC nogc(cx);
+
+ auto CheckContents = [functionName, len](const auto* chars) {
+ // Check the function in parts:
+ //
+ // * "function "
+ // * "A"
+ // * "() {"
+ // * "\n...\n"
+ // * "return 42; }"
+ return std::equal(chars, chars + FunctionNameOffset, FunctionStart) &&
+ chars[FunctionNameOffset] == functionName &&
+ std::equal(chars + FunctionNameOffset + 1,
+ chars + FunctionStartLength,
+ FunctionStart + FunctionNameOffset + 1) &&
+ std::all_of(chars + FunctionStartLength,
+ chars + len - FunctionEndLength,
+ [](auto c) { return c == FillerWhitespace; }) &&
+ std::equal(chars + len - FunctionEndLength, chars + len,
+ FunctionEnd);
+ };
+
+ bool hasExpectedContents;
+ if (JS::StringHasLatin1Chars(str)) {
+ const JS::Latin1Char* chars = JS::GetLatin1LinearStringChars(nogc, lstr);
+ hasExpectedContents = CheckContents(chars);
+ } else {
+ const char16_t* chars = JS::GetTwoByteLinearStringChars(nogc, lstr);
+ hasExpectedContents = CheckContents(chars);
+ }
+
+ return hasExpectedContents;
+}
+
+BEGIN_TEST(testScriptSourceCompression_inOneChunk) {
+ CHECK(run<char16_t>());
+ CHECK(run<Utf8Unit>());
+ return true;
+}
+
+template <typename Unit>
+bool run() {
+ constexpr size_t len = MinimumCompressibleLength + 55;
+ auto source = MakeSourceAllWhitespace<Unit>(cx, len);
+ CHECK(source);
+
+ // Write out a 'b' or 'c' function that is long enough to be compressed,
+ // that starts after source start and ends before source end.
+ constexpr char FunctionName = 'a' + sizeof(Unit);
+ WriteFunctionOfSizeAtOffset(source, len, FunctionName,
+ MinimumCompressibleLength,
+ len - MinimumCompressibleLength);
+
+ JS::Rooted<JSFunction*> fun(cx);
+ fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
+ CHECK(fun);
+
+ CompressSourceSync(fun, cx);
+
+ JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
+ CHECK(str);
+ CHECK(IsExpectedFunctionString(str, FunctionName, cx));
+
+ return true;
+}
+END_TEST(testScriptSourceCompression_inOneChunk)
+
+BEGIN_TEST(testScriptSourceCompression_endsAtBoundaryInOneChunk) {
+ CHECK(run<char16_t>());
+ CHECK(run<Utf8Unit>());
+ return true;
+}
+
+template <typename Unit>
+bool run() {
+ constexpr size_t len = ChunkSize / sizeof(Unit);
+ auto source = MakeSourceAllWhitespace<Unit>(cx, len);
+ CHECK(source);
+
+ // Write out a 'd' or 'e' function that is long enough to be compressed,
+ // that (for no particular reason) starts after source start and ends
+ // before usable source end.
+ constexpr char FunctionName = 'c' + sizeof(Unit);
+ WriteFunctionOfSizeAtOffset(source, len, FunctionName,
+ MinimumCompressibleLength,
+ len - MinimumCompressibleLength);
+
+ JS::Rooted<JSFunction*> fun(cx);
+ fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
+ CHECK(fun);
+
+ CompressSourceSync(fun, cx);
+
+ JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
+ CHECK(str);
+ CHECK(IsExpectedFunctionString(str, FunctionName, cx));
+
+ return true;
+}
+END_TEST(testScriptSourceCompression_endsAtBoundaryInOneChunk)
+
+BEGIN_TEST(testScriptSourceCompression_isExactChunk) {
+ CHECK(run<char16_t>());
+ CHECK(run<Utf8Unit>());
+ return true;
+}
+
+template <typename Unit>
+bool run() {
+ constexpr size_t len = ChunkSize / sizeof(Unit);
+ auto source = MakeSourceAllWhitespace<Unit>(cx, len);
+ CHECK(source);
+
+ // Write out a 'f' or 'g' function that occupies the entire source (and
+ // entire chunk, too).
+ constexpr char FunctionName = 'e' + sizeof(Unit);
+ WriteFunctionOfSizeAtOffset(source, len, FunctionName, len, 0);
+
+ JS::Rooted<JSFunction*> fun(cx);
+ fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
+ CHECK(fun);
+
+ CompressSourceSync(fun, cx);
+
+ JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
+ CHECK(str);
+ CHECK(IsExpectedFunctionString(str, FunctionName, cx));
+
+ return true;
+}
+END_TEST(testScriptSourceCompression_isExactChunk)
+
+BEGIN_TEST(testScriptSourceCompression_crossesChunkBoundary) {
+ CHECK(run<char16_t>());
+ CHECK(run<Utf8Unit>());
+ return true;
+}
+
+template <typename Unit>
+bool run() {
+ constexpr size_t len = ChunkSize / sizeof(Unit) + 293;
+ auto source = MakeSourceAllWhitespace<Unit>(cx, len);
+ CHECK(source);
+
+ // This function crosses a chunk boundary but does not end at one.
+ constexpr size_t FunctionSize = 177 + ChunkSize / sizeof(Unit);
+
+ // Write out a 'h' or 'i' function.
+ constexpr char FunctionName = 'g' + sizeof(Unit);
+ WriteFunctionOfSizeAtOffset(source, len, FunctionName, FunctionSize, 37);
+
+ JS::Rooted<JSFunction*> fun(cx);
+ fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
+ CHECK(fun);
+
+ CompressSourceSync(fun, cx);
+
+ JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
+ CHECK(str);
+ CHECK(IsExpectedFunctionString(str, FunctionName, cx));
+
+ return true;
+}
+END_TEST(testScriptSourceCompression_crossesChunkBoundary)
+
+BEGIN_TEST(testScriptSourceCompression_crossesChunkBoundary_endsAtBoundary) {
+ CHECK(run<char16_t>());
+ CHECK(run<Utf8Unit>());
+ return true;
+}
+
+template <typename Unit>
+bool run() {
+ // Exactly two chunks.
+ constexpr size_t len = (2 * ChunkSize) / sizeof(Unit);
+ auto source = MakeSourceAllWhitespace<Unit>(cx, len);
+ CHECK(source);
+
+ // This function crosses a chunk boundary, and it ends exactly at the end
+ // of both the second chunk and the full source.
+ constexpr size_t FunctionSize = 1 + ChunkSize / sizeof(Unit);
+
+ // Write out a 'j' or 'k' function.
+ constexpr char FunctionName = 'i' + sizeof(Unit);
+ WriteFunctionOfSizeAtOffset(source, len, FunctionName, FunctionSize,
+ len - FunctionSize);
+
+ JS::Rooted<JSFunction*> fun(cx);
+ fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
+ CHECK(fun);
+
+ CompressSourceSync(fun, cx);
+
+ JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
+ CHECK(str);
+ CHECK(IsExpectedFunctionString(str, FunctionName, cx));
+
+ return true;
+}
+END_TEST(testScriptSourceCompression_crossesChunkBoundary_endsAtBoundary)
+
+BEGIN_TEST(testScriptSourceCompression_containsWholeChunk) {
+ CHECK(run<char16_t>());
+ CHECK(run<Utf8Unit>());
+ return true;
+}
+
+template <typename Unit>
+bool run() {
+ constexpr size_t len = (2 * ChunkSize) / sizeof(Unit) + 17;
+ auto source = MakeSourceAllWhitespace<Unit>(cx, len);
+ CHECK(source);
+
+ // This function crosses two chunk boundaries and begins/ends in the middle
+ // of chunk boundaries.
+ constexpr size_t FunctionSize = 2 + ChunkSize / sizeof(Unit);
+
+ // Write out a 'l' or 'm' function.
+ constexpr char FunctionName = 'k' + sizeof(Unit);
+ WriteFunctionOfSizeAtOffset(source, len, FunctionName, FunctionSize,
+ ChunkSize / sizeof(Unit) - 1);
+
+ JS::Rooted<JSFunction*> fun(cx);
+ fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
+ CHECK(fun);
+
+ CompressSourceSync(fun, cx);
+
+ JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
+ CHECK(str);
+ CHECK(IsExpectedFunctionString(str, FunctionName, cx));
+
+ return true;
+}
+END_TEST(testScriptSourceCompression_containsWholeChunk)
+
+BEGIN_TEST(testScriptSourceCompression_containsWholeChunk_endsAtBoundary) {
+ CHECK(run<char16_t>());
+ CHECK(run<Utf8Unit>());
+ return true;
+}
+
+template <typename Unit>
+bool run() {
+ // Exactly three chunks.
+ constexpr size_t len = (3 * ChunkSize) / sizeof(Unit);
+ auto source = MakeSourceAllWhitespace<Unit>(cx, len);
+ CHECK(source);
+
+ // This function crosses two chunk boundaries and ends at a chunk boundary.
+ constexpr size_t FunctionSize = 1 + (2 * ChunkSize) / sizeof(Unit);
+
+ // Write out a 'n' or 'o' function.
+ constexpr char FunctionName = 'm' + sizeof(Unit);
+ WriteFunctionOfSizeAtOffset(source, len, FunctionName, FunctionSize,
+ ChunkSize / sizeof(Unit) - 1);
+
+ JS::Rooted<JSFunction*> fun(cx);
+ fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
+ CHECK(fun);
+
+ CompressSourceSync(fun, cx);
+
+ JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
+ CHECK(str);
+ CHECK(IsExpectedFunctionString(str, FunctionName, cx));
+
+ return true;
+}
+END_TEST(testScriptSourceCompression_containsWholeChunk_endsAtBoundary)
+
+BEGIN_TEST(testScriptSourceCompression_spansMultipleMiddleChunks) {
+ CHECK(run<char16_t>());
+ CHECK(run<Utf8Unit>());
+ return true;
+}
+
+template <typename Unit>
+bool run() {
+ // Four chunks.
+ constexpr size_t len = (4 * ChunkSize) / sizeof(Unit);
+ auto source = MakeSourceAllWhitespace<Unit>(cx, len);
+ CHECK(source);
+
+ // This function spans the two middle chunks and further extends one
+ // character to each side.
+ constexpr size_t FunctionSize = 2 + (2 * ChunkSize) / sizeof(Unit);
+
+ // Write out a 'p' or 'q' function.
+ constexpr char FunctionName = 'o' + sizeof(Unit);
+ WriteFunctionOfSizeAtOffset(source, len, FunctionName, FunctionSize,
+ ChunkSize / sizeof(Unit) - 1);
+
+ JS::Rooted<JSFunction*> fun(cx);
+ fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
+ CHECK(fun);
+
+ CompressSourceSync(fun, cx);
+
+ JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
+ CHECK(str);
+ CHECK(IsExpectedFunctionString(str, FunctionName, cx));
+
+ return true;
+}
+END_TEST(testScriptSourceCompression_spansMultipleMiddleChunks)
+
+BEGIN_TEST(testScriptSourceCompression_automatic) {
+ constexpr size_t len = MinimumCompressibleLength + 55;
+ auto chars = MakeSourceAllWhitespace<char16_t>(cx, len);
+ CHECK(chars);
+
+ JS::SourceText<char16_t> source;
+ CHECK(source.init(cx, std::move(chars), len));
+
+ JS::CompileOptions options(cx);
+ JS::Rooted<JSScript*> script(cx, JS::Compile(cx, options, source));
+ CHECK(script);
+
+ // Check that source compression was triggered by the compile. If the
+ // off-thread source compression system is globally disabled, the source will
+ // remain uncompressed.
+ js::RunPendingSourceCompressions(cx->runtime());
+ bool expected = js::IsOffThreadSourceCompressionEnabled();
+ CHECK(script->scriptSource()->hasCompressedSource() == expected);
+
+ return true;
+}
+END_TEST(testScriptSourceCompression_automatic)
+
+BEGIN_TEST(testScriptSourceCompression_offThread) {
+ constexpr size_t len = MinimumCompressibleLength + 55;
+ auto chars = MakeSourceAllWhitespace<char16_t>(cx, len);
+ CHECK(chars);
+
+ JS::SourceText<char16_t> source;
+ CHECK(source.init(cx, std::move(chars), len));
+
+ js::Monitor monitor MOZ_UNANNOTATED(js::mutexid::ShellOffThreadState);
+ JS::CompileOptions options(cx);
+ JS::OffThreadToken* token;
+
+ // Force off-thread even though if this is a small file.
+ options.forceAsync = true;
+
+ CHECK(token = JS::CompileToStencilOffThread(cx, options, source, callback,
+ &monitor));
+
+ {
+ // Finish any active GC in case it is blocking off-thread work.
+ js::gc::FinishGC(cx);
+
+ js::AutoLockMonitor lock(monitor);
+ lock.wait();
+ }
+
+ RefPtr<JS::Stencil> stencil = JS::FinishOffThreadStencil(cx, token);
+ CHECK(stencil);
+ JS::InstantiateOptions instantiateOptions(options);
+ JS::Rooted<JSScript*> script(
+ cx, JS::InstantiateGlobalStencil(cx, instantiateOptions, stencil));
+ CHECK(script);
+
+ // Check that source compression was triggered by the compile. If the
+ // off-thread source compression system is globally disabled, the source will
+ // remain uncompressed.
+ js::RunPendingSourceCompressions(cx->runtime());
+ bool expected = js::IsOffThreadSourceCompressionEnabled();
+ CHECK(script->scriptSource()->hasCompressedSource() == expected);
+
+ return true;
+}
+
+static void callback(JS::OffThreadToken* token, void* context) {
+ js::Monitor& monitor = *static_cast<js::Monitor*>(context);
+
+ js::AutoLockMonitor lock(monitor);
+ lock.notify();
+}
+
+END_TEST(testScriptSourceCompression_offThread)