tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

testScriptSourceCompression.cpp (17164B)


      1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
      2 * vim: set ts=8 sts=4 et sw=4 tw=99:
      3 */
      4 /* This Source Code Form is subject to the terms of the Mozilla Public
      5 * License, v. 2.0. If a copy of the MPL was not distributed with this
      6 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      7 
      8 #include "mozilla/Assertions.h"  // MOZ_RELEASE_ASSERT
      9 #include "mozilla/RefPtr.h"      // RefPtr
     10 #include "mozilla/Utf8.h"        // mozilla::Utf8Unit
     11 
     12 #include <algorithm>  // std::all_of, std::equal, std::move, std::transform
     13 #include <memory>     // std::uninitialized_fill_n
     14 #include <stddef.h>   // size_t
     15 #include <stdint.h>   // uint32_t
     16 
     17 #include "jsapi.h"  // JS_EnsureLinearString, JS_GC, JS_Get{Latin1,TwoByte}LinearStringChars, JS_GetStringLength, JS_ValueToFunction
     18 #include "jstypes.h"  // JS_PUBLIC_API
     19 
     20 #include "gc/GC.h"                        // js::gc::FinishGC
     21 #include "js/CompilationAndEvaluation.h"  // JS::Evaluate
     22 #include "js/CompileOptions.h"  // JS::CompileOptions, JS::InstantiateOptions
     23 #include "js/Conversions.h"     // JS::ToString
     24 #include "js/experimental/JSStencil.h"  // JS::Stencil, JS::InstantiateGlobalStencil
     25 #include "js/MemoryFunctions.h"         // JS_malloc
     26 #include "js/RootingAPI.h"              // JS::MutableHandle, JS::Rooted
     27 #include "js/SourceText.h"              // JS::SourceOwnership, JS::SourceText
     28 #include "js/String.h"  // JS::GetLatin1LinearStringChars, JS::GetTwoByteLinearStringChars, JS::StringHasLatin1Chars
     29 #include "js/UniquePtr.h"  // js::UniquePtr
     30 #include "js/Utility.h"    // JS::FreePolicy
     31 #include "js/Value.h"      // JS::NullValue, JS::ObjectValue, JS::Value
     32 #include "jsapi-tests/tests.h"
     33 #include "util/Text.h"         // js_strlen
     34 #include "vm/Compression.h"    // js::Compressor::CHUNK_SIZE
     35 #include "vm/HelperThreads.h"  // js::RunPendingSourceCompressions
     36 #include "vm/JSFunction.h"     // JSFunction::getOrCreateScript
     37 #include "vm/JSScript.h"  // JSScript, js::ScriptSource::MinimumCompressibleLength, js::SynchronouslyCompressSource
     38 #include "vm/Monitor.h"   // js::Monitor, js::AutoLockMonitor
     39 
     40 using mozilla::Utf8Unit;
     41 
     42 struct JS_PUBLIC_API JSContext;
     43 class JS_PUBLIC_API JSString;
     44 
     45 template <typename Unit>
     46 using Source = js::UniquePtr<Unit[], JS::FreePolicy>;
     47 
     48 constexpr size_t ChunkSize = js::Compressor::CHUNK_SIZE;
     49 constexpr size_t MinimumCompressibleLength =
     50    js::ScriptSource::MinimumCompressibleLength;
     51 
     52 // Don't use ' ' to spread stuff across lines.
     53 constexpr char FillerWhitespace = '\n';
     54 
     55 template <typename Unit>
     56 static Source<Unit> MakeSourceAllWhitespace(JSContext* cx, size_t len) {
     57  static_assert(ChunkSize % sizeof(Unit) == 0,
     58                "chunk size presumed to be a multiple of char size");
     59 
     60  Source<Unit> source(
     61      reinterpret_cast<Unit*>(JS_malloc(cx, len * sizeof(Unit))));
     62  if (source) {
     63    std::uninitialized_fill_n(source.get(), len, FillerWhitespace);
     64  }
     65  return source;
     66 }
     67 
     68 template <typename Unit>
     69 static JSFunction* EvaluateChars(JSContext* cx, Source<Unit> chars, size_t len,
     70                                 char functionName, const char* func) {
     71  JS::CompileOptions options(cx);
     72  options.setFileAndLine(func, 1);
     73 
     74  // Evaluate the provided source text, containing a function named
     75  // |functionName|.
     76  JS::SourceText<Unit> sourceText;
     77  if (!sourceText.init(cx, std::move(chars), len)) {
     78    return nullptr;
     79  }
     80 
     81  {
     82    JS::Rooted<JS::Value> dummy(cx);
     83    if (!JS::Evaluate(cx, options, sourceText, &dummy)) {
     84      return nullptr;
     85    }
     86  }
     87 
     88  // Evaluate the name of that function.
     89  JS::Rooted<JS::Value> rval(cx);
     90  const char16_t name[] = {char16_t(functionName)};
     91  JS::SourceText<char16_t> srcbuf;
     92  if (!srcbuf.init(cx, name, std::size(name), JS::SourceOwnership::Borrowed)) {
     93    return nullptr;
     94  }
     95  if (!JS::Evaluate(cx, options, srcbuf, &rval)) {
     96    return nullptr;
     97  }
     98 
     99  // Return the function.
    100  MOZ_RELEASE_ASSERT(rval.isObject());
    101  return JS_ValueToFunction(cx, rval);
    102 }
    103 
    104 static void CompressSourceSync(JS::Handle<JSFunction*> fun, JSContext* cx) {
    105  JS::Rooted<JSScript*> script(cx, JSFunction::getOrCreateScript(cx, fun));
    106  MOZ_RELEASE_ASSERT(script);
    107  MOZ_RELEASE_ASSERT(script->scriptSource()->hasSourceText());
    108 
    109  MOZ_RELEASE_ASSERT(js::SynchronouslyCompressSource(cx, script));
    110 
    111  MOZ_RELEASE_ASSERT(script->scriptSource()->hasCompressedSource());
    112 }
    113 
    114 static constexpr char FunctionStart[] = "function @() {";
    115 constexpr size_t FunctionStartLength = js_strlen(FunctionStart);
    116 constexpr size_t FunctionNameOffset = 9;
    117 
    118 static_assert(FunctionStart[FunctionNameOffset] == '@',
    119              "offset must correctly point at the function name location");
    120 
    121 static constexpr char FunctionEnd[] = "return 42; }";
    122 constexpr size_t FunctionEndLength = js_strlen(FunctionEnd);
    123 
    124 template <typename Unit>
    125 static void WriteFunctionOfSizeAtOffset(Source<Unit>& source,
    126                                        size_t usableSourceLen,
    127                                        char functionName,
    128                                        size_t functionLength, size_t offset) {
    129  MOZ_RELEASE_ASSERT(functionLength >= MinimumCompressibleLength,
    130                     "function must be a certain size to be compressed");
    131  MOZ_RELEASE_ASSERT(offset <= usableSourceLen,
    132                     "offset must not exceed usable source");
    133  MOZ_RELEASE_ASSERT(functionLength <= usableSourceLen,
    134                     "function must fit in usable source");
    135  MOZ_RELEASE_ASSERT(offset <= usableSourceLen - functionLength,
    136                     "function must not extend past usable source");
    137 
    138  // Assigning |char| to |char16_t| is permitted, but we deliberately require a
    139  // cast to assign |char| to |Utf8Unit|.  |std::copy_n| would handle the first
    140  // case, but the required transformation for UTF-8 demands |std::transform|.
    141  auto TransformToUnit = [](char c) { return Unit(c); };
    142 
    143  // Fill in the function start.
    144  std::transform(FunctionStart, FunctionStart + FunctionStartLength,
    145                 &source[offset], TransformToUnit);
    146  source[offset + FunctionNameOffset] = Unit(functionName);
    147 
    148  // Fill in the function end.
    149  std::transform(FunctionEnd, FunctionEnd + FunctionEndLength,
    150                 &source[offset + functionLength - FunctionEndLength],
    151                 TransformToUnit);
    152 }
    153 
    154 static JSString* DecompressSource(JSContext* cx, JS::Handle<JSFunction*> fun) {
    155  JS::Rooted<JS::Value> fval(cx, JS::ObjectValue(*JS_GetFunctionObject(fun)));
    156  return JS::ToString(cx, fval);
    157 }
    158 
    159 static bool IsExpectedFunctionString(JS::Handle<JSString*> str,
    160                                     char functionName, JSContext* cx) {
    161  JSLinearString* lstr = JS_EnsureLinearString(cx, str);
    162  MOZ_RELEASE_ASSERT(lstr);
    163 
    164  size_t len = JS_GetStringLength(str);
    165  if (len < FunctionStartLength || len < FunctionEndLength) {
    166    return false;
    167  }
    168 
    169  JS::AutoAssertNoGC nogc(cx);
    170 
    171  auto CheckContents = [functionName, len](const auto* chars) {
    172    // Check the function in parts:
    173    //
    174    //   * "function "
    175    //   * "A"
    176    //   * "() {"
    177    //   * "\n...\n"
    178    //   * "return 42; }"
    179    return std::equal(chars, chars + FunctionNameOffset, FunctionStart) &&
    180           chars[FunctionNameOffset] == functionName &&
    181           std::equal(chars + FunctionNameOffset + 1,
    182                      chars + FunctionStartLength,
    183                      FunctionStart + FunctionNameOffset + 1) &&
    184           std::all_of(chars + FunctionStartLength,
    185                       chars + len - FunctionEndLength,
    186                       [](auto c) { return c == FillerWhitespace; }) &&
    187           std::equal(chars + len - FunctionEndLength, chars + len,
    188                      FunctionEnd);
    189  };
    190 
    191  bool hasExpectedContents;
    192  if (JS::StringHasLatin1Chars(str)) {
    193    const JS::Latin1Char* chars = JS::GetLatin1LinearStringChars(nogc, lstr);
    194    hasExpectedContents = CheckContents(chars);
    195  } else {
    196    const char16_t* chars = JS::GetTwoByteLinearStringChars(nogc, lstr);
    197    hasExpectedContents = CheckContents(chars);
    198  }
    199 
    200  return hasExpectedContents;
    201 }
    202 
    203 BEGIN_TEST(testScriptSourceCompression_inOneChunk) {
    204  CHECK(run<char16_t>());
    205  CHECK(run<Utf8Unit>());
    206  return true;
    207 }
    208 
    209 template <typename Unit>
    210 bool run() {
    211  constexpr size_t len = MinimumCompressibleLength + 55;
    212  auto source = MakeSourceAllWhitespace<Unit>(cx, len);
    213  CHECK(source);
    214 
    215  // Write out a 'b' or 'c' function that is long enough to be compressed,
    216  // that starts after source start and ends before source end.
    217  constexpr char FunctionName = 'a' + sizeof(Unit);
    218  WriteFunctionOfSizeAtOffset(source, len, FunctionName,
    219                              MinimumCompressibleLength,
    220                              len - MinimumCompressibleLength);
    221 
    222  JS::Rooted<JSFunction*> fun(cx);
    223  fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
    224  CHECK(fun);
    225 
    226  CompressSourceSync(fun, cx);
    227 
    228  JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
    229  CHECK(str);
    230  CHECK(IsExpectedFunctionString(str, FunctionName, cx));
    231 
    232  return true;
    233 }
    234 END_TEST(testScriptSourceCompression_inOneChunk)
    235 
    236 BEGIN_TEST(testScriptSourceCompression_endsAtBoundaryInOneChunk) {
    237  CHECK(run<char16_t>());
    238  CHECK(run<Utf8Unit>());
    239  return true;
    240 }
    241 
    242 template <typename Unit>
    243 bool run() {
    244  constexpr size_t len = ChunkSize / sizeof(Unit);
    245  auto source = MakeSourceAllWhitespace<Unit>(cx, len);
    246  CHECK(source);
    247 
    248  // Write out a 'd' or 'e' function that is long enough to be compressed,
    249  // that (for no particular reason) starts after source start and ends
    250  // before usable source end.
    251  constexpr char FunctionName = 'c' + sizeof(Unit);
    252  WriteFunctionOfSizeAtOffset(source, len, FunctionName,
    253                              MinimumCompressibleLength,
    254                              len - MinimumCompressibleLength);
    255 
    256  JS::Rooted<JSFunction*> fun(cx);
    257  fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
    258  CHECK(fun);
    259 
    260  CompressSourceSync(fun, cx);
    261 
    262  JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
    263  CHECK(str);
    264  CHECK(IsExpectedFunctionString(str, FunctionName, cx));
    265 
    266  return true;
    267 }
    268 END_TEST(testScriptSourceCompression_endsAtBoundaryInOneChunk)
    269 
    270 BEGIN_TEST(testScriptSourceCompression_isExactChunk) {
    271  CHECK(run<char16_t>());
    272  CHECK(run<Utf8Unit>());
    273  return true;
    274 }
    275 
    276 template <typename Unit>
    277 bool run() {
    278  constexpr size_t len = ChunkSize / sizeof(Unit);
    279  auto source = MakeSourceAllWhitespace<Unit>(cx, len);
    280  CHECK(source);
    281 
    282  // Write out a 'f' or 'g' function that occupies the entire source (and
    283  // entire chunk, too).
    284  constexpr char FunctionName = 'e' + sizeof(Unit);
    285  WriteFunctionOfSizeAtOffset(source, len, FunctionName, len, 0);
    286 
    287  JS::Rooted<JSFunction*> fun(cx);
    288  fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
    289  CHECK(fun);
    290 
    291  CompressSourceSync(fun, cx);
    292 
    293  JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
    294  CHECK(str);
    295  CHECK(IsExpectedFunctionString(str, FunctionName, cx));
    296 
    297  return true;
    298 }
    299 END_TEST(testScriptSourceCompression_isExactChunk)
    300 
    301 BEGIN_TEST(testScriptSourceCompression_crossesChunkBoundary) {
    302  CHECK(run<char16_t>());
    303  CHECK(run<Utf8Unit>());
    304  return true;
    305 }
    306 
    307 template <typename Unit>
    308 bool run() {
    309  constexpr size_t len = ChunkSize / sizeof(Unit) + 293;
    310  auto source = MakeSourceAllWhitespace<Unit>(cx, len);
    311  CHECK(source);
    312 
    313  // This function crosses a chunk boundary but does not end at one.
    314  constexpr size_t FunctionSize = 177 + ChunkSize / sizeof(Unit);
    315 
    316  // Write out a 'h' or 'i' function.
    317  constexpr char FunctionName = 'g' + sizeof(Unit);
    318  WriteFunctionOfSizeAtOffset(source, len, FunctionName, FunctionSize, 37);
    319 
    320  JS::Rooted<JSFunction*> fun(cx);
    321  fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
    322  CHECK(fun);
    323 
    324  CompressSourceSync(fun, cx);
    325 
    326  JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
    327  CHECK(str);
    328  CHECK(IsExpectedFunctionString(str, FunctionName, cx));
    329 
    330  return true;
    331 }
    332 END_TEST(testScriptSourceCompression_crossesChunkBoundary)
    333 
    334 BEGIN_TEST(testScriptSourceCompression_crossesChunkBoundary_endsAtBoundary) {
    335  CHECK(run<char16_t>());
    336  CHECK(run<Utf8Unit>());
    337  return true;
    338 }
    339 
    340 template <typename Unit>
    341 bool run() {
    342  // Exactly two chunks.
    343  constexpr size_t len = (2 * ChunkSize) / sizeof(Unit);
    344  auto source = MakeSourceAllWhitespace<Unit>(cx, len);
    345  CHECK(source);
    346 
    347  // This function crosses a chunk boundary, and it ends exactly at the end
    348  // of both the second chunk and the full source.
    349  constexpr size_t FunctionSize = 1 + ChunkSize / sizeof(Unit);
    350 
    351  // Write out a 'j' or 'k' function.
    352  constexpr char FunctionName = 'i' + sizeof(Unit);
    353  WriteFunctionOfSizeAtOffset(source, len, FunctionName, FunctionSize,
    354                              len - FunctionSize);
    355 
    356  JS::Rooted<JSFunction*> fun(cx);
    357  fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
    358  CHECK(fun);
    359 
    360  CompressSourceSync(fun, cx);
    361 
    362  JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
    363  CHECK(str);
    364  CHECK(IsExpectedFunctionString(str, FunctionName, cx));
    365 
    366  return true;
    367 }
    368 END_TEST(testScriptSourceCompression_crossesChunkBoundary_endsAtBoundary)
    369 
    370 BEGIN_TEST(testScriptSourceCompression_containsWholeChunk) {
    371  CHECK(run<char16_t>());
    372  CHECK(run<Utf8Unit>());
    373  return true;
    374 }
    375 
    376 template <typename Unit>
    377 bool run() {
    378  constexpr size_t len = (2 * ChunkSize) / sizeof(Unit) + 17;
    379  auto source = MakeSourceAllWhitespace<Unit>(cx, len);
    380  CHECK(source);
    381 
    382  // This function crosses two chunk boundaries and begins/ends in the middle
    383  // of chunk boundaries.
    384  constexpr size_t FunctionSize = 2 + ChunkSize / sizeof(Unit);
    385 
    386  // Write out a 'l' or 'm' function.
    387  constexpr char FunctionName = 'k' + sizeof(Unit);
    388  WriteFunctionOfSizeAtOffset(source, len, FunctionName, FunctionSize,
    389                              ChunkSize / sizeof(Unit) - 1);
    390 
    391  JS::Rooted<JSFunction*> fun(cx);
    392  fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
    393  CHECK(fun);
    394 
    395  CompressSourceSync(fun, cx);
    396 
    397  JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
    398  CHECK(str);
    399  CHECK(IsExpectedFunctionString(str, FunctionName, cx));
    400 
    401  return true;
    402 }
    403 END_TEST(testScriptSourceCompression_containsWholeChunk)
    404 
    405 BEGIN_TEST(testScriptSourceCompression_containsWholeChunk_endsAtBoundary) {
    406  CHECK(run<char16_t>());
    407  CHECK(run<Utf8Unit>());
    408  return true;
    409 }
    410 
    411 template <typename Unit>
    412 bool run() {
    413  // Exactly three chunks.
    414  constexpr size_t len = (3 * ChunkSize) / sizeof(Unit);
    415  auto source = MakeSourceAllWhitespace<Unit>(cx, len);
    416  CHECK(source);
    417 
    418  // This function crosses two chunk boundaries and ends at a chunk boundary.
    419  constexpr size_t FunctionSize = 1 + (2 * ChunkSize) / sizeof(Unit);
    420 
    421  // Write out a 'n' or 'o' function.
    422  constexpr char FunctionName = 'm' + sizeof(Unit);
    423  WriteFunctionOfSizeAtOffset(source, len, FunctionName, FunctionSize,
    424                              ChunkSize / sizeof(Unit) - 1);
    425 
    426  JS::Rooted<JSFunction*> fun(cx);
    427  fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
    428  CHECK(fun);
    429 
    430  CompressSourceSync(fun, cx);
    431 
    432  JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
    433  CHECK(str);
    434  CHECK(IsExpectedFunctionString(str, FunctionName, cx));
    435 
    436  return true;
    437 }
    438 END_TEST(testScriptSourceCompression_containsWholeChunk_endsAtBoundary)
    439 
    440 BEGIN_TEST(testScriptSourceCompression_spansMultipleMiddleChunks) {
    441  CHECK(run<char16_t>());
    442  CHECK(run<Utf8Unit>());
    443  return true;
    444 }
    445 
    446 template <typename Unit>
    447 bool run() {
    448  // Four chunks.
    449  constexpr size_t len = (4 * ChunkSize) / sizeof(Unit);
    450  auto source = MakeSourceAllWhitespace<Unit>(cx, len);
    451  CHECK(source);
    452 
    453  // This function spans the two middle chunks and further extends one
    454  // character to each side.
    455  constexpr size_t FunctionSize = 2 + (2 * ChunkSize) / sizeof(Unit);
    456 
    457  // Write out a 'p' or 'q' function.
    458  constexpr char FunctionName = 'o' + sizeof(Unit);
    459  WriteFunctionOfSizeAtOffset(source, len, FunctionName, FunctionSize,
    460                              ChunkSize / sizeof(Unit) - 1);
    461 
    462  JS::Rooted<JSFunction*> fun(cx);
    463  fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
    464  CHECK(fun);
    465 
    466  CompressSourceSync(fun, cx);
    467 
    468  JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
    469  CHECK(str);
    470  CHECK(IsExpectedFunctionString(str, FunctionName, cx));
    471 
    472  return true;
    473 }
    474 END_TEST(testScriptSourceCompression_spansMultipleMiddleChunks)
    475 
    476 BEGIN_TEST(testScriptSourceCompression_automatic) {
    477  constexpr size_t len = MinimumCompressibleLength + 55;
    478  auto chars = MakeSourceAllWhitespace<char16_t>(cx, len);
    479  CHECK(chars);
    480 
    481  JS::SourceText<char16_t> source;
    482  CHECK(source.init(cx, std::move(chars), len));
    483 
    484  JS::CompileOptions options(cx);
    485  JS::Rooted<JSScript*> script(cx, JS::Compile(cx, options, source));
    486  CHECK(script);
    487 
    488  // Check that source compression was triggered by the compile. If the
    489  // off-thread source compression system is globally disabled, the source will
    490  // remain uncompressed.
    491  js::RunPendingSourceCompressions(cx->runtime());
    492  bool expected = js::IsOffThreadSourceCompressionEnabled();
    493  CHECK(script->scriptSource()->hasCompressedSource() == expected);
    494 
    495  return true;
    496 }
    497 END_TEST(testScriptSourceCompression_automatic)