commit 025f8d5456bad832f04329a37f16480f94170fff
parent a82e3b9c431fbfe9a06c776440cc360973498c06
Author: Jan de Mooij <jdemooij@mozilla.com>
Date: Fri, 19 Dec 2025 08:40:39 +0000
Bug 2004893 part 4 - Use a single Compressor for each SourceCompressionTask. r=jonco
For short script sources, most of our time was spent under `deflateInit2` and `deflateEnd`.
It's faster to use `deflateReset` between script sources.
Differential Revision: https://phabricator.services.mozilla.com/D276487
Diffstat:
5 files changed, 87 insertions(+), 36 deletions(-)
diff --git a/js/src/vm/Compression.cpp b/js/src/vm/Compression.cpp
@@ -22,16 +22,9 @@ static void* zlib_alloc(void* cx, uInt items, uInt size) {
static void zlib_free(void* cx, void* addr) { js_free(addr); }
-Compressor::Compressor(const unsigned char* inp, size_t inplen)
- : inp(inp),
- inplen(inplen),
- initialized(false),
- finished(false),
- currentChunkSize(0) {
- MOZ_ASSERT(inplen > 0, "data to compress can't be empty");
-
+Compressor::Compressor() {
zs.opaque = nullptr;
- zs.next_in = (Bytef*)inp;
+ zs.next_in = nullptr;
zs.avail_in = 0;
zs.next_out = nullptr;
zs.avail_out = 0;
@@ -44,9 +37,6 @@ Compressor::Compressor(const unsigned char* inp, size_t inplen)
zs.data_type = 0;
zs.adler = 0;
zs.reserved = 0;
-
- // Reserve space for the CompressedDataHeader.
- outbytes = sizeof(CompressedDataHeader);
}
Compressor::~Compressor() {
@@ -66,9 +56,8 @@ Compressor::~Compressor() {
static const int WindowBits = -15;
bool Compressor::init() {
- if (inplen >= UINT32_MAX) {
- return false;
- }
+ MOZ_ASSERT(!initialized);
+
// zlib is slow and we'd rather be done compression sooner
// even if it means decompression is slower which penalizes
// Function.toString()
@@ -89,6 +78,42 @@ bool Compressor::init() {
return true;
}
+bool Compressor::setInput(const unsigned char* input, size_t inputLength) {
+ MOZ_ASSERT(initialized);
+
+ if (inputLength >= UINT32_MAX) {
+ return false;
+ }
+ MOZ_ASSERT(inputLength > 0, "data to compress can't be empty");
+
+ inp = input;
+ inplen = inputLength;
+
+ // Reserve space for the CompressedDataHeader.
+ outbytes = sizeof(CompressedDataHeader);
+
+ zs.next_in = (Bytef*)inp;
+ zs.avail_in = 0;
+ zs.next_out = nullptr;
+ zs.avail_out = 0;
+
+ // If we've already compressed another string, we need to reset our internal
+ // state. Note: deflateReset is much faster than deflateEnd + deflateInit2.
+ if (isFirstInput) {
+ MOZ_ASSERT(currentChunkSize == 0);
+ MOZ_ASSERT(chunkOffsets.empty());
+ MOZ_ASSERT(!finished);
+ isFirstInput = false;
+ } else {
+ currentChunkSize = 0;
+ chunkOffsets.clear();
+ finished = false;
+ int ret = deflateReset(&zs);
+ MOZ_RELEASE_ASSERT(ret == Z_OK);
+ }
+ return true;
+}
+
void Compressor::setOutput(unsigned char* out, size_t outlen) {
MOZ_ASSERT(outlen > outbytes);
zs.next_out = out + outbytes;
diff --git a/js/src/vm/Compression.h b/js/src/vm/Compression.h
@@ -31,15 +31,20 @@ class Compressor {
static constexpr size_t MAX_INPUT_SIZE = 2 * 1024;
z_stream zs;
- const unsigned char* inp;
- size_t inplen;
- size_t outbytes;
- bool initialized;
- bool finished;
+ const unsigned char* inp = nullptr;
+ size_t inplen = 0;
+ size_t outbytes = 0;
+ bool finished = false;
+
+ // True if |zs| has been initialized by |init|.
+ bool initialized = false;
+
+ // Flag set to |false| after the first call to |setInput|.
+ bool isFirstInput = true;
// The number of uncompressed bytes written for the current chunk. When this
// reaches CHUNK_SIZE, we finish the current chunk and start a new chunk.
- uint32_t currentChunkSize;
+ uint32_t currentChunkSize = 0;
// At the end of each chunk (and the end of the uncompressed data if it's
// not a chunk boundary), we record the offset in the compressed data.
@@ -48,9 +53,19 @@ class Compressor {
public:
enum Status { MOREOUTPUT, DONE, CONTINUE, OOM };
- Compressor(const unsigned char* inp, size_t inplen);
+ Compressor();
~Compressor();
- bool init();
+
+ Compressor(const Compressor&) = delete;
+ void operator=(const Compressor&) = delete;
+
+ // This should be called once per Compressor, before calling setInput.
+ [[nodiscard]] bool init();
+
+ // setInput can be called more than once, to compress multiple strings with
+ // minimal overhead. This will reset the compressor's state.
+ [[nodiscard]] bool setInput(const unsigned char* input, size_t inputLength);
+
void setOutput(unsigned char* out, size_t outlen);
/* Compress some of the input. Return true if it should be called again. */
Status compressMore();
diff --git a/js/src/vm/HelperThreadState.h b/js/src/vm/HelperThreadState.h
@@ -52,6 +52,7 @@ class JSTracer;
namespace js {
+class Compressor;
struct DelazifyTask;
struct FreeDelazifyTask;
struct PromiseHelperTask;
@@ -600,9 +601,9 @@ class SourceCompressionTaskEntry {
// text or UTF-16, for CharT either Utf8Unit or char16_t. Invoked by
// work() after doing a type-test of the ScriptSource*.
template <typename CharT>
- void workEncodingSpecific();
+ void workEncodingSpecific(Compressor& comp);
- void runTask();
+ void runTask(Compressor& comp);
void complete();
struct PerformTaskWork;
diff --git a/js/src/vm/JSScript.cpp b/js/src/vm/JSScript.cpp
@@ -1723,7 +1723,7 @@ template bool ScriptSource::assignSource(FrontendContext* fc,
}
template <typename Unit>
-void SourceCompressionTaskEntry::workEncodingSpecific() {
+void SourceCompressionTaskEntry::workEncodingSpecific(Compressor& comp) {
MOZ_ASSERT(source_->isUncompressed<Unit>());
// Try to keep the maximum memory usage down by only allocating half the
@@ -1736,8 +1736,8 @@ void SourceCompressionTaskEntry::workEncodingSpecific() {
}
const Unit* chars = source_->uncompressedData<Unit>()->units();
- Compressor comp(reinterpret_cast<const unsigned char*>(chars), inputBytes);
- if (!comp.init()) {
+ if (!comp.setInput(reinterpret_cast<const unsigned char*>(chars),
+ inputBytes)) {
return;
}
@@ -1802,12 +1802,14 @@ PendingSourceCompressionEntry::PendingSourceCompressionEntry(
struct SourceCompressionTaskEntry::PerformTaskWork {
SourceCompressionTaskEntry* const task_;
+ Compressor& comp_;
- explicit PerformTaskWork(SourceCompressionTaskEntry* task) : task_(task) {}
+ PerformTaskWork(SourceCompressionTaskEntry* task, Compressor& comp)
+ : task_(task), comp_(comp) {}
template <typename Unit, SourceRetrievable CanRetrieve>
void operator()(const ScriptSource::Uncompressed<Unit, CanRetrieve>&) {
- task_->workEncodingSpecific<Unit>();
+ task_->workEncodingSpecific<Unit>(comp_);
}
template <typename T>
@@ -1818,25 +1820,32 @@ struct SourceCompressionTaskEntry::PerformTaskWork {
}
};
-void ScriptSource::performTaskWork(SourceCompressionTaskEntry* task) {
+void ScriptSource::performTaskWork(SourceCompressionTaskEntry* task,
+ Compressor& comp) {
MOZ_ASSERT(hasUncompressedSource());
- data.match(SourceCompressionTaskEntry::PerformTaskWork(task));
+ data.match(SourceCompressionTaskEntry::PerformTaskWork(task, comp));
}
-void SourceCompressionTaskEntry::runTask() {
+void SourceCompressionTaskEntry::runTask(Compressor& comp) {
if (shouldCancel()) {
return;
}
MOZ_ASSERT(source_->hasUncompressedSource());
- source_->performTaskWork(this);
+ source_->performTaskWork(this, comp);
}
void SourceCompressionTask::runTask() {
MOZ_ASSERT(!entries_.empty());
+ // Note: here and in workEncodingSpecific we abort compression work on OOM
+ // since source compression is optional.
+ Compressor comp;
+ if (!comp.init()) {
+ return;
+ }
for (auto& entry : entries_) {
- entry.runTask();
+ entry.runTask(comp);
}
}
diff --git a/js/src/vm/JSScript.h b/js/src/vm/JSScript.h
@@ -51,6 +51,7 @@ class SourceText;
namespace js {
+class Compressor;
class FrontendContext;
class ScriptSource;
@@ -1007,7 +1008,7 @@ class ScriptSource {
size_t sourceLength);
private:
- void performTaskWork(SourceCompressionTaskEntry* task);
+ void performTaskWork(SourceCompressionTaskEntry* task, Compressor& comp);
struct TriggerConvertToCompressedSourceFromTask {
ScriptSource* const source_;