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)