diff options
Diffstat (limited to 'js/src/jsapi-tests/testScriptSourceCompression.cpp')
-rw-r--r-- | js/src/jsapi-tests/testScriptSourceCompression.cpp | 544 |
1 files changed, 544 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..a485ac41c6 --- /dev/null +++ b/js/src/jsapi-tests/testScriptSourceCompression.cpp @@ -0,0 +1,544 @@ +/* -*- 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/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 +#include "js/Conversions.h" // JS::ToString +#include "js/MemoryFunctions.h" // JS_malloc +#include "js/OffThreadScriptCompilation.h" // JS::CompileOffThread, JS::OffThreadToken, JS::FinishOffThreadScript +#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/HelperThreadState.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(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::CompileOffThread(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(); + } + + JS::Rooted<JSScript*> script(cx, JS::FinishOffThreadScript(cx, token)); + 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) |