Bug 1852740: add tests for the `fetchpriority` attribute in Link headers. r=necko...
[gecko.git] / js / src / jsapi-tests / testScriptSourceCompression.cpp
blob479a60b827cdbdec3a95e5970e48d10c4e2240a7
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/. */
8 #include "mozilla/Assertions.h" // MOZ_RELEASE_ASSERT
9 #include "mozilla/RefPtr.h" // RefPtr
10 #include "mozilla/Utf8.h" // mozilla::Utf8Unit
12 #include <algorithm> // std::all_of, std::equal, std::move, std::transform
13 #include <iterator> // std::size
14 #include <memory> // std::uninitialized_fill_n
15 #include <stddef.h> // size_t
16 #include <stdint.h> // uint32_t
18 #include "jsapi.h" // JS_EnsureLinearString, JS_GC, JS_Get{Latin1,TwoByte}LinearStringChars, JS_GetStringLength, JS_ValueToFunction
19 #include "jstypes.h" // JS_PUBLIC_API
21 #include "gc/GC.h" // js::gc::FinishGC
22 #include "js/CompilationAndEvaluation.h" // JS::Evaluate
23 #include "js/CompileOptions.h" // JS::CompileOptions, JS::InstantiateOptions
24 #include "js/Conversions.h" // JS::ToString
25 #include "js/experimental/JSStencil.h" // JS::Stencil, JS::InstantiateGlobalStencil
26 #include "js/MemoryFunctions.h" // JS_malloc
27 #include "js/RootingAPI.h" // JS::MutableHandle, JS::Rooted
28 #include "js/SourceText.h" // JS::SourceOwnership, JS::SourceText
29 #include "js/String.h" // JS::GetLatin1LinearStringChars, JS::GetTwoByteLinearStringChars, JS::StringHasLatin1Chars
30 #include "js/UniquePtr.h" // js::UniquePtr
31 #include "js/Utility.h" // JS::FreePolicy
32 #include "js/Value.h" // JS::NullValue, JS::ObjectValue, JS::Value
33 #include "jsapi-tests/tests.h"
34 #include "util/Text.h" // js_strlen
35 #include "vm/Compression.h" // js::Compressor::CHUNK_SIZE
36 #include "vm/HelperThreads.h" // js::RunPendingSourceCompressions
37 #include "vm/JSFunction.h" // JSFunction::getOrCreateScript
38 #include "vm/JSScript.h" // JSScript, js::ScriptSource::MinimumCompressibleLength, js::SynchronouslyCompressSource
39 #include "vm/Monitor.h" // js::Monitor, js::AutoLockMonitor
41 using mozilla::Utf8Unit;
43 struct JS_PUBLIC_API JSContext;
44 class JS_PUBLIC_API JSString;
46 template <typename Unit>
47 using Source = js::UniquePtr<Unit[], JS::FreePolicy>;
49 constexpr size_t ChunkSize = js::Compressor::CHUNK_SIZE;
50 constexpr size_t MinimumCompressibleLength =
51 js::ScriptSource::MinimumCompressibleLength;
53 // Don't use ' ' to spread stuff across lines.
54 constexpr char FillerWhitespace = '\n';
56 template <typename Unit>
57 static Source<Unit> MakeSourceAllWhitespace(JSContext* cx, size_t len) {
58 static_assert(ChunkSize % sizeof(Unit) == 0,
59 "chunk size presumed to be a multiple of char size");
61 Source<Unit> source(
62 reinterpret_cast<Unit*>(JS_malloc(cx, len * sizeof(Unit))));
63 if (source) {
64 std::uninitialized_fill_n(source.get(), len, FillerWhitespace);
66 return source;
69 template <typename Unit>
70 static JSFunction* EvaluateChars(JSContext* cx, Source<Unit> chars, size_t len,
71 char functionName, const char* func) {
72 JS::CompileOptions options(cx);
73 options.setFileAndLine(func, 1);
75 // Evaluate the provided source text, containing a function named
76 // |functionName|.
77 JS::SourceText<Unit> sourceText;
78 if (!sourceText.init(cx, std::move(chars), len)) {
79 return nullptr;
83 JS::Rooted<JS::Value> dummy(cx);
84 if (!JS::Evaluate(cx, options, sourceText, &dummy)) {
85 return nullptr;
89 // Evaluate the name of that function.
90 JS::Rooted<JS::Value> rval(cx);
91 const char16_t name[] = {char16_t(functionName)};
92 JS::SourceText<char16_t> srcbuf;
93 if (!srcbuf.init(cx, name, std::size(name), JS::SourceOwnership::Borrowed)) {
94 return nullptr;
96 if (!JS::Evaluate(cx, options, srcbuf, &rval)) {
97 return nullptr;
100 // Return the function.
101 MOZ_RELEASE_ASSERT(rval.isObject());
102 return JS_ValueToFunction(cx, rval);
105 static void CompressSourceSync(JS::Handle<JSFunction*> fun, JSContext* cx) {
106 JS::Rooted<JSScript*> script(cx, JSFunction::getOrCreateScript(cx, fun));
107 MOZ_RELEASE_ASSERT(script);
108 MOZ_RELEASE_ASSERT(script->scriptSource()->hasSourceText());
110 MOZ_RELEASE_ASSERT(js::SynchronouslyCompressSource(cx, script));
112 MOZ_RELEASE_ASSERT(script->scriptSource()->hasCompressedSource());
115 static constexpr char FunctionStart[] = "function @() {";
116 constexpr size_t FunctionStartLength = js_strlen(FunctionStart);
117 constexpr size_t FunctionNameOffset = 9;
119 static_assert(FunctionStart[FunctionNameOffset] == '@',
120 "offset must correctly point at the function name location");
122 static constexpr char FunctionEnd[] = "return 42; }";
123 constexpr size_t FunctionEndLength = js_strlen(FunctionEnd);
125 template <typename Unit>
126 static void WriteFunctionOfSizeAtOffset(Source<Unit>& source,
127 size_t usableSourceLen,
128 char functionName,
129 size_t functionLength, size_t offset) {
130 MOZ_RELEASE_ASSERT(functionLength >= MinimumCompressibleLength,
131 "function must be a certain size to be compressed");
132 MOZ_RELEASE_ASSERT(offset <= usableSourceLen,
133 "offset must not exceed usable source");
134 MOZ_RELEASE_ASSERT(functionLength <= usableSourceLen,
135 "function must fit in usable source");
136 MOZ_RELEASE_ASSERT(offset <= usableSourceLen - functionLength,
137 "function must not extend past usable source");
139 // Assigning |char| to |char16_t| is permitted, but we deliberately require a
140 // cast to assign |char| to |Utf8Unit|. |std::copy_n| would handle the first
141 // case, but the required transformation for UTF-8 demands |std::transform|.
142 auto TransformToUnit = [](char c) { return Unit(c); };
144 // Fill in the function start.
145 std::transform(FunctionStart, FunctionStart + FunctionStartLength,
146 &source[offset], TransformToUnit);
147 source[offset + FunctionNameOffset] = Unit(functionName);
149 // Fill in the function end.
150 std::transform(FunctionEnd, FunctionEnd + FunctionEndLength,
151 &source[offset + functionLength - FunctionEndLength],
152 TransformToUnit);
155 static JSString* DecompressSource(JSContext* cx, JS::Handle<JSFunction*> fun) {
156 JS::Rooted<JS::Value> fval(cx, JS::ObjectValue(*JS_GetFunctionObject(fun)));
157 return JS::ToString(cx, fval);
160 static bool IsExpectedFunctionString(JS::Handle<JSString*> str,
161 char functionName, JSContext* cx) {
162 JSLinearString* lstr = JS_EnsureLinearString(cx, str);
163 MOZ_RELEASE_ASSERT(lstr);
165 size_t len = JS_GetStringLength(str);
166 if (len < FunctionStartLength || len < FunctionEndLength) {
167 return false;
170 JS::AutoAssertNoGC nogc(cx);
172 auto CheckContents = [functionName, len](const auto* chars) {
173 // Check the function in parts:
175 // * "function "
176 // * "A"
177 // * "() {"
178 // * "\n...\n"
179 // * "return 42; }"
180 return std::equal(chars, chars + FunctionNameOffset, FunctionStart) &&
181 chars[FunctionNameOffset] == functionName &&
182 std::equal(chars + FunctionNameOffset + 1,
183 chars + FunctionStartLength,
184 FunctionStart + FunctionNameOffset + 1) &&
185 std::all_of(chars + FunctionStartLength,
186 chars + len - FunctionEndLength,
187 [](auto c) { return c == FillerWhitespace; }) &&
188 std::equal(chars + len - FunctionEndLength, chars + len,
189 FunctionEnd);
192 bool hasExpectedContents;
193 if (JS::StringHasLatin1Chars(str)) {
194 const JS::Latin1Char* chars = JS::GetLatin1LinearStringChars(nogc, lstr);
195 hasExpectedContents = CheckContents(chars);
196 } else {
197 const char16_t* chars = JS::GetTwoByteLinearStringChars(nogc, lstr);
198 hasExpectedContents = CheckContents(chars);
201 return hasExpectedContents;
204 BEGIN_TEST(testScriptSourceCompression_inOneChunk) {
205 CHECK(run<char16_t>());
206 CHECK(run<Utf8Unit>());
207 return true;
210 template <typename Unit>
211 bool run() {
212 constexpr size_t len = MinimumCompressibleLength + 55;
213 auto source = MakeSourceAllWhitespace<Unit>(cx, len);
214 CHECK(source);
216 // Write out a 'b' or 'c' function that is long enough to be compressed,
217 // that starts after source start and ends before source end.
218 constexpr char FunctionName = 'a' + sizeof(Unit);
219 WriteFunctionOfSizeAtOffset(source, len, FunctionName,
220 MinimumCompressibleLength,
221 len - MinimumCompressibleLength);
223 JS::Rooted<JSFunction*> fun(cx);
224 fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
225 CHECK(fun);
227 CompressSourceSync(fun, cx);
229 JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
230 CHECK(str);
231 CHECK(IsExpectedFunctionString(str, FunctionName, cx));
233 return true;
235 END_TEST(testScriptSourceCompression_inOneChunk)
237 BEGIN_TEST(testScriptSourceCompression_endsAtBoundaryInOneChunk) {
238 CHECK(run<char16_t>());
239 CHECK(run<Utf8Unit>());
240 return true;
243 template <typename Unit>
244 bool run() {
245 constexpr size_t len = ChunkSize / sizeof(Unit);
246 auto source = MakeSourceAllWhitespace<Unit>(cx, len);
247 CHECK(source);
249 // Write out a 'd' or 'e' function that is long enough to be compressed,
250 // that (for no particular reason) starts after source start and ends
251 // before usable source end.
252 constexpr char FunctionName = 'c' + sizeof(Unit);
253 WriteFunctionOfSizeAtOffset(source, len, FunctionName,
254 MinimumCompressibleLength,
255 len - MinimumCompressibleLength);
257 JS::Rooted<JSFunction*> fun(cx);
258 fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
259 CHECK(fun);
261 CompressSourceSync(fun, cx);
263 JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
264 CHECK(str);
265 CHECK(IsExpectedFunctionString(str, FunctionName, cx));
267 return true;
269 END_TEST(testScriptSourceCompression_endsAtBoundaryInOneChunk)
271 BEGIN_TEST(testScriptSourceCompression_isExactChunk) {
272 CHECK(run<char16_t>());
273 CHECK(run<Utf8Unit>());
274 return true;
277 template <typename Unit>
278 bool run() {
279 constexpr size_t len = ChunkSize / sizeof(Unit);
280 auto source = MakeSourceAllWhitespace<Unit>(cx, len);
281 CHECK(source);
283 // Write out a 'f' or 'g' function that occupies the entire source (and
284 // entire chunk, too).
285 constexpr char FunctionName = 'e' + sizeof(Unit);
286 WriteFunctionOfSizeAtOffset(source, len, FunctionName, len, 0);
288 JS::Rooted<JSFunction*> fun(cx);
289 fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
290 CHECK(fun);
292 CompressSourceSync(fun, cx);
294 JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
295 CHECK(str);
296 CHECK(IsExpectedFunctionString(str, FunctionName, cx));
298 return true;
300 END_TEST(testScriptSourceCompression_isExactChunk)
302 BEGIN_TEST(testScriptSourceCompression_crossesChunkBoundary) {
303 CHECK(run<char16_t>());
304 CHECK(run<Utf8Unit>());
305 return true;
308 template <typename Unit>
309 bool run() {
310 constexpr size_t len = ChunkSize / sizeof(Unit) + 293;
311 auto source = MakeSourceAllWhitespace<Unit>(cx, len);
312 CHECK(source);
314 // This function crosses a chunk boundary but does not end at one.
315 constexpr size_t FunctionSize = 177 + ChunkSize / sizeof(Unit);
317 // Write out a 'h' or 'i' function.
318 constexpr char FunctionName = 'g' + sizeof(Unit);
319 WriteFunctionOfSizeAtOffset(source, len, FunctionName, FunctionSize, 37);
321 JS::Rooted<JSFunction*> fun(cx);
322 fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
323 CHECK(fun);
325 CompressSourceSync(fun, cx);
327 JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
328 CHECK(str);
329 CHECK(IsExpectedFunctionString(str, FunctionName, cx));
331 return true;
333 END_TEST(testScriptSourceCompression_crossesChunkBoundary)
335 BEGIN_TEST(testScriptSourceCompression_crossesChunkBoundary_endsAtBoundary) {
336 CHECK(run<char16_t>());
337 CHECK(run<Utf8Unit>());
338 return true;
341 template <typename Unit>
342 bool run() {
343 // Exactly two chunks.
344 constexpr size_t len = (2 * ChunkSize) / sizeof(Unit);
345 auto source = MakeSourceAllWhitespace<Unit>(cx, len);
346 CHECK(source);
348 // This function crosses a chunk boundary, and it ends exactly at the end
349 // of both the second chunk and the full source.
350 constexpr size_t FunctionSize = 1 + ChunkSize / sizeof(Unit);
352 // Write out a 'j' or 'k' function.
353 constexpr char FunctionName = 'i' + sizeof(Unit);
354 WriteFunctionOfSizeAtOffset(source, len, FunctionName, FunctionSize,
355 len - FunctionSize);
357 JS::Rooted<JSFunction*> fun(cx);
358 fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
359 CHECK(fun);
361 CompressSourceSync(fun, cx);
363 JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
364 CHECK(str);
365 CHECK(IsExpectedFunctionString(str, FunctionName, cx));
367 return true;
369 END_TEST(testScriptSourceCompression_crossesChunkBoundary_endsAtBoundary)
371 BEGIN_TEST(testScriptSourceCompression_containsWholeChunk) {
372 CHECK(run<char16_t>());
373 CHECK(run<Utf8Unit>());
374 return true;
377 template <typename Unit>
378 bool run() {
379 constexpr size_t len = (2 * ChunkSize) / sizeof(Unit) + 17;
380 auto source = MakeSourceAllWhitespace<Unit>(cx, len);
381 CHECK(source);
383 // This function crosses two chunk boundaries and begins/ends in the middle
384 // of chunk boundaries.
385 constexpr size_t FunctionSize = 2 + ChunkSize / sizeof(Unit);
387 // Write out a 'l' or 'm' function.
388 constexpr char FunctionName = 'k' + sizeof(Unit);
389 WriteFunctionOfSizeAtOffset(source, len, FunctionName, FunctionSize,
390 ChunkSize / sizeof(Unit) - 1);
392 JS::Rooted<JSFunction*> fun(cx);
393 fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
394 CHECK(fun);
396 CompressSourceSync(fun, cx);
398 JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
399 CHECK(str);
400 CHECK(IsExpectedFunctionString(str, FunctionName, cx));
402 return true;
404 END_TEST(testScriptSourceCompression_containsWholeChunk)
406 BEGIN_TEST(testScriptSourceCompression_containsWholeChunk_endsAtBoundary) {
407 CHECK(run<char16_t>());
408 CHECK(run<Utf8Unit>());
409 return true;
412 template <typename Unit>
413 bool run() {
414 // Exactly three chunks.
415 constexpr size_t len = (3 * ChunkSize) / sizeof(Unit);
416 auto source = MakeSourceAllWhitespace<Unit>(cx, len);
417 CHECK(source);
419 // This function crosses two chunk boundaries and ends at a chunk boundary.
420 constexpr size_t FunctionSize = 1 + (2 * ChunkSize) / sizeof(Unit);
422 // Write out a 'n' or 'o' function.
423 constexpr char FunctionName = 'm' + sizeof(Unit);
424 WriteFunctionOfSizeAtOffset(source, len, FunctionName, FunctionSize,
425 ChunkSize / sizeof(Unit) - 1);
427 JS::Rooted<JSFunction*> fun(cx);
428 fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
429 CHECK(fun);
431 CompressSourceSync(fun, cx);
433 JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
434 CHECK(str);
435 CHECK(IsExpectedFunctionString(str, FunctionName, cx));
437 return true;
439 END_TEST(testScriptSourceCompression_containsWholeChunk_endsAtBoundary)
441 BEGIN_TEST(testScriptSourceCompression_spansMultipleMiddleChunks) {
442 CHECK(run<char16_t>());
443 CHECK(run<Utf8Unit>());
444 return true;
447 template <typename Unit>
448 bool run() {
449 // Four chunks.
450 constexpr size_t len = (4 * ChunkSize) / sizeof(Unit);
451 auto source = MakeSourceAllWhitespace<Unit>(cx, len);
452 CHECK(source);
454 // This function spans the two middle chunks and further extends one
455 // character to each side.
456 constexpr size_t FunctionSize = 2 + (2 * ChunkSize) / sizeof(Unit);
458 // Write out a 'p' or 'q' function.
459 constexpr char FunctionName = 'o' + sizeof(Unit);
460 WriteFunctionOfSizeAtOffset(source, len, FunctionName, FunctionSize,
461 ChunkSize / sizeof(Unit) - 1);
463 JS::Rooted<JSFunction*> fun(cx);
464 fun = EvaluateChars(cx, std::move(source), len, FunctionName, __FUNCTION__);
465 CHECK(fun);
467 CompressSourceSync(fun, cx);
469 JS::Rooted<JSString*> str(cx, DecompressSource(cx, fun));
470 CHECK(str);
471 CHECK(IsExpectedFunctionString(str, FunctionName, cx));
473 return true;
475 END_TEST(testScriptSourceCompression_spansMultipleMiddleChunks)
477 BEGIN_TEST(testScriptSourceCompression_automatic) {
478 constexpr size_t len = MinimumCompressibleLength + 55;
479 auto chars = MakeSourceAllWhitespace<char16_t>(cx, len);
480 CHECK(chars);
482 JS::SourceText<char16_t> source;
483 CHECK(source.init(cx, std::move(chars), len));
485 JS::CompileOptions options(cx);
486 JS::Rooted<JSScript*> script(cx, JS::Compile(cx, options, source));
487 CHECK(script);
489 // Check that source compression was triggered by the compile. If the
490 // off-thread source compression system is globally disabled, the source will
491 // remain uncompressed.
492 js::RunPendingSourceCompressions(cx->runtime());
493 bool expected = js::IsOffThreadSourceCompressionEnabled();
494 CHECK(script->scriptSource()->hasCompressedSource() == expected);
496 return true;
498 END_TEST(testScriptSourceCompression_automatic)