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:
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");
62 reinterpret_cast<Unit
*>(JS_malloc(cx
, len
* sizeof(Unit
))));
64 std::uninitialized_fill_n(source
.get(), len
, FillerWhitespace
);
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
77 JS::SourceText
<Unit
> sourceText
;
78 if (!sourceText
.init(cx
, std::move(chars
), len
)) {
83 JS::Rooted
<JS::Value
> dummy(cx
);
84 if (!JS::Evaluate(cx
, options
, sourceText
, &dummy
)) {
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
)) {
96 if (!JS::Evaluate(cx
, options
, srcbuf
, &rval
)) {
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
,
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
],
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
) {
170 JS::AutoAssertNoGC
nogc(cx
);
172 auto CheckContents
= [functionName
, len
](const auto* chars
) {
173 // Check the function in parts:
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
,
192 bool hasExpectedContents
;
193 if (JS::StringHasLatin1Chars(str
)) {
194 const JS::Latin1Char
* chars
= JS::GetLatin1LinearStringChars(nogc
, lstr
);
195 hasExpectedContents
= CheckContents(chars
);
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
>());
210 template <typename Unit
>
212 constexpr size_t len
= MinimumCompressibleLength
+ 55;
213 auto source
= MakeSourceAllWhitespace
<Unit
>(cx
, len
);
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__
);
227 CompressSourceSync(fun
, cx
);
229 JS::Rooted
<JSString
*> str(cx
, DecompressSource(cx
, fun
));
231 CHECK(IsExpectedFunctionString(str
, FunctionName
, cx
));
235 END_TEST(testScriptSourceCompression_inOneChunk
)
237 BEGIN_TEST(testScriptSourceCompression_endsAtBoundaryInOneChunk
) {
238 CHECK(run
<char16_t
>());
239 CHECK(run
<Utf8Unit
>());
243 template <typename Unit
>
245 constexpr size_t len
= ChunkSize
/ sizeof(Unit
);
246 auto source
= MakeSourceAllWhitespace
<Unit
>(cx
, len
);
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__
);
261 CompressSourceSync(fun
, cx
);
263 JS::Rooted
<JSString
*> str(cx
, DecompressSource(cx
, fun
));
265 CHECK(IsExpectedFunctionString(str
, FunctionName
, cx
));
269 END_TEST(testScriptSourceCompression_endsAtBoundaryInOneChunk
)
271 BEGIN_TEST(testScriptSourceCompression_isExactChunk
) {
272 CHECK(run
<char16_t
>());
273 CHECK(run
<Utf8Unit
>());
277 template <typename Unit
>
279 constexpr size_t len
= ChunkSize
/ sizeof(Unit
);
280 auto source
= MakeSourceAllWhitespace
<Unit
>(cx
, len
);
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__
);
292 CompressSourceSync(fun
, cx
);
294 JS::Rooted
<JSString
*> str(cx
, DecompressSource(cx
, fun
));
296 CHECK(IsExpectedFunctionString(str
, FunctionName
, cx
));
300 END_TEST(testScriptSourceCompression_isExactChunk
)
302 BEGIN_TEST(testScriptSourceCompression_crossesChunkBoundary
) {
303 CHECK(run
<char16_t
>());
304 CHECK(run
<Utf8Unit
>());
308 template <typename Unit
>
310 constexpr size_t len
= ChunkSize
/ sizeof(Unit
) + 293;
311 auto source
= MakeSourceAllWhitespace
<Unit
>(cx
, len
);
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__
);
325 CompressSourceSync(fun
, cx
);
327 JS::Rooted
<JSString
*> str(cx
, DecompressSource(cx
, fun
));
329 CHECK(IsExpectedFunctionString(str
, FunctionName
, cx
));
333 END_TEST(testScriptSourceCompression_crossesChunkBoundary
)
335 BEGIN_TEST(testScriptSourceCompression_crossesChunkBoundary_endsAtBoundary
) {
336 CHECK(run
<char16_t
>());
337 CHECK(run
<Utf8Unit
>());
341 template <typename Unit
>
343 // Exactly two chunks.
344 constexpr size_t len
= (2 * ChunkSize
) / sizeof(Unit
);
345 auto source
= MakeSourceAllWhitespace
<Unit
>(cx
, len
);
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
,
357 JS::Rooted
<JSFunction
*> fun(cx
);
358 fun
= EvaluateChars(cx
, std::move(source
), len
, FunctionName
, __FUNCTION__
);
361 CompressSourceSync(fun
, cx
);
363 JS::Rooted
<JSString
*> str(cx
, DecompressSource(cx
, fun
));
365 CHECK(IsExpectedFunctionString(str
, FunctionName
, cx
));
369 END_TEST(testScriptSourceCompression_crossesChunkBoundary_endsAtBoundary
)
371 BEGIN_TEST(testScriptSourceCompression_containsWholeChunk
) {
372 CHECK(run
<char16_t
>());
373 CHECK(run
<Utf8Unit
>());
377 template <typename Unit
>
379 constexpr size_t len
= (2 * ChunkSize
) / sizeof(Unit
) + 17;
380 auto source
= MakeSourceAllWhitespace
<Unit
>(cx
, len
);
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__
);
396 CompressSourceSync(fun
, cx
);
398 JS::Rooted
<JSString
*> str(cx
, DecompressSource(cx
, fun
));
400 CHECK(IsExpectedFunctionString(str
, FunctionName
, cx
));
404 END_TEST(testScriptSourceCompression_containsWholeChunk
)
406 BEGIN_TEST(testScriptSourceCompression_containsWholeChunk_endsAtBoundary
) {
407 CHECK(run
<char16_t
>());
408 CHECK(run
<Utf8Unit
>());
412 template <typename Unit
>
414 // Exactly three chunks.
415 constexpr size_t len
= (3 * ChunkSize
) / sizeof(Unit
);
416 auto source
= MakeSourceAllWhitespace
<Unit
>(cx
, len
);
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__
);
431 CompressSourceSync(fun
, cx
);
433 JS::Rooted
<JSString
*> str(cx
, DecompressSource(cx
, fun
));
435 CHECK(IsExpectedFunctionString(str
, FunctionName
, cx
));
439 END_TEST(testScriptSourceCompression_containsWholeChunk_endsAtBoundary
)
441 BEGIN_TEST(testScriptSourceCompression_spansMultipleMiddleChunks
) {
442 CHECK(run
<char16_t
>());
443 CHECK(run
<Utf8Unit
>());
447 template <typename Unit
>
450 constexpr size_t len
= (4 * ChunkSize
) / sizeof(Unit
);
451 auto source
= MakeSourceAllWhitespace
<Unit
>(cx
, len
);
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__
);
467 CompressSourceSync(fun
, cx
);
469 JS::Rooted
<JSString
*> str(cx
, DecompressSource(cx
, fun
));
471 CHECK(IsExpectedFunctionString(str
, FunctionName
, cx
));
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
);
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
));
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
);
498 END_TEST(testScriptSourceCompression_automatic
)